thatcher 1.0.6 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -16
- package/package.json +12 -18
- package/src/adapters/google-auth.js +4 -43
- package/src/adapters/google-drive.js +12 -67
- package/src/app/api/audit/dashboard/route.js +14 -11
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +6 -3
- package/src/app/api/audit/permissions/route.js +10 -7
- package/src/app/api/audit/permissions/stats/route.js +8 -5
- package/src/app/api/audit/route.js +4 -4
- package/src/app/api/audit/stats/route.js +1 -1
- package/src/app/api/auth/google/callback/route.js +5 -2
- package/src/app/api/auth/google/route.js +4 -1
- package/src/app/api/auth/login/route.js +5 -2
- package/src/app/api/auth/mwr-bridge/route.js +11 -21
- package/src/app/api/auth/password-reset/route.js +15 -14
- package/src/app/api/cron/trigger/route.js +4 -1
- package/src/app/api/debug/[[...path]]/route.js +6 -5
- package/src/app/api/debug/sqlite/route.js +7 -12
- package/src/app/api/debug/sync/route.js +5 -5
- package/src/app/api/domains/route.js +4 -1
- package/src/app/api/email/allocate/batch/route.js +19 -29
- package/src/app/api/email/allocate/route.js +16 -17
- package/src/app/api/email/receive/route.js +31 -42
- package/src/app/api/email/send/route.js +28 -16
- package/src/app/api/email/unallocated/route.js +10 -13
- package/src/app/api/files/[id]/route.js +4 -1
- package/src/app/api/formula/[[...path]]/route.js +0 -4
- package/src/app/api/health/route.js +10 -9
- package/src/app/api/metrics/route.js +1 -1
- package/src/cli.js +4 -4
- package/src/config/config-loader.js +1 -21
- package/src/config/constants.js +0 -35
- package/src/config/env.js +0 -30
- package/src/config/spec-helpers.js +6 -95
- package/src/engine.js +23 -0
- package/src/engine.server.js +8 -78
- package/src/index.js +42 -67
- package/src/lib/alert-manager.js +7 -3
- package/src/lib/api-helpers.js +4 -3
- package/src/lib/api.js +10 -10
- package/src/lib/audit-logger-enhanced.js +77 -83
- package/src/lib/auth-middleware.js +1 -32
- package/src/lib/auth-route-helpers.js +5 -0
- package/src/lib/busybase-adapter.js +0 -5
- package/src/lib/busybase-audit-reads.js +142 -0
- package/src/lib/busybase-audit.js +41 -0
- package/src/lib/busybase-lucia-adapter.js +72 -0
- package/src/lib/busybase-store.js +338 -0
- package/src/lib/config-field-helpers.js +0 -17
- package/src/lib/config-generator-engine.js +19 -36
- package/src/lib/config-helpers.js +0 -21
- package/src/lib/crud-action-helpers.js +2 -2
- package/src/lib/crud-factory.js +1 -16
- package/src/lib/crud-handlers.js +17 -52
- package/src/lib/csrf-protection.js +5 -2
- package/src/lib/date-utils.js +3 -127
- package/src/lib/debug-registry.js +0 -5
- package/src/lib/domain-loader.js +4 -1
- package/src/lib/email-sender.js +25 -23
- package/src/lib/error-handler.js +29 -88
- package/src/lib/error-recovery.js +10 -7
- package/src/lib/error-resilience.js +7 -5
- package/src/lib/events-engine.js +9 -6
- package/src/lib/export-sink.js +0 -5
- package/src/lib/field-iterator.js +7 -76
- package/src/lib/field-registry.js +0 -22
- package/src/lib/generic-crud-handler.js +0 -16
- package/src/lib/hook-engine.js +5 -53
- package/src/lib/hot-reload/checkpoint.js +5 -2
- package/src/lib/hot-reload/promise-container.js +7 -3
- package/src/lib/hot-reload/route-wrapper.js +4 -1
- package/src/lib/hot-reload/supervisor.js +6 -3
- package/src/lib/hyperformula-service.js +0 -5
- package/src/lib/id-helpers.js +10 -0
- package/src/lib/index.js +4 -7
- package/src/lib/log-aggregator.js +0 -1
- package/src/lib/logger.js +0 -15
- package/src/lib/metrics-collector.js +2 -46
- package/src/lib/next-compat.js +15 -1
- package/src/lib/observability-bootstrap.js +0 -5
- package/src/lib/perf-monitor.js +4 -1
- package/src/lib/perf-profiler.js +0 -5
- package/src/lib/query-string-adapter.js +5 -47
- package/src/lib/realtime-server.js +4 -26
- package/src/lib/request-tracing.js +0 -5
- package/src/lib/resource-monitor.js +4 -1
- package/src/lib/response-formatter.js +0 -38
- package/src/lib/route-helpers.js +3 -1
- package/src/lib/safe-json.js +5 -1
- package/src/lib/server-bootstrap.js +10 -9
- package/src/lib/stage-pipeline.js +2 -2
- package/src/lib/state-transport-client.js +4 -1
- package/src/lib/static-server.js +2 -2
- package/src/lib/status-helpers.js +2 -51
- package/src/lib/tracing.js +0 -5
- package/src/lib/utils.js +6 -67
- package/src/lib/validate.js +5 -88
- package/src/lib/validators.js +0 -38
- package/src/lib/with-audit-logging.js +1 -1
- package/src/lib/workflow-engine.js +10 -77
- package/src/lib/xstate-workflow-engine.js +3 -9
- package/src/plugins/index.js +5 -3
- package/src/server/server.js +26 -27
- package/src/services/collaborator-role.service.js +21 -77
- package/src/services/email-sender.js +7 -34
- package/src/services/notification-engine.js +12 -33
- package/src/services/permission.service.js +10 -73
- package/src/ui/advanced-search-renderer.js +3 -2
- package/src/ui/advanced-widgets.js +1 -1
- package/src/ui/auth-pages.js +10 -9
- package/src/ui/auth-styles.js +1 -1
- package/src/ui/client.js +1 -1
- package/src/ui/component-engine.js +14 -5
- package/src/ui/dashboard-renderer.js +5 -5
- package/src/ui/dialog-engine.js +6 -9
- package/src/ui/dialog-factory.js +2 -2
- package/src/ui/engagement-cards.js +7 -7
- package/src/ui/engagement-dialogs.js +2 -3
- package/src/ui/engagement-grid-renderer.js +20 -19
- package/src/ui/entity-renderer.js +56 -42
- package/src/ui/file-dialogs.js +25 -9
- package/src/ui/flexup-report-renderer.js +21 -18
- package/src/ui/format-helpers.js +27 -28
- package/src/ui/highlight-threading-renderer.js +9 -9
- package/src/ui/job-management-renderer.js +18 -16
- package/src/ui/layout.js +51 -25
- package/src/ui/ml-console-renderer.js +15 -16
- package/src/ui/monitoring-dashboard-client.js +9 -8
- package/src/ui/notifications-renderer.js +11 -10
- package/src/ui/page-handler-admin.js +32 -23
- package/src/ui/page-handler-helpers.js +27 -23
- package/src/ui/page-handler-reviews.js +71 -35
- package/src/ui/page-handler-rfi.js +28 -14
- package/src/ui/page-handler.js +65 -48
- package/src/ui/picker-dialogs.js +26 -19
- package/src/ui/render-helpers.js +84 -46
- package/src/ui/review-comparison-renderer.js +8 -7
- package/src/ui/review-detail-panels.js +8 -9
- package/src/ui/review-detail-renderer.js +3 -3
- package/src/ui/review-detail-script.js +13 -12
- package/src/ui/review-mwr-renderer.js +6 -5
- package/src/ui/review-renderer.js +7 -6
- package/src/ui/review-widgets.js +3 -3
- package/src/ui/rfi-detail-renderer.js +37 -20
- package/src/ui/rfi-report-renderer.js +14 -9
- package/src/ui/settings-renderer-advanced.js +21 -18
- package/src/ui/settings-renderer-advanced2.js +20 -19
- package/src/ui/settings-renderer-teams.js +4 -4
- package/src/ui/settings-renderer.js +4 -4
- package/src/ui/spacing-system.js +5 -2
- package/src/ui/standalone-login.js +2 -2
- package/src/ui/styles.css +2 -1
- package/src/ui/styles2.css +75 -5
- package/src/ui/test-page.js +4 -4
- package/src/ui/widgets.js +20 -15
- package/src/lib/audit-logger.js +0 -193
- package/src/lib/database-core.js +0 -255
- package/src/lib/database-migrations.js +0 -350
- package/src/lib/db-backup.js +0 -97
- package/src/lib/error-boundary.js +0 -134
- package/src/lib/errors.js +0 -69
- package/src/lib/health-monitor.js +0 -134
- package/src/lib/index-optimizer.js +0 -96
- package/src/lib/query-engine-write.js +0 -221
- package/src/lib/query-engine.js +0 -446
- package/src/ui/collaboration-dialogs.js +0 -31
- package/src/ui/global-tags.js +0 -144
- package/src/ui/idle-logout.js +0 -156
- package/src/ui/letter-dialogs.js +0 -37
- package/src/ui/password-reset-page.js +0 -146
- package/src/ui/perf-renderer.js +0 -71
- package/src/ui/rfi-renderer.js +0 -194
- package/src/ui/validation-ui.js +0 -147
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { list, get } from '@/lib/
|
|
1
|
+
import { list, get } from '@/lib/busybase-store.js';
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
2
3
|
import { getSpec } from '@/config/spec-helpers.js';
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
const log = createLogger('[PageHandlerReviews]');
|
|
4
6
|
import { renderEntityList, renderAccessDenied } from '@/ui/renderer.js';
|
|
5
7
|
import { renderSectionReport, renderReviewListTabbed } from '@/ui/review-renderer.js';
|
|
6
8
|
import { renderChecklistDetails, renderChecklistsHome } from '@/ui/checklist-renderer.js';
|
|
@@ -21,14 +23,14 @@ async function lazyRenderer(name) {
|
|
|
21
23
|
return import(`file://${__dirname_rv}${name}?t=${t}`);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
export function handleFilteredReviewList(user, filter) {
|
|
26
|
+
export async function handleFilteredReviewList(user, filter) {
|
|
25
27
|
if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
|
|
26
28
|
const spec = getSpec('review'); if (!spec) return null;
|
|
27
|
-
|
|
29
|
+
const where = filter === 'archive' ? { status: 'archived' } : {};
|
|
30
|
+
let items = await list('review', where);
|
|
28
31
|
if (filter === 'active') items = items.filter(r => r.status === 'active' || r.status === 'open');
|
|
29
32
|
else if (filter === 'priority') { const pids = user.priority_reviews || []; items = items.filter(r => pids.includes(r.id)); }
|
|
30
33
|
else if (filter === 'history') items = items.filter(r => r.status === 'closed' || r.status === 'completed');
|
|
31
|
-
else if (filter === 'archive') items = items.filter(r => r.status === 'archived');
|
|
32
34
|
return renderEntityList('review', resolveRefFields(items, spec), spec, user);
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -40,25 +42,27 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
|
|
|
40
42
|
|
|
41
43
|
if (normalized === '/reviews' || normalized === '/review') {
|
|
42
44
|
if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
|
|
43
|
-
let reviews = []; try { reviews =
|
|
44
|
-
const engMap = {}; try { list('engagement', {}).forEach(e => { engMap[e.id] = e.name; }); } catch {}
|
|
45
|
+
let reviews = []; try { reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } }); } catch {}
|
|
46
|
+
const engMap = {}; try { (await list('engagement', {})).forEach(e => { engMap[e.id] = e.name; }); } catch {}
|
|
45
47
|
return renderReviewListTabbed(user, reviews.map(r => ({ ...r, engagement_name: engMap[r.engagement_id] || r.engagement_name || null })));
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
if (segments[0] === 'review' && segments.length === 3 && segments[2] === 'sections') {
|
|
49
51
|
const reviewId = segments[1];
|
|
50
52
|
if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
|
|
51
|
-
const review = get('review', reviewId)
|
|
53
|
+
const review = await get('review', reviewId);
|
|
52
54
|
if (!review) return null;
|
|
53
55
|
let sections = [];
|
|
54
|
-
try { sections = list('review_section', {
|
|
56
|
+
try { sections = (await list('review_section', { review_id: reviewId })); } catch (e) { log.error('Failed to fetch sections', { reviewId, message: e?.message || String(e) }); }
|
|
55
57
|
return renderSectionReport(user, review, sections);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
if (normalized === '/reviews/analytics' || normalized === '/review/analytics') {
|
|
59
61
|
if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
|
|
60
62
|
let reviews = [], highlights = [], activity = [];
|
|
61
|
-
try { reviews = list('review', {}); } catch {}
|
|
63
|
+
try { reviews = await list('review', {}); } catch {}
|
|
64
|
+
try { highlights = await list('highlight', {}); } catch {}
|
|
65
|
+
try { activity = (await list('activity_log', {})).slice(0, 50); } catch {}
|
|
62
66
|
return renderReviewAnalytics(user, { reviews, highlights, recentActivity: activity });
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -67,40 +71,41 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
|
|
|
67
71
|
const url = new URL(req.url, `http://${req.headers.host||'localhost'}`);
|
|
68
72
|
const leftId = url.searchParams.get('left'), rightId = url.searchParams.get('right');
|
|
69
73
|
if (leftId && rightId) {
|
|
70
|
-
const leftReview = get('review', leftId), rightReview = get('review', rightId);
|
|
74
|
+
const leftReview = await get('review', leftId), rightReview = await get('review', rightId);
|
|
71
75
|
if (!leftReview || !rightReview) return null;
|
|
72
76
|
let leftH = [], rightH = [];
|
|
73
|
-
try {
|
|
77
|
+
try { const all = await list('highlight', {}); leftH = all.filter(h => h.review_id === leftId); rightH = all.filter(h => h.review_id === rightId); } catch {}
|
|
74
78
|
return renderReviewComparison(user, leftReview, rightReview, leftH, rightH);
|
|
75
79
|
}
|
|
76
|
-
let reviews = []; try { reviews = list('review', {}); } catch {}
|
|
80
|
+
let reviews = []; try { reviews = await list('review', {}); } catch {}
|
|
77
81
|
return renderComparisonPicker(user, reviews);
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
if (normalized === '/reviews/tenders' || normalized === '/review/tenders') {
|
|
81
85
|
if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
|
|
82
86
|
let tenders = [], reviews = [];
|
|
83
|
-
try { tenders = list('tender', {}); } catch {}
|
|
87
|
+
try { tenders = await list('tender', {}); } catch {}
|
|
88
|
+
try { reviews = await list('review', {}); } catch {}
|
|
84
89
|
return renderTenderDashboard(user, tenders.map(t => { const r = reviews.find(rev => rev.id === t.review_id); return { ...t, review_name: r?.name || r?.title || '' }; }), reviews);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
if (normalized === '/reviews/batch' || normalized === '/review/batch') {
|
|
88
93
|
if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit');
|
|
89
|
-
let reviews = []; try { reviews = list('review', {}); } catch {}
|
|
94
|
+
let reviews = []; try { reviews = await list('review', {}); } catch {}
|
|
90
95
|
const spec = getSpec('review'); if (spec) reviews = resolveRefFields(reviews, spec);
|
|
91
96
|
return renderBatchOperations(user, reviews);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
if (segments[0] === 'review' && segments.length === 3) {
|
|
95
100
|
const reviewId = segments[1], action = segments[2];
|
|
96
|
-
const getReview = (id) => get('review', id)
|
|
101
|
+
const getReview = (id) => get('review', id);
|
|
97
102
|
if (action === 'pdf') {
|
|
98
103
|
if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
|
|
99
|
-
const review = getReview(reviewId); if (!review) return null;
|
|
104
|
+
const review = await getReview(reviewId); if (!review) return null;
|
|
100
105
|
let h = [], s = [];
|
|
101
|
-
try { h = list('highlight', {}).filter(x => x.review_id === reviewId); } catch {}
|
|
102
|
-
try { s = list('review_section', {}).filter(x => x.review_id === reviewId); } catch {}
|
|
103
|
-
// Flatten bounding_rect JSON
|
|
106
|
+
try { h = (await list('highlight', {})).filter(x => x.review_id === reviewId); } catch {}
|
|
107
|
+
try { s = (await list('review_section', {})).filter(x => x.review_id === reviewId); } catch {}
|
|
108
|
+
// Flatten bounding_rect JSON -> x/y/width/height for the overlay renderer.
|
|
104
109
|
h = h.map((x) => {
|
|
105
110
|
let br = null; try { br = typeof x.bounding_rect === 'string' ? JSON.parse(x.bounding_rect) : x.bounding_rect; } catch {}
|
|
106
111
|
let rects = []; try { rects = typeof x.rects === 'string' ? JSON.parse(x.rects) : (x.rects || []); } catch {}
|
|
@@ -116,16 +121,31 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
|
|
|
116
121
|
});
|
|
117
122
|
return renderPdfViewer(user, review, h, s);
|
|
118
123
|
}
|
|
119
|
-
if (action === 'editor') { if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit'); const review = getReview(reviewId); if (!review) return null; return renderPdfEditorPlaceholder(user, review); }
|
|
120
|
-
if (action === 'highlights') {
|
|
121
|
-
|
|
124
|
+
if (action === 'editor') { if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit'); const review = await getReview(reviewId); if (!review) return null; return renderPdfEditorPlaceholder(user, review); }
|
|
125
|
+
if (action === 'highlights') {
|
|
126
|
+
if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
|
|
127
|
+
const review = await getReview(reviewId); if (!review) return null;
|
|
128
|
+
let h = []; try { h = (await list('highlight', {})).filter(x => x.review_id === reviewId); } catch {}
|
|
129
|
+
const rm = {}; const allResp = await list('highlight_response', {}).catch(() => []);
|
|
130
|
+
for (const x of h) rm[x.id] = allResp.filter(r => r.highlight_id === x.id);
|
|
131
|
+
return renderHighlightThreading(user, review, h, rm);
|
|
132
|
+
}
|
|
133
|
+
if (action === 'resolution') {
|
|
134
|
+
if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
|
|
135
|
+
const review = await getReview(reviewId); if (!review) return null;
|
|
136
|
+
let s = []; try { s = (await list('review_section', {})).filter(x => x.review_id === reviewId); } catch {}
|
|
137
|
+
const allH = await list('highlight', {}).catch(() => []);
|
|
138
|
+
const hbs = {};
|
|
139
|
+
for (const sec of s) hbs[sec.id] = allH.filter(h => h.review_id === reviewId && h.section_id === sec.id);
|
|
140
|
+
return renderSectionResolution(user, review, s, hbs);
|
|
141
|
+
}
|
|
122
142
|
if (action === 'edit') {
|
|
123
143
|
if (!canEdit(user, 'review')) return renderAccessDenied(user, 'review', 'edit');
|
|
124
|
-
const review = get('review', reviewId)
|
|
144
|
+
const review = await get('review', reviewId);
|
|
125
145
|
if (!review) return null;
|
|
126
146
|
let engagements = [], teams = [];
|
|
127
|
-
try { engagements = list('engagement', {}); } catch {}
|
|
128
|
-
try { teams = list('team', {}); } catch {}
|
|
147
|
+
try { engagements = await list('engagement', {}); } catch {}
|
|
148
|
+
try { teams = await list('team', {}); } catch {}
|
|
129
149
|
const { renderReviewEdit } = await lazyRenderer('review-edit-renderer.js');
|
|
130
150
|
return renderReviewEdit(user, review, engagements, teams);
|
|
131
151
|
}
|
|
@@ -135,13 +155,19 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
|
|
|
135
155
|
if (segments.length === 2 && segments[0] === 'review' && segments[1] !== 'new') {
|
|
136
156
|
const reviewId = segments[1];
|
|
137
157
|
if (!canView(user, 'review')) return renderAccessDenied(user, 'review', 'view');
|
|
138
|
-
const review = get('review', reviewId)
|
|
158
|
+
const review = await get('review', reviewId); if (!review) return null;
|
|
139
159
|
let highlights = [], collaborators = [], checklists = [], sections = [], tenders = [];
|
|
140
|
-
try { highlights = list('highlight', {}).filter(h => h.review_id === reviewId); } catch {}
|
|
141
|
-
try { collaborators = list('collaborator', {}).filter(c => c.review_id === reviewId); } catch {}
|
|
142
|
-
try {
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
try { highlights = (await list('highlight', {})).filter(h => h.review_id === reviewId); } catch {}
|
|
161
|
+
try { collaborators = (await list('collaborator', {})).filter(c => c.review_id === reviewId); } catch {}
|
|
162
|
+
try {
|
|
163
|
+
const allItems = await list('checklist_item', {});
|
|
164
|
+
checklists = (await list('checklist', {})).filter(c => c.review_id === reviewId).map(c => {
|
|
165
|
+
const items = allItems.filter(i => i.checklist_id === c.id);
|
|
166
|
+
return { ...c, total_items: items.length, completed_items: items.filter(i => i.completed).length };
|
|
167
|
+
});
|
|
168
|
+
} catch {}
|
|
169
|
+
try { sections = (await list('review_section', {})).filter(s => s.review_id === reviewId); } catch {}
|
|
170
|
+
try { tenders = (await list('tender', {})).filter(t => t.review_id === reviewId); } catch {}
|
|
145
171
|
const { renderReviewDetail } = await lazyRenderer('review-detail-renderer.js');
|
|
146
172
|
return renderReviewDetail(user, review, highlights, collaborators, checklists, sections, tenders);
|
|
147
173
|
}
|
|
@@ -149,15 +175,25 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
|
|
|
149
175
|
if (segments[0] === 'checklist' && segments.length === 2 && segments[1] !== 'new') {
|
|
150
176
|
const checklistId = segments[1];
|
|
151
177
|
if (!canView(user, 'checklist')) return renderAccessDenied(user, 'checklist', 'view');
|
|
152
|
-
const checklist = get('checklist', checklistId); if (!checklist) return null;
|
|
153
|
-
let items = [];
|
|
178
|
+
const checklist = await get('checklist', checklistId); if (!checklist) return null;
|
|
179
|
+
let items = [];
|
|
180
|
+
try { items = (await list('checklist_item', { checklist_id: checklistId })).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || (a.created_at ?? 0) - (b.created_at ?? 0)); } catch {}
|
|
154
181
|
return renderChecklistDetails(user, checklist, items);
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
if (normalized === '/checklists' || normalized === '/checklist') {
|
|
158
185
|
if (!canList(user, 'checklist')) return renderAccessDenied(user, 'checklist', 'list');
|
|
159
186
|
let checklists = [];
|
|
160
|
-
try {
|
|
187
|
+
try {
|
|
188
|
+
// Old GROUP BY checklist_id (total + done) -> client-side aggregate.
|
|
189
|
+
const allItems = await list('checklist_item', {});
|
|
190
|
+
const statsMap = {};
|
|
191
|
+
for (const it of allItems) {
|
|
192
|
+
const s = statsMap[it.checklist_id] || (statsMap[it.checklist_id] = { total: 0, done: 0 });
|
|
193
|
+
s.total++; if (it.is_done === 1 || it.is_done === true || it.completed) s.done++;
|
|
194
|
+
}
|
|
195
|
+
checklists = (await list('checklist', {})).map(c => ({ ...c, total_items: statsMap[c.id]?.total || 0, completed_items: statsMap[c.id]?.done || 0 }));
|
|
196
|
+
} catch {}
|
|
161
197
|
return renderChecklistsHome(user, checklists);
|
|
162
198
|
}
|
|
163
199
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { list, get } from '@/lib/
|
|
2
|
-
import { getDatabase } from '@/lib/database-core.js';
|
|
1
|
+
import { list, get } from '@/lib/busybase-store.js';
|
|
3
2
|
import { canView, canList, isClerk, isPartner } from '@/ui/permissions-ui.js';
|
|
4
3
|
import { renderAccessDenied } from '@/ui/renderer.js';
|
|
5
4
|
import { renderRfiList } from '@/ui/rfi-list-renderer.js';
|
|
@@ -9,31 +8,46 @@ const lazyR = (name) => import(`file://${__dirname_rfi}${name}?t=${globalThis.__
|
|
|
9
8
|
|
|
10
9
|
export async function handleRfiDetail(user, rfiId) {
|
|
11
10
|
if (!canView(user, 'rfi')) return renderAccessDenied(user, 'rfi', 'view');
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const rfi = await get('rfi', rfiId); if (!rfi) return null;
|
|
12
|
+
let questions = [];
|
|
13
|
+
try {
|
|
14
|
+
questions = (await list('rfi_question', { rfi_id: rfiId }));
|
|
15
|
+
// Old correlated subquery (response_count per question) -> client-side aggregate.
|
|
16
|
+
const responses = await list('rfi_response', {});
|
|
17
|
+
const counts = {};
|
|
18
|
+
for (const r of responses) counts[r.question_id] = (counts[r.question_id] || 0) + 1;
|
|
19
|
+
questions = questions.map(q => ({ ...q, response_count: counts[q.id] || 0 }));
|
|
20
|
+
} catch {}
|
|
21
|
+
let sections = []; try {
|
|
22
|
+
const [bySectionId, byEngId] = await Promise.all([
|
|
23
|
+
list('rfi_section', { rfi_id: rfiId }),
|
|
24
|
+
list('rfi_section', { engagement_id: rfi.engagement_id }),
|
|
25
|
+
]);
|
|
26
|
+
const seen = new Set(); sections = [...bySectionId, ...byEngId].filter(s => seen.has(s.id) ? false : seen.add(s.id));
|
|
27
|
+
} catch {}
|
|
28
|
+
let engagement = null; try { if (rfi.engagement_id) engagement = await get('engagement', rfi.engagement_id); } catch {}
|
|
17
29
|
const { renderRfiDetail } = await lazyR('rfi-detail-renderer.js');
|
|
18
30
|
return renderRfiDetail(user, rfi, questions, sections, engagement);
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
export async function handleRfiList(user) {
|
|
22
34
|
if (!canList(user, 'rfi')) return renderAccessDenied(user, 'rfi', 'list');
|
|
23
|
-
let rfis = []; try { rfis =
|
|
24
|
-
let engagements = []; try { engagements = list('engagement', {}); } catch {}
|
|
35
|
+
let rfis = []; try { rfis = await list('rfi', {}, { sort: { field: 'created_at', dir: 'ASC' } }); } catch {}
|
|
36
|
+
let engagements = []; try { engagements = await list('engagement', {}); } catch {}
|
|
25
37
|
if (isClerk(user)) rfis = rfis.filter(r => engagements.filter(e => e.assigned_to === user.id || e.team_id === user.team_id).some(e => e.id === r.engagement_id));
|
|
26
38
|
return renderRfiList(user, rfis.map((r, i) => ({ ...r, display_name: 'RFI #' + (i + 1) })), engagements);
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export async function handleRfiReport(user, rfiId, res) {
|
|
30
|
-
const db = getDatabase();
|
|
31
42
|
let rfi = null, questions = [], responses = [], engagement = null;
|
|
32
|
-
try { rfi = get('rfi', rfiId)
|
|
43
|
+
try { rfi = await get('rfi', rfiId); } catch {}
|
|
33
44
|
if (!rfi) return null;
|
|
34
|
-
try { questions =
|
|
35
|
-
try {
|
|
36
|
-
|
|
45
|
+
try { questions = await list('rfi_question', { rfi_id: rfiId }, { sort: { field: 'created_at', dir: 'ASC' } }); } catch {}
|
|
46
|
+
try {
|
|
47
|
+
const qIds = new Set(questions.map(q => q.id));
|
|
48
|
+
if (qIds.size) responses = (await list('rfi_response', {})).filter(r => qIds.has(r.question_id));
|
|
49
|
+
} catch {}
|
|
50
|
+
try { engagement = rfi.engagement_id ? await get('engagement', rfi.engagement_id) : null; } catch {}
|
|
37
51
|
const { renderRfiReport } = await lazyR('rfi-report-renderer.js');
|
|
38
52
|
const html = renderRfiReport(user, rfi, engagement, questions, responses);
|
|
39
53
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
package/src/ui/page-handler.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { getUser, setCurrentRequest } from '@/engine.server.js';
|
|
2
2
|
import { hasGoogleAuth } from '@/config/env.js';
|
|
3
3
|
import { getSpec } from '@/config/spec-helpers.js';
|
|
4
|
-
import { list, get } from '@/lib/
|
|
5
|
-
import { getDatabase } from '@/lib/database-core.js';
|
|
4
|
+
import { list, get } from '@/lib/busybase-store.js';
|
|
6
5
|
import { renderLogin, renderDashboard, renderEntityList, renderEntityDetail, renderEntityForm, renderAccessDenied, renderPasswordReset, renderPasswordResetConfirm, REDIRECT } from '@/ui/renderer.js';
|
|
7
6
|
import { renderClientDashboard, renderClientList } from '@/ui/client-renderer.js';
|
|
8
7
|
import { canList, canView, canCreate, canEdit, isPartner, isClerk, isClientUser, canClientAccessEntity } from '@/ui/permissions-ui.js';
|
|
@@ -25,25 +24,32 @@ function reqUrl(req) {
|
|
|
25
24
|
|
|
26
25
|
async function handleEngagementDetail(user, engId) {
|
|
27
26
|
if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
|
|
28
|
-
const
|
|
29
|
-
const engagement = get('engagement', engId) || db.prepare('SELECT * FROM engagement WHERE id=?').get(engId);
|
|
27
|
+
const engagement = await get('engagement', engId);
|
|
30
28
|
if (!engagement) return null;
|
|
31
|
-
let client = null; try { client = engagement.client_id ? get('client', engagement.client_id) : null; } catch {}
|
|
32
|
-
let rfis = []; try { rfis =
|
|
33
|
-
let sections = [];
|
|
34
|
-
|
|
29
|
+
let client = null; try { client = engagement.client_id ? await get('client', engagement.client_id) : null; } catch {}
|
|
30
|
+
let rfis = []; try { rfis = await list('rfi', { engagement_id: engId }); } catch {}
|
|
31
|
+
let sections = [];
|
|
32
|
+
try {
|
|
33
|
+
sections = await list('rfi_section', { engagement_id: engId }, { sort: { field: 'sort_order', dir: 'ASC' } });
|
|
34
|
+
} catch {}
|
|
35
|
+
let team = null; try { team = engagement.team_id ? await get('team', engagement.team_id) : null; } catch {}
|
|
35
36
|
let assignedUsers = [];
|
|
36
|
-
try {
|
|
37
|
+
try {
|
|
38
|
+
const ids = JSON.parse(engagement.users || '[]');
|
|
39
|
+
const users = await list('user', {});
|
|
40
|
+
const um = Object.fromEntries(users.map(u => [u.id, u.name || u.email]));
|
|
41
|
+
assignedUsers = ids.map(id => ({ id, name: um[id] || null })).filter(u => u.name);
|
|
42
|
+
} catch {}
|
|
37
43
|
const { renderEngagementDetail } = await lazyRenderer('engagement-detail-renderer.js');
|
|
38
44
|
return renderEngagementDetail(user, { ...engagement, team_name: team?.name || engagement.team_name, client_name: client?.name || engagement.client_name, assigned_users_resolved: assignedUsers }, client, rfis, sections);
|
|
39
45
|
}
|
|
40
46
|
async function handleEngagementList(user, req) {
|
|
41
47
|
if (!canList(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'list');
|
|
42
|
-
let engagements = list('engagement', {});
|
|
43
|
-
const clientMap = Object.fromEntries(list('client', {}).map(c => [c.id, c.name]));
|
|
48
|
+
let engagements = await list('engagement', {});
|
|
49
|
+
const clientMap = Object.fromEntries((await list('client', {})).map(c => [c.id, c.name]));
|
|
44
50
|
engagements = engagements.map(e => ({ ...e, client_name: clientMap[e.client_id] || e.client_name || '-' }));
|
|
45
51
|
const spec = getSpec('engagement'); if (spec) engagements = resolveRefFields(engagements, spec);
|
|
46
|
-
let teams = []; try { teams = list('team', {}); } catch {}
|
|
52
|
+
let teams = []; try { teams = await list('team', {}); } catch {}
|
|
47
53
|
const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
|
|
48
54
|
engagements = engagements.map(e => ({ ...e, team_name: teamMap[e.team_id] || e.team_name || '-' }));
|
|
49
55
|
const years = [...new Set(engagements.map(e => { if (!e.year) return null; const m = String(e.year).match(/\b(20\d{2}|19\d{2})\b/); return m ? m[1] : null; }).filter(Boolean))].sort().reverse();
|
|
@@ -53,10 +59,10 @@ async function handleEngagementList(user, req) {
|
|
|
53
59
|
async function handleSearch(user, req) {
|
|
54
60
|
const url = reqUrl(req);
|
|
55
61
|
const q = url.searchParams.get('q') || '', entityFilter = url.searchParams.get('entity') || '', statusFilter = url.searchParams.get('status') || '';
|
|
56
|
-
let teams = []; try { teams = list('team', {}); } catch {}
|
|
62
|
+
let teams = []; try { teams = await list('team', {}); } catch {}
|
|
57
63
|
const results = {};
|
|
58
64
|
for (const eName of (entityFilter ? [entityFilter] : ['engagement', 'client', 'rfi', 'review'])) {
|
|
59
|
-
try { let items = list(eName, {}); if (q) items = items.filter(i => JSON.stringify(i).toLowerCase().includes(q.toLowerCase())); if (statusFilter) items = items.filter(i => i.status === statusFilter); const spec = getSpec(eName); if (spec) items = resolveRefFields(items, spec); results[eName] = items.slice(0, 50); } catch {}
|
|
65
|
+
try { let items = await list(eName, {}); if (q) items = items.filter(i => JSON.stringify(i).toLowerCase().includes(q.toLowerCase())); if (statusFilter) items = items.filter(i => i.status === statusFilter); const spec = getSpec(eName); if (spec) items = resolveRefFields(items, spec); results[eName] = items.slice(0, 50); } catch {}
|
|
60
66
|
}
|
|
61
67
|
return renderAdvancedSearch(user, results, { teams, stages: ['info_gathering', 'commencement', 'team_execution', 'partner_review', 'finalization', 'closeout'] });
|
|
62
68
|
}
|
|
@@ -67,10 +73,10 @@ async function handleGenericEntityView(user, entityName, id) {
|
|
|
67
73
|
if (!canCreate(user, entityName)) return renderAccessDenied(user, entityName, 'create');
|
|
68
74
|
const resolvedSpec = resolveEnumOptions(spec);
|
|
69
75
|
const { renderEntityForm: lazyEntityForm } = await lazyRenderer('entity-renderer.js');
|
|
70
|
-
return lazyEntityForm(entityName, null, resolvedSpec, user, true, getRefOptions(resolvedSpec));
|
|
76
|
+
return lazyEntityForm(entityName, null, resolvedSpec, user, true, await getRefOptions(resolvedSpec));
|
|
71
77
|
}
|
|
72
78
|
if (!canView(user, entityName)) return renderAccessDenied(user, entityName, 'view');
|
|
73
|
-
const item = get(entityName, id); if (!item) return null;
|
|
79
|
+
const item = await get(entityName, id); if (!item) return null;
|
|
74
80
|
if (item.team_id && user.team_id && item.team_id !== user.team_id && !isPartner(user)) return renderAccessDenied(user, entityName, 'view');
|
|
75
81
|
if (isClientUser(user) && user.client_id && item.client_id && item.client_id !== user.client_id) return renderAccessDenied(user, entityName, 'view');
|
|
76
82
|
const [resolvedItem] = resolveRefFields([item], spec);
|
|
@@ -79,14 +85,14 @@ async function handleGenericEntityView(user, entityName, id) {
|
|
|
79
85
|
}
|
|
80
86
|
async function handleClientSubRoute(user, clientId, subRoute) {
|
|
81
87
|
if (!canView(user, 'client')) return renderAccessDenied(user, 'client', 'view');
|
|
82
|
-
const client = get('client', clientId); if (!client) return null;
|
|
88
|
+
const client = await get('client', clientId); if (!client) return null;
|
|
83
89
|
if (isClientUser(user) && user.client_id && user.client_id !== clientId) return renderAccessDenied(user, 'client', 'view');
|
|
84
|
-
if (subRoute === 'dashboard' || subRoute === 'users') return renderClientDashboard(user, client, getClientDashboardStats(clientId));
|
|
90
|
+
if (subRoute === 'dashboard' || subRoute === 'users') return renderClientDashboard(user, client, await getClientDashboardStats(clientId));
|
|
85
91
|
if (subRoute === 'progress') {
|
|
86
|
-
let engagements = []; try { engagements = list('engagement', {}).filter(e => e.client_id === clientId); } catch {}
|
|
92
|
+
let engagements = []; try { engagements = (await list('engagement', {})).filter(e => e.client_id === clientId); } catch {}
|
|
87
93
|
const spec = getSpec('engagement'); if (spec) engagements = resolveRefFields(engagements, spec);
|
|
88
94
|
let rfiStats = { total: 0, responded: 0, overdue: 0 };
|
|
89
|
-
try { const allRfis = list('rfi', {}).filter(r => engagements.some(e => e.id === r.engagement_id)); const now = Math.floor(Date.now() / 1000); rfiStats = { total: allRfis.length, responded: allRfis.filter(r => r.status === 'responded' || r.status === 'completed').length, overdue: allRfis.filter(r => r.due_date && r.due_date < now && r.status !== 'closed').length }; } catch {}
|
|
95
|
+
try { const allRfis = (await list('rfi', {})).filter(r => engagements.some(e => e.id === r.engagement_id)); const now = Math.floor(Date.now() / 1000); rfiStats = { total: allRfis.length, responded: allRfis.filter(r => r.status === 'responded' || r.status === 'completed').length, overdue: allRfis.filter(r => r.due_date && r.due_date < now && r.status !== 'closed').length }; } catch {}
|
|
90
96
|
return renderClientProgress(user, client, engagements, rfiStats);
|
|
91
97
|
}
|
|
92
98
|
return null;
|
|
@@ -94,11 +100,11 @@ async function handleClientSubRoute(user, clientId, subRoute) {
|
|
|
94
100
|
async function handleGenericEntityEdit(user, entityName, id) {
|
|
95
101
|
const spec = getSpec(entityName); if (!spec) return null;
|
|
96
102
|
if (!canEdit(user, entityName)) return renderAccessDenied(user, entityName, 'edit');
|
|
97
|
-
const item = get(entityName, id); if (!item) return null;
|
|
103
|
+
const item = await get(entityName, id); if (!item) return null;
|
|
98
104
|
if (item.team_id && user.team_id && item.team_id !== user.team_id && !isPartner(user)) return renderAccessDenied(user, entityName, 'edit');
|
|
99
105
|
const resolvedSpec = resolveEnumOptions(spec);
|
|
100
106
|
const { renderEntityForm: lazyEntityForm } = await lazyRenderer('entity-renderer.js');
|
|
101
|
-
return lazyEntityForm(entityName, item, resolvedSpec, user, false, getRefOptions(resolvedSpec));
|
|
107
|
+
return lazyEntityForm(entityName, item, resolvedSpec, user, false, await getRefOptions(resolvedSpec));
|
|
102
108
|
}
|
|
103
109
|
export async function handlePage(pathname, req, res) {
|
|
104
110
|
setCurrentRequest(req);
|
|
@@ -119,18 +125,31 @@ export async function handlePage(pathname, req, res) {
|
|
|
119
125
|
const user = await getUser();
|
|
120
126
|
if (!user) { res.writeHead(302, { Location: '/login' }); res.end(); return REDIRECT; }
|
|
121
127
|
if (normalized === '/unauthorized') return renderAccessDenied(user, 'system', 'access');
|
|
122
|
-
if (normalized === '/notifications') { let notifs=[]; try{notifs=
|
|
128
|
+
if (normalized === '/notifications') { let notifs=[]; try{notifs=await list('notification',{user_id:user.id},{sort:{field:'created_at',dir:'DESC'},limit:100})}catch{} const{renderNotificationsPage}=await lazyRenderer('notifications-renderer.js'); return renderNotificationsPage(user,notifs); }
|
|
123
129
|
if (normalized === '/' || normalized === '/dashboard') return renderDashboard(user, await getDashboardStats(user));
|
|
124
130
|
if (normalized.startsWith('/admin/') || normalized === '/admin/jobs') return handleAdminPage(normalized, segments, user);
|
|
125
131
|
if (segments[0] === 'client' && segments.length === 3 && ['dashboard', 'users', 'progress'].includes(segments[2])) return handleClientSubRoute(user, segments[1], segments[2]);
|
|
126
132
|
if (isClerk(user) && segments.length >= 1 && ['user', 'team'].includes(segments[0])) return renderAccessDenied(user, segments[0], 'list');
|
|
127
133
|
if (normalized === '/mwr' || normalized === '/mwr/home') {
|
|
128
134
|
if (!canList(user, 'review')) return renderAccessDenied(user, 'review', 'list');
|
|
129
|
-
const db = getDatabase();
|
|
130
135
|
let myReviews = [], sharedReviews = [], recentActivity = [];
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
|
|
136
|
+
// OR across two columns: busybase eq() is single-column, so fetch + filter in JS.
|
|
137
|
+
try {
|
|
138
|
+
const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } });
|
|
139
|
+
myReviews = reviews.filter(r => r.created_by === user.id || r.assigned_to === user.id).slice(0, 100);
|
|
140
|
+
} catch {}
|
|
141
|
+
// Old JOIN collaborator -> two-step: collaborator rows for this user, then their reviews.
|
|
142
|
+
try {
|
|
143
|
+
const collabs = await list('collaborator', { user_id: user.id });
|
|
144
|
+
const ids = new Set(collabs.map(c => c.review_id));
|
|
145
|
+
if (ids.size) {
|
|
146
|
+
const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } });
|
|
147
|
+
sharedReviews = reviews.filter(r => ids.has(r.id)).slice(0, 100);
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
150
|
+
try {
|
|
151
|
+
recentActivity = (await list('audit_logs', { entity_type: 'review' }, { sort: { field: 'created_at', dir: 'DESC' } })).slice(0, 50);
|
|
152
|
+
} catch {}
|
|
134
153
|
const all = [...myReviews, ...sharedReviews];
|
|
135
154
|
const stats = { myReviews, sharedReviews, recentActivity, totalReviews: all.length, activeReviews: all.filter(r => (r.status||'open') !== 'archived' && (r.status||'open') !== 'completed' && (r.status||'open') !== 'closed').length, flaggedReviews: all.filter(r => r.flagged).length, overdueReviews: 0 };
|
|
136
155
|
const { renderMwrHome } = await lazyRenderer('review-mwr-renderer.js');
|
|
@@ -151,18 +170,16 @@ export async function handlePage(pathname, req, res) {
|
|
|
151
170
|
return renderFlexUpView(user);
|
|
152
171
|
}
|
|
153
172
|
if (normalized === '/ml-console') {
|
|
154
|
-
const db = getDatabase();
|
|
155
173
|
let candidates = []; let reviewMap = {};
|
|
156
174
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
ORDER BY created_at DESC LIMIT 50`).all();
|
|
175
|
+
// Old: highlights with a non-trivial comment (LIKE '%?' OR length>40), newest 50.
|
|
176
|
+
const all = await list('highlight', {}, { sort: { field: 'created_at', dir: 'DESC' } });
|
|
177
|
+
candidates = all.filter(c => c.comment && (c.comment.includes('?') || c.comment.length > 40)).slice(0, 50);
|
|
161
178
|
const revIds = [...new Set(candidates.map((c) => c.review_id))];
|
|
162
179
|
if (revIds.length) {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
reviewMap = Object.fromEntries(
|
|
180
|
+
const reviews = await list('review', {});
|
|
181
|
+
const byId = new Map(reviews.map(r => [r.id, r.name || '-']));
|
|
182
|
+
reviewMap = Object.fromEntries(revIds.map(id => [id, byId.get(id) || '-']));
|
|
166
183
|
}
|
|
167
184
|
} catch {}
|
|
168
185
|
const { renderMlConsole } = await lazyRenderer('ml-console-renderer.js');
|
|
@@ -171,19 +188,19 @@ export async function handlePage(pathname, req, res) {
|
|
|
171
188
|
if (segments.length === 2 && (segments[0] === 'engagements' || segments[0] === 'engagement') && segments[1] !== 'new') return handleEngagementDetail(user, segments[1]);
|
|
172
189
|
if (segments[0] === 'engagement' && segments.length === 3 && segments[2] === 'letter') {
|
|
173
190
|
if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
|
|
174
|
-
const engagement = get('engagement', segments[1]); if (!engagement) return null;
|
|
191
|
+
const engagement = await get('engagement', segments[1]); if (!engagement) return null;
|
|
175
192
|
return renderLetterWorkflow(user, engagement);
|
|
176
193
|
}
|
|
177
194
|
if (segments[0] === 'engagement' && segments.length === 3 && segments[2] === 'report') {
|
|
178
195
|
if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
|
|
179
|
-
const engId = segments[1]; const engagement = get('engagement', engId); if (!engagement) return null;
|
|
180
|
-
|
|
181
|
-
try{client=engagement.client_id?get('client',engagement.client_id):null}catch{}
|
|
182
|
-
try{team=engagement.team_id?get('team',engagement.team_id):null}catch{}
|
|
183
|
-
try{rfis=
|
|
184
|
-
try{reviews=
|
|
185
|
-
try{const rids=reviews.map(r=>r.id);if(rids.
|
|
186
|
-
try{activity=
|
|
196
|
+
const engId = segments[1]; const engagement = await get('engagement', engId); if (!engagement) return null;
|
|
197
|
+
let client=null,team=null,rfis=[],reviews=[],highlights=[],activity=[];
|
|
198
|
+
try{client=engagement.client_id?await get('client',engagement.client_id):null}catch{}
|
|
199
|
+
try{team=engagement.team_id?await get('team',engagement.team_id):null}catch{}
|
|
200
|
+
try{rfis=await list('rfi',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'}})}catch{}
|
|
201
|
+
try{reviews=await list('review',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'}})}catch{}
|
|
202
|
+
try{const rids=new Set(reviews.map(r=>r.id));if(rids.size){const allHl=await list('highlight',{});highlights=allHl.filter(h=>rids.has(h.review_id))}}catch{}
|
|
203
|
+
try{activity=(await list('audit_logs',{entity_type:'engagement',entity_id:engId},{sort:{field:'created_at',dir:'DESC'}})).slice(0,20)}catch{}
|
|
187
204
|
const{renderFlexupReport}=await lazyRenderer('flexup-report-renderer.js');
|
|
188
205
|
return renderFlexupReport(user,engagement,client,rfis,reviews,highlights,activity,team);
|
|
189
206
|
}
|
|
@@ -192,7 +209,7 @@ export async function handlePage(pathname, req, res) {
|
|
|
192
209
|
if (segments.length === 1 && segments[0] === 'rfi') return handleRfiList(user);
|
|
193
210
|
if (segments.length === 1 && segments[0] === 'client') {
|
|
194
211
|
if (!canList(user, 'client')) return renderAccessDenied(user, 'client', 'list');
|
|
195
|
-
let clients = list('client', {});
|
|
212
|
+
let clients = await list('client', {});
|
|
196
213
|
if (isClientUser(user) && user.client_id) clients = clients.filter(c => c.id === user.client_id);
|
|
197
214
|
return renderClientList(user, clients);
|
|
198
215
|
}
|
|
@@ -201,7 +218,7 @@ export async function handlePage(pathname, req, res) {
|
|
|
201
218
|
const spec = getSpec(entityName); if (!spec) return null;
|
|
202
219
|
if (isClientUser(user) && !canClientAccessEntity(user, entityName)) return renderAccessDenied(user, entityName, 'list');
|
|
203
220
|
if (!canList(user, entityName)) return renderAccessDenied(user, entityName, 'list');
|
|
204
|
-
let items = list(entityName, {});
|
|
221
|
+
let items = await list(entityName, {});
|
|
205
222
|
if (isClientUser(user) && user.client_id) items = items.filter(item => { if (item.client_id) return item.client_id === user.client_id; if (item.assigned_to) return item.assigned_to === user.id; return true; });
|
|
206
223
|
const { renderEntityList: lazyEntityList } = await lazyRenderer('entity-renderer.js');
|
|
207
224
|
return lazyEntityList(entityName, resolveRefFields(items, spec), spec, user);
|
|
@@ -209,8 +226,8 @@ export async function handlePage(pathname, req, res) {
|
|
|
209
226
|
if (segments.length === 2 && segments[0] === 'client' && segments[1] !== 'new') {
|
|
210
227
|
if (!canView(user, 'client')) return renderAccessDenied(user, 'client', 'view');
|
|
211
228
|
if (isClientUser(user) && user.client_id && user.client_id !== segments[1]) return renderAccessDenied(user, 'client', 'view');
|
|
212
|
-
const client = get('client', segments[1]); if (!client) return null;
|
|
213
|
-
return renderClientDashboard(user, client, getClientDashboardStats(segments[1]));
|
|
229
|
+
const client = await get('client', segments[1]); if (!client) return null;
|
|
230
|
+
return renderClientDashboard(user, client, await getClientDashboardStats(segments[1]));
|
|
214
231
|
}
|
|
215
232
|
if (segments.length === 2) return handleGenericEntityView(user, segments[0], segments[1]);
|
|
216
233
|
if (segments.length === 3 && segments[2] === 'edit') return handleGenericEntityEdit(user, segments[0], segments[1]);
|