thatcher 1.0.41 → 1.0.43
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 +27 -0
- package/package.json +2 -5
- package/src/app/api/audit/dashboard/route.js +2 -2
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +2 -2
- package/src/app/api/audit/permissions/route.js +3 -3
- package/src/app/api/audit/permissions/stats/route.js +2 -2
- package/src/app/api/audit/route.js +2 -3
- package/src/app/api/audit/stats/route.js +2 -2
- package/src/app/api/auth/google/callback/route.js +1 -1
- package/src/app/api/auth/google/route.js +1 -1
- package/src/app/api/auth/login/route.js +1 -1
- package/src/app/api/auth/logout/route.js +1 -1
- package/src/app/api/auth/me/route.js +1 -1
- package/src/app/api/auth/mwr-bridge/route.js +1 -1
- package/src/app/api/auth/password-reset/route.js +1 -1
- package/src/app/api/csrf-token/route.js +1 -1
- package/src/app/api/debug/[[...path]]/route.js +2 -2
- package/src/app/api/debug/config/route.js +1 -1
- package/src/app/api/debug/hooks/route.js +1 -1
- package/src/app/api/debug/plugins/route.js +1 -1
- package/src/app/api/debug/sqlite/route.js +1 -1
- package/src/app/api/debug/sync/route.js +1 -1
- package/src/app/api/debug/workflow/route.js +1 -1
- package/src/app/api/domains/[domain]/route.js +1 -2
- package/src/app/api/domains/route.js +1 -1
- package/src/app/api/email/allocate/batch/route.js +1 -1
- package/src/app/api/email/allocate/route.js +1 -1
- package/src/app/api/email/receive/route.js +1 -1
- package/src/app/api/email/send/route.js +1 -1
- package/src/app/api/email/unallocated/route.js +1 -1
- package/src/app/api/files/[id]/route.js +1 -1
- package/src/app/api/health/route.js +1 -2
- package/src/app/api/metrics/route.js +1 -2
- package/src/engine.js +3 -3
- package/src/engine.server.js +3 -3
- package/src/index.js +35 -10
- package/src/lib/action-factory.js +1 -1
- package/src/lib/api-helpers.js +1 -1
- package/src/lib/api.js +3 -4
- package/src/lib/audit-logger-enhanced.js +1 -1
- package/src/lib/auth-middleware.js +1 -1
- package/src/lib/auth-route-helpers.js +1 -1
- package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
- package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
- package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
- package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
- package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
- package/src/lib/config-generator-engine.js +13 -1
- package/src/lib/crud-action-helpers.js +1 -1
- package/src/lib/crud-handlers.js +4 -4
- package/src/lib/email-sender.js +1 -1
- package/src/lib/errors/index.js +3 -0
- package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
- package/src/lib/{error-handler.js → errors/types.js} +1 -1
- package/src/lib/errors/wrap.js +225 -0
- package/src/lib/events-engine.js +1 -1
- package/src/lib/hot-reload/index.js +34 -0
- package/src/lib/index.js +5 -5
- package/src/lib/keyed-cache.js +112 -0
- package/src/lib/monitor.js +285 -0
- package/src/lib/next-shim.js +294 -0
- package/src/lib/observability-init.js +213 -0
- package/src/lib/perf.js +468 -0
- package/src/lib/query-cache.js +40 -46
- package/src/lib/realtime-server.js +18 -0
- package/src/lib/render-cache.js +14 -28
- package/src/lib/{request-tracing.js → request-trace.js} +57 -2
- package/src/lib/response-formatter.js +18 -0
- package/src/lib/route-helpers.js +1 -1
- package/src/lib/state-protocol.js +13 -0
- package/src/lib/state-transport-client.js +6 -0
- package/src/lib/state-transport-reconnect.js +5 -0
- package/src/lib/state-transport-server.js +8 -0
- package/src/lib/utils.js +1 -1
- package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
- package/src/lib/validation/index.js +1 -1
- package/src/lib/validation/security-validators.js +1 -1
- package/src/lib/validation-middleware.js +2 -2
- package/src/lib/workflow-engine.js +23 -3
- package/src/lib/xstate-workflow-engine.js +18 -2
- package/src/services/collaborator-role.service.js +2 -2
- package/src/services/notification-engine.js +5 -5
- package/src/services/permission.service.js +1 -1
- package/src/ui/format-helpers.js +1 -4
- package/src/ui/highlight-threading-renderer.js +1 -1
- package/src/ui/page-handler-admin.js +6 -5
- package/src/ui/page-handler-helpers.js +2 -2
- package/src/ui/page-handler-reviews.js +4 -4
- package/src/ui/page-handler-rfi.js +1 -1
- package/src/ui/page-handler.js +2 -2
- package/src/ui/render-helpers.js +1 -1
- package/src/ui/renderer.js +1 -1
- package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
- package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
- package/src/ui/review/index.js +32 -0
- package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
- package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
- package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
- package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
- package/src/ui/settings/shared.js +45 -0
- package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
- package/src/ui/settings/templates.js +116 -0
- package/src/lib/api-error-wrapper.js +0 -125
- package/src/lib/db-monitor.js +0 -127
- package/src/lib/error-resilience.js +0 -132
- package/src/lib/monitoring-init.js +0 -67
- package/src/lib/next-compat.js +0 -94
- package/src/lib/next-polyfills.js +0 -135
- package/src/lib/observability-bootstrap.js +0 -116
- package/src/lib/perf-monitor.js +0 -94
- package/src/lib/perf-profiler.js +0 -233
- package/src/lib/query-perf.js +0 -117
- package/src/lib/request-tracker.js +0 -43
- package/src/lib/resource-monitor.js +0 -120
- package/src/lib/validators.js +0 -67
- package/src/lib/with-error-handler.js +0 -31
- package/src/ui/settings-renderer-advanced2.js +0 -159
- /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
- /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
- /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
- /package/src/ui/{review-zone-nav.js → review/zone-nav.js} +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { list, get } from '@/lib/busybase
|
|
1
|
+
import { list, get } from '@/lib/busybase/store.js';
|
|
2
2
|
import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml, REDIRECT } from '@/ui/renderer.js';
|
|
3
|
-
import { renderSettingsHome, renderSettingsSystem, renderSettingsUsers
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { renderSettingsHome, renderSettingsSystem, renderSettingsUsers } from '@/ui/settings/home.js';
|
|
4
|
+
import { renderSettingsTeams, renderSettingsRfiSections } from '@/ui/settings/teams.js';
|
|
5
|
+
import { renderSettingsTemplates, renderSettingsChecklists, renderSettingsEntityTypes, renderSettingsEngagementTypes, renderSettingsTemplateManage } from '@/ui/settings/templates.js';
|
|
6
|
+
import { renderSettingsNotifications, renderSettingsIntegrations, renderSettingsRecreation, renderSettingsReviewSettings, renderSettingsFileReview, renderSettingsMwrPermissions } from '@/ui/settings/review.js';
|
|
6
7
|
import { renderChecklistsManagement } from '@/ui/checklist-renderer.js';
|
|
7
8
|
import { renderJobManagement } from '@/ui/job-management-renderer.js';
|
|
8
9
|
import { isPartner, isManager } from '@/ui/permissions-ui.js';
|
|
@@ -36,7 +37,7 @@ export async function handleAdminPage(normalized, segments, user) {
|
|
|
36
37
|
if (normalized === '/admin/settings/system') { const config = await getSystemConfig(); return renderSettingsSystem(user, config); }
|
|
37
38
|
if (normalized === '/admin/settings/users') { return renderSettingsUsers(user, await list('user', {})); }
|
|
38
39
|
if (normalized === '/admin/settings/teams') {
|
|
39
|
-
const { renderSettingsTeams: _rt } = await lazyRenderer('settings
|
|
40
|
+
const { renderSettingsTeams: _rt } = await lazyRenderer('settings/teams.js');
|
|
40
41
|
return _rt(user, await list('team', {}), await list('user', {}));
|
|
41
42
|
}
|
|
42
43
|
if (normalized === '/admin/settings/rfi-sections') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { list, get, count } from '@/lib/busybase
|
|
1
|
+
import { list, get, count } from '@/lib/busybase/store.js';
|
|
2
2
|
import { createLogger } from '@/lib/logger.js';
|
|
3
3
|
|
|
4
4
|
const log = createLogger('[PageHandlerHelpers]');
|
|
@@ -26,7 +26,7 @@ export async function getRefOptions(spec) {
|
|
|
26
26
|
return refOptions;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// busybase
|
|
29
|
+
// busybase/store's list()/get() already attach `<field>_display` for ref fields (the
|
|
30
30
|
// old LEFT JOIN equivalent), so this is now a sync pass-through: it just guarantees a
|
|
31
31
|
// `_display` key exists (falling back to the raw id) without issuing any query.
|
|
32
32
|
export function resolveRefFields(items, spec) {
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { list, get } from '@/lib/busybase
|
|
1
|
+
import { list, get } from '@/lib/busybase/store.js';
|
|
2
2
|
import { createLogger } from '@/lib/logger.js';
|
|
3
3
|
import { getSpec } from '@/config/spec-helpers.js';
|
|
4
4
|
|
|
5
5
|
const log = createLogger('[PageHandlerReviews]');
|
|
6
6
|
import { renderEntityList, renderAccessDenied } from '@/ui/renderer.js';
|
|
7
|
-
import { renderSectionReport, renderReviewListTabbed } from '@/ui/review
|
|
7
|
+
import { renderSectionReport, renderReviewListTabbed } from '@/ui/review/index.js';
|
|
8
8
|
import { renderChecklistDetails, renderChecklistsHome } from '@/ui/checklist-renderer.js';
|
|
9
9
|
import { renderPdfViewer, renderPdfEditorPlaceholder } from '@/ui/pdf-viewer-renderer.js';
|
|
10
10
|
import { renderReviewAnalytics } from '@/ui/review-analytics-renderer.js';
|
|
11
11
|
import { renderHighlightThreading } from '@/ui/highlight-threading-renderer.js';
|
|
12
12
|
import { renderSectionResolution } from '@/ui/section-resolution-renderer.js';
|
|
13
|
-
import { renderReviewComparison, renderComparisonPicker } from '@/ui/review
|
|
13
|
+
import { renderReviewComparison, renderComparisonPicker } from '@/ui/review/index.js';
|
|
14
14
|
import { renderTenderDashboard } from '@/ui/tender-dashboard-renderer.js';
|
|
15
15
|
import { renderBatchOperations } from '@/ui/batch-review-renderer.js';
|
|
16
16
|
import { canList, canView, canEdit } from '@/ui/permissions-ui.js';
|
|
@@ -168,7 +168,7 @@ export async function handleReviewRoutes(normalized, segments, user, req) {
|
|
|
168
168
|
} catch {}
|
|
169
169
|
try { sections = (await list('review_section', {})).filter(s => s.review_id === reviewId); } catch {}
|
|
170
170
|
try { tenders = (await list('tender', {})).filter(t => t.review_id === reviewId); } catch {}
|
|
171
|
-
const { renderReviewDetail } = await lazyRenderer('review
|
|
171
|
+
const { renderReviewDetail } = await lazyRenderer('review/detail-renderer.js');
|
|
172
172
|
return renderReviewDetail(user, review, highlights, collaborators, checklists, sections, tenders);
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { list, get } from '@/lib/busybase
|
|
1
|
+
import { list, get } from '@/lib/busybase/store.js';
|
|
2
2
|
import { canView, canList, isClerk, isPartner } from '@/ui/permissions-ui.js';
|
|
3
3
|
import { renderAccessDenied } from '@/ui/renderer.js';
|
|
4
4
|
import { renderRfiList } from '@/ui/rfi-list-renderer.js';
|
package/src/ui/page-handler.js
CHANGED
|
@@ -1,7 +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/busybase
|
|
4
|
+
import { list, get } from '@/lib/busybase/store.js';
|
|
5
5
|
import { renderLogin, renderDashboard, renderEntityList, renderEntityDetail, renderEntityForm, renderAccessDenied, renderPasswordReset, renderPasswordResetConfirm, REDIRECT } from '@/ui/renderer.js';
|
|
6
6
|
import { renderClientDashboard, renderClientList } from '@/ui/client-renderer.js';
|
|
7
7
|
import { canList, canView, canCreate, canEdit, isPartner, isClerk, isClientUser, canClientAccessEntity } from '@/ui/permissions-ui.js';
|
|
@@ -152,7 +152,7 @@ export async function handlePage(pathname, req, res) {
|
|
|
152
152
|
} catch {}
|
|
153
153
|
const all = [...myReviews, ...sharedReviews];
|
|
154
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 };
|
|
155
|
-
const { renderMwrHome } = await lazyRenderer('review
|
|
155
|
+
const { renderMwrHome } = await lazyRenderer('review/mwr.js');
|
|
156
156
|
return renderMwrHome(user, stats);
|
|
157
157
|
}
|
|
158
158
|
const reviewResult = await handleReviewRoutes(normalized, segments, user, req);
|
package/src/ui/render-helpers.js
CHANGED
|
@@ -51,7 +51,7 @@ export const TOAST_SCRIPT = `window.showToast=(m,t='info')=>{let c=document.getE
|
|
|
51
51
|
export const TABLE_SCRIPT = `(function(){let sortCol=null,sortDir=1;function filterTable(){const search=(document.getElementById('search-input')?.value||'').toLowerCase();const filters={};document.querySelectorAll('[data-filter]').forEach(el=>{if(el.value)filters[el.dataset.filter]=el.value.toLowerCase()});let shown=0,total=0;document.querySelectorAll('tbody tr[data-row]').forEach(row=>{total++;const text=row.textContent.toLowerCase();const matchSearch=!search||text.includes(search);const matchFilters=Object.entries(filters).every(([key,val])=>{const cell=row.querySelector('[data-col="'+key+'"]');return !cell||cell.textContent.toLowerCase().includes(val)});const visible=matchSearch&&matchFilters;row.style.display=visible?'':'none';if(visible)shown++});const counter=document.getElementById('row-count');if(counter)counter.textContent=shown===total?total+' items':shown+' of '+total+' items'}function sortTable(col){if(sortCol===col)sortDir*=-1;else{sortCol=col;sortDir=1;}document.querySelectorAll('th[data-sort]').forEach(th=>{th.classList.remove('sort-asc','sort-desc');if(th.dataset.sort===col){th.classList.add(sortDir===1?'sort-asc':'sort-desc');th.setAttribute('aria-sort',sortDir===1?'ascending':'descending');th.setAttribute('aria-label','Sort by '+(th.textContent||th.dataset.sort).trim()+', '+(sortDir===1?'ascending':'descending'))}else{th.removeAttribute('aria-sort')}});const tbody=document.querySelector('tbody');if(!tbody)return;const rows=Array.from(tbody.querySelectorAll('tr[data-row]'));rows.sort((a,b)=>{const av=a.querySelector('[data-col="'+col+'"]')?.textContent?.trim()||'';const bv=b.querySelector('[data-col="'+col+'"]')?.textContent?.trim()||'';return av.localeCompare(bv,undefined,{numeric:true})*sortDir});rows.forEach(r=>tbody.appendChild(r))}window.filterTable=filterTable;window.sortTable=sortTable;const style=document.createElement('style');style.textContent='th[data-sort]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}';document.head.appendChild(style);document.addEventListener('DOMContentLoaded',()=>{document.getElementById('search-input')?.addEventListener('input',filterTable);document.querySelectorAll('[data-filter]').forEach(el=>el.addEventListener('change',filterTable));document.querySelectorAll('th[data-sort]').forEach(th=>{th.setAttribute('tabindex','0');th.setAttribute('role','button');if(!th.getAttribute('aria-label'))th.setAttribute('aria-label','Sort by '+(th.textContent||th.dataset.sort).trim());th.addEventListener('click',e=>{e.preventDefault();sortTable(th.dataset.sort)});th.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '||e.key==='Spacebar'){e.preventDefault();sortTable(th.dataset.sort)}});th.addEventListener('keypress',e=>{if(e.key==='Enter'){e.preventDefault();sortTable(th.dataset.sort)}})})})})();`
|
|
52
52
|
|
|
53
53
|
export function esc(s) {
|
|
54
|
-
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')
|
|
54
|
+
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Canonical empty-row for a <tbody>. One styling for every "no rows" message so
|
package/src/ui/renderer.js
CHANGED
|
@@ -27,7 +27,7 @@ export {
|
|
|
27
27
|
export {
|
|
28
28
|
mobileReviewCard, sidebarReviewDetails, archiveReviewDialog,
|
|
29
29
|
reviewOpenCloseToggle, reviewPrivateToggle, markAllHighlightsResolved
|
|
30
|
-
} from '@/ui/review
|
|
30
|
+
} from '@/ui/review/widgets.js'
|
|
31
31
|
|
|
32
32
|
export {
|
|
33
33
|
colorPickerDialog, dateChoiceDialog, stageTransitionDialog,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statusLabel } from '@/ui/renderer.js';
|
|
2
2
|
import { page } from '@/ui/layout.js';
|
|
3
|
-
import { emptyRow, esc } from '
|
|
3
|
+
import { emptyRow, esc } from '../render-helpers.js';
|
|
4
4
|
|
|
5
5
|
function reviewSummaryPanel(review, highlights, side) {
|
|
6
6
|
const total = highlights.length;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js';
|
|
2
|
-
import { reviewZoneNav } from '@/ui/review
|
|
2
|
+
import { reviewZoneNav } from '@/ui/review/zone-nav.js';
|
|
3
3
|
export { reviewZoneNav };
|
|
4
4
|
import { canEdit } from '@/ui/permissions-ui.js';
|
|
5
5
|
import { esc, statusBadge, TOAST_SCRIPT, icon } from '@/ui/render-helpers.js';
|
|
6
|
-
import { reviewDetailScript } from '@/ui/review
|
|
7
|
-
import { highlightRow, collaboratorRow, addCollaboratorDialog } from '@/ui/review
|
|
6
|
+
import { reviewDetailScript } from '@/ui/review/detail-script.js';
|
|
7
|
+
import { highlightRow, collaboratorRow, addCollaboratorDialog } from '@/ui/review/detail-panels.js';
|
|
8
8
|
import { tenderPanelHtml, tenderDialog, linksPanelHtml, linkDialog, sectionsPanelHtml, compareDialogHtml } from '@/ui/review-detail-panels-extra.js';
|
|
9
9
|
import { retryHighlightDialog, switchTemplateButton } from '@/ui/engagement-dialogs.js';
|
|
10
10
|
import { reviewTemplateChoiceDialog } from '@/ui/review-dialogs.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public surface of the review/* renderer group -- re-exports every symbol an
|
|
3
|
+
* external caller (page-handler-reviews.js, page-handler.js, renderer.js,
|
|
4
|
+
* highlight-threading-renderer.js) actually imports from this group today.
|
|
5
|
+
* Individual modules can still be imported directly by path (and the
|
|
6
|
+
* lazyRenderer() dynamic-import call sites do exactly that, by design --
|
|
7
|
+
* see page-handler-reviews.js/page-handler.js), this barrel is for the
|
|
8
|
+
* static-import call sites.
|
|
9
|
+
*/
|
|
10
|
+
export {
|
|
11
|
+
reviewCreateDialog, reviewTemplateChoiceDialog, reviewContextMenu, reviewFlagsDialog,
|
|
12
|
+
reviewTagsDialog, reviewValueDialog, reviewDeadlineDialog, reviewNotificationDialog,
|
|
13
|
+
renderReviewListTabbed, reviewSearchField, hideEmptyReviewsToggle, reviewGroupedList,
|
|
14
|
+
renderReviewSections,
|
|
15
|
+
} from './renderer.js';
|
|
16
|
+
|
|
17
|
+
export { renderMwrHome, renderSectionReport } from './mwr.js';
|
|
18
|
+
|
|
19
|
+
export { renderReviewDetail } from './detail-renderer.js';
|
|
20
|
+
|
|
21
|
+
export { highlightRow, collaboratorRow, addCollaboratorDialog } from './detail-panels.js';
|
|
22
|
+
|
|
23
|
+
export { reviewDetailScript } from './detail-script.js';
|
|
24
|
+
|
|
25
|
+
export { renderReviewComparison, renderComparisonPicker } from './comparison.js';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
mobileReviewCard, sidebarReviewDetails, archiveReviewDialog,
|
|
29
|
+
reviewOpenCloseToggle, reviewPrivateToggle, markAllHighlightsResolved,
|
|
30
|
+
} from './widgets.js';
|
|
31
|
+
|
|
32
|
+
export { reviewZoneNav } from './zone-nav.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js';
|
|
2
|
-
import { reviewZoneNav } from '@/ui/review
|
|
2
|
+
import { reviewZoneNav } from '@/ui/review/zone-nav.js';
|
|
3
3
|
import { SPACING, renderCard, renderButton, renderProgress, renderEmptyState, renderStatsRow, renderPageHeader } from '@/ui/spacing-system.js';
|
|
4
4
|
import { icon, esc } from '@/ui/format-helpers.js';
|
|
5
5
|
import { TOAST_SCRIPT } from '@/ui/render-helpers.js';
|
|
@@ -5,7 +5,7 @@ import { reviewCreateDialog, reviewTemplateChoiceDialog, reviewContextMenu, revi
|
|
|
5
5
|
import { SPACING, renderButton, renderEmptyState, renderStatsRow, renderPageHeader } from '@/ui/spacing-system.js';
|
|
6
6
|
|
|
7
7
|
export { reviewCreateDialog, reviewTemplateChoiceDialog, reviewContextMenu, reviewFlagsDialog, reviewTagsDialog, reviewValueDialog, reviewDeadlineDialog, reviewNotificationDialog };
|
|
8
|
-
export { renderMwrHome, renderSectionReport } from '@/ui/review
|
|
8
|
+
export { renderMwrHome, renderSectionReport } from '@/ui/review/mwr.js';
|
|
9
9
|
|
|
10
10
|
const TOAST_SCRIPT = `window.showToast=(m,t='info')=>{let c=document.getElementById('toast-container');if(!c){c=document.createElement('div');c.id='toast-container';c.className='toast-container';c.setAttribute('role','status');c.setAttribute('aria-live','polite');c.setAttribute('aria-atomic','true');document.body.appendChild(c)}const d=document.createElement('div');d.className='toast toast-'+t;d.textContent=m;c.appendChild(d);setTimeout(()=>{d.style.opacity='0';setTimeout(()=>d.remove(),300)},3000)};`;
|
|
11
11
|
|
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export function settingsPage(user, title, bc, content, scripts = []) {
|
|
9
|
-
return page(user, title, bc, content, scripts);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function settingsBack() {
|
|
13
|
-
return `<a href="/admin/settings" class="btn btn-ghost btn-sm gap-1 mb-4">Back to Settings</a>`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function inlineTable(headers, rows, emptyMsg) {
|
|
17
|
-
const ths = headers.map(h => `<th>${h}</th>`).join('');
|
|
18
|
-
const empty = `<tr><td colspan="${headers.length}" class="text-center py-8 text-base-content/40 text-sm">${emptyMsg}</td></tr>`;
|
|
19
|
-
return `<div class="table-wrap"><table class="data-table"><thead><tr>${ths}</tr></thead><tbody>${rows || empty}</tbody></table></div>`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const KNOWN_ROLE_LABELS = { admin:'Admin', partner:'Partner', manager:'Manager', clerk:'Clerk', user:'User', auditor:'Auditor', client_admin:'Client Admin', client_user:'Client User' };
|
|
23
|
-
|
|
24
|
-
export function roleBadge(role) {
|
|
25
|
-
const r = (role || '').toLowerCase();
|
|
26
|
-
const pillMap = { admin:'pill pill-danger', partner:'pill pill-info', manager:'pill pill-success', clerk:'pill pill-warning', user:'pill pill-neutral', auditor:'pill pill-neutral', client_admin:'pill pill-info', client_user:'pill pill-neutral' };
|
|
27
|
-
const cls = pillMap[r] || 'pill pill-neutral';
|
|
28
|
-
const label = KNOWN_ROLE_LABELS[r] || (r.length > 20 ? 'Staff' : (r.charAt(0).toUpperCase() + r.slice(1))) || 'Staff';
|
|
29
|
-
return `<span class="${cls}">${label}</span>`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function statusBadge(status) { return _statusBadge(status); }
|
|
1
|
+
/*
|
|
2
|
+
* Settings landing page + system info + users list. Ported unchanged (function
|
|
3
|
+
* bodies verbatim) from the original settings-renderer.js, which also carried
|
|
4
|
+
* the shared page-shell helpers (now settings/shared.js) and re-exported teams/
|
|
5
|
+
* rfi-sections (now settings/teams.js, imported directly by callers instead).
|
|
6
|
+
*/
|
|
7
|
+
import { settingsPage, settingsBack, icon, SPACING, esc, TABLE_SCRIPT, roleBadge, statusBadge } from '@/ui/settings/shared.js';
|
|
33
8
|
|
|
34
9
|
const SETTINGS_CARDS = [
|
|
35
10
|
{ key: 'system', icon: 'gear', title: 'System Info', desc: 'Database, server, cache configuration', href: '/admin/settings/system' },
|
|
@@ -163,5 +138,3 @@ export function renderSettingsUsers(user, users = []) {
|
|
|
163
138
|
</div>`;
|
|
164
139
|
return settingsPage(user, 'Users - Settings', [{ href: '/', label: 'Dashboard' }, { href: '/admin/settings', label: 'Settings' }, { label: 'Users' }], content, [TABLE_SCRIPT]);
|
|
165
140
|
}
|
|
166
|
-
|
|
167
|
-
export { renderSettingsTeams, renderSettingsRfiSections } from '@/ui/settings-renderer-teams.js';
|
|
@@ -1,33 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/*
|
|
2
|
+
* Behaviour/config settings pages: audit-log (recreation) view, integrations,
|
|
3
|
+
* notifications, review settings, file-review settings, and MWR permissions.
|
|
4
|
+
* Ported unchanged from settings-renderer-advanced.js (renderSettingsRecreation,
|
|
5
|
+
* renderSettingsIntegrations, renderSettingsNotifications) and
|
|
6
|
+
* settings-renderer-advanced2.js (renderSettingsReviewSettings,
|
|
7
|
+
* renderSettingsFileReview, renderSettingsMwrPermissions).
|
|
8
|
+
*/
|
|
9
|
+
import { TOAST_SCRIPT, settingsPage, settingsBack, inlineTable, bc, esc, icon } from '@/ui/settings/shared.js';
|
|
3
10
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function renderSettingsTemplates(user, templates = []) {
|
|
13
|
-
const rows = templates.map(t => `<tr ${trClick('/review_template/'+t.id)}>
|
|
14
|
-
<td class="text-sm font-medium">${esc(t.name||'-')}</td>
|
|
15
|
-
<td><span class="badge badge-flat-primary text-xs">${esc(t.type||'standard')}</span></td>
|
|
16
|
-
<td>${t.is_active ? '<span class="badge badge-success badge-flat-success text-xs">Active</span>' : '<span class="badge badge-flat-secondary text-xs">Inactive</span>'}</td>
|
|
17
|
-
<td>${editBtn('/review_template/'+t.id+'/edit')}</td>
|
|
18
|
-
</tr>`).join('');
|
|
19
|
-
return settingsPage(user, 'Templates - Settings', bc('Templates'), hdr('Templates', '/review_template/new', '+ Add Template') + inlineTable(['Name', 'Type', 'Status', 'Actions'], rows, 'No templates found'));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function renderSettingsChecklists(user, checklists = []) {
|
|
23
|
-
const rows = checklists.map(c => `<tr ${trClick('/checklist/'+c.id)}>
|
|
24
|
-
<td class="text-sm font-medium">${esc(c.name||'-')}</td>
|
|
25
|
-
<td class="text-sm">${esc(c.type||'-')}</td>
|
|
26
|
-
<td class="text-sm text-base-content/70">${esc(c.review_id||'-')}</td>
|
|
27
|
-
<td>${editBtn('/checklist/'+c.id+'/edit')}</td>
|
|
28
|
-
</tr>`).join('');
|
|
29
|
-
return settingsPage(user, 'Checklists - Settings', bc('Checklists'), hdr('Checklists', '/checklist/new', '+ Add Checklist') + inlineTable(['Name', 'Type', 'Review', 'Actions'], rows, 'No checklists found'));
|
|
30
|
-
}
|
|
11
|
+
// Module-level togRow (base-content/80) backs renderSettingsReviewSettings and
|
|
12
|
+
// renderSettingsFileReview, ported from settings-renderer-advanced2.js's
|
|
13
|
+
// module-level togRow. renderSettingsNotifications below defines its OWN
|
|
14
|
+
// togRowLocal (base-content/70) -- the two were never actually the same
|
|
15
|
+
// helper in the original files (advanced.js's was function-scoped inside
|
|
16
|
+
// renderSettingsNotifications with a different opacity class), so they stay
|
|
17
|
+
// separate here rather than being incorrectly merged into one shared const.
|
|
18
|
+
const togRow = tg => `<div class="flex justify-between items-center py-3 border-b border-base-200"><div><div class="text-sm font-semibold">${tg.label}</div><div class="text-xs text-base-content/80">${tg.desc}</div></div><input type="checkbox" name="${tg.id}" ${tg.checked ? 'checked' : ''} class="checkbox checkbox-primary"/></div>`;
|
|
31
19
|
|
|
32
20
|
export function renderSettingsRecreation(user, _logs = [], _users = []) {
|
|
33
21
|
const filters = `<div class="card-clean" style="margin-bottom:1rem"><div class="card-clean-body">
|
|
@@ -115,7 +103,7 @@ export function renderSettingsNotifications(user, config = {}) {
|
|
|
115
103
|
const t = config.thresholds || {};
|
|
116
104
|
const rfi = t.rfi || {};
|
|
117
105
|
const notif = t.notification || {};
|
|
118
|
-
const
|
|
106
|
+
const togRowLocal = tg => `<div class="flex justify-between items-center py-3 border-b border-base-200"><div><div class="text-sm font-semibold">${tg.label}</div><div class="text-xs text-base-content/70">${tg.desc}</div></div><input type="checkbox" name="${tg.id}" ${tg.checked ? 'checked' : ''} class="checkbox checkbox-primary"/></div>`;
|
|
119
107
|
const toggles = [
|
|
120
108
|
{ id: 'rfi_reminders', label: 'RFI Reminders', desc: 'Send reminders for outstanding RFIs', checked: true },
|
|
121
109
|
{ id: 'deadline_alerts', label: 'Deadline Alerts', desc: 'Alert when deadlines are approaching', checked: true },
|
|
@@ -137,7 +125,7 @@ export function renderSettingsNotifications(user, config = {}) {
|
|
|
137
125
|
</div>`;
|
|
138
126
|
const content = `${settingsBack()}<h1 class="text-2xl font-bold mb-6">Notifications</h1>
|
|
139
127
|
<form id="notif-form"><div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
140
|
-
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-2">Notification Toggles</h2>${toggles.map(
|
|
128
|
+
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-2">Notification Toggles</h2>${toggles.map(togRowLocal).join('')}</div></div>
|
|
141
129
|
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-4">Configuration</h2>
|
|
142
130
|
<div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">RFI Notification Days</span></label><input type="text" name="notification_days" class="input input-solid max-w-full" value="${(rfi.notification_days || [7,3,1,0]).join(', ')}"/></div>
|
|
143
131
|
<div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">Escalation Delay (hours)</span></label><input type="number" name="escalation_delay_hours" class="input input-solid max-w-full" value="${rfi.escalation_delay_hours || 24}"/></div>
|
|
@@ -166,33 +154,106 @@ loadTriggers();`;
|
|
|
166
154
|
return settingsPage(user, 'Notifications - Settings', bc('Notifications'), content, [script]);
|
|
167
155
|
}
|
|
168
156
|
|
|
169
|
-
function
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
return settingsPage(user,
|
|
157
|
+
export function renderSettingsReviewSettings(user, config = {}) {
|
|
158
|
+
const review = config.review || {};
|
|
159
|
+
const toggles = [
|
|
160
|
+
{ id: 'auto_save', label: 'Auto-save', desc: 'Automatically save review changes', checked: review.auto_save !== false },
|
|
161
|
+
{ id: 'highlight_notifications', label: 'Highlight Notifications', desc: 'Notify on new highlights', checked: review.highlight_notifications !== false },
|
|
162
|
+
{ id: 'require_resolution', label: 'Require Resolution', desc: 'All highlights resolved before closing', checked: !!review.require_resolution },
|
|
163
|
+
{ id: 'allow_private', label: 'Allow Private Reviews', desc: 'Enable private review visibility', checked: review.allow_private !== false },
|
|
164
|
+
{ id: 'enable_sections', label: 'Enable Sections', desc: 'Allow reviews organized into sections', checked: review.enable_sections !== false },
|
|
165
|
+
{ id: 'enable_wip_value', label: 'Enable WIP Value', desc: 'Track work-in-progress value', checked: !!review.enable_wip_value },
|
|
166
|
+
];
|
|
167
|
+
const content = `${settingsBack()}<h1 class="text-2xl font-bold mb-6">Review Settings</h1>
|
|
168
|
+
<form id="review-settings-form"><div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
169
|
+
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-2">Review Options</h2>${toggles.map(togRow).join('')}</div></div>
|
|
170
|
+
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-4">Defaults</h2>
|
|
171
|
+
<div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">Default Status</span></label><select name="default_status" class="select select-solid max-w-full"><option value="active" ${review.default_status==='active'?'selected':''}>Active</option><option value="draft" ${review.default_status==='draft'?'selected':''}>Draft</option></select></div>
|
|
172
|
+
<div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">Max Highlights Per Review</span></label><input type="number" name="max_highlights" class="input input-solid max-w-full" value="${review.max_highlights || 500}" min="1"/></div>
|
|
173
|
+
<div class="form-group"><label class="label"><span class="label-text font-semibold">Default Currency</span></label><select name="default_currency" class="select select-solid max-w-full"><option value="ZAR" ${!review.default_currency||review.default_currency==='ZAR'?'selected':''}>ZAR</option><option value="USD" ${review.default_currency==='USD'?'selected':''}>USD</option><option value="EUR" ${review.default_currency==='EUR'?'selected':''}>EUR</option><option value="GBP" ${review.default_currency==='GBP'?'selected':''}>GBP</option></select></div>
|
|
174
|
+
</div></div>
|
|
175
|
+
</div><button type="submit" class="btn btn-primary">Save Review Settings</button></form>`;
|
|
176
|
+
const script = `${TOAST_SCRIPT}document.getElementById('review-settings-form').addEventListener('submit',async(e)=>{e.preventDefault();const fd=new FormData(e.target);const data={};for(const[k,v]of fd.entries())data[k]=v;document.querySelectorAll('#review-settings-form input[type=checkbox]').forEach(cb=>{data[cb.name]=cb.checked});document.querySelectorAll('#review-settings-form input[type=number]').forEach(n=>{if(data[n.name])data[n.name]=Number(data[n.name])});try{const res=await fetch('/api/admin/settings/review',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});if(res.ok){showToast('Settings saved','success')}else{showToast('Save failed','error')}}catch(e){showToast('Error: '+e.message,'error')}})`;
|
|
177
|
+
return settingsPage(user, 'Review Settings', bc('Review Settings'), content, [script]);
|
|
190
178
|
}
|
|
191
179
|
|
|
192
|
-
export function
|
|
193
|
-
|
|
180
|
+
export function renderSettingsFileReview(user, config = {}, frSettings = {}) {
|
|
181
|
+
const fr = config.fileReview || {};
|
|
182
|
+
const reviewFlags = frSettings.review_flags || [];
|
|
183
|
+
const tenderFlags = frSettings.tender_flags || [];
|
|
184
|
+
const flagManagers = frSettings.flags_managers || [];
|
|
185
|
+
const tempAccess = frSettings.temp_review_access_period || 0;
|
|
186
|
+
const toggles = [
|
|
187
|
+
{ id: 'auto_pdf_cache', label: 'Auto-cache PDFs', desc: 'Cache PDF files for faster loading', checked: fr.auto_pdf_cache !== false },
|
|
188
|
+
{ id: 'allow_annotations', label: 'Allow Annotations', desc: 'Enable PDF annotation tools', checked: fr.allow_annotations !== false },
|
|
189
|
+
{ id: 'mobile_resize', label: 'Mobile Resize', desc: 'Enable mobile-friendly resizable highlights', checked: fr.mobile_resize !== false },
|
|
190
|
+
{ id: 'coordinate_snap', label: 'Coordinate Snap', desc: 'Snap highlight coordinates to text boundaries', checked: !!fr.coordinate_snap },
|
|
191
|
+
];
|
|
192
|
+
const flagChips = (flags, type) => flags.map(f => `<span class="pill pill-neutral" style="display:inline-flex;align-items:center;gap:4px">${esc(f)}<button type="button" data-action="removeFlag" data-args='${esc(JSON.stringify([type, f]))}' aria-label="Remove flag" style="background:none;border:none;cursor:pointer;line-height:1;color:inherit;display:inline-flex">${icon('close',16)}</button></span>`).join('');
|
|
193
|
+
const content = `${settingsBack()}<h1 class="text-2xl font-bold mb-6">File Review Settings</h1>
|
|
194
|
+
<div id="fr-review-flags" data-flags='${JSON.stringify(reviewFlags)}'></div>
|
|
195
|
+
<div id="fr-tender-flags" data-flags='${JSON.stringify(tenderFlags)}'></div>
|
|
196
|
+
<form id="file-review-settings-form">
|
|
197
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
198
|
+
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-2">File Review Options</h2>${toggles.map(togRow).join('')}</div></div>
|
|
199
|
+
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-4">Access & Limits</h2>
|
|
200
|
+
<div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">Temp Access Period (days)</span></label><input type="number" name="temp_review_access_period" class="input input-solid max-w-full" value="${tempAccess}" min="0"/></div>
|
|
201
|
+
<div class="form-group mb-3"><label class="label"><span class="label-text font-semibold">Max File Size (MB)</span></label><input type="number" name="max_file_size_mb" class="input input-solid max-w-full" value="${fr.max_file_size_mb || 50}" min="1"/></div>
|
|
202
|
+
<div class="form-group"><label class="label"><span class="label-text font-semibold">Allowed File Types</span></label><input type="text" name="allowed_types" class="input input-solid max-w-full" value="${fr.allowed_types || 'pdf,doc,docx,xls,xlsx,png,jpg'}"/></div>
|
|
203
|
+
</div></div>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
206
|
+
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-3">Review Flags</h2>
|
|
207
|
+
<div id="review-flags-list" class="flex flex-wrap gap-2 mb-3">${flagChips(reviewFlags, 'review')}</div>
|
|
208
|
+
<div class="flex gap-2"><input type="text" id="new-review-flag" class="input input-solid" placeholder="New flag label..." style="flex:1"/><button type="button" data-action="addFlag" data-args='["review"]' class="btn btn-primary btn-sm">Add</button></div>
|
|
209
|
+
</div></div>
|
|
210
|
+
<div class="card-clean"><div class="card-clean-body"><h2 class="card-title text-base mb-3">Tender Flags</h2>
|
|
211
|
+
<div id="tender-flags-list" class="flex flex-wrap gap-2 mb-3">${flagChips(tenderFlags, 'tender')}</div>
|
|
212
|
+
<div class="flex gap-2"><input type="text" id="new-tender-flag" class="input input-solid" placeholder="New flag label..." style="flex:1"/><button type="button" data-action="addFlag" data-args='["tender"]' class="btn btn-primary btn-sm">Add</button></div>
|
|
213
|
+
</div></div>
|
|
214
|
+
</div>
|
|
215
|
+
<button type="submit" class="btn btn-primary">Save File Review Settings</button></form>`;
|
|
216
|
+
const script = `${TOAST_SCRIPT}
|
|
217
|
+
var _rvFlags=${JSON.stringify(reviewFlags)};
|
|
218
|
+
var _tdFlags=${JSON.stringify(tenderFlags)};
|
|
219
|
+
function _escFlag(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
|
220
|
+
function renderFlags(type){const list=type==='review'?_rvFlags:_tdFlags;const el=document.getElementById(type+'-flags-list');if(!el)return;el.innerHTML=list.map(f=>'<span class="pill pill-neutral" style="display:inline-flex;align-items:center;gap:4px">'+_escFlag(f)+'<button type="button" data-action="removeFlag" data-args=\\''+_escFlag(JSON.stringify([type,f]))+'\\' aria-label="Remove flag" style="background:none;border:none;cursor:pointer;font-size:1rem;line-height:1;color:inherit">×</button></span>').join('')}
|
|
221
|
+
window.addFlag=function(type){const inp=document.getElementById('new-'+type+'-flag');const val=(inp?.value||'').trim();if(!val)return;if(type==='review'){_rvFlags=[..._rvFlags,val]}else{_tdFlags=[..._tdFlags,val]};inp.value='';renderFlags(type)};
|
|
222
|
+
window.removeFlag=function(type,flag){if(type==='review'){_rvFlags=_rvFlags.filter(f=>f!==flag)}else{_tdFlags=_tdFlags.filter(f=>f!==flag)};renderFlags(type)};
|
|
223
|
+
document.getElementById('file-review-settings-form').addEventListener('submit',async(e)=>{e.preventDefault();const fd=new FormData(e.target);const data={};for(const[k,v]of fd.entries())data[k]=v;document.querySelectorAll('#file-review-settings-form input[type=checkbox]').forEach(cb=>{data[cb.name]=cb.checked});document.querySelectorAll('#file-review-settings-form input[type=number]').forEach(n=>{if(data[n.name])data[n.name]=Number(data[n.name])});data.review_flags=_rvFlags;data.tender_flags=_tdFlags;try{const res=await fetch('/api/admin/settings/file-review',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});if(res.ok){showToast('Settings saved','success')}else{showToast('Save failed','error')}}catch(err){showToast('Error: '+err.message,'error')}})`;
|
|
224
|
+
return settingsPage(user, 'File Review Settings', bc('File Review Settings'), content, [script]);
|
|
194
225
|
}
|
|
195
226
|
|
|
196
|
-
export function
|
|
197
|
-
|
|
227
|
+
export function renderSettingsMwrPermissions(user, permissions = []) {
|
|
228
|
+
const rows = permissions.map(p => `<tr>
|
|
229
|
+
<td class="text-sm">${esc(p.entity_type||'-')}</td>
|
|
230
|
+
<td class="text-sm">${esc(p.entity_id||'-')}</td>
|
|
231
|
+
<td class="text-sm">${esc(p.user_id||'-')}</td>
|
|
232
|
+
<td class="text-sm">${esc(p.permission_type||'-')}</td>
|
|
233
|
+
<td class="text-sm">${p.granted_at ? new Date(p.granted_at * 1000).toLocaleDateString('en-ZA',{day:'2-digit',month:'short',year:'numeric'}) : '-'}</td>
|
|
234
|
+
<td><button type="button" class="btn btn-error btn-xs btn-outline" data-action="revokePermission" data-args='${esc(JSON.stringify([p.id]))}'>Revoke</button></td>
|
|
235
|
+
</tr>`).join('');
|
|
236
|
+
const content = `${settingsBack()}<div class="flex justify-between items-center mb-6">
|
|
237
|
+
<h1 class="text-2xl font-bold">MWR Permissions</h1>
|
|
238
|
+
<button class="btn btn-primary btn-sm" data-action="openDialog" data-args='["grant-perm-dialog"]'>+ Grant Permission</button>
|
|
239
|
+
</div>
|
|
240
|
+
${inlineTable(['Entity Type','Entity ID','User ID','Permission','Granted','Actions'], rows, 'No permissions configured')}
|
|
241
|
+
<div id="grant-perm-dialog" class="dialog-overlay" style="display:none" role="dialog" aria-modal="true" aria-labelledby="gp-dialog-title" data-dialog-close="grant-perm-dialog">
|
|
242
|
+
<div class="dialog-panel" style="max-width:min(460px,90vw)">
|
|
243
|
+
<div class="dialog-header"><span class="dialog-title" id="gp-dialog-title">Grant Permission</span><button class="dialog-close" data-dialog-close="grant-perm-dialog" aria-label="Close">×</button></div>
|
|
244
|
+
<div class="dialog-body">
|
|
245
|
+
<div class="modal-form-group"><label>Entity Type</label><input id="gp-entity-type" class="form-input" placeholder="e.g. review"/></div>
|
|
246
|
+
<div class="modal-form-group"><label>Entity ID</label><input id="gp-entity-id" class="form-input" placeholder="e.g. 1a2b3c"/></div>
|
|
247
|
+
<div class="modal-form-group"><label>User ID</label><input id="gp-user-id" class="form-input" placeholder="e.g. user_42"/></div>
|
|
248
|
+
<div class="modal-form-group"><label>Permission Type</label><select id="gp-perm-type" class="form-input"><option value="view">view</option><option value="edit">edit</option><option value="admin">admin</option></select></div>
|
|
249
|
+
<div id="gp-result" style="display:none;margin-top:8px"></div>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="dialog-footer">
|
|
252
|
+
<button class="btn btn-ghost btn-sm" data-dialog-close="grant-perm-dialog">Cancel</button>
|
|
253
|
+
<button class="btn btn-primary btn-sm" data-action="grantPermission">Grant</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>`;
|
|
257
|
+
const script = `${TOAST_SCRIPT}window.grantPermission=async function(){var body={entity_type:document.getElementById('gp-entity-type').value.trim(),entity_id:document.getElementById('gp-entity-id').value.trim(),user_id:document.getElementById('gp-user-id').value.trim(),permission_type:document.getElementById('gp-perm-type').value};var res=document.getElementById('gp-result');if(!body.entity_type||!body.entity_id||!body.user_id){res.style.display='block';res.innerHTML='<div style="color:var(--color-danger);font-size:13px">All fields required.</div>';return}try{var r=await fetch('/api/mwr/permissions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});var d=await r.json();if(r.ok&&d.success){showToast('Permission granted','success');(window.closeDialog?window.closeDialog('grant-perm-dialog'):document.getElementById('grant-perm-dialog').style.display='none');setTimeout(function(){location.reload()},500)}else{res.style.display='block';res.innerHTML='<div style="color:var(--color-danger);font-size:13px">'+(d.error||'Failed')+'</div>'}}catch(e){showToast('Error: '+e.message,'error')}};window.revokePermission=async function(id){if(!(await window.gmConfirm({title:'Revoke permission',message:'Revoke this permission?',confirmLabel:'Revoke',danger:true})))return;try{var r=await fetch('/api/permission/'+id,{method:'DELETE'});if(r.ok){showToast('Revoked','success');setTimeout(function(){location.reload()},500)}else{showToast('Failed','error')}}catch(e){showToast('Error: '+e.message,'error')}}`;
|
|
258
|
+
return settingsPage(user, 'MWR Permissions', bc('MWR Permissions'), content, [script]);
|
|
198
259
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared settings-page building blocks: the page shell (settingsPage/settingsBack),
|
|
3
|
+
* the settings-scoped breadcrumb builder (bc), a plain inline table renderer, and
|
|
4
|
+
* the role badge. Previously settings-renderer-advanced.js and
|
|
5
|
+
* settings-renderer-advanced2.js each defined an identical local `bc()` breadcrumb
|
|
6
|
+
* helper (byte-for-byte the same arrow function) -- deduped here to one definition
|
|
7
|
+
* every settings/*.js file imports.
|
|
8
|
+
*/
|
|
9
|
+
import { page } from '@/ui/layout.js';
|
|
10
|
+
import { TOAST_SCRIPT, TABLE_SCRIPT, statusBadge as _statusBadge, esc } from '@/ui/render-helpers.js';
|
|
11
|
+
import { icon } from '@/ui/format-helpers.js';
|
|
12
|
+
import { SPACING, renderCard } from '@/ui/spacing-system.js';
|
|
13
|
+
|
|
14
|
+
export { TOAST_SCRIPT, TABLE_SCRIPT, esc, icon, SPACING, renderCard };
|
|
15
|
+
|
|
16
|
+
export function settingsPage(user, title, bc, content, scripts = []) {
|
|
17
|
+
return page(user, title, bc, content, scripts);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function settingsBack() {
|
|
21
|
+
return `<a href="/admin/settings" class="btn btn-ghost btn-sm gap-1 mb-4">Back to Settings</a>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Settings-scoped breadcrumb builder: Dashboard -> Settings -> <label>. The
|
|
25
|
+
// duplicate this replaces lived identically in settings-renderer-advanced.js
|
|
26
|
+
// and settings-renderer-advanced2.js.
|
|
27
|
+
export const bc = (label) => [{ href: '/', label: 'Dashboard' }, { href: '/admin/settings', label: 'Settings' }, { label }];
|
|
28
|
+
|
|
29
|
+
export function inlineTable(headers, rows, emptyMsg) {
|
|
30
|
+
const ths = headers.map(h => `<th>${h}</th>`).join('');
|
|
31
|
+
const empty = `<tr><td colspan="${headers.length}" class="text-center py-8 text-base-content/40 text-sm">${emptyMsg}</td></tr>`;
|
|
32
|
+
return `<div class="table-wrap"><table class="data-table"><thead><tr>${ths}</tr></thead><tbody>${rows || empty}</tbody></table></div>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const KNOWN_ROLE_LABELS = { admin:'Admin', partner:'Partner', manager:'Manager', clerk:'Clerk', user:'User', auditor:'Auditor', client_admin:'Client Admin', client_user:'Client User' };
|
|
36
|
+
|
|
37
|
+
export function roleBadge(role) {
|
|
38
|
+
const r = (role || '').toLowerCase();
|
|
39
|
+
const pillMap = { admin:'pill pill-danger', partner:'pill pill-info', manager:'pill pill-success', clerk:'pill pill-warning', user:'pill pill-neutral', auditor:'pill pill-neutral', client_admin:'pill pill-info', client_user:'pill pill-neutral' };
|
|
40
|
+
const cls = pillMap[r] || 'pill pill-neutral';
|
|
41
|
+
const label = KNOWN_ROLE_LABELS[r] || (r.length > 20 ? 'Staff' : (r.charAt(0).toUpperCase() + r.slice(1))) || 'Staff';
|
|
42
|
+
return `<span class="${cls}">${label}</span>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function statusBadge(status) { return _statusBadge(status); }
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { TOAST_SCRIPT, settingsPage, settingsBack, inlineTable } from '@/ui/settings
|
|
2
|
-
import { esc } from '@/ui/render-helpers.js';
|
|
1
|
+
import { TOAST_SCRIPT, settingsPage, settingsBack, inlineTable, esc } from '@/ui/settings/shared.js';
|
|
3
2
|
|
|
4
3
|
const RFI_PALETTE = ['#B0B0B0', '#44BBA4', '#FF4141', '#7F7EFF', '#3b82f6', '#f59e0b', '#ec4899', '#8b5cf6'];
|
|
5
4
|
|