@unson/brainbase-mcp 0.41.6 → 0.41.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/ui/brainbase-tokens.css +2 -0
- package/ui/meeting-minutes.css +293 -60
- package/ui/meeting-minutes.js +555 -201
package/ui/meeting-minutes.js
CHANGED
|
@@ -4,10 +4,43 @@
|
|
|
4
4
|
* The screen only speaks the provider-independent HTTP contract. A host
|
|
5
5
|
* supplies the fetcher, token and base path; no calendar, repository, or
|
|
6
6
|
* organization connector is assumed here.
|
|
7
|
+
*
|
|
8
|
+
* List order and labels are a projection. Canonical records are not rewritten.
|
|
9
|
+
* Calendar links stay out until a meeting-side event id is verified.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
export const MEETING_MINUTES_UI_CONTRACT_VERSION = 'brainbase.meeting-minutes-ui.v1';
|
|
10
13
|
|
|
14
|
+
const WEEKDAYS = ['日', '月', '火', '水', '木', '金', '土'];
|
|
15
|
+
const TOKYO = 'Asia/Tokyo';
|
|
16
|
+
|
|
17
|
+
function tokyoParts(date) {
|
|
18
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
19
|
+
timeZone: TOKYO,
|
|
20
|
+
year: 'numeric',
|
|
21
|
+
month: '2-digit',
|
|
22
|
+
day: '2-digit',
|
|
23
|
+
hour: '2-digit',
|
|
24
|
+
minute: '2-digit',
|
|
25
|
+
hourCycle: 'h23',
|
|
26
|
+
}).formatToParts(date);
|
|
27
|
+
const value = (type) => parts.find((part) => part.type === type)?.value ?? '';
|
|
28
|
+
let hour = Number(value('hour'));
|
|
29
|
+
if (hour === 24) hour = 0;
|
|
30
|
+
return {
|
|
31
|
+
year: Number(value('year')),
|
|
32
|
+
month: Number(value('month')),
|
|
33
|
+
day: Number(value('day')),
|
|
34
|
+
hour,
|
|
35
|
+
minute: Number(value('minute')),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tokyoDayNumber(date) {
|
|
40
|
+
const { year, month, day } = tokyoParts(date);
|
|
41
|
+
return Math.floor(Date.UTC(year, month - 1, day) / 86400000);
|
|
42
|
+
}
|
|
43
|
+
|
|
11
44
|
function isRecord(value) {
|
|
12
45
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
46
|
}
|
|
@@ -62,18 +95,161 @@ function labelInput(doc, label, { type = 'text', value = '', name, placeholder }
|
|
|
62
95
|
return { wrapper, input };
|
|
63
96
|
}
|
|
64
97
|
|
|
65
|
-
function
|
|
66
|
-
if (!value) return
|
|
98
|
+
function parseDate(value) {
|
|
99
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
67
100
|
const date = new Date(value);
|
|
68
|
-
return Number.isNaN(date.getTime()) ?
|
|
101
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
69
102
|
}
|
|
70
103
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
function pad(part) {
|
|
105
|
+
return String(part).padStart(2, '0');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatClock(date) {
|
|
109
|
+
const { hour, minute } = tokyoParts(date);
|
|
110
|
+
return `${pad(hour)}:${pad(minute)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatMonthDay(date) {
|
|
114
|
+
const { month, day } = tokyoParts(date);
|
|
115
|
+
return `${month}/${day}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatStamp(value) {
|
|
119
|
+
const date = parseDate(value);
|
|
120
|
+
return date ? `${formatMonthDay(date)} ${formatClock(date)}` : '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function dayHeading(date, today) {
|
|
124
|
+
const { year, month, day } = tokyoParts(date);
|
|
125
|
+
const weekday = WEEKDAYS[new Date(Date.UTC(year, month - 1, day)).getUTCDay()];
|
|
126
|
+
const label = `${month}/${day}(${weekday})`;
|
|
127
|
+
return tokyoDayNumber(date) === tokyoDayNumber(today) ? `今日 ${label}` : label;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dateKey(date) {
|
|
131
|
+
const { year, month, day } = tokyoParts(date);
|
|
132
|
+
return `${year}-${month}-${day}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function relativeDay(date, today) {
|
|
136
|
+
const days = tokyoDayNumber(today) - tokyoDayNumber(date);
|
|
137
|
+
if (days === 0) return '今日';
|
|
138
|
+
if (days === 1) return '昨日';
|
|
139
|
+
if (days > 1) return `${days}日前`;
|
|
140
|
+
if (days === -1) return '明日';
|
|
141
|
+
return `${Math.abs(days)}日後`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function httpUrl(value) {
|
|
145
|
+
const raw = text(value);
|
|
146
|
+
if (!/^https?:\/\//i.test(raw)) return '';
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(raw);
|
|
149
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return '';
|
|
150
|
+
return url.href;
|
|
151
|
+
} catch {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function firstDate(values) {
|
|
157
|
+
for (const value of values) {
|
|
158
|
+
const date = parseDate(value);
|
|
159
|
+
if (date) return date;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function calendarStart(meeting) {
|
|
165
|
+
const meta = isRecord(meeting?.metadata) ? meeting.metadata : {};
|
|
166
|
+
const event = isRecord(meeting?.calendar_event) ? meeting.calendar_event
|
|
167
|
+
: isRecord(meta.calendar_event) ? meta.calendar_event : null;
|
|
168
|
+
return firstDate([
|
|
169
|
+
meeting?.calendar_start,
|
|
170
|
+
event?.start,
|
|
171
|
+
event?.starts_at,
|
|
172
|
+
event?.start_at,
|
|
173
|
+
meta.calendar_start,
|
|
174
|
+
meta.event_start,
|
|
175
|
+
meta.scheduled_start,
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function listedVersions(entry) {
|
|
180
|
+
const versions = [];
|
|
181
|
+
if (Array.isArray(entry?.versions)) versions.push(...entry.versions.filter(isRecord));
|
|
182
|
+
for (const documentRecord of Array.isArray(entry?.minutes) ? entry.minutes : []) {
|
|
183
|
+
if (Array.isArray(documentRecord?.versions)) versions.push(...documentRecord.versions.filter(isRecord));
|
|
184
|
+
}
|
|
185
|
+
return versions;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function latestVersionTime(entry) {
|
|
189
|
+
let latest = null;
|
|
190
|
+
for (const version of listedVersions(entry)) {
|
|
191
|
+
const date = parseDate(version.created_at);
|
|
192
|
+
if (date && (!latest || date > latest)) latest = date;
|
|
193
|
+
}
|
|
194
|
+
return latest ?? firstDate([
|
|
195
|
+
entry?.latest_version_created_at,
|
|
196
|
+
entry?.meeting?.latest_version_created_at,
|
|
197
|
+
isRecord(entry?.meeting?.metadata) ? entry.meeting.metadata.latest_version_created_at : null,
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function importTime(entry) {
|
|
202
|
+
let latest = parseDate(entry?.meeting?.created_at);
|
|
203
|
+
for (const documentRecord of Array.isArray(entry?.minutes) ? entry.minutes : []) {
|
|
204
|
+
const date = parseDate(documentRecord?.created_at);
|
|
205
|
+
if (date && (!latest || date > latest)) latest = date;
|
|
206
|
+
}
|
|
207
|
+
return latest;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function meetingTiming(entry) {
|
|
211
|
+
const meeting = isRecord(entry?.meeting) ? entry.meeting : {};
|
|
212
|
+
const scheduled = parseDate(meeting.scheduled_at);
|
|
213
|
+
if (scheduled) return { kind: 'scheduled', source: 'scheduled', at: scheduled.getTime(), date: scheduled };
|
|
214
|
+
const calendar = calendarStart(meeting);
|
|
215
|
+
if (calendar) return { kind: 'fallback', source: 'calendar', at: calendar.getTime(), date: calendar };
|
|
216
|
+
const versionAt = latestVersionTime(entry);
|
|
217
|
+
if (versionAt) return { kind: 'fallback', source: 'version', at: versionAt.getTime(), date: versionAt };
|
|
218
|
+
const imported = importTime(entry);
|
|
219
|
+
return { kind: 'unregistered', source: 'import', at: imported ? imported.getTime() : Number.NEGATIVE_INFINITY, date: imported };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function unscheduledLabel(timing) {
|
|
223
|
+
if (!timing?.date) return '会議の日時が未登録';
|
|
224
|
+
const day = formatMonthDay(timing.date);
|
|
225
|
+
if (timing.source === 'calendar') return `日時未登録・予定の開始 ${day}`;
|
|
226
|
+
if (timing.source === 'version') return `日時未登録・版の作成 ${day}`;
|
|
227
|
+
return `日時未登録・取り込み ${day}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function byCreatedAt(versions) {
|
|
231
|
+
return versions
|
|
232
|
+
.map((version, index) => ({ version, index }))
|
|
233
|
+
.sort((left, right) => {
|
|
234
|
+
const leftTime = parseDate(left.version?.created_at)?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
235
|
+
const rightTime = parseDate(right.version?.created_at)?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
236
|
+
if (leftTime !== rightTime) return leftTime - rightTime;
|
|
237
|
+
return left.index - right.index;
|
|
238
|
+
})
|
|
239
|
+
.map((item) => item.version);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sortMeetings(meetings) {
|
|
243
|
+
return meetings
|
|
244
|
+
.map((entry, index) => ({ entry, index, timing: meetingTiming(entry) }))
|
|
245
|
+
.sort((left, right) => {
|
|
246
|
+
const leftBottom = left.timing.kind === 'unregistered';
|
|
247
|
+
const rightBottom = right.timing.kind === 'unregistered';
|
|
248
|
+
if (leftBottom !== rightBottom) return leftBottom ? 1 : -1;
|
|
249
|
+
if (left.timing.at !== right.timing.at) return right.timing.at - left.timing.at;
|
|
250
|
+
return left.index - right.index;
|
|
251
|
+
})
|
|
252
|
+
.map((item) => item.entry);
|
|
77
253
|
}
|
|
78
254
|
|
|
79
255
|
function idempotencyKey() {
|
|
@@ -113,25 +289,71 @@ function preserveLoadedStateOnFailure(error) {
|
|
|
113
289
|
function sourceLabel(source) {
|
|
114
290
|
if (!isRecord(source)) return '';
|
|
115
291
|
const provider = text(source.provider, '外部ソース');
|
|
116
|
-
const locator = text(source.locator, '
|
|
117
|
-
const revision = text(source.revision, '
|
|
292
|
+
const locator = text(source.locator, '場所は記録されていません');
|
|
293
|
+
const revision = text(source.revision, '版は記録されていません');
|
|
118
294
|
return `${provider} / ${locator} / ${revision}`;
|
|
119
295
|
}
|
|
120
296
|
|
|
297
|
+
function renderMarkdown(doc, body) {
|
|
298
|
+
const root = makeElement(doc, 'div', { className: 'bb-minutes-markdown' });
|
|
299
|
+
const lines = String(body).replace(/\r\n/g, '\n').split('\n');
|
|
300
|
+
let list = null;
|
|
301
|
+
let listKind = null;
|
|
302
|
+
const closeList = () => {
|
|
303
|
+
if (list) root.append(list);
|
|
304
|
+
list = null;
|
|
305
|
+
listKind = null;
|
|
306
|
+
};
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
if (/^\s*[-*_]{3,}\s*$/u.test(line)) {
|
|
309
|
+
closeList();
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const heading = /^(#{1,6})\s+(.+)$/u.exec(line);
|
|
313
|
+
if (heading) {
|
|
314
|
+
closeList();
|
|
315
|
+
const level = Math.min(heading[1].length, 3);
|
|
316
|
+
root.append(makeElement(doc, `h${level + 2}`, {
|
|
317
|
+
className: 'bb-minutes-md-heading',
|
|
318
|
+
textContent: heading[2].trim(),
|
|
319
|
+
}));
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const bullet = /^\s*[-*]\s+(.+)$/u.exec(line);
|
|
323
|
+
const numbered = /^\s*\d+[.)]\s+(.+)$/u.exec(line);
|
|
324
|
+
if (bullet || numbered) {
|
|
325
|
+
const kind = bullet ? 'ul' : 'ol';
|
|
326
|
+
if (!list || listKind !== kind) {
|
|
327
|
+
closeList();
|
|
328
|
+
list = makeElement(doc, kind, { className: 'bb-minutes-md-list' });
|
|
329
|
+
listKind = kind;
|
|
330
|
+
}
|
|
331
|
+
list.append(makeElement(doc, 'li', { textContent: (bullet ?? numbered)[1].trim() }));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (!line.trim()) {
|
|
335
|
+
closeList();
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
closeList();
|
|
339
|
+
root.append(makeElement(doc, 'p', { className: 'bb-minutes-md-paragraph', textContent: line }));
|
|
340
|
+
}
|
|
341
|
+
closeList();
|
|
342
|
+
return root;
|
|
343
|
+
}
|
|
344
|
+
|
|
121
345
|
function versionContent(doc, version) {
|
|
122
346
|
if (!isRecord(version)) return makeElement(doc, 'p', { textContent: '版の形式を確認できません。', className: 'bb-minutes-warning' });
|
|
123
347
|
if (isRecord(version.source_ref)) {
|
|
124
|
-
return makeElement(doc, 'p', { className: 'bb-minutes-source', textContent:
|
|
125
|
-
}
|
|
126
|
-
if (typeof version.body === 'string') {
|
|
127
|
-
return makeElement(doc, 'pre', { className: 'bb-minutes-body', textContent: version.body });
|
|
348
|
+
return makeElement(doc, 'p', { className: 'bb-minutes-source', textContent: `本文は元の記録にあります。${sourceLabel(version.source_ref)}` });
|
|
128
349
|
}
|
|
350
|
+
if (typeof version.body === 'string') return renderMarkdown(doc, version.body);
|
|
129
351
|
return makeElement(doc, 'p', { className: 'bb-minutes-warning', textContent: '本文または外部参照を確認できません。' });
|
|
130
352
|
}
|
|
131
353
|
|
|
132
354
|
function detailVersions(detail, minutesId) {
|
|
133
355
|
if (!isRecord(detail) || !Array.isArray(detail.versions)) return [];
|
|
134
|
-
return detail.versions.filter((version) => version?.minutes_id === minutesId);
|
|
356
|
+
return byCreatedAt(detail.versions.filter((version) => version?.minutes_id === minutesId));
|
|
135
357
|
}
|
|
136
358
|
|
|
137
359
|
function versionLabel(versions, version) {
|
|
@@ -139,6 +361,82 @@ function versionLabel(versions, version) {
|
|
|
139
361
|
return index >= 0 ? `第${index + 1}版` : '版';
|
|
140
362
|
}
|
|
141
363
|
|
|
364
|
+
function confirmationDetail(confirmation) {
|
|
365
|
+
const when = formatStamp(confirmation?.confirmed_at) || text(confirmation?.confirmed_at);
|
|
366
|
+
const person = text(confirmation?.actor_id);
|
|
367
|
+
if (when && person) return `${when} ${person}が確認`;
|
|
368
|
+
if (when) return `${when} 確認した人の記録がありません`;
|
|
369
|
+
if (person) return `${person}が確認`;
|
|
370
|
+
return '確認済み';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function versionState(version, versions, latest) {
|
|
374
|
+
if (!isRecord(version)) return { code: 'unknown', label: '確認の状態を確かめられません', tone: 'muted' };
|
|
375
|
+
if (version.creating === true || version.status === 'creating') {
|
|
376
|
+
return { code: 'creating', label: '版の作成中', tone: 'muted' };
|
|
377
|
+
}
|
|
378
|
+
if (version.confirmation != null && !isRecord(version.confirmation)) {
|
|
379
|
+
return { code: 'unknown', label: '確認の状態を確かめられません', tone: 'muted' };
|
|
380
|
+
}
|
|
381
|
+
const isLatest = Boolean(latest) && version.version_id === latest.version_id;
|
|
382
|
+
if (isRecord(version.confirmation)) {
|
|
383
|
+
return { code: 'confirmed', label: '確認済み', tone: 'ok', detail: confirmationDetail(version.confirmation) };
|
|
384
|
+
}
|
|
385
|
+
if (!isLatest && latest) {
|
|
386
|
+
return { code: 'replaced', label: `確認されないまま${versionLabel(versions, latest)}に置き換え`, tone: 'muted' };
|
|
387
|
+
}
|
|
388
|
+
return { code: 'pending', label: '確認待ち', tone: 'wait' };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function creatorFact(version) {
|
|
392
|
+
const creator = text(version?.created_actor_id);
|
|
393
|
+
return creator ? `作成 ${creator}` : '作成者は未記録';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function historyStamp(version, today) {
|
|
397
|
+
const created = parseDate(version?.created_at);
|
|
398
|
+
return created
|
|
399
|
+
? `${formatMonthDay(created)} ${formatClock(created)} 作成(${relativeDay(created, today)})`
|
|
400
|
+
: '作成日時は未記録';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function historySecondLine(version, versions, latest) {
|
|
404
|
+
const state = versionState(version, versions, latest);
|
|
405
|
+
const confirmed = state.code === 'confirmed'
|
|
406
|
+
? (state.detail || confirmationDetail(version.confirmation))
|
|
407
|
+
: state.label;
|
|
408
|
+
const parts = [creatorFact(version), confirmed];
|
|
409
|
+
if (version?.predecessor_version_id) {
|
|
410
|
+
const predecessor = versions.find((candidate) => candidate.version_id === version.predecessor_version_id);
|
|
411
|
+
parts.push(`${versionLabel(versions, predecessor)}を訂正`);
|
|
412
|
+
}
|
|
413
|
+
return parts.join('・');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function listedVersionNumber(entry) {
|
|
417
|
+
const versions = listedVersions(entry);
|
|
418
|
+
const documents = Array.isArray(entry?.minutes) ? entry.minutes : [];
|
|
419
|
+
if (versions.length >= 2) {
|
|
420
|
+
const currentId = documents.map((documentRecord) => text(documentRecord?.current_version_id)).find(Boolean);
|
|
421
|
+
const index = versions.findIndex((version) => version.version_id === currentId);
|
|
422
|
+
return index >= 0 ? index + 1 : versions.length;
|
|
423
|
+
}
|
|
424
|
+
const count = documents.reduce((max, documentRecord) => Math.max(
|
|
425
|
+
max,
|
|
426
|
+
Array.isArray(documentRecord?.version_ids) ? documentRecord.version_ids.length : 0,
|
|
427
|
+
), 0);
|
|
428
|
+
return count >= 2 ? count : 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function listState(entry) {
|
|
432
|
+
const versions = listedVersions(entry);
|
|
433
|
+
if (!versions.length) return null;
|
|
434
|
+
const documents = Array.isArray(entry?.minutes) ? entry.minutes : [];
|
|
435
|
+
const currentId = documents.map((documentRecord) => text(documentRecord?.current_version_id)).find(Boolean);
|
|
436
|
+
const latest = versions.find((version) => version.version_id === currentId) ?? versions[versions.length - 1];
|
|
437
|
+
return versionState(latest, versions, latest);
|
|
438
|
+
}
|
|
439
|
+
|
|
142
440
|
function appendIdentifiers(doc, parent, { meetingId, minutesId, versionId } = {}) {
|
|
143
441
|
const details = makeElement(doc, 'details', { className: 'bb-minutes-identifiers' });
|
|
144
442
|
details.append(makeElement(doc, 'summary', { textContent: '識別情報' }));
|
|
@@ -217,6 +515,7 @@ export function createMeetingMinutesUI({
|
|
|
217
515
|
meetingLabel,
|
|
218
516
|
canCreateMeeting,
|
|
219
517
|
canEditMinutes,
|
|
518
|
+
now = () => new Date(),
|
|
220
519
|
} = {}) {
|
|
221
520
|
if (!root) throw new TypeError('root is required');
|
|
222
521
|
const doc = explicitDocument ?? (typeof document === 'undefined' ? null : document);
|
|
@@ -225,8 +524,6 @@ export function createMeetingMinutesUI({
|
|
|
225
524
|
? fetcher
|
|
226
525
|
: typeof globalThis.fetch === 'function' ? (path, init) => globalThis.fetch(path, init) : null;
|
|
227
526
|
const base = String(basePath).replace(/\/+$/u, '');
|
|
228
|
-
const createBlockedMessage = 'この画面では会議を作成できません。';
|
|
229
|
-
const editBlockedMessage = 'この画面では議事録を編集できません。';
|
|
230
527
|
const state = {
|
|
231
528
|
meetings: [],
|
|
232
529
|
detail: null,
|
|
@@ -245,19 +542,29 @@ export function createMeetingMinutesUI({
|
|
|
245
542
|
heading.append(
|
|
246
543
|
makeElement(doc, 'div', { className: 'bb-minutes-eyebrow', textContent: page?.source ?? '正本の議事録' }),
|
|
247
544
|
makeElement(doc, 'h1', { textContent: '議事録' }),
|
|
248
|
-
makeElement(doc, 'p', { className: 'bb-minutes-lead', textContent: '
|
|
545
|
+
makeElement(doc, 'p', { className: 'bb-minutes-lead', textContent: '新しい会議から読み、確認待ちを見つけて確認します。' }),
|
|
249
546
|
);
|
|
250
547
|
const status = makeElement(doc, 'p', { className: 'bb-minutes-status', attrs: { role: 'status', 'aria-live': 'polite' } });
|
|
251
548
|
const columns = makeElement(doc, 'div', { className: 'bb-minutes-columns' });
|
|
252
549
|
const listPane = makeElement(doc, 'section', {
|
|
253
550
|
className: 'bb-minutes-list-pane',
|
|
254
|
-
attrs: { 'aria-label': '
|
|
551
|
+
attrs: { 'aria-label': '議事録', tabindex: '-1' },
|
|
255
552
|
});
|
|
256
|
-
const detailPane = makeElement(doc, 'section', { className: 'bb-minutes-detail-pane', attrs: { 'aria-label': '
|
|
553
|
+
const detailPane = makeElement(doc, 'section', { className: 'bb-minutes-detail-pane', attrs: { 'aria-label': '議事録詳細' } });
|
|
257
554
|
columns.append(listPane, detailPane);
|
|
258
555
|
screen.append(heading, status, columns);
|
|
259
556
|
root.replaceChildren(screen);
|
|
260
557
|
|
|
558
|
+
function clock() {
|
|
559
|
+
try {
|
|
560
|
+
const value = typeof now === 'function' ? now() : new Date();
|
|
561
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
|
|
562
|
+
} catch {
|
|
563
|
+
/* use the runtime clock */
|
|
564
|
+
}
|
|
565
|
+
return new Date();
|
|
566
|
+
}
|
|
567
|
+
|
|
261
568
|
function setStatus(message, kind = '') {
|
|
262
569
|
status.textContent = message;
|
|
263
570
|
status.className = `bb-minutes-status${kind ? ` is-${kind}` : ''}`;
|
|
@@ -312,25 +619,41 @@ export function createMeetingMinutesUI({
|
|
|
312
619
|
return canEditMinutes !== false;
|
|
313
620
|
}
|
|
314
621
|
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
})
|
|
622
|
+
function stateChip(versionStatus, { detailed = false } = {}) {
|
|
623
|
+
const label = detailed && versionStatus.detail ? versionStatus.detail : versionStatus.label;
|
|
624
|
+
return makeElement(doc, 'span', {
|
|
625
|
+
className: `bb-minutes-state is-${versionStatus.tone}`,
|
|
626
|
+
textContent: label,
|
|
627
|
+
});
|
|
321
628
|
}
|
|
322
629
|
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
630
|
+
function appendLinePart(line, node) {
|
|
631
|
+
if (line.children.length) line.append(makeElement(doc, 'span', { className: 'bb-minutes-line-sep', textContent: ' · ' }));
|
|
632
|
+
line.append(node);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function appendReadOnlyNotice(parent, version) {
|
|
636
|
+
const source = isRecord(version?.source_ref) ? version.source_ref : null;
|
|
637
|
+
const url = httpUrl(source?.locator);
|
|
638
|
+
const provider = text(source?.provider, '元の記録');
|
|
639
|
+
const notice = makeElement(doc, 'div', { className: 'bb-minutes-edit-guard is-info' });
|
|
640
|
+
notice.append(makeElement(doc, 'p', {
|
|
641
|
+
textContent: canEdit()
|
|
642
|
+
? 'この画面では読むことと確認だけができます。'
|
|
643
|
+
: 'この画面では読むことだけができます。確認はプロジェクトの編集権限がある人が行います。',
|
|
333
644
|
}));
|
|
645
|
+
if (url) {
|
|
646
|
+
const line = makeElement(doc, 'p', { textContent: '本文を直すときは、元の記録で編集してください。' });
|
|
647
|
+
line.append(makeElement(doc, 'a', {
|
|
648
|
+
className: 'bb-minutes-source-link',
|
|
649
|
+
textContent: `元の記録を開く(${provider})`,
|
|
650
|
+
attrs: { href: url, target: '_blank', rel: 'noopener noreferrer' },
|
|
651
|
+
}));
|
|
652
|
+
notice.append(line);
|
|
653
|
+
} else {
|
|
654
|
+
notice.append(makeElement(doc, 'p', { textContent: '本文を直すときは、議事録を取り込んだ元で編集してください。' }));
|
|
655
|
+
}
|
|
656
|
+
parent.append(notice);
|
|
334
657
|
}
|
|
335
658
|
|
|
336
659
|
function appendLoadError(parent, message, retry) {
|
|
@@ -343,92 +666,178 @@ export function createMeetingMinutesUI({
|
|
|
343
666
|
parent.append(notice);
|
|
344
667
|
}
|
|
345
668
|
|
|
669
|
+
function renderMeetingRow(entry) {
|
|
670
|
+
const meeting = entry.meeting;
|
|
671
|
+
const timing = meetingTiming(entry);
|
|
672
|
+
const item = makeElement(doc, 'button', {
|
|
673
|
+
className: `bb-minutes-meeting-item${state.meetingId === meeting.meeting_id ? ' is-selected' : ''}`,
|
|
674
|
+
type: 'button',
|
|
675
|
+
attrs: { type: 'button' },
|
|
676
|
+
});
|
|
677
|
+
item.append(makeElement(doc, 'strong', {
|
|
678
|
+
className: 'bb-minutes-item-title',
|
|
679
|
+
textContent: text(meeting.title, '無題の会議'),
|
|
680
|
+
}));
|
|
681
|
+
const line = makeElement(doc, 'span', { className: 'bb-minutes-item-line' });
|
|
682
|
+
if (timing.kind === 'scheduled') {
|
|
683
|
+
appendLinePart(line, makeElement(doc, 'span', { textContent: formatClock(timing.date) }));
|
|
684
|
+
} else if (timing.kind === 'fallback' || (timing.kind === 'unregistered' && timing.date)) {
|
|
685
|
+
appendLinePart(line, makeElement(doc, 'span', { textContent: unscheduledLabel(timing) }));
|
|
686
|
+
}
|
|
687
|
+
const label = meetingLabelText(meetingLabel, meeting);
|
|
688
|
+
if (label) appendLinePart(line, makeElement(doc, 'span', { className: 'bb-minutes-meeting-label', textContent: label }));
|
|
689
|
+
const rowState = listState(entry);
|
|
690
|
+
if (rowState) appendLinePart(line, stateChip(rowState));
|
|
691
|
+
const versionNumber = listedVersionNumber(entry);
|
|
692
|
+
if (versionNumber >= 2) appendLinePart(line, makeElement(doc, 'span', { textContent: `第${versionNumber}版` }));
|
|
693
|
+
const minutesCount = Array.isArray(entry.minutes) ? entry.minutes.length : 0;
|
|
694
|
+
if (minutesCount >= 2) appendLinePart(line, makeElement(doc, 'span', { textContent: `議事録 ${minutesCount}件` }));
|
|
695
|
+
item.append(line);
|
|
696
|
+
item.addEventListener('click', () => void loadDetail(meeting.meeting_id));
|
|
697
|
+
return item;
|
|
698
|
+
}
|
|
699
|
+
|
|
346
700
|
function renderList() {
|
|
347
701
|
clear(listPane);
|
|
348
702
|
const head = makeElement(doc, 'div', { className: 'bb-minutes-pane-head' });
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}));
|
|
355
|
-
if (!createAllowed) appendCreateBlockedNotice(head);
|
|
703
|
+
const meetings = state.meetings.filter((entry) => isRecord(entry?.meeting) && typeof entry.meeting.meeting_id === 'string');
|
|
704
|
+
if (canCreate()) {
|
|
705
|
+
head.append(button(doc, '会議を作成', () => renderCreateForm(), { className: 'bb-minutes-button is-primary' }));
|
|
706
|
+
}
|
|
707
|
+
head.append(makeElement(doc, 'p', { className: 'bb-minutes-sort-note', textContent: `新しい順・${meetings.length}件` }));
|
|
356
708
|
listPane.append(head);
|
|
709
|
+
if (!canCreate()) {
|
|
710
|
+
listPane.append(makeElement(doc, 'p', {
|
|
711
|
+
className: 'bb-minutes-create-guard',
|
|
712
|
+
textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
357
715
|
if (state.listError) {
|
|
358
716
|
appendLoadError(listPane, `会議一覧を読み込めませんでした。理由: ${state.listError}`, () => loadMeetings({ restoreFocus: true }));
|
|
359
717
|
}
|
|
360
|
-
if (
|
|
718
|
+
if (meetings.length === 0) {
|
|
361
719
|
if (state.listError) return;
|
|
362
|
-
listPane.append(makeElement(doc, 'p', {
|
|
720
|
+
listPane.append(makeElement(doc, 'p', {
|
|
721
|
+
className: 'bb-minutes-empty',
|
|
722
|
+
textContent: 'まだ議事録はありません。会議を取り込むと、ここに新しい順で並びます。',
|
|
723
|
+
}));
|
|
363
724
|
return;
|
|
364
725
|
}
|
|
365
726
|
const list = makeElement(doc, 'div', { className: 'bb-minutes-meeting-list' });
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
727
|
+
const today = clock();
|
|
728
|
+
let currentKey = null;
|
|
729
|
+
for (const entry of meetings) {
|
|
730
|
+
const timing = meetingTiming(entry);
|
|
731
|
+
if (timing.kind === 'unregistered') continue;
|
|
732
|
+
const key = dateKey(timing.date);
|
|
733
|
+
if (key !== currentKey) {
|
|
734
|
+
currentKey = key;
|
|
735
|
+
list.append(makeElement(doc, 'h3', {
|
|
736
|
+
className: 'bb-minutes-date-heading',
|
|
737
|
+
textContent: dayHeading(timing.date, today),
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
list.append(renderMeetingRow(entry));
|
|
741
|
+
}
|
|
742
|
+
const unregistered = meetings.filter((entry) => meetingTiming(entry).kind === 'unregistered');
|
|
743
|
+
if (unregistered.length) {
|
|
744
|
+
list.append(makeElement(doc, 'h3', {
|
|
745
|
+
className: 'bb-minutes-date-heading',
|
|
746
|
+
textContent: '会議の日時が未登録(取り込んだ日の新しい順)',
|
|
747
|
+
}));
|
|
748
|
+
for (const entry of unregistered) list.append(renderMeetingRow(entry));
|
|
383
749
|
}
|
|
384
750
|
listPane.append(list);
|
|
385
751
|
}
|
|
386
752
|
|
|
387
|
-
function
|
|
388
|
-
return state.detail && state.detail.meeting?.meeting_id === state.meetingId ? state.detail : null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function renderRail(version, documentRecord) {
|
|
753
|
+
function renderRail(version, documentRecord, meeting) {
|
|
392
754
|
if (!rail) return;
|
|
393
755
|
clear(rail);
|
|
394
756
|
const block = makeElement(doc, 'section', { className: 'bb-minutes-rail-block' });
|
|
395
|
-
block.append(makeElement(doc, 'h2', { textContent: '
|
|
396
|
-
if (!version) {
|
|
397
|
-
block.append(makeElement(doc, 'p', { textContent: '
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
block.append(
|
|
401
|
-
makeElement(doc, 'p', { className: 'bb-minutes-rail-title', textContent: text(documentRecord?.title, '議事録') }),
|
|
402
|
-
makeElement(doc, 'p', { textContent: versionLabel(versions, version) }),
|
|
403
|
-
makeElement(doc, 'p', { textContent: version.confirmation ? `確認済み(${displayDate(version.confirmation.confirmed_at)})` : '未確認' }),
|
|
404
|
-
);
|
|
757
|
+
block.append(makeElement(doc, 'h2', { textContent: '関連' }));
|
|
758
|
+
if (!meeting && !version) {
|
|
759
|
+
block.append(makeElement(doc, 'p', { textContent: '議事録を選ぶと、ここに本文が出ます。' }));
|
|
760
|
+
rail.append(block);
|
|
761
|
+
return;
|
|
405
762
|
}
|
|
763
|
+
block.append(makeElement(doc, 'h3', { textContent: '予定' }));
|
|
764
|
+
block.append(makeElement(doc, 'p', { className: 'bb-minutes-rail-note', textContent: '予定とのつながりは未対応です。' }));
|
|
765
|
+
block.append(makeElement(doc, 'h3', { textContent: 'プロジェクト' }));
|
|
766
|
+
const label = meetingLabelText(meetingLabel, meeting);
|
|
767
|
+
block.append(makeElement(doc, 'p', { textContent: label || 'プロジェクトは記録されていません' }));
|
|
768
|
+
appendIdentifiers(doc, block, {
|
|
769
|
+
meetingId: meeting?.meeting_id,
|
|
770
|
+
minutesId: documentRecord?.minutes_id,
|
|
771
|
+
versionId: version?.version_id,
|
|
772
|
+
});
|
|
406
773
|
rail.append(block);
|
|
407
774
|
}
|
|
408
775
|
|
|
409
|
-
function renderVersion(detail, documentRecord, selectedVersion) {
|
|
776
|
+
function renderVersion(detail, documentRecord, selectedVersion, noticeState) {
|
|
410
777
|
const versions = detailVersions(detail, documentRecord.minutes_id);
|
|
411
|
-
const
|
|
778
|
+
const latest = versions.find((version) => version.version_id === documentRecord.current_version_id) ?? versions[versions.length - 1] ?? null;
|
|
779
|
+
const selected = selectedVersion ?? latest;
|
|
412
780
|
state.versionId = selected?.version_id ?? null;
|
|
413
781
|
const selectedLabel = versionLabel(versions, selected);
|
|
782
|
+
const selectedState = versionState(selected, versions, latest);
|
|
783
|
+
const latestState = versionState(latest, versions, latest);
|
|
784
|
+
const viewingOld = Boolean(selected && latest && selected.version_id !== latest.version_id);
|
|
414
785
|
const section = makeElement(doc, 'section', { className: 'bb-minutes-document' });
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
makeElement(doc, '
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
786
|
+
if (viewingOld) {
|
|
787
|
+
const banner = makeElement(doc, 'div', { className: 'bb-minutes-old-version' });
|
|
788
|
+
const latestName = latestState.code === 'confirmed' ? '確認済み' : latestState.label;
|
|
789
|
+
banner.append(makeElement(doc, 'p', {
|
|
790
|
+
textContent: `古い版を表示しています。最新は${versionLabel(versions, latest)}(${latestName})です。`,
|
|
791
|
+
}));
|
|
792
|
+
banner.append(button(doc, '最新の版を見る', () => {
|
|
793
|
+
clear(detailPane);
|
|
794
|
+
renderDetail(detail, documentRecord, latest);
|
|
795
|
+
}));
|
|
796
|
+
section.append(banner);
|
|
797
|
+
}
|
|
798
|
+
const band = makeElement(doc, 'div', { className: 'bb-minutes-version-band' });
|
|
799
|
+
const latestMark = selected && latest && selected.version_id === latest.version_id ? '(最新)' : '';
|
|
800
|
+
band.append(makeElement(doc, 'strong', { textContent: `${selectedLabel}${latestMark}` }));
|
|
801
|
+
const created = formatStamp(selected?.created_at);
|
|
802
|
+
if (created) band.append(makeElement(doc, 'span', { className: 'bb-minutes-item-meta', textContent: created }));
|
|
803
|
+
band.append(stateChip(selectedState, { detailed: true }));
|
|
804
|
+
const history = makeElement(doc, 'div', { className: 'bb-minutes-version-history is-collapsed', attrs: { 'aria-label': '版の履歴' } });
|
|
805
|
+
const historyButton = button(doc, `版の履歴(${versions.length}件)`, () => {
|
|
806
|
+
const collapsed = history.className.includes('is-collapsed');
|
|
807
|
+
history.className = collapsed ? 'bb-minutes-version-history' : 'bb-minutes-version-history is-collapsed';
|
|
808
|
+
historyButton.textContent = collapsed ? `版の履歴を閉じる(${versions.length}件)` : `版の履歴(${versions.length}件)`;
|
|
809
|
+
});
|
|
810
|
+
band.append(historyButton);
|
|
811
|
+
section.append(band);
|
|
812
|
+
if (!noticeState.placed) {
|
|
813
|
+
noticeState.placed = true;
|
|
814
|
+
appendReadOnlyNotice(section, selected);
|
|
815
|
+
}
|
|
423
816
|
for (const version of [...versions].reverse()) {
|
|
424
|
-
const
|
|
425
|
-
|
|
817
|
+
const versionButton = makeElement(doc, 'button', {
|
|
818
|
+
className: `bb-minutes-version${version.version_id === selected?.version_id ? ' is-selected' : ''}`,
|
|
819
|
+
type: 'button',
|
|
820
|
+
attrs: { type: 'button' },
|
|
821
|
+
});
|
|
822
|
+
const itemState = versionState(version, versions, latest);
|
|
823
|
+
const isLatest = Boolean(latest) && version.version_id === latest.version_id;
|
|
824
|
+
const line1 = makeElement(doc, 'span', { className: 'bb-minutes-version-line' });
|
|
825
|
+
line1.append(
|
|
826
|
+
makeElement(doc, 'strong', { textContent: `${versionLabel(versions, version)}${isLatest ? '(最新)' : ''}` }),
|
|
827
|
+
stateChip(itemState),
|
|
828
|
+
makeElement(doc, 'span', { className: 'bb-minutes-version-stamp', textContent: historyStamp(version, clock()) }),
|
|
829
|
+
);
|
|
426
830
|
versionButton.append(
|
|
427
|
-
|
|
428
|
-
makeElement(doc, 'span', {
|
|
429
|
-
|
|
831
|
+
line1,
|
|
832
|
+
makeElement(doc, 'span', {
|
|
833
|
+
className: 'bb-minutes-version-line is-secondary',
|
|
834
|
+
textContent: historySecondLine(version, versions, latest),
|
|
835
|
+
}),
|
|
430
836
|
);
|
|
431
|
-
versionButton.addEventListener('click', () => {
|
|
837
|
+
versionButton.addEventListener('click', () => {
|
|
838
|
+
clear(detailPane);
|
|
839
|
+
renderDetail(detail, documentRecord, version);
|
|
840
|
+
});
|
|
432
841
|
history.append(versionButton);
|
|
433
842
|
}
|
|
434
843
|
section.append(history);
|
|
@@ -437,23 +846,13 @@ export function createMeetingMinutesUI({
|
|
|
437
846
|
className: 'bb-minutes-external-body-slot',
|
|
438
847
|
attrs: { 'data-minutes-external-body': 'true' },
|
|
439
848
|
});
|
|
440
|
-
content.append(
|
|
441
|
-
if (selected?.predecessor_version_id) {
|
|
442
|
-
const predecessor = versions.find((version) => version.version_id === selected.predecessor_version_id);
|
|
443
|
-
content.append(makeElement(doc, 'p', {
|
|
444
|
-
className: 'bb-minutes-item-meta',
|
|
445
|
-
textContent: `訂正元: ${versionLabel(versions, predecessor)}`,
|
|
446
|
-
}));
|
|
447
|
-
}
|
|
849
|
+
content.append(versionContent(doc, selected), bodyRoot);
|
|
448
850
|
section.append(content);
|
|
449
|
-
if (selected) {
|
|
450
|
-
const
|
|
451
|
-
const editAllowed = canEdit();
|
|
452
|
-
const confirm = button(doc, selected.confirmation ? '確認済み' : 'この版を確認', async () => {
|
|
453
|
-
if (selected.confirmation) return;
|
|
851
|
+
if (selected && canEdit() && !isRecord(selected.confirmation) && selectedState.code !== 'unknown' && selectedState.code !== 'creating') {
|
|
852
|
+
const confirm = button(doc, 'この版を確認済みにする', async () => {
|
|
454
853
|
if (!canEdit()) {
|
|
455
|
-
|
|
456
|
-
|
|
854
|
+
clear(detailPane);
|
|
855
|
+
renderDetail(detail, documentRecord, selected);
|
|
457
856
|
return;
|
|
458
857
|
}
|
|
459
858
|
confirm.disabled = true;
|
|
@@ -462,16 +861,14 @@ export function createMeetingMinutesUI({
|
|
|
462
861
|
version_id: selected.version_id,
|
|
463
862
|
expected_revision: documentRecord.revision,
|
|
464
863
|
});
|
|
465
|
-
setStatus('確認を保存しました。', 'success');
|
|
466
864
|
await loadDetail(detail.meeting.meeting_id);
|
|
865
|
+
if (!state.detailError) setStatus(`${selectedLabel}を確認済みにしました。`, 'success');
|
|
467
866
|
} catch (error) {
|
|
468
867
|
confirm.disabled = false;
|
|
469
868
|
setStatus(error instanceof Error ? error.message : '確認を保存できませんでした。', 'error');
|
|
470
869
|
}
|
|
471
|
-
}, { className: 'bb-minutes-button is-primary'
|
|
472
|
-
|
|
473
|
-
section.append(actions);
|
|
474
|
-
if (!editAllowed) appendEditBlockedNotice(section);
|
|
870
|
+
}, { className: 'bb-minutes-button is-primary' });
|
|
871
|
+
band.append(confirm);
|
|
475
872
|
}
|
|
476
873
|
appendVersionActionExtensions(doc, section, {
|
|
477
874
|
extensions: versionActionExtensions,
|
|
@@ -489,45 +886,10 @@ export function createMeetingMinutesUI({
|
|
|
489
886
|
minutesId: documentRecord.minutes_id,
|
|
490
887
|
versionId: selected?.version_id,
|
|
491
888
|
});
|
|
492
|
-
|
|
493
|
-
renderRail(selected, documentRecord);
|
|
889
|
+
renderRail(selected, documentRecord, detail.meeting);
|
|
494
890
|
return section;
|
|
495
891
|
}
|
|
496
892
|
|
|
497
|
-
function appendSaveForm(section, detail, documentRecord) {
|
|
498
|
-
if (!canEdit()) {
|
|
499
|
-
appendEditBlockedNotice(section);
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
const form = makeElement(doc, 'form', { className: 'bb-minutes-save-form' });
|
|
503
|
-
form.append(makeElement(doc, 'h4', { textContent: '訂正版を保存' }));
|
|
504
|
-
const field = labelInput(doc, '本文(保存すると新しい版になります)', { type: 'textarea', name: 'body', placeholder: '議事録の本文を入力' });
|
|
505
|
-
field.input.rows = 8;
|
|
506
|
-
const save = makeElement(doc, 'button', { className: 'bb-minutes-button is-primary', textContent: '新しい版を保存', type: 'submit' });
|
|
507
|
-
form.append(field.wrapper, save);
|
|
508
|
-
form.addEventListener('submit', async (event) => {
|
|
509
|
-
event.preventDefault();
|
|
510
|
-
if (!canEdit()) {
|
|
511
|
-
save.disabled = false;
|
|
512
|
-
appendEditBlockedNotice(section, { alert: true });
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
save.disabled = true;
|
|
516
|
-
try {
|
|
517
|
-
await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes/${encodeURIComponent(documentRecord.minutes_id)}`, {
|
|
518
|
-
body: field.input.value,
|
|
519
|
-
expected_revision: documentRecord.revision,
|
|
520
|
-
});
|
|
521
|
-
setStatus('新しい版を保存しました。', 'success');
|
|
522
|
-
await loadDetail(detail.meeting.meeting_id);
|
|
523
|
-
} catch (error) {
|
|
524
|
-
save.disabled = false;
|
|
525
|
-
setStatus(error instanceof Error ? error.message : '版を保存できませんでした。', 'error');
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
section.append(form);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
893
|
function renderDetail(detail, documentRecord = null, selectedVersion = null) {
|
|
532
894
|
clear(detailPane);
|
|
533
895
|
if (state.detailError) {
|
|
@@ -538,11 +900,11 @@ export function createMeetingMinutesUI({
|
|
|
538
900
|
}
|
|
539
901
|
if (!detail || !isRecord(detail.meeting)) {
|
|
540
902
|
if (state.detailError) {
|
|
541
|
-
renderRail(null, null);
|
|
903
|
+
renderRail(null, null, null);
|
|
542
904
|
return;
|
|
543
905
|
}
|
|
544
|
-
detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '
|
|
545
|
-
renderRail(null, null);
|
|
906
|
+
detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
|
|
907
|
+
renderRail(null, null, null);
|
|
546
908
|
return;
|
|
547
909
|
}
|
|
548
910
|
const meeting = detail.meeting;
|
|
@@ -550,58 +912,43 @@ export function createMeetingMinutesUI({
|
|
|
550
912
|
header.append(makeElement(doc, 'h2', { textContent: text(meeting.title, '無題の会議') }));
|
|
551
913
|
const label = meetingLabelText(meetingLabel, meeting);
|
|
552
914
|
if (label) header.append(makeElement(doc, 'p', { className: 'bb-minutes-meeting-label', textContent: label }));
|
|
553
|
-
|
|
915
|
+
const timing = meetingTiming({ meeting, minutes: detail.minutes, versions: detail.versions });
|
|
916
|
+
const when = timing.kind === 'scheduled'
|
|
917
|
+
? `${formatMonthDay(timing.date)} ${formatClock(timing.date)}`
|
|
918
|
+
: unscheduledLabel(timing);
|
|
919
|
+
const participants = Array.isArray(meeting.participant_ids) && meeting.participant_ids.length
|
|
920
|
+
? meeting.participant_ids.join('、')
|
|
921
|
+
: '';
|
|
922
|
+
header.append(makeElement(doc, 'p', {
|
|
923
|
+
className: 'bb-minutes-item-meta',
|
|
924
|
+
textContent: participants ? `${when} / ${participants}` : when,
|
|
925
|
+
}));
|
|
554
926
|
detailPane.append(header);
|
|
555
927
|
const documents = Array.isArray(detail.minutes) ? detail.minutes : [];
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
detailPane.append(
|
|
928
|
+
const noticeState = { placed: false };
|
|
929
|
+
if (documents.length === 0) {
|
|
930
|
+
detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-warning', textContent: '議事録文書を読み取れません。' }));
|
|
931
|
+
appendReadOnlyNotice(detailPane, null);
|
|
932
|
+
noticeState.placed = true;
|
|
559
933
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
return guard;
|
|
934
|
+
for (const documentRecordValue of documents) {
|
|
935
|
+
detailPane.append(renderVersion(
|
|
936
|
+
detail,
|
|
937
|
+
documentRecordValue,
|
|
938
|
+
documentRecordValue.minutes_id === documentRecord?.minutes_id ? selectedVersion : null,
|
|
939
|
+
noticeState,
|
|
940
|
+
));
|
|
568
941
|
}
|
|
569
|
-
const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
|
|
570
|
-
form.append(makeElement(doc, 'h3', { textContent: '議事録文書を追加' }));
|
|
571
|
-
const title = labelInput(doc, '議事録の名前', { name: 'title', value: '議事録' });
|
|
572
|
-
const body = labelInput(doc, '本文', { type: 'textarea', name: 'body', placeholder: '本文を入力' });
|
|
573
|
-
body.input.rows = 5;
|
|
574
|
-
const save = makeElement(doc, 'button', { className: 'bb-minutes-button', textContent: '議事録を追加', type: 'submit' });
|
|
575
|
-
form.append(title.wrapper, body.wrapper, save);
|
|
576
|
-
form.addEventListener('submit', async (event) => {
|
|
577
|
-
event.preventDefault();
|
|
578
|
-
if (!canEdit()) {
|
|
579
|
-
save.disabled = false;
|
|
580
|
-
appendEditBlockedNotice(form, { alert: true });
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
save.disabled = true;
|
|
584
|
-
try {
|
|
585
|
-
await mutate(`/${encodeURIComponent(detail.meeting.meeting_id)}/minutes`, {
|
|
586
|
-
title: title.input.value,
|
|
587
|
-
body: body.input.value,
|
|
588
|
-
expected_revision: detail.snapshot_revision,
|
|
589
|
-
});
|
|
590
|
-
setStatus('議事録文書を追加しました。', 'success');
|
|
591
|
-
await loadMeetings();
|
|
592
|
-
await loadDetail(detail.meeting.meeting_id);
|
|
593
|
-
} catch (error) {
|
|
594
|
-
save.disabled = false;
|
|
595
|
-
setStatus(error instanceof Error ? error.message : '議事録を追加できませんでした。', 'error');
|
|
596
|
-
}
|
|
597
|
-
});
|
|
598
|
-
return form;
|
|
599
942
|
}
|
|
600
943
|
|
|
601
944
|
function renderCreateForm() {
|
|
602
945
|
clear(detailPane);
|
|
603
946
|
if (!canCreate()) {
|
|
604
|
-
|
|
947
|
+
detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
|
|
948
|
+
detailPane.append(makeElement(doc, 'p', {
|
|
949
|
+
className: 'bb-minutes-create-guard',
|
|
950
|
+
textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
|
|
951
|
+
}));
|
|
605
952
|
return;
|
|
606
953
|
}
|
|
607
954
|
const form = makeElement(doc, 'form', { className: 'bb-minutes-create-form' });
|
|
@@ -617,8 +964,14 @@ export function createMeetingMinutesUI({
|
|
|
617
964
|
form.addEventListener('submit', async (event) => {
|
|
618
965
|
event.preventDefault();
|
|
619
966
|
if (!canCreate()) {
|
|
620
|
-
save.disabled =
|
|
621
|
-
|
|
967
|
+
save.disabled = true;
|
|
968
|
+
renderList();
|
|
969
|
+
clear(detailPane);
|
|
970
|
+
detailPane.append(makeElement(doc, 'p', { className: 'bb-minutes-empty', textContent: '議事録を選ぶと、ここに本文が出ます。' }));
|
|
971
|
+
detailPane.append(makeElement(doc, 'p', {
|
|
972
|
+
className: 'bb-minutes-create-guard',
|
|
973
|
+
textContent: '会議を作成する権限がありません。作成はプロジェクトの編集権限がある人が行います。',
|
|
974
|
+
}));
|
|
622
975
|
return;
|
|
623
976
|
}
|
|
624
977
|
save.disabled = true;
|
|
@@ -653,7 +1006,7 @@ export function createMeetingMinutesUI({
|
|
|
653
1006
|
const payload = await call('');
|
|
654
1007
|
if (!isCurrentRequest()) return state.meetings;
|
|
655
1008
|
if (!isRecord(payload) || !Array.isArray(payload.meetings) || payload.absence_confirmed !== true) throw new Error('会議一覧の形式を確認できません。');
|
|
656
|
-
state.meetings = payload.meetings;
|
|
1009
|
+
state.meetings = sortMeetings(payload.meetings);
|
|
657
1010
|
state.listError = null;
|
|
658
1011
|
const selectedMeetingStillListed = state.meetingId !== null
|
|
659
1012
|
&& state.meetings.some((entry) => entry?.meeting?.meeting_id === state.meetingId);
|
|
@@ -670,8 +1023,8 @@ export function createMeetingMinutesUI({
|
|
|
670
1023
|
if (hadDetailError) renderDetail(null);
|
|
671
1024
|
}
|
|
672
1025
|
renderList();
|
|
673
|
-
if (state.
|
|
674
|
-
setStatus(state.meetings.length ? `${state.meetings.length}件の会議を読み込みました。` : '
|
|
1026
|
+
if (!state.detail) renderDetail(null);
|
|
1027
|
+
setStatus(state.meetings.length ? `${state.meetings.length}件の会議を読み込みました。` : 'まだ議事録はありません。');
|
|
675
1028
|
if (restoreFocus && typeof listPane.focus === 'function') listPane.focus({ preventScroll: true });
|
|
676
1029
|
} catch (error) {
|
|
677
1030
|
if (!isCurrentRequest()) return state.meetings;
|
|
@@ -728,6 +1081,7 @@ export function createMeetingMinutesUI({
|
|
|
728
1081
|
return state.detail;
|
|
729
1082
|
}
|
|
730
1083
|
|
|
1084
|
+
renderDetail(null);
|
|
731
1085
|
void loadMeetings();
|
|
732
1086
|
return {
|
|
733
1087
|
contractVersion: MEETING_MINUTES_UI_CONTRACT_VERSION,
|