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.
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
/**
|
|
* Admin calendar API client.
|
|
*
|
|
* Wraps the single aggregate endpoint
|
|
* GET /api/admin/calendar/items?from=YYYY-MM-DD&to=YYYY-MM-DD
|
|
* exposed by `backend/src/routes/adminCalendar.js` (E.3).
|
|
*
|
|
* The response is a discriminated union on `kind` so the calendar page
|
|
* can switch render style per item type without juggling four separate
|
|
* react-query subscriptions.
|
|
*
|
|
* Hour-entry mutations stay on the existing customerAdmin.service —
|
|
* the calendar's drag-create / inline-edit modals call those directly.
|
|
*/
|
|
|
|
import { api } from '../config/api';
|
|
|
|
export type CalendarItemKind = 'event' | 'hours' | 'quote' | 'contract';
|
|
|
|
interface CalendarItemBase {
|
|
kind: CalendarItemKind;
|
|
customerName: string | null;
|
|
}
|
|
|
|
export interface CalendarEventItem extends CalendarItemBase {
|
|
kind: 'event';
|
|
id: number;
|
|
slug: string;
|
|
eventName: string;
|
|
eventDate: string;
|
|
eventTimeStart: string | null;
|
|
eventTimeEnd: string | null;
|
|
isFullDay: boolean;
|
|
}
|
|
|
|
export interface CalendarHoursItem extends CalendarItemBase {
|
|
kind: 'hours';
|
|
id: number;
|
|
customerAccountId: number;
|
|
entryDate: string;
|
|
startTime: string;
|
|
endTime: string;
|
|
description: string | null;
|
|
status: 'unbilled' | 'billed' | 'cancelled';
|
|
invoiceId: number | null;
|
|
invoiceStatus: string | null;
|
|
/**
|
|
* True when this entry's invoice has been armed for send (monthly
|
|
* draft cleared + scheduled_send_at set, OR status sent/paid/cancelled).
|
|
* Backend computes this via customerHoursService._internal.isEntryLocked.
|
|
* Drives the calendar's locked badge + edit-disabled state.
|
|
*/
|
|
locked: boolean;
|
|
}
|
|
|
|
export interface CalendarQuoteItem extends CalendarItemBase {
|
|
kind: 'quote';
|
|
id: number;
|
|
quoteNumber: string;
|
|
eventName: string | null;
|
|
eventDate: string;
|
|
eventTimeStart: string | null;
|
|
eventTimeEnd: string | null;
|
|
status: 'sent' | 'accepted';
|
|
}
|
|
|
|
export interface CalendarContractItem extends CalendarItemBase {
|
|
kind: 'contract';
|
|
id: number;
|
|
contractNumber: string;
|
|
eventName: string | null;
|
|
eventDate: string;
|
|
eventTimeStart: string | null;
|
|
eventTimeEnd: string | null;
|
|
status: 'signed_by_customer' | 'fully_signed';
|
|
}
|
|
|
|
export type CalendarItem =
|
|
| CalendarEventItem
|
|
| CalendarHoursItem
|
|
| CalendarQuoteItem
|
|
| CalendarContractItem;
|
|
|
|
export interface CalendarItemsResponse {
|
|
items: CalendarItem[];
|
|
range: { from: string; to: string };
|
|
}
|
|
|
|
export const calendarService = {
|
|
/**
|
|
* Fetch all four layers (events, hours, pending quotes, pending
|
|
* contracts) for the supplied date range. Both ends are inclusive,
|
|
* ISO YYYY-MM-DD. Backend caps the range at 90 days.
|
|
*/
|
|
async list(params: { from: string; to: string }): Promise<CalendarItemsResponse> {
|
|
const { data } = await api.get('/admin/calendar/items', { params });
|
|
return data.data || data;
|
|
},
|
|
};
|