diff --git a/src/App.tsx b/src/App.tsx index 7e8407d..a646754 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,52 +1,11 @@ import { useState } from 'react' -import { useKV } from '@github/spark/hooks' -import { useNotifications } from '@/hooks/use-notifications' import { useSampleData } from '@/hooks/use-sample-data' -import { toast } from 'sonner' +import { useNotifications } from '@/hooks/use-notifications' +import { useAppData } from '@/hooks/use-app-data' +import { useAppActions } from '@/hooks/use-app-actions' import { Sidebar } from '@/components/navigation' import { NotificationCenter } from '@/components/NotificationCenter' -import { - DashboardView, - TimesheetsView, - BillingView, - PayrollView, - ComplianceView, - ExpensesView -} from '@/components/views' -import { ReportsView } from '@/components/ReportsView' -import { CurrencyManagement } from '@/components/CurrencyManagement' -import { EmailTemplateManager } from '@/components/EmailTemplateManager' -import { InvoiceTemplateManager } from '@/components/InvoiceTemplateManager' -import { QRTimesheetScanner } from '@/components/QRTimesheetScanner' -import { MissingTimesheetsReport } from '@/components/MissingTimesheetsReport' -import { PurchaseOrderManager } from '@/components/PurchaseOrderManager' -import { OnboardingWorkflowManager } from '@/components/OnboardingWorkflowManager' -import { AuditTrailViewer } from '@/components/AuditTrailViewer' -import { NotificationRulesManager } from '@/components/NotificationRulesManager' -import { BatchImportManager } from '@/components/BatchImportManager' -import { RateTemplateManager } from '@/components/RateTemplateManager' -import { CustomReportBuilder } from '@/components/CustomReportBuilder' -import { HolidayPayManager } from '@/components/HolidayPayManager' -import { ContractValidator } from '@/components/ContractValidator' -import { ShiftPatternManager } from '@/components/ShiftPatternManager' -import { QueryLanguageGuide } from '@/components/QueryLanguageGuide' -import { RoadmapView } from '@/components/roadmap-view' -import { ComponentShowcase } from '@/components/ComponentShowcase' -import { BusinessLogicDemo } from '@/components/BusinessLogicDemo' -import type { - Timesheet, - Invoice, - PayrollRun, - Worker, - DashboardMetrics, - ComplianceDocument, - ComplianceStatus, - Expense, - ExpenseStatus, - RateCard, - InvoiceStatus, - ShiftEntry -} from '@/lib/types' +import { ViewRouter } from '@/components/ViewRouter' 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' @@ -56,328 +15,34 @@ function App() { const [currentView, setCurrentView] = useState('dashboard') const [currentEntity, setCurrentEntity] = useState('Main Agency') const [searchQuery, setSearchQuery] = useState('') + const { notifications, addNotification, markAsRead, markAllAsRead, deleteNotification, unreadCount } = useNotifications() - const [timesheets = [], setTimesheets] = useKV('timesheets', []) - const [invoices = [], setInvoices] = useKV('invoices', []) - const [payrollRuns = [], setPayrollRuns] = useKV('payroll-runs', []) - const [workers = [], setWorkers] = useKV('workers', []) - const [complianceDocs = [], setComplianceDocs] = useKV('compliance-docs', []) - const [expenses = [], setExpenses] = useKV('expenses', []) - const [rateCards = [], setRateCards] = useKV('rate-cards', []) + const { + timesheets, + setTimesheets, + invoices, + setInvoices, + payrollRuns, + setPayrollRuns, + workers, + complianceDocs, + setComplianceDocs, + expenses, + setExpenses, + rateCards, + metrics + } = useAppData() - const metrics: DashboardMetrics = { - pendingTimesheets: timesheets.filter(t => t.status === 'pending').length, - pendingApprovals: timesheets.filter(t => t.status === 'pending').length, - overdueInvoices: invoices.filter(i => i.status === 'overdue').length, - complianceAlerts: complianceDocs.filter(d => d.status === 'expiring' || d.status === 'expired').length, - monthlyRevenue: invoices.reduce((sum, inv) => sum + inv.amount, 0), - monthlyPayroll: payrollRuns.reduce((sum, pr) => sum + pr.totalAmount, 0), - grossMargin: 0, - activeWorkers: workers.filter(w => w.status === 'active').length, - pendingExpenses: expenses.filter(e => e.status === 'pending').length - } - - metrics.grossMargin = metrics.monthlyRevenue > 0 - ? ((metrics.monthlyRevenue - metrics.monthlyPayroll) / metrics.monthlyRevenue) * 100 - : 0 - - const handleApproveTimesheet = (id: string) => { - setTimesheets(current => { - if (!current) return [] - return current.map(t => - t.id === id - ? { ...t, status: 'approved', approvedDate: new Date().toISOString() } - : t - ) - }) - const timesheet = timesheets.find(t => t.id === id) - if (timesheet) { - addNotification({ - type: 'timesheet', - priority: 'medium', - title: 'Timesheet Approved', - message: `${timesheet.workerName}'s timesheet for ${new Date(timesheet.weekEnding).toLocaleDateString()} has been approved`, - relatedId: id - }) - } - toast.success('Timesheet approved successfully') - } - - const handleRejectTimesheet = (id: string) => { - setTimesheets(current => { - if (!current) return [] - return current.map(t => - t.id === id - ? { ...t, status: 'rejected' } - : t - ) - }) - const timesheet = timesheets.find(t => t.id === id) - if (timesheet) { - addNotification({ - type: 'timesheet', - priority: 'medium', - title: 'Timesheet Rejected', - message: `${timesheet.workerName}'s timesheet for ${new Date(timesheet.weekEnding).toLocaleDateString()} has been rejected`, - relatedId: id - }) - } - toast.error('Timesheet rejected') - } - - const handleAdjustTimesheet = (timesheetId: string, adjustment: any) => { - setTimesheets(current => { - if (!current) return [] - return current.map(t => { - if (t.id !== timesheetId) return t - - const newAdjustment = { - id: `ADJ-${Date.now()}`, - adjustmentDate: new Date().toISOString(), - ...adjustment - } - - return { - ...t, - hours: adjustment.newHours, - rate: adjustment.newRate, - amount: adjustment.newHours * adjustment.newRate, - adjustments: [...(t.adjustments || []), newAdjustment] - } - }) - }) - } - - const handleCreateInvoice = (timesheetId: string) => { - const timesheet = timesheets.find(t => t.id === timesheetId) - if (!timesheet) return - - const newInvoice: Invoice = { - id: `INV-${Date.now()}`, - invoiceNumber: `INV-${String(invoices.length + 1).padStart(5, '0')}`, - clientName: timesheet.clientName, - issueDate: new Date().toISOString().split('T')[0], - dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], - amount: timesheet.amount, - status: 'draft', - currency: 'GBP' - } - - setInvoices(current => [...(current || []), newInvoice]) - toast.success(`Invoice ${newInvoice.invoiceNumber} created`) - } - - const handleCreateTimesheet = (data: { - workerName: string - clientName: string - hours: number - rate: number - weekEnding: string - }) => { - const newTimesheet: Timesheet = { - id: `TS-${Date.now()}`, - workerId: `W-${Date.now()}`, - workerName: data.workerName, - clientName: data.clientName, - weekEnding: data.weekEnding, - hours: data.hours, - status: 'pending', - submittedDate: new Date().toISOString(), - amount: data.hours * data.rate - } - - setTimesheets(current => [...(current || []), newTimesheet]) - toast.success('Timesheet created successfully') - } - - const handleCreateDetailedTimesheet = (data: { - workerName: string - clientName: string - weekEnding: string - shifts: ShiftEntry[] - totalHours: number - totalAmount: number - baseRate: number - }) => { - const newTimesheet: Timesheet = { - id: `TS-${Date.now()}`, - workerId: `W-${Date.now()}`, - workerName: data.workerName, - clientName: data.clientName, - weekEnding: data.weekEnding, - hours: data.totalHours, - status: 'pending', - submittedDate: new Date().toISOString(), - amount: data.totalAmount, - rate: data.baseRate, - shifts: data.shifts - } - - setTimesheets(current => [...(current || []), newTimesheet]) - toast.success(`Detailed timesheet created with ${data.shifts.length} shifts`) - } - - const handleBulkImport = (csvData: string) => { - const lines = csvData.trim().split('\n') - if (lines.length < 2) { - toast.error('Invalid CSV format') - return - } - - const headers = lines[0].split(',').map(h => h.trim()) - const newTimesheets: Timesheet[] = [] - - for (let i = 1; i < lines.length; i++) { - const values = lines[i].split(',').map(v => v.trim()) - if (values.length !== headers.length) continue - - const workerName = values[headers.indexOf('workerName')] || values[0] - const clientName = values[headers.indexOf('clientName')] || values[1] - const hours = parseFloat(values[headers.indexOf('hours')] || values[2] || '0') - const rate = parseFloat(values[headers.indexOf('rate')] || values[3] || '0') - const weekEnding = values[headers.indexOf('weekEnding')] || values[4] - - if (workerName && clientName && hours > 0 && rate > 0) { - newTimesheets.push({ - id: `TS-${Date.now()}-${i}`, - workerId: `W-${Date.now()}-${i}`, - workerName, - clientName, - weekEnding, - hours, - status: 'pending', - submittedDate: new Date().toISOString(), - amount: hours * rate - }) - } - } - - if (newTimesheets.length > 0) { - setTimesheets(current => [...(current || []), ...newTimesheets]) - toast.success(`Imported ${newTimesheets.length} timesheets`) - } else { - toast.error('No valid timesheets found in CSV') - } - } - - const handleSendInvoice = (invoiceId: string) => { - setInvoices(current => { - if (!current) return [] - return current.map(inv => - inv.id === invoiceId - ? { ...inv, status: 'sent' as InvoiceStatus } - : inv - ) - }) - toast.success('Invoice sent to client via email') - } - - const handleUploadDocument = (data: { - workerId: string - workerName: string - documentType: string - expiryDate: string - }) => { - const expiryDateObj = new Date(data.expiryDate) - const now = new Date() - const daysUntilExpiry = Math.floor((expiryDateObj.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) - - let status: ComplianceStatus = 'valid' - if (daysUntilExpiry < 0) status = 'expired' - else if (daysUntilExpiry < 30) status = 'expiring' - - const newDoc: ComplianceDocument = { - id: `DOC-${Date.now()}`, - workerId: data.workerId, - workerName: data.workerName, - documentType: data.documentType, - expiryDate: data.expiryDate, - status, - daysUntilExpiry - } - - setComplianceDocs(current => [...(current || []), newDoc]) - - if (status === 'expiring' || status === 'expired') { - addNotification({ - type: 'compliance', - priority: status === 'expired' ? 'urgent' : 'high', - title: status === 'expired' ? 'Document Expired' : 'Document Expiring Soon', - message: `${data.documentType} for ${data.workerName} ${status === 'expired' ? 'has expired' : `expires in ${daysUntilExpiry} days`}`, - relatedId: newDoc.id - }) - } - - toast.success('Document uploaded successfully') - } - - const handleCreateExpense = (data: { - workerName: string - clientName: string - date: string - category: string - description: string - amount: number - billable: boolean - }) => { - const newExpense: Expense = { - id: `EXP-${Date.now()}`, - workerId: `W-${Date.now()}`, - workerName: data.workerName, - clientName: data.clientName, - date: data.date, - category: data.category, - description: data.description, - amount: data.amount, - currency: 'GBP', - status: 'pending', - submittedDate: new Date().toISOString(), - billable: data.billable - } - - setExpenses(current => [...(current || []), newExpense]) - addNotification({ - type: 'expense', - priority: 'low', - title: 'New Expense Submitted', - message: `${data.workerName} submitted a £${data.amount.toFixed(2)} expense`, - relatedId: newExpense.id - }) - toast.success('Expense created successfully') - } - - const handleApproveExpense = (id: string) => { - setExpenses(current => { - if (!current) return [] - return current.map(e => - e.id === id - ? { ...e, status: 'approved' as ExpenseStatus, approvedDate: new Date().toISOString() } - : e - ) - }) - toast.success('Expense approved') - } - - const handleRejectExpense = (id: string) => { - setExpenses(current => { - if (!current) return [] - return current.map(e => - e.id === id - ? { ...e, status: 'rejected' as ExpenseStatus } - : e - ) - }) - toast.error('Expense rejected') - } - - const handleCreatePlacementInvoice = (invoice: Invoice) => { - setInvoices(current => [...(current || []), invoice]) - } - - const handleCreateCreditNote = (creditNote: any, creditInvoice: Invoice) => { - setInvoices(current => [...(current || []), creditInvoice]) - } + const actions = useAppActions( + timesheets, + setTimesheets, + invoices, + setInvoices, + setComplianceDocs, + setExpenses, + addNotification + ) return (
@@ -404,172 +69,22 @@ function App() {
- {currentView === 'dashboard' && ( - - )} - - {currentView === 'timesheets' && ( - - )} - - {currentView === 'billing' && ( - - )} - - {currentView === 'payroll' && ( - { - setPayrollRuns((current) => [...(current || []), run]) - }} - /> - )} - - {currentView === 'expenses' && ( - - )} - - {currentView === 'compliance' && ( - - )} - - {currentView === 'reports' && ( - - )} - - {currentView === 'missing-timesheets' && ( - - )} - - {currentView === 'currency' && ( - - )} - - {currentView === 'qr-scanner' && ( - { - const newTimesheet: Timesheet = { - ...timesheet, - id: `TS-${Date.now()}`, - status: 'pending', - submittedDate: new Date().toISOString() - } - setTimesheets(current => [...(current || []), newTimesheet]) - }} - /> - )} - - {currentView === 'email-templates' && ( - - )} - - {currentView === 'invoice-templates' && ( - - )} - - {currentView === 'purchase-orders' && ( - - )} - - {currentView === 'onboarding' && ( - - )} - - {currentView === 'audit-trail' && ( - - )} - - {currentView === 'notification-rules' && ( - - )} - - {currentView === 'batch-import' && ( - { - toast.success(`Imported ${data.length} records`) - }} - /> - )} - - {currentView === 'rate-templates' && ( - - )} - - {currentView === 'custom-reports' && ( - - )} - - {currentView === 'holiday-pay' && ( - - )} - - {currentView === 'contract-validation' && ( - - )} - - {currentView === 'shift-patterns' && ( - - )} - - {currentView === 'query-guide' && ( - - )} - - {currentView === 'roadmap' && ( - - )} - - {currentView === 'component-showcase' && ( - - )} - - {currentView === 'business-logic-demo' && ( - - )} +
diff --git a/src/components/ViewRouter.tsx b/src/components/ViewRouter.tsx new file mode 100644 index 0000000..1e052b8 --- /dev/null +++ b/src/components/ViewRouter.tsx @@ -0,0 +1,242 @@ +import type { View } from '@/App' +import type { + Timesheet, + Invoice, + PayrollRun, + Worker, + ComplianceDocument, + Expense, + RateCard, + DashboardMetrics +} from '@/lib/types' +import { + DashboardView, + TimesheetsView, + BillingView, + PayrollView, + ComplianceView, + ExpensesView +} from '@/components/views' +import { ReportsView } from '@/components/ReportsView' +import { CurrencyManagement } from '@/components/CurrencyManagement' +import { EmailTemplateManager } from '@/components/EmailTemplateManager' +import { InvoiceTemplateManager } from '@/components/InvoiceTemplateManager' +import { QRTimesheetScanner } from '@/components/QRTimesheetScanner' +import { MissingTimesheetsReport } from '@/components/MissingTimesheetsReport' +import { PurchaseOrderManager } from '@/components/PurchaseOrderManager' +import { OnboardingWorkflowManager } from '@/components/OnboardingWorkflowManager' +import { AuditTrailViewer } from '@/components/AuditTrailViewer' +import { NotificationRulesManager } from '@/components/NotificationRulesManager' +import { BatchImportManager } from '@/components/BatchImportManager' +import { RateTemplateManager } from '@/components/RateTemplateManager' +import { CustomReportBuilder } from '@/components/CustomReportBuilder' +import { HolidayPayManager } from '@/components/HolidayPayManager' +import { ContractValidator } from '@/components/ContractValidator' +import { ShiftPatternManager } from '@/components/ShiftPatternManager' +import { QueryLanguageGuide } from '@/components/QueryLanguageGuide' +import { RoadmapView } from '@/components/roadmap-view' +import { ComponentShowcase } from '@/components/ComponentShowcase' +import { BusinessLogicDemo } from '@/components/BusinessLogicDemo' +import { toast } from 'sonner' + +interface ViewRouterProps { + currentView: View + searchQuery: string + setSearchQuery: (query: string) => void + metrics: DashboardMetrics + timesheets: Timesheet[] + invoices: Invoice[] + payrollRuns: PayrollRun[] + workers: Worker[] + complianceDocs: ComplianceDocument[] + expenses: Expense[] + rateCards: RateCard[] + setTimesheets: (updater: (current: Timesheet[]) => Timesheet[]) => void + setPayrollRuns: (updater: (current: PayrollRun[]) => PayrollRun[]) => void + actions: any +} + +export function ViewRouter({ + currentView, + searchQuery, + setSearchQuery, + metrics, + timesheets, + invoices, + payrollRuns, + workers, + complianceDocs, + expenses, + rateCards, + setTimesheets, + setPayrollRuns, + actions +}: ViewRouterProps) { + switch (currentView) { + case 'dashboard': + return + + case 'timesheets': + return ( + + ) + + case 'billing': + return ( + + ) + + case 'payroll': + return ( + { + setPayrollRuns((current) => [...current, run]) + }} + /> + ) + + case 'expenses': + return ( + + ) + + case 'compliance': + return ( + + ) + + case 'reports': + return ( + + ) + + case 'missing-timesheets': + return ( + + ) + + case 'currency': + return + + case 'qr-scanner': + return ( + { + const newTimesheet: Timesheet = { + ...timesheet, + id: `TS-${Date.now()}`, + status: 'pending', + submittedDate: new Date().toISOString() + } + setTimesheets(current => [...current, newTimesheet]) + }} + /> + ) + + case 'email-templates': + return + + case 'invoice-templates': + return + + case 'purchase-orders': + return + + case 'onboarding': + return + + case 'audit-trail': + return + + case 'notification-rules': + return + + case 'batch-import': + return ( + { + toast.success(`Imported ${data.length} records`) + }} + /> + ) + + case 'rate-templates': + return + + case 'custom-reports': + return ( + + ) + + case 'holiday-pay': + return + + case 'contract-validation': + return ( + + ) + + case 'shift-patterns': + return + + case 'query-guide': + return + + case 'roadmap': + return + + case 'component-showcase': + return + + case 'business-logic-demo': + return + + default: + return + } +} diff --git a/src/hooks/use-app-actions.ts b/src/hooks/use-app-actions.ts new file mode 100644 index 0000000..a4b26c9 --- /dev/null +++ b/src/hooks/use-app-actions.ts @@ -0,0 +1,329 @@ +import { toast } from 'sonner' +import type { + Timesheet, + Invoice, + ComplianceDocument, + ComplianceStatus, + Expense, + ExpenseStatus, + InvoiceStatus, + ShiftEntry +} from '@/lib/types' + +export function useAppActions( + timesheets: Timesheet[], + setTimesheets: (updater: (current: Timesheet[]) => Timesheet[]) => void, + invoices: Invoice[], + setInvoices: (updater: (current: Invoice[]) => Invoice[]) => void, + setComplianceDocs: (updater: (current: ComplianceDocument[]) => ComplianceDocument[]) => void, + setExpenses: (updater: (current: Expense[]) => Expense[]) => void, + addNotification: (notification: any) => void +) { + const handleApproveTimesheet = (id: string) => { + setTimesheets(current => + current.map(t => + t.id === id + ? { ...t, status: 'approved', approvedDate: new Date().toISOString() } + : t + ) + ) + const timesheet = timesheets.find(t => t.id === id) + if (timesheet) { + addNotification({ + type: 'timesheet', + priority: 'medium', + title: 'Timesheet Approved', + message: `${timesheet.workerName}'s timesheet for ${new Date(timesheet.weekEnding).toLocaleDateString()} has been approved`, + relatedId: id + }) + } + toast.success('Timesheet approved successfully') + } + + const handleRejectTimesheet = (id: string) => { + setTimesheets(current => + current.map(t => + t.id === id + ? { ...t, status: 'rejected' } + : t + ) + ) + const timesheet = timesheets.find(t => t.id === id) + if (timesheet) { + addNotification({ + type: 'timesheet', + priority: 'medium', + title: 'Timesheet Rejected', + message: `${timesheet.workerName}'s timesheet for ${new Date(timesheet.weekEnding).toLocaleDateString()} has been rejected`, + relatedId: id + }) + } + toast.error('Timesheet rejected') + } + + const handleAdjustTimesheet = (timesheetId: string, adjustment: any) => { + setTimesheets(current => + current.map(t => { + if (t.id !== timesheetId) return t + + const newAdjustment = { + id: `ADJ-${Date.now()}`, + adjustmentDate: new Date().toISOString(), + ...adjustment + } + + return { + ...t, + hours: adjustment.newHours, + rate: adjustment.newRate, + amount: adjustment.newHours * adjustment.newRate, + adjustments: [...(t.adjustments || []), newAdjustment] + } + }) + ) + } + + const handleCreateInvoice = (timesheetId: string) => { + const timesheet = timesheets.find(t => t.id === timesheetId) + if (!timesheet) return + + const newInvoice: Invoice = { + id: `INV-${Date.now()}`, + invoiceNumber: `INV-${String(invoices.length + 1).padStart(5, '0')}`, + clientName: timesheet.clientName, + issueDate: new Date().toISOString().split('T')[0], + dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], + amount: timesheet.amount, + status: 'draft', + currency: 'GBP' + } + + setInvoices(current => [...current, newInvoice]) + toast.success(`Invoice ${newInvoice.invoiceNumber} created`) + } + + const handleCreateTimesheet = (data: { + workerName: string + clientName: string + hours: number + rate: number + weekEnding: string + }) => { + const newTimesheet: Timesheet = { + id: `TS-${Date.now()}`, + workerId: `W-${Date.now()}`, + workerName: data.workerName, + clientName: data.clientName, + weekEnding: data.weekEnding, + hours: data.hours, + status: 'pending', + submittedDate: new Date().toISOString(), + amount: data.hours * data.rate + } + + setTimesheets(current => [...current, newTimesheet]) + toast.success('Timesheet created successfully') + } + + const handleCreateDetailedTimesheet = (data: { + workerName: string + clientName: string + weekEnding: string + shifts: ShiftEntry[] + totalHours: number + totalAmount: number + baseRate: number + }) => { + const newTimesheet: Timesheet = { + id: `TS-${Date.now()}`, + workerId: `W-${Date.now()}`, + workerName: data.workerName, + clientName: data.clientName, + weekEnding: data.weekEnding, + hours: data.totalHours, + status: 'pending', + submittedDate: new Date().toISOString(), + amount: data.totalAmount, + rate: data.baseRate, + shifts: data.shifts + } + + setTimesheets(current => [...current, newTimesheet]) + toast.success(`Detailed timesheet created with ${data.shifts.length} shifts`) + } + + const handleBulkImport = (csvData: string) => { + const lines = csvData.trim().split('\n') + if (lines.length < 2) { + toast.error('Invalid CSV format') + return + } + + const headers = lines[0].split(',').map(h => h.trim()) + const newTimesheets: Timesheet[] = [] + + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(',').map(v => v.trim()) + if (values.length !== headers.length) continue + + const workerName = values[headers.indexOf('workerName')] || values[0] + const clientName = values[headers.indexOf('clientName')] || values[1] + const hours = parseFloat(values[headers.indexOf('hours')] || values[2] || '0') + const rate = parseFloat(values[headers.indexOf('rate')] || values[3] || '0') + const weekEnding = values[headers.indexOf('weekEnding')] || values[4] + + if (workerName && clientName && hours > 0 && rate > 0) { + newTimesheets.push({ + id: `TS-${Date.now()}-${i}`, + workerId: `W-${Date.now()}-${i}`, + workerName, + clientName, + weekEnding, + hours, + status: 'pending', + submittedDate: new Date().toISOString(), + amount: hours * rate + }) + } + } + + if (newTimesheets.length > 0) { + setTimesheets(current => [...current, ...newTimesheets]) + toast.success(`Imported ${newTimesheets.length} timesheets`) + } else { + toast.error('No valid timesheets found in CSV') + } + } + + const handleSendInvoice = (invoiceId: string) => { + setInvoices(current => + current.map(inv => + inv.id === invoiceId + ? { ...inv, status: 'sent' as InvoiceStatus } + : inv + ) + ) + toast.success('Invoice sent to client via email') + } + + const handleUploadDocument = (data: { + workerId: string + workerName: string + documentType: string + expiryDate: string + }) => { + const expiryDateObj = new Date(data.expiryDate) + const now = new Date() + const daysUntilExpiry = Math.floor((expiryDateObj.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) + + let status: ComplianceStatus = 'valid' + if (daysUntilExpiry < 0) status = 'expired' + else if (daysUntilExpiry < 30) status = 'expiring' + + const newDoc: ComplianceDocument = { + id: `DOC-${Date.now()}`, + workerId: data.workerId, + workerName: data.workerName, + documentType: data.documentType, + expiryDate: data.expiryDate, + status, + daysUntilExpiry + } + + setComplianceDocs(current => [...current, newDoc]) + + if (status === 'expiring' || status === 'expired') { + addNotification({ + type: 'compliance', + priority: status === 'expired' ? 'urgent' : 'high', + title: status === 'expired' ? 'Document Expired' : 'Document Expiring Soon', + message: `${data.documentType} for ${data.workerName} ${status === 'expired' ? 'has expired' : `expires in ${daysUntilExpiry} days`}`, + relatedId: newDoc.id + }) + } + + toast.success('Document uploaded successfully') + } + + const handleCreateExpense = (data: { + workerName: string + clientName: string + date: string + category: string + description: string + amount: number + billable: boolean + }) => { + const newExpense: Expense = { + id: `EXP-${Date.now()}`, + workerId: `W-${Date.now()}`, + workerName: data.workerName, + clientName: data.clientName, + date: data.date, + category: data.category, + description: data.description, + amount: data.amount, + currency: 'GBP', + status: 'pending', + submittedDate: new Date().toISOString(), + billable: data.billable + } + + setExpenses(current => [...current, newExpense]) + addNotification({ + type: 'expense', + priority: 'low', + title: 'New Expense Submitted', + message: `${data.workerName} submitted a £${data.amount.toFixed(2)} expense`, + relatedId: newExpense.id + }) + toast.success('Expense created successfully') + } + + const handleApproveExpense = (id: string) => { + setExpenses(current => + current.map(e => + e.id === id + ? { ...e, status: 'approved' as ExpenseStatus, approvedDate: new Date().toISOString() } + : e + ) + ) + toast.success('Expense approved') + } + + const handleRejectExpense = (id: string) => { + setExpenses(current => + current.map(e => + e.id === id + ? { ...e, status: 'rejected' as ExpenseStatus } + : e + ) + ) + toast.error('Expense rejected') + } + + const handleCreatePlacementInvoice = (invoice: Invoice) => { + setInvoices(current => [...current, invoice]) + } + + const handleCreateCreditNote = (creditNote: any, creditInvoice: Invoice) => { + setInvoices(current => [...current, creditInvoice]) + } + + return { + handleApproveTimesheet, + handleRejectTimesheet, + handleAdjustTimesheet, + handleCreateInvoice, + handleCreateTimesheet, + handleCreateDetailedTimesheet, + handleBulkImport, + handleSendInvoice, + handleUploadDocument, + handleCreateExpense, + handleApproveExpense, + handleRejectExpense, + handleCreatePlacementInvoice, + handleCreateCreditNote + } +} diff --git a/src/hooks/use-app-data.ts b/src/hooks/use-app-data.ts new file mode 100644 index 0000000..3299ca6 --- /dev/null +++ b/src/hooks/use-app-data.ts @@ -0,0 +1,55 @@ +import { useKV } from '@github/spark/hooks' +import type { + Timesheet, + Invoice, + PayrollRun, + Worker, + ComplianceDocument, + Expense, + RateCard, + DashboardMetrics +} from '@/lib/types' + +export function useAppData() { + const [timesheets = [], setTimesheets] = useKV('timesheets', []) + const [invoices = [], setInvoices] = useKV('invoices', []) + const [payrollRuns = [], setPayrollRuns] = useKV('payroll-runs', []) + const [workers = [], setWorkers] = useKV('workers', []) + const [complianceDocs = [], setComplianceDocs] = useKV('compliance-docs', []) + const [expenses = [], setExpenses] = useKV('expenses', []) + const [rateCards = [], setRateCards] = useKV('rate-cards', []) + + const metrics: DashboardMetrics = { + pendingTimesheets: timesheets.filter(t => t.status === 'pending').length, + pendingApprovals: timesheets.filter(t => t.status === 'pending').length, + overdueInvoices: invoices.filter(i => i.status === 'overdue').length, + complianceAlerts: complianceDocs.filter(d => d.status === 'expiring' || d.status === 'expired').length, + monthlyRevenue: invoices.reduce((sum, inv) => sum + inv.amount, 0), + monthlyPayroll: payrollRuns.reduce((sum, pr) => sum + pr.totalAmount, 0), + grossMargin: 0, + activeWorkers: workers.filter(w => w.status === 'active').length, + pendingExpenses: expenses.filter(e => e.status === 'pending').length + } + + metrics.grossMargin = metrics.monthlyRevenue > 0 + ? ((metrics.monthlyRevenue - metrics.monthlyPayroll) / metrics.monthlyRevenue) * 100 + : 0 + + return { + timesheets, + setTimesheets, + invoices, + setInvoices, + payrollRuns, + setPayrollRuns, + workers, + setWorkers, + complianceDocs, + setComplianceDocs, + expenses, + setExpenses, + rateCards, + setRateCards, + metrics + } +}