feat: rework admin portal with pricing breakdown and document management

- Added pricing snapshot columns to the leads table: daily_subtotal, weekend_subtotal, subtotal_eur, vat_eur, total_eur, deposit_eur, total_days, weekday_count, weekend_day_count.
- Updated create_lead RPC to accept and store new pricing parameters.
- Enhanced frontend app.js to compute and send pricing details during lead creation.
- Introduced new UI elements in admin.html for displaying pricing and documents in a tabbed dialog.
- Updated i18n.js with new translation keys for pricing and document management.
- Improved styles in styles.css for new dialog components and pricing cards.
- Added migration script 06-admin-pricing-documents.sql for database schema changes.
This commit is contained in:
LagoESP
2026-04-29 17:27:37 +02:00
parent bc61ffa206
commit 30e296f61b
9 changed files with 1187 additions and 40 deletions
+435
View File
@@ -0,0 +1,435 @@
# 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