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