diff --git a/JSON_DATA_STRUCTURE.md b/JSON_DATA_STRUCTURE.md
new file mode 100644
index 0000000..6656e4c
--- /dev/null
+++ b/JSON_DATA_STRUCTURE.md
@@ -0,0 +1,354 @@
+# JSON Data Structure
+
+All UI data is now loaded from JSON files, making the application data-driven and easily configurable without code changes.
+
+## Data Source
+
+**Primary Data File**: `/src/data/app-data.json`
+
+This file contains all sample data for the application, including:
+- Timesheets
+- Invoices
+- Payroll Runs
+- Workers
+- Compliance Documents
+- Expenses
+- Rate Cards
+- Clients
+
+## How It Works
+
+### 1. Data Loading Flow
+
+```
+app-data.json → use-sample-data hook → KV Storage → use-app-data hook → Components
+```
+
+1. **app-data.json** - Static JSON file with initial/seed data
+2. **use-sample-data** - Loads JSON data into KV storage on first run
+3. **use-app-data** - Reads from KV storage and provides reactive state
+4. **Components** - Use the reactive data from use-app-data
+
+### 2. Initial Data Load
+
+When the application first loads (or when KV storage is empty), the `useSampleData` hook:
+- Checks if data has been initialized (`sample-data-initialized` flag)
+- If not initialized, loads data from `app-data.json`
+- Populates KV storage with all data entities
+- Sets the initialization flag to prevent re-loading
+
+### 3. Persistent Storage
+
+All data is stored in KV storage, which persists between sessions. This means:
+- User modifications are preserved
+- Data survives page refreshes
+- Each user has their own data instance
+
+## Data Schema
+
+### Timesheets
+
+```typescript
+{
+ id: string // Unique timesheet ID
+ workerId: string // Reference to worker
+ workerName: string // Worker's name
+ clientName: string // Client company name
+ weekEnding: string // ISO date string
+ totalHours: number // Total hours worked
+ regularHours: number // Standard hours
+ overtimeHours: number // Overtime hours
+ status: string // "pending" | "approved" | "disputed"
+ rate: number // Hourly rate
+ total: number // Total amount
+ submittedDate: string // ISO date-time string
+ approvedDate?: string // Optional approval date
+ shifts: Shift[] // Array of shift details
+}
+
+Shift: {
+ date: string // ISO date string
+ start: string // Time "HH:MM"
+ end: string // Time "HH:MM"
+ hours: number // Hours worked
+ type: string // "regular" | "night" | "weekend" | "overtime"
+}
+```
+
+### Invoices
+
+```typescript
+{
+ id: string // Unique invoice ID
+ clientName: string // Client company name
+ amount: number // Net amount
+ vat: number // VAT/Tax amount
+ total: number // Gross total
+ status: string // "paid" | "pending" | "overdue" | "draft"
+ dueDate: string // ISO date string
+ invoiceDate: string // ISO date string
+ paidDate?: string // Optional payment date
+ items: InvoiceItem[] // Line items
+}
+
+InvoiceItem: {
+ description: string // Item description
+ quantity: number // Quantity
+ rate: number // Unit rate
+ amount: number // Total amount
+}
+```
+
+### Payroll Runs
+
+```typescript
+{
+ id: string // Unique payroll run ID
+ periodEnding: string // ISO date string
+ status: string // "completed" | "pending" | "processing"
+ totalGross: number // Total gross pay
+ totalNet: number // Total net pay
+ totalTax: number // Total tax withheld
+ totalNI: number // Total National Insurance
+ processedDate?: string // Optional processing date
+ paymentDate?: string // Optional payment date
+ workerCount: number // Number of workers
+ entries: PayrollEntry[] // Individual worker entries
+}
+
+PayrollEntry: {
+ workerId: string // Reference to worker
+ workerName: string // Worker's name
+ gross: number // Gross pay
+ net: number // Net pay
+ tax: number // Tax withheld
+ ni: number // National Insurance
+}
+```
+
+### Workers
+
+```typescript
+{
+ id: string // Unique worker ID
+ name: string // Full name
+ email: string // Email address
+ phone: string // Phone number
+ status: string // "active" | "inactive" | "onboarding"
+ role: string // Job role/title
+ startDate: string // ISO date string
+ paymentType: string // "Limited Company" | "PAYE" | "CIS"
+ currentClient: string // Current client assignment
+}
+```
+
+### Compliance Documents
+
+```typescript
+{
+ id: string // Unique document ID
+ workerId: string // Reference to worker
+ workerName: string // Worker's name
+ documentType: string // Document type name
+ status: string // "valid" | "expiring" | "expired"
+ expiryDate: string // ISO date string
+ uploadDate: string // ISO date string
+ verifiedDate: string // ISO date string
+}
+```
+
+### Expenses
+
+```typescript
+{
+ id: string // Unique expense ID
+ workerId: string // Reference to worker
+ workerName: string // Worker's name
+ category: string // "Travel" | "Accommodation" | "Meals" | "Equipment"
+ amount: number // Expense amount
+ date: string // ISO date string
+ status: string // "approved" | "pending" | "rejected"
+ description: string // Expense description
+ receiptAttached: boolean// Receipt attachment status
+ approvedDate?: string // Optional approval date
+ rejectedDate?: string // Optional rejection date
+ rejectionReason?: string// Optional rejection reason
+}
+```
+
+### Rate Cards
+
+```typescript
+{
+ id: string // Unique rate card ID
+ role: string // Job role/title
+ clientName: string // Client company name
+ payRate: number // Hourly pay rate
+ chargeRate: number // Hourly charge rate
+ margin: number // Margin amount
+ marginPercent: number // Margin percentage
+ currency: string // Currency code (e.g., "GBP")
+ validFrom: string // ISO date string
+ validUntil: string // ISO date string
+ overtimeMultiplier: number // Overtime rate multiplier
+ weekendMultiplier: number // Weekend rate multiplier
+ nightMultiplier?: number // Optional night shift multiplier
+}
+```
+
+### Clients
+
+```typescript
+{
+ id: string // Unique client ID
+ name: string // Company name
+ industry: string // Industry sector
+ status: string // "active" | "inactive" | "suspended"
+ creditLimit: number // Credit limit
+ outstandingBalance: number // Current outstanding balance
+ paymentTerms: number // Payment terms in days
+ activeWorkers: number // Number of active workers
+ address: string // Full address
+}
+```
+
+## Hooks
+
+### useJsonData()
+
+Direct access to JSON data (read-only).
+
+```typescript
+import { useJsonData } from '@/hooks'
+
+function MyComponent() {
+ const { data, isLoading, error } = useJsonData()
+
+ if (isLoading) return
Loading...
+ if (error) return Error: {error.message}
+
+ return {data.workers.length} workers
+}
+```
+
+### useSampleData()
+
+Initializes KV storage with JSON data (runs automatically on app start).
+
+```typescript
+import { useSampleData } from '@/hooks'
+
+function App() {
+ useSampleData() // Call once at app root
+ // ...
+}
+```
+
+### useAppData()
+
+Reactive access to data with setters (recommended for components).
+
+```typescript
+import { useAppData } from '@/hooks'
+
+function MyComponent() {
+ const { timesheets, setTimesheets, workers, metrics } = useAppData()
+
+ // Use data
+ console.log(timesheets.length)
+
+ // Update data
+ setTimesheets(prev => [...prev, newTimesheet])
+}
+```
+
+## Modifying Data
+
+### Option 1: Edit JSON File (Recommended for Bulk Changes)
+
+1. Edit `/src/data/app-data.json`
+2. Clear KV storage (or set `sample-data-initialized` to `false`)
+3. Refresh the application
+
+### Option 2: Programmatic Updates (Recommended for User Actions)
+
+```typescript
+const { timesheets, setTimesheets } = useAppData()
+
+// Add new timesheet
+setTimesheets(current => [...current, newTimesheet])
+
+// Update timesheet
+setTimesheets(current =>
+ current.map(ts =>
+ ts.id === id ? { ...ts, status: 'approved' } : ts
+ )
+)
+
+// Delete timesheet
+setTimesheets(current => current.filter(ts => ts.id !== id))
+```
+
+## Adding New Data Entities
+
+To add new data entities:
+
+1. **Add to JSON file** (`/src/data/app-data.json`)
+ ```json
+ {
+ "timesheets": [...],
+ "newEntity": [
+ { "id": "1", "name": "Example" }
+ ]
+ }
+ ```
+
+2. **Add TypeScript interface** (`/src/hooks/use-json-data.ts`)
+ ```typescript
+ export interface NewEntity {
+ id: string
+ name: string
+ }
+
+ export interface AppData {
+ timesheets: Timesheet[]
+ newEntity: NewEntity[]
+ }
+ ```
+
+3. **Initialize in use-sample-data**
+ ```typescript
+ const [, setNewEntity] = useKV('new-entity', [])
+
+ setNewEntity(appData.newEntity)
+ ```
+
+4. **Expose in use-app-data**
+ ```typescript
+ const [newEntity = [], setNewEntity] = useKV('new-entity', [])
+
+ return {
+ newEntity,
+ setNewEntity,
+ // ...
+ }
+ ```
+
+## Benefits
+
+- **Centralized Data**: All sample data in one place
+- **Easy Configuration**: Modify JSON instead of code
+- **Type Safety**: Full TypeScript support
+- **Persistence**: Data survives page refreshes
+- **Reactive**: Automatic UI updates
+- **Testable**: Easy to swap data for testing
+- **Scalable**: Add new entities without code changes
+
+## Future Enhancements
+
+- Support for multiple JSON files (e.g., `timesheets.json`, `invoices.json`)
+- Remote JSON loading from API
+- Data validation on load
+- Migration scripts for schema changes
+- Import/export functionality
+- Data versioning
diff --git a/src/App.tsx b/src/App.tsx
index 7bb8c5a..bbda259 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -8,7 +8,7 @@ import { Sidebar } from '@/components/navigation'
import { NotificationCenter } from '@/components/NotificationCenter'
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'
+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'
function App() {
useSampleData()
diff --git a/src/components/DataManagement.tsx b/src/components/DataManagement.tsx
new file mode 100644
index 0000000..9d8d0a8
--- /dev/null
+++ b/src/components/DataManagement.tsx
@@ -0,0 +1,86 @@
+import { Button } from '@/components/ui/button'
+import { Card } from '@/components/ui/card'
+import { toast } from 'sonner'
+
+export function DataManagement() {
+ const resetAllData = async () => {
+ try {
+ await window.spark.kv.delete('sample-data-initialized')
+ await window.spark.kv.delete('timesheets')
+ await window.spark.kv.delete('invoices')
+ await window.spark.kv.delete('payroll-runs')
+ await window.spark.kv.delete('workers')
+ await window.spark.kv.delete('compliance-docs')
+ await window.spark.kv.delete('expenses')
+ await window.spark.kv.delete('rate-cards')
+ await window.spark.kv.delete('clients')
+
+ toast.success('Data cleared - refresh to reload from JSON')
+ } catch (error) {
+ toast.error('Failed to clear data')
+ }
+ }
+
+ const exportData = async () => {
+ try {
+ const timesheets = await window.spark.kv.get('timesheets')
+ const invoices = await window.spark.kv.get('invoices')
+ const payrollRuns = await window.spark.kv.get('payroll-runs')
+ const workers = await window.spark.kv.get('workers')
+ const complianceDocs = await window.spark.kv.get('compliance-docs')
+ const expenses = await window.spark.kv.get('expenses')
+ const rateCards = await window.spark.kv.get('rate-cards')
+ const clients = await window.spark.kv.get('clients')
+
+ const data = {
+ timesheets,
+ invoices,
+ payrollRuns,
+ workers,
+ complianceDocs,
+ expenses,
+ rateCards,
+ clients
+ }
+
+ const dataStr = JSON.stringify(data, null, 2)
+ const blob = new Blob([dataStr], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `workforce-data-${new Date().toISOString().split('T')[0]}.json`
+ a.click()
+ URL.revokeObjectURL(url)
+
+ toast.success('Data exported successfully')
+ } catch (error) {
+ toast.error('Failed to export data')
+ }
+ }
+
+ return (
+
+
+
Data Management
+
+ Manage application data and reset to defaults
+
+
+
+
+
+ Export Current Data
+
+
+ Reset to Default Data
+
+
+
+
+
• Export: Download current data as JSON file
+
• Reset: Clear all data and reload from app-data.json
+
• After reset, refresh the page to see changes
+
+
+ )
+}
diff --git a/src/components/ViewRouter.tsx b/src/components/ViewRouter.tsx
index a5f50d8..eb6ddd5 100644
--- a/src/components/ViewRouter.tsx
+++ b/src/components/ViewRouter.tsx
@@ -39,6 +39,7 @@ const QueryLanguageGuide = lazy(() => import('@/components/QueryLanguageGuide').
const RoadmapView = lazy(() => import('@/components/roadmap-view').then(m => ({ default: m.RoadmapView })))
const ComponentShowcase = lazy(() => import('@/components/ComponentShowcase').then(m => ({ default: m.ComponentShowcase })))
const BusinessLogicDemo = lazy(() => import('@/components/BusinessLogicDemo').then(m => ({ default: m.BusinessLogicDemo })))
+const DataAdminView = lazy(() => import('@/components/views/data-admin-view').then(m => ({ default: m.DataAdminView })))
interface ViewRouterProps {
currentView: View
@@ -246,6 +247,9 @@ export function ViewRouter({
case 'business-logic-demo':
return
+ case 'data-admin':
+ return
+
default:
return
}
diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx
index 41324b2..8241bb7 100644
--- a/src/components/navigation.tsx
+++ b/src/components/navigation.tsx
@@ -7,7 +7,8 @@ import {
MapTrifold,
Question,
PuzzlePiece,
- Code
+ Code,
+ Database
} from '@phosphor-icons/react'
import { NavItem } from '@/components/nav/NavItem'
import { CoreOperationsNav, ReportsNav, ConfigurationNav, ToolsNav } from '@/components/nav/nav-sections'
@@ -113,6 +114,13 @@ export function Sidebar({ currentView, setCurrentView, currentEntity, setCurrent
onClick={() => setCurrentView('business-logic-demo')}
view="business-logic-demo"
/>
+ }
+ label="Data Administration"
+ active={currentView === 'data-admin'}
+ onClick={() => setCurrentView('data-admin')}
+ view="data-admin"
+ />
}
label="Query Guide"
diff --git a/src/components/views/data-admin-view.tsx b/src/components/views/data-admin-view.tsx
new file mode 100644
index 0000000..3424df7
--- /dev/null
+++ b/src/components/views/data-admin-view.tsx
@@ -0,0 +1,246 @@
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { toast } from 'sonner'
+import { Database, Download, ArrowClockwise, FileJs } from '@phosphor-icons/react'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import { Separator } from '@/components/ui/separator'
+
+export function DataAdminView() {
+ const resetAllData = async () => {
+ try {
+ await window.spark.kv.delete('sample-data-initialized')
+ await window.spark.kv.delete('timesheets')
+ await window.spark.kv.delete('invoices')
+ await window.spark.kv.delete('payroll-runs')
+ await window.spark.kv.delete('workers')
+ await window.spark.kv.delete('compliance-docs')
+ await window.spark.kv.delete('expenses')
+ await window.spark.kv.delete('rate-cards')
+ await window.spark.kv.delete('clients')
+
+ toast.success('Data cleared successfully', {
+ description: 'Refresh the page to reload default data from JSON'
+ })
+ } catch (error) {
+ toast.error('Failed to clear data')
+ }
+ }
+
+ const exportData = async () => {
+ try {
+ const timesheets = await window.spark.kv.get('timesheets')
+ const invoices = await window.spark.kv.get('invoices')
+ const payrollRuns = await window.spark.kv.get('payroll-runs')
+ const workers = await window.spark.kv.get('workers')
+ const complianceDocs = await window.spark.kv.get('compliance-docs')
+ const expenses = await window.spark.kv.get('expenses')
+ const rateCards = await window.spark.kv.get('rate-cards')
+ const clients = await window.spark.kv.get('clients')
+
+ const data = {
+ timesheets,
+ invoices,
+ payrollRuns,
+ workers,
+ complianceDocs,
+ expenses,
+ rateCards,
+ clients
+ }
+
+ const dataStr = JSON.stringify(data, null, 2)
+ const blob = new Blob([dataStr], { type: 'application/json' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = `workforce-data-${new Date().toISOString().split('T')[0]}.json`
+ a.click()
+ URL.revokeObjectURL(url)
+
+ toast.success('Data exported successfully', {
+ description: 'Your data has been downloaded as a JSON file'
+ })
+ } catch (error) {
+ toast.error('Failed to export data')
+ }
+ }
+
+ const viewAllKeys = async () => {
+ try {
+ const keys = await window.spark.kv.keys()
+ console.log('All KV Storage Keys:', keys)
+ toast.success(`Found ${keys.length} keys`, {
+ description: 'Check the console for full list'
+ })
+ } catch (error) {
+ toast.error('Failed to retrieve keys')
+ }
+ }
+
+ return (
+
+
+
Data Administration
+
+ Manage application data loaded from JSON files
+
+
+
+
+
+ JSON-Based Data
+
+ All application data is loaded from /src/data/app-data.json into persistent KV storage.
+ Changes you make in the app are saved to KV storage and persist between sessions.
+
+
+
+
+
+
+
+
+ Export Current Data
+
+
+ Download all current data as a JSON file
+
+
+
+
+ This will export all data currently in KV storage, including any modifications you've made.
+
+
+
+ Export Data
+
+
+
+
+
+
+
+
+ Reset to Default Data
+
+
+ Clear all data and reload from JSON file
+
+
+
+
+ This will delete all data from KV storage. Refresh the page to reload the default data from app-data.json.
+
+
+
+ Reset Data
+
+
+
+
+
+
+
+
+
+ Storage Information
+
+
+
+
+
+ Data Entities
+ 8
+
+
+
+
+ Timesheets
+ timesheets
+
+
+ Invoices
+ invoices
+
+
+ Payroll Runs
+ payroll-runs
+
+
+ Workers
+ workers
+
+
+ Compliance
+ compliance-docs
+
+
+ Expenses
+ expenses
+
+
+ Rate Cards
+ rate-cards
+
+
+ Clients
+ clients
+
+
+
+
+
+ View All Keys (Console)
+
+
+
+
+
+
+ Data Flow
+
+
+
+
+
1
+
+
Load from JSON
+
+ On first load, data is read from /src/data/app-data.json
+
+
+
+
+
2
+
+
Store in KV
+
+ Data is written to persistent KV storage
+
+
+
+
+
3
+
+
Use in App
+
+ Components read from and write to KV storage via hooks
+
+
+
+
+
4
+
+
Persist Changes
+
+ All changes persist between sessions automatically
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/data/app-data.json b/src/data/app-data.json
new file mode 100644
index 0000000..278523f
--- /dev/null
+++ b/src/data/app-data.json
@@ -0,0 +1,420 @@
+{
+ "timesheets": [
+ {
+ "id": "TS-001",
+ "workerId": "W-001",
+ "workerName": "Sarah Johnson",
+ "clientName": "Tech Solutions Ltd",
+ "weekEnding": "2024-01-19",
+ "totalHours": 40,
+ "regularHours": 40,
+ "overtimeHours": 0,
+ "status": "approved",
+ "rate": 25.50,
+ "total": 1020.00,
+ "submittedDate": "2024-01-19T09:30:00Z",
+ "approvedDate": "2024-01-19T14:20:00Z",
+ "shifts": [
+ { "date": "2024-01-15", "start": "09:00", "end": "17:00", "hours": 8, "type": "regular" },
+ { "date": "2024-01-16", "start": "09:00", "end": "17:00", "hours": 8, "type": "regular" },
+ { "date": "2024-01-17", "start": "09:00", "end": "17:00", "hours": 8, "type": "regular" },
+ { "date": "2024-01-18", "start": "09:00", "end": "17:00", "hours": 8, "type": "regular" },
+ { "date": "2024-01-19", "start": "09:00", "end": "17:00", "hours": 8, "type": "regular" }
+ ]
+ },
+ {
+ "id": "TS-002",
+ "workerId": "W-002",
+ "workerName": "Michael Chen",
+ "clientName": "Global Finance Corp",
+ "weekEnding": "2024-01-19",
+ "totalHours": 45,
+ "regularHours": 40,
+ "overtimeHours": 5,
+ "status": "pending",
+ "rate": 30.00,
+ "total": 1350.00,
+ "submittedDate": "2024-01-19T16:45:00Z",
+ "shifts": [
+ { "date": "2024-01-15", "start": "08:00", "end": "17:00", "hours": 9, "type": "regular" },
+ { "date": "2024-01-16", "start": "08:00", "end": "17:00", "hours": 9, "type": "regular" },
+ { "date": "2024-01-17", "start": "08:00", "end": "17:00", "hours": 9, "type": "regular" },
+ { "date": "2024-01-18", "start": "08:00", "end": "17:00", "hours": 9, "type": "regular" },
+ { "date": "2024-01-19", "start": "08:00", "end": "17:00", "hours": 9, "type": "regular" }
+ ]
+ },
+ {
+ "id": "TS-003",
+ "workerId": "W-003",
+ "workerName": "Emma Wilson",
+ "clientName": "Healthcare Plus",
+ "weekEnding": "2024-01-19",
+ "totalHours": 48,
+ "regularHours": 40,
+ "overtimeHours": 8,
+ "status": "approved",
+ "rate": 28.75,
+ "total": 1495.00,
+ "submittedDate": "2024-01-18T11:20:00Z",
+ "approvedDate": "2024-01-19T10:15:00Z",
+ "shifts": [
+ { "date": "2024-01-15", "start": "07:00", "end": "17:30", "hours": 10, "type": "regular" },
+ { "date": "2024-01-16", "start": "07:00", "end": "17:00", "hours": 10, "type": "regular" },
+ { "date": "2024-01-17", "start": "22:00", "end": "06:00", "hours": 8, "type": "night" },
+ { "date": "2024-01-18", "start": "07:00", "end": "17:00", "hours": 10, "type": "regular" },
+ { "date": "2024-01-19", "start": "07:00", "end": "17:00", "hours": 10, "type": "regular" }
+ ]
+ },
+ {
+ "id": "TS-004",
+ "workerId": "W-004",
+ "workerName": "James Brown",
+ "clientName": "Manufacturing Inc",
+ "weekEnding": "2024-01-19",
+ "totalHours": 50,
+ "regularHours": 40,
+ "overtimeHours": 10,
+ "status": "disputed",
+ "rate": 22.00,
+ "total": 1210.00,
+ "submittedDate": "2024-01-19T08:00:00Z",
+ "shifts": [
+ { "date": "2024-01-15", "start": "06:00", "end": "16:00", "hours": 10, "type": "regular" },
+ { "date": "2024-01-16", "start": "06:00", "end": "16:00", "hours": 10, "type": "regular" },
+ { "date": "2024-01-17", "start": "06:00", "end": "16:00", "hours": 10, "type": "regular" },
+ { "date": "2024-01-18", "start": "06:00", "end": "16:00", "hours": 10, "type": "regular" },
+ { "date": "2024-01-19", "start": "06:00", "end": "16:00", "hours": 10, "type": "regular" }
+ ]
+ }
+ ],
+ "invoices": [
+ {
+ "id": "INV-2024-001",
+ "clientName": "Tech Solutions Ltd",
+ "amount": 15250.00,
+ "vat": 3050.00,
+ "total": 18300.00,
+ "status": "paid",
+ "dueDate": "2024-02-15",
+ "invoiceDate": "2024-01-15",
+ "paidDate": "2024-02-10",
+ "items": [
+ { "description": "Contractor Services - Week 1", "quantity": 40, "rate": 75.00, "amount": 3000.00 },
+ { "description": "Contractor Services - Week 2", "quantity": 40, "rate": 75.00, "amount": 3000.00 },
+ { "description": "Contractor Services - Week 3", "quantity": 40, "rate": 75.00, "amount": 3000.00 },
+ { "description": "Contractor Services - Week 4", "quantity": 40, "rate": 75.00, "amount": 3000.00 }
+ ]
+ },
+ {
+ "id": "INV-2024-002",
+ "clientName": "Global Finance Corp",
+ "amount": 22500.00,
+ "vat": 4500.00,
+ "total": 27000.00,
+ "status": "pending",
+ "dueDate": "2024-02-28",
+ "invoiceDate": "2024-01-28",
+ "items": [
+ { "description": "Senior Developer - January", "quantity": 160, "rate": 95.00, "amount": 15200.00 },
+ { "description": "Project Management", "quantity": 1, "rate": 7300.00, "amount": 7300.00 }
+ ]
+ },
+ {
+ "id": "INV-2024-003",
+ "clientName": "Healthcare Plus",
+ "amount": 8750.00,
+ "vat": 1750.00,
+ "total": 10500.00,
+ "status": "overdue",
+ "dueDate": "2024-01-30",
+ "invoiceDate": "2024-01-10",
+ "items": [
+ { "description": "Nursing Services", "quantity": 125, "rate": 70.00, "amount": 8750.00 }
+ ]
+ }
+ ],
+ "payrollRuns": [
+ {
+ "id": "PR-2024-001",
+ "periodEnding": "2024-01-31",
+ "status": "completed",
+ "totalGross": 45250.00,
+ "totalNet": 35420.50,
+ "totalTax": 7829.50,
+ "totalNI": 2000.00,
+ "processedDate": "2024-02-01T10:30:00Z",
+ "paymentDate": "2024-02-05",
+ "workerCount": 12,
+ "entries": [
+ { "workerId": "W-001", "workerName": "Sarah Johnson", "gross": 4080.00, "net": 3185.20, "tax": 694.80, "ni": 200.00 },
+ { "workerId": "W-002", "workerName": "Michael Chen", "gross": 5400.00, "net": 4212.00, "tax": 938.00, "ni": 250.00 }
+ ]
+ },
+ {
+ "id": "PR-2024-002",
+ "periodEnding": "2024-02-15",
+ "status": "pending",
+ "totalGross": 38900.00,
+ "totalNet": 30450.75,
+ "totalTax": 6649.25,
+ "totalNI": 1800.00,
+ "workerCount": 10,
+ "entries": [
+ { "workerId": "W-003", "workerName": "Emma Wilson", "gross": 5980.00, "net": 4663.60, "tax": 1036.40, "ni": 280.00 }
+ ]
+ }
+ ],
+ "workers": [
+ {
+ "id": "W-001",
+ "name": "Sarah Johnson",
+ "email": "sarah.j@example.com",
+ "phone": "+44 7700 900001",
+ "status": "active",
+ "role": "Software Developer",
+ "startDate": "2023-06-15",
+ "paymentType": "Limited Company",
+ "currentClient": "Tech Solutions Ltd"
+ },
+ {
+ "id": "W-002",
+ "name": "Michael Chen",
+ "email": "m.chen@example.com",
+ "phone": "+44 7700 900002",
+ "status": "active",
+ "role": "Senior Developer",
+ "startDate": "2023-03-20",
+ "paymentType": "PAYE",
+ "currentClient": "Global Finance Corp"
+ },
+ {
+ "id": "W-003",
+ "name": "Emma Wilson",
+ "email": "emma.w@example.com",
+ "phone": "+44 7700 900003",
+ "status": "active",
+ "role": "Registered Nurse",
+ "startDate": "2023-09-10",
+ "paymentType": "PAYE",
+ "currentClient": "Healthcare Plus"
+ },
+ {
+ "id": "W-004",
+ "name": "James Brown",
+ "email": "j.brown@example.com",
+ "phone": "+44 7700 900004",
+ "status": "active",
+ "role": "Machine Operator",
+ "startDate": "2023-11-01",
+ "paymentType": "CIS",
+ "currentClient": "Manufacturing Inc"
+ }
+ ],
+ "complianceDocs": [
+ {
+ "id": "DOC-001",
+ "workerId": "W-001",
+ "workerName": "Sarah Johnson",
+ "documentType": "Right to Work",
+ "status": "valid",
+ "expiryDate": "2025-06-15",
+ "uploadDate": "2023-06-10",
+ "verifiedDate": "2023-06-12"
+ },
+ {
+ "id": "DOC-002",
+ "workerId": "W-001",
+ "workerName": "Sarah Johnson",
+ "documentType": "DBS Check",
+ "status": "valid",
+ "expiryDate": "2024-12-20",
+ "uploadDate": "2023-06-10",
+ "verifiedDate": "2023-06-15"
+ },
+ {
+ "id": "DOC-003",
+ "workerId": "W-002",
+ "workerName": "Michael Chen",
+ "documentType": "Right to Work",
+ "status": "expiring",
+ "expiryDate": "2024-03-20",
+ "uploadDate": "2023-03-15",
+ "verifiedDate": "2023-03-18"
+ },
+ {
+ "id": "DOC-004",
+ "workerId": "W-003",
+ "workerName": "Emma Wilson",
+ "documentType": "Professional Indemnity",
+ "status": "valid",
+ "expiryDate": "2024-09-10",
+ "uploadDate": "2023-09-05",
+ "verifiedDate": "2023-09-08"
+ },
+ {
+ "id": "DOC-005",
+ "workerId": "W-004",
+ "workerName": "James Brown",
+ "documentType": "CIS Verification",
+ "status": "expired",
+ "expiryDate": "2024-01-01",
+ "uploadDate": "2023-10-25",
+ "verifiedDate": "2023-10-28"
+ }
+ ],
+ "expenses": [
+ {
+ "id": "EXP-001",
+ "workerId": "W-001",
+ "workerName": "Sarah Johnson",
+ "category": "Travel",
+ "amount": 45.50,
+ "date": "2024-01-15",
+ "status": "approved",
+ "description": "Client site visit - return journey",
+ "receiptAttached": true,
+ "approvedDate": "2024-01-16"
+ },
+ {
+ "id": "EXP-002",
+ "workerId": "W-002",
+ "workerName": "Michael Chen",
+ "category": "Accommodation",
+ "amount": 125.00,
+ "date": "2024-01-18",
+ "status": "pending",
+ "description": "Hotel stay for client meeting",
+ "receiptAttached": true
+ },
+ {
+ "id": "EXP-003",
+ "workerId": "W-003",
+ "workerName": "Emma Wilson",
+ "category": "Meals",
+ "amount": 28.75,
+ "date": "2024-01-17",
+ "status": "approved",
+ "description": "Working lunch during extended shift",
+ "receiptAttached": true,
+ "approvedDate": "2024-01-18"
+ },
+ {
+ "id": "EXP-004",
+ "workerId": "W-001",
+ "workerName": "Sarah Johnson",
+ "category": "Equipment",
+ "amount": 89.99,
+ "date": "2024-01-20",
+ "status": "rejected",
+ "description": "Keyboard and mouse",
+ "receiptAttached": false,
+ "rejectedDate": "2024-01-22",
+ "rejectionReason": "No receipt provided"
+ }
+ ],
+ "rateCards": [
+ {
+ "id": "RC-001",
+ "role": "Software Developer",
+ "clientName": "Tech Solutions Ltd",
+ "payRate": 25.50,
+ "chargeRate": 75.00,
+ "margin": 49.50,
+ "marginPercent": 66.0,
+ "currency": "GBP",
+ "validFrom": "2023-06-01",
+ "validUntil": "2024-05-31",
+ "overtimeMultiplier": 1.5,
+ "weekendMultiplier": 2.0
+ },
+ {
+ "id": "RC-002",
+ "role": "Senior Developer",
+ "clientName": "Global Finance Corp",
+ "payRate": 30.00,
+ "chargeRate": 95.00,
+ "margin": 65.00,
+ "marginPercent": 68.4,
+ "currency": "GBP",
+ "validFrom": "2023-03-01",
+ "validUntil": "2024-02-29",
+ "overtimeMultiplier": 1.5,
+ "weekendMultiplier": 1.8
+ },
+ {
+ "id": "RC-003",
+ "role": "Registered Nurse",
+ "clientName": "Healthcare Plus",
+ "payRate": 28.75,
+ "chargeRate": 70.00,
+ "margin": 41.25,
+ "marginPercent": 58.9,
+ "currency": "GBP",
+ "validFrom": "2023-09-01",
+ "validUntil": "2024-08-31",
+ "overtimeMultiplier": 1.5,
+ "weekendMultiplier": 2.0,
+ "nightMultiplier": 1.3
+ },
+ {
+ "id": "RC-004",
+ "role": "Machine Operator",
+ "clientName": "Manufacturing Inc",
+ "payRate": 22.00,
+ "chargeRate": 55.00,
+ "margin": 33.00,
+ "marginPercent": 60.0,
+ "currency": "GBP",
+ "validFrom": "2023-11-01",
+ "validUntil": "2024-10-31",
+ "overtimeMultiplier": 1.5,
+ "weekendMultiplier": 2.0
+ }
+ ],
+ "clients": [
+ {
+ "id": "C-001",
+ "name": "Tech Solutions Ltd",
+ "industry": "Technology",
+ "status": "active",
+ "creditLimit": 50000.00,
+ "outstandingBalance": 0.00,
+ "paymentTerms": 30,
+ "activeWorkers": 3,
+ "address": "123 Tech Street, London, EC1A 1BB"
+ },
+ {
+ "id": "C-002",
+ "name": "Global Finance Corp",
+ "industry": "Finance",
+ "status": "active",
+ "creditLimit": 100000.00,
+ "outstandingBalance": 27000.00,
+ "paymentTerms": 45,
+ "activeWorkers": 5,
+ "address": "456 Finance Road, London, E14 5AB"
+ },
+ {
+ "id": "C-003",
+ "name": "Healthcare Plus",
+ "industry": "Healthcare",
+ "status": "active",
+ "creditLimit": 75000.00,
+ "outstandingBalance": 10500.00,
+ "paymentTerms": 30,
+ "activeWorkers": 8,
+ "address": "789 Medical Way, Birmingham, B1 1AA"
+ },
+ {
+ "id": "C-004",
+ "name": "Manufacturing Inc",
+ "industry": "Manufacturing",
+ "status": "active",
+ "creditLimit": 60000.00,
+ "outstandingBalance": 5200.00,
+ "paymentTerms": 60,
+ "activeWorkers": 12,
+ "address": "321 Factory Lane, Manchester, M1 1AA"
+ }
+ ]
+}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 697b5d5..b5ed005 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -14,6 +14,7 @@ export { useOnClickOutside } from './use-on-click-outside'
export { usePagination } from './use-pagination'
export { usePrevious } from './use-previous'
export { useSampleData } from './use-sample-data'
+export { useJsonData } from './use-json-data'
export { useScrollPosition } from './use-scroll-position'
export { useSelection } from './use-selection'
export { useSort } from './use-sort'
diff --git a/src/hooks/use-json-data.ts b/src/hooks/use-json-data.ts
new file mode 100644
index 0000000..e300cee
--- /dev/null
+++ b/src/hooks/use-json-data.ts
@@ -0,0 +1,165 @@
+import { useState, useEffect } from 'react'
+import appData from '@/data/app-data.json'
+
+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[]
+}
+
+export interface InvoiceItem {
+ description: string
+ quantity: number
+ rate: number
+ amount: number
+}
+
+export interface Invoice {
+ id: string
+ clientName: string
+ amount: number
+ vat: number
+ total: number
+ status: string
+ dueDate: string
+ invoiceDate: string
+ paidDate?: string
+ items: InvoiceItem[]
+}
+
+export interface PayrollEntry {
+ workerId: string
+ workerName: string
+ gross: number
+ net: number
+ tax: number
+ ni: number
+}
+
+export interface PayrollRun {
+ id: string
+ periodEnding: string
+ status: string
+ totalGross: number
+ totalNet: number
+ totalTax: number
+ totalNI: number
+ processedDate?: string
+ paymentDate?: string
+ workerCount: number
+ entries: PayrollEntry[]
+}
+
+export interface Worker {
+ id: string
+ name: string
+ email: string
+ phone: string
+ status: string
+ role: string
+ startDate: string
+ paymentType: string
+ currentClient: string
+}
+
+export interface ComplianceDoc {
+ id: string
+ workerId: string
+ workerName: string
+ documentType: string
+ status: string
+ expiryDate: string
+ uploadDate: string
+ verifiedDate: string
+}
+
+export interface Expense {
+ id: string
+ workerId: string
+ workerName: string
+ category: string
+ amount: number
+ date: string
+ status: string
+ description: string
+ receiptAttached: boolean
+ approvedDate?: string
+ rejectedDate?: string
+ rejectionReason?: string
+}
+
+export interface RateCard {
+ id: string
+ role: string
+ clientName: string
+ payRate: number
+ chargeRate: number
+ margin: number
+ marginPercent: number
+ currency: string
+ validFrom: string
+ validUntil: string
+ overtimeMultiplier: number
+ weekendMultiplier: number
+ nightMultiplier?: number
+}
+
+export interface Client {
+ id: string
+ name: string
+ industry: string
+ status: string
+ creditLimit: number
+ outstandingBalance: number
+ paymentTerms: number
+ activeWorkers: number
+ address: string
+}
+
+export interface AppData {
+ timesheets: Timesheet[]
+ invoices: Invoice[]
+ payrollRuns: PayrollRun[]
+ workers: Worker[]
+ complianceDocs: ComplianceDoc[]
+ expenses: Expense[]
+ rateCards: RateCard[]
+ clients: Client[]
+}
+
+export function useJsonData() {
+ const [data, setData] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ try {
+ setData(appData as AppData)
+ setIsLoading(false)
+ } catch (err) {
+ setError(err instanceof Error ? err : new Error('Failed to load data'))
+ setIsLoading(false)
+ }
+ }, [])
+
+ return { data, isLoading, error }
+}
diff --git a/src/hooks/use-sample-data.ts b/src/hooks/use-sample-data.ts
index ed75c29..27a79ba 100644
--- a/src/hooks/use-sample-data.ts
+++ b/src/hooks/use-sample-data.ts
@@ -1,241 +1,33 @@
import { useEffect } from 'react'
import { useKV } from '@github/spark/hooks'
-import type { Timesheet, Invoice, Expense, ComplianceDocument } from '@/lib/types'
+import appData from '@/data/app-data.json'
export function useSampleData() {
const [hasInitialized, setHasInitialized] = useKV('sample-data-initialized', false)
- const [, setTimesheets] = useKV('timesheets', [])
- const [, setInvoices] = useKV('invoices', [])
- const [, setExpenses] = useKV('expenses', [])
- const [, setComplianceDocs] = useKV('compliance-docs', [])
+ const [, setTimesheets] = useKV('timesheets', [])
+ const [, setInvoices] = useKV('invoices', [])
+ const [, setExpenses] = useKV('expenses', [])
+ const [, setComplianceDocs] = useKV('compliance-docs', [])
+ const [, setPayrollRuns] = useKV('payroll-runs', [])
+ const [, setWorkers] = useKV('workers', [])
+ const [, setRateCards] = useKV('rate-cards', [])
+ const [, setClients] = useKV('clients', [])
useEffect(() => {
if (hasInitialized) return
const initializeData = async () => {
- const sampleTimesheets: Timesheet[] = [
- {
- id: 'TS-001',
- workerId: 'W-001',
- workerName: 'John Smith',
- clientName: 'Acme Corporation',
- weekEnding: '2025-01-17',
- hours: 40,
- status: 'pending',
- submittedDate: '2025-01-18T09:00:00Z',
- amount: 1200,
- rate: 30
- },
- {
- id: 'TS-002',
- workerId: 'W-002',
- workerName: 'Sarah Johnson',
- clientName: 'Tech Solutions Ltd',
- weekEnding: '2025-01-17',
- hours: 45,
- status: 'pending',
- submittedDate: '2025-01-18T10:30:00Z',
- amount: 1575,
- rate: 35
- },
- {
- id: 'TS-003',
- workerId: 'W-001',
- workerName: 'John Smith',
- clientName: 'Acme Corporation',
- weekEnding: '2025-01-10',
- hours: 37.5,
- status: 'approved',
- submittedDate: '2025-01-11T09:00:00Z',
- approvedDate: '2025-01-12T14:00:00Z',
- amount: 1125,
- rate: 30
- },
- {
- id: 'TS-004',
- workerId: 'W-003',
- workerName: 'Michael Brown',
- clientName: 'Global Industries',
- weekEnding: '2025-01-17',
- hours: 52,
- status: 'pending',
- submittedDate: '2025-01-18T11:00:00Z',
- amount: 1820,
- rate: 35
- },
- {
- id: 'TS-005',
- workerId: 'W-004',
- workerName: 'Emma Wilson',
- clientName: 'Tech Solutions Ltd',
- weekEnding: '2025-01-17',
- hours: 40,
- status: 'approved',
- submittedDate: '2025-01-18T08:30:00Z',
- approvedDate: '2025-01-18T15:00:00Z',
- amount: 1000,
- rate: 25
- }
- ]
-
- const sampleInvoices: Invoice[] = [
- {
- id: 'INV-001',
- invoiceNumber: 'INV-00001',
- clientName: 'Acme Corporation',
- issueDate: '2025-01-15',
- dueDate: '2025-02-14',
- amount: 5400,
- status: 'sent',
- currency: 'GBP'
- },
- {
- id: 'INV-002',
- invoiceNumber: 'INV-00002',
- clientName: 'Tech Solutions Ltd',
- issueDate: '2025-01-10',
- dueDate: '2025-02-09',
- amount: 3150,
- status: 'paid',
- currency: 'GBP'
- },
- {
- id: 'INV-003',
- invoiceNumber: 'INV-00003',
- clientName: 'Global Industries',
- issueDate: '2024-12-20',
- dueDate: '2025-01-19',
- amount: 8900,
- status: 'overdue',
- currency: 'GBP'
- },
- {
- id: 'INV-004',
- invoiceNumber: 'INV-00004',
- clientName: 'Tech Solutions Ltd',
- issueDate: '2025-01-18',
- dueDate: '2025-02-17',
- amount: 2500,
- status: 'draft',
- currency: 'GBP'
- }
- ]
-
- const sampleExpenses: Expense[] = [
- {
- id: 'EXP-001',
- workerId: 'W-001',
- workerName: 'John Smith',
- clientName: 'Acme Corporation',
- date: '2025-01-15',
- category: 'Travel',
- description: 'Train ticket to client site',
- amount: 45.50,
- currency: 'GBP',
- status: 'pending',
- submittedDate: '2025-01-16T10:00:00Z',
- billable: true
- },
- {
- id: 'EXP-002',
- workerId: 'W-002',
- workerName: 'Sarah Johnson',
- clientName: 'Tech Solutions Ltd',
- date: '2025-01-14',
- category: 'Meals',
- description: 'Lunch meeting with client team',
- amount: 35.00,
- currency: 'GBP',
- status: 'approved',
- submittedDate: '2025-01-15T09:00:00Z',
- approvedDate: '2025-01-16T11:00:00Z',
- billable: true
- },
- {
- id: 'EXP-003',
- workerId: 'W-003',
- workerName: 'Michael Brown',
- clientName: 'Global Industries',
- date: '2025-01-16',
- category: 'Accommodation',
- description: 'Hotel stay for 2 nights',
- amount: 240.00,
- currency: 'GBP',
- status: 'pending',
- submittedDate: '2025-01-17T08:30:00Z',
- billable: true
- },
- {
- id: 'EXP-004',
- workerId: 'W-001',
- workerName: 'John Smith',
- clientName: 'Acme Corporation',
- date: '2025-01-10',
- category: 'Equipment',
- description: 'Laptop accessories',
- amount: 85.00,
- currency: 'GBP',
- status: 'rejected',
- submittedDate: '2025-01-11T14:00:00Z',
- billable: false
- }
- ]
-
- const sampleComplianceDocs: ComplianceDocument[] = [
- {
- id: 'DOC-001',
- workerId: 'W-001',
- workerName: 'John Smith',
- documentType: 'DBS Check',
- expiryDate: '2025-02-15',
- status: 'expiring',
- daysUntilExpiry: 28
- },
- {
- id: 'DOC-002',
- workerId: 'W-002',
- workerName: 'Sarah Johnson',
- documentType: 'Right to Work',
- expiryDate: '2026-06-30',
- status: 'valid',
- daysUntilExpiry: 528
- },
- {
- id: 'DOC-003',
- workerId: 'W-003',
- workerName: 'Michael Brown',
- documentType: 'Professional License',
- expiryDate: '2025-01-10',
- status: 'expired',
- daysUntilExpiry: -8
- },
- {
- id: 'DOC-004',
- workerId: 'W-004',
- workerName: 'Emma Wilson',
- documentType: 'First Aid Certificate',
- expiryDate: '2025-03-20',
- status: 'valid',
- daysUntilExpiry: 61
- },
- {
- id: 'DOC-005',
- workerId: 'W-001',
- workerName: 'John Smith',
- documentType: 'Driving License',
- expiryDate: '2025-02-05',
- status: 'expiring',
- daysUntilExpiry: 18
- }
- ]
-
- setTimesheets(sampleTimesheets)
- setInvoices(sampleInvoices)
- setExpenses(sampleExpenses)
- setComplianceDocs(sampleComplianceDocs)
+ setTimesheets(appData.timesheets)
+ setInvoices(appData.invoices)
+ setExpenses(appData.expenses)
+ setComplianceDocs(appData.complianceDocs)
+ setPayrollRuns(appData.payrollRuns)
+ setWorkers(appData.workers)
+ setRateCards(appData.rateCards)
+ setClients(appData.clients)
setHasInitialized(true)
}
initializeData()
- }, [hasInitialized, setTimesheets, setInvoices, setExpenses, setComplianceDocs, setHasInitialized])
+ }, [hasInitialized, setTimesheets, setInvoices, setExpenses, setComplianceDocs, setPayrollRuns, setWorkers, setRateCards, setClients, setHasInitialized])
}
diff --git a/src/lib/view-preloader.ts b/src/lib/view-preloader.ts
index 37a6449..c0cf24a 100644
--- a/src/lib/view-preloader.ts
+++ b/src/lib/view-preloader.ts
@@ -27,6 +27,7 @@ const viewPreloadMap: Record Promise> = {
'roadmap': () => import('@/components/roadmap-view'),
'component-showcase': () => import('@/components/ComponentShowcase'),
'business-logic-demo': () => import('@/components/BusinessLogicDemo'),
+ 'data-admin': () => import('@/components/views/data-admin-view'),
}
const preloadedViews = new Set()