commit 1c5c195abc58ae0f3c8a819c7607ab97dd1467cf Author: Vivek Date: Tue Jun 9 22:29:32 2026 +0000 feat: initialize new project with Kanban dashboard and ledger diff --git a/.env b/.env new file mode 100644 index 0000000..447ae27 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_SUPABASE_URL=http://supabasekong-fih38liv55125sbp2w17z2da.187.127.178.110.sslip.io +VITE_SUPABASE_ANON_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc4MDg1ODgwMCwiZXhwIjo0OTM2NTMyNDAwLCJyb2xlIjoiYW5vbiJ9.zAIGJwSek9J4_GCYKTRqquFeiajBDAzChWBlidP3Ayk \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..93b1444 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Build stage +FROM node:18-alpine AS builder + +# Install git (needed by some npm postinstall scripts) +RUN apk add --no-cache git + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy source files +COPY . . + +# Build the Vite production bundle +ENV NODE_ENV=production +RUN npm run build + +# Production stage - tiny static server +FROM nginx:alpine + +# Copy custom config (removes default, sets SPA fallback) +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built assets +COPY --from=builder /app/dist /usr/share/nginx/html + +# Expose port (Coolify expects 80 by default) +EXPOSE 80 + +# Start nginx in foreground +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README-COOLIFY.md b/README-COOLIFY.md new file mode 100644 index 0000000..ecf74ba --- /dev/null +++ b/README-COOLIFY.md @@ -0,0 +1,53 @@ +# Coolify Deployment Guide for Internal Freight App + +## Prerequisites +- Supabase project URL & anon key (set in `.env`) +- Node.js >= 18 (handled via Docker build) +- Your VPS has Docker + Coolify installed + +## File Structure +``` +internal-freight-app/ +├── Dockerfile # Docker build for Coolify +├── .dockerignore # Ignore unwanted files +├── .env.example # Environment variables template +├── nginx.conf # Nginx reverse proxy config (default Coolify) +├── vite.config.ts # Vite config (for asset handling) +├── src/ +│ └── ... # React + TypeScript source +└── package.json +``` + +## Deployment Steps on Coolify + +1. **Create Application in Coolify** + - Source: **Git Repository** → URL = `http://forgejo-vil3xyowqk0qsh4hiqy77e3h.187.127.178.110.sslip.io/iamcoolvivek007/internal-freight-app.git` + - Build Pack: **Dockerfile** + - Port: `5173` (Vite dev) or `3000` (if using a different server) + +2. **Environment Variables** + - In **Coolify > Application > Environment**, set these: + - `VITE_SUPABASE_URL` = your Supabase project URL (e.g., `https://xyz.supabase.co`) + - `VITE_SUPABASE_ANON_KEY` = your Supabase anon/public key + +3. **Deploy** + - Click **Save & Deploy**. Coolify will push every commit to main automatically (or via webhook). + +4. **Database** + - Apply the Supabase schema from `supabase-schema.sql` in Supabase SQL Editor: + ```sql + -- (schema content) + ``` + +## Local Development (Optional) +```bash +git clone +cp .env.example .env +# Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY +npm install +npm run dev +``` + +## Notes +- All data stored in Supabase (no local database required). +- For production, swap `npm run dev` with `npm run preview` or add a Nginx server in Coolify (Dockerfile handles this automatically). \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2748318 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# New Project + +A new freight forwarding project. + +## Setup + +1. Clone the repo +2. Install dependencies +3. Run the project \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..45be857 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Freight Internal App + + +
+ + + \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..b1c011d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,13 @@ +server { + listen 80 default_server; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri /index.html; + } + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml+rss; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..16a1cbd --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "internal-freight-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@supabase/supabase-js": "^2.39.0", + "@tanstack/react-query": "^5.28.0", + "@tanstack/react-table": "^8.15.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.24", + "@types/react-dom": "^18.2.8", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.21", + "tailwindcss": "^3.3.2", + "vite": "^5.2.0", + "typescript": "^5.4.5" + } +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..96bb01e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..2bf1203 --- /dev/null +++ b/public/index.html @@ -0,0 +1,13 @@ + + + + + + + FreightDesk Internal + + +
+ + + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..93ab1a3 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,36 @@ +import React from 'react' +import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom' +import Dashboard from './components/Dashboard.jsx' +import Kanban from './components/Kanban.jsx' +import Shippers from './components/Shippers.jsx' +import Drivers from './components/Drivers.jsx' + +function App() { + return ( + +
+
+
+

