mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: CRUD operations go into IndexedDB
This commit is contained in:
273
INDEXEDDB_CRUD.md
Normal file
273
INDEXEDDB_CRUD.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# IndexedDB CRUD Operations
|
||||
|
||||
This application uses IndexedDB for all data persistence and CRUD (Create, Read, Update, Delete) operations. This provides better performance, offline support, and structured data management compared to simple key-value storage.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Structure
|
||||
- **Database Name**: `WorkForceProDB`
|
||||
- **Version**: 2
|
||||
- **Stores**:
|
||||
- `sessions` - User session data
|
||||
- `appState` - General application state
|
||||
- `timesheets` - Timesheet entities
|
||||
- `invoices` - Invoice entities
|
||||
- `payrollRuns` - Payroll run entities
|
||||
- `workers` - Worker entities
|
||||
- `complianceDocs` - Compliance document entities
|
||||
- `expenses` - Expense entities
|
||||
- `rateCards` - Rate card entities
|
||||
|
||||
### Indexes
|
||||
Each entity store has relevant indexes for efficient querying:
|
||||
- **timesheets**: `workerId`, `status`, `weekEnding`
|
||||
- **invoices**: `clientId`, `status`, `invoiceDate`
|
||||
- **payrollRuns**: `status`, `periodEnding`
|
||||
- **workers**: `status`, `email`
|
||||
- **complianceDocs**: `workerId`, `status`, `expiryDate`
|
||||
- **expenses**: `workerId`, `status`, `date`
|
||||
- **rateCards**: `clientId`, `role`
|
||||
|
||||
## Usage
|
||||
|
||||
### Low-Level API (Direct IndexedDB Access)
|
||||
|
||||
```typescript
|
||||
import { indexedDB, STORES } from '@/lib/indexed-db'
|
||||
|
||||
// Create
|
||||
const newTimesheet = { id: 'ts-001', workerId: 'w-123', hours: 40, status: 'pending' }
|
||||
await indexedDB.create(STORES.TIMESHEETS, newTimesheet)
|
||||
|
||||
// Read one
|
||||
const timesheet = await indexedDB.read(STORES.TIMESHEETS, 'ts-001')
|
||||
|
||||
// Read all
|
||||
const allTimesheets = await indexedDB.readAll(STORES.TIMESHEETS)
|
||||
|
||||
// Read by index
|
||||
const pendingTimesheets = await indexedDB.readByIndex(STORES.TIMESHEETS, 'status', 'pending')
|
||||
|
||||
// Update
|
||||
timesheet.status = 'approved'
|
||||
await indexedDB.update(STORES.TIMESHEETS, timesheet)
|
||||
|
||||
// Delete
|
||||
await indexedDB.delete(STORES.TIMESHEETS, 'ts-001')
|
||||
|
||||
// Delete all
|
||||
await indexedDB.deleteAll(STORES.TIMESHEETS)
|
||||
|
||||
// Bulk operations
|
||||
await indexedDB.bulkCreate(STORES.TIMESHEETS, [timesheet1, timesheet2, timesheet3])
|
||||
await indexedDB.bulkUpdate(STORES.TIMESHEETS, [updatedTimesheet1, updatedTimesheet2])
|
||||
|
||||
// Query with predicate
|
||||
const highValueTimesheets = await indexedDB.query(
|
||||
STORES.TIMESHEETS,
|
||||
(ts) => ts.hours > 40
|
||||
)
|
||||
```
|
||||
|
||||
### React Hook API (Recommended)
|
||||
|
||||
#### Generic CRUD Hook
|
||||
|
||||
```typescript
|
||||
import { useCRUD } from '@/hooks/use-crud'
|
||||
import { STORES } from '@/lib/indexed-db'
|
||||
|
||||
function MyComponent() {
|
||||
const {
|
||||
data, // Current data in state
|
||||
isLoading, // Loading state
|
||||
error, // Error state
|
||||
create, // Create entity
|
||||
read, // Read single entity
|
||||
readAll, // Read all entities
|
||||
readByIndex, // Read by index
|
||||
update, // Update entity
|
||||
remove, // Delete entity
|
||||
removeAll, // Delete all entities
|
||||
bulkCreate, // Bulk create
|
||||
bulkUpdate, // Bulk update
|
||||
query, // Query with predicate
|
||||
refresh, // Refresh data
|
||||
} = useCRUD<Timesheet>(STORES.TIMESHEETS)
|
||||
|
||||
// Usage examples
|
||||
const handleCreate = async () => {
|
||||
await create({ id: 'ts-001', workerId: 'w-123', hours: 40 })
|
||||
}
|
||||
|
||||
const handleUpdate = async (id: string) => {
|
||||
const timesheet = await read(id)
|
||||
if (timesheet) {
|
||||
await update({ ...timesheet, status: 'approved' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await remove(id)
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
const results = await query((ts) => ts.hours > 40)
|
||||
console.log(results)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
readAll() // Load initial data
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading && <Spinner />}
|
||||
{error && <Alert>{error.message}</Alert>}
|
||||
{data.map(item => <Card key={item.id}>{item.name}</Card>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Entity-Specific CRUD Hooks
|
||||
|
||||
Pre-configured hooks for each entity type:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
useTimesheetsCRUD,
|
||||
useInvoicesCRUD,
|
||||
usePayrollRunsCRUD,
|
||||
useWorkersCRUD,
|
||||
useComplianceDocsCRUD,
|
||||
useExpensesCRUD,
|
||||
useRateCardsCRUD
|
||||
} from '@/hooks/use-entity-crud'
|
||||
|
||||
function TimesheetsView() {
|
||||
const timesheets = useTimesheetsCRUD()
|
||||
|
||||
useEffect(() => {
|
||||
timesheets.readAll()
|
||||
}, [])
|
||||
|
||||
const handleApprove = async (id: string) => {
|
||||
const timesheet = await timesheets.read(id)
|
||||
if (timesheet) {
|
||||
await timesheets.update({ ...timesheet, status: 'approved' })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{timesheets.data.map(ts => (
|
||||
<TimesheetCard
|
||||
key={ts.id}
|
||||
timesheet={ts}
|
||||
onApprove={() => handleApprove(ts.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### IndexedDB State Hook (for backwards compatibility)
|
||||
|
||||
Automatically detects entity stores and uses appropriate storage:
|
||||
|
||||
```typescript
|
||||
import { useIndexedDBState } from '@/hooks/use-indexed-db-state'
|
||||
import { STORES } from '@/lib/indexed-db'
|
||||
|
||||
function MyComponent() {
|
||||
// For entity stores: uses IndexedDB entity storage
|
||||
const [timesheets, setTimesheets] = useIndexedDBState<Timesheet[]>(
|
||||
STORES.TIMESHEETS,
|
||||
[]
|
||||
)
|
||||
|
||||
// For non-entity data: uses appState storage
|
||||
const [preferences, setPreferences] = useIndexedDBState(
|
||||
'user-preferences',
|
||||
{ theme: 'light' }
|
||||
)
|
||||
|
||||
// Update always uses functional form for safety
|
||||
const addTimesheet = (newTimesheet: Timesheet) => {
|
||||
setTimesheets(current => [...current, newTimesheet])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from KV Storage
|
||||
|
||||
The application has been migrated from KV storage to IndexedDB. Key changes:
|
||||
|
||||
1. **`use-app-data` hook**: Now uses `useIndexedDBState` instead of `useKV`
|
||||
2. **Data persistence**: All entity data is stored in dedicated IndexedDB stores
|
||||
3. **Performance**: Bulk operations and indexed queries provide better performance
|
||||
4. **Querying**: Native support for filtering and querying via indexes
|
||||
|
||||
### Before (KV Storage)
|
||||
```typescript
|
||||
const [timesheets, setTimesheets] = useKV<Timesheet[]>('timesheets', [])
|
||||
```
|
||||
|
||||
### After (IndexedDB)
|
||||
```typescript
|
||||
const [timesheets, setTimesheets] = useIndexedDBState<Timesheet[]>(STORES.TIMESHEETS, [])
|
||||
// OR
|
||||
const timesheets = useTimesheetsCRUD()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use functional updates** when modifying arrays/objects:
|
||||
```typescript
|
||||
// ✅ Good
|
||||
setTimesheets(current => [...current, newItem])
|
||||
|
||||
// ❌ Bad (stale closure)
|
||||
setTimesheets([...timesheets, newItem])
|
||||
```
|
||||
|
||||
2. **Use entity-specific hooks** for typed operations:
|
||||
```typescript
|
||||
const timesheets = useTimesheetsCRUD()
|
||||
```
|
||||
|
||||
3. **Leverage indexes** for efficient queries:
|
||||
```typescript
|
||||
const pendingItems = await readByIndex('status', 'pending')
|
||||
```
|
||||
|
||||
4. **Handle errors appropriately**:
|
||||
```typescript
|
||||
try {
|
||||
await timesheets.create(newTimesheet)
|
||||
} catch (error) {
|
||||
toast.error('Failed to create timesheet')
|
||||
}
|
||||
```
|
||||
|
||||
5. **Use bulk operations** for multiple items:
|
||||
```typescript
|
||||
await timesheets.bulkCreate([item1, item2, item3])
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
- ✅ **Structured storage**: Proper relational-style data organization
|
||||
- ✅ **Indexed queries**: Fast lookups by common fields
|
||||
- ✅ **Bulk operations**: Efficient batch processing
|
||||
- ✅ **Type safety**: Full TypeScript support
|
||||
- ✅ **Offline support**: Works without network connection
|
||||
- ✅ **Performance**: Better than key-value for complex data
|
||||
- ✅ **Transactional**: ACID guarantees for data integrity
|
||||
- ✅ **Observable**: React hooks provide reactive updates
|
||||
|
||||
## Debugging
|
||||
|
||||
Open browser DevTools → Application → IndexedDB → WorkForceProDB to inspect data directly.
|
||||
@@ -1,36 +1,68 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { toast } from 'sonner'
|
||||
import { indexedDB, STORES } from '@/lib/indexed-db'
|
||||
import { Database, Download, Trash, ArrowsClockwise, HardDrive } from '@phosphor-icons/react'
|
||||
|
||||
export function DataManagement() {
|
||||
const [stats, setStats] = useState<Record<string, number>>({})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const loadStats = async () => {
|
||||
const newStats: Record<string, number> = {}
|
||||
|
||||
for (const [key, storeName] of Object.entries(STORES)) {
|
||||
if (key !== 'SESSIONS' && key !== 'APP_STATE') {
|
||||
try {
|
||||
const data = await indexedDB.readAll(storeName)
|
||||
newStats[storeName] = data.length
|
||||
} catch (error) {
|
||||
newStats[storeName] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStats(newStats)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadStats()
|
||||
}, [])
|
||||
|
||||
const resetAllData = async () => {
|
||||
setIsLoading(true)
|
||||
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')
|
||||
await indexedDB.deleteAll(STORES.TIMESHEETS)
|
||||
await indexedDB.deleteAll(STORES.INVOICES)
|
||||
await indexedDB.deleteAll(STORES.PAYROLL_RUNS)
|
||||
await indexedDB.deleteAll(STORES.WORKERS)
|
||||
await indexedDB.deleteAll(STORES.COMPLIANCE_DOCS)
|
||||
await indexedDB.deleteAll(STORES.EXPENSES)
|
||||
await indexedDB.deleteAll(STORES.RATE_CARDS)
|
||||
await indexedDB.deleteAppState('sample-data-initialized')
|
||||
|
||||
await loadStats()
|
||||
toast.success('Data cleared - refresh to reload from JSON')
|
||||
} catch (error) {
|
||||
toast.error('Failed to clear data')
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportData = async () => {
|
||||
setIsLoading(true)
|
||||
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 timesheets = await indexedDB.readAll(STORES.TIMESHEETS)
|
||||
const invoices = await indexedDB.readAll(STORES.INVOICES)
|
||||
const payrollRuns = await indexedDB.readAll(STORES.PAYROLL_RUNS)
|
||||
const workers = await indexedDB.readAll(STORES.WORKERS)
|
||||
const complianceDocs = await indexedDB.readAll(STORES.COMPLIANCE_DOCS)
|
||||
const expenses = await indexedDB.readAll(STORES.EXPENSES)
|
||||
const rateCards = await indexedDB.readAll(STORES.RATE_CARDS)
|
||||
|
||||
const data = {
|
||||
timesheets,
|
||||
@@ -40,7 +72,8 @@ export function DataManagement() {
|
||||
complianceDocs,
|
||||
expenses,
|
||||
rateCards,
|
||||
clients
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: 2
|
||||
}
|
||||
|
||||
const dataStr = JSON.stringify(data, null, 2)
|
||||
@@ -48,38 +81,118 @@ export function DataManagement() {
|
||||
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.download = `workforce-indexeddb-export-${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')
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const clearStore = async (storeName: string, displayName: string) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await indexedDB.deleteAll(storeName)
|
||||
await loadStats()
|
||||
toast.success(`${displayName} cleared`)
|
||||
} catch (error) {
|
||||
toast.error(`Failed to clear ${displayName}`)
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
<Card className="p-6 space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Database size={24} className="text-primary" weight="duotone" />
|
||||
<h3 className="text-lg font-semibold">IndexedDB Data Management</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage application data stored in browser IndexedDB
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={loadStats}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ArrowsClockwise size={16} />
|
||||
</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 className="space-y-3">
|
||||
<h4 className="text-sm font-medium flex items-center gap-2">
|
||||
<HardDrive size={16} />
|
||||
Data Stores
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{[
|
||||
{ store: STORES.TIMESHEETS, name: 'Timesheets' },
|
||||
{ store: STORES.INVOICES, name: 'Invoices' },
|
||||
{ store: STORES.PAYROLL_RUNS, name: 'Payroll Runs' },
|
||||
{ store: STORES.WORKERS, name: 'Workers' },
|
||||
{ store: STORES.COMPLIANCE_DOCS, name: 'Compliance Docs' },
|
||||
{ store: STORES.EXPENSES, name: 'Expenses' },
|
||||
{ store: STORES.RATE_CARDS, name: 'Rate Cards' },
|
||||
].map(({ store, name }) => (
|
||||
<div
|
||||
key={store}
|
||||
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{name}</span>
|
||||
<Badge variant="secondary">
|
||||
{stats[store] ?? 0} records
|
||||
</Badge>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => clearStore(store, name)}
|
||||
disabled={isLoading || !stats[store]}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 pt-4 border-t">
|
||||
<Button
|
||||
onClick={exportData}
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Download size={16} />
|
||||
Export All Data (JSON)
|
||||
</Button>
|
||||
<Button
|
||||
onClick={resetAllData}
|
||||
variant="destructive"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Trash size={16} />
|
||||
Clear All Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-1 pt-2 border-t">
|
||||
<p>• <strong>Export:</strong> Download all data as JSON file</p>
|
||||
<p>• <strong>Clear All:</strong> Delete all data and reload from app-data.json</p>
|
||||
<p>• <strong>Clear Store:</strong> Delete data from specific entity store</p>
|
||||
<p>• <strong>Note:</strong> After clearing data, refresh the page to reload defaults</p>
|
||||
<p className="text-primary">• <strong>Storage:</strong> Using IndexedDB for structured data persistence</p>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -106,6 +106,16 @@ export { useSessionStorage } from './use-session-storage'
|
||||
export { useSessionTimeout } from './use-session-timeout'
|
||||
export { useSessionTimeoutPreferences } from './use-session-timeout-preferences'
|
||||
export { useIndexedDBState, useIndexedDBCache } from './use-indexed-db-state'
|
||||
export { useCRUD } from './use-crud'
|
||||
export {
|
||||
useTimesheetsCRUD,
|
||||
useInvoicesCRUD,
|
||||
usePayrollRunsCRUD,
|
||||
useWorkersCRUD,
|
||||
useComplianceDocsCRUD,
|
||||
useExpensesCRUD,
|
||||
useRateCardsCRUD
|
||||
} from './use-entity-crud'
|
||||
|
||||
export type { AsyncState } from './use-async'
|
||||
export type { FormErrors } from './use-form-validation'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useIndexedDBState } from '@/hooks/use-indexed-db-state'
|
||||
import { STORES } from '@/lib/indexed-db'
|
||||
import type {
|
||||
Timesheet,
|
||||
Invoice,
|
||||
@@ -12,13 +13,13 @@ import type {
|
||||
} 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 [timesheets = [], setTimesheets] = useIndexedDBState<Timesheet[]>(STORES.TIMESHEETS, [])
|
||||
const [invoices = [], setInvoices] = useIndexedDBState<Invoice[]>(STORES.INVOICES, [])
|
||||
const [payrollRuns = [], setPayrollRuns] = useIndexedDBState<PayrollRun[]>(STORES.PAYROLL_RUNS, [])
|
||||
const [workers = [], setWorkers] = useIndexedDBState<Worker[]>(STORES.WORKERS, [])
|
||||
const [complianceDocs = [], setComplianceDocs] = useIndexedDBState<ComplianceDocument[]>(STORES.COMPLIANCE_DOCS, [])
|
||||
const [expenses = [], setExpenses] = useIndexedDBState<Expense[]>(STORES.EXPENSES, [])
|
||||
const [rateCards = [], setRateCards] = useIndexedDBState<RateCard[]>(STORES.RATE_CARDS, [])
|
||||
|
||||
const metrics: DashboardMetrics = useMemo(() => {
|
||||
const monthlyRevenue = invoices.reduce((sum, inv) => sum + (inv.amount || 0), 0)
|
||||
|
||||
179
src/hooks/use-crud.ts
Normal file
179
src/hooks/use-crud.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { indexedDB, BaseEntity } from '@/lib/indexed-db'
|
||||
|
||||
interface CRUDHookResult<T extends BaseEntity> {
|
||||
data: T[]
|
||||
isLoading: boolean
|
||||
error: Error | null
|
||||
create: (entity: T) => Promise<T>
|
||||
read: (id: string) => Promise<T | null>
|
||||
readAll: () => Promise<T[]>
|
||||
readByIndex: (indexName: string, value: any) => Promise<T[]>
|
||||
update: (entity: T) => Promise<T>
|
||||
remove: (id: string) => Promise<void>
|
||||
removeAll: () => Promise<void>
|
||||
bulkCreate: (entities: T[]) => Promise<T[]>
|
||||
bulkUpdate: (entities: T[]) => Promise<T[]>
|
||||
query: (predicate: (entity: T) => boolean) => Promise<T[]>
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
export function useCRUD<T extends BaseEntity>(storeName: string): CRUDHookResult<T> {
|
||||
const [data, setData] = useState<T[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const entities = await indexedDB.readAll<T>(storeName)
|
||||
setData(entities)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to load data')
|
||||
setError(error)
|
||||
throw error
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [storeName])
|
||||
|
||||
const create = useCallback(async (entity: T): Promise<T> => {
|
||||
setError(null)
|
||||
try {
|
||||
const created = await indexedDB.create(storeName, entity)
|
||||
await refresh()
|
||||
return created
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to create entity')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName, refresh])
|
||||
|
||||
const read = useCallback(async (id: string): Promise<T | null> => {
|
||||
setError(null)
|
||||
try {
|
||||
return await indexedDB.read<T>(storeName, id)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to read entity')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName])
|
||||
|
||||
const readAll = useCallback(async (): Promise<T[]> => {
|
||||
setError(null)
|
||||
try {
|
||||
const entities = await indexedDB.readAll<T>(storeName)
|
||||
setData(entities)
|
||||
return entities
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to read all entities')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName])
|
||||
|
||||
const readByIndex = useCallback(async (indexName: string, value: any): Promise<T[]> => {
|
||||
setError(null)
|
||||
try {
|
||||
return await indexedDB.readByIndex<T>(storeName, indexName, value)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to read entities by index')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName])
|
||||
|
||||
const update = useCallback(async (entity: T): Promise<T> => {
|
||||
setError(null)
|
||||
try {
|
||||
const updated = await indexedDB.update(storeName, entity)
|
||||
await refresh()
|
||||
return updated
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to update entity')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName, refresh])
|
||||
|
||||
const remove = useCallback(async (id: string): Promise<void> => {
|
||||
setError(null)
|
||||
try {
|
||||
await indexedDB.delete(storeName, id)
|
||||
await refresh()
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to delete entity')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName, refresh])
|
||||
|
||||
const removeAll = useCallback(async (): Promise<void> => {
|
||||
setError(null)
|
||||
try {
|
||||
await indexedDB.deleteAll(storeName)
|
||||
setData([])
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to delete all entities')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName])
|
||||
|
||||
const bulkCreate = useCallback(async (entities: T[]): Promise<T[]> => {
|
||||
setError(null)
|
||||
try {
|
||||
const created = await indexedDB.bulkCreate(storeName, entities)
|
||||
await refresh()
|
||||
return created
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to bulk create entities')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName, refresh])
|
||||
|
||||
const bulkUpdate = useCallback(async (entities: T[]): Promise<T[]> => {
|
||||
setError(null)
|
||||
try {
|
||||
const updated = await indexedDB.bulkUpdate(storeName, entities)
|
||||
await refresh()
|
||||
return updated
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to bulk update entities')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName, refresh])
|
||||
|
||||
const query = useCallback(async (predicate: (entity: T) => boolean): Promise<T[]> => {
|
||||
setError(null)
|
||||
try {
|
||||
return await indexedDB.query<T>(storeName, predicate)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error('Failed to query entities')
|
||||
setError(error)
|
||||
throw error
|
||||
}
|
||||
}, [storeName])
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
create,
|
||||
read,
|
||||
readAll,
|
||||
readByIndex,
|
||||
update,
|
||||
remove,
|
||||
removeAll,
|
||||
bulkCreate,
|
||||
bulkUpdate,
|
||||
query,
|
||||
refresh,
|
||||
}
|
||||
}
|
||||
39
src/hooks/use-entity-crud.ts
Normal file
39
src/hooks/use-entity-crud.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useCRUD } from './use-crud'
|
||||
import { STORES } from '@/lib/indexed-db'
|
||||
import type {
|
||||
Timesheet,
|
||||
Invoice,
|
||||
PayrollRun,
|
||||
Worker,
|
||||
ComplianceDocument,
|
||||
Expense,
|
||||
RateCard
|
||||
} from '@/lib/types'
|
||||
|
||||
export function useTimesheetsCRUD() {
|
||||
return useCRUD<Timesheet>(STORES.TIMESHEETS)
|
||||
}
|
||||
|
||||
export function useInvoicesCRUD() {
|
||||
return useCRUD<Invoice>(STORES.INVOICES)
|
||||
}
|
||||
|
||||
export function usePayrollRunsCRUD() {
|
||||
return useCRUD<PayrollRun>(STORES.PAYROLL_RUNS)
|
||||
}
|
||||
|
||||
export function useWorkersCRUD() {
|
||||
return useCRUD<Worker>(STORES.WORKERS)
|
||||
}
|
||||
|
||||
export function useComplianceDocsCRUD() {
|
||||
return useCRUD<ComplianceDocument>(STORES.COMPLIANCE_DOCS)
|
||||
}
|
||||
|
||||
export function useExpensesCRUD() {
|
||||
return useCRUD<Expense>(STORES.EXPENSES)
|
||||
}
|
||||
|
||||
export function useRateCardsCRUD() {
|
||||
return useCRUD<RateCard>(STORES.RATE_CARDS)
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { indexedDB } from '@/lib/indexed-db'
|
||||
import { indexedDB, STORES } from '@/lib/indexed-db'
|
||||
|
||||
const ENTITY_STORE_NAMES = [
|
||||
STORES.TIMESHEETS,
|
||||
STORES.INVOICES,
|
||||
STORES.PAYROLL_RUNS,
|
||||
STORES.WORKERS,
|
||||
STORES.COMPLIANCE_DOCS,
|
||||
STORES.EXPENSES,
|
||||
STORES.RATE_CARDS,
|
||||
] as string[]
|
||||
|
||||
export function useIndexedDBState<T>(
|
||||
key: string,
|
||||
@@ -7,11 +17,25 @@ export function useIndexedDBState<T>(
|
||||
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
|
||||
const [state, setState] = useState<T>(defaultValue)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const isEntityStore = ENTITY_STORE_NAMES.includes(key)
|
||||
|
||||
useEffect(() => {
|
||||
const loadState = async () => {
|
||||
try {
|
||||
const storedValue = await indexedDB.getAppState<T>(key)
|
||||
let storedValue: T | null = null
|
||||
|
||||
if (isEntityStore) {
|
||||
try {
|
||||
const entities = await indexedDB.readAll(key)
|
||||
storedValue = (entities.length > 0 ? entities : null) as T | null
|
||||
} catch (error) {
|
||||
console.warn(`Store "${key}" not accessible, using default value`, error)
|
||||
storedValue = null
|
||||
}
|
||||
} else {
|
||||
storedValue = await indexedDB.getAppState<T>(key)
|
||||
}
|
||||
|
||||
if (storedValue !== null) {
|
||||
setState(storedValue)
|
||||
}
|
||||
@@ -23,7 +47,7 @@ export function useIndexedDBState<T>(
|
||||
}
|
||||
|
||||
loadState()
|
||||
}, [key])
|
||||
}, [key, isEntityStore])
|
||||
|
||||
const updateState = useCallback((value: T | ((prev: T) => T)) => {
|
||||
setState(prevState => {
|
||||
@@ -32,21 +56,39 @@ export function useIndexedDBState<T>(
|
||||
: value
|
||||
|
||||
if (isInitialized) {
|
||||
indexedDB.saveAppState(key, newState).catch(error => {
|
||||
console.error(`Failed to save state for key "${key}":`, error)
|
||||
})
|
||||
if (isEntityStore && Array.isArray(newState)) {
|
||||
indexedDB.deleteAll(key)
|
||||
.then(() => {
|
||||
if (newState.length > 0) {
|
||||
return indexedDB.bulkCreate(key, newState)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Failed to save entities for store "${key}":`, error)
|
||||
})
|
||||
} else {
|
||||
indexedDB.saveAppState(key, newState).catch(error => {
|
||||
console.error(`Failed to save state for key "${key}":`, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return newState
|
||||
})
|
||||
}, [key, isInitialized])
|
||||
}, [key, isInitialized, isEntityStore])
|
||||
|
||||
const deleteState = useCallback(() => {
|
||||
setState(defaultValue)
|
||||
indexedDB.deleteAppState(key).catch(error => {
|
||||
console.error(`Failed to delete state for key "${key}":`, error)
|
||||
})
|
||||
}, [key, defaultValue])
|
||||
if (isEntityStore) {
|
||||
indexedDB.deleteAll(key).catch(error => {
|
||||
console.error(`Failed to delete entities from store "${key}":`, error)
|
||||
})
|
||||
} else {
|
||||
indexedDB.deleteAppState(key).catch(error => {
|
||||
console.error(`Failed to delete state for key "${key}":`, error)
|
||||
})
|
||||
}
|
||||
}, [key, defaultValue, isEntityStore])
|
||||
|
||||
return [state, updateState, deleteState]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
const DB_NAME = 'WorkForceProDB'
|
||||
const DB_VERSION = 1
|
||||
const DB_VERSION = 2
|
||||
const SESSION_STORE = 'sessions'
|
||||
const APP_STATE_STORE = 'appState'
|
||||
const TIMESHEETS_STORE = 'timesheets'
|
||||
const INVOICES_STORE = 'invoices'
|
||||
const PAYROLL_RUNS_STORE = 'payrollRuns'
|
||||
const WORKERS_STORE = 'workers'
|
||||
const COMPLIANCE_DOCS_STORE = 'complianceDocs'
|
||||
const EXPENSES_STORE = 'expenses'
|
||||
const RATE_CARDS_STORE = 'rateCards'
|
||||
|
||||
interface SessionData {
|
||||
id: string
|
||||
@@ -24,6 +31,11 @@ interface AppStateData {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
interface BaseEntity {
|
||||
id: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
class IndexedDBManager {
|
||||
private db: IDBDatabase | null = null
|
||||
private initPromise: Promise<void> | null = null
|
||||
@@ -56,6 +68,52 @@ class IndexedDBManager {
|
||||
if (!db.objectStoreNames.contains(APP_STATE_STORE)) {
|
||||
db.createObjectStore(APP_STATE_STORE, { keyPath: 'key' })
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(TIMESHEETS_STORE)) {
|
||||
const timesheetsStore = db.createObjectStore(TIMESHEETS_STORE, { keyPath: 'id' })
|
||||
timesheetsStore.createIndex('workerId', 'workerId', { unique: false })
|
||||
timesheetsStore.createIndex('status', 'status', { unique: false })
|
||||
timesheetsStore.createIndex('weekEnding', 'weekEnding', { unique: false })
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(INVOICES_STORE)) {
|
||||
const invoicesStore = db.createObjectStore(INVOICES_STORE, { keyPath: 'id' })
|
||||
invoicesStore.createIndex('clientId', 'clientId', { unique: false })
|
||||
invoicesStore.createIndex('status', 'status', { unique: false })
|
||||
invoicesStore.createIndex('invoiceDate', 'invoiceDate', { unique: false })
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(PAYROLL_RUNS_STORE)) {
|
||||
const payrollStore = db.createObjectStore(PAYROLL_RUNS_STORE, { keyPath: 'id' })
|
||||
payrollStore.createIndex('status', 'status', { unique: false })
|
||||
payrollStore.createIndex('periodEnding', 'periodEnding', { unique: false })
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(WORKERS_STORE)) {
|
||||
const workersStore = db.createObjectStore(WORKERS_STORE, { keyPath: 'id' })
|
||||
workersStore.createIndex('status', 'status', { unique: false })
|
||||
workersStore.createIndex('email', 'email', { unique: false })
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(COMPLIANCE_DOCS_STORE)) {
|
||||
const complianceStore = db.createObjectStore(COMPLIANCE_DOCS_STORE, { keyPath: 'id' })
|
||||
complianceStore.createIndex('workerId', 'workerId', { unique: false })
|
||||
complianceStore.createIndex('status', 'status', { unique: false })
|
||||
complianceStore.createIndex('expiryDate', 'expiryDate', { unique: false })
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(EXPENSES_STORE)) {
|
||||
const expensesStore = db.createObjectStore(EXPENSES_STORE, { keyPath: 'id' })
|
||||
expensesStore.createIndex('workerId', 'workerId', { unique: false })
|
||||
expensesStore.createIndex('status', 'status', { unique: false })
|
||||
expensesStore.createIndex('date', 'date', { unique: false })
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(RATE_CARDS_STORE)) {
|
||||
const rateCardsStore = db.createObjectStore(RATE_CARDS_STORE, { keyPath: 'id' })
|
||||
rateCardsStore.createIndex('clientId', 'clientId', { unique: false })
|
||||
rateCardsStore.createIndex('role', 'role', { unique: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -273,7 +331,232 @@ class IndexedDBManager {
|
||||
this.initPromise = null
|
||||
}
|
||||
}
|
||||
|
||||
async create<T extends BaseEntity>(storeName: string, entity: T): Promise<T> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.add(entity)
|
||||
|
||||
request.onsuccess = () => resolve(entity)
|
||||
request.onerror = () => reject(new Error(`Failed to create entity in ${storeName}`))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async read<T extends BaseEntity>(storeName: string, id: string): Promise<T | null> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readonly')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.get(id)
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null)
|
||||
request.onerror = () => reject(new Error(`Failed to read entity from ${storeName}`))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async readAll<T extends BaseEntity>(storeName: string): Promise<T[]> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readonly')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.getAll()
|
||||
|
||||
request.onsuccess = () => resolve(request.result || [])
|
||||
request.onerror = () => reject(new Error(`Failed to read all entities from ${storeName}`))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async readByIndex<T extends BaseEntity>(
|
||||
storeName: string,
|
||||
indexName: string,
|
||||
value: any
|
||||
): Promise<T[]> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readonly')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const index = store.index(indexName)
|
||||
const request = index.getAll(value)
|
||||
|
||||
request.onsuccess = () => resolve(request.result || [])
|
||||
request.onerror = () => reject(new Error(`Failed to read entities by index from ${storeName}`))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async update<T extends BaseEntity>(storeName: string, entity: T): Promise<T> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.put(entity)
|
||||
|
||||
request.onsuccess = () => resolve(entity)
|
||||
request.onerror = () => reject(new Error(`Failed to update entity in ${storeName}`))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async delete(storeName: string, id: string): Promise<void> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.delete(id)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(new Error(`Failed to delete entity from ${storeName}`))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async deleteAll(storeName: string): Promise<void> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.clear()
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(new Error(`Failed to clear store ${storeName}`))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async bulkCreate<T extends BaseEntity>(storeName: string, entities: T[]): Promise<T[]> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
let completed = 0
|
||||
const errors: Error[] = []
|
||||
|
||||
entities.forEach(entity => {
|
||||
const request = store.add(entity)
|
||||
request.onsuccess = () => {
|
||||
completed++
|
||||
if (completed === entities.length) {
|
||||
if (errors.length > 0) {
|
||||
reject(new Error(`Failed to create ${errors.length} entities in ${storeName}`))
|
||||
} else {
|
||||
resolve(entities)
|
||||
}
|
||||
}
|
||||
}
|
||||
request.onerror = () => {
|
||||
errors.push(new Error(`Failed to create entity with id ${entity.id}`))
|
||||
completed++
|
||||
if (completed === entities.length) {
|
||||
reject(new Error(`Failed to create ${errors.length} entities in ${storeName}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (entities.length === 0) {
|
||||
resolve([])
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async bulkUpdate<T extends BaseEntity>(storeName: string, entities: T[]): Promise<T[]> {
|
||||
const db = await this.ensureDb()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
let completed = 0
|
||||
const errors: Error[] = []
|
||||
|
||||
entities.forEach(entity => {
|
||||
const request = store.put(entity)
|
||||
request.onsuccess = () => {
|
||||
completed++
|
||||
if (completed === entities.length) {
|
||||
if (errors.length > 0) {
|
||||
reject(new Error(`Failed to update ${errors.length} entities in ${storeName}`))
|
||||
} else {
|
||||
resolve(entities)
|
||||
}
|
||||
}
|
||||
}
|
||||
request.onerror = () => {
|
||||
errors.push(new Error(`Failed to update entity with id ${entity.id}`))
|
||||
completed++
|
||||
if (completed === entities.length) {
|
||||
reject(new Error(`Failed to update ${errors.length} entities in ${storeName}`))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (entities.length === 0) {
|
||||
resolve([])
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async query<T extends BaseEntity>(
|
||||
storeName: string,
|
||||
predicate: (entity: T) => boolean
|
||||
): Promise<T[]> {
|
||||
const all = await this.readAll<T>(storeName)
|
||||
return all.filter(predicate)
|
||||
}
|
||||
}
|
||||
|
||||
export const indexedDB = new IndexedDBManager()
|
||||
export type { SessionData, AppStateData }
|
||||
|
||||
export const STORES = {
|
||||
SESSIONS: SESSION_STORE,
|
||||
APP_STATE: APP_STATE_STORE,
|
||||
TIMESHEETS: TIMESHEETS_STORE,
|
||||
INVOICES: INVOICES_STORE,
|
||||
PAYROLL_RUNS: PAYROLL_RUNS_STORE,
|
||||
WORKERS: WORKERS_STORE,
|
||||
COMPLIANCE_DOCS: COMPLIANCE_DOCS_STORE,
|
||||
EXPENSES: EXPENSES_STORE,
|
||||
RATE_CARDS: RATE_CARDS_STORE,
|
||||
} as const
|
||||
|
||||
export type { SessionData, AppStateData, BaseEntity }
|
||||
|
||||
Reference in New Issue
Block a user