feat: Initialize monorepo structure and comprehensive documentation

This commit establishes the new monorepo architecture for the KROW Workforce platform.

Key changes include:
- Reorganized project into `frontend-web`, `mobile-apps`, `firebase`, `scripts`, and `secrets` directories.
- Updated `Makefile` to support the new monorepo layout and automate Base44 export integration.
- Fixed `scripts/prepare-export.js` for ES module compatibility and global component import resolution.
- Created and updated `CONTRIBUTING.md` for developer onboarding.
- Restructured, renamed, and translated all `docs/` files for clarity and consistency.
- Implemented an interactive internal launchpad with diagram viewing capabilities.
- Configured base Firebase project files (`firebase.json`, security rules).
- Updated `README.md` to reflect the new project structure and documentation overview.
This commit is contained in:
bwnyasse
2025-11-12 12:50:55 -05:00
parent 92fd0118be
commit 554dc9f9e3
203 changed files with 1414 additions and 732 deletions

View File

@@ -0,0 +1,514 @@
import React, { useState, useEffect, useRef } from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Download,
Printer,
CheckCircle2,
Clock,
Eye,
ZoomIn,
ZoomOut,
Maximize2,
Minimize2,
ChevronLeft,
ChevronRight,
FileText,
RefreshCw,
PenTool,
Upload, // Added Upload icon
} from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import { useQuery } from "@tanstack/react-query"; // Added useQuery import
import { base44 } from "@/api/base44Client"; // Added base44 import
export default function DocumentViewer({
documentUrl,
documentName,
documentType = "Contract",
onAcknowledge,
initialNotes = "",
isAcknowledged = false,
timeSpent = 0,
}) {
const [notes, setNotes] = useState(initialNotes);
const [startTime] = useState(Date.now());
const [reviewTime, setReviewTime] = useState(timeSpent);
const [pageNumber, setPageNumber] = useState(1);
const [zoom, setZoom] = useState(100);
const [isFullscreen, setIsFullscreen] = useState(false);
const [signerName, setSignerName] = useState("");
const [signatureData, setSignatureData] = useState(null);
const [isDrawing, setIsDrawing] = useState(false);
const canvasRef = useRef(null);
const { toast } = useToast();
// Fetch current user to check for saved signature
const { data: currentUser } = useQuery({
queryKey: ['current-user-doc-viewer'],
queryFn: () => base44.auth.me(),
});
// Track time spent
useEffect(() => {
const interval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 60000);
setReviewTime(timeSpent + elapsed);
}, 60000);
return () => clearInterval(interval);
}, [startTime, timeSpent]);
// Initialize canvas
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#1C323E';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
}, []);
// Pre-fill signer name from user profile
useEffect(() => {
if (currentUser?.full_name && !signerName) {
setSignerName(currentUser.full_name);
}
}, [currentUser, signerName]);
const handlePrint = () => {
const printWindow = window.open(documentUrl, '_blank');
if (printWindow) {
printWindow.addEventListener('load', () => {
printWindow.focus();
setTimeout(() => {
printWindow.print();
}, 500);
});
}
};
const handleDownloadPDF = () => {
const link = document.createElement('a');
link.href = documentUrl;
link.download = `${documentName}.pdf`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleAcknowledge = async () => {
if (!signerName.trim()) {
toast({
title: "Name Required",
description: "Please enter your full name before accepting.",
variant: "destructive",
});
return;
}
if (!signatureData) {
toast({
title: "Signature Required",
description: "Please sign the document before accepting.",
variant: "destructive",
});
return;
}
// Save signature to user profile for future use
if (currentUser?.id && signatureData) {
try {
await base44.auth.updateMe({ saved_signature: signatureData });
} catch (error) {
console.error("Failed to save signature to profile:", error);
}
}
if (onAcknowledge) {
onAcknowledge({
notes,
reviewTime,
signerName,
signature: signatureData,
acknowledgedDate: new Date().toISOString()
});
}
};
const handleFullscreenToggle = () => {
if (!isFullscreen) {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
document.documentElement.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.exitFullscreen(); // Corrected for webkit
} else if (document.msExitFullscreen) {
document.exitFullscreen(); // Corrected for ms
}
}
setIsFullscreen(!isFullscreen);
};
// Signature pad functions
const startDrawing = (e) => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
setIsDrawing(true);
const x = (e.clientX || e.touches?.[0]?.clientX) - rect.left;
const y = (e.clientY || e.touches?.[0]?.clientY) - rect.top;
ctx.beginPath();
ctx.moveTo(x, y);
};
const draw = (e) => {
if (!isDrawing) return;
e.preventDefault();
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
const x = (e.clientX || e.touches?.[0]?.clientX) - rect.left;
const y = (e.clientY || e.touches?.[0]?.clientY) - rect.top;
ctx.lineTo(x, y);
ctx.stroke();
};
const stopDrawing = () => {
if (isDrawing) {
const canvas = canvasRef.current;
if (canvas) {
setSignatureData(canvas.toDataURL('image/png'));
}
setIsDrawing(false);
}
};
const clearSignature = () => {
const canvas = canvasRef.current;
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
setSignatureData(null);
};
// NEW: Adopt saved signature
const adoptSavedSignature = () => {
if (!currentUser?.saved_signature) {
toast({
title: "No Saved Signature",
description: "You don't have a saved signature yet.",
variant: "destructive",
});
return;
}
const canvas = canvasRef.current;
if (!canvas) {
toast({
title: "Error",
description: "Signature pad not ready.",
variant: "destructive",
});
return;
}
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
setSignatureData(currentUser.saved_signature);
toast({
title: "Signature Adopted",
description: "Your saved signature has been loaded.",
});
};
img.onerror = () => {
toast({
title: "Failed to Load Signature",
description: "Could not load your saved signature.",
variant: "destructive",
});
};
img.src = currentUser.saved_signature;
};
const googleDocsViewerUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(documentUrl)}&embedded=true`;
return (
<div className="space-y-4">
{/* Simplified Control Bar */}
<Card className="border-2 border-[#0A39DF] shadow-2xl sticky top-4 z-20 bg-gradient-to-r from-[#0A39DF] to-[#1C323E]">
<CardContent className="p-4">
<div className="flex items-center justify-between gap-4">
{/* Left: Document Info */}
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center">
<FileText className="w-6 h-6 text-[#0A39DF]" />
</div>
<div>
<h3 className="font-bold text-white">{documentName}</h3>
<div className="flex items-center gap-3 mt-1">
<Badge variant="outline" className="text-xs bg-white/20 text-white border-white/30">
{documentType}
</Badge>
<span className="text-xs text-white/80 flex items-center gap-1">
<Clock className="w-3 h-3" />
{reviewTime} min
</span>
</div>
</div>
</div>
{/* Right: Actions */}
<div className="flex items-center gap-2">
<Button
onClick={handleDownloadPDF}
size="sm"
variant="outline"
className="bg-white/10 border-white/30 text-white hover:bg-white/20"
>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
<Button
onClick={handlePrint}
size="sm"
variant="outline"
className="bg-white/10 border-white/30 text-white hover:bg-white/20"
>
<Printer className="w-4 h-4 mr-2" />
Print
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Full Width PDF Viewer */}
<Card className="border-slate-200 shadow-xl">
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-100 p-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Eye className="w-5 h-5 text-[#0A39DF]" />
Document Preview
</CardTitle>
{/* PDF Controls */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 bg-slate-100 rounded-lg p-1">
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<Input
type="number"
value={pageNumber}
onChange={(e) => setPageNumber(parseInt(e.target.value) || 1)}
className="w-16 h-8 text-center text-sm"
min="1"
/>
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => setPageNumber(pageNumber + 1)}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
<div className="flex items-center gap-1 bg-slate-100 rounded-lg p-1">
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => setZoom(Math.max(50, zoom - 10))}
>
<ZoomOut className="w-4 h-4" />
</Button>
<span className="text-xs font-medium px-2">{zoom}%</span>
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={() => setZoom(Math.min(200, zoom + 10))}
>
<ZoomIn className="w-4 h-4" />
</Button>
</div>
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={handleFullscreenToggle}
>
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className={`w-full bg-slate-50 ${isFullscreen ? 'h-screen' : 'h-[800px]'}`}>
<iframe
src={googleDocsViewerUrl}
className="w-full h-full"
title={documentName}
style={{ border: 'none' }}
/>
</div>
</CardContent>
</Card>
{/* Acknowledge Section */}
{!isAcknowledged && (
<Card className="border-slate-200 shadow-xl">
<CardHeader className="bg-gradient-to-br from-green-50 to-white border-b border-slate-100">
<CardTitle className="text-base flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-green-600" />
Acknowledge & Accept
</CardTitle>
</CardHeader>
<CardContent className="p-6 space-y-6">
{/* Full Name Input */}
<div>
<Label className="text-sm font-medium">Full Name *</Label>
<Input
value={signerName}
onChange={(e) => setSignerName(e.target.value)}
placeholder="Enter your full legal name"
className="mt-2"
required
/>
</div>
{/* Signature Pad */}
<div>
<Label className="text-sm font-medium flex items-center gap-2">
<PenTool className="w-4 h-4" />
Digital Signature *
</Label>
<p className="text-xs text-slate-500 mt-1 mb-2">
Sign using your mouse or fingertip on touch devices
</p>
<div className="border-2 border-dashed border-slate-300 rounded-lg p-4 bg-white">
<canvas
ref={canvasRef}
width={600}
height={200}
className="w-full border border-slate-200 rounded cursor-crosshair touch-none"
style={{ touchAction: 'none' }}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
onTouchStart={startDrawing}
onTouchMove={draw}
onTouchEnd={stopDrawing}
/>
<div className="flex justify-between items-center mt-2">
{/* NEW: Adopt Signature Button */}
{currentUser?.saved_signature && (
<Button
type="button"
variant="outline"
size="sm"
onClick={adoptSavedSignature}
className="text-[#0A39DF] border-[#0A39DF] hover:bg-blue-50"
>
<Upload className="w-3 h-3 mr-2" />
Use Saved Signature
</Button>
)}
<div className={currentUser?.saved_signature ? "" : "ml-auto"}>
<Button
type="button"
variant="outline"
size="sm"
onClick={clearSignature}
className="text-slate-600"
>
<RefreshCw className="w-3 h-3 mr-2" />
Clear Signature
</Button>
</div>
</div>
</div>
</div>
{/* Additional Notes */}
<div>
<Label className="text-sm font-medium">Additional Notes (Optional)</Label>
<Textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Add any notes or comments about this document..."
rows={3}
className="mt-2"
/>
</div>
{/* Acknowledgment Statement */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-900">
<strong>By signing below, I acknowledge that:</strong>
</p>
<ul className="text-sm text-blue-800 mt-2 space-y-1 list-disc list-inside">
<li>I have read and understood this {documentType}</li>
<li>I agree to the terms and conditions outlined</li>
<li>My electronic signature is legally binding</li>
<li>Review time: {reviewTime} {reviewTime === 1 ? 'minute' : 'minutes'}</li>
</ul>
</div>
<Button
onClick={handleAcknowledge}
className="w-full bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white py-6 text-lg font-bold shadow-xl"
>
<CheckCircle2 className="w-6 h-6 mr-3" />
I Have Read and Accept This {documentType}
</Button>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,149 @@
import React from "react";
import { Link } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { MapPin, Users, Calendar, Clock } from "lucide-react";
import { format, differenceInHours, parseISO } from "date-fns";
function UpcomingOrderItem({ order }) {
const assignedCount = order.assigned_staff?.length || 0;
const requestedCount = order.requested || 0;
const fillPercentage = requestedCount > 0 ? Math.round((assignedCount / requestedCount) * 100) : 0;
// Calculate ETA (hours until event)
const eventDate = order.date ? parseISO(order.date) : new Date();
const hoursUntil = differenceInHours(eventDate, new Date());
const eta = hoursUntil > 24 ? `${Math.round(hoursUntil / 24)}d` : `${hoursUntil}h`;
// Determine status
let status = "On track";
let statusColor = "bg-emerald-100 text-emerald-700 border-emerald-200";
if (fillPercentage < 60) {
status = "At risk";
statusColor = "bg-red-100 text-red-700 border-red-200";
} else if (fillPercentage < 90) {
status = "Attention";
statusColor = "bg-amber-100 text-amber-700 border-amber-200";
}
// Progress bar color based on status
let progressColor = "bg-[#1C323E]";
if (fillPercentage < 60) progressColor = "bg-red-500";
else if (fillPercentage < 90) progressColor = "bg-amber-500";
return (
<div className="p-5 rounded-2xl bg-white border-2 border-slate-200 hover:border-[#0A39DF] transition-all shadow-sm hover:shadow-md">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-bold text-[#1C323E] mb-1">
{order.business_name || "Client"} {order.event_name}
</h3>
</div>
<Badge className={`${statusColor} border font-semibold`}>
{status}
</Badge>
</div>
{/* Staff Info */}
<div className="flex items-center gap-2 text-slate-600 mb-3">
<Users className="w-4 h-4" />
<span className="text-sm font-medium">
{assignedCount} × {order.shifts?.[0]?.roles?.[0]?.role || "Staff"}
{requestedCount > assignedCount && (
<span className="text-slate-400 ml-1">/ {requestedCount} needed</span>
)}
</span>
</div>
{/* Date/Time */}
<div className="flex items-center gap-2 text-slate-600 mb-4">
<Calendar className="w-4 h-4" />
<span className="text-sm">
{order.date ? format(parseISO(order.date), "EEE, HH:mm") : "Date TBD"}
{order.shifts?.[0]?.roles?.[0]?.end_time && (
<span>{order.shifts[0].roles[0].end_time}</span>
)}
</span>
</div>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex items-center justify-between text-xs text-slate-600 mb-2">
<span className="font-semibold">{fillPercentage}%</span>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span className="font-medium">ETA {eta}</span>
</div>
</div>
<div className="w-full h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className={`h-full ${progressColor} transition-all duration-500 rounded-full`}
style={{ width: `${fillPercentage}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-3">
<Link to={createPageUrl(`EventDetail?id=${order.id}`)} className="flex-1">
<Button
variant="outline"
className="w-full rounded-xl border-slate-300 hover:bg-slate-50"
>
View
</Button>
</Link>
<Link to={createPageUrl(`EventDetail?id=${order.id}`)} className="flex-1">
<Button className="w-full bg-[#1C323E] hover:bg-[#1C323E]/90 text-white rounded-xl font-semibold">
Smart Assign
</Button>
</Link>
</div>
</div>
);
}
export default function UpcomingOrdersCard({ orders }) {
if (!orders || orders.length === 0) {
return (
<Card className="bg-white border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<CardTitle className="text-base flex items-center gap-2">
<MapPin className="w-5 h-5 text-[#0A39DF]" />
Upcoming Orders
</CardTitle>
</CardHeader>
<CardContent className="p-8 text-center">
<Calendar className="w-12 h-12 mx-auto text-slate-300 mb-3" />
<p className="text-sm text-slate-500 font-medium">No upcoming orders</p>
<p className="text-xs text-slate-400 mt-1">New orders will appear here</p>
</CardContent>
</Card>
);
}
return (
<Card className="bg-white border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<MapPin className="w-5 h-5 text-[#0A39DF]" />
Upcoming Orders
</CardTitle>
<Badge variant="outline" className="font-semibold">
{orders.length} {orders.length === 1 ? 'order' : 'orders'}
</Badge>
</div>
</CardHeader>
<CardContent className="p-5 space-y-4">
{orders.map((order) => (
<UpcomingOrderItem key={order.id} order={order} />
))}
</CardContent>
</Card>
);
}