- Implemented PayrollPage for managing payroll runs with CRUD operations. - Created TimeClocksPage for tracking employee clock-ins and clock-outs. - Developed TimeSheetsPage for displaying employee timesheets. - Added EmployeeCertificationForm and EmployeeDocumentForm for managing employee certifications and documents. - Introduced LeaveRequestForm for submitting and managing leave requests. - Added usePermissions hook for UI-side permission checks. - Created LeaveRequestsClient and PayrollClient for API interactions.
36 KiB
Reparee → Garage Box Feature Parity — Implementation Roadmap
Purpose of this doc. Single source of truth for closing the gap between Reparee's current dashboard and Garage Box. Captures the gap analysis, the canonical Reparee patterns to mimic, and a per-phase implementation spec detailed enough for a fresh agent to pick up any phase and ship it without re-discovering conventions.
Status (2026-05-21). Phase 1 (Integrations placeholder) is the only phase ready to execute. Phases 2–8 are spec'd here but not started. Pick one, re-open it in a focused planning session, and implement.
0. Reading order
- Part A — Gap analysis (what's missing and why).
- Part B — Project conventions cheatsheet (the patterns every phase MUST follow).
- Part C — Phase index (the at-a-glance table).
- Part D — Per-phase implementation specs (Phases 1–8, in recommended build order).
Part A — Gap analysis (Garage Box vs Reparee)
Legend: ✅ exists ⚠️ partial / different location ❌ missing
Already covered
- Sales (Customers, Vehicles, Inspections, Estimates, Job Cards, Invoices, Payments Received, Credit Notes) — ✅
- Purchases (Vendors, Expenses, POs, Bills, Payments Made, Vendor Credits) — ✅
- Employees / Productivity (Employees, Time Clocks, Time Sheets, Payroll, Shop Calendars, Shop Timing, Holidays, Tasks, Leave Requests) — ✅ (Reparee adds Leave Requests beyond Garage Box)
- Items (Services, Parts, Expense Item, Service Group, Inventory Adjustments) — ✅
- Settings Company / Shop Types / Tax & Rates / Configurations — ✅
Gaps
| Garage Box | Status in Reparee | Phase |
|---|---|---|
| Settings → Integrations | ❌ → placeholder | Phase 1 |
| Settings → Master (hub for body types + make & model + other lookups) | ❌ as a hub (data scattered) | Phase 2 |
| Settings → Templates (notification template library, ~35 templates × 4 channels) | ⚠️ only inspection templates | Phase 3 |
| Reports (top-level module) | ❌ | Phase 4 |
| CRM (Leads pipeline, Calls log + calendar, Tasks pipeline) | ❌ (Reparee has only productivity tasks, not CRM tasks) | Phase 5 |
| Marketing (Service Reminders, Rating & Reviews, Google Business Reviews) | ❌ | Phase 6 |
| Settings → Integrations (real connectors: Google, Microsoft, Zoho, Interakt, Respond, Vonage, Geidea, MSG91, Wati, Xero, WhatsApp) | ❌ | Phase 7 |
| Accountants (Chart of Accounts + Manual Journals + default account wiring) | ❌ | Phase 8 |
Cross-cutting Garage Box conventions to mimic
- Pipelines (Leads, Tasks) have user-editable column sections (add / rename / reorder). Default sections seeded on first run.
+ Labelchip on every record (taxonomy / colored tags).- Configurable doc-number sequences with a gear icon next to the
#field (LD-000001,CL-000001,TK-000001,JN-000001, etc.). - Notification templates are channel-aware (SMS / WhatsApp / Email / Push) with per-channel toggles.
- Per-row Department filter is universal — every list view has a Department dropdown in the toolbar.
Part B — Project conventions cheatsheet (MUST follow in every phase)
Every phase below assumes these patterns. Don't re-invent.
B1. Frontend module structure
- Modules live at
garage-erp/apps/dashboard/modules/<area>/. - Each resource gets:
<resource>-form.tsx,<resource>.schema.ts(Zod), optional<resource>-actions.tsx, optional inline section components (e.g.<resource>-services-section.tsx). - Reference:
garage-erp/apps/dashboard/modules/estimates/is the canonical example. Mirror its file layout for any new resource.
B2. Routes (Next.js App Router)
- List page:
app/(authenticated)/<area>/<resource>/page.tsx("use client"+ResourcePage<Client>). - Detail page:
app/(authenticated)/<area>/<resource>/[id]/page.tsx(async server component, usesgetServerApi()+DashboardPage). - Sub-tabs of a detail (e.g. notes, documents):
[id]/<subroute>/page.tsx. - Reference:
app/(authenticated)/sales/estimates/page.tsx(list) +[id]/page.tsx(detail).
B3. Tables & row actions
- Use TanStack Table via
ResourcePagewrapper. - Row actions via
actionsColumn({ extraItems: (row) => [...] }). - Print buttons via
useDocumentPrint(type, id)fromapps/dashboard/shared/hooks/use-document-print.ts.
B4. Forms
- React Hook Form 7 + Zod 4. Schema lives in
<resource>.schema.ts. - Use
useResourceForm<FormValues, ApiPayload>({ schema, resourceId, initialize, mapToFormValues })fromapps/dashboard/shared/hooks/. - Mutations:
useFormMutationfor create/update; invalidate viatableQuery.invalidateQuery()or the resource's query key.
B5. Sidebar navigation
- Single source:
garage-erp/apps/dashboard/config/navGroups.tsx— hard-coded, no i18n layer. - Add a new group entry or a sub-item; pick a
lucide-reacticon already imported (or add to the import block at top of file).
B6. API client (@garage/api)
- All clients extend
CrudClient<INDEX_ROUTE, BY_ID_ROUTE>(path:packages/api/src/clients/<name>.ts). - After backend changes, regenerate:
pnpm --filter @garage/api generate(runsgenerate:openapi && generate:types). - The generated files in
packages/api/are NOT hand-edited; only the thin client wrappers are.
B7. Backend routes
- Legacy string controller resolution in
reparee_backend/routes/api.php:Route::get('/leads', 'LeadController@index'); Route::post('/leads', 'LeadController@store'); // etc. - Every route is gated by
permission.checkmiddleware (auto-applied group-wide).
B8. Backend controllers
- Standard CRUD shape:
index,show,store,update,destroyreturningJsonResponse. - Path:
reparee_backend/app/Http/Controllers/Api/<Name>Controller.php. - Use
vyuldashev/laravel-openapiannotations so OpenAPI spec stays in sync (mirrorEstimateController).
B9. Models & migrations
- Models in
reparee_backend/app/Models/. Use$fillable,$casts, relations. - Migrations:
php artisan make:migration create_<table>_table→ standardSchema::createwith$table->id()+timestamps().
B10. Permissions
- Permissions are columns on the
rolestable:can_{view|create|update|delete}_{resource}. - To add a new resource:
- Create a migration that adds the four boolean columns to
roles(defaultfalse). - Update
database/seeders/RoleSeeder.phpso Super Admin getstruefor all new columns (it auto-discoverscan_*columns — verify). - The
permission.checkmiddleware reads the column name from the route name; ensure route is named<resource>.<ability>or matches its existing convention.
- Create a migration that adds the four boolean columns to
- Reference migration:
2026_04_06_120000_create_roles_table_and_refactor_employee_permissions.php.
B11. i18n (backend)
- Pairs required: every new key in
lang/en/<file>.phpMUST have the matchinglang/ar/<file>.phpkey in the same change. - Use
__('api.<area>.<key>')in controllers and responses. - Dashboard nav labels are NOT translated (hard-coded in
navGroups.tsx).
B12. Document-print (PDF)
- Single entry:
POST /api/document-print→DocumentPrintController@handle. - To add a printable type:
- Add string to
DocumentPrintController::TYPES. - Add
payload<Type>()method on the controller. - Add
resources/views/pdf/document-print-<type>.blade.php. - Add the type to the
DocumentPrintTypeunion inpackages/api/src/clients/document-print.ts.
- Add string to
B13. Pipeline-with-sections precedent
TaskSectionmodel +task-section-form.tsxalready exist. Fields:title,arrangement(sort order),is_default.- Reuse this pattern for
LeadSection,CallSection(if needed),JournalSection, etc.
B14. House rules (from CLAUDE.md)
- Never
git commit / push / tag / reset --hard / rebase— stop at file edits. - Don't add new dependencies without explicit approval.
- After edits, run
pnpm --filter dashboard typecheck,pnpm --filter dashboard lint, andphp -lon changed PHP files, then report results. - Match existing conventions before introducing new patterns.
Part C — Phase index
| # | Phase | Effort | Backend changes | Frontend changes | Depends on |
|---|---|---|---|---|---|
| 1 | Integrations placeholder | XS (1 hr) | none | 2 files | — |
| 2 | Settings → Master hub | S (1 day) | none (regroup) | 1 layout + tabs | — |
| 3 | Notification Templates library | M (3 days) | new table + CRUD + permission | full module | — |
| 4 | Reports (4 reports locked) | M (1 wk) | 4 aggregation endpoints | reports module + 4 reports w/ Recharts | reads existing data |
| 5 | CRM (Leads, Calls, Tasks-CRM) | XL (2–3 wks) | 3 resources + sections + permissions + i18n | 3 modules + pipelines | reuses TaskSection pattern |
| 6 | Marketing (Reminders email+SMS, Reviews) | L (1–2 wks) | reminder engine + Twilio integration + reviews | 3 sub-pages | needs Phase 3 (templates); Twilio composer dep |
| 7 | DEFERRED | — | — | — | |
| 8A | Accountants — COA + Manual Journals | L (1.5 wks) | ledger UI only, no auto-post | 3 sub-pages | — |
| 8B | Accountants — auto-posting | L (1–2 wks) | observers on Invoice/Bill/Payment/Expense + reversal | linkage panels on source docs | requires 8A stable |
Part D — Per-phase implementation specs
Phase 1 — Settings → Integrations (Coming Soon)
Goal. Add an Integrations entry under Settings that renders a "Coming soon" placeholder.
Backend
None.
Frontend
-
Edit
garage-erp/apps/dashboard/config/navGroups.tsx(around line 183):- There is already a commented-out entry:
// { title: "Integrations", href: "/settings/integrations/providers", icon: <PlugZapIcon /> } - Uncomment it but change
hrefto/settings/integrations(single page, no sub-tabs yet). - Insert between
Inspection TemplatesandConfigurations. ConfirmPlugZapIconis in the top-of-file imports; if not, addPlugZapIconto thelucide-reactimport.
- There is already a commented-out entry:
-
Create
garage-erp/apps/dashboard/app/(authenticated)/settings/integrations/page.tsx:- Use
DashboardPagewithheaderProps={{ title: "Integrations" }}(matchsettings/insurance-types/page.tsx). - Centered
Emptyblock (from@/shared/components/ui/empty):EmptyMedia:<PlugZapIcon className="size-12 text-muted-foreground" />EmptyTitle: "Coming soon"EmptyDescription: "Connect WhatsApp, payments, accounting and email providers. Available in an upcoming release."
- Use
Verification
pnpm --filter dashboard dev.- Sidebar → Settings expands → new "Integrations" entry visible between Inspection Templates and Configurations.
- Click → page renders with the Empty state.
pnpm --filter dashboard typecheck && pnpm --filter dashboard lint.
Out of scope
Sub-tabs (Providers / Integrations), any provider logic, backend.
Phase 2 — Settings → Master hub
Goal. Group existing lookup data screens under one "Master" sidebar entry with tabs, matching Garage Box's UX.
Tabs (in order)
- Body Types (existing
/settings/...) - Make & Model (existing
/settings/make-and-models) - Insurance Types (existing
/settings/insurance-types) - Departments (existing
/settings/departments) - Shop Types (move from top-level OR alias)
- Vehicle Transmissions, Fuel Types, Colors, Unit Types, Payment Terms, Payment Modes, Labor Rates, Reasons, Referral Sources, Document Types, Labels — surface those that currently lack a dedicated screen.
Backend
None new. Verify every tab's data source has a controller in app/Http/Controllers/Api/ (most already do: MakeAndModelController, InsuranceTypeController, DepartmentController, VehicleBodyTypeController, VehicleTransmissionController, VehicleFuelTypeController, VehicleColorController, UnitTypeController, PaymentTermController, PaymentModeController, LaborRateController, ReasonController, ReferralSourceController, DocumentTypeController, LabelController).
Frontend
- Create
app/(authenticated)/settings/master/layout.tsxwith aTabsheader (use existingTabsfrompackages/ui) and the 11+ tab labels above. - Create
app/(authenticated)/settings/master/page.tsxthat redirects to./body-types. - Create
app/(authenticated)/settings/master/<each-tab>/page.tsx. Each is a thin wrapper that imports and renders the existing module list component frommodules/settings/<existing-folder>/. - Edit
config/navGroups.tsx: add{ title: "Master", href: "/settings/master", icon: <DatabaseIcon /> }after Integrations. Keep the existing top-level entries (Insurance Types, Departments, Make & Models) in the Settings list — they coexist as aliases for backwards-compat (per Part F decision).
Verification
- Sidebar → Settings → Master → tabs render and each tab CRUDs correctly.
- Direct URL
/settings/master/insurance-typesworks. - Removed/aliased entries still resolve to the same screen.
Phase 3 — Notification Templates library
Goal. Manage ~35 notification templates (per Garage Box screenshot 16) with per-channel toggles and bodies. Required for Phase 6 (Marketing) and any reminder/automation.
Data model
| Table | Columns |
|---|---|
notification_templates |
id, key (unique e.g. appointment_created), title, description (nullable), created_at, updated_at |
notification_template_channels |
id, notification_template_id (FK), channel enum(sms,whatsapp,email,push), enabled bool, subject (nullable, for email), body text, created_at, updated_at |
Seed all template keys from this list (extracted from Garage Box image 16):
appointment_assigned,appointment_cancelled,appointment_confirmed,appointment_create,appointment_no_shows,appointment_reminder,appointment_requested,appointment_reschedule,appointment_unconfirmedattendance_details,attendance_remindercall_assigned,call_cancelled,call_completed,call_rescheduled,call_scheduledestimate_approved_by_admin,estimate_approved_by_customer,estimate_sendjob_card_customer,job_card_technicianlogin_otpnew_lead_notificationpart_requisition_createpurchase_order_createratings_reviews_linksalary_change_notificationsend_customer_statement,send_inspection,send_invoice,send_vendor_statementservice_reminder_cancelled,service_reminder_reschedule,service_reminder_scheduled,service_reminder_sendwork_request_form_send,workorder_create
Backend
NotificationTemplateController(index,show,updateonly — templates are seeded, not user-created).- Routes (string-resolution style):
Route::get('/notification-templates', 'NotificationTemplateController@index'); Route::get('/notification-templates/{key}', 'NotificationTemplateController@show'); Route::put('/notification-templates/{key}', 'NotificationTemplateController@update'); - Permissions:
can_view_notification_templates,can_update_notification_templates(skip create/delete — seed-managed). - Lang keys in
lang/en/api.php+lang/ar/api.phpundernotification_templates.
Frontend
- Module
apps/dashboard/modules/notification-templates/withnotification-template-row.tsx,notification-template-edit-dialog.tsx,notification-template.schema.ts. - Route
app/(authenticated)/settings/templates/notifications/page.tsx— TanStack Table with columns: Title, SMS (✓/—), WhatsApp, Email, Push, Actions. - Sidebar entry:
config/navGroups.tsxadd{ title: "Templates", href: "/settings/templates/notifications", icon: <FileTextIcon /> }between Inspection Templates and Integrations. (Or restructure existing/settings/templatesif free.) - Edit dialog: tabs per channel, body editor with variable picker (e.g.
{{customer.name}},{{vehicle.plate}},{{appointment.date}}). Variables list lives in anotification-template-variables.tsconstants file co-located with the module. - API client:
packages/api/src/clients/notification-templates.tsextendsCrudClient.
Verification
- Seeded list shows all 35+ rows.
- Toggle a channel → persists.
- Edit body → re-render shows updated body.
- Backend
__('api.notification_templates.updated')returns Arabic when locale isar.
Phase 4 — Reports
Goal. Top-level Reports module with categorized reports across Sales, Purchases, Inventory, Employees, Accountants.
Reports to ship (v1 — locked per Part F)
- Sales by Customer — revenue grouped by customer with date range + department filter.
- Invoices Aging — outstanding invoices bucketed 0–30 / 31–60 / 61–90 / 90+ days.
- Technician Productivity — hours logged vs billable per technician, derived from
TimeSheet+ job-card service rows. - Inventory Valuation + Low-Stock — current stock value by part + list of parts under reorder threshold.
All other reports candidates (Sales by Service/Vehicle, Payments Summary, Estimates Conversion, Purchases by Vendor, Bills Aging, Stock Movement, Attendance, Payroll, Job Card Turnaround/Status) are deferred to a follow-up phase. Architect the reports module so adding the 5th report is trivial.
Charting
Use Recharts via the already-installed wrapper garage-erp/apps/dashboard/shared/components/ui/chart.tsx. No new dependency. Wrapper exports ChartContainer, ChartTooltip, ChartTooltipContent, etc. — mimic shadcn's chart usage docs.
Backend
app/Http/Controllers/Api/Reports/directory — one controller per report category (SalesReportController,PurchasesReportController, etc.).- Each report = single GET endpoint accepting
from,to, optionaldepartment_id,customer_id, etc. Returns aggregated rows. - Use Eloquent +
selectRawaggregations. Index any frequently-grouped columns via migration. - Routes grouped under
/api/reports/.... New permission per category:can_view_reports_sales,can_view_reports_purchases, ... - No new models or tables — pure read aggregation over existing data.
- Lang keys for report titles in
lang/en/reports.php+lang/ar/reports.php.
Frontend
- Module
apps/dashboard/modules/reports/with:report-card.tsx(preview tile in the index).report-filters.tsx(date range, department, customer/vendor).- One subdirectory per category:
sales/,purchases/,inventory/,employees/,job-cards/. Each contains a<report-name>-report.tsxdata view.
- Routes:
app/(authenticated)/reports/page.tsx— index grid of report cards grouped by category.app/(authenticated)/reports/<category>/<slug>/page.tsx— individual report view with filters + table + export buttons.
- Sidebar: add a new top-level group
{ title: "Reports", href: "/reports", icon: <BarChart3Icon /> }after Items/Employees innavGroups.tsx. - Export: reuse
useDocumentPrintpattern for PDF; for Excel use existingmaatwebsite/excelbackend integration (add new export classes inapp/Exports/Reports/). - Charts: prefer
rechartsif already inpackages/uidependencies; otherwise propose to user before adding.
Verification
- Each report renders with sample data filtered by date range.
- PDF export uses the same layout shell as other prints (
resources/views/pdf/layouts/document.blade.php). - Excel export downloads a valid
.xlsx. - Permissions block access when role lacks
can_view_reports_<category>.
Phase 5 — CRM (Leads, Calls, Tasks-CRM)
Goal. Add the missing CRM module with three pipelines: Leads, Calls, Tasks (distinct from existing productivity tasks). Mirrors Garage Box screenshots 1–8.
Sub-phase 5a — Leads
Data model
| Table | Key columns |
|---|---|
lead_sections |
id, title, arrangement, is_default, kind enum(open,won,lost) (kind drives default behavior). Reuse TaskSection pattern. |
leads |
id, number (unique, e.g. LD-000001), title, date, lead_owner_id (FK users), referral_source_id (FK), lead_status_id (FK), department_id, description, lead_section_id (FK), customer_id (nullable), vehicle_id (nullable), salutation, first/last/company name, phone, email, address fields, vehicle fields (make, model, plate, body_type_id). |
lead_services |
pivot to services |
lead_parts |
pivot to parts |
lead_labels |
pivot to labels table |
Backend
LeadController,LeadSectionController,LeadServiceController,LeadPartController(mirror Estimate's split).- Permissions:
can_view_leads,can_create_leads,can_update_leads,can_delete_leads, pluscan_manage_lead_sections. - Number sequence: reuse
InvoiceSequenceControllerpattern (addLeadSequenceController). - Lang keys under
lang/en/api.phpleadssection +lang/ar/api.php. - OpenAPI annotations on each method.
Frontend
- Module
apps/dashboard/modules/leads/withlead-form.tsx,lead.schema.ts,lead-pipeline.tsx(Kanban view),lead-actions.tsx, inline sections for services/parts. - Routes
app/(authenticated)/crm/leads/page.tsx(Kanban + table toggle),[id]/page.tsx(detail). - Sidebar new group
{ title: "CRM", icon: <UsersIcon />, items: [Leads, Calls, Tasks] }after Purchases innavGroups.tsx. - Pipeline UI: Kanban with
@dnd-kit(check if already in deps before adding). Each column = aLeadSection. "+ Section" button opens a section dialog (reusetask-section-form.tsxpattern). - API client
packages/api/src/clients/leads.ts.
Sub-phase 5b — Calls
Data model
| Table | Key columns |
|---|---|
calls |
id, number (CL-000001), call_for (polymorphic morphs to Customer/Lead), vehicle_id, status enum(requested,scheduled,completed,cancelled), type enum(inbound,outbound), subject, call_owner_id, call_date, from_time, to_time, department_id, purpose_id (FK to lookup), agenda text, outcome_id (nullable FK), description, duration_minutes, duration_seconds, voice_recording_path, send_notification bool. |
call_reminders |
id, call_id, unit int, unit_type enum(minute,hour,day), event enum(before,after), channel enum(sms,email,whatsapp). |
call_purposes, call_outcomes |
lookups |
Backend
CallController(CRUD + special endpoints:complete,cancel,reschedule).CallReminderController(nested).- Reminder dispatcher: a Laravel scheduled job in
app/Console/Commands/SendDueCallReminders.phpruns every minute, sends pending reminders via the relevant notification template (Phase 3) through the provider configured in Phase 7. If Phase 7 not yet shipped, log-only. - Permissions: standard four +
can_log_calls,can_complete_calls.
Frontend
- Module
apps/dashboard/modules/calls/withcall-form.tsx(schedule),call-log-form.tsx(log),call.schema.ts,call-calendar.tsx. - Routes
app/(authenticated)/crm/calls/page.tsx(table + tabs Requested/Scheduled/Completed/Cancelled),crm/calls/calendar/page.tsx(month view),[id]/page.tsx.
Sub-phase 5c — Tasks (CRM)
Approach locked per Part F: Option A — extend existing tasks table.
Migration
Add to the existing tasks table:
contextenum (productivity,crm), defaultproductivity, NOT NULL.customer_idnullable FK tocustomers.vehicle_idnullable FK tovehicles.- Index on
(context, status)for the pipeline filter.
Backend
- Backfill existing rows to
context = 'productivity'. - All existing
TaskControllerqueries get a->where('context', 'productivity')filter so/productivity/taskskeeps showing only what it shows today. - Add
CrmTaskController(thin) that scopes tocontext = 'crm'and exposes the same CRUD + section endpoints. Reuse theTaskSectiontable for sections (add acontextcolumn there too, same enum). - Permissions: introduce
can_view_crm_tasks,can_create_crm_tasks,can_update_crm_tasks,can_delete_crm_tasksseparate from the existing productivity-task permissions.
Frontend
- New module
apps/dashboard/modules/crm-tasks/mirroring Leads pipeline pattern (Kanban with sections). - Route
app/(authenticated)/crm/tasks/page.tsx(Kanban + table toggle). - Re-use schema fragments from
modules/tasks/where possible — extract shared bits to amodules/tasks/shared/if duplication grows.
Verification (Phase 5 overall)
- Pipeline drag-and-drop persists section + arrangement.
- New lead with vehicle info creates a corresponding draft Customer + Vehicle (or links existing) — confirm desired behavior.
- Call scheduled with a 10-min reminder → cron job fires at expected time.
- Number sequences advance correctly when a record is created.
Phase 6 — Marketing (Service Reminders + Reviews)
Goal. Automate service reminders based on mileage / time, plus collect and surface customer reviews.
Service Reminders
- New table
service_reminders:id,customer_id,vehicle_id,reminder_type(mileage/time),interval_value,interval_unit,last_triggered_at,next_due_at,statusenum(scheduled,sent,cancelled),channelsJSON (subset of['email','sms']). - Scheduled command
app/Console/Commands/SendDueServiceReminders.phpruns daily; sends via Phase 3 templates (service_reminder_send). - Channels (per Part F): Email via Laravel
Mail::to(...)->send(...)using the configured mail sender; SMS via Twilio.- Install
twilio/sdk(Composer) — flag this dependency add for user approval per CLAUDE.md house rules. - Add a
NotificationDispatcherservice inapp/Services/Notifications/withsendEmail()andsendSms()methods. Reads Twilio credentials fromconfig/services.php(twilio.sid,twilio.token,twilio.from) which read from.env(TWILIO_SID,TWILIO_TOKEN,TWILIO_FROM). - WhatsApp/Push channel toggles render but are disabled with a "Requires integration" tooltip — wiring up to providers happens in deferred Phase 7.
- Install
- Frontend:
app/(authenticated)/marketing/service-reminders/page.tsx— list with status tabs, "Create reminder" form with Email + SMS channel checkboxes.
Rating & Reviews
- New table
customer_reviews:id,customer_id,job_card_id,rating1–5,comment,submitted_at,sourceenum(internal,google). - Public-facing review form already partially served by
PublicInspectionControllerpattern — extend withPublicReviewController. - After job-card completion, fire
ratings_reviews_linktemplate (Phase 3). - Frontend list at
app/(authenticated)/marketing/reviews/page.tsx.
Google Business Reviews
- Read-only sync via Google Business Profile API (requires Phase 7 Google integration).
- Background job polls every 6 h.
- Render in same Reviews list with
source = googlebadge.
Sidebar
- Add CRM-style group
{ title: "Marketing", icon: <MegaphoneIcon />, items: [Service Reminders, Reviews, Google Reviews] }after CRM.
Verification
- Create a mileage-based reminder → next_due_at calculated.
- Cron tick at due time → template send is recorded (or logged if Phase 7 incomplete).
- Submit a public review → appears in admin list.
Phase 7 — Integrations (real connectors) — DEFERRED
Status (per Part F decision): NOT IN ROADMAP. The Phase 1 "Coming soon" placeholder is the long-term state for now. Twilio (SMS) and Laravel mail (email) used by Phase 6 are configured via .env only — no user-facing integrations UI.
Keep this spec as reference for when integrations become a priority later.
Goal (when revived). Replace the Phase 1 placeholder with a real Providers/Integrations page connecting Google, Microsoft, Zoho, Interakt, Respond, Vonage, Geidea, MSG91, Wati, Xero, WhatsApp. Twilio also moves into the UI at that point.
Data model
| Table | Key columns |
|---|---|
integration_providers |
seeded list: id, key (slug), name, purpose, icon_path, auth_type (oauth2/api_key/webhook), is_active. |
integration_connections |
per-tenant connection: id, provider_id, credentials (encrypted JSON), status (connected/disconnected/error), last_synced_at. |
Backend
IntegrationProviderController(list),IntegrationConnectionController(CRUD +connect,disconnect,test).- One service class per provider in
app/Services/Integrations/<Provider>/. Each implements a common contractIntegrationDriverwithconnect(),disconnect(),sendMessage(),pullData(). - OAuth callback routes:
/integrations/{provider}/callback. - Permissions:
can_manage_integrations. - Lang keys for connection status messages.
Frontend
- Replace the Phase 1 placeholder page with
app/(authenticated)/settings/integrations/page.tsxshowing aTabsof "Providers" (catalog) + "Integrations" (connected list). - Provider catalog tile with "Connect" button → opens OAuth popup or credentials dialog.
- Update
navGroups.tsx:href: "/settings/integrations/providers"(the original commented-out path).
Verification
- Connect Google → OAuth popup → callback persists tokens → "Connected" badge appears.
- Test SMS via Vonage → message delivered (in sandbox).
- Disconnect → credentials zeroed.
Out of scope for first cut
Real-time webhook handlers for each provider (do them incrementally, one provider per follow-up PR).
Phase 8 — Accountants
Goal. Introduce a real general ledger. Split into two sub-phases per Part F: ship the ledger UI first (8A), wire auto-posting into existing modules afterward (8B).
Phase 8A — Chart of Accounts + Manual Journals + Default Configuration
Ship the standalone ledger. No existing controllers are touched. Users can manually post journal entries; nothing auto-posts.
Data model
| Table | Key columns |
|---|---|
chart_of_accounts |
id, name, code (nullable), account_type enum(asset,liability,equity,income,expense,cogs), parent_id (nullable, for sub-accounts), is_system bool (lock icon in UI), note. |
journals |
id, number (JN-000001), date, reference, notes, total_amount (must balance), posted_by, posted_at. |
journal_lines |
id, journal_id, account_id, debit, credit, description. Constraint: sum(debit) == sum(credit) per journal. |
default_account_settings |
single-row config table for: sales_account_id, purchase_account_id, inventory_account_id, adjustment_account_id, customer_advance_account_id, deposit_to_account_id, vendor_advance_account_id, paid_through_account_id, purchase_discount_account_id, expense_account_id. |
Seed
Seed ~80 standard accounts matching Garage Box screenshot 9 (Accounts Payable, Accounts Receivable, Advance Tax, … VAT Payable). Mark system accounts (is_system = true).
Backend (8A only)
ChartOfAccountController,JournalController,DefaultAccountSettingsController.- Permissions:
can_view_accountants,can_manage_chart_of_accounts,can_post_journals. - OpenAPI annotations.
- No changes to existing controllers in this sub-phase — Invoices/Bills/Payments/Expenses are untouched.
Frontend
- Sidebar group
{ title: "Accountants", icon: <BookOpenIcon />, items: [Manual Journals, Chart Of Accounts] }after Purchases. - Module
apps/dashboard/modules/accountants/withchart-of-account-list.tsx,chart-of-account-form.tsx,journal-form.tsx(multi-line debit/credit with running balance check),default-account-settings-form.tsx. - Routes:
app/(authenticated)/accountants/chart-of-accounts/page.tsx,accountants/manual-journals/page.tsx,[id]/page.tsx.
Verification (8A)
- Seed list matches Garage Box exactly (account names + types).
- Create a manual journal with unbalanced lines → backend rejects.
- Default Configuration dialog persists.
- Invoices/Bills/Payments/Expenses still work unchanged (no regression).
Phase 8B — Auto-posting + cross-doc linking
Wire the ledger into operational modules. Only start after 8A is stable in production.
Backend
- New service
app/Services/Accounting/JournalPoster.phpwith methods likepostInvoice(Invoice $i),postBill(Bill $b),postPaymentReceived(...),postPaymentMade(...),postExpense(...). - Hook into existing controllers via Eloquent
created/updatedmodel observers (cleaner than editing every controller). Observers live inapp/Observers/, registered inEventServiceProvider. - Each posted journal gets a
source_type+source_idmorphable pair so it links back to the originating document. - Reversal logic: when a source doc is deleted or voided, post the reverse journal (don't soft-delete the original).
- New permission:
can_post_automatic_journals(default true for finance roles).
Frontend
- On Invoice / Bill / Payment / Expense detail pages: add a "Journal Entry" panel showing the posted journal lines (read-only) with a deep link to
/accountants/manual-journals/<id>. - On Journal detail: show "Source Document" link back to the originating doc.
Verification (8B)
- Create an Invoice → Journal auto-posts (debit AR, credit Sales, credit Tax).
- Edit Invoice amount → existing journal reversed + new one posted.
- Delete Invoice → reversal posted; original journal preserved for audit.
- Default Configuration changes are respected on next post.
- Audit trail: every Journal has a non-null
source_type/source_idwhen auto-posted.
Part E — Cross-cutting follow-ups (every phase finishes with)
- Run
pnpm --filter dashboard typecheck && pnpm --filter dashboard lint— must pass. - Run
php -lon every changed PHP file. - Regenerate API client if backend changed:
pnpm --filter @garage/api generate. - Update lang pairs (
lang/en/*.php+lang/ar/*.php). - Update permission migration + seeder for any new permission columns.
- Use the
code-review-graphMCPdetect_changes_tool+get_review_context_toolbefore opening the PR to self-review. - Update
claude-timeline/YYYY-MM-DD.mdwith a one-line note about the phase shipped (per global memory rule). - Do not commit — leave the diff for the user to review.
Part F — Resolved decisions (2026-05-21)
All open questions answered by the user. These are now binding for the phases referenced.
- Phase 2 (Master) → Keep top-level entries as aliases. Insurance Types, Departments, Make & Models remain in the Settings sidebar AND appear under Master tabs. Backwards-compat for bookmarks. No redirects, no removals.
- Phase 4 (Reports) — charts → Use Recharts (already installed) via the existing wrapper at
garage-erp/apps/dashboard/shared/components/ui/chart.tsx. No new dependency. - Phase 4 (Reports) — v1 scope → ship exactly these four reports:
- Sales by Customer
- Invoices Aging (0–30 / 31–60 / 61–90 / 90+ days)
- Technician Productivity (hours logged vs billable per technician)
- Inventory Valuation + Low-Stock All other reports listed in Phase 4 are deferred.
- Phase 5c (CRM Tasks) → Extend the existing
taskstable (Option A). Addcontextenum (productivity|crm), nullablecustomer_id, nullablevehicle_id. All existing task queries get acontext = 'productivity'filter. - Phase 6 (Marketing reminders) — channels → Email + SMS in v1; WhatsApp deferred.
- Email sends via Laravel's configured mail sender (
config/mail.php) — works out of the box, no Integrations UI required. - SMS sends via Twilio. Configured server-side via
.env(TWILIO_SID,TWILIO_TOKEN,TWILIO_FROM) — not surfaced in the Integrations page yet. - WhatsApp channel toggles are visible but disabled with a "Requires integration" tooltip.
- Email sends via Laravel's configured mail sender (
- Phase 7 (Integrations real) → Stays "coming soon." The Phase 1 placeholder is the long-term state for now. Twilio + Laravel mail are configured server-side only; no user-facing connector UI in this roadmap. Phase 7's detailed spec is preserved as a future reference but no work is planned.
- Phase 8 (Accountants) → Split into two sub-phases:
- Phase 8A: Chart of Accounts + Manual Journals + Default Configuration UI. No automatic posting from other modules.
- Phase 8B: Auto-post journals on Invoice / Bill / Payment / Expense create + link from each source doc to its posted journal entry. Big blast radius — ship only after 8A is stable.