mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Salesforce style login screen, Introduce redux and apply to all pages/custom hooks where needed
This commit is contained in:
11
PRD.md
11
PRD.md
@@ -8,14 +8,21 @@ A cloud-based workforce back-office platform that centralizes timesheet manageme
|
||||
3. **Transparent** - Clear visibility into workflow status, approval chains, and data lineage with comprehensive audit trails and real-time updates
|
||||
|
||||
**Complexity Level**: Complex Application (advanced functionality, likely with multiple views)
|
||||
This is a multi-module enterprise platform requiring navigation between distinct functional areas (timesheets, billing, payroll, compliance), role-based access control, real-time dashboards, complex form workflows, and extensive data visualization capabilities.
|
||||
This is a multi-module enterprise platform requiring navigation between distinct functional areas (timesheets, billing, payroll, compliance), role-based access control, real-time dashboards, complex form workflows, extensive data visualization capabilities, and Redux-powered state management for scalable, predictable application state.
|
||||
|
||||
## Essential Features
|
||||
|
||||
**Authentication & Login**
|
||||
- Functionality: Salesforce-style login screen with email/password authentication
|
||||
- Purpose: Secure access to the platform with professional enterprise-grade authentication
|
||||
- Trigger: User navigates to application without authenticated session
|
||||
- Progression: Enter credentials → Validate → Show loading state → Redirect to dashboard
|
||||
- Success criteria: Smooth authentication flow, password visibility toggle, remember me option, secure session management
|
||||
|
||||
**Dashboard Overview**
|
||||
- Functionality: Displays real-time KPIs, alerts, and quick actions across all modules
|
||||
- Purpose: Provides at-a-glance operational health and reduces time to critical actions
|
||||
- Trigger: User login or navigation to home
|
||||
- Trigger: Successful user login or navigation to home
|
||||
- Progression: Login → Dashboard loads with widgets → User scans metrics → Clicks widget to drill down → Navigates to relevant module
|
||||
- Success criteria: All KPIs update in real-time, widgets are interactive, no data older than 5 minutes
|
||||
|
||||
|
||||
300
REDUX_GUIDE.md
Normal file
300
REDUX_GUIDE.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Redux Integration Guide
|
||||
|
||||
This application uses Redux Toolkit for centralized state management across the entire platform.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The Redux store is configured in `/src/store/store.ts` and provides the following slices:
|
||||
|
||||
### State Slices
|
||||
|
||||
#### 1. Auth Slice (`/src/store/slices/authSlice.ts`)
|
||||
Manages authentication state and user information.
|
||||
|
||||
**State:**
|
||||
- `user`: Current authenticated user object
|
||||
- `isAuthenticated`: Boolean flag for auth status
|
||||
- `currentEntity`: Selected organizational entity
|
||||
|
||||
**Actions:**
|
||||
- `login(user)`: Authenticates user and stores user data
|
||||
- `logout()`: Clears user session
|
||||
- `setCurrentEntity(entity)`: Switches between organizational entities
|
||||
|
||||
#### 2. UI Slice (`/src/store/slices/uiSlice.ts`)
|
||||
Manages global UI state.
|
||||
|
||||
**State:**
|
||||
- `currentView`: Active view/page in the application
|
||||
- `searchQuery`: Global search query string
|
||||
- `sidebarCollapsed`: Sidebar visibility state
|
||||
|
||||
**Actions:**
|
||||
- `setCurrentView(view)`: Navigate to different views
|
||||
- `setSearchQuery(query)`: Update search query
|
||||
- `toggleSidebar()`: Toggle sidebar collapsed state
|
||||
|
||||
#### 3. Timesheets Slice (`/src/store/slices/timesheetsSlice.ts`)
|
||||
Manages timesheet data.
|
||||
|
||||
**State:**
|
||||
- `timesheets`: Array of timesheet objects
|
||||
- `loading`: Loading state for async operations
|
||||
|
||||
**Actions:**
|
||||
- `setTimesheets(timesheets)`: Replace all timesheets
|
||||
- `addTimesheet(timesheet)`: Add new timesheet
|
||||
- `updateTimesheet(timesheet)`: Update existing timesheet
|
||||
- `deleteTimesheet(id)`: Remove timesheet by ID
|
||||
- `setLoading(boolean)`: Set loading state
|
||||
|
||||
#### 4. Invoices Slice (`/src/store/slices/invoicesSlice.ts`)
|
||||
Manages invoice data.
|
||||
|
||||
**State:**
|
||||
- `invoices`: Array of invoice objects
|
||||
- `loading`: Loading state for async operations
|
||||
|
||||
**Actions:**
|
||||
- `setInvoices(invoices)`: Replace all invoices
|
||||
- `addInvoice(invoice)`: Add new invoice
|
||||
- `updateInvoice(invoice)`: Update existing invoice
|
||||
- `deleteInvoice(id)`: Remove invoice by ID
|
||||
- `setLoading(boolean)`: Set loading state
|
||||
|
||||
#### 5. Payroll Slice (`/src/store/slices/payrollSlice.ts`)
|
||||
Manages payroll run data.
|
||||
|
||||
**State:**
|
||||
- `payrollRuns`: Array of payroll run objects
|
||||
- `loading`: Loading state for async operations
|
||||
|
||||
**Actions:**
|
||||
- `setPayrollRuns(runs)`: Replace all payroll runs
|
||||
- `addPayrollRun(run)`: Add new payroll run
|
||||
- `updatePayrollRun(run)`: Update existing payroll run
|
||||
- `setLoading(boolean)`: Set loading state
|
||||
|
||||
#### 6. Compliance Slice (`/src/store/slices/complianceSlice.ts`)
|
||||
Manages compliance document data.
|
||||
|
||||
**State:**
|
||||
- `documents`: Array of compliance document objects
|
||||
- `loading`: Loading state for async operations
|
||||
|
||||
**Actions:**
|
||||
- `setComplianceDocs(docs)`: Replace all compliance documents
|
||||
- `addComplianceDoc(doc)`: Add new compliance document
|
||||
- `updateComplianceDoc(doc)`: Update existing compliance document
|
||||
- `deleteComplianceDoc(id)`: Remove compliance document by ID
|
||||
- `setLoading(boolean)`: Set loading state
|
||||
|
||||
#### 7. Expenses Slice (`/src/store/slices/expensesSlice.ts`)
|
||||
Manages expense data.
|
||||
|
||||
**State:**
|
||||
- `expenses`: Array of expense objects
|
||||
- `loading`: Loading state for async operations
|
||||
|
||||
**Actions:**
|
||||
- `setExpenses(expenses)`: Replace all expenses
|
||||
- `addExpense(expense)`: Add new expense
|
||||
- `updateExpense(expense)`: Update existing expense
|
||||
- `deleteExpense(id)`: Remove expense by ID
|
||||
- `setLoading(boolean)`: Set loading state
|
||||
|
||||
#### 8. Notifications Slice (`/src/store/slices/notificationsSlice.ts`)
|
||||
Manages system notifications.
|
||||
|
||||
**State:**
|
||||
- `notifications`: Array of notification objects
|
||||
- `unreadCount`: Number of unread notifications
|
||||
|
||||
**Actions:**
|
||||
- `setNotifications(notifications)`: Replace all notifications
|
||||
- `addNotification(notification)`: Add new notification
|
||||
- `markAsRead(id)`: Mark notification as read
|
||||
- `markAllAsRead()`: Mark all notifications as read
|
||||
- `deleteNotification(id)`: Remove notification by ID
|
||||
|
||||
## Using Redux in Components
|
||||
|
||||
### 1. Import the typed hooks
|
||||
|
||||
```typescript
|
||||
import { useAppSelector, useAppDispatch } from '@/store/hooks'
|
||||
```
|
||||
|
||||
### 2. Access state with useAppSelector
|
||||
|
||||
```typescript
|
||||
const user = useAppSelector(state => state.auth.user)
|
||||
const isAuthenticated = useAppSelector(state => state.auth.isAuthenticated)
|
||||
const timesheets = useAppSelector(state => state.timesheets.timesheets)
|
||||
```
|
||||
|
||||
### 3. Dispatch actions with useAppDispatch
|
||||
|
||||
```typescript
|
||||
import { login, logout } from '@/store/slices/authSlice'
|
||||
import { setCurrentView } from '@/store/slices/uiSlice'
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Dispatch actions
|
||||
dispatch(login({ id: '1', email: 'user@example.com', name: 'User', role: 'Admin' }))
|
||||
dispatch(setCurrentView('dashboard'))
|
||||
dispatch(logout())
|
||||
```
|
||||
|
||||
## Custom Hooks with Redux
|
||||
|
||||
We've created several custom hooks that wrap Redux logic for cleaner component code:
|
||||
|
||||
### useAuth Hook (`/src/hooks/use-auth.ts`)
|
||||
|
||||
```typescript
|
||||
import { useAuth } from '@/hooks/use-auth'
|
||||
|
||||
function MyComponent() {
|
||||
const { user, isAuthenticated, currentEntity, logout } = useAuth()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isAuthenticated && <p>Welcome, {user?.name}!</p>}
|
||||
<button onClick={logout}>Log Out</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useNavigation Hook (`/src/hooks/use-navigation.ts`)
|
||||
|
||||
```typescript
|
||||
import { useNavigation } from '@/hooks/use-navigation'
|
||||
|
||||
function MyComponent() {
|
||||
const { currentView, navigateTo } = useNavigation()
|
||||
|
||||
return (
|
||||
<button onClick={() => navigateTo('dashboard')}>
|
||||
Go to Dashboard
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useReduxNotifications Hook (`/src/hooks/use-redux-notifications.ts`)
|
||||
|
||||
```typescript
|
||||
import { useReduxNotifications } from '@/hooks/use-redux-notifications'
|
||||
|
||||
function MyComponent() {
|
||||
const { notifications, unreadCount, addNotification, markAsRead } = useReduxNotifications()
|
||||
|
||||
const handleAction = () => {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
message: 'Action completed successfully',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Unread: {unreadCount}</p>
|
||||
{notifications.map(n => (
|
||||
<div key={n.id} onClick={() => markAsRead(n.id)}>
|
||||
{n.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use typed hooks**: Always use `useAppSelector` and `useAppDispatch` instead of the untyped versions from `react-redux`
|
||||
|
||||
2. **Create custom hooks**: Wrap common Redux patterns in custom hooks for reusability and cleaner components
|
||||
|
||||
3. **Keep slices focused**: Each slice should manage a single domain of state
|
||||
|
||||
4. **Use Redux DevTools**: The store is configured with Redux DevTools support for debugging
|
||||
|
||||
5. **Immutable updates**: Redux Toolkit uses Immer internally, so you can write "mutating" code in reducers that is automatically converted to immutable updates
|
||||
|
||||
6. **Async operations**: For async operations, consider using Redux Toolkit's `createAsyncThunk` or handle them in components/custom hooks
|
||||
|
||||
## Example: Complete Component with Redux
|
||||
|
||||
```typescript
|
||||
import { useAppSelector, useAppDispatch } from '@/store/hooks'
|
||||
import { setTimesheets, addTimesheet } from '@/store/slices/timesheetsSlice'
|
||||
import { addNotification } from '@/store/slices/notificationsSlice'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export function TimesheetList() {
|
||||
const dispatch = useAppDispatch()
|
||||
const timesheets = useAppSelector(state => state.timesheets.timesheets)
|
||||
const loading = useAppSelector(state => state.timesheets.loading)
|
||||
|
||||
const handleAddTimesheet = () => {
|
||||
const newTimesheet = {
|
||||
id: `TS-${Date.now()}`,
|
||||
workerName: 'New Worker',
|
||||
status: 'pending',
|
||||
// ... other fields
|
||||
}
|
||||
|
||||
dispatch(addTimesheet(newTimesheet))
|
||||
dispatch(addNotification({
|
||||
id: `notif-${Date.now()}`,
|
||||
type: 'success',
|
||||
title: 'Timesheet Added',
|
||||
message: 'New timesheet created successfully',
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
}))
|
||||
}
|
||||
|
||||
if (loading) return <div>Loading...</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={handleAddTimesheet}>Add Timesheet</Button>
|
||||
{timesheets.map(ts => (
|
||||
<div key={ts.id}>{ts.workerName}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from useState to Redux
|
||||
|
||||
For existing components using local state, follow this pattern:
|
||||
|
||||
**Before (local state):**
|
||||
```typescript
|
||||
const [currentView, setCurrentView] = useState('dashboard')
|
||||
```
|
||||
|
||||
**After (Redux):**
|
||||
```typescript
|
||||
const currentView = useAppSelector(state => state.ui.currentView)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// To update:
|
||||
dispatch(setCurrentView('timesheets'))
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Consider adding these Redux features as the application grows:
|
||||
|
||||
- **Redux Persist**: Persist state to localStorage/sessionStorage
|
||||
- **RTK Query**: For API data fetching and caching
|
||||
- **createAsyncThunk**: For complex async operations
|
||||
- **Entity Adapters**: For normalized state management of collections
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WorkForce Pro - Enhanced Back Office Platform</title>
|
||||
<title>WorkForce Pro - Back Office Platform</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
94
package-lock.json
generated
94
package-lock.json
generated
@@ -40,6 +40,7 @@
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.1",
|
||||
@@ -60,6 +61,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.15.1",
|
||||
"sonner": "^2.0.1",
|
||||
@@ -3083,6 +3085,32 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^11.0.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.47",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
|
||||
@@ -3557,6 +3585,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
@@ -4404,6 +4438,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
||||
@@ -7196,6 +7236,16 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
|
||||
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -8557,6 +8607,29 @@
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-remove-scroll": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||
@@ -8713,6 +8786,27 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.1",
|
||||
@@ -64,6 +65,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.15.1",
|
||||
"sonner": "^2.0.1",
|
||||
@@ -94,4 +96,4 @@
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/App.tsx
25
src/App.tsx
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react'
|
||||
import { useSampleData } from '@/hooks/use-sample-data'
|
||||
import { useNotifications } from '@/hooks/use-notifications'
|
||||
import { useAppData } from '@/hooks/use-app-data'
|
||||
@@ -8,17 +7,23 @@ import { Sidebar } from '@/components/navigation'
|
||||
import { NotificationCenter } from '@/components/NotificationCenter'
|
||||
import { ViewRouter } from '@/components/ViewRouter'
|
||||
import { LanguageSwitcher } from '@/components/LanguageSwitcher'
|
||||
import LoginScreen from '@/components/LoginScreen'
|
||||
import { useAppSelector, useAppDispatch } from '@/store/hooks'
|
||||
import { setCurrentView, setSearchQuery } from '@/store/slices/uiSlice'
|
||||
import { setCurrentEntity } from '@/store/slices/authSlice'
|
||||
|
||||
export type View = 'dashboard' | 'timesheets' | 'billing' | 'payroll' | 'compliance' | 'expenses' | 'roadmap' | 'reports' | 'currency' | 'email-templates' | 'invoice-templates' | 'qr-scanner' | 'missing-timesheets' | 'purchase-orders' | 'onboarding' | 'audit-trail' | 'notification-rules' | 'batch-import' | 'rate-templates' | 'custom-reports' | 'holiday-pay' | 'contract-validation' | 'shift-patterns' | 'query-guide' | 'component-showcase' | 'business-logic-demo' | 'data-admin' | 'translation-demo'
|
||||
|
||||
function App() {
|
||||
const dispatch = useAppDispatch()
|
||||
const isAuthenticated = useAppSelector(state => state.auth.isAuthenticated)
|
||||
const currentEntity = useAppSelector(state => state.auth.currentEntity)
|
||||
const currentView = useAppSelector(state => state.ui.currentView)
|
||||
const searchQuery = useAppSelector(state => state.ui.searchQuery)
|
||||
|
||||
useSampleData()
|
||||
useViewPreload()
|
||||
|
||||
const [currentView, setCurrentView] = useState<View>('dashboard')
|
||||
const [currentEntity, setCurrentEntity] = useState('Main Agency')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const { notifications, addNotification, markAsRead, markAllAsRead, deleteNotification, unreadCount } = useNotifications()
|
||||
|
||||
const {
|
||||
@@ -47,13 +52,17 @@ function App() {
|
||||
addNotification
|
||||
)
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <LoginScreen />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background overflow-hidden">
|
||||
<Sidebar
|
||||
currentView={currentView}
|
||||
setCurrentView={setCurrentView}
|
||||
setCurrentView={(view) => dispatch(setCurrentView(view as View))}
|
||||
currentEntity={currentEntity}
|
||||
setCurrentEntity={setCurrentEntity}
|
||||
setCurrentEntity={(entity) => dispatch(setCurrentEntity(entity))}
|
||||
metrics={metrics}
|
||||
/>
|
||||
|
||||
@@ -78,7 +87,7 @@ function App() {
|
||||
<ViewRouter
|
||||
currentView={currentView}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
setSearchQuery={(query) => dispatch(setSearchQuery(query))}
|
||||
metrics={metrics}
|
||||
timesheets={timesheets}
|
||||
invoices={invoices}
|
||||
|
||||
212
src/components/LoginScreen.tsx
Normal file
212
src/components/LoginScreen.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Buildings, Lock, User, Eye, EyeSlash } from '@phosphor-icons/react'
|
||||
import { useAppDispatch } from '@/store/hooks'
|
||||
import { login } from '@/store/slices/authSlice'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export default function LoginScreen() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [rememberMe, setRememberMe] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!email || !password) {
|
||||
toast.error('Please enter your email and password')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(login({
|
||||
id: '1',
|
||||
email,
|
||||
name: email.split('@')[0].split('.').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' '),
|
||||
role: 'Admin',
|
||||
avatarUrl: undefined,
|
||||
}))
|
||||
|
||||
toast.success('Welcome back!')
|
||||
setIsLoading(false)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex">
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-primary via-accent to-primary/80 relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-20 left-20 w-96 h-96 bg-white rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-20 right-20 w-80 h-80 bg-white rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 left-1/2 w-72 h-72 bg-white rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col justify-center px-16 text-white">
|
||||
<div className="mb-8">
|
||||
<Buildings size={64} weight="duotone" />
|
||||
</div>
|
||||
<h1 className="text-5xl font-semibold mb-6 leading-tight">
|
||||
Welcome to<br />WorkForce Pro
|
||||
</h1>
|
||||
<p className="text-xl text-white/90 leading-relaxed max-w-md">
|
||||
Your complete back-office platform for workforce management, billing, and compliance.
|
||||
</p>
|
||||
|
||||
<div className="mt-12 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<div className="w-2 h-2 rounded-full bg-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-lg mb-1">Streamlined Operations</h3>
|
||||
<p className="text-white/80 text-sm">Automate timesheets, billing, and payroll in one unified platform</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<div className="w-2 h-2 rounded-full bg-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-lg mb-1">Real-Time Insights</h3>
|
||||
<p className="text-white/80 text-sm">Monitor KPIs and make data-driven decisions instantly</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<div className="w-2 h-2 rounded-full bg-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-lg mb-1">Enterprise Security</h3>
|
||||
<p className="text-white/80 text-sm">Bank-level encryption and compliance-ready audit trails</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-1/2 flex items-center justify-center p-8 bg-background">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-10">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Buildings size={32} className="text-primary" weight="duotone" />
|
||||
<span className="text-2xl font-semibold text-foreground">WorkForce Pro</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-semibold text-foreground mb-2">Log in to your account</h2>
|
||||
<p className="text-muted-foreground">Enter your credentials to access the platform</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium">
|
||||
Email Address
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||
<User size={18} />
|
||||
</div>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@company.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="pl-10 h-11"
|
||||
autoComplete="email"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-sm font-medium">
|
||||
Password
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||
<Lock size={18} />
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="pl-10 pr-10 h-11"
|
||||
autoComplete="current-password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? <EyeSlash size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
checked={rememberMe}
|
||||
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Label htmlFor="remember" className="text-sm font-normal cursor-pointer">
|
||||
Remember me
|
||||
</Label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-accent hover:text-accent/80 font-medium transition-colors"
|
||||
disabled={isLoading}
|
||||
>
|
||||
Forgot password?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-11 text-base font-medium"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Logging in...' : 'Log In'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
<button className="text-accent hover:text-accent/80 font-medium transition-colors">
|
||||
Contact Sales
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-border">
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
By logging in, you agree to our{' '}
|
||||
<button className="text-foreground hover:text-accent transition-colors">
|
||||
Terms of Service
|
||||
</button>{' '}
|
||||
and{' '}
|
||||
<button className="text-foreground hover:text-accent transition-colors">
|
||||
Privacy Policy
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
ChartBar,
|
||||
Buildings,
|
||||
@@ -8,10 +9,12 @@ import {
|
||||
Question,
|
||||
PuzzlePiece,
|
||||
Code,
|
||||
Database
|
||||
Database,
|
||||
SignOut
|
||||
} from '@phosphor-icons/react'
|
||||
import { NavItem } from '@/components/nav/NavItem'
|
||||
import { CoreOperationsNav, ReportsNav, ConfigurationNav, ToolsNav } from '@/components/nav/nav-sections'
|
||||
import { useAuth } from '@/hooks/use-auth'
|
||||
import type { View } from '@/App'
|
||||
import type { DashboardMetrics } from '@/lib/types'
|
||||
|
||||
@@ -25,6 +28,7 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({ currentView, setCurrentView, currentEntity, setCurrentEntity, metrics }: SidebarProps) {
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(['core']))
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
const toggleGroup = (groupId: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
@@ -38,6 +42,16 @@ export function Sidebar({ currentView, setCurrentView, currentEntity, setCurrent
|
||||
})
|
||||
}
|
||||
|
||||
const getUserInitials = () => {
|
||||
if (!user) return 'U'
|
||||
return user.name
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-64 border-r border-border bg-card flex flex-col">
|
||||
<div className="p-6 border-b border-border">
|
||||
@@ -137,16 +151,25 @@ export function Sidebar({ currentView, setCurrentView, currentEntity, setCurrent
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-border">
|
||||
<div className="p-4 border-t border-border space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
|
||||
AD
|
||||
{getUserInitials()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">Admin User</p>
|
||||
<p className="text-xs text-muted-foreground truncate">admin@workforce.io</p>
|
||||
<p className="text-sm font-medium truncate">{user?.name || 'User'}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{user?.email || ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-start gap-2"
|
||||
onClick={logout}
|
||||
>
|
||||
<SignOut size={16} />
|
||||
Log Out
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
|
||||
20
src/hooks/use-auth.ts
Normal file
20
src/hooks/use-auth.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useAppSelector, useAppDispatch } from '@/store/hooks'
|
||||
import { logout } from '@/store/slices/authSlice'
|
||||
import { setCurrentView } from '@/store/slices/uiSlice'
|
||||
|
||||
export function useAuth() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { user, isAuthenticated, currentEntity } = useAppSelector(state => state.auth)
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout())
|
||||
dispatch(setCurrentView('dashboard'))
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated,
|
||||
currentEntity,
|
||||
logout: handleLogout,
|
||||
}
|
||||
}
|
||||
19
src/hooks/use-navigation.ts
Normal file
19
src/hooks/use-navigation.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useAppSelector, useAppDispatch } from '@/store/hooks'
|
||||
import { setCurrentView } from '@/store/slices/uiSlice'
|
||||
|
||||
type View = 'dashboard' | 'timesheets' | 'billing' | 'payroll' | 'compliance' | 'expenses' | 'roadmap' | 'reports' | 'currency' | 'email-templates' | 'invoice-templates' | 'qr-scanner' | 'missing-timesheets' | 'purchase-orders' | 'onboarding' | 'audit-trail' | 'notification-rules' | 'batch-import' | 'rate-templates' | 'custom-reports' | 'holiday-pay' | 'contract-validation' | 'shift-patterns' | 'query-guide' | 'component-showcase' | 'business-logic-demo' | 'data-admin' | 'translation-demo'
|
||||
|
||||
export function useNavigation() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { currentView, searchQuery } = useAppSelector(state => state.ui)
|
||||
|
||||
const navigateTo = (view: View) => {
|
||||
dispatch(setCurrentView(view))
|
||||
}
|
||||
|
||||
return {
|
||||
currentView,
|
||||
searchQuery,
|
||||
navigateTo,
|
||||
}
|
||||
}
|
||||
44
src/hooks/use-redux-notifications.ts
Normal file
44
src/hooks/use-redux-notifications.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useAppSelector, useAppDispatch } from '@/store/hooks'
|
||||
import {
|
||||
addNotification as addNotificationAction,
|
||||
markAsRead,
|
||||
markAllAsRead,
|
||||
deleteNotification
|
||||
} from '@/store/slices/notificationsSlice'
|
||||
import type { Notification } from '@/store/slices/notificationsSlice'
|
||||
|
||||
export function useReduxNotifications() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { notifications, unreadCount } = useAppSelector(state => state.notifications)
|
||||
|
||||
const addNotification = (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => {
|
||||
const newNotification: Notification = {
|
||||
...notification,
|
||||
id: `notif-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
}
|
||||
dispatch(addNotificationAction(newNotification))
|
||||
}
|
||||
|
||||
const markNotificationAsRead = (id: string) => {
|
||||
dispatch(markAsRead(id))
|
||||
}
|
||||
|
||||
const markAllNotificationsAsRead = () => {
|
||||
dispatch(markAllAsRead())
|
||||
}
|
||||
|
||||
const deleteNotificationById = (id: string) => {
|
||||
dispatch(deleteNotification(id))
|
||||
}
|
||||
|
||||
return {
|
||||
notifications,
|
||||
unreadCount,
|
||||
addNotification,
|
||||
markAsRead: markNotificationAsRead,
|
||||
markAllAsRead: markAllNotificationsAsRead,
|
||||
deleteNotification: deleteNotificationById,
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Provider } from 'react-redux'
|
||||
import "@github/spark/spark"
|
||||
|
||||
import App from './App.tsx'
|
||||
import { ErrorFallback } from './ErrorFallback.tsx'
|
||||
import { store } from './store/store'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
|
||||
import "./main.css"
|
||||
import "./styles/theme.css"
|
||||
@@ -11,6 +14,9 @@ import "./index.css"
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<App />
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
5
src/store/hooks.ts
Normal file
5
src/store/hooks.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import type { RootState, AppDispatch } from './store'
|
||||
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
42
src/store/slices/authSlice.ts
Normal file
42
src/store/slices/authSlice.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
role: string
|
||||
avatarUrl?: string
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
currentEntity: string
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
currentEntity: 'Main Agency',
|
||||
}
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
login: (state, action: PayloadAction<User>) => {
|
||||
state.user = action.payload
|
||||
state.isAuthenticated = true
|
||||
},
|
||||
logout: (state) => {
|
||||
state.user = null
|
||||
state.isAuthenticated = false
|
||||
},
|
||||
setCurrentEntity: (state, action: PayloadAction<string>) => {
|
||||
state.currentEntity = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { login, logout, setCurrentEntity } = authSlice.actions
|
||||
export default authSlice.reducer
|
||||
51
src/store/slices/complianceSlice.ts
Normal file
51
src/store/slices/complianceSlice.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface ComplianceDoc {
|
||||
id: string
|
||||
workerId: string
|
||||
workerName: string
|
||||
documentType: string
|
||||
status: string
|
||||
expiryDate: string
|
||||
uploadedDate: string
|
||||
verifiedDate?: string
|
||||
daysUntilExpiry: number
|
||||
}
|
||||
|
||||
interface ComplianceState {
|
||||
documents: ComplianceDoc[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState: ComplianceState = {
|
||||
documents: [],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
const complianceSlice = createSlice({
|
||||
name: 'compliance',
|
||||
initialState,
|
||||
reducers: {
|
||||
setComplianceDocs: (state, action: PayloadAction<ComplianceDoc[]>) => {
|
||||
state.documents = action.payload
|
||||
},
|
||||
addComplianceDoc: (state, action: PayloadAction<ComplianceDoc>) => {
|
||||
state.documents.push(action.payload)
|
||||
},
|
||||
updateComplianceDoc: (state, action: PayloadAction<ComplianceDoc>) => {
|
||||
const index = state.documents.findIndex(d => d.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.documents[index] = action.payload
|
||||
}
|
||||
},
|
||||
deleteComplianceDoc: (state, action: PayloadAction<string>) => {
|
||||
state.documents = state.documents.filter(d => d.id !== action.payload)
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setComplianceDocs, addComplianceDoc, updateComplianceDoc, deleteComplianceDoc, setLoading } = complianceSlice.actions
|
||||
export default complianceSlice.reducer
|
||||
53
src/store/slices/expensesSlice.ts
Normal file
53
src/store/slices/expensesSlice.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface Expense {
|
||||
id: string
|
||||
workerId: string
|
||||
workerName: string
|
||||
category: string
|
||||
amount: number
|
||||
currency: string
|
||||
date: string
|
||||
status: string
|
||||
description: string
|
||||
receiptUrl?: string
|
||||
billable: boolean
|
||||
}
|
||||
|
||||
interface ExpensesState {
|
||||
expenses: Expense[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState: ExpensesState = {
|
||||
expenses: [],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
const expensesSlice = createSlice({
|
||||
name: 'expenses',
|
||||
initialState,
|
||||
reducers: {
|
||||
setExpenses: (state, action: PayloadAction<Expense[]>) => {
|
||||
state.expenses = action.payload
|
||||
},
|
||||
addExpense: (state, action: PayloadAction<Expense>) => {
|
||||
state.expenses.push(action.payload)
|
||||
},
|
||||
updateExpense: (state, action: PayloadAction<Expense>) => {
|
||||
const index = state.expenses.findIndex(e => e.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.expenses[index] = action.payload
|
||||
}
|
||||
},
|
||||
deleteExpense: (state, action: PayloadAction<string>) => {
|
||||
state.expenses = state.expenses.filter(e => e.id !== action.payload)
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setExpenses, addExpense, updateExpense, deleteExpense, setLoading } = expensesSlice.actions
|
||||
export default expensesSlice.reducer
|
||||
53
src/store/slices/invoicesSlice.ts
Normal file
53
src/store/slices/invoicesSlice.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface Invoice {
|
||||
id: string
|
||||
clientName: string
|
||||
amount: number
|
||||
vat: number
|
||||
total: number
|
||||
status: string
|
||||
dueDate: string
|
||||
invoiceDate: string
|
||||
paidDate?: string
|
||||
currency: string
|
||||
timesheetIds: string[]
|
||||
}
|
||||
|
||||
interface InvoicesState {
|
||||
invoices: Invoice[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState: InvoicesState = {
|
||||
invoices: [],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
const invoicesSlice = createSlice({
|
||||
name: 'invoices',
|
||||
initialState,
|
||||
reducers: {
|
||||
setInvoices: (state, action: PayloadAction<Invoice[]>) => {
|
||||
state.invoices = action.payload
|
||||
},
|
||||
addInvoice: (state, action: PayloadAction<Invoice>) => {
|
||||
state.invoices.push(action.payload)
|
||||
},
|
||||
updateInvoice: (state, action: PayloadAction<Invoice>) => {
|
||||
const index = state.invoices.findIndex(i => i.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.invoices[index] = action.payload
|
||||
}
|
||||
},
|
||||
deleteInvoice: (state, action: PayloadAction<string>) => {
|
||||
state.invoices = state.invoices.filter(i => i.id !== action.payload)
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setInvoices, addInvoice, updateInvoice, deleteInvoice, setLoading } = invoicesSlice.actions
|
||||
export default invoicesSlice.reducer
|
||||
61
src/store/slices/notificationsSlice.ts
Normal file
61
src/store/slices/notificationsSlice.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
type: 'info' | 'warning' | 'error' | 'success'
|
||||
title: string
|
||||
message: string
|
||||
timestamp: string
|
||||
read: boolean
|
||||
actionUrl?: string
|
||||
}
|
||||
|
||||
interface NotificationsState {
|
||||
notifications: Notification[]
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
const initialState: NotificationsState = {
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
}
|
||||
|
||||
const notificationsSlice = createSlice({
|
||||
name: 'notifications',
|
||||
initialState,
|
||||
reducers: {
|
||||
setNotifications: (state, action: PayloadAction<Notification[]>) => {
|
||||
state.notifications = action.payload
|
||||
state.unreadCount = action.payload.filter(n => !n.read).length
|
||||
},
|
||||
addNotification: (state, action: PayloadAction<Notification>) => {
|
||||
state.notifications.unshift(action.payload)
|
||||
if (!action.payload.read) {
|
||||
state.unreadCount += 1
|
||||
}
|
||||
},
|
||||
markAsRead: (state, action: PayloadAction<string>) => {
|
||||
const notification = state.notifications.find(n => n.id === action.payload)
|
||||
if (notification && !notification.read) {
|
||||
notification.read = true
|
||||
state.unreadCount -= 1
|
||||
}
|
||||
},
|
||||
markAllAsRead: (state) => {
|
||||
state.notifications.forEach(n => {
|
||||
n.read = true
|
||||
})
|
||||
state.unreadCount = 0
|
||||
},
|
||||
deleteNotification: (state, action: PayloadAction<string>) => {
|
||||
const notification = state.notifications.find(n => n.id === action.payload)
|
||||
if (notification && !notification.read) {
|
||||
state.unreadCount -= 1
|
||||
}
|
||||
state.notifications = state.notifications.filter(n => n.id !== action.payload)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setNotifications, addNotification, markAsRead, markAllAsRead, deleteNotification } = notificationsSlice.actions
|
||||
export default notificationsSlice.reducer
|
||||
47
src/store/slices/payrollSlice.ts
Normal file
47
src/store/slices/payrollSlice.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface PayrollRun {
|
||||
id: string
|
||||
periodStart: string
|
||||
periodEnd: string
|
||||
status: string
|
||||
totalWorkers: number
|
||||
totalAmount: number
|
||||
processedDate?: string
|
||||
paymentDate: string
|
||||
}
|
||||
|
||||
interface PayrollState {
|
||||
payrollRuns: PayrollRun[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState: PayrollState = {
|
||||
payrollRuns: [],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
const payrollSlice = createSlice({
|
||||
name: 'payroll',
|
||||
initialState,
|
||||
reducers: {
|
||||
setPayrollRuns: (state, action: PayloadAction<PayrollRun[]>) => {
|
||||
state.payrollRuns = action.payload
|
||||
},
|
||||
addPayrollRun: (state, action: PayloadAction<PayrollRun>) => {
|
||||
state.payrollRuns.push(action.payload)
|
||||
},
|
||||
updatePayrollRun: (state, action: PayloadAction<PayrollRun>) => {
|
||||
const index = state.payrollRuns.findIndex(p => p.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.payrollRuns[index] = action.payload
|
||||
}
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setPayrollRuns, addPayrollRun, updatePayrollRun, setLoading } = payrollSlice.actions
|
||||
export default payrollSlice.reducer
|
||||
64
src/store/slices/timesheetsSlice.ts
Normal file
64
src/store/slices/timesheetsSlice.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface Shift {
|
||||
date: string
|
||||
start: string
|
||||
end: string
|
||||
hours: number
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface Timesheet {
|
||||
id: string
|
||||
workerId: string
|
||||
workerName: string
|
||||
clientName: string
|
||||
weekEnding: string
|
||||
totalHours: number
|
||||
regularHours: number
|
||||
overtimeHours: number
|
||||
status: string
|
||||
rate: number
|
||||
total: number
|
||||
submittedDate: string
|
||||
approvedDate?: string
|
||||
shifts: Shift[]
|
||||
}
|
||||
|
||||
interface TimesheetsState {
|
||||
timesheets: Timesheet[]
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState: TimesheetsState = {
|
||||
timesheets: [],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
const timesheetsSlice = createSlice({
|
||||
name: 'timesheets',
|
||||
initialState,
|
||||
reducers: {
|
||||
setTimesheets: (state, action: PayloadAction<Timesheet[]>) => {
|
||||
state.timesheets = action.payload
|
||||
},
|
||||
addTimesheet: (state, action: PayloadAction<Timesheet>) => {
|
||||
state.timesheets.push(action.payload)
|
||||
},
|
||||
updateTimesheet: (state, action: PayloadAction<Timesheet>) => {
|
||||
const index = state.timesheets.findIndex(t => t.id === action.payload.id)
|
||||
if (index !== -1) {
|
||||
state.timesheets[index] = action.payload
|
||||
}
|
||||
},
|
||||
deleteTimesheet: (state, action: PayloadAction<string>) => {
|
||||
state.timesheets = state.timesheets.filter(t => t.id !== action.payload)
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setTimesheets, addTimesheet, updateTimesheet, deleteTimesheet, setLoading } = timesheetsSlice.actions
|
||||
export default timesheetsSlice.reducer
|
||||
34
src/store/slices/uiSlice.ts
Normal file
34
src/store/slices/uiSlice.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
type View = 'dashboard' | 'timesheets' | 'billing' | 'payroll' | 'compliance' | 'expenses' | 'roadmap' | 'reports' | 'currency' | 'email-templates' | 'invoice-templates' | 'qr-scanner' | 'missing-timesheets' | 'purchase-orders' | 'onboarding' | 'audit-trail' | 'notification-rules' | 'batch-import' | 'rate-templates' | 'custom-reports' | 'holiday-pay' | 'contract-validation' | 'shift-patterns' | 'query-guide' | 'component-showcase' | 'business-logic-demo' | 'data-admin' | 'translation-demo'
|
||||
|
||||
interface UIState {
|
||||
currentView: View
|
||||
searchQuery: string
|
||||
sidebarCollapsed: boolean
|
||||
}
|
||||
|
||||
const initialState: UIState = {
|
||||
currentView: 'dashboard',
|
||||
searchQuery: '',
|
||||
sidebarCollapsed: false,
|
||||
}
|
||||
|
||||
const uiSlice = createSlice({
|
||||
name: 'ui',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentView: (state, action: PayloadAction<View>) => {
|
||||
state.currentView = action.payload
|
||||
},
|
||||
setSearchQuery: (state, action: PayloadAction<string>) => {
|
||||
state.searchQuery = action.payload
|
||||
},
|
||||
toggleSidebar: (state) => {
|
||||
state.sidebarCollapsed = !state.sidebarCollapsed
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { setCurrentView, setSearchQuery, toggleSidebar } = uiSlice.actions
|
||||
export default uiSlice.reducer
|
||||
25
src/store/store.ts
Normal file
25
src/store/store.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import authReducer from './slices/authSlice'
|
||||
import timesheetsReducer from './slices/timesheetsSlice'
|
||||
import invoicesReducer from './slices/invoicesSlice'
|
||||
import payrollReducer from './slices/payrollSlice'
|
||||
import complianceReducer from './slices/complianceSlice'
|
||||
import expensesReducer from './slices/expensesSlice'
|
||||
import notificationsReducer from './slices/notificationsSlice'
|
||||
import uiReducer from './slices/uiSlice'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
timesheets: timesheetsReducer,
|
||||
invoices: invoicesReducer,
|
||||
payroll: payrollReducer,
|
||||
compliance: complianceReducer,
|
||||
expenses: expensesReducer,
|
||||
notifications: notificationsReducer,
|
||||
ui: uiReducer,
|
||||
},
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
Reference in New Issue
Block a user