pramodmisra Claude Opus 4.6 (1M context) commited on
Commit
ab38dce
Β·
1 Parent(s): a061388

Major feature update: DOCX generation, preview columns, bug fixes, dashboard enhancements

Browse files

Activities 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 ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ /data/
5
+ *.db
6
+ .env
Feedback/Commission_Agreement_DRAFT_Template.docx ADDED
Binary file (11.9 kB). View file
 
Feedback/Commission_Agreement_FINAL_Template.docx ADDED
Binary file (11.9 kB). View file
 
Feedback/commission_intake_refinement_prompt_v2.md ADDED
@@ -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!
Feedback/email.docx ADDED
Binary file (17.9 kB). View file
 
app/docx_generator.py ADDED
@@ -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()
app/models.py CHANGED
@@ -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")
app/pdf_generator.py CHANGED
@@ -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 5 columns:
399
- Role | Agreement Type | Commission % | Production % | ABE Status
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.20, # Role
406
- page_width * 0.28, # Agreement Type
407
- page_width * 0.18, # Commission %
408
- page_width * 0.18, # Production %
409
- page_width * 0.16, # ABE Status
 
 
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>Commission %</b>", styles["TableCellBold"]),
422
- Paragraph("<b>Production %</b>", styles["TableCellBold"]),
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)
app/routes/form_routes.py CHANGED
@@ -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 _lookup_producer_email(db: Session, producer_code: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
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
- pdf_dir = "/data/pdfs"
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],
app/rules_engine.py CHANGED
@@ -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": 100.0, "production_pct": 100.0,
232
  "abe_status": "ABE", "code": "ACCHO1", "name": "House Account"})
233
- if p1pfx:
234
- year_rows.append({"role": "Producer 1 (Approval Processor)",
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
- year_rows.append({"role": "CE as Producer", "agreement_type": "Ceded Accounts β€” CE as Producer",
264
- "commission_pct": consts.get("CE_PROD_COMM", 10),
 
 
 
 
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})
app/seed_db.py CHANGED
@@ -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": "Year 1", "comm": 20, "credit": 100, "agr": "Life Origination Fee Agreement"},
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 = ["Sarah Johnson", "Mike Thompson", "Karen Lee", "James Wilson"]
 
 
 
 
 
 
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
 
app/templates/dashboard.html CHANGED
@@ -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
- <p class="text-xs text-gray-400 mt-0.5">Last edited: {{ d.updated_at }}</p>
 
 
 
 
 
 
 
 
 
 
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
- <div class="flex items-center justify-between mb-2">
 
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-3">
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 Approval</span>
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 }} &middot; Submission #{{ s.id }}</p>
84
 
85
- <!-- Approval Progress Bar -->
 
 
 
 
 
 
 
 
 
 
 
86
  {% if s.total_approvals > 0 %}
87
  <div class="mb-2">
88
- <div class="flex items-center space-x-2 mb-1">
 
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
- <!-- Approver Details (collapsible) -->
101
- <details class="text-sm">
102
- <summary class="cursor-pointer text-blue-600 hover:text-blue-800 text-xs font-medium">
103
- Show approval details
104
- </summary>
105
- <div class="mt-2 border border-gray-100 rounded-lg overflow-hidden">
106
- <table class="w-full text-xs">
107
- <thead>
108
- <tr class="bg-gray-50 text-gray-500">
109
- <th class="px-3 py-2 text-left font-medium">Role</th>
110
- <th class="px-3 py-2 text-left font-medium">Name</th>
111
- <th class="px-3 py-2 text-left font-medium">Status</th>
112
- <th class="px-3 py-2 text-left font-medium">Date</th>
113
- </tr>
114
- </thead>
115
- <tbody class="divide-y divide-gray-50">
116
- {% for a in s.approvals %}
117
- <tr>
118
- <td class="px-3 py-2 text-gray-700">{{ a.role_label }}</td>
119
- <td class="px-3 py-2 text-gray-700">{{ a.approver_name }}</td>
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
- </details>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 %}
app/templates/form.html CHANGED
@@ -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
- <label class="flex items-center space-x-2 text-sm font-medium text-gray-700 mb-2">
153
- <input type="checkbox" id="include_ce">
154
- <span>Include CE Layer (Client Executive)</span>
155
- </label>
156
- <div id="ce_picker" class="hidden">
157
- <select id="client_exec" class="w-full md:w-1/2 border border-gray-300 rounded-md px-3 py-2 text-sm">
158
- <option value="">-- Select CE --</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
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="submitForm()"
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: document.getElementById('include_ce').checked,
413
- ce_prefix: document.getElementById('include_ce').checked ? (document.getElementById('client_exec').value || null) : null,
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">Production %</th>' +
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 form
539
- async function submitForm() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Draft PDF</a>' : '') +
 
 
 
 
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">&times;</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 &amp; 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 &amp; 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);
commission_intake_refactor_prompt.md ADDED
@@ -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.
requirements.txt CHANGED
@@ -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
start.py CHANGED
@@ -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% β†’ "Year 1" at 20% + "Year 2+" Ended
71
  life_agr = s.query(CommissionAgreement).filter(
72
  CommissionAgreement.name == "Life Origination Fee Agreement"
73
  ).first()
74
  if life_agr:
75
- all_years_tiers = [t for t in life_agr.tiers if t.years_label == "All Years"]
76
- if all_years_tiers:
77
- # Update existing tier to Year 1
78
- all_years_tiers[0].years_label = "Year 1"
79
- # Add Year 2+ Ended tier
80
- max_sort = max((t.sort_order for t in life_agr.tiers), default=0)
81
- ended_tier = CommissionTier(
82
- agreement_id=life_agr.id,
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: Year 1 at 20%, Year 2+ Ended")
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