first commit

This commit is contained in:
yashwin-foxy 2025-09-05 18:09:40 +05:30
parent e911c3c835
commit bcaa1fbdaa
72 changed files with 5763 additions and 186 deletions

View File

@ -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<ZohoProjectsScreenProps> = ({
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 <LoadingSpinner />;
}
// Error state
if (error) {
return <ErrorState onRetry={() => dispatch(fetchZohoProjects())} />;
}
return (
<Container>
<ScrollView
style={styles.container}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
<View style={styles.header}>
<Text style={styles.title}>Zoho Projects</Text>
<Icon
name="insights"
size={24}
color={COLORS.primary}
/>
</View>
{/* Dashboard content */}
<View style={styles.content}>
{/* Widgets and components */}
</View>
</ScrollView>
</Container>
);
};
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<RevenueChartProps> = ({
data,
period,
onPeriodChange,
onPress,
}) => {
const totalRevenue = data.reduce((sum, item) => sum + item.amount, 0);
return (
<TouchableOpacity
style={styles.container}
onPress={onPress}
activeOpacity={0.8}
>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Icon name="trending-up" size={20} color={COLORS.primary} />
<Text style={styles.title}>Revenue Analytics</Text>
</View>
<View style={styles.periodSelector}>
{['month', 'quarter', 'year'].map((p) => (
<TouchableOpacity
key={p}
style={[
styles.periodButton,
period === p && styles.periodButtonActive,
]}
onPress={() => onPeriodChange(p as any)}
>
<Text
style={[
styles.periodText,
period === p && styles.periodTextActive,
]}
>
{p.charAt(0).toUpperCase() + p.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={styles.content}>
<Text style={styles.amount}>
${totalRevenue.toLocaleString()}
</Text>
<Text style={styles.subtitle}>Total Revenue</Text>
{/* Chart component would go here */}
<View style={styles.chartPlaceholder}>
<Text style={styles.chartText}>Chart Component</Text>
</View>
</View>
</TouchableOpacity>
);
};
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<typeof store.getState>;
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<ZohoProject>) => {
const response = await zohoProjectsAPI.updateProject(project);
return response.data;
}
);
// Slice
const zohoProjectsSlice = createSlice({
name: 'zohoProjects',
initialState,
reducers: {
setFilters: (state, action: PayloadAction<Partial<ZohoProjectsFilters>>) => {
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<FinanceData>) => {
const response = await financeAPI.updateData(data);
return response.data;
}
);
// Slice
const financeSlice = createSlice({
name: 'finance',
initialState,
reducers: {
setFilters: (state, action: PayloadAction<Partial<FinanceFilters>>) => {
state.filters = { ...state.filters, ...action.payload };
},
clearError: (state) => {
state.error = null;
},
resetState: () => initialState,
},
extraReducers: (builder) => {
builder
// Fetch data cases
.addCase(fetchFinanceData

View File

@ -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 = () => (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const iconName = getTabIconName(route.name);
return <Icon name={iconName} size={size} color={color} />;
},
})}
>
<Tab.Screen name="ZohoProjects" component={ZohoProjectsStack} />
<Tab.Screen name="HR" component={HRStack} />
<Tab.Screen name="Profile" component={ProfileStack} />
</Tab.Navigator>
);
```
### Stack Navigators for Each Module
```typescript
// HR Stack
const HRStack = () => (
<Stack.Navigator>
<Stack.Screen name="HRDashboard" component={HRDashboardScreen} />
<Stack.Screen name="EmployeeMetrics" component={EmployeeMetricsScreen} />
<Stack.Screen name="Attendance" component={AttendanceScreen} />
<Stack.Screen name="Recruitment" component={RecruitmentScreen} />
</Stack.Navigator>
);
// Zoho Projects Stack
const ZohoProjectsStack = () => (
<Stack.Navigator>
<Stack.Screen name="ZohoProjectsDashboard" component={ZohoProjectsDashboardScreen} />
<Stack.Screen name="ProjectPerformance" component={ProjectPerformanceScreen} />
<Stack.Screen name="ResourceUtilization" component={ResourceUtilizationScreen} />
<Stack.Screen name="ClientAnalytics" component={ClientAnalyticsScreen} />
</Stack.Navigator>
);
// Profile Stack
const ProfileStack = () => (
<Stack.Navigator>
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="EditProfile" component={EditProfileScreen} />
</Stack.Navigator>
);
```
---
## 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 (
<View style={{
backgroundColor: colors.surface,
padding: spacing.md,
borderRadius: 12,
...shadows.medium,
}} />
);
};
```
---
## 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.

152
App.tsx
View File

@ -6,126 +6,50 @@
*/ */
import React from 'react'; import React from 'react';
import type {PropsWithChildren} from 'react'; import { Provider } from 'react-redux';
import { import { PersistGate } from 'redux-persist/integration/react';
ScrollView, import { useSelector } from 'react-redux';
StatusBar, import { NavigationContainer } from '@react-navigation/native';
StyleSheet, import AppNavigator from '@/navigation/AppNavigator';
Text, import { store, persistor } from '@/store/store';
useColorScheme, import { ThemeProvider } from '@/shared/styles/ThemeProvider';
View, import LoadingSpinner from '@/shared/components/ui/LoadingSpinner';
} from 'react-native'; 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 { function AppContent(): React.JSX.Element {
Colors, const isAuthenticated = useSelector((s: RootState) => Boolean(s.auth.token));
DebugInstructions, const selectedService = useSelector((s: RootState) => s.integrations.selectedService);
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
type SectionProps = PropsWithChildren<{
title: string;
}>;
function Section({children, title}: SectionProps): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
return ( return (
<View style={styles.sectionContainer}> <ThemeProvider>
<Text <PersistGate loading={<LoadingSpinner />} persistor={persistor}>
style={[ <StatusBar backgroundColor={'#FFFFFF'} barStyle={'dark-content'} />
styles.sectionTitle, {!isAuthenticated ? (
{ <NavigationContainer>
color: isDarkMode ? Colors.white : Colors.black, <AuthNavigator />
}, </NavigationContainer>
]}> ) : (
{title} !selectedService ? (
</Text> <NavigationContainer>
<Text <IntegrationsNavigator/>
style={[ </NavigationContainer>
styles.sectionDescription, ) : (
{ <AppNavigator />
color: isDarkMode ? Colors.light : Colors.dark, )
}, )}
]}> </PersistGate>
{children} </ThemeProvider>
</Text>
</View>
); );
} }
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 ( return (
<View style={backgroundStyle}> <Provider store={store}>
<StatusBar <AppContent />
barStyle={isDarkMode ? 'light-content' : 'dark-content'} </Provider>
backgroundColor={backgroundStyle.backgroundColor}
/>
<ScrollView
style={backgroundStyle}>
<View style={{paddingRight: safePadding}}>
<Header/>
</View>
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
paddingHorizontal: safePadding,
paddingBottom: safePadding,
}}>
<Section title="Step One">
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
screen and then come back to see your edits.
</Section>
<Section title="See Your Changes">
<ReloadInstructions />
</Section>
<Section title="Debug">
<DebugInstructions />
</Section>
<Section title="Learn More">
Read the docs to discover what to do next:
</Section>
<LearnMoreLinks />
</View>
</ScrollView>
</View>
); );
} }
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;

