Generated by Spark: Integrate CRUD hooks into Timesheets view for create/update/delete operations

This commit is contained in:
2026-01-24 02:59:32 +00:00
committed by GitHub
parent 06789aa91f
commit a7ccfcef0c
4 changed files with 239 additions and 46 deletions

View File

@@ -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
</Button>
)}
{onDelete && hasPermission('timesheets.delete') && (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation()
setShowDeleteDialog(true)
}}
>
<Trash size={16} className="text-destructive" />
</Button>
)}
</div>
</div>
</CardContent>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
<AlertDialogHeader>
<AlertDialogTitle>Delete Timesheet</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this timesheet for {timesheet.workerName}? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
onDelete?.(timesheet.id)
setShowDeleteDialog(false)
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
)
}

View File

@@ -120,16 +120,9 @@ export function ViewRouter({
case 'timesheets':
return (
<TimesheetsView
timesheets={timesheets}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
onApprove={actions.handleApproveTimesheet}
onReject={actions.handleRejectTimesheet}
onCreateInvoice={actions.handleCreateInvoice}
onCreateTimesheet={actions.handleCreateTimesheet}
onCreateDetailedTimesheet={actions.handleCreateDetailedTimesheet}
onBulkImport={actions.handleBulkImport}
onAdjust={actions.handleAdjustTimesheet}
/>
)

View File

@@ -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 (
<Tabs defaultValue="pending" className="space-y-4">
@@ -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}
/>
))}
</TabsContent>
@@ -86,6 +90,7 @@ export function TimesheetTabs({
onCreateInvoice={onCreateInvoice}
onAdjust={onAdjust}
onViewDetails={onViewDetails}
onDelete={onDelete}
/>
))}
</TabsContent>

View File

@@ -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<Timesheet | null>(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}
/>
</Stack>
}
@@ -329,11 +474,12 @@ export function TimesheetsView({
<TimesheetTabs
filteredTimesheets={timesheetsWithValidation}
onApprove={onApprove}
onReject={onReject}
onApprove={handleApprove}
onReject={handleReject}
onCreateInvoice={onCreateInvoice}
onAdjust={setSelectedTimesheet}
onViewDetails={setViewingTimesheet}
onDelete={handleDelete}
/>
<TimesheetDetailDialog
@@ -351,7 +497,7 @@ export function TimesheetsView({
onOpenChange={(open) => {
if (!open) setSelectedTimesheet(null)
}}
onAdjust={onAdjust}
onAdjust={(id, adjustment) => handleAdjust(id, adjustment)}
/>
)}
</Stack>