From e239dd91b79c90a9c5782192b9e14b72d009a563 Mon Sep 17 00:00:00 2001 From: Ray Date: Sun, 12 Jul 2026 14:42:44 -0400 Subject: [PATCH] added full settings options --- src/App.tsx | 25 +- src/components/ROForm.tsx | 77 +- .../appointments/AppointmentModal.tsx | 4 +- src/components/appointments/CheckInModal.tsx | 4 +- .../customers/CustomerFormModal.tsx | 5 + .../quoteGenerator/CustomerInfoPanel.tsx | 12 +- .../quoteGenerator/QuoteSummary.tsx | 82 +- src/components/quoteGenerator/ServiceRow.tsx | 7 + .../quoteGenerator/ServicesTable.tsx | 12 +- src/lib/ai.ts | 142 +++- src/lib/dataFormatting.ts | 209 ++++++ src/lib/pdf.ts | 15 +- src/lib/settings.ts | 64 ++ src/lib/spellcheck.ts | 8 + src/lib/totals.ts | 17 +- src/pages/Customers.tsx | 39 +- src/pages/QuoteGenerator.tsx | 13 +- src/pages/RODetailModal.tsx | 30 +- src/pages/ServiceCatalog.tsx | 15 +- src/pages/Settings.tsx | 699 +++++++++++++++++- src/schemas/settings.ts | 64 ++ src/store/quote.ts | 18 +- src/types.ts | 66 ++ 23 files changed, 1439 insertions(+), 188 deletions(-) create mode 100644 src/lib/dataFormatting.ts create mode 100644 src/lib/spellcheck.ts diff --git a/src/App.tsx b/src/App.tsx index f53fd04..0cc0e26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,7 @@ import AdvisorLayout from './components/advisor/AdvisorLayout'; import MobileLayout from './components/mobile/MobileLayout'; import TechnicianShell from './components/technician/TechnicianLayout'; import Login from './pages/Login'; -import { isSetupComplete, loadSettingsForUser } from './lib/settings'; +import { isSetupComplete, loadSettings, loadSettingsForUser } from './lib/settings'; import { getCurrentUserRole, isTechnicianRole, isOwnerRole, type UserRole } from './lib/rbx'; import { useSettingsStore } from './store/settings'; import { TechnicianProvider } from './components/technician/TechnicianContext'; @@ -54,6 +54,22 @@ function RoleHomeRedirect() { return ; } +function applySavedTheme() { + const theme = loadSettings().appTheme; + const systemDark = typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches; + const dark = theme === 'dark' || (theme === 'system' && systemDark); + document.documentElement.classList.toggle('dark', dark); + localStorage.setItem('spq-dark-mode', String(dark)); +} + +function OwnerHome() { + const defaultLandingPage = useSettingsStore((s) => s.settings.defaultLandingPage); + if (defaultLandingPage && defaultLandingPage !== '/') { + return ; + } + return ; +} + function OwnerLayout() { const role = getCurrentUserRole(); if (isTechnicianRole(role)) { @@ -116,6 +132,7 @@ function RequireSetup({ children }: { children: React.ReactNode }) { const settings = await loadSettingsForUser(user?.id); if (!active) return; useSettingsStore.getState().setSettings(settings); + applySavedTheme(); setSetupReady(isSetupComplete(settings)); } @@ -190,7 +207,7 @@ function AppRoutes() { } /> } /> }> - } /> + } /> } /> } /> } /> @@ -224,6 +241,10 @@ function AppRoutes() { } function App() { + useEffect(() => { + applySavedTheme(); + }, []); + return ( diff --git a/src/components/ROForm.tsx b/src/components/ROForm.tsx index b0c7786..ce56ed6 100644 --- a/src/components/ROForm.tsx +++ b/src/components/ROForm.tsx @@ -18,6 +18,8 @@ import { formatCurrency } from '../lib/format'; import { getStatusConfig, SERVICE_SUB_STATUS_CONFIG } from '../lib/status'; import { logError } from '../lib/userMessages'; import { composeName, parseName, findMatchingCustomer } from '../lib/customerName'; +import { formatCustomerNamePart, formatMileage, formatVehicleInfo, formatVin } from '../lib/dataFormatting'; +import { spellCheckProps } from '../lib/spellcheck'; import { pb } from '../lib/pocketbase'; import type { CustomerWithVehicles } from '../pages/Customers'; @@ -277,7 +279,7 @@ function ServiceRow({ export interface ROFormProps { editRO: RepairOrder | null; existingRoNumbers: Set; - settings: { taxRate: number; shopChargeRate: number; shopChargeCap: number; defaultLaborRate: number }; + settings: { taxEnabled?: boolean; taxRate: number; shopChargeRate: number; shopChargeCap: number; defaultLaborRate: number }; customers?: CustomerWithVehicles[]; // for customer matching / auto-create onSave: (data: Partial) => Promise | void; onCancel: () => void; @@ -336,6 +338,8 @@ export default function ROForm({ const [catalogSearch, setCatalogSearch] = useState(''); const [catalogLoaded, setCatalogLoaded] = useState(false); const userId = pb.authStore.model?.id; + const appSettings = useSettings(); + const internalNotesSpellCheck = spellCheckProps(appSettings, appSettings.spellCheckInternalNotes); const isEditing = !!editRO; @@ -459,18 +463,25 @@ export default function ROForm({ /* ── Customer matching (debounced) ─── */ const runMatching = useCallback(() => { - if (!customerId && !dismissedMatch) { - const match = findMatchingCustomer(customers, { - firstName, - middleName, - lastName, + if (appSettings.duplicateCustomerWarning === 'off') { + setSuggestedCustomer(null); + return; + } + if (!customerId && !dismissedMatch) { + const matchFirstName = appSettings.duplicateCustomerWarning === 'phone_only' ? '' : firstName; + const matchMiddleName = appSettings.duplicateCustomerWarning === 'phone_only' ? '' : middleName; + const matchLastName = appSettings.duplicateCustomerWarning === 'phone_only' ? '' : lastName; + const match = findMatchingCustomer(customers, { + firstName: matchFirstName, + middleName: matchMiddleName, + lastName: matchLastName, phone: customerPhone, - }); - setSuggestedCustomer(match); + }); + setSuggestedCustomer(match); } else { setSuggestedCustomer(null); } - }, [firstName, middleName, lastName, customerPhone, customerId, customers, dismissedMatch]); + }, [appSettings.duplicateCustomerWarning, firstName, middleName, lastName, customerPhone, customerId, customers, dismissedMatch]); useEffect(() => { if (isEditing) return; // don't match while editing an existing RO @@ -499,22 +510,29 @@ export default function ROForm({ }, []); /* ── Auto-create customer in PB ─── */ - const ensureCustomerRecord = useCallback(async (): Promise => { - if (customerId) return customerId; + const ensureCustomerRecord = useCallback(async (): Promise => { + if (customerId) return customerId; + if (!appSettings.autoCreateCustomerFromRO) return ''; - const uid = pb.authStore.model?.id; - if (!uid) return ''; + const uid = pb.authStore.model?.id; + if (!uid) return ''; - const name = composeName(firstName, middleName, lastName); - if (!name.trim()) return ''; + const formattedFirstName = formatCustomerNamePart(firstName, appSettings); + const formattedMiddleName = formatCustomerNamePart(middleName, appSettings); + const formattedLastName = formatCustomerNamePart(lastName, appSettings); + const name = composeName(formattedFirstName, formattedMiddleName, formattedLastName); + if (!name.trim()) return ''; - // Double-check for an existing customer - const existingMatch = findMatchingCustomer(customers, { - firstName, - middleName, - lastName, - phone: customerPhone, - }); + // Double-check for an existing customer + const matchFirstName = appSettings.duplicateCustomerWarning === 'phone_only' ? '' : firstName; + const matchMiddleName = appSettings.duplicateCustomerWarning === 'phone_only' ? '' : middleName; + const matchLastName = appSettings.duplicateCustomerWarning === 'phone_only' ? '' : lastName; + const existingMatch = findMatchingCustomer(customers, { + firstName: matchFirstName, + middleName: matchMiddleName, + lastName: matchLastName, + phone: customerPhone, + }); if (existingMatch) { setCustomerId(existingMatch.id); return existingMatch.id; @@ -535,7 +553,7 @@ export default function ROForm({ logError('Failed to create customer record', err); return ''; } - }, [customerId, firstName, middleName, lastName, customerPhone, customers]); + }, [appSettings, customerId, firstName, middleName, lastName, customerPhone, customers]); const handleUseExisting = handleUseExistingCustomer; const handleDismiss = handleDismissMatch; @@ -673,7 +691,10 @@ export default function ROForm({ }); const handleSubmit = async () => { - const name = composeName(firstName, middleName, lastName); + const formattedFirstName = formatCustomerNamePart(firstName, appSettings); + const formattedMiddleName = formatCustomerNamePart(middleName, appSettings); + const formattedLastName = formatCustomerNamePart(lastName, appSettings); + const name = composeName(formattedFirstName, formattedMiddleName, formattedLastName); if (!name.trim()) return; setSaving(true); try { @@ -704,9 +725,9 @@ export default function ROForm({ customerId: cId || customerId, customerName: name, customerPhone: customerPhone.trim(), - vehicleInfo: vehicleInfo.trim(), - vin: vin.trim(), - mileage: mileage.trim(), + vehicleInfo: formatVehicleInfo(vehicleInfo, appSettings), + vin: formatVin(vin, appSettings), + mileage: formatMileage(mileage, appSettings), advisorName: advisorName.trim(), roNumber: roNumber.trim() || uniqueRONumber(existingRoNumbers), notes: notes.trim(), @@ -734,7 +755,6 @@ export default function ROForm({ } }; - const appSettings = useSettings(); const defaultAdvisor = appSettings.serviceAdvisor; const roleLabel = getRoleLabel(appSettings); const customerTypeLabels = getCustomerTypeLabels(appSettings); @@ -1266,6 +1286,7 @@ export default function ROForm({ onChange={(e) => setNotes(e.target.value)} rows={3} placeholder="Customer concerns, technician instructions, etc." + {...internalNotesSpellCheck} className="mt-1 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" /> diff --git a/src/components/appointments/AppointmentModal.tsx b/src/components/appointments/AppointmentModal.tsx index 6c914bd..602efa7 100644 --- a/src/components/appointments/AppointmentModal.tsx +++ b/src/components/appointments/AppointmentModal.tsx @@ -7,6 +7,7 @@ import { formatPhoneInput } from '../../lib/phone'; import { logError } from '../../lib/userMessages'; import { isValidDate, todayISO } from '../../lib/appointments'; import { composeName, parseName, findMatchingCustomer } from '../../lib/customerName'; +import { spellCheckProps } from '../../lib/spellcheck'; import type { CustomerWithVehicles, CustomerFormData } from '../../pages/Customers'; import type { Appointment } from '../../types'; @@ -70,6 +71,7 @@ function AppointmentModalImpl({ customers, }: Props) { const setupSettings = useSettings(); + const customerNotesSpellCheck = spellCheckProps(setupSettings, setupSettings.spellCheckCustomerNotes); const roleLabel = getRoleLabel(setupSettings); const showLocationFields = showServiceLocationFields(setupSettings); const [form, setForm] = useState({ ...EMPTY_FORM, advisorName: loadSettings().serviceAdvisor }); @@ -530,7 +532,7 @@ function AppointmentModalImpl({
-