View File

@ -117,3 +117,4 @@ dependencies {
implementation jscFlavor implementation jscFlavor
} }
} }
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,6 +4,7 @@
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style> </style>
</resources> </resources>

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -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"
}
]
}

View File

@ -1,3 +1,15 @@
module.exports = { module.exports = {
presets: ['module:@react-native/babel-preset'], presets: ['module:@react-native/babel-preset'],
plugins: [
'react-native-reanimated/plugin',
[
'module-resolver',
{
root: ['./'],
alias: {
'@': './src',
},
},
],
],
}; };

View File

@ -1,7 +1,7 @@
/** /**
* @format * @format
*/ */
import 'react-native-gesture-handler';
import {AppRegistry} from 'react-native'; import {AppRegistry} from 'react-native';
import App from './App'; import App from './App';
import {name as appName} from './app.json'; import {name as appName} from './app.json';

View File

@ -11,6 +11,12 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -35,6 +41,12 @@
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = CentralizedReportingSystem/AppDelegate.swift; sourceTree = "<group>"; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = CentralizedReportingSystem/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = CentralizedReportingSystem/LaunchScreen.storyboard; sourceTree = "<group>"; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = CentralizedReportingSystem/LaunchScreen.storyboard; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 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 = "<group>"; 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 = "<group>"; 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 = "<group>"; 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 = "<group>"; 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 = "<group>"; 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 = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -93,6 +105,7 @@
83CBBA001A601CBA00E9B192 /* Products */, 83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */, 2D16E6871FA4F8E400B85C8A /* Frameworks */,
BBD78D7AC51CEA395F1C20DB /* Pods */, BBD78D7AC51CEA395F1C20DB /* Pods */,
034FBE3A8610463F9085B792 /* Resources */,
); );
indentWidth = 2; indentWidth = 2;
sourceTree = "<group>"; sourceTree = "<group>";
@ -116,6 +129,20 @@
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
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 = "<group>";
path = "";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -185,6 +212,12 @@
files = ( files = (
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets 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; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -26,14 +26,13 @@
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<false/> <false/>
<key>NSAllowsLocalNetworking</key> <key>NSAllowsLocalNetworking</key>
<true/> <true/>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
@ -48,5 +47,14 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>UIAppFonts</key>
<array>
<string>Roboto-Bold.ttf</string>
<string>Roboto-Light.ttf</string>
<string>Roboto-Medium.ttf</string>
<string>Roboto-Regular.ttf</string>
<string>Roboto-SemiBold.ttf</string>
<string>Roboto-Thin.ttf</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -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"
}
]
}

1278
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,40 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "npx react-native run-android",
"ios": "react-native run-ios", "ios": "npx react-native run-ios",
"lint": "eslint .", "lint": "eslint .",
"start": "react-native start", "start": "react-native start",
"test": "jest" "test": "jest"
}, },
"dependencies": { "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": "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": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
@ -27,6 +52,7 @@
"@types/jest": "^29.5.13", "@types/jest": "^29.5.13",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-test-renderer": "^19.0.0", "@types/react-test-renderer": "^19.0.0",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"jest": "^29.6.3", "jest": "^29.6.3",
"prettier": "2.8.8", "prettier": "2.8.8",

7
react-native.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
project: {
ios: {},
android: {},
},
assets: ['./src/assets/fonts'], // adjust according to your path
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="Login" component={LoginScreen} options={{
headerShown: false, // Hide header for login screen
}}/>
</Stack.Navigator>
);
export default AuthNavigator;

View File

