@things-factory/worklist 10.1.249 → 10.1.250
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/approval-progress.d.ts +11 -2
- package/dist-client/approval-progress.js +19 -8
- package/dist-client/approval-progress.js.map +1 -1
- package/dist-client/pages/activity-approval/activity-approval-page.js +1 -0
- package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-view.js +1 -0
- package/dist-client/pages/activity-instance/activity-instance-view.js.map +1 -1
- package/dist-client/pages/activity-thread/activity-thread-page.js +1 -0
- package/dist-client/pages/activity-thread/activity-thread-page.js.map +1 -1
- package/dist-client/pages/activity-thread/activity-thread-view-page.js +1 -0
- package/dist-client/pages/activity-thread/activity-thread-view-page.js.map +1 -1
- package/dist-client/pages/approval-progress/approval-progress-page.d.ts +46 -0
- package/dist-client/pages/approval-progress/approval-progress-page.js +294 -0
- package/dist-client/pages/approval-progress/approval-progress-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/activity-approval/approval-line.d.ts +2 -2
- package/dist-server/controllers/activity-approval/approval-line.js +5 -6
- package/dist-server/controllers/activity-approval/approval-line.js.map +1 -1
- package/dist-server/service/approval-progress/approval-progress-query.d.ts +14 -0
- package/dist-server/service/approval-progress/approval-progress-query.js +142 -0
- package/dist-server/service/approval-progress/approval-progress-query.js.map +1 -0
- package/dist-server/service/approval-progress/approval-progress-rule.d.ts +67 -0
- package/dist-server/service/approval-progress/approval-progress-rule.js +102 -0
- package/dist-server/service/approval-progress/approval-progress-rule.js.map +1 -0
- package/dist-server/service/approval-progress/approval-progress-type.d.ts +44 -0
- package/dist-server/service/approval-progress/approval-progress-type.js +172 -0
- package/dist-server/service/approval-progress/approval-progress-type.js.map +1 -0
- package/dist-server/service/approval-progress/index.d.ts +2 -0
- package/dist-server/service/approval-progress/index.js +6 -0
- package/dist-server/service/approval-progress/index.js.map +1 -0
- package/dist-server/service/approval-progress/waiting-on.d.ts +15 -0
- package/dist-server/service/approval-progress/waiting-on.js +40 -0
- package/dist-server/service/approval-progress/waiting-on.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +7 -2
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/shared/approval-step.d.ts +21 -0
- package/shared/approval-step.js +55 -0
- package/tests/approval-progress-db.test.ts +143 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +20 -1
- package/translations/es.json +20 -1
- package/translations/ja.json +20 -1
- package/translations/ko.json +20 -1
- package/translations/ms.json +20 -1
- package/translations/zh.json +20 -1
|
@@ -92,7 +92,7 @@ export declare function isApproved(judgment: string | undefined): boolean;
|
|
|
92
92
|
* 목록 질의가 이 배열을 그대로 쓴다. 두 벌로 두면 값이 늘 때 한쪽만 늘고, 그 판정을 받은 행이 대기
|
|
93
93
|
* 목록에 영원히 남는다 — 실제로 그렇게 됐다(아래).
|
|
94
94
|
*/
|
|
95
|
-
export
|
|
95
|
+
export { TERMINATED_JUDGMENTS } from '../../../shared/approval-step.js';
|
|
96
96
|
/**
|
|
97
97
|
* 이 판정이 끝난 것인가 — 더 기다릴 것이 없나.
|
|
98
98
|
*
|
|
@@ -103,7 +103,7 @@ export declare const TERMINATED_JUDGMENTS: readonly ["approved", "escalated", "d
|
|
|
103
103
|
* 그래서 중단된 건의 결재가 그 사람의 **승인 대기 목록에 영원히 남았다.** 눌러 봐야 이미 끝난 건이고,
|
|
104
104
|
* 목록에서 사라지지도 않는다. 닫는 쪽은 제대로 닫았는데 읽는 쪽이 그것을 못 읽었다.
|
|
105
105
|
*/
|
|
106
|
-
export
|
|
106
|
+
export { isTerminated } from '../../../shared/approval-step.js';
|
|
107
107
|
/**
|
|
108
108
|
* Is the seat at this turn declared as a **reference** — someone who comments but does not decide?
|
|
109
109
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TERMINATED_JUDGMENTS = exports.FIRST_ORDER = void 0;
|
|
3
|
+
exports.isTerminated = exports.TERMINATED_JUDGMENTS = exports.FIRST_ORDER = void 0;
|
|
4
4
|
exports.isApprovalStep = isApprovalStep;
|
|
5
5
|
exports.isReferenceStep = isReferenceStep;
|
|
6
6
|
exports.lineItemFor = lineItemFor;
|
|
@@ -11,7 +11,6 @@ exports.nextApproval = nextApproval;
|
|
|
11
11
|
exports.isLastApproval = isLastApproval;
|
|
12
12
|
exports.needsApproval = needsApproval;
|
|
13
13
|
exports.isApproved = isApproved;
|
|
14
|
-
exports.isTerminated = isTerminated;
|
|
15
14
|
exports.isReferenceOrder = isReferenceOrder;
|
|
16
15
|
/**
|
|
17
16
|
* 결재선에서 다음 결재자를 찾는다.
|
|
@@ -135,7 +134,8 @@ function isApproved(judgment) {
|
|
|
135
134
|
* 목록 질의가 이 배열을 그대로 쓴다. 두 벌로 두면 값이 늘 때 한쪽만 늘고, 그 판정을 받은 행이 대기
|
|
136
135
|
* 목록에 영원히 남는다 — 실제로 그렇게 됐다(아래).
|
|
137
136
|
*/
|
|
138
|
-
|
|
137
|
+
var approval_step_js_1 = require("../../../shared/approval-step.js");
|
|
138
|
+
Object.defineProperty(exports, "TERMINATED_JUDGMENTS", { enumerable: true, get: function () { return approval_step_js_1.TERMINATED_JUDGMENTS; } });
|
|
139
139
|
/**
|
|
140
140
|
* 이 판정이 끝난 것인가 — 더 기다릴 것이 없나.
|
|
141
141
|
*
|
|
@@ -146,9 +146,8 @@ exports.TERMINATED_JUDGMENTS = ['approved', 'escalated', 'delegated', 'rejected'
|
|
|
146
146
|
* 그래서 중단된 건의 결재가 그 사람의 **승인 대기 목록에 영원히 남았다.** 눌러 봐야 이미 끝난 건이고,
|
|
147
147
|
* 목록에서 사라지지도 않는다. 닫는 쪽은 제대로 닫았는데 읽는 쪽이 그것을 못 읽었다.
|
|
148
148
|
*/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
149
|
+
var approval_step_js_2 = require("../../../shared/approval-step.js");
|
|
150
|
+
Object.defineProperty(exports, "isTerminated", { enumerable: true, get: function () { return approval_step_js_2.isTerminated; } });
|
|
152
151
|
/**
|
|
153
152
|
* Is the seat at this turn declared as a **reference** — someone who comments but does not decide?
|
|
154
153
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-line.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approval-line.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **타입만 가져온다.** 값으로 가져오면 `@things-factory/organization` 이 실려 오고, 그것이 auth-base 의\n * 라우터까지 끌고 온다. 순수 규칙 하나를 시험하려는데 서버가 통째로 뜬다.\n */\nimport type { ApprovalLineItem } from '@things-factory/organization'\n\n/**\n * 결재선에서 다음 결재자를 찾는다.\n *\n * ── 왜 따로 뺐나 ───────────────────────────────────────────────────────────\n * 같은 규칙이 두 파일에 나뉘어 있었다.\n *\n * submit.ts 첫 결재를 `order: 1` 로 만들고 `approvalLine[0]` 을 넣는다\n * approve.ts `order` 를 승인하면 `approvalLine[order]` 를 다음 결재자로 삼는다\n *\n * `order` 와 배열 자리가 1만큼 다르다. 지금은 맞게 짝이 맞는데, 한쪽만 고치면 결재자가 한 명 빠지거나\n * 같은 사람이 두 번 들어간다. 그리고 **오류가 안 난다** — 결재가 다 끝나고 나서야 안다.\n *\n * 그래서 한 자리에 적고 양쪽이 이것을 부른다.\n *\n * `order` 는 사람이 세는 수다 — 1차 결재, 2차 결재. 배열은 0부터이므로 그 차이를 여기서만 다룬다.\n */\n\n/**\n * 이 항목이 **결재**인가 — 비어 있으면 결재다.\n *\n * 지금 도는 결재선에는 이 칸이 없다(gangsters·dataset·quotation·accounting). 그 항목들이 전부 결재자로\n * 읽혀야 거동이 안 바뀐다. 그래서 「참조라고 적힌 것만 참조」다.\n */\nexport function isApprovalStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation !== 'reference'\n}\n\n/** 이 항목이 참조인가 — 의견만 내고 결재를 막지 않는다. */\nexport function isReferenceStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation === 'reference'\n}\n\n/**\n * 이 차례의 결재자는 결재선의 어느 줄인가. `order` 는 1부터.\n *\n * **참조 항목도 자리를 차지한다.** `order` 는 배열 자리 그대로다 — 다시 매기면 이미 쌓인 행의 뜻이\n * 바뀐다. 그래서 결재자의 차례가 1·3·4 처럼 띄엄띄엄해질 수 있고, 그것이 맞다.\n *\n * 이 함수는 그 자리에 무엇이 있든 돌려준다. 결재인지 아닌지는 부르는 쪽이 본다(§`nextApproval`).\n */\nexport function lineItemFor(approvalLine: ApprovalLineItem[] | undefined, order: number): ApprovalLineItem | undefined {\n if (!approvalLine?.length || order < 1) {\n return undefined\n }\n\n return approvalLine[order - 1]\n}\n\n/**\n * 첫 **결재자**는 몇 번째 자리인가 — 앞에 참조가 있으면 건너뛴다.\n *\n * 결재선이 참조로만 이루어져 있으면 `undefined` 다. 그때는 결재 없이 끝난다(§`needsApproval`).\n */\nexport function firstApprovalOrder(approvalLine: ApprovalLineItem[] | undefined): number | undefined {\n if (!approvalLine?.length) return undefined\n\n const index = approvalLine.findIndex(isApprovalStep)\n return index < 0 ? undefined : index + 1\n}\n\n/**\n * 이 결재선의 참조들 — **언제 알릴지와 함께.**\n *\n * `notifyAfter` 가 없으면 `0` 으로 답한다. 0 은 「상신 즉시」다 — 아직 아무 결재도 끝나지 않은 시점.\n */\nexport function referenceSteps(\n approvalLine: ApprovalLineItem[] | undefined\n): { item: ApprovalLineItem; order: number; notifyAfter: number }[] {\n if (!approvalLine?.length) return []\n\n return approvalLine\n .map((item, index) => ({ item, order: index + 1, notifyAfter: Number((item as any).notifyAfter ?? 0) || 0 }))\n .filter(x => isReferenceStep(x.item))\n}\n\n/** 이번에 알려야 할 참조들 — `afterOrder` 번째 결재가 막 끝났을 때. 상신 직후는 `0`. */\nexport function referencesToNotify(\n approvalLine: ApprovalLineItem[] | undefined,\n afterOrder: number\n): { item: ApprovalLineItem; order: number }[] {\n return referenceSteps(approvalLine)\n .filter(x => x.notifyAfter === afterOrder)\n .map(({ item, order }) => ({ item, order }))\n}\n\n/** 첫 결재의 차례. */\nexport const FIRST_ORDER = 1\n\n/**\n * 이 차례를 승인하면 다음 결재자가 있나. 없으면 이것이 마지막이다.\n *\n * **참조는 건너뛴다.** 참조자가 아직 안 읽었어도 결재는 다음 결재자로 넘어간다 — 막게 만들면 그 사람이\n * 휴가일 때 결재가 선다. 막아야 하는 자격은 합의이고, 그것은 아직 없다.\n */\nexport function nextApproval(\n approvalLine: ApprovalLineItem[] | undefined,\n order: number\n): { item: ApprovalLineItem; order: number } | undefined {\n if (!approvalLine?.length) return undefined\n\n for (let next = order + 1; next <= approvalLine.length; next++) {\n const item = lineItemFor(approvalLine, next)\n if (isApprovalStep(item)) return { item: item!, order: next }\n }\n\n return undefined\n}\n\n/** 이 차례가 마지막인가. */\nexport function isLastApproval(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean {\n return !nextApproval(approvalLine, order)\n}\n\n/**\n * 결재가 필요한가 — **결재자가 하나라도 있어야 한다.**\n *\n * 비어 있으면 결재 없이 끝난다. 그리고 **참조만 있는 결재선도 결재 없이 끝난다** — 참조는 막지 않으므로\n * 기다릴 것이 없다. 참조가 있다고 결재를 기다리게 하면, 그 사람이 승인 권한도 없이 결재를 세운다.\n */\nexport function needsApproval(approvalLine: ApprovalLineItem[] | undefined): boolean {\n return firstApprovalOrder(approvalLine) !== undefined\n}\n\n/**\n * 이 판정이 「승인했다」인가.\n *\n * ── 옛 표기를 여기서 흡수한다 ──────────────────────────────────────────────\n * 2026-08-31 전에는 다음 결재자가 있으면 승인을 `escalated` 로 적었다. **그 행은 고치지 않는다** —\n * 승인 기록을 뒤에서 고쳐 쓰는 것은 규제 산업이 금지하는 바로 그 행위다.\n *\n * 대신 읽는 자리 하나에서 옛 표기를 새 뜻으로 읽는다. 이 규율이 두 곳으로 늘어나면 한쪽만 고쳐지는\n * 날이 온다. 「승인했나」를 묻는 코드는 전부 이것을 부른다.\n */\nexport function isApproved(judgment: string | undefined): boolean {\n return judgment === 'approved' || judgment === 'escalated'\n}\n\n/**\n * 더 기다릴 것이 없는 판정들 — **목록 질의와 이 파일이 같은 값을 본다.**\n *\n * 목록 질의가 이 배열을 그대로 쓴다. 두 벌로 두면 값이 늘 때 한쪽만 늘고, 그 판정을 받은 행이 대기\n * 목록에 영원히 남는다 — 실제로 그렇게 됐다(아래).\n */\nexport const TERMINATED_JUDGMENTS = ['approved', 'escalated', 'delegated', 'rejected', 'aborted', 'closed'] as const\n\n/**\n * 이 판정이 끝난 것인가 — 더 기다릴 것이 없나.\n *\n * ── `aborted` 가 빠져 있었다 (2026-09-01) ─────────────────────────────────\n * 스레드를 중단하면 그 스레드의 대기 결재가 `aborted` 로 닫힌다(§`activity-thread/_abort.ts`).\n * 그런데 **목록 질의도 이 함수도 그 값을 「끝난 것」으로 세지 않았다.**\n *\n * 그래서 중단된 건의 결재가 그 사람의 **승인 대기 목록에 영원히 남았다.** 눌러 봐야 이미 끝난 건이고,\n * 목록에서 사라지지도 않는다. 닫는 쪽은 제대로 닫았는데 읽는 쪽이 그것을 못 읽었다.\n */\nexport function isTerminated(judgment: string | undefined): boolean {\n return (TERMINATED_JUDGMENTS as readonly string[]).includes(judgment ?? '')\n}\n\n/**\n * Is the seat at this turn declared as a **reference** — someone who comments but does not decide?\n *\n * ── Why this is a function and not two inline calls ────────────────────────\n * ApprovalParticipation has said from the start that a reference only comments and does not affect\n * the judgment. create-approval, the escalation in approve(), and both list queries honour that.\n * The two places where a person presses a button did not: approve() and reject() never asked whether\n * the row they are about to stamp is an approver's row.\n *\n * A reference row is created with judgment Pending, so it passes every check those two functions do\n * make. Approving one stamps it as a real approval and then nextApproval() picks the approver that\n * sits *after* that seat, which moves the approval a step forward. With no approver after it, one\n * reference ends the whole thread. Rejecting is worse: an activity with rejectionIsFinal cannot be\n * restarted, so the case closes for good.\n *\n * Writing the check inline in both callers would put the rule back in two places, which is the\n * reason this file exists.\n *\n * ── The approval line is the authority, not the row ───────────────────────\n * The capacity is also copied onto ActivityApproval.participation. approve() already lets the\n * approval line decide who comes next (nextApproval(approvalLine, order)); letting the row decide\n * who may judge would put two authorities in one function, with nothing to settle a disagreement.\n * The line is loaded either way, so this costs no extra read. (2026-09-09, architect's ruling)\n *\n * ── False when the seat is not in the line ────────────────────────────────\n * That answers \"not declared a reference\", which is not the same as \"is an approver\". One boolean\n * cannot carry both facts, so this function reports only what is written down and the caller\n * refuses on a definite yes. A missing seat means the line grew shorter after submission; telling\n * that person they are a reference would state something the data does not say. Whether the line\n * can be edited after submission is being measured separately.\n */\nexport function isReferenceOrder(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean {\n return isReferenceStep(lineItemFor(approvalLine, order))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"approval-line.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approval-line.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **타입만 가져온다.** 값으로 가져오면 `@things-factory/organization` 이 실려 오고, 그것이 auth-base 의\n * 라우터까지 끌고 온다. 순수 규칙 하나를 시험하려는데 서버가 통째로 뜬다.\n */\nimport type { ApprovalLineItem } from '@things-factory/organization'\n\n/**\n * 결재선에서 다음 결재자를 찾는다.\n *\n * ── 왜 따로 뺐나 ───────────────────────────────────────────────────────────\n * 같은 규칙이 두 파일에 나뉘어 있었다.\n *\n * submit.ts 첫 결재를 `order: 1` 로 만들고 `approvalLine[0]` 을 넣는다\n * approve.ts `order` 를 승인하면 `approvalLine[order]` 를 다음 결재자로 삼는다\n *\n * `order` 와 배열 자리가 1만큼 다르다. 지금은 맞게 짝이 맞는데, 한쪽만 고치면 결재자가 한 명 빠지거나\n * 같은 사람이 두 번 들어간다. 그리고 **오류가 안 난다** — 결재가 다 끝나고 나서야 안다.\n *\n * 그래서 한 자리에 적고 양쪽이 이것을 부른다.\n *\n * `order` 는 사람이 세는 수다 — 1차 결재, 2차 결재. 배열은 0부터이므로 그 차이를 여기서만 다룬다.\n */\n\n/**\n * 이 항목이 **결재**인가 — 비어 있으면 결재다.\n *\n * 지금 도는 결재선에는 이 칸이 없다(gangsters·dataset·quotation·accounting). 그 항목들이 전부 결재자로\n * 읽혀야 거동이 안 바뀐다. 그래서 「참조라고 적힌 것만 참조」다.\n */\nexport function isApprovalStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation !== 'reference'\n}\n\n/** 이 항목이 참조인가 — 의견만 내고 결재를 막지 않는다. */\nexport function isReferenceStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation === 'reference'\n}\n\n/**\n * 이 차례의 결재자는 결재선의 어느 줄인가. `order` 는 1부터.\n *\n * **참조 항목도 자리를 차지한다.** `order` 는 배열 자리 그대로다 — 다시 매기면 이미 쌓인 행의 뜻이\n * 바뀐다. 그래서 결재자의 차례가 1·3·4 처럼 띄엄띄엄해질 수 있고, 그것이 맞다.\n *\n * 이 함수는 그 자리에 무엇이 있든 돌려준다. 결재인지 아닌지는 부르는 쪽이 본다(§`nextApproval`).\n */\nexport function lineItemFor(approvalLine: ApprovalLineItem[] | undefined, order: number): ApprovalLineItem | undefined {\n if (!approvalLine?.length || order < 1) {\n return undefined\n }\n\n return approvalLine[order - 1]\n}\n\n/**\n * 첫 **결재자**는 몇 번째 자리인가 — 앞에 참조가 있으면 건너뛴다.\n *\n * 결재선이 참조로만 이루어져 있으면 `undefined` 다. 그때는 결재 없이 끝난다(§`needsApproval`).\n */\nexport function firstApprovalOrder(approvalLine: ApprovalLineItem[] | undefined): number | undefined {\n if (!approvalLine?.length) return undefined\n\n const index = approvalLine.findIndex(isApprovalStep)\n return index < 0 ? undefined : index + 1\n}\n\n/**\n * 이 결재선의 참조들 — **언제 알릴지와 함께.**\n *\n * `notifyAfter` 가 없으면 `0` 으로 답한다. 0 은 「상신 즉시」다 — 아직 아무 결재도 끝나지 않은 시점.\n */\nexport function referenceSteps(\n approvalLine: ApprovalLineItem[] | undefined\n): { item: ApprovalLineItem; order: number; notifyAfter: number }[] {\n if (!approvalLine?.length) return []\n\n return approvalLine\n .map((item, index) => ({ item, order: index + 1, notifyAfter: Number((item as any).notifyAfter ?? 0) || 0 }))\n .filter(x => isReferenceStep(x.item))\n}\n\n/** 이번에 알려야 할 참조들 — `afterOrder` 번째 결재가 막 끝났을 때. 상신 직후는 `0`. */\nexport function referencesToNotify(\n approvalLine: ApprovalLineItem[] | undefined,\n afterOrder: number\n): { item: ApprovalLineItem; order: number }[] {\n return referenceSteps(approvalLine)\n .filter(x => x.notifyAfter === afterOrder)\n .map(({ item, order }) => ({ item, order }))\n}\n\n/** 첫 결재의 차례. */\nexport const FIRST_ORDER = 1\n\n/**\n * 이 차례를 승인하면 다음 결재자가 있나. 없으면 이것이 마지막이다.\n *\n * **참조는 건너뛴다.** 참조자가 아직 안 읽었어도 결재는 다음 결재자로 넘어간다 — 막게 만들면 그 사람이\n * 휴가일 때 결재가 선다. 막아야 하는 자격은 합의이고, 그것은 아직 없다.\n */\nexport function nextApproval(\n approvalLine: ApprovalLineItem[] | undefined,\n order: number\n): { item: ApprovalLineItem; order: number } | undefined {\n if (!approvalLine?.length) return undefined\n\n for (let next = order + 1; next <= approvalLine.length; next++) {\n const item = lineItemFor(approvalLine, next)\n if (isApprovalStep(item)) return { item: item!, order: next }\n }\n\n return undefined\n}\n\n/** 이 차례가 마지막인가. */\nexport function isLastApproval(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean {\n return !nextApproval(approvalLine, order)\n}\n\n/**\n * 결재가 필요한가 — **결재자가 하나라도 있어야 한다.**\n *\n * 비어 있으면 결재 없이 끝난다. 그리고 **참조만 있는 결재선도 결재 없이 끝난다** — 참조는 막지 않으므로\n * 기다릴 것이 없다. 참조가 있다고 결재를 기다리게 하면, 그 사람이 승인 권한도 없이 결재를 세운다.\n */\nexport function needsApproval(approvalLine: ApprovalLineItem[] | undefined): boolean {\n return firstApprovalOrder(approvalLine) !== undefined\n}\n\n/**\n * 이 판정이 「승인했다」인가.\n *\n * ── 옛 표기를 여기서 흡수한다 ──────────────────────────────────────────────\n * 2026-08-31 전에는 다음 결재자가 있으면 승인을 `escalated` 로 적었다. **그 행은 고치지 않는다** —\n * 승인 기록을 뒤에서 고쳐 쓰는 것은 규제 산업이 금지하는 바로 그 행위다.\n *\n * 대신 읽는 자리 하나에서 옛 표기를 새 뜻으로 읽는다. 이 규율이 두 곳으로 늘어나면 한쪽만 고쳐지는\n * 날이 온다. 「승인했나」를 묻는 코드는 전부 이것을 부른다.\n */\nexport function isApproved(judgment: string | undefined): boolean {\n return judgment === 'approved' || judgment === 'escalated'\n}\n\n/**\n * 더 기다릴 것이 없는 판정들 — **목록 질의와 이 파일이 같은 값을 본다.**\n *\n * 목록 질의가 이 배열을 그대로 쓴다. 두 벌로 두면 값이 늘 때 한쪽만 늘고, 그 판정을 받은 행이 대기\n * 목록에 영원히 남는다 — 실제로 그렇게 됐다(아래).\n */\nexport { TERMINATED_JUDGMENTS } from '../../../shared/approval-step.js'\n\n/**\n * 이 판정이 끝난 것인가 — 더 기다릴 것이 없나.\n *\n * ── `aborted` 가 빠져 있었다 (2026-09-01) ─────────────────────────────────\n * 스레드를 중단하면 그 스레드의 대기 결재가 `aborted` 로 닫힌다(§`activity-thread/_abort.ts`).\n * 그런데 **목록 질의도 이 함수도 그 값을 「끝난 것」으로 세지 않았다.**\n *\n * 그래서 중단된 건의 결재가 그 사람의 **승인 대기 목록에 영원히 남았다.** 눌러 봐야 이미 끝난 건이고,\n * 목록에서 사라지지도 않는다. 닫는 쪽은 제대로 닫았는데 읽는 쪽이 그것을 못 읽었다.\n */\nexport { isTerminated } from '../../../shared/approval-step.js'\n\n/**\n * Is the seat at this turn declared as a **reference** — someone who comments but does not decide?\n *\n * ── Why this is a function and not two inline calls ────────────────────────\n * ApprovalParticipation has said from the start that a reference only comments and does not affect\n * the judgment. create-approval, the escalation in approve(), and both list queries honour that.\n * The two places where a person presses a button did not: approve() and reject() never asked whether\n * the row they are about to stamp is an approver's row.\n *\n * A reference row is created with judgment Pending, so it passes every check those two functions do\n * make. Approving one stamps it as a real approval and then nextApproval() picks the approver that\n * sits *after* that seat, which moves the approval a step forward. With no approver after it, one\n * reference ends the whole thread. Rejecting is worse: an activity with rejectionIsFinal cannot be\n * restarted, so the case closes for good.\n *\n * Writing the check inline in both callers would put the rule back in two places, which is the\n * reason this file exists.\n *\n * ── The approval line is the authority, not the row ───────────────────────\n * The capacity is also copied onto ActivityApproval.participation. approve() already lets the\n * approval line decide who comes next (nextApproval(approvalLine, order)); letting the row decide\n * who may judge would put two authorities in one function, with nothing to settle a disagreement.\n * The line is loaded either way, so this costs no extra read. (2026-09-09, architect's ruling)\n *\n * ── False when the seat is not in the line ────────────────────────────────\n * That answers \"not declared a reference\", which is not the same as \"is an approver\". One boolean\n * cannot carry both facts, so this function reports only what is written down and the caller\n * refuses on a definite yes. A missing seat means the line grew shorter after submission; telling\n * that person they are a reference would state something the data does not say. Whether the line\n * can be edited after submission is being measured separately.\n */\nexport function isReferenceOrder(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean {\n return isReferenceStep(lineItemFor(approvalLine, order))\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Pagination } from '@things-factory/shell';
|
|
2
|
+
import { ApprovalProgressFilter, ApprovalProgressList } from './approval-progress-type.js';
|
|
3
|
+
/**
|
|
4
|
+
* 결재 현황 — the domain's every approval case and where it stands, whatever its kind (ADR-0183).
|
|
5
|
+
*
|
|
6
|
+
* Read by whoever reads every case (`activity-instance` query, the rule `assert-may-read.ts` names): a line shows
|
|
7
|
+
* the headline and the requester of a case the reader may already open.
|
|
8
|
+
*
|
|
9
|
+
* A case is one activity instance with a deciding approval row. Its state, step and waiting are read from the rows
|
|
10
|
+
* by `progressOf`; the filter and the counts run on those lines, so a count says what the list would show.
|
|
11
|
+
*/
|
|
12
|
+
export declare class ApprovalProgressQuery {
|
|
13
|
+
approvalProgress(filter: ApprovalProgressFilter, pagination: Pagination, context: ResolverContext): Promise<ApprovalProgressList>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApprovalProgressQuery = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
7
|
+
const organization_1 = require("@things-factory/organization");
|
|
8
|
+
const shell_1 = require("@things-factory/shell");
|
|
9
|
+
const activity_approval_js_1 = require("../activity-approval/activity-approval.js");
|
|
10
|
+
const decision_key_declaration_js_1 = require("../activity/decision-key-declaration.js");
|
|
11
|
+
const approval_progress_rule_js_1 = require("./approval-progress-rule.js");
|
|
12
|
+
const approval_progress_type_js_1 = require("./approval-progress-type.js");
|
|
13
|
+
const waiting_on_js_1 = require("./waiting-on.js");
|
|
14
|
+
/**
|
|
15
|
+
* 결재 현황 — the domain's every approval case and where it stands, whatever its kind (ADR-0183).
|
|
16
|
+
*
|
|
17
|
+
* Read by whoever reads every case (`activity-instance` query, the rule `assert-may-read.ts` names): a line shows
|
|
18
|
+
* the headline and the requester of a case the reader may already open.
|
|
19
|
+
*
|
|
20
|
+
* A case is one activity instance with a deciding approval row. Its state, step and waiting are read from the rows
|
|
21
|
+
* by `progressOf`; the filter and the counts run on those lines, so a count says what the list would show.
|
|
22
|
+
*/
|
|
23
|
+
let ApprovalProgressQuery = class ApprovalProgressQuery {
|
|
24
|
+
async approvalProgress(filter, pagination, context) {
|
|
25
|
+
const { domain } = context.state;
|
|
26
|
+
filter = filter || {};
|
|
27
|
+
const rows = await (0, shell_1.getRepository)(activity_approval_js_1.ActivityApproval)
|
|
28
|
+
.createQueryBuilder('ap')
|
|
29
|
+
.innerJoinAndSelect('ap.activityThread', 'at')
|
|
30
|
+
.innerJoinAndSelect('at.activityInstance', 'ai')
|
|
31
|
+
.leftJoinAndSelect('ai.activity', 'act')
|
|
32
|
+
.where('ap.domain = :domainId', { domainId: domain.id })
|
|
33
|
+
.getMany();
|
|
34
|
+
const cases = new Map();
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const id = row.activityThread.activityInstance.id;
|
|
37
|
+
cases.set(id, [...(cases.get(id) || []), row]);
|
|
38
|
+
}
|
|
39
|
+
const requesterOf = (approvals) => {
|
|
40
|
+
const thread = approvals[0].activityThread;
|
|
41
|
+
return thread.assigneeId || thread.activityInstance.creatorId;
|
|
42
|
+
};
|
|
43
|
+
const requesterIds = [...new Set([...cases.values()].map(requesterOf).filter(Boolean))];
|
|
44
|
+
const employees = requesterIds.length
|
|
45
|
+
? await (0, shell_1.getRepository)(organization_1.Employee)
|
|
46
|
+
.createQueryBuilder('e')
|
|
47
|
+
.leftJoinAndSelect('e.user', 'u')
|
|
48
|
+
.leftJoinAndSelect('e.department', 'd')
|
|
49
|
+
.where('e.domain = :domainId', { domainId: domain.id })
|
|
50
|
+
.andWhere('u.id IN (:...requesterIds)', { requesterIds })
|
|
51
|
+
.getMany()
|
|
52
|
+
: [];
|
|
53
|
+
const employeeOf = new Map(employees.map(one => [one.user?.id, one]));
|
|
54
|
+
const users = requesterIds.length
|
|
55
|
+
? await (0, shell_1.getRepository)(auth_base_1.User).createQueryBuilder('u').where('u.id IN (:...requesterIds)', { requesterIds }).getMany()
|
|
56
|
+
: [];
|
|
57
|
+
const userName = new Map(users.map(one => [one.id, one.name]));
|
|
58
|
+
const lines = [];
|
|
59
|
+
for (const [id, approvals] of cases) {
|
|
60
|
+
/* A case seated only for references is not an approval. */
|
|
61
|
+
if (!approvals.some(row => row.participation !== 'reference'))
|
|
62
|
+
continue;
|
|
63
|
+
const instance = approvals[0].activityThread.activityInstance;
|
|
64
|
+
const requesterId = requesterOf(approvals);
|
|
65
|
+
const employee = employeeOf.get(requesterId);
|
|
66
|
+
lines.push({
|
|
67
|
+
id,
|
|
68
|
+
approvals,
|
|
69
|
+
activityName: instance.activity?.name ?? '',
|
|
70
|
+
headline: instance.name ?? '',
|
|
71
|
+
requesterName: employee?.name || userName.get(requesterId),
|
|
72
|
+
departmentId: employee?.department?.id,
|
|
73
|
+
progress: (0, approval_progress_rule_js_1.progressOf)({
|
|
74
|
+
instanceState: instance.state,
|
|
75
|
+
dueAt: instance.dueAt,
|
|
76
|
+
approvalLine: instance.approvalLine,
|
|
77
|
+
approvals
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const passing = lines.filter(line => (0, approval_progress_rule_js_1.passes)(line, filter)).sort(approval_progress_rule_js_1.inProgressOrder);
|
|
82
|
+
const { states, overdue } = (0, approval_progress_rule_js_1.countsOf)(lines, filter);
|
|
83
|
+
const limit = pagination?.limit || 50;
|
|
84
|
+
const page = pagination?.page || 1;
|
|
85
|
+
const pageLines = passing.slice((page - 1) * limit, page * limit);
|
|
86
|
+
const items = [];
|
|
87
|
+
for (const line of pageLines) {
|
|
88
|
+
const instance = line.approvals[0].activityThread.activityInstance;
|
|
89
|
+
const { names, roleName } = await (0, waiting_on_js_1.namesWaitedOn)((0, shell_1.getRepository)(activity_approval_js_1.ActivityApproval).manager, line.progress.waiting);
|
|
90
|
+
const employee = employeeOf.get(requesterOf(line.approvals));
|
|
91
|
+
items.push({
|
|
92
|
+
id: line.id,
|
|
93
|
+
activityName: line.activityName,
|
|
94
|
+
activityLabel: instance.activity?.label || line.activityName,
|
|
95
|
+
activityLabelTKey: line.activityName ? (0, decision_key_declaration_js_1.activityTKey)(line.activityName, 'label') : undefined,
|
|
96
|
+
headline: line.headline,
|
|
97
|
+
requesterName: line.requesterName,
|
|
98
|
+
departmentName: employee?.department?.name,
|
|
99
|
+
state: line.progress.state,
|
|
100
|
+
step: line.progress.step,
|
|
101
|
+
steps: line.progress.steps,
|
|
102
|
+
waitingOn: names,
|
|
103
|
+
waitingRole: roleName,
|
|
104
|
+
waitingSince: line.progress.waitingSince,
|
|
105
|
+
dueAt: instance.dueAt,
|
|
106
|
+
overdue: line.progress.overdue,
|
|
107
|
+
raisedAt: line.progress.raisedAt,
|
|
108
|
+
endedAt: line.progress.endedAt
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/* The kinds that have a case here - what the kind filter offers. */
|
|
112
|
+
const kinds = new Map();
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const activity = line.approvals[0].activityThread.activityInstance.activity;
|
|
115
|
+
if (line.activityName && !kinds.has(line.activityName))
|
|
116
|
+
kinds.set(line.activityName, { name: line.activityName, label: activity?.label || line.activityName, labelTKey: (0, decision_key_declaration_js_1.activityTKey)(line.activityName, 'label') });
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
items,
|
|
120
|
+
kinds: [...kinds.values()].sort((a, b) => a.label.localeCompare(b.label)),
|
|
121
|
+
total: passing.length,
|
|
122
|
+
counts: approval_progress_rule_js_1.APPROVAL_PROGRESS_STATES.map(state => ({ state, count: states[state] })),
|
|
123
|
+
overdue
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
exports.ApprovalProgressQuery = ApprovalProgressQuery;
|
|
128
|
+
tslib_1.__decorate([
|
|
129
|
+
(0, type_graphql_1.Directive)('@privilege(category: "activity-instance", privilege: "query", domainOwnerGranted: true)'),
|
|
130
|
+
(0, type_graphql_1.Query)(returns => approval_progress_type_js_1.ApprovalProgressList, { description: 'The approvals of this domain, every kind, and where each stands.' }),
|
|
131
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('filter', type => approval_progress_type_js_1.ApprovalProgressFilter, { nullable: true })),
|
|
132
|
+
tslib_1.__param(1, (0, type_graphql_1.Arg)('pagination', type => shell_1.Pagination, { nullable: true })),
|
|
133
|
+
tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
|
|
134
|
+
tslib_1.__metadata("design:type", Function),
|
|
135
|
+
tslib_1.__metadata("design:paramtypes", [approval_progress_type_js_1.ApprovalProgressFilter,
|
|
136
|
+
shell_1.Pagination, Object]),
|
|
137
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
138
|
+
], ApprovalProgressQuery.prototype, "approvalProgress", null);
|
|
139
|
+
exports.ApprovalProgressQuery = ApprovalProgressQuery = tslib_1.__decorate([
|
|
140
|
+
(0, type_graphql_1.Resolver)()
|
|
141
|
+
], ApprovalProgressQuery);
|
|
142
|
+
//# sourceMappingURL=approval-progress-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-progress-query.js","sourceRoot":"","sources":["../../../server/service/approval-progress/approval-progress-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Query, Resolver } from 'type-graphql'\n\nimport { User } from '@things-factory/auth-base'\nimport { Employee } from '@things-factory/organization'\nimport { getRepository, Pagination } from '@things-factory/shell'\n\nimport { ActivityApproval } from '../activity-approval/activity-approval.js'\nimport { activityTKey } from '../activity/decision-key-declaration.js'\nimport {\n APPROVAL_PROGRESS_STATES,\n countsOf,\n inProgressOrder,\n passes,\n progressOf,\n ProgressLine\n} from './approval-progress-rule.js'\nimport { ApprovalProgress, ApprovalProgressFilter, ApprovalProgressList } from './approval-progress-type.js'\nimport { namesWaitedOn } from './waiting-on.js'\n\n/**\n * 결재 현황 — the domain's every approval case and where it stands, whatever its kind (ADR-0183).\n *\n * Read by whoever reads every case (`activity-instance` query, the rule `assert-may-read.ts` names): a line shows\n * the headline and the requester of a case the reader may already open.\n *\n * A case is one activity instance with a deciding approval row. Its state, step and waiting are read from the rows\n * by `progressOf`; the filter and the counts run on those lines, so a count says what the list would show.\n */\n@Resolver()\nexport class ApprovalProgressQuery {\n @Directive('@privilege(category: \"activity-instance\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => ApprovalProgressList, { description: 'The approvals of this domain, every kind, and where each stands.' })\n async approvalProgress(\n @Arg('filter', type => ApprovalProgressFilter, { nullable: true }) filter: ApprovalProgressFilter,\n @Arg('pagination', type => Pagination, { nullable: true }) pagination: Pagination,\n @Ctx() context: ResolverContext\n ): Promise<ApprovalProgressList> {\n const { domain } = context.state\n filter = filter || {}\n\n const rows = await getRepository(ActivityApproval)\n .createQueryBuilder('ap')\n .innerJoinAndSelect('ap.activityThread', 'at')\n .innerJoinAndSelect('at.activityInstance', 'ai')\n .leftJoinAndSelect('ai.activity', 'act')\n .where('ap.domain = :domainId', { domainId: domain.id })\n .getMany()\n\n const cases = new Map<string, ActivityApproval[]>()\n for (const row of rows) {\n const id = row.activityThread.activityInstance.id\n cases.set(id, [...(cases.get(id) || []), row])\n }\n\n const requesterOf = (approvals: ActivityApproval[]) => {\n const thread = approvals[0].activityThread\n return thread.assigneeId || thread.activityInstance.creatorId\n }\n\n const requesterIds = [...new Set([...cases.values()].map(requesterOf).filter(Boolean))]\n const employees = requesterIds.length\n ? await getRepository(Employee)\n .createQueryBuilder('e')\n .leftJoinAndSelect('e.user', 'u')\n .leftJoinAndSelect('e.department', 'd')\n .where('e.domain = :domainId', { domainId: domain.id })\n .andWhere('u.id IN (:...requesterIds)', { requesterIds })\n .getMany()\n : []\n const employeeOf = new Map(employees.map(one => [one.user?.id, one]))\n const users = requesterIds.length\n ? await getRepository(User).createQueryBuilder('u').where('u.id IN (:...requesterIds)', { requesterIds }).getMany()\n : []\n const userName = new Map(users.map(one => [one.id, one.name]))\n\n const lines: (ProgressLine & { id: string; approvals: ActivityApproval[] })[] = []\n for (const [id, approvals] of cases) {\n /* A case seated only for references is not an approval. */\n if (!approvals.some(row => row.participation !== 'reference')) continue\n\n const instance = approvals[0].activityThread.activityInstance\n const requesterId = requesterOf(approvals)\n const employee = employeeOf.get(requesterId)\n\n lines.push({\n id,\n approvals,\n activityName: instance.activity?.name ?? '',\n headline: instance.name ?? '',\n requesterName: employee?.name || userName.get(requesterId),\n departmentId: employee?.department?.id,\n progress: progressOf({\n instanceState: instance.state,\n dueAt: instance.dueAt,\n approvalLine: instance.approvalLine as any,\n approvals\n })\n })\n }\n\n const passing = lines.filter(line => passes(line, filter)).sort(inProgressOrder)\n const { states, overdue } = countsOf(lines, filter)\n\n const limit = pagination?.limit || 50\n const page = pagination?.page || 1\n const pageLines = passing.slice((page - 1) * limit, page * limit)\n\n const items: ApprovalProgress[] = []\n for (const line of pageLines) {\n const instance = line.approvals[0].activityThread.activityInstance\n const { names, roleName } = await namesWaitedOn(getRepository(ActivityApproval).manager, line.progress.waiting)\n const employee = employeeOf.get(requesterOf(line.approvals))\n\n items.push({\n id: line.id,\n activityName: line.activityName,\n activityLabel: instance.activity?.label || line.activityName,\n activityLabelTKey: line.activityName ? activityTKey(line.activityName, 'label') : undefined,\n headline: line.headline,\n requesterName: line.requesterName,\n departmentName: employee?.department?.name,\n state: line.progress.state,\n step: line.progress.step,\n steps: line.progress.steps,\n waitingOn: names,\n waitingRole: roleName,\n waitingSince: line.progress.waitingSince,\n dueAt: instance.dueAt,\n overdue: line.progress.overdue,\n raisedAt: line.progress.raisedAt,\n endedAt: line.progress.endedAt\n })\n }\n\n /* The kinds that have a case here - what the kind filter offers. */\n const kinds = new Map<string, { name: string; label: string; labelTKey: string }>()\n for (const line of lines) {\n const activity = line.approvals[0].activityThread.activityInstance.activity\n if (line.activityName && !kinds.has(line.activityName))\n kinds.set(line.activityName, { name: line.activityName, label: activity?.label || line.activityName, labelTKey: activityTKey(line.activityName, 'label') })\n }\n\n return {\n items,\n kinds: [...kinds.values()].sort((a, b) => a.label.localeCompare(b.label)),\n total: passing.length,\n counts: APPROVAL_PROGRESS_STATES.map(state => ({ state, count: states[state] })),\n overdue\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type ApprovalProgressState = 'PENDING' | 'APPROVED' | 'REJECTED' | 'ABORTED';
|
|
2
|
+
export declare const APPROVAL_PROGRESS_STATES: readonly ApprovalProgressState[];
|
|
3
|
+
export interface ProgressApprovalRow {
|
|
4
|
+
round?: number | null;
|
|
5
|
+
order?: number | null;
|
|
6
|
+
judgment?: string | null;
|
|
7
|
+
participation?: string | null;
|
|
8
|
+
createdAt?: Date | null;
|
|
9
|
+
terminatedAt?: Date | null;
|
|
10
|
+
approverId?: string | null;
|
|
11
|
+
approverRoleId?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ProgressFacts {
|
|
14
|
+
instanceState?: string | null;
|
|
15
|
+
dueAt?: Date | null;
|
|
16
|
+
approvalLine?: {
|
|
17
|
+
participation?: string;
|
|
18
|
+
}[] | null;
|
|
19
|
+
approvals: ProgressApprovalRow[];
|
|
20
|
+
}
|
|
21
|
+
export interface Progress {
|
|
22
|
+
state: ApprovalProgressState;
|
|
23
|
+
/** The step waited on now, counted among the deciding steps (references skipped) — 1 for the first approver. */
|
|
24
|
+
step?: number;
|
|
25
|
+
/** How many deciding steps the line has. */
|
|
26
|
+
steps: number;
|
|
27
|
+
/** The rows of the step waited on now — whom it waits on. */
|
|
28
|
+
waiting: ProgressApprovalRow[];
|
|
29
|
+
/** Since when the step waited on now has waited — its row is made when the step before it is approved. */
|
|
30
|
+
waitingSince?: Date;
|
|
31
|
+
/** When it was first put up for approval. */
|
|
32
|
+
raisedAt?: Date;
|
|
33
|
+
/** When it ended, for what is no longer waiting. */
|
|
34
|
+
endedAt?: Date;
|
|
35
|
+
overdue: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function progressOf(facts: ProgressFacts, now?: Date): Progress;
|
|
38
|
+
export interface ProgressFilter {
|
|
39
|
+
activityNames?: string[] | null;
|
|
40
|
+
states?: string[] | null;
|
|
41
|
+
departmentIds?: string[] | null;
|
|
42
|
+
/** The first day raised, inclusive. */
|
|
43
|
+
from?: Date | null;
|
|
44
|
+
/** The last moment raised, exclusive. */
|
|
45
|
+
to?: Date | null;
|
|
46
|
+
overdueOnly?: boolean | null;
|
|
47
|
+
search?: string | null;
|
|
48
|
+
}
|
|
49
|
+
export interface ProgressLine {
|
|
50
|
+
activityName: string;
|
|
51
|
+
headline: string;
|
|
52
|
+
requesterName?: string;
|
|
53
|
+
departmentId?: string;
|
|
54
|
+
progress: Progress;
|
|
55
|
+
}
|
|
56
|
+
/** Whether a line passes the filter. `ignore` leaves out the conditions a count is taken across. */
|
|
57
|
+
export declare function passes(line: ProgressLine, filter: ProgressFilter, ignore?: {
|
|
58
|
+
state?: boolean;
|
|
59
|
+
overdue?: boolean;
|
|
60
|
+
}): boolean;
|
|
61
|
+
/** What waits stands first, the longest waiting at the top; what ended follows, the latest first. */
|
|
62
|
+
export declare function inProgressOrder(a: ProgressLine, b: ProgressLine): number;
|
|
63
|
+
/** How many lines stand in each state, and how many are overdue — under the filter's other conditions. */
|
|
64
|
+
export declare function countsOf(lines: ProgressLine[], filter: ProgressFilter): {
|
|
65
|
+
states: Record<ApprovalProgressState, number>;
|
|
66
|
+
overdue: number;
|
|
67
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.APPROVAL_PROGRESS_STATES = void 0;
|
|
4
|
+
exports.progressOf = progressOf;
|
|
5
|
+
exports.passes = passes;
|
|
6
|
+
exports.inProgressOrder = inProgressOrder;
|
|
7
|
+
exports.countsOf = countsOf;
|
|
8
|
+
const approval_step_js_1 = require("../../../shared/approval-step.js");
|
|
9
|
+
exports.APPROVAL_PROGRESS_STATES = ['PENDING', 'APPROVED', 'REJECTED', 'ABORTED'];
|
|
10
|
+
const deciding = (row) => row.participation !== 'reference';
|
|
11
|
+
const timeOf = (value) => (value ? new Date(value).getTime() : undefined);
|
|
12
|
+
const earliest = (values) => {
|
|
13
|
+
const times = values.map(timeOf).filter((one) => one !== undefined);
|
|
14
|
+
return times.length ? new Date(Math.min(...times)) : undefined;
|
|
15
|
+
};
|
|
16
|
+
const latest = (values) => {
|
|
17
|
+
const times = values.map(timeOf).filter((one) => one !== undefined);
|
|
18
|
+
return times.length ? new Date(Math.max(...times)) : undefined;
|
|
19
|
+
};
|
|
20
|
+
/** The orders of the deciding steps of the line — a reference takes a place in the line and is not a step. */
|
|
21
|
+
function decidingOrders(facts) {
|
|
22
|
+
const line = facts.approvalLine || [];
|
|
23
|
+
if (line.length) {
|
|
24
|
+
return line.map((item, index) => (item?.participation === 'reference' ? 0 : index + 1)).filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
/* A case with no line kept on it: the orders its deciding rows carry. */
|
|
27
|
+
return [...new Set(facts.approvals.filter(deciding).map(row => Number(row.order) || 0))].sort((a, b) => a - b);
|
|
28
|
+
}
|
|
29
|
+
function progressOf(facts, now = new Date()) {
|
|
30
|
+
const approvals = facts.approvals || [];
|
|
31
|
+
const round = (0, approval_step_js_1.latestRound)(approvals);
|
|
32
|
+
const thisRound = approvals.filter(row => (Number(row.round) || 0) === round && deciding(row));
|
|
33
|
+
const orders = decidingOrders(facts);
|
|
34
|
+
/*
|
|
35
|
+
* 반려가 중단보다 먼저다. Office 의 문은 반려되면 업무를 닫는다(`rejectionIsFinal`) — 그 건은 aborted 이고
|
|
36
|
+
* 그 차수에 rejected 가 남는다. 중단을 먼저 읽으면 반려된 결재가 「중단」으로 보인다.
|
|
37
|
+
*/
|
|
38
|
+
const state = thisRound.some(row => row.judgment === 'rejected')
|
|
39
|
+
? 'REJECTED'
|
|
40
|
+
: facts.instanceState === 'aborted'
|
|
41
|
+
? 'ABORTED'
|
|
42
|
+
: (0, approval_step_js_1.currentStepOf)(approvals)
|
|
43
|
+
? 'PENDING'
|
|
44
|
+
: 'APPROVED';
|
|
45
|
+
const step = state === 'PENDING' ? (0, approval_step_js_1.currentStepOf)(approvals) : undefined;
|
|
46
|
+
const waiting = state === 'PENDING' ? (0, approval_step_js_1.currentStepRows)(approvals) : [];
|
|
47
|
+
const dueAt = timeOf(facts.dueAt);
|
|
48
|
+
return {
|
|
49
|
+
state,
|
|
50
|
+
step: step ? (orders.indexOf(step.order) + 1 || step.order) : undefined,
|
|
51
|
+
steps: orders.length,
|
|
52
|
+
waiting,
|
|
53
|
+
waitingSince: earliest(waiting.map(row => row.createdAt)),
|
|
54
|
+
raisedAt: earliest(approvals.filter(deciding).map(row => row.createdAt)),
|
|
55
|
+
endedAt: state === 'PENDING' ? undefined : latest(thisRound.map(row => row.terminatedAt)),
|
|
56
|
+
overdue: state === 'PENDING' && dueAt !== undefined && dueAt < now.getTime()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Whether a line passes the filter. `ignore` leaves out the conditions a count is taken across. */
|
|
60
|
+
function passes(line, filter, ignore = {}) {
|
|
61
|
+
const { progress } = line;
|
|
62
|
+
if (filter.activityNames?.length && !filter.activityNames.includes(line.activityName))
|
|
63
|
+
return false;
|
|
64
|
+
if (!ignore.state && filter.states?.length && !filter.states.includes(progress.state))
|
|
65
|
+
return false;
|
|
66
|
+
if (filter.departmentIds?.length && !filter.departmentIds.includes(line.departmentId ?? ''))
|
|
67
|
+
return false;
|
|
68
|
+
const raised = timeOf(progress.raisedAt);
|
|
69
|
+
if (filter.from && (raised === undefined || raised < new Date(filter.from).getTime()))
|
|
70
|
+
return false;
|
|
71
|
+
if (filter.to && (raised === undefined || raised >= new Date(filter.to).getTime()))
|
|
72
|
+
return false;
|
|
73
|
+
if (!ignore.overdue && filter.overdueOnly && !progress.overdue)
|
|
74
|
+
return false;
|
|
75
|
+
const search = filter.search?.trim().toLowerCase();
|
|
76
|
+
if (search && ![line.headline, line.requesterName ?? ''].some(text => text.toLowerCase().includes(search)))
|
|
77
|
+
return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/** What waits stands first, the longest waiting at the top; what ended follows, the latest first. */
|
|
81
|
+
function inProgressOrder(a, b) {
|
|
82
|
+
const pendingA = a.progress.state === 'PENDING';
|
|
83
|
+
const pendingB = b.progress.state === 'PENDING';
|
|
84
|
+
if (pendingA !== pendingB)
|
|
85
|
+
return pendingA ? -1 : 1;
|
|
86
|
+
if (pendingA)
|
|
87
|
+
return (timeOf(a.progress.waitingSince) ?? 0) - (timeOf(b.progress.waitingSince) ?? 0);
|
|
88
|
+
return (timeOf(b.progress.endedAt ?? b.progress.raisedAt) ?? 0) - (timeOf(a.progress.endedAt ?? a.progress.raisedAt) ?? 0);
|
|
89
|
+
}
|
|
90
|
+
/** How many lines stand in each state, and how many are overdue — under the filter's other conditions. */
|
|
91
|
+
function countsOf(lines, filter) {
|
|
92
|
+
const states = Object.fromEntries(exports.APPROVAL_PROGRESS_STATES.map(state => [state, 0]));
|
|
93
|
+
let overdue = 0;
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
if (passes(line, filter, { state: true, overdue: true }))
|
|
96
|
+
states[line.progress.state]++;
|
|
97
|
+
if (passes(line, filter, { overdue: true }) && line.progress.overdue)
|
|
98
|
+
overdue++;
|
|
99
|
+
}
|
|
100
|
+
return { states, overdue };
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=approval-progress-rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-progress-rule.js","sourceRoot":"","sources":["../../../server/service/approval-progress/approval-progress-rule.ts"],"names":[],"mappings":";;;AAoEA,gCAgCC;AAuBD,wBAYC;AAGD,0CAMC;AAGD,4BAQC;AA3JD,uEAA8F;AASjF,QAAA,wBAAwB,GAAqC,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC,CAAA;AAqCxH,MAAM,QAAQ,GAAG,CAAC,GAAwB,EAAE,EAAE,CAAC,GAAG,CAAC,aAAa,KAAK,WAAW,CAAA;AAChF,MAAM,MAAM,GAAG,CAAC,KAAmB,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AACvF,MAAM,QAAQ,GAAG,CAAC,MAAmC,EAAE,EAAE;IACvD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAA;IAClF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAChE,CAAC,CAAA;AACD,MAAM,MAAM,GAAG,CAAC,MAAmC,EAAE,EAAE;IACrD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAA;IAClF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAChE,CAAC,CAAA;AAED,8GAA8G;AAC9G,SAAS,cAAc,CAAC,KAAoB;IAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,YAAY,IAAI,EAAE,CAAA;IACrC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,aAAa,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACzG,CAAC;IAED,yEAAyE;IACzE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;AAChH,CAAC;AAED,SAAgB,UAAU,CAAC,KAAoB,EAAE,MAAY,IAAI,IAAI,EAAE;IACrE,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,IAAI,EAAE,CAAA;IACvC,MAAM,KAAK,GAAG,IAAA,8BAAW,EAAC,SAAS,CAAC,CAAA;IACpC,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9F,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA;IAEpC;;;OAGG;IACH,MAAM,KAAK,GAA0B,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,UAAU,CAAC;QACrF,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,KAAK,CAAC,aAAa,KAAK,SAAS;YACjC,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,IAAA,gCAAa,EAAC,SAAS,CAAC;gBACxB,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,UAAU,CAAA;IAElB,MAAM,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAA,gCAAa,EAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACvE,MAAM,OAAO,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAA,kCAAe,EAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACrE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAEjC,OAAO;QACL,KAAK;QACL,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;QACvE,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,OAAO;QACP,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACzD,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACxE,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QACzF,OAAO,EAAE,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE;KAC7E,CAAA;AACH,CAAC;AAsBD,oGAAoG;AACpG,SAAgB,MAAM,CAAC,IAAkB,EAAE,MAAsB,EAAE,SAAiD,EAAE;IACpH,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;IACzB,IAAI,MAAM,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC;QAAE,OAAO,KAAK,CAAA;IACnG,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACnG,IAAI,MAAM,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;QAAE,OAAO,KAAK,CAAA;IACzG,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;IACxC,IAAI,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,KAAK,CAAA;IACnG,IAAI,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,KAAK,CAAA;IAChG,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,WAAW,IAAI,CAAC,QAAQ,CAAC,OAAO;QAAE,OAAO,KAAK,CAAA;IAC5E,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAClD,IAAI,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IACxH,OAAO,IAAI,CAAA;AACb,CAAC;AAED,qGAAqG;AACrG,SAAgB,eAAe,CAAC,CAAe,EAAE,CAAe;IAC9D,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAA;IAC/C,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAA;IAC/C,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACnD,IAAI,QAAQ;QAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAA;IACpG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;AAC5H,CAAC;AAED,0GAA0G;AAC1G,SAAgB,QAAQ,CAAC,KAAqB,EAAE,MAAsB;IACpE,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,gCAAwB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAA0C,CAAA;IAC7H,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAA;QACvF,IAAI,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO;YAAE,OAAO,EAAE,CAAA;IACjF,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;AAC5B,CAAC","sourcesContent":["import { currentStepOf, currentStepRows, latestRound } from '../../../shared/approval-step.js'\n\n/*\n * 결재 현황의 규칙 — 결재 한 건의 줄 하나를 결재 행에서 읽는다(ADR-0183 결정 1). DB 도 화면도 가져오지 않는다:\n * 질의가 행을 모아 넘기고, 시험이 같은 함수를 부른다.\n */\n\nexport type ApprovalProgressState = 'PENDING' | 'APPROVED' | 'REJECTED' | 'ABORTED'\n\nexport const APPROVAL_PROGRESS_STATES: readonly ApprovalProgressState[] = ['PENDING', 'APPROVED', 'REJECTED', 'ABORTED']\n\nexport interface ProgressApprovalRow {\n round?: number | null\n order?: number | null\n judgment?: string | null\n participation?: string | null\n createdAt?: Date | null\n terminatedAt?: Date | null\n approverId?: string | null\n approverRoleId?: string | null\n}\n\nexport interface ProgressFacts {\n instanceState?: string | null\n dueAt?: Date | null\n approvalLine?: { participation?: string }[] | null\n approvals: ProgressApprovalRow[]\n}\n\nexport interface Progress {\n state: ApprovalProgressState\n /** The step waited on now, counted among the deciding steps (references skipped) — 1 for the first approver. */\n step?: number\n /** How many deciding steps the line has. */\n steps: number\n /** The rows of the step waited on now — whom it waits on. */\n waiting: ProgressApprovalRow[]\n /** Since when the step waited on now has waited — its row is made when the step before it is approved. */\n waitingSince?: Date\n /** When it was first put up for approval. */\n raisedAt?: Date\n /** When it ended, for what is no longer waiting. */\n endedAt?: Date\n overdue: boolean\n}\n\nconst deciding = (row: ProgressApprovalRow) => row.participation !== 'reference'\nconst timeOf = (value?: Date | null) => (value ? new Date(value).getTime() : undefined)\nconst earliest = (values: (Date | null | undefined)[]) => {\n const times = values.map(timeOf).filter((one): one is number => one !== undefined)\n return times.length ? new Date(Math.min(...times)) : undefined\n}\nconst latest = (values: (Date | null | undefined)[]) => {\n const times = values.map(timeOf).filter((one): one is number => one !== undefined)\n return times.length ? new Date(Math.max(...times)) : undefined\n}\n\n/** The orders of the deciding steps of the line — a reference takes a place in the line and is not a step. */\nfunction decidingOrders(facts: ProgressFacts): number[] {\n const line = facts.approvalLine || []\n if (line.length) {\n return line.map((item, index) => (item?.participation === 'reference' ? 0 : index + 1)).filter(Boolean)\n }\n\n /* A case with no line kept on it: the orders its deciding rows carry. */\n return [...new Set(facts.approvals.filter(deciding).map(row => Number(row.order) || 0))].sort((a, b) => a - b)\n}\n\nexport function progressOf(facts: ProgressFacts, now: Date = new Date()): Progress {\n const approvals = facts.approvals || []\n const round = latestRound(approvals)\n const thisRound = approvals.filter(row => (Number(row.round) || 0) === round && deciding(row))\n const orders = decidingOrders(facts)\n\n /*\n * 반려가 중단보다 먼저다. Office 의 문은 반려되면 업무를 닫는다(`rejectionIsFinal`) — 그 건은 aborted 이고\n * 그 차수에 rejected 가 남는다. 중단을 먼저 읽으면 반려된 결재가 「중단」으로 보인다.\n */\n const state: ApprovalProgressState = thisRound.some(row => row.judgment === 'rejected')\n ? 'REJECTED'\n : facts.instanceState === 'aborted'\n ? 'ABORTED'\n : currentStepOf(approvals)\n ? 'PENDING'\n : 'APPROVED'\n\n const step = state === 'PENDING' ? currentStepOf(approvals) : undefined\n const waiting = state === 'PENDING' ? currentStepRows(approvals) : []\n const dueAt = timeOf(facts.dueAt)\n\n return {\n state,\n step: step ? (orders.indexOf(step.order) + 1 || step.order) : undefined,\n steps: orders.length,\n waiting,\n waitingSince: earliest(waiting.map(row => row.createdAt)),\n raisedAt: earliest(approvals.filter(deciding).map(row => row.createdAt)),\n endedAt: state === 'PENDING' ? undefined : latest(thisRound.map(row => row.terminatedAt)),\n overdue: state === 'PENDING' && dueAt !== undefined && dueAt < now.getTime()\n }\n}\n\nexport interface ProgressFilter {\n activityNames?: string[] | null\n states?: string[] | null\n departmentIds?: string[] | null\n /** The first day raised, inclusive. */\n from?: Date | null\n /** The last moment raised, exclusive. */\n to?: Date | null\n overdueOnly?: boolean | null\n search?: string | null\n}\n\nexport interface ProgressLine {\n activityName: string\n headline: string\n requesterName?: string\n departmentId?: string\n progress: Progress\n}\n\n/** Whether a line passes the filter. `ignore` leaves out the conditions a count is taken across. */\nexport function passes(line: ProgressLine, filter: ProgressFilter, ignore: { state?: boolean; overdue?: boolean } = {}): boolean {\n const { progress } = line\n if (filter.activityNames?.length && !filter.activityNames.includes(line.activityName)) return false\n if (!ignore.state && filter.states?.length && !filter.states.includes(progress.state)) return false\n if (filter.departmentIds?.length && !filter.departmentIds.includes(line.departmentId ?? '')) return false\n const raised = timeOf(progress.raisedAt)\n if (filter.from && (raised === undefined || raised < new Date(filter.from).getTime())) return false\n if (filter.to && (raised === undefined || raised >= new Date(filter.to).getTime())) return false\n if (!ignore.overdue && filter.overdueOnly && !progress.overdue) return false\n const search = filter.search?.trim().toLowerCase()\n if (search && ![line.headline, line.requesterName ?? ''].some(text => text.toLowerCase().includes(search))) return false\n return true\n}\n\n/** What waits stands first, the longest waiting at the top; what ended follows, the latest first. */\nexport function inProgressOrder(a: ProgressLine, b: ProgressLine): number {\n const pendingA = a.progress.state === 'PENDING'\n const pendingB = b.progress.state === 'PENDING'\n if (pendingA !== pendingB) return pendingA ? -1 : 1\n if (pendingA) return (timeOf(a.progress.waitingSince) ?? 0) - (timeOf(b.progress.waitingSince) ?? 0)\n return (timeOf(b.progress.endedAt ?? b.progress.raisedAt) ?? 0) - (timeOf(a.progress.endedAt ?? a.progress.raisedAt) ?? 0)\n}\n\n/** How many lines stand in each state, and how many are overdue — under the filter's other conditions. */\nexport function countsOf(lines: ProgressLine[], filter: ProgressFilter): { states: Record<ApprovalProgressState, number>; overdue: number } {\n const states = Object.fromEntries(APPROVAL_PROGRESS_STATES.map(state => [state, 0])) as Record<ApprovalProgressState, number>\n let overdue = 0\n for (const line of lines) {\n if (passes(line, filter, { state: true, overdue: true })) states[line.progress.state]++\n if (passes(line, filter, { overdue: true }) && line.progress.overdue) overdue++\n }\n return { states, overdue }\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare class ApprovalProgressFilter {
|
|
2
|
+
activityNames?: string[];
|
|
3
|
+
states?: string[];
|
|
4
|
+
departmentIds?: string[];
|
|
5
|
+
from?: Date;
|
|
6
|
+
to?: Date;
|
|
7
|
+
overdueOnly?: boolean;
|
|
8
|
+
search?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class ApprovalProgress {
|
|
11
|
+
id: string;
|
|
12
|
+
activityName: string;
|
|
13
|
+
activityLabel?: string;
|
|
14
|
+
activityLabelTKey?: string;
|
|
15
|
+
headline: string;
|
|
16
|
+
requesterName?: string;
|
|
17
|
+
departmentName?: string;
|
|
18
|
+
state: string;
|
|
19
|
+
step?: number;
|
|
20
|
+
steps: number;
|
|
21
|
+
waitingOn: string[];
|
|
22
|
+
waitingRole?: string;
|
|
23
|
+
waitingSince?: Date;
|
|
24
|
+
dueAt?: Date;
|
|
25
|
+
overdue: boolean;
|
|
26
|
+
raisedAt?: Date;
|
|
27
|
+
endedAt?: Date;
|
|
28
|
+
}
|
|
29
|
+
export declare class ApprovalProgressStateCount {
|
|
30
|
+
state: string;
|
|
31
|
+
count: number;
|
|
32
|
+
}
|
|
33
|
+
export declare class ApprovalProgressKind {
|
|
34
|
+
name: string;
|
|
35
|
+
label: string;
|
|
36
|
+
labelTKey: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class ApprovalProgressList {
|
|
39
|
+
items: ApprovalProgress[];
|
|
40
|
+
kinds: ApprovalProgressKind[];
|
|
41
|
+
total: number;
|
|
42
|
+
counts: ApprovalProgressStateCount[];
|
|
43
|
+
overdue: number;
|
|
44
|
+
}
|