Compare commits
2 commits
f1c75faba1
...
d8b41e613b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8b41e613b | ||
|
|
4f53ee4210 |
10 changed files with 400 additions and 0 deletions
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "freightdesk-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port 3000",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0",
|
||||||
|
"@tanstack/react-query": "^5.0.0",
|
||||||
|
"@tanstack/react-router": "^1.0.0",
|
||||||
|
"@supabase/supabase-js": "^2.45.0",
|
||||||
|
"styled-components": "^6.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
frontend/src/App.jsx
Normal file
24
frontend/src/App.jsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { AppRouter } from './router';
|
||||||
|
|
||||||
|
// Create QueryClient instance
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider value={queryClient}>
|
||||||
|
<AppRouter />
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
132
frontend/src/components/LoadsList.jsx
Normal file
132
frontend/src/components/LoadsList.jsx
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { supabase } from '../supabaseClient';
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
'settled': 'success',
|
||||||
|
'completed': 'success',
|
||||||
|
'commission received': 'success',
|
||||||
|
'reconciled': 'success',
|
||||||
|
'loaded / in transit': 'primary',
|
||||||
|
'assigned': 'primary',
|
||||||
|
'assigned vehicle': 'primary',
|
||||||
|
'pending collection': 'warning',
|
||||||
|
'partially pending': 'warning',
|
||||||
|
'fully pending from shipper': 'warning',
|
||||||
|
'commission due': 'warning',
|
||||||
|
'cancelled': 'danger',
|
||||||
|
'partial': 'secondary',
|
||||||
|
'available vehicle': 'secondary',
|
||||||
|
};
|
||||||
|
return colors[status] || 'secondary';
|
||||||
|
};
|
||||||
|
|
||||||
|
function LoadsList() {
|
||||||
|
const [filterStatus, setFilterStatus] = useState('');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const { data: loads = [], isLoading, isError, refetch } = 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)
|
||||||
|
`)
|
||||||
|
.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}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query;
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <div className="text-center py-5">Loading loads...</div>;
|
||||||
|
if (isError) return <div className="text-center py-5 text-danger">Error loading loads</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-4">
|
||||||
|
<h2>Loads Management</h2>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="row mb-3 g-2">
|
||||||
|
<div className="col-md-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Search cities..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => setFilterStatus(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="settled">Settled</option>
|
||||||
|
<option value="loaded / in transit">In Transit</option>
|
||||||
|
<option value="pending collection">Pending Collection</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loads Table */}
|
||||||
|
<table className="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Route</th>
|
||||||
|
<th>Shipper</th>
|
||||||
|
<th>Freight</th>
|
||||||
|
<th>Commission</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</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>{formatINR(load.commission)}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`badge bg-${getStatusColor(load.status)}`}>
|
||||||
|
{load.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadsList;
|
||||||
90
frontend/src/components/ShippersList.jsx
Normal file
90
frontend/src/components/ShippersList.jsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { supabase } from '../supabaseClient';
|
||||||
|
|
||||||
|
function ShippersList() {
|
||||||
|
const [filterName, setFilterName] = useState('');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const { data: shippers = [], isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['shippers', filterName, searchTerm],
|
||||||
|
queryFn: async () => {
|
||||||
|
let query = supabase
|
||||||
|
.from('shippers')
|
||||||
|
.select('id, name, phone, email, city, state')
|
||||||
|
.order('name');
|
||||||
|
|
||||||
|
if (filterName) {
|
||||||
|
query = query.eq('name', filterName);
|
||||||
|
}
|
||||||
|
if (searchTerm) {
|
||||||
|
query = query.or(`name.ilike.%${searchTerm}%,email.ilike.%${searchTerm}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query;
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <div className="text-center py-5">Loading shippers...</div>;
|
||||||
|
if (isError) return <div className="text-center py-5 text-danger">Error loading shippers</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-4">
|
||||||
|
<h2>Shippers</h2>
|
||||||
|
|
||||||
|
<div className="row mb-3 g-2">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Search shippers..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Filter by name"
|
||||||
|
value={filterName}
|
||||||
|
onChange={(e) => setFilterName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Phone</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>City</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{shippers.map((shipper) => (
|
||||||
|
<tr key={shipper.id}>
|
||||||
|
<td>{shipper.name}</td>
|
||||||
|
<td>{shipper.phone}</td>
|
||||||
|
<td>{shipper.email}</td>
|
||||||
|
<td>{shipper.city}</td>
|
||||||
|
<td>{shipper.state}</td>
|
||||||
|
<td>
|
||||||
|
<button className="btn btn-sm btn-outline-primary">View</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShippersList;
|
||||||
28
frontend/src/index.css
Normal file
28
frontend/src/index.css
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/* Basic styling for FreightDesk frontend */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badge colors override */
|
||||||
|
.badge.bg-success {
|
||||||
|
background-color: #28a745 !important;
|
||||||
|
}
|
||||||
|
.badge.bg-warning {
|
||||||
|
background-color: #ffc107 !important;
|
||||||
|
color: #212529 !important;
|
||||||
|
}
|
||||||
|
.badge.bg-danger {
|
||||||
|
background-color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
.badge.bg-primary {
|
||||||
|
background-color: #0d6efd !important;
|
||||||
|
}
|
||||||
|
.badge.bg-secondary {
|
||||||
|
background-color: #6c757d !important;
|
||||||
|
}
|
||||||
21
frontend/src/index.jsx
Normal file
21
frontend/src/index.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
// Create QueryClient instance
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
// We'll define query state overrides in individual components
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider value={queryClient}>
|
||||||
|
<supabaseClient />
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
12
frontend/src/main.jsx
Normal file
12
frontend/src/main.jsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
55
frontend/src/router.jsx
Normal file
55
frontend/src/router.jsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { createRootRoute, RouterProvider, createBrowserRouter } from '@tanstack/react-router';
|
||||||
|
import LoadsList from './components/LoadsList';
|
||||||
|
import ShippersList from './components/ShippersList';
|
||||||
|
|
||||||
|
// Root layout – can later include a navbar or sidebar
|
||||||
|
function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Simple header */}
|
||||||
|
<header className="bg-primary text-white p-3">
|
||||||
|
<h1 className="mb-0" style={{ fontSize: '1.5rem' }}>FreightDesk Dashboard</h1>
|
||||||
|
</header>
|
||||||
|
<main>{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the root route (layout)
|
||||||
|
const rootRoute = createRootRoute({
|
||||||
|
component: RootLayout,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Loads page route
|
||||||
|
const loadsRoute = rootRoute.createRoute({
|
||||||
|
path: '/loads',
|
||||||
|
component: LoadsList,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shippers page route
|
||||||
|
const shippersRoute = rootRoute.createRoute({
|
||||||
|
path: '/shippers',
|
||||||
|
component: ShippersList,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default route – redirect to /loads
|
||||||
|
const indexRoute = rootRoute.createRoute({
|
||||||
|
path: '/',
|
||||||
|
component: () => {
|
||||||
|
// Simple redirect component
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.location.replace('/loads');
|
||||||
|
}, []);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build the router
|
||||||
|
const routeTree = rootRoute.addChildren([loadsRoute, shippersRoute, indexRoute]);
|
||||||
|
|
||||||
|
export const router = createBrowserRouter({ routeTree });
|
||||||
|
|
||||||
|
export function AppRouter() {
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
}
|
||||||
6
frontend/src/supabaseClient.js
Normal file
6
frontend/src/supabaseClient.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||||
|
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
|
export const supabase = createClient(supabaseUrl, supabaseKey);
|
||||||
10
frontend/vite.config.js
Normal file
10
frontend/vite.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue