diff --git a/.cursor/rules/coding_statndard.mdc b/.cursor/rules/coding_statndard.mdc new file mode 100644 index 0000000..f9a7798 --- /dev/null +++ b/.cursor/rules/coding_statndard.mdc @@ -0,0 +1,510 @@ +--- +description: +globs: +alwaysApply: true +--- +# SME Centralized Reporting System - Coding Standards & Development Patterns + +## OVERVIEW +This document defines coding standards, development patterns, and implementation guidelines for the SME Centralized Reporting System React Native application. + +--- + +## 1. CODING STANDARDS & CONVENTIONS + +### File Naming Conventions +- **Components**: PascalCase (`ZohoProjectsDashboardScreen.tsx`) +- **Services**: camelCase (`zohoProjectsAPI.ts`) +- **Store files**: camelCase (`zohoProjectsSlice.ts`) +- **Types**: PascalCase (`ProfileTypes.ts`) +- **Constants**: UPPER_SNAKE_CASE (`API_ENDPOINTS.ts`) +- **Utilities**: camelCase (`dateUtils.ts`) +- **Hooks**: camelCase with 'use' prefix (`useZohoProjectsData.ts`) + +### Directory Naming Conventions +- **Modules**: camelCase (`auth`, `zohoProjects`, `hr`, `profile`) +- **Components**: camelCase (`widgets`, `forms`, `screens`) +- **Services**: camelCase (`integrations`, `analytics`) + +### Module Directory Rules +- Place `screens`, `widgets`, and `forms` inside `components/` within each module. +- Keep `components/` for UI-only code. Do not add `utils` or `constants` under modules. +- Share cross-cutting helpers via `src/shared/utils` and global constants via `src/shared/constants`. + +### Variable & Function Naming +```typescript +// Variables: camelCase +const projectList = []; +const isLoading = false; +const userPreferences = {}; + +// Functions: camelCase with descriptive verbs +const fetchZohoProjectsData = () => {}; +const calculateOpenTasks = () => {}; +const handleSubmitForm = () => {}; + +// Constants: UPPER_SNAKE_CASE +const API_BASE_URL = ''; +const MAX_RETRY_ATTEMPTS = 3; +const DEFAULT_PAGE_SIZE = 20; + +// Interfaces/Types: PascalCase +interface ZohoProject { + id: string; + name: string; +} + +type LoadingState = 'idle' | 'loading' | 'succeeded' | 'failed'; +``` + +--- + +### Inline Commenting Guidelines +- Always add clear, concise inline comments for non-obvious logic, complex conditions, side-effects, and business rules. +- Prefer sentence-case, developer-oriented comments that explain "why" not just "what". +- Document assumptions, edge cases, units, and non-trivial parameter/return semantics. +- Keep comments up-to-date when changing code; remove stale comments immediately. +- For components, briefly annotate major sections (effects, handlers, conditional rendering) and tricky UI logic. + +Example: +```typescript +// Fetch projects on mount and when filters change to ensure fresh data +useEffect(() => { + // Guard: avoid duplicate fetch while loading + if (loading) return; + dispatch(fetchZohoProjects()); +}, [dispatch, loading, filters]); + +// Compute SLA breach percentage (0-100). Includes only active tasks. +const breachRate = useMemo(() => { + if (totalTasks === 0) return 0; // Avoid divide-by-zero + return Math.round((breachedTasks / totalTasks) * 100); +}, [breachedTasks, totalTasks]); +``` + +--- + +## 2. COMPONENT STRUCTURE TEMPLATES + +### Screen Component Template +```typescript +import React, { useEffect, useState } from 'react'; +import { + View, + Text, + ScrollView, + RefreshControl, + StyleSheet, +} from 'react-native'; +import { useSelector, useDispatch } from 'react-redux'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +// Shared imports +import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui'; +import { COLORS, FONTS, SPACING } from '@/shared/styles/theme'; + +// Module imports +import { selectZohoProjects, selectZohoProjectsLoading } from '../store/selectors'; +import { fetchZohoProjects } from '../store/zohoProjectsSlice'; + +// Types +import type { ZohoProjectsScreenProps } from '../types'; + +const ZohoProjectsDashboardScreen: React.FC = ({ + navigation, + route, +}) => { + // State + const [refreshing, setRefreshing] = useState(false); + + // Redux + const dispatch = useDispatch(); + const data = useSelector(selectZohoProjects); + const loading = useSelector(selectZohoProjectsLoading); + + // Effects + useEffect(() => { + dispatch(fetchZohoProjects()); + }, [dispatch]); + + // Handlers + const handleRefresh = async () => { + setRefreshing(true); + await dispatch(fetchZohoProjects()).unwrap(); + setRefreshing(false); + }; + + const handleNavigateToDetails = (id: string) => { + navigation.navigate('ProjectDetails', { id }); + }; + + // Loading state + if (loading && !data.length) { + return ; + } + + // Error state + if (error) { + return dispatch(fetchZohoProjects())} />; + } + + return ( + + + } + > + + Zoho Projects + + + + {/* Dashboard content */} + + {/* Widgets and components */} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: SPACING.md, + backgroundColor: COLORS.surface, + }, + title: { + fontSize: 24, + fontFamily: FONTS.bold, + color: COLORS.text, + }, + content: { + padding: SPACING.md, + }, +}); + +export default ZohoProjectsDashboardScreen; +``` + +### Widget Component Template +```typescript +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { COLORS, FONTS, SPACING, SHADOWS } from '@/shared/styles/theme'; + +interface RevenueChartProps { + data: RevenueData[]; + period: 'month' | 'quarter' | 'year'; + onPeriodChange: (period: 'month' | 'quarter' | 'year') => void; + onPress?: () => void; +} + +const RevenueChart: React.FC = ({ + data, + period, + onPeriodChange, + onPress, +}) => { + const totalRevenue = data.reduce((sum, item) => sum + item.amount, 0); + + return ( + + + + + Revenue Analytics + + + + {['month', 'quarter', 'year'].map((p) => ( + onPeriodChange(p as any)} + > + + {p.charAt(0).toUpperCase() + p.slice(1)} + + + ))} + + + + + + ${totalRevenue.toLocaleString()} + + Total Revenue + + {/* Chart component would go here */} + + Chart Component + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: COLORS.surface, + borderRadius: 12, + padding: SPACING.md, + ...SHADOWS.medium, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: SPACING.md, + }, + titleContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + title: { + fontSize: 16, + fontFamily: FONTS.medium, + color: COLORS.text, + marginLeft: SPACING.xs, + }, + periodSelector: { + flexDirection: 'row', + backgroundColor: COLORS.background, + borderRadius: 6, + padding: 2, + }, + periodButton: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + }, + periodButtonActive: { + backgroundColor: COLORS.primary, + }, + periodText: { + fontSize: 12, + fontFamily: FONTS.regular, + color: COLORS.textLight, + }, + periodTextActive: { + color: COLORS.surface, + }, + content: { + alignItems: 'flex-start', + }, + amount: { + fontSize: 28, + fontFamily: FONTS.bold, + color: COLORS.text, + }, + subtitle: { + fontSize: 14, + fontFamily: FONTS.regular, + color: COLORS.textLight, + marginBottom: SPACING.md, + }, + chartPlaceholder: { + width: '100%', + height: 120, + backgroundColor: COLORS.background, + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + }, + chartText: { + fontSize: 14, + fontFamily: FONTS.regular, + color: COLORS.textLight, + }, +}); + +export default RevenueChart; +``` + +--- + +## 3. REDUX STATE MANAGEMENT PATTERNS + +### Store Configuration +```typescript +// src/store/store.ts +import { configureStore } from '@reduxjs/toolkit'; +import { persistStore, persistReducer } from 'redux-persist'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { combineReducers } from '@reduxjs/toolkit'; + +// Import slices +import authSlice from '@/modules/auth/store/authSlice'; +import hrSlice from '@/modules/hr/store/hrSlice'; +import zohoProjectsSlice from '@/modules/zohoProjects/store/zohoProjectsSlice'; +import profileSlice from '@/modules/profile/store/profileSlice'; +import uiSlice from '@/shared/store/uiSlice'; + +const rootReducer = combineReducers({ + auth: authSlice.reducer, + hr: hrSlice.reducer, + zohoProjects: zohoProjectsSlice.reducer, + profile: profileSlice.reducer, + ui: uiSlice.reducer, +}); + +const persistConfig = { + key: 'root', + storage: AsyncStorage, + whitelist: ['auth', 'hr', 'zohoProjects', 'profile'], + blacklist: ['ui'], // Don't persist UI state +}; + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: persistedReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ['persist/FLUSH', 'persist/REHYDRATE', 'persist/PAUSE', 'persist/PERSIST', 'persist/PURGE', 'persist/REGISTER'], + }, + }), +}); + +export const persistor = persistStore(store); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; +``` + +### Redux Slice Template (Zoho Projects example) +```typescript +// src/modules/zohoProjects/store/zohoProjectsSlice.ts +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { zohoProjectsAPI } from '../services/zohoProjectsAPI'; +import type { ZohoProjectsState, ZohoProject, ZohoProjectsFilters } from './types'; + +// Initial state +const initialState: ZohoProjectsState = { + projects: [], + loading: false, + error: null, + filters: { owner: 'all', status: 'all' }, + lastUpdated: null, +}; + +// Async thunks +export const fetchZohoProjects = createAsyncThunk( + 'zohoProjects/fetch', + async () => { + const response = await zohoProjectsAPI.getProjects(); + return response.data; + } +); + +export const updateZohoProject = createAsyncThunk( + 'zohoProjects/update', + async (project: Partial) => { + const response = await zohoProjectsAPI.updateProject(project); + return response.data; + } +); + +// Slice +const zohoProjectsSlice = createSlice({ + name: 'zohoProjects', + initialState, + reducers: { + setFilters: (state, action: PayloadAction>) => { + state.filters = { ...state.filters, ...action.payload }; + }, + clearError: (state) => { + state.error = null; + }, + resetState: () => initialState, + }, + extraReducers: (builder) => { + // add async case reducers here + }, +}); + +export default zohoProjectsSlice; +``` + +### Redux Slice Template +```typescript +// src/modules/finance/store/financeSlice.ts +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { financeAPI } from '../services/financeAPI'; +import type { FinanceState, FinanceData, FinanceFilters } from './types'; + +// Initial state +const initialState: FinanceState = { + data: [], + loading: false, + error: null, + filters: { + period: 'month', + category: 'all', + dateRange: null, + }, + lastUpdated: null, +}; + +// Async thunks +export const fetchFinanceData = createAsyncThunk( + 'finance/fetchData', + async (params?: { refresh?: boolean }) => { + const response = await financeAPI.getData(params); + return response.data; + } +); + +export const updateFinanceData = createAsyncThunk( + 'finance/updateData', + async (data: Partial) => { + const response = await financeAPI.updateData(data); + return response.data; + } +); + +// Slice +const financeSlice = createSlice({ + name: 'finance', + initialState, + reducers: { + setFilters: (state, action: PayloadAction>) => { + state.filters = { ...state.filters, ...action.payload }; + }, + clearError: (state) => { + state.error = null; + }, + resetState: () => initialState, + }, + extraReducers: (builder) => { + builder + // Fetch data cases + .addCase(fetchFinanceData \ No newline at end of file diff --git a/.cursor/rules/project_structure.mdc b/.cursor/rules/project_structure.mdc new file mode 100644 index 0000000..4a640b5 --- /dev/null +++ b/.cursor/rules/project_structure.mdc @@ -0,0 +1,522 @@ +--- +description: +globs: +alwaysApply: true +--- +# SME Centralized Reporting System - Project Architecture & Structure + +## PROJECT OVERVIEW +**Application**: Centralized Reporting Dashboard for SMEs +**Platform**: React Native (Cross-platform iOS/Android) +**Architecture**: Modular Component-Based Architecture +**State Management**: Redux + Redux Persist + AsyncStorage + +--- + +## 1. PROJECT STRUCTURE + +### Root Directory Structure +``` +CentralizedReportingSystem/ +├── src/ +│ ├── modules/ # Core business modules +│ ├── shared/ # Shared components and utilities +│ ├── navigation/ # Navigation configuration +│ ├── store/ # Redux store configuration +│ ├── services/ # API services and integrations +│ ├── assets/ # Static assets +│ └── types/ # TypeScript type definitions +├── android/ # Android specific files +├── ios/ # iOS specific files +└── package.json +``` + +### Core Module Structure (Each module follows this pattern) +``` +src/modules/[moduleName]/ +├── screens/ # Screen components (module routes) +├── components/ # Reusable UI for this module (no screens) +│ ├── widgets/ # Dashboard/widgets +│ └── forms/ # Form components +├── navigation/ # Module-level stack navigator(s) +│ └── [ModuleName]Navigator.tsx +├── services/ # Module-specific API services +├── store/ # Redux slices and actions +│ ├── slice.ts # Redux toolkit slice +│ ├── selectors.ts # State selectors +│ └── types.ts # Module-specific types +└── index.ts # Module exports (optional) +``` + + + +### Shared Directory Structure +``` +src/shared/ +├── components/ # Reusable UI components +│ ├── charts/ # Chart components +│ ├── forms/ # Form controls +│ ├── layout/ # Layout components +│ └── ui/ # Basic UI components +├── hooks/ # Custom React hooks +├── utils/ # Utility functions +├── constants/ # Global constants +├── styles/ # Global styles and themes +└── types/ # Shared type definitions +``` + +--- + +## 2. CORE MODULES DEFINITION + +### Module 1: Authentication (`auth`) +``` +src/modules/auth/ +├── screens/ +│ ├── LoginScreen.tsx +│ ├── RegisterScreen.tsx +│ └── ForgotPasswordScreen.tsx +├── components/ +│ └── widgets/ +├── navigation/ +│ └── AuthNavigator.tsx # Stack with Login, Register, ForgotPassword +├── services/ +│ ├── authAPI.ts +│ └── integrations/ +└── store/ + ├── authSlice.ts + ├── selectors.ts + └── types.ts +``` + +### Module 2: HR (`hr`) +``` +src/modules/hr/ +├── screens/ +│ ├── HRDashboardScreen.tsx +│ ├── EmployeeMetricsScreen.tsx +│ ├── AttendanceScreen.tsx +│ └── RecruitmentScreen.tsx +├── components/ +│ ├── widgets/ +│ │ ├── EmployeeStatsWidget.tsx +│ │ ├── AttendanceChart.tsx +│ │ └── PerformanceWidget.tsx +│ └── forms/ +│ └── EmployeeForm.tsx +├── navigation/ +│ └── HRNavigator.tsx # Stack with HRDashboard, EmployeeMetrics, Attendance, Recruitment +├── services/ +│ ├── hrAPI.ts +│ └── integrations/ +│ ├── zohoPeopleService.ts +│ ├── bambooHRService.ts +│ └── kekaService.ts +└── store/ + ├── hrSlice.ts + ├── selectors.ts + └── types.ts +``` + +### Module 3: Zoho Projects (`zohoProjects`) +``` +src/modules/zohoProjects/ +├── screens/ +│ ├── ZohoProjectsDashboardScreen.tsx +│ ├── ProjectPerformanceScreen.tsx +│ ├── ResourceUtilizationScreen.tsx +│ └── ClientAnalyticsScreen.tsx +├── components/ +│ ├── widgets/ +│ │ ├── ProjectTimelineWidget.tsx +│ │ ├── ResourceAllocationChart.tsx +│ │ └── QualityMetricsWidget.tsx +│ └── forms/ +│ └── ProjectForm.tsx +├── navigation/ +│ └── ZohoProjectsNavigator.tsx # Stack with ZohoProjects screens +├── services/ +│ ├── zohoProjectsAPI.ts +│ └── integrations/ +│ └── zohoProjectsService.ts +└── store/ + ├── zohoProjectsSlice.ts + ├── selectors.ts + └── types.ts +``` + +### Module 4: Profile (`profile`) +``` +src/modules/profile/ +├── screens/ +│ ├── ProfileScreen.tsx +│ └── EditProfileScreen.tsx +├── components/ +│ └── widgets/ +├── navigation/ +│ └── ProfileNavigator.tsx # Stack with Profile and EditProfile +├── services/ +│ ├── profileAPI.ts +└── store/ + ├── profileSlice.ts + ├── selectors.ts + └── types.ts +``` + +--- + +## 3. NAVIGATION STRUCTURE + +### Navigation Configuration +```typescript +// src/navigation/AppNavigator.tsx +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { createStackNavigator } from '@react-navigation/stack'; +import Icon from 'react-native-vector-icons/MaterialIcons'; + +const Tab = createBottomTabNavigator(); +const Stack = createStackNavigator(); + +const AppNavigator = () => ( + ({ + tabBarIcon: ({ focused, color, size }) => { + const iconName = getTabIconName(route.name); + return ; + }, + })} + > + + + + +); +``` + +### Stack Navigators for Each Module +```typescript +// HR Stack +const HRStack = () => ( + + + + + + +); + +// Zoho Projects Stack +const ZohoProjectsStack = () => ( + + + + + + +); + +// Profile Stack +const ProfileStack = () => ( + + + + +); +``` + +--- + +## 4. INTEGRATION FRAMEWORK + +### Primary Integration Services + +#### HR Systems Integration +``` +src/modules/hr/services/integrations/ +├── zohoPeopleService.ts # Zoho People integration +├── bambooHRService.ts # BambooHR integration +├── workdayService.ts # Workday integration +└── kekaService.ts # Keka integration +``` + +#### Project Management Integration +``` +src/modules/zohoProjects/services/integrations/ +└── zohoProjectsService.ts # Zoho Projects integration +``` + +#### Communication & Collaboration +``` +src/shared/services/integrations/ +├── slackService.ts # Slack integration +├── teamsService.ts # Microsoft Teams integration +└── zoomService.ts # Zoom integration +``` + +--- + +## 5. DESIGN SYSTEM & STYLING + +### Theme Configuration +```typescript +// src/shared/styles/theme.ts +export const COLORS = { + // Primary colors + primary: '#2C5F4A', + primaryLight: '#4A8B6A', + primaryDark: '#1A3D2E', + + // Secondary colors + secondary: '#FF6B35', + secondaryLight: '#FF8F65', + secondaryDark: '#E55A2B', + + // UI colors + background: '#F8F9FA', + surface: '#FFFFFF', + text: '#2D3748', + textLight: '#718096', + border: '#E2E8F0', + error: '#E53E3E', + success: '#38A169', + warning: '#D69E2E', + + // Dashboard specific + chartPrimary: '#2C5F4A', + chartSecondary: '#FF6B35', + chartTertiary: '#4299E1', + chartQuaternary: '#48BB78', +}; + +export const FONTS = { + regular: 'Roboto-Regular', + medium: 'Roboto-Medium', + bold: 'Roboto-Bold', + light: 'Roboto-Light', + black: 'Roboto-Black', +}; + +export const FONT_SIZES = { + xs: 12, + sm: 14, + md: 16, + lg: 18, + xl: 20, + xxl: 24, + xxxl: 32, +}; + +export const SPACING = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, +}; + +export const BORDER_RADIUS = { + sm: 4, + md: 8, + lg: 12, + xl: 16, +}; + +export const SHADOWS = { + light: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + medium: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 4, + elevation: 4, + }, + heavy: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 8, + }, +}; +``` + +### Theme System and Context +``` +src/shared/styles/ +├── theme.ts # Semantic tokens (colors, spacing, fonts, shadows) +├── ThemeProvider.tsx # App-level provider with light/dark palettes +├── useTheme.ts # Hook to consume the theme +└── types.ts # Theme typing (Theme, Palette, SemanticTokens) +``` + +Guidelines +- Use semantic tokens from `theme.ts` (e.g., `COLORS.surface`, `COLORS.text`) instead of hard-coded hex values. +- Support light and dark palettes; default to Light as per the provided UI (white surfaces, cool blue header, soft gray background, subtle shadows). +- Wrap the app at the root with `ThemeProvider`; access tokens via `useTheme()` in components. +- Gradients: primary header/hero areas may use a cool-blue gradient (from `#3AA0FF` to `#2D6BFF`). Keep content surfaces white. +- Spacing and radii: use `SPACING` and `BORDER_RADIUS` tokens; avoid magic numbers. +- Elevation: use `SHADOWS.light|medium|heavy` for cards and bottom toolbars. + +Palettes (inspired by the attached design) +- Light palette + - primary: `#2D6BFF` (buttons/icons), primaryLight: `#3AA0FF` + - background: `#F4F6F9`, surface: `#FFFFFF` + - text: `#1F2937`, textLight: `#6B7280` + - accent (success): `#22C55E`, accent (warning): `#F59E0B`, accent (error): `#EF4444` + - chartPrimary: `#2D6BFF`, chartSecondary: `#3AA0FF`, chartTertiary: `#10B981`, chartQuaternary: `#F59E0B` +- Dark palette (optional) + - background: `#0F172A`, surface: `#111827`, text: `#E5E7EB`, textLight: `#9CA3AF`, primary: `#60A5FA` + +Provider responsibilities +- Expose `{ colors, spacing, fonts, shadows, isDark, setScheme }`. +- Persist the scheme preference with AsyncStorage. +- Mirror system scheme when `followSystem=true`. + +Usage rules +- Import tokens from `useTheme()` or direct constants from `theme.ts` for static styles. +- Do not inline hex codes in components; never duplicate spacing or font sizes. +- For icons and charts, pull colors from `theme.colors` so they adapt to scheme changes. + +Example usage +```typescript +// Inside a component +import { useTheme } from '@/shared/styles/useTheme'; + +const MyCard = () => { + const { colors, spacing, shadows } = useTheme(); + return ( + + ); +}; +``` + +--- + +## 6. ASSETS ORGANIZATION + +### Assets Directory Structure +``` +src/assets/ +├── fonts/ # Roboto font files +│ ├── Roboto-Regular.ttf +│ ├── Roboto-Medium.ttf +│ ├── Roboto-Bold.ttf +│ ├── Roboto-Light.ttf +│ └── Roboto-Black.ttf +├── images/ # Image assets +│ ├── logos/ +│ ├── icons/ +│ ├── illustrations/ +│ └── backgrounds/ +├── animations/ # Lottie animation files +└── data/ # Static data files + ├── currencies.json + ├── countries.json + └── industries.json +``` + +--- + +## 7. SHARED COMPONENTS LIBRARY + +### UI Components Structure +``` +src/shared/components/ +├── ui/ # Basic UI components +│ ├── Button/ +│ ├── Input/ +│ ├── Card/ +│ ├── Modal/ +│ ├── LoadingSpinner/ +│ └── EmptyState/ +├── charts/ # Chart components +│ ├── LineChart/ +│ ├── BarChart/ +│ ├── PieChart/ +│ ├── DonutChart/ +│ └── ProgressChart/ +├── forms/ # Form components +│ ├── FormInput/ +│ ├── FormSelect/ +│ ├── FormDatePicker/ +│ ├── FormCheckbox/ +│ └── FormRadio/ +├── layout/ # Layout components +│ ├── Container/ +│ ├── Header/ +│ ├── Footer/ +│ ├── Sidebar/ +│ └── Grid/ +└── widgets/ # Reusable widget components + ├── KPIWidget/ + ├── ChartWidget/ + ├── ListWidget/ + ├── StatWidget/ + └── FilterWidget/ +``` + +--- + +## 8. PERFORMANCE OPTIMIZATION GUIDELINES + +### Code Organization for Performance +- Implement lazy loading for non-critical screens +- Use React.memo for expensive dashboard widgets +- Implement virtualized lists for large datasets +- Cache frequently accessed data using Redux Persist +- Use image optimization and caching strategies + +### Data Management Optimization +- Implement pagination for large datasets (>100 records) +- Use memoization for expensive calculations +- Cache API responses with appropriate TTL +- Implement background data refresh strategies +- Use efficient data structures for complex operations + +--- + +## 9. SECURITY & DATA PROTECTION + +### Data Security Implementation +``` +src/shared/security/ +├── encryption/ # Data encryption utilities +├── storage/ # Secure storage implementation +├── authentication/ # Auth security measures +└── validation/ # Input validation and sanitization +``` + +### Security Measures +- Encrypt sensitive data before storing in AsyncStorage +- Implement secure token storage +- Add certificate pinning for API communications +- Implement proper input validation and sanitization + +--- + +## 10. ACCESSIBILITY & INTERNATIONALIZATION + +### Accessibility Structure +``` +src/shared/accessibility/ +├── labels.ts # Accessibility labels +├── hints.ts # Accessibility hints +├── roles.ts # Accessibility roles +└── utils.ts # Accessibility utilities +``` + + + +This architecture file provides the complete project structure and organization guidelines for building a maintainable, scalable SME reporting system. diff --git a/App.tsx b/App.tsx index 7d8838a..85d9178 100644 --- a/App.tsx +++ b/App.tsx @@ -6,126 +6,50 @@ */ import React from 'react'; -import type {PropsWithChildren} from 'react'; -import { - ScrollView, - StatusBar, - StyleSheet, - Text, - useColorScheme, - View, -} from 'react-native'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/integration/react'; +import { useSelector } from 'react-redux'; +import { NavigationContainer } from '@react-navigation/native'; +import AppNavigator from '@/navigation/AppNavigator'; +import { store, persistor } from '@/store/store'; +import { ThemeProvider } from '@/shared/styles/ThemeProvider'; +import LoadingSpinner from '@/shared/components/ui/LoadingSpinner'; +import AuthNavigator from '@/modules/auth/navigation/AuthNavigator'; +import type { RootState } from '@/store/store'; +import IntegrationsNavigator from '@/modules/integrations/navigation/IntegrationsNavigator'; +import { StatusBar } from 'react-native'; -import { - Colors, - DebugInstructions, - Header, - LearnMoreLinks, - ReloadInstructions, -} from 'react-native/Libraries/NewAppScreen'; +function AppContent(): React.JSX.Element { + const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.token)); + const selectedService = useSelector((s: RootState) => s.integrations.selectedService); + return ( + + } persistor={persistor}> + + {!isAuthenticated ? ( + + + + ) : ( + !selectedService ? ( + + + + ) : ( + + ) + )} + + -type SectionProps = PropsWithChildren<{ - title: string; -}>; - -function Section({children, title}: SectionProps): React.JSX.Element { - const isDarkMode = useColorScheme() === 'dark'; - return ( - - - {title} - - - {children} - - ); } -function App(): React.JSX.Element { - const isDarkMode = useColorScheme() === 'dark'; - - const backgroundStyle = { - backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, - }; - - /* - * To keep the template simple and small we're adding padding to prevent view - * from rendering under the System UI. - * For bigger apps the recommendation is to use `react-native-safe-area-context`: - * https://github.com/AppAndFlow/react-native-safe-area-context - * - * You can read more about it here: - * https://github.com/react-native-community/discussions-and-proposals/discussions/827 - */ - const safePadding = '5%'; +export default function App() { return ( - - - - -
- - -
- Edit App.tsx to change this - screen and then come back to see your edits. -
-
- -
-
- -
-
- Read the docs to discover what to do next: -
- -
- - + + + ); } - -const styles = StyleSheet.create({ - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, - }, - sectionTitle: { - fontSize: 24, - fontWeight: '600', - }, - sectionDescription: { - marginTop: 8, - fontSize: 18, - fontWeight: '400', - }, - highlight: { - fontWeight: '700', - }, -}); - -export default App; diff --git a/android/app/build.gradle b/android/app/build.gradle index 2759afb..9ce89e2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -117,3 +117,4 @@ dependencies { implementation jscFlavor } } +apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") \ No newline at end of file diff --git a/android/app/src/main/assets/fonts/Roboto-Bold.ttf b/android/app/src/main/assets/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..9d7cf22 Binary files /dev/null and b/android/app/src/main/assets/fonts/Roboto-Bold.ttf differ diff --git a/android/app/src/main/assets/fonts/Roboto-Light.ttf b/android/app/src/main/assets/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..6fcd5f9 Binary files /dev/null and b/android/app/src/main/assets/fonts/Roboto-Light.ttf differ diff --git a/android/app/src/main/assets/fonts/Roboto-Medium.ttf b/android/app/src/main/assets/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..d629e98 Binary files /dev/null and b/android/app/src/main/assets/fonts/Roboto-Medium.ttf differ diff --git a/android/app/src/main/assets/fonts/Roboto-Regular.ttf b/android/app/src/main/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/android/app/src/main/assets/fonts/Roboto-Regular.ttf differ diff --git a/android/app/src/main/assets/fonts/Roboto-SemiBold.ttf b/android/app/src/main/assets/fonts/Roboto-SemiBold.ttf new file mode 100644 index 0000000..3f34834 Binary files /dev/null and b/android/app/src/main/assets/fonts/Roboto-SemiBold.ttf differ diff --git a/android/app/src/main/assets/fonts/Roboto-Thin.ttf b/android/app/src/main/assets/fonts/Roboto-Thin.ttf new file mode 100644 index 0000000..6ee97b8 Binary files /dev/null and b/android/app/src/main/assets/fonts/Roboto-Thin.ttf differ diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 7ba83a2..cbde07f 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -4,6 +4,7 @@ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 37f853b..df97d72 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/android/link-assets-manifest.json b/android/link-assets-manifest.json new file mode 100644 index 0000000..b2a9e85 --- /dev/null +++ b/android/link-assets-manifest.json @@ -0,0 +1,29 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "src/assets/fonts/Roboto-Bold.ttf", + "sha1": "508c35dee818addce6cc6d1fb6e42f039da5a7cf" + }, + { + "path": "src/assets/fonts/Roboto-Light.ttf", + "sha1": "318b44c0a32848f78bf11d4fbf3355d00647a796" + }, + { + "path": "src/assets/fonts/Roboto-Medium.ttf", + "sha1": "fa5192203f85ddb667579e1bdf26f12098bb873b" + }, + { + "path": "src/assets/fonts/Roboto-Regular.ttf", + "sha1": "3bff51436aa7eb995d84cfc592cc63e1316bb400" + }, + { + "path": "src/assets/fonts/Roboto-SemiBold.ttf", + "sha1": "9ca139684fe902c8310dd82991648376ac9838db" + }, + { + "path": "src/assets/fonts/Roboto-Thin.ttf", + "sha1": "8e098a207d2ace83e873d02b76acba903d819f74" + } + ] +} diff --git a/babel.config.js b/babel.config.js index f7b3da3..96c23dd 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,15 @@ module.exports = { presets: ['module:@react-native/babel-preset'], + plugins: [ + 'react-native-reanimated/plugin', + [ + 'module-resolver', + { + root: ['./'], + alias: { + '@': './src', + }, + }, + ], + ], }; diff --git a/index.js b/index.js index a850d03..c59a71e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /** * @format */ - +import 'react-native-gesture-handler'; import {AppRegistry} from 'react-native'; import App from './App'; import {name as appName} from './app.json'; diff --git a/ios/CentralizedReportingSystem.xcodeproj/project.pbxproj b/ios/CentralizedReportingSystem.xcodeproj/project.pbxproj index 9a853e1..32c6723 100644 --- a/ios/CentralizedReportingSystem.xcodeproj/project.pbxproj +++ b/ios/CentralizedReportingSystem.xcodeproj/project.pbxproj @@ -11,6 +11,12 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + A801363574154F078613978B /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 458C914037F54A19AE2417A9 /* Roboto-Bold.ttf */; }; + D8D552DE41234B46B618DA9C /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D3E3B83E3BE94F648FE32533 /* Roboto-Light.ttf */; }; + 41A05860AC884CE9B220E56B /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F33DA39824A64E8698EC1759 /* Roboto-Medium.ttf */; }; + 1DBB692CDE86454AA93F82CF /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3F9A64BDC9774C47B9DA8A37 /* Roboto-Regular.ttf */; }; + 630A45FC21FC40178AA38E83 /* Roboto-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E19FD65B95242228BFF7689 /* Roboto-SemiBold.ttf */; }; + 43C39FA7C3054FEF8ADEC8F1 /* Roboto-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BCECBACE615341DA9967F551 /* Roboto-Thin.ttf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,6 +41,12 @@ 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = CentralizedReportingSystem/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = CentralizedReportingSystem/LaunchScreen.storyboard; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + 458C914037F54A19AE2417A9 /* Roboto-Bold.ttf */ = {isa = PBXFileReference; name = "Roboto-Bold.ttf"; path = "../src/assets/fonts/Roboto-Bold.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + D3E3B83E3BE94F648FE32533 /* Roboto-Light.ttf */ = {isa = PBXFileReference; name = "Roboto-Light.ttf"; path = "../src/assets/fonts/Roboto-Light.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + F33DA39824A64E8698EC1759 /* Roboto-Medium.ttf */ = {isa = PBXFileReference; name = "Roboto-Medium.ttf"; path = "../src/assets/fonts/Roboto-Medium.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 3F9A64BDC9774C47B9DA8A37 /* Roboto-Regular.ttf */ = {isa = PBXFileReference; name = "Roboto-Regular.ttf"; path = "../src/assets/fonts/Roboto-Regular.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + 9E19FD65B95242228BFF7689 /* Roboto-SemiBold.ttf */ = {isa = PBXFileReference; name = "Roboto-SemiBold.ttf"; path = "../src/assets/fonts/Roboto-SemiBold.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; + BCECBACE615341DA9967F551 /* Roboto-Thin.ttf */ = {isa = PBXFileReference; name = "Roboto-Thin.ttf"; path = "../src/assets/fonts/Roboto-Thin.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -93,6 +105,7 @@ 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, BBD78D7AC51CEA395F1C20DB /* Pods */, + 034FBE3A8610463F9085B792 /* Resources */, ); indentWidth = 2; sourceTree = ""; @@ -116,6 +129,20 @@ path = Pods; sourceTree = ""; }; + 034FBE3A8610463F9085B792 /* Resources */ = { + isa = "PBXGroup"; + children = ( + 458C914037F54A19AE2417A9 /* Roboto-Bold.ttf */, + D3E3B83E3BE94F648FE32533 /* Roboto-Light.ttf */, + F33DA39824A64E8698EC1759 /* Roboto-Medium.ttf */, + 3F9A64BDC9774C47B9DA8A37 /* Roboto-Regular.ttf */, + 9E19FD65B95242228BFF7689 /* Roboto-SemiBold.ttf */, + BCECBACE615341DA9967F551 /* Roboto-Thin.ttf */, + ); + name = Resources; + sourceTree = ""; + path = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -185,6 +212,12 @@ files = ( 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + A801363574154F078613978B /* Roboto-Bold.ttf in Resources */, + D8D552DE41234B46B618DA9C /* Roboto-Light.ttf in Resources */, + 41A05860AC884CE9B220E56B /* Roboto-Medium.ttf in Resources */, + 1DBB692CDE86454AA93F82CF /* Roboto-Regular.ttf in Resources */, + 630A45FC21FC40178AA38E83 /* Roboto-SemiBold.ttf in Resources */, + 43C39FA7C3054FEF8ADEC8F1 /* Roboto-Thin.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/CentralizedReportingSystem/Info.plist b/ios/CentralizedReportingSystem/Info.plist index 460d2fe..4cc9758 100644 --- a/ios/CentralizedReportingSystem/Info.plist +++ b/ios/CentralizedReportingSystem/Info.plist @@ -26,14 +26,13 @@ NSAppTransportSecurity - NSAllowsArbitraryLoads NSAllowsLocalNetworking NSLocationWhenInUseUsageDescription - + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -48,5 +47,14 @@ UIViewControllerBasedStatusBarAppearance + UIAppFonts + + Roboto-Bold.ttf + Roboto-Light.ttf + Roboto-Medium.ttf + Roboto-Regular.ttf + Roboto-SemiBold.ttf + Roboto-Thin.ttf + diff --git a/ios/link-assets-manifest.json b/ios/link-assets-manifest.json new file mode 100644 index 0000000..b2a9e85 --- /dev/null +++ b/ios/link-assets-manifest.json @@ -0,0 +1,29 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "src/assets/fonts/Roboto-Bold.ttf", + "sha1": "508c35dee818addce6cc6d1fb6e42f039da5a7cf" + }, + { + "path": "src/assets/fonts/Roboto-Light.ttf", + "sha1": "318b44c0a32848f78bf11d4fbf3355d00647a796" + }, + { + "path": "src/assets/fonts/Roboto-Medium.ttf", + "sha1": "fa5192203f85ddb667579e1bdf26f12098bb873b" + }, + { + "path": "src/assets/fonts/Roboto-Regular.ttf", + "sha1": "3bff51436aa7eb995d84cfc592cc63e1316bb400" + }, + { + "path": "src/assets/fonts/Roboto-SemiBold.ttf", + "sha1": "9ca139684fe902c8310dd82991648376ac9838db" + }, + { + "path": "src/assets/fonts/Roboto-Thin.ttf", + "sha1": "8e098a207d2ace83e873d02b76acba903d819f74" + } + ] +} diff --git a/package-lock.json b/package-lock.json index 0e5e064..7ceb62e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,33 @@ "name": "CentralizedReportingSystem", "version": "0.0.1", "dependencies": { + "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-clipboard/clipboard": "^1.16.1", + "@react-native-community/datetimepicker": "^8.4.4", + "@react-native-community/netinfo": "^11.4.1", + "@react-navigation/bottom-tabs": "^7.4.7", + "@react-navigation/native": "^7.1.17", + "@react-navigation/native-stack": "^7.3.26", + "@react-navigation/stack": "^7.4.8", + "@reduxjs/toolkit": "^2.9.0", + "apisauce": "^3.2.0", "react": "19.0.0", - "react-native": "0.79.0" + "react-native": "0.79.0", + "react-native-chart-kit": "^6.12.0", + "react-native-element-dropdown": "^2.12.4", + "react-native-gesture-handler": "^2.28.0", + "react-native-linear-gradient": "^2.8.3", + "react-native-permissions": "^5.2.4", + "react-native-raw-bottom-sheet": "^3.0.0", + "react-native-reanimated": "^3.19.1", + "react-native-safe-area-context": "^5.6.1", + "react-native-screens": "^4.16.0", + "react-native-share": "^12.0.9", + "react-native-svg": "^15.12.1", + "react-native-toast-message": "^2.2.1", + "react-native-vector-icons": "^10.3.0", + "react-redux": "^9.2.0", + "redux-persist": "^6.0.0" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -25,6 +50,7 @@ "@types/jest": "^29.5.13", "@types/react": "^19.0.0", "@types/react-test-renderer": "^19.0.0", + "babel-plugin-module-resolver": "^5.0.2", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", @@ -140,7 +166,6 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -169,7 +194,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -191,7 +215,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -235,7 +258,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -279,7 +301,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -319,7 +340,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", @@ -337,7 +357,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -685,7 +704,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -803,7 +821,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -836,7 +853,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -920,7 +936,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", @@ -954,7 +969,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1257,7 +1271,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1343,7 +1356,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1428,7 +1440,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1650,7 +1661,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1699,7 +1709,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1731,7 +1740,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1784,7 +1792,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -1914,6 +1921,25 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", @@ -1994,6 +2020,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", @@ -2717,6 +2755,41 @@ "node": ">= 8" } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native-clipboard/clipboard": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz", + "integrity": "sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": ">= 16.9.0", + "react-native": ">= 0.61.5", + "react-native-macos": ">= 0.61.0", + "react-native-windows": ">= 0.61.0" + }, + "peerDependenciesMeta": { + "react-native-macos": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, "node_modules/@react-native-community/cli": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-18.0.0.tgz", @@ -2950,6 +3023,38 @@ "node": ">=10" } }, + "node_modules/@react-native-community/datetimepicker": { + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.4.4.tgz", + "integrity": "sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": ">=52.0.0", + "react": "*", + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/@react-native-community/netinfo": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz", + "integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==", + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.59" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.0", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.0.tgz", @@ -3284,6 +3389,151 @@ "dev": true, "license": "MIT" }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.7.tgz", + "integrity": "sha512-SQ4KuYV9yr3SV/thefpLWhAD0CU2CrBMG1l0w/QKl3GYuGWdN5OQmdQdmaPZGtsjjVOb+N9Qo7Tf6210P4TlpA==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.4", + "color": "^4.2.3" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.17", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.12.4", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.12.4.tgz", + "integrity": "sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.1", + "escape-string-regexp": "^4.0.0", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/elements": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.6.4.tgz", + "integrity": "sha512-O3X9vWXOEhAO56zkQS7KaDzL8BvjlwZ0LGSteKpt1/k6w6HONG+2Wkblrb057iKmehTkEkQMzMLkXiuLmN5x9Q==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.1.17", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.1.17", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.17.tgz", + "integrity": "sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.12.4", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.3.26", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.3.26.tgz", + "integrity": "sha512-EjaBWzLZ76HJGOOcWCFf+h/M+Zg7M1RalYioDOb6ZdXHz7AwYNidruT3OUAQgSzg3gVLqvu5OYO0jFsNDPCZxQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.4", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.17", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.1.tgz", + "integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@react-navigation/stack": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-7.4.8.tgz", + "integrity": "sha512-zZsX52Nw1gsq33Hx4aNgGV2RmDJgVJM71udomCi3OdlntqXDguav3J2t5oe/Acf/9uU8JiJE9W8JGtoRZ6nXIg==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.6.4", + "color": "^4.2.3" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.17", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-gesture-handler": ">= 2.0.0", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -3332,6 +3582,18 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3382,6 +3644,12 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3460,9 +3728,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "license": "MIT", "dependencies": { "undici-types": "~7.10.0" @@ -3501,6 +3769,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -3938,6 +4212,15 @@ "node": ">= 8" } }, + "node_modules/apisauce": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/apisauce/-/apisauce-3.2.0.tgz", + "integrity": "sha512-uEvNyBl86g9znFzb5DsBN0kaC9cs9Seo6Ztippf1lJgMEj/mcwE7YMsv4NZeKHjpxGOQ4AHDPItnDRiEgNIdDA==", + "license": "MIT", + "dependencies": { + "axios": "^1.10.0" + } + }, "node_modules/appdirsjs": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/appdirsjs/-/appdirsjs-1.2.7.tgz", @@ -4132,6 +4415,12 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4148,6 +4437,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4216,6 +4516,62 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/babel-plugin-module-resolver": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", + "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-babel-config": "^2.1.1", + "glob": "^9.3.3", + "pkg-up": "^3.1.0", + "reselect": "^4.1.7", + "resolve": "^1.22.8" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-module-resolver/node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", @@ -4399,6 +4755,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -4526,7 +4888,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4797,6 +5158,19 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4815,6 +5189,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", @@ -4822,6 +5206,18 @@ "devOptional": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", @@ -5018,6 +5414,47 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5113,6 +5550,15 @@ "node": ">=0.10.0" } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dedent": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", @@ -5194,6 +5640,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5259,11 +5714,65 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5314,6 +5823,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -5442,7 +5963,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5452,7 +5972,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5490,7 +6009,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5503,7 +6021,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6225,7 +6742,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6334,6 +6850,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -6379,6 +6904,16 @@ "node": ">= 0.8" } }, + "node_modules/find-babel-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.1.2.tgz", + "integrity": "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6424,6 +6959,26 @@ "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6440,6 +6995,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -6488,7 +7059,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6547,7 +7117,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6581,7 +7150,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "devOptional": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6736,7 +7304,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6813,7 +7380,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6826,7 +7392,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6842,7 +7407,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6866,6 +7430,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6980,6 +7559,16 @@ "node": ">=16.x" } }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -7382,6 +7971,15 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8801,7 +9399,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -9052,12 +9649,17 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -9074,6 +9676,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9598,6 +10212,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -9616,6 +10240,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9691,6 +10333,18 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -9713,7 +10367,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10054,6 +10707,40 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10064,6 +10751,15 @@ "node": ">=8" } }, + "node_modules/paths-js": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/paths-js/-/paths-js-0.4.11.tgz", + "integrity": "sha512-3mqcLomDBXOo7Fo+UlaenG6f71bk1ZezPQy2JCmYHy2W2k5VKpP+Jbin9H0bjXynelTbglCqdFhSEkeIkKTYUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.11.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10160,6 +10856,91 @@ "node": ">=8" } }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -10239,6 +11020,13 @@ "@types/yargs-parser": "*" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -10266,7 +11054,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -10278,7 +11065,12 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, "node_modules/punycode": { @@ -10324,6 +11116,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -10419,11 +11229,22 @@ } } }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "devOptional": true, + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", "license": "MIT" }, "node_modules/react-native": { @@ -10485,6 +11306,244 @@ } } }, + "node_modules/react-native-chart-kit": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz", + "integrity": "sha512-nZLGyCFzZ7zmX0KjYeeSV1HKuPhl1wOMlTAqa0JhlyW62qV/1ZPXHgT8o9s8mkFaGxdqbspOeuaa6I9jUQDgnA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.13", + "paths-js": "^0.4.10", + "point-in-polygon": "^1.0.1" + }, + "peerDependencies": { + "react": "> 16.7.0", + "react-native": ">= 0.50.0", + "react-native-svg": "> 6.4.1" + } + }, + "node_modules/react-native-element-dropdown": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/react-native-element-dropdown/-/react-native-element-dropdown-2.12.4.tgz", + "integrity": "sha512-abZc5SVji9FIt7fjojRYrbuvp03CoeZJrgvezQoDoSOrpiTqkX69ix5m+j06W2AVncA0VWvbT+vCMam8SoVadw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-gesture-handler": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", + "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz", + "integrity": "sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-linear-gradient": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz", + "integrity": "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-permissions": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.4.2.tgz", + "integrity": "sha512-XNMoG1fxrB9q73MLn/ZfTaP7pS8qPu0KWypbeFKVTvoR+JJ3O7uedMOTH/mts9bTG+GKhShOoZ+k0CR63q9jwA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.1.0", + "react-native": ">=0.70.0", + "react-native-windows": ">=0.70.0" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, + "node_modules/react-native-raw-bottom-sheet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-native-raw-bottom-sheet/-/react-native-raw-bottom-sheet-3.0.0.tgz", + "integrity": "sha512-kHR7j2ExCLqf/AO3MECozMJXi48O1+YxUYSRgRo/5Ftm7mEcrxJEzvjqMmqUbVhhKlfk5hLCGFnEQ5Z9OHCUtg==", + "license": "MIT" + }, + "node_modules/react-native-reanimated": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.19.1.tgz", + "integrity": "sha512-ILL0FSNzSVIg6WuawrsMBvNxk2yJFiTUcahimXDAeNiE/09eagVUlHhYWXAAmH0umvAOafBaGjO7YfBhUrf5ZQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-class-properties": "^7.0.0-0", + "@babel/plugin-transform-classes": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", + "@babel/plugin-transform-unicode-regex": "^7.0.0-0", + "@babel/preset-typescript": "^7.16.7", + "convert-source-map": "^2.0.0", + "invariant": "^2.2.4", + "react-native-is-edge-to-edge": "1.1.7" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz", + "integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", + "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "react-native-is-edge-to-edge": "^1.2.1", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens/node_modules/react-native-is-edge-to-edge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-share": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-12.2.0.tgz", + "integrity": "sha512-f6MB9BsKa9xVvu0DKbxq5jw4IyYHqQeqUlCNkD8eAFoJx6SD31ObPAn7SQ6NG9AOuhCy6aYuSJYJvx25DaoMZQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/react-native-svg": { + "version": "15.12.1", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", + "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-toast-message": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/react-native-toast-message/-/react-native-toast-message-2.3.3.tgz", + "integrity": "sha512-4IIUHwUPvKHu4gjD0Vj2aGQzqPATiblL1ey8tOqsxOWRPGGu52iIbL8M/mCz4uyqecvPdIcMY38AfwRuUADfQQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-vector-icons": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz", + "integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==", + "deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2", + "yargs": "^16.1.1" + }, + "bin": { + "fa-upgrade.sh": "bin/fa-upgrade.sh", + "fa5-upgrade": "bin/fa5-upgrade.sh", + "fa6-upgrade": "bin/fa6-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-vector-icons/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { "version": "0.79.0", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.0.tgz", @@ -10561,6 +11620,29 @@ "node": ">=10" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -10584,13 +11666,6 @@ "react": "^19.0.0" } }, - "node_modules/react-test-renderer/node_modules/react-is": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", - "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -10606,6 +11681,30 @@ "node": ">= 6" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10633,14 +11732,12 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -10680,7 +11777,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -10698,14 +11794,12 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -10718,7 +11812,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -10743,6 +11836,12 @@ "devOptional": true, "license": "ISC" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -11242,6 +12341,21 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -11323,6 +12437,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11400,6 +12523,15 @@ "node": ">= 0.4" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11951,7 +13083,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -11961,7 +13092,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -11975,7 +13105,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -11985,7 +13114,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -12050,6 +13178,24 @@ "punycode": "^2.1.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.4.tgz", + "integrity": "sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12106,6 +13252,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 14feb03..e7fff90 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,40 @@ "version": "0.0.1", "private": true, "scripts": { - "android": "react-native run-android", - "ios": "react-native run-ios", + "android": "npx react-native run-android", + "ios": "npx react-native run-ios", "lint": "eslint .", "start": "react-native start", "test": "jest" }, "dependencies": { + "@react-native-async-storage/async-storage": "^2.2.0", + "@react-native-clipboard/clipboard": "^1.16.1", + "@react-native-community/datetimepicker": "^8.4.4", + "@react-native-community/netinfo": "^11.4.1", + "@react-navigation/bottom-tabs": "^7.4.7", + "@react-navigation/native": "^7.1.17", + "@react-navigation/native-stack": "^7.3.26", + "@react-navigation/stack": "^7.4.8", + "@reduxjs/toolkit": "^2.9.0", + "apisauce": "^3.2.0", "react": "19.0.0", - "react-native": "0.79.0" + "react-native": "0.79.0", + "react-native-chart-kit": "^6.12.0", + "react-native-element-dropdown": "^2.12.4", + "react-native-gesture-handler": "^2.28.0", + "react-native-linear-gradient": "^2.8.3", + "react-native-permissions": "^5.2.4", + "react-native-raw-bottom-sheet": "^3.0.0", + "react-native-reanimated": "^3.19.1", + "react-native-safe-area-context": "^5.6.1", + "react-native-screens": "^4.16.0", + "react-native-share": "^12.0.9", + "react-native-svg": "^15.12.1", + "react-native-toast-message": "^2.2.1", + "react-native-vector-icons": "^10.3.0", + "react-redux": "^9.2.0", + "redux-persist": "^6.0.0" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -27,6 +52,7 @@ "@types/jest": "^29.5.13", "@types/react": "^19.0.0", "@types/react-test-renderer": "^19.0.0", + "babel-plugin-module-resolver": "^5.0.2", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", @@ -36,4 +62,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 0000000..af9904e --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,7 @@ +module.exports = { + project: { + ios: {}, + android: {}, + }, + assets: ['./src/assets/fonts'], // adjust according to your path + }; \ No newline at end of file diff --git a/src/assets/fonts/Roboto-Bold.ttf b/src/assets/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..9d7cf22 Binary files /dev/null and b/src/assets/fonts/Roboto-Bold.ttf differ diff --git a/src/assets/fonts/Roboto-Light.ttf b/src/assets/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..6fcd5f9 Binary files /dev/null and b/src/assets/fonts/Roboto-Light.ttf differ diff --git a/src/assets/fonts/Roboto-Medium.ttf b/src/assets/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..d629e98 Binary files /dev/null and b/src/assets/fonts/Roboto-Medium.ttf differ diff --git a/src/assets/fonts/Roboto-Regular.ttf b/src/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/src/assets/fonts/Roboto-Regular.ttf differ diff --git a/src/assets/fonts/Roboto-SemiBold.ttf b/src/assets/fonts/Roboto-SemiBold.ttf new file mode 100644 index 0000000..3f34834 Binary files /dev/null and b/src/assets/fonts/Roboto-SemiBold.ttf differ diff --git a/src/assets/fonts/Roboto-Thin.ttf b/src/assets/fonts/Roboto-Thin.ttf new file mode 100644 index 0000000..6ee97b8 Binary files /dev/null and b/src/assets/fonts/Roboto-Thin.ttf differ diff --git a/src/modules/auth/navigation/AuthNavigator.tsx b/src/modules/auth/navigation/AuthNavigator.tsx new file mode 100644 index 0000000..9342551 --- /dev/null +++ b/src/modules/auth/navigation/AuthNavigator.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import LoginScreen from '@/modules/auth/screens/LoginScreen'; + +const Stack = createStackNavigator(); + +const AuthNavigator = () => ( + + + +); + +export default AuthNavigator; + + diff --git a/src/modules/auth/screens/LoginScreen.tsx b/src/modules/auth/screens/LoginScreen.tsx new file mode 100644 index 0000000..f263fd4 --- /dev/null +++ b/src/modules/auth/screens/LoginScreen.tsx @@ -0,0 +1,297 @@ +import React from 'react'; +import { + View, + Text, + StyleSheet, + TextInput, + TouchableOpacity, + Pressable, +} from 'react-native'; +import GradientBackground from '@/shared/components/layout/GradientBackground'; +import LinearGradient from 'react-native-linear-gradient'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { useDispatch, useSelector } from 'react-redux'; +import { login } from '@/modules/auth/store/authSlice'; +import type { RootState } from '@/store/store'; +import { useTheme } from '@/shared/styles/useTheme'; + +const LoginScreen: React.FC = () => { + const dispatch = useDispatch(); + const { colors, fonts } = useTheme(); + const { loading, error } = useSelector((s: RootState) => s.auth); + + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [showPassword, setShowPassword] = React.useState(false); + const [rememberMe, setRememberMe] = React.useState(false); + const [focused, setFocused] = React.useState(null); + + const emailRef = React.useRef(null); + const passwordRef = React.useRef(null); + + const handleLogin = () => { + // @ts-ignore + dispatch(login({ email, password })); + }; + return ( + + + {/* Card */} + + {/* Logo placeholder */} + + + + + Login + Enter your email and password to log in + + {/* Email input */} + emailRef.current?.focus()} + > + + setFocused('email')} + onBlur={() => setFocused(null)} + onChangeText={setEmail} + onSubmitEditing={() => passwordRef.current?.focus()} + style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]} + /> + + + {/* Password input */} + passwordRef.current?.focus()} + > + + setFocused('password')} + onBlur={() => setFocused(null)} + onChangeText={setPassword} + style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]} + /> + setShowPassword(v => !v)} + hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} + accessibilityRole="button" + accessibilityLabel={showPassword ? 'Hide password' : 'Show password'} + > + + + + + {/* Row: Remember me + Forgot password */} + + setRememberMe(v => !v)}> + + Remember me + + + + Forgot Password ? + + + + {/* Login button */} + + + {loading ? 'Logging in...' : 'Log In'} + + + + {/* Or divider */} + + + Or login with + + + + {/* Social buttons */} + + + + + + + + + + + + + {/* Sign up */} + + Don’t have an account? + + Sign up + + + + {/* Error */} + {!!error && {error}} + + + + ); +}; + +const styles = StyleSheet.create({ + gradient: { + flex: 1, + }, + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 16, + }, + card: { + width: '100%', + maxWidth: 380, + borderRadius: 16, + padding: 16, + borderWidth: 1, + }, + logoCircle: { + alignSelf: 'center', + width: 48, + height: 48, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 8, + }, + title: { + fontSize: 24, + textAlign: 'center', + }, + subtitle: { + textAlign: 'center', + marginTop: 4, + marginBottom: 12, + }, + inputWrapper: { + marginTop: 12, + borderWidth: 1, + borderRadius: 10, + paddingHorizontal: 12, + height: 52, + flexDirection: 'row', + alignItems: 'center', + }, + input: { + paddingVertical: 0, + marginLeft: 8, + flex: 1, + }, + iconButton: { + position: 'absolute', + right: 8, + top: 0, + bottom: 0, + width: 36, + alignItems: 'center', + justifyContent: 'center', + }, + rowBetween: { + marginTop: 12, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + row: { + flexDirection: 'row', + alignItems: 'center', + }, + rememberText: { + marginLeft: 6, + }, + link: {}, + loginButton: { + height: 48, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + loginButtonText: { + color: '#FFFFFF', + fontSize: 16, + }, + orContainer: { + flexDirection: 'row', + alignItems: 'center', + marginVertical: 16, + }, + orLine: { + flex: 1, + height: 1, + }, + orText: { + marginHorizontal: 8, + fontSize: 12, + }, + socialRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + socialButton: { + width: 52, + height: 44, + borderRadius: 8, + borderWidth: 1, + alignItems: 'center', + justifyContent: 'center', + marginHorizontal: 6, + flex: 1, + }, + signupRow: { + flexDirection: 'row', + justifyContent: 'center', + marginTop: 16, + }, + signupText: { + fontSize: 12, + }, + errorText: { + marginTop: 12, + color: '#EF4444', + textAlign: 'center', + }, +}); + +export default LoginScreen; \ No newline at end of file diff --git a/src/modules/auth/services/authAPI.ts b/src/modules/auth/services/authAPI.ts new file mode 100644 index 0000000..209e489 --- /dev/null +++ b/src/modules/auth/services/authAPI.ts @@ -0,0 +1,8 @@ +import http from '@/services/http'; +import { API_ENDPOINTS } from '@/shared/constants/API_ENDPOINTS'; + +export const authAPI = { + login: (email: string, password: string) => http.post(API_ENDPOINTS.AUTH_LOGIN, { email, password }), +}; + + diff --git a/src/modules/auth/store/authSlice.ts b/src/modules/auth/store/authSlice.ts new file mode 100644 index 0000000..f3255b1 --- /dev/null +++ b/src/modules/auth/store/authSlice.ts @@ -0,0 +1,63 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; + +export interface AuthUser { + id: string; + name: string; + email: string; +} + +export interface AuthState { + user: AuthUser | null; + token: string | null; + loading: boolean; + error: string | null; +} + +const initialState: AuthState = { + user: null, + token: null, + loading: false, + error: null, +}; + +export const login = createAsyncThunk( + 'auth/login', + async (payload: { email: string; password: string }) => { + // TODO: integrate real API + await new Promise(r => setTimeout(r, 300)); + return { token: 'mock-token', user: { id: '1', name: 'User', email: payload.email } as AuthUser }; + }, +); + +const authSlice = createSlice({ + name: 'auth', + initialState, + reducers: { + logout: state => { + state.user = null; + state.token = null; + state.error = null; + }, + }, + extraReducers: builder => { + builder + .addCase(login.pending, state => { + state.loading = true; + state.error = null; + }) + .addCase(login.fulfilled, (state, action: PayloadAction<{ token: string; user: AuthUser }>) => { + state.loading = false; + state.token = action.payload.token; + state.user = action.payload.user; + }) + .addCase(login.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message || 'Login failed'; + }); + }, +}); + +export const { logout } = authSlice.actions; +export default authSlice; + + diff --git a/src/modules/collab/navigation/CollabNavigator.tsx b/src/modules/collab/navigation/CollabNavigator.tsx new file mode 100644 index 0000000..41922b2 --- /dev/null +++ b/src/modules/collab/navigation/CollabNavigator.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import CollabDashboardScreen from '@/modules/collab/screens/CollabDashboardScreen'; + +const Stack = createStackNavigator(); + +const CollabNavigator = () => ( + + + +); + +export default CollabNavigator; + + diff --git a/src/modules/collab/screens/CollabDashboardScreen.tsx b/src/modules/collab/screens/CollabDashboardScreen.tsx new file mode 100644 index 0000000..5de1e0e --- /dev/null +++ b/src/modules/collab/screens/CollabDashboardScreen.tsx @@ -0,0 +1,197 @@ +import React, { useMemo } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { Container } from '@/shared/components/ui'; +import { useTheme } from '@/shared/styles/useTheme'; + +const CollabDashboardScreen: React.FC = () => { + const { colors, fonts } = useTheme(); + + const mock = useMemo(() => { + const messagesToday = 4820; + const activeChannels = 38; + const mentions = 126; + const calls = 24; + const messageTrend = [540, 620, 580, 640, 710, 680]; + const heatmap = [ + [1, 2, 3, 2, 4, 1, 0], + [0, 1, 2, 3, 2, 1, 2], + [2, 3, 4, 5, 4, 3, 2], + [1, 2, 3, 3, 2, 2, 1], + [0, 1, 2, 1, 2, 3, 4], + ]; // rows=weeks, cols=days + const channelDist = [ + { label: 'General', value: 28, color: '#3AA0FF' }, + { label: 'Projects', value: 22, color: '#10B981' }, + { label: 'Support', value: 18, color: '#F59E0B' }, + { label: 'Social', value: 8, color: '#6366F1' }, + { label: 'Other', value: 6, color: '#EF4444' }, + ]; + const activeList = [ + { name: '#proj-alpha', unread: 18 }, + { name: '#support', unread: 12 }, + { name: '#marketing', unread: 9 }, + { name: '#random', unread: 4 }, + ]; + const mentionsList = [ + { user: 'Aarti', when: '2h', ctx: 'Please review the draft' }, + { user: 'Rahul', when: '5h', ctx: 'Meeting moved to 3 PM' }, + { user: 'Meera', when: '1d', ctx: 'Approved the change' }, + ]; + return { messagesToday, activeChannels, mentions, calls, messageTrend, heatmap, channelDist, activeList, mentionsList }; + }, []); + return ( + + + Communication & Collaboration + + {/* KPI Pills */} + + + + + + + + + + {/* Activity Heatmap */} + + Weekly Activity Heatmap + + Rows represent weeks, columns represent days (Mon-Sun) + + + {/* Message Volume */} + + Message Volume + + + + {/* Channel Distribution */} + + Channel Distribution + a + b.value, 0)} /> + + {mock.channelDist.map(s => ( + + + {s.label} + + ))} + + + + {/* Lists */} + + + Active Channels + {mock.activeList.map(c => ( + + {c.name} + + + ))} + + + Recent Mentions + {mock.mentionsList.map(m => ( + + {m.user} + {m.ctx} · {m.when} + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + wrap: { flex: 1, padding: 16 }, + title: { fontSize: 18, marginBottom: 8 }, + kpiRow: { flexDirection: 'row', justifyContent: 'space-between' }, + pill: { flex: 1, marginRight: 12, borderRadius: 12, paddingVertical: 12, paddingHorizontal: 14 }, + pillLast: { marginRight: 0 }, + pillTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + dot: { width: 8, height: 8, borderRadius: 4 }, + pillLabel: { fontSize: 12, opacity: 0.8 }, + pillValue: { fontSize: 18, marginTop: 6 }, + card: { borderRadius: 12, borderWidth: 1, padding: 12, marginTop: 12 }, + cardTitle: { fontSize: 16, marginBottom: 8 }, + heatRow: { flexDirection: 'row', marginBottom: 6 }, + heatCell: { width: 14, height: 14, borderRadius: 3, marginRight: 6 }, + bars: { flexDirection: 'row', alignItems: 'flex-end' }, + bar: { flex: 1, marginRight: 6, borderTopLeftRadius: 4, borderTopRightRadius: 4 }, + legendRow: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }, + legendItem: { flexDirection: 'row', alignItems: 'center', marginRight: 12, marginTop: 6 }, + legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, + row: { marginTop: 12 }, + col: { flex: 1, marginRight: 8 }, + listRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#E5E7EB' }, + listPrimary: { fontSize: 14, flex: 1, paddingRight: 8 }, + listSecondary: { fontSize: 12 }, + badge: { backgroundColor: '#E6F2FF', borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2 }, +}); + +export default CollabDashboardScreen; + +// UI pieces (no external deps) +const Pill: React.FC<{ label: string; value: string; color: string; fonts: any; bg: string; dot: string }> = ({ label, value, color, fonts, bg, dot }) => { + return ( + + + {label} + + + {value} + + ); +}; + +const Heatmap: React.FC<{ data: number[][]; baseColor: string }> = ({ data, baseColor }) => { + const max = Math.max(1, ...data.flat()); + const tint = (val: number) => { + const alpha = 0.2 + (val / max) * 0.8; // 0.2 - 1 + return `${baseColor}${Math.round(alpha * 255).toString(16).padStart(2, '0')}`; // add alpha to hex + }; + return ( + + {data.map((row, i) => ( + + {row.map((v, j) => ( + + ))} + + ))} + + ); +}; + +const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => { + return ( + + {data.map((v, i) => ( + + ))} + + ); +}; + +const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => { + return ( + + {segments.map(s => ( + + ))} + + ); +}; + +const Badge: React.FC<{ text: string; fonts: any }> = ({ text, fonts }) => ( + + {text} + +); + + diff --git a/src/modules/crm/navigation/CrmNavigator.tsx b/src/modules/crm/navigation/CrmNavigator.tsx new file mode 100644 index 0000000..a413358 --- /dev/null +++ b/src/modules/crm/navigation/CrmNavigator.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import CrmDashboardScreen from '@/modules/crm/screens/CrmDashboardScreen'; + +const Stack = createStackNavigator(); + +const CrmNavigator = () => ( + + + +); + +export default CrmNavigator; + + diff --git a/src/modules/crm/screens/CrmDashboardScreen.tsx b/src/modules/crm/screens/CrmDashboardScreen.tsx new file mode 100644 index 0000000..a2ad302 --- /dev/null +++ b/src/modules/crm/screens/CrmDashboardScreen.tsx @@ -0,0 +1,164 @@ +import React, { useMemo } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { Container } from '@/shared/components/ui'; +import { useTheme } from '@/shared/styles/useTheme'; + +const CrmDashboardScreen: React.FC = () => { + const { colors, fonts } = useTheme(); + + const mock = useMemo(() => { + const leads = 420; + const opportunities = 76; + const wonDeals = 28; + const conversionPct = 37; + const leadsTrend = [60, 62, 68, 70, 76, 84]; + const pipeline = [ + { label: 'Prospecting', value: 28, color: '#3AA0FF' }, + { label: 'Qualified', value: 18, color: '#10B981' }, + { label: 'Proposal', value: 12, color: '#F59E0B' }, + { label: 'Negotiation', value: 9, color: '#6366F1' }, + { label: 'Closed Won', value: 6, color: '#22C55E' }, + { label: 'Closed Lost', value: 7, color: '#EF4444' }, + ]; + const topOpps = [ + { name: 'Acme Upgrade', value: 48000 }, + { name: 'Globex Renewal', value: 36000 }, + { name: 'Initech Expansion', value: 29000 }, + ]; + const recent = [ + { who: 'Jane D.', what: 'Follow-up call completed', when: '2h' }, + { who: 'Sam R.', what: 'Demo scheduled', when: '5h' }, + { who: 'Priya K.', what: 'Proposal sent', when: '1d' }, + ]; + const sourceDist = [ + { label: 'Website', value: 180, color: '#3AA0FF' }, + { label: 'Referral', value: 120, color: '#10B981' }, + { label: 'Events', value: 64, color: '#F59E0B' }, + { label: 'Ads', value: 56, color: '#EF4444' }, + ]; + return { leads, opportunities, wonDeals, conversionPct, leadsTrend, pipeline, topOpps, recent, sourceDist }; + }, []); + return ( + + + CRM & Sales + + {/* KPIs */} + + + + + + + + {/* Leads Trend */} + + Leads Trend + + + + {/* Pipeline distribution */} + + Pipeline Stages + a + b.value, 0)} /> + + {mock.pipeline.map(s => ( + + + {s.label} + + ))} + + + + {/* Lead Sources */} + + Leads by Source + a + b.value, 0)} /> + + {mock.sourceDist.map(s => ( + + + {s.label} + + ))} + + + + {/* Lists */} + + + Top Opportunities + {mock.topOpps.map(o => ( + + {o.name} + ${o.value.toLocaleString()} + + ))} + + + Recent Activity + {mock.recent.map(r => ( + + {r.who} + {r.what} · {r.when} + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + wrap: { flex: 1, padding: 16 }, + title: { fontSize: 18, marginBottom: 8 }, + kpiGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, + kpiCard: { width: '48%', borderRadius: 12, borderWidth: 1, borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', padding: 12, marginBottom: 12 }, + kpiLabel: { fontSize: 12, opacity: 0.8 }, + kpiValueRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 8 }, + card: { borderRadius: 12, borderWidth: 1, padding: 12, marginTop: 12 }, + cardTitle: { fontSize: 16, marginBottom: 8 }, + bars: { flexDirection: 'row', alignItems: 'flex-end' }, + bar: { flex: 1, marginRight: 6, borderTopLeftRadius: 4, borderTopRightRadius: 4 }, + legendRow: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }, + legendItem: { flexDirection: 'row', alignItems: 'center', marginRight: 12, marginTop: 6 }, + legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, + row: { marginTop: 12 }, + col: { flex: 1, marginRight: 8 }, + listRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#E5E7EB' }, + listPrimary: { fontSize: 14, flex: 1, paddingRight: 8 }, + listSecondary: { fontSize: 12 }, +}); + +export default CrmDashboardScreen; + +// UI helpers +const Kpi: React.FC<{ label: string; value: string; color: string; fonts: any; accent: string }> = ({ label, value, color, fonts, accent }) => ( + + {label} + + {value} + + + +); + +const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => ( + + {data.map((v, i) => ( + + ))} + +); + +const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => ( + + {segments.map(s => ( + + ))} + +); + + diff --git a/src/modules/finance/navigation/FinanceNavigator.tsx b/src/modules/finance/navigation/FinanceNavigator.tsx new file mode 100644 index 0000000..375e699 --- /dev/null +++ b/src/modules/finance/navigation/FinanceNavigator.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import FinanceDashboardScreen from '@/modules/finance/screens/FinanceDashboardScreen'; + +const Stack = createStackNavigator(); + +const FinanceNavigator = () => ( + + + +); + +export default FinanceNavigator; + + diff --git a/src/modules/finance/screens/FinanceDashboardScreen.tsx b/src/modules/finance/screens/FinanceDashboardScreen.tsx new file mode 100644 index 0000000..b8db1c5 --- /dev/null +++ b/src/modules/finance/screens/FinanceDashboardScreen.tsx @@ -0,0 +1,230 @@ +import React, { useMemo } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { Container } from '@/shared/components/ui'; +import { useTheme } from '@/shared/styles/useTheme'; + +const FinanceDashboardScreen: React.FC = () => { + const { colors, fonts } = useTheme(); + + const mock = useMemo(() => { + // Zoho Books oriented metrics + const cash = 246000; + const invoices = { total: 485000, outstanding: 162000, overdue: 48000, paidThisMonth: 92000 }; + const monthlySales = [84, 92, 88, 104, 112, 118]; // in thousands + const arAging = [45, 30, 18, 7]; // 0-30, 31-60, 61-90, 90+ + const invoiceStatus = [ + { label: 'Draft', value: 30, color: '#94A3B8' }, + { label: 'Sent', value: 80, color: '#3AA0FF' }, + { label: 'Viewed', value: 50, color: '#6366F1' }, + { label: 'Paid', value: 210, color: '#10B981' }, + { label: 'Overdue', value: 18, color: '#EF4444' }, + ]; + const taxes = { collected: 38000, paid: 12500 }; + const topCustomers = [ + { client: 'Acme Corp', amount: 82000 }, + { client: 'Initech', amount: 54000 }, + { client: 'Umbrella', amount: 48000 }, + ]; + const bankAccounts = [ + { name: 'HDFC 1234', balance: 152000 }, + { name: 'ICICI 9981', balance: 78000 }, + ]; + const paymentModes = [ + { label: 'Online', value: 120, color: '#3AA0FF' }, + { label: 'Bank Transfer', value: 90, color: '#10B981' }, + { label: 'Cash', value: 40, color: '#F59E0B' }, + { label: 'Cheque', value: 12, color: '#6366F1' }, + ]; + const estimates = { sent: 24, accepted: 16, declined: 3 }; + return { cash, invoices, monthlySales, arAging, invoiceStatus, taxes, topCustomers, bankAccounts, paymentModes, estimates }; + }, []); + return ( + + + Accounts & Finance + + {/* KPIs */} + + + + + + + + {/* Sales Trend */} + + Sales Trend + + + + {/* Taxes & AR Aging */} + + + Taxes + {(() => { + const total = Math.max(1, mock.taxes.collected + mock.taxes.paid); + const colPct = Math.round((mock.taxes.collected / total) * 100); + const paidPct = Math.round((mock.taxes.paid / total) * 100); + return ( + <> + Collected: ${mock.taxes.collected.toLocaleString()} + + Paid: ${mock.taxes.paid.toLocaleString()} + + + ); + })()} + + + A/R Aging + + + 0-30 + 31-60 + 61-90 + 90+ + + + + + {/* Invoice Status Distribution */} + + Invoice Status + a + b.value, 0)} /> + + {mock.invoiceStatus.map(s => ( + + + {s.label} + + ))} + + + + {/* Lists */} + + + Top Customers + {mock.topCustomers.map(r => ( + + {r.client} + ${r.amount.toLocaleString()} + + ))} + + + Bank Accounts + {mock.bankAccounts.map(p => ( + + {p.name} + ${p.balance.toLocaleString()} + + ))} + + + + {/* Estimates & Payment Modes */} + + Estimates + + + Sent: {mock.estimates.sent} + + + Accepted: {mock.estimates.accepted} + + + Declined: {mock.estimates.declined} + + + + + + Payment Modes + a + b.value, 0)} /> + + {mock.paymentModes.map(s => ( + + + {s.label} + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + wrap: { flex: 1, padding: 16 }, + title: { fontSize: 18, marginBottom: 8 }, + kpiGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, + kpiCard: { width: '48%', borderRadius: 12, borderWidth: 1, borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', padding: 12, marginBottom: 12 }, + kpiLabel: { fontSize: 12, opacity: 0.8 }, + kpiValueRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 8 }, + card: { borderRadius: 12, borderWidth: 1, padding: 12, marginTop: 12 }, + cardTitle: { fontSize: 16, marginBottom: 8 }, + row: { flexDirection: 'row', marginTop: 12 }, + col: { flex: 1, marginRight: 8 }, + bars: { flexDirection: 'row', alignItems: 'flex-end' }, + bar: { flex: 1, marginRight: 6, borderTopLeftRadius: 4, borderTopRightRadius: 4 }, + progressWrap: { marginTop: 8 }, + progressTrack: { height: 8, borderRadius: 6, backgroundColor: '#E5E7EB', overflow: 'hidden' }, + progressFill: { height: '100%', borderRadius: 6 }, + legendRow: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }, + legendItem: { flexDirection: 'row', alignItems: 'center', marginRight: 12, marginTop: 6 }, + legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, + rowJustify: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 }, + listRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#E5E7EB' }, + listPrimary: { fontSize: 14, flex: 1, paddingRight: 8 }, + listSecondary: { fontSize: 12 }, +}); + +export default FinanceDashboardScreen; + +// UI bits +const Kpi: React.FC<{ label: string; value: string; color: string; fonts: any; accent: string }> = ({ label, value, color, fonts, accent }) => { + return ( + + {label} + + {value} + + + + ); +}; + +const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => { + return ( + + {data.map((v, i) => ( + + ))} + + ); +}; + +const Progress: React.FC<{ value: number; color: string; fonts: any }> = ({ value, color, fonts }) => { + return ( + + + + + {value}% + + ); +}; + +const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => { + return ( + + {segments.map(s => ( + + ))} + + ); +}; + + diff --git a/src/modules/hr/navigation/HRNavigator.tsx b/src/modules/hr/navigation/HRNavigator.tsx new file mode 100644 index 0000000..63f45fa --- /dev/null +++ b/src/modules/hr/navigation/HRNavigator.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import HRDashboardScreen from '@/modules/hr/screens/HRDashboardScreen'; + +const Stack = createStackNavigator(); + +const HRNavigator = () => ( + + + +); + +export default HRNavigator; + + diff --git a/src/modules/hr/screens/HRDashboardScreen.tsx b/src/modules/hr/screens/HRDashboardScreen.tsx new file mode 100644 index 0000000..4ac85d9 --- /dev/null +++ b/src/modules/hr/screens/HRDashboardScreen.tsx @@ -0,0 +1,209 @@ +import React, { useEffect, useMemo } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; +import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui'; +import { fetchHRMetrics } from '@/modules/hr/store/hrSlice'; +import type { RootState } from '@/store/store'; +import { useTheme } from '@/shared/styles/useTheme'; + +const HRDashboardScreen: React.FC = () => { + const dispatch = useDispatch(); + const { colors, fonts } = useTheme(); + const { metrics, loading, error } = useSelector((s: RootState) => s.hr); + + // Mock HR analytics (UI only) + const mock = useMemo(() => { + const headcount = 148; + const newHires30d = 9; + const attritionPct = 6; // lower is better + const attendancePct = 93; + const engagementPct = 78; + const hiresTrend = [2, 1, 3, 1, 2, 0]; + const exitsTrend = [1, 0, 1, 0, 1, 1]; + const deptDist = [ + { label: 'Engineering', value: 62, color: '#3AA0FF' }, + { label: 'Sales', value: 34, color: '#F59E0B' }, + { label: 'HR', value: 12, color: '#10B981' }, + { label: 'Ops', value: 28, color: '#6366F1' }, + { label: 'Finance', value: 12, color: '#EF4444' }, + ]; + const holidays = [ + { date: '2025-09-10', name: 'Ganesh Chaturthi' }, + { date: '2025-10-02', name: 'Gandhi Jayanti' }, + { date: '2025-10-31', name: 'Diwali' }, + ]; + const topPerformers = [ + { name: 'Aarti N.', score: 96 }, + { name: 'Rahul K.', score: 94 }, + { name: 'Meera S.', score: 92 }, + ]; + return { headcount, newHires30d, attritionPct, attendancePct, engagementPct, hiresTrend, exitsTrend, deptDist, holidays, topPerformers }; + }, []); + + useEffect(() => { + // @ts-ignore + dispatch(fetchHRMetrics()); + }, [dispatch]); + + if (loading && metrics.length === 0) { + return ; + } + if (error) { + return dispatch(fetchHRMetrics() as any)} />; + } + return ( + + + HR Dashboard + + + {/* KPI Cards */} + + + + + + + + {/* Trends: Hires vs Exits */} + + Workforce Movements + ({ in: h, out: mock.exitsTrend[i] }))} max={Math.max(...mock.hiresTrend.map((h, i) => Math.max(h, mock.exitsTrend[i])))} colorA="#10B981" colorB="#EF4444" /> + + Hires + Exits + + + + {/* People Health */} + + People Health + + + + + + {/* Department Distribution */} + + Department Distribution + a + b.value, 0)} /> + + {mock.deptDist.map(s => ( + + + {s.label} + + ))} + + + + {/* Lists: Holidays and Top Performers */} + + + Upcoming Holidays + {mock.holidays.map(h => ( + + {h.name} + {h.date} + + ))} + + + Top Performers + {mock.topPerformers.map(p => ( + + {p.name} + {p.score} + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + header: { + padding: 16, + backgroundColor: '#FFFFFF', + }, + title: { + fontSize: 24, + }, + content: { + padding: 16, + }, + kpiGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, + kpiCard: { width: '48%', borderRadius: 12, borderWidth: 1, borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', padding: 12, marginBottom: 12 }, + kpiLabel: { fontSize: 12, opacity: 0.8 }, + kpiValueRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 8 }, + card: { borderRadius: 12, borderWidth: 1, padding: 12, marginTop: 12 }, + cardTitle: { fontSize: 16, marginBottom: 8 }, + dualBarsRow: { flexDirection: 'row', alignItems: 'flex-end' }, + dualBarWrap: { flex: 1, marginRight: 8 }, + dualBarA: { borderTopLeftRadius: 4, borderTopRightRadius: 4 }, + dualBarB: { borderTopLeftRadius: 4, borderTopRightRadius: 4, marginTop: 4 }, + legendRow: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }, + legendItem: { flexDirection: 'row', alignItems: 'center', marginRight: 12, marginTop: 6 }, + legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, + row: { flexDirection: 'row', marginTop: 12 }, + col: { flex: 1, marginRight: 8 }, + listRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#E5E7EB' }, + listPrimary: { fontSize: 14, flex: 1, paddingRight: 8 }, + listSecondary: { fontSize: 12 }, +}); + +export default HRDashboardScreen; + +// UI helpers (no external deps) +const Kpi: React.FC<{ label: string; value: string; color: string; fonts: any; accent: string }> = ({ label, value, color, fonts, accent }) => { + return ( + + {label} + + {value} + + + + ); +}; + +const DualBars: React.FC<{ data: { in: number; out: number }[]; max: number; colorA: string; colorB: string }> = ({ data, max, colorA, colorB }) => { + return ( + + {data.map((d, i) => ( + + + + + ))} + + ); +}; + +const Progress: React.FC<{ label: string; value: number; color: string; fonts: any }> = ({ label, value, color, fonts }) => { + return ( + + + {label} + {value}% + + + + + + ); +}; + +const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => { + return ( + + {segments.map(s => ( + + ))} + + ); +}; + + diff --git a/src/modules/hr/services/hrAPI.ts b/src/modules/hr/services/hrAPI.ts new file mode 100644 index 0000000..cc67b97 --- /dev/null +++ b/src/modules/hr/services/hrAPI.ts @@ -0,0 +1,8 @@ +import http from '@/services/http'; +import { API_ENDPOINTS } from '@/shared/constants/API_ENDPOINTS'; + +export const hrAPI = { + getMetrics: () => http.get(API_ENDPOINTS.HR_METRICS), +}; + + diff --git a/src/modules/hr/store/hrSlice.ts b/src/modules/hr/store/hrSlice.ts new file mode 100644 index 0000000..08a2f5c --- /dev/null +++ b/src/modules/hr/store/hrSlice.ts @@ -0,0 +1,53 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; + +export interface EmployeeMetric { + id: string; + name: string; + value: number; +} + +export interface HRState { + metrics: EmployeeMetric[]; + loading: boolean; + error: string | null; +} + +const initialState: HRState = { + metrics: [], + loading: false, + error: null, +}; + +export const fetchHRMetrics = createAsyncThunk('hr/fetchMetrics', async () => { + // TODO: integrate real HR API + await new Promise(r => setTimeout(r, 300)); + return [ + { id: '1', name: 'Headcount', value: 42 }, + { id: '2', name: 'Attendance %', value: 96 }, + ] as EmployeeMetric[]; +}); + +const hrSlice = createSlice({ + name: 'hr', + initialState, + reducers: {}, + extraReducers: builder => { + builder + .addCase(fetchHRMetrics.pending, state => { + state.loading = true; + state.error = null; + }) + .addCase(fetchHRMetrics.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + state.metrics = action.payload; + }) + .addCase(fetchHRMetrics.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message || 'Failed to load HR metrics'; + }); + }, +}); + +export default hrSlice; + + diff --git a/src/modules/integrations/navigation/IntegrationsNavigator.tsx b/src/modules/integrations/navigation/IntegrationsNavigator.tsx new file mode 100644 index 0000000..fafc6e7 --- /dev/null +++ b/src/modules/integrations/navigation/IntegrationsNavigator.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import IntegrationsHomeScreen from '../screens/IntegrationsHomeScreen'; +import IntegrationCategoryScreen from '../screens/IntegrationCategoryScreen'; + +export type IntegrationsStackParamList = { + IntegrationsHome: undefined; + IntegrationCategory: { categoryKey: string; title: string }; +}; + +const Stack = createStackNavigator(); + +const IntegrationsNavigator = () => ( + + + ({ title: route.params.title,headerShown:false })} /> + +); + +export default IntegrationsNavigator; + + diff --git a/src/modules/integrations/screens/IntegrationCategoryScreen.tsx b/src/modules/integrations/screens/IntegrationCategoryScreen.tsx new file mode 100644 index 0000000..23a9a68 --- /dev/null +++ b/src/modules/integrations/screens/IntegrationCategoryScreen.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { useTheme } from '@/shared/styles/useTheme'; +import type { RouteProp } from '@react-navigation/native'; +import type { IntegrationsStackParamList } from '@/modules/integrations/navigation/IntegrationsNavigator'; +import { useDispatch } from 'react-redux'; +import { setSelectedService } from '@/modules/integrations/store/integrationsSlice'; +import type { AppDispatch } from '@/store/store'; + +type Route = RouteProp; + +const servicesMap: Record = { + operations: [ + { key: 'zohoProjects', title: 'Zoho Projects', icon: 'briefcase-check' }, + ], + finance: [ + { key: 'quickbooks', title: 'QuickBooks', icon: 'finance' }, + { key: 'zohoBooks', title: 'Zoho Books', icon: 'book-open-variant' }, + { key: 'tally', title: 'Tally', icon: 'calculator-variant' }, + { key: 'xero', title: 'Xero', icon: 'bank' }, + { key: 'sapb1', title: 'SAP Business One', icon: 'factory' }, + ], + hr: [ + { key: 'zohoPeople', title: 'Zoho People', icon: 'account' }, + { key: 'bamboohr', title: 'BambooHR', icon: 'sprout' }, + { key: 'workday', title: 'Workday', icon: 'briefcase' }, + { key: 'keka', title: 'Keka', icon: 'account-cash' }, + ], + crm: [ + { key: 'zohoCRM', title: 'Zoho CRM', icon: 'account-box-multiple' }, + { key: 'hubspot', title: 'HubSpot', icon: 'chart-timeline-variant' }, + { key: 'salesforce', title: 'Salesforce', icon: 'cloud' }, + { key: 'pipedrive', title: 'Pipedrive', icon: 'pipe' }, + ], + collab: [ + { key: 'slack', title: 'Slack', icon: 'slack' }, + { key: 'teams', title: 'Microsoft Teams', icon: 'microsoft-teams' }, + { key: 'zoom', title: 'Zoom', icon: 'video' }, + { key: 'gworkspace', title: 'Google Workspace', icon: 'google' }, + ], + storage: [ + { key: 'gdrive', title: 'Google Drive', icon: 'google-drive' }, + { key: 'dropbox', title: 'Dropbox Business', icon: 'dropbox' }, + { key: 'onedrive', title: 'OneDrive', icon: 'microsoft-onedrive' }, + { key: 'sharepoint', title: 'SharePoint', icon: 'microsoft-sharepoint' }, + { key: 'box', title: 'Box', icon: 'cube' }, + ], +}; + +interface Props { + route: Route; +} + +const IntegrationCategoryScreen: React.FC = ({ route }) => { + const { colors, fonts } = useTheme(); + const dispatch = useDispatch(); + const services = servicesMap[route.params.categoryKey] ?? []; + + return ( + + item.key} + contentContainerStyle={{ padding: 16 }} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + dispatch(setSelectedService(item.key))} + > + + + + {item.title} + + )} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1 }, + sep: { height: 1, opacity: 0.6 }, + row: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + }, + iconCircle: { + width: 36, + height: 36, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + title: { fontSize: 14 }, +}); + +export default IntegrationCategoryScreen; + + diff --git a/src/modules/integrations/screens/IntegrationsHomeScreen.tsx b/src/modules/integrations/screens/IntegrationsHomeScreen.tsx new file mode 100644 index 0000000..5c6302a --- /dev/null +++ b/src/modules/integrations/screens/IntegrationsHomeScreen.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { View, Text, StyleSheet, FlatList, TouchableOpacity, Dimensions, StatusBar } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { useTheme } from '@/shared/styles/useTheme'; +import type { StackNavigationProp } from '@react-navigation/stack'; +import type { IntegrationsStackParamList } from '@/modules/integrations/navigation/IntegrationsNavigator'; +import { useNavigation } from '@react-navigation/native'; +import GradientBackground from '@/shared/components/layout/GradientBackground'; + +type Nav = StackNavigationProp; + +const categories = [ + { key: 'operations', title: 'Operations', icon: 'cog' }, + { key: 'finance', title: 'Accounting & Finance Integration', icon: 'currency-usd' }, + { key: 'hr', title: 'HR Systems Integration', icon: 'account-group' }, + { key: 'crm', title: 'CRM & Sales Integration', icon: 'chart-line' }, + { key: 'collab', title: 'Communication & Collaboration', icon: 'message' }, + { key: 'storage', title: 'File Storage & Document Management', icon: 'folder' }, +]; + +const NUM_COLUMNS = 2; +const GUTTER = 12; +const screenWidth = Dimensions.get('window').width; +const CARD_WIDTH = (screenWidth - GUTTER * (NUM_COLUMNS + 1)) / NUM_COLUMNS; + +interface Props { + navigation: Nav; +} + +const IntegrationsHomeScreen: React.FC = () => { + const { colors, shadows, fonts } = useTheme(); + const navigation = useNavigation(); + + return ( + + + + Choose a Service + item.key} + numColumns={NUM_COLUMNS} + contentContainerStyle={{ padding: GUTTER }} + columnWrapperStyle={{ justifyContent: 'space-between' }} + renderItem={({ item }) => ( + navigation.navigate('IntegrationCategory', { categoryKey: item.key, title: item.title })} + style={[ + styles.card, + { backgroundColor: colors.surface, borderColor: colors.border, width: CARD_WIDTH, ...shadows.light }, + ]} + > + + + + + {item.title} + + + )} + /> + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + title: { + fontSize: 20, + marginTop: 12, + marginHorizontal: GUTTER, + marginBottom: 4, + }, + card: { + borderRadius: 12, + borderWidth: 1, + padding: 12, + marginBottom: GUTTER, + alignItems: 'center', + }, + iconCircle: { + width: 40, + height: 40, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 8, + }, + cardTitle: { + fontSize: 14, + textAlign: 'center', + }, +}); + +export default IntegrationsHomeScreen; + + diff --git a/src/modules/integrations/store/integrationsSlice.ts b/src/modules/integrations/store/integrationsSlice.ts new file mode 100644 index 0000000..20cb3dd --- /dev/null +++ b/src/modules/integrations/store/integrationsSlice.ts @@ -0,0 +1,27 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface IntegrationsState { + selectedService: string | null; +} + +const initialState: IntegrationsState = { + selectedService: null, +}; + +const integrationsSlice = createSlice({ + name: 'integrations', + initialState, + reducers: { + setSelectedService: (state, action: PayloadAction) => { + state.selectedService = action.payload; + }, + clearSelectedService: state => { + state.selectedService = null; + }, + }, +}); + +export const { setSelectedService, clearSelectedService } = integrationsSlice.actions; +export default integrationsSlice; + + diff --git a/src/modules/profile/navigation/ProfileNavigator.tsx b/src/modules/profile/navigation/ProfileNavigator.tsx new file mode 100644 index 0000000..979b772 --- /dev/null +++ b/src/modules/profile/navigation/ProfileNavigator.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import ProfileScreen from '@/modules/profile/screens/ProfileScreen'; + +const Stack = createStackNavigator(); + +const ProfileNavigator = () => ( + + + +); + +export default ProfileNavigator; + + diff --git a/src/modules/profile/screens/ProfileScreen.tsx b/src/modules/profile/screens/ProfileScreen.tsx new file mode 100644 index 0000000..1e4c9ec --- /dev/null +++ b/src/modules/profile/screens/ProfileScreen.tsx @@ -0,0 +1,169 @@ +import React, { useEffect } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Image } from 'react-native'; +import { useDispatch, useSelector } from 'react-redux'; +import { Container, ConfirmModal } from '@/shared/components/ui'; +import type { RootState } from '@/store/store'; +import { useTheme } from '@/shared/styles/useTheme'; +import { logout } from '@/modules/auth/store/authSlice'; +import { setProfile } from '@/modules/profile/store/profileSlice'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { clearSelectedService } from '@/modules/integrations/store/integrationsSlice'; + +const ProfileScreen: React.FC = () => { + const dispatch = useDispatch(); + const { colors, fonts } = useTheme(); + const { name, email } = useSelector((s: RootState) => s.profile); + + useEffect(() => { + // Seed dummy data if empty + if (!name && !email) { + dispatch(setProfile({ name: 'Jane Doe', email: 'jane.doe@example.com' })); + } + }, [dispatch, name, email]); + + const [showLogout, setShowLogout] = React.useState(false); + const handleLogout = () => setShowLogout(true); + const handleConfirmLogout = () => { + setShowLogout(false); + dispatch(clearSelectedService()); + dispatch(logout()); + }; + const handleCancelLogout = () => setShowLogout(false); + + return ( + + + {/* Avatar */} + + + {/* If you have an image, replace with */} + + + + + + + + {/* Name */} + {name || 'Sana Afzal'} + + {/* Email pill */} + + {email || 'sanaaafzal291@gmail.com'} + + + + {/* Settings card */} + + {}} /> + + {}} /> + + {}} /> + + {}} /> + + + + + + + ); +}; + +const styles = StyleSheet.create({ + top: { + alignItems: 'center', + paddingVertical: 32, + }, + avatarWrap: { + position: 'relative', + }, + avatarCircle: { + width: 104, + height: 104, + borderRadius: 52, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, + avatarImage: { + width: '100%', + height: '100%', + }, + editBadge: { + position: 'absolute', + right: -2, + bottom: 2, + width: 32, + height: 32, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + }, + displayName: { + fontSize: 22, + marginTop: 14, + }, + emailPill: { + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 14, + marginTop: 10, + }, + card: { + marginTop: 20, + marginHorizontal: 16, + borderRadius: 12, + borderWidth: 1, + }, +}); + +// Helpers +interface MenuItemProps { icon: string; label: string; onPress: () => void; danger?: boolean } +const MenuItem: React.FC = ({ icon, label, onPress, danger }) => { + const { colors, fonts } = useTheme(); + return ( + + + + {label} + + + + ); +}; + +const Separator: React.FC<{ color: string }> = ({ color }) => ( + +); + +const menuStyles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 14, + }, + left: { + flexDirection: 'row', + alignItems: 'center', + }, + label: { + marginLeft: 8, + fontSize: 14, + }, +}); + +export default ProfileScreen; + + diff --git a/src/modules/profile/store/profileSlice.ts b/src/modules/profile/store/profileSlice.ts new file mode 100644 index 0000000..d10d66c --- /dev/null +++ b/src/modules/profile/store/profileSlice.ts @@ -0,0 +1,38 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface ProfileState { + name: string; + email: string; + loading: boolean; + error: string | null; +} + +const initialState: ProfileState = { + name: '', + email: '', + loading: false, + error: null, +}; + +const profileSlice = createSlice({ + name: 'profile', + initialState, + reducers: { + setProfile: (state, action: PayloadAction<{ name: string; email: string }>) => { + state.name = action.payload.name; + state.email = action.payload.email; + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload; + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload; + }, + resetState: () => initialState, + }, +}); + +export const { setProfile, setLoading, setError, resetState } = profileSlice.actions; +export default profileSlice; + + diff --git a/src/modules/storage/navigation/StorageNavigator.tsx b/src/modules/storage/navigation/StorageNavigator.tsx new file mode 100644 index 0000000..d2c2b16 --- /dev/null +++ b/src/modules/storage/navigation/StorageNavigator.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import StorageDashboardScreen from '@/modules/storage/screens/StorageDashboardScreen'; + +const Stack = createStackNavigator(); + +const StorageNavigator = () => ( + + + +); + +export default StorageNavigator; + + diff --git a/src/modules/storage/screens/StorageDashboardScreen.tsx b/src/modules/storage/screens/StorageDashboardScreen.tsx new file mode 100644 index 0000000..17c0e4c --- /dev/null +++ b/src/modules/storage/screens/StorageDashboardScreen.tsx @@ -0,0 +1,155 @@ +import React, { useMemo } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { Container } from '@/shared/components/ui'; +import { useTheme } from '@/shared/styles/useTheme'; +import { useSelector } from 'react-redux'; +import type { RootState } from '@/store/store'; + +const StorageDashboardScreen: React.FC = () => { + const { colors, fonts } = useTheme(); + const selectedService = useSelector((s: RootState) => s.integrations.selectedService); + + const mock = useMemo(() => { + const totalUsageGb = '824/2000'; // GB + const filesCount = 58240; + const sharedLinks = 312; + const activeUsers = 86; + const monthlyUsage = [620, 640, 660, 700, 760, 824]; // GB + // Distribution within the active provider (file types) + const typeDist = [ + { label: 'Images', value: 260, color: '#3AA0FF' }, + { label: 'Videos', value: 140, color: '#6366F1' }, + { label: 'Documents', value: 280, color: '#10B981' }, + { label: 'PDFs', value: 96, color: '#F59E0B' }, + { label: 'Others', value: 48, color: '#EF4444' }, + ]; + const recentActivity = [ + { name: 'Q3_Report.pdf', action: 'Shared', when: '2h ago' }, + { name: 'BrandAssets.zip', action: 'Uploaded', when: '5h ago' }, + { name: 'Sales-2025.xlsx', action: 'Edited', when: '1d ago' }, + { name: 'Engineering-Docs', action: 'Moved', when: '2d ago' }, + ]; + const topFolders = [ + { name: 'Product Design', size: 186 }, + { name: 'Client Deliverables', size: 152 }, + { name: 'Marketing Assets', size: 104 }, + { name: 'Engineering Docs', size: 88 }, + ]; + return { totalUsageGb, filesCount, sharedLinks, activeUsers, monthlyUsage, typeDist, recentActivity, topFolders }; + }, []); + return ( + + + File Storage & Document Management + + {/* KPIs */} + + + + + + + + {/* Usage Trend */} + + Storage Growth + + + + {/* File Types Distribution (scoped to current provider) */} + + File Types + a + b.value, 0)} /> + + {mock.typeDist.map(s => ( + + + {s.label} + + ))} + + + + {/* Lists */} + + + Recent Activity + {mock.recentActivity.map(a => ( + + {a.name} + {a.action} · {a.when} + + ))} + + + Top Folders + {mock.topFolders.map(f => ( + + {f.name} + {f.size} GB + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + wrap: { flex: 1, padding: 16 }, + title: { fontSize: 18, marginBottom: 8 }, + kpiGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between' }, + kpiCard: { width: '48%', borderRadius: 12, borderWidth: 1, borderColor: '#E2E8F0', backgroundColor: '#FFFFFF', padding: 12, marginBottom: 12 }, + kpiLabel: { fontSize: 12, opacity: 0.8 }, + kpiValueRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 8 }, + card: { borderRadius: 12, borderWidth: 1, padding: 12, marginTop: 12 }, + cardTitle: { fontSize: 16, marginBottom: 8 }, + bars: { flexDirection: 'row', alignItems: 'flex-end' }, + bar: { flex: 1, marginRight: 6, borderTopLeftRadius: 4, borderTopRightRadius: 4 }, + legendRow: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }, + legendItem: { flexDirection: 'row', alignItems: 'center', marginRight: 12, marginTop: 6 }, + legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, + row: { marginTop: 12 }, + col: { flex: 1, marginRight: 8 }, + listRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#E5E7EB' }, + listPrimary: { fontSize: 14, flex: 1, paddingRight: 8 }, + listSecondary: { fontSize: 12 }, +}); + +export default StorageDashboardScreen; + +// UI bits +const Kpi: React.FC<{ label: string; value: string; color: string; fonts: any; accent: string }> = ({ label, value, color, fonts, accent }) => { + return ( + + {label} + + {value} + + + + ); +}; + +const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => { + return ( + + {data.map((v, i) => ( + + ))} + + ); +}; + +const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => { + return ( + + {segments.map(s => ( + + ))} + + ); +}; + + diff --git a/src/modules/zohoProjects/navigation/ZohoProjectsNavigator.tsx b/src/modules/zohoProjects/navigation/ZohoProjectsNavigator.tsx new file mode 100644 index 0000000..82e7ee4 --- /dev/null +++ b/src/modules/zohoProjects/navigation/ZohoProjectsNavigator.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import ZohoProjectsDashboardScreen from '@/modules/zohoProjects/screens/ZohoProjectsDashboardScreen'; + +const Stack = createStackNavigator(); + +const ZohoProjectsNavigator = () => ( + + + +); + +export default ZohoProjectsNavigator; + + diff --git a/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx b/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx new file mode 100644 index 0000000..e8a2903 --- /dev/null +++ b/src/modules/zohoProjects/screens/ZohoProjectsDashboardScreen.tsx @@ -0,0 +1,520 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { View, Text, ScrollView, RefreshControl, StyleSheet } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { useDispatch, useSelector } from 'react-redux'; + +import { Container, LoadingSpinner, ErrorState } from '@/shared/components/ui'; +import { useTheme } from '@/shared/styles/useTheme'; +import { fetchZohoProjects } from '@/modules/zohoProjects/store/zohoProjectsSlice'; +import type { RootState } from '@/store/store'; + +const ZohoProjectsDashboardScreen: React.FC = () => { + const { colors, fonts } = useTheme(); + const dispatch = useDispatch(); + const [refreshing, setRefreshing] = useState(false); + + const { projects, loading, error } = useSelector((s: RootState) => s.zohoProjects); + + useEffect(() => { + // Fetch projects on mount + // Guard: avoid duplicate fetch while loading + if (!loading) { + // @ts-ignore + dispatch(fetchZohoProjects()); + } + }, [dispatch]); + + const handleRefresh = async () => { + setRefreshing(true); + // @ts-ignore + await dispatch(fetchZohoProjects()); + setRefreshing(false); + }; + + // Mock analytics data (UI only) + const mock = useMemo(() => { + const backlog = 128; + const inProgress = 36; + const completed = 412; + const blocked = 7; + const onTimePct = 84; // percent + const qualityScore = 92; // percent + const utilizationPct = 73; // percent + const burndown = [32, 30, 28, 26, 24, 20, 18]; + const velocity = [18, 22, 19, 24, 26, 23]; + const statusDist = [ + { label: 'Open', value: backlog, color: '#3AA0FF' }, + { label: 'In Progress', value: inProgress, color: '#F59E0B' }, + { label: 'Blocked', value: blocked, color: '#EF4444' }, + { label: 'Done', value: completed, color: '#10B981' }, + ]; + const risks = [ + { id: 'R-1042', title: 'Scope creep in Phase 2', impact: 'High' }, + { id: 'R-1047', title: 'Resource bandwidth next sprint', impact: 'Medium' }, + { id: 'R-1051', title: 'Third-party API rate limits', impact: 'Medium' }, + ]; + const topClients = [ + { name: 'Acme Corp', projects: 12 }, + { name: 'Globex', projects: 9 }, + { name: 'Initech', projects: 7 }, + ]; + // New patterns + const kanban = { todo: 64, doing: 28, review: 12, done: completed }; + const milestones = [ + { name: 'M1: Requirements Freeze', due: 'Aug 25', progress: 100 }, + { name: 'M2: MVP Complete', due: 'Sep 10', progress: 72 }, + { name: 'M3: Beta Release', due: 'Sep 28', progress: 38 }, + ]; + const teams = [ + { name: 'Frontend', capacityPct: 76 }, + { name: 'Backend', capacityPct: 68 }, + { name: 'QA', capacityPct: 54 }, + { name: 'DevOps', capacityPct: 62 }, + ]; + // Zoho Projects specific + const sprintName = 'Sprint 24 - September'; + const sprintDates = 'Sep 01 → Sep 14'; + const scopeChange = +6; // tasks added + const bugSeverityDist = [ + { label: 'Critical', value: 4, color: '#EF4444' }, + { label: 'High', value: 12, color: '#F59E0B' }, + { label: 'Medium', value: 18, color: '#3AA0FF' }, + { label: 'Low', value: 9, color: '#10B981' }, + ]; + const priorityDist = [ + { label: 'P1', value: 8, color: '#EF4444' }, + { label: 'P2', value: 16, color: '#F59E0B' }, + { label: 'P3', value: 22, color: '#3AA0FF' }, + { label: 'P4', value: 12, color: '#10B981' }, + ]; + const timesheets = { totalHours: 436, billableHours: 312 }; + const backlogAging = [42, 31, 18, 12]; // 0-7, 8-14, 15-30, 30+ + const assigneeLoad = [ + { name: 'Aarti', pct: 82 }, + { name: 'Rahul', pct: 74 }, + { name: 'Meera', pct: 66 }, + { name: 'Neeraj', pct: 58 }, + ]; + return { backlog, inProgress, completed, blocked, onTimePct, qualityScore, utilizationPct, burndown, velocity, statusDist, risks, topClients, kanban, milestones, teams, sprintName, sprintDates, scopeChange, bugSeverityDist, priorityDist, timesheets, backlogAging, assigneeLoad }; + }, []); + + if (loading && projects.length === 0) { + return ; + } + + if (error) { + return dispatch(fetchZohoProjects() as any)} />; + } + + return ( + + } + > + + Zoho Projects + + + + + {/* Sprint Header */} + + Active Sprint + + + {mock.sprintName} + {mock.sprintDates} + + 0 ? 'Scope +' : 'Scope'} value={Math.abs(mock.scopeChange)} dot={mock.scopeChange > 0 ? '#F59E0B' : '#10B981'} /> + + + + {/* KPI Cards */} + + + + + + + + {/* Trend: Burndown & Velocity (mini bars) */} + + Sprint Trends + + + Burndown + + + + Velocity + + + + + + {/* Bugs & Priority Mix */} + + + Bugs by Severity + a + b.value, 0)} /> + + {mock.bugSeverityDist.map(s => ( + + + {s.label} + + ))} + + + + Tasks by Priority + a + b.value, 0)} /> + + {mock.priorityDist.map(s => ( + + + {s.label} + + ))} + + + + + {/* Timesheets & Aging */} + + + Timesheets + Total: {mock.timesheets.totalHours}h + {(() => { + const billablePct = Math.round((mock.timesheets.billableHours / Math.max(1, mock.timesheets.totalHours)) * 100); + return ; + })()} + {(() => { + const non = mock.timesheets.totalHours - mock.timesheets.billableHours; + const pct = Math.round((non / Math.max(1, mock.timesheets.totalHours)) * 100); + return ; + })()} + + + Backlog Aging + + + {['0-7d', '8-14d', '15-30d', '30+d'].map(label => ( + + + {label} + + ))} + + + + + {/* Assignee Workload */} + + Assignee Workload + {mock.assigneeLoad.map(a => ( + + ))} + + {/* Progress: On-time, Quality, Utilization */} + + Operational Health + + + + + + {/* Status distribution */} + + Status Distribution + a + b.value, 0)} /> + + {mock.statusDist.map(s => ( + + + {s.label} + + ))} + + + + {/* Kanban Snapshot */} + + Kanban Snapshot + + + + + + + + + {/* Lists: Risks and Top Clients */} + + + Top Risks + {mock.risks.map(r => ( + + {r.title} + {r.impact} + + ))} + + + Top Clients + {mock.topClients.map(c => ( + + {c.name} + {c.projects} projects + + ))} + + + + {/* Milestones Progress */} + + Milestones + {mock.milestones.map(m => ( + + + {m.name} + {m.due} + + + + + + ))} + + + {/* Team Capacity */} + + Team Capacity + {mock.teams.map(t => ( + + ))} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + backgroundColor: '#FFFFFF', + }, + title: { + fontSize: 24, + }, + content: { + padding: 16, + }, + kpiGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + kpiCard: { + width: '48%', + borderRadius: 12, + borderWidth: 1, + borderColor: '#E2E8F0', + backgroundColor: '#FFFFFF', + padding: 12, + marginBottom: 12, + }, + kpiLabel: { + fontSize: 12, + opacity: 0.8, + }, + kpiValueRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: 8, + }, + card: { + borderRadius: 12, + borderWidth: 1, + padding: 12, + marginTop: 12, + }, + cardTitle: { + fontSize: 16, + marginBottom: 8, + }, + trendRow: { + flexDirection: 'row', + }, + trendBlock: { + flex: 1, + paddingRight: 8, + }, + trendTitle: { + fontSize: 12, + opacity: 0.8, + marginBottom: 8, + }, + miniBars: { + flexDirection: 'row', + alignItems: 'flex-end', + }, + miniBar: { + flex: 1, + marginRight: 6, + borderTopLeftRadius: 4, + borderTopRightRadius: 4, + }, + progressRow: { + marginTop: 8, + }, + progressLabelWrap: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 6, + }, + progressTrack: { + height: 8, + borderRadius: 6, + backgroundColor: '#E5E7EB', + overflow: 'hidden', + }, + progressFill: { + height: '100%', + borderRadius: 6, + }, + stackedBar: { + height: 12, + borderRadius: 8, + backgroundColor: '#E5E7EB', + overflow: 'hidden', + }, + legendRow: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 8, + }, + legendItem: { + flexDirection: 'row', + alignItems: 'center', + marginRight: 12, + marginTop: 6, + }, + legendDot: { + width: 8, + height: 8, + borderRadius: 4, + marginRight: 6, + }, + twoCol: { + marginTop: 12, + }, + badgeRow: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + chip: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#F3F4F6', + paddingVertical: 6, + paddingHorizontal: 10, + borderRadius: 16, + marginRight: 8, + marginTop: 8, + }, + chipDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6 }, + col: { + flex: 1, + marginRight: 8, + }, + listRow: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 10, + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: '#E5E7EB', + }, + listPrimary: { + fontSize: 14, + flex: 1, + paddingRight: 8, + }, + listSecondary: { + fontSize: 12, + opacity: 0.8, + }, +}); + +export default ZohoProjectsDashboardScreen; + +// UI subcomponents (no external deps) +const KpiCard: React.FC<{ label: string; value: number | string; color: string; accent: string }> = ({ label, value, color, accent }) => { + const { fonts } = useTheme(); + return ( + + {label} + + {value} + + + + ); +}; + +const MiniBars: React.FC<{ data: number[]; color: string; max: number }> = ({ data, color, max }) => { + return ( + + {data.map((v, i) => ( + + ))} + + ); +}; + +const ProgressRow: React.FC<{ label: string; value: number; color: string }> = ({ label, value, color }) => { + const { fonts } = useTheme(); + return ( + + + {label} + {value}% + + + + + + ); +}; + +const StackedBar: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => { + return ( + + {segments.map(s => ( + + ))} + + ); +}; + +const Chip: React.FC<{ label: string; value: number; dot: string }> = ({ label, value, dot }) => { + const { fonts, colors } = useTheme(); + return ( + + + {label}: + {value} + + ); +}; + + diff --git a/src/modules/zohoProjects/services/zohoProjectsAPI.ts b/src/modules/zohoProjects/services/zohoProjectsAPI.ts new file mode 100644 index 0000000..80f38ca --- /dev/null +++ b/src/modules/zohoProjects/services/zohoProjectsAPI.ts @@ -0,0 +1,8 @@ +import http from '@/services/http'; +import { API_ENDPOINTS } from '@/shared/constants/API_ENDPOINTS'; + +export const zohoProjectsAPI = { + getProjects: () => http.get(API_ENDPOINTS.ZOHO_PROJECTS), +}; + + diff --git a/src/modules/zohoProjects/store/zohoProjectsSlice.ts b/src/modules/zohoProjects/store/zohoProjectsSlice.ts new file mode 100644 index 0000000..e704815 --- /dev/null +++ b/src/modules/zohoProjects/store/zohoProjectsSlice.ts @@ -0,0 +1,73 @@ +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; + +export interface ZohoProject { + id: string; + name: string; + owner: string; + status: 'active' | 'completed' | 'onHold'; +} + +export interface ZohoProjectsFilters { + owner: 'all' | string; + status: 'all' | ZohoProject['status']; +} + +export interface ZohoProjectsState { + projects: ZohoProject[]; + loading: boolean; + error: string | null; + filters: ZohoProjectsFilters; + lastUpdated: number | null; +} + +const initialState: ZohoProjectsState = { + projects: [], + loading: false, + error: null, + filters: { owner: 'all', status: 'all' }, + lastUpdated: null, +}; + +export const fetchZohoProjects = createAsyncThunk('zohoProjects/fetch', async () => { + // TODO: integrate real service + await new Promise(r => setTimeout(r, 300)); + return [ + { id: 'p1', name: 'CRM Revamp', owner: 'Alice', status: 'active' }, + { id: 'p2', name: 'Mobile App', owner: 'Bob', status: 'completed' }, + ] as ZohoProject[]; +}); + +const zohoProjectsSlice = createSlice({ + name: 'zohoProjects', + initialState, + reducers: { + setFilters: (state, action: PayloadAction>) => { + state.filters = { ...state.filters, ...action.payload } as ZohoProjectsFilters; + }, + clearError: state => { + state.error = null; + }, + resetState: () => initialState, + }, + extraReducers: builder => { + builder + .addCase(fetchZohoProjects.pending, state => { + state.loading = true; + state.error = null; + }) + .addCase(fetchZohoProjects.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + state.projects = action.payload; + state.lastUpdated = Date.now(); + }) + .addCase(fetchZohoProjects.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message || 'Failed to load projects'; + }); + }, +}); + +export const { setFilters, clearError, resetState } = zohoProjectsSlice.actions; +export default zohoProjectsSlice; + + diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..a89d737 --- /dev/null +++ b/src/navigation/AppNavigator.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { useTheme } from '@/shared/styles/useTheme'; +import { useDispatch, useSelector } from 'react-redux'; +import type { RootState } from '@/store/store'; +import { TouchableOpacity, View, StyleSheet } from 'react-native'; +import { clearSelectedService } from '@/modules/integrations/store/integrationsSlice'; + +// Module navigators +import ZohoProjectsNavigator from '@/modules/zohoProjects/navigation/ZohoProjectsNavigator'; +import HRNavigator from '@/modules/hr/navigation/HRNavigator'; +import ProfileNavigator from '@/modules/profile/navigation/ProfileNavigator'; +import FinanceNavigator from '@/modules/finance/navigation/FinanceNavigator'; +import CrmNavigator from '@/modules/crm/navigation/CrmNavigator'; +import CollabNavigator from '@/modules/collab/navigation/CollabNavigator'; +import StorageNavigator from '@/modules/storage/navigation/StorageNavigator'; + +const Tab = createBottomTabNavigator(); +// Stacks are now defined per module under their navigation folders + +const DashboardRouter: React.FC = () => { + const selectedService = useSelector((s: RootState) => s.integrations.selectedService); + const hrServices = ['zohoPeople', 'bamboohr', 'workday', 'keka']; + const financeServices = ['quickbooks', 'zohoBooks', 'tally', 'xero', 'sapb1']; + const crmServices = ['zohoCRM', 'hubspot', 'salesforce', 'pipedrive']; + const collabServices = ['slack', 'teams', 'zoom', 'gworkspace']; + const storageServices = ['gdrive', 'dropbox', 'onedrive', 'sharepoint', 'box']; + if (selectedService === 'zohoProjects') { + return ; + } + if (selectedService && hrServices.includes(selectedService)) { + return ; + } + if (selectedService && financeServices.includes(selectedService)) { + return ; + } + if (selectedService && crmServices.includes(selectedService)) { + return ; + } + if (selectedService && collabServices.includes(selectedService)) { + return ; + } + if (selectedService && storageServices.includes(selectedService)) { + return ; + } + // Default dashboard if unknown service + return ; +}; + +const AppTabs = () => { + const { colors } = useTheme(); + return ( + ({ + tabBarIcon: ({ color, size }) => { + const map: Record = { + Dashboard: 'dashboard', + Profile: 'person', + }; + const iconName = map[route.name] ?? 'dashboard'; + return ; + }, + headerShown: false, + tabBarActiveTintColor: colors.primary, + })} + > + + + + ); +}; + +const AppNavigator = () => ( + + + +); + +export default AppNavigator; + +// Local overlay wrapper to show a floating home button on Dashboard +const DashboardWithHome: React.FC = () => { + const { colors } = useTheme(); + const dispatch = useDispatch(); + return ( + + + dispatch(clearSelectedService())} + style={[styles.fab, { backgroundColor: colors.primary }]} + > + + + + ); +}; + +const styles = StyleSheet.create({ + fab: { + position: 'absolute', + right: 16, + bottom: 24, // sit above tab bar + width: 48, + height: 48, + borderRadius: 24, + alignItems: 'center', + justifyContent: 'center', + elevation: 6, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + }, +}); + + diff --git a/src/services/http.ts b/src/services/http.ts new file mode 100644 index 0000000..9055271 --- /dev/null +++ b/src/services/http.ts @@ -0,0 +1,10 @@ +import { create } from 'apisauce'; + +const http = create({ + baseURL: 'https://api.example.com', + timeout: 10000, +}); + +export default http; + + diff --git a/src/shared/components/layout/GradientBackground/index.tsx b/src/shared/components/layout/GradientBackground/index.tsx new file mode 100644 index 0000000..d078623 --- /dev/null +++ b/src/shared/components/layout/GradientBackground/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { ViewStyle, StyleProp, StatusBar } from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; + +type GradientPreset = 'warm' | 'cool'; + +interface GradientBackgroundProps { + children?: React.ReactNode; + style?: StyleProp; + colors?: string[]; + start?: { x: number; y: number }; + end?: { x: number; y: number }; + locations?: number[]; + preset?: GradientPreset; +} + +const PRESET_COLORS: Record = { + // Warm preset similar to the provided login mock + warm: ['#FFE9CC', '#F6E6FF'], + // Cool preset for headers/hero based on guidelines + cool: ['#3AA0FF', '#2D6BFF'], +}; + +const GradientBackground: React.FC = ({ + children, + style, + colors, + start, + end, + locations, + preset = 'warm', +}) => { + const gradientColors = colors ?? PRESET_COLORS[preset]; + const gradientStart = start ?? { x: 0, y: 0 }; + const gradientEnd = end ?? { x: 1, y: 1 }; + + return ( + + + {children} + + ); +}; + +export default GradientBackground; + + diff --git a/src/shared/components/ui/ConfirmModal/index.tsx b/src/shared/components/ui/ConfirmModal/index.tsx new file mode 100644 index 0000000..04d356d --- /dev/null +++ b/src/shared/components/ui/ConfirmModal/index.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Modal, View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useTheme } from '@/shared/styles/useTheme'; + +interface ConfirmModalProps { + visible: boolean; + title?: string; + message?: string; + confirmText?: string; + cancelText?: string; + onConfirm: () => void; + onCancel: () => void; +} + +const ConfirmModal: React.FC = ({ + visible, + title = 'Confirm', + message = 'Are you sure?', + confirmText = 'Yes', + cancelText = 'Cancel', + onConfirm, + onCancel, +}) => { + const { colors, fonts } = useTheme(); + + return ( + + + + {title} + {message} + + + + {cancelText} + + + {confirmText} + + + + + + ); +}; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.35)', + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + card: { + width: '100%', + borderRadius: 12, + borderWidth: 1, + padding: 16, + }, + title: { + fontSize: 18, + marginBottom: 8, + textAlign: 'center', + }, + message: { + fontSize: 14, + textAlign: 'center', + }, + actions: { + marginTop: 16, + flexDirection: 'row', + justifyContent: 'space-between', + }, + btn: { + flex: 1, + height: 44, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + btnOutline: { + borderWidth: 1, + marginRight: 8, + }, + btnPrimary: { + marginLeft: 8, + }, +}); + +export default ConfirmModal; + + diff --git a/src/shared/components/ui/Container/index.tsx b/src/shared/components/ui/Container/index.tsx new file mode 100644 index 0000000..9834092 --- /dev/null +++ b/src/shared/components/ui/Container/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { View, StyleSheet, ViewStyle, StyleProp, ScrollView } from 'react-native'; +import { useTheme } from '@/shared/styles/useTheme'; + +interface ContainerProps { + children?: React.ReactNode; + style?: StyleProp; +} + +const Container: React.FC = ({ children, style }) => { + const { colors } = useTheme(); + return {children}; +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); + +export default Container; + + diff --git a/src/shared/components/ui/ErrorState/index.tsx b/src/shared/components/ui/ErrorState/index.tsx new file mode 100644 index 0000000..f6fd5d9 --- /dev/null +++ b/src/shared/components/ui/ErrorState/index.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useTheme } from '@/shared/styles/useTheme'; + +interface ErrorStateProps { + message?: string; + onRetry?: () => void; +} + +const ErrorState: React.FC = ({ message = 'Something went wrong', onRetry }) => { + const { colors, spacing } = useTheme(); + return ( + + {message} + {onRetry ? ( + + Retry + + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + title: { + fontSize: 16, + marginBottom: 8, + }, + button: { + paddingHorizontal: 16, + paddingVertical: 8, + borderWidth: 1, + borderRadius: 6, + }, + buttonText: { + fontSize: 14, + }, +}); + +export default ErrorState; + + diff --git a/src/shared/components/ui/LoadingSpinner/index.tsx b/src/shared/components/ui/LoadingSpinner/index.tsx new file mode 100644 index 0000000..e81dd7d --- /dev/null +++ b/src/shared/components/ui/LoadingSpinner/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { ActivityIndicator, View, StyleSheet } from 'react-native'; +import { useTheme } from '@/shared/styles/useTheme'; + +const LoadingSpinner: React.FC = () => { + const { colors } = useTheme(); + return ( + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default LoadingSpinner; + + diff --git a/src/shared/components/ui/index.ts b/src/shared/components/ui/index.ts new file mode 100644 index 0000000..51a0363 --- /dev/null +++ b/src/shared/components/ui/index.ts @@ -0,0 +1,6 @@ +export { default as Container } from './Container'; +export { default as LoadingSpinner } from './LoadingSpinner'; +export { default as ErrorState } from './ErrorState'; +export { default as ConfirmModal } from './ConfirmModal'; + + diff --git a/src/shared/constants/API_ENDPOINTS.ts b/src/shared/constants/API_ENDPOINTS.ts new file mode 100644 index 0000000..4fc880f --- /dev/null +++ b/src/shared/constants/API_ENDPOINTS.ts @@ -0,0 +1,10 @@ +export const API_ENDPOINTS = { + AUTH_LOGIN: '/auth/login', + HR_METRICS: '/hr/metrics', + ZOHO_PROJECTS: '/zoho/projects', + PROFILE: '/profile', +} as const; + +export type ApiEndpointKey = keyof typeof API_ENDPOINTS; + + diff --git a/src/shared/store/uiSlice.ts b/src/shared/store/uiSlice.ts new file mode 100644 index 0000000..5f24467 --- /dev/null +++ b/src/shared/store/uiSlice.ts @@ -0,0 +1,37 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface UIState { + networkOnline: boolean; + globalLoading: boolean; + toastMessage: string | null; +} + +const initialState: UIState = { + networkOnline: true, + globalLoading: false, + toastMessage: null, +}; + +const uiSlice = createSlice({ + name: 'ui', + initialState, + reducers: { + setNetworkOnline: (state, action: PayloadAction) => { + state.networkOnline = action.payload; + }, + setGlobalLoading: (state, action: PayloadAction) => { + state.globalLoading = action.payload; + }, + showToast: (state, action: PayloadAction) => { + state.toastMessage = action.payload; + }, + clearToast: state => { + state.toastMessage = null; + }, + }, +}); + +export const { setNetworkOnline, setGlobalLoading, showToast, clearToast } = uiSlice.actions; +export default uiSlice; + + diff --git a/src/shared/styles/ThemeProvider.tsx b/src/shared/styles/ThemeProvider.tsx new file mode 100644 index 0000000..46e301c --- /dev/null +++ b/src/shared/styles/ThemeProvider.tsx @@ -0,0 +1,57 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { COLORS, FONTS, SPACING, SHADOWS } from './theme'; +import type { ColorScheme, ThemeContextValue, ThemeTokens } from './types'; + +export const ThemeContext = React.createContext(undefined); + +const STORAGE_KEY = 'theme.scheme'; + +const getInitialScheme = async (): Promise => { + try { + const stored = await AsyncStorage.getItem(STORAGE_KEY); + if (stored === 'dark' || stored === 'light') { + return stored; + } + } catch (_) { + // ignore read errors + } + return 'light'; +}; + +const buildTokens = (_scheme: ColorScheme): ThemeTokens => { + // For now, we use the same token set for both schemes. + return { + colors: COLORS, + spacing: SPACING, + fonts: FONTS, + shadows: SHADOWS, + }; +}; + +export const ThemeProvider: React.FC = ({ children }) => { + const [scheme, setSchemeState] = useState('light'); + + useEffect(() => { + // Hydrate theme on mount + getInitialScheme().then(setSchemeState); + }, []); + + const setScheme = useCallback((next: ColorScheme) => { + setSchemeState(next); + AsyncStorage.setItem(STORAGE_KEY, next).catch(() => undefined); + }, []); + + const tokens = useMemo(() => buildTokens(scheme), [scheme]); + + const value: ThemeContextValue = useMemo( + () => ({ ...tokens, isDark: scheme === 'dark', setScheme }), + [tokens, scheme, setScheme], + ); + + return {children}; +}; + +export default ThemeProvider; + + diff --git a/src/shared/styles/theme.ts b/src/shared/styles/theme.ts new file mode 100644 index 0000000..026734e --- /dev/null +++ b/src/shared/styles/theme.ts @@ -0,0 +1,87 @@ +export const COLORS = { + // Primary colors + primary: '#2C5F4A', + primaryLight: '#4A8B6A', + primaryDark: '#1A3D2E', + + // Secondary colors + secondary: '#FF6B35', + secondaryLight: '#FF8F65', + secondaryDark: '#E55A2B', + + // UI colors + background: '#F8F9FA', + surface: '#FFFFFF', + text: '#2D3748', + textLight: '#718096', + border: '#E2E8F0', + error: '#E53E3E', + success: '#38A169', + warning: '#D69E2E', + + // Dashboard specific + chartPrimary: '#2C5F4A', + chartSecondary: '#FF6B35', + chartTertiary: '#4299E1', + chartQuaternary: '#48BB78', +}; + +export const FONTS = { + regular: 'Roboto-Regular', + medium: 'Roboto-Medium', + bold: 'Roboto-Bold', + light: 'Roboto-Light', + black: 'Roboto-Black', +}; + +export const FONT_SIZES = { + xs: 12, + sm: 14, + md: 16, + lg: 18, + xl: 20, + xxl: 24, + xxxl: 32, +}; + +export const SPACING = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, +}; + +export const BORDER_RADIUS = { + sm: 4, + md: 8, + lg: 12, + xl: 16, +}; + +export const SHADOWS = { + light: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + medium: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 4, + elevation: 4, + }, + heavy: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 8, + }, +}; + + diff --git a/src/shared/styles/types.ts b/src/shared/styles/types.ts new file mode 100644 index 0000000..72c8aa3 --- /dev/null +++ b/src/shared/styles/types.ts @@ -0,0 +1,62 @@ +export type ColorScheme = 'light' | 'dark'; + +export interface ThemeColors { + primary: string; + primaryLight: string; + primaryDark: string; + + secondary: string; + secondaryLight: string; + secondaryDark: string; + + background: string; + surface: string; + text: string; + textLight: string; + border: string; + error: string; + success: string; + warning: string; + + chartPrimary: string; + chartSecondary: string; + chartTertiary: string; + chartQuaternary: string; +} + +export interface ThemeSpacing { + xs: number; + sm: number; + md: number; + lg: number; + xl: number; + xxl: number; +} + +export interface ThemeFonts { + regular: string; + medium: string; + bold: string; + light: string; + black: string; +} + +export interface ThemeShadows { + light: object; + medium: object; + heavy: object; +} + +export interface ThemeTokens { + colors: ThemeColors; + spacing: ThemeSpacing; + fonts: ThemeFonts; + shadows: ThemeShadows; +} + +export interface ThemeContextValue extends ThemeTokens { + isDark: boolean; + setScheme: (scheme: ColorScheme) => void; +} + + diff --git a/src/shared/styles/useTheme.ts b/src/shared/styles/useTheme.ts new file mode 100644 index 0000000..da93869 --- /dev/null +++ b/src/shared/styles/useTheme.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; +import { ThemeContext } from './ThemeProvider'; +import type { ThemeContextValue } from './types'; + +export const useTheme = (): ThemeContextValue => { + const ctx = useContext(ThemeContext); + if (!ctx) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return ctx; +}; + +export default useTheme; + + diff --git a/src/shared/utils/Toast.ts b/src/shared/utils/Toast.ts new file mode 100644 index 0000000..116c5cf --- /dev/null +++ b/src/shared/utils/Toast.ts @@ -0,0 +1,31 @@ +import Toast from 'react-native-toast-message'; + +export const showSuccess = (message?: string) => { + const text = (message ?? '').trim() || 'Action completed successfully'; + Toast.show({ + type: 'success', + text1: text, + }); +}; + +export const showError = (message?: string) => { + const text = (message ?? '').trim() || 'Something went wrong'; + Toast.show({ + type: 'error', + text1: text, + }); +}; +export const showWarning = (message?: string) => { + const text = (message ?? '').trim() || 'Please check this'; + Toast.show({ + type: 'warning', + text1: text, + }); +}; +export const showInfo = (message?: string) => { + const text = (message ?? '').trim() || 'Here is some information'; + Toast.show({ + type: 'info', + text1: text, + }); +}; \ No newline at end of file diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..5b32204 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,53 @@ +import { configureStore, combineReducers } from '@reduxjs/toolkit'; +import { persistReducer, persistStore } from 'redux-persist'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +// Feature slices (to be added) +import uiSlice from '@/shared/store/uiSlice'; +import authSlice from '@/modules/auth/store/authSlice'; +import hrSlice from '@/modules/hr/store/hrSlice'; +import zohoProjectsSlice from '@/modules/zohoProjects/store/zohoProjectsSlice'; +import profileSlice from '@/modules/profile/store/profileSlice'; +import integrationsSlice from '@/modules/integrations/store/integrationsSlice'; + +const rootReducer = combineReducers({ + auth: authSlice.reducer, + hr: hrSlice.reducer, + zohoProjects: zohoProjectsSlice.reducer, + profile: profileSlice.reducer, + integrations: integrationsSlice.reducer, + ui: uiSlice.reducer, +}); + +const persistConfig = { + key: 'root', + storage: AsyncStorage, + whitelist: ['auth', 'hr', 'zohoProjects', 'profile', 'integrations'], + blacklist: ['ui'], +}; + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: persistedReducer, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [ + 'persist/FLUSH', + 'persist/REHYDRATE', + 'persist/PAUSE', + 'persist/PERSIST', + 'persist/PURGE', + 'persist/REGISTER', + ], + }, + }), +}); + +export const persistor = persistStore(store); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + + diff --git a/src/types/react-native-vector-icons.d.ts b/src/types/react-native-vector-icons.d.ts new file mode 100644 index 0000000..d068d9f --- /dev/null +++ b/src/types/react-native-vector-icons.d.ts @@ -0,0 +1,25 @@ +declare module 'react-native-vector-icons/MaterialIcons' { + import { ComponentType } from 'react'; + import { TextProps } from 'react-native'; + interface IconProps extends TextProps { + name: string; + size?: number; + color?: string; + } + const Icon: ComponentType; + export default Icon; +} + +declare module 'react-native-vector-icons/MaterialCommunityIcons' { + import { ComponentType } from 'react'; + import { TextProps } from 'react-native'; + interface IconProps extends TextProps { + name: string; + size?: number; + color?: string; + } + const Icon: ComponentType; + export default Icon; +} + + diff --git a/tsconfig.json b/tsconfig.json index 304ab4e..cab5eac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,10 @@ { - "extends": "@react-native/typescript-config/tsconfig.json" + "extends": "@react-native/typescript-config/tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src", "App.tsx", "index.js"] }