@things-factory/worklist 10.1.198 → 10.1.203
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/dist-client/components/activity-thread-timeline.d.ts +2 -2
- package/dist-client/pages/activity/activity-list-page.d.ts +3 -0
- package/dist-client/pages/activity/activity-list-page.js +25 -13
- package/dist-client/pages/activity/activity-list-page.js.map +1 -1
- package/dist-client/pages/activity/activity-partial-view.js +1 -1
- package/dist-client/pages/activity/activity-partial-view.js.map +1 -1
- package/dist-client/pages/activity/assignee-roles-cell.d.ts +32 -0
- package/dist-client/pages/activity/assignee-roles-cell.js +26 -0
- package/dist-client/pages/activity/assignee-roles-cell.js.map +1 -0
- package/dist-client/pages/activity-catalog/activity-catalog-page.js +1 -1
- package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-view.d.ts +1 -1
- package/dist-client/pages/activity-thread/activity-thread-view.d.ts +1 -1
- package/dist-client/pages/work-door/door-sentence.d.ts +1 -1
- package/dist-client/pages/work-door/door-sentence.js +2 -2
- package/dist-client/pages/work-door/door-sentence.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types/activity-instance-type.d.ts +2 -2
- package/dist-client/types/activity-instance-type.js.map +1 -1
- package/dist-client/types/activity-instance.d.ts +1 -1
- package/dist-client/types/activity-instance.js.map +1 -1
- package/dist-client/types/activity.d.ts +1 -1
- package/dist-client/types/activity.js.map +1 -1
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.d.ts +2 -2
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js +2 -2
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js.map +1 -1
- package/dist-server/controllers/activity-instance/abort.js +1 -1
- package/dist-server/controllers/activity-instance/abort.js.map +1 -1
- package/dist-server/controllers/activity-instance/assign.d.ts +2 -1
- package/dist-server/controllers/activity-instance/assign.js +104 -2
- package/dist-server/controllers/activity-instance/assign.js.map +1 -1
- package/dist-server/controllers/activity-instance/assignee-input.d.ts +25 -0
- package/dist-server/controllers/activity-instance/assignee-input.js +28 -0
- package/dist-server/controllers/activity-instance/assignee-input.js.map +1 -0
- package/dist-server/controllers/activity-instance/draft.js +7 -2
- package/dist-server/controllers/activity-instance/draft.js.map +1 -1
- package/dist-server/controllers/activity-instance/end.js +1 -1
- package/dist-server/controllers/activity-instance/end.js.map +1 -1
- package/dist-server/controllers/activity-instance/entitlement.d.ts +1 -1
- package/dist-server/controllers/activity-instance/entitlement.js +7 -5
- package/dist-server/controllers/activity-instance/entitlement.js.map +1 -1
- package/dist-server/controllers/activity-instance/issue.js +28 -7
- package/dist-server/controllers/activity-instance/issue.js.map +1 -1
- package/dist-server/controllers/activity-instance/pick.js +22 -3
- package/dist-server/controllers/activity-instance/pick.js.map +1 -1
- package/dist-server/controllers/activity-read/assert-may-read.js +2 -1
- package/dist-server/controllers/activity-read/assert-may-read.js.map +1 -1
- package/dist-server/controllers/activity-read/participates.d.ts +2 -2
- package/dist-server/controllers/activity-read/participates.js +2 -2
- package/dist-server/controllers/activity-read/participates.js.map +1 -1
- package/dist-server/controllers/activity-thread/check-authority.js +22 -4
- package/dist-server/controllers/activity-thread/check-authority.js.map +1 -1
- package/dist-server/controllers/common.js +17 -12
- package/dist-server/controllers/common.js.map +1 -1
- package/dist-server/service/activity/activity-history.d.ts +1 -2
- package/dist-server/service/activity/activity-history.js +4 -8
- package/dist-server/service/activity/activity-history.js.map +1 -1
- package/dist-server/service/activity/activity-mutation.js +39 -8
- package/dist-server/service/activity/activity-mutation.js.map +1 -1
- package/dist-server/service/activity/activity-query.d.ts +1 -1
- package/dist-server/service/activity/activity-query.js +9 -9
- package/dist-server/service/activity/activity-query.js.map +1 -1
- package/dist-server/service/activity/activity-type.d.ts +2 -2
- package/dist-server/service/activity/activity-type.js +12 -6
- package/dist-server/service/activity/activity-type.js.map +1 -1
- package/dist-server/service/activity/activity.d.ts +2 -2
- package/dist-server/service/activity/activity.js +15 -7
- package/dist-server/service/activity/activity.js.map +1 -1
- package/dist-server/service/activity/event-subscriber.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-history.d.ts +1 -2
- package/dist-server/service/activity-instance/activity-instance-history.js +4 -8
- package/dist-server/service/activity-instance/activity-instance-history.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-mutation.d.ts +1 -1
- package/dist-server/service/activity-instance/activity-instance-mutation.js +10 -5
- package/dist-server/service/activity-instance/activity-instance-mutation.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-query.d.ts +1 -2
- package/dist-server/service/activity-instance/activity-instance-query.js +16 -25
- package/dist-server/service/activity-instance/activity-instance-query.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance-type.d.ts +2 -2
- package/dist-server/service/activity-instance/activity-instance-type.js +12 -6
- package/dist-server/service/activity-instance/activity-instance-type.js.map +1 -1
- package/dist-server/service/activity-instance/activity-instance.d.ts +2 -2
- package/dist-server/service/activity-instance/activity-instance.js +15 -7
- package/dist-server/service/activity-instance/activity-instance.js.map +1 -1
- package/dist-server/service/activity-instance/assignee-pool.d.ts +59 -0
- package/dist-server/service/activity-instance/assignee-pool.js +99 -0
- package/dist-server/service/activity-instance/assignee-pool.js.map +1 -0
- package/dist-server/service/activity-instance/event-subscriber.d.ts +9 -1
- package/dist-server/service/activity-instance/event-subscriber.js +20 -4
- package/dist-server/service/activity-instance/event-subscriber.js.map +1 -1
- package/dist-server/service/activity-summary/activity-summary-query.js +3 -3
- package/dist-server/service/activity-summary/activity-summary-query.js.map +1 -1
- package/dist-server/service/activity-thread/my-open-threads.d.ts +1 -1
- package/dist-server/service/activity-thread/my-open-threads.js +4 -3
- package/dist-server/service/activity-thread/my-open-threads.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/subscription/subscription-filters.d.ts +3 -2
- package/dist-server/service/subscription/subscription-filters.js +6 -3
- package/dist-server/service/subscription/subscription-filters.js.map +1 -1
- package/dist-server/service/work-door/work-door-service.js +9 -6
- package/dist-server/service/work-door/work-door-service.js.map +1 -1
- package/dist-server/service/work-door/work-door-type.d.ts +1 -1
- package/dist-server/service/work-door/work-door-type.js +4 -4
- package/dist-server/service/work-door/work-door-type.js.map +1 -1
- package/dist-server/service/work-door/work-door.d.ts +3 -3
- package/dist-server/service/work-door/work-door.js +2 -2
- package/dist-server/service/work-door/work-door.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/tests/assign-role-pool.test.ts +508 -0
- package/tests/assignee-pool-history.test.ts +81 -0
- package/tests/may-read-case.test.ts +3 -2
- package/tests/org-member-domain.test.ts +10 -4
- package/tests/pick-race.pg.test.ts +2 -2
- package/tests/published-shape.test.ts +2 -2
- package/tests/role-pool-race.pg.test.ts +182 -0
- package/tests/work-door.test.ts +3 -3
- package/translations/en.json +5 -0
- package/translations/es.json +5 -0
- package/translations/ja.json +5 -0
- package/translations/ko.json +5 -0
- package/translations/ms.json +5 -0
- package/translations/zh.json +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-subscriber.js","sourceRoot":"","sources":["../../../server/service/activity-instance/event-subscriber.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"event-subscriber.js","sourceRoot":"","sources":["../../../server/service/activity-instance/event-subscriber.ts"],"names":[],"mappings":";;;AAgBA,kCAYC;;AA5BD,qCAA6G;AAE7G,8DAAkE;AAClE,iDAA8C;AAE9C,iEAAyD;AACzD,iFAAwE;AACxE,yDAAyD;AAEzD;;;;;;GAMG;AACI,KAAK,UAAU,WAAW,CAAC,gBAAkC,EAAE,OAAsB;IAC1F,IAAI,CAAC,gBAAgB,EAAE,EAAE;QAAE,OAAO,gBAAgB,CAAA;IAClD,IAAI,gBAAgB,CAAC,eAAe,IAAI,gBAAgB,CAAC,aAAa,EAAE,CAAC;QACvE,OAAO,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,eAAe,EAAE,IAAA,4BAAS,EAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;IAC1F,CAAC;IAED,MAAM,IAAI,GAA2B,MAAM,OAAO,CAAC,KAAK,CACtD,UAAU,4BAAS,CAAC,UAAU,qBAAqB,4BAAS,CAAC,KAAK,UAAU,4BAAS,CAAC,UAAU,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAC1J,CAAC,gBAAgB,CAAC,EAAE,CAAC,CACtB,CAAA;IAED,OAAO,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AAC5F,CAAC;AAGM,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IACrC,QAAQ;QACN,OAAO,uCAAgB,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAoC;QACpD,cAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAClC,gBAAgB,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,MAA0B,EAAE,KAAK,CAAC,OAAO,CAAC;SACrF,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAoC;QACpD,cAAM,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAClC,gBAAgB,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,MAA0B,EAAE,KAAK,CAAC,OAAO,CAAC;SACrF,CAAC,CAAA;IACJ,CAAC;CACF,CAAA;AAhBY,gEAA0B;qCAA1B,0BAA0B;IADtC,IAAA,yBAAe,GAAE;GACL,0BAA0B,CAgBtC;AAGM,IAAM,uCAAuC,GAA7C,MAAM,uCAAwC,SAAQ,yCAG5D;IACC,IAAW,MAAM;QACf,OAAO,uCAAgB,CAAA;IACzB,CAAC;IAED,IAAW,aAAa;QACtB,OAAO,sDAAuB,CAAA;IAChC,CAAC;CAEF,CAAA;AAZY,0FAAuC;kDAAvC,uCAAuC;IADnD,IAAA,yBAAe,GAAE;GACL,uCAAuC,CAYnD","sourcesContent":["import { EntityManager, EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent } from 'typeorm'\n\nimport { HistoryEntitySubscriber } from '@operato/typeorm-history'\nimport { pubsub } from '@things-factory/shell'\n\nimport { ActivityInstance } from './activity-instance.js'\nimport { ActivityInstanceHistory } from './activity-instance-history.js'\nimport { CASE_POOL, poolIdsOf } from './assignee-pool.js'\n\n/**\n * The pool's role ids on what is published - the subscription filter decides by them (subscription-filters.ts).\n *\n * A many-to-many is not a column: the saved object carries the roles only when the caller had them, and its\n * RelationId is not filled in on a write. Asked here once per event when it is missing, never once per\n * subscriber. An update with no entity (a query-builder UPDATE) publishes nothing to read, as before.\n */\nexport async function withPoolIds(activityInstance: ActivityInstance, manager: EntityManager): Promise<ActivityInstance> {\n if (!activityInstance?.id) return activityInstance\n if (activityInstance.assigneeRoleIds || activityInstance.assigneeRoles) {\n return Object.assign(activityInstance, { assigneeRoleIds: poolIdsOf(activityInstance) })\n }\n\n const rows: { roles_id: string }[] = await manager.query(\n `SELECT ${CASE_POOL.roleColumn} AS roles_id FROM ${CASE_POOL.table} WHERE ${CASE_POOL.caseColumn} = ${manager.connection.driver.createParameter('id', 0)}`,\n [activityInstance.id]\n )\n\n return Object.assign(activityInstance, { assigneeRoleIds: rows.map(row => row.roles_id) })\n}\n\n@EventSubscriber()\nexport class ActivityInstanceSubscriber implements EntitySubscriberInterface<ActivityInstance> {\n listenTo() {\n return ActivityInstance\n }\n\n async afterInsert(event: InsertEvent<ActivityInstance>): Promise<any | void> {\n pubsub.publish('activity-instance', {\n activityInstance: await withPoolIds(event.entity as ActivityInstance, event.manager)\n })\n }\n\n async afterUpdate(event: UpdateEvent<ActivityInstance>): Promise<any | void> {\n pubsub.publish('activity-instance', {\n activityInstance: await withPoolIds(event.entity as ActivityInstance, event.manager)\n })\n }\n}\n\n@EventSubscriber()\nexport class ActivityInstanceHistoryEntitySubscriber extends HistoryEntitySubscriber<\n ActivityInstance,\n ActivityInstanceHistory\n> {\n public get entity() {\n return ActivityInstance\n }\n\n public get historyEntity() {\n return ActivityInstanceHistory\n }\n\n}\n"]}
|
|
@@ -7,6 +7,7 @@ const auth_base_1 = require("@things-factory/auth-base");
|
|
|
7
7
|
const shell_1 = require("@things-factory/shell");
|
|
8
8
|
const activity_summary_js_1 = require("./activity-summary.js");
|
|
9
9
|
const activity_instance_js_1 = require("../activity-instance/activity-instance.js");
|
|
10
|
+
const assignee_pool_js_1 = require("../activity-instance/assignee-pool.js");
|
|
10
11
|
const open_work_js_1 = require("./open-work.js");
|
|
11
12
|
let ActivitySummaryQuery = class ActivitySummaryQuery {
|
|
12
13
|
async activitySummary(context) {
|
|
@@ -49,8 +50,7 @@ let ActivitySummaryQuery = class ActivitySummaryQuery {
|
|
|
49
50
|
var { domain, user } = context.state;
|
|
50
51
|
/*
|
|
51
52
|
activity-instance 중에서 아직 created 상태이면서,
|
|
52
|
-
activity-instance의
|
|
53
|
-
assigneeRole이 지정되었으면 그 Role에 내가 해당된 경우이면서,
|
|
53
|
+
activity-instance 의 assigneeRoles 중 하나라도 내가 가진 경우이면서,
|
|
54
54
|
나는 아직 해당 activity-instance의 thread를 할당받지 않은 경우에 해당하는
|
|
55
55
|
activity-instance 리스트를 모두 가져오는 쿼리이다.
|
|
56
56
|
*/
|
|
@@ -72,7 +72,7 @@ let ActivitySummaryQuery = class ActivitySummaryQuery {
|
|
|
72
72
|
.andWhere('ai.state IN (:...status)', {
|
|
73
73
|
status: [activity_instance_js_1.ActivityInstanceStatus.Issued, activity_instance_js_1.ActivityInstanceStatus.Unassigned]
|
|
74
74
|
})
|
|
75
|
-
.andWhere('ai
|
|
75
|
+
.andWhere((0, assignee_pool_js_1.inPoolOf)('ai'), { roles })
|
|
76
76
|
.andWhere('threads.id IS NULL') /* 이미 내가 할당된 경우는 제외한다. */
|
|
77
77
|
.getCount();
|
|
78
78
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-summary-query.js","sourceRoot":"","sources":["../../../server/service/activity-summary/activity-summary-query.ts"],"names":[],"mappings":";;;;AACA,+CAAwE;AAExE,yDAAgD;AAChD,iDAAoF;AAEpF,+DAAuD;AAIvD,oFAAoG;AACpG,iDAA4D;AAGrD,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAEzB,AAAN,KAAK,CAAC,eAAe,CAAQ,OAAwB;QACnD,OAAO,EAAE,CAAA;IACX,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CAAS,eAAgC,EAAS,OAAwB;QAC3F,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEtC,gGAAgG;QAChG,OAAO,MAAM,IAAA,sBAAO,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC;IAGK,AAAN,KAAK,CAAC,wBAAwB,CACpB,eAAgC,EACjC,OAAwB;QAE/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEtC;;;;;;;;;;;;;;;;;;;;;;;WAuBG;QACH,OAAO,MAAM,IAAA,iCAAkB,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,CAAA;IAClD,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CAAS,eAAgC,EAAS,OAAwB;QAC3F,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEpC
|
|
1
|
+
{"version":3,"file":"activity-summary-query.js","sourceRoot":"","sources":["../../../server/service/activity-summary/activity-summary-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Brackets } from 'typeorm'\nimport { Ctx, FieldResolver, Query, Resolver, Root } from 'type-graphql'\n\nimport { User } from '@things-factory/auth-base'\nimport { getQueryBuilderFromListParams, getRepository } from '@things-factory/shell'\n\nimport { ActivitySummary } from './activity-summary.js'\nimport { ActivityThread, ActivityThreadStatus } from '../activity-thread/activity-thread.js'\nimport { ActivityApproval } from '../activity-approval/activity-approval.js'\nimport { myPendingApprovalsWhere } from '../activity-approval/my-pending-approvals.js'\nimport { ActivityInstance, ActivityInstanceStatus } from '../activity-instance/activity-instance.js'\nimport { inPoolOf } from '../activity-instance/assignee-pool.js'\nimport { pendingApprovalsOf, toDosOf } from './open-work.js'\n\n@Resolver(ActivitySummary)\nexport class ActivitySummaryQuery {\n @Query(returns => ActivitySummary!, { nullable: true, description: 'To fetch a Activity Summary' })\n async activitySummary(@Ctx() context: ResolverContext): Promise<ActivitySummary> {\n return {}\n }\n\n @FieldResolver(type => Number)\n async numberOfToDos(@Root() activitySummary: ActivitySummary, @Ctx() context: ResolverContext): Promise<number> {\n const { domain, user } = context.state\n\n /* The rule lives in `open-work.ts`, shared with the person 360 - one answer to one question. */\n return await toDosOf(domain, user.id)\n }\n\n @FieldResolver(type => Number)\n async numberOfApprovalPendings(\n @Root() activitySummary: ActivitySummary,\n @Ctx() context: ResolverContext\n ): Promise<number> {\n const { domain, user } = context.state\n\n /*\n * The badge asks the same question the inbox does, through the same clause.\n *\n * ── What the badge was counting ───────────────────────────────────────\n * It had its own copy of \"not finished yet\", written as NOT IN four judgments, and that copy\n * had drifted three ways from the list it labels:\n *\n * closed a reference seat the case closed when it ended\n * aborted an approval closed because the thread was aborted\n * reference a seat that comments and never decides\n *\n * TERMINATED_JUDGMENTS holds six; this named four. So every ended thread and every case a\n * reference had been on kept adding to the number, and a role-held row had no judgment test at\n * all — once closed with nobody having pressed, `approver` stays empty and it would have been\n * counted forever, for everyone holding that role.\n *\n * The badge therefore read higher than the inbox it points at. Opening it showed fewer, or\n * nothing: a number that sends someone to an empty screen teaches them to stop believing it.\n *\n * ── Why the clause is borrowed rather than fixed here ─────────────────\n * Naming the six here would make the count right today and drift again at the seventh. The\n * inbox list and the calendar already share one clause for this; the badge is the third reader\n * and now asks through it too. `myPendingApprovalsWhere` carries why each part is there.\n */\n return await pendingApprovalsOf(domain, user.id)\n }\n\n @FieldResolver(type => Number)\n async numberOfPicks(@Root() activitySummary: ActivitySummary, @Ctx() context: ResolverContext): Promise<number> {\n var { domain, user } = context.state\n\n /* \n activity-instance 중에서 아직 created 상태이면서, \n activity-instance 의 assigneeRoles 중 하나라도 내가 가진 경우이면서,\n 나는 아직 해당 activity-instance의 thread를 할당받지 않은 경우에 해당하는\n activity-instance 리스트를 모두 가져오는 쿼리이다.\n */\n\n user = await getRepository(User).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n const roles = user.roles.filter(role => role.domainId === domain.id).map(role => role.id)\n\n if (!roles.length) {\n return 0\n }\n\n return await getQueryBuilderFromListParams({\n repository: getRepository(ActivityInstance),\n params: {},\n domain,\n alias: 'ai'\n })\n .leftJoinAndSelect('ai.activityThreads', 'threads', 'threads.assignee = :user', { user: user.id })\n .andWhere('ai.state IN (:...status)', {\n status: [ActivityInstanceStatus.Issued, ActivityInstanceStatus.Unassigned]\n })\n .andWhere(inPoolOf('ai'), { roles })\n .andWhere('threads.id IS NULL') /* 이미 내가 할당된 경우는 제외한다. */\n .getCount()\n }\n\n @FieldResolver(type => Number)\n async numberOfDrafts(@Root() activitySummary: ActivitySummary, @Ctx() context: ResolverContext): Promise<number> {\n var { domain, user } = context.state\n\n /* 생성자가 자신이면서, 아직 Draft 상태인 activity-instance 리스트만 반환 */\n const status = [ActivityInstanceStatus.Draft]\n\n return await getQueryBuilderFromListParams({\n repository: getRepository(ActivityInstance),\n params: {},\n domain,\n alias: 'ai'\n })\n .andWhere('ai.state IN (:...status)', { status })\n .andWhere('ai.creator = :user', { user: user.id })\n .getCount()\n }\n}\n"]}
|
|
@@ -12,7 +12,7 @@ export declare const FINISHED: ActivityThreadStatus[];
|
|
|
12
12
|
* a different number than the list it opens. Nothing errors.
|
|
13
13
|
*
|
|
14
14
|
* Expects `at` (ActivityThread) and `ai` (ActivityInstance) to be joined already — the role branch
|
|
15
|
-
* reads
|
|
15
|
+
* reads the instance's pool (assignee-pool.ts).
|
|
16
16
|
*/
|
|
17
17
|
export declare function myOpenThreadsWhere({ userId, roles }: {
|
|
18
18
|
userId: string;
|
|
@@ -4,6 +4,7 @@ exports.FINISHED = exports.WAITING_ON_OTHERS = void 0;
|
|
|
4
4
|
exports.myOpenThreadsWhere = myOpenThreadsWhere;
|
|
5
5
|
exports.myThreadsOfOthersWhere = myThreadsOfOthersWhere;
|
|
6
6
|
const typeorm_1 = require("typeorm");
|
|
7
|
+
const assignee_pool_js_1 = require("../activity-instance/assignee-pool.js");
|
|
7
8
|
const activity_thread_js_1 = require("./activity-thread.js");
|
|
8
9
|
/**
|
|
9
10
|
* Thread states that are **not** my turn.
|
|
@@ -34,18 +35,18 @@ exports.FINISHED = [activity_thread_js_1.ActivityThreadStatus.Ended, activity_th
|
|
|
34
35
|
* a different number than the list it opens. Nothing errors.
|
|
35
36
|
*
|
|
36
37
|
* Expects `at` (ActivityThread) and `ai` (ActivityInstance) to be joined already — the role branch
|
|
37
|
-
* reads
|
|
38
|
+
* reads the instance's pool (assignee-pool.ts).
|
|
38
39
|
*/
|
|
39
40
|
function myOpenThreadsWhere({ userId, roles }) {
|
|
40
41
|
return new typeorm_1.Brackets(qb => {
|
|
41
42
|
qb.where('at.state NOT IN (:...notMyTurn)', { notMyTurn: NOT_MY_TURN }).andWhere('at.assignee = :userId', {
|
|
42
43
|
userId
|
|
43
44
|
});
|
|
44
|
-
/* 역할로 걸린 것은 아직 임자가 없다 — 그
|
|
45
|
+
/* 역할로 걸린 것은 아직 임자가 없다 — 그 역할들 중 하나라도 가진 사람 모두의 할 일이다. */
|
|
45
46
|
if (roles.length > 0) {
|
|
46
47
|
qb.orWhere(new typeorm_1.Brackets(subQb => {
|
|
47
48
|
subQb
|
|
48
|
-
.where('ai
|
|
49
|
+
.where((0, assignee_pool_js_1.inPoolOf)('ai'), { roles })
|
|
49
50
|
.andWhere('at.state = :unassigned', { unassigned: activity_thread_js_1.ActivityThreadStatus.Unassigned });
|
|
50
51
|
}));
|
|
51
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"my-open-threads.js","sourceRoot":"","sources":["../../../server/service/activity-thread/my-open-threads.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"my-open-threads.js","sourceRoot":"","sources":["../../../server/service/activity-thread/my-open-threads.ts"],"names":[],"mappings":";;;AAuCA,gDAiBC;AAYD,wDAMC;AA1ED,qCAAkC;AAElC,4EAAgE;AAChE,6DAA2D;AAE3D;;;;;;;;;GASG;AACH,MAAM,WAAW,GAAG;IAClB,yCAAoB,CAAC,UAAU;IAC/B,yCAAoB,CAAC,SAAS;IAC9B,yCAAoB,CAAC,SAAS;IAC9B,yCAAoB,CAAC,KAAK;IAC1B,yCAAoB,CAAC,OAAO;CAC7B,CAAA;AAED,2DAA2D;AAC9C,QAAA,iBAAiB,GAAG,CAAC,yCAAoB,CAAC,SAAS,EAAE,yCAAoB,CAAC,SAAS,CAAC,CAAA;AAEjG,6BAA6B;AAChB,QAAA,QAAQ,GAAG,CAAC,yCAAoB,CAAC,KAAK,EAAE,yCAAoB,CAAC,OAAO,CAAC,CAAA;AAElF;;;;;;;;;GASG;AACH,SAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAuC;IACvF,OAAO,IAAI,kBAAQ,CAAC,EAAE,CAAC,EAAE;QACvB,EAAE,CAAC,KAAK,CAAC,iCAAiC,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,uBAAuB,EAAE;YACxG,MAAM;SACP,CAAC,CAAA;QAEF,yDAAyD;QACzD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,EAAE,CAAC,OAAO,CACR,IAAI,kBAAQ,CAAC,KAAK,CAAC,EAAE;gBACnB,KAAK;qBACF,KAAK,CAAC,IAAA,2BAAQ,EAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;qBAChC,QAAQ,CAAC,wBAAwB,EAAE,EAAE,UAAU,EAAE,yCAAoB,CAAC,UAAU,EAAE,CAAC,CAAA;YACxF,CAAC,CAAC,CACH,CAAA;QACH,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,sBAAsB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAsD;IAC3G,OAAO,IAAI,kBAAQ,CAAC,EAAE,CAAC,EAAE;QACvB,EAAE,CAAC,KAAK,CAAC,uBAAuB,EAAE,EAAE,MAAM,EAAE,CAAC;aAC1C,QAAQ,CAAC,0BAA0B,EAAE,EAAE,MAAM,EAAE,CAAC;aAChD,QAAQ,CAAC,+CAA+C,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IAC1E,CAAC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { Brackets } from 'typeorm'\n\nimport { inPoolOf } from '../activity-instance/assignee-pool.js'\nimport { ActivityThreadStatus } from './activity-thread.js'\n\n/**\n * Thread states that are **not** my turn.\n *\n * Submitted and escalated wait on an approver: the thread is still assigned to me, but nothing I do\n * moves it. Counting them made 할 일 say 1 to a person who had just put up an expenditure request\n * and had nothing left to do (2026-10-16). They are the 「제출한 것」 group of the same box.\n *\n * Unassigned belongs here for the person branch only — a thread nobody holds is not on my plate\n * until I claim it, and the role branch below is what surfaces it to the people who could.\n */\nconst NOT_MY_TURN = [\n ActivityThreadStatus.Unassigned,\n ActivityThreadStatus.Submitted,\n ActivityThreadStatus.Escalated,\n ActivityThreadStatus.Ended,\n ActivityThreadStatus.Aborted\n]\n\n/** A thread I submitted, waiting on someone's judgment. */\nexport const WAITING_ON_OTHERS = [ActivityThreadStatus.Submitted, ActivityThreadStatus.Escalated]\n\n/** A thread that is over. */\nexport const FINISHED = [ActivityThreadStatus.Ended, ActivityThreadStatus.Aborted]\n\n/**\n * The work that is on my plate — one definition, three readers.\n *\n * 할 일 목록 asks this, and so do the calendar and the badge (open-work). Written twice they drift:\n * a new state, or a new way of being assigned, lands in one and not the other, and the badge says\n * a different number than the list it opens. Nothing errors.\n *\n * Expects `at` (ActivityThread) and `ai` (ActivityInstance) to be joined already — the role branch\n * reads the instance's pool (assignee-pool.ts).\n */\nexport function myOpenThreadsWhere({ userId, roles }: { userId: string; roles: string[] }): Brackets {\n return new Brackets(qb => {\n qb.where('at.state NOT IN (:...notMyTurn)', { notMyTurn: NOT_MY_TURN }).andWhere('at.assignee = :userId', {\n userId\n })\n\n /* 역할로 걸린 것은 아직 임자가 없다 — 그 역할들 중 하나라도 가진 사람 모두의 할 일이다. */\n if (roles.length > 0) {\n qb.orWhere(\n new Brackets(subQb => {\n subQb\n .where(inPoolOf('ai'), { roles })\n .andWhere('at.state = :unassigned', { unassigned: ActivityThreadStatus.Unassigned })\n })\n )\n }\n })\n}\n\n/**\n * My threads in these states, of work **someone else** put up.\n *\n * What I put up myself is followed in 상신함 — the box of what I gave others to do. A post thread\n * is mine only as the vehicle of my request: once submitted it waits on the approvers, and when it\n * ends it is my request that ended. Listing it again under 할 일 put one fact in two boxes. While it\n * is my turn - rejected and back with me - it stays in 할 일, which is myOpenThreadsWhere above.\n *\n * Expects `at` and `ai` joined.\n */\nexport function myThreadsOfOthersWhere({ userId, states }: { userId: string; states: ActivityThreadStatus[] }): Brackets {\n return new Brackets(qb => {\n qb.where('at.assignee = :userId', { userId })\n .andWhere('at.state IN (:...states)', { states })\n .andWhere('(ai.creator IS NULL OR ai.creator != :userId)', { userId })\n })\n}\n"]}
|
|
@@ -16,7 +16,7 @@ export * from './work-door/work-door-not-used.js';
|
|
|
16
16
|
export * from './work-door/work-door-journal.js';
|
|
17
17
|
export * from './work-door/work-door-journal-service.js';
|
|
18
18
|
export * from './work-door/work-door-service.js';
|
|
19
|
-
export declare const entities: (typeof import("./activity/activity.js").Activity | typeof import("./activity-approval/activity-approval.js").ActivityApproval | typeof import("./activity-thread/activity-thread.js").ActivityThread | typeof import("./activity-instance/activity-instance.js").ActivityInstance | typeof import("./installable-activity/installable-activity.js").InstallableActivity | typeof import("./activity-template/activity-template.js").ActivityTemplate | typeof import("./activity/activity-
|
|
19
|
+
export declare const entities: (typeof import("./activity/activity.js").Activity | typeof import("./activity-approval/activity-approval.js").ActivityApproval | typeof import("./activity-thread/activity-thread.js").ActivityThread | typeof import("./activity-instance/activity-instance.js").ActivityInstance | typeof import("./installable-activity/installable-activity.js").InstallableActivity | typeof import("./activity-template/activity-template.js").ActivityTemplate | typeof import("./activity-summary/activity-summary.js").ActivitySummary | typeof import("./activity/activity-history.js").ActivityHistory | typeof import("./activity-instance/activity-instance-history.js").ActivityInstanceHistory | typeof import("./activity-template/activity-template-history.js").ActivityTemplateHistory | typeof import("./activity-thread/activity-thread-history.js").ActivityThreadHistory | typeof import("./work-door/work-door-not-used.js").WorkDoorNotUsed | typeof import("./work-door/work-door-journal.js").WorkDoorJournal)[];
|
|
20
20
|
export declare const subscribers: (typeof import("./activity-approval/event-subscriber.js").ActivityApprovalSubscriber | typeof import("./activity/event-subscriber.js").ActivityHistoryEntitySubscriber | typeof import("./activity/declaration-guard-subscriber.js").ActivityDeclarationGuardSubscriber | typeof import("./activity-instance/event-subscriber.js").ActivityInstanceSubscriber | typeof import("./activity-instance/event-subscriber.js").ActivityInstanceHistoryEntitySubscriber | typeof import("./activity-thread/event-subscriber.js").ActivityThreadSubscriber | typeof import("./activity-thread/event-subscriber.js").ActivityThreadHistoryEntitySubscriber)[];
|
|
21
21
|
export declare const schema: {
|
|
22
22
|
resolverClasses: (typeof import("./activity-stats/activity-stats-query.js").ActivityStatsQuery | typeof import("./activity-approval/activity-approval-query.js").ActivityApprovalQuery | typeof import("./activity-approval/activity-approval-mutation.js").ActivityApprovalMutation | typeof import("./activity-approval/activity-approval-subscription.js").ActivityApprovalSubscription | typeof import("./activity/activity-mutation.js").ActivityMutation | typeof import("./activity/activity-query.js").ActivityQuery | typeof import("./activity-instance/activity-instance-mutation.js").ActivityInstanceMutation | typeof import("./activity-instance/activity-instance-query.js").ActivityInstanceQuery | typeof import("./activity-instance/activity-instance-subscription.js").ActivityInstanceSubscription | typeof import("./activity-template/activity-template-mutation.js").ActivityTemplateMutation | typeof import("./activity-template/activity-template-query.js").ActivityTemplateQuery | typeof import("./activity-thread/activity-thread-mutation.js").ActivityThreadMutation | typeof import("./activity-thread/activity-thread-query.js").ActivityThreadQuery | typeof import("./activity-thread/activity-thread-history-query.js").ActivityThreadHistoryQuery | typeof import("./activity-thread/activity-thread-subscription.js").ActivityThreadSubscription | typeof import("./installable-activity/installable-activity-query.js").InstallableActivityQuery | typeof import("./installable-activity/installable-activity-mutation.js").InstallableActivityMutation | typeof import("./activity-summary/activity-summary-query.js").ActivitySummaryQuery | typeof import("./activity-summary/person-open-work.js").PersonOpenWorkQuery | typeof import("./work-door/work-door-resolver.js").WorkDoorResolver)[];
|
|
@@ -33,13 +33,14 @@ export declare function hearsApproval(approval: {
|
|
|
33
33
|
id: string;
|
|
34
34
|
}[]>): Promise<boolean>;
|
|
35
35
|
/**
|
|
36
|
-
* An instance reaches whoever made it, and anyone holding the
|
|
36
|
+
* An instance reaches whoever made it, and anyone holding any one of the roles it is waiting for - once,
|
|
37
|
+
* however many of them they hold: this answers yes or no for one subscriber. The roles are
|
|
37
38
|
* asked only after the domain matched — the lookup is not free and is never needed for another tenant.
|
|
38
39
|
*/
|
|
39
40
|
export declare function hearsInstance(instance: {
|
|
40
41
|
domainId?: string;
|
|
41
42
|
creatorId?: string;
|
|
42
|
-
|
|
43
|
+
assigneeRoleIds?: string[];
|
|
43
44
|
}, domain: DomainLike, userId: string, rolesOf: () => Promise<{
|
|
44
45
|
id: string;
|
|
45
46
|
}[]>): Promise<boolean>;
|
|
@@ -39,7 +39,8 @@ async function hearsApproval(approval, domain, userId, rolesOf) {
|
|
|
39
39
|
return (await rolesOf()).some(role => role.id === approval.approverRoleId);
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* An instance reaches whoever made it, and anyone holding the
|
|
42
|
+
* An instance reaches whoever made it, and anyone holding any one of the roles it is waiting for - once,
|
|
43
|
+
* however many of them they hold: this answers yes or no for one subscriber. The roles are
|
|
43
44
|
* asked only after the domain matched — the lookup is not free and is never needed for another tenant.
|
|
44
45
|
*/
|
|
45
46
|
async function hearsInstance(instance, domain, userId, rolesOf) {
|
|
@@ -47,8 +48,10 @@ async function hearsInstance(instance, domain, userId, rolesOf) {
|
|
|
47
48
|
return false;
|
|
48
49
|
if (instance.creatorId === userId)
|
|
49
50
|
return true;
|
|
50
|
-
|
|
51
|
+
const poolIds = instance.assigneeRoleIds || [];
|
|
52
|
+
if (!poolIds.length)
|
|
51
53
|
return false;
|
|
52
|
-
|
|
54
|
+
const held = (await rolesOf()).map(role => role.id);
|
|
55
|
+
return poolIds.some(id => held.includes(id));
|
|
53
56
|
}
|
|
54
57
|
//# sourceMappingURL=subscription-filters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription-filters.js","sourceRoot":"","sources":["../../../server/service/subscription/subscription-filters.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AASH,kCAEC;AAUD,sCAWC;
|
|
1
|
+
{"version":3,"file":"subscription-filters.js","sourceRoot":"","sources":["../../../server/service/subscription/subscription-filters.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AASH,kCAEC;AAUD,sCAWC;AAOD,sCAaC;AAhDD,SAAS,QAAQ,CAAC,aAAiC,EAAE,MAAkB;IACrE,OAAO,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,IAAI,aAAa,KAAK,MAAM,CAAC,EAAE,CAAA;AACvE,CAAC;AAED,qDAAqD;AACrD,SAAgB,WAAW,CAAC,MAAkD,EAAE,MAAkB,EAAE,MAAc;IAChH,OAAO,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM,CAAA;AAC3E,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,aAAa,CACjC,QAA6E,EAC7E,MAAkB,EAClB,MAAc,EACd,OAAwC;IAExC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,QAAQ,CAAC,UAAU,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAC/C,IAAI,CAAC,QAAQ,CAAC,cAAc;QAAE,OAAO,KAAK,CAAA;IAE1C,OAAO,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,QAAQ,CAAC,cAAc,CAAC,CAAA;AAC5E,CAAC;AAED;;;;GAIG;AACI,KAAK,UAAU,aAAa,CACjC,QAA+E,EAC/E,MAAkB,EAClB,MAAc,EACd,OAAwC;IAExC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IACvD,IAAI,QAAQ,CAAC,SAAS,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAC9C,MAAM,OAAO,GAAG,QAAQ,CAAC,eAAe,IAAI,EAAE,CAAA;IAC9C,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAEjC,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACnD,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;AAC9C,CAAC","sourcesContent":["/**\n * Who hears which worklist event — the filters behind the three subscriptions, kept free of\n * entities and pubsub so they can be tested.\n *\n * Every filter compares the event's domain to the subscriber's first. A thread or an approval in\n * another tenant must never reach this socket, even when the ids beside it happen to match.\n * An event whose domain is not known is dropped: without it the filter cannot tell tenants apart.\n *\n * The ids are the entity's @RelationId fields. TypeORM fills them on the entity it hands the\n * subscriber, on insert as well as update - measured, and held by tests/published-shape.test.ts.\n */\n\ntype DomainLike = { id?: string } | null | undefined\n\nfunction inDomain(eventDomainId: string | undefined, domain: DomainLike): boolean {\n return !!eventDomainId && !!domain?.id && eventDomainId === domain.id\n}\n\n/** A thread reaches the person it is assigned to. */\nexport function hearsThread(thread: { domainId?: string; assigneeId?: string }, domain: DomainLike, userId: string) {\n return inDomain(thread?.domainId, domain) && thread.assigneeId === userId\n}\n\n/**\n * An approval reaches its approver, and anyone holding the role it is addressed to. A role-addressed\n * step has no approver - only the role - so without the second half nobody hears it. The roles are\n * asked only after the domain matched, as for an instance.\n *\n * Hearing is not being offered: the client refetches its summary, and the summary leaves out what the\n * listener issued (own-request.ts). So the issuer may hear their own step and still not see it.\n */\nexport async function hearsApproval(\n approval: { domainId?: string; approverId?: string; approverRoleId?: string },\n domain: DomainLike,\n userId: string,\n rolesOf: () => Promise<{ id: string }[]>\n): Promise<boolean> {\n if (!inDomain(approval?.domainId, domain)) return false\n if (approval.approverId === userId) return true\n if (!approval.approverRoleId) return false\n\n return (await rolesOf()).some(role => role.id === approval.approverRoleId)\n}\n\n/**\n * An instance reaches whoever made it, and anyone holding any one of the roles it is waiting for - once,\n * however many of them they hold: this answers yes or no for one subscriber. The roles are\n * asked only after the domain matched — the lookup is not free and is never needed for another tenant.\n */\nexport async function hearsInstance(\n instance: { domainId?: string; creatorId?: string; assigneeRoleIds?: string[] },\n domain: DomainLike,\n userId: string,\n rolesOf: () => Promise<{ id: string }[]>\n): Promise<boolean> {\n if (!inDomain(instance?.domainId, domain)) return false\n if (instance.creatorId === userId) return true\n const poolIds = instance.assigneeRoleIds || []\n if (!poolIds.length) return false\n\n const held = (await rolesOf()).map(role => role.id)\n return poolIds.some(id => held.includes(id))\n}\n"]}
|
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.workDoorsFor = workDoorsFor;
|
|
4
4
|
exports.seatsStanding = seatsStanding;
|
|
5
5
|
exports.seatsOffered = seatsOffered;
|
|
6
|
+
const typeorm_1 = require("typeorm");
|
|
6
7
|
const auth_base_1 = require("@things-factory/auth-base");
|
|
7
8
|
const shell_1 = require("@things-factory/shell");
|
|
8
9
|
const activity_lookup_js_1 = require("./activity-lookup.js");
|
|
9
10
|
const approval_line_standing_js_1 = require("./approval-line-standing.js");
|
|
10
11
|
const work_door_declaration_js_1 = require("./work-door-declaration.js");
|
|
11
12
|
const work_door_1 = require("./work-door");
|
|
13
|
+
const assignee_pool_js_1 = require("../activity-instance/assignee-pool.js");
|
|
12
14
|
const work_door_not_used_1 = require("./work-door-not-used");
|
|
13
15
|
/**
|
|
14
16
|
* 이 공장의 문 열둘을 읽어 판정한다 — **한 벌.**
|
|
@@ -57,7 +59,7 @@ async function workDoorsFor(manager, domain) {
|
|
|
57
59
|
installed: activity
|
|
58
60
|
? {
|
|
59
61
|
approvalLine: lineOwner?.approvalLine,
|
|
60
|
-
|
|
62
|
+
assigneeRoles: await roleNamesOf(manager, (0, assignee_pool_js_1.poolIdsOf)(activity)),
|
|
61
63
|
/*
|
|
62
64
|
* **빌려 온 것일 때만** 채운다. `Activity` 는 `domainId` 만 들고 있어 이름은 따로
|
|
63
65
|
* 읽는데, 자기 도메인이면 읽지도 않고 비운다 — 자기 이름을 채우면 화면이 모든 줄에
|
|
@@ -84,12 +86,13 @@ async function workDoorsFor(manager, domain) {
|
|
|
84
86
|
*
|
|
85
87
|
* 문마다 부르지 않고 배정 문에서만 부른다. 배정 문이 하나도 없는 지금은 한 번도 안 돈다.
|
|
86
88
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
/** The pool's role names, in the order the activity lists them. */
|
|
90
|
+
async function roleNamesOf(manager, roleIds) {
|
|
91
|
+
if (!roleIds.length) {
|
|
92
|
+
return [];
|
|
90
93
|
}
|
|
91
|
-
const found = await manager.
|
|
92
|
-
return found?.name;
|
|
94
|
+
const found = await manager.find(auth_base_1.Role, { where: { id: (0, typeorm_1.In)(roleIds) } });
|
|
95
|
+
return roleIds.map(id => found.find(role => role.id === id)?.name).filter(Boolean);
|
|
93
96
|
}
|
|
94
97
|
/**
|
|
95
98
|
* 부모 도메인의 이름. 못 읽으면 지어내지 않고 비운다 — 「본사」라고 적으면 없는 이름이 화면에 남는다.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-door-service.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-service.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"work-door-service.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In } from 'typeorm'\n\nimport { Role } from '@things-factory/auth-base'\nimport { Domain } from '@things-factory/shell'\n\nimport { activityNamedIn, seatReader } from './activity-lookup.js'\nimport { judgeApprovalLine } from './approval-line-standing.js'\nimport { declaredDoors, offeredSeatNames } from './work-door-declaration.js'\nimport { WorkDoor, judgeDoor } from './work-door'\nimport { poolIdsOf } from '../activity-instance/assignee-pool.js'\nimport { WorkDoorNotUsed } from './work-door-not-used'\n\n/** A judged door and the approval line that applies to it, as stored, for the status screen to name. */\nexport type WorkDoorRead = WorkDoor & { line?: { seats: unknown[]; domainId: string } }\n\n/**\n * 이 공장의 문 열둘을 읽어 판정한다 — **한 벌.**\n *\n * ── 왜 resolver 밖에 두나 ──────────────────────────────────────────────────\n * 이 판정을 묻는 자리가 둘이 됐다. 화면(`workDoors`)과 성숙도 표다. 각자 읽으면 둘이 다른 수를\n * 말하게 되고, **어느 쪽이 맞는지 보는 사람이 알 길이 없다.** 성숙도 표는 이 제품이 스스로에 대해\n * 하는 말이라 화면과 어긋나면 그 표 전체를 못 믿는다.\n *\n * ── 부모까지 보는 규칙은 상신하는 쪽의 것을 그대로 쓴다 ────────────────────\n * 상신이 `activityNamedIn` 으로 양식을 찾으므로, 보는 쪽이 다른 규칙으로 찾으면 **「없다」고\n * 말하는데 실제로는 서는** 상태가 생긴다.\n */\nexport async function workDoorsFor(manager: EntityManager, domain: Domain): Promise<WorkDoorRead[]> {\n /* 공장이 「안 쓴다」고 말한 것을 한 번에 읽는다. 문마다 읽으면 열두 번 묻는다. */\n const declared = new Map(\n (await manager.find(WorkDoorNotUsed, { where: { domain: { id: domain.id } } })).map(one => [\n one.doorName,\n one.because\n ])\n )\n\n const doors: WorkDoorRead[] = []\n\n /*\n * **포트 하나를 문 열일곱이 함께 쓴다.** 포트가 읽은 역할을 기억하므로 같은 역할을 여러 번 묻지\n * 않는다 — 결재선 대부분이 공장장으로 끝난다. 이 판정 동안만 산다.\n */\n const reader = seatReader(manager)\n\n for (const door of declaredDoors()) {\n const name = door.name\n const activity = await activityNamedIn(manager, domain, name)\n /*\n * **결재선을 빌려 쓰는 문**은 따르는 문의 결재선으로 센다(§`LINE_FOLLOWS`). 자기 칸을 세면, 올리는\n * 쪽은 발행 승인의 결재선으로 서는데 현황은 「결재자 없음」이라고 말한다.\n */\n const follows = door.lineFollows\n const lineOwner = follows ? await activityNamedIn(manager, domain, follows) : activity\n\n const judged = judgeDoor({\n name,\n kind: door.kind,\n whenNoLine: door.whenNoLine,\n whenNoLineBecause: door.because,\n notUsedBecause: declared.get(name),\n follows,\n /* 상신하는 쪽과 **같은 함수**로 센다. 여기서 따로 세면 화면과 관문이 갈린다. */\n approvalStanding: activity\n ? await judgeApprovalLine(lineOwner?.approvalLine as Parameters<typeof judgeApprovalLine>[0], reader)\n : undefined,\n installed: activity\n ? {\n approvalLine: lineOwner?.approvalLine as unknown[] | undefined,\n assigneeRoles: await roleNamesOf(manager, poolIdsOf(activity)),\n /*\n * **빌려 온 것일 때만** 채운다. `Activity` 는 `domainId` 만 들고 있어 이름은 따로\n * 읽는데, 자기 도메인이면 읽지도 않고 비운다 — 자기 이름을 채우면 화면이 모든 줄에\n * 「우리 공장에서 정했습니다」를 달고, 「결재자 없음」 옆에서 그 말이 거짓이 된다.\n */\n borrowedFrom:\n activity.domainId === domain.id ? undefined : await subdomainOf(manager, activity.domainId)\n }\n : undefined\n })\n\n /*\n * The line that applies, as stored, with the domain it was set in - the status screen names its\n * seats. It is the followed door's line when this one follows, so the names match what raising\n * this door actually goes through. Only for a door that stands: a missing or unused door goes\n * through no line.\n */\n const line = lineOwner?.approvalLine as unknown[] | undefined\n const lineStands = door.kind === 'approval' && (judged.standing === 'on' || judged.standing === 'off')\n\n doors.push(lineStands && line?.length ? { ...judged, line: { seats: line, domainId: lineOwner!.domainId } } : judged)\n }\n\n return doors\n}\n\n/**\n * 그 역할의 이름. 없으면 없다 — 지어내지 않는다.\n *\n * 문마다 부르지 않고 배정 문에서만 부른다. 배정 문이 하나도 없는 지금은 한 번도 안 돈다.\n */\n/** The pool's role names, in the order the activity lists them. */\nasync function roleNamesOf(manager: EntityManager, roleIds: string[]): Promise<string[]> {\n if (!roleIds.length) {\n return []\n }\n\n const found = await manager.find(Role, { where: { id: In(roleIds) } })\n\n return roleIds.map(id => found.find(role => role.id === id)?.name).filter(Boolean)\n}\n\n/**\n * 부모 도메인의 이름. 못 읽으면 지어내지 않고 비운다 — 「본사」라고 적으면 없는 이름이 화면에 남는다.\n *\n * 원문 SQL 로 안 읽는다. 자리표시자 철자가 드라이버마다 달라(`?` · `$1`) sqlite 에서는 통과하고\n * postgres 에서만 죽는다 — 그 부류는 여기서 절대 못 잡는다.\n */\nasync function subdomainOf(manager: EntityManager, domainId: string): Promise<string | undefined> {\n const found = await manager.findOne(Domain, { where: { id: domainId } })\n\n return found?.subdomain\n}\n\n/**\n * 이 제품이 내놓는 역할 자리 중 이 공장에 선 것이 몇인가.\n *\n * 결재선은 **역할을 가리킨다**(`[{type:'Role', value:<역할 id>}]`). 그러니 역할이 없는 공장에서는\n * 결재선을 만들 수 없다 — 양식 화면까지 가서야 그걸 알게 된다. 문 현황과 함께 내서 화면이 순서를\n * 말하게 한다.\n *\n * 공장이 스스로 만든 다른 역할은 안 센다. 이 수가 말하는 것은 「이 제품이 내놓은 자리」이고,\n * 그 밖의 역할로 결재선을 짜는 것도 물론 된다.\n */\nexport async function seatsStanding(manager: EntityManager, domain: Domain): Promise<number> {\n const offered = offeredSeatNames()\n const here = await manager.find(Role, { where: { domain: { id: domain.id } } })\n const names = new Set(here.map(one => one.name))\n\n return offered.filter(one => names.has(one)).length\n}\n\n/** 이 설치본의 앱들이 내놓는 자리의 수 — 화면이 목록을 따로 들지 않게 여기서 센다. */\nexport function seatsOffered(): number {\n return offeredSeatNames().length\n}\n"]}
|
|
@@ -57,7 +57,7 @@ export declare class WorkDoorType {
|
|
|
57
57
|
whenNoLine?: WhenNoLineValue;
|
|
58
58
|
whenNoLineBecause?: WhenNoLineBecauseValue;
|
|
59
59
|
followsLabelKey?: string;
|
|
60
|
-
|
|
60
|
+
assigneeRoles?: string[];
|
|
61
61
|
notUsedBecause?: string;
|
|
62
62
|
approvalLine?: ApprovalLineItem[];
|
|
63
63
|
}
|
|
@@ -120,9 +120,9 @@ tslib_1.__decorate([
|
|
|
120
120
|
tslib_1.__metadata("design:type", String)
|
|
121
121
|
], WorkDoorType.prototype, "followsLabelKey", void 0);
|
|
122
122
|
tslib_1.__decorate([
|
|
123
|
-
(0, type_graphql_1.Field)({ nullable: true, description: 'the
|
|
124
|
-
tslib_1.__metadata("design:type",
|
|
125
|
-
], WorkDoorType.prototype, "
|
|
123
|
+
(0, type_graphql_1.Field)(type => [String], { nullable: true, description: 'the roles an assignment door is bound to - anyone holding any one of them' }),
|
|
124
|
+
tslib_1.__metadata("design:type", Array)
|
|
125
|
+
], WorkDoorType.prototype, "assigneeRoles", void 0);
|
|
126
126
|
tslib_1.__decorate([
|
|
127
127
|
(0, type_graphql_1.Field)({ nullable: true, description: 'what the plant wrote when it said it does not use this door — shown as written' }),
|
|
128
128
|
tslib_1.__metadata("design:type", String)
|
|
@@ -192,7 +192,7 @@ function toDoorType(door) {
|
|
|
192
192
|
whenNoLine: door.whenNoLine ? member(WhenNoLineValue, door.whenNoLine) : undefined,
|
|
193
193
|
whenNoLineBecause: door.whenNoLineBecause ? member(WhenNoLineBecauseValue, door.whenNoLineBecause) : undefined,
|
|
194
194
|
followsLabelKey: door.followsLabelKey,
|
|
195
|
-
|
|
195
|
+
assigneeRoles: door.assigneeRoles,
|
|
196
196
|
notUsedBecause: door.notUsedBecause
|
|
197
197
|
};
|
|
198
198
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-door-type.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-type.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, Int, ObjectType, registerEnumType } from 'type-graphql'\n\nimport { ApprovalLineItem } from '@things-factory/organization'\n\nimport type { WorkDoor } from './work-door.js'\n\n/**\n * 「이 공장에서 어느 결재 문이 서 있나」를 화면에 낼 모양.\n *\n * **사실만 낸다. 문장은 화면이 번역으로 짓는다.** 서버가 한국어 문장을 지어 보내던 때 영어·일본어·\n * 중국어·말레이어 사용자도 한국어를 읽었고, 화면이 상태별로 달리 그리려면 그 문장을 다시 뜯어 읽어야\n * 했다. 열거형 값 셋은 `registerEnumType` 이 **멤버 이름**을 싣는다 — 화면이 `door-words.ts` 한 곳에서\n * 옮긴다.\n */\n\nexport enum DoorStandingValue {\n /** 채워졌다. */\n On = 'on',\n /** 양식은 섰는데 받을 사람이 없다. 조치 대상. */\n Off = 'off',\n /** 양식이 아예 안 섰다 — 설정이 아니라 결함이다. 조치 대상. */\n Missing = 'missing',\n /** 이 공장이 안 쓰기로 했다. 조치 대상이 아니다. */\n NotUsed = 'not-used'\n}\n\n/** 결재냐 배정이냐 — 비었을 때 결과가 반대라 화면이 갈라 그려야 한다. */\nexport enum DoorKindValue {\n Approval = 'approval',\n Assignment = 'assignment'\n}\n\n/** 왜 그 상태인가 — 까닭마다 관리자가 할 일이 다르다(§`DoorReason`). */\nexport enum DoorReasonValue {\n NotUsed = 'not-used',\n Missing = 'missing',\n LineEmpty = 'line-empty',\n RoleEmpty = 'role-empty',\n PersonAbsent = 'person-absent',\n NoAssignee = 'no-assignee',\n LineFilled = 'line-filled',\n AssigneeBound = 'assignee-bound'\n}\n\n/** 결재선이 비면 이 문이 어떻게 하나 — 선언 그대로(ADR-0077 결정 1). */\nexport enum WhenNoLineValue {\n Run = 'run',\n Refuse = 'refuse',\n AsEstablished = 'as-established'\n}\n\n/** 결재를 요구하는 까닭 — `refuse` · `as-established` 문만. */\nexport enum WhenNoLineBecauseValue {\n Irreversible = 'irreversible',\n ApprovalIsTheAct = 'approval-is-the-act',\n LowersAControl = 'lowers-a-control'\n}\n\nregisterEnumType(DoorReasonValue, {\n name: 'DoorReason',\n description: 'Why a door stands where it does — each reason asks for a different fix'\n})\n\nregisterEnumType(WhenNoLineValue, {\n name: 'WhenNoLine',\n description: 'What an approval door does when its approval line is empty, as the application declared it'\n})\n\nregisterEnumType(WhenNoLineBecauseValue, {\n name: 'WhenNoLineBecause',\n description: 'Why an approval door will not run without an approval line'\n})\n\nregisterEnumType(DoorKindValue, {\n name: 'DoorKind',\n description: 'Whether the plant fills an approval line or an assignee role on this door'\n})\n\nregisterEnumType(DoorStandingValue, {\n name: 'DoorStanding',\n description: 'Whether this plant has an approval step standing for that form'\n})\n\n@ObjectType({ description: 'One approval form and whether this plant has approvers on it' })\nexport class WorkDoorType {\n @Field({ description: 'the form name — the key the callback is found by' })\n name: string\n\n @Field({ description: 'the translation key of its label — computed from the name, never declared' })\n labelKey: string\n\n @Field(type => DoorKindValue)\n kind: DoorKindValue\n\n @Field(type => DoorStandingValue)\n standing: DoorStandingValue\n\n @Field(type => DoorReasonValue)\n reason: DoorReasonValue\n\n @Field(type => Int, { description: 'how many approvers — zero for an assignment door or an empty line' })\n approvers: number\n\n @Field({ nullable: true, description: 'which domain the approval line came from — a plant may use its parent\\'s' })\n from?: string\n\n @Field(type => WhenNoLineValue, { nullable: true, description: 'approval doors only' })\n whenNoLine?: WhenNoLineValue\n\n @Field(type => WhenNoLineBecauseValue, { nullable: true, description: 'refuse and as-established doors only' })\n whenNoLineBecause?: WhenNoLineBecauseValue\n\n @Field({ nullable: true, description: 'the label key of the door whose approval line this one follows' })\n followsLabelKey?: string\n\n @Field({ nullable: true, description: 'the role an assignment door is bound to' })\n assigneeRole?: string\n\n @Field({ nullable: true, description: 'what the plant wrote when it said it does not use this door — shown as written' })\n notUsedBecause?: string\n\n @Field(type => [ApprovalLineItem], {\n nullable: true,\n description: 'the approval line raising this door goes through, seat by seat — the followed door\\'s line when it follows one'\n })\n approvalLine?: ApprovalLineItem[]\n}\n\n@ObjectType({ description: 'Every approval form this product installs, and where this plant stands on each' })\nexport class WorkDoorsType {\n @Field(type => [WorkDoorType])\n doors: WorkDoorType[]\n\n @Field(type => Int, { description: 'how many still need filling — not-used doors are never counted' })\n todo: number\n\n @Field(type => Int, { description: 'doors standing' })\n on: number\n\n @Field(type => Int, { description: 'forms installed with nobody on them' })\n off: number\n\n @Field(type => Int, { description: 'forms that never installed' })\n missing: number\n\n @Field(type => Int, { description: 'doors this plant said it does not use' })\n notUsed: number\n\n /**\n * 이 제품이 내놓는 역할 자리 중 **이 공장에 실제로 선 것.**\n *\n * ── 왜 문 목록에 자리 수가 붙나 ────────────────────────────────────────\n * 문이 비었다는 말만 하면 사람은 양식 화면으로 간다. 그런데 **고를 역할이 없으면 그 화면의\n * 결재선 칸이 빈 목록**이다. 헛걸음하고 돌아와서, 무엇이 잘못됐는지 모른 채 이 화면을 다시 본다.\n *\n * 두 수를 함께 내면 화면이 순서를 말할 수 있다 — 자리부터 세우고, 그다음에 결재선을 건다.\n */\n @Field(type => Int, { description: 'how many of the role seats this product offers actually exist here' })\n seatsStanding: number\n\n @Field(type => Int, { description: 'how many role seats this product offers' })\n seatsOffered: number\n}\n\n/**\n * 판단의 모양을 화면의 모양으로. 판단부의 낱말(`'line-empty'`)을 열거형 **값**으로 찾아 멤버를 싣는다 —\n * 값과 낱말이 같게 적혀 있으니 표를 따로 들지 않는다. 못 찾으면 던진다: 새 낱말이 전선에서 조용히\n * 사라지면 화면이 「모른다」조차 못 그린다.\n */\nexport function toDoorType(door: WorkDoor): WorkDoorType {\n return {\n name: door.name,\n labelKey: door.labelKey,\n kind: member(DoorKindValue, door.kind),\n standing: member(DoorStandingValue, door.standing),\n reason: member(DoorReasonValue, door.reason),\n approvers: door.approvers,\n from: door.from,\n whenNoLine: door.whenNoLine ? member(WhenNoLineValue, door.whenNoLine) : undefined,\n whenNoLineBecause: door.whenNoLineBecause ? member(WhenNoLineBecauseValue, door.whenNoLineBecause) : undefined,\n followsLabelKey: door.followsLabelKey,\n assigneeRole: door.assigneeRole,\n notUsedBecause: door.notUsedBecause\n }\n}\n\nfunction member<T extends Record<string, string>>(values: T, word: string): T[keyof T] {\n const found = Object.values(values).find(one => one === word)\n\n if (!found) {\n throw new Error(`work door: no wire value for \"${word}\"`)\n }\n\n return found as T[keyof T]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"work-door-type.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-type.ts"],"names":[],"mappings":";;;AAyKA,gCAeC;;AAxLD,+CAAuE;AAEvE,+DAA+D;AAI/D;;;;;;;GAOG;AAEH,IAAY,iBASX;AATD,WAAY,iBAAiB;IAC3B,YAAY;IACZ,8BAAS,CAAA;IACT,gCAAgC;IAChC,gCAAW,CAAA;IACX,yCAAyC;IACzC,wCAAmB,CAAA;IACnB,kCAAkC;IAClC,yCAAoB,CAAA;AACtB,CAAC,EATW,iBAAiB,iCAAjB,iBAAiB,QAS5B;AAED,8CAA8C;AAC9C,IAAY,aAGX;AAHD,WAAY,aAAa;IACvB,sCAAqB,CAAA;IACrB,0CAAyB,CAAA;AAC3B,CAAC,EAHW,aAAa,6BAAb,aAAa,QAGxB;AAED,oDAAoD;AACpD,IAAY,eASX;AATD,WAAY,eAAe;IACzB,uCAAoB,CAAA;IACpB,sCAAmB,CAAA;IACnB,2CAAwB,CAAA;IACxB,2CAAwB,CAAA;IACxB,iDAA8B,CAAA;IAC9B,6CAA0B,CAAA;IAC1B,6CAA0B,CAAA;IAC1B,mDAAgC,CAAA;AAClC,CAAC,EATW,eAAe,+BAAf,eAAe,QAS1B;AAED,mDAAmD;AACnD,IAAY,eAIX;AAJD,WAAY,eAAe;IACzB,8BAAW,CAAA;IACX,oCAAiB,CAAA;IACjB,mDAAgC,CAAA;AAClC,CAAC,EAJW,eAAe,+BAAf,eAAe,QAI1B;AAED,oDAAoD;AACpD,IAAY,sBAIX;AAJD,WAAY,sBAAsB;IAChC,uDAA6B,CAAA;IAC7B,kEAAwC,CAAA;IACxC,6DAAmC,CAAA;AACrC,CAAC,EAJW,sBAAsB,sCAAtB,sBAAsB,QAIjC;AAED,IAAA,+BAAgB,EAAC,eAAe,EAAE;IAChC,IAAI,EAAE,YAAY;IAClB,WAAW,EAAE,wEAAwE;CACtF,CAAC,CAAA;AAEF,IAAA,+BAAgB,EAAC,eAAe,EAAE;IAChC,IAAI,EAAE,YAAY;IAClB,WAAW,EAAE,4FAA4F;CAC1G,CAAC,CAAA;AAEF,IAAA,+BAAgB,EAAC,sBAAsB,EAAE;IACvC,IAAI,EAAE,mBAAmB;IACzB,WAAW,EAAE,4DAA4D;CAC1E,CAAC,CAAA;AAEF,IAAA,+BAAgB,EAAC,aAAa,EAAE;IAC9B,IAAI,EAAE,UAAU;IAChB,WAAW,EAAE,2EAA2E;CACzF,CAAC,CAAA;AAEF,IAAA,+BAAgB,EAAC,iBAAiB,EAAE;IAClC,IAAI,EAAE,cAAc;IACpB,WAAW,EAAE,gEAAgE;CAC9E,CAAC,CAAA;AAGK,IAAM,YAAY,GAAlB,MAAM,YAAY;CA0CxB,CAAA;AA1CY,oCAAY;AAEvB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,kDAAkD,EAAE,CAAC;;0CAC/D;AAGZ;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;8CACpF;AAGhB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC;;0CACV;AAGnB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAiB,CAAC;;8CACN;AAG3B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC;;4CACR;AAGvB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC;;+CACxF;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0EAA0E,EAAE,CAAC;;0CACtG;AAGb;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,eAAe,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qBAAqB,EAAE,CAAC;;gDAC3D;AAG5B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,sBAAsB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,sCAAsC,EAAE,CAAC;;uDACrE;AAG1C;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;qDACjF;AAGxB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;mDAC9G;AAGxB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gFAAgF,EAAE,CAAC;;oDAClG;AAMvB;IAJC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,+BAAgB,CAAC,EAAE;QACjC,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,gHAAgH;KAC9H,CAAC;;kDAC+B;uBAzCtB,YAAY;IADxB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;GAC/E,YAAY,CA0CxB;AAGM,IAAM,aAAa,GAAnB,MAAM,aAAa;CAiCzB,CAAA;AAjCY,sCAAa;AAExB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;;4CACT;AAGrB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;2CAC1F;AAGZ;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC;;yCAC5C;AAGV;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;;0CAChE;AAGX;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;8CACnD;AAGf;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;8CAC9D;AAYf;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,oEAAoE,EAAE,CAAC;;oDACrF;AAGrB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,yCAAyC,EAAE,CAAC;;mDAC3D;wBAhCT,aAAa;IADzB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,gFAAgF,EAAE,CAAC;GACjG,aAAa,CAiCzB;AAED;;;;GAIG;AACH,SAAgB,UAAU,CAAC,IAAc;IACvC,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC;QACtC,QAAQ,EAAE,MAAM,CAAC,iBAAiB,EAAE,IAAI,CAAC,QAAQ,CAAC;QAClD,MAAM,EAAE,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC;QAC5C,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,eAAe,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS;QAClF,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,sBAAsB,EAAE,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS;QAC9G,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAA;AACH,CAAC;AAED,SAAS,MAAM,CAAmC,MAAS,EAAE,IAAY;IACvE,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAAA;IAE7D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,GAAG,CAAC,CAAA;IAC3D,CAAC;IAED,OAAO,KAAmB,CAAA;AAC5B,CAAC","sourcesContent":["import { Field, Int, ObjectType, registerEnumType } from 'type-graphql'\n\nimport { ApprovalLineItem } from '@things-factory/organization'\n\nimport type { WorkDoor } from './work-door.js'\n\n/**\n * 「이 공장에서 어느 결재 문이 서 있나」를 화면에 낼 모양.\n *\n * **사실만 낸다. 문장은 화면이 번역으로 짓는다.** 서버가 한국어 문장을 지어 보내던 때 영어·일본어·\n * 중국어·말레이어 사용자도 한국어를 읽었고, 화면이 상태별로 달리 그리려면 그 문장을 다시 뜯어 읽어야\n * 했다. 열거형 값 셋은 `registerEnumType` 이 **멤버 이름**을 싣는다 — 화면이 `door-words.ts` 한 곳에서\n * 옮긴다.\n */\n\nexport enum DoorStandingValue {\n /** 채워졌다. */\n On = 'on',\n /** 양식은 섰는데 받을 사람이 없다. 조치 대상. */\n Off = 'off',\n /** 양식이 아예 안 섰다 — 설정이 아니라 결함이다. 조치 대상. */\n Missing = 'missing',\n /** 이 공장이 안 쓰기로 했다. 조치 대상이 아니다. */\n NotUsed = 'not-used'\n}\n\n/** 결재냐 배정이냐 — 비었을 때 결과가 반대라 화면이 갈라 그려야 한다. */\nexport enum DoorKindValue {\n Approval = 'approval',\n Assignment = 'assignment'\n}\n\n/** 왜 그 상태인가 — 까닭마다 관리자가 할 일이 다르다(§`DoorReason`). */\nexport enum DoorReasonValue {\n NotUsed = 'not-used',\n Missing = 'missing',\n LineEmpty = 'line-empty',\n RoleEmpty = 'role-empty',\n PersonAbsent = 'person-absent',\n NoAssignee = 'no-assignee',\n LineFilled = 'line-filled',\n AssigneeBound = 'assignee-bound'\n}\n\n/** 결재선이 비면 이 문이 어떻게 하나 — 선언 그대로(ADR-0077 결정 1). */\nexport enum WhenNoLineValue {\n Run = 'run',\n Refuse = 'refuse',\n AsEstablished = 'as-established'\n}\n\n/** 결재를 요구하는 까닭 — `refuse` · `as-established` 문만. */\nexport enum WhenNoLineBecauseValue {\n Irreversible = 'irreversible',\n ApprovalIsTheAct = 'approval-is-the-act',\n LowersAControl = 'lowers-a-control'\n}\n\nregisterEnumType(DoorReasonValue, {\n name: 'DoorReason',\n description: 'Why a door stands where it does — each reason asks for a different fix'\n})\n\nregisterEnumType(WhenNoLineValue, {\n name: 'WhenNoLine',\n description: 'What an approval door does when its approval line is empty, as the application declared it'\n})\n\nregisterEnumType(WhenNoLineBecauseValue, {\n name: 'WhenNoLineBecause',\n description: 'Why an approval door will not run without an approval line'\n})\n\nregisterEnumType(DoorKindValue, {\n name: 'DoorKind',\n description: 'Whether the plant fills an approval line or an assignee role on this door'\n})\n\nregisterEnumType(DoorStandingValue, {\n name: 'DoorStanding',\n description: 'Whether this plant has an approval step standing for that form'\n})\n\n@ObjectType({ description: 'One approval form and whether this plant has approvers on it' })\nexport class WorkDoorType {\n @Field({ description: 'the form name — the key the callback is found by' })\n name: string\n\n @Field({ description: 'the translation key of its label — computed from the name, never declared' })\n labelKey: string\n\n @Field(type => DoorKindValue)\n kind: DoorKindValue\n\n @Field(type => DoorStandingValue)\n standing: DoorStandingValue\n\n @Field(type => DoorReasonValue)\n reason: DoorReasonValue\n\n @Field(type => Int, { description: 'how many approvers — zero for an assignment door or an empty line' })\n approvers: number\n\n @Field({ nullable: true, description: 'which domain the approval line came from — a plant may use its parent\\'s' })\n from?: string\n\n @Field(type => WhenNoLineValue, { nullable: true, description: 'approval doors only' })\n whenNoLine?: WhenNoLineValue\n\n @Field(type => WhenNoLineBecauseValue, { nullable: true, description: 'refuse and as-established doors only' })\n whenNoLineBecause?: WhenNoLineBecauseValue\n\n @Field({ nullable: true, description: 'the label key of the door whose approval line this one follows' })\n followsLabelKey?: string\n\n @Field(type => [String], { nullable: true, description: 'the roles an assignment door is bound to - anyone holding any one of them' })\n assigneeRoles?: string[]\n\n @Field({ nullable: true, description: 'what the plant wrote when it said it does not use this door — shown as written' })\n notUsedBecause?: string\n\n @Field(type => [ApprovalLineItem], {\n nullable: true,\n description: 'the approval line raising this door goes through, seat by seat — the followed door\\'s line when it follows one'\n })\n approvalLine?: ApprovalLineItem[]\n}\n\n@ObjectType({ description: 'Every approval form this product installs, and where this plant stands on each' })\nexport class WorkDoorsType {\n @Field(type => [WorkDoorType])\n doors: WorkDoorType[]\n\n @Field(type => Int, { description: 'how many still need filling — not-used doors are never counted' })\n todo: number\n\n @Field(type => Int, { description: 'doors standing' })\n on: number\n\n @Field(type => Int, { description: 'forms installed with nobody on them' })\n off: number\n\n @Field(type => Int, { description: 'forms that never installed' })\n missing: number\n\n @Field(type => Int, { description: 'doors this plant said it does not use' })\n notUsed: number\n\n /**\n * 이 제품이 내놓는 역할 자리 중 **이 공장에 실제로 선 것.**\n *\n * ── 왜 문 목록에 자리 수가 붙나 ────────────────────────────────────────\n * 문이 비었다는 말만 하면 사람은 양식 화면으로 간다. 그런데 **고를 역할이 없으면 그 화면의\n * 결재선 칸이 빈 목록**이다. 헛걸음하고 돌아와서, 무엇이 잘못됐는지 모른 채 이 화면을 다시 본다.\n *\n * 두 수를 함께 내면 화면이 순서를 말할 수 있다 — 자리부터 세우고, 그다음에 결재선을 건다.\n */\n @Field(type => Int, { description: 'how many of the role seats this product offers actually exist here' })\n seatsStanding: number\n\n @Field(type => Int, { description: 'how many role seats this product offers' })\n seatsOffered: number\n}\n\n/**\n * 판단의 모양을 화면의 모양으로. 판단부의 낱말(`'line-empty'`)을 열거형 **값**으로 찾아 멤버를 싣는다 —\n * 값과 낱말이 같게 적혀 있으니 표를 따로 들지 않는다. 못 찾으면 던진다: 새 낱말이 전선에서 조용히\n * 사라지면 화면이 「모른다」조차 못 그린다.\n */\nexport function toDoorType(door: WorkDoor): WorkDoorType {\n return {\n name: door.name,\n labelKey: door.labelKey,\n kind: member(DoorKindValue, door.kind),\n standing: member(DoorStandingValue, door.standing),\n reason: member(DoorReasonValue, door.reason),\n approvers: door.approvers,\n from: door.from,\n whenNoLine: door.whenNoLine ? member(WhenNoLineValue, door.whenNoLine) : undefined,\n whenNoLineBecause: door.whenNoLineBecause ? member(WhenNoLineBecauseValue, door.whenNoLineBecause) : undefined,\n followsLabelKey: door.followsLabelKey,\n assigneeRoles: door.assigneeRoles,\n notUsedBecause: door.notUsedBecause\n }\n}\n\nfunction member<T extends Record<string, string>>(values: T, word: string): T[keyof T] {\n const found = Object.values(values).find(one => one === word)\n\n if (!found) {\n throw new Error(`work door: no wire value for \"${word}\"`)\n }\n\n return found as T[keyof T]\n}\n"]}
|
|
@@ -102,7 +102,7 @@ export interface WorkDoor {
|
|
|
102
102
|
/** 다른 문의 결재선을 따르면 그 문의 이름 번역 키. 결재선은 그 문에서 정한다. */
|
|
103
103
|
followsLabelKey?: string;
|
|
104
104
|
/** 배정 문에 묶인 역할 이름. */
|
|
105
|
-
|
|
105
|
+
assigneeRoles?: string[];
|
|
106
106
|
/** 공장이 「안 쓴다」고 적은 사유 — 사람이 쓴 글이라 번역하지 않고 그대로 보인다. */
|
|
107
107
|
notUsedBecause?: string;
|
|
108
108
|
/**
|
|
@@ -126,13 +126,13 @@ export interface DoorReading {
|
|
|
126
126
|
/**
|
|
127
127
|
* 그 도메인(또는 부모)에 선 양식. 못 찾았으면 없다.
|
|
128
128
|
*
|
|
129
|
-
* 채워졌나를 보는 칸이 종류마다 다르다 — 결재는 `approvalLine`, 배정은 `
|
|
129
|
+
* 채워졌나를 보는 칸이 종류마다 다르다 — 결재는 `approvalLine`, 배정은 `assigneeRoles`(역할 이름들)이다.
|
|
130
130
|
* `borrowedFrom` 은 **다른 도메인의 양식일 때만** 채운다. 자기 것이면 비운다 — 읽는 쪽이
|
|
131
131
|
* 「빌려 왔나」를 이 칸 하나로 판단한다.
|
|
132
132
|
*/
|
|
133
133
|
installed?: {
|
|
134
134
|
approvalLine?: unknown[];
|
|
135
|
-
|
|
135
|
+
assigneeRoles?: string[];
|
|
136
136
|
borrowedFrom?: string;
|
|
137
137
|
};
|
|
138
138
|
/** 공장이 「안 쓴다」고 말했으면 그 사유. 말한 적 없으면 없다. */
|
|
@@ -59,8 +59,8 @@ function judgeDoor(reading) {
|
|
|
59
59
|
}
|
|
60
60
|
const from = installed.borrowedFrom ? { from: installed.borrowedFrom } : {};
|
|
61
61
|
if (!approval) {
|
|
62
|
-
return installed.
|
|
63
|
-
? { ...known, ...from, standing: 'on', reason: 'assignee-bound', approvers: 1,
|
|
62
|
+
return installed.assigneeRoles?.length
|
|
63
|
+
? { ...known, ...from, standing: 'on', reason: 'assignee-bound', approvers: 1, assigneeRoles: installed.assigneeRoles }
|
|
64
64
|
: { ...known, ...from, standing: 'off', reason: 'no-assignee' };
|
|
65
65
|
}
|
|
66
66
|
/*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-door.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * 이 공장에서 **어느 결재 문이 서 있나** — 순수 판단, DB 를 모른다.\n *\n * 결재선이 찼나를 세는 규칙은 상신하는 쪽과 **같은 함수**를 쓴다(§`approval-line-standing`). 여기서\n * 따로 세면 화면이 「섰다」로 그리고 상신이 거절하는 날이 온다 — 그 자리가 실제로 있었다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 이것을 볼 자리가 필요한가 ───────────────────────────────────────────\n * 양식은 제품이 기동할 때 세우고 **결재선은 그 공장이 채운다.** 채운 것이 「우리는 이 단계를 쓴다」는\n * 선언이다. 그래서 결재선이 비어 있는 것은 결함이 아니다 — 그 공장의 선택이다.\n *\n * 문제는 **그 선택이 어디에도 안 보인다**는 것이다. 업무 카탈로그는 「제품이 이 문을 만들었나」를\n * 보여 주지만 「이 공장에서 이 문이 서 있나」는 말하지 않는다. 두 질문이 다른데 하나만 보인다.\n *\n * 실측(2026-09-11): 미라텍 도메인에 양식 11개가 다 설치돼 있고 **11개 전부 결재선이 비어 있다.**\n * 그 공장에서는 실적 정정도 폐기도 승인 없이 바로 반영되는데, 화면에는 문이 있는 것처럼 보인다.\n *\n * ── 비어 있으면 그냥 통과가 아니다 ─────────────────────────────────────────\n * 빈 결재선에서 무엇이 일어나는지는 **문마다 선언한다**(ADR-0077 결정 1, `WhenNoLine`). `run` 문은 상신이\n * 곧 완료가 되어 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 문은 올리는 것 자체가 거절된다.\n * 「승인을 안 받는다」 한 벌로 말하면 둘 다 틀린다 — 판단은 그 선언을 그대로 싣는다.\n *\n * ── 기동 로그는 사실의 집이 아니다 ─────────────────────────────────────────\n * `install-boot.ts` 가 기동할 때 한 줄 찍는다. 그 줄은 한 번 지나가고, 그 뒤로 어느 화면도 이것을\n * 말하지 않는다. 사람이 조치해야 하는 사실은 물어볼 수 있는 자리에 있어야 한다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * 문이 두 종류다 — **축이 다르고, 비었을 때 결과도 다르다.**\n *\n * ```\n * approval 누가 승인하나 결재선이 비면 문의 선언대로 — 통과해 버리거나(run) 올릴 수 없다(refuse)\n * assignment 누가 받나 역할이 안 묶이면 배정 업무가 아무에게도 안 간다 → 아무도 안 받는다\n * ```\n *\n * 같은 `off` 인데 결과가 반대다. 한 벌 문장으로 뭉개면 관리자가 무엇을 해야 하는지 알 수 없다.\n */\nimport type { ApprovalLineStanding } from './approval-line-standing.js'\nimport { activityTKey } from '../activity/decision-key-declaration.js'\nimport type { WhenNoLine, WhenNoLineBecause } from './work-door-declaration.js'\n\nexport type DoorKind = 'approval' | 'assignment'\n\n/** 문 하나가 어떤 상태인가. */\nexport type DoorStanding =\n /** 채워졌다 — 이 공장은 이 문을 쓴다. */\n | 'on'\n /** 양식은 섰는데 비었다. **조치 대상.** */\n | 'off'\n /** 양식이 아예 안 섰다 — 기동이 못 세웠다. 공장의 선택이 아니라 결함이다. **조치 대상.** */\n | 'missing'\n /**\n * **이 공장은 이 문을 안 쓴다** — 공장이 그렇게 말했다.\n *\n * 조치 대상이 아니다. 이것이 없으면 이 조사는 영원히 잔소리하는 화면이 되고, 그러면 사람들이\n * 안 본다 — 그때 진짜 빈 문 하나가 그 잔소리에 섞여 묻힌다(아키텍트 판정 2026-09-11).\n *\n * **자동으로 추론하지 않는다.** 「석 달 안 썼으니 안 쓰는 것」은 조용한 한 철에 참이 된다.\n */\n | 'not-used'\n\n/**\n * **왜 그 상태인가** — 상태 하나에 까닭이 여럿이고, 까닭마다 관리자가 할 일이 다르다.\n *\n * ```\n * not-used 공장이 안 쓴다고 말했다\n * missing 양식이 기동 때 안 섰다 — 기동 기록을 볼 일\n * line-empty 결재선이 비었다 — 결재선을 채울 일\n * role-empty 결재선이 지목한 역할에 사람이 없다 — 그 역할에 사람을 넣을 일\n * person-absent 결재선이 지목한 사람이 이 공장에 없다 — 다른 사람을 지목할 일\n * no-assignee 배정 문에 받을 역할이 안 묶였다\n * line-filled 결재자가 있다\n * assignee-bound 받을 역할이 묶였다\n * ```\n *\n * 문장이 아니라 값이다. 서버가 한국어 문장을 만들어 보내면 다른 언어 사용자가 한국어를 읽고, 화면이\n * 그 문장을 다시 뜯어 읽게 된다 — 문장은 화면이 이 값과 번역으로 짓는다(`door-sentence.ts`).\n */\nexport type DoorReason =\n | 'not-used'\n | 'missing'\n | 'line-empty'\n | 'role-empty'\n | 'person-absent'\n | 'no-assignee'\n | 'line-filled'\n | 'assignee-bound'\n\n/** 이 문 하나에 대해 아는 것 — 사실만. 문장은 화면이 짓는다. */\nexport interface WorkDoor {\n /** 양식 이름. 콜백을 찾는 키다. */\n name: string\n /**\n * 화면에 낼 이름의 번역 키 — **이름에서 센다**(`activityTKey`). 선언이 고르지 않는다.\n *\n * 선언한 키를 읽던 때 plant 가 `activity.${PascalCase}.label` 로 지어 보냈고 번역은 kebab 이라, 화면에\n * 키 글자가 그대로 떴다(:4000, 2026-09-24). 아무도 고르지 않는 키는 아무도 틀리게 못 고른다.\n */\n labelKey: string\n kind: DoorKind\n standing: DoorStanding\n reason: DoorReason\n /** 결재자 수. 안 섰거나 비었으면 0. */\n approvers: number\n /**\n * **결재선이 비면 이 문이 어떻게 하나** — 선언 그대로(ADR-0077 결정 1). 결재 문에만 있다.\n *\n * 빈 결재선의 결과가 문마다 다르다. `run` 은 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 는\n * 올리는 것 자체가 거절된다. 한 벌 문장으로 뭉개면 거절하는 문에 「자기 건을 승인한다」가 적힌다 —\n * 실제로 그랬다(:4000, 개정 결재 넷, 2026-09-24).\n */\n whenNoLine?: WhenNoLine\n /** `refuse` · `as-established` 문이 결재를 요구하는 까닭. */\n whenNoLineBecause?: WhenNoLineBecause\n /** 다른 문의 결재선을 따르면 그 문의 이름 번역 키. 결재선은 그 문에서 정한다. */\n followsLabelKey?: string\n /** 배정 문에 묶인 역할 이름. */\n assigneeRole?: string\n /** 공장이 「안 쓴다」고 적은 사유 — 사람이 쓴 글이라 번역하지 않고 그대로 보인다. */\n notUsedBecause?: string\n /**\n * **남의 도메인에서 빌려 온 양식일 때만** 그 도메인의 이름.\n *\n * 본사에서 정하고 공장이 쓰는 것이 정상이라, 자기 것이 없으면 부모 것을 쓴다\n * (`activityNamedIn`). 그 사실을 안 말하면 「우리가 정한 적 없는데 왜 서 있나」가 된다.\n *\n * ⚠ 자기 도메인이면 **없다.** 자기 이름을 채우면 화면이 모든 줄에 「miratek 에서 정했습니다」를\n * 달고, 그 말은 아무것도 알려 주지 않는다 — 실제로 그랬다(2026-09-12, 화면으로 보고 알았다).\n */\n from?: string\n}\n\n/** 한 문에 대해 읽은 것. 엔티티를 통째로 받지 않는다 — 그러면 이 판단이 DB 를 끌고 온다. */\nexport interface DoorReading {\n name: string\n kind: DoorKind\n /** 결재 문의 선언 — 빈 결재선에서 어떻게 하나와 그 까닭. */\n whenNoLine?: WhenNoLine\n whenNoLineBecause?: WhenNoLineBecause\n /**\n * 그 도메인(또는 부모)에 선 양식. 못 찾았으면 없다.\n *\n * 채워졌나를 보는 칸이 종류마다 다르다 — 결재는 `approvalLine`, 배정은 `assigneeRole` 이다.\n * `borrowedFrom` 은 **다른 도메인의 양식일 때만** 채운다. 자기 것이면 비운다 — 읽는 쪽이\n * 「빌려 왔나」를 이 칸 하나로 판단한다.\n */\n installed?: { approvalLine?: unknown[]; assigneeRole?: string; borrowedFrom?: string }\n /** 공장이 「안 쓴다」고 말했으면 그 사유. 말한 적 없으면 없다. */\n notUsedBecause?: string\n /**\n * **다른 문의 결재선을 따르면** 그 문의 양식 이름. 그러면 `approvalStanding` 도 그 문의 것이다 —\n * 이 문에서 결재선을 고치러 가면 안 되고 그 문으로 가야 한다는 것을 화면이 말한다.\n */\n follows?: string\n /**\n * 결재선이 찼나 — **상신하는 쪽과 같은 함수**(`judgeApprovalLine`)가 낸 결과를 그대로 받는다.\n *\n * 여기서 다시 세지 않는다. 역할에 사람이 있는지는 DB 를 읽어야 알고, 이 판단은 DB 를 모른다.\n * 없으면 「못 읽었다」로 읽는다 — 「안 읽었다」가 「다 찼다」로 새면 도로 줄 수 세기가 된다.\n */\n approvalStanding?: ApprovalLineStanding\n}\n\nexport function judgeDoor(reading: DoorReading): WorkDoor {\n const { name, kind, installed, notUsedBecause, follows } = reading\n const approval = kind === 'approval'\n const known: WorkDoor = {\n name,\n labelKey: activityTKey(name, 'label'),\n kind,\n standing: 'off',\n reason: 'line-empty',\n approvers: 0,\n ...(approval && reading.whenNoLine ? { whenNoLine: reading.whenNoLine } : {}),\n ...(approval && reading.whenNoLineBecause ? { whenNoLineBecause: reading.whenNoLineBecause } : {}),\n ...(follows ? { followsLabelKey: activityTKey(follows, 'label') } : {})\n }\n\n /*\n * **공장이 안 쓴다고 말한 것이 먼저다.** 뒤에 두면 양식이 안 선 문을 「결함」으로 먼저 그리고,\n * 안 쓰기로 한 공장이 고치라는 말을 계속 듣는다.\n */\n if (notUsedBecause) {\n return { ...known, standing: 'not-used', reason: 'not-used', notUsedBecause }\n }\n\n if (!installed) {\n return { ...known, standing: 'missing', reason: 'missing' }\n }\n\n const from = installed.borrowedFrom ? { from: installed.borrowedFrom } : {}\n\n if (!approval) {\n return installed.assigneeRole\n ? { ...known, ...from, standing: 'on', reason: 'assignee-bound', approvers: 1, assigneeRole: installed.assigneeRole }\n : { ...known, ...from, standing: 'off', reason: 'no-assignee' }\n }\n\n /*\n * 결재 문은 **줄 수가 아니라 그 줄에 사람이 있는지**로 센다. 역할을 지목해 놓고 그 역할이 빈\n * 결재선은 화면에 「섰다」로 그려지고 상신하는 순간 터진다(ADR-0047 ③).\n */\n const line = reading.approvalStanding\n const filled = line && !line.refusal ? line.approvers : 0\n\n if (filled) {\n return { ...known, ...from, standing: 'on', reason: 'line-filled', approvers: filled }\n }\n\n /*\n * **관리자가 할 일이 다르다.** 결재선을 정하는 것과 그 역할에 사람을 넣는 것은 다른 일이고,\n * 뒤엣것을 앞엣것으로 말하면 이미 정해 둔 결재선을 다시 들여다보게 만든다.\n */\n const reason: DoorReason =\n line?.refusal?.code === 'approval-role-empty'\n ? 'role-empty'\n : line?.refusal?.code === 'approval-person-absent'\n ? 'person-absent'\n : 'line-empty'\n\n return { ...known, ...from, standing: 'off', reason }\n}\n\n/**\n * 아직 채울 것이 몇 개인가 — **안 쓰기로 한 것은 안 센다.**\n *\n * 그래야 0 이 「다 됐다」를 뜻한다. 안 쓰는 것을 남은 수에 넣으면 다 채운 공장에서도 수가 안 줄고,\n * 그 화면은 잔소리가 된다(아키텍트 판정 2026-09-11).\n */\nexport function doorsToFill(doors: readonly WorkDoor[]): number {\n return doors.filter(one => one.standing === 'off' || one.standing === 'missing').length\n}\n\n/**\n * 상태별 수 — **문장은 화면이 짓는다.**\n *\n * 목록만 내면 읽는 사람이 열두 줄을 세어야 해서 한 문장으로 요약한다. 그 문장을 서버가 한국어로\n * 지어 보내면 다른 언어 사용자가 한국어를 읽는다 — 그래서 수만 보낸다.\n */\nexport function countDoors(doors: readonly WorkDoor[]): { on: number; off: number; missing: number; notUsed: number } {\n return {\n on: doors.filter(one => one.standing === 'on').length,\n off: doors.filter(one => one.standing === 'off').length,\n missing: doors.filter(one => one.standing === 'missing').length,\n notUsed: doors.filter(one => one.standing === 'not-used').length\n }\n}\n\n/** The not-used declaration's content columns: which door and why. */\nexport const WORK_DOOR_NOT_USED_CONTENT = ['doorName', 'because'] as const\n\n/** The declaration as it was — its content, who declared it, and when — written when it is withdrawn. */\nexport function workDoorNotUsedBefore(declaration: {\n doorName: string\n because: string\n declaredBy?: { email?: string; name?: string } | null\n createdAt?: Date | null\n}) {\n return {\n ...Object.fromEntries(WORK_DOOR_NOT_USED_CONTENT.map(column => [column, declaration[column] ?? null])),\n declaredBy: declaration.declaredBy?.email ?? declaration.declaredBy?.name ?? null,\n declaredAt: declaration.createdAt ?? null\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"work-door.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * 이 공장에서 **어느 결재 문이 서 있나** — 순수 판단, DB 를 모른다.\n *\n * 결재선이 찼나를 세는 규칙은 상신하는 쪽과 **같은 함수**를 쓴다(§`approval-line-standing`). 여기서\n * 따로 세면 화면이 「섰다」로 그리고 상신이 거절하는 날이 온다 — 그 자리가 실제로 있었다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 이것을 볼 자리가 필요한가 ───────────────────────────────────────────\n * 양식은 제품이 기동할 때 세우고 **결재선은 그 공장이 채운다.** 채운 것이 「우리는 이 단계를 쓴다」는\n * 선언이다. 그래서 결재선이 비어 있는 것은 결함이 아니다 — 그 공장의 선택이다.\n *\n * 문제는 **그 선택이 어디에도 안 보인다**는 것이다. 업무 카탈로그는 「제품이 이 문을 만들었나」를\n * 보여 주지만 「이 공장에서 이 문이 서 있나」는 말하지 않는다. 두 질문이 다른데 하나만 보인다.\n *\n * 실측(2026-09-11): 미라텍 도메인에 양식 11개가 다 설치돼 있고 **11개 전부 결재선이 비어 있다.**\n * 그 공장에서는 실적 정정도 폐기도 승인 없이 바로 반영되는데, 화면에는 문이 있는 것처럼 보인다.\n *\n * ── 비어 있으면 그냥 통과가 아니다 ─────────────────────────────────────────\n * 빈 결재선에서 무엇이 일어나는지는 **문마다 선언한다**(ADR-0077 결정 1, `WhenNoLine`). `run` 문은 상신이\n * 곧 완료가 되어 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 문은 올리는 것 자체가 거절된다.\n * 「승인을 안 받는다」 한 벌로 말하면 둘 다 틀린다 — 판단은 그 선언을 그대로 싣는다.\n *\n * ── 기동 로그는 사실의 집이 아니다 ─────────────────────────────────────────\n * `install-boot.ts` 가 기동할 때 한 줄 찍는다. 그 줄은 한 번 지나가고, 그 뒤로 어느 화면도 이것을\n * 말하지 않는다. 사람이 조치해야 하는 사실은 물어볼 수 있는 자리에 있어야 한다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * 문이 두 종류다 — **축이 다르고, 비었을 때 결과도 다르다.**\n *\n * ```\n * approval 누가 승인하나 결재선이 비면 문의 선언대로 — 통과해 버리거나(run) 올릴 수 없다(refuse)\n * assignment 누가 받나 역할이 안 묶이면 배정 업무가 아무에게도 안 간다 → 아무도 안 받는다\n * ```\n *\n * 같은 `off` 인데 결과가 반대다. 한 벌 문장으로 뭉개면 관리자가 무엇을 해야 하는지 알 수 없다.\n */\nimport type { ApprovalLineStanding } from './approval-line-standing.js'\nimport { activityTKey } from '../activity/decision-key-declaration.js'\nimport type { WhenNoLine, WhenNoLineBecause } from './work-door-declaration.js'\n\nexport type DoorKind = 'approval' | 'assignment'\n\n/** 문 하나가 어떤 상태인가. */\nexport type DoorStanding =\n /** 채워졌다 — 이 공장은 이 문을 쓴다. */\n | 'on'\n /** 양식은 섰는데 비었다. **조치 대상.** */\n | 'off'\n /** 양식이 아예 안 섰다 — 기동이 못 세웠다. 공장의 선택이 아니라 결함이다. **조치 대상.** */\n | 'missing'\n /**\n * **이 공장은 이 문을 안 쓴다** — 공장이 그렇게 말했다.\n *\n * 조치 대상이 아니다. 이것이 없으면 이 조사는 영원히 잔소리하는 화면이 되고, 그러면 사람들이\n * 안 본다 — 그때 진짜 빈 문 하나가 그 잔소리에 섞여 묻힌다(아키텍트 판정 2026-09-11).\n *\n * **자동으로 추론하지 않는다.** 「석 달 안 썼으니 안 쓰는 것」은 조용한 한 철에 참이 된다.\n */\n | 'not-used'\n\n/**\n * **왜 그 상태인가** — 상태 하나에 까닭이 여럿이고, 까닭마다 관리자가 할 일이 다르다.\n *\n * ```\n * not-used 공장이 안 쓴다고 말했다\n * missing 양식이 기동 때 안 섰다 — 기동 기록을 볼 일\n * line-empty 결재선이 비었다 — 결재선을 채울 일\n * role-empty 결재선이 지목한 역할에 사람이 없다 — 그 역할에 사람을 넣을 일\n * person-absent 결재선이 지목한 사람이 이 공장에 없다 — 다른 사람을 지목할 일\n * no-assignee 배정 문에 받을 역할이 안 묶였다\n * line-filled 결재자가 있다\n * assignee-bound 받을 역할이 묶였다\n * ```\n *\n * 문장이 아니라 값이다. 서버가 한국어 문장을 만들어 보내면 다른 언어 사용자가 한국어를 읽고, 화면이\n * 그 문장을 다시 뜯어 읽게 된다 — 문장은 화면이 이 값과 번역으로 짓는다(`door-sentence.ts`).\n */\nexport type DoorReason =\n | 'not-used'\n | 'missing'\n | 'line-empty'\n | 'role-empty'\n | 'person-absent'\n | 'no-assignee'\n | 'line-filled'\n | 'assignee-bound'\n\n/** 이 문 하나에 대해 아는 것 — 사실만. 문장은 화면이 짓는다. */\nexport interface WorkDoor {\n /** 양식 이름. 콜백을 찾는 키다. */\n name: string\n /**\n * 화면에 낼 이름의 번역 키 — **이름에서 센다**(`activityTKey`). 선언이 고르지 않는다.\n *\n * 선언한 키를 읽던 때 plant 가 `activity.${PascalCase}.label` 로 지어 보냈고 번역은 kebab 이라, 화면에\n * 키 글자가 그대로 떴다(:4000, 2026-09-24). 아무도 고르지 않는 키는 아무도 틀리게 못 고른다.\n */\n labelKey: string\n kind: DoorKind\n standing: DoorStanding\n reason: DoorReason\n /** 결재자 수. 안 섰거나 비었으면 0. */\n approvers: number\n /**\n * **결재선이 비면 이 문이 어떻게 하나** — 선언 그대로(ADR-0077 결정 1). 결재 문에만 있다.\n *\n * 빈 결재선의 결과가 문마다 다르다. `run` 은 올린 사람이 자기 건을 승인한 것이 되고, `refuse` 는\n * 올리는 것 자체가 거절된다. 한 벌 문장으로 뭉개면 거절하는 문에 「자기 건을 승인한다」가 적힌다 —\n * 실제로 그랬다(:4000, 개정 결재 넷, 2026-09-24).\n */\n whenNoLine?: WhenNoLine\n /** `refuse` · `as-established` 문이 결재를 요구하는 까닭. */\n whenNoLineBecause?: WhenNoLineBecause\n /** 다른 문의 결재선을 따르면 그 문의 이름 번역 키. 결재선은 그 문에서 정한다. */\n followsLabelKey?: string\n /** 배정 문에 묶인 역할 이름. */\n assigneeRoles?: string[]\n /** 공장이 「안 쓴다」고 적은 사유 — 사람이 쓴 글이라 번역하지 않고 그대로 보인다. */\n notUsedBecause?: string\n /**\n * **남의 도메인에서 빌려 온 양식일 때만** 그 도메인의 이름.\n *\n * 본사에서 정하고 공장이 쓰는 것이 정상이라, 자기 것이 없으면 부모 것을 쓴다\n * (`activityNamedIn`). 그 사실을 안 말하면 「우리가 정한 적 없는데 왜 서 있나」가 된다.\n *\n * ⚠ 자기 도메인이면 **없다.** 자기 이름을 채우면 화면이 모든 줄에 「miratek 에서 정했습니다」를\n * 달고, 그 말은 아무것도 알려 주지 않는다 — 실제로 그랬다(2026-09-12, 화면으로 보고 알았다).\n */\n from?: string\n}\n\n/** 한 문에 대해 읽은 것. 엔티티를 통째로 받지 않는다 — 그러면 이 판단이 DB 를 끌고 온다. */\nexport interface DoorReading {\n name: string\n kind: DoorKind\n /** 결재 문의 선언 — 빈 결재선에서 어떻게 하나와 그 까닭. */\n whenNoLine?: WhenNoLine\n whenNoLineBecause?: WhenNoLineBecause\n /**\n * 그 도메인(또는 부모)에 선 양식. 못 찾았으면 없다.\n *\n * 채워졌나를 보는 칸이 종류마다 다르다 — 결재는 `approvalLine`, 배정은 `assigneeRoles`(역할 이름들)이다.\n * `borrowedFrom` 은 **다른 도메인의 양식일 때만** 채운다. 자기 것이면 비운다 — 읽는 쪽이\n * 「빌려 왔나」를 이 칸 하나로 판단한다.\n */\n installed?: { approvalLine?: unknown[]; assigneeRoles?: string[]; borrowedFrom?: string }\n /** 공장이 「안 쓴다」고 말했으면 그 사유. 말한 적 없으면 없다. */\n notUsedBecause?: string\n /**\n * **다른 문의 결재선을 따르면** 그 문의 양식 이름. 그러면 `approvalStanding` 도 그 문의 것이다 —\n * 이 문에서 결재선을 고치러 가면 안 되고 그 문으로 가야 한다는 것을 화면이 말한다.\n */\n follows?: string\n /**\n * 결재선이 찼나 — **상신하는 쪽과 같은 함수**(`judgeApprovalLine`)가 낸 결과를 그대로 받는다.\n *\n * 여기서 다시 세지 않는다. 역할에 사람이 있는지는 DB 를 읽어야 알고, 이 판단은 DB 를 모른다.\n * 없으면 「못 읽었다」로 읽는다 — 「안 읽었다」가 「다 찼다」로 새면 도로 줄 수 세기가 된다.\n */\n approvalStanding?: ApprovalLineStanding\n}\n\nexport function judgeDoor(reading: DoorReading): WorkDoor {\n const { name, kind, installed, notUsedBecause, follows } = reading\n const approval = kind === 'approval'\n const known: WorkDoor = {\n name,\n labelKey: activityTKey(name, 'label'),\n kind,\n standing: 'off',\n reason: 'line-empty',\n approvers: 0,\n ...(approval && reading.whenNoLine ? { whenNoLine: reading.whenNoLine } : {}),\n ...(approval && reading.whenNoLineBecause ? { whenNoLineBecause: reading.whenNoLineBecause } : {}),\n ...(follows ? { followsLabelKey: activityTKey(follows, 'label') } : {})\n }\n\n /*\n * **공장이 안 쓴다고 말한 것이 먼저다.** 뒤에 두면 양식이 안 선 문을 「결함」으로 먼저 그리고,\n * 안 쓰기로 한 공장이 고치라는 말을 계속 듣는다.\n */\n if (notUsedBecause) {\n return { ...known, standing: 'not-used', reason: 'not-used', notUsedBecause }\n }\n\n if (!installed) {\n return { ...known, standing: 'missing', reason: 'missing' }\n }\n\n const from = installed.borrowedFrom ? { from: installed.borrowedFrom } : {}\n\n if (!approval) {\n return installed.assigneeRoles?.length\n ? { ...known, ...from, standing: 'on', reason: 'assignee-bound', approvers: 1, assigneeRoles: installed.assigneeRoles }\n : { ...known, ...from, standing: 'off', reason: 'no-assignee' }\n }\n\n /*\n * 결재 문은 **줄 수가 아니라 그 줄에 사람이 있는지**로 센다. 역할을 지목해 놓고 그 역할이 빈\n * 결재선은 화면에 「섰다」로 그려지고 상신하는 순간 터진다(ADR-0047 ③).\n */\n const line = reading.approvalStanding\n const filled = line && !line.refusal ? line.approvers : 0\n\n if (filled) {\n return { ...known, ...from, standing: 'on', reason: 'line-filled', approvers: filled }\n }\n\n /*\n * **관리자가 할 일이 다르다.** 결재선을 정하는 것과 그 역할에 사람을 넣는 것은 다른 일이고,\n * 뒤엣것을 앞엣것으로 말하면 이미 정해 둔 결재선을 다시 들여다보게 만든다.\n */\n const reason: DoorReason =\n line?.refusal?.code === 'approval-role-empty'\n ? 'role-empty'\n : line?.refusal?.code === 'approval-person-absent'\n ? 'person-absent'\n : 'line-empty'\n\n return { ...known, ...from, standing: 'off', reason }\n}\n\n/**\n * 아직 채울 것이 몇 개인가 — **안 쓰기로 한 것은 안 센다.**\n *\n * 그래야 0 이 「다 됐다」를 뜻한다. 안 쓰는 것을 남은 수에 넣으면 다 채운 공장에서도 수가 안 줄고,\n * 그 화면은 잔소리가 된다(아키텍트 판정 2026-09-11).\n */\nexport function doorsToFill(doors: readonly WorkDoor[]): number {\n return doors.filter(one => one.standing === 'off' || one.standing === 'missing').length\n}\n\n/**\n * 상태별 수 — **문장은 화면이 짓는다.**\n *\n * 목록만 내면 읽는 사람이 열두 줄을 세어야 해서 한 문장으로 요약한다. 그 문장을 서버가 한국어로\n * 지어 보내면 다른 언어 사용자가 한국어를 읽는다 — 그래서 수만 보낸다.\n */\nexport function countDoors(doors: readonly WorkDoor[]): { on: number; off: number; missing: number; notUsed: number } {\n return {\n on: doors.filter(one => one.standing === 'on').length,\n off: doors.filter(one => one.standing === 'off').length,\n missing: doors.filter(one => one.standing === 'missing').length,\n notUsed: doors.filter(one => one.standing === 'not-used').length\n }\n}\n\n/** The not-used declaration's content columns: which door and why. */\nexport const WORK_DOOR_NOT_USED_CONTENT = ['doorName', 'because'] as const\n\n/** The declaration as it was — its content, who declared it, and when — written when it is withdrawn. */\nexport function workDoorNotUsedBefore(declaration: {\n doorName: string\n because: string\n declaredBy?: { email?: string; name?: string } | null\n createdAt?: Date | null\n}) {\n return {\n ...Object.fromEntries(WORK_DOOR_NOT_USED_CONTENT.map(column => [column, declaration[column] ?? null])),\n declaredBy: declaration.declaredBy?.email ?? declaration.declaredBy?.name ?? null,\n declaredAt: declaration.createdAt ?? null\n }\n}\n"]}
|