feat(ProCanvas): retro game overhaul with log actions, challenges & achievements modals
- Complete ProCanvas redesign with retro sports game aesthetic - Card-game style photo frame with tilt, shimmer, corner star accents - Daily Missions card with weekly challenge + individual quest progress bars - Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting) - Each log action opens themed modal with relevant input fields - Challenges modal with 6 active challenges and progress tracking - Achievements modal with all badges, unlock status, descriptions - Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals - Rewards & Checkpoints with named stages (Daily Grind to Legend Run) - Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings) - Hit The Map button with smooth pulsing glow animation - Grid pattern overlay in Leaderboard card - Light mode support via dark: Tailwind variants throughout - Top 3 badges displayed on profile card - Fixed dropdown option visibility in dark mode
This commit is contained in:
@@ -0,0 +1,527 @@
|
||||
# LLD — Database Schema (B2)
|
||||
|
||||
**Scope:** PostgreSQL schema — entity relationships, state machines, query patterns, data lifecycle
|
||||
**Companion doc:** `docs/backend/02_database_schema.md`
|
||||
|
||||
---
|
||||
|
||||
## Diagram 1 — Core Entity Relationship Diagram
|
||||
|
||||
All 16 tables and their foreign-key relationships. Key fields shown per entity.
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS {
|
||||
uuid id PK
|
||||
varchar legacy_id
|
||||
varchar email
|
||||
varchar emp_id
|
||||
varchar username
|
||||
varchar password_hash
|
||||
varchar full_name
|
||||
user_role role
|
||||
int xp
|
||||
int streak_days
|
||||
jsonb achievements
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
PROPERTIES {
|
||||
serial id PK
|
||||
varchar property_id
|
||||
varchar address
|
||||
decimal latitude
|
||||
decimal longitude
|
||||
jsonb polygon
|
||||
property_type property_type
|
||||
canvassing_status canvassing_status
|
||||
int estimated_market_value
|
||||
uuid assigned_agent_id FK
|
||||
bool pending_signature
|
||||
int proposal_value
|
||||
bool currently_rented
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
PROPERTY_PHOTOS {
|
||||
serial id PK
|
||||
int property_id FK
|
||||
varchar url
|
||||
varchar caption
|
||||
smallint sort_order
|
||||
}
|
||||
|
||||
MEETINGS {
|
||||
uuid id PK
|
||||
varchar legacy_id
|
||||
uuid agent_id FK
|
||||
uuid customer_id FK
|
||||
int property_id FK
|
||||
date meeting_date
|
||||
time meeting_time
|
||||
meeting_status status
|
||||
int deal_value
|
||||
text notes
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
MEETING_CHANGE_REQUESTS {
|
||||
uuid id PK
|
||||
uuid meeting_id FK
|
||||
uuid requested_by FK
|
||||
uuid reviewed_by FK
|
||||
date proposed_date
|
||||
change_request_status status
|
||||
}
|
||||
|
||||
PROJECTS {
|
||||
uuid id PK
|
||||
uuid owner_id FK
|
||||
int property_id FK
|
||||
varchar title
|
||||
project_status status
|
||||
smallint health_score
|
||||
int approved_budget_cents
|
||||
int actual_cost_cents
|
||||
smallint completion_pct
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
PROJECT_TASKS {
|
||||
uuid id PK
|
||||
uuid project_id FK
|
||||
uuid assigned_to FK
|
||||
varchar title
|
||||
task_status status
|
||||
task_priority priority
|
||||
date due_date
|
||||
}
|
||||
|
||||
CHANGE_ORDERS {
|
||||
uuid id PK
|
||||
uuid project_id FK
|
||||
uuid requested_by FK
|
||||
uuid approved_by FK
|
||||
varchar title
|
||||
int cost_impact_cents
|
||||
change_order_status status
|
||||
}
|
||||
|
||||
VENDORS {
|
||||
uuid id PK
|
||||
varchar company_name
|
||||
varchar trade_type
|
||||
vendor_status status
|
||||
decimal on_time_delivery_rate
|
||||
date coi_expiry_date
|
||||
bool is_compliant
|
||||
int total_spend_cents
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
VENDOR_COMPLIANCE_DOCS {
|
||||
uuid id PK
|
||||
uuid vendor_id FK
|
||||
compliance_doc_type doc_type
|
||||
compliance_doc_status status
|
||||
date expiry_date
|
||||
varchar file_url
|
||||
uuid uploaded_by FK
|
||||
}
|
||||
|
||||
VENDOR_ORDERS {
|
||||
uuid id PK
|
||||
uuid vendor_id FK
|
||||
uuid project_id FK
|
||||
varchar order_number
|
||||
int total_cents
|
||||
order_status status
|
||||
date expected_date
|
||||
date delivered_date
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
INVOICES {
|
||||
uuid id PK
|
||||
varchar invoice_number
|
||||
invoice_type invoice_type
|
||||
uuid issued_by FK
|
||||
uuid billed_to_user FK
|
||||
uuid vendor_id FK
|
||||
uuid project_id FK
|
||||
int total_cents
|
||||
int amount_paid_cents
|
||||
int balance_cents
|
||||
invoice_status status
|
||||
date due_date
|
||||
uuid approved_by FK
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
DOCUMENTS {
|
||||
uuid id PK
|
||||
varchar title
|
||||
document_category category
|
||||
document_review_status review_status
|
||||
uuid project_id FK
|
||||
uuid vendor_id FK
|
||||
uuid uploaded_by FK
|
||||
varchar file_url
|
||||
date expiry_date
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
SALES_HISTORY {
|
||||
uuid id PK
|
||||
uuid agent_id FK
|
||||
int property_id FK
|
||||
uuid meeting_id FK
|
||||
date closed_date
|
||||
int amount_cents
|
||||
deal_status status
|
||||
}
|
||||
|
||||
NOTIFICATIONS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
notification_type type
|
||||
varchar title
|
||||
bool is_read
|
||||
varchar deep_link
|
||||
jsonb metadata
|
||||
}
|
||||
|
||||
AUDIT_LOGS {
|
||||
bigserial id PK
|
||||
uuid actor_id FK
|
||||
varchar action
|
||||
varchar resource_type
|
||||
varchar resource_id
|
||||
jsonb old_value
|
||||
jsonb new_value
|
||||
inet ip_address
|
||||
}
|
||||
|
||||
USERS ||--o{ PROPERTIES : "assigned_agent_id"
|
||||
USERS ||--o{ MEETINGS : "agent_id"
|
||||
USERS ||--o{ MEETINGS : "customer_id"
|
||||
USERS ||--o{ PROJECTS : "owner_id"
|
||||
USERS ||--o{ PROJECT_TASKS : "assigned_to"
|
||||
USERS ||--o{ CHANGE_ORDERS : "requested_by"
|
||||
USERS ||--o{ INVOICES : "issued_by"
|
||||
USERS ||--o{ INVOICES : "billed_to_user"
|
||||
USERS ||--o{ SALES_HISTORY : "agent_id"
|
||||
USERS ||--o{ NOTIFICATIONS : "user_id"
|
||||
USERS ||--o{ AUDIT_LOGS : "actor_id"
|
||||
USERS ||--o{ VENDOR_COMPLIANCE_DOCS : "uploaded_by"
|
||||
|
||||
PROPERTIES ||--o{ PROPERTY_PHOTOS : "property_id"
|
||||
PROPERTIES ||--o{ MEETINGS : "property_id"
|
||||
PROPERTIES ||--o{ PROJECTS : "property_id"
|
||||
PROPERTIES ||--o{ SALES_HISTORY : "property_id"
|
||||
|
||||
MEETINGS ||--o{ MEETING_CHANGE_REQUESTS : "meeting_id"
|
||||
MEETINGS ||--o{ SALES_HISTORY : "meeting_id"
|
||||
|
||||
PROJECTS ||--o{ PROJECT_TASKS : "project_id"
|
||||
PROJECTS ||--o{ CHANGE_ORDERS : "project_id"
|
||||
PROJECTS ||--o{ VENDOR_ORDERS : "project_id"
|
||||
PROJECTS ||--o{ INVOICES : "project_id"
|
||||
PROJECTS ||--o{ DOCUMENTS : "project_id"
|
||||
|
||||
VENDORS ||--o{ VENDOR_COMPLIANCE_DOCS : "vendor_id"
|
||||
VENDORS ||--o{ VENDOR_ORDERS : "vendor_id"
|
||||
VENDORS ||--o{ INVOICES : "vendor_id"
|
||||
VENDORS ||--o{ DOCUMENTS : "vendor_id"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 2 — Meeting Status State Machine
|
||||
|
||||
A meeting moves through these states. Only ADMIN/OWNER can force-advance to any state. FIELD_AGENTs can only request changes.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Scheduled : POST /meetings\n(ADMIN creates)
|
||||
|
||||
Scheduled --> Rescheduled : PATCH status=Rescheduled\n(ADMIN approves change request)
|
||||
Rescheduled --> Scheduled : PATCH status=Scheduled\n(new date confirmed)
|
||||
|
||||
Scheduled --> InProgress : PATCH status=In Progress\n(agent marks on-site)
|
||||
Rescheduled --> InProgress : PATCH status=In Progress
|
||||
|
||||
InProgress --> Completed : PATCH status=Completed\n+ outcome + notes
|
||||
InProgress --> Cancelled : PATCH status=Cancelled\n+ reason
|
||||
|
||||
Completed --> Converted : PATCH status=Converted\n+ deal_value (closes deal)
|
||||
|
||||
Scheduled --> Cancelled : PATCH status=Cancelled
|
||||
Rescheduled --> Cancelled : PATCH status=Cancelled
|
||||
|
||||
Converted --> [*] : Terminal state\n→ writes to sales_history
|
||||
Cancelled --> [*] : Terminal state
|
||||
|
||||
note right of Scheduled
|
||||
FIELD_AGENT submits
|
||||
MeetingChangeRequest
|
||||
(cannot directly edit)
|
||||
end note
|
||||
|
||||
note right of Converted
|
||||
Triggers:
|
||||
sales_history INSERT
|
||||
notification to ADMIN
|
||||
end note
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 3 — Invoice Status State Machine
|
||||
|
||||
Invoices move through a lifecycle depending on payments, approvals, and time-based triggers.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Draft : POST /invoices\n(ADMIN / OWNER creates)
|
||||
|
||||
Draft --> Sent : PATCH status=Sent\n(email triggered to recipient)
|
||||
Draft --> Void : PATCH status=Void\n(cancel before sending)
|
||||
|
||||
Sent --> Viewed : Auto on first\nrecipient GET request
|
||||
|
||||
Viewed --> Partial : PATCH\namount_paid_cents > 0\nbut < total_cents
|
||||
Viewed --> Paid : PATCH\namount_paid_cents = total_cents
|
||||
|
||||
Partial --> Paid : PATCH\namount_paid_cents = total_cents
|
||||
Partial --> Overdue : Scheduler trigger\ndue_date < TODAY
|
||||
|
||||
Sent --> Overdue : Scheduler trigger\ndue_date < TODAY
|
||||
|
||||
Overdue --> Paid : PATCH\namount_paid_cents = total_cents
|
||||
|
||||
Viewed --> Disputed : PATCH status=Disputed\n+ dispute_reason
|
||||
Partial --> Disputed : PATCH status=Disputed
|
||||
Overdue --> Disputed : PATCH status=Disputed
|
||||
|
||||
Disputed --> Paid : OWNER resolves dispute
|
||||
Disputed --> Void : OWNER voids invoice
|
||||
|
||||
Paid --> [*] : Terminal state
|
||||
Void --> [*] : Terminal state
|
||||
|
||||
note right of Overdue
|
||||
Triggers:
|
||||
notification to OWNER + ADMIN
|
||||
SendGrid email to debtor
|
||||
end note
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 4 — Project Status State Machine
|
||||
|
||||
Projects managed through the Owner's Box.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> active : POST /projects\n(OWNER creates)
|
||||
|
||||
active --> completed : PATCH status=completed\nall tasks done\nfinal invoice paid
|
||||
|
||||
active --> delayed : Auto-trigger:\ntarget_end_date passed\ncompletion_pct < 100
|
||||
|
||||
active --> on_hold : PATCH status=on_hold\n+ reason (OWNER decision)
|
||||
|
||||
on_hold --> active : PATCH status=active\n(OWNER resumes)
|
||||
|
||||
active --> disputed : PATCH status=disputed\n+ dispute details\n(OWNER or CONTRACTOR)
|
||||
|
||||
disputed --> active : Dispute resolved\n(OWNER approves)
|
||||
disputed --> cancelled : Escalated dispute\n(OWNER decision)
|
||||
|
||||
active --> cancelled : PATCH status=cancelled\n(OWNER decision)
|
||||
|
||||
delayed --> active : PATCH status=active\n(new timeline set)
|
||||
delayed --> cancelled : PATCH status=cancelled
|
||||
|
||||
completed --> [*] : Terminal state
|
||||
cancelled --> [*] : Terminal state
|
||||
|
||||
note right of delayed
|
||||
Triggers:
|
||||
notification to OWNER
|
||||
health_score recalculated
|
||||
end note
|
||||
|
||||
note right of disputed
|
||||
Triggers:
|
||||
notification to CONTRACTOR
|
||||
change_orders locked
|
||||
end note
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 5 — Vendor Compliance Status Machine
|
||||
|
||||
Compliance documents transition based on expiry dates. A scheduler runs daily to check all docs.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Active : Document uploaded\n(expiry_date in future)
|
||||
|
||||
Active --> ExpiringSoon : Scheduler:\nexpiry_date ≤ TODAY + 30 days
|
||||
Active --> Expired : Scheduler:\nexpiry_date < TODAY
|
||||
Active --> Missing : Manual flag\nor doc deleted
|
||||
|
||||
ExpiringSoon --> Active : New document uploaded\nwith future expiry_date
|
||||
ExpiringSoon --> Expired : Scheduler:\nexpiry_date < TODAY
|
||||
|
||||
Expired --> Active : Replacement doc uploaded
|
||||
|
||||
Missing --> Active : Doc uploaded
|
||||
|
||||
note right of ExpiringSoon
|
||||
Triggers at 30, 14, 7 days:
|
||||
notification to OWNER
|
||||
email to vendor contact
|
||||
vendor.is_compliant = false
|
||||
end note
|
||||
|
||||
note right of Expired
|
||||
Triggers immediately:
|
||||
urgent notification to OWNER
|
||||
vendor.is_compliant = false
|
||||
vendor orders may be blocked
|
||||
end note
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 6 — Soft Delete Pattern
|
||||
|
||||
How "deletion" works across all tables. No row is ever hard-deleted in production.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
REQ["DELETE /properties/42\nor\nDELETE /vendors/uuid"] --> AUTH_CHECK["Auth: require_role\nADMIN or OWNER"]
|
||||
|
||||
AUTH_CHECK --> SOFT_DEL["UPDATE table\nSET deleted_at = NOW()\nWHERE id = ?"]
|
||||
|
||||
SOFT_DEL --> AUDIT["INSERT audit_logs\nactor_id, action=resource.deleted\nold_value = full row snapshot"]
|
||||
|
||||
SOFT_DEL --> NOTIFY["Trigger dependent cleanup:\n• Unassign agent from properties\n• Cancel pending meetings\n• Flag open invoices"]
|
||||
|
||||
subgraph AllQueries["All Application Queries"]
|
||||
Q1["SELECT * FROM properties\nWHERE deleted_at IS NULL"]
|
||||
Q2["SELECT * FROM meetings\nWHERE deleted_at IS NULL"]
|
||||
Q3["SELECT * FROM vendors\nWHERE deleted_at IS NULL"]
|
||||
end
|
||||
|
||||
SOFT_DEL -.->|"row hidden from"| AllQueries
|
||||
|
||||
subgraph AdminOnly["Admin / Recovery Queries"]
|
||||
Q_ADMIN["SELECT * FROM properties\nWHERE deleted_at IS NOT NULL\n(recovery / audit only)"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 7 — Audit Log Write Pattern
|
||||
|
||||
Which operations write to `audit_logs` and what data is captured.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Triggers["Write Operations That Trigger Audit Log"]
|
||||
P1["Property:\nassign_agent\nchange_status\npending_signature"]
|
||||
P2["Meeting:\nstatus change\nchange_request approved"]
|
||||
P3["Invoice:\ncreated\napproved\nvoided"]
|
||||
P4["User:\nrole changed\ndeactivated"]
|
||||
P5["Project:\ncreated\nstatus changed\nchange_order approved"]
|
||||
P6["Chatbot:\nquery with sensitive role context"]
|
||||
end
|
||||
|
||||
subgraph AuditLog["audit_logs INSERT"]
|
||||
AL["{\n actor_id: user.id,\n action: 'property.agent_assigned',\n resource_type: 'property',\n resource_id: '42',\n old_value: { assigned_agent_id: null },\n new_value: { assigned_agent_id: 'uuid' },\n ip_address: '1.2.3.4',\n created_at: NOW()\n}"]
|
||||
end
|
||||
|
||||
P1 & P2 & P3 & P4 & P5 & P6 --> AL
|
||||
|
||||
AL --> QUERY["OWNER / ADMIN\nGET /audit-logs?resource_type=property\n&resource_id=42"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 8 — Key Query Patterns by Role
|
||||
|
||||
How the ORM service layer scopes queries differently per role.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph PropertiesQuery["GET /properties — Role-Scoped Query"]
|
||||
PQ_START["Request arrives\nwith current_user"]
|
||||
PQ_START --> PQ_ROLE{current_user.role}
|
||||
|
||||
PQ_ROLE -->|"OWNER / ADMIN"| PQ_ALL["SELECT * FROM properties\nWHERE deleted_at IS NULL\n+ any filters"]
|
||||
|
||||
PQ_ROLE -->|"FIELD_AGENT"| PQ_AGENT["SELECT * FROM properties\nWHERE assigned_agent_id = current_user.id\nAND deleted_at IS NULL"]
|
||||
|
||||
PQ_ROLE -->|"CUSTOMER"| PQ_CUS["SELECT * FROM properties\nWHERE property_id = current_user.property_id\nAND deleted_at IS NULL\nLIMIT 1"]
|
||||
end
|
||||
|
||||
subgraph MeetingsQuery["GET /meetings — Role-Scoped Query"]
|
||||
MQ_START["Request arrives"]
|
||||
MQ_START --> MQ_ROLE{current_user.role}
|
||||
|
||||
MQ_ROLE -->|"OWNER / ADMIN"| MQ_ALL["SELECT * FROM meetings\nWHERE deleted_at IS NULL"]
|
||||
|
||||
MQ_ROLE -->|"FIELD_AGENT"| MQ_AGENT["SELECT * FROM meetings\nWHERE agent_id = current_user.id\nAND deleted_at IS NULL"]
|
||||
|
||||
MQ_ROLE -->|"CUSTOMER"| MQ_CUS["SELECT * FROM meetings\nWHERE customer_id = current_user.id\nAND deleted_at IS NULL"]
|
||||
end
|
||||
|
||||
subgraph InvoiceQuery["GET /invoices — Role-Scoped Query"]
|
||||
IQ_START["Request arrives"]
|
||||
IQ_START --> IQ_ROLE{current_user.role}
|
||||
|
||||
IQ_ROLE -->|"OWNER"| IQ_ALL["SELECT * FROM invoices\nWHERE deleted_at IS NULL\n(full financial view)"]
|
||||
|
||||
IQ_ROLE -->|"ADMIN"| IQ_ADM["SELECT * FROM invoices\nWHERE deleted_at IS NULL\nAND invoice_type != 'internal'"]
|
||||
|
||||
IQ_ROLE -->|"CONTRACTOR / SUBCONTRACTOR"| IQ_CON["SELECT * FROM invoices\nWHERE billed_to_user = current_user.id\nOR issued_by = current_user.id"]
|
||||
|
||||
IQ_ROLE -->|"VENDOR"| IQ_VEN["SELECT * FROM invoices\nWHERE vendor_id IN\n (SELECT id FROM vendors\n WHERE contact_user_id = current_user.id)"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 9 — Legacy ID Migration Path
|
||||
|
||||
How the `legacy_id` column bridges the mock frontend IDs (`'e1'`, `'own_001'`) to real UUIDs during the transition period.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as React Frontend
|
||||
participant API as FastAPI
|
||||
participant DB as PostgreSQL
|
||||
|
||||
Note over FE,DB: ── Transition Phase (Integration Team replacing mock calls) ──
|
||||
|
||||
FE->>API: GET /users/by-legacy-id/e1
|
||||
API->>DB: SELECT * FROM users WHERE legacy_id = 'e1'
|
||||
DB-->>API: { id: "3f8a...(uuid)", legacy_id: "e1", ... }
|
||||
API-->>FE: User object with real UUID
|
||||
|
||||
Note over FE: Frontend stores UUID in state<br/>All subsequent calls use UUID
|
||||
|
||||
FE->>API: GET /meetings?agent_id=3f8a...(uuid)
|
||||
API->>DB: SELECT * FROM meetings WHERE agent_id = '3f8a...'
|
||||
DB-->>API: Meeting rows
|
||||
API-->>FE: Meetings data
|
||||
|
||||
Note over FE,DB: ── Post-Migration Phase (legacy_id no longer needed) ──
|
||||
Note over DB: legacy_id column remains<br/>for audit trail but<br/>no longer queried by FE
|
||||
```
|
||||
Reference in New Issue
Block a user