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 React, { useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { supabase } from '../supabaseClient';
|
import { supabase } from '../supabaseClient';
|
||||||
|
import BidSubmissionModal from './BidSubmissionModal';
|
||||||
|
|
||||||
// Utility functions (mirroring india.js logic for now)
|
|
||||||
const formatINR = (n) => {
|
const formatINR = (n) => {
|
||||||
if (n === null || n === undefined || isNaN(n)) return '—';
|
if (n === null || n === undefined || isNaN(n)) return '—';
|
||||||
return '₹' + parseFloat(n).toLocaleString('en-IN');
|
return '₹' + parseFloat(n).toLocaleString('en-IN');
|
||||||
|
|
@ -26,41 +26,35 @@ const getStatusColor = (status) => {
|
||||||
'available vehicle': 'secondary',
|
'available vehicle': 'secondary',
|
||||||
};
|
};
|
||||||
return colors[status] || 'secondary';
|
return colors[status] || 'secondary';
|
||||||
};
|
}
|
||||||
|
|
||||||
function LoadsList() {
|
function LoadsList() {
|
||||||
const [filterStatus, setFilterStatus] = useState('');
|
const [filterStatus, setFilterStatus] = useState('');
|
||||||
const [searchTerm, setSearchTerm] = 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],
|
queryKey: ['loads', filterStatus, searchTerm],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
let query = supabase
|
let query = supabase
|
||||||
.from('loads')
|
.from('loads')
|
||||||
.select(`
|
.select('*, shipper:shippers(name), vehicle:vehicles(number)')
|
||||||
id,
|
|
||||||
date,
|
|
||||||
from_city,
|
|
||||||
to_city,
|
|
||||||
freight_charged,
|
|
||||||
commission,
|
|
||||||
status,
|
|
||||||
shipper:shippers(name),
|
|
||||||
vehicle:vehicles(number)
|
|
||||||
`)
|
|
||||||
.order('date', { ascending: false })
|
.order('date', { ascending: false })
|
||||||
.limit(100);
|
.limit(100);
|
||||||
|
|
||||||
if (filterStatus) query = query.eq('status', filterStatus);
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
query = query.or(`from_city.ilike.%${searchTerm}%,to_city.ilike.%${searchTerm}%`);
|
query = query.or(`from_city.ilike.%${searchTerm}%,to_city.ilike.%${searchTerm}%`);
|
||||||
}
|
}
|
||||||
|
if (filterStatus) {
|
||||||
|
query = query.eq('status', filterStatus);
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error } = await query;
|
const { data, error } = await query;
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <div className="text-center py-5">Loading loads...</div>;
|
if (isLoading) return <div className="text-center py-5">Loading loads...</div>;
|
||||||
|
|
@ -106,6 +100,7 @@ function LoadsList() {
|
||||||
<th>Freight</th>
|
<th>Freight</th>
|
||||||
<th>Commission</th>
|
<th>Commission</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -120,11 +115,31 @@ function LoadsList() {
|
||||||
<span className={`badge bg-${getStatusColor(load.status)}`}>
|
<span className={`badge bg-${getStatusColor(load.status)}`}>
|
||||||
{load.status}
|
{load.status}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline-primary ms-2"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedLoadId(load.id);
|
||||||
|
setShowBidModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bid
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{showBidModal && (
|
||||||
|
<BidSubmissionModal
|
||||||
|
loadId={selectedLoadId}
|
||||||
|
onClose={() => {
|
||||||
|
setShowBidModal(false);
|
||||||
|
setSelectedLoadId(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue