Transporter Dashboard 18/04/2025
Fri Apr 18 2025 04:51:45 GMT+0000 (Coordinated Universal Time)
Saved by @SrijanVerma
import { useEffect, useState } from "react" import { toast } from "react-toastify" import { useNavigate } from "react-router-dom" import { useSelector } from "react-redux" import axios from "axios" import { CheckCircle, XCircle, Home, MapPin, Package, PenToolIcon as Tool, FileText, Clock, Calendar, Truck, DollarSign, FileUp, CheckCheck, Award } from "lucide-react" import Navbar from "../components/Navbar" import { ConfirmationModal } from "../modals/ConfirmationModal" import API from "../API" import { useDispatch } from "react-redux" import { logout } from "../utils/UserSlice" const TransporterDashboardPage = () => { const [tenders, setTenders] = useState([]) const [history, setHistory] = useState([]) const [showModal, setShowModal] = useState(false) const [selectedTender, setSelectedTender] = useState(null) const [responseForm, setResponseForm] = useState({ price: "", vehicleNo: "", attachments: [], }) const [view, setView] = useState(false) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) const [confirmDialog, setConfirmDialog] = useState(null) const [historyLoading, setHistoryLoading] = useState(false) const [historyError, setHistoryError] = useState(null) const [transporters, setTransporters] = useState([]) const [transporterMap, setTransporterMap] = useState({}) const navigate = useNavigate() const dispatch = useDispatch() const userInfo = useSelector((state) => state.User?.userInfo) const userName = userInfo?.name || "RR User" const fetchTenders = async () => { try { const res = await fetch(`${API.FETCH_ALL_TENDERS}`, { method: "GET", headers: { "Content-Type": "application/json", }, credentials: "include", }) const data = await res.json() // console.log("fetch all tenders : ", data.data) const userId = userInfo?._id // Mark tenders where this user has submitted a quotation const updatedTenders = (data.data || []).map((tender) => { const hasUserQuoted = (tender.quotations || []).some((q) => { if (typeof q === 'object' && q.transportUser?._id) { return q.transportUser._id === userId } return false }) return { ...tender, hasUserQuoted, } }) setTenders(updatedTenders); } catch (err) { console.error("Fetch error:", err) setError("Failed to load tenders.") } finally { setLoading(false) } } useEffect(() => { fetchTenders() }, []) const fetchTransporters = async () => { try { const res = await axios.get(API.FETCH_ALL_TRANSPORTER, { withCredentials: true, }) const list = res.data?.data || [] // console.log("transporter's name : ", res.data) setTransporters(list) // create a map for quick lookup const map = {} list.forEach((t) => { map[t._id] = t.name }) setTransporterMap(map) } catch (err) { console.error("Error fetching transporter list:", err) } } // const handleReject = (id) => { // setConfirmDialog({ // message: "Are you sure you want to reject this tender?", // onConfirm: () => { // setTenders((prev) => prev.filter((t) => t.id !== id)) // toast.info("Tender rejected.") // setConfirmDialog(null) // }, // onCancel: () => setConfirmDialog(null), // }) // } const handleApprove = (tender) => { setSelectedTender(tender) setShowModal(true) } const handleFileChange = (e) => { const file = e.target.files?.[0] if (file && !["image/jpeg", "image/jpg"].includes(file.type)) { toast.error("Only JPG and JPEG files are allowed.") e.target.value = null // Reset input return } setResponseForm({ ...responseForm, attachments: e.target.files?.[0] ? [e.target.files[0]] : [], }) } const handleResponseChange = (e) => { setResponseForm({ ...responseForm, [e.target.name]: e.target.value }) } const handleSubmitResponse = async () => { if (!responseForm.price || !responseForm.vehicleNo) { toast.warning("Price and vehicle number are required.") return } const formData = new FormData() formData.append("price", responseForm.price) formData.append("vehicleNumber", responseForm.vehicleNo) if (responseForm.attachments.length > 0) { formData.append("file", responseForm.attachments[0]) } // console.log([...formData.entries()]) setIsSubmitting(true) try { const res = await fetch(`${API.SUBMIT_QUOTATION}/${selectedTender._id}`, { method: "POST", body: formData, credentials: "include", }) const result = await res.json() if (res.ok) { toast.success("Quotation submitted successfully!") setHistory((prev) => [ ...prev, { rrName: selectedTender.rrName, price: responseForm.price, vehicleNo: responseForm.vehicleNo, attachments: result.data.files?.map((f) => f.originalName || f.url) || [], dispatchLocation: selectedTender.dispatchLocation, materials: selectedTender.materials.map((m) => ({ item: m.material, subItem: m.subMaterial, weight: m.weight, quantity: m.quantity, })), }, ]) setShowModal(false) setResponseForm({ price: "", vehicleNo: "", attachments: [] }) // Refresh tender list to reflect quotation await fetchTenders() } else { toast.error(result.message || "Failed to submit quotation.") } } catch (error) { console.error("Quotation Submit Error:", error) toast.error("An error occurred while submitting your quotation.") } finally { setIsSubmitting(false) } } // const handleClearHistory = () => { // setConfirmDialog({ // message: "Are you sure you want to delete all history?", // onConfirm: () => { // setHistory([]) // toast.info("All history cleared.") // setConfirmDialog(null) // }, // onCancel: () => setConfirmDialog(null), // }) // } // const handleDeleteHistoryItem = (index) => { // setConfirmDialog({ // message: "Delete this history entry?", // onConfirm: () => { // setHistory((prev) => prev.filter((_, idx) => idx !== index)) // toast.success("History entry deleted.") // setConfirmDialog(null) // }, // onCancel: () => setConfirmDialog(null), // }) // } const handleLogout = async () => { try { await axios.post(API.LOGOUT_USER, {}, { withCredentials: true }) dispatch(logout()) toast.success("Logged out successfully!") } catch (err) { console.error("Logout failed:", err) toast.error("Logout failed. Please try again.") } finally { navigate("/signin") } } const handleToggleView = async () => { if (!view) { // Switching to history view setHistoryLoading(true) setHistoryError(null) try { await fetchTransporters() const res = await axios.get(API.HISTORY_FOR_QUOTATION_QUOTE, { withCredentials: true, headers: { "Content-Type": "application/json", }, }) // console.log("History response:", res.data) const enriched = res.data.data.map((entry) => { return { ...entry, transporterName: transporterMap[entry.tenderId] || "Unknown Transporter", } }) setHistory(enriched) } catch (err) { console.error("Failed to load history:", err) if (err.response) { console.error("Server responded with:", err.response.status, err.response.data) } else if (err.request) { console.error("No response received:", err.request) } setHistoryError("Failed to load submission history.") } finally { setHistoryLoading(false) } } setView((prev) => !prev) } const navbarActions = ( <button onClick={handleToggleView} className="px-4 py-2 bg-teal-600 text-white rounded-md hover:bg-teal-700 transition-colors duration-200 flex items-center gap-2 shadow-sm" > {view ? ( <> <Package className="h-4 w-4" /> Back to Dashboard </> ) : ( <> <Clock className="h-4 w-4" /> View History </> )} </button> ) const formatDate = (dateString) => { if (!dateString) return "N/A" return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }) } return ( <div className="min-h-screen bg-slate-200"> <Navbar title="Transporter Dashboard" userName={userName || "Transport User"} actions={navbarActions} onLogout={handleLogout} /> <div className="py-8 px-4 max-w-6xl mx-auto"> {loading ? ( <div className="flex justify-center items-center h-64"> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-teal-500"></div> </div> ) : error ? ( <div className="text-center p-8 bg-red-50 rounded-lg border border-red-200 text-red-600"> <XCircle className="h-8 w-8 mx-auto mb-2" /> {error} </div> ) : view ? ( historyLoading ? ( <div className="flex justify-center items-center h-64"> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-teal-500"></div> </div> ) : historyError ? ( <div className="text-center p-8 bg-red-50 rounded-lg border border-red-200 text-red-600"> <XCircle className="h-8 w-8 mx-auto mb-2" /> {historyError} </div> ) : history.length > 0 ? ( <div className="mb-10"> <div className="flex justify-between items-center mb-6"> <h3 className="text-2xl font-bold text-teal-700 flex items-center gap-2"> <FileText className="h-6 w-6 text-teal-600" /> Submission History </h3> {/* <button onClick={handleClearHistory} className=" bg-red-100 flex items-center gap-2 px-4 py-2 text-red-600 border border-red-300 rounded-md hover:bg-red-500 hover:text-white transition-colors duration-200"> <XCircle className="h-4 w-4" /> Clear All </button> */} </div> <div className="grid gap-6"> {history.map((entry, idx) => ( <div key={entry._id || idx} className="relative bg-white border border-slate-200 rounded-xl shadow-sm p-6 transition-all duration-200 hover:shadow-md" > {/* Delete Button */} {/* <button onClick={() => handleDeleteHistoryItem(idx)} className="absolute top-4 right-4 text-slate-400 hover:text-red-500 transition-colors duration-200" title="Delete Entry"> <XCircle className="h-5 w-5" /> </button> */} {/* Header with status badge */} <div className="mb-4 pb-4 border-b border-slate-100 "> <div className="flex flex-wrap items-start justify-between gap-2"> <h4 className="text-lg font-semibold text-slate-800">Quotation #{idx + 1}</h4> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-teal-100 text-teal-800"> Submitted </span> </div> </div> {/* Tender Info Grid */} <div className="grid sm:grid-cols-2 gap-4 text-sm"> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <Home className="h-4 w-4" /> </span> <div> <p className="text-slate-500">RR User</p> <p className="font-medium text-slate-800">{entry.tender?.createdBy?.name || "Unknown"}</p> <p className="text-xs text-slate-500"> {entry.tender?.createdBy?.email || "Email not available"} </p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <Calendar className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Delivery Window</p> <p className="font-medium text-slate-800"> {entry.tender?.deliveryWindow?.from && entry.tender?.deliveryWindow?.to ? `${formatDate(entry.tender.deliveryWindow.from)} to ${formatDate(entry.tender.deliveryWindow.to)}` : "N/A"} </p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <Clock className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Closing Date</p> <p className="font-medium text-slate-800">{formatDate(entry.tender?.closeDate)}</p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <Clock className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Submitted On</p> <p className="font-medium text-slate-800"> {new Date(entry.createdAt).toLocaleString("en-US", { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: true, })} </p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <MapPin className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Location</p> <p className="font-medium text-slate-800">{entry.tender?.dispatchLocation || "N/A"}</p> <p className="text-xs text-slate-500"> {entry.tender?.address ? `${entry.tender.address}, ` : ""} {entry.tender?.pincode || ""} </p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <Truck className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Vehicle Number</p> <p className="font-medium text-slate-800">{entry.vehicleNumber}</p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <DollarSign className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Price</p> <p className="font-medium text-slate-800">₹ {entry.price}</p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <Package className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Total Weight</p> <p className="font-medium text-slate-800">{entry.tender?.totalWeight ?? "N/A"} kg</p> </div> </div> <div className="flex items-start gap-2"> <span className="text-slate-500 mt-0.5"> <Package className="h-4 w-4" /> </span> <div> <p className="text-slate-500">Total Quantity</p> <p className="font-medium text-slate-800">{entry.tender?.totalQuantity ?? "N/A"} pcs</p> </div> </div> </div> {/* Remarks */} {entry.tender?.remarks && ( <div className="mt-4 bg-amber-50 border border-amber-100 rounded-lg p-3 text-sm"> <div className="flex gap-2"> <FileText className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" /> <div> <span className="font-medium text-amber-800">Remarks:</span> {entry.tender.remarks} </div> </div> </div> )} {/* Materials List */} {entry.tender?.materials?.length > 0 && ( <div className="mt-4 bg-slate-50 border border-slate-200 rounded-lg p-4"> <h4 className="text-sm font-semibold text-slate-800 mb-3 flex items-center gap-2"> <Tool className="h-4 w-4 text-slate-600" /> Materials </h4> <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead> <tr className="bg-slate-100 text-slate-600"> <th className="px-3 py-2 text-left">Material</th> <th className="px-3 py-2 text-left">Sub Item</th> <th className="px-3 py-2 text-right">Weight (Kg)</th> <th className="px-3 py-2 text-right">Quantity</th> </tr> </thead> <tbody> {entry.tender.materials.map((m, i) => ( <tr key={m._id || i} className="border-t border-slate-200"> <td className="px-3 py-2 font-medium">{m.material}</td> <td className="px-3 py-2">{m.subMaterial || "-"}</td> <td className="px-3 py-2 text-right">{m.weight || "-"}</td> <td className="px-3 py-2 text-right">{m.quantity || "-"}</td> </tr> ))} </tbody> </table> </div> </div> )} {/* Attachments */} <div className="mt-4"> <h4 className="text-sm font-semibold text-slate-800 mb-2 flex items-center gap-2"> <FileUp className="h-4 w-4 text-slate-600" /> Attachments </h4> {entry.files?.length > 0 ? ( <div className="flex flex-wrap gap-2"> {entry.files.map((f, i) => { const label = f.originalName || f.name || `File ${i + 1}` const fileUrl = f.url || f return ( <a key={i} href={fileUrl} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 text-slate-700 rounded-md hover:bg-slate-200 transition-colors duration-200 text-sm" > <FileText className="h-3.5 w-3.5" /> {label} </a> ) })} </div> ) : ( <p className="text-slate-500 text-sm">No attachments</p> )} </div> </div> ))} </div> </div> ) : ( <div className="flex flex-col items-center justify-center h-64 bg-white rounded-xl border border-slate-200 p-8"> <FileText className="h-12 w-12 text-slate-300 mb-4" /> <p className="text-slate-500 text-lg">No submission history available</p> <button onClick={handleToggleView} className="mt-4 px-4 py-2 bg-teal-600 text-white rounded-md hover:bg-teal-700 transition-colors duration-200" > Return to Dashboard </button> </div> ) ) : tenders.length === 0 ? ( <div className="flex flex-col items-center justify-center h-64 bg-white rounded-xl border border-slate-200 p-8"> <Package className="h-12 w-12 text-slate-300 mb-4" /> <p className="text-slate-500 text-lg">No pending tenders available</p> </div> ) : ( <> <h1 className="text-2xl font-bold text-teal-700 mb-6 flex items-center gap-2"> <Package className="h-6 w-6" /> Available Tenders </h1> <div className="space-y-8"> {tenders.map((tender) => ( <div key={tender._id} className="bg-white p-6 rounded-xl shadow-sm border border-slate-200 transition-all duration-200 hover:shadow-md" > {/* Header */} <div className="flex flex-wrap justify-between items-start gap-4 pb-4 border-b border-slate-100 mb-4"> <div> <h3 className="text-xl font-bold text-slate-800 mb-1"> Tender from {tender.createdBy?.name || "Unknown RR User"} </h3> <p className="text-sm text-slate-500">{tender.createdBy?.email || "Email not available"}</p> </div> {/* <div className="flex items-center gap-2"> <span className="text-xs font-medium px-2.5 py-0.5 rounded-full bg-amber-100 text-amber-800"> Pending Response </span> </div> */} <div className="flex items-center gap-2"> <span className={`text-xs font-medium px-2.5 py-0.5 rounded-full ${tender.hasUserQuoted ? "bg-green-100 text-green-800" : "bg-amber-100 text-amber-800"}`}> {tender.hasUserQuoted ? "Quotation Submitted" : "Pending Response"} </span> </div> </div> {/* Info Grid */} <div className="grid md:grid-cols-2 gap-4 mb-6"> <div className="flex items-start gap-3"> <div className="bg-teal-100 p-2 rounded-lg text-teal-600"> <Calendar className="h-5 w-5" /> </div> <div> <p className="text-sm text-slate-500">Delivery Window</p> <p className="font-medium text-slate-800"> {tender.deliveryWindow?.from && tender.deliveryWindow?.to ? `${formatDate(tender.deliveryWindow.from)} to ${formatDate(tender.deliveryWindow.to)}` : "N/A"} </p> </div> </div> <div className="flex items-start gap-3"> <div className="bg-purple-100 p-2 rounded-lg text-purple-600"> <Clock className="h-5 w-5" /> </div> <div> <p className="text-sm text-slate-500">Closing Date</p> <p className="font-medium text-slate-800">{formatDate(tender?.closeDate)}</p> </div> </div> <div className="flex items-start gap-3"> <div className="bg-rose-100 p-2 rounded-lg text-rose-600"> <MapPin className="h-5 w-5" /> </div> <div> <p className="text-sm text-slate-500">Location</p> <p className="font-medium text-slate-800">{tender.dispatchLocation || "N/A"}</p> </div> </div> <div className="flex items-start gap-3"> <div className="bg-sky-100 p-2 rounded-lg text-sky-600"> <Home className="h-5 w-5" /> </div> <div> <p className="text-sm text-slate-500">Address</p> <p className="font-medium text-slate-800"> {tender.address || "N/A"} {tender.pincode ? `, ${tender.pincode}` : ""} </p> </div> </div> </div> {/* Materials Table */} <div className="bg-slate-50 rounded-lg p-4 mb-6"> <h4 className="font-semibold mb-3 text-slate-700 flex items-center gap-2"> <Tool className="h-5 w-5 text-slate-600" /> Materials </h4> <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead> <tr className="bg-slate-100 text-slate-600"> <th className="px-4 py-2 text-left rounded-l-md">Material</th> <th className="px-4 py-2 text-left">Sub Item</th> <th className="px-4 py-2 text-right">Weight (Kg)</th> <th className="px-4 py-2 text-right rounded-r-md">Quantity</th> </tr> </thead> <tbody> {tender.materials.map((m, index) => ( <tr key={m._id} className={`${index % 2 === 0 ? "bg-white" : "bg-slate-50"} hover:bg-slate-100 transition-colors duration-150`} > <td className="px-4 py-2 font-medium">{m.material}</td> <td className="px-4 py-2">{m.subMaterial || "-"}</td> <td className="px-4 py-2 text-right">{m.weight || "-"}</td> <td className="px-4 py-2 text-right">{m.quantity || "-"}</td> </tr> ))} </tbody> </table> </div> </div> {/* Summary Stats */} <div className="flex flex-wrap gap-4 mb-4"> <div className="bg-teal-50 border border-teal-100 px-4 py-2 rounded-lg"> <span className="text-slate-600 text-sm">Total Weight: </span> <span className="text-teal-700 font-semibold">{tender?.totalWeight ?? "N/A"} kg</span> </div> <div className="bg-teal-50 border border-teal-100 px-4 py-2 rounded-lg"> <span className="text-slate-600 text-sm">Total Quantity: </span> <span className="text-teal-700 font-semibold">{tender?.totalQuantity ?? "N/A"} pcs</span> </div> </div> {/* Remarks */} {tender.remarks && ( <div className="mb-6 bg-amber-50 border border-amber-100 rounded-lg p-3 text-sm"> <div className="flex gap-2"> <FileText className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" /> <div> <span className="font-medium text-amber-800">Remarks:</span> {tender.remarks} </div> </div> </div> )} {/* Action Buttons */} {tender.hasUserQuoted ? ( <div className="flex justify-center mt-4 animate-fadeIn"> <div className="inline-flex items-center gap-2 px-5 py-3 bg-gradient-to-r from-green-50 to-emerald-50 text-emerald-700 font-medium text-sm border border-emerald-200 rounded-lg shadow-sm"> <Award className="h-5 w-5 text-emerald-500" /> <span>Quotation Submitted</span> <CheckCheck className="h-5 w-5 text-emerald-500 ml-1" /> </div> </div> ) : ( <div className="flex flex-col sm:flex-row gap-3 justify-center mt-6"> {/* <button onClick={() => handleReject(tender._id)} className="bg-white border border-slate-300 text-slate-700 px-6 py-2.5 rounded-lg hover:bg-slate-50 transition-all duration-200 flex items-center justify-center gap-2 shadow-sm hover:shadow" > <XCircle className="h-5 w-5 text-red-500" /> <span>Reject</span> </button> */} <button onClick={() => handleApprove(tender)} className="bg-gradient-to-r from-teal-600 to-emerald-600 text-white px-6 py-2.5 rounded-lg hover:from-teal-700 hover:to-emerald-700 transition-all duration-200 flex items-center justify-center gap-2 shadow-sm hover:shadow" > <CheckCircle className="h-5 w-5" /> <span>Approve</span> </button> </div> )} </div> ))} </div> </> )} </div> {/* Quotation Modal */} {showModal && selectedTender && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="bg-white rounded-xl p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto"> <h3 className="text-xl font-bold text-slate-800 mb-6 flex items-center gap-2"> <FileText className="h-5 w-5 text-teal-600" /> Submit Quotation </h3> <div className="space-y-5"> <div> <label className="block font-medium mb-1.5 text-sm text-slate-700">Price (₹)</label> <input name="price" value={responseForm.price} onChange={handleResponseChange} type="number" placeholder="Enter your price" className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent" required /> </div> <div> <label className="block font-medium mb-1.5 text-sm text-slate-700">Vehicle Details</label> <input name="vehicleNo" value={responseForm.vehicleNo} onChange={handleResponseChange} type="text" placeholder="Enter vehicle details" className="w-full px-3 py-2.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent" required /> </div> <div> <label className="block font-medium mb-1.5 text-sm text-slate-700">Attachments</label> <div className="border border-dashed border-slate-300 rounded-lg p-4 bg-slate-50"> <input type="file" accept=".jpg,.jpeg,image/jpeg" onChange={handleFileChange} className="w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-teal-50 file:text-teal-700 hover:file:bg-teal-100" /> <p className="text-xs text-slate-500 mt-2"> Only upload <strong>.jpg</strong> or <strong>.jpeg</strong> files </p> </div> </div> </div> <div className="flex flex-col sm:flex-row sm:justify-end gap-3 mt-8"> <button onClick={() => setShowModal(false)} className="px-5 py-2.5 border border-slate-300 rounded-lg text-sm text-slate-700 hover:bg-slate-50 transition-colors duration-200 order-2 sm:order-1" > Cancel </button> <button onClick={handleSubmitResponse} className="px-5 py-2.5 bg-teal-600 text-white rounded-lg text-sm hover:bg-teal-700 transition-colors duration-200 order-1 sm:order-2 flex items-center justify-center" disabled={isSubmitting} > {isSubmitting ? ( <> <svg className="animate-spin h-4 w-4 text-white mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" > <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" ></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path> </svg> Submitting... </> ) : ( "Submit Quotation" )} </button> </div> </div> </div> )} {/* Confirmation Modal */} {confirmDialog && ( <ConfirmationModal message={confirmDialog.message} onConfirm={confirmDialog.onConfirm} onCancel={confirmDialog.onCancel} /> )} </div> ) } export default TransporterDashboardPage
Comments