🔧 Wee Auto Car Care

Core source: seed generator, 4-state bookability logic, interactive booking

Python 3 Vanilla JS CSS Grid localStorage

🔍 About This Code Showcase

This curated showcase walks through the core logic behind Wee Auto Car Care: how the seed is generated with density and skill constraints, how the 4-state bookability classifier works, and how the interactive New Booking modal validates against both bay and mechanic availability.

Full HTML markup and styling are omitted for clarity. The source focuses on the decision logic that differentiates this dashboard from a naive free/busy grid.

📁 Project Structure

projects/wee-auto-car-care/ ├── generate_seed.py # Reproducible 12-day booking seed generator ├── build_dashboard.py # HTML template builder (embeds JSON) ├── workshop_bookings.json # 142 seed bookings — source of truth └── demo.html # Self-contained interactive dashboard (~96KB)

🌱 Seed Generator — Density + Skill Constraints

generate_seed.py builds 142 bookings across 12 days (Mar 2–14, 2026, Sundays closed) while enforcing: no day 100% booked, each mechanic keeps ≥2 free slots/day, multi-slot jobs (timing belt 3h, AC compressor 3h) book consecutive slots on the same bay + mechanic.

📄 generate_seed.py — Service catalog + mechanic skills
SERVICES = [ # name, duration_min, price_rm, skill, bay ("Tyre Replacement (4 wheels)", 45, 60, "tyre", "BAY1"), ("Hunter Alignment", 60, 80, "alignment", "BAY3"), ("Timing Belt Replacement", 180, 450, "timing_belt", "BAY2"), ("AC Service", 75, 180, "ac_service", "BAY2"), # ... 11 more services ] MECHANICS = { "Shamsul": ["oil", "spark_plug", "diagnostic", "timing_belt", "ac_topup", "ac_service", "ac_compressor", "battery", "tyre", "rotation", "balancing"], "Vishnu": ["tyre", "rotation", "balancing", "alignment", "oil", "spark_plug", "ac_topup", "battery"], "Albert": ["tyre", "rotation", "oil", "battery"], } # Only Vishnu has "alignment" -> Bay 3 can only be worked by Vishnu. # Only Shamsul has timing_belt / ac_service / ac_compressor.
📄 generate_seed.py — Placement with constraint checks
while placed < target and attempts < 800: attempts += 1 svc = random.choices(SERVICES, weights=SERVICE_WEIGHTS)[0] name, dur, price, skill, bay = svc n = slots_needed(dur) # ceil(duration / 60) start_idx = random.randint(0, 8 - n) # Bay must be free across all occupied slots if any(schedule[bay][start_idx + i] is not None for i in range(n)): continue # Pick a mechanic with the required skill, free across all slots eligibles = [m for m, skills in MECHANICS.items() if skill in skills] chosen = next( (m for m in eligibles if all(not mech_busy[m][start_idx + i] for i in range(n))), None) if not chosen: continue # Keep at least 2 free slots per mechanic per day if sum(mech_busy[chosen]) + n > 6: continue # Place the booking; mark bay + mechanic busy place_booking(booking, schedule, mech_busy, bay, chosen, start_idx, n) placed += n

🧠 4-State Bookability Classifier

The heart of the dashboard. For every empty cell in the bay grid, compute whether it is truly bookable (qualified mechanic free) or merely idle (bay free, but no one can work it).

📄 demo.html — freeSlotAnalysis()
function freeSlotAnalysis(bay, slotIdx, mechBusy) { // 1. Which mechanics are free at this slot? const freeMechs = MECHANICS.filter(m => !mechBusy[m][slotIdx]); // 2. Which services is this bay capable of hosting? const baySvcs = SERVICE_CATALOG.filter(s => s.bay === bay); // 3. For each service the bay can host, is ANY free mechanic qualified? const bookableServices = []; const bookableMechs = new Set(); baySvcs.forEach(svc => { const qualified = freeMechs.filter( m => MECHANIC_SKILLS[m].includes(svc.skill) ); if (qualified.length > 0) { bookableServices.push(svc); qualified.forEach(m => bookableMechs.add(m)); } }); // 4. Classify: if zero services bookable, the bay is IDLE if (bookableServices.length === 0) { return { state: "idle", reason: idleReason(bay, freeMechs) }; } return { state: "bookable", mechanics: MECHANICS.filter(m => bookableMechs.has(m)), services: bookableServices, }; }

Example: Mar 14, 1pm. Bay 3 is empty. Vishnu (only alignment-skilled mechanic) is busy doing tyre rotation in Bay 1. A naive grid shows Bay 3 as "free." This function flags it as IDLE — "No alignment-skilled mechanic free", which prevents the taukey from wrongly telling a customer to come for alignment.

➕ Interactive New Booking — Validation

The New Booking modal dynamically filters dropdowns so the taukey can never submit an impossible booking. The slot dropdown only shows slots where the service's required bay is free. The mechanic dropdown only shows skill-matched mechanics who are free across all occupied slots.

📄 demo.html — refreshSlotsAndMechanics()
function refreshSlotsAndMechanics() { const date = document.getElementById("f-date").value; const svcName = document.getElementById("f-service").value; const svc = SERVICE_CATALOG.find(s => s.name === svcName); const dur = SERVICE_DURATIONS()[svcName]; const n = slotsNeeded(dur); const sched = computeSchedule(date); // bayBusy + mechBusy from state.bookings // Slot dropdown: only list slots where bay is free across n consecutive slots const slotSel = document.getElementById("f-slot"); slotSel.innerHTML = ""; for (let i = 0; i <= 8 - n; i++) { const free = Array.from({length: n}, (_, k) => !sched.bayBusy[svc.bay][i+k]) .every(Boolean); if (free) { const opt = new Option( `${SLOT_LABELS[i]} (${n > 1 ? n + " slots" : "1 slot"}, ready ${readyTimeStr(SLOTS[i], dur)})`, SLOTS[i] ); slotSel.add(opt); } } refreshMechanicOnly(); // Chained: mechanic dropdown depends on selected slot } function refreshMechanicOnly() { // Filter: mechanic must have required skill AND be free across n slots const qualified = MECHANICS.filter( m => MECHANIC_SKILLS[m].includes(svc.skill) ); const avail = qualified.filter(m => Array.from({length: n}, (_, k) => !sched.mechBusy[m][startIdx+k]).every(Boolean) ); // Populate dropdown with only qualified + free mechanics avail.forEach(m => mechSel.add(new Option(m, m))); }

💾 localStorage Sandbox — Each Visitor Isolated

The dashboard is demo-ready without a backend because each visitor's edits persist in their own browser under a versioned storage key. Reset restores the canonical seed.

📄 demo.html — State management
const STORAGE_KEY = "wee-auto-bookings-v1"; function cloneInitial() { return JSON.parse(JSON.stringify(WORKSHOP_DATA)); } function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function resetState() { localStorage.removeItem(STORAGE_KEY); state = cloneInitial(); renderAll(); } // On page load: use saved edits if any, else fresh seed let state = loadState() || cloneInitial();

Production upgrade path: replace loadState / saveState with fetch("/api/bookings") calls to a FastAPI backend. The render functions and validation logic stay identical — only the state layer swaps.