feat[agent]: integrate bidding system into Loads page with modal submission
This commit is contained in:
parent
6a8e7490d2
commit
c1c680d92b
3 changed files with 195 additions and 18 deletions
71
frontend/src/components/BidFeed.jsx
Normal file
71
frontend/src/components/BidFeed.jsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '../supabaseClient';
|
||||
|
||||
function BidFeed({ loadId }) {
|
||||
const { data: bids = [], isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ['bids', loadId],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('bids')
|
||||
.select('*, driver:portal_users(username)')
|
||||
.eq('load_id', loadId)
|
||||
.order('created_at', { ascending: false });
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
refetchInterval: 10000, // Poll every 10 seconds for updates
|
||||
});
|
||||
|
||||
if (isLoading) return <div className="text-center py-4">Loading bids...</div>;
|
||||
if (isError) return <div className="text-center py-4 text-danger">Error loading bids</div>;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h2>Active Bids</h2>
|
||||
{bids.length === 0 ? (
|
||||
<p className="text-muted">No bids yet. Be the first to offer!</p>
|
||||
) : (
|
||||
<table className="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Driver</th>
|
||||
<th>Bid Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Time</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bids.map((bid) => (
|
||||
<tr key={bid.id}>
|
||||
<td>{bid.driver?.username || 'Unknown'}</td>
|
||||
<td>₹{parseFloat(bid.bid_amount).toLocaleString('en-IN')}</td>
|
||||
<td>
|
||||
<span className={`badge ${
|
||||
bid.status === 'accepted'
|
||||
? 'bg-success'
|
||||
: bid.status === 'rejected'
|
||||
? 'bg-danger'
|
||||
: bid.status === 'counter_offer'
|
||||
? 'bg-warning'
|
||||
: 'bg-secondary'
|
||||
}`}>
|
||||
{bid.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>{new Date(bid.created_at).toLocaleString()}</td>
|
||||
<td>
|
||||
{/* Action buttons will be added for shipper to accept/reject */}
|
||||
<button className="btn btn-sm btn-outline-primary">View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BidFeed;
|
||||
91
frontend/src/components/BidSubmissionModal.jsx
Normal file
91
frontend/src/components/BidSubmissionModal.jsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '../supabaseClient';
|
||||
|
||||
function BidSubmissionModal({ loadId, onClose, onSuccess }) {
|
||||
const [bidAmount, setBidAmount] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (bidData) => {
|
||||
const { data, error } = await supabase
|
||||
.from('bids')
|
||||
.insert({
|
||||
load_id: loadId,
|
||||
driver_id: (await supabase.auth.getUser()).data.user?.id,
|
||||
bid_amount: parseFloat(bidAmount),
|
||||
notes: notes,
|
||||
status: 'pending',
|
||||
})
|
||||
.select();
|
||||
if (error) throw error;
|
||||
return data[0];
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['bids', loadId] });
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await mutation.mutateAsync();
|
||||
} catch (err) {
|
||||
console.error('Bid submission failed:', err.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Submit Bid for Load</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Bid Amount (₹)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="form-control"
|
||||
value={bidAmount}
|
||||
onChange={(e) => setBidAmount(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Notes (Optional)</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
rows="3"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Bid'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BidSubmissionModal;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '../supabaseClient';
|
||||
import BidSubmissionModal from './BidSubmissionModal';
|
||||
|
||||
// Utility functions (mirroring india.js logic for now)
|
||||
const formatINR = (n) => {
|
||||
if (n === null || n === undefined || isNaN(n)) return '—';
|
||||
return '₹' + parseFloat(n).toLocaleString('en-IN');
|
||||
|
|
@ -26,41 +26,35 @@ const getStatusColor = (status) => {
|
|||
'available vehicle': 'secondary',
|
||||
};
|
||||
return colors[status] || 'secondary';
|
||||
};
|
||||
}
|
||||
|
||||
function LoadsList() {
|
||||
const [filterStatus, setFilterStatus] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showBidModal, setShowBidModal] = useState(false);
|
||||
const [selectedLoadId, setSelectedLoadId] = useState(null);
|
||||
|
||||
const { data: loads = [], isLoading, isError, refetch } = useQuery({
|
||||
const { data: loads = [], isLoading, isError } = useQuery({
|
||||
queryKey: ['loads', filterStatus, searchTerm],
|
||||
queryFn: async () => {
|
||||
let query = supabase
|
||||
.from('loads')
|
||||
.select(`
|
||||
id,
|
||||
date,
|
||||
from_city,
|
||||
to_city,
|
||||
freight_charged,
|
||||
commission,
|
||||
status,
|
||||
shipper:shippers(name),
|
||||
vehicle:vehicles(number)
|
||||
`)
|
||||
.select('*, shipper:shippers(name), vehicle:vehicles(number)')
|
||||
.order('date', { ascending: false })
|
||||
.limit(100);
|
||||
|
||||
if (filterStatus) query = query.eq('status', filterStatus);
|
||||
if (searchTerm) {
|
||||
query = query.or(`from_city.ilike.%${searchTerm}%,to_city.ilike.%${searchTerm}%`);
|
||||
}
|
||||
if (filterStatus) {
|
||||
query = query.eq('status', filterStatus);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
if (isLoading) return <div className="text-center py-5">Loading loads...</div>;
|
||||
|
|
@ -106,6 +100,7 @@ function LoadsList() {
|
|||
<th>Freight</th>
|
||||
<th>Commission</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -120,11 +115,31 @@ function LoadsList() {
|
|||
<span className={`badge bg-${getStatusColor(load.status)}`}>
|
||||
{load.status}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary ms-2"
|
||||
onClick={() => {
|
||||
setSelectedLoadId(load.id);
|
||||
setShowBidModal(true);
|
||||
}}
|
||||
>
|
||||
Bid
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Modal */}
|
||||
{showBidModal && (
|
||||
<BidSubmissionModal
|
||||
loadId={selectedLoadId}
|
||||
onClose={() => {
|
||||
setShowBidModal(false);
|
||||
setSelectedLoadId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue