510 lines
13 KiB
Plaintext
510 lines
13 KiB
Plaintext
---
|
|
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 |