feat(crm): frontend code — pages + services + components

Brings in the full frontend CRM stack: admin authoring pages,
customer-portal surfaces, public response flows, typed services,
and the supporting component library. i18n locale JSON is the next
commit (kept separate so reviewers can read it as data).

Pages
  - Quotes: list / editor / detail / public accept-decline
  - Invoices: list / editor / detail / public payment-check
  - Contracts: list / editor / detail / block library / public sign
  - Calendar (FullCalendar — admin-only v1)
  - Tax report (period picker + CSV/PDF export)
  - Hours (logged time entries, per-customer)
  - Deals lineage (DocumentLineageCard surfaces)
  - CRM Development (admin dev tools, gated by crmDevelopment flag)
  - Customer-portal pages for quotes / invoices / contracts
  - Settings reorg: CRM-Settings group + dedicated tabs for Business
    Profile, CRM behaviour, Contracts block library, Reminder emails
  - BrandingPage typography (PDF font picker)
  - EventDetailsPage / CustomerDetailPage / CreateEventPage extensions
    (event-time fields, hours toggle, per-event reminder override)

Services (typed)
  - quotes.service, bills.service, contracts.service
  - customerAdmin.service, deals.service, calendar.service,
    taxReport.service, contracts-blocks.service
  - businessProfile.service (timezone, font picker, bank accounts)
  - useInstallmentDefaults hook, useLocalizedDate dateInputLang extension

Components (admin)
  - CustomerPicker (shared across quote/invoice/contract editors)
  - LineItemsTable (hierarchy + details_text, memoised pricing)
  - InstallmentsPanel (simple + advanced toggle, fixed-date vs trigger)
  - DocumentLineageCard (deal_uuid grouped view)
  - EditInstallmentPlanModal (atomic post-spawn plan reshape)
  - EventReminderOverrideCard, EmailTemplateEditor (tiptap),
    PdfFontPicker, IntegrityCheckCard
  - Feature-flag context + RequireFeature wrapper + AdminSidebar
    featureFlagsAny derivation + UI-hiding sweep

Build infra
  - vite.config: fullcalendar chunk carved off (~200 KB lazy-loaded)
  - frontend/package.json: tiptap, fullcalendar, signature_pad,
    react-international-phone, et al.
  - tailwind + prose styles updated for editor surfaces