@ -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 | 'email' | 'password'>(null);
const emailRef = React.useRef<TextInput>(null);
const passwordRef = React.useRef<TextInput>(null);
const handleLogin = () => {
// @ts-ignore
dispatch(login({ email, password }));
};
return (
<GradientBackground preset="warm" style={styles.gradient}>
<View style={styles.container}>
{/* Card */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
{/* Logo placeholder */}
<View style={[styles.logoCircle, { backgroundColor: '#F1F5F9' }]}>
<Icon name="shield" size={28} color={colors.primary} />
</View>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Login</Text>
<Text style={[styles.subtitle, { color: colors.textLight, fontFamily: fonts.regular }]}>Enter your email and password to log in</Text>
{/* Email input */}
<Pressable
style={[
styles.inputWrapper,
{
borderColor: focused === 'email' ? colors.primary : colors.border,
backgroundColor: colors.surface,
},
]}
onPress={() => emailRef.current?.focus()}
>
<Icon name="email-outline" size={20} color={focused === 'email' ? colors.primary : colors.textLight} />
<TextInput
ref={emailRef}
placeholder="Email"
placeholderTextColor={colors.textLight}
autoCapitalize="none"
autoCorrect={false}
autoComplete="email"
keyboardType="email-address"
returnKeyType="next"
value={email}
onFocus={() => setFocused('email')}
onBlur={() => setFocused(null)}
onChangeText={setEmail}
onSubmitEditing={() => passwordRef.current?.focus()}
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
/>
</Pressable>
{/* Password input */}
<Pressable
style={[
styles.inputWrapper,
{
borderColor: focused === 'password' ? colors.primary : colors.border,
backgroundColor: colors.surface,
},
]}
onPress={() => passwordRef.current?.focus()}
>
<Icon name="lock-outline" size={20} color={focused === 'password' ? colors.primary : colors.textLight} />
<TextInput
ref={passwordRef}
placeholder="Password"
placeholderTextColor={colors.textLight}
secureTextEntry={!showPassword}
textContentType="password"
autoComplete="off"
autoCorrect={false}
autoCapitalize="none"
value={password}
onFocus={() => setFocused('password')}
onBlur={() => setFocused(null)}
onChangeText={setPassword}
style={[styles.input, { color: colors.text, fontFamily: fonts.regular }]}
/>
<TouchableOpacity
style={styles.iconButton}
onPress={() => setShowPassword(v => !v)}
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
accessibilityRole="button"
accessibilityLabel={showPassword ? 'Hide password' : 'Show password'}
>
<Icon name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={22} color={colors.textLight} />
</TouchableOpacity>
</Pressable>
{/* Row: Remember me + Forgot password */}
<View style={styles.rowBetween}>
<Pressable style={styles.row} onPress={() => setRememberMe(v => !v)}>
<Icon name={rememberMe ? 'checkbox-marked' : 'checkbox-blank-outline'} size={20} color={colors.primary} />
<Text style={[styles.rememberText, { color: colors.text, fontFamily: fonts.regular }]}>Remember me</Text>
</Pressable>
<TouchableOpacity>
<Text style={[styles.link, { color: colors.primary, fontFamily: fonts.medium }]}>Forgot Password ?</Text>
</TouchableOpacity>
</View>
{/* Login button */}
<TouchableOpacity activeOpacity={0.9} onPress={handleLogin} disabled={loading} style={{ marginTop: 12 }}>
<LinearGradient colors={["#3AA0FF", "#2D6BFF"]} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={styles.loginButton}>
<Text style={[styles.loginButtonText, { fontFamily: fonts.bold }]}>{loading ? 'Logging in...' : 'Log In'}</Text>
</LinearGradient>
</TouchableOpacity>
{/* Or divider */}
<View style={styles.orContainer}>
<View style={[styles.orLine, { backgroundColor: colors.border }]} />
<Text style={[styles.orText, { color: colors.textLight, fontFamily: fonts.regular }]}>Or login with</Text>
<View style={[styles.orLine, { backgroundColor: colors.border }]} />
</View>
{/* Social buttons */}
<View style={styles.socialRow}>
<TouchableOpacity style={[styles.socialButton, { borderColor: colors.border }]}>
<Icon name="google" size={20} color={colors.text} />
</TouchableOpacity>
<TouchableOpacity style={[styles.socialButton, { borderColor: colors.border }]}>
<Icon name="facebook" size={20} color={colors.text} />
</TouchableOpacity>
<TouchableOpacity style={[styles.socialButton, { borderColor: colors.border }]}>
<Icon name="apple" size={20} color={colors.text} />
</TouchableOpacity>
</View>
{/* Sign up */}
<View style={styles.signupRow}>
<Text style={[styles.signupText, { color: colors.textLight, fontFamily: fonts.regular }]}>Dont have an account? </Text>
<TouchableOpacity>
<Text style={[styles.link, { color: colors.primary, fontFamily: fonts.medium }]}>Sign up</Text>
</TouchableOpacity>
</View>
{/* Error */}
{!!error && <Text style={[styles.errorText, { fontFamily: fonts.regular }]}>{error}</Text>}
</View>
</View>
</GradientBackground>
);
};
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;

View File

@ -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 }),
};

View File

@ -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;

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="CollabDashboard" component={CollabDashboardScreen} options={{headerShown:false}} />
</Stack.Navigator>
);
export default CollabNavigator;

View File

