Files
mc_cars_gmbh_infraestructure/plans/admin-portal-rework.md
T
LagoESP 30e296f61b 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.
2026-04-29 17:27:37 +02:00

436 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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