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
|
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)
|
**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
|
## 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**
|
**Dashboard Overview**
|
||||||
- Functionality: Displays real-time KPIs, alerts, and quick actions across all modules
|
- 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
|
- 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
|
- 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
|
- 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>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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": "^1.1.2",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.83.1",
|
"@tanstack/react-query": "^5.83.1",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
@@ -3083,6 +3085,32 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.47",
|
"version": "1.0.0-beta.47",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
|
||||||
@@ -3557,6 +3585,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@standard-schema/utils": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
@@ -4404,6 +4438,12 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.48.0",
|
"version": "8.48.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
|
||||||
@@ -7196,6 +7236,16 @@
|
|||||||
"node": ">= 4"
|
"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": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"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==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||||
@@ -8713,6 +8786,27 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"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": "^1.1.2",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.83.1",
|
"@tanstack/react-query": "^5.83.1",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.1",
|
"recharts": "^2.15.1",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
@@ -94,4 +96,4 @@
|
|||||||
"packages/*"
|
"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 { useSampleData } from '@/hooks/use-sample-data'
|
||||||
import { useNotifications } from '@/hooks/use-notifications'
|
import { useNotifications } from '@/hooks/use-notifications'
|
||||||
import { useAppData } from '@/hooks/use-app-data'
|
import { useAppData } from '@/hooks/use-app-data'
|
||||||
@@ -8,17 +7,23 @@ import { Sidebar } from '@/components/navigation'
|
|||||||
import { NotificationCenter } from '@/components/NotificationCenter'
|
import { NotificationCenter } from '@/components/NotificationCenter'
|
||||||
import { ViewRouter } from '@/components/ViewRouter'
|
import { ViewRouter } from '@/components/ViewRouter'
|
||||||
import { LanguageSwitcher } from '@/components/LanguageSwitcher'
|
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'
|
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() {
|
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()
|
useSampleData()
|
||||||
useViewPreload()
|
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 { notifications, addNotification, markAsRead, markAllAsRead, deleteNotification, unreadCount } = useNotifications()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -47,13 +52,17 @@ function App() {
|
|||||||
addNotification
|
addNotification
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <LoginScreen />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-background overflow-hidden">
|
<div className="flex h-screen bg-background overflow-hidden">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
currentView={currentView}
|
currentView={currentView}
|
||||||
setCurrentView={setCurrentView}
|
setCurrentView={(view) => dispatch(setCurrentView(view as View))}
|
||||||
currentEntity={currentEntity}
|
currentEntity={currentEntity}
|
||||||
setCurrentEntity={setCurrentEntity}
|
setCurrentEntity={(entity) => dispatch(setCurrentEntity(entity))}
|
||||||
metrics={metrics}
|
metrics={metrics}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -78,7 +87,7 @@ function App() {
|
|||||||
<ViewRouter
|
<ViewRouter
|
||||||
currentView={currentView}
|
currentView={currentView}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={(query) => dispatch(setSearchQuery(query))}
|
||||||
metrics={metrics}
|
metrics={metrics}
|
||||||
timesheets={timesheets}
|
timesheets={timesheets}
|
||||||
invoices={invoices}
|
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 { useState } from 'react'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
ChartBar,
|
ChartBar,
|
||||||
Buildings,
|
Buildings,
|
||||||
@@ -8,10 +9,12 @@ import {
|
|||||||
Question,
|
Question,
|
||||||
PuzzlePiece,
|
PuzzlePiece,
|
||||||
Code,
|
Code,
|
||||||
Database
|
Database,
|
||||||
|
SignOut
|
||||||
} from '@phosphor-icons/react'
|
} from '@phosphor-icons/react'
|
||||||
import { NavItem } from '@/components/nav/NavItem'
|
import { NavItem } from '@/components/nav/NavItem'
|
||||||
import { CoreOperationsNav, ReportsNav, ConfigurationNav, ToolsNav } from '@/components/nav/nav-sections'
|
import { CoreOperationsNav, ReportsNav, ConfigurationNav, ToolsNav } from '@/components/nav/nav-sections'
|
||||||
|
import { useAuth } from '@/hooks/use-auth'
|
||||||
import type { View } from '@/App'
|
import type { View } from '@/App'
|
||||||
import type { DashboardMetrics } from '@/lib/types'
|
import type { DashboardMetrics } from '@/lib/types'
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ interface SidebarProps {
|
|||||||
|
|
||||||
export function Sidebar({ currentView, setCurrentView, currentEntity, setCurrentEntity, metrics }: SidebarProps) {
|
export function Sidebar({ currentView, setCurrentView, currentEntity, setCurrentEntity, metrics }: SidebarProps) {
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(['core']))
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(['core']))
|
||||||
|
const { user, logout } = useAuth()
|
||||||
|
|
||||||
const toggleGroup = (groupId: string) => {
|
const toggleGroup = (groupId: string) => {
|
||||||
setExpandedGroups(prev => {
|
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 (
|
return (
|
||||||
<aside className="w-64 border-r border-border bg-card flex flex-col">
|
<aside className="w-64 border-r border-border bg-card flex flex-col">
|
||||||
<div className="p-6 border-b border-border">
|
<div className="p-6 border-b border-border">
|
||||||
@@ -137,16 +151,25 @@ export function Sidebar({ currentView, setCurrentView, currentEntity, setCurrent
|
|||||||
/>
|
/>
|
||||||
</nav>
|
</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="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">
|
<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>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium truncate">Admin User</p>
|
<p className="text-sm font-medium truncate">{user?.name || 'User'}</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">admin@workforce.io</p>
|
<p className="text-xs text-muted-foreground truncate">{user?.email || ''}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={logout}
|
||||||
|
>
|
||||||
|
<SignOut size={16} />
|
||||||
|
Log Out
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</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 { createRoot } from 'react-dom/client'
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
import "@github/spark/spark"
|
import "@github/spark/spark"
|
||||||
|
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import { ErrorFallback } from './ErrorFallback.tsx'
|
import { ErrorFallback } from './ErrorFallback.tsx'
|
||||||
|
import { store } from './store/store'
|
||||||
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
|
|
||||||
import "./main.css"
|
import "./main.css"
|
||||||
import "./styles/theme.css"
|
import "./styles/theme.css"
|
||||||
@@ -11,6 +14,9 @@ import "./index.css"
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<App />
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
<Toaster />
|
||||||
|
</Provider>
|
||||||
</ErrorBoundary>
|
</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