@ -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 (
<Container>
<View style={[styles.wrap, { backgroundColor: colors.background }]}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Communication & Collaboration</Text>
{/* KPI Pills */}
<View style={styles.kpiRow}>
<Pill label="Messages Today" value={mock.messagesToday.toLocaleString()} color={colors.text} fonts={fonts} bg="#E6F2FF" dot="#3AA0FF" />
<Pill label="Active Channels" value={String(mock.activeChannels)} color={colors.text} fonts={fonts} bg="#E9FAF2" dot="#10B981" />
</View>
<View style={styles.kpiRow}>
<Pill label="Mentions" value={String(mock.mentions)} color={colors.text} fonts={fonts} bg="#FFF4E6" dot="#F59E0B" />
<Pill label="Calls" value={String(mock.calls)} color={colors.text} fonts={fonts} bg="#ECEBFF" dot="#6366F1" />
</View>
{/* Activity Heatmap */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Weekly Activity Heatmap</Text>
<Heatmap data={mock.heatmap} baseColor="#3AA0FF" />
<Text style={{ fontSize: 12, marginTop: 6, color: colors.text, fontFamily: fonts.regular }}>Rows represent weeks, columns represent days (Mon-Sun)</Text>
</View>
{/* Message Volume */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Message Volume</Text>
<Bars data={mock.messageTrend} max={Math.max(...mock.messageTrend)} color="#3AA0FF" />
</View>
{/* Channel Distribution */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Channel Distribution</Text>
<Stacked segments={mock.channelDist} total={mock.channelDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.channelDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lists */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Active Channels</Text>
{mock.activeList.map(c => (
<View key={c.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{c.name}</Text>
<Badge text={String(c.unread)} fonts={fonts} />
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Recent Mentions</Text>
{mock.mentionsList.map(m => (
<View key={`${m.user}-${m.when}`} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{m.user}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{m.ctx} · {m.when}</Text>
</View>
))}
</View>
</View>
</View>
</Container>
);
};
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 (
<View style={[styles.pill, { backgroundColor: bg }]}>
<View style={styles.pillTop}>
<Text style={[styles.pillLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={[styles.dot, { backgroundColor: dot }]} />
</View>
<Text style={[styles.pillValue, { color, fontFamily: fonts.bold }]}>{value}</Text>
</View>
);
};
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 (
<View>
{data.map((row, i) => (
<View key={i} style={styles.heatRow}>
{row.map((v, j) => (
<View key={j} style={[styles.heatCell, { backgroundColor: tint(v) }]} />
))}
</View>
))}
</View>
);
};
const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => {
return (
<View style={styles.bars}>
{data.map((v, i) => (
<View key={i} style={[styles.bar, { height: Math.max(6, (v / Math.max(1, max)) * 64), backgroundColor: color }]} />
))}
</View>
);
};
const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
<View style={{ height: 12, borderRadius: 8, backgroundColor: '#E5E7EB', overflow: 'hidden', flexDirection: 'row', marginTop: 8 }}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};
const Badge: React.FC<{ text: string; fonts: any }> = ({ text, fonts }) => (
<View style={styles.badge}>
<Text style={{ fontSize: 12, fontFamily: fonts.medium }}>{text}</Text>
</View>
);

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="CrmDashboard" component={CrmDashboardScreen} options={{headerShown:false}} />
</Stack.Navigator>
);
export default CrmNavigator;

View File

@ -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 (
<Container>
<View style={[styles.wrap, { backgroundColor: colors.background }]}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>CRM & Sales</Text>
{/* KPIs */}
<View style={styles.kpiGrid}>
<Kpi label="Leads (M)" value={String(mock.leads)} color={colors.text} fonts={fonts} accent="#3AA0FF" />
<Kpi label="Opportunities" value={String(mock.opportunities)} color={colors.text} fonts={fonts} accent="#6366F1" />
<Kpi label="Won Deals" value={String(mock.wonDeals)} color={colors.text} fonts={fonts} accent="#10B981" />
<Kpi label="Conversion" value={`${mock.conversionPct}%`} color={colors.text} fonts={fonts} accent="#F59E0B" />
</View>
{/* Leads Trend */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Leads Trend</Text>
<Bars data={mock.leadsTrend} max={Math.max(...mock.leadsTrend)} color="#3AA0FF" />
</View>
{/* Pipeline distribution */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Pipeline Stages</Text>
<Stacked segments={mock.pipeline} total={mock.pipeline.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.pipeline.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lead Sources */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Leads by Source</Text>
<Stacked segments={mock.sourceDist} total={mock.sourceDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.sourceDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lists */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Opportunities</Text>
{mock.topOpps.map(o => (
<View key={o.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{o.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>${o.value.toLocaleString()}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Recent Activity</Text>
{mock.recent.map(r => (
<View key={`${r.who}-${r.when}`} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{r.who}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{r.what} · {r.when}</Text>
</View>
))}
</View>
</View>
</View>
</Container>
);
};
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 }) => (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 20, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => (
<View style={styles.bars}>
{data.map((v, i) => (
<View key={i} style={[styles.bar, { height: Math.max(6, (v / Math.max(1, max)) * 64), backgroundColor: color }]} />
))}
</View>
);
const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => (
<View style={{ height: 12, borderRadius: 8, backgroundColor: '#E5E7EB', overflow: 'hidden', flexDirection: 'row', marginTop: 8 }}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="FinanceDashboard" component={FinanceDashboardScreen} options={{headerShown:false}}/>
</Stack.Navigator>
);
export default FinanceNavigator;

View File

@ -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 (
<Container>
<View style={[styles.wrap, { backgroundColor: colors.background }]}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Accounts & Finance</Text>
{/* KPIs */}
<View style={styles.kpiGrid}>
<Kpi label="Outstanding" value={`$${mock.invoices.outstanding.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#3AA0FF" />
<Kpi label="Overdue" value={`$${mock.invoices.overdue.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#EF4444" />
<Kpi label="Paid (This Month)" value={`$${mock.invoices.paidThisMonth.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#10B981" />
<Kpi label="Cash" value={`$${mock.cash.toLocaleString()}`} color={colors.text} fonts={fonts} accent="#6366F1" />
</View>
{/* Sales Trend */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Sales Trend</Text>
<Bars data={mock.monthlySales} max={Math.max(...mock.monthlySales)} color="#3AA0FF" />
</View>
{/* Taxes & AR Aging */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Taxes</Text>
{(() => {
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 (
<>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>Collected: ${mock.taxes.collected.toLocaleString()}</Text>
<Progress value={colPct} color="#10B981" fonts={fonts} />
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular, marginTop: 6 }}>Paid: ${mock.taxes.paid.toLocaleString()}</Text>
<Progress value={paidPct} color="#F59E0B" fonts={fonts} />
</>
);
})()}
</View>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>A/R Aging</Text>
<Bars data={mock.arAging} max={Math.max(...mock.arAging)} color="#F59E0B" />
<View style={styles.rowJustify}>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>0-30</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>31-60</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>61-90</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>90+</Text>
</View>
</View>
</View>
{/* Invoice Status Distribution */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Invoice Status</Text>
<Stacked segments={mock.invoiceStatus} total={mock.invoiceStatus.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.invoiceStatus.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lists */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Customers</Text>
{mock.topCustomers.map(r => (
<View key={r.client} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{r.client}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>${r.amount.toLocaleString()}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Bank Accounts</Text>
{mock.bankAccounts.map(p => (
<View key={p.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{p.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>${p.balance.toLocaleString()}</Text>
</View>
))}
</View>
</View>
{/* Estimates & Payment Modes */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Estimates</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
<View style={{ paddingVertical: 6, paddingHorizontal: 10, backgroundColor: '#F3F4F6', borderRadius: 14, marginRight: 8, marginTop: 8 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Sent: {mock.estimates.sent}</Text>
</View>
<View style={{ paddingVertical: 6, paddingHorizontal: 10, backgroundColor: '#E9FAF2', borderRadius: 14, marginRight: 8, marginTop: 8 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Accepted: {mock.estimates.accepted}</Text>
</View>
<View style={{ paddingVertical: 6, paddingHorizontal: 10, backgroundColor: '#FFF4E6', borderRadius: 14, marginRight: 8, marginTop: 8 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Declined: {mock.estimates.declined}</Text>
</View>
</View>
</View>
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Payment Modes</Text>
<Stacked segments={mock.paymentModes} total={mock.paymentModes.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.paymentModes.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
</View>
</Container>
);
};
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 (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 20, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
};
const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => {
return (
<View style={styles.bars}>
{data.map((v, i) => (
<View key={i} style={[styles.bar, { height: Math.max(6, (v / Math.max(1, max)) * 64), backgroundColor: color }]} />
))}
</View>
);
};
const Progress: React.FC<{ value: number; color: string; fonts: any }> = ({ value, color, fonts }) => {
return (
<View style={styles.progressWrap}>
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${value}%`, backgroundColor: color }]} />
</View>
<Text style={{ fontSize: 12, marginTop: 6, fontFamily: fonts.medium }}>{value}%</Text>
</View>
);
};
const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
<View style={{ height: 12, borderRadius: 8, backgroundColor: '#E5E7EB', overflow: 'hidden', flexDirection: 'row', marginTop: 8 }}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="HRDashboard" component={HRDashboardScreen} options={{headerShown:false}} />
</Stack.Navigator>
);
export default HRNavigator;

View File

@ -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 <LoadingSpinner />;
}
if (error) {
return <ErrorState message={error} onRetry={() => dispatch(fetchHRMetrics() as any)} />;
}
return (
<Container>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>HR Dashboard</Text>
</View>
<View style={styles.content}>
{/* KPI Cards */}
<View style={styles.kpiGrid}>
<Kpi label="Headcount" value={String(mock.headcount)} color={colors.text} fonts={fonts} accent="#3AA0FF" />
<Kpi label="New hires (30d)" value={String(mock.newHires30d)} color={colors.text} fonts={fonts} accent="#10B981" />
<Kpi label="Attrition" value={`${mock.attritionPct}%`} color={colors.text} fonts={fonts} accent="#EF4444" />
<Kpi label="Attendance" value={`${mock.attendancePct}%`} color={colors.text} fonts={fonts} accent="#6366F1" />
</View>
{/* Trends: Hires vs Exits */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Workforce Movements</Text>
<DualBars data={mock.hiresTrend.map((h, i) => ({ in: h, out: mock.exitsTrend[i] }))} max={Math.max(...mock.hiresTrend.map((h, i) => Math.max(h, mock.exitsTrend[i])))} colorA="#10B981" colorB="#EF4444" />
<View style={styles.legendRow}>
<View style={styles.legendItem}><View style={[styles.legendDot, { backgroundColor: '#10B981' }]} /><Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>Hires</Text></View>
<View style={styles.legendItem}><View style={[styles.legendDot, { backgroundColor: '#EF4444' }]} /><Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular }}>Exits</Text></View>
</View>
</View>
{/* People Health */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>People Health</Text>
<Progress label="Attendance" value={mock.attendancePct} color="#3AA0FF" fonts={fonts} />
<Progress label="Engagement" value={mock.engagementPct} color="#6366F1" fonts={fonts} />
<Progress label="Attrition (inverse)" value={100 - mock.attritionPct} color="#10B981" fonts={fonts} />
</View>
{/* Department Distribution */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Department Distribution</Text>
<Stacked segments={mock.deptDist} total={mock.deptDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.deptDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lists: Holidays and Top Performers */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Upcoming Holidays</Text>
{mock.holidays.map(h => (
<View key={h.date} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{h.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>{h.date}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Performers</Text>
{mock.topPerformers.map(p => (
<View key={p.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{p.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>{p.score}</Text>
</View>
))}
</View>
</View>
</View>
</Container>
);
};
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 (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 20, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
};
const DualBars: React.FC<{ data: { in: number; out: number }[]; max: number; colorA: string; colorB: string }> = ({ data, max, colorA, colorB }) => {
return (
<View style={styles.dualBarsRow}>
{data.map((d, i) => (
<View key={i} style={styles.dualBarWrap}>
<View style={[styles.dualBarA, { height: Math.max(6, (d.in / Math.max(1, max)) * 64), backgroundColor: colorA }]} />
<View style={[styles.dualBarB, { height: Math.max(6, (d.out / Math.max(1, max)) * 64), backgroundColor: colorB }]} />
</View>
))}
</View>
);
};
const Progress: React.FC<{ label: string; value: number; color: string; fonts: any }> = ({ label, value, color, fonts }) => {
return (
<View style={{ marginTop: 8 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular }}>{label}</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium }}>{value}%</Text>
</View>
<View style={{ height: 8, borderRadius: 6, backgroundColor: '#E5E7EB', overflow: 'hidden' }}>
<View style={{ height: '100%', width: `${value}%`, backgroundColor: color }} />
</View>
</View>
);
};
const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
<View style={{ height: 12, borderRadius: 8, backgroundColor: '#E5E7EB', overflow: 'hidden', flexDirection: 'row', marginTop: 8 }}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};

View File

@ -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),
};

View File

@ -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<EmployeeMetric[]>) => {
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;

View File

@ -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<IntegrationsStackParamList>();
const IntegrationsNavigator = () => (
<Stack.Navigator>
<Stack.Screen name="IntegrationsHome" component={IntegrationsHomeScreen} options={{ title: 'Integrations',headerShown:false }} />
<Stack.Screen name="IntegrationCategory" component={IntegrationCategoryScreen} options={({ route }) => ({ title: route.params.title,headerShown:false })} />
</Stack.Navigator>
);
export default IntegrationsNavigator;

View File

@ -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<IntegrationsStackParamList, 'IntegrationCategory'>;
const servicesMap: Record<string, { key: string; title: string; icon: string }[]> = {
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<Props> = ({ route }) => {
const { colors, fonts } = useTheme();
const dispatch = useDispatch<AppDispatch>();
const services = servicesMap[route.params.categoryKey] ?? [];
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<FlatList
data={services}
keyExtractor={item => item.key}
contentContainerStyle={{ padding: 16 }}
ItemSeparatorComponent={() => <View style={[styles.sep, { backgroundColor: colors.border }]} />}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.row}
activeOpacity={0.8}
onPress={() => dispatch(setSelectedService(item.key))}
>
<View style={[styles.iconCircle, { backgroundColor: '#F1F5F9' }]}>
<Icon name={item.icon} size={20} color={colors.primary} />
</View>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.medium }]}>{item.title}</Text>
</TouchableOpacity>
)}
/>
</View>
);
};
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;

View File

@ -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<IntegrationsStackParamList, 'IntegrationsHome'>;
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<Props> = () => {
const { colors, shadows, fonts } = useTheme();
const navigation = useNavigation();
return (
<GradientBackground colors={['#FFE9CC', '#F6E6FF']} style={{flex:1}}>
<View style={[styles.container]}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Choose a Service</Text>
<FlatList
data={categories}
keyExtractor={item => item.key}
numColumns={NUM_COLUMNS}
contentContainerStyle={{ padding: GUTTER }}
columnWrapperStyle={{ justifyContent: 'space-between' }}
renderItem={({ item }) => (
<TouchableOpacity
activeOpacity={0.8}
// @ts-ignore
onPress={() => navigation.navigate('IntegrationCategory', { categoryKey: item.key, title: item.title })}
style={[
styles.card,
{ backgroundColor: colors.surface, borderColor: colors.border, width: CARD_WIDTH, ...shadows.light },
]}
>
<View style={[styles.iconCircle, { backgroundColor: '#F1F5F9' }]}>
<Icon name={item.icon} size={24} color={colors.primary} />
</View>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]} numberOfLines={2}>
{item.title}
</Text>
</TouchableOpacity>
)}
/>
</View>
</GradientBackground>
);
};
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;

View File

@ -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<string>) => {
state.selectedService = action.payload;
},
clearSelectedService: state => {
state.selectedService = null;
},
},
});
export const { setSelectedService, clearSelectedService } = integrationsSlice.actions;
export default integrationsSlice;

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
export default ProfileNavigator;

