From 977c3dcbd60eaa7427e453243eadb21aa6aceb7a Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 23 Jan 2026 09:31:15 +0000 Subject: [PATCH] Generated by Spark: Salesforce style login screen, Introduce redux and apply to all pages/custom hooks where needed --- PRD.md | 11 +- REDUX_GUIDE.md | 300 +++++++++++++++++++++++++ index.html | 2 +- package-lock.json | 94 ++++++++ package.json | 4 +- src/App.tsx | 25 ++- src/components/LoginScreen.tsx | 212 +++++++++++++++++ src/components/navigation.tsx | 33 ++- src/hooks/use-auth.ts | 20 ++ src/hooks/use-navigation.ts | 19 ++ src/hooks/use-redux-notifications.ts | 44 ++++ src/main.tsx | 8 +- src/store/hooks.ts | 5 + src/store/slices/authSlice.ts | 42 ++++ src/store/slices/complianceSlice.ts | 51 +++++ src/store/slices/expensesSlice.ts | 53 +++++ src/store/slices/invoicesSlice.ts | 53 +++++ src/store/slices/notificationsSlice.ts | 61 +++++ src/store/slices/payrollSlice.ts | 47 ++++ src/store/slices/timesheetsSlice.ts | 64 ++++++ src/store/slices/uiSlice.ts | 34 +++ src/store/store.ts | 25 +++ 22 files changed, 1189 insertions(+), 18 deletions(-) create mode 100644 REDUX_GUIDE.md create mode 100644 src/components/LoginScreen.tsx create mode 100644 src/hooks/use-auth.ts create mode 100644 src/hooks/use-navigation.ts create mode 100644 src/hooks/use-redux-notifications.ts create mode 100644 src/store/hooks.ts create mode 100644 src/store/slices/authSlice.ts create mode 100644 src/store/slices/complianceSlice.ts create mode 100644 src/store/slices/expensesSlice.ts create mode 100644 src/store/slices/invoicesSlice.ts create mode 100644 src/store/slices/notificationsSlice.ts create mode 100644 src/store/slices/payrollSlice.ts create mode 100644 src/store/slices/timesheetsSlice.ts create mode 100644 src/store/slices/uiSlice.ts create mode 100644 src/store/store.ts diff --git a/PRD.md b/PRD.md index d6699c8..0f5434f 100644 --- a/PRD.md +++ b/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 diff --git a/REDUX_GUIDE.md b/REDUX_GUIDE.md new file mode 100644 index 0000000..73d9ae1 --- /dev/null +++ b/REDUX_GUIDE.md @@ -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 ( +
+ {isAuthenticated &&

Welcome, {user?.name}!

} + +
+ ) +} +``` + +### useNavigation Hook (`/src/hooks/use-navigation.ts`) + +```typescript +import { useNavigation } from '@/hooks/use-navigation' + +function MyComponent() { + const { currentView, navigateTo } = useNavigation() + + return ( + + ) +} +``` + +### 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 ( +
+

Unread: {unreadCount}

+ {notifications.map(n => ( +
markAsRead(n.id)}> + {n.title} +
+ ))} +
+ ) +} +``` + +## 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
Loading...
+ + return ( +
+ + {timesheets.map(ts => ( +
{ts.workerName}
+ ))} +
+ ) +} +``` + +## 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 diff --git a/index.html b/index.html index e7d2ef9..b42dd33 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - WorkForce Pro - Enhanced Back Office Platform + WorkForce Pro - Back Office Platform diff --git a/package-lock.json b/package-lock.json index a6e2376..fef6e3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index cf6c35f..a10a225 100644 --- a/package.json +++ b/package.json @@ -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/*" ] } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 458b72d..d3b32c3 100644 --- a/src/App.tsx +++ b/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('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 + } + return (
dispatch(setCurrentView(view as View))} currentEntity={currentEntity} - setCurrentEntity={setCurrentEntity} + setCurrentEntity={(entity) => dispatch(setCurrentEntity(entity))} metrics={metrics} /> @@ -78,7 +87,7 @@ function App() { dispatch(setSearchQuery(query))} metrics={metrics} timesheets={timesheets} invoices={invoices} diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx new file mode 100644 index 0000000..9beebc0 --- /dev/null +++ b/src/components/LoginScreen.tsx @@ -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 ( +
+
+
+
+
+
+
+ +
+
+ +
+

+ Welcome to
WorkForce Pro +

+

+ Your complete back-office platform for workforce management, billing, and compliance. +

+ +
+
+
+
+
+
+

Streamlined Operations

+

Automate timesheets, billing, and payroll in one unified platform

+
+
+
+
+
+
+
+

Real-Time Insights

+

Monitor KPIs and make data-driven decisions instantly

+
+
+
+
+
+
+
+

Enterprise Security

+

Bank-level encryption and compliance-ready audit trails

+
+
+
+
+
+ +
+
+
+
+ + WorkForce Pro +
+

Log in to your account

+

Enter your credentials to access the platform

+
+ +
+
+ +
+
+ +
+ setEmail(e.target.value)} + className="pl-10 h-11" + autoComplete="email" + disabled={isLoading} + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + className="pl-10 pr-10 h-11" + autoComplete="current-password" + disabled={isLoading} + /> + +
+
+ +
+
+ setRememberMe(checked as boolean)} + disabled={isLoading} + /> + +
+ +
+ + +
+ +
+

+ Don't have an account?{' '} + +

+
+ +
+

+ By logging in, you agree to our{' '} + {' '} + and{' '} + +

+
+
+
+
+ ) +} diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 8241bb7..25b5fc7 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -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>(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 ( ) diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts new file mode 100644 index 0000000..e0a8750 --- /dev/null +++ b/src/hooks/use-auth.ts @@ -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, + } +} diff --git a/src/hooks/use-navigation.ts b/src/hooks/use-navigation.ts new file mode 100644 index 0000000..890ef45 --- /dev/null +++ b/src/hooks/use-navigation.ts @@ -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, + } +} diff --git a/src/hooks/use-redux-notifications.ts b/src/hooks/use-redux-notifications.ts new file mode 100644 index 0000000..da08418 --- /dev/null +++ b/src/hooks/use-redux-notifications.ts @@ -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) => { + 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, + } +} diff --git a/src/main.tsx b/src/main.tsx index de0ac2e..bd68f4f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - + + + + ) diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 0000000..142b687 --- /dev/null +++ b/src/store/hooks.ts @@ -0,0 +1,5 @@ +import { useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from './store' + +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts new file mode 100644 index 0000000..a62c61b --- /dev/null +++ b/src/store/slices/authSlice.ts @@ -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) => { + state.user = action.payload + state.isAuthenticated = true + }, + logout: (state) => { + state.user = null + state.isAuthenticated = false + }, + setCurrentEntity: (state, action: PayloadAction) => { + state.currentEntity = action.payload + }, + }, +}) + +export const { login, logout, setCurrentEntity } = authSlice.actions +export default authSlice.reducer diff --git a/src/store/slices/complianceSlice.ts b/src/store/slices/complianceSlice.ts new file mode 100644 index 0000000..110c360 --- /dev/null +++ b/src/store/slices/complianceSlice.ts @@ -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) => { + state.documents = action.payload + }, + addComplianceDoc: (state, action: PayloadAction) => { + state.documents.push(action.payload) + }, + updateComplianceDoc: (state, action: PayloadAction) => { + const index = state.documents.findIndex(d => d.id === action.payload.id) + if (index !== -1) { + state.documents[index] = action.payload + } + }, + deleteComplianceDoc: (state, action: PayloadAction) => { + state.documents = state.documents.filter(d => d.id !== action.payload) + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload + }, + }, +}) + +export const { setComplianceDocs, addComplianceDoc, updateComplianceDoc, deleteComplianceDoc, setLoading } = complianceSlice.actions +export default complianceSlice.reducer diff --git a/src/store/slices/expensesSlice.ts b/src/store/slices/expensesSlice.ts new file mode 100644 index 0000000..e3aff6b --- /dev/null +++ b/src/store/slices/expensesSlice.ts @@ -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) => { + state.expenses = action.payload + }, + addExpense: (state, action: PayloadAction) => { + state.expenses.push(action.payload) + }, + updateExpense: (state, action: PayloadAction) => { + const index = state.expenses.findIndex(e => e.id === action.payload.id) + if (index !== -1) { + state.expenses[index] = action.payload + } + }, + deleteExpense: (state, action: PayloadAction) => { + state.expenses = state.expenses.filter(e => e.id !== action.payload) + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload + }, + }, +}) + +export const { setExpenses, addExpense, updateExpense, deleteExpense, setLoading } = expensesSlice.actions +export default expensesSlice.reducer diff --git a/src/store/slices/invoicesSlice.ts b/src/store/slices/invoicesSlice.ts new file mode 100644 index 0000000..bde786a --- /dev/null +++ b/src/store/slices/invoicesSlice.ts @@ -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) => { + state.invoices = action.payload + }, + addInvoice: (state, action: PayloadAction) => { + state.invoices.push(action.payload) + }, + updateInvoice: (state, action: PayloadAction) => { + const index = state.invoices.findIndex(i => i.id === action.payload.id) + if (index !== -1) { + state.invoices[index] = action.payload + } + }, + deleteInvoice: (state, action: PayloadAction) => { + state.invoices = state.invoices.filter(i => i.id !== action.payload) + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload + }, + }, +}) + +export const { setInvoices, addInvoice, updateInvoice, deleteInvoice, setLoading } = invoicesSlice.actions +export default invoicesSlice.reducer diff --git a/src/store/slices/notificationsSlice.ts b/src/store/slices/notificationsSlice.ts new file mode 100644 index 0000000..561b574 --- /dev/null +++ b/src/store/slices/notificationsSlice.ts @@ -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) => { + state.notifications = action.payload + state.unreadCount = action.payload.filter(n => !n.read).length + }, + addNotification: (state, action: PayloadAction) => { + state.notifications.unshift(action.payload) + if (!action.payload.read) { + state.unreadCount += 1 + } + }, + markAsRead: (state, action: PayloadAction) => { + 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) => { + 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 diff --git a/src/store/slices/payrollSlice.ts b/src/store/slices/payrollSlice.ts new file mode 100644 index 0000000..ea77cfd --- /dev/null +++ b/src/store/slices/payrollSlice.ts @@ -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) => { + state.payrollRuns = action.payload + }, + addPayrollRun: (state, action: PayloadAction) => { + state.payrollRuns.push(action.payload) + }, + updatePayrollRun: (state, action: PayloadAction) => { + const index = state.payrollRuns.findIndex(p => p.id === action.payload.id) + if (index !== -1) { + state.payrollRuns[index] = action.payload + } + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload + }, + }, +}) + +export const { setPayrollRuns, addPayrollRun, updatePayrollRun, setLoading } = payrollSlice.actions +export default payrollSlice.reducer diff --git a/src/store/slices/timesheetsSlice.ts b/src/store/slices/timesheetsSlice.ts new file mode 100644 index 0000000..e1dd097 --- /dev/null +++ b/src/store/slices/timesheetsSlice.ts @@ -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) => { + state.timesheets = action.payload + }, + addTimesheet: (state, action: PayloadAction) => { + state.timesheets.push(action.payload) + }, + updateTimesheet: (state, action: PayloadAction) => { + const index = state.timesheets.findIndex(t => t.id === action.payload.id) + if (index !== -1) { + state.timesheets[index] = action.payload + } + }, + deleteTimesheet: (state, action: PayloadAction) => { + state.timesheets = state.timesheets.filter(t => t.id !== action.payload) + }, + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload + }, + }, +}) + +export const { setTimesheets, addTimesheet, updateTimesheet, deleteTimesheet, setLoading } = timesheetsSlice.actions +export default timesheetsSlice.reducer diff --git a/src/store/slices/uiSlice.ts b/src/store/slices/uiSlice.ts new file mode 100644 index 0000000..b395d4b --- /dev/null +++ b/src/store/slices/uiSlice.ts @@ -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) => { + state.currentView = action.payload + }, + setSearchQuery: (state, action: PayloadAction) => { + state.searchQuery = action.payload + }, + toggleSidebar: (state) => { + state.sidebarCollapsed = !state.sidebarCollapsed + }, + }, +}) + +export const { setCurrentView, setSearchQuery, toggleSidebar } = uiSlice.actions +export default uiSlice.reducer diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..8e20bee --- /dev/null +++ b/src/store/store.ts @@ -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 +export type AppDispatch = typeof store.dispatch