pramodmisra Claude Opus 4.6 commited on
Commit
9aaf067
Β·
1 Parent(s): a55e4f2

Fix admin panel auth, form title, commission data, and validation

Browse files

- CRITICAL: Add auth token to ALL admin template fetch() calls (agreements,
agreement_edit, constants, bell_ringers, dropdowns, email_rules, users).
Previously only producer_emails had it, causing all admin saves to fail
with 401 Unauthorized.
- Add ?token= parameter to all admin navigation links so auth persists
when navigating between admin pages.
- Rename "Producer Intake Form" to "Commission Agreement Intake Form"
throughout (base.html, form.html, README.md).
- Move Department Code and Market Segment before Bell Ringer badge so
bell ringer auto-populates correctly.
- Add tooltip to Bell Ringer badge explaining it auto-populates.
- Fix Referral/Origination Fee tier label: "Year 1-2" -> "Year 1 & Year 2".
- Fix Life Origination Fee: "All Years" at 20% -> "Year 1" at 20%, "Year 2+" Ended.
- Add CCP + Bond Agreement incompatibility validation (client + server side).
- Add data migration in start.py to fix existing DB records on redeploy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Producer Intake Form
3
  emoji: πŸ“‹
4
  colorFrom: blue
5
  colorTo: gray
@@ -7,5 +7,5 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- # Producer Intake Form
11
 
 
1
  ---
2
+ title: Commission Agreement Intake Form
3
  emoji: πŸ“‹
4
  colorFrom: blue
5
  colorTo: gray
 
7
  pinned: false
8
  ---
9
 
10
+ # Commission Agreement Intake Form
11
 
app/rules_engine.py CHANGED
@@ -245,6 +245,15 @@ def compute_summary(db: Session, form_data: dict) -> dict:
245
  warnings.append("An Originating Producer/Employee is selected β€” "
246
  "the agreement must be Referral / Origination Fee Agreement.")
247
 
 
 
 
 
 
 
 
 
 
248
  # ── Build rows ────────────────────────────────────────────
249
 
250
  # ROW: Producer 1
 
245
  warnings.append("An Originating Producer/Employee is selected β€” "
246
  "the agreement must be Referral / Origination Fee Agreement.")
247
 
248
+ # CCP is not eligible for Bond Agreement
249
+ is_bond = "bond" in R["name"].lower()
250
+ if has_cc and is_bond:
251
+ warnings.append("Complex Claims Practice (CCP) is not eligible for Bond Agreement.")
252
+
253
+ # Validate CE is selected if checkbox was intended
254
+ if cepfx and not is_ce and not cename:
255
+ warnings.append("Client Executive is selected but no name found β€” verify selection.")
256
+
257
  # ── Build rows ────────────────────────────────────────────
258
 
259
  # ROW: Producer 1
app/seed_db.py CHANGED
@@ -73,10 +73,10 @@ def seed(session, reset=False):
73
  {
74
  "name": "Referral / Origination Fee Agreement", "p1_suffix": 1, "p2_suffix": 2,
75
  "tiers": [
76
- {"role": "P1", "years": "Year 1-2", "comm": 20, "credit": 100, "agr": "Referral Agreement"},
77
  {"role": "P1", "years": "Year 3+", "comm": 30, "credit": 100, "agr": "Standard Agreement",
78
  "note": "Transitions to Standard Agreement"},
79
- {"role": "P2", "years": "Year 1-2", "comm": 10, "credit": 0, "agr": "Origination Fee Agreement"},
80
  {"role": "P2", "years": "Year 3+", "comm": None, "credit": None, "agr": "Ended"},
81
  ]
82
  },
@@ -119,7 +119,9 @@ def seed(session, reset=False):
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
  {
 
73
  {
74
  "name": "Referral / Origination Fee Agreement", "p1_suffix": 1, "p2_suffix": 2,
75
  "tiers": [
76
+ {"role": "P1", "years": "Year 1 & Year 2", "comm": 20, "credit": 100, "agr": "Referral Agreement"},
77
  {"role": "P1", "years": "Year 3+", "comm": 30, "credit": 100, "agr": "Standard Agreement",
78
  "note": "Transitions to Standard Agreement"},
79
+ {"role": "P2", "years": "Year 1 & Year 2", "comm": 10, "credit": 0, "agr": "Origination Fee Agreement"},
80
  {"role": "P2", "years": "Year 3+", "comm": None, "credit": None, "agr": "Ended"},
81
  ]
82
  },
 
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
  {
app/templates/admin/agreement_edit.html CHANGED
@@ -4,7 +4,7 @@
4
  <div class="space-y-4">
5
  <div class="flex items-center justify-between">
6
  <div>
7
- <a href="/admin/agreements" class="text-sm text-gray-400 hover:text-gray-600">← Back to Agreements</a>
8
  <h1 class="text-2xl font-bold sw-blue-text mt-1">{{ agreement.name }}</h1>
9
  </div>
10
  <button onclick="saveTiers()" class="bg-green-600 text-white px-6 py-2 rounded-md text-sm font-semibold hover:bg-green-700">
@@ -111,6 +111,9 @@
111
  </div>
112
 
113
  <script>
 
 
 
114
  function addTier() {
115
  const html = `
116
  <div class="tier-row border border-gray-200 rounded-lg p-4 bg-blue-50">
@@ -182,7 +185,7 @@ async function saveTiers() {
182
  };
183
 
184
  const resp = await fetch('/admin/api/agreements/{{ agreement.id }}/tiers', {
185
- method: 'POST', headers: {'Content-Type': 'application/json'},
186
  body: JSON.stringify(body),
187
  });
188
  const data = await resp.json();
 
4
  <div class="space-y-4">
5
  <div class="flex items-center justify-between">
6
  <div>
7
+ <a href="/admin/agreements?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Back to Agreements</a>
8
  <h1 class="text-2xl font-bold sw-blue-text mt-1">{{ agreement.name }}</h1>
9
  </div>
10
  <button onclick="saveTiers()" class="bg-green-600 text-white px-6 py-2 rounded-md text-sm font-semibold hover:bg-green-700">
 
111
  </div>
112
 
113
  <script>
114
+ const authToken = '{{ token }}';
115
+ const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
116
+
117
  function addTier() {
118
  const html = `
119
  <div class="tier-row border border-gray-200 rounded-lg p-4 bg-blue-50">
 
185
  };
186
 
187
  const resp = await fetch('/admin/api/agreements/{{ agreement.id }}/tiers', {
188
+ method: 'POST', headers: authHeaders,
189
  body: JSON.stringify(body),
190
  });
191
  const data = await resp.json();
app/templates/admin/agreements.html CHANGED
@@ -40,7 +40,7 @@
40
  </button>
41
  </td>
42
  <td class="px-4 py-3 text-center">
43
- <a href="/admin/agreements/{{ agr.id }}" class="text-blue-600 hover:underline text-sm">Edit β†’</a>
44
  </td>
45
  </tr>
46
  {% endfor %}
@@ -87,6 +87,9 @@
87
  </div>
88
 
89
  <script>
 
 
 
90
  function showNewModal() { document.getElementById('newModal').classList.remove('hidden'); }
91
  function closeNewModal() { document.getElementById('newModal').classList.add('hidden'); }
92
 
@@ -99,19 +102,19 @@ async function createAgreement() {
99
  tiers: [],
100
  };
101
  const resp = await fetch('/admin/api/agreements/new', {
102
- method: 'POST', headers: {'Content-Type': 'application/json'},
103
  body: JSON.stringify(body),
104
  });
105
  const data = await resp.json();
106
  if (data.success) {
107
- window.location.href = '/admin/agreements/' + data.id;
108
  } else {
109
  alert(data.error || 'Failed to create agreement');
110
  }
111
  }
112
 
113
  async function toggleAgreement(id) {
114
- await fetch('/admin/api/agreements/' + id + '/toggle', { method: 'POST' });
115
  location.reload();
116
  }
117
  </script>
 
40
  </button>
41
  </td>
42
  <td class="px-4 py-3 text-center">
43
+ <a href="/admin/agreements/{{ agr.id }}?token={{ token }}" class="text-blue-600 hover:underline text-sm">Edit β†’</a>
44
  </td>
45
  </tr>
46
  {% endfor %}
 
87
  </div>
88
 
89
  <script>
90
+ const authToken = '{{ token }}';
91
+ const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
92
+
93
  function showNewModal() { document.getElementById('newModal').classList.remove('hidden'); }
94
  function closeNewModal() { document.getElementById('newModal').classList.add('hidden'); }
95
 
 
102
  tiers: [],
103
  };
104
  const resp = await fetch('/admin/api/agreements/new', {
105
+ method: 'POST', headers: authHeaders,
106
  body: JSON.stringify(body),
107
  });
108
  const data = await resp.json();
109
  if (data.success) {
110
+ window.location.href = '/admin/agreements/' + data.id + '?token=' + authToken;
111
  } else {
112
  alert(data.error || 'Failed to create agreement');
113
  }
114
  }
115
 
116
  async function toggleAgreement(id) {
117
+ await fetch('/admin/api/agreements/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
118
  location.reload();
119
  }
120
  </script>
app/templates/admin/bell_ringers.html CHANGED
@@ -2,7 +2,7 @@
2
  {% block title %}Bell Ringer Thresholds β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
- <a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <div class="flex items-center justify-between">
7
  <h1 class="text-2xl font-bold sw-blue-text">Bell Ringer Thresholds</h1>
8
  <button onclick="document.getElementById('addRow').classList.toggle('hidden')"
@@ -56,10 +56,13 @@
56
  </div>
57
  </div>
58
  <script>
 
 
 
59
  async function saveBR(id) {
60
  const val = parseFloat(document.getElementById('br_' + id).value);
61
  await fetch('/admin/api/bell-ringers/' + id, {
62
- method: 'POST', headers: {'Content-Type': 'application/json'},
63
  body: JSON.stringify({ threshold: val }),
64
  });
65
  document.getElementById('br_' + id).style.backgroundColor = '#d4edda';
@@ -67,12 +70,12 @@ async function saveBR(id) {
67
  }
68
  async function deleteBR(id) {
69
  if (!confirm('Delete this threshold?')) return;
70
- await fetch('/admin/api/bell-ringers/' + id, { method: 'DELETE' });
71
  location.reload();
72
  }
73
  async function addBR() {
74
  await fetch('/admin/api/bell-ringers/new', {
75
- method: 'POST', headers: {'Content-Type': 'application/json'},
76
  body: JSON.stringify({
77
  market_segment: document.getElementById('new_seg').value,
78
  dept_code: document.getElementById('new_dept').value,
 
2
  {% block title %}Bell Ringer Thresholds β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
+ <a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <div class="flex items-center justify-between">
7
  <h1 class="text-2xl font-bold sw-blue-text">Bell Ringer Thresholds</h1>
8
  <button onclick="document.getElementById('addRow').classList.toggle('hidden')"
 
56
  </div>
57
  </div>
58
  <script>
59
+ const authToken = '{{ token }}';
60
+ const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
61
+
62
  async function saveBR(id) {
63
  const val = parseFloat(document.getElementById('br_' + id).value);
64
  await fetch('/admin/api/bell-ringers/' + id, {
65
+ method: 'POST', headers: authHeaders,
66
  body: JSON.stringify({ threshold: val }),
67
  });
68
  document.getElementById('br_' + id).style.backgroundColor = '#d4edda';
 
70
  }
71
  async function deleteBR(id) {
72
  if (!confirm('Delete this threshold?')) return;
73
+ await fetch('/admin/api/bell-ringers/' + id, { method: 'DELETE', headers: {'Authorization': 'Bearer ' + authToken} });
74
  location.reload();
75
  }
76
  async function addBR() {
77
  await fetch('/admin/api/bell-ringers/new', {
78
+ method: 'POST', headers: authHeaders,
79
  body: JSON.stringify({
80
  market_segment: document.getElementById('new_seg').value,
81
  dept_code: document.getElementById('new_dept').value,
app/templates/admin/constants.html CHANGED
@@ -2,7 +2,7 @@
2
  {% block title %}Fixed Constants β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
- <a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <h1 class="text-2xl font-bold sw-blue-text">Fixed Constants</h1>
7
  <p class="text-gray-500 text-sm">Edit the percentages used for Client Executive and Complex Claims rows.</p>
8
 
@@ -33,10 +33,13 @@
33
  </div>
34
  </div>
35
  <script>
 
 
 
36
  async function saveConst(id) {
37
  const val = parseFloat(document.getElementById('const_' + id).value);
38
  await fetch('/admin/api/constants/' + id, {
39
- method: 'POST', headers: {'Content-Type': 'application/json'},
40
  body: JSON.stringify({ value: val }),
41
  });
42
  // Brief flash to confirm
 
2
  {% block title %}Fixed Constants β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
+ <a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <h1 class="text-2xl font-bold sw-blue-text">Fixed Constants</h1>
7
  <p class="text-gray-500 text-sm">Edit the percentages used for Client Executive and Complex Claims rows.</p>
8
 
 
33
  </div>
34
  </div>
35
  <script>
36
+ const authToken = '{{ token }}';
37
+ const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
38
+
39
  async function saveConst(id) {
40
  const val = parseFloat(document.getElementById('const_' + id).value);
41
  await fetch('/admin/api/constants/' + id, {
42
+ method: 'POST', headers: authHeaders,
43
  body: JSON.stringify({ value: val }),
44
  });
45
  // Brief flash to confirm
app/templates/admin/dashboard.html CHANGED
@@ -6,7 +6,7 @@
6
  <p class="text-gray-500 text-sm">Manage all configurable rules, dropdowns, and users without code changes.</p>
7
 
8
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
9
- <a href="/admin/agreements" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
10
  <div class="flex items-center justify-between">
11
  <div>
12
  <h3 class="font-semibold sw-blue-text group-hover:underline">Commission Agreements</h3>
@@ -16,7 +16,7 @@
16
  </div>
17
  </a>
18
 
19
- <a href="/admin/constants" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
20
  <div class="flex items-center justify-between">
21
  <div>
22
  <h3 class="font-semibold sw-blue-text group-hover:underline">Fixed Constants</h3>
@@ -26,7 +26,7 @@
26
  </div>
27
  </a>
28
 
29
- <a href="/admin/bell-ringers" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
30
  <div class="flex items-center justify-between">
31
  <div>
32
  <h3 class="font-semibold sw-blue-text group-hover:underline">Bell Ringer Thresholds</h3>
@@ -36,7 +36,7 @@
36
  </div>
37
  </a>
38
 
39
- <a href="/admin/dropdowns" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
40
  <div class="flex items-center justify-between">
41
  <div>
42
  <h3 class="font-semibold sw-blue-text group-hover:underline">Dropdown Options</h3>
@@ -46,7 +46,7 @@
46
  </div>
47
  </a>
48
 
49
- <a href="/admin/email-rules" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
50
  <div class="flex items-center justify-between">
51
  <div>
52
  <h3 class="font-semibold sw-blue-text group-hover:underline">Email Routing Rules</h3>
@@ -67,7 +67,7 @@
67
  </div>
68
  </a>
69
 
70
- <a href="/admin/users" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
71
  <div class="flex items-center justify-between">
72
  <div>
73
  <h3 class="font-semibold sw-blue-text group-hover:underline">User Management</h3>
 
6
  <p class="text-gray-500 text-sm">Manage all configurable rules, dropdowns, and users without code changes.</p>
7
 
8
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
9
+ <a href="/admin/agreements?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
10
  <div class="flex items-center justify-between">
11
  <div>
12
  <h3 class="font-semibold sw-blue-text group-hover:underline">Commission Agreements</h3>
 
16
  </div>
17
  </a>
18
 
19
+ <a href="/admin/constants?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
20
  <div class="flex items-center justify-between">
21
  <div>
22
  <h3 class="font-semibold sw-blue-text group-hover:underline">Fixed Constants</h3>
 
26
  </div>
27
  </a>
28
 
29
+ <a href="/admin/bell-ringers?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
30
  <div class="flex items-center justify-between">
31
  <div>
32
  <h3 class="font-semibold sw-blue-text group-hover:underline">Bell Ringer Thresholds</h3>
 
36
  </div>
37
  </a>
38
 
39
+ <a href="/admin/dropdowns?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
40
  <div class="flex items-center justify-between">
41
  <div>
42
  <h3 class="font-semibold sw-blue-text group-hover:underline">Dropdown Options</h3>
 
46
  </div>
47
  </a>
48
 
49
+ <a href="/admin/email-rules?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
50
  <div class="flex items-center justify-between">
51
  <div>
52
  <h3 class="font-semibold sw-blue-text group-hover:underline">Email Routing Rules</h3>
 
67
  </div>
68
  </a>
69
 
70
+ <a href="/admin/users?token={{ token }}" class="bg-white rounded-lg shadow p-5 hover:shadow-md transition group">
71
  <div class="flex items-center justify-between">
72
  <div>
73
  <h3 class="font-semibold sw-blue-text group-hover:underline">User Management</h3>
app/templates/admin/dropdowns.html CHANGED
@@ -2,7 +2,7 @@
2
  {% block title %}Dropdown Options β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
- <a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <h1 class="text-2xl font-bold sw-blue-text">Dropdown Options</h1>
7
  <p class="text-gray-500 text-sm">Manage the options shown in form dropdowns. Changes take effect immediately.</p>
8
 
@@ -35,23 +35,26 @@
35
  {% endfor %}
36
  </div>
37
  <script>
 
 
 
38
  async function addOption(category) {
39
  const label = prompt('Enter the display label for the new ' + category.replace('_',' ') + ' option:');
40
  if (!label) return;
41
  const value = prompt('Internal value (leave blank to use label):', label);
42
  await fetch('/admin/api/dropdowns/new', {
43
- method: 'POST', headers: {'Content-Type': 'application/json'},
44
  body: JSON.stringify({ category, label, value: value || label }),
45
  });
46
  location.reload();
47
  }
48
  async function toggleOption(id) {
49
- await fetch('/admin/api/dropdowns/' + id + '/toggle', { method: 'POST' });
50
  location.reload();
51
  }
52
  async function deleteOption(id) {
53
  if (!confirm('Delete this option permanently?')) return;
54
- await fetch('/admin/api/dropdowns/' + id, { method: 'DELETE' });
55
  location.reload();
56
  }
57
  </script>
 
2
  {% block title %}Dropdown Options β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
+ <a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <h1 class="text-2xl font-bold sw-blue-text">Dropdown Options</h1>
7
  <p class="text-gray-500 text-sm">Manage the options shown in form dropdowns. Changes take effect immediately.</p>
8
 
 
35
  {% endfor %}
36
  </div>
37
  <script>
38
+ const authToken = '{{ token }}';
39
+ const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
40
+
41
  async function addOption(category) {
42
  const label = prompt('Enter the display label for the new ' + category.replace('_',' ') + ' option:');
43
  if (!label) return;
44
  const value = prompt('Internal value (leave blank to use label):', label);
45
  await fetch('/admin/api/dropdowns/new', {
46
+ method: 'POST', headers: authHeaders,
47
  body: JSON.stringify({ category, label, value: value || label }),
48
  });
49
  location.reload();
50
  }
51
  async function toggleOption(id) {
52
+ await fetch('/admin/api/dropdowns/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
53
  location.reload();
54
  }
55
  async function deleteOption(id) {
56
  if (!confirm('Delete this option permanently?')) return;
57
+ await fetch('/admin/api/dropdowns/' + id, { method: 'DELETE', headers: {'Authorization': 'Bearer ' + authToken} });
58
  location.reload();
59
  }
60
  </script>
app/templates/admin/email_rules.html CHANGED
@@ -2,7 +2,7 @@
2
  {% block title %}Email Routing β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
- <a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <div class="flex items-center justify-between">
7
  <h1 class="text-2xl font-bold sw-blue-text">Email Routing Rules</h1>
8
  <button onclick="addRule()" class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-semibold hover:bg-green-700">+ Add Rule</button>
@@ -47,9 +47,12 @@
47
  </div>
48
  </div>
49
  <script>
 
 
 
50
  async function saveRule(id) {
51
  await fetch('/admin/api/email-rules/' + id, {
52
- method: 'POST', headers: {'Content-Type': 'application/json'},
53
  body: JSON.stringify({
54
  condition: document.getElementById('cond_' + id).value,
55
  email: document.getElementById('email_' + id).value,
@@ -62,7 +65,7 @@ async function saveRule(id) {
62
  }
63
  async function addRule() {
64
  await fetch('/admin/api/email-rules/new', {
65
- method: 'POST', headers: {'Content-Type': 'application/json'},
66
  body: JSON.stringify({ condition: 'always', email: '', description: 'New rule' }),
67
  });
68
  location.reload();
 
2
  {% block title %}Email Routing β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
+ <a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <div class="flex items-center justify-between">
7
  <h1 class="text-2xl font-bold sw-blue-text">Email Routing Rules</h1>
8
  <button onclick="addRule()" class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-semibold hover:bg-green-700">+ Add Rule</button>
 
47
  </div>
48
  </div>
49
  <script>
50
+ const authToken = '{{ token }}';
51
+ const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
52
+
53
  async function saveRule(id) {
54
  await fetch('/admin/api/email-rules/' + id, {
55
+ method: 'POST', headers: authHeaders,
56
  body: JSON.stringify({
57
  condition: document.getElementById('cond_' + id).value,
58
  email: document.getElementById('email_' + id).value,
 
65
  }
66
  async function addRule() {
67
  await fetch('/admin/api/email-rules/new', {
68
+ method: 'POST', headers: authHeaders,
69
  body: JSON.stringify({ condition: 'always', email: '', description: 'New rule' }),
70
  });
71
  location.reload();
app/templates/admin/users.html CHANGED
@@ -2,7 +2,7 @@
2
  {% block title %}User Management β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
- <a href="/admin/" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <div class="flex items-center justify-between">
7
  <h1 class="text-2xl font-bold sw-blue-text">User Management</h1>
8
  <button onclick="document.getElementById('addUser').classList.toggle('hidden')"
@@ -77,9 +77,12 @@
77
  </div>
78
  </div>
79
  <script>
 
 
 
80
  async function addUser() {
81
  const resp = await fetch('/admin/api/users/new', {
82
- method: 'POST', headers: {'Content-Type': 'application/json'},
83
  body: JSON.stringify({
84
  full_name: document.getElementById('new_name').value,
85
  email: document.getElementById('new_email').value,
@@ -92,7 +95,7 @@ async function addUser() {
92
  else alert(data.error || 'Failed');
93
  }
94
  async function toggleUser(id) {
95
- await fetch('/admin/api/users/' + id + '/toggle', { method: 'POST' });
96
  location.reload();
97
  }
98
  </script>
 
2
  {% block title %}User Management β€” Admin{% endblock %}
3
  {% block content %}
4
  <div class="space-y-4">
5
+ <a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
6
  <div class="flex items-center justify-between">
7
  <h1 class="text-2xl font-bold sw-blue-text">User Management</h1>
8
  <button onclick="document.getElementById('addUser').classList.toggle('hidden')"
 
77
  </div>
78
  </div>
79
  <script>
80
+ const authToken = '{{ token }}';
81
+ const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
82
+
83
  async function addUser() {
84
  const resp = await fetch('/admin/api/users/new', {
85
+ method: 'POST', headers: authHeaders,
86
  body: JSON.stringify({
87
  full_name: document.getElementById('new_name').value,
88
  email: document.getElementById('new_email').value,
 
95
  else alert(data.error || 'Failed');
96
  }
97
  async function toggleUser(id) {
98
+ await fetch('/admin/api/users/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
99
  location.reload();
100
  }
101
  </script>
app/templates/base.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{% block title %}Producer Intake Form{% endblock %}</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
  body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
@@ -21,7 +21,7 @@
21
  <nav class="sw-blue text-white shadow-lg">
22
  <div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
23
  <div class="flex items-center space-x-6">
24
- <span class="text-lg font-bold tracking-tight">SW Producer Intake</span>
25
  <a href="/form?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Form</a>
26
  {% if user.role in ('admin', 'superadmin') %}
27
  <a href="/admin/?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Admin Panel</a>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Commission Agreement Intake Form{% endblock %}</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <style>
9
  body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
 
21
  <nav class="sw-blue text-white shadow-lg">
22
  <div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
23
  <div class="flex items-center space-x-6">
24
+ <span class="text-lg font-bold tracking-tight">SW Commission Intake</span>
25
  <a href="/form?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Form</a>
26
  {% if user.role in ('admin', 'superadmin') %}
27
  <a href="/admin/?token={{ token }}" class="text-sm text-gray-200 hover:text-white transition">Admin Panel</a>
app/templates/form.html CHANGED
@@ -1,16 +1,16 @@
1
  {% extends "base.html" %}
2
- {% block title %}Producer Intake Form{% endblock %}
3
 
4
  {% block content %}
5
  <div class="space-y-6">
6
- <div
7
- <h1 class="text-2xl font-bold sw-blue-text">Producer Intake Form</h1>
8
  <p class="text-gray-500 text-sm mt-1">Complete all fields. The summary table updates <strong>live</strong>.</p>
9
  </div>
10
 
11
  <!-- ── Section 1: Client Information ──────────────────── -->
12
  <div class="bg-white rounded-lg shadow overflow-hidden">
13
- <div class="sw-blue text-white px-5 py-3 font-semibold">πŸ“‹ Client Information</div>
14
  <div class="px-5 py-4 space-y-3">
15
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
16
  <div>
@@ -43,24 +43,7 @@
43
  </div>
44
  </div>
45
 
46
- <!-- Bell Ringer Badge -->
47
- <div id="bell_badge" class="bell-idle text-sm">πŸ”” Bell Ringer: --</div>
48
-
49
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
50
- <div>
51
- <label class="block text-sm font-medium text-gray-700 mb-1">Policy Effective Date</label>
52
- <input type="date" id="effective_date"
53
- class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
54
- </div>
55
- <div>
56
- <label class="block text-sm font-medium text-gray-700 mb-1">Association</label>
57
- <select id="association" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
58
- <option value="">-- Select --</option>
59
- {% for a in associations %}
60
- <option value="{{ a.value }}">{{ a.label }}</option>
61
- {% endfor %}
62
- </select>
63
- </div>
64
  <div>
65
  <label class="block text-sm font-medium text-gray-700 mb-1">Department Code</label>
66
  <select id="dept_code" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
@@ -79,6 +62,26 @@
79
  </select>
80
  </div>
81
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  </div>
83
  </div>
84
 
@@ -313,15 +316,27 @@ async function refreshSummary() {
313
  if (!formData.agreement_id) {
314
  document.getElementById('summary_area').innerHTML =
315
  '<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded text-sm">' +
316
- '⚠️ Select a <strong>Commission Agreement</strong> to generate the summary.</div>';
317
  document.getElementById('warnings').innerHTML = '';
318
  document.getElementById('submit_btn').disabled = true;
319
  return;
320
  }
321
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  try {
323
  const resp = await fetch('/api/compute-summary', {
324
- method: 'POST',
325
  headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken},
326
  body: JSON.stringify(formData),
327
  });
@@ -330,7 +345,7 @@ async function refreshSummary() {
330
  // Warnings
331
  let warnHtml = '';
332
  (data.warnings || []).forEach(w => {
333
- warnHtml += `<div class="bg-amber-50 border border-amber-300 text-amber-800 px-4 py-2 rounded text-sm mb-2">⚠️ ${w}</div>`;
334
  });
335
  document.getElementById('warnings').innerHTML = warnHtml;
336
 
 
1
  {% extends "base.html" %}
2
+ {% block title %}Commission Agreement Intake Form{% endblock %}
3
 
4
  {% block content %}
5
  <div class="space-y-6">
6
+ <div>
7
+ <h1 class="text-2xl font-bold sw-blue-text">Commission Agreement Intake Form</h1>
8
  <p class="text-gray-500 text-sm mt-1">Complete all fields. The summary table updates <strong>live</strong>.</p>
9
  </div>
10
 
11
  <!-- ── Section 1: Client Information ──────────────────── -->
12
  <div class="bg-white rounded-lg shadow overflow-hidden">
13
+ <div class="sw-blue text-white px-5 py-3 font-semibold">Servicing Information</div>
14
  <div class="px-5 py-4 space-y-3">
15
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
16
  <div>
 
43
  </div>
44
  </div>
45
 
 
 
 
46
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  <div>
48
  <label class="block text-sm font-medium text-gray-700 mb-1">Department Code</label>
49
  <select id="dept_code" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
 
62
  </select>
63
  </div>
64
  </div>
65
+
66
+ <!-- Bell Ringer Badge (auto-populates based on revenue + department) -->
67
+ <div id="bell_badge" class="bell-idle text-sm cursor-default" title="Auto-populates based on estimated revenue and department code. Cannot be manually selected.">πŸ”” Bell Ringer: Select department code and enter revenue</div>
68
+
69
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
70
+ <div>
71
+ <label class="block text-sm font-medium text-gray-700 mb-1">Policy Effective Date</label>
72
+ <input type="date" id="effective_date"
73
+ class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
74
+ </div>
75
+ <div>
76
+ <label class="block text-sm font-medium text-gray-700 mb-1">Association</label>
77
+ <select id="association" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
78
+ <option value="">-- Select --</option>
79
+ {% for a in associations %}
80
+ <option value="{{ a.value }}">{{ a.label }}</option>
81
+ {% endfor %}
82
+ </select>
83
+ </div>
84
+ </div>
85
  </div>
86
  </div>
87
 
 
316
  if (!formData.agreement_id) {
317
  document.getElementById('summary_area').innerHTML =
318
  '<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded text-sm">' +
319
+ 'Select a <strong>Commission Agreement</strong> to generate the summary.</div>';
320
  document.getElementById('warnings').innerHTML = '';
321
  document.getElementById('submit_btn').disabled = true;
322
  return;
323
  }
324
 
325
+ // ── Client-side validation: CCP + Bond incompatibility ──
326
+ const agrSelect = document.getElementById('agreement');
327
+ const agrName = agrSelect.options[agrSelect.selectedIndex]?.text || '';
328
+ const ccpVal = document.getElementById('complex_claims').value;
329
+ if (ccpVal && agrName.toLowerCase().includes('bond')) {
330
+ document.getElementById('warnings').innerHTML =
331
+ '<div class="bg-red-50 border border-red-300 text-red-800 px-4 py-2 rounded text-sm mb-2">Complex Claims Practice (CCP) is not eligible for Bond Agreement.</div>';
332
+ document.getElementById('summary_area').innerHTML = '';
333
+ document.getElementById('submit_btn').disabled = true;
334
+ return;
335
+ }
336
+
337
  try {
338
  const resp = await fetch('/api/compute-summary', {
339
+ method: 'POST',
340
  headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken},
341
  body: JSON.stringify(formData),
342
  });
 
345
  // Warnings
346
  let warnHtml = '';
347
  (data.warnings || []).forEach(w => {
348
+ warnHtml += `<div class="bg-amber-50 border border-amber-300 text-amber-800 px-4 py-2 rounded text-sm mb-2">${w}</div>`;
349
  });
350
  document.getElementById('warnings').innerHTML = warnHtml;
351
 
start.py CHANGED
@@ -49,6 +49,51 @@ if not s.query(User).first():
49
  else:
50
  print("Database exists.")
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  # Import refresh data if present (uploaded by Colab daily refresh)
53
  REFRESH_FILE = "refresh_data.json"
54
  if os.path.exists(REFRESH_FILE):
 
49
  else:
50
  print("Database exists.")
51
 
52
+ # ── Data migration: fix commission tier labels ───────────
53
+ from app.models import CommissionAgreement, CommissionTier
54
+ print("Checking commission data migrations...")
55
+
56
+ # Fix Referral/Origination Fee: "Year 1-2" β†’ "Year 1 & Year 2"
57
+ ref_agr = s.query(CommissionAgreement).filter(
58
+ CommissionAgreement.name == "Referral / Origination Fee Agreement"
59
+ ).first()
60
+ if ref_agr:
61
+ updated = 0
62
+ for tier in ref_agr.tiers:
63
+ if tier.years_label == "Year 1-2":
64
+ tier.years_label = "Year 1 & Year 2"
65
+ updated += 1
66
+ if updated:
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
+
97
  # Import refresh data if present (uploaded by Colab daily refresh)
98
  REFRESH_FILE = "refresh_data.json"
99
  if os.path.exists(REFRESH_FILE):