@unbrained/pm-web 1.0.0
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/CHANGELOG.md +7 -0
- package/README.md +107 -0
- package/dist/auth.js +20 -0
- package/dist/auth.js.map +1 -0
- package/dist/crypto.js +42 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.js +16 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/routes/admin.js +207 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.js +163 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/github.js +354 -0
- package/dist/routes/github.js.map +1 -0
- package/dist/routes/groups.js +180 -0
- package/dist/routes/groups.js.map +1 -0
- package/dist/routes/pm.js +2446 -0
- package/dist/routes/pm.js.map +1 -0
- package/dist/routes/projects.js +151 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/sharing.js +155 -0
- package/dist/routes/sharing.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/services/pm-runner.js +190 -0
- package/dist/services/pm-runner.js.map +1 -0
- package/dist/services/sse.js +111 -0
- package/dist/services/sse.js.map +1 -0
- package/manifest.json +15 -0
- package/package.json +111 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +265 -0
- package/public/manifest.json +66 -0
- package/public/src/api.js +28 -0
- package/public/src/api.js.map +1 -0
- package/public/src/api.ts +29 -0
- package/public/src/app.js +926 -0
- package/public/src/app.js.map +1 -0
- package/public/src/app.ts +929 -0
- package/public/src/components/modals.js +62 -0
- package/public/src/components/modals.js.map +1 -0
- package/public/src/components/modals.ts +73 -0
- package/public/src/components/toast.js +10 -0
- package/public/src/components/toast.js.map +1 -0
- package/public/src/components/toast.ts +13 -0
- package/public/src/constants.js +30 -0
- package/public/src/constants.js.map +1 -0
- package/public/src/constants.ts +41 -0
- package/public/src/state.js +15 -0
- package/public/src/state.js.map +1 -0
- package/public/src/state.ts +19 -0
- package/public/src/types.js +5 -0
- package/public/src/types.js.map +1 -0
- package/public/src/types.ts +253 -0
- package/public/src/utils.js +57 -0
- package/public/src/utils.js.map +1 -0
- package/public/src/utils.ts +56 -0
- package/public/src/views/activity.js +47 -0
- package/public/src/views/activity.js.map +1 -0
- package/public/src/views/activity.ts +41 -0
- package/public/src/views/admin.js +435 -0
- package/public/src/views/admin.js.map +1 -0
- package/public/src/views/admin.ts +504 -0
- package/public/src/views/auth.js +81 -0
- package/public/src/views/auth.js.map +1 -0
- package/public/src/views/auth.ts +74 -0
- package/public/src/views/calendar.js +133 -0
- package/public/src/views/calendar.js.map +1 -0
- package/public/src/views/calendar.ts +129 -0
- package/public/src/views/comments-audit.js +109 -0
- package/public/src/views/comments-audit.js.map +1 -0
- package/public/src/views/comments-audit.ts +108 -0
- package/public/src/views/config.js +322 -0
- package/public/src/views/config.js.map +1 -0
- package/public/src/views/config.ts +344 -0
- package/public/src/views/context.js +98 -0
- package/public/src/views/context.js.map +1 -0
- package/public/src/views/context.ts +100 -0
- package/public/src/views/create.js +293 -0
- package/public/src/views/create.js.map +1 -0
- package/public/src/views/create.ts +246 -0
- package/public/src/views/dedupe.js +51 -0
- package/public/src/views/dedupe.js.map +1 -0
- package/public/src/views/dedupe.ts +43 -0
- package/public/src/views/export.js +300 -0
- package/public/src/views/export.js.map +1 -0
- package/public/src/views/export.ts +274 -0
- package/public/src/views/github.js +360 -0
- package/public/src/views/github.js.map +1 -0
- package/public/src/views/github.ts +308 -0
- package/public/src/views/graph-canvas.js +1986 -0
- package/public/src/views/graph-canvas.js.map +1 -0
- package/public/src/views/graph-canvas.ts +2218 -0
- package/public/src/views/graph.js +1824 -0
- package/public/src/views/graph.js.map +1 -0
- package/public/src/views/graph.ts +1891 -0
- package/public/src/views/groups.js +186 -0
- package/public/src/views/groups.js.map +1 -0
- package/public/src/views/groups.ts +172 -0
- package/public/src/views/guide.js +151 -0
- package/public/src/views/guide.js.map +1 -0
- package/public/src/views/guide.ts +162 -0
- package/public/src/views/health.js +105 -0
- package/public/src/views/health.js.map +1 -0
- package/public/src/views/health.ts +102 -0
- package/public/src/views/items.js +1306 -0
- package/public/src/views/items.js.map +1 -0
- package/public/src/views/items.ts +1196 -0
- package/public/src/views/normalize.js +67 -0
- package/public/src/views/normalize.js.map +1 -0
- package/public/src/views/normalize.ts +58 -0
- package/public/src/views/plan.js +454 -0
- package/public/src/views/plan.js.map +1 -0
- package/public/src/views/plan.ts +496 -0
- package/public/src/views/projects.js +204 -0
- package/public/src/views/projects.js.map +1 -0
- package/public/src/views/projects.ts +196 -0
- package/public/src/views/router.js +227 -0
- package/public/src/views/router.js.map +1 -0
- package/public/src/views/router.ts +188 -0
- package/public/src/views/search.js +103 -0
- package/public/src/views/search.js.map +1 -0
- package/public/src/views/search.ts +94 -0
- package/public/src/views/settings.js +272 -0
- package/public/src/views/settings.js.map +1 -0
- package/public/src/views/settings.ts +190 -0
- package/public/src/views/shared.js +49 -0
- package/public/src/views/shared.js.map +1 -0
- package/public/src/views/shared.ts +49 -0
- package/public/src/views/sharing.js +152 -0
- package/public/src/views/sharing.js.map +1 -0
- package/public/src/views/sharing.ts +139 -0
- package/public/src/views/stats.js +92 -0
- package/public/src/views/stats.js.map +1 -0
- package/public/src/views/stats.ts +88 -0
- package/public/src/views/templates.js +117 -0
- package/public/src/views/templates.js.map +1 -0
- package/public/src/views/templates.ts +113 -0
- package/public/src/views/validate.js +54 -0
- package/public/src/views/validate.js.map +1 -0
- package/public/src/views/validate.ts +48 -0
- package/public/styles.css +2231 -0
- package/public/sw.js +318 -0
- package/public/tsconfig.json +20 -0
- package/sql/schema.sql +105 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// CALENDAR VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml, typeIcon } from '../utils.js';
|
|
7
|
+
import { skeletonRows } from '../utils.js';
|
|
8
|
+
import { showModal, createModal } from '../components/modals.js';
|
|
9
|
+
import { renderItemRow } from './items.js';
|
|
10
|
+
export async function renderCalendarView() {
|
|
11
|
+
const el = document.getElementById('content-calendar');
|
|
12
|
+
if (!el)
|
|
13
|
+
return;
|
|
14
|
+
if (!state.currentProject) {
|
|
15
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
el.innerHTML = `
|
|
19
|
+
<div class="page-header">
|
|
20
|
+
<div><div class="page-title">Calendar</div><div class="page-subtitle">Upcoming events & deadlines</div></div>
|
|
21
|
+
<div class="page-actions">
|
|
22
|
+
<button class="btn btn-ghost btn-sm" onclick="window.__app.calNav(-1)">← Prev</button>
|
|
23
|
+
<span id="cal-month-label" style="font-size:13px;font-weight:600;min-width:120px;text-align:center"></span>
|
|
24
|
+
<button class="btn btn-ghost btn-sm" onclick="window.__app.calNav(1)">Next →</button>
|
|
25
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.renderCalendarView()">↺ Today</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div id="calendar-content">${skeletonRows(6)}</div>`;
|
|
29
|
+
try {
|
|
30
|
+
const [itemsData, calData] = await Promise.all([
|
|
31
|
+
api('GET', `/projects/${state.currentProject.id}/pm/list-all?limit=9999`),
|
|
32
|
+
api('GET', `/projects/${state.currentProject.id}/pm/calendar`).catch(() => ({ events: [] })),
|
|
33
|
+
]);
|
|
34
|
+
const allItems = (itemsData.items || []).filter((i) => i.deadline || i.type === 'Event' || i.type === 'Meeting' || i.type === 'Reminder');
|
|
35
|
+
const calEvents = calData.events || calData.items || [];
|
|
36
|
+
state.calOffset = state.calOffset || 0;
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const year = now.getFullYear();
|
|
39
|
+
const month = now.getMonth() + state.calOffset;
|
|
40
|
+
const calDate = new Date(year, month, 1);
|
|
41
|
+
const monthLabel = calDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
|
42
|
+
const labelEl = document.getElementById('cal-month-label');
|
|
43
|
+
if (labelEl)
|
|
44
|
+
labelEl.textContent = monthLabel;
|
|
45
|
+
const firstDay = calDate.getDay();
|
|
46
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
47
|
+
const daysInPrev = new Date(year, month, 0).getDate();
|
|
48
|
+
const today = new Date();
|
|
49
|
+
const dateMap = {};
|
|
50
|
+
allItems.forEach((item) => {
|
|
51
|
+
if (item.deadline) {
|
|
52
|
+
const d = new Date(item.deadline).toISOString().slice(0, 10);
|
|
53
|
+
if (!dateMap[d])
|
|
54
|
+
dateMap[d] = [];
|
|
55
|
+
dateMap[d].push(item);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
let gridHtml = '<div class="cal-grid">';
|
|
59
|
+
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].forEach(d => {
|
|
60
|
+
gridHtml += `<div class="cal-header">${d}</div>`;
|
|
61
|
+
});
|
|
62
|
+
for (let i = firstDay - 1; i >= 0; i--) {
|
|
63
|
+
gridHtml += `<div class="cal-day other-month"><div class="cal-day-num">${daysInPrev - i}</div></div>`;
|
|
64
|
+
}
|
|
65
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
66
|
+
const dateStr = `${calDate.getFullYear()}-${String(calDate.getMonth() + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
67
|
+
const isToday = d === today.getDate() && month === today.getMonth() && year === today.getFullYear();
|
|
68
|
+
const dayItems = dateMap[dateStr] || [];
|
|
69
|
+
gridHtml += `<div class="cal-day${isToday ? ' today' : ''}" onclick="window.__app.showDayItems('${dateStr}')">`;
|
|
70
|
+
gridHtml += `<div class="cal-day-num">${d}</div>`;
|
|
71
|
+
dayItems.slice(0, 3).forEach((item) => {
|
|
72
|
+
gridHtml += `<div class="cal-event-dot status-${item.status}" title="${escHtml(item.title)}" onclick="event.stopPropagation();window.__app.openItemDetail('${escHtml(item.id)}')">${escHtml(item.title.slice(0, 12))}</div>`;
|
|
73
|
+
});
|
|
74
|
+
if (dayItems.length > 3) {
|
|
75
|
+
gridHtml += `<div class="cal-event-dot more">+${dayItems.length - 3} more</div>`;
|
|
76
|
+
}
|
|
77
|
+
gridHtml += '</div>';
|
|
78
|
+
}
|
|
79
|
+
const totalCells = firstDay + daysInMonth;
|
|
80
|
+
const remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
|
|
81
|
+
for (let i = 1; i <= remaining; i++) {
|
|
82
|
+
gridHtml += `<div class="cal-day other-month"><div class="cal-day-num">${i}</div></div>`;
|
|
83
|
+
}
|
|
84
|
+
gridHtml += '</div>';
|
|
85
|
+
let upcomingHtml = '';
|
|
86
|
+
if (calEvents.length > 0) {
|
|
87
|
+
upcomingHtml = '<div class="card" style="margin-top:16px"><div class="card-header"><div class="card-title">Upcoming Events</div></div><div class="card-body">';
|
|
88
|
+
calEvents.slice(0, 10).forEach((ev) => {
|
|
89
|
+
upcomingHtml += `<div class="calendar-event" onclick="window.__app.openItemDetail('${escHtml(ev.id || ev.itemId || '')}')">
|
|
90
|
+
<div class="calendar-event-id">${typeIcon(ev.type || '')} ${escHtml(ev.id || '')}</div>
|
|
91
|
+
<div class="calendar-event-title">${escHtml(ev.title || ev.name || '')}</div>
|
|
92
|
+
<div class="calendar-event-date">${escHtml(ev.date || ev.dueDate || ev.timestamp || '')}</div>
|
|
93
|
+
</div>`;
|
|
94
|
+
});
|
|
95
|
+
upcomingHtml += '</div></div>';
|
|
96
|
+
}
|
|
97
|
+
const contentEl = document.getElementById('calendar-content');
|
|
98
|
+
if (contentEl)
|
|
99
|
+
contentEl.innerHTML = gridHtml + upcomingHtml;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const contentEl = document.getElementById('calendar-content');
|
|
103
|
+
if (contentEl)
|
|
104
|
+
contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function calNav(dir) {
|
|
108
|
+
state.calOffset = (state.calOffset || 0) + dir;
|
|
109
|
+
renderCalendarView();
|
|
110
|
+
}
|
|
111
|
+
export function showDayItems(dateStr) {
|
|
112
|
+
if (!state.currentProject)
|
|
113
|
+
return;
|
|
114
|
+
createModal('day-items-modal', `Items due ${dateStr}`, '<div class="loading-state"><div class="loading-spinner"></div></div>', '', true);
|
|
115
|
+
showModal('day-items-modal');
|
|
116
|
+
api('GET', `/projects/${state.currentProject.id}/pm/list-all?limit=9999`).then(data => {
|
|
117
|
+
const items = (data.items || []).filter((i) => {
|
|
118
|
+
if (!i.deadline)
|
|
119
|
+
return false;
|
|
120
|
+
return new Date(i.deadline).toISOString().slice(0, 10) === dateStr;
|
|
121
|
+
});
|
|
122
|
+
const bodyEl = document.getElementById('day-items-modal')?.querySelector('.modal-body');
|
|
123
|
+
if (bodyEl)
|
|
124
|
+
bodyEl.innerHTML = items.length === 0
|
|
125
|
+
? '<div style="color:var(--text-muted);font-size:13px">No items due on this date</div>'
|
|
126
|
+
: `<div class="item-list">${items.map((item) => renderItemRow(item)).join('')}</div>`;
|
|
127
|
+
}).catch((err) => {
|
|
128
|
+
const bodyEl = document.getElementById('day-items-modal')?.querySelector('.modal-body');
|
|
129
|
+
if (bodyEl)
|
|
130
|
+
bodyEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=calendar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendar.js","sourceRoot":"","sources":["calendar.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// CALENDAR VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml, typeIcon, statusBadge } from '../utils.js';
|
|
7
|
+
import { skeletonRows } from '../utils.js';
|
|
8
|
+
import { showModal, createModal } from '../components/modals.js';
|
|
9
|
+
import { renderItemRow } from './items.js';
|
|
10
|
+
|
|
11
|
+
export async function renderCalendarView(): Promise<void> {
|
|
12
|
+
const el = document.getElementById('content-calendar');
|
|
13
|
+
if (!el) return;
|
|
14
|
+
if (!state.currentProject) { el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>'; return; }
|
|
15
|
+
el.innerHTML = `
|
|
16
|
+
<div class="page-header">
|
|
17
|
+
<div><div class="page-title">Calendar</div><div class="page-subtitle">Upcoming events & deadlines</div></div>
|
|
18
|
+
<div class="page-actions">
|
|
19
|
+
<button class="btn btn-ghost btn-sm" onclick="window.__app.calNav(-1)">← Prev</button>
|
|
20
|
+
<span id="cal-month-label" style="font-size:13px;font-weight:600;min-width:120px;text-align:center"></span>
|
|
21
|
+
<button class="btn btn-ghost btn-sm" onclick="window.__app.calNav(1)">Next →</button>
|
|
22
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.renderCalendarView()">↺ Today</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div id="calendar-content">${skeletonRows(6)}</div>`;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const [itemsData, calData] = await Promise.all([
|
|
29
|
+
api('GET',`/projects/${state.currentProject.id}/pm/list-all?limit=9999`),
|
|
30
|
+
api('GET',`/projects/${state.currentProject.id}/pm/calendar`).catch(()=>({events:[]})),
|
|
31
|
+
]);
|
|
32
|
+
const allItems = ((itemsData as any).items || []).filter((i: any) => i.deadline || i.type === 'Event' || i.type === 'Meeting' || i.type === 'Reminder');
|
|
33
|
+
const calEvents = (calData as any).events || (calData as any).items || [];
|
|
34
|
+
|
|
35
|
+
state.calOffset = state.calOffset || 0;
|
|
36
|
+
const now = new Date();
|
|
37
|
+
const year = now.getFullYear();
|
|
38
|
+
const month = now.getMonth() + state.calOffset;
|
|
39
|
+
const calDate = new Date(year, month, 1);
|
|
40
|
+
const monthLabel = calDate.toLocaleDateString('en-US', {month:'long', year:'numeric'});
|
|
41
|
+
const labelEl = document.getElementById('cal-month-label');
|
|
42
|
+
if (labelEl) labelEl.textContent = monthLabel;
|
|
43
|
+
|
|
44
|
+
const firstDay = calDate.getDay();
|
|
45
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
46
|
+
const daysInPrev = new Date(year, month, 0).getDate();
|
|
47
|
+
const today = new Date();
|
|
48
|
+
|
|
49
|
+
const dateMap: Record<string, any[]> = {};
|
|
50
|
+
allItems.forEach((item: any) => {
|
|
51
|
+
if (item.deadline) {
|
|
52
|
+
const d = new Date(item.deadline).toISOString().slice(0, 10);
|
|
53
|
+
if (!dateMap[d]) dateMap[d] = [];
|
|
54
|
+
dateMap[d].push(item);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let gridHtml = '<div class="cal-grid">';
|
|
59
|
+
['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d => {
|
|
60
|
+
gridHtml += `<div class="cal-header">${d}</div>`;
|
|
61
|
+
});
|
|
62
|
+
for (let i = firstDay - 1; i >= 0; i--) {
|
|
63
|
+
gridHtml += `<div class="cal-day other-month"><div class="cal-day-num">${daysInPrev - i}</div></div>`;
|
|
64
|
+
}
|
|
65
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
66
|
+
const dateStr = `${calDate.getFullYear()}-${String(calDate.getMonth()+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
|
67
|
+
const isToday = d === today.getDate() && month === today.getMonth() && year === today.getFullYear();
|
|
68
|
+
const dayItems = dateMap[dateStr] || [];
|
|
69
|
+
gridHtml += `<div class="cal-day${isToday?' today':''}" onclick="window.__app.showDayItems('${dateStr}')">`;
|
|
70
|
+
gridHtml += `<div class="cal-day-num">${d}</div>`;
|
|
71
|
+
dayItems.slice(0, 3).forEach((item: any) => {
|
|
72
|
+
gridHtml += `<div class="cal-event-dot status-${item.status}" title="${escHtml(item.title)}" onclick="event.stopPropagation();window.__app.openItemDetail('${escHtml(item.id)}')">${escHtml(item.title.slice(0,12))}</div>`;
|
|
73
|
+
});
|
|
74
|
+
if (dayItems.length > 3) {
|
|
75
|
+
gridHtml += `<div class="cal-event-dot more">+${dayItems.length - 3} more</div>`;
|
|
76
|
+
}
|
|
77
|
+
gridHtml += '</div>';
|
|
78
|
+
}
|
|
79
|
+
const totalCells = firstDay + daysInMonth;
|
|
80
|
+
const remaining = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
|
|
81
|
+
for (let i = 1; i <= remaining; i++) {
|
|
82
|
+
gridHtml += `<div class="cal-day other-month"><div class="cal-day-num">${i}</div></div>`;
|
|
83
|
+
}
|
|
84
|
+
gridHtml += '</div>';
|
|
85
|
+
|
|
86
|
+
let upcomingHtml = '';
|
|
87
|
+
if (calEvents.length > 0) {
|
|
88
|
+
upcomingHtml = '<div class="card" style="margin-top:16px"><div class="card-header"><div class="card-title">Upcoming Events</div></div><div class="card-body">';
|
|
89
|
+
calEvents.slice(0, 10).forEach((ev: any) => {
|
|
90
|
+
upcomingHtml += `<div class="calendar-event" onclick="window.__app.openItemDetail('${escHtml(ev.id||ev.itemId||'')}')">
|
|
91
|
+
<div class="calendar-event-id">${typeIcon(ev.type||'')} ${escHtml(ev.id||'')}</div>
|
|
92
|
+
<div class="calendar-event-title">${escHtml(ev.title||ev.name||'')}</div>
|
|
93
|
+
<div class="calendar-event-date">${escHtml(ev.date||ev.dueDate||ev.timestamp||'')}</div>
|
|
94
|
+
</div>`;
|
|
95
|
+
});
|
|
96
|
+
upcomingHtml += '</div></div>';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const contentEl = document.getElementById('calendar-content');
|
|
100
|
+
if (contentEl) contentEl.innerHTML = gridHtml + upcomingHtml;
|
|
101
|
+
} catch(err: unknown) {
|
|
102
|
+
const contentEl = document.getElementById('calendar-content');
|
|
103
|
+
if (contentEl) contentEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function calNav(dir: number): void {
|
|
108
|
+
state.calOffset = (state.calOffset || 0) + dir;
|
|
109
|
+
renderCalendarView();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function showDayItems(dateStr: string): void {
|
|
113
|
+
if (!state.currentProject) return;
|
|
114
|
+
createModal('day-items-modal', `Items due ${dateStr}`, '<div class="loading-state"><div class="loading-spinner"></div></div>', '', true);
|
|
115
|
+
showModal('day-items-modal');
|
|
116
|
+
api('GET', `/projects/${state.currentProject.id}/pm/list-all?limit=9999`).then(data => {
|
|
117
|
+
const items = ((data as any).items || []).filter((i: any) => {
|
|
118
|
+
if (!i.deadline) return false;
|
|
119
|
+
return new Date(i.deadline).toISOString().slice(0,10) === dateStr;
|
|
120
|
+
});
|
|
121
|
+
const bodyEl = document.getElementById('day-items-modal')?.querySelector('.modal-body');
|
|
122
|
+
if (bodyEl) bodyEl.innerHTML = items.length === 0
|
|
123
|
+
? '<div style="color:var(--text-muted);font-size:13px">No items due on this date</div>'
|
|
124
|
+
: `<div class="item-list">${items.map((item: any) => renderItemRow(item)).join('')}</div>`;
|
|
125
|
+
}).catch((err: unknown) => {
|
|
126
|
+
const bodyEl = document.getElementById('day-items-modal')?.querySelector('.modal-body');
|
|
127
|
+
if (bodyEl) bodyEl.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// COMMENTS AUDIT VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml, statusBadge, typeIcon } from '../utils.js';
|
|
7
|
+
export async function renderCommentsAuditView() {
|
|
8
|
+
const el = document.getElementById('content-comments-audit');
|
|
9
|
+
if (!el)
|
|
10
|
+
return;
|
|
11
|
+
if (!state.currentProject) {
|
|
12
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
el.innerHTML = `
|
|
16
|
+
<div class="page-header">
|
|
17
|
+
<div>
|
|
18
|
+
<div class="page-title">Comments Audit</div>
|
|
19
|
+
<div class="page-subtitle">Review comment coverage across all items in ${escHtml(state.currentProject.name)}</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="page-actions">
|
|
22
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.renderCommentsAuditView()">↺ Refresh</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div id="comments-audit-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
|
|
26
|
+
try {
|
|
27
|
+
const data = await api('GET', `/projects/${state.currentProject.id}/pm/comments-audit`);
|
|
28
|
+
const items = data.items || [];
|
|
29
|
+
const summary = data.summary || {};
|
|
30
|
+
const totals = summary.totals || {};
|
|
31
|
+
const coverage = summary.coverage || {};
|
|
32
|
+
const byType = summary.by_type || [];
|
|
33
|
+
const el2 = document.getElementById('comments-audit-content');
|
|
34
|
+
if (!el2)
|
|
35
|
+
return;
|
|
36
|
+
const coveragePct = Math.round((coverage.items_with_comments_percent || 0));
|
|
37
|
+
el2.innerHTML = `
|
|
38
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-bottom:20px">
|
|
39
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
40
|
+
<div style="font-size:28px;font-weight:700;color:var(--accent)">${totals.items_scanned || 0}</div>
|
|
41
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">Items Scanned</div>
|
|
42
|
+
</div></div>
|
|
43
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
44
|
+
<div style="font-size:28px;font-weight:700;color:var(--status-open)">${totals.items_with_comments || 0}</div>
|
|
45
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">With Comments</div>
|
|
46
|
+
</div></div>
|
|
47
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
48
|
+
<div style="font-size:28px;font-weight:700;color:var(--text-secondary)">${totals.comments_total || 0}</div>
|
|
49
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">Total Comments</div>
|
|
50
|
+
</div></div>
|
|
51
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
52
|
+
<div style="font-size:28px;font-weight:700;color:${coveragePct >= 50 ? 'var(--status-closed)' : 'var(--status-open)'}">${coveragePct}%</div>
|
|
53
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">Coverage</div>
|
|
54
|
+
</div></div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
${byType.length > 0 ? `
|
|
58
|
+
<div class="card" style="margin-bottom:16px">
|
|
59
|
+
<div class="card-header"><div class="card-title">By Type</div></div>
|
|
60
|
+
<div class="card-body" style="padding:0">
|
|
61
|
+
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
|
62
|
+
<thead>
|
|
63
|
+
<tr style="border-bottom:1px solid var(--border)">
|
|
64
|
+
<th style="padding:8px 14px;text-align:left;color:var(--text-muted);font-weight:500">Type</th>
|
|
65
|
+
<th style="padding:8px 14px;text-align:right;color:var(--text-muted);font-weight:500">Items</th>
|
|
66
|
+
<th style="padding:8px 14px;text-align:right;color:var(--text-muted);font-weight:500">With Comments</th>
|
|
67
|
+
<th style="padding:8px 14px;text-align:right;color:var(--text-muted);font-weight:500">Total Comments</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
${byType.map((row) => `
|
|
72
|
+
<tr style="border-bottom:1px solid var(--border-subtle)">
|
|
73
|
+
<td style="padding:8px 14px">${typeIcon(row.type || '')} ${escHtml(row.type || '')}</td>
|
|
74
|
+
<td style="padding:8px 14px;text-align:right">${row.items_scanned || 0}</td>
|
|
75
|
+
<td style="padding:8px 14px;text-align:right">${row.items_with_comments || 0}</td>
|
|
76
|
+
<td style="padding:8px 14px;text-align:right">${row.comments_total || 0}</td>
|
|
77
|
+
</tr>`).join('')}
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
</div>` : ''}
|
|
82
|
+
|
|
83
|
+
<div class="card">
|
|
84
|
+
<div class="card-header"><div class="card-title">Items (${items.length})</div></div>
|
|
85
|
+
<div class="card-body" style="padding:0">
|
|
86
|
+
${items.length === 0
|
|
87
|
+
? '<div style="padding:20px;color:var(--text-muted);font-size:13px;text-align:center">No items found</div>'
|
|
88
|
+
: `<div class="item-list" style="border-radius:0">
|
|
89
|
+
${items.map((item) => `
|
|
90
|
+
<div class="item-row" onclick="window.__app.openItemDetail('${escHtml(item.id)}')">
|
|
91
|
+
${typeIcon(item.type || '')}
|
|
92
|
+
<span class="item-id">${escHtml(item.id)}</span>
|
|
93
|
+
<span class="item-title">${escHtml(item.title)}</span>
|
|
94
|
+
<div class="item-meta">
|
|
95
|
+
${statusBadge(item.status || 'draft')}
|
|
96
|
+
<span style="font-size:11px;color:var(--text-muted);background:var(--bg-input);padding:2px 8px;border-radius:10px">${item.comment_count || 0} comment${item.comment_count !== 1 ? 's' : ''}</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>`).join('')}
|
|
99
|
+
</div>`}
|
|
100
|
+
</div>
|
|
101
|
+
</div>`;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const el2 = document.getElementById('comments-audit-content');
|
|
105
|
+
if (el2)
|
|
106
|
+
el2.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=comments-audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments-audit.js","sourceRoot":"","sources":["comments-audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// COMMENTS AUDIT VIEW
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
import { state } from '../state.js';
|
|
5
|
+
import { api } from '../api.js';
|
|
6
|
+
import { escHtml, statusBadge, typeIcon } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
export async function renderCommentsAuditView(): Promise<void> {
|
|
9
|
+
const el = document.getElementById('content-comments-audit');
|
|
10
|
+
if (!el) return;
|
|
11
|
+
if (!state.currentProject) {
|
|
12
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-text">No project selected</div></div>';
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
el.innerHTML = `
|
|
16
|
+
<div class="page-header">
|
|
17
|
+
<div>
|
|
18
|
+
<div class="page-title">Comments Audit</div>
|
|
19
|
+
<div class="page-subtitle">Review comment coverage across all items in ${escHtml(state.currentProject.name)}</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="page-actions">
|
|
22
|
+
<button class="btn btn-secondary btn-sm" onclick="window.__app.renderCommentsAuditView()">↺ Refresh</button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div id="comments-audit-content"><div class="loading-state"><div class="loading-spinner"></div></div></div>`;
|
|
26
|
+
try {
|
|
27
|
+
const data = await api('GET', `/projects/${state.currentProject.id}/pm/comments-audit`);
|
|
28
|
+
const items: any[] = (data as any).items || [];
|
|
29
|
+
const summary: any = (data as any).summary || {};
|
|
30
|
+
const totals = summary.totals || {};
|
|
31
|
+
const coverage = summary.coverage || {};
|
|
32
|
+
const byType: any[] = summary.by_type || [];
|
|
33
|
+
|
|
34
|
+
const el2 = document.getElementById('comments-audit-content');
|
|
35
|
+
if (!el2) return;
|
|
36
|
+
|
|
37
|
+
const coveragePct = Math.round((coverage.items_with_comments_percent || 0));
|
|
38
|
+
|
|
39
|
+
el2.innerHTML = `
|
|
40
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-bottom:20px">
|
|
41
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
42
|
+
<div style="font-size:28px;font-weight:700;color:var(--accent)">${totals.items_scanned || 0}</div>
|
|
43
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">Items Scanned</div>
|
|
44
|
+
</div></div>
|
|
45
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
46
|
+
<div style="font-size:28px;font-weight:700;color:var(--status-open)">${totals.items_with_comments || 0}</div>
|
|
47
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">With Comments</div>
|
|
48
|
+
</div></div>
|
|
49
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
50
|
+
<div style="font-size:28px;font-weight:700;color:var(--text-secondary)">${totals.comments_total || 0}</div>
|
|
51
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">Total Comments</div>
|
|
52
|
+
</div></div>
|
|
53
|
+
<div class="card"><div class="card-body" style="text-align:center">
|
|
54
|
+
<div style="font-size:28px;font-weight:700;color:${coveragePct >= 50 ? 'var(--status-closed)' : 'var(--status-open)'}">${coveragePct}%</div>
|
|
55
|
+
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">Coverage</div>
|
|
56
|
+
</div></div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
${byType.length > 0 ? `
|
|
60
|
+
<div class="card" style="margin-bottom:16px">
|
|
61
|
+
<div class="card-header"><div class="card-title">By Type</div></div>
|
|
62
|
+
<div class="card-body" style="padding:0">
|
|
63
|
+
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
|
64
|
+
<thead>
|
|
65
|
+
<tr style="border-bottom:1px solid var(--border)">
|
|
66
|
+
<th style="padding:8px 14px;text-align:left;color:var(--text-muted);font-weight:500">Type</th>
|
|
67
|
+
<th style="padding:8px 14px;text-align:right;color:var(--text-muted);font-weight:500">Items</th>
|
|
68
|
+
<th style="padding:8px 14px;text-align:right;color:var(--text-muted);font-weight:500">With Comments</th>
|
|
69
|
+
<th style="padding:8px 14px;text-align:right;color:var(--text-muted);font-weight:500">Total Comments</th>
|
|
70
|
+
</tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
${byType.map((row: any) => `
|
|
74
|
+
<tr style="border-bottom:1px solid var(--border-subtle)">
|
|
75
|
+
<td style="padding:8px 14px">${typeIcon(row.type || '')} ${escHtml(row.type || '')}</td>
|
|
76
|
+
<td style="padding:8px 14px;text-align:right">${row.items_scanned || 0}</td>
|
|
77
|
+
<td style="padding:8px 14px;text-align:right">${row.items_with_comments || 0}</td>
|
|
78
|
+
<td style="padding:8px 14px;text-align:right">${row.comments_total || 0}</td>
|
|
79
|
+
</tr>`).join('')}
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
</div>` : ''}
|
|
84
|
+
|
|
85
|
+
<div class="card">
|
|
86
|
+
<div class="card-header"><div class="card-title">Items (${items.length})</div></div>
|
|
87
|
+
<div class="card-body" style="padding:0">
|
|
88
|
+
${items.length === 0
|
|
89
|
+
? '<div style="padding:20px;color:var(--text-muted);font-size:13px;text-align:center">No items found</div>'
|
|
90
|
+
: `<div class="item-list" style="border-radius:0">
|
|
91
|
+
${items.map((item: any) => `
|
|
92
|
+
<div class="item-row" onclick="window.__app.openItemDetail('${escHtml(item.id)}')">
|
|
93
|
+
${typeIcon(item.type || '')}
|
|
94
|
+
<span class="item-id">${escHtml(item.id)}</span>
|
|
95
|
+
<span class="item-title">${escHtml(item.title)}</span>
|
|
96
|
+
<div class="item-meta">
|
|
97
|
+
${statusBadge(item.status || 'draft')}
|
|
98
|
+
<span style="font-size:11px;color:var(--text-muted);background:var(--bg-input);padding:2px 8px;border-radius:10px">${item.comment_count || 0} comment${item.comment_count !== 1 ? 's' : ''}</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>`).join('')}
|
|
101
|
+
</div>`}
|
|
102
|
+
</div>
|
|
103
|
+
</div>`;
|
|
104
|
+
} catch(err: unknown) {
|
|
105
|
+
const el2 = document.getElementById('comments-audit-content');
|
|
106
|
+
if (el2) el2.innerHTML = `<div class="empty-state"><div class="empty-state-text">Error: ${escHtml(err instanceof Error ? err.message : String(err))}</div></div>`;
|
|
107
|
+
}
|
|
108
|
+
}
|