Initial commit from gitea_uploader script

This commit is contained in:
bizzle
2025-08-12 19:35:08 -04:00
parent c913946c08
commit d8bb989f8c
13 changed files with 234 additions and 18 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+25 -9
View File
@@ -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>
<ThemeToggle isDark={isDark} toggleTheme={toggleTheme} />
<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>
);
+90
View File
@@ -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>
);
}
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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
+71
View File
@@ -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;
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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">
+9
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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'
+30 -1
View File
@@ -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>
);