@unson/brainbase-mcp 0.41.11 → 0.41.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/contracts/judgment-dag/digest.json +2 -2
- package/dist/local-web-host.js +19 -2
- package/package.json +6 -2
- package/ui/area-nav.css +79 -0
- package/ui/area-nav.js +249 -0
- package/ui/foundation-overview.css +9 -0
- package/ui/foundation-overview.js +75 -5
- package/ui/graph-ontology-view.css +88 -0
- package/ui/graph-ontology-view.js +248 -0
- package/ui/graph-own-share.js +167 -0
- package/ui/graph-registry-view.css +272 -50
- package/ui/graph-registry-view.js +539 -456
- package/ui/graph-view-shared.css +32 -0
- package/ui/graph-view-shared.js +191 -12
- package/ui/local-web-shell.css +1 -13
- package/ui/local-web-shell.js +64 -40
- package/ui/outcome-knowledge.js +6 -2
- package/ui/people-and-companies-view.css +166 -15
- package/ui/people-and-companies-view.js +307 -93
- package/ui/world/world-canvas-notes.js +3 -2
- package/ui/world/world-view.css +19 -0
- package/ui/world/world-view.js +52 -10
- package/ui/world/world-work-rail.js +114 -8
|
@@ -3,14 +3,21 @@
|
|
|
3
3
|
* to, and whether they are internal, external or not yet known.
|
|
4
4
|
*
|
|
5
5
|
* Reads `GET {base}/people-and-companies` for the ledgers and
|
|
6
|
-
* `GET {base}/entities/:id` for the selected record
|
|
7
|
-
* `graph-view-shared.js`).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* `GET {base}/entities/:id` for the selected record (see
|
|
7
|
+
* `graph-view-shared.js`). Choosing a row turns the workspace into the
|
|
8
|
+
* record's page, as a CRM does: a way back to the ledgers with the record's
|
|
9
|
+
* place and previous / next, the head with what can be done to this record,
|
|
10
|
+
* then its memberships and projects. The record's facts (standing, kind,
|
|
11
|
+
* validity, id) sit in the right rail, or on the page when the host has no
|
|
12
|
+
* rail. So the page never depends on where a host puts its rail on a narrow
|
|
13
|
+
* screen. Read only unless the host says it can write (`canCorrect: true`):
|
|
14
|
+
* then it creates people and companies (`create_entity`), adds a company
|
|
15
|
+
* membership (`create_edge` of `member_of`) and ends one with an end date
|
|
16
|
+
* (`update_edge`), all through `POST {base}/corrections`. Nothing is
|
|
17
|
+
* deleted, and the page says so next to the actions. A viewer who cannot
|
|
18
|
+
* write sees the actions disabled with the reason. After a save the ledgers
|
|
19
|
+
* and the record are read again from the host. Other corrections stay in
|
|
20
|
+
* 「情報と関係」, which the page links to.
|
|
14
21
|
*
|
|
15
22
|
* The company text recorded on a person (`orgNote`) is shown only for
|
|
16
23
|
* people whose company is not yet known, and only as a note: it never
|
|
@@ -54,6 +61,7 @@ import {
|
|
|
54
61
|
validityLabel,
|
|
55
62
|
} from './graph-view-shared.js';
|
|
56
63
|
import {
|
|
64
|
+
workspaceButton,
|
|
57
65
|
workspaceDefinition,
|
|
58
66
|
workspaceDetailEmpty,
|
|
59
67
|
workspaceHostNotice,
|
|
@@ -61,7 +69,6 @@ import {
|
|
|
61
69
|
workspaceNotice,
|
|
62
70
|
workspacePageHeader,
|
|
63
71
|
workspaceRailBlock,
|
|
64
|
-
workspaceRailHead,
|
|
65
72
|
workspaceSectionTitle,
|
|
66
73
|
} from './workspace-kit.js';
|
|
67
74
|
|
|
@@ -82,6 +89,10 @@ const RELATED_ROLE_LABELS = Object.freeze({ customer: '顧客', partner: '協業
|
|
|
82
89
|
|
|
83
90
|
const INVALID = Object.freeze({ state: 'invalid', reason: '応答の形式が不正です' });
|
|
84
91
|
|
|
92
|
+
/** What the page says next to the actions: nothing is deleted, a membership is ended. */
|
|
93
|
+
export const NO_DELETE_NOTE = '消す操作はありません。所属は終了日を入れて「終える」と一覧から外れ、記録は履歴に残ります。';
|
|
94
|
+
const DEFAULT_READ_ONLY_NOTE = 'この画面では見るだけです。人・会社の追加と、所属を付ける・終える操作はできません。';
|
|
95
|
+
|
|
85
96
|
const OWN_COMPANY_REASONS = Object.freeze({
|
|
86
97
|
owner_has_no_company: 'あなた(本人)の所属する会社がGraphにありません。',
|
|
87
98
|
owner_has_several_companies: 'あなた(本人)が複数の会社に所属しているため、自社を1つに決められません。',
|
|
@@ -209,6 +220,12 @@ export function personOrgNote(person, ownCompanyId) {
|
|
|
209
220
|
* A host that places people in projects lists the projects a new person may join as
|
|
210
221
|
* `personProjects` (`[{ code, name }]`) in its read; 人を加える then requires one and sends it as
|
|
211
222
|
* `entity.projectCode`. Without that list (the local host) no project is asked or sent.
|
|
223
|
+
* @param {string | ((subject: { id: string, type: string, name: string } | null) => string | null)} [options.readOnlyNote]
|
|
224
|
+
* Without writes, why the actions are disabled (for example who can write). Without it a
|
|
225
|
+
* general sentence is shown; the part names no role itself.
|
|
226
|
+
* @param {string} [options.recordFixNote] When the host cannot correct a person's or company's own
|
|
227
|
+
* record (name, aliases, summary), why; the page then shows this instead of the link to
|
|
228
|
+
* 「情報と関係」.
|
|
212
229
|
* @param {string} [options.token] Launch token sent with a correction.
|
|
213
230
|
* @param {string} [options.tokenHeader] Header name for the token.
|
|
214
231
|
*/
|
|
@@ -228,6 +245,8 @@ export function createPeopleAndCompaniesView({
|
|
|
228
245
|
correctionScope,
|
|
229
246
|
sourceNotice,
|
|
230
247
|
emptyNotice,
|
|
248
|
+
readOnlyNote,
|
|
249
|
+
recordFixNote,
|
|
231
250
|
correctionHref = (id) => `#graph?entity_id=${encodeURIComponent(id)}`,
|
|
232
251
|
} = {}) {
|
|
233
252
|
if (!root) throw new TypeError('root is required');
|
|
@@ -251,6 +270,10 @@ export function createPeopleAndCompaniesView({
|
|
|
251
270
|
contractVersion: PEOPLE_AND_COMPANIES_VIEW_CONTRACT_VERSION,
|
|
252
271
|
});
|
|
253
272
|
const state = { list: { state: 'loading' }, detail: null };
|
|
273
|
+
// Where to move after the next render: the opened record's head, or the row it was opened from.
|
|
274
|
+
let pendingScroll = null;
|
|
275
|
+
let detailTop = null;
|
|
276
|
+
let detailHead = null;
|
|
254
277
|
const correction = createGraphCorrection({
|
|
255
278
|
client,
|
|
256
279
|
now,
|
|
@@ -371,10 +394,35 @@ export function createPeopleAndCompaniesView({
|
|
|
371
394
|
return cell;
|
|
372
395
|
}
|
|
373
396
|
|
|
374
|
-
function
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
397
|
+
function readOnlyText(subject) {
|
|
398
|
+
const value = typeof readOnlyNote === 'function' ? readOnlyNote(subject ?? null) : readOnlyNote;
|
|
399
|
+
return textOrNull(value) ?? DEFAULT_READ_ONLY_NOTE;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* One action. Allowed: a button. Withheld by the host's scope: nothing.
|
|
404
|
+
* Without writes: the button disabled, pointing at the reason shown next to it.
|
|
405
|
+
*/
|
|
406
|
+
function actionButton(text, allowed, onClick, { variant = 'default', reasonId, adds = false } = {}) {
|
|
407
|
+
const attrs = { 'data-adds': adds };
|
|
408
|
+
if (allowed) return workspaceButton(doc, { text, variant, onClick, attrs });
|
|
409
|
+
if (writable) return null;
|
|
410
|
+
return workspaceButton(doc, { text, variant, disabled: true, attrs: { ...attrs, 'aria-describedby': reasonId } });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** The actions with what the page says about them: why they are disabled, and that nothing is deleted. */
|
|
414
|
+
function actionGroup({ label, buttons, notes = [], subject, reasonId, className = '' }) {
|
|
415
|
+
const shown = buttons.filter(Boolean);
|
|
416
|
+
const group = makeElement(doc, 'div', { className: `bb-pc-action-group${className ? ` ${className}` : ''}`, attrs: { role: 'group', 'aria-label': label } });
|
|
417
|
+
if (shown.length > 0) {
|
|
418
|
+
const row = makeElement(doc, 'div', { className: 'bb-pc-action-row' });
|
|
419
|
+
row.append(...shown);
|
|
420
|
+
group.append(row);
|
|
421
|
+
}
|
|
422
|
+
if (!writable && shown.length > 0) group.append(makeElement(doc, 'p', { className: 'bb-pc-action-reason', text: readOnlyText(subject), attrs: { id: reasonId } }));
|
|
423
|
+
const shownNotes = notes.filter(Boolean);
|
|
424
|
+
for (const note of shownNotes) group.append(typeof note === 'string' ? makeElement(doc, 'p', { className: 'bb-pc-action-note', text: note }) : note);
|
|
425
|
+
return shown.length > 0 || shownNotes.length > 0 ? group : null;
|
|
378
426
|
}
|
|
379
427
|
|
|
380
428
|
function openCreateEntity(type) {
|
|
@@ -402,7 +450,6 @@ export function createPeopleAndCompaniesView({
|
|
|
402
450
|
function peopleSection(payload) {
|
|
403
451
|
const section = makeElement(doc, 'section', { className: 'bb-pc-people', attrs: { 'aria-label': '人', id: 'bb-pc-people' } });
|
|
404
452
|
section.append(workspaceSectionTitle(doc, { title: '人', lead: '社内か社外かは、所属(会社への所属の関係)と自社から決めています。印は別に持っていません。' }));
|
|
405
|
-
if (can.createPerson) section.append(sectionActions('人を加える', () => openCreateEntity('person')));
|
|
406
453
|
const groups = groupPeople(payload);
|
|
407
454
|
if (!groups.ownCompanyId) {
|
|
408
455
|
const reason = OWN_COMPANY_REASONS[payload.ownCompany.reason] ?? '自社を決められません。';
|
|
@@ -433,7 +480,6 @@ export function createPeopleAndCompaniesView({
|
|
|
433
480
|
title: '会社',
|
|
434
481
|
lead: '自社を先頭に、顧客・協業先など関わる会社を並べています。種類は、会社の属性、プロジェクトとの関係の役割、提案中の案件の記録から出しています。記録が無い会社には種類を出しません。',
|
|
435
482
|
}));
|
|
436
|
-
if (can.createCompany) section.append(sectionActions('会社を加える', () => openCreateEntity('org')));
|
|
437
483
|
const active = payload.companies.filter((company) => company.active);
|
|
438
484
|
section.append(workspaceLedger(doc, {
|
|
439
485
|
className: 'bb-pc-company-ledger',
|
|
@@ -469,10 +515,23 @@ export function createPeopleAndCompaniesView({
|
|
|
469
515
|
}
|
|
470
516
|
|
|
471
517
|
function workspace() {
|
|
518
|
+
if (state.detail) return detailPage();
|
|
519
|
+
const listed = state.list.state === 'ok' && !state.list.payload.absenceConfirmed;
|
|
520
|
+
const addActions = listed
|
|
521
|
+
? actionGroup({
|
|
522
|
+
label: '人と会社を加える',
|
|
523
|
+
className: 'bb-pc-add',
|
|
524
|
+
reasonId: 'bb-pc-add-reason',
|
|
525
|
+
buttons: [
|
|
526
|
+
actionButton('人を加える', can.createPerson, () => openCreateEntity('person'), { variant: 'primary', reasonId: 'bb-pc-add-reason', adds: true }),
|
|
527
|
+
actionButton('会社を加える', can.createCompany, () => openCreateEntity('org'), { reasonId: 'bb-pc-add-reason', adds: true }),
|
|
528
|
+
],
|
|
529
|
+
})
|
|
530
|
+
: null;
|
|
472
531
|
const children = [workspacePageHeader(doc, {
|
|
473
532
|
crumbs: context.crumbs,
|
|
474
533
|
title: '人と会社',
|
|
475
|
-
lead: 'Graph
|
|
534
|
+
lead: 'Graphに登録された人が、どの会社の人か、社内か社外か所属が分からないかを確かめます。行を押すと、その人・会社のページが開きます。',
|
|
476
535
|
source: context.source,
|
|
477
536
|
})];
|
|
478
537
|
const hostNotice = sourceNotice ? workspaceHostNotice(doc, sourceNotice) : null;
|
|
@@ -481,10 +540,192 @@ export function createPeopleAndCompaniesView({
|
|
|
481
540
|
if (notice) return [...children, notice];
|
|
482
541
|
const payload = state.list.payload;
|
|
483
542
|
if (payload.absenceConfirmed) {
|
|
484
|
-
|
|
485
|
-
|
|
543
|
+
// Nothing is listed yet, so creating the first record is the next step.
|
|
544
|
+
const empty = graphHostEmptyNotice(doc, emptyNotice)
|
|
545
|
+
?? workspaceNotice(doc, { label: '未登録', tone: 'info', role: 'status', text: 'Graphに人と会社の記録はまだありません。' });
|
|
546
|
+
const first = actionGroup({
|
|
547
|
+
label: '人と会社を加える',
|
|
548
|
+
className: 'bb-pc-add',
|
|
549
|
+
reasonId: 'bb-pc-add-reason',
|
|
550
|
+
buttons: [
|
|
551
|
+
actionButton('人を加える', can.createPerson, () => openCreateEntity('person'), { variant: 'primary', reasonId: 'bb-pc-add-reason', adds: true }),
|
|
552
|
+
actionButton('会社を加える', can.createCompany, () => openCreateEntity('org'), { reasonId: 'bb-pc-add-reason', adds: true }),
|
|
553
|
+
],
|
|
554
|
+
});
|
|
555
|
+
return [...children, empty, first, correction.render(doc)];
|
|
486
556
|
}
|
|
487
|
-
|
|
557
|
+
// A new record's form opens right under the buttons that open it.
|
|
558
|
+
return [...children, addActions, correction.render(doc), jumpLinks(payload), peopleSection(payload), companySection(payload), endedNote(payload)];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// -------------------------------------------------------------------------
|
|
562
|
+
// The record's page
|
|
563
|
+
|
|
564
|
+
/** The listed records of the opened record's kind, in the order the ledgers show them. */
|
|
565
|
+
function listedIds(type) {
|
|
566
|
+
if (state.list.state !== 'ok') return [];
|
|
567
|
+
const payload = state.list.payload;
|
|
568
|
+
if (type === 'org') return payload.companies.filter((company) => company.active).map((company) => company.id);
|
|
569
|
+
const groups = groupPeople(payload);
|
|
570
|
+
return [...groups.internal, ...groups.external.flatMap((group) => group.people), ...groups.unknown].map((person) => person.id);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function backToList() {
|
|
574
|
+
const id = state.detail?.id;
|
|
575
|
+
correction.close();
|
|
576
|
+
state.detail = null;
|
|
577
|
+
pendingScroll = id ? { row: id } : null;
|
|
578
|
+
controller.render();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function detailNav(entity) {
|
|
582
|
+
const nav = makeElement(doc, 'nav', { className: 'bb-pc-detail-nav', attrs: { 'aria-label': '人と会社の一覧の中の位置' } });
|
|
583
|
+
nav.append(button(doc, '← 人と会社の一覧へ', backToList, { className: 'bb-ws-button is-quiet bb-pc-back' }));
|
|
584
|
+
const type = entity?.type === 'org' ? 'org' : 'person';
|
|
585
|
+
const ids = listedIds(type);
|
|
586
|
+
const index = entity ? ids.indexOf(entity.id) : -1;
|
|
587
|
+
if (index >= 0) {
|
|
588
|
+
const place = makeElement(doc, 'span', { className: 'bb-pc-detail-place' });
|
|
589
|
+
place.append(
|
|
590
|
+
makeElement(doc, 'span', { text: type === 'org' ? '会社' : '人' }),
|
|
591
|
+
makeElement(doc, 'span', { className: 'bb-pc-count', text: `${index + 1} / ${ids.length}` }),
|
|
592
|
+
);
|
|
593
|
+
const step = (offset, text, label) => workspaceButton(doc, {
|
|
594
|
+
text,
|
|
595
|
+
variant: 'quiet',
|
|
596
|
+
disabled: !ids[index + offset],
|
|
597
|
+
attrs: { 'aria-label': label },
|
|
598
|
+
onClick: () => { if (ids[index + offset]) void controller.openEntity(ids[index + offset]); },
|
|
599
|
+
});
|
|
600
|
+
nav.append(place, step(-1, '‹', '前へ'), step(1, '›', '次へ'));
|
|
601
|
+
}
|
|
602
|
+
return nav;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function detailHeadOf(entity, standing, company) {
|
|
606
|
+
const head = makeElement(doc, 'header', { className: 'bb-pc-detail-head' });
|
|
607
|
+
const mark = makeElement(doc, 'span', {
|
|
608
|
+
className: `bb-pc-mark ${entity.type === 'org' ? 'bb-pc-company-mark' : 'bb-pc-person-mark'} bb-pc-tone-${markTone(entity.id)} bb-pc-detail-mark`,
|
|
609
|
+
text: nameInitial(entity.name),
|
|
610
|
+
attrs: { 'aria-hidden': 'true' },
|
|
611
|
+
});
|
|
612
|
+
const copy = makeElement(doc, 'div', { className: 'bb-pc-detail-copy' });
|
|
613
|
+
const listed = state.list.state === 'ok' ? state.list.payload.people.find((person) => person.id === entity.id) : null;
|
|
614
|
+
// The heading takes the focus when the page opens, so a keyboard or screen reader lands here.
|
|
615
|
+
const title = makeElement(doc, 'h2', { text: listed?.isOwner ? `${entity.name}(あなた)` : entity.name, attrs: { tabindex: '-1' } });
|
|
616
|
+
detailHead = title;
|
|
617
|
+
const sub = [entity.type === 'org' ? '会社' : '人', standing, entity.type === 'org' && company ? `所属する人 ${company.memberIds.length}人` : null]
|
|
618
|
+
.filter(Boolean).join(' · ');
|
|
619
|
+
copy.append(title, makeElement(doc, 'p', { className: 'bb-pc-detail-sub', text: sub }));
|
|
620
|
+
const summary = textOrNull(entity.summary);
|
|
621
|
+
if (summary) copy.append(makeElement(doc, 'p', { className: 'bb-pc-detail-lead', text: summary }));
|
|
622
|
+
head.append(mark, copy);
|
|
623
|
+
return head;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** What can be done to this record, gathered under its head. */
|
|
627
|
+
function detailActions(entity, memberships) {
|
|
628
|
+
const subject = { id: entity.id, type: entity.type, name: entity.name };
|
|
629
|
+
const reasonId = 'bb-pc-detail-reason';
|
|
630
|
+
const buttons = entity.type === 'org'
|
|
631
|
+
? [actionButton('所属する人を加える', can.addMembership, () => openMembership(entity), { variant: 'primary', reasonId, adds: true })]
|
|
632
|
+
: [
|
|
633
|
+
actionButton('所属を付ける', can.addMembership, () => openMembership(entity), { variant: 'primary', reasonId, adds: true }),
|
|
634
|
+
...memberships.filter((edge) => edge.active).map((edge) => actionButton(
|
|
635
|
+
`「${edge.counterpart.name}」の所属を終える`,
|
|
636
|
+
can.endMembership,
|
|
637
|
+
() => openEndMembership(edge, entity),
|
|
638
|
+
{ reasonId },
|
|
639
|
+
)),
|
|
640
|
+
];
|
|
641
|
+
const fix = recordFixNote
|
|
642
|
+
? makeElement(doc, 'p', { className: 'bb-pc-action-note', text: recordFixNote })
|
|
643
|
+
: (() => {
|
|
644
|
+
const line = makeElement(doc, 'p', { className: 'bb-pc-action-note' });
|
|
645
|
+
line.append(
|
|
646
|
+
makeElement(doc, 'span', { text: '名前・別名・要約の間違いは、' }),
|
|
647
|
+
makeElement(doc, 'a', { text: '情報と関係でこの記録を開いて直します', attrs: { href: correctionHref(entity.id) } }),
|
|
648
|
+
makeElement(doc, 'span', { text: '。' }),
|
|
649
|
+
);
|
|
650
|
+
return line;
|
|
651
|
+
})();
|
|
652
|
+
const endNote = entity.type === 'org' && (can.endMembership || !writable)
|
|
653
|
+
? `${NO_DELETE_NOTE}所属する人の所属は、下の「所属する人」の各行で終えます。`
|
|
654
|
+
: NO_DELETE_NOTE;
|
|
655
|
+
return actionGroup({ label: entity.type === 'org' ? 'この会社への操作' : 'この人への操作', className: 'bb-pc-detail-actions', buttons, notes: [endNote, fix], subject, reasonId });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function detailFacts(entity, listed, company, ownId) {
|
|
659
|
+
return workspaceDefinition(doc, [
|
|
660
|
+
...(entity.type === 'person' && listed ? [['区分', personStanding(listed, ownId)]] : []),
|
|
661
|
+
...(entity.type === 'person' && listed && personOrgNote(listed, ownId)
|
|
662
|
+
? [['記録上のメモ', `${personOrgNote(listed, ownId)}(人物の記録に文字で残っている会社名です。所属の記録ではありません)`]]
|
|
663
|
+
: []),
|
|
664
|
+
...(entity.type === 'org' && entity.id === ownId ? [['区分', '自社']] : []),
|
|
665
|
+
...(company && company.kinds.length > 0 ? [['種類', company.kinds.map((item) => {
|
|
666
|
+
const sources = item.sources.map((source) => ownKey(COMPANY_KIND_SOURCE_LABELS, source) ?? source).join('・');
|
|
667
|
+
return `${ownKey(COMPANY_KIND_LABELS, item.kind) ?? item.kind}(${sources}から)`;
|
|
668
|
+
}).join('、')]] : []),
|
|
669
|
+
...(company?.lifecycleStage ? [['記録上の段階', company.lifecycleStage]] : []),
|
|
670
|
+
['有効期間', validityLabel(entity, asOf())],
|
|
671
|
+
['ID', entity.id],
|
|
672
|
+
]);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function detailBlock(title, content) {
|
|
676
|
+
const block = makeElement(doc, 'section', { className: 'bb-pc-detail-block', attrs: { 'aria-label': title } });
|
|
677
|
+
block.append(makeElement(doc, 'h3', { text: title }));
|
|
678
|
+
for (const item of [content].flat()) if (item) block.append(item);
|
|
679
|
+
return block;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function detailPage() {
|
|
683
|
+
const detail = state.detail;
|
|
684
|
+
detailTop = null;
|
|
685
|
+
detailHead = null;
|
|
686
|
+
const notice = graphStateNotice(doc, detail, { onRetry: () => void controller.openEntity(detail.id) });
|
|
687
|
+
if (notice) {
|
|
688
|
+
const nav = detailNav(null);
|
|
689
|
+
detailTop = nav;
|
|
690
|
+
return [nav, notice];
|
|
691
|
+
}
|
|
692
|
+
const { entity, outgoing, incoming } = detail.payload;
|
|
693
|
+
const listed = state.list.state === 'ok' ? state.list.payload.people.find((person) => person.id === entity.id) : null;
|
|
694
|
+
const ownId = state.list.state === 'ok' ? state.list.payload.ownCompany.id : null;
|
|
695
|
+
const company = state.list.state === 'ok' && entity.type === 'org'
|
|
696
|
+
? state.list.payload.companies.find((item) => item.id === entity.id)
|
|
697
|
+
: null;
|
|
698
|
+
const standing = entity.type === 'person' && listed
|
|
699
|
+
? personStanding(listed, ownId)
|
|
700
|
+
: entity.type === 'org' && entity.id === ownId ? '自社' : companyKindLabel(company) || null;
|
|
701
|
+
const memberships = entity.type === 'org'
|
|
702
|
+
? incoming.filter((edge) => edge.relation === 'member_of')
|
|
703
|
+
: outgoing.filter((edge) => edge.relation === 'member_of');
|
|
704
|
+
const page = makeElement(doc, 'article', { className: 'bb-pc-detail', attrs: { 'aria-label': `${entity.type === 'org' ? '会社' : '人'}「${entity.name}」` } });
|
|
705
|
+
detailTop = page;
|
|
706
|
+
page.append(detailNav(entity), detailHeadOf(entity, standing, company));
|
|
707
|
+
const actions = detailActions(entity, memberships);
|
|
708
|
+
if (actions) page.append(actions);
|
|
709
|
+
// A membership form opens right under the actions that open it.
|
|
710
|
+
const panel = correction.render(doc);
|
|
711
|
+
if (panel) page.append(panel);
|
|
712
|
+
// Without a rail of the host's, the facts stay on the page.
|
|
713
|
+
if (layout.inline) page.append(detailBlock('詳細', detailFacts(entity, listed, company, ownId)));
|
|
714
|
+
if (entity.type === 'org') {
|
|
715
|
+
page.append(
|
|
716
|
+
detailBlock('所属する人', edgeList(memberships, '所属する人の記録はありません。', { actions: (edge) => membershipActions(edge, entity) })),
|
|
717
|
+
detailBlock('この会社のプロジェクト', edgeList(incoming.filter((edge) => edge.relation === 'owned_by'), 'この会社のプロジェクトの記録はありません。')),
|
|
718
|
+
detailBlock('顧客・協業先として関わるプロジェクト', edgeList(outgoing.filter((edge) => edge.relation === 'related_to'), '顧客・協業先として関わるプロジェクトの記録はありません。', { roleLabel: relatedRoleLabel })),
|
|
719
|
+
);
|
|
720
|
+
if (company && company.deals.length > 0) page.append(detailBlock('提案中の案件', dealList(company.deals)));
|
|
721
|
+
} else {
|
|
722
|
+
page.append(
|
|
723
|
+
// A person's memberships are ended from the actions under the head.
|
|
724
|
+
detailBlock('所属', edgeList(memberships, '会社への所属の記録はありません。')),
|
|
725
|
+
detailBlock('関わるプロジェクト', edgeList(outgoing.filter((edge) => edge.relation === 'participates_in' || edge.relation === 'accountable_for'), '関わるプロジェクトの記録はありません。')),
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return [page];
|
|
488
729
|
}
|
|
489
730
|
|
|
490
731
|
// -------------------------------------------------------------------------
|
|
@@ -528,21 +769,25 @@ export function createPeopleAndCompaniesView({
|
|
|
528
769
|
return list;
|
|
529
770
|
}
|
|
530
771
|
|
|
531
|
-
function
|
|
532
|
-
if (!can.endMembership || !edge.active) return [];
|
|
772
|
+
function openEndMembership(edge, entity) {
|
|
533
773
|
const person = entity.type === 'org' ? edge.counterpart.name : entity.name;
|
|
534
774
|
const company = entity.type === 'org' ? entity.name : edge.counterpart.name;
|
|
535
|
-
|
|
775
|
+
correction.openEdge(edge, {
|
|
536
776
|
mode: 'end',
|
|
537
777
|
title: '所属を終える',
|
|
538
|
-
subject: `${person}の「${company}
|
|
539
|
-
})
|
|
778
|
+
subject: `${person}の「${company}」への所属に終了日を入れます。記録は消さずに残り、終了日から先は一覧に出ません。`,
|
|
779
|
+
});
|
|
540
780
|
}
|
|
541
781
|
|
|
542
|
-
|
|
543
|
-
|
|
782
|
+
/** On a company's page, each member's row ends that membership (a person's page has it under the head). */
|
|
783
|
+
function membershipActions(edge, entity) {
|
|
784
|
+
if (!can.endMembership || !edge.active) return [];
|
|
785
|
+
return [button(doc, '所属を終える', () => openEndMembership(edge, entity))];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function openMembership(entity) {
|
|
544
789
|
const label = entity.type === 'org' ? '所属する人を加える' : '所属を付ける';
|
|
545
|
-
|
|
790
|
+
correction.openCreate(
|
|
546
791
|
{ id: entity.id, type: entity.type, name: entity.name },
|
|
547
792
|
{
|
|
548
793
|
relations: ['member_of'],
|
|
@@ -551,82 +796,42 @@ export function createPeopleAndCompaniesView({
|
|
|
551
796
|
title: label,
|
|
552
797
|
subject: entity.type === 'org' ? `「${entity.name}」に所属する人を選びます。` : `${entity.name}が所属する会社を選びます。`,
|
|
553
798
|
},
|
|
554
|
-
)
|
|
799
|
+
);
|
|
555
800
|
}
|
|
556
801
|
|
|
802
|
+
/** The rail: a guide while the ledgers show, the opened record's facts while its page shows. */
|
|
557
803
|
function railContent() {
|
|
558
|
-
const panel = correction.render(doc);
|
|
559
|
-
const blocks = railBlocks();
|
|
560
|
-
if (!panel) return blocks;
|
|
561
|
-
// A new record's form shows first; a membership form right after the record's head.
|
|
562
|
-
if (correction.form?.kind === 'create_entity' || !state.detail || state.detail.state !== 'ok') return [panel, ...blocks];
|
|
563
|
-
return [blocks[0], panel, ...blocks.slice(1)];
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function railBlocks() {
|
|
567
804
|
const detail = state.detail;
|
|
568
805
|
if (!detail) {
|
|
569
|
-
return [workspaceDetailEmpty(doc, { mark: '人', title: '人か会社を選んでください', text: '
|
|
806
|
+
return [workspaceDetailEmpty(doc, { mark: '人', title: '人か会社を選んでください', text: '一覧の行を押すと、その人・会社のページが開き、所属と関わるプロジェクト、できる操作が出ます。' })];
|
|
570
807
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
const listed = state.list.state === 'ok'
|
|
575
|
-
? state.list.payload.people.find((person) => person.id === entity.id)
|
|
576
|
-
: null;
|
|
808
|
+
if (layout.inline || detail.state !== 'ok') return [];
|
|
809
|
+
const { entity } = detail.payload;
|
|
810
|
+
const listed = state.list.state === 'ok' ? state.list.payload.people.find((person) => person.id === entity.id) : null;
|
|
577
811
|
const ownId = state.list.state === 'ok' ? state.list.payload.ownCompany.id : null;
|
|
578
812
|
const company = state.list.state === 'ok' && entity.type === 'org'
|
|
579
813
|
? state.list.payload.companies.find((item) => item.id === entity.id)
|
|
580
814
|
: null;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return `${ownKey(COMPANY_KIND_LABELS, item.kind) ?? item.kind}(${sources}から)`;
|
|
596
|
-
}).join('、')]] : []),
|
|
597
|
-
...(company?.lifecycleStage ? [['記録上の段階', company.lifecycleStage]] : []),
|
|
598
|
-
['有効期間', validityLabel(entity, asOf())],
|
|
599
|
-
]));
|
|
600
|
-
if (entity.type === 'org') {
|
|
601
|
-
blocks.push(workspaceRailBlock(doc, { title: '所属する人', content: [
|
|
602
|
-
edgeList(incoming.filter((edge) => edge.relation === 'member_of'), '所属する人の記録はありません。', { actions: (edge) => membershipActions(edge, entity) }),
|
|
603
|
-
addMembershipButton(entity),
|
|
604
|
-
] }));
|
|
605
|
-
blocks.push(workspaceRailBlock(doc, { title: 'この会社のプロジェクト', content: edgeList(incoming.filter((edge) => edge.relation === 'owned_by'), 'この会社のプロジェクトの記録はありません。') }));
|
|
606
|
-
blocks.push(workspaceRailBlock(doc, {
|
|
607
|
-
title: '顧客・協業先として関わるプロジェクト',
|
|
608
|
-
content: edgeList(outgoing.filter((edge) => edge.relation === 'related_to'), '顧客・協業先として関わるプロジェクトの記録はありません。', { roleLabel: relatedRoleLabel }),
|
|
609
|
-
}));
|
|
610
|
-
if (company && company.deals.length > 0) blocks.push(workspaceRailBlock(doc, { title: '提案中の案件', content: dealList(company.deals) }));
|
|
611
|
-
} else {
|
|
612
|
-
blocks.push(workspaceRailBlock(doc, { title: '所属', content: [
|
|
613
|
-
edgeList(outgoing.filter((edge) => edge.relation === 'member_of'), '会社への所属の記録はありません。', { actions: (edge) => membershipActions(edge, entity) }),
|
|
614
|
-
addMembershipButton(entity),
|
|
615
|
-
] }));
|
|
616
|
-
blocks.push(workspaceRailBlock(doc, {
|
|
617
|
-
title: '関わるプロジェクト',
|
|
618
|
-
content: edgeList(outgoing.filter((edge) => edge.relation === 'participates_in' || edge.relation === 'accountable_for'), '関わるプロジェクトの記録はありません。'),
|
|
619
|
-
}));
|
|
815
|
+
return [workspaceRailBlock(doc, { title: '詳細', content: detailFacts(entity, listed, company, ownId) })];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** After a render: bring the opened record's head, or the row it was opened from, into view. */
|
|
819
|
+
function settleScroll() {
|
|
820
|
+
const target = pendingScroll;
|
|
821
|
+
if (!target) return;
|
|
822
|
+
if (target.head) {
|
|
823
|
+
if (!detailTop) return;
|
|
824
|
+
// Settled only once the record is read: a loading or failed page is shown, but the move waits.
|
|
825
|
+
if (detailHead) pendingScroll = null;
|
|
826
|
+
detailTop.scrollIntoView?.({ block: 'start' });
|
|
827
|
+
detailHead?.focus?.({ preventScroll: true });
|
|
828
|
+
return;
|
|
620
829
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
makeElement(doc, 'a', { text: '情報と関係で開く', attrs: { href: correctionHref(entity.id) } }),
|
|
627
|
-
);
|
|
628
|
-
blocks.push(fix);
|
|
629
|
-
return blocks;
|
|
830
|
+
pendingScroll = null;
|
|
831
|
+
const key = String(target.row).replace(/["\\]/gu, '\\$&');
|
|
832
|
+
const row = root.querySelector?.(`[data-key="${key}"]`);
|
|
833
|
+
row?.scrollIntoView?.({ block: 'center' });
|
|
834
|
+
row?.focus?.({ preventScroll: true });
|
|
630
835
|
}
|
|
631
836
|
|
|
632
837
|
// -------------------------------------------------------------------------
|
|
@@ -641,6 +846,11 @@ export function createPeopleAndCompaniesView({
|
|
|
641
846
|
},
|
|
642
847
|
render() {
|
|
643
848
|
layout.render(workspace(), railContent());
|
|
849
|
+
settleScroll();
|
|
850
|
+
},
|
|
851
|
+
/** Back from a record's page to the ledgers, at the row it was opened from. */
|
|
852
|
+
showList() {
|
|
853
|
+
backToList();
|
|
644
854
|
},
|
|
645
855
|
/** Reads the ledgers. `keepDetail` keeps the open record (and the shown ledgers) while reading. */
|
|
646
856
|
async load({ keepDetail = false } = {}) {
|
|
@@ -652,9 +862,13 @@ export function createPeopleAndCompaniesView({
|
|
|
652
862
|
controller.render();
|
|
653
863
|
if (initialEntityId && !state.detail) await controller.openEntity(initialEntityId);
|
|
654
864
|
},
|
|
655
|
-
/**
|
|
865
|
+
/**
|
|
866
|
+
* Opens a record's page and moves to its head. Choosing another record closes an open form
|
|
867
|
+
* unless `keepCorrection`; reading the same record again (after a save) stays where it is.
|
|
868
|
+
*/
|
|
656
869
|
async openEntity(id, { keepCorrection = false } = {}) {
|
|
657
870
|
if (!keepCorrection && correction.form && state.detail?.id !== id) correction.close();
|
|
871
|
+
if (state.detail?.id !== id) pendingScroll = { head: true };
|
|
658
872
|
state.detail = { id, state: 'loading' };
|
|
659
873
|
controller.render();
|
|
660
874
|
const result = await client.read(`/entities/${encodeURIComponent(id)}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Compact notices are projections of the same source-read states as the full rail.
|
|
2
2
|
* A task's needs_check is a record gap, never an unavailable read or failed job.
|
|
3
3
|
*/
|
|
4
|
-
import { readReasonText, shortTime } from './world-work-rail.js';
|
|
4
|
+
import { readReasonShort, readReasonText, shortTime } from './world-work-rail.js';
|
|
5
5
|
|
|
6
6
|
const READ_LABELS = Object.freeze({ tasks: '仕事', persons: '人物', decisions: '方針の決定', relations: '関係' });
|
|
7
7
|
const STATE_TEXT = Object.freeze({ partial: '一部だけ読めた', failed: '読込失敗', forbidden: '権限がなく読めない', not_connected: '未接続', unavailable: '読めない' });
|
|
@@ -17,7 +17,8 @@ export function workCanvasNotes(work) {
|
|
|
17
17
|
const read = work.reads?.[key];
|
|
18
18
|
if (!read || read.state !== 'complete') {
|
|
19
19
|
const state = STATE_TEXT[read?.state] ?? '状態不明';
|
|
20
|
-
|
|
20
|
+
const short = readReasonShort(read?.reason);
|
|
21
|
+
notes.push({ label: `区画の${label}`, tone: 'warning', summary: `${label}:${state}${short ? `(${short})` : ''}`, text: `${label}は${state}。${readReasonText(read?.reason) ?? '理由不明'}。読めていない記録は0件とは限りません。` });
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
const summary = work.summary;
|
package/ui/world/world-view.css
CHANGED
|
@@ -386,6 +386,25 @@
|
|
|
386
386
|
border: 1px dashed #8a9096;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
/* A sign opens what it means (story-world-sign-meaning-v1); its name keeps entering the district. */
|
|
390
|
+
.bb-world-badge.is-pressable {
|
|
391
|
+
cursor: help;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.bb-world-badge.is-pressable:hover {
|
|
395
|
+
text-decoration: underline;
|
|
396
|
+
text-underline-offset: 2px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.bb-world-label.is-enterable:has(.bb-world-badge.is-pressable:hover)::after {
|
|
400
|
+
content: ' 札の意味を見る ›';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.bb-world-sign-gaps li {
|
|
404
|
+
display: grid;
|
|
405
|
+
gap: 2px;
|
|
406
|
+
}
|
|
407
|
+
|
|
389
408
|
.bb-world-label.is-site {
|
|
390
409
|
font-size: 10px;
|
|
391
410
|
padding: 1px 5px;
|