diff --git a/src/components/TimesheetCard.tsx b/src/components/TimesheetCard.tsx index 389eb97..8efe837 100644 --- a/src/components/TimesheetCard.tsx +++ b/src/components/TimesheetCard.tsx @@ -5,13 +5,24 @@ import { CheckCircle, XCircle, Receipt, - CaretDown + CaretDown, + Trash } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' import { cn } from '@/lib/utils' import { usePermissions } from '@/hooks/use-permissions' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import type { Timesheet } from '@/lib/types' interface TimesheetCardProps { @@ -21,6 +32,7 @@ interface TimesheetCardProps { onCreateInvoice: (id: string) => void onAdjust?: (timesheet: Timesheet) => void onViewDetails?: (timesheet: Timesheet) => void + onDelete?: (id: string) => void } export function TimesheetCard({ @@ -29,10 +41,12 @@ export function TimesheetCard({ onReject, onCreateInvoice, onAdjust, - onViewDetails + onViewDetails, + onDelete }: TimesheetCardProps) { const { hasPermission } = usePermissions() const [showShifts, setShowShifts] = useState(false) + const [showDeleteDialog, setShowDeleteDialog] = useState(false) const statusConfig = { pending: { icon: ClockCounterClockwise, color: 'text-warning' }, @@ -197,9 +211,44 @@ export function TimesheetCard({ Create Invoice )} + {onDelete && hasPermission('timesheets.delete') && ( + + )} + + + e.stopPropagation()}> + + Delete Timesheet + + Are you sure you want to delete this timesheet for {timesheet.workerName}? This action cannot be undone. + + + + Cancel + { + onDelete?.(timesheet.id) + setShowDeleteDialog(false) + }} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Delete + + + + ) } diff --git a/src/components/ViewRouter.tsx b/src/components/ViewRouter.tsx index 6d3a6ec..3e5d3cd 100644 --- a/src/components/ViewRouter.tsx +++ b/src/components/ViewRouter.tsx @@ -120,16 +120,9 @@ export function ViewRouter({ case 'timesheets': return ( ) diff --git a/src/components/timesheets/TimesheetTabs.tsx b/src/components/timesheets/TimesheetTabs.tsx index 233b403..8851d10 100644 --- a/src/components/timesheets/TimesheetTabs.tsx +++ b/src/components/timesheets/TimesheetTabs.tsx @@ -11,6 +11,7 @@ interface TimesheetTabsProps { onCreateInvoice: (id: string) => void onAdjust: (timesheet: Timesheet) => void onViewDetails: (timesheet: Timesheet) => void + onDelete?: (id: string) => void } export function TimesheetTabs({ @@ -19,7 +20,8 @@ export function TimesheetTabs({ onReject, onCreateInvoice, onAdjust, - onViewDetails + onViewDetails, + onDelete }: TimesheetTabsProps) { return ( @@ -47,6 +49,7 @@ export function TimesheetTabs({ onCreateInvoice={onCreateInvoice} onAdjust={onAdjust} onViewDetails={onViewDetails} + onDelete={onDelete} /> ))} {filteredTimesheets.filter(t => t.status === 'pending').length === 0 && ( @@ -70,6 +73,7 @@ export function TimesheetTabs({ onCreateInvoice={onCreateInvoice} onAdjust={onAdjust} onViewDetails={onViewDetails} + onDelete={onDelete} /> ))} @@ -86,6 +90,7 @@ export function TimesheetTabs({ onCreateInvoice={onCreateInvoice} onAdjust={onAdjust} onViewDetails={onViewDetails} + onDelete={onDelete} /> ))} diff --git a/src/components/views/TimesheetsView.tsx b/src/components/views/TimesheetsView.tsx index 8692182..a219771 100644 --- a/src/components/views/TimesheetsView.tsx +++ b/src/components/views/TimesheetsView.tsx @@ -10,7 +10,8 @@ import { FileText, CalendarBlank, CurrencyDollar, - TrendUp + TrendUp, + Trash } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' @@ -29,47 +30,21 @@ import { AdvancedSearch, type FilterField } from '@/components/AdvancedSearch' import { TimesheetCreateDialogs } from '@/components/timesheets/TimesheetCreateDialogs' import { TimesheetTabs } from '@/components/timesheets/TimesheetTabs' import { useTimeTracking } from '@/hooks/use-time-tracking' +import { useTimesheetsCrud } from '@/hooks/use-timesheets-crud' +import { usePermissions } from '@/hooks/use-permissions' import { toast } from 'sonner' import type { Timesheet, TimesheetStatus, ShiftEntry } from '@/lib/types' interface TimesheetsViewProps { - timesheets: Timesheet[] searchQuery: string setSearchQuery: (query: string) => void - onApprove: (id: string) => void - onReject: (id: string) => void onCreateInvoice: (id: string) => void - onCreateTimesheet: (data: { - workerName: string - clientName: string - hours: number - rate: number - weekEnding: string - }) => void - onCreateDetailedTimesheet: (data: { - workerName: string - clientName: string - weekEnding: string - shifts: ShiftEntry[] - totalHours: number - totalAmount: number - baseRate: number - }) => void - onBulkImport: (csvData: string) => void - onAdjust: (timesheetId: string, adjustment: any) => void } export function TimesheetsView({ - timesheets, searchQuery, setSearchQuery, - onApprove, - onReject, - onCreateInvoice, - onCreateTimesheet, - onCreateDetailedTimesheet, - onBulkImport, - onAdjust + onCreateInvoice }: TimesheetsViewProps) { const [statusFilter, setStatusFilter] = useState<'all' | TimesheetStatus>('all') const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) @@ -78,6 +53,8 @@ export function TimesheetsView({ const [viewingTimesheet, setViewingTimesheet] = useState(null) const [showAnalytics, setShowAnalytics] = useState(false) + const { hasPermission } = usePermissions() + const { validateTimesheet, analyzeWorkingTime, @@ -85,6 +62,174 @@ export function TimesheetsView({ determineShiftType } = useTimeTracking() + const { + timesheets, + createTimesheet, + updateTimesheet, + deleteTimesheet, + bulkCreateTimesheets + } = useTimesheetsCrud() + + const handleCreateTimesheet = useCallback(async (data: { + workerName: string + clientName: string + hours: number + rate: number + weekEnding: string + }) => { + try { + await createTimesheet({ + workerId: `worker-${Date.now()}`, + workerName: data.workerName, + clientName: data.clientName, + hours: data.hours, + rate: data.rate, + amount: data.hours * data.rate, + weekEnding: data.weekEnding, + status: 'pending', + submittedDate: new Date().toISOString(), + shifts: [] + }) + toast.success('Timesheet created successfully') + setIsCreateDialogOpen(false) + } catch (error) { + toast.error('Failed to create timesheet') + console.error('Error creating timesheet:', error) + } + }, [createTimesheet]) + + const handleCreateDetailedTimesheet = useCallback(async (data: { + workerName: string + clientName: string + weekEnding: string + shifts: ShiftEntry[] + totalHours: number + totalAmount: number + baseRate: number + }) => { + try { + await createTimesheet({ + workerId: `worker-${Date.now()}`, + workerName: data.workerName, + clientName: data.clientName, + hours: data.totalHours, + rate: data.baseRate, + amount: data.totalAmount, + weekEnding: data.weekEnding, + status: 'pending', + submittedDate: new Date().toISOString(), + shifts: data.shifts + }) + toast.success('Detailed timesheet created successfully') + setIsCreateDialogOpen(false) + } catch (error) { + toast.error('Failed to create detailed timesheet') + console.error('Error creating detailed timesheet:', error) + } + }, [createTimesheet]) + + const handleBulkImport = useCallback(async (csvData: string) => { + try { + const lines = csvData.trim().split('\n') + const headers = lines[0].split(',').map(h => h.trim()) + + const timesheetsData = lines.slice(1).map(line => { + const values = line.split(',').map(v => v.trim()) + const timesheet: any = {} + + headers.forEach((header, index) => { + timesheet[header] = values[index] + }) + + return { + workerId: timesheet.workerId || `worker-${Date.now()}-${Math.random()}`, + workerName: timesheet.workerName || timesheet.worker, + clientName: timesheet.clientName || timesheet.client, + hours: parseFloat(timesheet.hours) || 0, + rate: parseFloat(timesheet.rate) || 0, + amount: parseFloat(timesheet.amount) || (parseFloat(timesheet.hours) * parseFloat(timesheet.rate)), + weekEnding: timesheet.weekEnding, + status: 'pending' as TimesheetStatus, + submittedDate: new Date().toISOString(), + shifts: [] + } + }) + + await bulkCreateTimesheets(timesheetsData) + toast.success(`${timesheetsData.length} timesheets imported successfully`) + setIsBulkImportOpen(false) + } catch (error) { + toast.error('Failed to import timesheets') + console.error('Error importing timesheets:', error) + } + }, [bulkCreateTimesheets]) + + const handleApprove = useCallback(async (id: string) => { + if (!hasPermission('timesheets.approve')) { + toast.error('You do not have permission to approve timesheets') + return + } + + try { + await updateTimesheet(id, { + status: 'approved', + approvedDate: new Date().toISOString() + }) + toast.success('Timesheet approved') + } catch (error) { + toast.error('Failed to approve timesheet') + console.error('Error approving timesheet:', error) + } + }, [updateTimesheet, hasPermission]) + + const handleReject = useCallback(async (id: string) => { + if (!hasPermission('timesheets.approve')) { + toast.error('You do not have permission to reject timesheets') + return + } + + try { + await updateTimesheet(id, { + status: 'rejected' + }) + toast.error('Timesheet rejected') + } catch (error) { + toast.error('Failed to reject timesheet') + console.error('Error rejecting timesheet:', error) + } + }, [updateTimesheet, hasPermission]) + + const handleAdjust = useCallback(async (timesheetId: string, adjustment: any) => { + if (!hasPermission('timesheets.edit')) { + toast.error('You do not have permission to adjust timesheets') + return + } + + try { + await updateTimesheet(timesheetId, adjustment) + toast.success('Timesheet adjusted') + setSelectedTimesheet(null) + } catch (error) { + toast.error('Failed to adjust timesheet') + console.error('Error adjusting timesheet:', error) + } + }, [updateTimesheet, hasPermission]) + + const handleDelete = useCallback(async (id: string) => { + if (!hasPermission('timesheets.delete')) { + toast.error('You do not have permission to delete timesheets') + return + } + + try { + await deleteTimesheet(id) + toast.success('Timesheet deleted') + } catch (error) { + toast.error('Failed to delete timesheet') + console.error('Error deleting timesheet:', error) + } + }, [deleteTimesheet, hasPermission]) + const timesheetsToFilter = useMemo(() => { return timesheets.filter(t => { const matchesStatus = statusFilter === 'all' || t.status === statusFilter @@ -175,9 +320,9 @@ export function TimesheetsView({ setFormData={setFormData} csvData={csvData} setCsvData={setCsvData} - onCreateTimesheet={onCreateTimesheet} - onCreateDetailedTimesheet={onCreateDetailedTimesheet} - onBulkImport={onBulkImport} + onCreateTimesheet={handleCreateTimesheet} + onCreateDetailedTimesheet={handleCreateDetailedTimesheet} + onBulkImport={handleBulkImport} /> } @@ -329,11 +474,12 @@ export function TimesheetsView({ { if (!open) setSelectedTimesheet(null) }} - onAdjust={onAdjust} + onAdjust={(id, adjustment) => handleAdjust(id, adjustment)} /> )}