thatcher 1.0.4
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/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { memoize } from '@/lib/render-cache.js'
|
|
2
|
+
|
|
3
|
+
export const renderLargeList = memoize(function renderLargeList(items, renderFn, options = {}) {
|
|
4
|
+
const { chunkSize = 100, useVirtual = false } = options
|
|
5
|
+
if (!items || items.length === 0) return ''
|
|
6
|
+
if (items.length <= chunkSize || !useVirtual) {
|
|
7
|
+
return items.map(renderFn).join('')
|
|
8
|
+
}
|
|
9
|
+
const visible = items.slice(0, chunkSize)
|
|
10
|
+
const deferred = items.slice(chunkSize)
|
|
11
|
+
return visible.map(renderFn).join('') +
|
|
12
|
+
`<tr class="defer-load" data-count="${deferred.length}"><td colspan="99" class="text-center text-gray-500 py-4">Loading ${deferred.length} more...</td></tr>`
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const batchRender = memoize(function batchRender(items, renderFn, batchSize = 50) {
|
|
16
|
+
if (!items || items.length === 0) return ''
|
|
17
|
+
const batches = []
|
|
18
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
19
|
+
batches.push(items.slice(i, i + batchSize))
|
|
20
|
+
}
|
|
21
|
+
return batches.map((batch, idx) => {
|
|
22
|
+
const html = batch.map(renderFn).join('')
|
|
23
|
+
if (idx === 0) return html
|
|
24
|
+
return `<div class="defer-load" data-content="${escapeAttr(html)}"></div>`
|
|
25
|
+
}).join('')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function escapeAttr(str) {
|
|
29
|
+
return String(str).replace(/"/g, '"').replace(/'/g, ''')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function eventDelegation(containerId, selector, eventType, handlerCode) {
|
|
33
|
+
return `
|
|
34
|
+
(function(){
|
|
35
|
+
const c=document.getElementById('${containerId}');
|
|
36
|
+
if(!c)return;
|
|
37
|
+
c.addEventListener('${eventType}',function(e){
|
|
38
|
+
const t=e.target.closest('${selector}');
|
|
39
|
+
if(t){${handlerCode}}
|
|
40
|
+
});
|
|
41
|
+
})();
|
|
42
|
+
`.trim()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function optimizeTableScroll(tableId) {
|
|
46
|
+
return `
|
|
47
|
+
(function(){
|
|
48
|
+
const t=document.getElementById('${tableId}');
|
|
49
|
+
if(!t)return;
|
|
50
|
+
let ticking=false;
|
|
51
|
+
t.addEventListener('scroll',()=>{
|
|
52
|
+
if(!ticking){
|
|
53
|
+
requestAnimationFrame(()=>{
|
|
54
|
+
ticking=false;
|
|
55
|
+
});
|
|
56
|
+
ticking=true;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
})();
|
|
60
|
+
`.trim()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const styleFragment = memoize(function styleFragment(styles) {
|
|
64
|
+
return `<style>${Object.entries(styles).map(([k, v]) => `${k}{${v}}`).join('')}</style>`
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export function minifyHtml(html) {
|
|
68
|
+
return html
|
|
69
|
+
.replace(/\s+/g, ' ')
|
|
70
|
+
.replace(/>\s+</g, '><')
|
|
71
|
+
.replace(/\s+>/g, '>')
|
|
72
|
+
.replace(/<\s+/g, '<')
|
|
73
|
+
.trim()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const progressiveEnhance = memoize(function progressiveEnhance(baseHtml, enhancedHtml) {
|
|
77
|
+
return baseHtml + `<noscript>${enhancedHtml}</noscript>`
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export function createSkeleton(type = 'list', count = 3) {
|
|
81
|
+
if (type === 'list') {
|
|
82
|
+
return Array(count).fill(0).map(() =>
|
|
83
|
+
`<div class="skeleton-row" style="height:48px;background:#f3f4f6;margin:8px 0;border-radius:4px;animation:pulse 1.5s ease-in-out infinite"></div>`
|
|
84
|
+
).join('')
|
|
85
|
+
}
|
|
86
|
+
if (type === 'card') {
|
|
87
|
+
return Array(count).fill(0).map(() =>
|
|
88
|
+
`<div class="skeleton-card" style="width:100%;height:200px;background:#f3f4f6;border-radius:8px;animation:pulse 1.5s ease-in-out infinite"></div>`
|
|
89
|
+
).join('')
|
|
90
|
+
}
|
|
91
|
+
return ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function perfBudget(key, maxMs) {
|
|
95
|
+
return `performance.mark('${key}-start');(window.requestIdleCallback||function(cb){setTimeout(cb,1)})(()=>{performance.mark('${key}-end');const m=performance.measure('${key}','${key}-start','${key}-end');if(m.duration>${maxMs})console.warn('[PERF] ${key} exceeded budget: '+m.duration.toFixed(2)+'ms > ${maxMs}ms')});`
|
|
96
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { memoize } from '@/lib/render-cache.js'
|
|
2
|
+
import { measure } from '@/lib/perf-monitor.js'
|
|
3
|
+
import { virtualScrollScript, lazyLoadImages, deferOffscreen } from '@/ui/virtual-scroll.js'
|
|
4
|
+
import { eventDelegation, perfBudget, createSkeleton } from '@/ui/perf-helpers.js'
|
|
5
|
+
|
|
6
|
+
export function optimizedListRender(items, renderFn, options = {}) {
|
|
7
|
+
const { chunkSize = 100, containerId = 'list-container', enableVirtual = true } = options
|
|
8
|
+
return measure('list-render', () => {
|
|
9
|
+
if (!items || items.length === 0) return createSkeleton('list', 3)
|
|
10
|
+
if (items.length <= chunkSize) return items.map(renderFn).join('')
|
|
11
|
+
const visible = items.slice(0, chunkSize)
|
|
12
|
+
const deferred = items.slice(chunkSize)
|
|
13
|
+
return visible.map(renderFn).join('') +
|
|
14
|
+
(enableVirtual && deferred.length > 0
|
|
15
|
+
? `<div class="defer-load" data-deferred="${deferred.length}"></div>`
|
|
16
|
+
: deferred.map(renderFn).join(''))
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function withPerformanceMonitoring(renderFn, name) {
|
|
21
|
+
return function(...args) {
|
|
22
|
+
return measure(name, () => renderFn(...args))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function withMemoization(renderFn) {
|
|
27
|
+
return memoize(renderFn)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function optimizedTableScripts(tableId, options = {}) {
|
|
31
|
+
const { enableVirtual = false, enableDefer = true, rowHeight = 48, perfBudgetMs = 200 } = options
|
|
32
|
+
const scripts = []
|
|
33
|
+
if (enableVirtual) scripts.push(virtualScrollScript(tableId, rowHeight))
|
|
34
|
+
if (enableDefer) scripts.push(deferOffscreen('.defer-load'))
|
|
35
|
+
scripts.push(perfBudget(`${tableId}-render`, perfBudgetMs))
|
|
36
|
+
scripts.push(lazyLoadImages())
|
|
37
|
+
return scripts.filter(Boolean)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function batchOperations(operations, batchSize = 10) {
|
|
41
|
+
const batches = []
|
|
42
|
+
for (let i = 0; i < operations.length; i += batchSize) {
|
|
43
|
+
batches.push(operations.slice(i, i + batchSize))
|
|
44
|
+
}
|
|
45
|
+
return batches.map((batch, idx) => {
|
|
46
|
+
if (idx === 0) return batch
|
|
47
|
+
return { deferred: true, operations: batch }
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function progressiveLoad(content, priority = 'high') {
|
|
52
|
+
if (priority === 'high') return content
|
|
53
|
+
return `<div class="defer-load" data-content="${escapeAttr(content)}"></div>`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function escapeAttr(str) {
|
|
57
|
+
return String(str).replace(/"/g, '"').replace(/'/g, ''')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const perfConfig = {
|
|
61
|
+
tableChunkSize: 100,
|
|
62
|
+
cardChunkSize: 50,
|
|
63
|
+
virtualScrollThreshold: 200,
|
|
64
|
+
deferLoadThreshold: 100,
|
|
65
|
+
perfBudgets: {
|
|
66
|
+
'list-render': 100,
|
|
67
|
+
'table-render': 150,
|
|
68
|
+
'card-render': 80,
|
|
69
|
+
'page-render': 500
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { getConfigEngineSync } from '@/lib/config-generator-engine.js';
|
|
2
|
+
|
|
3
|
+
function getRoleHierarchy() {
|
|
4
|
+
const config = getConfigEngineSync();
|
|
5
|
+
const roles = config.getRoles();
|
|
6
|
+
const hierarchy = {};
|
|
7
|
+
let level = 0;
|
|
8
|
+
for (const [roleName] of Object.entries(roles)) {
|
|
9
|
+
hierarchy[roleName] = level++;
|
|
10
|
+
}
|
|
11
|
+
return hierarchy;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getEntityPermissions() {
|
|
15
|
+
const config = getConfigEngineSync();
|
|
16
|
+
const entityNames = config.getAllEntities();
|
|
17
|
+
const permissions = {};
|
|
18
|
+
|
|
19
|
+
for (const name of entityNames) {
|
|
20
|
+
if (!name) continue;
|
|
21
|
+
try {
|
|
22
|
+
const spec = config.generateEntitySpec(name);
|
|
23
|
+
|
|
24
|
+
const access = spec.access || {};
|
|
25
|
+
|
|
26
|
+
permissions[name] = {
|
|
27
|
+
list: access.list || [],
|
|
28
|
+
view: access.view || [],
|
|
29
|
+
create: access.create || [],
|
|
30
|
+
edit: access.edit || [],
|
|
31
|
+
delete: access.delete || [],
|
|
32
|
+
};
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(`[Permissions] Error loading permissions for ${name}:`, e.message);
|
|
35
|
+
permissions[name] = { list: [], view: [], create: [], edit: [], delete: [] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return permissions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function canAccess(user, entity, action) {
|
|
42
|
+
if (!user?.role) return false;
|
|
43
|
+
const config = getConfigEngineSync();
|
|
44
|
+
const allPermissions = getEntityPermissions();
|
|
45
|
+
const permissions = allPermissions[entity];
|
|
46
|
+
|
|
47
|
+
if (!permissions) return isPartner(user);
|
|
48
|
+
const allowed = permissions[action] || [];
|
|
49
|
+
if (allowed.includes(user.role)) return true;
|
|
50
|
+
if (!allowed.length) return false;
|
|
51
|
+
if (action === 'list' || action === 'view') return isClerk(user);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function canList(user, entity) {
|
|
56
|
+
return canAccess(user, entity, 'list');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function canView(user, entity) {
|
|
60
|
+
return canAccess(user, entity, 'view');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function canCreate(user, entity) {
|
|
64
|
+
return canAccess(user, entity, 'create');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function canEdit(user, entity) {
|
|
68
|
+
return canAccess(user, entity, 'edit');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function canDelete(user, entity) {
|
|
72
|
+
return canAccess(user, entity, 'delete');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isPartner(user) {
|
|
76
|
+
return ['partner', 'admin'].includes(user?.role);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isManager(user) {
|
|
80
|
+
return ['manager'].includes(user?.role);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isClerk(user) {
|
|
84
|
+
return ['clerk', 'user'].includes(user?.role);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isClientUser(user) {
|
|
88
|
+
const config = getConfigEngineSync();
|
|
89
|
+
const roles = config.getRoles();
|
|
90
|
+
const clientRoles = Object.keys(roles).filter(r => r.includes('client'));
|
|
91
|
+
return clientRoles.includes(user?.role);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function isClientAdmin(user) {
|
|
95
|
+
const config = getConfigEngineSync();
|
|
96
|
+
const roles = config.getRoles();
|
|
97
|
+
const clientAdminRole = Object.keys(roles).find(r => r === 'client_admin');
|
|
98
|
+
return user?.role === clientAdminRole;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isAuditor(user) {
|
|
102
|
+
const config = getConfigEngineSync();
|
|
103
|
+
const roles = config.getRoles();
|
|
104
|
+
const auditRoles = Object.keys(roles).filter(r => !r.includes('client'));
|
|
105
|
+
return auditRoles.includes(user?.role);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const CLIENT_VISIBLE_ENTITIES = ['engagement', 'rfi', 'review', 'file', 'message', 'rfi_response'];
|
|
109
|
+
|
|
110
|
+
export function canClientAccessEntity(user, entityName) {
|
|
111
|
+
if (!isClientUser(user)) return true;
|
|
112
|
+
return CLIENT_VISIBLE_ENTITIES.includes(entityName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getNavItems(user, domain) {
|
|
116
|
+
const items = [];
|
|
117
|
+
if (domain === 'mwr') return getMwrNavItems(user);
|
|
118
|
+
if (canList(user, 'engagement')) items.push({ href: '/engagements', label: 'Engagements' });
|
|
119
|
+
if (canList(user, 'client')) items.push({ href: '/client', label: 'Clients' });
|
|
120
|
+
if (canList(user, 'rfi')) items.push({ href: '/rfi', label: 'RFIs' });
|
|
121
|
+
if (canList(user, 'review')) items.push({ href: '/review', label: 'Reviews' });
|
|
122
|
+
if (canList(user, 'user')) items.push({ href: '/user', label: 'Users' });
|
|
123
|
+
if (canList(user, 'team')) items.push({ href: '/team', label: 'Teams' });
|
|
124
|
+
if (canList(user, 'tender')) items.push({ href: '/tender', label: 'Tenders' });
|
|
125
|
+
if (canList(user, 'checklist')) items.push({ href: '/checklist', label: 'Checklists' });
|
|
126
|
+
if (canList(user, 'file')) items.push({ href: '/file', label: 'Files' });
|
|
127
|
+
if (canList(user, 'letter')) items.push({ href: '/letter', label: 'Letters' });
|
|
128
|
+
return items;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getMwrNavItems(user) {
|
|
132
|
+
const items = [];
|
|
133
|
+
if (canCreate(user, 'review')) items.push({ href: '/review/new', label: 'Start Review' });
|
|
134
|
+
if (canList(user, 'review')) items.push({ href: '/reviews/active', label: 'Active Reviews' });
|
|
135
|
+
if (canList(user, 'review')) items.push({ href: '/reviews/priority', label: 'Priority Reviews' });
|
|
136
|
+
if (canList(user, 'review')) items.push({ href: '/reviews/history', label: 'History' });
|
|
137
|
+
if (canList(user, 'review')) items.push({ href: '/reviews/archive', label: 'Archive' });
|
|
138
|
+
return items;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function getAdminItems(user) {
|
|
142
|
+
const items = [];
|
|
143
|
+
if (canList(user, 'permission_audit')) items.push({ href: '/admin/audit', label: 'Audit' });
|
|
144
|
+
if (isPartner(user)) items.push({ href: '/admin/health', label: 'Health' });
|
|
145
|
+
if (isPartner(user)) items.push({ href: '/admin/settings', label: 'Settings' });
|
|
146
|
+
return items;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function getQuickActions(user) {
|
|
150
|
+
const actions = [];
|
|
151
|
+
if (isClerk(user)) {
|
|
152
|
+
actions.push({ href: '/rfi', label: 'View My RFIs', primary: true });
|
|
153
|
+
actions.push({ href: '/engagements', label: 'View Engagements', outline: true });
|
|
154
|
+
} else if (isClientUser(user)) {
|
|
155
|
+
actions.push({ href: '/rfi', label: 'View My RFIs', primary: true });
|
|
156
|
+
actions.push({ href: '/engagements', label: 'My Engagements', outline: true });
|
|
157
|
+
} else {
|
|
158
|
+
if (canCreate(user, 'engagement')) actions.push({ href: '/engagements/new', label: 'New Engagement', primary: true });
|
|
159
|
+
if (canCreate(user, 'client')) actions.push({ href: '/client/new', label: 'New Client', outline: true });
|
|
160
|
+
if (canCreate(user, 'review')) actions.push({ href: '/review/new', label: 'New Review', outline: true });
|
|
161
|
+
}
|
|
162
|
+
return actions;
|
|
163
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { h } from '@/ui/webjsx.js'
|
|
2
|
+
import { STAGE_COLORS, TOAST_SCRIPT } from '@/ui/render-helpers.js'
|
|
3
|
+
|
|
4
|
+
const DIALOG_COLORS = ['#B0B0B0','#44BBA4','#FF4141','#7F7EFF','#3b82f6','#f59e0b','#ec4899','#8b5cf6','#ef4444','#22c55e','#06b6d4','#f97316','#84cc16','#e11d48','#14b8a6','#6366f1']
|
|
5
|
+
|
|
6
|
+
export function colorPickerDialog(id = 'cpd', selected = '#B0B0B0', onSelect = '') {
|
|
7
|
+
const swatches = DIALOG_COLORS.map(c =>
|
|
8
|
+
`<div class="cpd-swatch${c === selected ? ' cpd-selected' : ''}" style="background:${c}" data-color="${c}" role="option" tabindex="0" aria-label="Color ${c}" aria-selected="${c === selected}" data-action="cpdSelect" data-args='["${id}","${c}",${onSelect ? "true" : "false"}]' onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();cpdSelect('${id}','${c}',${onSelect ? 'true' : 'false'})}"></div>`
|
|
9
|
+
).join('')
|
|
10
|
+
return `<div id="${id}-dialog" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="${id}-dialog-title" aria-hidden="true">
|
|
11
|
+
<div class="dialog-panel" style="max-width:360px">
|
|
12
|
+
<div class="dialog-header"><span class="dialog-title" id="${id}-dialog-title">Choose Color</span><button class="dialog-close" data-dialog-close="${id}-dialog" aria-label="Close dialog">×</button></div>
|
|
13
|
+
<div class="dialog-body"><div class="color-picker-grid" role="listbox" aria-label="Color options">${swatches}</div>
|
|
14
|
+
<div style="margin-top:0.75rem;display:flex;align-items:center;gap:0.75rem"><label class="text-sm text-gray-500" for="${id}-custom">Custom:</label><input type="color" id="${id}-custom" value="${selected}" onchange="cpdSelect('${id}',this.value,true)" aria-label="Custom color picker" style="width:40px;height:32px;border:none;cursor:pointer"/><span id="${id}-val" class="text-sm font-medium" aria-live="polite">${selected}</span></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="${id}-dialog">Cancel</button><button class="btn btn-primary btn-sm" data-action="cpdConfirm" data-args='["${id}"]'>Select</button></div>
|
|
17
|
+
</div></div>
|
|
18
|
+
<script>window._cpd=window._cpd||{};window._cpd['${id}']='${selected}';
|
|
19
|
+
window.cpdSelect=function(id,c){window._cpd[id]=c;document.getElementById(id+'-val').textContent=c;document.getElementById(id+'-custom').value=c;document.querySelectorAll('#'+id+'-dialog .cpd-swatch').forEach(function(el){el.classList.toggle('cpd-selected',el.dataset.color===c)})};
|
|
20
|
+
window.cpdConfirm=function(id){var c=window._cpd[id];document.getElementById(id+'-dialog').style.display='none';if(window._cpdCallback)window._cpdCallback(c)};
|
|
21
|
+
window.showColorPicker=function(id,current,cb){window._cpdCallback=cb;if(current)cpdSelect(id,current);document.getElementById(id+'-dialog').style.display='flex'};</script>`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function dateChoiceDialog(id = 'dcd') {
|
|
25
|
+
const presets = [
|
|
26
|
+
{ label: 'Today', days: 0 }, { label: 'Tomorrow', days: 1 }, { label: '+3 Days', days: 3 },
|
|
27
|
+
{ label: '+1 Week', days: 7 }, { label: '+2 Weeks', days: 14 }, { label: '+1 Month', days: 30 },
|
|
28
|
+
{ label: '+3 Months', days: 90 }, { label: 'End of Month', days: 'eom' }, { label: 'End of Quarter', days: 'eoq' },
|
|
29
|
+
]
|
|
30
|
+
const presetBtns = presets.map(p => `<button class="date-preset-btn" data-action="dcdPreset" data-args='["${id}",${typeof p.days === 'number' ? p.days : '"' + p.days + '"'}]'>${p.label}</button>`).join('')
|
|
31
|
+
return `<div id="${id}-dialog" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="${id}-dialog-title" aria-hidden="true">
|
|
32
|
+
<div class="dialog-panel" style="max-width:380px">
|
|
33
|
+
<div class="dialog-header"><span class="dialog-title" id="${id}-dialog-title">Choose Date</span><button class="dialog-close" data-dialog-close="${id}-dialog" aria-label="Close dialog">×</button></div>
|
|
34
|
+
<div class="dialog-body">
|
|
35
|
+
<div class="modal-form-group"><label for="${id}-input">Date</label><input type="date" id="${id}-input" class="input input-bordered w-full"/></div>
|
|
36
|
+
<div class="date-presets">${presetBtns}</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="${id}-dialog">Cancel</button><button class="btn btn-error btn-outline btn-sm" data-action="dcdClear" data-args='["${id}"]'>Clear</button><button class="btn btn-primary btn-sm" data-action="dcdConfirm" data-args='["${id}"]'>Select</button></div>
|
|
39
|
+
</div></div>
|
|
40
|
+
<script>window.dcdPreset=function(id,days){var d=new Date();if(days==='eom'){d=new Date(d.getFullYear(),d.getMonth()+1,0)}else if(days==='eoq'){var q=Math.floor(d.getMonth()/3);d=new Date(d.getFullYear(),(q+1)*3,0)}else{d.setDate(d.getDate()+days)}document.getElementById(id+'-input').value=d.toISOString().split('T')[0]};
|
|
41
|
+
window.dcdConfirm=function(id){var v=document.getElementById(id+'-input').value;document.getElementById(id+'-dialog').style.display='none';if(window._dcdCallback)window._dcdCallback(v||null)};
|
|
42
|
+
window.dcdClear=function(id){document.getElementById(id+'-input').value='';document.getElementById(id+'-dialog').style.display='none';if(window._dcdCallback)window._dcdCallback(null)};
|
|
43
|
+
window.showDateChoice=function(id,current,cb){window._dcdCallback=cb;if(current)document.getElementById(id+'-input').value=current;document.getElementById(id+'-dialog').style.display='flex'};</script>`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function stageTransitionDialog(stages = null) {
|
|
47
|
+
const stageKeys = stages || Object.keys(STAGE_COLORS)
|
|
48
|
+
return `<div id="stage-trans-dialog" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="stage-trans-dialog-title" aria-hidden="true">
|
|
49
|
+
<div class="dialog-panel">
|
|
50
|
+
<div class="dialog-header"><span class="dialog-title" id="stage-trans-dialog-title">Transition Stage</span><button class="dialog-close" data-dialog-close="stage-trans-dialog" aria-label="Close dialog">×</button></div>
|
|
51
|
+
<div class="dialog-body">
|
|
52
|
+
<div id="std-from" class="stage-trans-block stage-trans-current"></div>
|
|
53
|
+
<div class="stage-trans-arrow"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></div>
|
|
54
|
+
<div id="std-to" class="stage-trans-block stage-trans-next"></div>
|
|
55
|
+
<div class="modal-form-group" style="margin-top:1rem"><label class="form-label" for="std-reason">Reason (optional)</label><textarea id="std-reason" class="textarea textarea-bordered w-full" rows="2" placeholder="Why is this stage being changed?"></textarea></div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="stage-trans-dialog">Cancel</button><button class="btn btn-primary btn-sm" data-action="stdConfirm">Confirm Transition</button></div>
|
|
58
|
+
</div></div>
|
|
59
|
+
<script>${TOAST_SCRIPT}
|
|
60
|
+
window._stdData={};
|
|
61
|
+
window.showStageTransition=function(entityId,entityType,fromStage,toStage,cb){window._stdData={entityId:entityId,entityType:entityType,from:fromStage,to:toStage,cb:cb};document.getElementById('stage-trans-dialog').style.display='flex';var sc=${JSON.stringify(Object.fromEntries(stageKeys.map(k => [k, STAGE_COLORS[k] || { bg: '#f3f4f6', text: '#4b5563', label: k }])))};var from=sc[fromStage]||{bg:'#f3f4f6',text:'#4b5563',label:fromStage};var to=sc[toStage]||{bg:'#f3f4f6',text:'#4b5563',label:toStage};document.getElementById('std-from').innerHTML='<div class="stage-trans-dot" style="background:'+from.text+'"></div><div><div class="text-xs text-gray-500">Current</div><div class="font-medium" style="color:'+from.text+'">'+from.label+'</div></div>';document.getElementById('std-to').innerHTML='<div class="stage-trans-dot" style="background:'+to.text+'"></div><div><div class="text-xs text-gray-500">Next</div><div class="font-medium" style="color:'+to.text+'">'+to.label+'</div></div>'};
|
|
62
|
+
window.stdConfirm=async function(){var d=window._stdData;var reason=document.getElementById('std-reason').value;try{var res=await fetch('/api/'+d.entityType+'/'+d.entityId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({stage:d.to,stage_reason:reason})});if(res.ok){showToast('Stage updated','success');document.getElementById('stage-trans-dialog').style.display='none';if(d.cb)d.cb();else setTimeout(function(){location.reload()},500)}else{showToast('Transition failed','error')}}catch(e){showToast('Error: '+e.message,'error')}};</script>`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function teamAssignmentDialog(id = 'tad') {
|
|
66
|
+
return `<div id="${id}-dialog" class="dialog-overlay" style="display:none" data-dialog-close-overlay="true" onkeydown="if(event.key==='Escape')this.style.display='none'" role="dialog" aria-modal="true" aria-labelledby="${id}-dialog-title" aria-hidden="true">
|
|
67
|
+
<div class="dialog-panel">
|
|
68
|
+
<div class="dialog-header"><span class="dialog-title" id="${id}-dialog-title">Assign Team Members</span><button class="dialog-close" data-dialog-close="${id}-dialog" aria-label="Close dialog">×</button></div>
|
|
69
|
+
<div class="dialog-body">
|
|
70
|
+
<input type="text" id="${id}-search" class="tad-search" placeholder="Search by name or email..." aria-label="Search team members" oninput="tadFilter('${id}')"/>
|
|
71
|
+
<div id="${id}-list" class="tad-list"></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="dialog-footer"><button class="btn btn-ghost btn-sm" data-dialog-close="${id}-dialog">Cancel</button><button class="btn btn-primary btn-sm" data-action="tadConfirm" data-args='["${id}"]'>Assign</button></div>
|
|
74
|
+
</div></div>
|
|
75
|
+
<script>${TOAST_SCRIPT}
|
|
76
|
+
window._tad=window._tad||{};
|
|
77
|
+
window.showTeamAssignment=function(id,users,selected,cb){window._tad[id]={users:users,selected:new Set(selected||[]),cb:cb};document.getElementById(id+'-dialog').style.display='flex';tadRender(id)};
|
|
78
|
+
function tadRender(id){var d=window._tad[id];var c=document.getElementById(id+'-list');c.innerHTML='';d.users.forEach(function(u){var row=document.createElement('div');row.className='tad-row';row.dataset.name=(u.name||'').toLowerCase();row.dataset.email=(u.email||'').toLowerCase();var checked=d.selected.has(u.id)?'checked':'';row.innerHTML='<input type="checkbox" '+checked+' onchange="tadToggle(\\''+id+'\\',\\''+u.id+'\\') " aria-label="Select '+(u.name||'Unknown').replace(/"/g,'"')+'">'+'<div><div class="tad-name">'+(u.name||'Unknown')+'</div><div class="tad-email">'+(u.email||'')+'</div></div>';c.appendChild(row)})}
|
|
79
|
+
window.tadFilter=function(id){var q=document.getElementById(id+'-search').value.toLowerCase();document.querySelectorAll('#'+id+'-list .tad-row').forEach(function(r){r.style.display=(r.dataset.name.includes(q)||r.dataset.email.includes(q))?'':'none'})};
|
|
80
|
+
window.tadToggle=function(id,uid){var d=window._tad[id];if(d.selected.has(uid))d.selected.delete(uid);else d.selected.add(uid)};
|
|
81
|
+
window.tadConfirm=function(id){var d=window._tad[id];document.getElementById(id+'-dialog').style.display='none';if(d.cb)d.cb(Array.from(d.selected))};</script>`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function teamSelector(id = 'ts', teams = []) {
|
|
85
|
+
const items = teams.map(t =>
|
|
86
|
+
`<div class="tad-row" data-name="${(t.name || '').toLowerCase()}" data-action="tsSelect" data-args='["${id}","${t.id}","${(t.name || '').replace(/"/g, '"')}"]'><div class="tad-name">${t.name || 'Unknown'}</div><div class="tad-email">${t.member_count || 0} members</div></div>`
|
|
87
|
+
).join('')
|
|
88
|
+
return `<div class="ts-wrap" id="${id}-wrap">
|
|
89
|
+
<div id="${id}-badges" class="ts-badges"></div>
|
|
90
|
+
<input type="text" id="${id}-search" class="tad-search" placeholder="Search teams..." aria-label="Search teams" oninput="tsFilter('${id}')" onfocus="document.getElementById('${id}-dropdown').classList.add('ts-open')" />
|
|
91
|
+
<div id="${id}-dropdown" class="ts-dropdown">${items}</div>
|
|
92
|
+
<input type="hidden" id="${id}-value" name="team_id" value=""/>
|
|
93
|
+
</div>
|
|
94
|
+
<script>window._ts=window._ts||{};window._ts['${id}']=[];
|
|
95
|
+
window.tsFilter=function(id){var q=document.getElementById(id+'-search').value.toLowerCase();document.querySelectorAll('#'+id+'-dropdown .tad-row').forEach(function(r){r.style.display=r.dataset.name.includes(q)?'':'none'})};
|
|
96
|
+
window.tsSelect=function(id,tid,name){window._ts[id].push({id:tid,name:name});tsRender(id);document.getElementById(id+'-dropdown').classList.remove('ts-open');document.getElementById(id+'-search').value=''};
|
|
97
|
+
function tsRender(id){var b=document.getElementById(id+'-badges');b.innerHTML='';window._ts[id].forEach(function(t,i){b.innerHTML+='<span class="ts-badge">'+t.name+' <span class="ts-badge-x" data-action="tsRemove" data-args=\\'["'+id+'",'+i+']\\' >×</span></span>'});document.getElementById(id+'-value').value=window._ts[id].map(function(t){return t.id}).join(',')}
|
|
98
|
+
window.tsRemove=function(id,idx){window._ts[id].splice(idx,1);tsRender(id)};
|
|
99
|
+
document.addEventListener('click',function(e){if(!document.getElementById('${id}-wrap').contains(e.target))document.getElementById('${id}-dropdown').classList.remove('ts-open')});</script>`
|
|
100
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { h } from '@/ui/webjsx.js'
|
|
2
|
+
|
|
3
|
+
export const STAGE_COLORS = {
|
|
4
|
+
info_gathering: { bg: '#dbeafe', text: '#1e40af', label: 'Info Gathering', bgDark: '#1e3a8a', textDark: '#60a5fa' },
|
|
5
|
+
commencement: { bg: '#dbeafe', text: '#1e40af', label: 'Commencement', bgDark: '#1e3a8a', textDark: '#60a5fa' },
|
|
6
|
+
team_execution: { bg: '#fef3c7', text: '#92400e', label: 'Team Execution', bgDark: '#78350f', textDark: '#fbbf24' },
|
|
7
|
+
partner_review: { bg: '#fef3c7', text: '#92400e', label: 'Partner Review', bgDark: '#78350f', textDark: '#fbbf24' },
|
|
8
|
+
finalization: { bg: '#d1fae5', text: '#065f46', label: 'Finalization', bgDark: '#064e3b', textDark: '#34d399' },
|
|
9
|
+
closeout: { bg: '#d1fae5', text: '#065f46', label: 'Close Out', bgDark: '#064e3b', textDark: '#34d399' },
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const STATUS_COLORS = {
|
|
13
|
+
pending: { bg: '#fef3c7', text: '#92400e' },
|
|
14
|
+
active: { bg: '#dbeafe', text: '#1e40af' },
|
|
15
|
+
completed: { bg: '#d1fae5', text: '#065f46' },
|
|
16
|
+
archived: { bg: '#f3f4f6', text: '#4b5563' },
|
|
17
|
+
open: { bg: '#fef3c7', text: '#92400e' },
|
|
18
|
+
closed: { bg: '#d1fae5', text: '#065f46' },
|
|
19
|
+
draft: { bg: '#f3f4f6', text: '#6b7280' },
|
|
20
|
+
in_progress: { bg: '#dbeafe', text: '#1e40af' },
|
|
21
|
+
review: { bg: '#ede9fe', text: '#5b21b6' },
|
|
22
|
+
approved: { bg: '#d1fae5', text: '#065f46' },
|
|
23
|
+
rejected: { bg: '#fee2e2', text: '#991b1b' },
|
|
24
|
+
overdue: { bg: '#fee2e2', text: '#991b1b' },
|
|
25
|
+
cancelled: { bg: '#f3f4f6', text: '#6b7280' },
|
|
26
|
+
on_hold: { bg: '#fef3c7', text: '#92400e' },
|
|
27
|
+
resolved: { bg: '#d1fae5', text: '#065f46' },
|
|
28
|
+
unresolved: { bg: '#fee2e2', text: '#991b1b' },
|
|
29
|
+
flagged: { bg: '#fce7f3', text: '#9d174d' },
|
|
30
|
+
responded: { bg: '#dbeafe', text: '#1e40af' },
|
|
31
|
+
expired: { bg: '#f3f4f6', text: '#6b7280' },
|
|
32
|
+
private: { bg: '#ede9fe', text: '#5b21b6' },
|
|
33
|
+
public: { bg: '#dbeafe', text: '#1e40af' },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const STAGE_CONFIG = [
|
|
37
|
+
{ key: 'info_gathering', label: 'Info Gathering', badge: 'stage-pill stage-info_gathering', color: '#e53935' },
|
|
38
|
+
{ key: 'commencement', label: 'Commencement', badge: 'stage-pill stage-commencement', color: '#e65100' },
|
|
39
|
+
{ key: 'team_execution', label: 'Team Execution', badge: 'stage-pill stage-team_execution', color: '#1565c0' },
|
|
40
|
+
{ key: 'partner_review', label: 'Partner Review', badge: 'stage-pill stage-partner_review', color: '#283593' },
|
|
41
|
+
{ key: 'finalization', label: 'Finalization', badge: 'stage-pill stage-finalization', color: '#2e7d32' },
|
|
42
|
+
{ key: 'closeout', label: 'Close Out', badge: 'stage-pill stage-closeout', color: '#33691e' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
export const AVATAR_COLORS = ['#3b82f6','#ef4444','#10b981','#f59e0b','#8b5cf6','#ec4899','#06b6d4','#84cc16','#f97316','#6366f1','#14b8a6','#e11d48']
|
|
46
|
+
export const AVATAR_SIZES = { sm: 24, md: 32, lg: 40, xl: 48 }
|
|
47
|
+
|
|
48
|
+
export 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)};`
|
|
49
|
+
|
|
50
|
+
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')});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;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.addEventListener('click',()=>sortTable(th.dataset.sort)))})})();`
|
|
51
|
+
|
|
52
|
+
export function esc(s) {
|
|
53
|
+
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function stagePill(stage) {
|
|
57
|
+
const cfg = STAGE_CONFIG.find(s => s.key === stage)
|
|
58
|
+
const lbl = cfg ? cfg.label : (stage || '-')
|
|
59
|
+
const stageColor = STAGE_COLORS[stage]
|
|
60
|
+
if (stageColor) {
|
|
61
|
+
return `<span class="stage-pill stage-${esc(stage||'')}" data-stage="${stage}" style="background:${stageColor.bg};color:${stageColor.text}">${lbl}</span>`
|
|
62
|
+
}
|
|
63
|
+
return `<span class="stage-pill stage-${esc(stage||'')}">${lbl}</span>`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function statusPill(status) {
|
|
67
|
+
const map = { active:'pill pill-success', pending:'pill pill-warning', inactive:'pill pill-neutral', draft:'pill pill-neutral', closed:'pill pill-neutral', responded:'pill pill-success', overdue:'pill pill-danger', sent:'pill pill-warning' }
|
|
68
|
+
const cls = map[(status||'').toLowerCase()] || 'pill pill-neutral'
|
|
69
|
+
return `<span class="${cls}">${status ? status.charAt(0).toUpperCase()+status.slice(1) : '-'}</span>`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function statusBadge(status) {
|
|
73
|
+
const map = { active:'pill pill-success', inactive:'pill pill-danger', pending:'pill pill-warning', open:'pill pill-info', in_progress:'pill pill-info', completed:'pill pill-success', closed:'pill pill-neutral', archived:'pill pill-neutral', responded:'pill pill-info' }
|
|
74
|
+
const cls = map[(status||'').toLowerCase()] || 'pill pill-neutral'
|
|
75
|
+
return `<span class="${cls}">${status ? status.charAt(0).toUpperCase()+status.slice(1) : '-'}</span>`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function progressBar(pct) {
|
|
79
|
+
if (typeof pct !== 'number') return '-'
|
|
80
|
+
const p = Math.min(100, Math.max(0, Math.round(pct)))
|
|
81
|
+
return `<div style="display:flex;align-items:center;gap:8px;min-width:100px"><div style="flex:1;height:6px;background:#e2e8f0;border-radius:3px;overflow:hidden"><div style="height:100%;width:${p}%;background:var(--color-primary);border-radius:3px"></div></div><span style="font-size:12px;color:var(--color-text-muted);min-width:28px">${p}%</span></div>`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function fmtVal(value, fieldKey, item = {}) {
|
|
85
|
+
if (value === null || value === undefined) return '-'
|
|
86
|
+
if (fieldKey?.includes('_at') || fieldKey === 'created_at' || fieldKey === 'updated_at') {
|
|
87
|
+
const num = Number(value)
|
|
88
|
+
if (!isNaN(num) && num > 1000000000 && num < 3000000000) return new Date(num * 1000).toLocaleString()
|
|
89
|
+
}
|
|
90
|
+
if (fieldKey === 'year') { const n = Number(value); if (!isNaN(n)) return String(Math.floor(n)) }
|
|
91
|
+
if (fieldKey === 'stage' && STAGE_COLORS[value]) {
|
|
92
|
+
const s = STAGE_COLORS[value]
|
|
93
|
+
return h('span', { className: 'badge-stage', style: `background:${s.bg};color:${s.text}` }, s.label)
|
|
94
|
+
}
|
|
95
|
+
if (fieldKey === 'status' && STATUS_COLORS[value]) {
|
|
96
|
+
const s = STATUS_COLORS[value]
|
|
97
|
+
return h('span', { className: 'badge-status', style: `background:${s.bg};color:${s.text}` }, value.charAt(0).toUpperCase() + value.slice(1))
|
|
98
|
+
}
|
|
99
|
+
if (item[`${fieldKey}_display`]) return item[`${fieldKey}_display`]
|
|
100
|
+
return String(value)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function statusLabel(status) {
|
|
104
|
+
if (!status) return ''
|
|
105
|
+
const key = status.toLowerCase().replace(/\s+/g, '_')
|
|
106
|
+
const s = STATUS_COLORS[key] || { bg: '#f3f4f6', text: '#6b7280' }
|
|
107
|
+
const label = status.charAt(0).toUpperCase() + status.slice(1).replace(/_/g, ' ')
|
|
108
|
+
return h('span', { className: 'status-label', style: `background:${s.bg};color:${s.text}` }, label)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function nameHash(name) {
|
|
112
|
+
let hash = 0
|
|
113
|
+
for (let i = 0; i < (name || '').length; i++) hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0
|
|
114
|
+
return Math.abs(hash)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { getInitials } from '@/lib/utils.js'
|
|
118
|
+
|
|
119
|
+
export function fmtDate(ts) {
|
|
120
|
+
if (!ts) return '-';
|
|
121
|
+
const n = Number(ts);
|
|
122
|
+
if (!isNaN(n) && n > 1e9 && n < 3e9) return new Date(n * 1000).toLocaleDateString();
|
|
123
|
+
return String(ts);
|
|
124
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { renderDialog } from '@/ui/dialog-engine.js'
|
|
2
|
+
import { renderComponent, getComponentConfig, validateComponentProps } from '@/ui/component-engine.js'
|
|
3
|
+
|
|
4
|
+
export const REDIRECT = Symbol('REDIRECT')
|
|
5
|
+
export { renderDialog, renderComponent, getComponentConfig, validateComponentProps }
|
|
6
|
+
|
|
7
|
+
export { STAGE_COLORS, STATUS_COLORS, TOAST_SCRIPT, fmtVal, statusLabel } from '@/ui/render-helpers.js'
|
|
8
|
+
export { generateHtml, breadcrumb, nav, page, fullPage, statCards, confirmDialog, dataTable } from '@/ui/layout.js'
|
|
9
|
+
export { renderLogin, renderPasswordReset, renderPasswordResetConfirm, renderAccessDenied } from '@/ui/auth-pages.js'
|
|
10
|
+
export { renderDashboard, renderAuditDashboard, renderSystemHealth } from '@/ui/dashboard-renderer.js'
|
|
11
|
+
export { renderEntityList, renderEntityDetail, renderEntityForm, renderSettings } from '@/ui/entity-renderer.js'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
linearProgress, circularProgress, engagementProgress, emptyState,
|
|
15
|
+
getUserAvatarUrl, userAvatar, teamAvatarGroup, infoBubble,
|
|
16
|
+
sortableList, responseChoiceBox, responseAttachment,
|
|
17
|
+
accordion, divider, responsiveClass, reviewCalcFields
|
|
18
|
+
} from '@/ui/widgets.js'
|
|
19
|
+
|
|
20
|
+
export { dataGridAdvanced, collapsibleSidebar } from '@/ui/advanced-widgets.js'
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
engagementCard, mobileEngagementCard, stagePipeline,
|
|
24
|
+
activityTimeline, splashScreen, swUpdateBanner
|
|
25
|
+
} from '@/ui/engagement-cards.js'
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
mobileReviewCard, sidebarReviewDetails, archiveReviewDialog,
|
|
29
|
+
reviewOpenCloseToggle, reviewPrivateToggle, markAllHighlightsResolved
|
|
30
|
+
} from '@/ui/review-widgets.js'
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
colorPickerDialog, dateChoiceDialog, stageTransitionDialog,
|
|
34
|
+
teamAssignmentDialog, teamSelector
|
|
35
|
+
} from '@/ui/picker-dialogs.js'
|