Spaces:
Running
Major feature update: DOCX generation, preview columns, bug fixes, dashboard enhancements
Browse filesActivities completed:
1. Preview table: added Name & Code columns (7-col layout: Role|Name|Code|ABE|Agreement|Comm%|ProdCredit)
2. House Agreement: layers now deduct from 100% base (was incorrectly adding on top)
3. House Agreement: P1 hidden from commission table (kept in approval workflow)
4. Life Origination: fixed to apply All Years at 20% (was Year 1 only)
5. CE as Producer: required People Picker mode for Ceded Accounts, CCP deducts from CE
6. Submit safeguard: modal preview before submission with Confirm/Go Back buttons
7. Word document generation: populates DRAFT/FINAL .docx templates with full form data
8. Dashboard: agreement type, effective date, commission preview, approval timeline,
status filters, soft-delete (hide) for finalized submissions
9. Email fallback: generates FirstInitial+LastName@snellingswalters.com when not in DB
10. Employee originator: expanded to 20 employees with search filter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- .gitignore +6 -0
- Feedback/Commission_Agreement_DRAFT_Template.docx +0 -0
- Feedback/Commission_Agreement_FINAL_Template.docx +0 -0
- Feedback/commission_intake_refinement_prompt_v2.md +704 -0
- Feedback/email.docx +0 -0
- app/docx_generator.py +317 -0
- app/models.py +1 -0
- app/pdf_generator.py +19 -11
- app/routes/form_routes.py +145 -6
- app/rules_engine.py +15 -8
- app/seed_db.py +8 -4
- app/templates/dashboard.html +198 -45
- app/templates/form.html +229 -18
- commission_intake_refactor_prompt.md +379 -0
- requirements.txt +2 -1
- start.py +11 -19
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
/data/
|
| 5 |
+
*.db
|
| 6 |
+
.env
|
|
Binary file (11.9 kB). View file
|
|
|
|
Binary file (11.9 kB). View file
|
|
|
|
@@ -0,0 +1,704 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Commission Agreement Intake - Refinement & Power Automate Integration
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
This prompt refines the existing web app with additional features, improved validation, OneDrive integration, and Power Automate workflow compatibility. The web app handles form input and draft PDF generation, while Power Automate manages approval workflows, signature collection, and final document processing.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 1. COMMISSION PREVIEW TABLE - ADD NAME & CODE COLUMNS
|
| 9 |
+
|
| 10 |
+
### Current State
|
| 11 |
+
Table shows: Role | Agreement Type | Commission % | Production % | ABE Status
|
| 12 |
+
|
| 13 |
+
### Required Change
|
| 14 |
+
Add **Name** and **Producer Code** columns, reorder to:
|
| 15 |
+
|
| 16 |
+
**Role | Name | Producer Code | ABE Status | Agreement Type | Commission % | Prod Credit**
|
| 17 |
+
|
| 18 |
+
### Example Output
|
| 19 |
+
|
| 20 |
+
**Year 1**
|
| 21 |
+
| Role | Name | Producer Code | ABE Status | Agreement Type | Commission % | Prod Credit |
|
| 22 |
+
|------|------|---------------|------------|----------------|--------------|-------------|
|
| 23 |
+
| Producer 1 | Wade Beacham | BEAWA2 | Non-ABE | Beacham Agreement | 23% | 100% |
|
| 24 |
+
| CE Layer | John Smith | SMIJO1 | ABE | Client Executive Agreement | 2% | 0% |
|
| 25 |
+
| CCP Layer | Mike Parsa | PARSM1 | Non-ABE | Complex Claims Practice | 5% | 0% |
|
| 26 |
+
|
| 27 |
+
**Implementation Notes:**
|
| 28 |
+
- Name should be the full display name from People Picker
|
| 29 |
+
- Producer Code should be the generated/assigned code (e.g., BEAWA2, HERFR1, EMPOR2)
|
| 30 |
+
- For House Account: Name = "House Account", Code = "ACCHO1"
|
| 31 |
+
- This applies to ALL year tables (Year 1, Year 2, Year 3+)
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 2. SUBMIT SAFEGUARD - PREVIEW BEFORE SUBMISSION
|
| 36 |
+
|
| 37 |
+
### Current Issue
|
| 38 |
+
"Submit for Approval" button immediately submits without preview verification.
|
| 39 |
+
|
| 40 |
+
### Required Flow
|
| 41 |
+
|
| 42 |
+
1. User clicks **"Submit for Approval"** button
|
| 43 |
+
2. System generates draft PDF/Word doc preview
|
| 44 |
+
3. Show **modal/popup** displaying the generated document
|
| 45 |
+
4. Popup contains two buttons:
|
| 46 |
+
- **"Go Back & Edit"** - Closes popup, returns to form (no submission)
|
| 47 |
+
- **"Confirm & Submit"** - Saves to OneDrive, triggers Power Automate workflow
|
| 48 |
+
|
| 49 |
+
### Modal Requirements
|
| 50 |
+
- Display the full generated document (scrollable if needed)
|
| 51 |
+
- Clear heading: "Review Your Commission Agreement Before Submitting"
|
| 52 |
+
- Buttons should be prominently placed and clearly labeled
|
| 53 |
+
- Document should render in a readable format (embedded viewer or PDF preview)
|
| 54 |
+
|
| 55 |
+
### Only on "Confirm & Submit"
|
| 56 |
+
- Save Word document to OneDrive
|
| 57 |
+
- Save form data to database/SharePoint
|
| 58 |
+
- Mark agreement as "Pending Acknowledgment"
|
| 59 |
+
- Close modal and navigate to dashboard
|
| 60 |
+
|
| 61 |
+
**Error Handling:** If document generation fails, show error message and prevent submission.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 3. EMPLOYEE AS ORIGINATOR - ACTIVE DIRECTORY INTEGRATION
|
| 66 |
+
|
| 67 |
+
### Current State
|
| 68 |
+
Sample data for employee originators.
|
| 69 |
+
|
| 70 |
+
### Required Implementation
|
| 71 |
+
**Integration Approach:** Choose one of these options (your decision):
|
| 72 |
+
|
| 73 |
+
**Option A: Microsoft Graph API**
|
| 74 |
+
- Search Office 365 / Azure AD users in real-time
|
| 75 |
+
- Pull full name, email, job title
|
| 76 |
+
- Best for live sync with company directory
|
| 77 |
+
|
| 78 |
+
**Option B: Office 365 Users Connector**
|
| 79 |
+
- Same connector used for Producer People Picker
|
| 80 |
+
- Consistent user experience across all people selection fields
|
| 81 |
+
- Built-in search and user data
|
| 82 |
+
|
| 83 |
+
**Option C: Separate Employee Table**
|
| 84 |
+
- Maintain a separate list of active employees
|
| 85 |
+
- Requires periodic sync but simpler implementation
|
| 86 |
+
- More control over who appears in the list
|
| 87 |
+
|
| 88 |
+
**Requirements (regardless of option):**
|
| 89 |
+
- Must show ALL active employees (not just producers)
|
| 90 |
+
- Search functionality by name
|
| 91 |
+
- When selected, auto-assign code: **"EMPOR2"** (literal string for all employees)
|
| 92 |
+
- Employee must be added to approval workflow (treated like a producer for acknowledgment)
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## 4. EMAIL ADDRESS GENERATION & STORAGE
|
| 97 |
+
|
| 98 |
+
### Purpose
|
| 99 |
+
Generate or retrieve email addresses for all participants for Power Automate approval notifications.
|
| 100 |
+
|
| 101 |
+
### Email Formula
|
| 102 |
+
**Standard format:** `{FirstInitial}{LastName}@snellingswalters.com`
|
| 103 |
+
|
| 104 |
+
**Examples:**
|
| 105 |
+
- Frank Herron β FHerron@snellingswalters.com
|
| 106 |
+
- Wade Beacham β WBeacham@snellingswalters.com
|
| 107 |
+
- Mike Parsa β MParsa@snellingswalters.com
|
| 108 |
+
|
| 109 |
+
### Implementation Options (Your Choice)
|
| 110 |
+
|
| 111 |
+
**Option A: Calculate from Name**
|
| 112 |
+
```javascript
|
| 113 |
+
function generateEmail(fullName) {
|
| 114 |
+
const parts = fullName.trim().split(' ');
|
| 115 |
+
const firstName = parts[0];
|
| 116 |
+
const lastName = parts[parts.length - 1];
|
| 117 |
+
const firstInitial = firstName[0].toUpperCase();
|
| 118 |
+
const lastNameClean = lastName.charAt(0).toUpperCase() + lastName.slice(1).toLowerCase();
|
| 119 |
+
return `${firstInitial}${lastNameClean}@snellingswalters.com`;
|
| 120 |
+
}
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
**Option B: Pull from Active Directory**
|
| 124 |
+
- If using Microsoft Graph API or Office 365 Users connector, retrieve email directly from user profile
|
| 125 |
+
- More reliable but requires AD integration
|
| 126 |
+
- Fallback to calculated email if AD email unavailable
|
| 127 |
+
|
| 128 |
+
### Email Requirements
|
| 129 |
+
**Must generate emails for:**
|
| 130 |
+
- Producer 1 (always)
|
| 131 |
+
- Producer 2 (if present)
|
| 132 |
+
- Originating Producer (if present)
|
| 133 |
+
- CE Layer (if present)
|
| 134 |
+
- CCP Layer (if Mike Parsa selected - not House Account)
|
| 135 |
+
|
| 136 |
+
**Do NOT generate for:**
|
| 137 |
+
- Legacy Producer (text field only, no approval needed)
|
| 138 |
+
- House Account (ACCHO1 - not a person)
|
| 139 |
+
|
| 140 |
+
### Data Storage
|
| 141 |
+
Store emails in the agreement record for Power Automate to use:
|
| 142 |
+
- `Producer1Email`
|
| 143 |
+
- `Producer2Email` (nullable)
|
| 144 |
+
- `OriginatorEmail` (nullable)
|
| 145 |
+
- `CEEmail` (nullable)
|
| 146 |
+
- `CCPEmail` (nullable - only if Mike Parsa)
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
## 5. CEDED ACCOUNTS - CE AS PRODUCER (REQUIRED FIELD)
|
| 151 |
+
|
| 152 |
+
### Agreement Type: "Ceded Accounts - CE as Producer"
|
| 153 |
+
|
| 154 |
+
### Current Issue
|
| 155 |
+
CE still appears as an optional checkbox layer.
|
| 156 |
+
|
| 157 |
+
### Required Behavior
|
| 158 |
+
|
| 159 |
+
**When this agreement type is selected:**
|
| 160 |
+
|
| 161 |
+
1. **Remove "Include CE Layer" checkbox entirely**
|
| 162 |
+
2. **Show CE field as REQUIRED People Picker** (not optional)
|
| 163 |
+
3. Label should be: **"Client Executive (Producer)"** not "CE Layer"
|
| 164 |
+
4. CE gets their own commission line in preview table:
|
| 165 |
+
- Role: "Client Executive"
|
| 166 |
+
- Agreement Type: "Ceded Accounts - CE as Producer"
|
| 167 |
+
- Commission: 10%
|
| 168 |
+
- Production Credit: 100%
|
| 169 |
+
- ABE Status: Non-ABE (Code 2)
|
| 170 |
+
|
| 171 |
+
5. No Producer 1 field should appear (CE replaces P1)
|
| 172 |
+
6. CCP Layer can still be added as optional checkbox
|
| 173 |
+
|
| 174 |
+
### Commission Calculation
|
| 175 |
+
- CE: 10% commission, 100% production credit, Code 2
|
| 176 |
+
- If CCP added: CCP takes 5% from CE's 10%, leaving CE with 5%
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
## 6. HOUSE AGREEMENT - LAYER DEDUCTION FIX
|
| 181 |
+
|
| 182 |
+
### Agreement Type: "House Standard Agreement (100%)"
|
| 183 |
+
|
| 184 |
+
### Current Bug
|
| 185 |
+
Layers add on top of 100%, creating 102-107% total commission.
|
| 186 |
+
|
| 187 |
+
### Correct Behavior
|
| 188 |
+
**Layers must deduct from the 100% base:**
|
| 189 |
+
|
| 190 |
+
| Scenario | House Account Commission | CE | CCP | Total |
|
| 191 |
+
|----------|-------------------------|-----|-----|-------|
|
| 192 |
+
| No layers | 100% | - | - | 100% |
|
| 193 |
+
| CE only | 98% | 2% | - | 100% |
|
| 194 |
+
| CCP only | 95% | - | 5% | 100% |
|
| 195 |
+
| Both CE & CCP | 93% | 2% | 5% | 100% |
|
| 196 |
+
|
| 197 |
+
### Implementation
|
| 198 |
+
```javascript
|
| 199 |
+
let houseCommission = 100;
|
| 200 |
+
if (ceIncluded) houseCommission -= 2;
|
| 201 |
+
if (ccpIncluded) houseCommission -= 5;
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
### Important Notes
|
| 205 |
+
- Layers can be added to House agreements (leave checkboxes available)
|
| 206 |
+
- House Account should always show in preview table with adjusted commission
|
| 207 |
+
- Total commission must always equal 100%
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
|
| 211 |
+
## 7. HOUSE AGREEMENT - HIDE PRODUCER 1 FROM PREVIEW
|
| 212 |
+
|
| 213 |
+
### Agreement Type: "House Standard Agreement (100%)"
|
| 214 |
+
|
| 215 |
+
### Current Behavior
|
| 216 |
+
Producer 1 appears in commission preview table.
|
| 217 |
+
|
| 218 |
+
### Required Behavior
|
| 219 |
+
|
| 220 |
+
**Producer 1 should:**
|
| 221 |
+
- β Appear in the form (for selection during input)
|
| 222 |
+
- β Appear in acknowledgment/approval section (must approve)
|
| 223 |
+
- β NOT appear in commission preview table (getting 0% commission)
|
| 224 |
+
|
| 225 |
+
**Only show in preview table:**
|
| 226 |
+
- House Account (ACCHO1) - with adjusted commission based on layers
|
| 227 |
+
- CE Layer (if included)
|
| 228 |
+
- CCP Layer (if included)
|
| 229 |
+
|
| 230 |
+
**Rationale:** Producer 1 is approval processor only, not receiving commission, so shouldn't clutter the commission structure display.
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
+
|
| 234 |
+
## 8. LIFE ORIGINATION AGREEMENT - ALL YEARS
|
| 235 |
+
|
| 236 |
+
### Agreement Type: "Life Origination Fee Agreement"
|
| 237 |
+
|
| 238 |
+
### Current Bug
|
| 239 |
+
Agreement drops off after Year 1, showing no commission in Year 2+.
|
| 240 |
+
|
| 241 |
+
### Correct Behavior
|
| 242 |
+
**Life Origination Fee Agreement applies to ALL years:**
|
| 243 |
+
|
| 244 |
+
**Every Year (Year 1, Year 2, Year 3+):**
|
| 245 |
+
| Role | Name | Producer Code | ABE Status | Agreement Type | Commission % | Prod Credit |
|
| 246 |
+
|------|------|---------------|------------|----------------|--------------|-------------|
|
| 247 |
+
| Producer 1 | [Name] | [Code] | Non-ABE | Life Origination Fee Agreement | 20% | 100% |
|
| 248 |
+
| CE Layer | [Name] | [Code] | ABE | Client Executive Agreement | 2% | 0% |
|
| 249 |
+
|
| 250 |
+
**Key Point:** This does NOT transition to House Standard Agreement in Year 2+. The producer continues earning 20% commission indefinitely.
|
| 251 |
+
|
| 252 |
+
### BASE_RULES Update
|
| 253 |
+
```javascript
|
| 254 |
+
'LIFE': [
|
| 255 |
+
{ year: 1, label: 'All Years', p1: { comm: 20, prod: 100, code: 2 } }
|
| 256 |
+
]
|
| 257 |
+
// Remove the Year 2+ rule that transitions to House Account
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## 9. ONEDRIVE INTEGRATION & POWER AUTOMATE WORKFLOW
|
| 263 |
+
|
| 264 |
+
### Architecture Overview
|
| 265 |
+
|
| 266 |
+
```
|
| 267 |
+
βββββββββββββββ Generate ββββββββββββββββ Trigger ββββββββββββββββββ
|
| 268 |
+
β Web App ββββββββββββββββ>β OneDrive ββββββββββββββ>β Power Automate β
|
| 269 |
+
β (Form) β Word Doc β (Shared) β New File β (Workflow) β
|
| 270 |
+
βββββββββββββββ ββββββββββββββββ ββββββββββββββββββ
|
| 271 |
+
β β β
|
| 272 |
+
β Save Metadata β β
|
| 273 |
+
βΌ β βΌ
|
| 274 |
+
βββββββββββββββ β ββββββββββββββββββ
|
| 275 |
+
β SharePoint β β Final PDF β Email Approval β
|
| 276 |
+
β List οΏ½οΏ½οΏ½βββββββββββββββββββββββ΄βββββββββββββββββββββββ€ Tracking β
|
| 277 |
+
βββββββββββββββ Power Automate writes back β Signatures β
|
| 278 |
+
ββββββββββββββββββ
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
### OneDrive Implementation
|
| 282 |
+
|
| 283 |
+
**File Storage:**
|
| 284 |
+
- **Location:** Shared OneDrive folder accessible to both you and coworker
|
| 285 |
+
- **Folder:** `/Commission Agreements - Drafts/` (or similar)
|
| 286 |
+
- **Purpose:** Draft documents trigger Power Automate workflow
|
| 287 |
+
|
| 288 |
+
**File Naming Convention:**
|
| 289 |
+
```
|
| 290 |
+
{AccountCode}_{AgreementType}_{PolicyEffectiveDate}.docx
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
**Example:** `GEMCONT-01_PhD_2024-01-15.docx`
|
| 294 |
+
|
| 295 |
+
**Important:**
|
| 296 |
+
- Use **Policy Effective Date** (from form), NOT submission date
|
| 297 |
+
- Sanitize special characters from AgreementType (no spaces, slashes, etc.)
|
| 298 |
+
- Ensure uniqueness - if duplicate exists, append counter: `_v2`, `_v3`
|
| 299 |
+
|
| 300 |
+
### Word Document Generation
|
| 301 |
+
|
| 302 |
+
**Use attached template:** `Commission_Agreement_DRAFT_Template.docx`
|
| 303 |
+
|
| 304 |
+
**Web App Responsibilities (Fill These Fields):**
|
| 305 |
+
- Header: `{{AgreementType}}`, `{{AgreementId}}`
|
| 306 |
+
- Account Info: `{{AccountName}}`, `{{AccountCode}}`, `{{Department}}`, `{{Association}}`, `{{EstimatedRevenueDisplay}}`, `{{EffectiveDateShort}}`, `{{LegacyProducer}}`
|
| 307 |
+
- Bell Ringer: `{{BellRingerDisplay}}`
|
| 308 |
+
- Commission Tables: All `{{YearX_RowY_*}}` placeholders for up to 4 rows per year
|
| 309 |
+
- Year labels: `{{Year1Label}}`, `{{Year2Label}}`, `{{Year3Label}}`
|
| 310 |
+
- Totals: `{{Year1Total}}`, `{{Year2Total}}`, `{{Year3Total}}`
|
| 311 |
+
- Conditional sections: `{{Year2SectionStart}}...{{Year2SectionEnd}}`, `{{Year3SectionStart}}...{{Year3SectionEnd}}`
|
| 312 |
+
|
| 313 |
+
**Power Automate Responsibilities:**
|
| 314 |
+
- Signature section: `{{Signer1_Name}}`, `{{Signer1_RoleAgreement}}`, etc.
|
| 315 |
+
- Approval tracking: Date stamps, signature collection
|
| 316 |
+
- Final PDF generation with completed signatures
|
| 317 |
+
- Delivery to accounting/payroll
|
| 318 |
+
|
| 319 |
+
### Database/SharePoint List for Metadata
|
| 320 |
+
|
| 321 |
+
**Purpose:**
|
| 322 |
+
- Fallback if PDF generation fails
|
| 323 |
+
- Easier data access for Power Automate
|
| 324 |
+
- Reporting and analytics
|
| 325 |
+
- Dashboard data source
|
| 326 |
+
|
| 327 |
+
**Recommended: SharePoint List named "Agreements"**
|
| 328 |
+
|
| 329 |
+
**Required Columns:**
|
| 330 |
+
- `ID` (auto-generated)
|
| 331 |
+
- `AgreementId` (text - user-facing ID)
|
| 332 |
+
- `Status` (choice: Draft, Pending Acknowledgment, In Progress, Finalized)
|
| 333 |
+
- `AccountCode` (text)
|
| 334 |
+
- `AccountName` (text)
|
| 335 |
+
- `AgreementType` (text)
|
| 336 |
+
- `PolicyEffectiveDate` (date)
|
| 337 |
+
- `Department` (text)
|
| 338 |
+
- `Association` (text)
|
| 339 |
+
- `EstimatedRevenue` (currency)
|
| 340 |
+
- `BellRinger` (yes/no)
|
| 341 |
+
- `LegacyProducer` (text)
|
| 342 |
+
- `Producer1Name`, `Producer1Code`, `Producer1Email` (text)
|
| 343 |
+
- `Producer2Name`, `Producer2Code`, `Producer2Email` (text, nullable)
|
| 344 |
+
- `OriginatorName`, `OriginatorCode`, `OriginatorEmail` (text, nullable)
|
| 345 |
+
- `CEName`, `CECode`, `CEEmail` (text, nullable)
|
| 346 |
+
- `CCPName`, `CCPCode`, `CCPEmail` (text, nullable)
|
| 347 |
+
- `SubmittedBy` (person)
|
| 348 |
+
- `SubmittedOn` (datetime)
|
| 349 |
+
- `DraftDocumentUrl` (hyperlink - OneDrive path)
|
| 350 |
+
- `FinalDocumentUrl` (hyperlink - final PDF path)
|
| 351 |
+
- `ApprovalStatus` (JSON or multiline text - who has approved)
|
| 352 |
+
|
| 353 |
+
**Alternative:** Use a database of your choice, but ensure Power Automate can read/write to it easily.
|
| 354 |
+
|
| 355 |
+
### Power Automate Trigger
|
| 356 |
+
- **Trigger:** When a file is created in OneDrive folder
|
| 357 |
+
- **Folder:** `/Commission Agreements - Drafts/`
|
| 358 |
+
- **Action:** Parse filename to extract AccountCode, AgreementType, EffectiveDate
|
| 359 |
+
- **Action:** Query SharePoint list for matching agreement metadata
|
| 360 |
+
- **Action:** Generate approval emails to all participants
|
| 361 |
+
- **Action:** Track approvals and signatures
|
| 362 |
+
- **Action:** Generate final PDF with signatures
|
| 363 |
+
- **Action:** Update SharePoint list with final PDF URL and status
|
| 364 |
+
|
| 365 |
+
---
|
| 366 |
+
|
| 367 |
+
## 10. DASHBOARD - AGREEMENT STATUS & DISPLAY
|
| 368 |
+
|
| 369 |
+
### Dashboard Requirements
|
| 370 |
+
|
| 371 |
+
**Display ALL agreements for logged-in user:**
|
| 372 |
+
- Draft agreements
|
| 373 |
+
- Pending acknowledgment agreements
|
| 374 |
+
- In-progress agreements (partial approvals)
|
| 375 |
+
- Finalized agreements
|
| 376 |
+
|
| 377 |
+
### Agreement Cards/Rows Should Show:
|
| 378 |
+
|
| 379 |
+
**All Statuses:**
|
| 380 |
+
- Account Name
|
| 381 |
+
- Agreement Type
|
| 382 |
+
- Policy Effective Date
|
| 383 |
+
- Submission Date
|
| 384 |
+
- Current Status
|
| 385 |
+
|
| 386 |
+
**Draft Agreements:**
|
| 387 |
+
- Summary table preview
|
| 388 |
+
- Link to draft PDF
|
| 389 |
+
- "Edit" and "Delete" buttons
|
| 390 |
+
|
| 391 |
+
**Pending/In Progress Agreements:**
|
| 392 |
+
- Summary table preview
|
| 393 |
+
- Link to draft PDF
|
| 394 |
+
- **Approval Timeline** showing:
|
| 395 |
+
- Who has approved (β checkmark, timestamp)
|
| 396 |
+
- Who is pending (β circle, awaiting)
|
| 397 |
+
- Example: "Producer 1 β (3/10/2024) | CE Layer β (Pending) | CCP β (Pending)"
|
| 398 |
+
|
| 399 |
+
**Finalized Agreements:**
|
| 400 |
+
- Summary table preview
|
| 401 |
+
- Link to final PDF (with all signatures and timestamps)
|
| 402 |
+
- "Delete" button (visual only - data persists in database)
|
| 403 |
+
|
| 404 |
+
### Delete Functionality
|
| 405 |
+
- **Draft:** Deletes from database and removes draft file from OneDrive
|
| 406 |
+
- **Finalized:** Removes from user's dashboard view, but data remains in database for audit/reporting
|
| 407 |
+
- **In Progress:** Do not allow deletion (must cancel/reject first)
|
| 408 |
+
|
| 409 |
+
### Data Source
|
| 410 |
+
- Query SharePoint list (or database) filtered by `SubmittedBy = CurrentUser`
|
| 411 |
+
- Pull `DraftDocumentUrl` and `FinalDocumentUrl` for PDF links
|
| 412 |
+
- Parse `ApprovalStatus` to show timeline
|
| 413 |
+
|
| 414 |
+
---
|
| 415 |
+
|
| 416 |
+
## 11. TECHNICAL IMPLEMENTATION NOTES
|
| 417 |
+
|
| 418 |
+
### Email Generation Function
|
| 419 |
+
```javascript
|
| 420 |
+
function generateParticipantEmails(participants) {
|
| 421 |
+
const emails = {};
|
| 422 |
+
|
| 423 |
+
function toEmail(fullName) {
|
| 424 |
+
if (!fullName || fullName === 'House Account') return null;
|
| 425 |
+
const parts = fullName.trim().split(' ');
|
| 426 |
+
const firstName = parts[0];
|
| 427 |
+
const lastName = parts[parts.length - 1];
|
| 428 |
+
return `${firstName[0].toUpperCase()}${lastName}@snellingswalters.com`;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
if (participants.producer1) emails.Producer1Email = toEmail(participants.producer1.name);
|
| 432 |
+
if (participants.producer2) emails.Producer2Email = toEmail(participants.producer2.name);
|
| 433 |
+
if (participants.originator) emails.OriginatorEmail = toEmail(participants.originator.name);
|
| 434 |
+
if (participants.ce) emails.CEEmail = toEmail(participants.ce.name);
|
| 435 |
+
if (participants.ccp && participants.ccp.name === 'Mike Parsa') {
|
| 436 |
+
emails.CCPEmail = 'MParsa@snellingswalters.com';
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
return emails;
|
| 440 |
+
}
|
| 441 |
+
```
|
| 442 |
+
|
| 443 |
+
### File Naming Sanitization
|
| 444 |
+
```javascript
|
| 445 |
+
function generateFileName(accountCode, agreementType, effectiveDate) {
|
| 446 |
+
// Sanitize agreement type (remove special chars)
|
| 447 |
+
const cleanType = agreementType.replace(/[^a-zA-Z0-9]/g, '');
|
| 448 |
+
|
| 449 |
+
// Format date as YYYY-MM-DD
|
| 450 |
+
const dateStr = effectiveDate.toISOString().split('T')[0];
|
| 451 |
+
|
| 452 |
+
return `${accountCode}_${cleanType}_${dateStr}.docx`;
|
| 453 |
+
}
|
| 454 |
+
```
|
| 455 |
+
|
| 456 |
+
### Word Template Population
|
| 457 |
+
Use a library like:
|
| 458 |
+
- **docx-templates** (npm package)
|
| 459 |
+
- **PizZip + Docxtemplater** (for browser-based generation)
|
| 460 |
+
- **mammoth.js** (for reading, then custom write logic)
|
| 461 |
+
|
| 462 |
+
Choose based on your tech stack and whether generation happens client-side or server-side.
|
| 463 |
+
|
| 464 |
+
### SharePoint List Write
|
| 465 |
+
If using SharePoint REST API:
|
| 466 |
+
```javascript
|
| 467 |
+
async function saveAgreementMetadata(data) {
|
| 468 |
+
const listUrl = `${siteUrl}/_api/web/lists/getbytitle('Agreements')/items`;
|
| 469 |
+
|
| 470 |
+
const response = await fetch(listUrl, {
|
| 471 |
+
method: 'POST',
|
| 472 |
+
headers: {
|
| 473 |
+
'Accept': 'application/json;odata=verbose',
|
| 474 |
+
'Content-Type': 'application/json;odata=verbose',
|
| 475 |
+
'X-RequestDigest': formDigestValue
|
| 476 |
+
},
|
| 477 |
+
body: JSON.stringify({
|
| 478 |
+
'__metadata': { 'type': 'SP.Data.AgreementsListItem' },
|
| 479 |
+
'Title': data.agreementId,
|
| 480 |
+
'AccountCode': data.accountCode,
|
| 481 |
+
'AgreementType': data.agreementType,
|
| 482 |
+
// ... all other fields
|
| 483 |
+
})
|
| 484 |
+
});
|
| 485 |
+
|
| 486 |
+
return await response.json();
|
| 487 |
+
}
|
| 488 |
+
```
|
| 489 |
+
|
| 490 |
+
---
|
| 491 |
+
|
| 492 |
+
## 12. IMPLEMENTATION CHECKLIST
|
| 493 |
+
|
| 494 |
+
### Phase 1: UI Updates
|
| 495 |
+
- [ ] Add Name and Producer Code columns to preview tables
|
| 496 |
+
- [ ] Reorder columns: Role | Name | Code | ABE | Agreement | Comm% | ProdCredit
|
| 497 |
+
- [ ] Update all year tables (Year 1, Year 2, Year 3+) with new columns
|
| 498 |
+
|
| 499 |
+
### Phase 2: Submit Safeguard
|
| 500 |
+
- [ ] Create modal/popup component for document preview
|
| 501 |
+
- [ ] Generate draft document on "Submit" click
|
| 502 |
+
- [ ] Display document in modal with scroll
|
| 503 |
+
- [ ] Add "Go Back & Edit" button (closes modal)
|
| 504 |
+
- [ ] Add "Confirm & Submit" button (saves and submits)
|
| 505 |
+
- [ ] Handle document generation errors gracefully
|
| 506 |
+
|
| 507 |
+
### Phase 3: Employee Originator Integration
|
| 508 |
+
- [ ] Implement Active Directory / employee search (your choice of method)
|
| 509 |
+
- [ ] Add People Picker for originator selection
|
| 510 |
+
- [ ] Auto-assign "EMPOR2" code for all employee originators
|
| 511 |
+
- [ ] Add employee to approval workflow
|
| 512 |
+
|
| 513 |
+
### Phase 4: Email Generation
|
| 514 |
+
- [ ] Implement email generation function (calculate or pull from AD)
|
| 515 |
+
- [ ] Generate emails for all participants (except Legacy and House Account)
|
| 516 |
+
- [ ] Store emails in agreement metadata for Power Automate
|
| 517 |
+
|
| 518 |
+
### Phase 5: Ceded Accounts - CE as Producer
|
| 519 |
+
- [ ] Remove CE checkbox for this agreement type
|
| 520 |
+
- [ ] Show CE field as required People Picker
|
| 521 |
+
- [ ] Update label to "Client Executive (Producer)"
|
| 522 |
+
- [ ] Update commission calculation (CE = 10%, not layer deduction)
|
| 523 |
+
- [ ] Update preview table to show CE as main participant
|
| 524 |
+
|
| 525 |
+
### Phase 6: House Agreement Fixes
|
| 526 |
+
- [ ] Fix layer deduction logic (subtract from 100%, don't add)
|
| 527 |
+
- [ ] Remove Producer 1 from preview table (keep in approval section)
|
| 528 |
+
- [ ] Test all layer combinations (none, CE only, CCP only, both)
|
| 529 |
+
|
| 530 |
+
### Phase 7: Life Origination All Years
|
| 531 |
+
- [ ] Update BASE_RULES to apply to all years
|
| 532 |
+
- [ ] Remove Year 2+ transition to House Standard
|
| 533 |
+
- [ ] Verify preview shows same structure for all years
|
| 534 |
+
|
| 535 |
+
### Phase 8: OneDrive Integration
|
| 536 |
+
- [ ] Implement Word document generation from template
|
| 537 |
+
- [ ] Populate all required template placeholders
|
| 538 |
+
- [ ] Implement file naming convention with sanitization
|
| 539 |
+
- [ ] Save generated document to OneDrive shared folder
|
| 540 |
+
- [ ] Handle file upload errors and retries
|
| 541 |
+
|
| 542 |
+
### Phase 9: Database/SharePoint Metadata
|
| 543 |
+
- [ ] Create SharePoint list with all required columns (or set up database)
|
| 544 |
+
- [ ] Implement save function for agreement metadata
|
| 545 |
+
- [ ] Store all participant info, emails, URLs
|
| 546 |
+
- [ ] Handle create and update operations
|
| 547 |
+
|
| 548 |
+
### Phase 10: Dashboard
|
| 549 |
+
- [ ] Query agreements for current user
|
| 550 |
+
- [ ] Display cards/rows for all agreement statuses
|
| 551 |
+
- [ ] Show summary table preview for each agreement
|
| 552 |
+
- [ ] Add links to draft PDF and final PDF
|
| 553 |
+
- [ ] Implement approval timeline display (in progress agreements)
|
| 554 |
+
- [ ] Add delete functionality (draft & finalized)
|
| 555 |
+
- [ ] Prevent deletion of in-progress agreements
|
| 556 |
+
|
| 557 |
+
---
|
| 558 |
+
|
| 559 |
+
## 13. TESTING SCENARIOS
|
| 560 |
+
|
| 561 |
+
### Scenario 1: Standard Agreement with Preview Check
|
| 562 |
+
1. Fill out Standard Agreement form
|
| 563 |
+
2. Click "Submit for Approval"
|
| 564 |
+
3. Verify modal appears with draft PDF
|
| 565 |
+
4. Click "Go Back & Edit"
|
| 566 |
+
5. Verify modal closes, form still populated
|
| 567 |
+
6. Make edit, click "Submit" again
|
| 568 |
+
7. Verify modal shows updated PDF
|
| 569 |
+
8. Click "Confirm & Submit"
|
| 570 |
+
9. Verify document saved to OneDrive with correct filename
|
| 571 |
+
10. Verify metadata saved to SharePoint
|
| 572 |
+
11. Verify draft appears in dashboard
|
| 573 |
+
|
| 574 |
+
### Scenario 2: Ceded Accounts - CE as Producer
|
| 575 |
+
1. Select "Ceded Accounts - CE as Producer"
|
| 576 |
+
2. Verify CE checkbox removed
|
| 577 |
+
3. Verify CE field appears as required People Picker
|
| 578 |
+
4. Select CE from People Picker
|
| 579 |
+
5. Add CCP layer (optional)
|
| 580 |
+
6. Verify preview table shows:
|
| 581 |
+
- CE as main participant (10% commission)
|
| 582 |
+
- CCP layer (5% commission, reducing CE to 5%)
|
| 583 |
+
7. Verify CE email generated correctly
|
| 584 |
+
|
| 585 |
+
### Scenario 3: House Agreement with Layers
|
| 586 |
+
1. Select "House Standard Agreement"
|
| 587 |
+
2. Select Producer 1 (for approval)
|
| 588 |
+
3. Check CE layer box, select CE
|
| 589 |
+
4. Check CCP layer box, select Mike Parsa
|
| 590 |
+
5. Verify preview table shows:
|
| 591 |
+
- House Account: 93% commission
|
| 592 |
+
- CE Layer: 2%
|
| 593 |
+
- CCP Layer: 5%
|
| 594 |
+
- Total: 100%
|
| 595 |
+
- Producer 1 NOT in table
|
| 596 |
+
6. Generate draft PDF
|
| 597 |
+
7. Verify Producer 1 appears in signature section
|
| 598 |
+
|
| 599 |
+
### Scenario 4: Life Origination All Years
|
| 600 |
+
1. Select "Life Origination Fee Agreement"
|
| 601 |
+
2. Fill out form
|
| 602 |
+
3. Verify preview shows 3 year tables
|
| 603 |
+
4. Verify all 3 years show:
|
| 604 |
+
- Producer 1: Life Origination Fee Agreement, 20%
|
| 605 |
+
5. Verify Year 2+ does NOT transition to House Standard
|
| 606 |
+
|
| 607 |
+
### Scenario 5: Employee Originator
|
| 608 |
+
1. Select "Referral / Origination Fee Agreement"
|
| 609 |
+
2. Fill Producer 1
|
| 610 |
+
3. Check "Employee as Originator"
|
| 611 |
+
4. Search for employee (not a producer)
|
| 612 |
+
5. Select employee
|
| 613 |
+
6. Verify code auto-assigned as "EMPOR2"
|
| 614 |
+
7. Verify employee email generated
|
| 615 |
+
8. Verify employee appears in approval section
|
| 616 |
+
|
| 617 |
+
### Scenario 6: Dashboard Display
|
| 618 |
+
1. Submit 3 agreements in different states:
|
| 619 |
+
- One draft (save without submitting)
|
| 620 |
+
- One pending (submitted, no approvals yet)
|
| 621 |
+
- One in progress (submitted, P1 approved, CE pending)
|
| 622 |
+
2. Navigate to dashboard
|
| 623 |
+
3. Verify all 3 appear
|
| 624 |
+
4. Verify pending shows approval timeline
|
| 625 |
+
5. Verify in-progress shows checkmarks and timestamps
|
| 626 |
+
6. Click draft PDF link - verify opens
|
| 627 |
+
7. Delete draft agreement - verify removed
|
| 628 |
+
8. Attempt to delete in-progress - verify blocked
|
| 629 |
+
|
| 630 |
+
### Scenario 7: OneDrive & Power Automate Integration
|
| 631 |
+
1. Submit agreement "GEMCONT-01", "PhD Agreement", effective date 2024-03-15
|
| 632 |
+
2. Verify file appears in OneDrive as: `GEMCONT-01_PhDAgreement_2024-03-15.docx`
|
| 633 |
+
3. Verify Power Automate flow triggers (check flow run history)
|
| 634 |
+
4. Verify SharePoint list contains agreement metadata
|
| 635 |
+
5. Simulate approval in Power Automate
|
| 636 |
+
6. Verify dashboard updates with approval status
|
| 637 |
+
7. Verify final PDF appears in dashboard once flow completes
|
| 638 |
+
|
| 639 |
+
---
|
| 640 |
+
|
| 641 |
+
## 14. POWER AUTOMATE INTEGRATION REQUIREMENTS
|
| 642 |
+
|
| 643 |
+
**What the Web App Must Provide for Power Automate:**
|
| 644 |
+
|
| 645 |
+
1. **Trigger File in OneDrive**
|
| 646 |
+
- Correct naming convention
|
| 647 |
+
- Valid Word document with placeholders filled
|
| 648 |
+
|
| 649 |
+
2. **SharePoint Metadata Record**
|
| 650 |
+
- All participant names, codes, emails
|
| 651 |
+
- Agreement details (account, type, dates)
|
| 652 |
+
- Status field for Power Automate to update
|
| 653 |
+
- URLs for draft and final documents
|
| 654 |
+
|
| 655 |
+
3. **Consistent Data Structure**
|
| 656 |
+
- Agreement IDs must be unique and predictable
|
| 657 |
+
- Email addresses must be valid and formatted correctly
|
| 658 |
+
- All required fields populated (no nulls where not allowed)
|
| 659 |
+
|
| 660 |
+
**What Power Automate Will Handle:**
|
| 661 |
+
|
| 662 |
+
1. Detect new draft document in OneDrive
|
| 663 |
+
2. Query SharePoint for agreement metadata
|
| 664 |
+
3. Send approval emails to all participants
|
| 665 |
+
4. Track approval responses and timestamps
|
| 666 |
+
5. Update SharePoint with approval status
|
| 667 |
+
6. Generate final PDF with signatures
|
| 668 |
+
7. Upload final PDF to separate OneDrive folder
|
| 669 |
+
8. Update SharePoint with final PDF URL
|
| 670 |
+
9. Update status to "Finalized"
|
| 671 |
+
10. Send final PDF to accounting@snellingswalters.com and payroll@snellingswalters.com
|
| 672 |
+
|
| 673 |
+
**Critical:** Ensure the web app's data structure matches what Power Automate expects. Coordinate on SharePoint column names, data types, and OneDrive folder paths.
|
| 674 |
+
|
| 675 |
+
---
|
| 676 |
+
|
| 677 |
+
## 15. PRIORITY ORDER
|
| 678 |
+
|
| 679 |
+
If implementing incrementally:
|
| 680 |
+
|
| 681 |
+
1. **Preview table updates** (add Name, Code columns) - Quick visual improvement
|
| 682 |
+
2. **Submit safeguard modal** - Critical for validation
|
| 683 |
+
3. **Email generation** - Required for Power Automate
|
| 684 |
+
4. **OneDrive file generation & upload** - Core integration
|
| 685 |
+
5. **SharePoint metadata storage** - Backup and data source
|
| 686 |
+
6. **Dashboard display** - User visibility
|
| 687 |
+
7. **Bug fixes** (House layers, Life all years, CE as producer) - Correct calculations
|
| 688 |
+
8. **Employee originator** - Specific feature
|
| 689 |
+
9. **Approval timeline** - Nice-to-have enhancement
|
| 690 |
+
10. **Delete functionality** - Organization feature
|
| 691 |
+
|
| 692 |
+
---
|
| 693 |
+
|
| 694 |
+
## QUESTIONS TO CLARIFY BEFORE STARTING
|
| 695 |
+
|
| 696 |
+
If anything needs clarification:
|
| 697 |
+
- OneDrive authentication method (service account vs. user delegation)
|
| 698 |
+
- SharePoint site URL and list structure preferences
|
| 699 |
+
- Word document generation library preference
|
| 700 |
+
- Frontend framework being used (React, Vue, vanilla JS?)
|
| 701 |
+
- Whether backend API exists or all client-side
|
| 702 |
+
- Authentication/user context (how do you get current user?)
|
| 703 |
+
|
| 704 |
+
Let me know if you need any clarification on the requirements!
|
|
Binary file (17.9 kB). View file
|
|
|
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Word Document Generator for Commission Agreement Intake Form.
|
| 3 |
+
Populates the DRAFT or FINAL .docx template with form/summary data.
|
| 4 |
+
|
| 5 |
+
Templates are stored in Feedback/ folder:
|
| 6 |
+
- Commission_Agreement_DRAFT_Template.docx
|
| 7 |
+
- Commission_Agreement_FINAL_Template.docx
|
| 8 |
+
|
| 9 |
+
Template structure (tables indexed from 0):
|
| 10 |
+
Table 0: Header β {{AgreementType}}, {{AgreementId}}
|
| 11 |
+
Table 1: Account Information (empty cells to fill programmatically)
|
| 12 |
+
Table 2: Year 1 label β {{Year1Label}}
|
| 13 |
+
Table 3: Year 1 data β {{Year1RowN_*}} (4 data rows + totals)
|
| 14 |
+
Table 4: Year 2 label β {{Year2Label}}
|
| 15 |
+
Table 5: Year 2 data β {{Year2RowN_*}}
|
| 16 |
+
Table 6: Year 3 label β {{Year3Label}}
|
| 17 |
+
Table 7: Year 3 data β {{Year3RowN_*}}
|
| 18 |
+
Table 8: Signers 1 & 2
|
| 19 |
+
Table 9: Signers 3 & 4
|
| 20 |
+
|
| 21 |
+
Paragraphs: {{EffectiveDate}}, {{BellRingerDisplay}}, {{AgreementType}},
|
| 22 |
+
{{SubmittedBy}}, {{SubmittedOn}}, {{Year2SectionStart/End}},
|
| 23 |
+
{{Year3SectionStart/End}}
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import os
|
| 27 |
+
import re
|
| 28 |
+
from copy import deepcopy
|
| 29 |
+
from io import BytesIO
|
| 30 |
+
from datetime import datetime
|
| 31 |
+
|
| 32 |
+
from docx import Document
|
| 33 |
+
from docx.shared import Pt
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
# ββ Template paths ββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "Feedback")
|
| 38 |
+
DRAFT_TEMPLATE = os.path.join(TEMPLATE_DIR, "Commission_Agreement_DRAFT_Template.docx")
|
| 39 |
+
FINAL_TEMPLATE = os.path.join(TEMPLATE_DIR, "Commission_Agreement_FINAL_Template.docx")
|
| 40 |
+
|
| 41 |
+
# Department code β full name
|
| 42 |
+
DEPT_NAMES = {
|
| 43 |
+
"BD": "BD - Bond", "CL": "CL - Commercial Lines",
|
| 44 |
+
"CON": "CON - Connect", "EB": "EB - Employee Benefits",
|
| 45 |
+
"PL": "PL - Personal Lines", "VAN": "VAN - Vanguard", "LIF": "LIF - Life",
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _replace_in_paragraph(paragraph, old, new):
|
| 50 |
+
"""Replace placeholder text in a paragraph, handling single-run placeholders."""
|
| 51 |
+
for run in paragraph.runs:
|
| 52 |
+
if old in run.text:
|
| 53 |
+
run.text = run.text.replace(old, str(new) if new is not None else "")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _replace_in_cell(cell, old, new):
|
| 57 |
+
"""Replace placeholder text across all paragraphs in a table cell."""
|
| 58 |
+
for paragraph in cell.paragraphs:
|
| 59 |
+
_replace_in_paragraph(paragraph, old, new)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _replace_in_table(table, old, new):
|
| 63 |
+
"""Replace placeholder text across all cells in a table."""
|
| 64 |
+
for row in table.rows:
|
| 65 |
+
for cell in row.cells:
|
| 66 |
+
_replace_in_cell(cell, old, new)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _replace_all(doc, old, new):
|
| 70 |
+
"""Replace placeholder in all paragraphs and tables."""
|
| 71 |
+
for paragraph in doc.paragraphs:
|
| 72 |
+
_replace_in_paragraph(paragraph, old, new)
|
| 73 |
+
for table in doc.tables:
|
| 74 |
+
_replace_in_table(table, old, new)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _fill_account_info(table, form_data, bell_ringer):
|
| 78 |
+
"""Fill the Account Information table (Table 1) β it starts empty."""
|
| 79 |
+
# Table 1 has 1 row x 2 cells; we'll add text content
|
| 80 |
+
if len(table.rows) == 0:
|
| 81 |
+
return
|
| 82 |
+
|
| 83 |
+
cell_left = table.rows[0].cells[0]
|
| 84 |
+
cell_right = table.rows[0].cells[1]
|
| 85 |
+
|
| 86 |
+
client_code = form_data.get("client_code", "β") or "β"
|
| 87 |
+
client_name = form_data.get("client_name", "β") or "β"
|
| 88 |
+
dept_code = form_data.get("dept_code", "")
|
| 89 |
+
dept_display = DEPT_NAMES.get(dept_code, dept_code) or "β"
|
| 90 |
+
association = form_data.get("association", "β") or "β"
|
| 91 |
+
revenue = form_data.get("revenue", 0) or 0
|
| 92 |
+
legacy = form_data.get("legacy_producer", "β") or "β"
|
| 93 |
+
|
| 94 |
+
# Left cell: Account Code, Account Name, Department
|
| 95 |
+
left_lines = [
|
| 96 |
+
f"Account Code: {client_code}",
|
| 97 |
+
f"Account Name: {client_name}",
|
| 98 |
+
f"Department: {dept_display}",
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
# Right cell: Association, Revenue, Legacy Producer
|
| 102 |
+
revenue_display = f"${revenue:,.0f}" if revenue else "β"
|
| 103 |
+
right_lines = [
|
| 104 |
+
f"Association: {association}",
|
| 105 |
+
f"Estimated Revenue: {revenue_display}",
|
| 106 |
+
f"Legacy Producer: {legacy}",
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
# Clear existing content and add lines
|
| 110 |
+
for p in cell_left.paragraphs:
|
| 111 |
+
p.clear()
|
| 112 |
+
for p in cell_right.paragraphs:
|
| 113 |
+
p.clear()
|
| 114 |
+
|
| 115 |
+
# Write to left cell
|
| 116 |
+
for i, line in enumerate(left_lines):
|
| 117 |
+
if i == 0:
|
| 118 |
+
cell_left.paragraphs[0].text = line
|
| 119 |
+
else:
|
| 120 |
+
cell_left.add_paragraph(line)
|
| 121 |
+
# Style
|
| 122 |
+
for p in cell_left.paragraphs:
|
| 123 |
+
for run in p.runs:
|
| 124 |
+
run.font.size = Pt(9)
|
| 125 |
+
|
| 126 |
+
# Write to right cell
|
| 127 |
+
for i, line in enumerate(right_lines):
|
| 128 |
+
if i == 0:
|
| 129 |
+
cell_right.paragraphs[0].text = line
|
| 130 |
+
else:
|
| 131 |
+
cell_right.add_paragraph(line)
|
| 132 |
+
for p in cell_right.paragraphs:
|
| 133 |
+
for run in p.runs:
|
| 134 |
+
run.font.size = Pt(9)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def _fill_year_table(doc, year_index, year_data, year_tables):
|
| 138 |
+
"""Fill a year's label table and data table.
|
| 139 |
+
|
| 140 |
+
year_index: 0=Year1, 1=Year2, 2=Year3
|
| 141 |
+
year_data: dict with year_label and rows from compute_summary, or None
|
| 142 |
+
"""
|
| 143 |
+
prefix = f"Year{year_index + 1}"
|
| 144 |
+
label_table_idx = 2 + year_index * 2 # Tables 2, 4, 6
|
| 145 |
+
data_table_idx = 3 + year_index * 2 # Tables 3, 5, 7
|
| 146 |
+
|
| 147 |
+
if year_data:
|
| 148 |
+
# Fill year label
|
| 149 |
+
label = year_data.get("year_label", f"Year {year_index + 1}")
|
| 150 |
+
_replace_in_table(doc.tables[label_table_idx], f"{{{{{prefix}Label}}}}", label)
|
| 151 |
+
|
| 152 |
+
# Fill data rows (up to 4)
|
| 153 |
+
rows = year_data.get("rows", [])
|
| 154 |
+
for ri in range(4):
|
| 155 |
+
row_prefix = f"{prefix}Row{ri + 1}"
|
| 156 |
+
if ri < len(rows):
|
| 157 |
+
r = rows[ri]
|
| 158 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}_Role}}}}", r.get("role", ""))
|
| 159 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}_Name}}}}", r.get("name", "") or "")
|
| 160 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}_Code}}}}", r.get("code", "") or "")
|
| 161 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}_Type}}}}", r.get("abe_status", ""))
|
| 162 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}_Agreement}}}}", r.get("agreement_type", ""))
|
| 163 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}_CommPct}}}}", f"{r.get('commission_pct', 0)}%")
|
| 164 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}_ProdCredit}}}}", f"{r.get('production_pct', 0)}%")
|
| 165 |
+
else:
|
| 166 |
+
# Clear unused rows
|
| 167 |
+
for field in ["_Role", "_Name", "_Code", "_Type", "_Agreement", "_CommPct", "_ProdCredit"]:
|
| 168 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}{field}}}}}", "")
|
| 169 |
+
|
| 170 |
+
# Total
|
| 171 |
+
total_comm = sum(r.get("commission_pct", 0) for r in rows)
|
| 172 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{prefix}Total}}}}", f"{total_comm}%")
|
| 173 |
+
else:
|
| 174 |
+
# No data for this year β clear all placeholders
|
| 175 |
+
_replace_in_table(doc.tables[label_table_idx], f"{{{{{prefix}Label}}}}", "")
|
| 176 |
+
for ri in range(4):
|
| 177 |
+
row_prefix = f"{prefix}Row{ri + 1}"
|
| 178 |
+
for field in ["_Role", "_Name", "_Code", "_Type", "_Agreement", "_CommPct", "_ProdCredit"]:
|
| 179 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{row_prefix}{field}}}}}", "")
|
| 180 |
+
_replace_in_table(doc.tables[data_table_idx], f"{{{{{prefix}Total}}}}", "")
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _fill_signers(doc, participants, approvals, is_draft):
|
| 184 |
+
"""Fill signer blocks in Tables 8 and 9 (up to 4 signers)."""
|
| 185 |
+
# DRAFT template: SignerN_Name, SignerN_RoleAgreement (+ Signature/Date lines)
|
| 186 |
+
# FINAL template: SignerN_Name, SignerN_RoleAgreement, SignerN_Status, SignerN_AcknowledgedOn
|
| 187 |
+
|
| 188 |
+
approval_map = {}
|
| 189 |
+
if approvals:
|
| 190 |
+
for a in approvals:
|
| 191 |
+
code = a.get("producer_code", "")
|
| 192 |
+
if code:
|
| 193 |
+
approval_map[code] = a
|
| 194 |
+
|
| 195 |
+
for i in range(4):
|
| 196 |
+
signer_num = i + 1
|
| 197 |
+
name_key = f"{{{{Signer{signer_num}_Name}}}}"
|
| 198 |
+
role_key = f"{{{{Signer{signer_num}_RoleAgreement}}}}"
|
| 199 |
+
|
| 200 |
+
if i < len(participants):
|
| 201 |
+
p = participants[i]
|
| 202 |
+
_replace_all(doc, name_key, p.get("name", ""))
|
| 203 |
+
_replace_all(doc, role_key, f"{p.get('role', '')} β {p.get('code', '')}")
|
| 204 |
+
|
| 205 |
+
# FINAL template fields
|
| 206 |
+
if not is_draft:
|
| 207 |
+
status_key = f"{{{{Signer{signer_num}_Status}}}}"
|
| 208 |
+
date_key = f"{{{{Signer{signer_num}_AcknowledgedOn}}}}"
|
| 209 |
+
a = approval_map.get(p.get("code", ""), {})
|
| 210 |
+
status = a.get("status", "pending")
|
| 211 |
+
if status == "approved":
|
| 212 |
+
_replace_all(doc, status_key, "Acknowledged")
|
| 213 |
+
elif status == "rejected":
|
| 214 |
+
_replace_all(doc, status_key, "Rejected")
|
| 215 |
+
else:
|
| 216 |
+
_replace_all(doc, status_key, "Pending")
|
| 217 |
+
responded = a.get("responded_at")
|
| 218 |
+
if responded and isinstance(responded, str):
|
| 219 |
+
_replace_all(doc, date_key, responded)
|
| 220 |
+
elif responded and hasattr(responded, "strftime"):
|
| 221 |
+
_replace_all(doc, date_key, responded.strftime("%m/%d/%Y %I:%M %p"))
|
| 222 |
+
else:
|
| 223 |
+
_replace_all(doc, date_key, "β")
|
| 224 |
+
else:
|
| 225 |
+
_replace_all(doc, name_key, "")
|
| 226 |
+
_replace_all(doc, role_key, "")
|
| 227 |
+
if not is_draft:
|
| 228 |
+
_replace_all(doc, f"{{{{Signer{signer_num}_Status}}}}", "")
|
| 229 |
+
_replace_all(doc, f"{{{{Signer{signer_num}_AcknowledgedOn}}}}", "")
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _remove_year_section(doc, year_num):
|
| 233 |
+
"""Remove paragraphs and tables for unused year sections.
|
| 234 |
+
|
| 235 |
+
Looks for {{YearNSectionStart}} / {{YearNSectionEnd}} markers and
|
| 236 |
+
hides content between them by clearing text.
|
| 237 |
+
"""
|
| 238 |
+
start_marker = f"{{{{Year{year_num}SectionStart}}}}"
|
| 239 |
+
end_marker = f"{{{{Year{year_num}SectionEnd}}}}"
|
| 240 |
+
|
| 241 |
+
# Just clear the marker paragraphs (the tables between them will have
|
| 242 |
+
# empty placeholders already cleared by _fill_year_table)
|
| 243 |
+
for p in doc.paragraphs:
|
| 244 |
+
if start_marker in p.text or end_marker in p.text:
|
| 245 |
+
p.text = ""
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def generate_docx(form_data: dict, summary_data: dict,
|
| 249 |
+
approvals: list = None, is_draft: bool = True,
|
| 250 |
+
submission_id: int = None,
|
| 251 |
+
submitter_name: str = None) -> bytes:
|
| 252 |
+
"""
|
| 253 |
+
Generate a branded Word document from the template.
|
| 254 |
+
|
| 255 |
+
Returns: DOCX file as bytes.
|
| 256 |
+
"""
|
| 257 |
+
template_path = DRAFT_TEMPLATE if is_draft else FINAL_TEMPLATE
|
| 258 |
+
if not os.path.exists(template_path):
|
| 259 |
+
raise FileNotFoundError(f"Template not found: {template_path}")
|
| 260 |
+
|
| 261 |
+
doc = Document(template_path)
|
| 262 |
+
|
| 263 |
+
agreement_name = summary_data.get("agreement_name", "β")
|
| 264 |
+
year_tables = summary_data.get("year_tables", [])
|
| 265 |
+
participants = summary_data.get("participants", [])
|
| 266 |
+
bell_ringer = summary_data.get("bell_ringer", {})
|
| 267 |
+
|
| 268 |
+
# ββ Global paragraph replacements βββββββββββββββββββββ
|
| 269 |
+
effective_date = form_data.get("effective_date", "β") or "β"
|
| 270 |
+
_replace_all(doc, "{{EffectiveDate}}", effective_date)
|
| 271 |
+
_replace_all(doc, "{{AgreementType}}", agreement_name)
|
| 272 |
+
|
| 273 |
+
# Agreement ID
|
| 274 |
+
agr_id = f"CA-{submission_id:04d}" if submission_id else "CA-0000"
|
| 275 |
+
_replace_all(doc, "{{AgreementId}}", agr_id)
|
| 276 |
+
|
| 277 |
+
# Bell Ringer
|
| 278 |
+
bell_status = bell_ringer.get("status", "")
|
| 279 |
+
if bell_status == "triggered":
|
| 280 |
+
bell_display = f"BELL RINGER TRIGGERED (threshold ${bell_ringer.get('threshold', 0):,.0f})"
|
| 281 |
+
elif bell_status == "not_triggered":
|
| 282 |
+
bell_display = f"Bell Ringer: not triggered (threshold ${bell_ringer.get('threshold', 0):,.0f})"
|
| 283 |
+
else:
|
| 284 |
+
bell_display = "Bell Ringer: N/A"
|
| 285 |
+
_replace_all(doc, "{{BellRingerDisplay}}", bell_display)
|
| 286 |
+
|
| 287 |
+
# Submitter info
|
| 288 |
+
_replace_all(doc, "{{SubmittedBy}}", submitter_name or "β")
|
| 289 |
+
_replace_all(doc, "{{SubmittedOn}}", datetime.now().strftime("%m/%d/%Y %I:%M %p"))
|
| 290 |
+
|
| 291 |
+
# ββ Account Information (Table 1) βββββββββββββββββββββ
|
| 292 |
+
if len(doc.tables) > 1:
|
| 293 |
+
_fill_account_info(doc.tables[1], form_data, bell_ringer)
|
| 294 |
+
|
| 295 |
+
# ββ Year tables (Tables 2-7) ββββββββββββββββββββββββββ
|
| 296 |
+
for yi in range(3):
|
| 297 |
+
year_data = year_tables[yi] if yi < len(year_tables) else None
|
| 298 |
+
_fill_year_table(doc, yi, year_data, year_tables)
|
| 299 |
+
|
| 300 |
+
# Remove unused year sections
|
| 301 |
+
if len(year_tables) < 2:
|
| 302 |
+
_remove_year_section(doc, 2)
|
| 303 |
+
if len(year_tables) < 3:
|
| 304 |
+
_remove_year_section(doc, 3)
|
| 305 |
+
|
| 306 |
+
# Clear section markers for visible years too
|
| 307 |
+
for yn in [2, 3]:
|
| 308 |
+
_replace_all(doc, f"{{{{Year{yn}SectionStart}}}}", "")
|
| 309 |
+
_replace_all(doc, f"{{{{Year{yn}SectionEnd}}}}", "")
|
| 310 |
+
|
| 311 |
+
# ββ Signers (Tables 8-9) βββββββββββββββββββββββββββββ
|
| 312 |
+
_fill_signers(doc, participants, approvals, is_draft)
|
| 313 |
+
|
| 314 |
+
# ββ Save to bytes βββββββββββββββββββββββββββββββββββββ
|
| 315 |
+
buf = BytesIO()
|
| 316 |
+
doc.save(buf)
|
| 317 |
+
return buf.getvalue()
|
|
@@ -194,6 +194,7 @@ class IntakeSubmission(Base):
|
|
| 194 |
|
| 195 |
bell_ringer_triggered = Column(Boolean, default=False)
|
| 196 |
dept_code = Column(String(10), nullable=True)
|
|
|
|
| 197 |
|
| 198 |
approvals = relationship("Approval", back_populates="submission",
|
| 199 |
cascade="all, delete-orphan")
|
|
|
|
| 194 |
|
| 195 |
bell_ringer_triggered = Column(Boolean, default=False)
|
| 196 |
dept_code = Column(String(10), nullable=True)
|
| 197 |
+
hidden = Column(Boolean, default=False) # soft-delete for finalized
|
| 198 |
|
| 199 |
approvals = relationship("Approval", back_populates="submission",
|
| 200 |
cascade="all, delete-orphan")
|
|
@@ -395,18 +395,20 @@ def _field_pair(label, value, styles):
|
|
| 395 |
|
| 396 |
# ββ Per-year table builder βββββββββββββββββββββββββββββββ
|
| 397 |
def _build_year_table(year_table_data, styles, page_width):
|
| 398 |
-
"""Build a single year's commission table with
|
| 399 |
-
Role |
|
| 400 |
"""
|
| 401 |
year_label = year_table_data.get("year_label", "")
|
| 402 |
rows = year_table_data.get("rows", [])
|
| 403 |
|
| 404 |
col_widths = [
|
| 405 |
-
page_width * 0.
|
| 406 |
-
page_width * 0.
|
| 407 |
-
page_width * 0.
|
| 408 |
-
page_width * 0.
|
| 409 |
-
page_width * 0.
|
|
|
|
|
|
|
| 410 |
]
|
| 411 |
|
| 412 |
# Year label header
|
|
@@ -417,16 +419,20 @@ def _build_year_table(year_table_data, styles, page_width):
|
|
| 417 |
# Table header row
|
| 418 |
header = [
|
| 419 |
Paragraph("<b>Role</b>", styles["TableCellBold"]),
|
|
|
|
|
|
|
|
|
|
| 420 |
Paragraph("<b>Agreement Type</b>", styles["TableCellBold"]),
|
| 421 |
-
Paragraph("<b>
|
| 422 |
-
Paragraph("<b>
|
| 423 |
-
Paragraph("<b>ABE Status</b>", styles["TableCellBold"]),
|
| 424 |
]
|
| 425 |
|
| 426 |
data = [header]
|
| 427 |
|
| 428 |
for row in rows:
|
| 429 |
role = row.get("role", "")
|
|
|
|
|
|
|
| 430 |
agr_type = row.get("agreement_type", "β")
|
| 431 |
comm_pct = row.get("commission_pct")
|
| 432 |
prod_pct = row.get("production_pct")
|
|
@@ -443,10 +449,12 @@ def _build_year_table(year_table_data, styles, page_width):
|
|
| 443 |
|
| 444 |
data.append([
|
| 445 |
Paragraph(f"<b>{role}</b>", styles["TableCell"]),
|
|
|
|
|
|
|
|
|
|
| 446 |
Paragraph(agr_type, styles["TableCell"]),
|
| 447 |
Paragraph(f"<b>{comm_text}</b>", styles["TableCell"]),
|
| 448 |
Paragraph(prod_text, styles["TableCell"]),
|
| 449 |
-
Paragraph(abe_text, styles["TableCell"]),
|
| 450 |
])
|
| 451 |
|
| 452 |
t = Table(data, colWidths=col_widths, repeatRows=1)
|
|
|
|
| 395 |
|
| 396 |
# ββ Per-year table builder βββββββββββββββββββββββββββββββ
|
| 397 |
def _build_year_table(year_table_data, styles, page_width):
|
| 398 |
+
"""Build a single year's commission table with 7 columns:
|
| 399 |
+
Role | Name | Code | ABE Status | Agreement Type | Commission % | Prod Credit
|
| 400 |
"""
|
| 401 |
year_label = year_table_data.get("year_label", "")
|
| 402 |
rows = year_table_data.get("rows", [])
|
| 403 |
|
| 404 |
col_widths = [
|
| 405 |
+
page_width * 0.14, # Role
|
| 406 |
+
page_width * 0.16, # Name
|
| 407 |
+
page_width * 0.10, # Code
|
| 408 |
+
page_width * 0.10, # ABE Status
|
| 409 |
+
page_width * 0.22, # Agreement Type
|
| 410 |
+
page_width * 0.14, # Commission %
|
| 411 |
+
page_width * 0.14, # Prod Credit
|
| 412 |
]
|
| 413 |
|
| 414 |
# Year label header
|
|
|
|
| 419 |
# Table header row
|
| 420 |
header = [
|
| 421 |
Paragraph("<b>Role</b>", styles["TableCellBold"]),
|
| 422 |
+
Paragraph("<b>Name</b>", styles["TableCellBold"]),
|
| 423 |
+
Paragraph("<b>Code</b>", styles["TableCellBold"]),
|
| 424 |
+
Paragraph("<b>ABE</b>", styles["TableCellBold"]),
|
| 425 |
Paragraph("<b>Agreement Type</b>", styles["TableCellBold"]),
|
| 426 |
+
Paragraph("<b>Comm %</b>", styles["TableCellBold"]),
|
| 427 |
+
Paragraph("<b>Prod Credit</b>", styles["TableCellBold"]),
|
|
|
|
| 428 |
]
|
| 429 |
|
| 430 |
data = [header]
|
| 431 |
|
| 432 |
for row in rows:
|
| 433 |
role = row.get("role", "")
|
| 434 |
+
name = row.get("name", "β") or "β"
|
| 435 |
+
code = row.get("code", "β") or "β"
|
| 436 |
agr_type = row.get("agreement_type", "β")
|
| 437 |
comm_pct = row.get("commission_pct")
|
| 438 |
prod_pct = row.get("production_pct")
|
|
|
|
| 449 |
|
| 450 |
data.append([
|
| 451 |
Paragraph(f"<b>{role}</b>", styles["TableCell"]),
|
| 452 |
+
Paragraph(name, styles["TableCell"]),
|
| 453 |
+
Paragraph(code, styles["TableCell"]),
|
| 454 |
+
Paragraph(abe_text, styles["TableCell"]),
|
| 455 |
Paragraph(agr_type, styles["TableCell"]),
|
| 456 |
Paragraph(f"<b>{comm_text}</b>", styles["TableCell"]),
|
| 457 |
Paragraph(prod_text, styles["TableCell"]),
|
|
|
|
| 458 |
])
|
| 459 |
|
| 460 |
t = Table(data, colWidths=col_widths, repeatRows=1)
|
|
@@ -12,6 +12,7 @@ from app.models import (
|
|
| 12 |
)
|
| 13 |
from app.rules_engine import compute_summary, check_bell_ringer
|
| 14 |
from app.pdf_generator import generate_pdf
|
|
|
|
| 15 |
|
| 16 |
router = APIRouter()
|
| 17 |
templates = Jinja2Templates(directory="app/templates")
|
|
@@ -98,7 +99,19 @@ async def api_check_bell(request: Request,
|
|
| 98 |
return JSONResponse(result)
|
| 99 |
|
| 100 |
|
| 101 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
if not producer_code:
|
| 103 |
return None
|
| 104 |
producer = db.query(Producer).filter(
|
|
@@ -107,6 +120,10 @@ def _lookup_producer_email(db: Session, producer_code: str) -> str:
|
|
| 107 |
).first()
|
| 108 |
if producer and producer.email:
|
| 109 |
return producer.email
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
return None
|
| 111 |
|
| 112 |
|
|
@@ -180,7 +197,7 @@ async def api_submit_form(request: Request,
|
|
| 180 |
continue
|
| 181 |
|
| 182 |
approval_token = uuid.uuid4().hex
|
| 183 |
-
approver_email = _lookup_producer_email(db, code)
|
| 184 |
if not approver_email:
|
| 185 |
emails_missing.append(f"{name} ({code})")
|
| 186 |
|
|
@@ -221,6 +238,10 @@ async def api_submit_form(request: Request,
|
|
| 221 |
db.refresh(submission)
|
| 222 |
|
| 223 |
# Generate DRAFT PDF
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
try:
|
| 225 |
pdf_bytes = generate_pdf(
|
| 226 |
form_data=body,
|
|
@@ -229,10 +250,7 @@ async def api_submit_form(request: Request,
|
|
| 229 |
is_draft=True,
|
| 230 |
submission_id=submission.id,
|
| 231 |
)
|
| 232 |
-
|
| 233 |
-
import os
|
| 234 |
-
os.makedirs(pdf_dir, exist_ok=True)
|
| 235 |
-
pdf_path = os.path.join(pdf_dir, f"submission_{submission.id}_draft.pdf")
|
| 236 |
with open(pdf_path, "wb") as f:
|
| 237 |
f.write(pdf_bytes)
|
| 238 |
pdf_generated = True
|
|
@@ -242,6 +260,26 @@ async def api_submit_form(request: Request,
|
|
| 242 |
traceback.print_exc()
|
| 243 |
pdf_generated = False
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
# Alert admin about missing emails
|
| 246 |
if emails_missing:
|
| 247 |
from app.email_service import send_missing_emails_alert
|
|
@@ -262,6 +300,7 @@ async def api_submit_form(request: Request,
|
|
| 262 |
"success": True,
|
| 263 |
"submission_id": submission.id,
|
| 264 |
"pdf_available": pdf_generated,
|
|
|
|
| 265 |
"approvals_count": len(approvals_created),
|
| 266 |
"emails_sent": emails_sent,
|
| 267 |
"emails_missing": emails_missing,
|
|
@@ -328,6 +367,75 @@ async def download_pdf(submission_id: int,
|
|
| 328 |
)
|
| 329 |
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
@router.get("/api/submission/{submission_id}")
|
| 332 |
async def get_submission(submission_id: int,
|
| 333 |
request: Request,
|
|
@@ -418,6 +526,24 @@ async def delete_draft(draft_id: int,
|
|
| 418 |
return JSONResponse({"success": True})
|
| 419 |
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
# =====================================================================
|
| 422 |
# USER DASHBOARD
|
| 423 |
# =====================================================================
|
|
@@ -430,17 +556,23 @@ async def user_dashboard(request: Request,
|
|
| 430 |
|
| 431 |
submissions = db.query(IntakeSubmission).filter(
|
| 432 |
IntakeSubmission.submitted_by == current_user.id,
|
|
|
|
| 433 |
).order_by(IntakeSubmission.updated_at.desc()).all()
|
| 434 |
|
| 435 |
drafts = []
|
| 436 |
submitted = []
|
| 437 |
|
| 438 |
for s in submissions:
|
|
|
|
|
|
|
|
|
|
| 439 |
if s.status == "draft":
|
| 440 |
drafts.append({
|
| 441 |
"id": s.id,
|
| 442 |
"title": s.title or "Untitled Draft",
|
| 443 |
"updated_at": s.updated_at.strftime("%b %d, %Y %I:%M %p") if s.updated_at else "β",
|
|
|
|
|
|
|
| 444 |
})
|
| 445 |
else:
|
| 446 |
approvals = db.query(Approval).filter(
|
|
@@ -450,19 +582,26 @@ async def user_dashboard(request: Request,
|
|
| 450 |
pending_count = sum(1 for a in approvals if a.status == "pending")
|
| 451 |
rejected_count = sum(1 for a in approvals if a.status == "rejected")
|
| 452 |
|
|
|
|
|
|
|
|
|
|
| 453 |
submitted.append({
|
| 454 |
"id": s.id,
|
| 455 |
"title": s.title or f"Submission #{s.id}",
|
| 456 |
"status": s.status,
|
| 457 |
"submitted_at": s.submitted_at.strftime("%b %d, %Y %I:%M %p") if s.submitted_at else "β",
|
| 458 |
"bell_ringer": s.bell_ringer_triggered,
|
|
|
|
|
|
|
| 459 |
"total_approvals": len(approvals),
|
| 460 |
"approved_count": approved_count,
|
| 461 |
"pending_count": pending_count,
|
| 462 |
"rejected_count": rejected_count,
|
|
|
|
| 463 |
"approvals": [{
|
| 464 |
"role_label": a.role_label,
|
| 465 |
"approver_name": a.approver_name,
|
|
|
|
| 466 |
"status": a.status,
|
| 467 |
"responded_at": a.responded_at.strftime("%b %d, %Y %I:%M %p") if a.responded_at else None,
|
| 468 |
} for a in approvals],
|
|
|
|
| 12 |
)
|
| 13 |
from app.rules_engine import compute_summary, check_bell_ringer
|
| 14 |
from app.pdf_generator import generate_pdf
|
| 15 |
+
from app.docx_generator import generate_docx
|
| 16 |
|
| 17 |
router = APIRouter()
|
| 18 |
templates = Jinja2Templates(directory="app/templates")
|
|
|
|
| 99 |
return JSONResponse(result)
|
| 100 |
|
| 101 |
|
| 102 |
+
def _generate_email_from_name(full_name: str) -> str:
|
| 103 |
+
"""Generate email as FirstInitial + LastName @ snellingswalters.com"""
|
| 104 |
+
if not full_name or full_name.lower() == 'house account':
|
| 105 |
+
return None
|
| 106 |
+
parts = full_name.strip().split()
|
| 107 |
+
if len(parts) < 2:
|
| 108 |
+
return None
|
| 109 |
+
first_initial = parts[0][0].upper()
|
| 110 |
+
last_name = parts[-1].capitalize()
|
| 111 |
+
return f"{first_initial}{last_name}@snellingswalters.com"
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _lookup_producer_email(db: Session, producer_code: str, producer_name: str = None) -> str:
|
| 115 |
if not producer_code:
|
| 116 |
return None
|
| 117 |
producer = db.query(Producer).filter(
|
|
|
|
| 120 |
).first()
|
| 121 |
if producer and producer.email:
|
| 122 |
return producer.email
|
| 123 |
+
# Fallback: generate from name
|
| 124 |
+
name = (producer.name if producer else producer_name) or producer_name
|
| 125 |
+
if name:
|
| 126 |
+
return _generate_email_from_name(name)
|
| 127 |
return None
|
| 128 |
|
| 129 |
|
|
|
|
| 197 |
continue
|
| 198 |
|
| 199 |
approval_token = uuid.uuid4().hex
|
| 200 |
+
approver_email = _lookup_producer_email(db, code, name)
|
| 201 |
if not approver_email:
|
| 202 |
emails_missing.append(f"{name} ({code})")
|
| 203 |
|
|
|
|
| 238 |
db.refresh(submission)
|
| 239 |
|
| 240 |
# Generate DRAFT PDF
|
| 241 |
+
import os
|
| 242 |
+
doc_dir = "/data/pdfs"
|
| 243 |
+
os.makedirs(doc_dir, exist_ok=True)
|
| 244 |
+
|
| 245 |
try:
|
| 246 |
pdf_bytes = generate_pdf(
|
| 247 |
form_data=body,
|
|
|
|
| 250 |
is_draft=True,
|
| 251 |
submission_id=submission.id,
|
| 252 |
)
|
| 253 |
+
pdf_path = os.path.join(doc_dir, f"submission_{submission.id}_draft.pdf")
|
|
|
|
|
|
|
|
|
|
| 254 |
with open(pdf_path, "wb") as f:
|
| 255 |
f.write(pdf_bytes)
|
| 256 |
pdf_generated = True
|
|
|
|
| 260 |
traceback.print_exc()
|
| 261 |
pdf_generated = False
|
| 262 |
|
| 263 |
+
# Generate DRAFT Word Document
|
| 264 |
+
docx_generated = False
|
| 265 |
+
try:
|
| 266 |
+
docx_bytes = generate_docx(
|
| 267 |
+
form_data=body,
|
| 268 |
+
summary_data=summary,
|
| 269 |
+
approvals=approvals_created,
|
| 270 |
+
is_draft=True,
|
| 271 |
+
submission_id=submission.id,
|
| 272 |
+
submitter_name=current_user.full_name,
|
| 273 |
+
)
|
| 274 |
+
docx_path = os.path.join(doc_dir, f"submission_{submission.id}_draft.docx")
|
| 275 |
+
with open(docx_path, "wb") as f:
|
| 276 |
+
f.write(docx_bytes)
|
| 277 |
+
docx_generated = True
|
| 278 |
+
except Exception as e:
|
| 279 |
+
print(f"[DOCX] Draft generation failed: {e}")
|
| 280 |
+
import traceback
|
| 281 |
+
traceback.print_exc()
|
| 282 |
+
|
| 283 |
# Alert admin about missing emails
|
| 284 |
if emails_missing:
|
| 285 |
from app.email_service import send_missing_emails_alert
|
|
|
|
| 300 |
"success": True,
|
| 301 |
"submission_id": submission.id,
|
| 302 |
"pdf_available": pdf_generated,
|
| 303 |
+
"docx_available": docx_generated,
|
| 304 |
"approvals_count": len(approvals_created),
|
| 305 |
"emails_sent": emails_sent,
|
| 306 |
"emails_missing": emails_missing,
|
|
|
|
| 367 |
)
|
| 368 |
|
| 369 |
|
| 370 |
+
@router.get("/api/download-docx/{submission_id}")
|
| 371 |
+
async def download_docx(submission_id: int,
|
| 372 |
+
request: Request,
|
| 373 |
+
current_user: User = Depends(get_current_user),
|
| 374 |
+
db: Session = Depends(get_db)):
|
| 375 |
+
submission = db.query(IntakeSubmission).filter(
|
| 376 |
+
IntakeSubmission.id == submission_id
|
| 377 |
+
).first()
|
| 378 |
+
|
| 379 |
+
if not submission:
|
| 380 |
+
return JSONResponse({"error": "Submission not found"}, status_code=404)
|
| 381 |
+
|
| 382 |
+
import os
|
| 383 |
+
|
| 384 |
+
# Check for cached files first
|
| 385 |
+
final_path = f"/data/pdfs/submission_{submission_id}_final.docx"
|
| 386 |
+
draft_path = f"/data/pdfs/submission_{submission_id}_draft.docx"
|
| 387 |
+
|
| 388 |
+
if os.path.exists(final_path):
|
| 389 |
+
docx_path = final_path
|
| 390 |
+
filename = f"CA_{submission_id}_FINAL.docx"
|
| 391 |
+
elif os.path.exists(draft_path):
|
| 392 |
+
docx_path = draft_path
|
| 393 |
+
filename = f"CA_{submission_id}_DRAFT.docx"
|
| 394 |
+
else:
|
| 395 |
+
# Generate on the fly
|
| 396 |
+
approvals = db.query(Approval).filter(
|
| 397 |
+
Approval.submission_id == submission_id
|
| 398 |
+
).all()
|
| 399 |
+
approval_dicts = [{
|
| 400 |
+
"role_label": a.role_label,
|
| 401 |
+
"approver_name": a.approver_name,
|
| 402 |
+
"producer_code": a.producer_code,
|
| 403 |
+
"status": a.status,
|
| 404 |
+
"responded_at": a.responded_at.strftime("%m/%d/%Y %I:%M %p") if a.responded_at else None,
|
| 405 |
+
} for a in approvals]
|
| 406 |
+
|
| 407 |
+
is_final = submission.status == "approved"
|
| 408 |
+
submitter = db.query(User).filter(User.id == submission.submitted_by).first()
|
| 409 |
+
|
| 410 |
+
try:
|
| 411 |
+
docx_bytes = generate_docx(
|
| 412 |
+
form_data=submission.form_data,
|
| 413 |
+
summary_data=submission.summary_data,
|
| 414 |
+
approvals=approval_dicts,
|
| 415 |
+
is_draft=not is_final,
|
| 416 |
+
submission_id=submission_id,
|
| 417 |
+
submitter_name=submitter.full_name if submitter else "β",
|
| 418 |
+
)
|
| 419 |
+
except FileNotFoundError:
|
| 420 |
+
return JSONResponse({"error": "Word template not found on server"}, status_code=500)
|
| 421 |
+
|
| 422 |
+
filename = f"CA_{submission_id}_{'FINAL' if is_final else 'DRAFT'}.docx"
|
| 423 |
+
return Response(
|
| 424 |
+
content=docx_bytes,
|
| 425 |
+
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 426 |
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
with open(docx_path, "rb") as f:
|
| 430 |
+
docx_bytes = f.read()
|
| 431 |
+
|
| 432 |
+
return Response(
|
| 433 |
+
content=docx_bytes,
|
| 434 |
+
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 435 |
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
|
| 439 |
@router.get("/api/submission/{submission_id}")
|
| 440 |
async def get_submission(submission_id: int,
|
| 441 |
request: Request,
|
|
|
|
| 526 |
return JSONResponse({"success": True})
|
| 527 |
|
| 528 |
|
| 529 |
+
@router.post("/api/hide-submission/{submission_id}")
|
| 530 |
+
async def hide_submission(submission_id: int,
|
| 531 |
+
current_user: User = Depends(get_current_user),
|
| 532 |
+
db: Session = Depends(get_db)):
|
| 533 |
+
"""Soft-delete: hides a finalized/rejected submission from dashboard (data persists)."""
|
| 534 |
+
submission = db.query(IntakeSubmission).filter(
|
| 535 |
+
IntakeSubmission.id == submission_id,
|
| 536 |
+
IntakeSubmission.submitted_by == current_user.id,
|
| 537 |
+
).first()
|
| 538 |
+
if not submission:
|
| 539 |
+
return JSONResponse({"error": "Submission not found"}, status_code=404)
|
| 540 |
+
if submission.status == "pending_approval":
|
| 541 |
+
return JSONResponse({"error": "Cannot hide in-progress submissions. Cancel or reject first."}, status_code=400)
|
| 542 |
+
submission.hidden = True
|
| 543 |
+
db.commit()
|
| 544 |
+
return JSONResponse({"success": True})
|
| 545 |
+
|
| 546 |
+
|
| 547 |
# =====================================================================
|
| 548 |
# USER DASHBOARD
|
| 549 |
# =====================================================================
|
|
|
|
| 556 |
|
| 557 |
submissions = db.query(IntakeSubmission).filter(
|
| 558 |
IntakeSubmission.submitted_by == current_user.id,
|
| 559 |
+
IntakeSubmission.hidden != True,
|
| 560 |
).order_by(IntakeSubmission.updated_at.desc()).all()
|
| 561 |
|
| 562 |
drafts = []
|
| 563 |
submitted = []
|
| 564 |
|
| 565 |
for s in submissions:
|
| 566 |
+
fd = s.form_data or {}
|
| 567 |
+
sd = s.summary_data or {}
|
| 568 |
+
|
| 569 |
if s.status == "draft":
|
| 570 |
drafts.append({
|
| 571 |
"id": s.id,
|
| 572 |
"title": s.title or "Untitled Draft",
|
| 573 |
"updated_at": s.updated_at.strftime("%b %d, %Y %I:%M %p") if s.updated_at else "β",
|
| 574 |
+
"agreement_name": sd.get("agreement_name") or fd.get("agreement_name", ""),
|
| 575 |
+
"effective_date": fd.get("effective_date", ""),
|
| 576 |
})
|
| 577 |
else:
|
| 578 |
approvals = db.query(Approval).filter(
|
|
|
|
| 582 |
pending_count = sum(1 for a in approvals if a.status == "pending")
|
| 583 |
rejected_count = sum(1 for a in approvals if a.status == "rejected")
|
| 584 |
|
| 585 |
+
# Commission preview from summary_data
|
| 586 |
+
year_tables = sd.get("year_tables", [])
|
| 587 |
+
|
| 588 |
submitted.append({
|
| 589 |
"id": s.id,
|
| 590 |
"title": s.title or f"Submission #{s.id}",
|
| 591 |
"status": s.status,
|
| 592 |
"submitted_at": s.submitted_at.strftime("%b %d, %Y %I:%M %p") if s.submitted_at else "β",
|
| 593 |
"bell_ringer": s.bell_ringer_triggered,
|
| 594 |
+
"agreement_name": sd.get("agreement_name", ""),
|
| 595 |
+
"effective_date": fd.get("effective_date", ""),
|
| 596 |
"total_approvals": len(approvals),
|
| 597 |
"approved_count": approved_count,
|
| 598 |
"pending_count": pending_count,
|
| 599 |
"rejected_count": rejected_count,
|
| 600 |
+
"year_tables": year_tables,
|
| 601 |
"approvals": [{
|
| 602 |
"role_label": a.role_label,
|
| 603 |
"approver_name": a.approver_name,
|
| 604 |
+
"producer_code": a.producer_code,
|
| 605 |
"status": a.status,
|
| 606 |
"responded_at": a.responded_at.strftime("%b %d, %Y %I:%M %p") if a.responded_at else None,
|
| 607 |
} for a in approvals],
|
|
@@ -227,14 +227,17 @@ def compute_summary(db: Session, form_data: dict) -> dict:
|
|
| 227 |
p1_ded = p2_ded = orig_ded = pp
|
| 228 |
|
| 229 |
if is_house_standard:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
year_rows.append({"role": "House Account", "agreement_type": "House Standard Agreement",
|
| 231 |
-
"commission_pct":
|
| 232 |
"abe_status": "ABE", "code": "ACCHO1", "name": "House Account"})
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
"agreement_type": "House Standard Agreement",
|
| 236 |
-
"commission_pct": 0.0, "production_pct": 0.0,
|
| 237 |
-
"abe_status": _abe_status(p1suf), "code": p1code, "name": p1name})
|
| 238 |
else:
|
| 239 |
if not is_ce and p1_tier and p1_tier["commission"] is not None:
|
| 240 |
year_rows.append({"role": "Producer 1", "agreement_type": p1_tier["agreement"],
|
|
@@ -260,8 +263,12 @@ def compute_summary(db: Session, form_data: dict) -> dict:
|
|
| 260 |
"commission_pct": CE_COMM, "production_pct": consts.get("CE_CREDIT", 0),
|
| 261 |
"abe_status": _abe_status(ce_suf), "code": cecode, "name": cename})
|
| 262 |
elif is_ce and cepfx:
|
| 263 |
-
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
"production_pct": consts.get("CE_PROD_CREDIT", 100),
|
| 266 |
"abe_status": _abe_status(int(consts.get("CE_PROD_SUFFIX", 2))),
|
| 267 |
"code": cecode, "name": cename})
|
|
|
|
| 227 |
p1_ded = p2_ded = orig_ded = pp
|
| 228 |
|
| 229 |
if is_house_standard:
|
| 230 |
+
# Layers DEDUCT from 100% base β total must always equal 100%
|
| 231 |
+
house_comm = 100.0
|
| 232 |
+
if cepfx and not is_ce:
|
| 233 |
+
house_comm -= CE_COMM
|
| 234 |
+
if has_cc and not is_bond:
|
| 235 |
+
house_comm -= CC_COMM
|
| 236 |
year_rows.append({"role": "House Account", "agreement_type": "House Standard Agreement",
|
| 237 |
+
"commission_pct": house_comm, "production_pct": 100.0,
|
| 238 |
"abe_status": "ABE", "code": "ACCHO1", "name": "House Account"})
|
| 239 |
+
# P1 is approval processor only β NOT shown in commission table
|
| 240 |
+
# (kept in participants list for approval workflow)
|
|
|
|
|
|
|
|
|
|
| 241 |
else:
|
| 242 |
if not is_ce and p1_tier and p1_tier["commission"] is not None:
|
| 243 |
year_rows.append({"role": "Producer 1", "agreement_type": p1_tier["agreement"],
|
|
|
|
| 263 |
"commission_pct": CE_COMM, "production_pct": consts.get("CE_CREDIT", 0),
|
| 264 |
"abe_status": _abe_status(ce_suf), "code": cecode, "name": cename})
|
| 265 |
elif is_ce and cepfx:
|
| 266 |
+
ce_prod_comm = consts.get("CE_PROD_COMM", 10)
|
| 267 |
+
# CCP deducts from CE when CE acts as producer
|
| 268 |
+
if has_cc and not is_bond:
|
| 269 |
+
ce_prod_comm = max(ce_prod_comm - CC_COMM, 0)
|
| 270 |
+
year_rows.append({"role": "Client Executive", "agreement_type": "Ceded Accounts β CE as Producer",
|
| 271 |
+
"commission_pct": ce_prod_comm,
|
| 272 |
"production_pct": consts.get("CE_PROD_CREDIT", 100),
|
| 273 |
"abe_status": _abe_status(int(consts.get("CE_PROD_SUFFIX", 2))),
|
| 274 |
"code": cecode, "name": cename})
|
|
@@ -119,9 +119,7 @@ def seed(session, reset=False):
|
|
| 119 |
{
|
| 120 |
"name": "Life Origination Fee Agreement", "p1_suffix": 2, "p2_suffix": None,
|
| 121 |
"tiers": [
|
| 122 |
-
{"role": "P1", "years": "
|
| 123 |
-
{"role": "P1", "years": "Year 2+", "comm": None, "credit": None, "agr": "Ended",
|
| 124 |
-
"note": "Life Referral is 20% Year 1 only. Year 2 it is removed."},
|
| 125 |
]
|
| 126 |
},
|
| 127 |
{
|
|
@@ -206,7 +204,13 @@ def seed(session, reset=False):
|
|
| 206 |
for i, (lbl, val) in enumerate(departments):
|
| 207 |
session.add(DropdownOption(category="department", label=lbl, value=val, display_order=i))
|
| 208 |
|
| 209 |
-
employees = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
for i, e in enumerate(employees):
|
| 211 |
session.add(DropdownOption(category="employee", label=e, value=e, display_order=i))
|
| 212 |
|
|
|
|
| 119 |
{
|
| 120 |
"name": "Life Origination Fee Agreement", "p1_suffix": 2, "p2_suffix": None,
|
| 121 |
"tiers": [
|
| 122 |
+
{"role": "P1", "years": "All Years", "comm": 20, "credit": 100, "agr": "Life Origination Fee Agreement"},
|
|
|
|
|
|
|
| 123 |
]
|
| 124 |
},
|
| 125 |
{
|
|
|
|
| 204 |
for i, (lbl, val) in enumerate(departments):
|
| 205 |
session.add(DropdownOption(category="department", label=lbl, value=val, display_order=i))
|
| 206 |
|
| 207 |
+
employees = [
|
| 208 |
+
"Sarah Johnson", "Mike Thompson", "Karen Lee", "James Wilson",
|
| 209 |
+
"Beth Johnston", "Beth Potter", "Terrell Wallace", "Stephanie Pena",
|
| 210 |
+
"Jason Goodwyn", "Steve Gebhardt", "Wade Beacham", "Mike Parsa",
|
| 211 |
+
"Frank Herron", "David Chen", "Lisa Martinez", "Robert Brown",
|
| 212 |
+
"Jennifer Davis", "Amanda Taylor", "Christopher Moore", "Michelle Anderson",
|
| 213 |
+
]
|
| 214 |
for i, e in enumerate(employees):
|
| 215 |
session.add(DropdownOption(category="employee", label=e, value=e, display_order=i))
|
| 216 |
|
|
@@ -14,6 +14,18 @@
|
|
| 14 |
</a>
|
| 15 |
</div>
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
<!-- ββ Drafts Section ββββββββββββββββββββββββββββββββββββ -->
|
| 18 |
<div class="bg-white rounded-lg shadow overflow-hidden">
|
| 19 |
<div class="sw-blue text-white px-5 py-3 font-semibold flex items-center justify-between">
|
|
@@ -27,7 +39,17 @@
|
|
| 27 |
<div>
|
| 28 |
<a href="/form?token={{ token }}&draft_id={{ d.id }}"
|
| 29 |
class="font-medium sw-blue-text hover:underline">{{ d.title }}</a>
|
| 30 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
</div>
|
| 32 |
<div class="flex items-center space-x-2">
|
| 33 |
<a href="/form?token={{ token }}&draft_id={{ d.id }}"
|
|
@@ -56,19 +78,23 @@
|
|
| 56 |
<span class="bg-white/20 text-xs px-2 py-0.5 rounded">{{ submitted|length }}</span>
|
| 57 |
</div>
|
| 58 |
{% if submitted %}
|
| 59 |
-
<div class="divide-y divide-gray-100">
|
| 60 |
{% for s in submitted %}
|
| 61 |
-
<div class="px-5 py-4">
|
| 62 |
-
<
|
|
|
|
| 63 |
<div class="flex items-center space-x-3">
|
| 64 |
<span class="font-medium sw-blue-text">{{ s.title }}</span>
|
|
|
|
|
|
|
|
|
|
| 65 |
{% if s.bell_ringer %}
|
| 66 |
<span class="text-xs bg-orange-100 text-orange-700 px-2 py-0.5 rounded font-semibold">Bell Ringer</span>
|
| 67 |
{% endif %}
|
| 68 |
</div>
|
| 69 |
-
<div class="flex items-center space-x-
|
| 70 |
{% if s.status == 'pending_approval' %}
|
| 71 |
-
<span class="text-xs bg-yellow-100 text-yellow-800 px-2.5 py-1 rounded font-semibold">Pending
|
| 72 |
{% elif s.status == 'approved' %}
|
| 73 |
<span class="text-xs bg-green-100 text-green-800 px-2.5 py-1 rounded font-semibold">Approved</span>
|
| 74 |
{% elif s.status == 'rejected' %}
|
|
@@ -78,14 +104,36 @@
|
|
| 78 |
class="text-xs bg-blue-50 text-blue-700 px-3 py-1 rounded hover:bg-blue-100 transition">
|
| 79 |
PDF
|
| 80 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
</div>
|
| 82 |
</div>
|
| 83 |
-
<p class="text-xs text-gray-400 mb-3">Submitted: {{ s.submitted_at }} · Submission #{{ s.id }}</p>
|
| 84 |
|
| 85 |
-
<!--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
{% if s.total_approvals > 0 %}
|
| 87 |
<div class="mb-2">
|
| 88 |
-
<
|
|
|
|
| 89 |
<div class="flex-1 bg-gray-200 rounded-full h-2 overflow-hidden">
|
| 90 |
{% set pct = (s.approved_count / s.total_approvals * 100)|int %}
|
| 91 |
<div class="h-2 rounded-full transition-all {% if s.rejected_count > 0 %}bg-red-500{% else %}bg-green-500{% endif %}"
|
|
@@ -95,44 +143,111 @@
|
|
| 95 |
{{ s.approved_count }}/{{ s.total_approvals }} approved
|
| 96 |
</span>
|
| 97 |
</div>
|
| 98 |
-
</div>
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
</
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
<td class="px-3 py-2">
|
| 121 |
-
{% if a.status == 'approved' %}
|
| 122 |
-
<span class="text-green-600 font-semibold">Approved</span>
|
| 123 |
-
{% elif a.status == 'rejected' %}
|
| 124 |
-
<span class="text-red-600 font-semibold">Rejected</span>
|
| 125 |
-
{% else %}
|
| 126 |
-
<span class="text-yellow-600 font-semibold">Pending</span>
|
| 127 |
-
{% endif %}
|
| 128 |
-
</td>
|
| 129 |
-
<td class="px-3 py-2 text-gray-400">{{ a.responded_at or 'β' }}</td>
|
| 130 |
-
</tr>
|
| 131 |
-
{% endfor %}
|
| 132 |
-
</tbody>
|
| 133 |
-
</table>
|
| 134 |
</div>
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
{% endif %}
|
| 137 |
</div>
|
| 138 |
{% endfor %}
|
|
@@ -147,6 +262,11 @@
|
|
| 147 |
{% endblock %}
|
| 148 |
|
| 149 |
{% block scripts %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
<script>
|
| 151 |
const authToken = new URLSearchParams(window.location.search).get('token');
|
| 152 |
|
|
@@ -167,5 +287,38 @@ async function deleteDraft(draftId, btn) {
|
|
| 167 |
alert('Error: ' + e.message);
|
| 168 |
}
|
| 169 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
</script>
|
| 171 |
{% endblock %}
|
|
|
|
| 14 |
</a>
|
| 15 |
</div>
|
| 16 |
|
| 17 |
+
<!-- ββ Status Filter Tabs ββββββββββββββββββββββββββββββββββ -->
|
| 18 |
+
<div class="flex items-center space-x-2 text-sm" id="filter_tabs">
|
| 19 |
+
<button onclick="filterSubmissions('all')" class="filter-tab active px-3 py-1.5 rounded-lg font-medium transition"
|
| 20 |
+
data-filter="all">All</button>
|
| 21 |
+
<button onclick="filterSubmissions('pending_approval')" class="filter-tab px-3 py-1.5 rounded-lg font-medium transition"
|
| 22 |
+
data-filter="pending_approval">Pending</button>
|
| 23 |
+
<button onclick="filterSubmissions('approved')" class="filter-tab px-3 py-1.5 rounded-lg font-medium transition"
|
| 24 |
+
data-filter="approved">Approved</button>
|
| 25 |
+
<button onclick="filterSubmissions('rejected')" class="filter-tab px-3 py-1.5 rounded-lg font-medium transition"
|
| 26 |
+
data-filter="rejected">Rejected</button>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
<!-- ββ Drafts Section ββββββββββββββββββββββββββββββββββββ -->
|
| 30 |
<div class="bg-white rounded-lg shadow overflow-hidden">
|
| 31 |
<div class="sw-blue text-white px-5 py-3 font-semibold flex items-center justify-between">
|
|
|
|
| 39 |
<div>
|
| 40 |
<a href="/form?token={{ token }}&draft_id={{ d.id }}"
|
| 41 |
class="font-medium sw-blue-text hover:underline">{{ d.title }}</a>
|
| 42 |
+
<div class="flex items-center space-x-3 text-xs text-gray-400 mt-0.5">
|
| 43 |
+
<span>Last edited: {{ d.updated_at }}</span>
|
| 44 |
+
{% if d.agreement_name %}
|
| 45 |
+
<span class="text-gray-300">|</span>
|
| 46 |
+
<span>{{ d.agreement_name }}</span>
|
| 47 |
+
{% endif %}
|
| 48 |
+
{% if d.effective_date %}
|
| 49 |
+
<span class="text-gray-300">|</span>
|
| 50 |
+
<span>Eff: {{ d.effective_date }}</span>
|
| 51 |
+
{% endif %}
|
| 52 |
+
</div>
|
| 53 |
</div>
|
| 54 |
<div class="flex items-center space-x-2">
|
| 55 |
<a href="/form?token={{ token }}&draft_id={{ d.id }}"
|
|
|
|
| 78 |
<span class="bg-white/20 text-xs px-2 py-0.5 rounded">{{ submitted|length }}</span>
|
| 79 |
</div>
|
| 80 |
{% if submitted %}
|
| 81 |
+
<div class="divide-y divide-gray-100" id="submissions_list">
|
| 82 |
{% for s in submitted %}
|
| 83 |
+
<div class="px-5 py-4 submission-card" data-status="{{ s.status }}">
|
| 84 |
+
<!-- Header row -->
|
| 85 |
+
<div class="flex items-center justify-between mb-1">
|
| 86 |
<div class="flex items-center space-x-3">
|
| 87 |
<span class="font-medium sw-blue-text">{{ s.title }}</span>
|
| 88 |
+
{% if s.agreement_name %}
|
| 89 |
+
<span class="text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded">{{ s.agreement_name }}</span>
|
| 90 |
+
{% endif %}
|
| 91 |
{% if s.bell_ringer %}
|
| 92 |
<span class="text-xs bg-orange-100 text-orange-700 px-2 py-0.5 rounded font-semibold">Bell Ringer</span>
|
| 93 |
{% endif %}
|
| 94 |
</div>
|
| 95 |
+
<div class="flex items-center space-x-2">
|
| 96 |
{% if s.status == 'pending_approval' %}
|
| 97 |
+
<span class="text-xs bg-yellow-100 text-yellow-800 px-2.5 py-1 rounded font-semibold">Pending</span>
|
| 98 |
{% elif s.status == 'approved' %}
|
| 99 |
<span class="text-xs bg-green-100 text-green-800 px-2.5 py-1 rounded font-semibold">Approved</span>
|
| 100 |
{% elif s.status == 'rejected' %}
|
|
|
|
| 104 |
class="text-xs bg-blue-50 text-blue-700 px-3 py-1 rounded hover:bg-blue-100 transition">
|
| 105 |
PDF
|
| 106 |
</a>
|
| 107 |
+
<a href="/api/download-docx/{{ s.id }}?token={{ token }}"
|
| 108 |
+
class="text-xs bg-indigo-50 text-indigo-700 px-3 py-1 rounded hover:bg-indigo-100 transition">
|
| 109 |
+
Word
|
| 110 |
+
</a>
|
| 111 |
+
{% if s.status != 'pending_approval' %}
|
| 112 |
+
<button onclick="hideSubmission({{ s.id }}, this)"
|
| 113 |
+
class="text-xs bg-red-50 text-red-600 px-3 py-1 rounded hover:bg-red-100 transition"
|
| 114 |
+
title="Hide from dashboard (data is preserved)">
|
| 115 |
+
Hide
|
| 116 |
+
</button>
|
| 117 |
+
{% endif %}
|
| 118 |
</div>
|
| 119 |
</div>
|
|
|
|
| 120 |
|
| 121 |
+
<!-- Meta row -->
|
| 122 |
+
<div class="flex items-center space-x-3 text-xs text-gray-400 mb-3">
|
| 123 |
+
<span>Submitted: {{ s.submitted_at }}</span>
|
| 124 |
+
<span class="text-gray-300">|</span>
|
| 125 |
+
<span>#{{ s.id }}</span>
|
| 126 |
+
{% if s.effective_date %}
|
| 127 |
+
<span class="text-gray-300">|</span>
|
| 128 |
+
<span>Eff: {{ s.effective_date }}</span>
|
| 129 |
+
{% endif %}
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
<!-- Approval Timeline -->
|
| 133 |
{% if s.total_approvals > 0 %}
|
| 134 |
<div class="mb-2">
|
| 135 |
+
<!-- Progress bar -->
|
| 136 |
+
<div class="flex items-center space-x-2 mb-2">
|
| 137 |
<div class="flex-1 bg-gray-200 rounded-full h-2 overflow-hidden">
|
| 138 |
{% set pct = (s.approved_count / s.total_approvals * 100)|int %}
|
| 139 |
<div class="h-2 rounded-full transition-all {% if s.rejected_count > 0 %}bg-red-500{% else %}bg-green-500{% endif %}"
|
|
|
|
| 143 |
{{ s.approved_count }}/{{ s.total_approvals }} approved
|
| 144 |
</span>
|
| 145 |
</div>
|
|
|
|
| 146 |
|
| 147 |
+
<!-- Inline approval timeline (visual) -->
|
| 148 |
+
<div class="flex flex-wrap gap-2 mb-2">
|
| 149 |
+
{% for a in s.approvals %}
|
| 150 |
+
<div class="flex items-center space-x-1 text-xs
|
| 151 |
+
{% if a.status == 'approved' %}text-green-700
|
| 152 |
+
{% elif a.status == 'rejected' %}text-red-700
|
| 153 |
+
{% else %}text-gray-500{% endif %}">
|
| 154 |
+
{% if a.status == 'approved' %}
|
| 155 |
+
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
|
| 156 |
+
{% elif a.status == 'rejected' %}
|
| 157 |
+
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
| 158 |
+
{% else %}
|
| 159 |
+
<span class="w-3 h-3 rounded-full border-2 border-gray-300 inline-block"></span>
|
| 160 |
+
{% endif %}
|
| 161 |
+
<span class="font-medium">{{ a.role_label }}</span>
|
| 162 |
+
{% if a.responded_at %}
|
| 163 |
+
<span class="text-gray-400">({{ a.responded_at }})</span>
|
| 164 |
+
{% endif %}
|
| 165 |
+
</div>
|
| 166 |
+
{% endfor %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
</div>
|
| 168 |
+
|
| 169 |
+
<!-- Expandable detail table + commission preview -->
|
| 170 |
+
<details class="text-sm">
|
| 171 |
+
<summary class="cursor-pointer text-blue-600 hover:text-blue-800 text-xs font-medium">
|
| 172 |
+
Show details
|
| 173 |
+
</summary>
|
| 174 |
+
<div class="mt-2 space-y-3">
|
| 175 |
+
<!-- Approval detail table -->
|
| 176 |
+
<div class="border border-gray-100 rounded-lg overflow-hidden">
|
| 177 |
+
<table class="w-full text-xs">
|
| 178 |
+
<thead>
|
| 179 |
+
<tr class="bg-gray-50 text-gray-500">
|
| 180 |
+
<th class="px-3 py-2 text-left font-medium">Role</th>
|
| 181 |
+
<th class="px-3 py-2 text-left font-medium">Name</th>
|
| 182 |
+
<th class="px-3 py-2 text-left font-medium">Code</th>
|
| 183 |
+
<th class="px-3 py-2 text-left font-medium">Status</th>
|
| 184 |
+
<th class="px-3 py-2 text-left font-medium">Date</th>
|
| 185 |
+
</tr>
|
| 186 |
+
</thead>
|
| 187 |
+
<tbody class="divide-y divide-gray-50">
|
| 188 |
+
{% for a in s.approvals %}
|
| 189 |
+
<tr>
|
| 190 |
+
<td class="px-3 py-2 text-gray-700">{{ a.role_label }}</td>
|
| 191 |
+
<td class="px-3 py-2 text-gray-700">{{ a.approver_name }}</td>
|
| 192 |
+
<td class="px-3 py-2 text-gray-500 font-mono">{{ a.producer_code or 'β' }}</td>
|
| 193 |
+
<td class="px-3 py-2">
|
| 194 |
+
{% if a.status == 'approved' %}
|
| 195 |
+
<span class="text-green-600 font-semibold">Approved</span>
|
| 196 |
+
{% elif a.status == 'rejected' %}
|
| 197 |
+
<span class="text-red-600 font-semibold">Rejected</span>
|
| 198 |
+
{% else %}
|
| 199 |
+
<span class="text-yellow-600 font-semibold">Pending</span>
|
| 200 |
+
{% endif %}
|
| 201 |
+
</td>
|
| 202 |
+
<td class="px-3 py-2 text-gray-400">{{ a.responded_at or 'β' }}</td>
|
| 203 |
+
</tr>
|
| 204 |
+
{% endfor %}
|
| 205 |
+
</tbody>
|
| 206 |
+
</table>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
<!-- Commission preview tables -->
|
| 210 |
+
{% if s.year_tables %}
|
| 211 |
+
<div class="space-y-2">
|
| 212 |
+
<p class="text-xs font-semibold text-gray-600">Commission Structure</p>
|
| 213 |
+
{% for yt in s.year_tables %}
|
| 214 |
+
<div>
|
| 215 |
+
<p class="text-xs font-semibold sw-blue-text mb-1">{{ yt.year_label }}</p>
|
| 216 |
+
<div class="border border-gray-100 rounded-lg overflow-hidden">
|
| 217 |
+
<table class="w-full text-xs">
|
| 218 |
+
<thead>
|
| 219 |
+
<tr class="bg-gray-50 text-gray-500">
|
| 220 |
+
<th class="px-2 py-1.5 text-left font-medium">Role</th>
|
| 221 |
+
<th class="px-2 py-1.5 text-left font-medium">Name</th>
|
| 222 |
+
<th class="px-2 py-1.5 text-left font-medium">Code</th>
|
| 223 |
+
<th class="px-2 py-1.5 text-left font-medium">ABE</th>
|
| 224 |
+
<th class="px-2 py-1.5 text-left font-medium">Agreement</th>
|
| 225 |
+
<th class="px-2 py-1.5 text-left font-medium">Comm%</th>
|
| 226 |
+
<th class="px-2 py-1.5 text-left font-medium">Prod Credit</th>
|
| 227 |
+
</tr>
|
| 228 |
+
</thead>
|
| 229 |
+
<tbody class="divide-y divide-gray-50">
|
| 230 |
+
{% for row in yt.rows %}
|
| 231 |
+
<tr>
|
| 232 |
+
<td class="px-2 py-1.5 font-semibold">{{ row.role }}</td>
|
| 233 |
+
<td class="px-2 py-1.5">{{ row.name or 'β' }}</td>
|
| 234 |
+
<td class="px-2 py-1.5 font-mono">{{ row.code or 'β' }}</td>
|
| 235 |
+
<td class="px-2 py-1.5 {% if row.abe_status == 'ABE' %}text-green-700 font-semibold{% endif %}">{{ row.abe_status }}</td>
|
| 236 |
+
<td class="px-2 py-1.5">{{ row.agreement_type }}</td>
|
| 237 |
+
<td class="px-2 py-1.5 font-semibold">{{ row.commission_pct }}%</td>
|
| 238 |
+
<td class="px-2 py-1.5">{{ row.production_pct }}%</td>
|
| 239 |
+
</tr>
|
| 240 |
+
{% endfor %}
|
| 241 |
+
</tbody>
|
| 242 |
+
</table>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
{% endfor %}
|
| 246 |
+
</div>
|
| 247 |
+
{% endif %}
|
| 248 |
+
</div>
|
| 249 |
+
</details>
|
| 250 |
+
</div>
|
| 251 |
{% endif %}
|
| 252 |
</div>
|
| 253 |
{% endfor %}
|
|
|
|
| 262 |
{% endblock %}
|
| 263 |
|
| 264 |
{% block scripts %}
|
| 265 |
+
<style>
|
| 266 |
+
.filter-tab { background: #f0f2f5; color: #666; }
|
| 267 |
+
.filter-tab:hover { background: #e2e5e9; }
|
| 268 |
+
.filter-tab.active { background: #2e4057; color: #fff; }
|
| 269 |
+
</style>
|
| 270 |
<script>
|
| 271 |
const authToken = new URLSearchParams(window.location.search).get('token');
|
| 272 |
|
|
|
|
| 287 |
alert('Error: ' + e.message);
|
| 288 |
}
|
| 289 |
}
|
| 290 |
+
|
| 291 |
+
async function hideSubmission(submissionId, btn) {
|
| 292 |
+
if (!confirm('Hide this submission from your dashboard? The data will be preserved in the system.')) return;
|
| 293 |
+
try {
|
| 294 |
+
const resp = await fetch('/api/hide-submission/' + submissionId, {
|
| 295 |
+
method: 'POST',
|
| 296 |
+
headers: {'Authorization': 'Bearer ' + authToken},
|
| 297 |
+
});
|
| 298 |
+
const data = await resp.json();
|
| 299 |
+
if (data.success) {
|
| 300 |
+
btn.closest('.submission-card').remove();
|
| 301 |
+
} else {
|
| 302 |
+
alert(data.error || 'Failed to hide submission');
|
| 303 |
+
}
|
| 304 |
+
} catch (e) {
|
| 305 |
+
alert('Error: ' + e.message);
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
function filterSubmissions(status) {
|
| 310 |
+
// Update active tab
|
| 311 |
+
document.querySelectorAll('.filter-tab').forEach(tab => {
|
| 312 |
+
tab.classList.toggle('active', tab.dataset.filter === status);
|
| 313 |
+
});
|
| 314 |
+
// Filter cards
|
| 315 |
+
document.querySelectorAll('.submission-card').forEach(card => {
|
| 316 |
+
if (status === 'all') {
|
| 317 |
+
card.style.display = '';
|
| 318 |
+
} else {
|
| 319 |
+
card.style.display = card.dataset.status === status ? '' : 'none';
|
| 320 |
+
}
|
| 321 |
+
});
|
| 322 |
+
}
|
| 323 |
</script>
|
| 324 |
{% endblock %}
|
|
@@ -137,6 +137,9 @@
|
|
| 137 |
</select>
|
| 138 |
</div>
|
| 139 |
<div id="orig_employee_row" class="hidden">
|
|
|
|
|
|
|
|
|
|
| 140 |
<select id="orig_employee" class="border border-gray-300 rounded-md px-3 py-2 text-sm w-full md:w-1/2">
|
| 141 |
<option value="">-- Select Employee --</option>
|
| 142 |
{% for e in employees %}
|
|
@@ -147,19 +150,33 @@
|
|
| 147 |
</div>
|
| 148 |
</div>
|
| 149 |
|
| 150 |
-
<!-- CE Layer (checkbox + picker) -->
|
| 151 |
<div id="ce_section" class="hidden border-t pt-3 mt-3">
|
| 152 |
-
<
|
| 153 |
-
|
| 154 |
-
<
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
<
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
{% for p in producers %}
|
| 160 |
<option value="{{ p.prefix }}">{{ p.label }}</option>
|
| 161 |
{% endfor %}
|
| 162 |
</select>
|
|
|
|
| 163 |
</div>
|
| 164 |
</div>
|
| 165 |
|
|
@@ -204,13 +221,45 @@
|
|
| 204 |
class="bg-gray-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-gray-600 transition text-sm">
|
| 205 |
Save Draft
|
| 206 |
</button>
|
| 207 |
-
<button id="submit_btn" onclick="
|
| 208 |
class="bg-green-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-green-700 transition disabled:opacity-50"
|
| 209 |
disabled>
|
| 210 |
Submit for Approval
|
| 211 |
</button>
|
| 212 |
</div>
|
| 213 |
<div id="draft_msg" class="text-right text-sm text-gray-400 mt-1 hidden"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
</div>
|
| 215 |
{% endblock %}
|
| 216 |
|
|
@@ -300,6 +349,7 @@ function updateFieldVisibility(agrName) {
|
|
| 300 |
section.classList.remove('hidden');
|
| 301 |
resetProducerFields();
|
| 302 |
|
|
|
|
| 303 |
const p1Section = document.getElementById('p1_section');
|
| 304 |
const p1Select = document.getElementById('producer1');
|
| 305 |
const p1Locked = document.getElementById('p1_locked_display');
|
|
@@ -329,14 +379,31 @@ function updateFieldVisibility(agrName) {
|
|
| 329 |
document.getElementById('originator_section').classList.toggle('hidden', !vis.originator);
|
| 330 |
document.getElementById('ce_section').classList.toggle('hidden', !vis.ce);
|
| 331 |
document.getElementById('ccp_section').classList.toggle('hidden', !vis.ccp);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
}
|
| 333 |
|
| 334 |
function resetProducerFields() {
|
| 335 |
document.getElementById('producer1').value = '';
|
| 336 |
document.getElementById('producer2').value = '';
|
| 337 |
document.getElementById('client_exec').value = '';
|
|
|
|
| 338 |
document.getElementById('include_ce').checked = false;
|
| 339 |
document.getElementById('ce_picker').classList.add('hidden');
|
|
|
|
|
|
|
| 340 |
document.getElementById('include_ccp').checked = false;
|
| 341 |
document.getElementById('ccp_picker').classList.add('hidden');
|
| 342 |
document.getElementById('ccp_selection').value = '';
|
|
@@ -404,13 +471,24 @@ function getFormData() {
|
|
| 404 |
const agrName = agrSelect.options[agrSelect.selectedIndex]?.dataset?.name || '';
|
| 405 |
const vis = AGREEMENT_VISIBILITY[agrName] || {};
|
| 406 |
const isEmployeeOrig = document.getElementById('employee_as_originator').checked;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
return {
|
| 409 |
agreement_id: parseInt(agrSelect.value) || null,
|
| 410 |
producer1_prefix: vis.autoFill ? null : (document.getElementById('producer1').value || null),
|
| 411 |
producer2_prefix: document.getElementById('producer2').value || null,
|
| 412 |
-
include_ce:
|
| 413 |
-
ce_prefix:
|
| 414 |
include_ccp: document.getElementById('include_ccp').checked,
|
| 415 |
ccp_selection: document.getElementById('include_ccp').checked ? (document.getElementById('ccp_selection').value || null) : null,
|
| 416 |
employee_as_originator: isEmployeeOrig,
|
|
@@ -468,10 +546,12 @@ async function refreshSummary() {
|
|
| 468 |
html += '<table class="w-full border-collapse border border-gray-200 rounded-lg overflow-hidden">';
|
| 469 |
html += '<thead><tr class="sw-blue text-white text-sm">' +
|
| 470 |
'<th class="px-4 py-2 text-left">Role</th>' +
|
|
|
|
|
|
|
|
|
|
| 471 |
'<th class="px-4 py-2 text-left">Agreement Type</th>' +
|
| 472 |
'<th class="px-4 py-2 text-left">Commission %</th>' +
|
| 473 |
-
'<th class="px-4 py-2 text-left">
|
| 474 |
-
'<th class="px-4 py-2 text-left">ABE Status</th>' +
|
| 475 |
'</tr></thead><tbody>';
|
| 476 |
|
| 477 |
(yt.rows || []).forEach((row, ri) => {
|
|
@@ -479,10 +559,12 @@ async function refreshSummary() {
|
|
| 479 |
const abeClass = row.abe_status === 'ABE' ? 'text-green-700 font-semibold' : 'text-gray-600';
|
| 480 |
html += `<tr class="${bg}">` +
|
| 481 |
`<td class="px-4 py-2 text-sm font-semibold sw-blue-text">${row.role}</td>` +
|
|
|
|
|
|
|
|
|
|
| 482 |
`<td class="px-4 py-2 text-sm">${row.agreement_type}</td>` +
|
| 483 |
`<td class="px-4 py-2 text-sm font-semibold">${row.commission_pct}%</td>` +
|
| 484 |
`<td class="px-4 py-2 text-sm">${row.production_pct}%</td>` +
|
| 485 |
-
`<td class="px-4 py-2 text-sm ${abeClass}">${row.abe_status}</td>` +
|
| 486 |
`</tr>`;
|
| 487 |
});
|
| 488 |
|
|
@@ -500,6 +582,25 @@ async function refreshSummary() {
|
|
| 500 |
}
|
| 501 |
}
|
| 502 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
// Save draft
|
| 504 |
async function saveDraft() {
|
| 505 |
const btn = document.getElementById('save_draft_btn');
|
|
@@ -535,8 +636,111 @@ async function saveDraft() {
|
|
| 535 |
btn.textContent = 'Save Draft';
|
| 536 |
}
|
| 537 |
|
| 538 |
-
// Submit
|
| 539 |
-
async function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
const formData = getFormData();
|
| 541 |
if (currentDraftId) formData.draft_id = currentDraftId;
|
| 542 |
try {
|
|
@@ -546,6 +750,7 @@ async function submitForm() {
|
|
| 546 |
body: JSON.stringify(formData),
|
| 547 |
});
|
| 548 |
const data = await resp.json();
|
|
|
|
| 549 |
if (data.success) {
|
| 550 |
const sid = data.submission_id;
|
| 551 |
const btnArea = document.getElementById('submit_btn').parentElement;
|
|
@@ -558,7 +763,11 @@ async function submitForm() {
|
|
| 558 |
(data.pdf_available ?
|
| 559 |
'<a href="/api/download-pdf/' + sid + '?token=' + authToken + '" ' +
|
| 560 |
'class="inline-block bg-blue-600 text-white px-6 py-2 rounded-lg font-semibold hover:bg-blue-700 transition">' +
|
| 561 |
-
'Download
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
'<a href="/dashboard?token=' + authToken + '" ' +
|
| 563 |
'class="inline-block bg-gray-600 text-white px-6 py-2 rounded-lg font-semibold hover:bg-gray-700 transition">' +
|
| 564 |
'Go to Dashboard</a></div>' +
|
|
@@ -569,10 +778,12 @@ async function submitForm() {
|
|
| 569 |
} catch (e) {
|
| 570 |
alert('Submission failed: ' + e.message);
|
| 571 |
}
|
|
|
|
|
|
|
| 572 |
}
|
| 573 |
|
| 574 |
// Wire all observers
|
| 575 |
-
const refreshInputs = ['producer1', 'producer2', 'client_exec', 'orig_producer', 'orig_employee', 'ccp_selection'];
|
| 576 |
refreshInputs.forEach(id => {
|
| 577 |
const el = document.getElementById(id);
|
| 578 |
if (el) el.addEventListener('change', refreshSummary);
|
|
|
|
| 137 |
</select>
|
| 138 |
</div>
|
| 139 |
<div id="orig_employee_row" class="hidden">
|
| 140 |
+
<input type="text" id="orig_employee_search" placeholder="Type to search employees..."
|
| 141 |
+
class="border border-gray-300 rounded-md px-3 py-2 text-sm w-full md:w-1/2 mb-1"
|
| 142 |
+
autocomplete="off">
|
| 143 |
<select id="orig_employee" class="border border-gray-300 rounded-md px-3 py-2 text-sm w-full md:w-1/2">
|
| 144 |
<option value="">-- Select Employee --</option>
|
| 145 |
{% for e in employees %}
|
|
|
|
| 150 |
</div>
|
| 151 |
</div>
|
| 152 |
|
| 153 |
+
<!-- CE Layer (checkbox + picker) OR CE as Producer (required) -->
|
| 154 |
<div id="ce_section" class="hidden border-t pt-3 mt-3">
|
| 155 |
+
<!-- Standard CE checkbox mode -->
|
| 156 |
+
<div id="ce_checkbox_mode">
|
| 157 |
+
<label class="flex items-center space-x-2 text-sm font-medium text-gray-700 mb-2">
|
| 158 |
+
<input type="checkbox" id="include_ce">
|
| 159 |
+
<span>Include CE Layer (Client Executive)</span>
|
| 160 |
+
</label>
|
| 161 |
+
<div id="ce_picker" class="hidden">
|
| 162 |
+
<select id="client_exec" class="w-full md:w-1/2 border border-gray-300 rounded-md px-3 py-2 text-sm">
|
| 163 |
+
<option value="">-- Select CE --</option>
|
| 164 |
+
{% for p in producers %}
|
| 165 |
+
<option value="{{ p.prefix }}">{{ p.label }}</option>
|
| 166 |
+
{% endfor %}
|
| 167 |
+
</select>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
<!-- CE as Producer mode (required, no checkbox) -->
|
| 171 |
+
<div id="ce_producer_mode" class="hidden">
|
| 172 |
+
<label class="block text-sm font-medium text-gray-700 mb-1">Client Executive (Producer) <span class="text-red-500">*</span></label>
|
| 173 |
+
<select id="client_exec_required" class="w-full md:w-1/2 border border-gray-300 rounded-md px-3 py-2 text-sm">
|
| 174 |
+
<option value="">-- Select Client Executive --</option>
|
| 175 |
{% for p in producers %}
|
| 176 |
<option value="{{ p.prefix }}">{{ p.label }}</option>
|
| 177 |
{% endfor %}
|
| 178 |
</select>
|
| 179 |
+
<p class="text-xs text-gray-400 mt-1">CE acts as the primary producer (10% commission, 100% production credit, Non-ABE)</p>
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
|
|
|
|
| 221 |
class="bg-gray-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-gray-600 transition text-sm">
|
| 222 |
Save Draft
|
| 223 |
</button>
|
| 224 |
+
<button id="submit_btn" onclick="showSubmitModal()"
|
| 225 |
class="bg-green-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-green-700 transition disabled:opacity-50"
|
| 226 |
disabled>
|
| 227 |
Submit for Approval
|
| 228 |
</button>
|
| 229 |
</div>
|
| 230 |
<div id="draft_msg" class="text-right text-sm text-gray-400 mt-1 hidden"></div>
|
| 231 |
+
|
| 232 |
+
<!-- Submit Safeguard Modal -->
|
| 233 |
+
<div id="submit_modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
|
| 234 |
+
<div class="flex items-center justify-center min-h-screen px-4 py-8">
|
| 235 |
+
<!-- Backdrop -->
|
| 236 |
+
<div class="fixed inset-0 bg-black/50 transition-opacity" onclick="closeSubmitModal()"></div>
|
| 237 |
+
<!-- Modal Content -->
|
| 238 |
+
<div class="relative bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[85vh] flex flex-col z-10">
|
| 239 |
+
<!-- Modal Header -->
|
| 240 |
+
<div class="sw-blue text-white px-6 py-4 rounded-t-xl flex items-center justify-between">
|
| 241 |
+
<h2 class="text-lg font-bold">Review Your Commission Agreement Before Submitting</h2>
|
| 242 |
+
<button onclick="closeSubmitModal()" class="text-white/70 hover:text-white text-xl">×</button>
|
| 243 |
+
</div>
|
| 244 |
+
<!-- Modal Body (scrollable) -->
|
| 245 |
+
<div id="modal_body" class="overflow-y-auto flex-1 px-6 py-4">
|
| 246 |
+
<!-- Populated dynamically -->
|
| 247 |
+
</div>
|
| 248 |
+
<!-- Modal Footer -->
|
| 249 |
+
<div class="border-t px-6 py-4 flex justify-end space-x-3 bg-gray-50 rounded-b-xl">
|
| 250 |
+
<button onclick="closeSubmitModal()"
|
| 251 |
+
class="bg-gray-500 text-white px-6 py-2.5 rounded-lg font-semibold hover:bg-gray-600 transition text-sm">
|
| 252 |
+
Go Back & Edit
|
| 253 |
+
</button>
|
| 254 |
+
<button onclick="confirmSubmit()"
|
| 255 |
+
id="confirm_submit_btn"
|
| 256 |
+
class="bg-green-600 text-white px-6 py-2.5 rounded-lg font-semibold hover:bg-green-700 transition text-sm">
|
| 257 |
+
Confirm & Submit
|
| 258 |
+
</button>
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
</div>
|
| 264 |
{% endblock %}
|
| 265 |
|
|
|
|
| 349 |
section.classList.remove('hidden');
|
| 350 |
resetProducerFields();
|
| 351 |
|
| 352 |
+
const isCEasProducer = agrName === 'Ceded Accounts - CE as Producer';
|
| 353 |
const p1Section = document.getElementById('p1_section');
|
| 354 |
const p1Select = document.getElementById('producer1');
|
| 355 |
const p1Locked = document.getElementById('p1_locked_display');
|
|
|
|
| 379 |
document.getElementById('originator_section').classList.toggle('hidden', !vis.originator);
|
| 380 |
document.getElementById('ce_section').classList.toggle('hidden', !vis.ce);
|
| 381 |
document.getElementById('ccp_section').classList.toggle('hidden', !vis.ccp);
|
| 382 |
+
|
| 383 |
+
// CE as Producer mode: show required picker, hide checkbox mode
|
| 384 |
+
const ceCheckboxMode = document.getElementById('ce_checkbox_mode');
|
| 385 |
+
const ceProducerMode = document.getElementById('ce_producer_mode');
|
| 386 |
+
if (isCEasProducer) {
|
| 387 |
+
ceCheckboxMode.classList.add('hidden');
|
| 388 |
+
ceProducerMode.classList.remove('hidden');
|
| 389 |
+
// Auto-check include_ce so getFormData sends the right data
|
| 390 |
+
document.getElementById('include_ce').checked = true;
|
| 391 |
+
} else {
|
| 392 |
+
ceCheckboxMode.classList.remove('hidden');
|
| 393 |
+
ceProducerMode.classList.add('hidden');
|
| 394 |
+
document.getElementById('client_exec_required').value = '';
|
| 395 |
+
}
|
| 396 |
}
|
| 397 |
|
| 398 |
function resetProducerFields() {
|
| 399 |
document.getElementById('producer1').value = '';
|
| 400 |
document.getElementById('producer2').value = '';
|
| 401 |
document.getElementById('client_exec').value = '';
|
| 402 |
+
document.getElementById('client_exec_required').value = '';
|
| 403 |
document.getElementById('include_ce').checked = false;
|
| 404 |
document.getElementById('ce_picker').classList.add('hidden');
|
| 405 |
+
document.getElementById('ce_checkbox_mode').classList.remove('hidden');
|
| 406 |
+
document.getElementById('ce_producer_mode').classList.add('hidden');
|
| 407 |
document.getElementById('include_ccp').checked = false;
|
| 408 |
document.getElementById('ccp_picker').classList.add('hidden');
|
| 409 |
document.getElementById('ccp_selection').value = '';
|
|
|
|
| 471 |
const agrName = agrSelect.options[agrSelect.selectedIndex]?.dataset?.name || '';
|
| 472 |
const vis = AGREEMENT_VISIBILITY[agrName] || {};
|
| 473 |
const isEmployeeOrig = document.getElementById('employee_as_originator').checked;
|
| 474 |
+
const isCEasProducer = agrName === 'Ceded Accounts - CE as Producer';
|
| 475 |
+
|
| 476 |
+
// CE prefix: from required picker (CE as Producer mode) or checkbox picker (standard mode)
|
| 477 |
+
let ceChecked = document.getElementById('include_ce').checked;
|
| 478 |
+
let cePrefix = null;
|
| 479 |
+
if (isCEasProducer) {
|
| 480 |
+
ceChecked = true;
|
| 481 |
+
cePrefix = document.getElementById('client_exec_required').value || null;
|
| 482 |
+
} else if (ceChecked) {
|
| 483 |
+
cePrefix = document.getElementById('client_exec').value || null;
|
| 484 |
+
}
|
| 485 |
|
| 486 |
return {
|
| 487 |
agreement_id: parseInt(agrSelect.value) || null,
|
| 488 |
producer1_prefix: vis.autoFill ? null : (document.getElementById('producer1').value || null),
|
| 489 |
producer2_prefix: document.getElementById('producer2').value || null,
|
| 490 |
+
include_ce: ceChecked,
|
| 491 |
+
ce_prefix: cePrefix,
|
| 492 |
include_ccp: document.getElementById('include_ccp').checked,
|
| 493 |
ccp_selection: document.getElementById('include_ccp').checked ? (document.getElementById('ccp_selection').value || null) : null,
|
| 494 |
employee_as_originator: isEmployeeOrig,
|
|
|
|
| 546 |
html += '<table class="w-full border-collapse border border-gray-200 rounded-lg overflow-hidden">';
|
| 547 |
html += '<thead><tr class="sw-blue text-white text-sm">' +
|
| 548 |
'<th class="px-4 py-2 text-left">Role</th>' +
|
| 549 |
+
'<th class="px-4 py-2 text-left">Name</th>' +
|
| 550 |
+
'<th class="px-4 py-2 text-left">Code</th>' +
|
| 551 |
+
'<th class="px-4 py-2 text-left">ABE Status</th>' +
|
| 552 |
'<th class="px-4 py-2 text-left">Agreement Type</th>' +
|
| 553 |
'<th class="px-4 py-2 text-left">Commission %</th>' +
|
| 554 |
+
'<th class="px-4 py-2 text-left">Prod Credit</th>' +
|
|
|
|
| 555 |
'</tr></thead><tbody>';
|
| 556 |
|
| 557 |
(yt.rows || []).forEach((row, ri) => {
|
|
|
|
| 559 |
const abeClass = row.abe_status === 'ABE' ? 'text-green-700 font-semibold' : 'text-gray-600';
|
| 560 |
html += `<tr class="${bg}">` +
|
| 561 |
`<td class="px-4 py-2 text-sm font-semibold sw-blue-text">${row.role}</td>` +
|
| 562 |
+
`<td class="px-4 py-2 text-sm">${row.name || 'β'}</td>` +
|
| 563 |
+
`<td class="px-4 py-2 text-sm font-mono">${row.code || 'β'}</td>` +
|
| 564 |
+
`<td class="px-4 py-2 text-sm ${abeClass}">${row.abe_status}</td>` +
|
| 565 |
`<td class="px-4 py-2 text-sm">${row.agreement_type}</td>` +
|
| 566 |
`<td class="px-4 py-2 text-sm font-semibold">${row.commission_pct}%</td>` +
|
| 567 |
`<td class="px-4 py-2 text-sm">${row.production_pct}%</td>` +
|
|
|
|
| 568 |
`</tr>`;
|
| 569 |
});
|
| 570 |
|
|
|
|
| 582 |
}
|
| 583 |
}
|
| 584 |
|
| 585 |
+
// Employee search filter for originator
|
| 586 |
+
(function() {
|
| 587 |
+
const searchInput = document.getElementById('orig_employee_search');
|
| 588 |
+
const empSelect = document.getElementById('orig_employee');
|
| 589 |
+
const allOptions = Array.from(empSelect.options).slice(1); // skip placeholder
|
| 590 |
+
|
| 591 |
+
searchInput.addEventListener('input', function() {
|
| 592 |
+
const filter = this.value.toLowerCase();
|
| 593 |
+
const current = empSelect.value;
|
| 594 |
+
empSelect.innerHTML = '<option value="">-- Select Employee --</option>';
|
| 595 |
+
allOptions.forEach(opt => {
|
| 596 |
+
if (!filter || opt.text.toLowerCase().includes(filter)) {
|
| 597 |
+
empSelect.add(opt.cloneNode(true));
|
| 598 |
+
}
|
| 599 |
+
});
|
| 600 |
+
empSelect.value = current;
|
| 601 |
+
});
|
| 602 |
+
})();
|
| 603 |
+
|
| 604 |
// Save draft
|
| 605 |
async function saveDraft() {
|
| 606 |
const btn = document.getElementById('save_draft_btn');
|
|
|
|
| 636 |
btn.textContent = 'Save Draft';
|
| 637 |
}
|
| 638 |
|
| 639 |
+
// Submit safeguard β show modal with preview before submission
|
| 640 |
+
async function showSubmitModal() {
|
| 641 |
+
const formData = getFormData();
|
| 642 |
+
if (!formData.agreement_id) return;
|
| 643 |
+
|
| 644 |
+
// Fetch fresh summary for the modal
|
| 645 |
+
try {
|
| 646 |
+
const resp = await fetch('/api/compute-summary', {
|
| 647 |
+
method: 'POST',
|
| 648 |
+
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken},
|
| 649 |
+
body: JSON.stringify(formData),
|
| 650 |
+
});
|
| 651 |
+
const data = await resp.json();
|
| 652 |
+
|
| 653 |
+
if (data.warnings && data.warnings.length > 0) {
|
| 654 |
+
alert('Please resolve all warnings before submitting.');
|
| 655 |
+
return;
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
// Build modal body content
|
| 659 |
+
let html = '';
|
| 660 |
+
|
| 661 |
+
// Form details summary
|
| 662 |
+
html += '<div class="mb-4 bg-gray-50 rounded-lg p-4 text-sm space-y-1">';
|
| 663 |
+
html += `<div><strong>Client:</strong> ${formData.client_name || formData.client_code || 'β'}</div>`;
|
| 664 |
+
html += `<div><strong>Agreement:</strong> ${data.agreement_name || 'β'}</div>`;
|
| 665 |
+
html += `<div><strong>Department:</strong> ${formData.dept_code || 'β'}</div>`;
|
| 666 |
+
html += `<div><strong>Effective Date:</strong> ${formData.effective_date || 'β'}</div>`;
|
| 667 |
+
if (formData.association) html += `<div><strong>Association:</strong> ${formData.association}</div>`;
|
| 668 |
+
if (formData.legacy_producer) html += `<div><strong>Legacy Producer:</strong> ${formData.legacy_producer}</div>`;
|
| 669 |
+
if (formData.revenue) html += `<div><strong>Estimated Revenue:</strong> $${Number(formData.revenue).toLocaleString()}</div>`;
|
| 670 |
+
html += '</div>';
|
| 671 |
+
|
| 672 |
+
// Bell ringer
|
| 673 |
+
const bell = data.bell_ringer || {};
|
| 674 |
+
if (bell.status === 'triggered') {
|
| 675 |
+
html += '<div class="mb-4 bg-orange-50 border border-orange-300 text-orange-800 px-4 py-2 rounded text-sm font-semibold">' +
|
| 676 |
+
'Bell Ringer TRIGGERED (threshold $' + (bell.threshold || 0).toLocaleString() + ')</div>';
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
// Commission tables
|
| 680 |
+
const yearTables = data.year_tables || [];
|
| 681 |
+
if (yearTables.length > 0) {
|
| 682 |
+
html += '<h3 class="font-bold sw-blue-text text-base mb-2">Commission Structure</h3>';
|
| 683 |
+
yearTables.forEach(yt => {
|
| 684 |
+
html += `<div class="mb-4">`;
|
| 685 |
+
html += `<h4 class="font-semibold text-sm sw-blue-text mb-1">${yt.year_label}</h4>`;
|
| 686 |
+
html += '<table class="w-full border-collapse border border-gray-200 rounded text-sm mb-2">';
|
| 687 |
+
html += '<thead><tr class="sw-blue text-white text-xs">' +
|
| 688 |
+
'<th class="px-3 py-1.5 text-left">Role</th>' +
|
| 689 |
+
'<th class="px-3 py-1.5 text-left">Name</th>' +
|
| 690 |
+
'<th class="px-3 py-1.5 text-left">Code</th>' +
|
| 691 |
+
'<th class="px-3 py-1.5 text-left">ABE</th>' +
|
| 692 |
+
'<th class="px-3 py-1.5 text-left">Agreement</th>' +
|
| 693 |
+
'<th class="px-3 py-1.5 text-left">Comm%</th>' +
|
| 694 |
+
'<th class="px-3 py-1.5 text-left">Prod Credit</th>' +
|
| 695 |
+
'</tr></thead><tbody>';
|
| 696 |
+
(yt.rows || []).forEach((row, ri) => {
|
| 697 |
+
const bg = ri % 2 === 1 ? 'bg-gray-50' : '';
|
| 698 |
+
html += `<tr class="${bg}">` +
|
| 699 |
+
`<td class="px-3 py-1.5 font-semibold">${row.role}</td>` +
|
| 700 |
+
`<td class="px-3 py-1.5">${row.name || 'β'}</td>` +
|
| 701 |
+
`<td class="px-3 py-1.5 font-mono">${row.code || 'β'}</td>` +
|
| 702 |
+
`<td class="px-3 py-1.5">${row.abe_status}</td>` +
|
| 703 |
+
`<td class="px-3 py-1.5">${row.agreement_type}</td>` +
|
| 704 |
+
`<td class="px-3 py-1.5 font-semibold">${row.commission_pct}%</td>` +
|
| 705 |
+
`<td class="px-3 py-1.5">${row.production_pct}%</td>` +
|
| 706 |
+
`</tr>`;
|
| 707 |
+
});
|
| 708 |
+
html += '</tbody></table></div>';
|
| 709 |
+
});
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
// Participants for approval
|
| 713 |
+
const participants = data.participants || [];
|
| 714 |
+
if (participants.length > 0) {
|
| 715 |
+
html += '<h3 class="font-bold sw-blue-text text-base mb-2 mt-4">Participants Requiring Approval</h3>';
|
| 716 |
+
html += '<div class="space-y-1 text-sm">';
|
| 717 |
+
participants.forEach(p => {
|
| 718 |
+
html += `<div class="flex items-center space-x-2">` +
|
| 719 |
+
`<span class="inline-block w-2 h-2 rounded-full bg-yellow-400"></span>` +
|
| 720 |
+
`<span><strong>${p.role}:</strong> ${p.name} (${p.code})</span></div>`;
|
| 721 |
+
});
|
| 722 |
+
html += '</div>';
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
document.getElementById('modal_body').innerHTML = html;
|
| 726 |
+
document.getElementById('submit_modal').classList.remove('hidden');
|
| 727 |
+
document.body.style.overflow = 'hidden';
|
| 728 |
+
|
| 729 |
+
} catch (e) {
|
| 730 |
+
alert('Failed to generate preview: ' + e.message);
|
| 731 |
+
}
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
function closeSubmitModal() {
|
| 735 |
+
document.getElementById('submit_modal').classList.add('hidden');
|
| 736 |
+
document.body.style.overflow = '';
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
async function confirmSubmit() {
|
| 740 |
+
const btn = document.getElementById('confirm_submit_btn');
|
| 741 |
+
btn.disabled = true;
|
| 742 |
+
btn.textContent = 'Submitting...';
|
| 743 |
+
|
| 744 |
const formData = getFormData();
|
| 745 |
if (currentDraftId) formData.draft_id = currentDraftId;
|
| 746 |
try {
|
|
|
|
| 750 |
body: JSON.stringify(formData),
|
| 751 |
});
|
| 752 |
const data = await resp.json();
|
| 753 |
+
closeSubmitModal();
|
| 754 |
if (data.success) {
|
| 755 |
const sid = data.submission_id;
|
| 756 |
const btnArea = document.getElementById('submit_btn').parentElement;
|
|
|
|
| 763 |
(data.pdf_available ?
|
| 764 |
'<a href="/api/download-pdf/' + sid + '?token=' + authToken + '" ' +
|
| 765 |
'class="inline-block bg-blue-600 text-white px-6 py-2 rounded-lg font-semibold hover:bg-blue-700 transition">' +
|
| 766 |
+
'Download PDF</a>' : '') +
|
| 767 |
+
(data.docx_available ?
|
| 768 |
+
'<a href="/api/download-docx/' + sid + '?token=' + authToken + '" ' +
|
| 769 |
+
'class="inline-block bg-indigo-600 text-white px-6 py-2 rounded-lg font-semibold hover:bg-indigo-700 transition">' +
|
| 770 |
+
'Download Word Doc</a>' : '') +
|
| 771 |
'<a href="/dashboard?token=' + authToken + '" ' +
|
| 772 |
'class="inline-block bg-gray-600 text-white px-6 py-2 rounded-lg font-semibold hover:bg-gray-700 transition">' +
|
| 773 |
'Go to Dashboard</a></div>' +
|
|
|
|
| 778 |
} catch (e) {
|
| 779 |
alert('Submission failed: ' + e.message);
|
| 780 |
}
|
| 781 |
+
btn.disabled = false;
|
| 782 |
+
btn.textContent = 'Confirm & Submit';
|
| 783 |
}
|
| 784 |
|
| 785 |
// Wire all observers
|
| 786 |
+
const refreshInputs = ['producer1', 'producer2', 'client_exec', 'client_exec_required', 'orig_producer', 'orig_employee', 'ccp_selection'];
|
| 787 |
refreshInputs.forEach(id => {
|
| 788 |
const el = document.getElementById(id);
|
| 789 |
if (el) el.addEventListener('change', refreshSummary);
|
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Commission Agreement Intake Form - Refactor Instructions
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
I need you to refactor the existing Commission Agreement Intake web app with significant structural and logic changes. This is a complete rework of field visibility, commission calculation logic, and the preview display.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 1. FORM STRUCTURE REORGANIZATION
|
| 9 |
+
|
| 10 |
+
### Agreement Type as Primary Driver
|
| 11 |
+
**Move the Agreement Type dropdown to the TOP of the form** (right after Account selection). All subsequent fields should show/hide based on the selected agreement type.
|
| 12 |
+
|
| 13 |
+
### Dynamic Field Visibility Rules
|
| 14 |
+
|
| 15 |
+
Replace warning text with actual field visibility control:
|
| 16 |
+
|
| 17 |
+
| Agreement Type | Producer 1 | Producer 2 | Originator | CE Layer | CCP Layer | Legacy |
|
| 18 |
+
|----------------|------------|------------|------------|----------|-----------|---------|
|
| 19 |
+
| Standard Agreement | β | β | β | β | β | β |
|
| 20 |
+
| Standard Agreement PL | β | β | β | β | β | β |
|
| 21 |
+
| PhD Lead / PhD Agreement | β | β | β | β | β | β |
|
| 22 |
+
| Partner Agreement | β | β | β | β | β | β |
|
| 23 |
+
| Closer Agreement | β | β | β | β | β | β |
|
| 24 |
+
| Referral / Origination Fee Agreement | β | β | β | β | β | β |
|
| 25 |
+
| Bond Agreement | β | β | β | β | β | β |
|
| 26 |
+
| Acquisition Agreement | β | β | β | β | β | β |
|
| 27 |
+
| Ceded Accounts Agreement | β | β | β | β | β | β |
|
| 28 |
+
| Ceded Accounts - CE as Producer | β | β | β | β | β | β |
|
| 29 |
+
| House Standard Agreement / Emerging Markets - House Standard | β | β | β | β | β | β |
|
| 30 |
+
| Emerging Markets Agreement | β | β | β | β | β | β |
|
| 31 |
+
| Life Origination Fee Agreement | β | β | β | β | β | β |
|
| 32 |
+
| Gebhardt Agreement | β (auto-fill) | β | β | β | β | β |
|
| 33 |
+
| Beacham Agreement | β (auto-fill) | β | β | β | β | β |
|
| 34 |
+
|
| 35 |
+
**Key Implementation Notes:**
|
| 36 |
+
- **Legacy Field**: Always visible for all agreement types. This is a simple text input for historical record-keeping with no active data connections.
|
| 37 |
+
- **Producer Fields**: Should clear/reset when agreement type changes
|
| 38 |
+
- **Originator Field**: ONLY appears for "Referral / Origination Fee Agreement"
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 2. ORIGINATING PRODUCER FIELD (Referral Agreements Only)
|
| 43 |
+
|
| 44 |
+
### When "Referral / Origination Fee Agreement" is selected:
|
| 45 |
+
|
| 46 |
+
1. Show "Originating Producer" section with:
|
| 47 |
+
- People Picker control (linked to Active Directory)
|
| 48 |
+
- "Employee as Originator" checkbox
|
| 49 |
+
|
| 50 |
+
2. **Remove the 1/2 dropdown** next to the originator field
|
| 51 |
+
|
| 52 |
+
3. When "Employee as Originator" is checked:
|
| 53 |
+
- Allow selection of any employee via People Picker
|
| 54 |
+
- Auto-assign code: **"EMPOR2"** (literal string, same for all employees)
|
| 55 |
+
- This employee must approve the agreement (add to approval flow)
|
| 56 |
+
|
| 57 |
+
4. When "Employee as Originator" is unchecked:
|
| 58 |
+
- Allow selection of any producer
|
| 59 |
+
- Generate code normally (standard producer code logic)
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## 3. CE AND CCP LAYER LOGIC OVERHAUL
|
| 64 |
+
|
| 65 |
+
### CE Layer (Client Executive)
|
| 66 |
+
- Change to **checkbox-based**: "Include CE Layer"
|
| 67 |
+
- When checked: Show People Picker to select CE
|
| 68 |
+
- When unchecked: Hide People Picker, exclude from calculations
|
| 69 |
+
|
| 70 |
+
### CCP Layer (Complex Claims Practice)
|
| 71 |
+
- Change to **checkbox-based**: "Include Complex Claims Practice"
|
| 72 |
+
- When checked: Show **dropdown** with only two options:
|
| 73 |
+
- "Mike Parsa"
|
| 74 |
+
- "House Account"
|
| 75 |
+
- When unchecked: Hide dropdown, exclude from calculations
|
| 76 |
+
|
| 77 |
+
**CRITICAL**: Even if "House Account" is selected for CCP, **Mike Parsa must still approve** in the acknowledgment flow.
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 4. LAYER DEDUCTION CALCULATION LOGIC
|
| 82 |
+
|
| 83 |
+
### Default Rule (Most Agreement Types)
|
| 84 |
+
**Layers deduct evenly from ALL producers present:**
|
| 85 |
+
|
| 86 |
+
- If P1 and P2 exist:
|
| 87 |
+
- CE takes 1% from P1 + 1% from P2 = 2% total
|
| 88 |
+
- CCP takes 2.5% from P1 + 2.5% from P2 = 5% total
|
| 89 |
+
|
| 90 |
+
- If only P1 exists:
|
| 91 |
+
- CE takes 2% from P1
|
| 92 |
+
- CCP takes 5% from P1
|
| 93 |
+
|
| 94 |
+
### Exception: Closer Agreement & Referral Agreement
|
| 95 |
+
**Layers deduct ONLY from Producer 1:**
|
| 96 |
+
|
| 97 |
+
- CE takes 2% from P1 (P2 unaffected)
|
| 98 |
+
- CCP takes 5% from P1 (P2 unaffected)
|
| 99 |
+
|
| 100 |
+
**Note**: "Life Origination Fee Agreement" is NOT an exception - it follows the default rule (but only has P1 anyway).
|
| 101 |
+
|
| 102 |
+
### Agreement Types Using Default Rule:
|
| 103 |
+
- Standard Agreement
|
| 104 |
+
- Standard Agreement PL
|
| 105 |
+
- PhD Lead / PhD Agreement
|
| 106 |
+
- Partner Agreement
|
| 107 |
+
- Bond Agreement
|
| 108 |
+
- Acquisition Agreement
|
| 109 |
+
- Ceded Accounts Agreement
|
| 110 |
+
- Ceded Accounts - CE as Producer
|
| 111 |
+
- House Standard Agreement
|
| 112 |
+
- Emerging Markets Agreement
|
| 113 |
+
- Life Origination Fee Agreement
|
| 114 |
+
- Gebhardt Agreement
|
| 115 |
+
- Beacham Agreement
|
| 116 |
+
|
| 117 |
+
### Agreement Types Using P1-Only Deduction:
|
| 118 |
+
- **Closer Agreement**
|
| 119 |
+
- **Referral / Origination Fee Agreement**
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## 5. BELL RINGER LOGIC - DEPARTMENT-BASED ONLY
|
| 124 |
+
|
| 125 |
+
### Remove Market Segment Dropdown Entirely
|
| 126 |
+
|
| 127 |
+
Bell Ringer thresholds should be determined **only by Department Code**:
|
| 128 |
+
|
| 129 |
+
| Department Code | Threshold | Notes |
|
| 130 |
+
|-----------------|-----------|-------|
|
| 131 |
+
| BD - Bond | No threshold | Never triggers Bell Ringer |
|
| 132 |
+
| CL - Commercial Lines | $75,000 | Middle Market |
|
| 133 |
+
| CON - Connect | $7,500 | (Emerging Markets Commercial) |
|
| 134 |
+
| EB - Employee Benefits | $75,000 | Middle Market Benefits |
|
| 135 |
+
| PL - Personal Lines | $4,500 | Always emerging |
|
| 136 |
+
| VAN - Vanguard | $10,000 | (Emerging Markets Benefits) |
|
| 137 |
+
| LIF - Life | No threshold | Never triggers Bell Ringer |
|
| 138 |
+
|
| 139 |
+
**Logic**:
|
| 140 |
+
```javascript
|
| 141 |
+
const BELL_THRESHOLDS = {
|
| 142 |
+
'CL': 75000,
|
| 143 |
+
'CON': 7500,
|
| 144 |
+
'EB': 75000,
|
| 145 |
+
'PL': 4500,
|
| 146 |
+
'VAN': 10000
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
// BD and LIF have no threshold (undefined = no bell ringer)
|
| 150 |
+
const isBellRinger = BELL_THRESHOLDS[deptCode] && estimatedRevenue >= BELL_THRESHOLDS[deptCode];
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## 6. FIXED-CODE AGREEMENT HANDLING
|
| 156 |
+
|
| 157 |
+
### House Standard Agreement (Emerging Markets - House Standard)
|
| 158 |
+
When this agreement type is selected:
|
| 159 |
+
|
| 160 |
+
1. **Producer 1 field appears but is for APPROVAL PROCESSOR only**
|
| 161 |
+
2. Commission calculation shows:
|
| 162 |
+
- **House Account (ACCHO1)**: 100% commission, 100% production credit
|
| 163 |
+
3. The selected Producer 1 processes approval but receives NO commission
|
| 164 |
+
|
| 165 |
+
### Gebhardt Agreement
|
| 166 |
+
When selected:
|
| 167 |
+
1. **Auto-fill Producer 1 field** with:
|
| 168 |
+
- Name: "Steve Gebhardt"
|
| 169 |
+
- Code: "GEBST2"
|
| 170 |
+
2. Field is **locked/disabled** (cannot change)
|
| 171 |
+
3. Steve Gebhardt appears in commission preview with his specific percentages
|
| 172 |
+
4. Steve Gebhardt must approve
|
| 173 |
+
|
| 174 |
+
### Beacham Agreement
|
| 175 |
+
When selected:
|
| 176 |
+
1. **Auto-fill Producer 1 field** with:
|
| 177 |
+
- Name: "Wade Beacham"
|
| 178 |
+
- Code: "BEAWA2"
|
| 179 |
+
2. Field is **locked/disabled** (cannot change)
|
| 180 |
+
3. Wade Beacham appears in commission preview with his specific percentages
|
| 181 |
+
4. Wade Beacham must approve
|
| 182 |
+
|
| 183 |
+
**Note**: For all three types, CE and CCP can still be added as layers.
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## 7. COMMISSION PREVIEW TABLE - COMPLETE REDESIGN
|
| 188 |
+
|
| 189 |
+
### Current Problem
|
| 190 |
+
Currently shows all years in a single table with years as columns.
|
| 191 |
+
|
| 192 |
+
### New Design: One Table Per Year
|
| 193 |
+
|
| 194 |
+
Generate a **separate table for each year** showing the commission structure for that specific year.
|
| 195 |
+
|
| 196 |
+
### Table Structure
|
| 197 |
+
|
| 198 |
+
**Each table should have these columns:**
|
| 199 |
+
|
| 200 |
+
| Role | Agreement Type | Commission % | Production % | ABE Status |
|
| 201 |
+
|------|----------------|--------------|--------------|------------|
|
| 202 |
+
|
| 203 |
+
- **Role**: "Producer 1", "Producer 2", "Originating Producer", "CE Layer", "CCP Layer"
|
| 204 |
+
- **Agreement Type**: The specific agreement name for that role in that year (e.g., "PhD Lead Agreement", "Standard Agreement")
|
| 205 |
+
- **Commission %**: Percentage only (e.g., "10%", "15%")
|
| 206 |
+
- **Production %**: Percentage only (e.g., "100%", "50%", "0%")
|
| 207 |
+
- **ABE Status**:
|
| 208 |
+
- "ABE" if code = 1
|
| 209 |
+
- "Non-ABE" if code = 2
|
| 210 |
+
|
| 211 |
+
### DO NOT SHOW:
|
| 212 |
+
- Dollar amounts
|
| 213 |
+
- Any financial calculations
|
| 214 |
+
- Just percentages
|
| 215 |
+
|
| 216 |
+
### Example Output for PhD Agreement:
|
| 217 |
+
|
| 218 |
+
**Year 1**
|
| 219 |
+
| Role | Agreement Type | Commission % | Production % | ABE Status |
|
| 220 |
+
|------|----------------|--------------|--------------|------------|
|
| 221 |
+
| Producer 1 | PhD Lead Agreement | 10% | 100% | ABE |
|
| 222 |
+
| Producer 2 | PhD Agreement | 20% | 0% | Non-ABE |
|
| 223 |
+
| CE Layer | Client Executive Agreement | 2% | 0% | ABE |
|
| 224 |
+
| CCP Layer | Complex Claims Practice | 5% | 0% | ABE |
|
| 225 |
+
|
| 226 |
+
**Year 2**
|
| 227 |
+
| Role | Agreement Type | Commission % | Production % | ABE Status |
|
| 228 |
+
|------|----------------|--------------|--------------|------------|
|
| 229 |
+
| Producer 1 | PhD Lead Agreement | 15% | 100% | ABE |
|
| 230 |
+
| Producer 2 | PhD Agreement | 15% | 0% | Non-ABE |
|
| 231 |
+
| CE Layer | Client Executive Agreement | 2% | 0% | ABE |
|
| 232 |
+
| CCP Layer | Complex Claims Practice | 5% | 0% | ABE |
|
| 233 |
+
|
| 234 |
+
**Year 3+**
|
| 235 |
+
| Role | Agreement Type | Commission % | Production % | ABE Status |
|
| 236 |
+
|------|----------------|--------------|--------------|------------|
|
| 237 |
+
| Producer 1 | Standard Agreement | 30% | 100% | ABE |
|
| 238 |
+
| CE Layer | Client Executive Agreement | 2% | 0% | ABE |
|
| 239 |
+
| CCP Layer | Complex Claims Practice | 5% | 0% | ABE |
|
| 240 |
+
|
| 241 |
+
**Key Points:**
|
| 242 |
+
- Year 3 shows the transition to Standard Agreement for P1
|
| 243 |
+
- P2 no longer appears (PhD agreement ends)
|
| 244 |
+
- Agreement Type column shows the actual agreement name change
|
| 245 |
+
- All values are percentages, never dollar amounts
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
## 8. IMPLEMENTATION CHECKLIST
|
| 250 |
+
|
| 251 |
+
### Phase 1: Field Visibility
|
| 252 |
+
- [ ] Move Agreement Type selector to top of form
|
| 253 |
+
- [ ] Implement visibility logic for all fields based on agreement type
|
| 254 |
+
- [ ] Add field reset logic when agreement type changes
|
| 255 |
+
- [ ] Implement Legacy text field (always visible)
|
| 256 |
+
|
| 257 |
+
### Phase 2: Originating Producer
|
| 258 |
+
- [ ] Add Originating Producer section (Referral agreements only)
|
| 259 |
+
- [ ] Remove 1/2 dropdown from originator
|
| 260 |
+
- [ ] Implement "Employee as Originator" checkbox
|
| 261 |
+
- [ ] Auto-assign "EMPOR2" code for employee originators
|
| 262 |
+
- [ ] Add employee originator to approval flow
|
| 263 |
+
|
| 264 |
+
### Phase 3: Layer Controls
|
| 265 |
+
- [ ] Convert CE to checkbox + People Picker pattern
|
| 266 |
+
- [ ] Convert CCP to checkbox + dropdown (Mike Parsa / House Account only)
|
| 267 |
+
- [ ] Ensure Mike Parsa approval even with House Account selected
|
| 268 |
+
|
| 269 |
+
### Phase 4: Commission Calculation
|
| 270 |
+
- [ ] Implement default layer deduction logic (split across all producers)
|
| 271 |
+
- [ ] Implement exception for Closer Agreement (P1 only)
|
| 272 |
+
- [ ] Implement exception for Referral Agreement (P1 only)
|
| 273 |
+
- [ ] Verify Life Origination uses default logic (not exception)
|
| 274 |
+
|
| 275 |
+
### Phase 5: Bell Ringer
|
| 276 |
+
- [ ] Remove Market Segment dropdown entirely
|
| 277 |
+
- [ ] Implement department-only threshold logic
|
| 278 |
+
- [ ] Update threshold values per specifications
|
| 279 |
+
|
| 280 |
+
### Phase 6: Fixed Code Agreements
|
| 281 |
+
- [ ] Implement House Standard logic (House Account gets 100%)
|
| 282 |
+
- [ ] Implement Gebhardt auto-fill and lock
|
| 283 |
+
- [ ] Implement Beacham auto-fill and lock
|
| 284 |
+
|
| 285 |
+
### Phase 7: Preview Tables
|
| 286 |
+
- [ ] Redesign to show one table per year
|
| 287 |
+
- [ ] Add "Agreement Type" column
|
| 288 |
+
- [ ] Add "ABE Status" column
|
| 289 |
+
- [ ] Remove all dollar amounts
|
| 290 |
+
- [ ] Show only percentages
|
| 291 |
+
- [ ] Handle year transitions (e.g., PhD β Standard in Year 3)
|
| 292 |
+
|
| 293 |
+
---
|
| 294 |
+
|
| 295 |
+
## 9. TESTING SCENARIOS
|
| 296 |
+
|
| 297 |
+
After implementation, test these specific cases:
|
| 298 |
+
|
| 299 |
+
1. **PhD Agreement with both producers and layers**
|
| 300 |
+
- Verify Year 1, 2, 3+ tables appear separately
|
| 301 |
+
- Verify Year 3 shows "Standard Agreement" for P1
|
| 302 |
+
- Verify layers deduct evenly from both producers in Years 1-2
|
| 303 |
+
- Verify P2 disappears in Year 3
|
| 304 |
+
|
| 305 |
+
2. **Closer Agreement with layers**
|
| 306 |
+
- Verify layers deduct ONLY from P1
|
| 307 |
+
- Verify P2 commission unchanged
|
| 308 |
+
|
| 309 |
+
3. **Referral Agreement**
|
| 310 |
+
- Verify Originator field appears
|
| 311 |
+
- Verify "Employee as Originator" assigns "EMPOR2"
|
| 312 |
+
- Verify layers deduct ONLY from P1
|
| 313 |
+
|
| 314 |
+
4. **House Standard Agreement**
|
| 315 |
+
- Verify P1 is approval processor only
|
| 316 |
+
- Verify House Account (ACCHO1) shows 100%/100% in preview
|
| 317 |
+
|
| 318 |
+
5. **Gebhardt/Beacham Agreements**
|
| 319 |
+
- Verify names auto-fill and lock
|
| 320 |
+
- Verify they appear in approval flow
|
| 321 |
+
|
| 322 |
+
6. **Bell Ringer Logic**
|
| 323 |
+
- Test CL department with $75K+ revenue β Bell Ringer triggered
|
| 324 |
+
- Test PL department with $4,500+ revenue β Bell Ringer triggered
|
| 325 |
+
- Test BD department with any revenue β No Bell Ringer
|
| 326 |
+
|
| 327 |
+
7. **Field Visibility**
|
| 328 |
+
- Change agreement types and verify correct fields show/hide
|
| 329 |
+
- Verify producer fields clear when switching types
|
| 330 |
+
- Verify Legacy field always visible
|
| 331 |
+
|
| 332 |
+
---
|
| 333 |
+
|
| 334 |
+
## 10. TECHNICAL NOTES
|
| 335 |
+
|
| 336 |
+
### State Management Considerations
|
| 337 |
+
- Agreement type selection should trigger immediate field visibility updates
|
| 338 |
+
- Producer field values should clear on agreement type change
|
| 339 |
+
- CE/CCP checkboxes should control visibility AND calculation inclusion
|
| 340 |
+
- Fixed-code agreements need special handling for disabled fields
|
| 341 |
+
|
| 342 |
+
### Validation Rules
|
| 343 |
+
- House Standard: Require P1 selection even though P1 gets no commission
|
| 344 |
+
- Gebhardt/Beacham: P1 field is pre-filled and locked
|
| 345 |
+
- Originator with "Employee as Originator": Must select from People Picker
|
| 346 |
+
- All agreements: Validate required participants based on agreement type
|
| 347 |
+
|
| 348 |
+
### Code Organization
|
| 349 |
+
- Create helper function: `getVisibleFields(agreementType)` β returns field visibility config
|
| 350 |
+
- Create helper function: `calculateLayerDeductions(agreementType, producers, layers)` β handles split logic
|
| 351 |
+
- Create helper function: `generateYearTables(agreementType, participants)` β builds per-year preview tables
|
| 352 |
+
- Create helper function: `getBellRingerThreshold(deptCode)` β returns threshold or undefined
|
| 353 |
+
|
| 354 |
+
---
|
| 355 |
+
|
| 356 |
+
## PRIORITY ORDER
|
| 357 |
+
|
| 358 |
+
If doing this incrementally, implement in this order:
|
| 359 |
+
|
| 360 |
+
1. **Agreement type field reordering** (top of form)
|
| 361 |
+
2. **Field visibility logic** (most impactful)
|
| 362 |
+
3. **CE/CCP checkbox pattern** (affects all agreements)
|
| 363 |
+
4. **Layer deduction calculation fix** (Closer/Referral exceptions)
|
| 364 |
+
5. **Bell Ringer department-only logic** (remove market segment)
|
| 365 |
+
6. **Preview table redesign** (one per year, no dollar amounts)
|
| 366 |
+
7. **Originating producer field** (Referral only)
|
| 367 |
+
8. **Fixed-code agreement handling** (House/Gebhardt/Beacham)
|
| 368 |
+
|
| 369 |
+
---
|
| 370 |
+
|
| 371 |
+
## QUESTIONS TO CLARIFY BEFORE STARTING
|
| 372 |
+
|
| 373 |
+
If anything is unclear, ask about:
|
| 374 |
+
- Current state management approach (how fields are currently shown/hidden)
|
| 375 |
+
- How People Picker is currently implemented
|
| 376 |
+
- How commission calculation engine is structured
|
| 377 |
+
- Current preview table rendering logic
|
| 378 |
+
|
| 379 |
+
This will ensure the refactor integrates smoothly with existing code patterns.
|
|
@@ -8,4 +8,5 @@ python-multipart==0.0.12
|
|
| 8 |
jinja2==3.1.4
|
| 9 |
aiofiles==24.1.0
|
| 10 |
pydantic[email]==2.9.0
|
| 11 |
-
reportlab==4.1.0
|
|
|
|
|
|
| 8 |
jinja2==3.1.4
|
| 9 |
aiofiles==24.1.0
|
| 10 |
pydantic[email]==2.9.0
|
| 11 |
+
reportlab==4.1.0
|
| 12 |
+
python-docx==1.1.2
|
|
@@ -27,6 +27,7 @@ db_path = "/data/app.db"
|
|
| 27 |
print("Checking for schema migrations...")
|
| 28 |
add_column_if_missing(db_path, "producers", "email", "VARCHAR(255)")
|
| 29 |
add_column_if_missing(db_path, "approvals", "approval_token", "VARCHAR(64)")
|
|
|
|
| 30 |
# Add unique index separately (SQLite doesn't support UNIQUE in ALTER TABLE)
|
| 31 |
conn = sqlite3.connect(db_path)
|
| 32 |
try:
|
|
@@ -67,30 +68,21 @@ if ref_agr:
|
|
| 67 |
s.commit()
|
| 68 |
print(f" Fixed Referral/Origination Fee: {updated} tier labels updated to 'Year 1 & Year 2'")
|
| 69 |
|
| 70 |
-
# Fix Life Origination Fee: "All Years" at 20%
|
| 71 |
life_agr = s.query(CommissionAgreement).filter(
|
| 72 |
CommissionAgreement.name == "Life Origination Fee Agreement"
|
| 73 |
).first()
|
| 74 |
if life_agr:
|
| 75 |
-
|
| 76 |
-
if
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
producer_role="P1",
|
| 84 |
-
years_label="Year 2+",
|
| 85 |
-
commission=None,
|
| 86 |
-
credit=None,
|
| 87 |
-
agreement_label="Ended",
|
| 88 |
-
note="Life Referral is 20% Year 1 only. Year 2 it is removed.",
|
| 89 |
-
sort_order=max_sort + 1,
|
| 90 |
-
)
|
| 91 |
-
s.add(ended_tier)
|
| 92 |
s.commit()
|
| 93 |
-
print(" Fixed Life Origination Fee:
|
| 94 |
|
| 95 |
print("Commission data migrations complete.")
|
| 96 |
|
|
|
|
| 27 |
print("Checking for schema migrations...")
|
| 28 |
add_column_if_missing(db_path, "producers", "email", "VARCHAR(255)")
|
| 29 |
add_column_if_missing(db_path, "approvals", "approval_token", "VARCHAR(64)")
|
| 30 |
+
add_column_if_missing(db_path, "intake_submissions", "hidden", "BOOLEAN DEFAULT 0")
|
| 31 |
# Add unique index separately (SQLite doesn't support UNIQUE in ALTER TABLE)
|
| 32 |
conn = sqlite3.connect(db_path)
|
| 33 |
try:
|
|
|
|
| 68 |
s.commit()
|
| 69 |
print(f" Fixed Referral/Origination Fee: {updated} tier labels updated to 'Year 1 & Year 2'")
|
| 70 |
|
| 71 |
+
# Fix Life Origination Fee: should be "All Years" at 20% (not Year 1 only)
|
| 72 |
life_agr = s.query(CommissionAgreement).filter(
|
| 73 |
CommissionAgreement.name == "Life Origination Fee Agreement"
|
| 74 |
).first()
|
| 75 |
if life_agr:
|
| 76 |
+
year1_tiers = [t for t in life_agr.tiers if t.years_label == "Year 1" and t.commission == 20]
|
| 77 |
+
ended_tiers = [t for t in life_agr.tiers if t.years_label == "Year 2+" and t.commission is None]
|
| 78 |
+
if year1_tiers:
|
| 79 |
+
# Update to All Years
|
| 80 |
+
year1_tiers[0].years_label = "All Years"
|
| 81 |
+
# Remove the Ended tier
|
| 82 |
+
for et in ended_tiers:
|
| 83 |
+
s.delete(et)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
s.commit()
|
| 85 |
+
print(" Fixed Life Origination Fee: All Years at 20%")
|
| 86 |
|
| 87 |
print("Commission data migrations complete.")
|
| 88 |
|