View File

@ -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 (
<Container>
<View style={[styles.top, { backgroundColor: colors.background }]}>
{/* Avatar */}
<View style={styles.avatarWrap}>
<View style={[styles.avatarCircle, { backgroundColor: '#E5E7EB' }]}>
{/* If you have an image, replace with <Image source={{ uri: ... }} style={styles.avatarImage}/> */}
<Icon name="account" size={56} color={colors.primary} />
</View>
<View style={[styles.editBadge, { backgroundColor: colors.primary }]}>
<Icon name="pencil" size={14} color={colors.surface} />
</View>
</View>
{/* Name */}
<Text style={[styles.displayName, { color: colors.text, fontFamily: fonts.bold }]}>{name || 'Sana Afzal'}</Text>
{/* Email pill */}
<View style={[styles.emailPill, { backgroundColor: '#DFE9FF' }]}>
<Text style={{ color: colors.text, fontFamily: fonts.regular, fontSize: 12 }}>{email || 'sanaaafzal291@gmail.com'}</Text>
</View>
</View>
{/* Settings card */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<MenuItem icon="pencil-outline" label="Edit Profile" onPress={() => {}} />
<Separator color={colors.border} />
<MenuItem icon="shield-lock-outline" label="Add Pin" onPress={() => {}} />
<Separator color={colors.border} />
<MenuItem icon="cog-outline" label="Settings" onPress={() => {}} />
<Separator color={colors.border} />
<MenuItem icon="account-plus-outline" label="Invite a friend" onPress={() => {}} />
<Separator color={colors.border} />
<MenuItem icon="logout" label="Logout" danger onPress={handleLogout} />
</View>
<ConfirmModal
visible={showLogout}
title="Logout"
message="Are you sure you want to logout?"
confirmText="Logout"
cancelText="Cancel"
onConfirm={handleConfirmLogout}
onCancel={handleCancelLogout}
/>
</Container>
);
};
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<MenuItemProps> = ({ icon, label, onPress, danger }) => {
const { colors, fonts } = useTheme();
return (
<TouchableOpacity style={menuStyles.row} activeOpacity={0.8} onPress={onPress}>
<View style={[menuStyles.left, danger && { opacity: 0.9 }]}>
<Icon name={icon} size={18} color={danger ? '#EF4444' : colors.text} />
<Text style={[menuStyles.label, { color: danger ? '#EF4444' : colors.text, fontFamily: fonts.regular }]}>{label}</Text>
</View>
<Icon name="chevron-right" size={20} color={danger ? '#EF4444' : colors.text} />
</TouchableOpacity>
);
};
const Separator: React.FC<{ color: string }> = ({ color }) => (
<View style={{ height: 1, backgroundColor: color, opacity: 0.6 }} />
);
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;

