Generated by Spark: Load all ui data from json

This commit is contained in:
2026-01-23 09:07:35 +00:00
committed by GitHub
parent 7659d9482e
commit b461e65bba
11 changed files with 1305 additions and 228 deletions

354
JSON_DATA_STRUCTURE.md Normal file
View File

@@ -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 <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{data.workers.length} workers</div>
}
```
### 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<NewEntity[]>('new-entity', [])
setNewEntity(appData.newEntity)
```
4. **Expose in use-app-data**
```typescript
const [newEntity = [], setNewEntity] = useKV<NewEntity[]>('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

View File

@@ -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()

View File

@@ -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 (
<Card className="p-6 space-y-4">
<div>
<h3 className="text-lg font-semibold mb-2">Data Management</h3>
<p className="text-sm text-muted-foreground mb-4">
Manage application data and reset to defaults
</p>
</div>
<div className="flex flex-col gap-2">
<Button onClick={exportData} variant="outline">
Export Current Data
</Button>
<Button onClick={resetAllData} variant="destructive">
Reset to Default Data
</Button>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p> Export: Download current data as JSON file</p>
<p> Reset: Clear all data and reload from app-data.json</p>
<p> After reset, refresh the page to see changes</p>
</div>
</Card>
)
}

View File

@@ -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 <BusinessLogicDemo />
case 'data-admin':
return <DataAdminView />
default:
return <DashboardView metrics={metrics} />
}

View File

@@ -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"
/>
<NavItem
icon={<Database size={20} />}
label="Data Administration"
active={currentView === 'data-admin'}
onClick={() => setCurrentView('data-admin')}
view="data-admin"
/>
<NavItem
icon={<Question size={20} />}
label="Query Guide"

View File

@@ -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 (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-semibold mb-2">Data Administration</h1>
<p className="text-muted-foreground">
Manage application data loaded from JSON files
</p>
</div>
<Alert>
<FileJs className="h-4 w-4" />
<AlertTitle>JSON-Based Data</AlertTitle>
<AlertDescription>
All application data is loaded from <code className="px-1 py-0.5 bg-muted rounded text-xs">/src/data/app-data.json</code> into persistent KV storage.
Changes you make in the app are saved to KV storage and persist between sessions.
</AlertDescription>
</Alert>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Download className="h-5 w-5" />
Export Current Data
</CardTitle>
<CardDescription>
Download all current data as a JSON file
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
This will export all data currently in KV storage, including any modifications you've made.
</p>
<Button onClick={exportData} className="w-full">
<Download className="mr-2 h-4 w-4" />
Export Data
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ArrowClockwise className="h-5 w-5" />
Reset to Default Data
</CardTitle>
<CardDescription>
Clear all data and reload from JSON file
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
This will delete all data from KV storage. Refresh the page to reload the default data from app-data.json.
</p>
<Button onClick={resetAllData} variant="destructive" className="w-full">
<ArrowClockwise className="mr-2 h-4 w-4" />
Reset Data
</Button>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Storage Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Data Entities</span>
<Badge variant="secondary">8</Badge>
</div>
<Separator />
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Timesheets</span>
<Badge variant="outline">timesheets</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Invoices</span>
<Badge variant="outline">invoices</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Payroll Runs</span>
<Badge variant="outline">payroll-runs</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Workers</span>
<Badge variant="outline">workers</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Compliance</span>
<Badge variant="outline">compliance-docs</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Expenses</span>
<Badge variant="outline">expenses</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Rate Cards</span>
<Badge variant="outline">rate-cards</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Clients</span>
<Badge variant="outline">clients</Badge>
</div>
</div>
</div>
<Button onClick={viewAllKeys} variant="outline" className="w-full">
<Database className="mr-2 h-4 w-4" />
View All Keys (Console)
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Data Flow</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-start gap-3">
<Badge className="mt-0.5">1</Badge>
<div>
<p className="font-medium text-sm">Load from JSON</p>
<p className="text-xs text-muted-foreground">
On first load, data is read from <code>/src/data/app-data.json</code>
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Badge className="mt-0.5">2</Badge>
<div>
<p className="font-medium text-sm">Store in KV</p>
<p className="text-xs text-muted-foreground">
Data is written to persistent KV storage
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Badge className="mt-0.5">3</Badge>
<div>
<p className="font-medium text-sm">Use in App</p>
<p className="text-xs text-muted-foreground">
Components read from and write to KV storage via hooks
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Badge className="mt-0.5">4</Badge>
<div>
<p className="font-medium text-sm">Persist Changes</p>
<p className="text-xs text-muted-foreground">
All changes persist between sessions automatically
</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

420
src/data/app-data.json Normal file
View File

@@ -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"
}
]
}

View File

@@ -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'

165
src/hooks/use-json-data.ts Normal file
View File

@@ -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<AppData | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<Error | null>(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 }
}

View File

@@ -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<boolean>('sample-data-initialized', false)
const [, setTimesheets] = useKV<Timesheet[]>('timesheets', [])
const [, setInvoices] = useKV<Invoice[]>('invoices', [])
const [, setExpenses] = useKV<Expense[]>('expenses', [])
const [, setComplianceDocs] = useKV<ComplianceDocument[]>('compliance-docs', [])
const [, setTimesheets] = useKV<any[]>('timesheets', [])
const [, setInvoices] = useKV<any[]>('invoices', [])
const [, setExpenses] = useKV<any[]>('expenses', [])
const [, setComplianceDocs] = useKV<any[]>('compliance-docs', [])
const [, setPayrollRuns] = useKV<any[]>('payroll-runs', [])
const [, setWorkers] = useKV<any[]>('workers', [])
const [, setRateCards] = useKV<any[]>('rate-cards', [])
const [, setClients] = useKV<any[]>('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])
}

View File

@@ -27,6 +27,7 @@ const viewPreloadMap: Record<View, () => Promise<any>> = {
'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<View>()