- Employee name validation for all submissions - Complete entry validation (all fields required when any field has input) - Dynamic auto-resizing work description fields - Optimized field layout (Homeowner → Hours → Work Description) - Professional PDF generation with validation - Email/share functionality with validation - Data import/export capabilities - Dark/light theme support - Mobile-responsive design - Local data persistence - Multiple entries per day support - Custom homeowner management - Comprehensive README with deployment instructions - Production build ready for web hosting
159 lines
5.7 KiB
React
159 lines
5.7 KiB
React
import DatePicker from './DatePicker';
|
|
import DayEntries from './DayEntries';
|
|
import PDFExport from './PDFExport';
|
|
import EmailPDF from './EmailPDF';
|
|
import officeContact from './OfficeContact.vcf?url';
|
|
import ImportExport from './ImportExport';
|
|
|
|
const TimeSheet = ({ timesheetData }) => {
|
|
const {
|
|
selectedDate,
|
|
setSelectedDate,
|
|
weekDays,
|
|
timeEntries,
|
|
updateTimeEntry,
|
|
addTimeEntry,
|
|
removeTimeEntry,
|
|
allHomeowners,
|
|
addCustomHomeowner,
|
|
customHomeowners,
|
|
importTimesheet,
|
|
userName,
|
|
setUserName,
|
|
getEntryValidation
|
|
} = timesheetData;
|
|
|
|
const totalHours = Object.values(timeEntries).reduce((total, dayEntries) => {
|
|
const dayTotal = dayEntries.reduce((daySum, entry) => {
|
|
return daySum + (parseFloat(entry.hoursWorked) || 0);
|
|
}, 0);
|
|
return total + dayTotal;
|
|
}, 0);
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto p-6 space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
|
COASTAL CONTRACTING OF FL TIME SHEET
|
|
</h1>
|
|
<p className="text-gray-600 dark:text-gray-300">
|
|
Track your daily work hours and generate timesheets Sherry can actually read
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-end">
|
|
<div className="flex-1">
|
|
<label htmlFor="userName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Employee Name <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="userName"
|
|
value={userName}
|
|
onChange={(e) => setUserName(e.target.value)}
|
|
placeholder="Enter your name"
|
|
className={`w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white ${
|
|
userName.trim() === ''
|
|
? 'border-red-500 bg-red-50 dark:bg-red-900/20 dark:border-red-400'
|
|
: 'border-gray-300 dark:border-gray-600'
|
|
}`}
|
|
/>
|
|
{userName.trim() === '' && (
|
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
|
Employee name is required for timesheet submission
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<DatePicker
|
|
selectedDate={selectedDate}
|
|
onDateChange={setSelectedDate}
|
|
weekDays={weekDays}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
Time Entries
|
|
</h2>
|
|
<div className="text-lg font-medium text-gray-900 dark:text-white">
|
|
Total: {totalHours.toFixed(1)} hours
|
|
</div>
|
|
</div>
|
|
|
|
{weekDays.map((day) => {
|
|
const dayKey = day.toISOString().split('T')[0];
|
|
const dayEntries = timeEntries[dayKey] || [{
|
|
id: `${dayKey}-0`,
|
|
workDescription: '',
|
|
hoursWorked: '',
|
|
homeowner: ''
|
|
}];
|
|
|
|
return (
|
|
<DayEntries
|
|
key={dayKey}
|
|
date={day}
|
|
entries={dayEntries}
|
|
onUpdate={updateTimeEntry}
|
|
onAdd={addTimeEntry}
|
|
onRemove={removeTimeEntry}
|
|
homeowners={allHomeowners}
|
|
onAddCustomHomeowner={addCustomHomeowner}
|
|
getEntryValidation={getEntryValidation}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center gap-2 pt-6">
|
|
<div className="flex flex-wrap justify-center items-center gap-3">
|
|
<ImportExport
|
|
weekDays={weekDays}
|
|
timeEntries={timeEntries}
|
|
customHomeowners={customHomeowners}
|
|
onImport={importTimesheet}
|
|
/>
|
|
<PDFExport
|
|
weekDays={weekDays}
|
|
timeEntries={timeEntries}
|
|
userName={userName}
|
|
getEntryValidation={getEntryValidation}
|
|
/>
|
|
<EmailPDF
|
|
weekDays={weekDays}
|
|
timeEntries={timeEntries}
|
|
userName={userName}
|
|
getEntryValidation={getEntryValidation}
|
|
/>
|
|
<a
|
|
href={officeContact}
|
|
download="Coastal_Office_Contact.vcf"
|
|
className="flex items-center gap-2 px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-100 rounded-lg font-medium transition-colors"
|
|
title="Download and import the office contact to your phone so the To field is one tap away"
|
|
>
|
|
Import Office Contact
|
|
</a>
|
|
</div>
|
|
<div className="text-xs text-gray-600 dark:text-gray-400 text-center max-w-xl">
|
|
<p className="mb-1">
|
|
Export: Save a JSON backup that you can import later. Download PDF: Save a printable copy. Share/Email PDF: Attach the PDF via your device share sheet or download and open email with the subject pre-filled.
|
|
</p>
|
|
<p>
|
|
If all else fails: email your timesheet to <span className="font-medium">Office@CoastalContractingFL.com</span> with subject
|
|
<span className="font-medium"> "{userName || 'Employee'} - {weekDays && weekDays.length ? weekDays[0].toLocaleDateString() + ' - ' + weekDays[6].toLocaleDateString() : 'Timesheet'}"</span>.
|
|
</p>
|
|
<p className="mt-1">
|
|
Tip: You can add a contact for Office@CoastalContractingFL.com on your device so the To field is one tap away.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TimeSheet; |