Generated by Spark: Complete translation integration for remaining 4 components (NotificationRules, ShiftPattern, HolidayPay, ContractValidator)

This commit is contained in:
2026-02-05 18:10:08 +00:00
committed by GitHub
parent 92297fa8cd
commit 55ecf66ee9
3 changed files with 101 additions and 103 deletions

View File

@@ -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>

View File

@@ -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) => (

View File

@@ -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>
) : (