diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..2fdae96
--- /dev/null
+++ b/frontend/package.json
@@ -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"
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..71a7041
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -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 (
+
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/frontend/src/components/LoadsList.jsx b/frontend/src/components/LoadsList.jsx
new file mode 100644
index 0000000..fcc46c4
--- /dev/null
+++ b/frontend/src/components/LoadsList.jsx
@@ -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
Loading loads...
;
+ if (isError) return Error loading loads
;
+
+ return (
+
+
Loads Management
+
+ {/* Filters */}
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+ {/* Loads Table */}
+
+
+
+ | Date |
+ Route |
+ Shipper |
+ Freight |
+ Commission |
+ Status |
+
+
+
+ {loads.map((load) => (
+
+ | {new Date(load.date).toLocaleDateString('en-IN')} |
+ {load.from_city} → {load.to_city} |
+ {load.shipper?.name || '—'} |
+ {formatINR(load.freight_charged)} |
+ {formatINR(load.commission)} |
+
+
+ {load.status}
+
+ |
+
+ ))}
+
+
+
+ );
+}
+
+export default LoadsList;
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..c56cc20
--- /dev/null
+++ b/frontend/src/index.css
@@ -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;
+}
\ No newline at end of file
diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx
new file mode 100644
index 0000000..6f9c721
--- /dev/null
+++ b/frontend/src/index.jsx
@@ -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 (
+
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..2dc0623
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -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(
+
+
+
+);
\ No newline at end of file
diff --git a/frontend/src/router.jsx b/frontend/src/router.jsx
new file mode 100644
index 0000000..2e3fb8d
--- /dev/null
+++ b/frontend/src/router.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { createRootRoute, RouterProvider, createBrowserRouter } from '@tanstack/react-router';
+import LoadsList from './components/LoadsList';
+
+// Root layout – can later include a navbar or sidebar
+function RootLayout({ children }) {
+ return (
+
+ {/* Simple header */}
+
+ FreightDesk Dashboard
+
+ {children}
+
+ );
+}
+
+// Define the root route (layout)
+const rootRoute = createRootRoute({
+ component: RootLayout,
+});
+
+// Loads page route
+const loadsRoute = rootRoute.createRoute({
+ path: '/loads',
+ component: LoadsList,
+});
+
+// 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, indexRoute]);
+
+export const router = createBrowserRouter({ routeTree });
+
+export function AppRouter() {
+ return ;
+}
diff --git a/frontend/src/supabaseClient.js b/frontend/src/supabaseClient.js
new file mode 100644
index 0000000..d9b39ad
--- /dev/null
+++ b/frontend/src/supabaseClient.js
@@ -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);
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..030fe91
--- /dev/null
+++ b/frontend/vite.config.js
@@ -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,
+ },
+});