Compare commits

...

2 commits

10 changed files with 400 additions and 0 deletions

22
frontend/package.json Normal file
View 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
View 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;

View 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;

View 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
View 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
View 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
View 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
View 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} />;
}

View 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
View 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,
},
});