From 809c05f8658267a8fcab5dbdaec56b0621f5832f Mon Sep 17 00:00:00 2001 From: Baba-Yoga Date: Thu, 25 Jun 2026 00:46:01 +0000 Subject: [PATCH] feat: add multi-entity management (#562) and subscription pause (#563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #562 – Multi-entity/brand management: - src/types/entity.ts: Entity hierarchy types, roles, consolidated invoice, merge/divest results - src/store/entityStore.ts: EntityService (pure fns) + Zustand store with createEntity, mergeEntities, divestitureEntity, computeEntityAnalytics, buildConsolidatedInvoice - src/screens/EntityManagementScreen.tsx: UI for parent/child entity tree, add/remove, merge (acquisition), divest, member role management Issue #563 – Subscription holiday/vacation pause: - src/types/pause.ts: PauseState machine, PauseLimits, PauseRecord, PausePreview types - src/store/pauseStore.ts: ProrationCalculator + PauseService (validatePauseRequest, initiatePause, resumePause, expireCredit) + Zustand store; max 2 pauses/year, 7-90 days - src/screens/PauseSubscriptionScreen.tsx: UI with duration picker, reason selector, credit preview, early resume, pause history Navigation: - src/navigation/types.ts: EntityManagement and PauseSubscription routes added - src/navigation/AppNavigator.tsx: lazy screens registered in HomeStack + SettingsStack Chore: - .gitignore: add Rust target/, Python __pycache__, jest-results, copy file patterns --- .gitignore | 24 ++ src/navigation/AppNavigator.tsx | 22 ++ src/navigation/types.ts | 2 + src/screens/EntityManagementScreen.tsx | 395 +++++++++++++++++++++ src/screens/PauseSubscriptionScreen.tsx | 450 ++++++++++++++++++++++++ src/store/entityStore.ts | 400 +++++++++++++++++++++ src/store/pauseStore.ts | 255 ++++++++++++++ src/types/entity.ts | 87 +++++ src/types/pause.ts | 65 ++++ 9 files changed, 1700 insertions(+) create mode 100644 src/screens/EntityManagementScreen.tsx create mode 100644 src/screens/PauseSubscriptionScreen.tsx create mode 100644 src/store/entityStore.ts create mode 100644 src/store/pauseStore.ts create mode 100644 src/types/entity.ts create mode 100644 src/types/pause.ts diff --git a/.gitignore b/.gitignore index d5d62ee1..87901272 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,27 @@ PR_BODY_SUBSCRIPTION_ADVANCED_SEARCH.md PR_CI_Optimizations.md WCAG_COMPLIANCE.md issue*.json + +# Rust build artifacts +contracts/target/ +target/ + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +*.so + +# Jest test snapshots (all locations) +**/__snapshots__/** +jest-results/ +test-results/ + +# Misc generated +*.orig +lib copy.* +*copy.* diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 42d96431..44c4faea 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -80,6 +80,8 @@ const AnalyticsDashboard = lazyScreen(() => import('../../app/screens/AnalyticsD const RenewalWorkspaceScreen = lazyScreen(() => import('../../app/screens/RenewalWorkspaceScreen').then((m) => ({ default: m.default })) ); +const EntityManagementScreen = lazyScreen(() => import('../screens/EntityManagementScreen')); +const PauseSubscriptionScreen = lazyScreen(() => import('../screens/PauseSubscriptionScreen')); const Tab = createBottomTabNavigator(); const Stack = createNativeStackNavigator(); @@ -207,6 +209,16 @@ const HomeStack = () => ( component={RenewalWorkspaceScreen} options={{ title: 'Renewal Workspace', headerShown: true }} /> + + ); @@ -363,6 +375,16 @@ const SettingsStack = () => ( component={AnalyticsDashboard} options={{ title: 'Analytics Dashboard', headerShown: true }} /> + + ); diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 4bbdeced..b6d144c2 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -49,6 +49,8 @@ export type RootStackParamList = { PaymentMethods: undefined; AnalyticsDashboard: undefined; RenewalWorkspace: { renewalId?: string } | undefined; + EntityManagement: undefined; + PauseSubscription: { subscriptionId: string }; NotFound: { reason?: string }; }; diff --git a/src/screens/EntityManagementScreen.tsx b/src/screens/EntityManagementScreen.tsx new file mode 100644 index 00000000..81bb3b90 --- /dev/null +++ b/src/screens/EntityManagementScreen.tsx @@ -0,0 +1,395 @@ +import React, { useMemo, useState } from 'react'; +import { Alert, StyleSheet, Text, TextInput, View } from 'react-native'; +import { Button } from '../components/common/Button'; +import { Card } from '../components/common/Card'; +import { ListScreen } from '../components/common/ScreenTemplates'; +import { useEntityStore } from '../store/entityStore'; +import { Entity, EntityRole, EntityStatus } from '../types/entity'; +import { colors, spacing, typography } from '../utils/constants'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const statusColor = (status: EntityStatus): string => { + switch (status) { + case EntityStatus.ACTIVE: + return colors.success; + case EntityStatus.INACTIVE: + return colors.warning; + case EntityStatus.ACQUIRED: + case EntityStatus.DIVESTED: + return colors.textSecondary; + default: + return colors.textSecondary; + } +}; + +const roleBadge = (role: EntityRole): string => { + switch (role) { + case EntityRole.GLOBAL_ADMIN: + return 'Global Admin'; + case EntityRole.ENTITY_ADMIN: + return 'Admin'; + default: + return 'Viewer'; + } +}; + +// --------------------------------------------------------------------------- +// AddEntityModal – inline form (no external modal dependency) +// --------------------------------------------------------------------------- + +interface AddEntityFormProps { + parentId: string | null; + existingCount: number; + onAdd: (name: string, currency: string, parentId: string | null) => void; + onCancel: () => void; +} + +const AddEntityForm: React.FC = ({ + parentId, + existingCount, + onAdd, + onCancel, +}) => { + const [name, setName] = useState(`Entity ${existingCount + 1}`); + const [currency, setCurrency] = useState('USD'); + + return ( + + {parentId ? 'Add Subsidiary' : 'Add Holding Entity'} + Display Name + + Currency (ISO 4217) + + +