diff --git a/.gitignore b/.gitignore index e69de29b..1b5f4104 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.env +dist/ +build/ +.vscode/ +*.log +.DS_Store diff --git a/AbrarKivande/1_pgadmin_tables.png b/AbrarKivande/1_pgadmin_tables.png new file mode 100644 index 00000000..1e5e9da0 Binary files /dev/null and b/AbrarKivande/1_pgadmin_tables.png differ diff --git a/AbrarKivande/1_ui_main.png b/AbrarKivande/1_ui_main.png new file mode 100644 index 00000000..afe9e7a7 Binary files /dev/null and b/AbrarKivande/1_ui_main.png differ diff --git a/AbrarKivande/2_ui_routes_add.png b/AbrarKivande/2_ui_routes_add.png new file mode 100644 index 00000000..e981c883 Binary files /dev/null and b/AbrarKivande/2_ui_routes_add.png differ diff --git a/AbrarKivande/4_ui_schedule_add.png b/AbrarKivande/4_ui_schedule_add.png new file mode 100644 index 00000000..9d264d23 Binary files /dev/null and b/AbrarKivande/4_ui_schedule_add.png differ diff --git a/AbrarKivande/6_ui_delete_route_after.png b/AbrarKivande/6_ui_delete_route_after.png new file mode 100644 index 00000000..a682c7a5 Binary files /dev/null and b/AbrarKivande/6_ui_delete_route_after.png differ diff --git a/AbrarKivande/7_server_terminal.png b/AbrarKivande/7_server_terminal.png new file mode 100644 index 00000000..dbbe06e5 Binary files /dev/null and b/AbrarKivande/7_server_terminal.png differ diff --git a/README.md b/README.md index 7059a962..e71c3400 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,30 @@ -# React + Vite +# Cab Booking — Local Setup (by Abrar) -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +## Overview +This repo contains: +- server (Express + pg) -> `server/index.js` +- DB schema + seed -> `server/sql/db_schema_fixed.sql` +- Postman collection -> `server/sql/CabBooking.postman_collection.json` +- Demo images in `AbrarKivande/` -Currently, two official plugins are available: +## Run locally (Windows 11) +1. Create DB in pgAdmin4: `cab_booking`. +2. In pgAdmin Query Tool run `server/sql/db_schema_fixed.sql`. +3. In `server` folder create `.env` from `.env.example` and set `PGPASSWORD`. +4. Start server: +cd server +npm install +node index.js +5. Start client (if client in repo root or `client` folder): +cd ..\client # or repo root if client present +npm install +npm start +6. Visit `http://localhost:3000`. -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +## Test steps (quick) +- Add/delete Cab, Route, Schedule in UI. +- Optional: import Postman collection from `server/sql` and test API calls. -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +## Notes +- Do not commit real credentials (.env is ignored). +- If issues occur, check server console for stack traces. diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 00000000..52ede4c9 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,6 @@ +PGUSER=postgres +PGHOST=localhost +PGDATABASE=cab_booking +PGPASSWORD=your_postgres_password_here +PGPORT=5432 +PORT=5000 diff --git a/server/index.js b/server/index.js index 7e297381..564ea238 100644 --- a/server/index.js +++ b/server/index.js @@ -1,3 +1,4 @@ +// server/index.js (FINAL version) - replace your current file with this exact content const express = require('express'); const cors = require('cors'); const { Pool } = require('pg'); @@ -6,102 +7,247 @@ const app = express(); app.use(cors()); app.use(express.json()); +// DB config - set env vars if you prefer const pool = new Pool({ - user: 'postgres', - host: 'localhost', - database: 'cab_booking', - password: 'kamlesh@2004', - port: 5432, + user: process.env.PGUSER || 'postgres', + host: process.env.PGHOST || 'localhost', + database: process.env.PGDATABASE || 'cab_booking', + password: process.env.PGPASSWORD || '', // <-- update if different + port: process.env.PGPORT ? Number(process.env.PGPORT) : 5432, }); -// Get all cab operators -app.get('/api/cab-operators', async (req, res) => { - const result = await pool.query('SELECT * FROM cab_operators WHERE active = TRUE'); - res.json(result.rows); +function logAndSendError(res, err) { + console.error('SERVER ERROR:', err && err.stack ? err.stack : err); + // Always return JSON (so front-end res.json() will not fail) + res.status(500).json({ error: String(err && err.message ? err.message : err) }); +} + +/* ----------------- Utility helpers ----------------- */ + +// Parse a "time" input which may be "HH:mm" or an ISO string. +// Returns "HH:mm" string suitable for Postgres TIME. +function normalizeTimeForDb(input) { + if (!input) return null; + // If already HH:mm + if (/^\d{1,2}:\d{2}$/.test(input)) { + const [h, m] = input.split(':').map(Number); + const hh = String(h).padStart(2, '0'); + const mm = String(m).padStart(2, '0'); + return `${hh}:${mm}`; + } + // Try parsing as ISO / Date + const d = new Date(input); + if (!isNaN(d.getTime())) { + // Use local time hours/minutes + const hh = String(d.getHours()).padStart(2, '0'); + const mm = String(d.getMinutes()).padStart(2, '0'); + return `${hh}:${mm}`; + } + // fallback null + return null; +} + +// If the DB schema requires eta_min / base_fare not-null, provide sensible defaults +function defaultEtaAndFare(distanceKm) { + // crude ETA: 2 min per km (round up), min 10 + const eta = Math.max(10, Math.ceil((Number(distanceKm) || 0) * 2)); + // crude base fare: 1.5 per km, min 10 + const fare = Math.max(10, (Number(distanceKm) || 0) * 1.5); + // round fare to 2 decimals + return { eta_min: eta, base_fare: Math.round(fare * 100) / 100 }; +} + +/* ----------------- Cab operators ----------------- */ + +app.get('/api/cab-operators', async (_req, res) => { + try { + const r = await pool.query('SELECT * FROM cab_operators WHERE active = TRUE ORDER BY id'); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -// Updated Get all routes -app.get('/api/routes', async (req, res) => { - const result = await pool.query("SELECT id, origin AS from, destination AS to, CONCAT(distance_km, ' km') AS distance FROM routes WHERE active = TRUE"); - res.json(result.rows); +/* ----------------- Routes ----------------- */ +// Frontend expects GET /api/routes -> array of { id, from, to, distance } +// Frontend POST sends { from, to, distance } (per the UI fields) +app.get('/api/routes', async (_req, res) => { + try { + // Return distance as "xx.xx km" string to match UI formatting + const q = `SELECT id, origin AS "from", destination AS "to", CONCAT(TRIM(TRAILING '.0' FROM TRIM(TO_CHAR(distance_km,'FM99999.99'))),' km') AS distance + FROM routes WHERE active = TRUE ORDER BY id`; + const r = await pool.query(q); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -// Get reports -app.get('/api/reports', async (req, res) => { - const result = await pool.query('SELECT * FROM reports'); - res.json(result.rows); +app.post('/api/routes', async (req, res) => { + try { + const { from, to, distance } = req.body || {}; + + if (!from || !to || (distance == null || distance === '')) { + return res.status(400).json({ error: 'from, to and distance are required' }); + } + + // Accept distance either "12.5" or "12.5 km" and coerce to number + const distanceStr = String(distance).replace(/[^0-9.]/g, ''); + const distanceKm = Number(distanceStr); + if (Number.isNaN(distanceKm)) return res.status(400).json({ error: 'invalid distance' }); + + // Provide defaults for eta_min and base_fare if schema requires not-null + const { eta_min, base_fare } = defaultEtaAndFare(distanceKm); + + const q = `INSERT INTO routes (origin, destination, distance_km, eta_min, base_fare, active) + VALUES ($1,$2,$3,$4,$5, TRUE) + RETURNING id, origin AS "from", destination AS "to", CONCAT(TO_CHAR(distance_km,'FM99999.99'), ' km') AS distance`; + const vals = [from, to, distanceKm, eta_min, base_fare]; + const r = await pool.query(q, vals); + // return the created object (frontend expects object) + res.json(r.rows[0]); + } catch (err) { logAndSendError(res, err); } }); -// Get top revenue -app.get('/api/revenue', async (req, res) => { - const result = await pool.query('SELECT * FROM revenue ORDER BY total_revenue DESC LIMIT 5'); - res.json(result.rows); +app.delete('/api/routes/:id', async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + if (!id) return res.status(400).json({ error: 'invalid id' }); + await pool.query('DELETE FROM routes WHERE id=$1', [id]); + res.json({ success: true }); + } catch (err) { logAndSendError(res, err); } }); -// Get all vendors -app.get('/api/vendors', async (req, res) => { - const result = await pool.query('SELECT * FROM vendors'); - res.json(result.rows); +/* ----------------- Cabs ----------------- */ + +app.get('/api/cabs', async (_req, res) => { + try { + const r = await pool.query('SELECT * FROM cabs ORDER BY id'); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -// Get all customers -app.get('/api/customers', async (req, res) => { - const result = await pool.query('SELECT * FROM customers'); - res.json(result.rows); +app.post('/api/cabs', async (req, res) => { + try { + const { name, type, seats } = req.body || {}; + if (!name || !type || !seats) return res.status(400).json({ error: 'name,type,seats required' }); + const r = await pool.query('INSERT INTO cabs (name, type, seats) VALUES ($1,$2,$3) RETURNING *', [name, type, Number(seats)]); + res.json(r.rows[0]); + } catch (err) { logAndSendError(res, err); } }); -// Get all cabs -app.get('/api/cabs', async (req, res) => { - const result = await pool.query('SELECT * FROM cabs'); - res.json(result.rows); +app.delete('/api/cabs/:id', async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + if (!id) return res.status(400).json({ error: 'invalid id' }); + await pool.query('DELETE FROM cabs WHERE id=$1', [id]); + res.json({ success: true }); + } catch (err) { logAndSendError(res, err); } }); -// Updated Get all schedules -app.get('/api/schedules', async (req, res) => { - const result = await pool.query("SELECT s.id, c.name AS cab, CONCAT(r.origin, ' - ', r.destination) AS route, s.frequency, s.time, s.price FROM schedules s JOIN cabs c ON s.cab_id = c.id JOIN routes r ON s.route_id = r.id"); - res.json(result.rows); +/* ----------------- Schedules ----------------- */ +// GET returns rows shaped: { id, cab, route, frequency, time, price } +app.get('/api/schedules', async (_req, res) => { + try { + const q = `SELECT s.id, c.name AS cab, CONCAT(r.origin,' - ', r.destination) AS route, + s.frequency, TO_CHAR(s.time,'HH24:MI') AS time, s.price + FROM schedules s + JOIN cabs c ON s.cab_id = c.id + JOIN routes r ON s.route_id = r.id + ORDER BY s.id`; + const r = await pool.query(q); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -// Add a cab -app.post('/api/cabs', async (req, res) => { - const { name, type, seats } = req.body; - const result = await pool.query('INSERT INTO cabs (name, type, seats) VALUES ($1, $2, $3) RETURNING *', [name, type, seats]); - res.json(result.rows[0]); +// POST expects frontend sends: { cab: , route: " - ", frequency, time, price } +// Accepts time as "HH:mm" or an ISO timestamp and converts to "HH:mm" for DB. +app.post('/api/schedules', async (req, res) => { + try { + const { cab, route, frequency, time, price } = req.body || {}; + if (!cab || !route || !frequency || !time || price == null) { + return res.status(400).json({ error: 'cab, route, frequency, time and price required' }); + } + + // Look up cab_id by name (frontend uses cab name) + const cabRes = await pool.query('SELECT id FROM cabs WHERE name=$1 LIMIT 1', [cab]); + if (!cabRes.rows[0]) return res.status(400).json({ error: `Cab not found: ${cab}` }); + const cabId = cabRes.rows[0].id; + + // Look up route_id by splitting route string or by matching origin/destination + // route value format is usually "Origin - Destination" + let routeId = null; + // Try parsing "Origin - Destination" + const parts = String(route).split(' - ').map(p => p.trim()); + if (parts.length === 2) { + const [originPart, destPart] = parts; + const rRes = await pool.query('SELECT id FROM routes WHERE origin = $1 AND destination = $2 LIMIT 1', [originPart, destPart]); + if (rRes.rows[0]) routeId = rRes.rows[0].id; + } + // If still not found, try searching by concatenation fallback + if (!routeId) { + const rRes2 = await pool.query("SELECT id FROM routes WHERE (origin || ' - ' || destination) = $1 LIMIT 1", [route]); + if (rRes2.rows[0]) routeId = rRes2.rows[0].id; + } + if (!routeId) return res.status(400).json({ error: `Route not found: ${route}` }); + + // Normalize time input + const normalizedTime = normalizeTimeForDb(String(time)); + if (!normalizedTime) return res.status(400).json({ error: 'invalid time format' }); + + // Insert schedule + const insertQ = `INSERT INTO schedules (cab_id, route_id, frequency, time, price) + VALUES ($1,$2,$3,$4,$5) RETURNING id`; + const insertVals = [cabId, routeId, frequency, normalizedTime, Number(price)]; + const ins = await pool.query(insertQ, insertVals); + + // Respond with object shaped like frontend expects + res.json({ + id: ins.rows[0].id, + cab, + route, + frequency, + time: normalizedTime, + price: Number(price) + }); + } catch (err) { logAndSendError(res, err); } }); -// Delete a cab -app.delete('/api/cabs/:id', async (req, res) => { - const { id } = req.params; - await pool.query('DELETE FROM cabs WHERE id = $1', [id]); - res.json({ success: true }); +app.delete('/api/schedules/:id', async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + if (!id) return res.status(400).json({ error: 'invalid id' }); + await pool.query('DELETE FROM schedules WHERE id=$1', [id]); + res.json({ success: true }); + } catch (err) { logAndSendError(res, err); } }); -// Add a route -app.post('/api/routes', async (req, res) => { - const { origin, destination, distance_km, eta_min, base_fare, active, cab_operator_id } = req.body; - const result = await pool.query('INSERT INTO routes (origin, destination, distance_km, eta_min, base_fare, active, cab_operator_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *', [origin, destination, distance_km, eta_min, base_fare, active, cab_operator_id]); - res.json(result.rows[0]); +/* ----------------- Reports / Revenue / Vendors / Customers ----------------- */ + +app.get('/api/reports', async (_req, res) => { + try { + const r = await pool.query('SELECT * FROM reports ORDER BY date DESC LIMIT 100'); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -// Delete a route -app.delete('/api/routes/:id', async (req, res) => { - const { id } = req.params; - await pool.query('DELETE FROM routes WHERE id = $1', [id]); - res.json({ success: true }); +app.get('/api/revenue', async (_req, res) => { + try { + const r = await pool.query('SELECT * FROM revenue ORDER BY total_revenue DESC LIMIT 10'); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -// Add a schedule -app.post('/api/schedules', async (req, res) => { - const { cab_id, route_id, frequency, time, price } = req.body; - const result = await pool.query('INSERT INTO schedules (cab_id, route_id, frequency, time, price) VALUES ($1, $2, $3, $4, $5) RETURNING *', [cab_id, route_id, frequency, time, price]); - res.json(result.rows[0]); +app.get('/api/vendors', async (_req, res) => { + try { + const r = await pool.query('SELECT id, name, contact, address, kyc_status AS "kycStatus" FROM vendors ORDER BY id'); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -// Delete a schedule -app.delete('/api/schedules/:id', async (req, res) => { - const { id } = req.params; - await pool.query('DELETE FROM schedules WHERE id = $1', [id]); - res.json({ success: true }); +app.get('/api/customers', async (_req, res) => { + try { + const r = await pool.query('SELECT * FROM customers ORDER BY id'); + res.json(r.rows); + } catch (err) { logAndSendError(res, err); } }); -app.listen(5000, () => console.log('Server running on port 5000')); \ No newline at end of file +/* ----------------- Start server ----------------- */ +const PORT = process.env.PORT ? Number(process.env.PORT) : 5000; +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/server/sql/CabBooking.postman_collection.json b/server/sql/CabBooking.postman_collection.json new file mode 100644 index 00000000..856ae7f4 --- /dev/null +++ b/server/sql/CabBooking.postman_collection.json @@ -0,0 +1,306 @@ +{ + "info": { + "name": "Cab Booking API (Local)", + "_postman_id": "cab-booking-local-collection", + "description": "Local Postman collection for Cab Booking app on http://localhost:5000", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Cab Operators - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/cab-operators", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "cab-operators" + ] + } + } + }, + { + "name": "Cabs - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/cabs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "cabs" + ] + } + } + }, + { + "name": "Cabs - POST", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Cab 404\",\n \"type\": \"Van\",\n \"seats\": 9\n}" + }, + "url": { + "raw": "http://localhost:5000/api/cabs", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "cabs" + ] + } + } + }, + { + "name": "Cabs - DELETE (set :id)", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:5000/api/cabs/:id", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "cabs", + ":id" + ] + } + } + }, + { + "name": "Routes - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/routes", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "routes" + ] + } + } + }, + { + "name": "Routes - POST", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"origin\": \"Manhattan\",\n \"destination\": \"JFK\",\n \"distance_km\": 27.5,\n \"eta_min\": 55,\n \"base_fare\": 40,\n \"active\": true,\n \"cab_operator_id\": 1\n}" + }, + "url": { + "raw": "http://localhost:5000/api/routes", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "routes" + ] + } + } + }, + { + "name": "Routes - DELETE (set :id)", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:5000/api/routes/:id", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "routes", + ":id" + ] + } + } + }, + { + "name": "Schedules - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/schedules", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "schedules" + ] + } + } + }, + { + "name": "Schedules - POST", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"cab_id\": 1,\n \"route_id\": 1,\n \"frequency\": \"Daily\",\n \"time\": \"09:00\",\n \"price\": 25\n}" + }, + "url": { + "raw": "http://localhost:5000/api/schedules", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "schedules" + ] + } + } + }, + { + "name": "Schedules - DELETE (set :id)", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:5000/api/schedules/:id", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "schedules", + ":id" + ] + } + } + }, + { + "name": "Reports - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/reports", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "reports" + ] + } + } + }, + { + "name": "Revenue - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/revenue", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "revenue" + ] + } + } + }, + { + "name": "Vendors - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/vendors", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "vendors" + ] + } + } + }, + { + "name": "Customers - GET", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5000/api/customers", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "api", + "customers" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/server/sql/db_schema_fixed.sql b/server/sql/db_schema_fixed.sql new file mode 100644 index 00000000..0d5b0178 --- /dev/null +++ b/server/sql/db_schema_fixed.sql @@ -0,0 +1,148 @@ +-- db_schema_fixed.sql +-- Rebuild schema for Cab Booking app (safe for a fresh database). +-- Run this in pgAdmin4 Query Tool connected to database: cab_booking + +BEGIN; + +-- Drop existing (ignore errors if they don't exist) +DROP VIEW IF EXISTS v_schedules_list; +DROP VIEW IF EXISTS v_routes_list; +DROP TABLE IF EXISTS reports CASCADE; +DROP TABLE IF EXISTS revenue CASCADE; +DROP TABLE IF EXISTS schedules CASCADE; +DROP TABLE IF EXISTS routes CASCADE; +DROP TABLE IF EXISTS cabs CASCADE; +DROP TABLE IF EXISTS cab_operators CASCADE; +DROP TABLE IF EXISTS vendors CASCADE; +DROP TABLE IF EXISTS customers CASCADE; + +-- Core tables +CREATE TABLE cab_operators ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + active BOOLEAN DEFAULT TRUE +); + +CREATE TABLE cabs ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + type VARCHAR(50) NOT NULL, + seats INT NOT NULL CHECK (seats > 0) +); + +CREATE TABLE routes ( + id SERIAL PRIMARY KEY, + origin VARCHAR(100) NOT NULL, + destination VARCHAR(100) NOT NULL, + distance_km FLOAT NOT NULL CHECK (distance_km >= 0), + eta_min INT NOT NULL CHECK (eta_min > 0), + base_fare FLOAT NOT NULL CHECK (base_fare >= 0), + active BOOLEAN DEFAULT TRUE, + cab_operator_id INT REFERENCES cab_operators(id) +); + +CREATE TABLE schedules ( + id SERIAL PRIMARY KEY, + cab_id INT REFERENCES cabs(id) ON DELETE CASCADE, + route_id INT REFERENCES routes(id) ON DELETE CASCADE, + frequency VARCHAR(20) NOT NULL, + time TIME NOT NULL, + price FLOAT NOT NULL CHECK (price >= 0) +); + +CREATE TABLE reports ( + id SERIAL PRIMARY KEY, + route_id INT REFERENCES routes(id) ON DELETE CASCADE, + date DATE NOT NULL, + bookings INT DEFAULT 0, + completed INT DEFAULT 0, + cancellations INT DEFAULT 0, + revenue FLOAT DEFAULT 0 +); + +CREATE TABLE revenue ( + id SERIAL PRIMARY KEY, + route_id INT REFERENCES routes(id) ON DELETE CASCADE, + total_revenue FLOAT NOT NULL DEFAULT 0 +); + +CREATE TABLE vendors ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + contact VARCHAR(20), + address VARCHAR(200), + kyc_status VARCHAR(20) DEFAULT 'Pending' +); + +CREATE TABLE customers ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100), + phone VARCHAR(20), + profile_pic VARCHAR(200), + status VARCHAR(20), + disputes INT DEFAULT 0, + rating FLOAT, + joined DATE DEFAULT CURRENT_DATE +); + +-- Seed data +INSERT INTO cab_operators (name, active) VALUES + ('Cabz Inc', TRUE), + ('CityRide', TRUE); + +INSERT INTO cabs (name, type, seats) VALUES + ('Cab 101', 'Sedan', 4), + ('Cab 202', 'SUV', 6), + ('Cab 303', 'Van', 8); + +INSERT INTO routes (origin, destination, distance_km, eta_min, base_fare, active, cab_operator_id) VALUES + ('New York', 'Brooklyn', 15.50, 35, 25.00, TRUE, 1), + ('Brooklyn', 'Queens', 22.80, 45, 32.50, TRUE, 1), + ('San Jose', 'Palo Alto', 24.00, 40, 30.00, TRUE, 2); + +INSERT INTO schedules (cab_id, route_id, frequency, time, price) VALUES + (1, 1, 'Daily', '09:00', 25.00), + (2, 2, 'Daily', '10:00', 32.50), + (3, 3, 'Weekly', '08:30', 35.00); + +INSERT INTO reports (route_id, date, bookings, completed, cancellations, revenue) VALUES + (1, DATE '2025-08-01', 120, 115, 5, 2112.00), + (2, DATE '2025-08-02', 90, 85, 5, 1820.00); + +INSERT INTO revenue (route_id, total_revenue) VALUES + (1, 2112.00), + (2, 1440.00), + (3, 1350.00); + +INSERT INTO vendors (name, contact, address, kyc_status) VALUES + ('Cab Vendor A', '+1-555-0100', '123 5th Ave', 'Verified'), + ('Cab Vendor B', '+1-555-0101', '456 Market St', 'Pending'); + +INSERT INTO customers (name, email, phone, status, disputes, rating, joined) VALUES + ('Alice', 'alice@example.com', '555-1212', 'Active', 0, 4.8, CURRENT_DATE - INTERVAL '30 days'), + ('Bob', 'bob@example.com', '555-3434', 'Active', 1, 4.2, CURRENT_DATE - INTERVAL '10 days'); + +-- Convenience views for frontend list pages +CREATE OR REPLACE VIEW v_routes_list AS +SELECT + id, + origin AS "from", + destination AS "to", + CONCAT(distance_km, ' km') AS distance +FROM routes +WHERE active = TRUE; + +CREATE OR REPLACE VIEW v_schedules_list AS +SELECT + s.id, + c.name AS cab, + (r.origin || ' - ' || r.destination) AS route, + s.frequency, + TO_CHAR(s.time, 'HH24:MI') AS time, + s.price +FROM schedules s +JOIN cabs c ON s.cab_id = c.id +JOIN routes r ON s.route_id = r.id; + +COMMIT;