3-way merge note: 1 conflict (CustomerDetailPage.tsx) hand-resolved
to keep upstream's SUPPORTED_LANGUAGES.map() data-driven pattern
over feat/crm's hardcoded option list; feat/crm's DecimalInput
import preserved alongside.
This commit is contained in:
Luca
2026-05-26 18:19:32 +02:00
parent d543949188
commit a7e16e7bf6
96 changed files with 18370 additions and 572 deletions
+27 -2
View File
@@ -14,12 +14,28 @@
*/
import React, { createContext, useContext, useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import i18n from '../i18n/config';
import { customerService, type CustomerProfile } from '../services/customer.service';
/**
* Apply a customer's preferred language to the portal UI. The admin-set
* `preferred_language` already drives PDF rendering and queued email
* locale resolution; this closes the third surface so the portal UI
* actually honours the same setting. Swallows errors because i18n init
* can race with the auth flow — failing to switch language must never
* break login. Pattern lifted from QuoteResponsePage.tsx.
*/
function applyCustomerLocale(lang?: string | null) {
if (!lang) return;
if (lang === i18n.language) return;
i18n.changeLanguage(lang).catch(() => {});
}
export interface CustomerFeatureFlags {
calendar: boolean;
quotes: boolean;
bills: boolean;
contracts: boolean;
}
export interface CustomerBrandingFlags {
@@ -61,7 +77,7 @@ const STORAGE_KEY = 'customer_profile';
const FEATURES_KEY = 'customer_features';
const BRANDING_KEY = 'customer_branding';
const DEFAULT_FEATURES: CustomerFeatureFlags = { calendar: false, quotes: false, bills: false };
const DEFAULT_FEATURES: CustomerFeatureFlags = { calendar: false, quotes: false, bills: false, contracts: false };
const DEFAULT_BRANDING: CustomerBrandingFlags = { showLogo: true, showCompanyName: true };
interface ProviderProps { children: ReactNode; }
@@ -113,6 +129,7 @@ export const CustomerAuthProvider: React.FC<ProviderProps> = ({ children }) => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(response.customer));
sessionStorage.setItem(FEATURES_KEY, JSON.stringify(response.features));
sessionStorage.setItem(BRANDING_KEY, JSON.stringify(response.branding));
applyCustomerLocale(response.customer.preferredLanguage);
} else {
// Explicit 401 — server says no.
setCustomerState(null);
@@ -128,7 +145,13 @@ export const CustomerAuthProvider: React.FC<ProviderProps> = ({ children }) => {
// cookie is still valid and overwrites stale data.
try {
const cached = sessionStorage.getItem(STORAGE_KEY);
if (cached) setCustomerState(JSON.parse(cached));
if (cached) {
const parsed = JSON.parse(cached) as CustomerProfile;
setCustomerState(parsed);
// Apply the cached locale immediately so a hard refresh doesn't
// flash the default language before refreshSession lands.
applyCustomerLocale(parsed.preferredLanguage);
}
const cachedFeatures = sessionStorage.getItem(FEATURES_KEY);
if (cachedFeatures) setFeatures(JSON.parse(cachedFeatures));
const cachedBranding = sessionStorage.getItem(BRANDING_KEY);
@@ -174,6 +197,7 @@ export const CustomerAuthProvider: React.FC<ProviderProps> = ({ children }) => {
const setCustomer = (c: CustomerProfile) => {
setCustomerState(c);
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(c));
applyCustomerLocale(c.preferredLanguage);
};
const setSession = (s: { customer: CustomerProfile; features: CustomerFeatureFlags; branding: CustomerBrandingFlags }) => {
@@ -183,6 +207,7 @@ export const CustomerAuthProvider: React.FC<ProviderProps> = ({ children }) => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(s.customer));
sessionStorage.setItem(FEATURES_KEY, JSON.stringify(s.features));
sessionStorage.setItem(BRANDING_KEY, JSON.stringify(s.branding));
applyCustomerLocale(s.customer.preferredLanguage);
};
const logout = async () => {
+28 -2
View File
@@ -11,7 +11,11 @@ export type { FeatureKey, FeatureFlags };
// hasn't run its migration yet on this instance).
export const DEFAULT_FLAGS: FeatureFlags = {
galleries: true,
reminderEmails: true,
// F.3 — these three are placeholders. The UI cards are locked
// (lockedReason: NOT_YET_AVAILABLE) because the underlying flows
// aren't implemented yet. Default FALSE so the toggle isn't
// confusingly "on but locked".
reminderEmails: false,
calendar: false,
calendarBooking: false,
quotes: false,
@@ -28,6 +32,19 @@ export const DEFAULT_FLAGS: FeatureFlags = {
// logins are opt-in. Migration 095 flips this to TRUE on existing
// installs (events>0).
customerPortal: false,
// CRM developer tools sub-tab. Strictly opt-in.
crmDevelopment: false,
// Tax / Steuer report sub-tab. Independent toggle; forced off when
// `bills` is off (no invoices → nothing to report).
taxReport: false,
// Hours logging master (migration 129). Off by default — admin
// enables in Settings → Features once they're ready to surface the
// per-customer Hours card.
hoursLogging: false,
// Contracts (migration 130). Off by default — admin enables in
// Settings → Features once they've reviewed the seeded block
// library with their lawyer.
contracts: false,
};
export const FEATURE_FLAGS_QUERY_KEY = ['feature-flags'] as const;
@@ -59,6 +76,7 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
out.galleries = true; // foundation — always on
if (out.quotes === false) out.bills = false; // bills depend on quotes
if (out.calendar === false) out.calendarBooking = false; // booking depends on calendar
if (out.bills === false) out.taxReport = false; // tax report depends on bills
// Clients parent flag is DERIVED from its children. Admins don't
// toggle it directly — enabling any CRM-area sub-feature
// (Accounts today; future Calendar / Quotes / Bills / Messaging)
@@ -66,7 +84,15 @@ function applyDependencyRules(flags: FeatureFlags): FeatureFlags {
// disabling all of them hides it again.
out.clients = Boolean(
out.customerPortal
// future siblings: || out.calendar || out.quotes || out.bills || out.messaging
|| out.crmDevelopment
|| out.quotes
|| out.bills
|| out.taxReport
|| out.hoursLogging
|| out.contracts
// Migration 137 — admin calendar lights up the Clients section.
|| out.calendar
// future siblings: || out.messaging
);
return out;
}