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)}
/>
)}