feat[shipper-dashboard]: add ShipperDashboard component with bid management UI
This commit is contained in:
parent
c1c680d92b
commit
8e8e7a5ff2
1 changed files with 164 additions and 0 deletions
164
frontend/src/components/ShipperDashboard.jsx
Normal file
164
frontend/src/components/ShipperDashboard.jsx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '../supabaseClient';
|
||||
import { formatINR } from '../lib/india';
|
||||
import { getStatusColor } from '../lib/india';
|
||||
|
||||
function ShipperDashboard() {
|
||||
const [selectedLoadId, setSelectedLoadId] = useState(null);
|
||||
const [bidModalOpen, setBidModalOpen] = useState(false);
|
||||
|
||||
const { data: loads, isLoading, isError } = useQuery({
|
||||
queryKey: ['loads'],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('loads')
|
||||
.select('*, shipper:shippers(name), vehicle:vehicles(number)')
|
||||
.order('date', { ascending: false })
|
||||
.limit(100);
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: bids } = useQuery({
|
||||
queryKey: ['bids', selectedLoadId],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('bids')
|
||||
.select('*, driver:portal_users(username)')
|
||||
.eq('load_id', selectedLoadId)
|
||||
.order('created_at', { ascending: false });
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const handleAccept = async (bidId) => {
|
||||
await fetch('/api/update-bid-status', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ bidId, newStatus: 'accepted' }),
|
||||
});
|
||||
// Refresh data after update
|
||||
await refetch();
|
||||
};
|
||||
|
||||
const { refetch } = useQuery({
|
||||
queryKey: ['loads'],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('loads')
|
||||
.select('*, shipper:shippers(name), vehicle:vehicles(number)')
|
||||
.order('date', { ascending: false })
|
||||
.limit(100);
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h2>Shipper Dashboard</h2>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-5">Loading loads...</div>
|
||||
) : isError ? (
|
||||
<div className="text-center py-5 text-danger">{isError}</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="d-flex justify-content-between mb-3">
|
||||
<button className="btn btn-outline-secondary" onClick={() => setBidModalOpen(true)}>
|
||||
New Bid
|
||||
</button>
|
||||
{selectedLoadId && (
|
||||
<button className="btn btn-outline-primary" onClick={() => setBidModalOpen(true)}>
|
||||
New Bid
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Route</th>
|
||||
<th>Shipper</th>
|
||||
<th>Freight</th>
|
||||
<th>Status</th>
|
||||
<th>Bids</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loads.map((load) => (
|
||||
<tr key={load.id}>
|
||||
<td>{new Date(load.date).toLocaleDateString('en-IN')}</td>
|
||||
<td>{load.from_city} → {load.to_city}</td>
|
||||
<td>{load.shipper?.name || ' — '}</td>
|
||||
<td>{formatINR(load.freight_charged)}</td>
|
||||
<td>
|
||||
<span className={`badge bg-${getStatusColor(load.status)}`}>
|
||||
{load.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{bids ? (
|
||||
<div>
|
||||
{bids.map((b) => (
|
||||
<div key={bid.id} className="badge bg-secondary mb-1">
|
||||
{bid.driver?.username || ' — '}: {formatINR(bid.bid_amount)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : ' — '}
|
||||
{selectedLoadId === load.id && (
|
||||
<div>
|
||||
{bids.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-success"
|
||||
onClick={() => handleAccept(bid.bid_id)}
|
||||
disabled={bid.status !== 'pending'}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-danger ms-1"
|
||||
onClick={() => {
|
||||
if (confirm('Reject this bid?')) {
|
||||
await fetch('/api/update-bid-status', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ bidId: bid.id, newStatus: 'rejected' })
|
||||
});
|
||||
await refetch();
|
||||
}
|
||||
}
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Modal for creating a new bid */}
|
||||
{bidModalOpen && (
|
||||
<BidSubmissionModal
|
||||
loadId={selectedLoadId}
|
||||
onClose={() => {
|
||||
setBidModalOpen(false);
|
||||
setSelectedLoadId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShipperDashboard;
|
||||
Loading…
Reference in a new issue