mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Make app.tsx minimal
This commit is contained in:
575
src/App.tsx
575
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<View>('dashboard')
|
||||
const [currentEntity, setCurrentEntity] = useState('Main Agency')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const { notifications, addNotification, markAsRead, markAllAsRead, deleteNotification, unreadCount } = useNotifications()
|
||||
|
||||
const [timesheets = [], setTimesheets] = useKV<Timesheet[]>('timesheets', [])
|
||||
const [invoices = [], setInvoices] = useKV<Invoice[]>('invoices', [])
|
||||
const [payrollRuns = [], setPayrollRuns] = useKV<PayrollRun[]>('payroll-runs', [])
|
||||
const [workers = [], setWorkers] = useKV<Worker[]>('workers', [])
|
||||
const [complianceDocs = [], setComplianceDocs] = useKV<ComplianceDocument[]>('compliance-docs', [])
|
||||
const [expenses = [], setExpenses] = useKV<Expense[]>('expenses', [])
|
||||
const [rateCards = [], setRateCards] = useKV<RateCard[]>('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 (
|
||||
<div className="flex h-screen bg-background overflow-hidden">
|
||||
@@ -404,172 +69,22 @@ function App() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{currentView === 'dashboard' && (
|
||||
<DashboardView metrics={metrics} />
|
||||
)}
|
||||
|
||||
{currentView === 'timesheets' && (
|
||||
<TimesheetsView
|
||||
timesheets={timesheets}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onApprove={handleApproveTimesheet}
|
||||
onReject={handleRejectTimesheet}
|
||||
onCreateInvoice={handleCreateInvoice}
|
||||
onCreateTimesheet={handleCreateTimesheet}
|
||||
onCreateDetailedTimesheet={handleCreateDetailedTimesheet}
|
||||
onBulkImport={handleBulkImport}
|
||||
onAdjust={handleAdjustTimesheet}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'billing' && (
|
||||
<BillingView
|
||||
invoices={invoices}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onSendInvoice={handleSendInvoice}
|
||||
onCreatePlacementInvoice={handleCreatePlacementInvoice}
|
||||
onCreateCreditNote={handleCreateCreditNote}
|
||||
rateCards={rateCards}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'payroll' && (
|
||||
<PayrollView
|
||||
payrollRuns={payrollRuns}
|
||||
timesheets={timesheets}
|
||||
onPayrollComplete={(run) => {
|
||||
setPayrollRuns((current) => [...(current || []), run])
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'expenses' && (
|
||||
<ExpensesView
|
||||
expenses={expenses}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onCreateExpense={handleCreateExpense}
|
||||
onApprove={handleApproveExpense}
|
||||
onReject={handleRejectExpense}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'compliance' && (
|
||||
<ComplianceView
|
||||
complianceDocs={complianceDocs}
|
||||
onUploadDocument={handleUploadDocument}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'reports' && (
|
||||
<ReportsView
|
||||
invoices={invoices}
|
||||
payrollRuns={payrollRuns}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'missing-timesheets' && (
|
||||
<MissingTimesheetsReport
|
||||
workers={workers}
|
||||
timesheets={timesheets}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'currency' && (
|
||||
<CurrencyManagement />
|
||||
)}
|
||||
|
||||
{currentView === 'qr-scanner' && (
|
||||
<QRTimesheetScanner
|
||||
onTimesheetScanned={(timesheet) => {
|
||||
const newTimesheet: Timesheet = {
|
||||
...timesheet,
|
||||
id: `TS-${Date.now()}`,
|
||||
status: 'pending',
|
||||
submittedDate: new Date().toISOString()
|
||||
}
|
||||
setTimesheets(current => [...(current || []), newTimesheet])
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'email-templates' && (
|
||||
<EmailTemplateManager />
|
||||
)}
|
||||
|
||||
{currentView === 'invoice-templates' && (
|
||||
<InvoiceTemplateManager />
|
||||
)}
|
||||
|
||||
{currentView === 'purchase-orders' && (
|
||||
<PurchaseOrderManager />
|
||||
)}
|
||||
|
||||
{currentView === 'onboarding' && (
|
||||
<OnboardingWorkflowManager />
|
||||
)}
|
||||
|
||||
{currentView === 'audit-trail' && (
|
||||
<AuditTrailViewer />
|
||||
)}
|
||||
|
||||
{currentView === 'notification-rules' && (
|
||||
<NotificationRulesManager />
|
||||
)}
|
||||
|
||||
{currentView === 'batch-import' && (
|
||||
<BatchImportManager
|
||||
onImportComplete={(data) => {
|
||||
toast.success(`Imported ${data.length} records`)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'rate-templates' && (
|
||||
<RateTemplateManager />
|
||||
)}
|
||||
|
||||
{currentView === 'custom-reports' && (
|
||||
<CustomReportBuilder
|
||||
timesheets={timesheets}
|
||||
invoices={invoices}
|
||||
payrollRuns={payrollRuns}
|
||||
expenses={expenses}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'holiday-pay' && (
|
||||
<HolidayPayManager />
|
||||
)}
|
||||
|
||||
{currentView === 'contract-validation' && (
|
||||
<ContractValidator
|
||||
timesheets={timesheets}
|
||||
rateCards={rateCards}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentView === 'shift-patterns' && (
|
||||
<ShiftPatternManager />
|
||||
)}
|
||||
|
||||
{currentView === 'query-guide' && (
|
||||
<QueryLanguageGuide />
|
||||
)}
|
||||
|
||||
{currentView === 'roadmap' && (
|
||||
<RoadmapView />
|
||||
)}
|
||||
|
||||
{currentView === 'component-showcase' && (
|
||||
<ComponentShowcase />
|
||||
)}
|
||||
|
||||
{currentView === 'business-logic-demo' && (
|
||||
<BusinessLogicDemo />
|
||||
)}
|
||||
<ViewRouter
|
||||
currentView={currentView}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
metrics={metrics}
|
||||
timesheets={timesheets}
|
||||
invoices={invoices}
|
||||
payrollRuns={payrollRuns}
|
||||
workers={workers}
|
||||
complianceDocs={complianceDocs}
|
||||
expenses={expenses}
|
||||
rateCards={rateCards}
|
||||
setTimesheets={setTimesheets}
|
||||
setPayrollRuns={setPayrollRuns}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
242
src/components/ViewRouter.tsx
Normal file
242
src/components/ViewRouter.tsx
Normal file
@@ -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 <DashboardView metrics={metrics} />
|
||||
|
||||
case 'timesheets':
|
||||
return (
|
||||
<TimesheetsView
|
||||
timesheets={timesheets}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onApprove={actions.handleApproveTimesheet}
|
||||
onReject={actions.handleRejectTimesheet}
|
||||
onCreateInvoice={actions.handleCreateInvoice}
|
||||
onCreateTimesheet={actions.handleCreateTimesheet}
|
||||
onCreateDetailedTimesheet={actions.handleCreateDetailedTimesheet}
|
||||
onBulkImport={actions.handleBulkImport}
|
||||
onAdjust={actions.handleAdjustTimesheet}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'billing':
|
||||
return (
|
||||
<BillingView
|
||||
invoices={invoices}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onSendInvoice={actions.handleSendInvoice}
|
||||
onCreatePlacementInvoice={actions.handleCreatePlacementInvoice}
|
||||
onCreateCreditNote={actions.handleCreateCreditNote}
|
||||
rateCards={rateCards}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'payroll':
|
||||
return (
|
||||
<PayrollView
|
||||
payrollRuns={payrollRuns}
|
||||
timesheets={timesheets}
|
||||
onPayrollComplete={(run) => {
|
||||
setPayrollRuns((current) => [...current, run])
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'expenses':
|
||||
return (
|
||||
<ExpensesView
|
||||
expenses={expenses}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onCreateExpense={actions.handleCreateExpense}
|
||||
onApprove={actions.handleApproveExpense}
|
||||
onReject={actions.handleRejectExpense}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'compliance':
|
||||
return (
|
||||
<ComplianceView
|
||||
complianceDocs={complianceDocs}
|
||||
onUploadDocument={actions.handleUploadDocument}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'reports':
|
||||
return (
|
||||
<ReportsView
|
||||
invoices={invoices}
|
||||
payrollRuns={payrollRuns}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'missing-timesheets':
|
||||
return (
|
||||
<MissingTimesheetsReport
|
||||
workers={workers}
|
||||
timesheets={timesheets}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'currency':
|
||||
return <CurrencyManagement />
|
||||
|
||||
case 'qr-scanner':
|
||||
return (
|
||||
<QRTimesheetScanner
|
||||
onTimesheetScanned={(timesheet) => {
|
||||
const newTimesheet: Timesheet = {
|
||||
...timesheet,
|
||||
id: `TS-${Date.now()}`,
|
||||
status: 'pending',
|
||||
submittedDate: new Date().toISOString()
|
||||
}
|
||||
setTimesheets(current => [...current, newTimesheet])
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'email-templates':
|
||||
return <EmailTemplateManager />
|
||||
|
||||
case 'invoice-templates':
|
||||
return <InvoiceTemplateManager />
|
||||
|
||||
case 'purchase-orders':
|
||||
return <PurchaseOrderManager />
|
||||
|
||||
case 'onboarding':
|
||||
return <OnboardingWorkflowManager />
|
||||
|
||||
case 'audit-trail':
|
||||
return <AuditTrailViewer />
|
||||
|
||||
case 'notification-rules':
|
||||
return <NotificationRulesManager />
|
||||
|
||||
case 'batch-import':
|
||||
return (
|
||||
<BatchImportManager
|
||||
onImportComplete={(data) => {
|
||||
toast.success(`Imported ${data.length} records`)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'rate-templates':
|
||||
return <RateTemplateManager />
|
||||
|
||||
case 'custom-reports':
|
||||
return (
|
||||
<CustomReportBuilder
|
||||
timesheets={timesheets}
|
||||
invoices={invoices}
|
||||
payrollRuns={payrollRuns}
|
||||
expenses={expenses}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'holiday-pay':
|
||||
return <HolidayPayManager />
|
||||
|
||||
case 'contract-validation':
|
||||
return (
|
||||
<ContractValidator
|
||||
timesheets={timesheets}
|
||||
rateCards={rateCards}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'shift-patterns':
|
||||
return <ShiftPatternManager />
|
||||
|
||||
case 'query-guide':
|
||||
return <QueryLanguageGuide />
|
||||
|
||||
case 'roadmap':
|
||||
return <RoadmapView />
|
||||
|
||||
case 'component-showcase':
|
||||
return <ComponentShowcase />
|
||||
|
||||
case 'business-logic-demo':
|
||||
return <BusinessLogicDemo />
|
||||
|
||||
default:
|
||||
return <DashboardView metrics={metrics} />
|
||||
}
|
||||
}
|
||||
329
src/hooks/use-app-actions.ts
Normal file
329
src/hooks/use-app-actions.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
55
src/hooks/use-app-data.ts
Normal file
55
src/hooks/use-app-data.ts
Normal file
@@ -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<Timesheet[]>('timesheets', [])
|
||||
const [invoices = [], setInvoices] = useKV<Invoice[]>('invoices', [])
|
||||
const [payrollRuns = [], setPayrollRuns] = useKV<PayrollRun[]>('payroll-runs', [])
|
||||
const [workers = [], setWorkers] = useKV<Worker[]>('workers', [])
|
||||
const [complianceDocs = [], setComplianceDocs] = useKV<ComplianceDocument[]>('compliance-docs', [])
|
||||
const [expenses = [], setExpenses] = useKV<Expense[]>('expenses', [])
|
||||
const [rateCards = [], setRateCards] = useKV<RateCard[]>('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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user