thatcher 1.0.57 → 1.0.58
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/package.json +1 -1
- package/src/ui/page-handler.js +22 -22
package/package.json
CHANGED
package/src/ui/page-handler.js
CHANGED
|
@@ -28,13 +28,13 @@ function reqUrl(req) {
|
|
|
28
28
|
|
|
29
29
|
async function handleEngagementDetail(user, engId) {
|
|
30
30
|
if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
|
|
31
|
-
const engagement = await get('engagement', engId);
|
|
31
|
+
const engagement = await get('engagement', engId, { user });
|
|
32
32
|
if (!engagement) return null;
|
|
33
33
|
let client = null; try { client = engagement.client_id ? await get('client', engagement.client_id) : null; } catch {}
|
|
34
|
-
let rfis = []; try { rfis = await list('rfi', { engagement_id: engId }); } catch {}
|
|
34
|
+
let rfis = []; try { rfis = await list('rfi', { engagement_id: engId }, { user }); } catch {}
|
|
35
35
|
let sections = [];
|
|
36
36
|
try {
|
|
37
|
-
sections = await list('rfi_section', { engagement_id: engId }, { sort: { field: 'sort_order', dir: 'ASC' } });
|
|
37
|
+
sections = await list('rfi_section', { engagement_id: engId }, { sort: { field: 'sort_order', dir: 'ASC' }, user });
|
|
38
38
|
} catch {}
|
|
39
39
|
let team = null; try { team = engagement.team_id ? await get('team', engagement.team_id) : null; } catch {}
|
|
40
40
|
let assignedUsers = [];
|
|
@@ -49,7 +49,7 @@ async function handleEngagementDetail(user, engId) {
|
|
|
49
49
|
}
|
|
50
50
|
async function handleEngagementList(user, req) {
|
|
51
51
|
if (!canList(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'list');
|
|
52
|
-
let engagements = await list('engagement', {});
|
|
52
|
+
let engagements = await list('engagement', {}, { user });
|
|
53
53
|
const clientMap = Object.fromEntries((await list('client', {})).map(c => [c.id, c.name]));
|
|
54
54
|
engagements = engagements.map(e => ({ ...e, client_name: clientMap[e.client_id] || e.client_name || '-' }));
|
|
55
55
|
const spec = getSpec('engagement'); if (spec) engagements = resolveRefFields(engagements, spec);
|
|
@@ -91,14 +91,14 @@ async function handleGenericEntityView(user, entityName, id) {
|
|
|
91
91
|
}
|
|
92
92
|
async function handleClientSubRoute(user, clientId, subRoute) {
|
|
93
93
|
if (!canView(user, 'client')) return renderAccessDenied(user, 'client', 'view');
|
|
94
|
-
const client = await get('client', clientId); if (!client) return null;
|
|
94
|
+
const client = await get('client', clientId, { user }); if (!client) return null;
|
|
95
95
|
if (isClientUser(user) && user.client_id && user.client_id !== clientId) return renderAccessDenied(user, 'client', 'view');
|
|
96
96
|
if (subRoute === 'dashboard' || subRoute === 'users') return renderClientDashboard(user, client, await getClientDashboardStats(clientId));
|
|
97
97
|
if (subRoute === 'progress') {
|
|
98
|
-
let engagements = []; try { engagements = (await list('engagement', {})).filter(e => e.client_id === clientId); } catch {}
|
|
98
|
+
let engagements = []; try { engagements = (await list('engagement', {}, { user })).filter(e => e.client_id === clientId); } catch {}
|
|
99
99
|
const spec = getSpec('engagement'); if (spec) engagements = resolveRefFields(engagements, spec);
|
|
100
100
|
let rfiStats = { total: 0, responded: 0, overdue: 0 };
|
|
101
|
-
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 {}
|
|
101
|
+
try { const allRfis = (await list('rfi', {}, { user })).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 {}
|
|
102
102
|
return renderClientProgress(user, client, engagements, rfiStats);
|
|
103
103
|
}
|
|
104
104
|
return null;
|
|
@@ -106,7 +106,7 @@ async function handleClientSubRoute(user, clientId, subRoute) {
|
|
|
106
106
|
async function handleGenericEntityEdit(user, entityName, id) {
|
|
107
107
|
const spec = getSpec(entityName); if (!spec) return null;
|
|
108
108
|
if (!canEdit(user, entityName)) return renderAccessDenied(user, entityName, 'edit');
|
|
109
|
-
const item = await get(entityName, id); if (!item) return null;
|
|
109
|
+
const item = await get(entityName, id, { user }); if (!item) return null;
|
|
110
110
|
if (item.team_id && user.team_id && item.team_id !== user.team_id && !isPartner(user)) return renderAccessDenied(user, entityName, 'edit');
|
|
111
111
|
const resolvedSpec = resolveEnumOptions(spec);
|
|
112
112
|
const { renderEntityForm: lazyEntityForm } = await lazyRenderer('entity-renderer.js');
|
|
@@ -131,7 +131,7 @@ export async function handlePage(pathname, req, res) {
|
|
|
131
131
|
const user = await getUser();
|
|
132
132
|
if (!user) { res.writeHead(302, { Location: '/login' }); res.end(); return REDIRECT; }
|
|
133
133
|
if (normalized === '/unauthorized') return renderAccessDenied(user, 'system', 'access');
|
|
134
|
-
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); }
|
|
134
|
+
if (normalized === '/notifications') { let notifs=[]; try{notifs=await list('notification',{user_id:user.id},{sort:{field:'created_at',dir:'DESC'},limit:100,user})}catch{} const{renderNotificationsPage}=await lazyRenderer('notifications-renderer.js'); return renderNotificationsPage(user,notifs); }
|
|
135
135
|
if (normalized === '/' || normalized === '/dashboard') return renderDashboard(user, await getDashboardStats(user));
|
|
136
136
|
if (normalized.startsWith('/admin/') || normalized === '/admin/jobs') return handleAdminPage(normalized, segments, user);
|
|
137
137
|
if (segments[0] === 'client' && segments.length === 3 && ['dashboard', 'users', 'progress'].includes(segments[2])) return handleClientSubRoute(user, segments[1], segments[2]);
|
|
@@ -141,7 +141,7 @@ export async function handlePage(pathname, req, res) {
|
|
|
141
141
|
let myReviews = [], sharedReviews = [], recentActivity = [];
|
|
142
142
|
// OR across two columns: busybase eq() is single-column, so fetch + filter in JS.
|
|
143
143
|
try {
|
|
144
|
-
const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } });
|
|
144
|
+
const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' }, user });
|
|
145
145
|
myReviews = reviews.filter(r => r.created_by === user.id || r.assigned_to === user.id).slice(0, 100);
|
|
146
146
|
} catch {}
|
|
147
147
|
// Old JOIN collaborator -> two-step: collaborator rows for this user, then their reviews.
|
|
@@ -149,12 +149,12 @@ export async function handlePage(pathname, req, res) {
|
|
|
149
149
|
const collabs = await list('collaborator', { user_id: user.id });
|
|
150
150
|
const ids = new Set(collabs.map(c => c.review_id));
|
|
151
151
|
if (ids.size) {
|
|
152
|
-
const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' } });
|
|
152
|
+
const reviews = await list('review', {}, { sort: { field: 'updated_at', dir: 'DESC' }, user });
|
|
153
153
|
sharedReviews = reviews.filter(r => ids.has(r.id)).slice(0, 100);
|
|
154
154
|
}
|
|
155
155
|
} catch {}
|
|
156
156
|
try {
|
|
157
|
-
recentActivity = (await list('audit_logs', { entity_type: 'review' }, { sort: { field: 'created_at', dir: 'DESC' } })).slice(0, 50);
|
|
157
|
+
recentActivity = (await list('audit_logs', { entity_type: 'review' }, { sort: { field: 'created_at', dir: 'DESC' }, user })).slice(0, 50);
|
|
158
158
|
} catch {}
|
|
159
159
|
const all = [...myReviews, ...sharedReviews];
|
|
160
160
|
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 };
|
|
@@ -179,11 +179,11 @@ export async function handlePage(pathname, req, res) {
|
|
|
179
179
|
let candidates = []; let reviewMap = {};
|
|
180
180
|
try {
|
|
181
181
|
// Old: highlights with a non-trivial comment (LIKE '%?' OR length>40), newest 50.
|
|
182
|
-
const all = await list('highlight', {}, { sort: { field: 'created_at', dir: 'DESC' } });
|
|
182
|
+
const all = await list('highlight', {}, { sort: { field: 'created_at', dir: 'DESC' }, user });
|
|
183
183
|
candidates = all.filter(c => c.comment && (c.comment.includes('?') || c.comment.length > 40)).slice(0, 50);
|
|
184
184
|
const revIds = [...new Set(candidates.map((c) => c.review_id))];
|
|
185
185
|
if (revIds.length) {
|
|
186
|
-
const reviews = await list('review', {});
|
|
186
|
+
const reviews = await list('review', {}, { user });
|
|
187
187
|
const byId = new Map(reviews.map(r => [r.id, r.name || '-']));
|
|
188
188
|
reviewMap = Object.fromEntries(revIds.map(id => [id, byId.get(id) || '-']));
|
|
189
189
|
}
|
|
@@ -194,19 +194,19 @@ export async function handlePage(pathname, req, res) {
|
|
|
194
194
|
if (segments.length === 2 && (segments[0] === 'engagements' || segments[0] === 'engagement') && segments[1] !== 'new') return handleEngagementDetail(user, segments[1]);
|
|
195
195
|
if (segments[0] === 'engagement' && segments.length === 3 && segments[2] === 'letter') {
|
|
196
196
|
if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
|
|
197
|
-
const engagement = await get('engagement', segments[1]); if (!engagement) return null;
|
|
197
|
+
const engagement = await get('engagement', segments[1], { user }); if (!engagement) return null;
|
|
198
198
|
return renderLetterWorkflow(user, engagement);
|
|
199
199
|
}
|
|
200
200
|
if (segments[0] === 'engagement' && segments.length === 3 && segments[2] === 'report') {
|
|
201
201
|
if (!canView(user, 'engagement')) return renderAccessDenied(user, 'engagement', 'view');
|
|
202
|
-
const engId = segments[1]; const engagement = await get('engagement', engId); if (!engagement) return null;
|
|
202
|
+
const engId = segments[1]; const engagement = await get('engagement', engId, { user }); if (!engagement) return null;
|
|
203
203
|
let client=null,team=null,rfis=[],reviews=[],highlights=[],activity=[];
|
|
204
204
|
try{client=engagement.client_id?await get('client',engagement.client_id):null}catch{}
|
|
205
205
|
try{team=engagement.team_id?await get('team',engagement.team_id):null}catch{}
|
|
206
|
-
try{rfis=await list('rfi',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'}})}catch{}
|
|
207
|
-
try{reviews=await list('review',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'}})}catch{}
|
|
208
|
-
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{}
|
|
209
|
-
try{activity=(await list('audit_logs',{entity_type:'engagement',entity_id:engId},{sort:{field:'created_at',dir:'DESC'}})).slice(0,20)}catch{}
|
|
206
|
+
try{rfis=await list('rfi',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'},user})}catch{}
|
|
207
|
+
try{reviews=await list('review',{engagement_id:engId},{sort:{field:'created_at',dir:'DESC'},user})}catch{}
|
|
208
|
+
try{const rids=new Set(reviews.map(r=>r.id));if(rids.size){const allHl=await list('highlight',{},{user});highlights=allHl.filter(h=>rids.has(h.review_id))}}catch{}
|
|
209
|
+
try{activity=(await list('audit_logs',{entity_type:'engagement',entity_id:engId},{sort:{field:'created_at',dir:'DESC'},user})).slice(0,20)}catch{}
|
|
210
210
|
const{renderFlexupReport}=await lazyRenderer('flexup-report-renderer.js');
|
|
211
211
|
return renderFlexupReport(user,engagement,client,rfis,reviews,highlights,activity,team);
|
|
212
212
|
}
|
|
@@ -215,7 +215,7 @@ export async function handlePage(pathname, req, res) {
|
|
|
215
215
|
if (segments.length === 1 && segments[0] === 'rfi') return handleRfiList(user);
|
|
216
216
|
if (segments.length === 1 && segments[0] === 'client') {
|
|
217
217
|
if (!canList(user, 'client')) return renderAccessDenied(user, 'client', 'list');
|
|
218
|
-
let clients = await list('client', {});
|
|
218
|
+
let clients = await list('client', {}, { user });
|
|
219
219
|
if (isClientUser(user) && user.client_id) clients = clients.filter(c => c.id === user.client_id);
|
|
220
220
|
return renderClientList(user, clients);
|
|
221
221
|
}
|
|
@@ -261,7 +261,7 @@ export async function handlePage(pathname, req, res) {
|
|
|
261
261
|
if (segments.length === 2 && segments[0] === 'client' && segments[1] !== 'new') {
|
|
262
262
|
if (!canView(user, 'client')) return renderAccessDenied(user, 'client', 'view');
|
|
263
263
|
if (isClientUser(user) && user.client_id && user.client_id !== segments[1]) return renderAccessDenied(user, 'client', 'view');
|
|
264
|
-
const client = await get('client', segments[1]); if (!client) return null;
|
|
264
|
+
const client = await get('client', segments[1], { user }); if (!client) return null;
|
|
265
265
|
return renderClientDashboard(user, client, await getClientDashboardStats(segments[1]));
|
|
266
266
|
}
|
|
267
267
|
if (segments.length === 2) return handleGenericEntityView(user, segments[0], segments[1]);
|