Initial commit from gitea_uploader script
This commit is contained in:
BIN
Binary file not shown.
+24
-8
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import TimeSheet from './components/TimeSheet';
|
||||
import Help from './Help';
|
||||
import ThemeToggle from './components/ThemeToggle';
|
||||
import MiniCalendar from './components/MiniCalendar';
|
||||
import { useTheme } from './hooks/useTheme';
|
||||
@@ -10,14 +11,15 @@ function App() {
|
||||
const { isDark, toggleTheme } = useTheme();
|
||||
const timesheetData = useTimeSheet();
|
||||
const [showMobileCalendar, setShowMobileCalendar] = useState(false);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
{/* Top right controls */}
|
||||
<div className="fixed top-4 right-4 z-20 flex flex-col items-end gap-4">
|
||||
<div className="fixed top-4 right-4 z-20 flex flex-col items-end gap-4 pointer-events-none">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Desktop calendar - always visible on large screens */}
|
||||
<div className="hidden lg:block">
|
||||
<div className="hidden lg:block pointer-events-auto">
|
||||
<MiniCalendar
|
||||
selectedDate={timesheetData.selectedDate}
|
||||
onDateChange={timesheetData.setSelectedDate}
|
||||
@@ -27,18 +29,33 @@ function App() {
|
||||
{/* Mobile calendar toggle */}
|
||||
<button
|
||||
onClick={() => setShowMobileCalendar(!showMobileCalendar)}
|
||||
className="lg:hidden p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
|
||||
className="lg:hidden p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors pointer-events-auto"
|
||||
aria-label="Toggle calendar"
|
||||
>
|
||||
<Calendar className="h-5 w-5 text-gray-700 dark:text-gray-300" />
|
||||
</button>
|
||||
|
||||
<div className="pointer-events-auto">
|
||||
<ThemeToggle isDark={isDark} toggleTheme={toggleTheme} />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowHelp(true)}
|
||||
className="pointer-events-auto px-3 py-2 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-medium"
|
||||
>
|
||||
Help
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile calendar dropdown */}
|
||||
{/* Mobile calendar dropdown moved to content flow */}
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto py-8 lg:pr-80 px-4">
|
||||
{showHelp ? (
|
||||
<Help onBack={() => setShowHelp(false)} />
|
||||
) : (
|
||||
<>
|
||||
{showMobileCalendar && (
|
||||
<div className="lg:hidden mt-2">
|
||||
<div className="lg:hidden mb-4">
|
||||
<MiniCalendar
|
||||
selectedDate={timesheetData.selectedDate}
|
||||
onDateChange={(date) => {
|
||||
@@ -48,10 +65,9 @@ function App() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto py-8 lg:pr-80 px-4">
|
||||
<TimeSheet timesheetData={timesheetData} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { ArrowLeft, Smartphone, Monitor, FileText, Share2, Upload } from 'lucide-react';
|
||||
|
||||
const Section = ({ title, children }) => (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 space-y-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300 space-y-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function Help({ onBack }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-4">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Troubleshooting & How-To</h1>
|
||||
<button onClick={onBack} className="flex items-center gap-2 px-3 py-2 rounded bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-100 hover:bg-gray-300 dark:hover:bg-gray-600">
|
||||
<ArrowLeft className="h-4 w-4" /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 font-medium text-blue-800 dark:text-blue-200"><Smartphone className="h-4 w-4"/> iPhone/iPad</div>
|
||||
<ul className="list-disc ml-5 mt-2 text-sm text-blue-900 dark:text-blue-100 space-y-1">
|
||||
<li>Use Share/Email PDF to attach via Share Sheet</li>
|
||||
<li>If To isn’t filled, import the Office contact first</li>
|
||||
<li>Find downloads in Files → Downloads</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 font-medium text-green-800 dark:text-green-200">Android</div>
|
||||
<ul className="list-disc ml-5 mt-2 text-sm text-green-900 dark:text-green-100 space-y-1">
|
||||
<li>Pick Gmail or Email from the Share Sheet</li>
|
||||
<li>Downloads are in Files/Downloads app</li>
|
||||
<li>Add the Office contact to speed up addressing</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 font-medium text-purple-800 dark:text-purple-200"><Monitor className="h-4 w-4"/> Windows/Mac</div>
|
||||
<ul className="list-disc ml-5 mt-2 text-sm text-purple-900 dark:text-purple-100 space-y-1">
|
||||
<li>Use Download PDF then attach in Outlook/Mail</li>
|
||||
<li>Export JSON to save/restore your timesheet</li>
|
||||
<li>Subject should be: “Name - WeekRange Timesheet”</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Section title="Common actions">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-medium"><FileText className="h-4 w-4"/> Download PDF</div>
|
||||
<ol className="list-decimal ml-5 space-y-1">
|
||||
<li>Tap Download PDF</li>
|
||||
<li>Open your Downloads folder</li>
|
||||
<li>Attach PDF to email, send to Office@CoastalContractingFL.com</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-medium"><Share2 className="h-4 w-4"/> Share/Email PDF (mobile)</div>
|
||||
<ol className="list-decimal ml-5 space-y-1">
|
||||
<li>Tap Share/Email PDF</li>
|
||||
<li>Choose Mail/Gmail from the sheet</li>
|
||||
<li>Subject prefilled; select Office contact if needed</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-medium"><Upload className="h-4 w-4"/> Export / Import</div>
|
||||
<ol className="list-decimal ml-5 space-y-1">
|
||||
<li>Export saves a JSON backup</li>
|
||||
<li>Import restores a saved JSON file</li>
|
||||
<li>Useful when switching devices or browsers</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="If something doesn’t work">
|
||||
<ul className="list-disc ml-5 space-y-1">
|
||||
<li>Try another browser (Safari/Chrome/Edge)</li>
|
||||
<li>Ensure pop-ups/downloads are allowed</li>
|
||||
<li>Check your device storage isn’t full</li>
|
||||
<li>Manually email to Office@CoastalContractingFL.com with subject “Name - WeekRange Timesheet”</li>
|
||||
</ul>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const DatePicker = ({ selectedDate, onDateChange, weekDays }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 bg-white dark:bg-gray-800 p-3 sm:p-4 rounded-lg shadow-md w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
@@ -21,7 +21,7 @@ const DatePicker = ({ selectedDate, onDateChange, weekDays }) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||
<button
|
||||
onClick={() => navigateWeek(-1)}
|
||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-300"
|
||||
@@ -30,7 +30,7 @@ const DatePicker = ({ selectedDate, onDateChange, weekDays }) => {
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<span className="font-semibold text-gray-900 dark:text-white min-w-[200px] text-center">
|
||||
<span className="font-semibold text-gray-900 dark:text-white text-sm sm:text-base text-center sm:text-left break-words">
|
||||
{formatWeekRange(weekDays)}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const SingleEntry = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-3 p-3 bg-white dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 p-3 bg-white dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-xs font-medium text-gray-600 dark:text-gray-300 mb-1">
|
||||
Work Description
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { pdf } from '@react-pdf/renderer';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { formatWeekRange } from '../utils/dateUtils';
|
||||
import TimesheetPDF from './TimesheetPDF';
|
||||
|
||||
const EmailPDF = ({ weekDays, timeEntries, userName }) => {
|
||||
const handleEmail = async () => {
|
||||
try {
|
||||
const weekRange = formatWeekRange(weekDays);
|
||||
const subject = `${userName || 'Employee'} - ${weekRange} Timesheet`;
|
||||
|
||||
// Generate PDF
|
||||
const blob = await pdf(
|
||||
<TimesheetPDF weekDays={weekDays} timeEntries={timeEntries} userName={userName} />
|
||||
).toBlob();
|
||||
const sanitizedRange = weekRange.replace(/[^\w\s-]/g, '').replace(/\s+/g, '_');
|
||||
const fileName = `Coastal_Timesheet_${sanitizedRange}.pdf`;
|
||||
const file = new File([blob], fileName, { type: 'application/pdf' });
|
||||
|
||||
// Preferred: native share with attachment; some clients use 'title' as subject
|
||||
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||
try {
|
||||
await navigator.share({ files: [file], title: subject, text: 'Timesheet attached.' });
|
||||
return;
|
||||
} catch (err) {
|
||||
// fall through to fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: download PDF and open mailto with subject only
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
const mailto = `mailto:?subject=${encodeURIComponent(subject)}`;
|
||||
window.location.href = mailto;
|
||||
} catch (error) {
|
||||
console.error('Error preparing email with PDF:', error);
|
||||
alert('Error preparing email. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const hasEntries = Object.values(timeEntries).some((dayEntries) =>
|
||||
dayEntries.some((entry) => entry.workDescription || entry.hoursWorked || entry.homeowner)
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleEmail}
|
||||
disabled={!hasEntries}
|
||||
className={`flex items-center gap-2 px-6 py-3 rounded-lg font-medium transition-colors ${
|
||||
hasEntries
|
||||
? 'bg-red-600 hover:bg-red-700 text-white'
|
||||
: 'bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
title={hasEntries ? 'Attach PDF via share (if supported) or download it, and open email with subject pre-filled' : 'No data to email'}
|
||||
>
|
||||
<Mail className="h-5 w-5" />
|
||||
Share/Email PDF
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailPDF;
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ const ImportExport = ({ weekDays, timeEntries, customHomeowners, onImport }) =>
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
title={hasData ? 'Export timesheet data' : 'No data to export'}
|
||||
title={hasData ? 'Export a JSON backup of your timesheet (for re-import later)' : 'No data to export'}
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
Export
|
||||
@@ -143,7 +143,7 @@ const ImportExport = ({ weekDays, timeEntries, customHomeowners, onImport }) =>
|
||||
<button
|
||||
onClick={handleImportClick}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium transition-colors"
|
||||
title="Import timesheet data"
|
||||
title="Import a previously exported JSON timesheet"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
Import
|
||||
|
||||
@@ -80,7 +80,7 @@ const MiniCalendar = ({ selectedDate, onDateChange }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 text-white rounded-lg p-4 shadow-lg min-w-[320px]">
|
||||
<div className="bg-gray-800 text-white rounded-lg p-4 shadow-lg w-full max-w-sm">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium">
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
FN:Coastal Contracting Office
|
||||
N:Office;Coastal Contracting;;;
|
||||
EMAIL;TYPE=INTERNET;TYPE=WORK:Office@CoastalContractingFL.com
|
||||
ORG:Coastal Contracting of FL
|
||||
END:VCARD
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const PDFExport = ({ weekDays, timeEntries, userName }) => {
|
||||
? 'bg-green-600 hover:bg-green-700 text-white'
|
||||
: 'bg-gray-300 dark:bg-gray-600 text-gray-500 dark:text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
title={hasEntries ? 'Download a printable PDF of your timesheet' : 'No data to export as PDF'}
|
||||
>
|
||||
<Download className="h-5 w-5" />
|
||||
Download PDF
|
||||
|
||||
@@ -15,7 +15,7 @@ const TimeEntryRow = ({
|
||||
const isToday = date.toDateString() === new Date().toDateString();
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-4 gap-4 p-4 rounded-lg border-2 transition-colors ${
|
||||
<div className={`grid grid-cols-1 md:grid-cols-4 gap-4 p-4 rounded-lg border-2 transition-colors ${
|
||||
isToday
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 dark:border-blue-400'
|
||||
: 'border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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 }) => {
|
||||
@@ -97,7 +99,8 @@ const TimeSheet = ({ timesheetData }) => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center gap-4 pt-6">
|
||||
<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}
|
||||
@@ -109,6 +112,32 @@ const TimeSheet = ({ timesheetData }) => {
|
||||
timeEntries={timeEntries}
|
||||
userName={userName}
|
||||
/>
|
||||
<EmailPDF
|
||||
weekDays={weekDays}
|
||||
timeEntries={timeEntries}
|
||||
userName={userName}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user