From 2ab58267dd92c892a2555e983147b7c5c9083a1d Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Thu, 25 Dec 2025 12:54:00 -0600 Subject: [PATCH] feat: expand documents to include manuals --- MVP-COLOR-SCHEME.md | 74 +++++++++++++++++++ .../documents/data/documents.repository.ts | 10 ++- .../documents/domain/documents.service.ts | 1 + .../documents/domain/documents.types.ts | 5 +- .../002_add_manual_document_type.sql | 17 +++++ docs/PROMPTS.md | 23 ++---- .../documents/components/DocumentForm.tsx | 32 +++++++- .../documents/types/documents.types.ts | 5 +- 8 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 MVP-COLOR-SCHEME.md create mode 100644 backend/src/features/documents/migrations/002_add_manual_document_type.sql diff --git a/MVP-COLOR-SCHEME.md b/MVP-COLOR-SCHEME.md new file mode 100644 index 0000000..296c3cd --- /dev/null +++ b/MVP-COLOR-SCHEME.md @@ -0,0 +1,74 @@ +# MotoVaultPro Color Scheme + +Complete collection of available colors for the MotoVaultPro Application + +## Color Reference Table + +| Color Name | Hex | RGB | CMYK | RAL | +|------------|-----|-----|------|-----| +| Argento Nurburgring | #CACBCE | RGB(202,203,206) | 2, 1, 0, 19 | RAL 7047 | +| Bianco Avorio | #E5DEDC | RGB(229,222,220) | 0, 3, 4, 10 | RAL 9003 | +| Bianco Avus | #F2F3F6 | RGB(242,243,246) | 2, 1, 0, 4 | RAL 9003 | +| Blu Abu Dhabi | #2885B5 | RGB(40,133,181) | 78, 27, 0, 29 | RAL 5012 | +| Blu Pozzi | #2C3970 | RGB(44,57,112) | 61, 49, 0, 56 | RAL 5022 | +| Blu Scozia | #505C77 | RGB(80,92,119) | 33, 23, 0, 53 | RAL 5000 | +| Blu Swaters | #163166 | RGB(22,49,102) | 78, 52, 0, 60 | RAL 5022 | +| Blu Tour De France | #2243AA | RGB(34,67,170) | 80, 61, 0, 33 | RAL 5002 | +| Canna Di Fucile | #7E8792 | RGB(126,135,146) | 14, 8, 0, 43 | RAL 7046 | +| Giallo Modena | #FCE903 | RGB(252,233,3) | 0, 8, 99, 1 | RAL 1016 | +| Grigio Alloy | #A3C7E9 | RGB(163,199,233) | 30, 15, 0, 9 | RAL 7047 | +| Grigio Ferro | #626062 | RGB(98,96,98) | 0, 2, 0, 62 | RAL 7012 | +| Grigio Ingrid | #756D62 | RGB(117,109,98) | 0, 7, 16, 54 | RAL 7006 | +| Grigio Scuro | #4C4E4D | RGB(76,78,77) | 3, 0, 1, 69 | RAL 7043 | +| Grigio Silverstone | #585C64 | RGB(88,92,100) | 12, 8, 0, 61 | RAL 7015 | +| Grigio Titanio | #A8B8C0 | RGB(168,184,192) | 13, 4, 0, 25 | RAL 7040 | +| Nero Daytona | #231F1C | RGB(35,31,28) | 0, 11, 20, 86 | RAL 8022 | +| Rosso 70 Anni | #C40C19 | RGB(196,12,25) | 0, 94, 87, 23 | RAL 3020 | +| Rosso Corsa | #D40000 | RGB(212,0,0) | 0, 100, 100, 17 | RAL 3028 | +| Rosso Dino | #FC652E | RGB(252,101,46) | 0, 60, 82, 1 | RAL 2008 | +| Rosso Fiorano | #5D0001 | RGB(93,0,1) | 0, 100, 99, 64 | RAL 3004 | +| Rosso Fuoco | #D13442 | RGB(209,52,66) | 0, 75, 68, 18 | RAL 3018 | +| Rosso Mugello | #7A212A | RGB(122,33,42) | 0, 73, 66, 52 | RAL 3032 | +| Rosso Scuderia | #FF2800 | RGB(255,40,0) | 0, 84, 100, 0 | RAL 3024 | +| Verde British | #004225 | RGB(0,66,37) | 100, 0, 44, 74 | RAL 6035 | + +## Color Categories + +### Whites & Silvers (3) +- Argento Nurburgring - Metallic silver +- Bianco Avorio - Cream white +- Bianco Avus - Classic white + +### Blues (5) +- Blu Abu Dhabi - Light blue +- Blu Pozzi - Deep blue +- Blu Scozia - Dark blue +- Blu Swaters - Royal blue +- Blu Tour De France - Classic blue + +### Greys (7) +- Canna Di Fucile - Metallic grey +- Grigio Alloy - Light grey (blue) +- Grigio Ferro - Light grey +- Grigio Ingrid - Beige grey +- Grigio Scuro - Deep grey +- Grigio Silverstone - Dark grey +- Grigio Titanio - Classic grey + +### Yellows (1) +- Giallo Modena - Triple layer yellow + +### Blacks (1) +- Nero Daytona - Classic black + +### Reds (7) +- Rosso 70 Anni - 70th anniversary red +- Rosso Corsa - Classic Ferrari red +- Rosso Dino - Red/orange +- Rosso Fiorano - Ruby red +- Rosso Fuoco - Triple layer red +- Rosso Mugello - Dark red +- Rosso Scuderia - Bright red + +### Greens (1) +- Verde British - British racing green diff --git a/backend/src/features/documents/data/documents.repository.ts b/backend/src/features/documents/data/documents.repository.ts index af8d1b4..0f9ca1e 100644 --- a/backend/src/features/documents/data/documents.repository.ts +++ b/backend/src/features/documents/data/documents.repository.ts @@ -27,6 +27,7 @@ export class DocumentsRepository { issuedDate: row.issued_date, expirationDate: row.expiration_date, emailNotifications: row.email_notifications, + scanForMaintenance: row.scan_for_maintenance, createdAt: row.created_at, updatedAt: row.updated_at, deletedAt: row.deleted_at @@ -48,11 +49,12 @@ export class DocumentsRepository { issuedDate?: string | null; expirationDate?: string | null; emailNotifications?: boolean; + scanForMaintenance?: boolean; }): Promise { const res = await this.db.query( `INSERT INTO documents ( - id, user_id, vehicle_id, document_type, title, notes, details, issued_date, expiration_date, email_notifications - ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) + id, user_id, vehicle_id, document_type, title, notes, details, issued_date, expiration_date, email_notifications, scan_for_maintenance + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING *`, [ doc.id, @@ -65,6 +67,7 @@ export class DocumentsRepository { doc.issuedDate ?? null, doc.expirationDate ?? null, doc.emailNotifications ?? false, + doc.scanForMaintenance ?? false, ] ); return this.mapDocumentRecord(res.rows[0]); @@ -91,7 +94,7 @@ export class DocumentsRepository { await this.db.query(`UPDATE documents SET deleted_at = NOW() WHERE id = $1 AND user_id = $2`, [id, userId]); } - async updateMetadata(id: string, userId: string, patch: Partial>): Promise { + async updateMetadata(id: string, userId: string, patch: Partial>): Promise { const fields: string[] = []; const params: any[] = []; let i = 1; @@ -101,6 +104,7 @@ export class DocumentsRepository { if (patch.issuedDate !== undefined) { fields.push(`issued_date = $${i++}`); params.push(patch.issuedDate); } if (patch.expirationDate !== undefined) { fields.push(`expiration_date = $${i++}`); params.push(patch.expirationDate); } if (patch.emailNotifications !== undefined) { fields.push(`email_notifications = $${i++}`); params.push(patch.emailNotifications); } + if (patch.scanForMaintenance !== undefined) { fields.push(`scan_for_maintenance = $${i++}`); params.push(patch.scanForMaintenance); } if (!fields.length) return this.findById(id, userId); params.push(id, userId); const sql = `UPDATE documents SET ${fields.join(', ')} WHERE id = $${i++} AND user_id = $${i++} AND deleted_at IS NULL RETURNING *`; diff --git a/backend/src/features/documents/domain/documents.service.ts b/backend/src/features/documents/domain/documents.service.ts index c47bf59..eeff00b 100644 --- a/backend/src/features/documents/domain/documents.service.ts +++ b/backend/src/features/documents/domain/documents.service.ts @@ -20,6 +20,7 @@ export class DocumentsService { issuedDate: body.issuedDate ?? null, expirationDate: body.expirationDate ?? null, emailNotifications: body.emailNotifications ?? false, + scanForMaintenance: body.scanForMaintenance ?? false, }); } diff --git a/backend/src/features/documents/domain/documents.types.ts b/backend/src/features/documents/domain/documents.types.ts index 182a9ec..a748cad 100644 --- a/backend/src/features/documents/domain/documents.types.ts +++ b/backend/src/features/documents/domain/documents.types.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const DocumentTypeSchema = z.enum(['insurance', 'registration']); +export const DocumentTypeSchema = z.enum(['insurance', 'registration', 'manual']); export type DocumentType = z.infer; // API response type (camelCase for frontend) @@ -21,6 +21,7 @@ export interface DocumentRecord { issuedDate?: string | null; expirationDate?: string | null; emailNotifications?: boolean; + scanForMaintenance?: boolean; createdAt: string; updatedAt: string; deletedAt?: string | null; @@ -36,6 +37,7 @@ export const CreateDocumentBodySchema = z.object({ issuedDate: z.string().optional(), expirationDate: z.string().optional(), emailNotifications: z.boolean().optional(), + scanForMaintenance: z.boolean().optional(), }); export type CreateDocumentBody = z.infer; @@ -46,6 +48,7 @@ export const UpdateDocumentBodySchema = z.object({ issuedDate: z.string().nullable().optional(), expirationDate: z.string().nullable().optional(), emailNotifications: z.boolean().optional(), + scanForMaintenance: z.boolean().optional(), }); export type UpdateDocumentBody = z.infer; diff --git a/backend/src/features/documents/migrations/002_add_manual_document_type.sql b/backend/src/features/documents/migrations/002_add_manual_document_type.sql new file mode 100644 index 0000000..969323c --- /dev/null +++ b/backend/src/features/documents/migrations/002_add_manual_document_type.sql @@ -0,0 +1,17 @@ +-- Migration: Add 'manual' document type and scan_for_maintenance column +-- Purpose: Support vehicle/equipment manuals with future maintenance schedule scanning + +-- Step 1: Extend document_type CHECK constraint to include 'manual' +-- Note: Must drop and recreate since PostgreSQL doesn't support ALTER CONSTRAINT +ALTER TABLE documents DROP CONSTRAINT IF EXISTS documents_document_type_check; +ALTER TABLE documents ADD CONSTRAINT documents_document_type_check + CHECK (document_type IN ('insurance', 'registration', 'manual')); + +-- Step 2: Add scan_for_maintenance boolean column +-- Only applicable to 'manual' type, defaults to false +ALTER TABLE documents ADD COLUMN IF NOT EXISTS scan_for_maintenance BOOLEAN DEFAULT false; + +-- Step 3: Create partial index for future maintenance scanning queries +CREATE INDEX IF NOT EXISTS idx_documents_scan_for_maintenance + ON documents(scan_for_maintenance) + WHERE scan_for_maintenance = true AND deleted_at IS NULL AND document_type = 'manual'; diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index 8e3005f..8bf1cea 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -22,25 +22,18 @@ You are a senior software engineer specializsing in NodeJS, Typescript, front en - Make no assumptions. - Ask clarifying questions. - Ultrathink -- You will be implementing a backup and restore functionality directly in this application. +- You will be extending the "Documents" feature to include manuals. *** CONTEXT *** - This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s. - Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change. -- There is no backup and restore functionality in this system. -- There needs to be a new section added in the admin settings. Here are the files and line numbers of existing admin settings to base this change off of and mirror those. -frontend/src/pages/admin/AdminEmailTemplatesPage.tsx -192: Manage notification email templates - -frontend/src/pages/SettingsPage.tsx -436: secondary="Manage notification email templates" - -frontend/src/features/settings/mobile/MobileSettingsScreen.tsx -430:
Manage notification email templates
-- There currently is a folder data/backups/ that is empty. Evaluate if this should be used or if another one makes more sense. -- The admin page should show all the local backups. But also allow for uploading a backup. -- The admin page should have a option to create a manual backup and download it. -- The admin page should have the ability to schedule backups as hourly, daily, weekly, monthly. And also allow multiple schedules and retention policies. +- You need to extend the Documents feature to include a third "Document Type" +- Right now the document has two types. Insurance and Registration +- The third type will be called "Manual" +- This document will just have the uploaded file and a notes field and Title field +- When implementing this we need to play for the future feature of scanning the document for maintenance schedules +- Add a toggle for this scanning. Label it "Scan for Maintenance Schedule" +- Do not implement this feature at this time but put the toggle in the interface and the backend changes to facility this workflow. *** CHANGES TO IMPLEMENT *** - Research this code base and ask iterative questions to compile a complete plan. diff --git a/frontend/src/features/documents/components/DocumentForm.tsx b/frontend/src/features/documents/components/DocumentForm.tsx index 2bb4ae1..b756b22 100644 --- a/frontend/src/features/documents/components/DocumentForm.tsx +++ b/frontend/src/features/documents/components/DocumentForm.tsx @@ -36,6 +36,9 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel const [registrationExpirationDate, setRegistrationExpirationDate] = React.useState(''); const [registrationCost, setRegistrationCost] = React.useState(''); + // Manual fields + const [scanForMaintenance, setScanForMaintenance] = React.useState(false); + const [file, setFile] = React.useState(null); const [uploadProgress, setUploadProgress] = React.useState(0); const [error, setError] = React.useState(null); @@ -57,6 +60,7 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel setLicensePlate(''); setRegistrationExpirationDate(''); setRegistrationCost(''); + setScanForMaintenance(false); setFile(null); setUploadProgress(0); setError(null); @@ -94,15 +98,17 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel details.cost = registrationCost ? parseFloat(registrationCost) : undefined; expiration_date = registrationExpirationDate || undefined; } + // Manual type: no details or dates, just scanForMaintenance flag const created = await create.mutateAsync({ vehicleId: vehicleID, documentType: documentType, title: title.trim(), notes: notes.trim() || undefined, - details, + details: Object.keys(details).length > 0 ? details : undefined, issuedDate: issued_date, expirationDate: expiration_date, + scanForMaintenance: documentType === 'manual' ? scanForMaintenance : undefined, }); if (file) { @@ -175,6 +181,7 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel > + @@ -184,7 +191,11 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel className="h-11 min-h-[44px] rounded-lg border border-slate-300 px-3 focus:outline-none focus:ring-2 focus:ring-primary-500" type="text" value={title} - placeholder={documentType === 'insurance' ? 'e.g., Progressive Policy 2025' : 'e.g., Registration 2025'} + placeholder={ + documentType === 'insurance' ? 'e.g., Progressive Policy 2025' : + documentType === 'registration' ? 'e.g., Registration 2025' : + 'e.g., Honda CBR600RR Service Manual' + } onChange={(e) => setTitle(e.target.value)} required /> @@ -336,6 +347,23 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel )} + {documentType === 'manual' && ( +
+ + (Coming soon) +
+ )} +