View File

@ -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<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
resetState: () => initialState,
},
});
export const { setProfile, setLoading, setError, resetState } = profileSlice.actions;
export default profileSlice;

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="StorageDashboard" component={StorageDashboardScreen} options={{headerShown:false}}/>
</Stack.Navigator>
);
export default StorageNavigator;

View File

@ -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 (
<Container>
<View style={[styles.wrap, { backgroundColor: colors.background }]}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>File Storage & Document Management</Text>
{/* KPIs */}
<View style={styles.kpiGrid}>
<Kpi label="Total Usage" value={`${mock.totalUsageGb} GB`} color={colors.text} fonts={fonts} accent="#3AA0FF" />
<Kpi label="Files" value={mock.filesCount.toLocaleString()} color={colors.text} fonts={fonts} accent="#10B981" />
<Kpi label="Shared Links" value={String(mock.sharedLinks)} color={colors.text} fonts={fonts} accent="#F59E0B" />
<Kpi label="Active Users" value={String(mock.activeUsers)} color={colors.text} fonts={fonts} accent="#6366F1" />
</View>
{/* Usage Trend */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Storage Growth</Text>
<Bars data={mock.monthlyUsage} max={Math.max(...mock.monthlyUsage)} color="#3AA0FF" />
</View>
{/* File Types Distribution (scoped to current provider) */}
<View style={[styles.card, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>File Types</Text>
<Stacked segments={mock.typeDist} total={mock.typeDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.typeDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Lists */}
<View style={styles.row}>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Recent Activity</Text>
{mock.recentActivity.map(a => (
<View key={`${a.name}-${a.when}`} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{a.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>{a.action} · {a.when}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: colors.border, backgroundColor: colors.surface }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Folders</Text>
{mock.topFolders.map(f => (
<View key={f.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{f.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.medium }]}>{f.size} GB</Text>
</View>
))}
</View>
</View>
</View>
</Container>
);
};
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 (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 20, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
};
const Bars: React.FC<{ data: number[]; max: number; color: string }> = ({ data, max, color }) => {
return (
<View style={styles.bars}>
{data.map((v, i) => (
<View key={i} style={[styles.bar, { height: Math.max(6, (v / Math.max(1, max)) * 64), backgroundColor: color }]} />
))}
</View>
);
};
const Stacked: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
<View style={{ height: 12, borderRadius: 8, backgroundColor: '#E5E7EB', overflow: 'hidden', flexDirection: 'row', marginTop: 8 }}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};

View File

@ -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 = () => (
<Stack.Navigator>
<Stack.Screen name="ZohoProjectsDashboard" component={ZohoProjectsDashboardScreen} options={{headerShown:false}}/>
</Stack.Navigator>
);
export default ZohoProjectsNavigator;

View File

@ -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 <LoadingSpinner />;
}
if (error) {
return <ErrorState message={error} onRetry={() => dispatch(fetchZohoProjects() as any)} />;
}
return (
<Container>
<ScrollView
style={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>Zoho Projects</Text>
<Icon name="insights" size={24} color={colors.primary} />
</View>
<View style={styles.content}>
{/* Sprint Header */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Active Sprint</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View>
<Text style={{ fontSize: 14, color: colors.text, fontFamily: fonts.bold }}>{mock.sprintName}</Text>
<Text style={{ fontSize: 12, color: colors.text, fontFamily: fonts.regular, opacity: 0.8 }}>{mock.sprintDates}</Text>
</View>
<Chip label={mock.scopeChange > 0 ? 'Scope +' : 'Scope'} value={Math.abs(mock.scopeChange)} dot={mock.scopeChange > 0 ? '#F59E0B' : '#10B981'} />
</View>
</View>
{/* KPI Cards */}
<View style={styles.kpiGrid}>
<KpiCard label="Backlog" value={mock.backlog} color={colors.text} accent="#3AA0FF" />
<KpiCard label="In Progress" value={mock.inProgress} color={colors.text} accent="#F59E0B" />
<KpiCard label="Completed" value={mock.completed} color={colors.text} accent="#10B981" />
<KpiCard label="Blocked" value={mock.blocked} color={colors.text} accent="#EF4444" />
</View>
{/* Trend: Burndown & Velocity (mini bars) */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Sprint Trends</Text>
<View style={styles.trendRow}>
<View style={styles.trendBlock}>
<Text style={[styles.trendTitle, { color: colors.text, fontFamily: fonts.medium }]}>Burndown</Text>
<MiniBars data={mock.burndown} color="#3AA0FF" max={Math.max(...mock.burndown)} />
</View>
<View style={styles.trendBlock}>
<Text style={[styles.trendTitle, { color: colors.text, fontFamily: fonts.medium }]}>Velocity</Text>
<MiniBars data={mock.velocity} color="#10B981" max={Math.max(...mock.velocity)} />
</View>
</View>
</View>
{/* Bugs & Priority Mix */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Bugs by Severity</Text>
<StackedBar segments={mock.bugSeverityDist} total={mock.bugSeverityDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.bugSeverityDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Tasks by Priority</Text>
<StackedBar segments={mock.priorityDist} total={mock.priorityDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.priorityDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
</View>
{/* Timesheets & Aging */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Timesheets</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>Total: {mock.timesheets.totalHours}h</Text>
{(() => {
const billablePct = Math.round((mock.timesheets.billableHours / Math.max(1, mock.timesheets.totalHours)) * 100);
return <ProgressRow label={`Billable (${mock.timesheets.billableHours}h)`} value={billablePct} color="#10B981" />;
})()}
{(() => {
const non = mock.timesheets.totalHours - mock.timesheets.billableHours;
const pct = Math.round((non / Math.max(1, mock.timesheets.totalHours)) * 100);
return <ProgressRow label={`Non-billable (${non}h)`} value={pct} color="#F59E0B" />;
})()}
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Backlog Aging</Text>
<MiniBars data={mock.backlogAging} color="#6366F1" max={Math.max(...mock.backlogAging)} />
<View style={styles.legendRow}>
{['0-7d', '8-14d', '15-30d', '30+d'].map(label => (
<View key={label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: '#6366F1' }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{label}</Text>
</View>
))}
</View>
</View>
</View>
{/* Assignee Workload */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Assignee Workload</Text>
{mock.assigneeLoad.map(a => (
<ProgressRow key={a.name} label={a.name} value={a.pct} color="#3AA0FF" />
))}
</View>
{/* Progress: On-time, Quality, Utilization */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Operational Health</Text>
<ProgressRow label="On-time delivery" value={mock.onTimePct} color="#3AA0FF" />
<ProgressRow label="Quality score" value={mock.qualityScore} color="#10B981" />
<ProgressRow label="Resource utilization" value={mock.utilizationPct} color="#F59E0B" />
</View>
{/* Status distribution */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Status Distribution</Text>
<StackedBar segments={mock.statusDist} total={mock.statusDist.reduce((a, b) => a + b.value, 0)} />
<View style={styles.legendRow}>
{mock.statusDist.map(s => (
<View key={s.label} style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: s.color }]} />
<Text style={{ color: colors.text, fontFamily: fonts.regular }}>{s.label}</Text>
</View>
))}
</View>
</View>
{/* Kanban Snapshot */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Kanban Snapshot</Text>
<View style={styles.badgeRow}>
<Chip label="To Do" value={mock.kanban.todo} dot="#3AA0FF" />
<Chip label="Doing" value={mock.kanban.doing} dot="#F59E0B" />
<Chip label="Review" value={mock.kanban.review} dot="#6366F1" />
<Chip label="Done" value={mock.kanban.done} dot="#10B981" />
</View>
</View>
{/* Lists: Risks and Top Clients */}
<View style={styles.twoCol}>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Risks</Text>
{mock.risks.map(r => (
<View key={r.id} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{r.title}</Text>
<Text style={[styles.listSecondary, { color: '#EF4444', fontFamily: fonts.regular }]}>{r.impact}</Text>
</View>
))}
</View>
<View style={[styles.card, styles.col, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Top Clients</Text>
{mock.topClients.map(c => (
<View key={c.name} style={styles.listRow}>
<Text style={[styles.listPrimary, { color: colors.text, fontFamily: fonts.regular }]}>{c.name}</Text>
<Text style={[styles.listSecondary, { color: colors.text, fontFamily: fonts.regular }]}>{c.projects} projects</Text>
</View>
))}
</View>
</View>
{/* Milestones Progress */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Milestones</Text>
{mock.milestones.map(m => (
<View key={m.name} style={{ marginTop: 8 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>{m.name}</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium, color: colors.text }}>{m.due}</Text>
</View>
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${m.progress}%`, backgroundColor: '#3AA0FF' }]} />
</View>
</View>
))}
</View>
{/* Team Capacity */}
<View style={[styles.card, { borderColor: '#E2E8F0', backgroundColor: '#FFFFFF' }]}>
<Text style={[styles.cardTitle, { color: colors.text, fontFamily: fonts.medium }]}>Team Capacity</Text>
{mock.teams.map(t => (
<ProgressRow key={t.name} label={t.name} value={t.capacityPct} color="#6366F1" />
))}
</View>
</View>
</ScrollView>
</Container>
);
};
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 (
<View style={styles.kpiCard}>
<Text style={[styles.kpiLabel, { color, fontFamily: fonts.regular }]}>{label}</Text>
<View style={styles.kpiValueRow}>
<Text style={{ color, fontSize: 22, fontFamily: fonts.bold }}>{value}</Text>
<View style={{ width: 10, height: 10, borderRadius: 5, backgroundColor: accent }} />
</View>
</View>
);
};
const MiniBars: React.FC<{ data: number[]; color: string; max: number }> = ({ data, color, max }) => {
return (
<View style={styles.miniBars}>
{data.map((v, i) => (
<View key={i} style={[styles.miniBar, { height: Math.max(6, (v / Math.max(1, max)) * 64), backgroundColor: color }]} />
))}
</View>
);
};
const ProgressRow: React.FC<{ label: string; value: number; color: string }> = ({ label, value, color }) => {
const { fonts } = useTheme();
return (
<View style={styles.progressRow}>
<View style={styles.progressLabelWrap}>
<Text style={{ fontSize: 12, fontFamily: fonts.regular }}>{label}</Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium }}>{value}%</Text>
</View>
<View style={styles.progressTrack}>
<View style={[styles.progressFill, { width: `${value}%`, backgroundColor: color }]} />
</View>
</View>
);
};
const StackedBar: React.FC<{ segments: { label: string; value: number; color: string }[]; total: number }> = ({ segments, total }) => {
return (
<View style={[styles.stackedBar, { flexDirection: 'row', marginTop: 8 }]}>
{segments.map(s => (
<View key={s.label} style={{ width: `${(s.value / Math.max(1, total)) * 100}%`, backgroundColor: s.color }} />
))}
</View>
);
};
const Chip: React.FC<{ label: string; value: number; dot: string }> = ({ label, value, dot }) => {
const { fonts, colors } = useTheme();
return (
<View style={styles.chip}>
<View style={[styles.chipDot, { backgroundColor: dot }]} />
<Text style={{ fontSize: 12, fontFamily: fonts.regular, color: colors.text }}>{label}: </Text>
<Text style={{ fontSize: 12, fontFamily: fonts.medium, color: colors.text }}>{value}</Text>
</View>
);
};

View File

@ -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),
};

View File

@ -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<Partial<ZohoProjectsFilters>>) => {
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<ZohoProject[]>) => {
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;

View File

@ -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 <ZohoProjectsNavigator />;
}
if (selectedService && hrServices.includes(selectedService)) {
return <HRNavigator />;
}
if (selectedService && financeServices.includes(selectedService)) {
return <FinanceNavigator />;
}
if (selectedService && crmServices.includes(selectedService)) {
return <CrmNavigator />;
}
if (selectedService && collabServices.includes(selectedService)) {
return <CollabNavigator />;
}
if (selectedService && storageServices.includes(selectedService)) {
return <StorageNavigator />;
}
// Default dashboard if unknown service
return <ZohoProjectsNavigator />;
};
const AppTabs = () => {
const { colors } = useTheme();
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
const map: Record<string, string> = {
Dashboard: 'dashboard',
Profile: 'person',
};
const iconName = map[route.name] ?? 'dashboard';
return <Icon name={iconName} size={size} color={color} />;
},
headerShown: false,
tabBarActiveTintColor: colors.primary,
})}
>
<Tab.Screen name="Dashboard" component={DashboardWithHome} />
<Tab.Screen name="Profile" component={ProfileNavigator} />
</Tab.Navigator>
);
};
const AppNavigator = () => (
<NavigationContainer>
<AppTabs />
</NavigationContainer>
);
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 (
<View style={{ flex: 1 }}>
<DashboardRouter />
<TouchableOpacity
activeOpacity={0.85}
onPress={() => dispatch(clearSelectedService())}
style={[styles.fab, { backgroundColor: colors.primary }]}
>
<Icon name="home" size={24} color="#FFFFFF" />
</TouchableOpacity>
</View>
);
};
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,
},
});

10
src/services/http.ts Normal file
View File

@ -0,0 +1,10 @@
import { create } from 'apisauce';
const http = create({
baseURL: 'https://api.example.com',
timeout: 10000,
});
export default http;

View File

@ -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<ViewStyle>;
colors?: string[];
start?: { x: number; y: number };
end?: { x: number; y: number };
locations?: number[];
preset?: GradientPreset;
}
const PRESET_COLORS: Record<GradientPreset, string[]> = {
// 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<GradientBackgroundProps> = ({
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 (
<LinearGradient colors={gradientColors} start={gradientStart} end={gradientEnd} locations={locations} style={style}>
<StatusBar backgroundColor={'#FFE9CC'} barStyle={'dark-content'} />
{children}
</LinearGradient>
);
};
export default GradientBackground;

View File

@ -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<ConfirmModalProps> = ({
visible,
title = 'Confirm',
message = 'Are you sure?',
confirmText = 'Yes',
cancelText = 'Cancel',
onConfirm,
onCancel,
}) => {
const { colors, fonts } = useTheme();
return (
<Modal visible={visible} animationType="fade" transparent onRequestClose={onCancel}>
<View style={styles.overlay}>
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Text style={[styles.title, { color: colors.text, fontFamily: fonts.bold }]}>{title}</Text>
<Text style={[styles.message, { color: colors.text, fontFamily: fonts.regular }]}>{message}</Text>
<View style={styles.actions}>
<TouchableOpacity style={[styles.btn, styles.btnOutline, { borderColor: colors.border }]} onPress={onCancel}>
<Text style={{ color: colors.text, fontFamily: fonts.medium }}>{cancelText}</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, styles.btnPrimary, { backgroundColor: colors.primary }]} onPress={onConfirm}>
<Text style={{ color: colors.surface, fontFamily: fonts.bold }}>{confirmText}</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
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;

View File

@ -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<ViewStyle>;
}
const Container: React.FC<ContainerProps> = ({ children, style }) => {
const { colors } = useTheme();
return <ScrollView style={[styles.container, { backgroundColor: colors.background }, style]} showsVerticalScrollIndicator={false}>{children}</ScrollView>;
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default Container;

View File

@ -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<ErrorStateProps> = ({ message = 'Something went wrong', onRetry }) => {
const { colors, spacing } = useTheme();
return (
<View style={[styles.container, { padding: spacing.md }]}>
<Text style={[styles.title, { color: colors.error }]}>{message}</Text>
{onRetry ? (
<TouchableOpacity onPress={onRetry} style={[styles.button, { borderColor: colors.error }]}>
<Text style={[styles.buttonText, { color: colors.error }]}>Retry</Text>
</TouchableOpacity>
) : null}
</View>
);
};
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;

View File

@ -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 (
<View style={styles.container}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
export default LoadingSpinner;

View File

@ -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';

View File

@ -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;

View File

@ -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<boolean>) => {
state.networkOnline = action.payload;
},
setGlobalLoading: (state, action: PayloadAction<boolean>) => {
state.globalLoading = action.payload;
},
showToast: (state, action: PayloadAction<string>) => {
state.toastMessage = action.payload;
},
clearToast: state => {
state.toastMessage = null;
},
},
});
export const { setNetworkOnline, setGlobalLoading, showToast, clearToast } = uiSlice.actions;
export default uiSlice;

View File

@ -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<ThemeContextValue | undefined>(undefined);
const STORAGE_KEY = 'theme.scheme';
const getInitialScheme = async (): Promise<ColorScheme> => {
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<React.PropsWithChildren> = ({ children }) => {
const [scheme, setSchemeState] = useState<ColorScheme>('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 <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
};
export default ThemeProvider;

View File

@ -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,
},
};

View File

@ -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;
}

View File

@ -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;

31
src/shared/utils/Toast.ts Normal file
View File

@ -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,
});
};

53
src/store/store.ts Normal file
View File

@ -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<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@ -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<IconProps>;
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<IconProps>;
export default Icon;
}

View File

@ -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"]
} }