FreightDesk Internal

+ +
+
+
+ + } /> + } /> + } /> + } /> + +
+
+
+ ) +} + +export default App \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..be6c2e9 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,10 @@ +import LoadTable from './components/LoadTable'; + +export default function App() { + return ( +
+

Freight Desk - Internal App

+ +
+ ); +} \ No newline at end of file diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx new file mode 100644 index 0000000..553fb64 --- /dev/null +++ b/src/components/Dashboard.jsx @@ -0,0 +1,92 @@ +import React, { useState, useEffect } from 'react' +import { supabase } from '../services/supabase.js' + +function Dashboard() { + const [stats, setStats] = useState({ + totalLoads: 0, + pendingLoads: 0, + totalCommission: 0, + pendingFromShipper: 0, + pendingToDriver: 0 + }) + + useEffect(() => { + // Fetch stats from Supabase + const fetchStats = async () => { + try { + // Total loads + const { count: totalLoads } = await supabase + .from('loads') + .select('*', { count: 'exact', head: true }) + + // Pending loads (not settled) + const { count: pendingLoads } = await supabase + .from('loads') + .select('*', { count: 'exact', head: true }) + .neq('status', 'settled') + + // Sum of commission + const { data: commissionData } = await supabase + .from('loads') + .select('commission') + + const totalCommission = commissionData?.reduce((sum, l) => sum + (parseFloat(l.commission) || 0), 0) || 0 + + // Sum of pending_from_shipper + const { data: pendingShipperData } = await supabase + .from('loads') + .select('pending_from_shipper') + + const pendingFromShipper = pendingShipperData?.reduce((sum, l) => sum + (parseFloat(l.pending_from_shipper) || 0), 0) || 0 + + // Sum of pending_to_driver + const { data: pendingDriverData } = await supabase + .from('loads') + .select('pending_to_driver') + + const pendingToDriver = pendingDriverData?.reduce((sum, l) => sum + (parseFloat(l.pending_to_driver) || 0), 0) || 0 + + setStats({ + totalLoads: totalLoads || 0, + pendingLoads: pendingLoads || 0, + totalCommission, + pendingFromShipper, + pendingToDriver + }) + } catch (error) { + console.error('Error fetching stats:', error) + } + } + + fetchStats() + }, []) + + return ( +
+

Dashboard

+
+ + + + + +
+
+ ) +} + +function StatCard({ title, value, icon }) { + return ( +
+
+
+

{title}

+

{value}

+
+ {icon} +
+
+ ) +} + +export default Dashboard \ No newline at end of file diff --git a/src/components/Drivers.jsx b/src/components/Drivers.jsx new file mode 100644 index 0000000..54b94cf --- /dev/null +++ b/src/components/Drivers.jsx @@ -0,0 +1,58 @@ +import React, { useState, useEffect } from 'react' +import { supabase } from '../services/supabase.js' + +function Drivers() { + const [drivers, setDrivers] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchDrivers = async () => { + try { + const { data, error } = await supabase + .from('drivers') + .select('*') + .order('created_at', { ascending: false }) + + if (error) throw error + setDrivers(data || []) + } catch (error) { + console.error('Error fetching drivers:', error) + } finally { + setLoading(false) + } + } + + fetchDrivers() + }, []) + + if (loading) return
Loading drivers...
+ + return ( +
+

Drivers

+
+ {drivers.length > 0 ? ( + drivers.map(driver => ( + + )) + ) : ( +

No drivers found. Add your first driver!

+ )} +
+
+ ) +} + +function DriverCard({ driver }) { + return ( +
+
{driver.name}
+
Vehicle: {driver.vehicle_number || 'N/A'}
+
Contact: {driver.contact || 'N/A'}
+
Total Earnings: ₹{driver.total_earnings?.toLocaleString() || '0'}
+
Pending Payments: ₹{driver.pending_payments?.toLocaleString() || '0'}
+
+ ) +} + +export default Drivers \ No newline at end of file diff --git a/src/components/Kanban.jsx b/src/components/Kanban.jsx new file mode 100644 index 0000000..beab60b --- /dev/null +++ b/src/components/Kanban.jsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react' +import { supabase } from '../services/supabase.js' + +const STATUS_COLUMNS = [ + { id: 'pending', title: 'Pending', color: 'bg-gray-200' }, + { id: 'assigned', title: 'Assigned', color: 'bg-blue-200' }, + { id: 'loaded', title: 'Loaded / In Transit', color: 'bg-yellow-200' }, + { id: 'delivered', title: 'Delivered', color: 'bg-green-200' }, + { id: 'settled', title: 'Settled', color: 'bg-purple-200' } +] + +function Kanban() { + const [loads, setLoads] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchLoads = async () => { + try { + const { data, error } = await supabase + .from('loads') + .select('*') + .order('date', { ascending: false }) + + if (error) throw error + setLoads(data || []) + } catch (error) { + console.error('Error fetching loads:', error) + } finally { + setLoading(false) + } + } + + fetchLoads() + + // Subscribe to real-time updates + const subscription = supabase + .from('loads') + .on('*', (payload) => { + setLoads(prev => { + const updated = prev.filter(l => l.id !== payload.new.id) + return [...updated, payload.new] + }) + }) + .subscribe() + + return () => { + subscription.unsubscribe() + } + }, []) + + const handleStatusChange = async (loadId, newStatus) => { + try { + const { error } = await supabase + .from('loads') + .update({ status: newStatus }) + .eq('id', loadId) + + if (error) throw error + } catch (error) { + console.error('Error updating status:', error) + } + } + + if (loading) return
Loading loads...
+ + return ( +
+

Loads Board

+
+ {STATUS_COLUMNS.map((column) => ( +
+

{column.title}

+
+ {loads + .filter(load => + column.id === 'pending' + ? load.status === 'pending lead' || load.status === 'pending collection' + : column.id === 'assigned' + ? load.status === 'assigned vehicle' + : column.id === 'loaded' + ? load.status === 'loaded / in transit' + : column.id === 'delivered' + ? load.status === 'delivered' + : load.status === 'settled' + ) + .map(load => ( + + )) + } +
+
+ ))} +
+
+ ) +} + +function LoadCard({ load, onStatusChange }) { + return ( +
+
{load.shipper || load.id}
+
+ {load.from} → {load.to} +
+
+ ₹{parseFloat(load.freight_charged || 0).toLocaleString()} +
+
+ ) +} + +export default Kanban \ No newline at end of file diff --git a/src/components/LoadForm.tsx b/src/components/LoadForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/LoadTable.tsx b/src/components/LoadTable.tsx new file mode 100644 index 0000000..a82fa23 --- /dev/null +++ b/src/components/LoadTable.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function LoadTable() { + return ( +
+

Freight Loads

+

Data will load from Supabase

+
+ ); +} \ No newline at end of file diff --git a/src/components/Payments.jsx b/src/components/Payments.jsx new file mode 100644 index 0000000..50d0ba9 --- /dev/null +++ b/src/components/Payments.jsx @@ -0,0 +1,102 @@ +import React, { useState, useEffect } from 'react' +import { supabase } from '../services/supabase.js' + +function Payments() { + const [payments, setPayments] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const fetchPayments = async () => { + try { + const { data, error } = await supabase + .from('loads') + .select('*') + .neq('status', 'pending lead') + .neq('status', 'available vehicle') + .order('date', { ascending: false }) + + if (error) throw error + setPayments(data || []) + } catch (error) { + console.error('Error fetching payments:', error) + } finally { + setLoading(false) + } + } + + fetchPayments() + }, []) + + const totalAdvance = payments.reduce((sum, p) => sum + (parseFloat(p.advance_received) || 0), 0) + const totalPaidToDriver = payments.reduce((sum, p) => sum + (parseFloat(p.paid_to_driver) || 0), 0) + const totalPendingShipper = payments.reduce((sum, p) => sum + (parseFloat(p.pending_from_shipper) || 0), 0) + const totalPendingDriver = payments.reduce((sum, p) => sum + (parseFloat(p.pending_to_driver) || 0), 0) + + if (loading) return
Loading payments...
+ + return ( +
+

Payments

+ + {/* Summary Cards */} +
+
+
Total Advance Received
+
₹{totalAdvance.toLocaleString()}
+
+
+
Paid to Drivers
+
₹{totalPaidToDriver.toLocaleString()}
+
+
+
Pending from Shipper
+
₹{totalPendingShipper.toLocaleString()}
+
+
+
Pending to Driver
+
₹{totalPendingDriver.toLocaleString()}
+
+
+ + {/* Payment Table */} +
+ + + + + + + + + + + + + + {payments.map(payment => ( + + + + + + + + + + ))} + +
DateShipperRouteFreightAdvancePaid to DriverStatus
{payment.date || 'N/A'}{payment.shipper || payment.id}{payment.from} → {payment.to}₹{parseFloat(payment.freight_charged || 0).toLocaleString()}₹{parseFloat(payment.advance_received || 0).toLocaleString()}₹{parseFloat(payment.paid_to_driver || 0).toLocaleString()} + + {payment.status} + +
+
+
+ ) +} + +export default Payments \ No newline at end of file diff --git a/src/components/Shippers.jsx b/src/components/Shippers.jsx new file mode 100644 index 0000000..c30aa89 --- /dev/null +++ b/src/components/Shippers.jsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react' +import { supabase } from '../services/supabase.js' + +function Shippers() { + const [shippers, setShippers] = useState([]) + const [loading, setLoading] = useState(true) + const [newShipper, setNewShipper] = useState({ + name: '', + contact: '', + email: '' + }) + + useEffect(() => { + const fetchShippers = async () => { + try { + const { data, error } = await supabase + .from('shippers') + .select('*') + .order('created_at', { ascending: false }) + + if (error) throw error + setShippers(data || []) + } catch (error) { + console.error('Error fetching shippers:', error) + } finally { + setLoading(false) + } + } + + fetchShippers() + }, []) + + const handleSubmit = async (e) => { + e.preventDefault() + try { + const { error } = await supabase + .from('shippers') + .insert([newShipper]) + + if (error) throw error + + // Refresh list + const { data } = await supabase + .from('shippers') + .select('*') + .order('created_at', { ascending: false }) + + setShippers(data || []) + setNewShipper({ name: '', contact: '', email: '' }) + } catch (error) { + console.error('Error adding shipper:', error) + } + } + + if (loading) return
Loading shippers...
+ + return ( +
+

Shippers

+ + {/* Add Shipper Form */} +
+
+
+
+ + setNewShipper({ ...newShipper, name: e.target.value })} + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + required + /> +
+
+ + setNewShipper({ ...newShipper, contact: e.target.value })} + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + required + /> +
+
+
+ + setNewShipper({ ...newShipper, email: e.target.value })} + className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ +
+
+ + {/* Shippers List */} +
+ {shippers.length > 0 ? ( +
+ {shippers.map(shipper => ( + + ))} +
+ ) : ( +

No shippers found. Add your first shipper above!

+ )} +
+
+ ) +} + +function ShipperCard({ shipper }) { + return ( +
+
{shipper.name}
+
Contact: {shipper.contact}
+
Email: {shipper.email || 'N/A'}
+
+ ) +} + +export default Shippers \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts new file mode 100644 index 0000000..828e280 --- /dev/null +++ b/src/lib/supabase.ts @@ -0,0 +1,6 @@ +import { createClient } from '@supabase/supabase-js'; + +export const supabase = createClient( + process.env.VITE_SUPABASE_URL as string, + process.env.VITE_SUPABASE_ANON_KEY as string +); \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..6e9fa26 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,9 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +) \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..b3c11a7 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); \ No newline at end of file diff --git a/src/services/supabase.js b/src/services/supabase.js new file mode 100644 index 0000000..31acd4c --- /dev/null +++ b/src/services/supabase.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) \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..192b11a --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,48 @@ +export interface Load { + id: string; + date: string | null; + status: string; + vehicle: string | null; + from: string | null; + via: string | null; + to: string | null; + shipper: string | null; + load_type: string | null; + item: string | null; + deliveries: string | null; + freight_charged: number | null; + advance_received: number | null; + paid_to_driver: number | null; + commission: number | null; + driver_freight: number | null; + pending_from_shipper: number | null; + pending_to_driver: number | null; + notes: string | null; + created_at?: string; + updated_at?: string; +} + +export type LoadFormData = Omit; + +export const STATUS_OPTIONS = [ + 'settled', + 'commission received', + 'commission adjusted', + 'commission due', + 'pending collection', + 'partially pending', + 'fully pending from shipper', + 'assigned vehicle', + 'loaded / in transit', + 'partial', + 'available vehicle', + 'pending lead', + 'handled directly by shipper', + 'reconciled', +] as const; + +export const LOAD_TYPE_OPTIONS = [ + 'Full load', + 'Part load', + 'Mixed / multi-drop', +] as const; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..74769d1 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..6b2481a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +}); \ No newline at end of file