@things-factory/work-shift 10.0.18 → 10.0.19
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/pages/work-shift.d.ts +106 -0
- package/dist-client/pages/work-shift.js +797 -42
- package/dist-client/pages/work-shift.js.map +1 -1
- package/dist-client/shift-coverage.d.ts +76 -0
- package/dist-client/shift-coverage.js +146 -0
- package/dist-client/shift-coverage.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/work-shift-range.js +23 -36
- package/dist-server/controllers/work-shift-range.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +2 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/work-shift/index.d.ts +4 -2
- package/dist-server/service/work-shift/index.js +4 -2
- package/dist-server/service/work-shift/index.js.map +1 -1
- package/dist-server/service/work-shift/work-shift-mutation.d.ts +10 -0
- package/dist-server/service/work-shift/work-shift-mutation.js +51 -1
- package/dist-server/service/work-shift/work-shift-mutation.js.map +1 -1
- package/dist-server/service/work-shift/work-shift-revision-query.d.ts +20 -0
- package/dist-server/service/work-shift/work-shift-revision-query.js +74 -0
- package/dist-server/service/work-shift/work-shift-revision-query.js.map +1 -0
- package/dist-server/service/work-shift/work-shift-revision-service.d.ts +69 -0
- package/dist-server/service/work-shift/work-shift-revision-service.js +186 -0
- package/dist-server/service/work-shift/work-shift-revision-service.js.map +1 -0
- package/dist-server/service/work-shift/work-shift-revision.d.ts +67 -0
- package/dist-server/service/work-shift/work-shift-revision.js +116 -0
- package/dist-server/service/work-shift/work-shift-revision.js.map +1 -0
- package/dist-server/service/work-shift/work-shift-selection.d.ts +50 -0
- package/dist-server/service/work-shift/work-shift-selection.js +65 -0
- package/dist-server/service/work-shift/work-shift-selection.js.map +1 -0
- package/dist-server/service/work-shift/work-shift-type.d.ts +5 -0
- package/dist-server/service/work-shift/work-shift-type.js +16 -1
- package/dist-server/service/work-shift/work-shift-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/translations/en.json +31 -1
- package/translations/ko.json +31 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shiftSetOf = shiftSetOf;
|
|
4
|
+
exports.sameShiftSet = sameShiftSet;
|
|
5
|
+
exports.inForceSince = inForceSince;
|
|
6
|
+
exports.recordRevision = recordRevision;
|
|
7
|
+
exports.shiftsInForceAt = shiftsInForceAt;
|
|
8
|
+
exports.announceShiftSet = announceShiftSet;
|
|
9
|
+
exports.cancelAnnouncement = cancelAnnouncement;
|
|
10
|
+
exports.announcementsOf = announcementsOf;
|
|
11
|
+
const typeorm_1 = require("typeorm");
|
|
12
|
+
const work_shift_1 = require("./work-shift");
|
|
13
|
+
const work_shift_revision_1 = require("./work-shift-revision");
|
|
14
|
+
const work_shift_selection_1 = require("./work-shift-selection");
|
|
15
|
+
/**
|
|
16
|
+
* 편성이 바뀔 때 지나간 것을 남기고, 지난 시각의 편성을 되찾는다.
|
|
17
|
+
*/
|
|
18
|
+
/** 편성 한 벌에서 남길 것만 뽑는다. id·감사 칸은 지워진 행의 것이라 되살려도 쓸 데가 없다. */
|
|
19
|
+
function shiftSetOf(shifts) {
|
|
20
|
+
return shifts
|
|
21
|
+
.map(shift => ({
|
|
22
|
+
name: shift.name,
|
|
23
|
+
fromDate: shift.fromDate,
|
|
24
|
+
fromTime: shift.fromTime,
|
|
25
|
+
toDate: shift.toDate,
|
|
26
|
+
toTime: shift.toTime
|
|
27
|
+
}))
|
|
28
|
+
.sort((a, b) => a.fromDate - b.fromDate || String(a.fromTime).localeCompare(String(b.fromTime)));
|
|
29
|
+
}
|
|
30
|
+
/** 두 편성이 같은가. 같으면 남기지 않는다 — 바뀌지 않은 저장으로 이력이 늘면 읽을 수 없게 된다. */
|
|
31
|
+
function sameShiftSet(before, after) {
|
|
32
|
+
return JSON.stringify(before) === JSON.stringify(after);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 지금 편성이 **언제부터**인가.
|
|
36
|
+
*
|
|
37
|
+
* 저장이 전량 삭제 후 재삽입이라, 지금 행들 중 가장 이른 `createdAt` 이 곧 설치 시각이다. 행이 없으면
|
|
38
|
+
* 편성이 없는 것이므로 없다고 답한다 — 지어내면 그 시각 이전의 사실이 지금 편성으로 읽힌다.
|
|
39
|
+
*/
|
|
40
|
+
function inForceSince(shifts) {
|
|
41
|
+
const times = shifts.map(shift => shift.createdAt?.getTime()).filter((time) => Boolean(time));
|
|
42
|
+
return times.length ? new Date(Math.min(...times)) : undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 바꾸기 **전에** 지금 쓰던 편성을 남긴다.
|
|
46
|
+
*
|
|
47
|
+
* ── 순서가 이 함수의 전부다 ────────────────────────────────────────────────
|
|
48
|
+
* 저장은 그 도메인의 교대를 전부 지우고 다시 넣는다. 지운 뒤에 남기려 하면 남길 것이 없다.
|
|
49
|
+
*
|
|
50
|
+
* ── 지금 쓰던 것이 편집기의 내용이 아닐 수 있다 ────────────────────────────
|
|
51
|
+
* 예고한 시각이 지났으면 **예고분이 적용 중**이고, 편집기(`WorkShift`)의 내용은 그때 이미 답이 아니다.
|
|
52
|
+
* 그 상태에서 편집기 내용만 이력에 남기면 실제로 쓰이던 편성이 기록에서 빠진다.
|
|
53
|
+
*
|
|
54
|
+
* 그리고 그 열린 벌을 닫지 않으면 **저장한 것이 적용되지 않는다** — 시각으로 고를 때 열린 벌이 계속
|
|
55
|
+
* 이기기 때문이다. 사람은 저장했는데 아무것도 안 바뀌는 것을 보게 된다.
|
|
56
|
+
*
|
|
57
|
+
* 적용 중이던 예고분은 닫는 것만으로 이력이 된다. 따로 한 줄 더 남기면 같은 편성이 두 번 적힌다.
|
|
58
|
+
*/
|
|
59
|
+
async function recordRevision(manager, domain, next, user, now = new Date()) {
|
|
60
|
+
/* 지금 실제로 적용 중인 벌 — 편집기의 것일 수도, 시작된 예고분일 수도 있다. */
|
|
61
|
+
const started = await manager.findOne(work_shift_revision_1.WorkShiftRevision, {
|
|
62
|
+
where: { domain: { id: domain.id }, effectiveTo: (0, typeorm_1.IsNull)() },
|
|
63
|
+
order: { effectiveFrom: 'DESC' }
|
|
64
|
+
});
|
|
65
|
+
if (started && started.effectiveFrom <= now) {
|
|
66
|
+
if (sameShiftSet(started.shifts, next)) {
|
|
67
|
+
/* 적용 중인 예고분과 같은 내용이다. 닫으면 뒤가 빈 구간이 되므로 그대로 둔다. */
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
/* 닫는 것만으로 이력이 된다. 그리고 닫아야 이번 저장이 적용된다. */
|
|
71
|
+
await manager.update(work_shift_revision_1.WorkShiftRevision, { id: started.id }, { effectiveTo: now });
|
|
72
|
+
return { ...started, effectiveTo: now };
|
|
73
|
+
}
|
|
74
|
+
const current = await manager.find(work_shift_1.WorkShift, { where: { domain: { id: domain.id } } });
|
|
75
|
+
if (!current.length) {
|
|
76
|
+
/* 첫 설치다. 지나간 편성이 없다. */
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const before = shiftSetOf(current);
|
|
80
|
+
if (sameShiftSet(before, next)) {
|
|
81
|
+
/* 내용이 그대로다. 남기면 이력이 「바뀌지 않음」으로 채워져 읽을 수 없게 된다. */
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return await manager.save(work_shift_revision_1.WorkShiftRevision, {
|
|
85
|
+
domain,
|
|
86
|
+
/* 언제부터 쓰던 것인가 — 모르면 그만둔 시각으로 둔다(길이 0인 구간). 지어내지 않는다. */
|
|
87
|
+
effectiveFrom: inForceSince(current) ?? now,
|
|
88
|
+
effectiveTo: now,
|
|
89
|
+
shifts: before,
|
|
90
|
+
creator: user
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** 이 도메인의 지나간 벌과 예고된 벌 전부. 고르는 일은 순수 함수가 한다. */
|
|
94
|
+
async function revisionsOf(manager, domain) {
|
|
95
|
+
return await manager.find(work_shift_revision_1.WorkShiftRevision, {
|
|
96
|
+
where: { domain: { id: domain.id } },
|
|
97
|
+
order: { effectiveFrom: 'ASC' }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 그 시각에 쓰던 편성 — **지금 편성이 아닐 수 있다.**
|
|
102
|
+
*
|
|
103
|
+
* 지나간 시각이면 이력에서, 예고한 시각이 지났으면 예고분에서 고른다. 고르는 규칙은
|
|
104
|
+
* `work-shift-selection.ts` 에 순수하게 있다.
|
|
105
|
+
*
|
|
106
|
+
* **이력보다 앞선 시각은 없다고 답한다.** 그때 무엇을 쓰고 있었는지 이 시스템은 모른다.
|
|
107
|
+
*/
|
|
108
|
+
async function shiftsInForceAt(manager, domain, at) {
|
|
109
|
+
const current = await manager.find(work_shift_1.WorkShift, {
|
|
110
|
+
where: { domain: { id: domain.id } },
|
|
111
|
+
order: { fromDate: 'ASC', fromTime: 'ASC' }
|
|
112
|
+
});
|
|
113
|
+
const revisions = await revisionsOf(manager, domain);
|
|
114
|
+
const selected = (0, work_shift_selection_1.selectSetAt)(at, current.length ? shiftSetOf(current) : undefined, inForceSince(current), revisions.map(revision => ({
|
|
115
|
+
effectiveFrom: revision.effectiveFrom,
|
|
116
|
+
effectiveTo: revision.effectiveTo,
|
|
117
|
+
shifts: revision.shifts
|
|
118
|
+
})));
|
|
119
|
+
return selected.shifts ? { shifts: selected.shifts, origin: selected.origin, from: selected.from } : undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 앞으로 쓸 편성을 미리 적어 둔다.
|
|
123
|
+
*
|
|
124
|
+
* ── 지난 시각으로는 예고하지 않는다 ────────────────────────────────────────
|
|
125
|
+
* 지난 시각으로 넣으면 이미 적힌 사실들의 교대 이름이 통째로 바뀐다. 그것은 예고가 아니라 **기록을
|
|
126
|
+
* 고치는 것**이고, 이 문으로 할 일이 아니다.
|
|
127
|
+
*
|
|
128
|
+
* ── 앞의 열린 벌을 닫는다 ──────────────────────────────────────────────────
|
|
129
|
+
* 끝이 없는 벌이 둘이면 어느 것이 답인지 갈린다. 새 예고의 시작이 앞엣것의 끝이 된다.
|
|
130
|
+
*/
|
|
131
|
+
async function announceShiftSet(manager, domain, effectiveFrom, shifts, user, now = new Date()) {
|
|
132
|
+
if (effectiveFrom <= now) {
|
|
133
|
+
throw new Error('announceShiftSet: 예고는 앞으로의 시각으로만 한다 — 지난 시각으로 넣으면 이미 적힌 사실의 교대가 바뀐다');
|
|
134
|
+
}
|
|
135
|
+
if (!shifts.length) {
|
|
136
|
+
throw new Error('announceShiftSet: 빈 편성은 예고할 수 없다 — 그 시각부터 교대가 없다는 뜻이 되어 버린다');
|
|
137
|
+
}
|
|
138
|
+
const open = await manager.findOne(work_shift_revision_1.WorkShiftRevision, {
|
|
139
|
+
where: { domain: { id: domain.id }, effectiveTo: (0, typeorm_1.IsNull)() },
|
|
140
|
+
order: { effectiveFrom: 'DESC' }
|
|
141
|
+
});
|
|
142
|
+
if (open) {
|
|
143
|
+
if (open.effectiveFrom >= effectiveFrom) {
|
|
144
|
+
throw new Error('announceShiftSet: 이미 더 늦은 예고가 있다 — 그것을 먼저 물리고 다시 예고한다');
|
|
145
|
+
}
|
|
146
|
+
/* 앞의 열린 벌은 새 예고가 시작하는 시각에 끝난다. */
|
|
147
|
+
await manager.update(work_shift_revision_1.WorkShiftRevision, { id: open.id }, { effectiveTo: effectiveFrom });
|
|
148
|
+
}
|
|
149
|
+
return await manager.save(work_shift_revision_1.WorkShiftRevision, {
|
|
150
|
+
domain,
|
|
151
|
+
effectiveFrom,
|
|
152
|
+
effectiveTo: null,
|
|
153
|
+
shifts,
|
|
154
|
+
creator: user
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 예고를 물린다.
|
|
159
|
+
*
|
|
160
|
+
* **아직 시작하지 않은 것만** 물릴 수 있다. 이미 적용 중인 벌을 지우면 그 사이에 적힌 사실들이 가리키던
|
|
161
|
+
* 편성이 사라진다 — 그때는 지우는 것이 아니라 새로 예고하거나 지금 편성을 저장한다.
|
|
162
|
+
*/
|
|
163
|
+
async function cancelAnnouncement(manager, domain, id, now = new Date()) {
|
|
164
|
+
const found = await manager.findOne(work_shift_revision_1.WorkShiftRevision, { where: { id, domain: { id: domain.id } } });
|
|
165
|
+
if (!found) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (found.effectiveFrom <= now) {
|
|
169
|
+
throw new Error('cancelAnnouncement: 이미 적용된 편성은 물릴 수 없다 — 그 사이에 적힌 사실이 이것을 가리킨다');
|
|
170
|
+
}
|
|
171
|
+
await manager.delete(work_shift_revision_1.WorkShiftRevision, { id: found.id });
|
|
172
|
+
/* 앞의 벌이 이 예고 때문에 닫혀 있었다면 다시 열어 준다 — 아니면 그 뒤가 빈 구간이 된다. */
|
|
173
|
+
const previous = await manager.findOne(work_shift_revision_1.WorkShiftRevision, {
|
|
174
|
+
where: { domain: { id: domain.id }, effectiveTo: found.effectiveFrom },
|
|
175
|
+
order: { effectiveFrom: 'DESC' }
|
|
176
|
+
});
|
|
177
|
+
if (previous) {
|
|
178
|
+
await manager.update(work_shift_revision_1.WorkShiftRevision, { id: previous.id }, { effectiveTo: null });
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
/** 아직 시작하지 않은 벌들 — 화면이 「예고된 편성」으로 보여 줄 것. */
|
|
183
|
+
async function announcementsOf(manager, domain, now = new Date()) {
|
|
184
|
+
return (0, work_shift_selection_1.announcedSets)(now, await revisionsOf(manager, domain));
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=work-shift-revision-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-shift-revision-service.js","sourceRoot":"","sources":["../../../server/service/work-shift/work-shift-revision-service.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, IsNull } from 'typeorm'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nimport { WorkShift } from './work-shift'\nimport { WorkShiftRevision } from './work-shift-revision'\nimport { RevisionLike, SetOrigin, announcedSets, selectSetAt } from './work-shift-selection'\n\n/**\n * 편성이 바뀔 때 지나간 것을 남기고, 지난 시각의 편성을 되찾는다.\n */\n\n/** 편성 한 벌에서 남길 것만 뽑는다. id·감사 칸은 지워진 행의 것이라 되살려도 쓸 데가 없다. */\nexport function shiftSetOf(shifts: WorkShift[]): Record<string, unknown>[] {\n return shifts\n .map(shift => ({\n name: shift.name,\n fromDate: shift.fromDate,\n fromTime: shift.fromTime,\n toDate: shift.toDate,\n toTime: shift.toTime\n }))\n .sort((a, b) => a.fromDate - b.fromDate || String(a.fromTime).localeCompare(String(b.fromTime)))\n}\n\n/** 두 편성이 같은가. 같으면 남기지 않는다 — 바뀌지 않은 저장으로 이력이 늘면 읽을 수 없게 된다. */\nexport function sameShiftSet(before: Record<string, unknown>[], after: Record<string, unknown>[]): boolean {\n return JSON.stringify(before) === JSON.stringify(after)\n}\n\n/**\n * 지금 편성이 **언제부터**인가.\n *\n * 저장이 전량 삭제 후 재삽입이라, 지금 행들 중 가장 이른 `createdAt` 이 곧 설치 시각이다. 행이 없으면\n * 편성이 없는 것이므로 없다고 답한다 — 지어내면 그 시각 이전의 사실이 지금 편성으로 읽힌다.\n */\nexport function inForceSince(shifts: WorkShift[]): Date | undefined {\n const times = shifts.map(shift => shift.createdAt?.getTime()).filter((time): time is number => Boolean(time))\n\n return times.length ? new Date(Math.min(...times)) : undefined\n}\n\n/**\n * 바꾸기 **전에** 지금 쓰던 편성을 남긴다.\n *\n * ── 순서가 이 함수의 전부다 ────────────────────────────────────────────────\n * 저장은 그 도메인의 교대를 전부 지우고 다시 넣는다. 지운 뒤에 남기려 하면 남길 것이 없다.\n *\n * ── 지금 쓰던 것이 편집기의 내용이 아닐 수 있다 ────────────────────────────\n * 예고한 시각이 지났으면 **예고분이 적용 중**이고, 편집기(`WorkShift`)의 내용은 그때 이미 답이 아니다.\n * 그 상태에서 편집기 내용만 이력에 남기면 실제로 쓰이던 편성이 기록에서 빠진다.\n *\n * 그리고 그 열린 벌을 닫지 않으면 **저장한 것이 적용되지 않는다** — 시각으로 고를 때 열린 벌이 계속\n * 이기기 때문이다. 사람은 저장했는데 아무것도 안 바뀌는 것을 보게 된다.\n *\n * 적용 중이던 예고분은 닫는 것만으로 이력이 된다. 따로 한 줄 더 남기면 같은 편성이 두 번 적힌다.\n */\nexport async function recordRevision(\n manager: EntityManager,\n domain: Domain,\n next: Record<string, unknown>[],\n user?: User,\n now = new Date()\n): Promise<WorkShiftRevision | undefined> {\n /* 지금 실제로 적용 중인 벌 — 편집기의 것일 수도, 시작된 예고분일 수도 있다. */\n const started = await manager.findOne(WorkShiftRevision, {\n where: { domain: { id: domain.id }, effectiveTo: IsNull() },\n order: { effectiveFrom: 'DESC' }\n })\n\n if (started && started.effectiveFrom <= now) {\n if (sameShiftSet(started.shifts, next)) {\n /* 적용 중인 예고분과 같은 내용이다. 닫으면 뒤가 빈 구간이 되므로 그대로 둔다. */\n return undefined\n }\n\n /* 닫는 것만으로 이력이 된다. 그리고 닫아야 이번 저장이 적용된다. */\n await manager.update(WorkShiftRevision, { id: started.id }, { effectiveTo: now })\n\n return { ...started, effectiveTo: now } as WorkShiftRevision\n }\n\n const current = await manager.find(WorkShift, { where: { domain: { id: domain.id } } })\n\n if (!current.length) {\n /* 첫 설치다. 지나간 편성이 없다. */\n return undefined\n }\n\n const before = shiftSetOf(current)\n\n if (sameShiftSet(before, next)) {\n /* 내용이 그대로다. 남기면 이력이 「바뀌지 않음」으로 채워져 읽을 수 없게 된다. */\n return undefined\n }\n\n return await manager.save(WorkShiftRevision, {\n domain,\n /* 언제부터 쓰던 것인가 — 모르면 그만둔 시각으로 둔다(길이 0인 구간). 지어내지 않는다. */\n effectiveFrom: inForceSince(current) ?? now,\n effectiveTo: now,\n shifts: before,\n creator: user\n } as WorkShiftRevision)\n}\n\n/** 이 도메인의 지나간 벌과 예고된 벌 전부. 고르는 일은 순수 함수가 한다. */\nasync function revisionsOf(manager: EntityManager, domain: Domain): Promise<WorkShiftRevision[]> {\n return await manager.find(WorkShiftRevision, {\n where: { domain: { id: domain.id } },\n order: { effectiveFrom: 'ASC' }\n })\n}\n\n/**\n * 그 시각에 쓰던 편성 — **지금 편성이 아닐 수 있다.**\n *\n * 지나간 시각이면 이력에서, 예고한 시각이 지났으면 예고분에서 고른다. 고르는 규칙은\n * `work-shift-selection.ts` 에 순수하게 있다.\n *\n * **이력보다 앞선 시각은 없다고 답한다.** 그때 무엇을 쓰고 있었는지 이 시스템은 모른다.\n */\nexport async function shiftsInForceAt(\n manager: EntityManager,\n domain: Domain,\n at: Date\n): Promise<{ shifts: Record<string, unknown>[]; origin: SetOrigin; from?: Date } | undefined> {\n const current = await manager.find(WorkShift, {\n where: { domain: { id: domain.id } },\n order: { fromDate: 'ASC', fromTime: 'ASC' }\n })\n\n const revisions = await revisionsOf(manager, domain)\n\n const selected = selectSetAt<Record<string, unknown>[]>(\n at,\n current.length ? shiftSetOf(current) : undefined,\n inForceSince(current),\n revisions.map(revision => ({\n effectiveFrom: revision.effectiveFrom,\n effectiveTo: revision.effectiveTo,\n shifts: revision.shifts\n }))\n )\n\n return selected.shifts ? { shifts: selected.shifts, origin: selected.origin, from: selected.from } : undefined\n}\n\n/**\n * 앞으로 쓸 편성을 미리 적어 둔다.\n *\n * ── 지난 시각으로는 예고하지 않는다 ────────────────────────────────────────\n * 지난 시각으로 넣으면 이미 적힌 사실들의 교대 이름이 통째로 바뀐다. 그것은 예고가 아니라 **기록을\n * 고치는 것**이고, 이 문으로 할 일이 아니다.\n *\n * ── 앞의 열린 벌을 닫는다 ──────────────────────────────────────────────────\n * 끝이 없는 벌이 둘이면 어느 것이 답인지 갈린다. 새 예고의 시작이 앞엣것의 끝이 된다.\n */\nexport async function announceShiftSet(\n manager: EntityManager,\n domain: Domain,\n effectiveFrom: Date,\n shifts: Record<string, unknown>[],\n user?: User,\n now = new Date()\n): Promise<WorkShiftRevision> {\n if (effectiveFrom <= now) {\n throw new Error('announceShiftSet: 예고는 앞으로의 시각으로만 한다 — 지난 시각으로 넣으면 이미 적힌 사실의 교대가 바뀐다')\n }\n\n if (!shifts.length) {\n throw new Error('announceShiftSet: 빈 편성은 예고할 수 없다 — 그 시각부터 교대가 없다는 뜻이 되어 버린다')\n }\n\n const open = await manager.findOne(WorkShiftRevision, {\n where: { domain: { id: domain.id }, effectiveTo: IsNull() },\n order: { effectiveFrom: 'DESC' }\n })\n\n if (open) {\n if (open.effectiveFrom >= effectiveFrom) {\n throw new Error(\n 'announceShiftSet: 이미 더 늦은 예고가 있다 — 그것을 먼저 물리고 다시 예고한다'\n )\n }\n\n /* 앞의 열린 벌은 새 예고가 시작하는 시각에 끝난다. */\n await manager.update(WorkShiftRevision, { id: open.id }, { effectiveTo: effectiveFrom })\n }\n\n return await manager.save(WorkShiftRevision, {\n domain,\n effectiveFrom,\n effectiveTo: null,\n shifts,\n creator: user\n } as unknown as WorkShiftRevision)\n}\n\n/**\n * 예고를 물린다.\n *\n * **아직 시작하지 않은 것만** 물릴 수 있다. 이미 적용 중인 벌을 지우면 그 사이에 적힌 사실들이 가리키던\n * 편성이 사라진다 — 그때는 지우는 것이 아니라 새로 예고하거나 지금 편성을 저장한다.\n */\nexport async function cancelAnnouncement(\n manager: EntityManager,\n domain: Domain,\n id: string,\n now = new Date()\n): Promise<boolean> {\n const found = await manager.findOne(WorkShiftRevision, { where: { id, domain: { id: domain.id } } })\n\n if (!found) {\n return false\n }\n\n if (found.effectiveFrom <= now) {\n throw new Error('cancelAnnouncement: 이미 적용된 편성은 물릴 수 없다 — 그 사이에 적힌 사실이 이것을 가리킨다')\n }\n\n await manager.delete(WorkShiftRevision, { id: found.id })\n\n /* 앞의 벌이 이 예고 때문에 닫혀 있었다면 다시 열어 준다 — 아니면 그 뒤가 빈 구간이 된다. */\n const previous = await manager.findOne(WorkShiftRevision, {\n where: { domain: { id: domain.id }, effectiveTo: found.effectiveFrom },\n order: { effectiveFrom: 'DESC' }\n })\n\n if (previous) {\n await manager.update(WorkShiftRevision, { id: previous.id }, { effectiveTo: null })\n }\n\n return true\n}\n\n/** 아직 시작하지 않은 벌들 — 화면이 「예고된 편성」으로 보여 줄 것. */\nexport async function announcementsOf(\n manager: EntityManager,\n domain: Domain,\n now = new Date()\n): Promise<WorkShiftRevision[]> {\n return announcedSets(now, await revisionsOf(manager, domain)) as WorkShiftRevision[]\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { User } from '@things-factory/auth-base';
|
|
3
|
+
/**
|
|
4
|
+
* 교대 편성 한 벌 — **지나간 것과 예고된 것.**
|
|
5
|
+
*
|
|
6
|
+
* ── 무엇이 사라지고 있었나 ─────────────────────────────────────────────────
|
|
7
|
+
* `updateMultipleWorkShift` 는 그 도메인의 교대를 **전부 지우고 다시 넣는다.** 오탈자 하나를 고쳐도
|
|
8
|
+
* 이전 편성이 데이터베이스에서 없어진다. 그래서 이런 물음에 답할 수 없었다.
|
|
9
|
+
*
|
|
10
|
+
* 6월 실적의 「주간」은 몇 시부터 몇 시였나
|
|
11
|
+
* 3교대로 바꾼 것이 언제부터인가
|
|
12
|
+
* 누가 언제 편성을 바꿨나
|
|
13
|
+
*
|
|
14
|
+
* ── 왜 유효기간 칸이 아니라 별도 표인가 ────────────────────────────────────
|
|
15
|
+
* `WorkShift` 에 `effectiveStart`/`effectiveEnd` 를 다는 길도 있었다. 그러려면 유니크 인덱스 둘
|
|
16
|
+
* (`[domain, name]` · `[domain, fromDate, fromTime]`)을 풀어야 한다 — 같은 이름이 기간별로 여러 행이어야
|
|
17
|
+
* 하기 때문이다. **그 인덱스는 지금 잘못된 편성을 막고 있는 가드**이고, 다섯 드라이버에서 그것을
|
|
18
|
+
* 풀면서 코드로 대신 지키는 것은 얻는 것보다 잃는 것이 크다.
|
|
19
|
+
*
|
|
20
|
+
* 별도 표는 `WorkShift` 를 **한 글자도 건드리지 않는다.** 지금 쓰는 편성은 그대로 그 표에 있고, 이
|
|
21
|
+
* 표에는 **지나간 것만** 쌓인다. 그래서 두 곳이 어긋날 수가 없다.
|
|
22
|
+
*
|
|
23
|
+
* ── 행이 아니라 벌이 단위다 ────────────────────────────────────────────────
|
|
24
|
+
* 교대는 한 줄씩 바뀌지 않는다. 「2교대에서 3교대로」처럼 **편성 전체가** 바뀐다. 그래서 이 표의 한 행이
|
|
25
|
+
* 그때 쓰던 편성 한 벌이다.
|
|
26
|
+
*
|
|
27
|
+
* ── 지금 쓰는 편성은 여기 없다 ─────────────────────────────────────────────
|
|
28
|
+
* 넣으면 같은 사실이 두 곳에 살고, 둘이 어긋나는 날이 온다. 지금 편성이 **언제부터**인지는 `WorkShift`
|
|
29
|
+
* 행들의 `createdAt` 중 가장 이른 것이 말해 준다 — 전량 삭제 후 재삽입이라 그 값이 곧 설치 시각이다.
|
|
30
|
+
*
|
|
31
|
+
* ── 앞으로 쓸 편성도 여기 든다 ─────────────────────────────────────────────
|
|
32
|
+
* 「다음 달 1일부터 3교대」를 미리 적어 둘 수 있어야 한다. 그러지 못하면 바뀌는 날 자정에 사람이 손으로
|
|
33
|
+
* 고쳐야 하고, 그 시각에 도는 창이 어긋난다.
|
|
34
|
+
*
|
|
35
|
+
* 예고된 벌은 `effectiveFrom` 이 미래이고 `effectiveTo` 가 비어 있다. **바뀌는 날 무엇을 옮기는 일이
|
|
36
|
+
* 없다** — 시각으로 고르기 때문에 그 시각이 지나면 저절로 그 벌이 답이 된다. 자정에 도는 일감을 두면
|
|
37
|
+
* 그날 서버가 내려가 있을 때 편성이 안 바뀌고, 아무도 그것을 모른다.
|
|
38
|
+
*
|
|
39
|
+
* ```
|
|
40
|
+
* 지나간 것 effectiveFrom ≤ effectiveTo ≤ 지금
|
|
41
|
+
* 예고된 것 effectiveFrom > 지금 · effectiveTo 없음
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare class WorkShiftRevision {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
domain?: Domain;
|
|
47
|
+
domainId?: string;
|
|
48
|
+
/** 이 편성을 쓰기 시작한 시각. */
|
|
49
|
+
effectiveFrom: Date;
|
|
50
|
+
/**
|
|
51
|
+
* 이 편성을 그만 쓴 시각. **예고된 벌은 비어 있다** — 아직 끝나지 않았다.
|
|
52
|
+
*
|
|
53
|
+
* 비어 있는 벌이 둘이면 어느 것이 답인지 갈리므로, 새 예고를 넣을 때 앞의 열린 벌을 닫는다.
|
|
54
|
+
*/
|
|
55
|
+
effectiveTo?: Date;
|
|
56
|
+
/**
|
|
57
|
+
* 그때 쓰던 교대 한 벌 — `[{ name, fromDate, fromTime, toDate, toTime }]`.
|
|
58
|
+
*
|
|
59
|
+
* `WorkShift` 를 가리키는 참조로 두지 않는다. 그 행들은 이미 지워졌고, 지워진 것을 가리키는 참조는
|
|
60
|
+
* 아무것도 답하지 못한다.
|
|
61
|
+
*/
|
|
62
|
+
shifts: Record<string, unknown>[];
|
|
63
|
+
createdAt?: Date;
|
|
64
|
+
/** 누가 바꿨나. 앞서는 전량 삭제 때문에 이것도 매번 사라졌다. */
|
|
65
|
+
creator?: User;
|
|
66
|
+
creatorId?: string;
|
|
67
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkShiftRevision = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const type_graphql_1 = require("type-graphql");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
9
|
+
/**
|
|
10
|
+
* 교대 편성 한 벌 — **지나간 것과 예고된 것.**
|
|
11
|
+
*
|
|
12
|
+
* ── 무엇이 사라지고 있었나 ─────────────────────────────────────────────────
|
|
13
|
+
* `updateMultipleWorkShift` 는 그 도메인의 교대를 **전부 지우고 다시 넣는다.** 오탈자 하나를 고쳐도
|
|
14
|
+
* 이전 편성이 데이터베이스에서 없어진다. 그래서 이런 물음에 답할 수 없었다.
|
|
15
|
+
*
|
|
16
|
+
* 6월 실적의 「주간」은 몇 시부터 몇 시였나
|
|
17
|
+
* 3교대로 바꾼 것이 언제부터인가
|
|
18
|
+
* 누가 언제 편성을 바꿨나
|
|
19
|
+
*
|
|
20
|
+
* ── 왜 유효기간 칸이 아니라 별도 표인가 ────────────────────────────────────
|
|
21
|
+
* `WorkShift` 에 `effectiveStart`/`effectiveEnd` 를 다는 길도 있었다. 그러려면 유니크 인덱스 둘
|
|
22
|
+
* (`[domain, name]` · `[domain, fromDate, fromTime]`)을 풀어야 한다 — 같은 이름이 기간별로 여러 행이어야
|
|
23
|
+
* 하기 때문이다. **그 인덱스는 지금 잘못된 편성을 막고 있는 가드**이고, 다섯 드라이버에서 그것을
|
|
24
|
+
* 풀면서 코드로 대신 지키는 것은 얻는 것보다 잃는 것이 크다.
|
|
25
|
+
*
|
|
26
|
+
* 별도 표는 `WorkShift` 를 **한 글자도 건드리지 않는다.** 지금 쓰는 편성은 그대로 그 표에 있고, 이
|
|
27
|
+
* 표에는 **지나간 것만** 쌓인다. 그래서 두 곳이 어긋날 수가 없다.
|
|
28
|
+
*
|
|
29
|
+
* ── 행이 아니라 벌이 단위다 ────────────────────────────────────────────────
|
|
30
|
+
* 교대는 한 줄씩 바뀌지 않는다. 「2교대에서 3교대로」처럼 **편성 전체가** 바뀐다. 그래서 이 표의 한 행이
|
|
31
|
+
* 그때 쓰던 편성 한 벌이다.
|
|
32
|
+
*
|
|
33
|
+
* ── 지금 쓰는 편성은 여기 없다 ─────────────────────────────────────────────
|
|
34
|
+
* 넣으면 같은 사실이 두 곳에 살고, 둘이 어긋나는 날이 온다. 지금 편성이 **언제부터**인지는 `WorkShift`
|
|
35
|
+
* 행들의 `createdAt` 중 가장 이른 것이 말해 준다 — 전량 삭제 후 재삽입이라 그 값이 곧 설치 시각이다.
|
|
36
|
+
*
|
|
37
|
+
* ── 앞으로 쓸 편성도 여기 든다 ─────────────────────────────────────────────
|
|
38
|
+
* 「다음 달 1일부터 3교대」를 미리 적어 둘 수 있어야 한다. 그러지 못하면 바뀌는 날 자정에 사람이 손으로
|
|
39
|
+
* 고쳐야 하고, 그 시각에 도는 창이 어긋난다.
|
|
40
|
+
*
|
|
41
|
+
* 예고된 벌은 `effectiveFrom` 이 미래이고 `effectiveTo` 가 비어 있다. **바뀌는 날 무엇을 옮기는 일이
|
|
42
|
+
* 없다** — 시각으로 고르기 때문에 그 시각이 지나면 저절로 그 벌이 답이 된다. 자정에 도는 일감을 두면
|
|
43
|
+
* 그날 서버가 내려가 있을 때 편성이 안 바뀌고, 아무도 그것을 모른다.
|
|
44
|
+
*
|
|
45
|
+
* ```
|
|
46
|
+
* 지나간 것 effectiveFrom ≤ effectiveTo ≤ 지금
|
|
47
|
+
* 예고된 것 effectiveFrom > 지금 · effectiveTo 없음
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
let WorkShiftRevision = class WorkShiftRevision {
|
|
51
|
+
};
|
|
52
|
+
exports.WorkShiftRevision = WorkShiftRevision;
|
|
53
|
+
tslib_1.__decorate([
|
|
54
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
55
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID),
|
|
56
|
+
tslib_1.__metadata("design:type", String)
|
|
57
|
+
], WorkShiftRevision.prototype, "id", void 0);
|
|
58
|
+
tslib_1.__decorate([
|
|
59
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain),
|
|
60
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain),
|
|
61
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
62
|
+
], WorkShiftRevision.prototype, "domain", void 0);
|
|
63
|
+
tslib_1.__decorate([
|
|
64
|
+
(0, typeorm_1.RelationId)((revision) => revision.domain),
|
|
65
|
+
tslib_1.__metadata("design:type", String)
|
|
66
|
+
], WorkShiftRevision.prototype, "domainId", void 0);
|
|
67
|
+
tslib_1.__decorate([
|
|
68
|
+
(0, typeorm_1.Column)(),
|
|
69
|
+
(0, type_graphql_1.Field)(),
|
|
70
|
+
tslib_1.__metadata("design:type", Date
|
|
71
|
+
/**
|
|
72
|
+
* 이 편성을 그만 쓴 시각. **예고된 벌은 비어 있다** — 아직 끝나지 않았다.
|
|
73
|
+
*
|
|
74
|
+
* 비어 있는 벌이 둘이면 어느 것이 답인지 갈리므로, 새 예고를 넣을 때 앞의 열린 벌을 닫는다.
|
|
75
|
+
*/
|
|
76
|
+
)
|
|
77
|
+
], WorkShiftRevision.prototype, "effectiveFrom", void 0);
|
|
78
|
+
tslib_1.__decorate([
|
|
79
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
80
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
81
|
+
tslib_1.__metadata("design:type", Date
|
|
82
|
+
/**
|
|
83
|
+
* 그때 쓰던 교대 한 벌 — `[{ name, fromDate, fromTime, toDate, toTime }]`.
|
|
84
|
+
*
|
|
85
|
+
* `WorkShift` 를 가리키는 참조로 두지 않는다. 그 행들은 이미 지워졌고, 지워진 것을 가리키는 참조는
|
|
86
|
+
* 아무것도 답하지 못한다.
|
|
87
|
+
*/
|
|
88
|
+
)
|
|
89
|
+
], WorkShiftRevision.prototype, "effectiveTo", void 0);
|
|
90
|
+
tslib_1.__decorate([
|
|
91
|
+
(0, typeorm_1.Column)({ type: 'simple-json' }),
|
|
92
|
+
(0, type_graphql_1.Field)(type => String, { description: 'the shift set that was in force, as JSON text' }),
|
|
93
|
+
tslib_1.__metadata("design:type", Array)
|
|
94
|
+
], WorkShiftRevision.prototype, "shifts", void 0);
|
|
95
|
+
tslib_1.__decorate([
|
|
96
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
97
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
98
|
+
tslib_1.__metadata("design:type", Date
|
|
99
|
+
/** 누가 바꿨나. 앞서는 전량 삭제 때문에 이것도 매번 사라졌다. */
|
|
100
|
+
)
|
|
101
|
+
], WorkShiftRevision.prototype, "createdAt", void 0);
|
|
102
|
+
tslib_1.__decorate([
|
|
103
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
104
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
105
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
106
|
+
], WorkShiftRevision.prototype, "creator", void 0);
|
|
107
|
+
tslib_1.__decorate([
|
|
108
|
+
(0, typeorm_1.RelationId)((revision) => revision.creator),
|
|
109
|
+
tslib_1.__metadata("design:type", String)
|
|
110
|
+
], WorkShiftRevision.prototype, "creatorId", void 0);
|
|
111
|
+
exports.WorkShiftRevision = WorkShiftRevision = tslib_1.__decorate([
|
|
112
|
+
(0, typeorm_1.Entity)(),
|
|
113
|
+
(0, typeorm_1.Index)('ix_work_shift_revision_0', (revision) => [revision.domain, revision.effectiveFrom]),
|
|
114
|
+
(0, type_graphql_1.ObjectType)({ description: 'A work shift arrangement that used to be in force' })
|
|
115
|
+
], WorkShiftRevision);
|
|
116
|
+
//# sourceMappingURL=work-shift-revision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-shift-revision.js","sourceRoot":"","sources":["../../../server/service/work-shift/work-shift-revision.ts"],"names":[],"mappings":";;;;AAAA,qCAAgH;AAChH,+CAAoD;AAEpD,iDAA8C;AAC9C,yDAAgD;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAII,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;CA+C7B,CAAA;AA/CY,8CAAiB;AAGnB;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,CAAC;;6CACC;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;sCACb,cAAM;iDAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,QAA2B,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;;mDAC5C;AAKjB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,GAAE;sCACO,IAAI;IAEnB;;;;OAIG;;wDANgB;AASnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACZ,IAAI;IAElB;;;;;OAKG;;sDAPe;AAUlB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IAC/B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,+CAA+C,EAAE,CAAC;;iDACvD;AAIjC;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;IAEhB,yCAAyC;;oDAFzB;AAKhB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCAC9B,gBAAI;kDAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,QAA2B,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;;oDAC5C;4BA9CP,iBAAiB;IAH7B,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,0BAA0B,EAAE,CAAC,QAA2B,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC7G,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;GACpE,iBAAiB,CA+C7B","sourcesContent":["import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 교대 편성 한 벌 — **지나간 것과 예고된 것.**\n *\n * ── 무엇이 사라지고 있었나 ─────────────────────────────────────────────────\n * `updateMultipleWorkShift` 는 그 도메인의 교대를 **전부 지우고 다시 넣는다.** 오탈자 하나를 고쳐도\n * 이전 편성이 데이터베이스에서 없어진다. 그래서 이런 물음에 답할 수 없었다.\n *\n * 6월 실적의 「주간」은 몇 시부터 몇 시였나\n * 3교대로 바꾼 것이 언제부터인가\n * 누가 언제 편성을 바꿨나\n *\n * ── 왜 유효기간 칸이 아니라 별도 표인가 ────────────────────────────────────\n * `WorkShift` 에 `effectiveStart`/`effectiveEnd` 를 다는 길도 있었다. 그러려면 유니크 인덱스 둘\n * (`[domain, name]` · `[domain, fromDate, fromTime]`)을 풀어야 한다 — 같은 이름이 기간별로 여러 행이어야\n * 하기 때문이다. **그 인덱스는 지금 잘못된 편성을 막고 있는 가드**이고, 다섯 드라이버에서 그것을\n * 풀면서 코드로 대신 지키는 것은 얻는 것보다 잃는 것이 크다.\n *\n * 별도 표는 `WorkShift` 를 **한 글자도 건드리지 않는다.** 지금 쓰는 편성은 그대로 그 표에 있고, 이\n * 표에는 **지나간 것만** 쌓인다. 그래서 두 곳이 어긋날 수가 없다.\n *\n * ── 행이 아니라 벌이 단위다 ────────────────────────────────────────────────\n * 교대는 한 줄씩 바뀌지 않는다. 「2교대에서 3교대로」처럼 **편성 전체가** 바뀐다. 그래서 이 표의 한 행이\n * 그때 쓰던 편성 한 벌이다.\n *\n * ── 지금 쓰는 편성은 여기 없다 ─────────────────────────────────────────────\n * 넣으면 같은 사실이 두 곳에 살고, 둘이 어긋나는 날이 온다. 지금 편성이 **언제부터**인지는 `WorkShift`\n * 행들의 `createdAt` 중 가장 이른 것이 말해 준다 — 전량 삭제 후 재삽입이라 그 값이 곧 설치 시각이다.\n *\n * ── 앞으로 쓸 편성도 여기 든다 ─────────────────────────────────────────────\n * 「다음 달 1일부터 3교대」를 미리 적어 둘 수 있어야 한다. 그러지 못하면 바뀌는 날 자정에 사람이 손으로\n * 고쳐야 하고, 그 시각에 도는 창이 어긋난다.\n *\n * 예고된 벌은 `effectiveFrom` 이 미래이고 `effectiveTo` 가 비어 있다. **바뀌는 날 무엇을 옮기는 일이\n * 없다** — 시각으로 고르기 때문에 그 시각이 지나면 저절로 그 벌이 답이 된다. 자정에 도는 일감을 두면\n * 그날 서버가 내려가 있을 때 편성이 안 바뀌고, 아무도 그것을 모른다.\n *\n * ```\n * 지나간 것 effectiveFrom ≤ effectiveTo ≤ 지금\n * 예고된 것 effectiveFrom > 지금 · effectiveTo 없음\n * ```\n */\n@Entity()\n@Index('ix_work_shift_revision_0', (revision: WorkShiftRevision) => [revision.domain, revision.effectiveFrom])\n@ObjectType({ description: 'A work shift arrangement that used to be in force' })\nexport class WorkShiftRevision {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((revision: WorkShiftRevision) => revision.domain)\n domainId?: string\n\n /** 이 편성을 쓰기 시작한 시각. */\n @Column()\n @Field()\n effectiveFrom: Date\n\n /**\n * 이 편성을 그만 쓴 시각. **예고된 벌은 비어 있다** — 아직 끝나지 않았다.\n *\n * 비어 있는 벌이 둘이면 어느 것이 답인지 갈리므로, 새 예고를 넣을 때 앞의 열린 벌을 닫는다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true })\n effectiveTo?: Date\n\n /**\n * 그때 쓰던 교대 한 벌 — `[{ name, fromDate, fromTime, toDate, toTime }]`.\n *\n * `WorkShift` 를 가리키는 참조로 두지 않는다. 그 행들은 이미 지워졌고, 지워진 것을 가리키는 참조는\n * 아무것도 답하지 못한다.\n */\n @Column({ type: 'simple-json' })\n @Field(type => String, { description: 'the shift set that was in force, as JSON text' })\n shifts: Record<string, unknown>[]\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n /** 누가 바꿨나. 앞서는 전량 삭제 때문에 이것도 매번 사라졌다. */\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((revision: WorkShiftRevision) => revision.creator)\n creatorId?: string\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 어느 벌이 그 시각의 답인가 — **순수 계산.** 데이터베이스를 만지지 않는다.
|
|
3
|
+
*
|
|
4
|
+
* ── 세 곳에 벌이 있다 ──────────────────────────────────────────────────────
|
|
5
|
+
* ```
|
|
6
|
+
* 지나간 것 revision effectiveFrom ≤ effectiveTo ≤ 지금
|
|
7
|
+
* 지금 것 WorkShift 가장 이른 createdAt 부터
|
|
8
|
+
* 예고된 것 revision effectiveFrom 이 미래 · effectiveTo 없음
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* ── 예고를 옮기지 않는다 ───────────────────────────────────────────────────
|
|
12
|
+
* 예고한 시각이 되면 **저절로 그 벌이 답이 된다.** 자정에 도는 일감이 편성을 옮기게 만들면, 그날 서버가
|
|
13
|
+
* 내려가 있는 경우 편성이 안 바뀌고 아무도 그것을 모른다. 시각으로 고르면 그런 창이 아예 없다.
|
|
14
|
+
*
|
|
15
|
+
* ── 늦게 시작한 벌이 이긴다 ────────────────────────────────────────────────
|
|
16
|
+
* 「10월 1일부터」와 「12월 1일부터」가 둘 다 있고 12월 5일을 물으면 뒤엣것이 답이다. 그래서 시작 시각이
|
|
17
|
+
* 늦은 것부터 본다.
|
|
18
|
+
*
|
|
19
|
+
* ── 모르는 것은 모른다고 한다 ──────────────────────────────────────────────
|
|
20
|
+
* 이력보다 앞선 시각은 이 시스템이 무엇을 쓰고 있었는지 모른다. 지금 편성으로 대신하면 그 시각의 사실이
|
|
21
|
+
* 있지도 않았던 교대 이름으로 읽힌다.
|
|
22
|
+
*/
|
|
23
|
+
export interface RevisionLike<T = unknown> {
|
|
24
|
+
effectiveFrom: Date;
|
|
25
|
+
/** 없으면 아직 끝나지 않은 벌이다(예고). */
|
|
26
|
+
effectiveTo?: Date | null;
|
|
27
|
+
shifts: T;
|
|
28
|
+
}
|
|
29
|
+
export type SetOrigin = 'current' | 'past' | 'announced' | 'unknown';
|
|
30
|
+
export interface SelectedSet<T = unknown> {
|
|
31
|
+
shifts: T | undefined;
|
|
32
|
+
origin: SetOrigin;
|
|
33
|
+
/** 고른 벌이 언제부터인가. 화면이 「예고분이 적용 중입니다」를 말하는 근거. */
|
|
34
|
+
from?: Date;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 그 시각의 편성을 고른다.
|
|
38
|
+
*
|
|
39
|
+
* @param at 물어보는 시각
|
|
40
|
+
* @param currentShifts 지금 편집 중인 편성(`WorkShift`). 없으면 `undefined`
|
|
41
|
+
* @param currentSince 그 편성이 언제부터인가. 모르면 `undefined`
|
|
42
|
+
* @param revisions 지나간 것과 예고된 것 전부
|
|
43
|
+
*/
|
|
44
|
+
export declare function selectSetAt<T>(at: Date, currentShifts: T | undefined, currentSince: Date | undefined, revisions: RevisionLike<T>[]): SelectedSet<T>;
|
|
45
|
+
/**
|
|
46
|
+
* 아직 시작하지 않은 벌들 — 화면이 「예고된 편성」으로 보여 줄 것.
|
|
47
|
+
*
|
|
48
|
+
* 시작이 이른 것부터. 사람이 「다음에 무엇이 오나」를 읽는 차례다.
|
|
49
|
+
*/
|
|
50
|
+
export declare function announcedSets<T>(now: Date, revisions: RevisionLike<T>[]): RevisionLike<T>[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 어느 벌이 그 시각의 답인가 — **순수 계산.** 데이터베이스를 만지지 않는다.
|
|
4
|
+
*
|
|
5
|
+
* ── 세 곳에 벌이 있다 ──────────────────────────────────────────────────────
|
|
6
|
+
* ```
|
|
7
|
+
* 지나간 것 revision effectiveFrom ≤ effectiveTo ≤ 지금
|
|
8
|
+
* 지금 것 WorkShift 가장 이른 createdAt 부터
|
|
9
|
+
* 예고된 것 revision effectiveFrom 이 미래 · effectiveTo 없음
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* ── 예고를 옮기지 않는다 ───────────────────────────────────────────────────
|
|
13
|
+
* 예고한 시각이 되면 **저절로 그 벌이 답이 된다.** 자정에 도는 일감이 편성을 옮기게 만들면, 그날 서버가
|
|
14
|
+
* 내려가 있는 경우 편성이 안 바뀌고 아무도 그것을 모른다. 시각으로 고르면 그런 창이 아예 없다.
|
|
15
|
+
*
|
|
16
|
+
* ── 늦게 시작한 벌이 이긴다 ────────────────────────────────────────────────
|
|
17
|
+
* 「10월 1일부터」와 「12월 1일부터」가 둘 다 있고 12월 5일을 물으면 뒤엣것이 답이다. 그래서 시작 시각이
|
|
18
|
+
* 늦은 것부터 본다.
|
|
19
|
+
*
|
|
20
|
+
* ── 모르는 것은 모른다고 한다 ──────────────────────────────────────────────
|
|
21
|
+
* 이력보다 앞선 시각은 이 시스템이 무엇을 쓰고 있었는지 모른다. 지금 편성으로 대신하면 그 시각의 사실이
|
|
22
|
+
* 있지도 않았던 교대 이름으로 읽힌다.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.selectSetAt = selectSetAt;
|
|
26
|
+
exports.announcedSets = announcedSets;
|
|
27
|
+
/**
|
|
28
|
+
* 그 시각의 편성을 고른다.
|
|
29
|
+
*
|
|
30
|
+
* @param at 물어보는 시각
|
|
31
|
+
* @param currentShifts 지금 편집 중인 편성(`WorkShift`). 없으면 `undefined`
|
|
32
|
+
* @param currentSince 그 편성이 언제부터인가. 모르면 `undefined`
|
|
33
|
+
* @param revisions 지나간 것과 예고된 것 전부
|
|
34
|
+
*/
|
|
35
|
+
function selectSetAt(at, currentShifts, currentSince, revisions) {
|
|
36
|
+
/* 시작한 벌만 후보다. 시작 시각이 늦은 것부터 본다 — 늦게 시작한 벌이 이긴다. */
|
|
37
|
+
const started = revisions
|
|
38
|
+
.filter(revision => revision.effectiveFrom <= at)
|
|
39
|
+
.sort((a, b) => b.effectiveFrom.getTime() - a.effectiveFrom.getTime());
|
|
40
|
+
const covering = started.find(revision => !revision.effectiveTo || revision.effectiveTo > at);
|
|
41
|
+
if (covering) {
|
|
42
|
+
return {
|
|
43
|
+
shifts: covering.shifts,
|
|
44
|
+
/* 끝이 없는 벌이 지금을 덮고 있으면 그것은 예고했던 것이 적용 중인 것이다. */
|
|
45
|
+
origin: covering.effectiveTo ? 'past' : 'announced',
|
|
46
|
+
from: covering.effectiveFrom
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (currentShifts !== undefined && (!currentSince || at >= currentSince)) {
|
|
50
|
+
return { shifts: currentShifts, origin: 'current', from: currentSince };
|
|
51
|
+
}
|
|
52
|
+
/* 이력보다 앞선 시각이다. 무엇을 쓰고 있었는지 모른다. */
|
|
53
|
+
return { shifts: undefined, origin: 'unknown' };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 아직 시작하지 않은 벌들 — 화면이 「예고된 편성」으로 보여 줄 것.
|
|
57
|
+
*
|
|
58
|
+
* 시작이 이른 것부터. 사람이 「다음에 무엇이 오나」를 읽는 차례다.
|
|
59
|
+
*/
|
|
60
|
+
function announcedSets(now, revisions) {
|
|
61
|
+
return revisions
|
|
62
|
+
.filter(revision => revision.effectiveFrom > now)
|
|
63
|
+
.sort((a, b) => a.effectiveFrom.getTime() - b.effectiveFrom.getTime());
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=work-shift-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-shift-selection.js","sourceRoot":"","sources":["../../../server/service/work-shift/work-shift-selection.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;AA0BH,kCA4BC;AAOD,sCAIC;AA/CD;;;;;;;GAOG;AACH,SAAgB,WAAW,CACzB,EAAQ,EACR,aAA4B,EAC5B,YAA8B,EAC9B,SAA4B;IAE5B,mDAAmD;IACnD,MAAM,OAAO,GAAG,SAAS;SACtB,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,aAAa,IAAI,EAAE,CAAC;SAChD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAA;IAExE,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW,GAAG,EAAE,CAAC,CAAA;IAE7F,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,gDAAgD;YAChD,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW;YACnD,IAAI,EAAE,QAAQ,CAAC,aAAa;SAC7B,CAAA;IACH,CAAC;IAED,IAAI,aAAa,KAAK,SAAS,IAAI,CAAC,CAAC,YAAY,IAAI,EAAE,IAAI,YAAY,CAAC,EAAE,CAAC;QACzE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,CAAA;IACzE,CAAC;IAED,oCAAoC;IACpC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;AACjD,CAAC;AAED;;;;GAIG;AACH,SAAgB,aAAa,CAAI,GAAS,EAAE,SAA4B;IACtE,OAAO,SAAS;SACb,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,aAAa,GAAG,GAAG,CAAC;SAChD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAA;AAC1E,CAAC","sourcesContent":["/**\n * 어느 벌이 그 시각의 답인가 — **순수 계산.** 데이터베이스를 만지지 않는다.\n *\n * ── 세 곳에 벌이 있다 ──────────────────────────────────────────────────────\n * ```\n * 지나간 것 revision effectiveFrom ≤ effectiveTo ≤ 지금\n * 지금 것 WorkShift 가장 이른 createdAt 부터\n * 예고된 것 revision effectiveFrom 이 미래 · effectiveTo 없음\n * ```\n *\n * ── 예고를 옮기지 않는다 ───────────────────────────────────────────────────\n * 예고한 시각이 되면 **저절로 그 벌이 답이 된다.** 자정에 도는 일감이 편성을 옮기게 만들면, 그날 서버가\n * 내려가 있는 경우 편성이 안 바뀌고 아무도 그것을 모른다. 시각으로 고르면 그런 창이 아예 없다.\n *\n * ── 늦게 시작한 벌이 이긴다 ────────────────────────────────────────────────\n * 「10월 1일부터」와 「12월 1일부터」가 둘 다 있고 12월 5일을 물으면 뒤엣것이 답이다. 그래서 시작 시각이\n * 늦은 것부터 본다.\n *\n * ── 모르는 것은 모른다고 한다 ──────────────────────────────────────────────\n * 이력보다 앞선 시각은 이 시스템이 무엇을 쓰고 있었는지 모른다. 지금 편성으로 대신하면 그 시각의 사실이\n * 있지도 않았던 교대 이름으로 읽힌다.\n */\n\nexport interface RevisionLike<T = unknown> {\n effectiveFrom: Date\n /** 없으면 아직 끝나지 않은 벌이다(예고). */\n effectiveTo?: Date | null\n shifts: T\n}\n\nexport type SetOrigin = 'current' | 'past' | 'announced' | 'unknown'\n\nexport interface SelectedSet<T = unknown> {\n shifts: T | undefined\n origin: SetOrigin\n /** 고른 벌이 언제부터인가. 화면이 「예고분이 적용 중입니다」를 말하는 근거. */\n from?: Date\n}\n\n/**\n * 그 시각의 편성을 고른다.\n *\n * @param at 물어보는 시각\n * @param currentShifts 지금 편집 중인 편성(`WorkShift`). 없으면 `undefined`\n * @param currentSince 그 편성이 언제부터인가. 모르면 `undefined`\n * @param revisions 지나간 것과 예고된 것 전부\n */\nexport function selectSetAt<T>(\n at: Date,\n currentShifts: T | undefined,\n currentSince: Date | undefined,\n revisions: RevisionLike<T>[]\n): SelectedSet<T> {\n /* 시작한 벌만 후보다. 시작 시각이 늦은 것부터 본다 — 늦게 시작한 벌이 이긴다. */\n const started = revisions\n .filter(revision => revision.effectiveFrom <= at)\n .sort((a, b) => b.effectiveFrom.getTime() - a.effectiveFrom.getTime())\n\n const covering = started.find(revision => !revision.effectiveTo || revision.effectiveTo > at)\n\n if (covering) {\n return {\n shifts: covering.shifts,\n /* 끝이 없는 벌이 지금을 덮고 있으면 그것은 예고했던 것이 적용 중인 것이다. */\n origin: covering.effectiveTo ? 'past' : 'announced',\n from: covering.effectiveFrom\n }\n }\n\n if (currentShifts !== undefined && (!currentSince || at >= currentSince)) {\n return { shifts: currentShifts, origin: 'current', from: currentSince }\n }\n\n /* 이력보다 앞선 시각이다. 무엇을 쓰고 있었는지 모른다. */\n return { shifts: undefined, origin: 'unknown' }\n}\n\n/**\n * 아직 시작하지 않은 벌들 — 화면이 「예고된 편성」으로 보여 줄 것.\n *\n * 시작이 이른 것부터. 사람이 「다음에 무엇이 오나」를 읽는 차례다.\n */\nexport function announcedSets<T>(now: Date, revisions: RevisionLike<T>[]): RevisionLike<T>[] {\n return revisions\n .filter(revision => revision.effectiveFrom > now)\n .sort((a, b) => a.effectiveFrom.getTime() - b.effectiveFrom.getTime())\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WorkShift, WorkShiftDateType } from './work-shift';
|
|
2
|
+
import { WorkShiftRevision } from './work-shift-revision';
|
|
2
3
|
export declare class WorkShiftPatch {
|
|
3
4
|
name: string;
|
|
4
5
|
description?: string;
|
|
@@ -11,6 +12,10 @@ export declare class WorkShiftList {
|
|
|
11
12
|
items: WorkShift[];
|
|
12
13
|
total: number;
|
|
13
14
|
}
|
|
15
|
+
export declare class WorkShiftRevisionList {
|
|
16
|
+
items: WorkShiftRevision[];
|
|
17
|
+
total: number;
|
|
18
|
+
}
|
|
14
19
|
export declare class WorkShiftInfo {
|
|
15
20
|
workDate?: string;
|
|
16
21
|
workShift?: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WorkShiftInfo = exports.WorkShiftList = exports.WorkShiftPatch = void 0;
|
|
3
|
+
exports.WorkShiftInfo = exports.WorkShiftRevisionList = exports.WorkShiftList = exports.WorkShiftPatch = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const type_graphql_1 = require("type-graphql");
|
|
6
6
|
const work_shift_1 = require("./work-shift");
|
|
7
|
+
const work_shift_revision_1 = require("./work-shift-revision");
|
|
7
8
|
let WorkShiftPatch = class WorkShiftPatch {
|
|
8
9
|
};
|
|
9
10
|
exports.WorkShiftPatch = WorkShiftPatch;
|
|
@@ -48,6 +49,20 @@ tslib_1.__decorate([
|
|
|
48
49
|
exports.WorkShiftList = WorkShiftList = tslib_1.__decorate([
|
|
49
50
|
(0, type_graphql_1.ObjectType)()
|
|
50
51
|
], WorkShiftList);
|
|
52
|
+
let WorkShiftRevisionList = class WorkShiftRevisionList {
|
|
53
|
+
};
|
|
54
|
+
exports.WorkShiftRevisionList = WorkShiftRevisionList;
|
|
55
|
+
tslib_1.__decorate([
|
|
56
|
+
(0, type_graphql_1.Field)(type => [work_shift_revision_1.WorkShiftRevision]),
|
|
57
|
+
tslib_1.__metadata("design:type", Array)
|
|
58
|
+
], WorkShiftRevisionList.prototype, "items", void 0);
|
|
59
|
+
tslib_1.__decorate([
|
|
60
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int),
|
|
61
|
+
tslib_1.__metadata("design:type", Number)
|
|
62
|
+
], WorkShiftRevisionList.prototype, "total", void 0);
|
|
63
|
+
exports.WorkShiftRevisionList = WorkShiftRevisionList = tslib_1.__decorate([
|
|
64
|
+
(0, type_graphql_1.ObjectType)()
|
|
65
|
+
], WorkShiftRevisionList);
|
|
51
66
|
let WorkShiftInfo = class WorkShiftInfo {
|
|
52
67
|
};
|
|
53
68
|
exports.WorkShiftInfo = WorkShiftInfo;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work-shift-type.js","sourceRoot":"","sources":["../../../server/service/work-shift/work-shift-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAsF;AACtF,6CAA2D;
|
|
1
|
+
{"version":3,"file":"work-shift-type.js","sourceRoot":"","sources":["../../../server/service/work-shift/work-shift-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAsF;AACtF,6CAA2D;AAC3D,+DAAyD;AAGlD,IAAM,cAAc,GAApB,MAAM,cAAc;CAkB1B,CAAA;AAlBY,wCAAc;AAEzB;IADC,IAAA,oBAAK,GAAE;;4CACI;AAGZ;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;mDACN;AAGpB;IADC,IAAA,oBAAK,GAAE;;gDACmB;AAG3B;IADC,IAAA,oBAAK,GAAE;;gDACQ;AAGhB;IADC,IAAA,oBAAK,GAAE;;8CACiB;AAGzB;IADC,IAAA,oBAAK,GAAE;;8CACM;yBAjBH,cAAc;IAD1B,IAAA,wBAAS,GAAE;GACC,cAAc,CAkB1B;AAGM,IAAM,aAAa,GAAnB,MAAM,aAAa;CAMzB,CAAA;AANY,sCAAa;AAExB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,sBAAS,CAAC,CAAC;;4CACT;AAGlB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,CAAC;;4CACN;wBALF,aAAa;IADzB,IAAA,yBAAU,GAAE;GACA,aAAa,CAMzB;AAGM,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;CAMjC,CAAA;AANY,sDAAqB;AAEhC;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,uCAAiB,CAAC,CAAC;;oDACT;AAG1B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,CAAC;;oDACN;gCALF,qBAAqB;IADjC,IAAA,yBAAU,GAAE;GACA,qBAAqB,CAMjC;AAGM,IAAM,aAAa,GAAnB,MAAM,aAAa;CAYzB,CAAA;AAZY,sCAAa;AAExB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;+CACT;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;gDACR;AAGlB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;gDACxB;AAGlB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;iDACvB;wBAXR,aAAa;IADzB,IAAA,yBAAU,GAAE;GACA,aAAa,CAYzB","sourcesContent":["import { Field, ID, InputType, Int, ObjectType, registerEnumType } from 'type-graphql'\nimport { WorkShift, WorkShiftDateType } from './work-shift'\nimport { WorkShiftRevision } from './work-shift-revision'\n\n@InputType()\nexport class WorkShiftPatch {\n @Field()\n name: string\n\n @Field({ nullable: true })\n description?: string\n\n @Field()\n fromDate: WorkShiftDateType\n\n @Field()\n fromTime: string\n\n @Field()\n toDate: WorkShiftDateType\n\n @Field()\n toTime: string\n}\n\n@ObjectType()\nexport class WorkShiftList {\n @Field(type => [WorkShift])\n items: WorkShift[]\n\n @Field(type => Int)\n total: number\n}\n\n@ObjectType()\nexport class WorkShiftRevisionList {\n @Field(type => [WorkShiftRevision])\n items: WorkShiftRevision[]\n\n @Field(type => Int)\n total: number\n}\n\n@ObjectType()\nexport class WorkShiftInfo {\n @Field({ nullable: true })\n workDate?: string\n\n @Field({ nullable: true })\n workShift?: string\n\n @Field(type => [Date], { nullable: true })\n dateRange?: Date[]\n\n @Field(type => [Date], { nullable: true })\n shiftRange?: Date[]\n}\n"]}
|