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:
514
frontend-web/src/components/vendor/DocumentViewer.jsx
vendored
Normal file
514
frontend-web/src/components/vendor/DocumentViewer.jsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
149
frontend-web/src/components/vendor/UpcomingOrdersCard.jsx
vendored
Normal file
149
frontend-web/src/components/vendor/UpcomingOrdersCard.jsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user