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:
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user