Build a complete job card and service tracking system for automotive workshops
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are an expert full-stack developer specialising in Next.js, Supabase, and Tailwind CSS. Build a complete digital job card management system for a Malaysian car workshop called "{{workshop_name}}". This system replaces manual carbon-copy job cards and paid workshop software like AutoCount Workshop.
---
## Tech Stack
- **Frontend**: Next.js 14 (App Router) + TypeScript
- **Styling**: Tailwind CSS + shadcn/ui (new-york style, light mode)
- **Database**: Supabase (PostgreSQL + Row Level Security)
- **Storage**: Supabase Storage (vehicle photo uploads)
- **Deployment**: Vercel
- **Fonts**: Inter + Noto Sans SC (for Chinese character support)
---
## Database Schema
Create the following tables in Supabase:
### customers
sql
create table customers (
id uuid primary key default gen_random_uuid(),
name text not null,
phone text not null unique,
email text,
ic_number text,
preferred_language text default 'zh',
notes text,
created_at timestamptz default now()
);
create index on customers (phone);
### vehicles
sql
create table vehicles (
id uuid primary key default gen_random_uuid(),
customer_id uuid references customers(id) not null,
plate_number text not null unique,
make text not null,
model text not null,
year integer,
colour text,
engine_cc integer,
fuel_type text default 'petrol',
current_mileage integer,
vin text,
notes text,
created_at timestamptz default now()
);
create index on vehicles (plate_number);
create index on vehicles (customer_id);
### mechanics
sql
create table mechanics (
id uuid primary key default gen_random_uuid(),
name text not null,
phone text,
specialisation text[],
is_active boolean default true,
created_at timestamptz default now()
);
### job_cards
sql
create table job_cards (
id uuid primary key default gen_random_uuid(),
job_number text not null unique,
vehicle_id uuid references vehicles(id) not null,
customer_id uuid references customers(id) not null,
assigned_mechanic_id uuid references mechanics(id),
status text not null default 'waiting'
check (status in ('waiting','diagnosed','in_progress','quality_check','ready','collected','cancelled')),
mileage_in integer,
mileage_out integer,
customer_complaint text,
diagnosis_notes text,
internal_notes text,
estimated_cost numeric(10,2),
actual_cost numeric(10,2),
labour_total numeric(10,2) default 0,
parts_total numeric(10,2) default 0,
discount numeric(10,2) default 0,
sst_rate numeric(5,2) default {{tax_rate}},
sst_amount numeric(10,2) default 0,
grand_total numeric(10,2) default 0,
promised_date date,
promised_time time,
collected_at timestamptz,
warranty_expiry date,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index on job_cards (vehicle_id);
create index on job_cards (customer_id);
create index on job_cards (assigned_mechanic_id);
create index on job_cards (status);
create index on job_cards (created_at);
### service_items
sql
create table service_items (
id uuid primary key default gen_random_uuid(),
job_card_id uuid references job_cards(id) on delete cascade not null,
service_name text not null,
description text,
labour_hours numeric(5,2) default 1,
labour_rate numeric(10,2) default 0,
labour_total numeric(10,2) generated always as (labour_hours * labour_rate) stored,
is_completed boolean default false,
completed_at timestamptz,
sort_order integer default 0
);
create index on service_items (job_card_id);
### parts_catalogue
sql
create table parts_catalogue (
id uuid primary key default gen_random_uuid(),
part_code text not null unique,
name text not null,
name_zh text,
category text,
unit text default 'pcs',
cost_price numeric(10,2) default 0,
selling_price numeric(10,2) default 0,
stock_qty integer default 0,
reorder_level integer default 5,
supplier text,
created_at timestamptz default now()
);
create index on parts_catalogue (part_code);
create index on parts_catalogue (name);
### job_parts
sql
create table job_parts (
id uuid primary key default gen_random_uuid(),
job_card_id uuid references job_cards(id) on delete cascade not null,
part_id uuid references parts_catalogue(id),
part_code text,
part_name text not null,
quantity integer not null default 1,
unit_price numeric(10,2) not null,
total_price numeric(10,2) generated always as (quantity * unit_price) stored,
warranty_months integer default 0,
warranty_expiry date
);
create index on job_parts (job_card_id);
create index on job_parts (part_id);
### invoices
sql
create table invoices (
id uuid primary key default gen_random_uuid(),
invoice_number text not null unique,
job_card_id uuid references job_cards(id) not null,
customer_id uuid references customers(id) not null,
subtotal numeric(10,2) not null,
discount numeric(10,2) default 0,
sst_amount numeric(10,2) default 0,
grand_total numeric(10,2) not null,
payment_method text check (payment_method in ('cash','card','qr','bank_transfer','credit')),
payment_status text default 'unpaid' check (payment_status in ('unpaid','partial','paid')),
amount_paid numeric(10,2) default 0,
issued_at timestamptz default now(),
paid_at timestamptz,
notes text
);
create index on invoices (job_card_id);
create index on invoices (customer_id);
### vehicle_photos
sql
create table vehicle_photos (
id uuid primary key default gen_random_uuid(),
job_card_id uuid references job_cards(id) on delete cascade not null,
storage_path text not null,
caption text,
photo_type text default 'condition'
check (photo_type in ('condition','damage','before','after')),
uploaded_at timestamptz default now()
);
create index on vehicle_photos (job_card_id);
### status_history
sql
create table status_history (
id uuid primary key default gen_random_uuid(),
job_card_id uuid references job_cards(id) on delete cascade not null,
from_status text,
to_status text not null,
changed_by text,
notes text,
changed_at timestamptz default now()
);
create index on status_history (job_card_id);
---
## Core Features
### 1. Kanban Dashboard (/dashboard)
Build a drag-and-drop Kanban board using `@dnd-kit/core` with six columns:
**Waiting → Diagnosed → In Progress → Quality Check → Ready → Collected**
Each job card on the board shows:
- Plate number in large bold text (e.g., `WKL 3847`)
- Vehicle make and model (e.g., `Perodua Myvi 2022`)
- Service summary (first 2 service items)
- Assigned mechanic name
- Promised completion time
- Estimated cost in RM
- Visual urgency: red border if overdue, yellow border if due within 1 hour
Dragging a card to a new column triggers a Supabase update and inserts a row into `status_history`.
### 2. New Job Card Creation (/job-cards/new)
Step 1 — Plate Number Lookup:
- Large search input at the top
- On input, query `vehicles` table with debounce (300ms)
- Match found: auto-populate customer name, phone, vehicle details, and show last 3 service summaries
- No match: reveal inline form to register new customer and vehicle
- Pre-populated Malaysian vehicle makes: Perodua (Myvi, Axia, Bezza, Alza, Ativa), Proton (Saga, Persona, Iriz, X50, X70), Toyota (Vios, Yaris, Hilux), Honda (City, Civic, HR-V), Nissan (Almera, Navara)
Step 2 — Job Card Details:
- Customer complaint / service request (textarea, bilingual placeholder)
- Mileage in (number input)
- Promised date + time (date/time pickers)
- Mechanic assignment (select dropdown showing name + current active job count)
- Service items (multi-select from: {{service_types}}; each row has service name, labour hours, labour rate)
Step 3 — Auto-generate Job Number:
- Format: `JC-2024-0001` (4-digit zero-padded counter resets each year)
- Use a PostgreSQL function with `FOR UPDATE` locking to avoid race conditions
### 3. Job Card Detail Page (/job-cards/[id])
Two-column layout on desktop, single-column scroll on mobile:
Left column:
- Vehicle info card: plate, make, model, year, colour, engine size, fuel type
- Customer info card: name, phone (tap-to-call on mobile), lifetime spend
- Status timeline: vertical timeline showing each status change with timestamp and actor
Right column:
- Service checklist: each item is checkable; shows labour hours and rate; staff tap to mark complete
- Parts used: searchable autocomplete against `parts_catalogue`; adding a part shows qty and unit price; saving deducts `stock_qty`
- Photo upload: grid of uploaded photos with type labels; file input uses `accept="image/*" capture="environment"` for mobile camera; uploads to Supabase Storage bucket `vehicle-photos`
- Diagnosis notes and internal notes (separate text areas)
Bottom cost summary bar:
- Labour subtotal, parts subtotal, discount input, SST ({{tax_rate}}%), grand total
- "Generate Invoice" button (yellow/gold CTA) — disabled until status is 'ready' or 'collected'
### 4. Vehicle History Page (/vehicles/[plate])
- Vertical timeline of all past job cards for this vehicle
- Each entry: date, job number, service items, parts replaced, total cost, mileage at time of service
- Mileage progression chart (recharts line chart)
- Filter by year or service category
- "Book New Service" shortcut button pre-filling the plate number
### 5. Invoice Generation (/invoices/[id])
- Letterhead: workshop name, address, phone, SST registration number field
- Bill-to: customer name, IC/company, phone
- Job card reference number and date
- Line items table: service items (labour) + parts used
- Subtotal, discount, SST ({{tax_rate}}%), grand total
- Payment method selector: Cash, Credit/Debit Card, DuitNow QR, Bank Transfer
- Partial payment support: record amount paid, show outstanding balance
- Print-optimised layout (`@media print` hides nav/buttons)
- PDF download using `react-to-pdf` or client-side `jsPDF`
- Invoice number format: `INV-2024-0001`
### 6. Mechanic Workload View (/mechanics)
- Grid of {{mechanic_count}} mechanic cards
- Each card: photo placeholder, name, active job count, today's completed jobs
- Click a mechanic to see their full job list for today
- Drag-and-drop job reassignment between mechanics
- Weekly utilisation bar chart (recharts)
### 7. Parts Inventory (/inventory)
- Searchable, paginated parts catalogue table
- Columns: Part Code, Name, Category, Stock Qty, Reorder Level, Selling Price, Supplier
- Rows with stock below `reorder_level` highlighted in red with a "Low Stock" badge
- Edit selling price and stock qty inline
- CSV bulk import (parse with `papaparse`, upsert to Supabase)
- Usage history tab: shows which job cards consumed each part
### 8. Dashboard Summary Cards
Top of `/dashboard`, show four metric cards:
1. Today's jobs: new / in progress / ready for collection
2. Today's estimated revenue (RM) from active job cards
3. Outstanding invoices: count and total RM unpaid
4. Low stock alerts: number of parts below reorder level
---
## Business Rules
1. **Job number generation**: Format `JC-YYYY-NNNN`. Use a PostgreSQL sequence per year. Counter resets on January 1st. Implement as a `SECURITY DEFINER` function callable from the app.
2. **SST calculation**: Apply `{{tax_rate}}%` SST to labour and parts subtotal after discount. Display SST amount as a separate line. Include an SST registration number field on invoices.
3. **Parts warranty**: When a part is added to a job, record `warranty_months`. Compute `warranty_expiry = job date + warranty_months`. Surface on vehicle history so staff can check if a complaint is under warranty.
4. **Labour warranty**: Default 30-day warranty on workmanship. `job_cards.warranty_expiry = completed date + 30 days`.
5. **Customer notification**: When job status changes to `ready`, trigger a WhatsApp message via whapi.cloud API: "Hi [Customer Name], your vehicle [Plate Number] is ready for collection. Thank you for choosing {{workshop_name}}! 😊" Send as a background Server Action.
6. **Inventory deduction**: When a `job_parts` row is inserted, run a Supabase database trigger to decrement `parts_catalogue.stock_qty`. On job cancellation, reverse the deduction.
7. **Mileage update**: On job card collection, update `vehicles.current_mileage` with `job_cards.mileage_out`.
8. **Overdue detection**: A job is overdue if `now() > promised_date + promised_time` and status is not 'collected' or 'cancelled'. Surface visually on Kanban with red border.
---
## UI Design Specifications
**Colour palette**:
- Dark navy `#0F172A` navigation bar with white text
- White `#FFFFFF` main content area
- Yellow/gold `#FCD34D` for primary CTA buttons and important callouts
- Slate `#64748B` for secondary text
- Status colour coding: Waiting (slate) → Diagnosed (blue) → In Progress (amber) → Quality Check (purple) → Ready (green) → Collected (dark green)
**Typography**: Inter for Latin text, Noto Sans SC for Chinese labels. Font size hierarchy: plate numbers 20px bold, card titles 14px semibold, metadata 12px regular.
**Responsive**: Kanban collapses to vertical list on mobile (<768px). Job card detail becomes single-column. Photo upload uses native camera on mobile. Mechanic grid becomes 2-column on tablet.
**Malaysian localisation**:
- All currency formatted as `RM X,XXX.XX`
- Date format `DD/MM/YYYY`
- Phone numbers with `+60` prefix
- Plate number examples: WKL 3847, VKR 2233, JHF 888, SAB 1234 (including Sabah/Sarawak formats)
- Support both Roman and Jawi customer names in input fields
---
Please implement all features completely. Structure the code with clear file comments. After building, provide deployment instructions for Vercel and Supabase environment variable setup.