# Admin Portal Rework — Plan ## Goal Rework the Admin Portal to show: 1. **Full pricing breakdown** on every Lead (captured at booking time) 2. **Documents tab** inside Lead detail (view/download uploaded ID & income proof) 3. **Enhanced Customer detail** with Documents tab + Order History (all leads merged via email) 4. **UI polish** — integrate all new features smoothly into the existing dark theme --- ## Architecture Overview ```mermaid erDiagram leads { uuid id PK text name text email text phone uuid vehicle_id FK text vehicle_label date date_from date date_to text message text status boolean is_active text admin_notes text source timestamptz created_at timestamptz updated_at timestamptz qualified_at uuid qualified_by FK integer daily_subtotal integer weekend_subtotal integer subtotal_eur integer vat_eur integer total_eur integer deposit_eur integer total_days integer weekday_count integer weekend_day_count } lead_attachments { uuid id PK uuid lead_id FK text bucket text file_path text file_name text mime_type text kind timestamptz created_at } customers { uuid id PK uuid lead_id FK text name text email text phone timestamptz first_contacted_at text notes text status timestamptz created_at timestamptz updated_at uuid created_by FK } customer_attachments { uuid id PK uuid customer_id FK uuid lead_id FK text bucket text file_path text file_name text mime_type text kind timestamptz created_at } vehicles { uuid id PK text brand text model integer daily_price_eur integer weekend_price_eur integer kaution_eur integer max_daily_km integer max_km_weekend } leads }o--o{ lead_attachments : "has" leads }o--o{ customers : "becomes" customers }o--o{ customer_attachments : "has" customers }o--o{ customers : "order history (same email)" leads }o--o{ vehicles : "references" ``` --- ## Migration Plan ### New Migration: `06-admin-pricing-documents.sql` #### 1. Add pricing columns to `leads` table ```sql alter table public.leads add column if not exists daily_subtotal integer not null default 0; alter table public.leads add column if not exists weekend_subtotal integer not null default 0; alter table public.leads add column if not exists subtotal_eur integer not null default 0; alter table public.leads add column if not exists vat_eur integer not null default 0; alter table public.leads add column if not exists total_eur integer not null default 0; alter table public.leads add column if not exists deposit_eur integer not null default 0; alter table public.leads add column if not exists total_days integer not null default 0; alter table public.leads add column if not exists weekday_count integer not null default 0; alter table public.leads add column if not exists weekend_day_count integer not null default 0; ``` These columns capture a **snapshot** of the pricing at booking time. They are never modified after creation. #### 2. Update `create_lead` RPC (in `05-create-lead-rpc.sql`) The RPC needs to accept optional pricing parameters and store them: ```sql create or replace function public.create_lead( p_name text, p_email text, p_phone text default '', p_vehicle_id uuid default null, p_vehicle_label text default '', p_date_from date default null, p_date_to date default null, p_message text default '', p_source text default 'website', -- New pricing snapshot params p_daily_subtotal integer default 0, p_weekend_subtotal integer default 0, p_subtotal_eur integer default 0, p_vat_eur integer default 0, p_total_eur integer default 0, p_deposit_eur integer default 0, p_total_days integer default 0, p_weekday_count integer default 0, p_weekend_day_count integer default 0 ) ``` #### 3. Update frontend `app.js` — pass pricing to RPC In the existing `bpfSubmit` handler ([`app.js:405`](frontend/app.js:405)), before calling `create_lead`, compute the pricing (already done in `updateSidebar`) and pass it as additional payload: ```js const payload = { p_name: bpfName.value, p_email: bpfEmail.value, // ... existing fields ... // NEW: pricing snapshot p_daily_subtotal: weekdayCost, p_weekend_subtotal: weekendCost, p_subtotal_eur: subtotal, p_vat_eur: vat, p_total_eur: total, p_deposit_eur: deposit, p_total_days: totalDays, p_weekday_count: weekdays, p_weekend_day_count: weekendDays, }; ``` --- ## UI Changes ### A. Lead Table — new columns The leads table ([`admin.html:90`](frontend/admin.html:90)) gains two new columns: | Current | New | |---------|-----| | Eingang | — | | Name / E-Mail | — | | Fahrzeug | — | | Zeitraum | — | | **Preis Gesamt (€)** | ← NEW | | Status | — | | (actions) | — | Reordered columns: `Eingang → Name/E-Mail → Fahrzeug → Zeitraum → Preis Gesamt → Status → Aktionen` ### B. Lead Detail Dialog — full redesign Replace the current simple `dl.kv` key-value list with a **tabbed dialog**: ``` ┌─────────────────────────────────────────────┐ │ John Doe · new [×] │ ├─────────────────────────────────────────────┤ │ [Allgemein] [Preise] [Dokumente] [Notiz] │ ├─────────────────────────────────────────────┤ │ │ │ Tab content... │ │ │ ├─────────────────────────────────────────────┤ │ [Ablehnen] [Qualifizieren] │ └─────────────────────────────────────────────┘ ``` #### Tab: "Allgemein" (General) - Name, E-Mail, Telefon - Fahrzeug (brand + model) - Zeitraum (date_from → date_to, total_days) - Nachricht (message, multiline) - Quelle (source) - Eingang (created_at) #### Tab: "Preise" (Pricing) ``` ┌──────────────────────────────────────┐ │ Tagesmiete (3 × € 850) € 2.550│ │ Wochenendmiete (0 × € 0) € 0│ │ ─────────────────────────────────── │ │ Zwischensumme € 2.550│ │ MwSt. (20%) € 510│ │ ─────────────────────────────────── │ │ Gesamtbetrag € 3.060│ │ │ │ Kaution € 5.000│ │ Inkl. km 450 km │ └──────────────────────────────────────┘ ``` #### Tab: "Dokumente" (Documents) - List all `lead_attachments` for this lead - Each row: document kind icon, file name, MIME type, upload date, **Download** button - Download link uses Supabase `getPublicUrl` for `customer-documents` bucket (private bucket, requires authenticated JWT) - If no documents: show "Keine Dokumente hochgeladen" #### Tab: "Notiz" (Notes) - `admin_notes` textarea (editable) - Save button to update via `supabase.from("leads").update({ admin_notes })` ### C. Customer Detail — new dialog Currently customers have **no detail dialog**. Add one: ``` ┌─────────────────────────────────────────────┐ │ John Doe · active [×] │ ├─────────────────────────────────────────────┤ │ [Info] [Dokumente] [Order History] │ ├─────────────────────────────────────────────┤ │ │ │ Tab content... │ │ │ ├─────────────────────────────────────────────┤ │ [Inaktiv setzen] │ └─────────────────────────────────────────────┘ ``` #### Tab: "Info" - Name, E-Mail, Telefon - Erster Kontakt (first_contacted_at) - Status (active/inactive toggle) - Notizen (notes, editable) #### Tab: "Dokumente" - List all `customer_attachments` for this customer - Each row: document kind icon, file name, MIME type, upload date, **Download** button - Also show any documents inherited from the original lead (via `lead_id` join) #### Tab: "Order History" - Query: `select * from leads where lower(email) = lower(customer.email) order by created_at desc` - Table: Eingang → Fahrzeug → Zeitraum → Gesamtbetrag → Status - Footer: **Gesamtwert aller Buchungen** (sum of total_eur) ### D. Customer Table — new column Add `Gesamtwert` (total lifetime value) column: | Current | New | |---------|-----| | Erster Kontakt | — | | Name / E-Mail | — | | Telefon | — | | Quelle (Lead) | — | | **Gesamtwert** | ← NEW | | Status | — | | (actions) | — | Compute by summing `total_eur` from all associated leads (via email match). --- ## i18n Additions Add to [`frontend/i18n.js`](frontend/i18n.js) translations object (both `de` and `en`): ```js // Lead table adminTotalPrice: "Gesamtbetrag", adminTotalPriceEn: "Total", // Lead dialog tabs adminTabGeneral: "Allgemein", adminTabGeneralEn: "General", adminTabPricing: "Preise", adminTabPricingEn: "Pricing", adminTabDocuments: "Dokumente", adminTabDocumentsEn: "Documents", adminTabNotes: "Notiz", adminTabNotesEn: "Notes", // Pricing tab adminWeekdays: "Tagesmiete", adminWeekdaysEn: "Weekday rate", adminWeekendRateLabel: "Wochenendmiete", adminWeekendRateLabelEn: "Weekend rate", adminSubtotalLabel: "Zwischensumme", adminSubtotalLabelEn: "Subtotal", adminVatLabel: "MwSt. (20%)", adminVatLabelEn: "VAT (20%)", adminTotalLabel: "Gesamtbetrag", adminTotalLabelEn: "Total", adminDepositLabel: "Kaution", adminDepositLabelEn: "Deposit", adminIncludedKmLabel: "Inkl. km", adminIncludedKmLabelEn: "Included km", adminTotalDaysLabel: "Tage gesamt", adminTotalDaysLabelEn: "Total days", // Documents tab adminDownload: "Download", adminDownloadEn: "Download", adminNoDocuments: "Keine Dokumente hochgeladen", adminNoDocumentsEn: "No documents uploaded", adminIdDoc: "Ausweis / Führerschein", adminIdDocEn: "ID / Driving license", adminIncomeDoc: "Lohnzettel", adminIncomeDocEn: "Pay slip", adminOtherDoc: "Sonstiges", adminOtherDocEn: "Other", // Customer dialog adminTabOrderHistory: "Order History", adminTabOrderHistoryEn: "Order History", adminLifetimeValue: "Gesamtwert aller Buchungen", adminLifetimeValueEn: "Lifetime value", adminFirstContacted: "Erster Kontakt", adminFirstContactedEn: "First contacted", // Customer table adminLifetimeValueCol: "Gesamtwert", adminLifetimeValueColEn: "Lifetime", ``` --- ## File Change Summary | File | Change | |------|--------| | `supabase/migrations/06-admin-pricing-documents.sql` | **NEW** — pricing columns, updated `create_lead` RPC | | `supabase/migrations/post-boot.sql` | Append `06-admin-pricing-documents.sql` to entrypoint command | | `docker-compose.yml` | Mount new migration file in `post-init` service | | `frontend/app.js` | Pass pricing snapshot in `create_lead` RPC call | | `frontend/admin.html` | Add customer detail dialog, update lead dialog structure with tabs | | `frontend/admin.js` | Rewrite `openLead()` with tabbed dialog, add `openCustomer()` dialog, add document rendering, add order history query, update `renderCustomers()` with lifetime value | | `frontend/styles.css` | Add tab styles for lead/customer dialogs, document list styles, pricing card styles | | `frontend/i18n.js` | Add all new translation keys | --- ## Implementation Order 1. **Database migration** (`06-admin-pricing-documents.sql`) — add columns, update RPC 2. **docker-compose.yml** — mount new migration 3. **frontend/app.js** — pass pricing snapshot at booking time 4. **frontend/i18n.js** — add translation keys 5. **frontend/admin.html** — add tabbed dialog HTML structures 6. **frontend/admin.js** — rewrite `openLead()`, add `openCustomer()`, document rendering, order history 7. **frontend/styles.css** — add tab, document list, pricing card styles --- ## Diagram: Lead Detail Dialog Flow ```mermaid stateDiagram-v2 [*] --> LeadTableClick LeadTableClick --> LeadDialogOpen LeadDialogOpen --> TabGeneral LeadDialogOpen --> TabPricing LeadDialogOpen --> TabDocuments LeadDialogOpen --> TabNotes TabGeneral --> LeadDialogClose TabPricing --> LeadDialogClose TabDocuments --> LeadDialogClose TabNotes --> TabNotesSave TabNotesSave --> LeadDialogClose TabNotesSave --> TabNotes LeadDialogClose --> [*] ``` ## Diagram: Customer Qualification Flow (updated) ```mermaid sequenceDiagram participant Admin participant AdminUI participant RPC participant DB participant Storage Admin->>AdminUI: Click "Qualifizieren" AdminUI->>RPC: qualify_lead(lead_id, notes) RPC->>DB: Mark lead qualified + inactive RPC->>DB: Upsert customer by email RPC->>DB: Transfer lead_attachments to customer_attachments DB-->>RPC: customer row RPC-->>AdminUI: customer AdminUI->>DB: Reload leads + customers (realtime) AdminUI->>AdminUI: Refresh tables ``` --- ## Notes - All migrations are **idempotent** (`add column if not exists`, `create or replace function`) - Existing leads will have `0` for all new pricing columns — a one-time backfill RPC can be added later if needed to retroactively compute prices for historical leads - Documents in `customer-documents` bucket are **private** — admin must be authenticated to download (Supabase JWT handles this) - The `lead_attachments` and `customer_attachments` tables already exist from migration `03-booking-flow.sql` — no schema changes needed there