Skip to content

Commit 9f0ab82

Browse files
Add policy document naming, simplify settings UI, improve sync icons
- Add customizable policy document naming pattern with {year}, {sequence}, {description} tokens - Simplify Upload Settings card by removing verbose token grid - Change Paperless sync icons to cloud-based design (green cloud-check for synced, orange cloud-arrow for pending) - Fix benefit plan modal to show renamed filename instead of original - Fix delete button stretching in document list - Rename attachments when expense details change (date, category, provider, amount) - Add background sync with longer timeout for Paperless 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent acf27e1 commit 9f0ab82

File tree

14 files changed

+584
-168
lines changed

14 files changed

+584
-168
lines changed

app/assets/css/pages/settings.css

Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -540,40 +540,6 @@ html.dark-mode .toggle-slider:before {
540540
margin: 0;
541541
}
542542

543-
/* Naming Tokens Grid */
544-
.naming-tokens-grid {
545-
display: flex;
546-
flex-wrap: wrap;
547-
gap: var(--spacing-xs);
548-
margin-bottom: var(--spacing-lg);
549-
padding: var(--spacing-sm);
550-
background: var(--bg-secondary);
551-
border-radius: var(--radius-md);
552-
}
553-
554-
.token-item {
555-
display: flex;
556-
align-items: center;
557-
gap: var(--spacing-xs);
558-
font-size: 0.8rem;
559-
}
560-
561-
.token-item code {
562-
background: var(--bg-primary);
563-
color: var(--accent-blue);
564-
padding: 2px 6px;
565-
border-radius: 4px;
566-
font-family: var(--font-mono);
567-
font-size: 0.75rem;
568-
font-weight: 500;
569-
border: 1px solid var(--border-color);
570-
}
571-
572-
.token-item span {
573-
color: var(--text-secondary);
574-
font-size: 0.75rem;
575-
}
576-
577543
/* Naming Pattern Groups */
578544
.naming-pattern-group {
579545
margin-bottom: var(--spacing-md);
@@ -628,16 +594,16 @@ html.dark-mode .toggle-slider:before {
628594
border-radius: var(--radius-sm);
629595
}
630596

631-
/* Dark mode adjustments */
632-
html.dark-mode .naming-tokens-grid {
633-
background: rgba(255, 255, 255, 0.05);
634-
}
635-
636-
html.dark-mode .token-item code {
637-
background: var(--bg-primary);
638-
border-color: var(--border-color);
597+
/* Inline preview (compact) */
598+
.naming-preview-inline {
599+
display: block;
600+
font-family: var(--font-mono);
601+
font-size: 0.8rem;
602+
color: var(--text-secondary);
603+
margin-top: 4px;
639604
}
640605

606+
/* Dark mode adjustments */
641607
html.dark-mode .naming-preview {
642608
background: rgba(255, 255, 255, 0.05);
643609
}

app/assets/js/services/expenses.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,37 +1051,40 @@ function getSyncStatusIcon(syncStatus, paperlessDocId = null) {
10511051
// Get Paperless URL from global config (set by server)
10521052
const paperlessUrl = window.paperlessConfig?.url;
10531053

1054+
// Cloud icon paths
1055+
const cloudPath = 'M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z';
1056+
10541057
switch (syncStatus) {
10551058
case 'synced':
10561059
// If we have paperless URL and doc ID, make it a link
10571060
if (paperlessUrl && paperlessDocId) {
10581061
return `<a href="${paperlessUrl}/documents/${paperlessDocId}" target="_blank" rel="noopener"
10591062
class="sync-status sync-synced" title="View in Paperless (opens in new tab)">
10601063
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1061-
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
1062-
<polyline points="22 4 12 14.01 9 11.01"/>
1064+
<path d="${cloudPath}"/>
1065+
<polyline points="9 13 12 16 16 11"/>
10631066
</svg>
10641067
</a>`;
10651068
}
10661069
return `<span class="sync-status sync-synced" title="Synced to Paperless">
10671070
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1068-
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
1069-
<polyline points="22 4 12 14.01 9 11.01"/>
1071+
<path d="${cloudPath}"/>
1072+
<polyline points="9 13 12 16 16 11"/>
10701073
</svg>
10711074
</span>`;
10721075
case 'failed':
10731076
return `<span class="sync-status sync-failed" title="Paperless sync failed - click to retry">
10741077
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1075-
<circle cx="12" cy="12" r="10"/>
1076-
<line x1="15" y1="9" x2="9" y2="15"/>
1077-
<line x1="9" y1="9" x2="15" y2="15"/>
1078+
<path d="${cloudPath}"/>
1079+
<line x1="10" y1="11" x2="14" y2="15"/>
1080+
<line x1="14" y1="11" x2="10" y2="15"/>
10781081
</svg>
10791082
</span>`;
10801083
case 'pending':
1081-
return `<span class="sync-status sync-pending" title="Sync pending">
1084+
return `<span class="sync-status sync-pending" title="Syncing to Paperless">
10821085
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1083-
<circle cx="12" cy="12" r="10"/>
1084-
<polyline points="12 6 12 12 16 14"/>
1086+
<path d="${cloudPath}"/>
1087+
<polyline points="12 17 12 11 15 14"/>
10851088
</svg>
10861089
</span>`;
10871090
default:

app/assets/js/services/planYears.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,8 @@ export function handlePlanYearFormSubmit(e) {
452452
const targetPlanYearId = planYearId || result.data.plan_year_id;
453453
if (hasFiles && targetPlanYearId) {
454454
try {
455-
await uploadPolicyDocuments(targetPlanYearId, fileInput.files);
455+
// Pass silent=true since we show our own consolidated message
456+
await uploadPolicyDocuments(targetPlanYearId, fileInput.files, true);
456457
showAlert(planYearId ? 'Benefit plan updated successfully with documents!' : 'Benefit plan added successfully with documents!', 'success');
457458
} catch (error) {
458459
console.error('Error uploading documents:', error);

app/assets/js/services/policyYearDocuments.js

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,40 @@ function getSyncStatusIcon(syncStatus, paperlessDocId = null) {
1111
// Get Paperless URL from global config (set by server)
1212
const paperlessUrl = window.paperlessConfig?.url;
1313

14+
// Cloud icon path
15+
const cloudPath = 'M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z';
16+
1417
switch (syncStatus) {
1518
case 'synced':
1619
// If we have paperless URL and doc ID, make it a link
1720
if (paperlessUrl && paperlessDocId) {
1821
return `<a href="${paperlessUrl}/documents/${paperlessDocId}" target="_blank" rel="noopener"
1922
class="sync-status sync-synced" title="View in Paperless (opens in new tab)">
2023
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
21-
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
22-
<polyline points="22 4 12 14.01 9 11.01"/>
24+
<path d="${cloudPath}"/>
25+
<polyline points="9 13 12 16 16 11"/>
2326
</svg>
2427
</a>`;
2528
}
2629
return `<span class="sync-status sync-synced" title="Synced to Paperless">
2730
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
28-
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
29-
<polyline points="22 4 12 14.01 9 11.01"/>
31+
<path d="${cloudPath}"/>
32+
<polyline points="9 13 12 16 16 11"/>
3033
</svg>
3134
</span>`;
3235
case 'failed':
3336
return `<span class="sync-status sync-failed" title="Paperless sync failed - click to retry">
3437
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
35-
<circle cx="12" cy="12" r="10"/>
36-
<line x1="15" y1="9" x2="9" y2="15"/>
37-
<line x1="9" y1="9" x2="15" y2="15"/>
38+
<path d="${cloudPath}"/>
39+
<line x1="10" y1="11" x2="14" y2="15"/>
40+
<line x1="14" y1="11" x2="10" y2="15"/>
3841
</svg>
3942
</span>`;
4043
case 'pending':
41-
return `<span class="sync-status sync-pending" title="Sync pending">
44+
return `<span class="sync-status sync-pending" title="Syncing to Paperless">
4245
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
43-
<circle cx="12" cy="12" r="10"/>
44-
<polyline points="12 6 12 12 16 14"/>
46+
<path d="${cloudPath}"/>
47+
<polyline points="12 17 12 11 15 14"/>
4548
</svg>
4649
</span>`;
4750
default:
@@ -132,7 +135,9 @@ function createPolicyDocumentItem(doc) {
132135
const docItem = document.createElement('div');
133136
docItem.className = 'existing-receipt-item';
134137

135-
const fileName = doc.original_filename;
138+
// Extract the renamed filename from file_path (the actual saved filename)
139+
const filePathParts = doc.file_path.split('/');
140+
const fileName = filePathParts[filePathParts.length - 1];
136141
const fileType = doc.file_type.toLowerCase();
137142
// Remove data/{user_id}/ prefix to show just benefit_plans/{year}/filename
138143
const filePath = doc.file_path.replace(/^data\/\d+\//, '');
@@ -190,7 +195,10 @@ function createPolicyDocumentItem(doc) {
190195
}
191196
}
192197

193-
// Add delete button
198+
// Add actions container with delete button (prevents button from stretching)
199+
const actionsDiv = document.createElement('div');
200+
actionsDiv.className = 'existing-receipt-actions';
201+
194202
const deleteBtn = document.createElement('button');
195203
deleteBtn.type = 'button';
196204
deleteBtn.className = 'btn btn-small existing-receipt-delete receipt-delete-btn';
@@ -200,15 +208,19 @@ function createPolicyDocumentItem(doc) {
200208
deletePolicyDocument(doc.id);
201209
};
202210

203-
docItem.appendChild(deleteBtn);
211+
actionsDiv.appendChild(deleteBtn);
212+
docItem.appendChild(actionsDiv);
204213

205214
return docItem;
206215
}
207216

208217
/**
209218
* Upload documents for a policy year
219+
* @param {number} planYearId - The plan year ID
220+
* @param {FileList} files - Files to upload
221+
* @param {boolean} silent - If true, don't show success toast (caller handles it)
210222
*/
211-
export async function uploadPolicyDocuments(planYearId, files) {
223+
export async function uploadPolicyDocuments(planYearId, files, silent = false) {
212224
if (!files || files.length === 0) {
213225
return;
214226
}
@@ -227,7 +239,10 @@ export async function uploadPolicyDocuments(planYearId, files) {
227239
const data = await response.json();
228240

229241
if (response.ok && data.success) {
230-
showAlert(`${data.uploaded.length} document(s) uploaded successfully!`, 'success');
242+
// Only show toast if not silent (caller may handle messaging)
243+
if (!silent) {
244+
showAlert(`${data.uploaded.length} document(s) uploaded successfully!`, 'success');
245+
}
231246

232247
// Reload documents
233248
const documents = await loadPolicyDocuments(planYearId);

app/database.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,12 @@ def init_database():
400400
except sqlite3.OperationalError:
401401
pass # Column already exists
402402

403+
# Add policy_naming_pattern column to users table if it doesn't exist
404+
try:
405+
cursor.execute("ALTER TABLE users ADD COLUMN policy_naming_pattern TEXT DEFAULT 'Policy_{year}_{sequence}'")
406+
except sqlite3.OperationalError:
407+
pass # Column already exists
408+
403409
# Add OIDC columns to users table for SSO authentication
404410
try:
405411
cursor.execute("ALTER TABLE users ADD COLUMN oidc_sub TEXT")

app/routes/attachments.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,11 @@ def add_attachments(expense_id):
147147
}
148148
})
149149

150-
# Sync to Paperless after transaction is committed
150+
# Sync to Paperless in background (non-blocking)
151151
for sync_item in pending_syncs:
152152
try:
153-
from services.paperless_service import sync_to_paperless_safe
154-
sync_to_paperless_safe(
153+
from services.paperless_service import sync_to_paperless_background
154+
sync_to_paperless_background(
155155
user_id=user_id,
156156
file_path=sync_item['file_path'],
157157
metadata=sync_item['metadata'],
@@ -207,7 +207,7 @@ def delete_attachment(attachment_id):
207207
try:
208208
from services.paperless_service import delete_from_paperless
209209
# Build title to search for if no doc_id
210-
title = f"HSA-{paperless_info['date']}"
210+
title = f"HealthReceipts-{paperless_info['date']}"
211211
if paperless_info['provider']:
212212
title += f"-{paperless_info['provider']}"
213213
title += f"-{paperless_info['category']}"

app/routes/expenses.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,11 @@ def create_expense():
178178

179179
conn.commit()
180180

181-
# Sync to Paperless after transaction is committed
181+
# Sync to Paperless in background (non-blocking)
182182
for sync_item in pending_syncs:
183183
try:
184-
from services.paperless_service import sync_to_paperless_safe
185-
sync_to_paperless_safe(
184+
from services.paperless_service import sync_to_paperless_background
185+
sync_to_paperless_background(
186186
user_id=user_id,
187187
file_path=sync_item['file_path'],
188188
metadata=sync_item['metadata'],

app/routes/plan_year_documents.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from werkzeug.utils import secure_filename
66
from database import db_connection, db_readonly
77
from utils.decorators import login_required
8-
from utils.helpers import get_benefit_plan_documents_folder
8+
from utils.helpers import get_benefit_plan_documents_folder, generate_policy_document_filename, get_user_policy_naming_pattern
99
from services.file_service import save_uploaded_file, delete_file_safely, get_file_size
1010

1111
plan_year_documents_bp = Blueprint('plan_year_documents', __name__)
@@ -52,7 +52,8 @@ def list_documents(plan_year_id):
5252

5353
# Get all documents for this plan year
5454
documents = conn.execute('''
55-
SELECT id, original_filename, file_type, file_size, file_path, created_at
55+
SELECT id, original_filename, file_type, file_size, file_path, created_at,
56+
paperless_sync_status, paperless_doc_id
5657
FROM plan_year_documents
5758
WHERE plan_year_id = ?
5859
ORDER BY created_at DESC
@@ -95,22 +96,42 @@ def add_documents(plan_year_id):
9596
cursor = conn.cursor()
9697
plan_year_start_str = plan_year['plan_year_start']
9798

98-
for file in files:
99-
# Save file (no compression for documents)
99+
# Get user's policy naming pattern
100+
policy_pattern = get_user_policy_naming_pattern(user_id)
101+
102+
# Get existing document count for sequence numbering
103+
existing_count = conn.execute(
104+
'SELECT COUNT(*) as count FROM plan_year_documents WHERE plan_year_id = ?',
105+
(plan_year_id,)
106+
).fetchone()['count']
107+
108+
for idx, file in enumerate(files):
109+
sequence = existing_count + idx + 1
110+
111+
# Get original filename for display (before renaming)
112+
original_filename = secure_filename(file.filename)
113+
114+
# Generate proper filename for the document using user's pattern
115+
base_filename = generate_policy_document_filename(
116+
pattern=policy_pattern,
117+
year=year,
118+
sequence=sequence,
119+
original_filename=original_filename
120+
)
121+
122+
# Save file with generated filename (no compression for documents)
100123
result = save_uploaded_file(
101124
file,
102125
docs_folder,
103126
user_id=None, # No compression for documents
127+
filename_override=base_filename,
104128
compress=False
105129
)
106130

107131
if result:
108132
file_path, saved_filename, db_file_type = result
109133
file_size = get_file_size(file_path)
110134

111-
# Get original filename for display
112-
original_filename = secure_filename(file.filename)
113-
114135
cursor.execute('''
115136
INSERT INTO plan_year_documents (plan_year_id, file_path, file_type, original_filename, file_size)
116137
VALUES (?, ?, ?, ?, ?)
@@ -130,15 +151,15 @@ def add_documents(plan_year_id):
130151
'plan_year_start': plan_year_start_str
131152
})
132153

133-
# Sync to Paperless after transaction is committed
154+
# Sync to Paperless in background (non-blocking)
134155
for sync_item in pending_syncs:
135156
try:
136-
from services.paperless_service import sync_to_paperless_safe
157+
from services.paperless_service import sync_to_paperless_background
137158
metadata = {
138159
'date': sync_item['plan_year_start'],
139160
'category': 'policy'
140161
}
141-
sync_to_paperless_safe(
162+
sync_to_paperless_background(
142163
user_id=user_id,
143164
file_path=sync_item['file_path'],
144165
metadata=metadata,
@@ -193,7 +214,7 @@ def delete_document(document_id):
193214
try:
194215
from services.paperless_service import delete_from_paperless
195216
# Build title to search for if no doc_id
196-
title = f"HSA-Policy-{paperless_info['date']}"
217+
title = f"HealthReceipts-Policy-{paperless_info['date']}"
197218

198219
delete_from_paperless(
199220
user_id=user_id,

0 commit comments

Comments
 (0)