mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Integrate CRUD hooks into Timesheets view for create/update/delete operations
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user