feat: initialize new project with Kanban dashboard and ledger
This commit is contained in:
commit
1c5c195abc
26 changed files with 820 additions and 0 deletions
2
.env
Normal file
2
.env
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
VITE_SUPABASE_URL=http://supabasekong-fih38liv55125sbp2w17z2da.187.127.178.110.sslip.io
|
||||||
|
VITE_SUPABASE_ANON_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc4MDg1ODgwMCwiZXhwIjo0OTM2NTMyNDAwLCJyb2xlIjoiYW5vbiJ9.zAIGJwSek9J4_GCYKTRqquFeiajBDAzChWBlidP3Ayk
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
|
|
@ -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;"]
|
||||||
53
README-COOLIFY.md
Normal file
53
README-COOLIFY.md
Normal file
|
|
@ -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 <repo-url>
|
||||||
|
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).
|
||||||
9
README.md
Normal file
9
README.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# New Project
|
||||||
|
|
||||||
|
A new freight forwarding project.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repo
|
||||||
|
2. Install dependencies
|
||||||
|
3. Run the project
|
||||||
12
index.html
Normal file
12
index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Freight Internal App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
nginx.conf
Normal file
13
nginx.conf
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
27
package.json
Normal file
27
package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
13
public/index.html
Normal file
13
public/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>FreightDesk Internal</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
src/App.jsx
Normal file
36
src/App.jsx
Normal file
|
|
@ -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 (
|
||||||
|
<Router>
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<header className="bg-white shadow">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">FreightDesk Internal</h1>
|
||||||
|
<nav className="mt-2 flex space-x-4">
|
||||||
|
<Link to="/" className="text-blue-600 hover:underline">Dashboard</Link>
|
||||||
|
<Link to="/kanban" className="text-blue-600 hover:underline">Loads Board</Link>
|
||||||
|
<Link to="/shippers" className="text-blue-600 hover:underline">Shippers</Link>
|
||||||
|
<Link to="/drivers" className="text-blue-600 hover:underline">Drivers</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="max-w-7xl mx-auto p-4">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
<Route path="/kanban" element={<Kanban />} />
|
||||||
|
<Route path="/shippers" element={<Shippers />} />
|
||||||
|
<Route path="/drivers" element={<Drivers />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
10
src/App.tsx
Normal file
10
src/App.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import LoadTable from './components/LoadTable';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Freight Desk - Internal App</h1>
|
||||||
|
<LoadTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/components/Dashboard.jsx
Normal file
92
src/components/Dashboard.jsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-xl font-semibold">Dashboard</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
|
<StatCard title="Total Loads" value={stats.totalLoads} icon="📦" />
|
||||||
|
<StatCard title="Pending Loads" value={stats.pendingLoads} icon="⏳" />
|
||||||
|
<StatCard title="Total Commission" value={`₹${stats.totalCommission.toLocaleString()}`} icon="💰" />
|
||||||
|
<StatCard title="Pending from Shipper" value={`₹${stats.pendingFromShipper.toLocaleString()}`} icon="📥" />
|
||||||
|
<StatCard title="Pending to Driver" value={`₹${stats.pendingToDriver.toLocaleString()}`} icon="📤" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ title, value, icon }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-500">{title}</p>
|
||||||
|
<p className="text-2xl font-bold text-gray-900 mt-1">{value}</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-3xl">{icon}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard
|
||||||
58
src/components/Drivers.jsx
Normal file
58
src/components/Drivers.jsx
Normal file
|
|
@ -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 <div className="text-center py-8">Loading drivers...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Drivers</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{drivers.length > 0 ? (
|
||||||
|
drivers.map(driver => (
|
||||||
|
<DriverCard key={driver.id} driver={driver} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 col-span-full">No drivers found. Add your first driver!</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DriverCard({ driver }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-4 border-l-4 border-blue-500">
|
||||||
|
<div className="font-semibold">{driver.name}</div>
|
||||||
|
<div className="text-sm text-gray-600">Vehicle: {driver.vehicle_number || 'N/A'}</div>
|
||||||
|
<div className="text-sm text-gray-600">Contact: {driver.contact || 'N/A'}</div>
|
||||||
|
<div className="text-sm text-gray-600">Total Earnings: ₹{driver.total_earnings?.toLocaleString() || '0'}</div>
|
||||||
|
<div className="text-sm text-gray-600">Pending Payments: ₹{driver.pending_payments?.toLocaleString() || '0'}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Drivers
|
||||||
116
src/components/Kanban.jsx
Normal file
116
src/components/Kanban.jsx
Normal file
|
|
@ -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 <div className="text-center py-8">Loading loads...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Loads Board</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
|
{STATUS_COLUMNS.map((column) => (
|
||||||
|
<div key={column.id} className={`rounded-lg p-4 ${column.color}`}>
|
||||||
|
<h3 className="font-semibold mb-3">{column.title}</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{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 => (
|
||||||
|
<LoadCard
|
||||||
|
key={load.id}
|
||||||
|
load={load}
|
||||||
|
onStatusChange={handleStatusChange}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadCard({ load, onStatusChange }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-3 cursor-move">
|
||||||
|
<div className="font-medium text-sm">{load.shipper || load.id}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{load.from} → {load.to}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-700 mt-1">
|
||||||
|
₹{parseFloat(load.freight_charged || 0).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Kanban
|
||||||
0
src/components/LoadForm.tsx
Normal file
0
src/components/LoadForm.tsx
Normal file
10
src/components/LoadTable.tsx
Normal file
10
src/components/LoadTable.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function LoadTable() {
|
||||||
|
return (
|
||||||
|
<div className="p-6 border rounded-md shadow-sm">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Freight Loads</h2>
|
||||||
|
<p className="text-gray-600">Data will load from Supabase</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/components/Payments.jsx
Normal file
102
src/components/Payments.jsx
Normal file
|
|
@ -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 <div className="text-center py-8">Loading payments...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Payments</h2>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-sm text-gray-500">Total Advance Received</div>
|
||||||
|
<div className="text-2xl font-bold text-green-600">₹{totalAdvance.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-sm text-gray-500">Paid to Drivers</div>
|
||||||
|
<div className="text-2xl font-bold text-red-600">₹{totalPaidToDriver.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-sm text-gray-500">Pending from Shipper</div>
|
||||||
|
<div className="text-2xl font-bold text-yellow-600">₹{totalPendingShipper.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-4">
|
||||||
|
<div className="text-sm text-gray-500">Pending to Driver</div>
|
||||||
|
<div className="text-2xl font-bold text-purple-600">₹{totalPendingDriver.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Table */}
|
||||||
|
<div className="bg-white rounded-lg shadow overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Shipper</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Route</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Freight</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Advance</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Paid to Driver</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{payments.map(payment => (
|
||||||
|
<tr key={payment.id}>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{payment.date || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{payment.shipper || payment.id}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">{payment.from} → {payment.to}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">₹{parseFloat(payment.freight_charged || 0).toLocaleString()}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">₹{parseFloat(payment.advance_received || 0).toLocaleString()}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">₹{parseFloat(payment.paid_to_driver || 0).toLocaleString()}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs rounded-full ${
|
||||||
|
payment.status === 'settled' ? 'bg-green-100 text-green-800' :
|
||||||
|
payment.status === 'pending collection' ? 'bg-yellow-100 text-yellow-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{payment.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Payments
|
||||||
130
src/components/Shippers.jsx
Normal file
130
src/components/Shippers.jsx
Normal file
|
|
@ -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 <div className="text-center py-8">Loading shippers...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h2 className="text-xl font-semibold">Shippers</h2>
|
||||||
|
|
||||||
|
{/* Add Shipper Form */}
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newShipper.name}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Contact</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={newShipper.contact}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={newShipper.email}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-800"
|
||||||
|
>
|
||||||
|
Add Shipper
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shippers List */}
|
||||||
|
<div>
|
||||||
|
{shippers.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{shippers.map(shipper => (
|
||||||
|
<ShipperCard key={shipper.id} shipper={shipper} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">No shippers found. Add your first shipper above!</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShipperCard({ shipper }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-4 border-l-4 border-green-500">
|
||||||
|
<div className="font-semibold">{shipper.name}</div>
|
||||||
|
<div className="text-sm text-gray-600">Contact: {shipper.contact}</div>
|
||||||
|
<div className="text-sm text-gray-600">Email: {shipper.email || 'N/A'}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Shippers
|
||||||
3
src/index.css
Normal file
3
src/index.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
6
src/lib/supabase.ts
Normal file
6
src/lib/supabase.ts
Normal file
|
|
@ -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
|
||||||
|
);
|
||||||
9
src/main.jsx
Normal file
9
src/main.jsx
Normal file
|
|
@ -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(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
|
|
@ -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(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
6
src/services/supabase.js
Normal file
6
src/services/supabase.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)
|
||||||
48
src/types/index.ts
Normal file
48
src/types/index.ts
Normal file
|
|
@ -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<Load, 'id' | 'created_at' | 'updated_at'>;
|
||||||
|
|
||||||
|
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;
|
||||||
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue