mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Complete translation integration for remaining 4 components (NotificationRules, ShiftPattern, HolidayPay, ContractValidator)
This commit is contained in:
@@ -3,6 +3,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Warning, CheckCircle, XCircle, ShieldCheck } from '@phosphor-icons/react'
|
||||
import { useTranslation } from '@/hooks/use-translation'
|
||||
import type { Timesheet, RateCard, ValidationRule } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -12,6 +13,7 @@ interface ContractValidatorProps {
|
||||
}
|
||||
|
||||
export function ContractValidator({ timesheets, rateCards }: ContractValidatorProps) {
|
||||
const { t } = useTranslation()
|
||||
const validateTimesheet = (timesheet: Timesheet): {
|
||||
isValid: boolean
|
||||
errors: string[]
|
||||
@@ -23,7 +25,7 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
const rateCard = rateCards.find(rc => rc.id === timesheet.rateCardId)
|
||||
|
||||
if (!rateCard) {
|
||||
errors.push('No rate card assigned')
|
||||
errors.push(t('contractValidator.noRateCard'))
|
||||
return { isValid: false, errors, warnings }
|
||||
}
|
||||
|
||||
@@ -41,11 +43,11 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
}
|
||||
|
||||
if (!timesheet.rate || timesheet.rate < rateCard.standardRate * 0.5) {
|
||||
errors.push(`Rate £${timesheet.rate || 0} is below minimum allowed (£${rateCard.standardRate * 0.5})`)
|
||||
errors.push(t('contractValidator.rateTooLow', { rate: timesheet.rate || 0, minimum: (rateCard.standardRate * 0.5).toFixed(2) }))
|
||||
}
|
||||
|
||||
if (timesheet.rate && timesheet.rate > rateCard.standardRate * 3) {
|
||||
warnings.push(`Rate £${timesheet.rate} exceeds 3x standard rate`)
|
||||
warnings.push(t('contractValidator.rateTooHigh', { rate: timesheet.rate }))
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -94,8 +96,8 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold tracking-tight">Contract Validation</h2>
|
||||
<p className="text-muted-foreground mt-1">Validate timesheets against rate cards and compliance rules</p>
|
||||
<h2 className="text-3xl font-semibold tracking-tight">{t('contractValidator.title')}</h2>
|
||||
<p className="text-muted-foreground mt-1">{t('contractValidator.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -103,12 +105,12 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<XCircle size={18} className="text-destructive" weight="fill" />
|
||||
Validation Errors
|
||||
{t('contractValidator.validationErrors')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">{withErrors.length}</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">Timesheets blocked from processing</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('contractValidator.validationErrorsBlocked')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -116,12 +118,12 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<Warning size={18} className="text-warning" weight="fill" />
|
||||
Warnings
|
||||
{t('contractValidator.warnings')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">{withWarnings.length}</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">Review recommended</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('contractValidator.warningsReview')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -129,12 +131,12 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<CheckCircle size={18} className="text-success" weight="fill" />
|
||||
Compliant
|
||||
{t('contractValidator.compliant')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">{compliant.length}</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">Ready for processing</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('contractValidator.compliantReady')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -144,10 +146,10 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<XCircle size={20} className="text-destructive" weight="fill" />
|
||||
Validation Errors - Action Required
|
||||
{t('contractValidator.errorDescription')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
These timesheets have critical validation errors and cannot be processed
|
||||
{t('contractValidator.errorDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
@@ -158,19 +160,19 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold">{timesheet.workerName}</h3>
|
||||
<Badge variant="destructive">Error</Badge>
|
||||
<Badge variant="destructive">{t('contractValidator.error')}</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Client</p>
|
||||
<p className="text-muted-foreground">{t('contractValidator.client')}</p>
|
||||
<p className="font-medium">{timesheet.clientName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Week Ending</p>
|
||||
<p className="text-muted-foreground">{t('contractValidator.weekEnding')}</p>
|
||||
<p className="font-medium">{new Date(timesheet.weekEnding).toLocaleDateString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Hours</p>
|
||||
<p className="text-muted-foreground">{t('contractValidator.hours')}</p>
|
||||
<p className="font-medium font-mono">{timesheet.hours}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,7 +185,7 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">
|
||||
Fix Issues
|
||||
{t('contractValidator.fixIssues')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -198,10 +200,10 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Warning size={20} className="text-warning" weight="fill" />
|
||||
Warnings - Review Recommended
|
||||
{t('contractValidator.reviewRecommended')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
These timesheets have potential issues but can be processed
|
||||
{t('contractValidator.warningDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
@@ -212,7 +214,7 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<div className="space-y-2 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold">{timesheet.workerName}</h3>
|
||||
<Badge variant="warning">Warning</Badge>
|
||||
<Badge variant="warning">{t('contractValidator.warning')}</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
@@ -238,10 +240,10 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline">
|
||||
Review
|
||||
{t('contractValidator.review')}
|
||||
</Button>
|
||||
<Button size="sm" style={{ backgroundColor: 'var(--success)', color: 'var(--success-foreground)' }}>
|
||||
Approve Anyway
|
||||
{t('contractValidator.approveAnyway')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,10 +259,10 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<CheckCircle size={20} className="text-success" weight="fill" />
|
||||
Compliant Timesheets - Ready to Process
|
||||
{t('contractValidator.readyToProcess')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
These timesheets passed all validation checks
|
||||
{t('contractValidator.compliantDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -282,7 +284,7 @@ export function ContractValidator({ timesheets, rateCards }: ContractValidatorPr
|
||||
))}
|
||||
{compliant.length > 5 && (
|
||||
<p className="text-sm text-muted-foreground text-center py-2">
|
||||
+ {compliant.length - 5} more compliant timesheets
|
||||
{t('contractValidator.moreCompliantTimesheets', { count: compliant.length - 5 })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useTranslation } from '@/hooks/use-translation'
|
||||
import {
|
||||
Calendar,
|
||||
Plus,
|
||||
@@ -42,6 +43,7 @@ interface HolidayRequest {
|
||||
}
|
||||
|
||||
export function HolidayPayManager() {
|
||||
const { t } = useTranslation()
|
||||
const [accruals = [], setAccruals] = useKV<HolidayAccrual[]>('holiday-accruals', [])
|
||||
const [requests = [], setRequests] = useKV<HolidayRequest[]>('holiday-requests', [])
|
||||
const [isRequestDialogOpen, setIsRequestDialogOpen] = useState(false)
|
||||
@@ -90,7 +92,7 @@ export function HolidayPayManager() {
|
||||
|
||||
const handleRequestHoliday = () => {
|
||||
if (!formData.workerName || !formData.startDate || !formData.endDate || formData.days <= 0) {
|
||||
toast.error('Please fill in all fields')
|
||||
toast.error(t('holidayPay.fillAllFields'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -106,7 +108,7 @@ export function HolidayPayManager() {
|
||||
}
|
||||
|
||||
setRequests((current) => [...(current || []), newRequest])
|
||||
toast.success('Holiday request submitted')
|
||||
toast.success(t('holidayPay.requestCreated'))
|
||||
|
||||
setFormData({
|
||||
workerId: '',
|
||||
@@ -124,7 +126,7 @@ export function HolidayPayManager() {
|
||||
|
||||
const accrual = accruals.find(a => a.workerId === request.workerId)
|
||||
if (!accrual || accrual.remainingDays < request.days) {
|
||||
toast.error('Insufficient holiday balance')
|
||||
toast.error(t('holidayPay.insufficientBalance'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ export function HolidayPayManager() {
|
||||
)
|
||||
)
|
||||
|
||||
toast.success('Holiday request approved')
|
||||
toast.success(t('holidayPay.requestApproved'))
|
||||
}
|
||||
|
||||
const handleRejectRequest = (requestId: string) => {
|
||||
@@ -158,7 +160,7 @@ export function HolidayPayManager() {
|
||||
r.id === requestId ? { ...r, status: 'rejected' as const } : r
|
||||
)
|
||||
)
|
||||
toast.error('Holiday request rejected')
|
||||
toast.error(t('holidayPay.requestRejected'))
|
||||
}
|
||||
|
||||
const calculateDaysBetweenDates = (start: string, end: string) => {
|
||||
@@ -174,36 +176,36 @@ export function HolidayPayManager() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold tracking-tight">Holiday Pay Management</h2>
|
||||
<p className="text-muted-foreground mt-1">Track accruals, requests, and balances</p>
|
||||
<h2 className="text-3xl font-semibold tracking-tight">{t('holidayPay.title')}</h2>
|
||||
<p className="text-muted-foreground mt-1">{t('holidayPay.subtitle')}</p>
|
||||
</div>
|
||||
<Dialog open={isRequestDialogOpen} onOpenChange={setIsRequestDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus size={18} className="mr-2" />
|
||||
New Holiday Request
|
||||
{t('holidayPay.newHolidayRequest')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Holiday Request</DialogTitle>
|
||||
<DialogTitle>{t('holidayPay.createDialog.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Submit a new holiday request for approval
|
||||
{t('holidayPay.createDialog.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reqWorker">Worker Name</Label>
|
||||
<Label htmlFor="reqWorker">{t('holidayPay.workerNameLabel')}</Label>
|
||||
<Input
|
||||
id="reqWorker"
|
||||
placeholder="Enter worker name"
|
||||
placeholder={t('holidayPay.workerNamePlaceholder')}
|
||||
value={formData.workerName}
|
||||
onChange={(e) => setFormData({ ...formData, workerName: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="startDate">Start Date</Label>
|
||||
<Label htmlFor="startDate">{t('holidayPay.startDateLabel')}</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
type="date"
|
||||
@@ -218,7 +220,7 @@ export function HolidayPayManager() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="endDate">End Date</Label>
|
||||
<Label htmlFor="endDate">{t('holidayPay.endDateLabel')}</Label>
|
||||
<Input
|
||||
id="endDate"
|
||||
type="date"
|
||||
@@ -234,7 +236,7 @@ export function HolidayPayManager() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="days">Days Requested</Label>
|
||||
<Label htmlFor="days">{t('holidayPay.daysRequestedLabel')}</Label>
|
||||
<Input
|
||||
id="days"
|
||||
type="number"
|
||||
@@ -244,8 +246,8 @@ export function HolidayPayManager() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsRequestDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleRequestHoliday}>Submit Request</Button>
|
||||
<Button variant="outline" onClick={() => setIsRequestDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleRequestHoliday}>{t('holidayPay.submitRequest')}</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -254,18 +256,18 @@ export function HolidayPayManager() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm text-muted-foreground">Total Accrued</CardTitle>
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('holidayPay.totalAccruedLabel')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">
|
||||
{accruals.reduce((sum, a) => sum + a.accruedDays, 0).toFixed(1)} days
|
||||
{t('holidayPay.daysLabel', { count: accruals.reduce((sum, a) => sum + a.accruedDays, 0).toFixed(1) })}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm text-muted-foreground">Pending Requests</CardTitle>
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('holidayPay.pendingRequests')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">
|
||||
@@ -276,11 +278,11 @@ export function HolidayPayManager() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm text-muted-foreground">Days Taken (YTD)</CardTitle>
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('holidayPay.daysTakenYTD')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">
|
||||
{accruals.reduce((sum, a) => sum + a.takenDays, 0).toFixed(1)} days
|
||||
{t('holidayPay.daysLabel', { count: accruals.reduce((sum, a) => sum + a.takenDays, 0).toFixed(1) })}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -289,10 +291,10 @@ export function HolidayPayManager() {
|
||||
<Tabs defaultValue="accruals" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="accruals">
|
||||
Accruals ({accruals.length})
|
||||
{t('holidayPay.tabs.accruals', { count: accruals.length })}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="requests">
|
||||
Requests ({requests.filter(r => r.status === 'pending').length} pending)
|
||||
{t('holidayPay.tabs.requests', { count: requests.filter(r => r.status === 'pending').length })}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -300,8 +302,8 @@ export function HolidayPayManager() {
|
||||
{accruals.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<Calendar size={48} className="mx-auto text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No holiday accruals</h3>
|
||||
<p className="text-muted-foreground">Accruals are calculated automatically from timesheets</p>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('holidayPay.noAccruals')}</h3>
|
||||
<p className="text-muted-foreground">{t('holidayPay.noAccrualsDescription')}</p>
|
||||
</Card>
|
||||
) : (
|
||||
accruals.map((accrual) => (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useTranslation } from '@/hooks/use-translation'
|
||||
import {
|
||||
Clock,
|
||||
Plus,
|
||||
@@ -24,15 +25,7 @@ import { toast } from 'sonner'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ShiftPatternTemplate, ShiftType, DayOfWeek, RecurrencePattern } from '@/lib/types'
|
||||
|
||||
const DAYS_OF_WEEK: { value: DayOfWeek; label: string }[] = [
|
||||
{ value: 'monday', label: 'Monday' },
|
||||
{ value: 'tuesday', label: 'Tuesday' },
|
||||
{ value: 'wednesday', label: 'Wednesday' },
|
||||
{ value: 'thursday', label: 'Thursday' },
|
||||
{ value: 'friday', label: 'Friday' },
|
||||
{ value: 'saturday', label: 'Saturday' },
|
||||
{ value: 'sunday', label: 'Sunday' }
|
||||
]
|
||||
const DAYS_OF_WEEK: DayOfWeek[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
||||
|
||||
const SHIFT_TYPES: { value: ShiftType; label: string; icon: any; color: string }[] = [
|
||||
{ value: 'night', label: 'Night Shift', icon: Moon, color: 'bg-purple-500/10 text-purple-500 border-purple-500/20' },
|
||||
@@ -46,6 +39,7 @@ const SHIFT_TYPES: { value: ShiftType; label: string; icon: any; color: string }
|
||||
]
|
||||
|
||||
export function ShiftPatternManager() {
|
||||
const { t } = useTranslation()
|
||||
const [patterns = [], setPatterns] = useKV<ShiftPatternTemplate[]>('shift-patterns', [])
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [editingPattern, setEditingPattern] = useState<ShiftPatternTemplate | null>(null)
|
||||
@@ -63,7 +57,7 @@ export function ShiftPatternManager() {
|
||||
|
||||
const handleCreatePattern = () => {
|
||||
if (!formData.name || !formData.shiftType || !formData.daysOfWeek || formData.daysOfWeek.length === 0) {
|
||||
toast.error('Please fill in all required fields')
|
||||
toast.error(t('shiftPatterns.fillAllFields'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,14 +80,14 @@ export function ShiftPatternManager() {
|
||||
}
|
||||
|
||||
setPatterns(current => [...(current || []), newPattern])
|
||||
toast.success('Shift pattern template created')
|
||||
toast.success(t('shiftPatterns.patternCreated'))
|
||||
resetForm()
|
||||
setIsCreateDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleUpdatePattern = () => {
|
||||
if (!editingPattern || !formData.name || !formData.shiftType || !formData.daysOfWeek || formData.daysOfWeek.length === 0) {
|
||||
toast.error('Please fill in all required fields')
|
||||
toast.error(t('shiftPatterns.fillAllFields'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,7 +111,7 @@ export function ShiftPatternManager() {
|
||||
: p
|
||||
)
|
||||
})
|
||||
toast.success('Shift pattern template updated')
|
||||
toast.success(t('shiftPatterns.patternUpdated'))
|
||||
resetForm()
|
||||
setEditingPattern(null)
|
||||
}
|
||||
@@ -127,7 +121,7 @@ export function ShiftPatternManager() {
|
||||
if (!current) return []
|
||||
return current.filter(p => p.id !== id)
|
||||
})
|
||||
toast.success('Shift pattern template deleted')
|
||||
toast.success(t('shiftPatterns.patternDeleted'))
|
||||
}
|
||||
|
||||
const handleDuplicatePattern = (pattern: ShiftPatternTemplate) => {
|
||||
@@ -139,7 +133,7 @@ export function ShiftPatternManager() {
|
||||
usageCount: 0
|
||||
}
|
||||
setPatterns(current => [...(current || []), duplicated])
|
||||
toast.success('Shift pattern template duplicated')
|
||||
toast.success(t('shiftPatterns.patternDuplicated'))
|
||||
}
|
||||
|
||||
const handleEditPattern = (pattern: ShiftPatternTemplate) => {
|
||||
@@ -204,8 +198,8 @@ export function ShiftPatternManager() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-semibold tracking-tight">Shift Pattern Templates</h2>
|
||||
<p className="text-muted-foreground mt-1">Create reusable templates for recurring shift schedules</p>
|
||||
<h2 className="text-3xl font-semibold tracking-tight">{t('shiftPatterns.title')}</h2>
|
||||
<p className="text-muted-foreground mt-1">{t('shiftPatterns.subtitle')}</p>
|
||||
</div>
|
||||
<Dialog
|
||||
open={isCreateDialogOpen || editingPattern !== null}
|
||||
@@ -222,32 +216,32 @@ export function ShiftPatternManager() {
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus size={18} className="mr-2" />
|
||||
Create Template
|
||||
{t('shiftPatterns.createTemplate')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingPattern ? 'Edit' : 'Create'} Shift Pattern Template</DialogTitle>
|
||||
<DialogTitle>{editingPattern ? t('shiftPatterns.createDialog.editTitle') : t('shiftPatterns.createDialog.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define a reusable template for recurring shift schedules
|
||||
{t('shiftPatterns.createDialog.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pattern-name">Template Name *</Label>
|
||||
<Label htmlFor="pattern-name">{t('shiftPatterns.patternNameLabel')}</Label>
|
||||
<Input
|
||||
id="pattern-name"
|
||||
placeholder="e.g. Night Shift - Mon-Fri"
|
||||
placeholder={t('shiftPatterns.patternNamePlaceholder')}
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pattern-description">Description</Label>
|
||||
<Label htmlFor="pattern-description">{t('shiftPatterns.descriptionLabel')}</Label>
|
||||
<Textarea
|
||||
id="pattern-description"
|
||||
placeholder="Optional description of the shift pattern"
|
||||
placeholder={t('shiftPatterns.descriptionPlaceholder')}
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
rows={2}
|
||||
@@ -255,7 +249,7 @@ export function ShiftPatternManager() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="shift-type">Shift Type *</Label>
|
||||
<Label htmlFor="shift-type">{t('shiftPatterns.shiftTypeLabel')}</Label>
|
||||
<Select
|
||||
value={formData.shiftType}
|
||||
onValueChange={(value) => setFormData({ ...formData, shiftType: value as ShiftType })}
|
||||
@@ -266,7 +260,7 @@ export function ShiftPatternManager() {
|
||||
<SelectContent>
|
||||
{SHIFT_TYPES.map(type => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
{t(`shiftPatterns.shiftTypes.${type.value}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -275,7 +269,7 @@ export function ShiftPatternManager() {
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="start-time">Start Time *</Label>
|
||||
<Label htmlFor="start-time">{t('shiftPatterns.startTimeLabel')}</Label>
|
||||
<Input
|
||||
id="start-time"
|
||||
type="time"
|
||||
@@ -284,7 +278,7 @@ export function ShiftPatternManager() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="end-time">End Time *</Label>
|
||||
<Label htmlFor="end-time">{t('shiftPatterns.endTimeLabel')}</Label>
|
||||
<Input
|
||||
id="end-time"
|
||||
type="time"
|
||||
@@ -293,7 +287,7 @@ export function ShiftPatternManager() {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="break-minutes">Break (mins)</Label>
|
||||
<Label htmlFor="break-minutes">{t('shiftPatterns.breakMinutesLabel')}</Label>
|
||||
<Input
|
||||
id="break-minutes"
|
||||
type="number"
|
||||
@@ -306,7 +300,7 @@ export function ShiftPatternManager() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="rate-multiplier">Rate Multiplier</Label>
|
||||
<Label htmlFor="rate-multiplier">{t('shiftPatterns.rateMultiplierLabel')}</Label>
|
||||
<Input
|
||||
id="rate-multiplier"
|
||||
type="number"
|
||||
@@ -316,25 +310,25 @@ export function ShiftPatternManager() {
|
||||
onChange={(e) => setFormData({ ...formData, rateMultiplier: parseFloat(e.target.value) || 1.0 })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Standard rate × {formData.rateMultiplier || 1.0} = {((formData.rateMultiplier || 1.0) * 25).toFixed(2)} per hour (example at £25/hr)
|
||||
{t('shiftPatterns.rateMultiplierHelper', { multiplier: formData.rateMultiplier || 1.0, rate: ((formData.rateMultiplier || 1.0) * 25).toFixed(2) })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>Days of Week *</Label>
|
||||
<Label>{t('shiftPatterns.daysOfWeekLabel')}</Label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{DAYS_OF_WEEK.map(day => (
|
||||
<Button
|
||||
key={day.value}
|
||||
key={day}
|
||||
type="button"
|
||||
variant={formData.daysOfWeek?.includes(day.value) ? 'default' : 'outline'}
|
||||
variant={formData.daysOfWeek?.includes(day) ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => toggleDayOfWeek(day.value)}
|
||||
onClick={() => toggleDayOfWeek(day)}
|
||||
className="w-full"
|
||||
>
|
||||
{day.label.substring(0, 3)}
|
||||
{t(`shiftPatterns.daysOfWeekShort.${day}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
@@ -342,20 +336,20 @@ export function ShiftPatternManager() {
|
||||
|
||||
{formData.defaultStartTime && formData.defaultEndTime && (
|
||||
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm font-medium">Pattern Summary</p>
|
||||
<p className="text-sm font-medium">{t('shiftPatterns.patternSummary')}</p>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<p>
|
||||
Hours per shift: {calculateHours(
|
||||
{t('shiftPatterns.hoursPerShift')}: {calculateHours(
|
||||
formData.defaultStartTime,
|
||||
formData.defaultEndTime,
|
||||
formData.defaultBreakMinutes || 0
|
||||
).toFixed(2)}h
|
||||
</p>
|
||||
<p>
|
||||
Days per week: {formData.daysOfWeek?.length || 0}
|
||||
{t('shiftPatterns.daysPerWeek')}: {formData.daysOfWeek?.length || 0}
|
||||
</p>
|
||||
<p>
|
||||
Total weekly hours: {(
|
||||
{t('shiftPatterns.totalWeeklyHours')}: {(
|
||||
calculateHours(
|
||||
formData.defaultStartTime,
|
||||
formData.defaultEndTime,
|
||||
@@ -376,10 +370,10 @@ export function ShiftPatternManager() {
|
||||
resetForm()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={editingPattern ? handleUpdatePattern : handleCreatePattern}>
|
||||
{editingPattern ? 'Update' : 'Create'} Template
|
||||
{editingPattern ? t('common.edit') : t('common.save')} {t('shiftPatterns.createTemplate')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -389,35 +383,35 @@ export function ShiftPatternManager() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm text-muted-foreground">Total Templates</CardTitle>
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('shiftPatterns.totalTemplates')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">{patterns.length}</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">Active shift patterns</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('shiftPatterns.activeShiftPatterns')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm text-muted-foreground">Most Used</CardTitle>
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('shiftPatterns.mostUsed')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">
|
||||
{patterns.length > 0 ? Math.max(...patterns.map(p => p.usageCount)) : 0}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">Times applied</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('shiftPatterns.timesApplied')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm text-muted-foreground">Night Shifts</CardTitle>
|
||||
<CardTitle className="text-sm text-muted-foreground">{t('shiftPatterns.nightShifts')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold">
|
||||
{patterns.filter(p => p.shiftType === 'night').length}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">Night shift templates</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t('shiftPatterns.nightShiftTemplates')}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -425,11 +419,11 @@ export function ShiftPatternManager() {
|
||||
{patterns.length === 0 ? (
|
||||
<Card className="p-12 text-center">
|
||||
<Clock size={48} className="mx-auto text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No shift patterns yet</h3>
|
||||
<p className="text-muted-foreground mb-4">Create your first template to streamline recurring shift scheduling</p>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('shiftPatterns.noPatterns')}</h3>
|
||||
<p className="text-muted-foreground mb-4">{t('shiftPatterns.noPatternsDescription')}</p>
|
||||
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
||||
<Plus size={18} className="mr-2" />
|
||||
Create Template
|
||||
{t('shiftPatterns.createTemplate')}
|
||||
</Button>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user