-
-
- {steps.map((step, index) => {
- const isComplete = index < currentStep
- const isCurrent = index === currentStep
- const isClickable = onStepClick && index <= currentStep
+export interface StepperProps {
+ steps: Step[]
+ orientation?: 'horizontal' | 'vertical'
+ className?: string
+}
- return (
-
- {index !== steps.length - 1 && (
-
- )}
- isClickable && onStepClick(index)}
- disabled={!isClickable}
- className={cn(
- 'group relative flex flex-col items-start',
- isClickable && 'cursor-pointer',
- !isClickable && 'cursor-default'
- )}
- >
-
-
- {index + 1}
-
-
-
-
- {step.label}
-
- {step.description && (
-
- {step.description}
-
- )}
-
-
-
- )
- })}
-
-
+export function Stepper({ steps, orientation = 'horizontal', className }: StepperProps) {
+ return (
+
+ {steps.map((step, index) => (
+
+
+ {index < steps.length - 1 && (
+
+ )}
+
+ ))}
+
+ )
+}
+
+function StepItem({
+ step,
+ index,
+ orientation
+}: {
+ step: Step
+ index: number
+ orientation: 'horizontal' | 'vertical'
+}) {
+ return (
+
+
+
+ {step.status === 'completed' ? (
+
+ ) : (
+ {index + 1}
+ )}
+
- )
- }
-)
+
+
+ {step.label}
+
+ {step.description && (
+ {step.description}
+ )}
+
+
+ )
+}
-Stepper.displayName = 'Stepper'
+function StepConnector({
+ orientation,
+ completed
+}: {
+ orientation: 'horizontal' | 'vertical'
+ completed: boolean
+}) {
+ return (
+
+
+ {completed && (
+
+ )}
+
+
+ )
+}
diff --git a/src/components/ui/toolbar.tsx b/src/components/ui/toolbar.tsx
new file mode 100644
index 0000000..561b9aa
--- /dev/null
+++ b/src/components/ui/toolbar.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react'
+import { cn } from '@/lib/utils'
+
+export interface ToolbarProps extends React.HTMLAttributes
{
+ children: React.ReactNode
+ className?: string
+}
+
+export function Toolbar({ children, className, ...props }: ToolbarProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export interface ToolbarSectionProps extends React.HTMLAttributes {
+ children: React.ReactNode
+ className?: string
+}
+
+export function ToolbarSection({ children, className, ...props }: ToolbarSectionProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export interface ToolbarSeparatorProps extends React.HTMLAttributes {
+ className?: string
+}
+
+export function ToolbarSeparator({ className, ...props }: ToolbarSeparatorProps) {
+ return (
+
+ )
+}
diff --git a/src/hooks/EXTENDED_HOOKS.md b/src/hooks/EXTENDED_HOOKS.md
new file mode 100644
index 0000000..1503ae6
--- /dev/null
+++ b/src/hooks/EXTENDED_HOOKS.md
@@ -0,0 +1,236 @@
+# Custom Hooks Library - Extended
+
+This document describes the newly added custom hooks to the WorkForce Pro platform.
+
+## Batch Operations
+
+### `useBatchActions`
+Manage batch selection and operations on items with IDs.
+
+```tsx
+const {
+ selectedIds,
+ selectedCount,
+ toggleSelection,
+ selectAll,
+ clearSelection,
+ isSelected,
+ hasSelection
+} = useBatchActions()
+```
+
+**Use cases:**
+- Bulk approve timesheets
+- Batch invoice sending
+- Multi-select delete operations
+
+## Date Management
+
+### `useDateRange(initialRange?)`
+Handle date range selection with presets (today, this week, last month, etc.).
+
+```tsx
+const {
+ dateRange,
+ preset,
+ applyPreset,
+ setCustomRange
+} = useDateRange()
+
+applyPreset('last30Days')
+```
+
+**Presets:** `today`, `yesterday`, `thisWeek`, `lastWeek`, `thisMonth`, `lastMonth`, `last7Days`, `last30Days`, `custom`
+
+## Data Export
+
+### `useExport()`
+Export data to CSV or JSON formats.
+
+```tsx
+const { exportToCSV, exportToJSON } = useExport()
+
+exportToCSV(invoices, 'invoices-2024')
+exportToJSON(timesheets, 'timesheets-backup')
+```
+
+## Currency Formatting
+
+### `useCurrency(currency, options)`
+Format and parse currency values with internationalization support.
+
+```tsx
+const { format, parse, symbol, code } = useCurrency('GBP', {
+ locale: 'en-GB',
+ showSymbol: true
+})
+
+format(1250.50) // "£1,250.50"
+```
+
+## Permissions & Authorization
+
+### `usePermissions(userRole)`
+Check user permissions based on role.
+
+```tsx
+const { hasPermission, hasAnyPermission, hasAllPermissions } = usePermissions('manager')
+
+if (hasPermission('invoices.send')) {
+ // Show send button
+}
+```
+
+**Roles:** `admin`, `manager`, `accountant`, `viewer`
+
+## Data Grid Management
+
+### `useDataGrid(options)`
+Advanced data grid with sorting, filtering, and pagination.
+
+```tsx
+const {
+ data,
+ totalRows,
+ currentPage,
+ setCurrentPage,
+ sortConfig,
+ handleSort,
+ filters,
+ handleFilter,
+ clearFilters
+} = useDataGrid({
+ data: timesheets,
+ columns: columnConfig,
+ pageSize: 20
+})
+```
+
+## Keyboard Shortcuts
+
+### `useHotkeys(configs)`
+Register keyboard shortcuts for actions.
+
+```tsx
+useHotkeys([
+ { keys: 'ctrl+s', callback: handleSave, description: 'Save' },
+ { keys: 'ctrl+k', callback: openSearch, description: 'Search' }
+])
+```
+
+## Auto-Save
+
+### `useAutoSave(data, onSave, delay)`
+Automatically save data after changes with debouncing.
+
+```tsx
+useAutoSave(formData, async (data) => {
+ await saveToServer(data)
+}, 2000)
+```
+
+## Multi-Select
+
+### `useMultiSelect(items)`
+Advanced multi-selection with range support.
+
+```tsx
+const {
+ selectedIds,
+ toggle,
+ selectRange,
+ selectAll,
+ deselectAll,
+ getSelectedItems,
+ isAllSelected
+} = useMultiSelect(workers)
+```
+
+## Column Visibility
+
+### `useColumnVisibility(initialColumns)`
+Manage visible/hidden columns in tables.
+
+```tsx
+const {
+ visibleColumns,
+ toggleColumn,
+ showAll,
+ hideAll,
+ reorderColumns,
+ resizeColumn
+} = useColumnVisibility(columns)
+```
+
+## Form Validation
+
+### `useValidation(initialValues)`
+Form validation with rules and error tracking.
+
+```tsx
+const {
+ values,
+ errors,
+ touched,
+ setValue,
+ validate,
+ isValid
+} = useValidation({ email: '', amount: 0 })
+
+const rules = {
+ email: [{ validate: (v) => v.includes('@'), message: 'Invalid email' }]
+}
+validate(rules)
+```
+
+## Best Practices
+
+1. **Performance:** Use hooks with proper dependencies to avoid unnecessary re-renders
+2. **Type Safety:** Always provide generic types for hooks that accept data
+3. **Composition:** Combine multiple hooks for complex features
+4. **Cleanup:** Hooks handle cleanup automatically, but be mindful of async operations
+
+## Examples
+
+### Bulk Invoice Processing
+```tsx
+function InvoiceList() {
+ const { selectedIds, toggleSelection, selectAll } = useBatchActions()
+ const { exportToCSV } = useExport()
+
+ const handleBulkExport = () => {
+ const selected = invoices.filter(inv => selectedIds.has(inv.id))
+ exportToCSV(selected, 'bulk-invoices')
+ }
+
+ return (
+
+ Select All
+ Export Selected
+ {/* ... */}
+
+ )
+}
+```
+
+### Advanced Filtering
+```tsx
+function TimesheetTable() {
+ const { dateRange, applyPreset } = useDateRange()
+ const { data, handleFilter, handleSort } = useDataGrid({
+ data: timesheets.filter(t =>
+ new Date(t.weekEnding) >= dateRange.from &&
+ new Date(t.weekEnding) <= dateRange.to
+ ),
+ columns,
+ pageSize: 25
+ })
+
+ return (
+
+
+
+
+ )
+}
+```
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 0db828c..cd12d8e 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -38,6 +38,17 @@ export { useArray } from './use-array'
export { useTimeout } from './use-timeout'
export { useMap } from './use-map'
export { useSet } from './use-set'
+export { useBatchActions } from './use-batch-actions'
+export { useDateRange } from './use-date-range'
+export { useExport } from './use-export'
+export { useCurrency } from './use-currency'
+export { usePermissions } from './use-permissions'
+export { useDataGrid } from './use-data-grid'
+export { useHotkeys } from './use-hotkeys'
+export { useAutoSave } from './use-auto-save'
+export { useMultiSelect } from './use-multi-select'
+export { useColumnVisibility } from './use-column-visibility'
+export { useValidation } from './use-validation'
export type { AsyncState } from './use-async'
export type { FormErrors } from './use-form-validation'
@@ -54,3 +65,12 @@ export type { UseDisclosureReturn } from './use-disclosure'
export type { UseClipboardOptions } from './use-clipboard'
export type { UseDownloadReturn, DownloadFormat } from './use-download'
export type { UseIntervalOptions } from './use-interval'
+export type { DateRangePreset, DateRange } from './use-date-range'
+export type { ExportFormat } from './use-export'
+export type { CurrencyFormatOptions } from './use-currency'
+export type { Permission, Role } from './use-permissions'
+export type { DataGridColumn, DataGridOptions } from './use-data-grid'
+export type { HotkeyConfig } from './use-hotkeys'
+export type { ColumnConfig } from './use-column-visibility'
+export type { ValidationRule, FieldConfig } from './use-validation'
+
diff --git a/src/hooks/use-auto-save.ts b/src/hooks/use-auto-save.ts
new file mode 100644
index 0000000..7e8641c
--- /dev/null
+++ b/src/hooks/use-auto-save.ts
@@ -0,0 +1,30 @@
+import { useEffect, useRef } from 'react'
+
+export function useAutoSave(
+ data: T,
+ onSave: (data: T) => void | Promise,
+ delay: number = 2000
+) {
+ const timeoutRef = useRef | undefined>(undefined)
+ const dataRef = useRef(data)
+
+ useEffect(() => {
+ dataRef.current = data
+ }, [data])
+
+ useEffect(() => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+
+ timeoutRef.current = setTimeout(() => {
+ onSave(dataRef.current)
+ }, delay)
+
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+ }
+ }, [data, delay, onSave])
+}
diff --git a/src/hooks/use-batch-actions.ts b/src/hooks/use-batch-actions.ts
new file mode 100644
index 0000000..dfde000
--- /dev/null
+++ b/src/hooks/use-batch-actions.ts
@@ -0,0 +1,48 @@
+import { useState, useCallback } from 'react'
+
+export function useBatchActions() {
+ const [selectedIds, setSelectedIds] = useState>(new Set())
+
+ const toggleSelection = useCallback((id: string) => {
+ setSelectedIds(prev => {
+ const next = new Set(prev)
+ if (next.has(id)) {
+ next.delete(id)
+ } else {
+ next.add(id)
+ }
+ return next
+ })
+ }, [])
+
+ const selectAll = useCallback((items: T[]) => {
+ setSelectedIds(new Set(items.map(item => item.id)))
+ }, [])
+
+ const clearSelection = useCallback(() => {
+ setSelectedIds(new Set())
+ }, [])
+
+ const isSelected = useCallback((id: string) => {
+ return selectedIds.has(id)
+ }, [selectedIds])
+
+ const toggleAll = useCallback((items: T[]) => {
+ if (selectedIds.size === items.length) {
+ clearSelection()
+ } else {
+ selectAll(items)
+ }
+ }, [selectedIds.size, selectAll, clearSelection])
+
+ return {
+ selectedIds,
+ selectedCount: selectedIds.size,
+ toggleSelection,
+ selectAll,
+ clearSelection,
+ isSelected,
+ toggleAll,
+ hasSelection: selectedIds.size > 0
+ }
+}
diff --git a/src/hooks/use-column-visibility.ts b/src/hooks/use-column-visibility.ts
new file mode 100644
index 0000000..6efd695
--- /dev/null
+++ b/src/hooks/use-column-visibility.ts
@@ -0,0 +1,58 @@
+import { useState, useCallback } from 'react'
+
+export interface ColumnConfig {
+ id: string
+ label: string
+ visible: boolean
+ width?: number
+ order: number
+}
+
+export function useColumnVisibility(initialColumns: ColumnConfig[]) {
+ const [columns, setColumns] = useState(initialColumns)
+
+ const toggleColumn = useCallback((id: string) => {
+ setColumns(prev =>
+ prev.map(col =>
+ col.id === id ? { ...col, visible: !col.visible } : col
+ )
+ )
+ }, [])
+
+ const showAll = useCallback(() => {
+ setColumns(prev => prev.map(col => ({ ...col, visible: true })))
+ }, [])
+
+ const hideAll = useCallback(() => {
+ setColumns(prev => prev.map(col => ({ ...col, visible: false })))
+ }, [])
+
+ const reorderColumns = useCallback((fromIndex: number, toIndex: number) => {
+ setColumns(prev => {
+ const newColumns = [...prev]
+ const [removed] = newColumns.splice(fromIndex, 1)
+ newColumns.splice(toIndex, 0, removed)
+ return newColumns.map((col, index) => ({ ...col, order: index }))
+ })
+ }, [])
+
+ const resizeColumn = useCallback((id: string, width: number) => {
+ setColumns(prev =>
+ prev.map(col =>
+ col.id === id ? { ...col, width } : col
+ )
+ )
+ }, [])
+
+ const visibleColumns = columns.filter(col => col.visible).sort((a, b) => a.order - b.order)
+
+ return {
+ columns,
+ visibleColumns,
+ toggleColumn,
+ showAll,
+ hideAll,
+ reorderColumns,
+ resizeColumn
+ }
+}
diff --git a/src/hooks/use-currency.ts b/src/hooks/use-currency.ts
new file mode 100644
index 0000000..ceef083
--- /dev/null
+++ b/src/hooks/use-currency.ts
@@ -0,0 +1,45 @@
+import { useMemo } from 'react'
+
+export interface CurrencyFormatOptions {
+ locale?: string
+ showSymbol?: boolean
+ showCode?: boolean
+ minimumFractionDigits?: number
+ maximumFractionDigits?: number
+}
+
+export function useCurrency(currency: string = 'GBP', options: CurrencyFormatOptions = {}) {
+ const {
+ locale = 'en-GB',
+ showSymbol = true,
+ showCode = false,
+ minimumFractionDigits = 2,
+ maximumFractionDigits = 2
+ } = options
+
+ const formatter = useMemo(() => {
+ return new Intl.NumberFormat(locale, {
+ style: showSymbol ? 'currency' : 'decimal',
+ currency,
+ minimumFractionDigits,
+ maximumFractionDigits
+ })
+ }, [locale, currency, showSymbol, minimumFractionDigits, maximumFractionDigits])
+
+ const format = (amount: number): string => {
+ const formatted = formatter.format(amount)
+ return showCode ? `${formatted} ${currency}` : formatted
+ }
+
+ const parse = (value: string): number => {
+ const cleaned = value.replace(/[^0-9.-]/g, '')
+ return parseFloat(cleaned) || 0
+ }
+
+ return {
+ format,
+ parse,
+ symbol: showSymbol ? formatter.formatToParts(0).find(part => part.type === 'currency')?.value : '',
+ code: currency
+ }
+}
diff --git a/src/hooks/use-data-grid.ts b/src/hooks/use-data-grid.ts
new file mode 100644
index 0000000..3ccea88
--- /dev/null
+++ b/src/hooks/use-data-grid.ts
@@ -0,0 +1,88 @@
+import { useState, useMemo, useCallback } from 'react'
+
+export interface DataGridColumn {
+ key: string
+ label: string
+ sortable?: boolean
+ filterable?: boolean
+ width?: number
+ render?: (value: any, row: T) => React.ReactNode
+}
+
+export interface DataGridOptions {
+ data: T[]
+ columns: DataGridColumn[]
+ pageSize?: number
+ initialSort?: { key: string; direction: 'asc' | 'desc' }
+}
+
+export function useDataGrid>(options: DataGridOptions) {
+ const { data, columns, pageSize = 10, initialSort } = options
+
+ const [currentPage, setCurrentPage] = useState(1)
+ const [sortConfig, setSortConfig] = useState(initialSort || { key: '', direction: 'asc' as const })
+ const [filters, setFilters] = useState>({})
+
+ const filteredData = useMemo(() => {
+ return data.filter(row => {
+ return Object.entries(filters).every(([key, value]) => {
+ if (!value) return true
+ const cellValue = String(row[key] || '').toLowerCase()
+ return cellValue.includes(value.toLowerCase())
+ })
+ })
+ }, [data, filters])
+
+ const sortedData = useMemo(() => {
+ if (!sortConfig.key) return filteredData
+
+ return [...filteredData].sort((a, b) => {
+ const aVal = a[sortConfig.key]
+ const bVal = b[sortConfig.key]
+
+ if (aVal === bVal) return 0
+
+ const comparison = aVal > bVal ? 1 : -1
+ return sortConfig.direction === 'asc' ? comparison : -comparison
+ })
+ }, [filteredData, sortConfig])
+
+ const paginatedData = useMemo(() => {
+ const start = (currentPage - 1) * pageSize
+ const end = start + pageSize
+ return sortedData.slice(start, end)
+ }, [sortedData, currentPage, pageSize])
+
+ const totalPages = Math.ceil(sortedData.length / pageSize)
+
+ const handleSort = useCallback((key: string) => {
+ setSortConfig(prev => ({
+ key,
+ direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
+ }))
+ }, [])
+
+ const handleFilter = useCallback((key: string, value: string) => {
+ setFilters(prev => ({ ...prev, [key]: value }))
+ setCurrentPage(1)
+ }, [])
+
+ const clearFilters = useCallback(() => {
+ setFilters({})
+ setCurrentPage(1)
+ }, [])
+
+ return {
+ data: paginatedData,
+ totalRows: sortedData.length,
+ currentPage,
+ totalPages,
+ setCurrentPage,
+ sortConfig,
+ handleSort,
+ filters,
+ handleFilter,
+ clearFilters,
+ hasFilters: Object.values(filters).some(v => v)
+ }
+}
diff --git a/src/hooks/use-date-range.ts b/src/hooks/use-date-range.ts
new file mode 100644
index 0000000..9df8932
--- /dev/null
+++ b/src/hooks/use-date-range.ts
@@ -0,0 +1,78 @@
+import { useState, useCallback } from 'react'
+import { addDays, startOfWeek, endOfWeek, startOfMonth, endOfMonth, subDays, subMonths } from 'date-fns'
+
+export type DateRangePreset = 'today' | 'yesterday' | 'thisWeek' | 'lastWeek' | 'thisMonth' | 'lastMonth' | 'last7Days' | 'last30Days' | 'custom'
+
+export interface DateRange {
+ from: Date
+ to: Date
+}
+
+export function useDateRange(initialRange?: DateRange) {
+ const [dateRange, setDateRange] = useState(
+ initialRange || {
+ from: startOfMonth(new Date()),
+ to: endOfMonth(new Date())
+ }
+ )
+ const [preset, setPreset] = useState('thisMonth')
+
+ const applyPreset = useCallback((presetName: DateRangePreset) => {
+ const now = new Date()
+ let from: Date
+ let to: Date
+
+ switch (presetName) {
+ case 'today':
+ from = new Date(now.setHours(0, 0, 0, 0))
+ to = new Date(now.setHours(23, 59, 59, 999))
+ break
+ case 'yesterday':
+ from = subDays(new Date(now.setHours(0, 0, 0, 0)), 1)
+ to = subDays(new Date(now.setHours(23, 59, 59, 999)), 1)
+ break
+ case 'thisWeek':
+ from = startOfWeek(now, { weekStartsOn: 1 })
+ to = endOfWeek(now, { weekStartsOn: 1 })
+ break
+ case 'lastWeek':
+ from = startOfWeek(subDays(now, 7), { weekStartsOn: 1 })
+ to = endOfWeek(subDays(now, 7), { weekStartsOn: 1 })
+ break
+ case 'thisMonth':
+ from = startOfMonth(now)
+ to = endOfMonth(now)
+ break
+ case 'lastMonth':
+ from = startOfMonth(subMonths(now, 1))
+ to = endOfMonth(subMonths(now, 1))
+ break
+ case 'last7Days':
+ from = subDays(now, 6)
+ to = now
+ break
+ case 'last30Days':
+ from = subDays(now, 29)
+ to = now
+ break
+ default:
+ return
+ }
+
+ setDateRange({ from, to })
+ setPreset(presetName)
+ }, [])
+
+ const setCustomRange = useCallback((range: DateRange) => {
+ setDateRange(range)
+ setPreset('custom')
+ }, [])
+
+ return {
+ dateRange,
+ preset,
+ applyPreset,
+ setCustomRange,
+ setDateRange: setCustomRange
+ }
+}
diff --git a/src/hooks/use-export.ts b/src/hooks/use-export.ts
new file mode 100644
index 0000000..45444fa
--- /dev/null
+++ b/src/hooks/use-export.ts
@@ -0,0 +1,56 @@
+import { useCallback } from 'react'
+import { toast } from 'sonner'
+
+export type ExportFormat = 'csv' | 'json' | 'xlsx'
+
+export function useExport() {
+ const exportToCSV = useCallback((data: any[], filename: string) => {
+ if (!data || data.length === 0) {
+ toast.error('No data to export')
+ return
+ }
+
+ const headers = Object.keys(data[0])
+ const csv = [
+ headers.join(','),
+ ...data.map(row =>
+ headers.map(header => {
+ const value = row[header]
+ const stringValue = value?.toString() || ''
+ return stringValue.includes(',') ? `"${stringValue}"` : stringValue
+ }).join(',')
+ )
+ ].join('\n')
+
+ downloadFile(csv, `${filename}.csv`, 'text/csv')
+ toast.success('Exported to CSV')
+ }, [])
+
+ const exportToJSON = useCallback((data: any[], filename: string) => {
+ if (!data || data.length === 0) {
+ toast.error('No data to export')
+ return
+ }
+
+ const json = JSON.stringify(data, null, 2)
+ downloadFile(json, `${filename}.json`, 'application/json')
+ toast.success('Exported to JSON')
+ }, [])
+
+ const downloadFile = (content: string, filename: string, mimeType: string) => {
+ const blob = new Blob([content], { type: mimeType })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ URL.revokeObjectURL(url)
+ }
+
+ return {
+ exportToCSV,
+ exportToJSON
+ }
+}
diff --git a/src/hooks/use-hotkeys.ts b/src/hooks/use-hotkeys.ts
new file mode 100644
index 0000000..eb92da3
--- /dev/null
+++ b/src/hooks/use-hotkeys.ts
@@ -0,0 +1,42 @@
+import { useEffect, useCallback } from 'react'
+
+export interface HotkeyConfig {
+ keys: string
+ callback: (event: KeyboardEvent) => void
+ description?: string
+ preventDefault?: boolean
+ enabled?: boolean
+}
+
+export function useHotkeys(configs: HotkeyConfig[]) {
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
+ configs.forEach(({ keys, callback, preventDefault = true, enabled = true }) => {
+ if (!enabled) return
+
+ const parts = keys.toLowerCase().split('+')
+ const key = parts[parts.length - 1]
+ const requiresCtrl = parts.includes('ctrl') || parts.includes('control')
+ const requiresShift = parts.includes('shift')
+ const requiresAlt = parts.includes('alt')
+ const requiresMeta = parts.includes('meta') || parts.includes('cmd')
+
+ const keyMatches = event.key.toLowerCase() === key
+ const ctrlMatches = requiresCtrl ? event.ctrlKey || event.metaKey : true
+ const shiftMatches = requiresShift ? event.shiftKey : true
+ const altMatches = requiresAlt ? event.altKey : true
+ const metaMatches = requiresMeta ? event.metaKey : true
+
+ if (keyMatches && ctrlMatches && shiftMatches && altMatches && metaMatches) {
+ if (preventDefault) {
+ event.preventDefault()
+ }
+ callback(event)
+ }
+ })
+ }, [configs])
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [handleKeyDown])
+}
diff --git a/src/hooks/use-multi-select.ts b/src/hooks/use-multi-select.ts
new file mode 100644
index 0000000..6122b0e
--- /dev/null
+++ b/src/hooks/use-multi-select.ts
@@ -0,0 +1,63 @@
+import { useState, useCallback } from 'react'
+
+export function useMultiSelect(items: T[]) {
+ const [selectedIds, setSelectedIds] = useState>(new Set())
+
+ const toggle = useCallback((id: string) => {
+ setSelectedIds(prev => {
+ const next = new Set(prev)
+ if (next.has(id)) {
+ next.delete(id)
+ } else {
+ next.add(id)
+ }
+ return next
+ })
+ }, [])
+
+ const selectRange = useCallback((fromId: string, toId: string) => {
+ const fromIndex = items.findIndex(item => item.id === fromId)
+ const toIndex = items.findIndex(item => item.id === toId)
+
+ if (fromIndex === -1 || toIndex === -1) return
+
+ const start = Math.min(fromIndex, toIndex)
+ const end = Math.max(fromIndex, toIndex)
+
+ setSelectedIds(prev => {
+ const next = new Set(prev)
+ for (let i = start; i <= end; i++) {
+ next.add(items[i].id)
+ }
+ return next
+ })
+ }, [items])
+
+ const selectAll = useCallback(() => {
+ setSelectedIds(new Set(items.map(item => item.id)))
+ }, [items])
+
+ const deselectAll = useCallback(() => {
+ setSelectedIds(new Set())
+ }, [])
+
+ const isSelected = useCallback((id: string) => {
+ return selectedIds.has(id)
+ }, [selectedIds])
+
+ const getSelectedItems = useCallback(() => {
+ return items.filter(item => selectedIds.has(item.id))
+ }, [items, selectedIds])
+
+ return {
+ selectedIds,
+ selectedCount: selectedIds.size,
+ toggle,
+ selectRange,
+ selectAll,
+ deselectAll,
+ isSelected,
+ getSelectedItems,
+ isAllSelected: selectedIds.size === items.length && items.length > 0
+ }
+}
diff --git a/src/hooks/use-permissions.ts b/src/hooks/use-permissions.ts
new file mode 100644
index 0000000..9a3f5f9
--- /dev/null
+++ b/src/hooks/use-permissions.ts
@@ -0,0 +1,81 @@
+import { useMemo } from 'react'
+
+export type Permission =
+ | 'timesheets.view'
+ | 'timesheets.approve'
+ | 'timesheets.create'
+ | 'timesheets.edit'
+ | 'invoices.view'
+ | 'invoices.create'
+ | 'invoices.send'
+ | 'payroll.view'
+ | 'payroll.process'
+ | 'compliance.view'
+ | 'compliance.upload'
+ | 'expenses.view'
+ | 'expenses.approve'
+ | 'reports.view'
+ | 'settings.manage'
+ | 'users.manage'
+
+export type Role = 'admin' | 'manager' | 'accountant' | 'viewer'
+
+const ROLE_PERMISSIONS: Record = {
+ admin: [
+ 'timesheets.view', 'timesheets.approve', 'timesheets.create', 'timesheets.edit',
+ 'invoices.view', 'invoices.create', 'invoices.send',
+ 'payroll.view', 'payroll.process',
+ 'compliance.view', 'compliance.upload',
+ 'expenses.view', 'expenses.approve',
+ 'reports.view',
+ 'settings.manage', 'users.manage'
+ ],
+ manager: [
+ 'timesheets.view', 'timesheets.approve', 'timesheets.create',
+ 'invoices.view', 'invoices.create',
+ 'payroll.view',
+ 'compliance.view', 'compliance.upload',
+ 'expenses.view', 'expenses.approve',
+ 'reports.view'
+ ],
+ accountant: [
+ 'timesheets.view',
+ 'invoices.view', 'invoices.create', 'invoices.send',
+ 'payroll.view', 'payroll.process',
+ 'expenses.view', 'expenses.approve',
+ 'reports.view'
+ ],
+ viewer: [
+ 'timesheets.view',
+ 'invoices.view',
+ 'payroll.view',
+ 'compliance.view',
+ 'expenses.view',
+ 'reports.view'
+ ]
+}
+
+export function usePermissions(userRole: Role = 'viewer') {
+ const permissions = useMemo(() => {
+ return new Set(ROLE_PERMISSIONS[userRole] || [])
+ }, [userRole])
+
+ const hasPermission = (permission: Permission): boolean => {
+ return permissions.has(permission)
+ }
+
+ const hasAnyPermission = (...perms: Permission[]): boolean => {
+ return perms.some(p => permissions.has(p))
+ }
+
+ const hasAllPermissions = (...perms: Permission[]): boolean => {
+ return perms.every(p => permissions.has(p))
+ }
+
+ return {
+ hasPermission,
+ hasAnyPermission,
+ hasAllPermissions,
+ permissions: Array.from(permissions)
+ }
+}
diff --git a/src/hooks/use-validation.ts b/src/hooks/use-validation.ts
new file mode 100644
index 0000000..6f58d98
--- /dev/null
+++ b/src/hooks/use-validation.ts
@@ -0,0 +1,74 @@
+import { useState, useCallback } from 'react'
+
+export interface ValidationRule {
+ validate: (value: T) => boolean
+ message: string
+}
+
+export interface FieldConfig {
+ value: T
+ rules?: ValidationRule[]
+}
+
+export function useValidation>(initialValues: T) {
+ const [values, setValues] = useState(initialValues)
+ const [errors, setErrors] = useState>>({})
+ const [touched, setTouched] = useState>>({})
+
+ const validateField = useCallback((name: keyof T, value: any, rules?: ValidationRule[]) => {
+ if (!rules || rules.length === 0) return ''
+
+ for (const rule of rules) {
+ if (!rule.validate(value)) {
+ return rule.message
+ }
+ }
+ return ''
+ }, [])
+
+ const setValue = useCallback((name: keyof T, value: any) => {
+ setValues(prev => ({ ...prev, [name]: value }))
+ }, [])
+
+ const setError = useCallback((name: keyof T, error: string) => {
+ setErrors(prev => ({ ...prev, [name]: error }))
+ }, [])
+
+ const setTouchedField = useCallback((name: keyof T) => {
+ setTouched(prev => ({ ...prev, [name]: true }))
+ }, [])
+
+ const validate = useCallback((fields: Partial[]>>) => {
+ const newErrors: Partial> = {}
+ let isValid = true
+
+ Object.entries(fields).forEach(([name, rules]) => {
+ const error = validateField(name as keyof T, values[name as keyof T], rules as ValidationRule[])
+ if (error) {
+ newErrors[name as keyof T] = error
+ isValid = false
+ }
+ })
+
+ setErrors(newErrors)
+ return isValid
+ }, [values, validateField])
+
+ const reset = useCallback(() => {
+ setValues(initialValues)
+ setErrors({})
+ setTouched({})
+ }, [initialValues])
+
+ return {
+ values,
+ errors,
+ touched,
+ setValue,
+ setError,
+ setTouchedField,
+ validate,
+ reset,
+ isValid: Object.keys(errors).length === 0
+ }
+}