- CustomerList, CustomerForm, CustomerDetail: various updates - Orders: removed OrderDetail and OrderForm, updated OrderList and index - DeviceDetail: updates - index.css: added new styles - CRM_STATUS_SYSTEM_PLAN.md: new planning document - Added customer-status assets and CustomerDetail subfolder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
405 lines
18 KiB
Markdown
405 lines
18 KiB
Markdown
# CRM Customer Status System — Implementation Plan
|
||
|
||
## Context
|
||
|
||
This project is a Vue/React + FastAPI + Firestore admin console located at `C:\development\bellsystems-cp`.
|
||
|
||
The frontend lives in `frontend/src/` and the backend in `backend/`.
|
||
The CRM module is at `frontend/src/crm/` and `backend/crm/`.
|
||
|
||
Currently, customers have two flat boolean flags on their Firestore document:
|
||
- `negotiating: bool`
|
||
- `has_problem: bool`
|
||
|
||
These need to be replaced with a richer, structured system as described below.
|
||
|
||
---
|
||
|
||
## 1. Target Data Model
|
||
|
||
### 1A. On the Customer Document (`customers/{id}`)
|
||
|
||
Remove `negotiating` and `has_problem`. Add the following:
|
||
|
||
```
|
||
relationship_status: string
|
||
— one of: "lead" | "prospect" | "active" | "inactive" | "churned"
|
||
— default: "lead"
|
||
|
||
technical_issues: array of {
|
||
active: bool,
|
||
opened_date: Firestore Timestamp,
|
||
resolved_date: Firestore Timestamp | null,
|
||
note: string,
|
||
opened_by: string, ← display name or user ID of staff member
|
||
resolved_by: string | null
|
||
}
|
||
|
||
install_support: array of {
|
||
active: bool,
|
||
opened_date: Firestore Timestamp,
|
||
resolved_date: Firestore Timestamp | null,
|
||
note: string,
|
||
opened_by: string,
|
||
resolved_by: string | null
|
||
}
|
||
|
||
transaction_history: array of {
|
||
date: Firestore Timestamp,
|
||
flow: string, ← "invoice" | "payment" | "refund" | "credit"
|
||
payment_type: string | null, ← "cash" | "bank_transfer" | "card" | "paypal" — null for invoices
|
||
category: string, ← "full_payment" | "advance" | "installment"
|
||
amount: number,
|
||
currency: string, ← default "EUR"
|
||
invoice_ref: string | null,
|
||
order_ref: string | null, ← references an order document ID, nullable
|
||
recorded_by: string,
|
||
note: string
|
||
}
|
||
```
|
||
|
||
### 1B. Orders Subcollection (`customers/{id}/orders/{order_id}`)
|
||
|
||
Orders live **exclusively** as a subcollection under each customer. There is no top-level `orders`
|
||
collection. The existing top-level `orders` collection in Firestore and its corresponding backend
|
||
routes should be **removed entirely** and replaced with subcollection-based routes under
|
||
`/crm/customers/{customer_id}/orders/`.
|
||
|
||
If cross-customer order querying is ever needed in the future, use Firestore's native
|
||
`collectionGroup("orders")` query — no top-level mirror collection is required.
|
||
|
||
Each order document carries the following fields:
|
||
|
||
```
|
||
order_number: string ← e.g. "ORD-2026-041" (already exists — keep)
|
||
title: string ← NEW: human-readable name e.g. "3x Wall Mount Units - Athens Office"
|
||
created_by: string ← NEW: staff user ID or display name
|
||
|
||
status: string ← REPLACE existing OrderStatus enum with new values:
|
||
— "negotiating" | "awaiting_quotation" | "awaiting_customer_confirmation"
|
||
| "awaiting_fulfilment" | "awaiting_payment" | "manufacturing"
|
||
| "shipped" | "installed" | "declined" | "complete"
|
||
|
||
status_updated_date: Firestore Timestamp ← NEW
|
||
status_updated_by: string ← NEW
|
||
|
||
payment_status: object { ← NEW — replaces the flat PaymentStatus enum
|
||
required_amount: number,
|
||
received_amount: number, ← computed from transaction_history where order_ref matches
|
||
balance_due: number, ← computed: required_amount - received_amount
|
||
advance_required: bool,
|
||
advance_amount: number | null,
|
||
payment_complete: bool
|
||
}
|
||
|
||
timeline: array of { ← NEW — order event log
|
||
date: Firestore Timestamp,
|
||
type: string, ← "quote_request" | "quote_sent" | "quote_accepted" | "quote_declined"
|
||
| "mfg_started" | "mfg_complete" | "order_shipped" | "installed"
|
||
| "payment_received" | "invoice_sent" | "note"
|
||
note: string,
|
||
updated_by: string
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Backend Changes
|
||
|
||
### 2A. `backend/crm/models.py`
|
||
|
||
- **Remove** `negotiating: bool` and `has_problem: bool` from `CustomerCreate` and `CustomerUpdate`.
|
||
- **Add** `relationship_status: Optional[str] = "lead"` to `CustomerCreate` and `CustomerUpdate`.
|
||
- **Add** `technical_issues: List[dict] = []` to `CustomerCreate` and `CustomerUpdate`.
|
||
- **Add** `install_support: List[dict] = []` to `CustomerCreate` and `CustomerUpdate`.
|
||
- **Add** `transaction_history: List[dict] = []` to `CustomerCreate` and `CustomerUpdate`.
|
||
- **Add** proper Pydantic models for each of the above array item shapes:
|
||
- `TechnicalIssue` model
|
||
- `InstallSupportEntry` model
|
||
- `TransactionEntry` model
|
||
- **Update** `OrderStatus` enum with the new values:
|
||
`negotiating`, `awaiting_quotation`, `awaiting_customer_confirmation`,
|
||
`awaiting_fulfilment`, `awaiting_payment`, `manufacturing`,
|
||
`shipped`, `installed`, `declined`, `complete`
|
||
- **Replace** the flat `PaymentStatus` enum on `OrderCreate` / `OrderUpdate` with a new `OrderPaymentStatus` Pydantic model matching the structure above.
|
||
- **Add** `title: Optional[str]`, `created_by: Optional[str]`, `status_updated_date: Optional[str]`,
|
||
`status_updated_by: Optional[str]`, and `timeline: List[dict] = []` to `OrderCreate` and `OrderUpdate`.
|
||
|
||
### 2B. `backend/crm/customers_router.py`
|
||
|
||
- Update any route that reads/writes `negotiating` or `has_problem` to use the new fields.
|
||
- Add new dedicated endpoints:
|
||
|
||
```
|
||
POST /crm/customers/{id}/technical-issues
|
||
— body: { note: str, opened_by: str }
|
||
— appends a new active issue to the array
|
||
|
||
PATCH /crm/customers/{id}/technical-issues/{index}/resolve
|
||
— body: { resolved_by: str }
|
||
— sets active=false and resolved_date=now on the item at that index
|
||
|
||
POST /crm/customers/{id}/install-support
|
||
— same pattern as technical-issues above
|
||
|
||
PATCH /crm/customers/{id}/install-support/{index}/resolve
|
||
— same as technical-issues resolve
|
||
|
||
POST /crm/customers/{id}/transactions
|
||
— body: TransactionEntry (see model above)
|
||
— appends to transaction_history
|
||
|
||
PATCH /crm/customers/{id}/relationship-status
|
||
— body: { status: str }
|
||
— updates relationship_status field
|
||
```
|
||
|
||
### 2C. `backend/crm/orders_router.py`
|
||
|
||
- **Remove** all top-level `/crm/orders/` routes entirely.
|
||
- Re-implement all order CRUD under `/crm/customers/{customer_id}/orders/`:
|
||
|
||
```
|
||
GET /crm/customers/{customer_id}/orders/
|
||
POST /crm/customers/{customer_id}/orders/
|
||
GET /crm/customers/{customer_id}/orders/{order_id}
|
||
PATCH /crm/customers/{customer_id}/orders/{order_id}
|
||
DELETE /crm/customers/{customer_id}/orders/{order_id}
|
||
```
|
||
|
||
- Add endpoint to append a timeline event:
|
||
|
||
```
|
||
POST /crm/customers/{customer_id}/orders/{order_id}/timeline
|
||
— body: { type: str, note: str, updated_by: str }
|
||
— appends to the timeline array and updates status_updated_date + status_updated_by
|
||
```
|
||
|
||
- Add endpoint to update payment status:
|
||
|
||
```
|
||
PATCH /crm/customers/{customer_id}/orders/{order_id}/payment-status
|
||
— body: OrderPaymentStatus fields (partial update allowed)
|
||
```
|
||
|
||
- Add a dedicated "Init Negotiations" endpoint:
|
||
|
||
```
|
||
POST /crm/customers/{customer_id}/orders/init-negotiations
|
||
— body: { title: str, note: str, date: datetime, created_by: str }
|
||
— creates a new order with status="negotiating", auto-fills all other fields
|
||
— simultaneously updates the customer's relationship_status to "active"
|
||
(only if currently "lead" or "prospect" — do not downgrade an already "active" customer)
|
||
— returns the newly created order document
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Frontend Changes
|
||
|
||
### 3A. `frontend/src/crm/customers/CustomerList.jsx`
|
||
|
||
- When Notes: Quick filter is set, replace the `negotiating` and `has_problem` boolean badge display in the Status column with:
|
||
- A **relationship status chip** (color-coded pill: lead=grey, prospect=blue, active=green, inactive=amber, churned=soft red)
|
||
- A small **red dot / warning icon** if `technical_issues.some(i => i.active)` is true, under a new "Support" column. Add this column to the list of arrangeable and toggleable columns.
|
||
- A small **amber dot / support icon** if `install_support.some(i => i.active)` is true, under the same "Support" column.
|
||
- These are derived from the arrays — do not store a separate boolean on the document.
|
||
- When Notes: Expanded filter is set, replace the `negotiating` and `has_problem` verbose displays with the active order status (if any) in this format:
|
||
`"<Status Label> — <Date> — <Note>"` e.g. `"Negotiating — 24.03.26 — Customer requested a more affordable quotation"`
|
||
|
||
### 3B. `frontend/src/crm/customers/CustomerDetail.jsx`
|
||
|
||
The customer detail page currently has a tab structure: Overview, Orders, Quotations, Communication, Files & Media, Devices.
|
||
|
||
Make the following changes:
|
||
|
||
#### Whole page
|
||
- On the top of the page where we display the name, organization and full address, change it to:
|
||
Line 1: `Full Title + Name + Surname`
|
||
Line 2: `Organization · City` (city only, not full address)
|
||
- Remove the horizontal separation line after the title and before the tabs.
|
||
- On the top right side, there is an Edit Customer button. To its left, add **3 new buttons** in this
|
||
order (left → right): **Init Negotiations**, **Record Issue/Support**, **Record Payment**, then
|
||
the existing Edit button. All 4 buttons are the same size. Add solid single-color icons to each.
|
||
|
||
**"Init Negotiations" button** (blue/indigo accent):
|
||
- Opens a mini modal.
|
||
- Fields: Date (defaults to NOW), Title (text input, required), Note (textarea, optional).
|
||
- Auto-filled server-side: `status = "negotiating"`, `created_by` = current user,
|
||
`status_updated_date` = now, `status_updated_by` = current user,
|
||
`payment_status` defaults to zeroed object.
|
||
- On confirm: calls `POST /crm/customers/{id}/orders/init-negotiations`.
|
||
- After success: refreshes customer data and orders list. The customer's `relationship_status`
|
||
is set to `"active"` server-side — no separate frontend call needed.
|
||
- This is a fast-entry shortcut only. All subsequent edits to this order happen via the Orders tab.
|
||
|
||
**"Record Issue/Support" button** (amber/orange accent):
|
||
- Opens a mini modal.
|
||
- At the top: a **2-button toggle selector** (not a dropdown) to choose: `Technical Issue` | `Install Support`.
|
||
- Fields: Date (defaults to NOW), Note (textarea, required).
|
||
- On confirm: calls `POST /crm/customers/{id}/technical-issues` or
|
||
`POST /crm/customers/{id}/install-support` depending on selection.
|
||
|
||
**"Record Payment" button** (green accent):
|
||
- Opens a mini modal.
|
||
- Fields: Date (defaults to NOW), Payment Type (cash | bank transfer | card | paypal),
|
||
Category (full payment | advance | installment), Amount (number), Currency (defaults to EUR),
|
||
Invoice Ref (searchable over the customer's invoices, optional),
|
||
Order Ref (searchable/selectable from the customer's orders, optional),
|
||
Note (textarea, optional).
|
||
- On confirm: calls `POST /crm/customers/{id}/transactions`.
|
||
|
||
#### Overview Tab
|
||
- The main hero section gets a complete overhaul — start fresh:
|
||
- **Row 1 — Relationship Status selector**: The 5 statuses (`lead | prospect | active | inactive | churned`) as styled pill/tab buttons in a row. Current status is highlighted with a glow effect. Color-code using global CSS variables (add to `index.css` if not already present). Clicking a status immediately calls `PATCH /crm/customers/{id}/relationship-status`.
|
||
- **Row 2 — Customer info**: All fields except Name and Organization (shown in page header). Include language, religion, tags, etc.
|
||
- **Row 3 — Contacts**: All contact entries (phone, email, WhatsApp, etc.).
|
||
- **Row 4 — Notes**: Responsive column grid. 1 column below 1100px, 2 columns 1100–2000px, 3 columns above 2000px. Masonry/wrap layout with no gaps between note cards.
|
||
- Move the Latest Orders section to just below the hero section, before Latest Communications.
|
||
Hide this section entirely if no orders exist for this customer.
|
||
- For all other sections (Latest Communications, Latest Quotations, Devices): hide each section
|
||
entirely if it has no data. Show dynamically when data exists.
|
||
|
||
#### New "Support" Tab (add to TABS array, after Overview)
|
||
Two full-width section cards:
|
||
|
||
**Technical Issues Card**
|
||
- Header shows active count badge (e.g. "2 active")
|
||
- All issues listed, newest first (active and resolved)
|
||
- Each row: colored status dot, opened date, note, opened_by — "Resolve" button if active
|
||
- If more than 5 items: list is scrollable (fixed max-height), does not expand the page
|
||
- "Report New Issue" button → small inline form with note field + submit
|
||
|
||
**Install Support Card**
|
||
- Identical structure to Technical Issues card
|
||
- Same scrollable behavior if more than 5 items
|
||
|
||
#### New "Financials" Tab (add to TABS array, after Support)
|
||
Two sections:
|
||
|
||
**Active Order Payment Status** (shown only if an active order exists)
|
||
- required_amount, received_amount, balance_due
|
||
- Advance required indicator + advance amount if applicable
|
||
- Payment complete indicator
|
||
|
||
**Transaction History**
|
||
- Ledger table: Date | Flow | Amount | Currency | Method | Category | Order Ref | Invoice Ref | Note | Recorded By | Actions
|
||
- "Add Transaction" button → modal with all TransactionEntry fields
|
||
- Totals row: Total Invoiced vs Total Paid vs Outstanding Balance
|
||
- Each row: right-aligned **Actions** button (consistent with other tables in the project)
|
||
with options: **Edit** (opens edit form) and **Delete** (requires confirmation dialog)
|
||
|
||
#### Orders Tab (existing — update in place)
|
||
- Each order card/row shows:
|
||
- `title` as primary heading
|
||
- `status` with human-readable label and color coding (see Section 4)
|
||
- `payment_status` summary: required / received / balance due
|
||
- **"View Timeline"** toggle: expands a vertical event log below the order card
|
||
- **"Add Timeline Event"** button: small inline form with type dropdown + note field
|
||
- Update all API calls to use `/crm/customers/{customer_id}/orders/` routes.
|
||
|
||
### 3C. `frontend/src/crm/customers/CustomerForm.jsx`
|
||
|
||
- Remove `negotiating` and `has_problem` fields.
|
||
- Add `relationship_status` dropdown (default: `"lead"`).
|
||
- No issue/transaction forms needed here — managed from the detail page.
|
||
|
||
### 3D. `frontend/src/crm/orders/OrderForm.jsx` and `OrderDetail.jsx`
|
||
|
||
- Update status dropdown with new values and labels:
|
||
- `negotiating` → "Negotiating"
|
||
- `awaiting_quotation` → "Awaiting Quotation"
|
||
- `awaiting_customer_confirmation` → "Awaiting Customer Confirmation"
|
||
- `awaiting_fulfilment` → "Awaiting Fulfilment"
|
||
- `awaiting_payment` → "Awaiting Payment"
|
||
- `manufacturing` → "Manufacturing"
|
||
- `shipped` → "Shipped"
|
||
- `installed` → "Installed"
|
||
- `declined` → "Declined"
|
||
- `complete` → "Complete"
|
||
- Add `title` input field (required).
|
||
- Replace flat `payment_status` enum with the new `payment_status` object fields.
|
||
- Add Timeline section to `OrderDetail.jsx`: vertical event log + add-entry inline form.
|
||
- Update all API calls to use `/crm/customers/{customer_id}/orders/` routes.
|
||
|
||
---
|
||
|
||
## 4. Status Color Coding Reference
|
||
|
||
Define all as CSS variables in `index.css` and use consistently across all views:
|
||
|
||
### Relationship Status
|
||
| Status | Color |
|
||
|---|---|
|
||
| lead | grey / muted |
|
||
| prospect | blue |
|
||
| active | green |
|
||
| inactive | amber |
|
||
| churned | dark or soft red |
|
||
|
||
### Order Status
|
||
| Status | Color |
|
||
|---|---|
|
||
| negotiating | blue |
|
||
| awaiting_quotation | purple |
|
||
| awaiting_customer_confirmation | indigo |
|
||
| awaiting_fulfilment | amber |
|
||
| awaiting_payment | orange |
|
||
| manufacturing | cyan |
|
||
| shipped | teal |
|
||
| installed | green |
|
||
| declined | red |
|
||
| complete | muted/grey |
|
||
|
||
### Issue / Support Flags
|
||
| State | Color |
|
||
|---|---|
|
||
| active issue | red |
|
||
| active support | amber |
|
||
| resolved | muted/grey |
|
||
|
||
---
|
||
|
||
## 5. Migration Notes
|
||
|
||
- The old `negotiating` and `has_problem` fields will remain in Firestore until the migration script is run. The backend should **read both old and new fields** during the transition period, preferring the new structure if present.
|
||
- A one-time migration script (`backend/migrate_customer_flags.py`) should:
|
||
1. Read all customer documents
|
||
2. If `negotiating: true` → create an order in the customer's `orders` subcollection with `status = "negotiating"` and set `relationship_status = "active"` on the customer
|
||
3. If `has_problem: true` → append one entry to `technical_issues` with `active: true`, `opened_date: customer.updated_at`, `note: "Migrated from legacy has_problem flag"`, `opened_by: "system"`
|
||
4. Remove `negotiating` and `has_problem` from the customer document
|
||
- Do **not** run the migration script until all frontend and backend changes are deployed and tested.
|
||
|
||
---
|
||
|
||
## 6. File Summary — What to Touch
|
||
|
||
```
|
||
backend/crm/models.py ← model updates (primary changes)
|
||
backend/crm/customers_router.py ← new endpoints + field updates
|
||
backend/crm/orders_router.py ← remove top-level routes, re-implement as subcollection,
|
||
add timeline + payment-status + init-negotiations endpoints
|
||
backend/migrate_customer_flags.py ← NEW one-time migration script
|
||
|
||
frontend/src/index.css ← add CSS variables for all new status colors
|
||
frontend/src/crm/customers/CustomerList.jsx ← relationship status chip + support flag dots column
|
||
frontend/src/crm/customers/CustomerDetail.jsx ← page header, 3 new quick-entry buttons + modals,
|
||
Overview tab overhaul, new Support tab,
|
||
new Financials tab, Orders tab updates
|
||
frontend/src/crm/customers/CustomerForm.jsx ← remove old flags, add relationship_status
|
||
frontend/src/crm/orders/OrderForm.jsx ← new status values, title field, payment_status,
|
||
updated API route paths
|
||
frontend/src/crm/orders/OrderDetail.jsx ← timeline section, updated status/payment,
|
||
updated API route paths
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Do NOT Change (out of scope)
|
||
|
||
- Quotations system — leave as-is
|
||
- Communications / inbox — leave as-is
|
||
- Files & Media tab — leave as-is
|
||
- Devices tab — leave as-is
|
||
- Any other module outside `crm/`
|