Changes
7 changed files (+366/-160)
-
-
@@ -19,6 +19,7 @@ import { v9 } from "./migrations/v0009";import { v10 } from "./migrations/v0010"; import { v11 } from "./migrations/v0011"; import { v12 } from "./migrations/v0012"; import { v13 } from "./migrations/v0013"; import { automaticallyProvide } from "./yamori/worker/v1/paid_leave_provision";
-
@@ -34,10 +35,10 @@ return service(request, this[CONTEXT]);} } const migrations = [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12] as const; const migrations = [v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13] as const; export async function idbBackend(): Promise<IDBBackend> { const db = await openDB<YamoriDB>("yamori", 12, { const db = await openDB<YamoriDB>("yamori", 13, { async upgrade(db, oldVersion, _newVersion, transaction, _event) { for (let i = 0, l = migrations.length; i < l; i++) { const version = i + 1;
-
-
-
@@ -7,13 +7,16 @@ export const v11: Migration = async (_db, transaction) => {const store = transaction.objectStore("workRecords"); for await (const record of store.iterate()) { // @ts-expect-error - record プロパティは削除された。 switch (record.value.record?.type) { case "legal_leave": case "special_leave": store.put({ ...record.value, // @ts-expect-error - record プロパティは削除された。 record: { type: "workspace_defined_leave", // @ts-expect-error - record プロパティは削除された。 leave_id: record.value.record.leave_id, }, });
-
-
-
@@ -0,0 +1,109 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only // @ts-nocheck - `workRecords.record` というプロパティは現行のコードでは削除 // されているため、該当部分すべてがエラーとなる。 import type { Migration } from "./types"; export const v13: Migration = async (_db, transaction) => { const workRecords = transaction.objectStore("workRecords"); for await (const cursor of workRecords.iterate()) { const { value } = cursor; if (!value.record || value.kind) { continue; } switch (value.record.type) { case "working_day": { const halvedPL = value.record.timeOffs.find( (t) => t.type === "halved_paid_leave", ); const hourlyPL = value.record.timeOffs.find( (t) => t.type === "hourly_paid_leave", ); if (halvedPL) { await cursor.update({ ...value, record: undefined, kind: { // データとして午前・午後が残っていないのでとりあえず午後に有給休暇を設定する type: "day_halved", am: { type: value.record.hasWorked ? "worked" : "skipped", hourlyPaidLeave: hourlyPL && { hours: hourlyPL.hours, providedAtPacked: hourlyPL.providedAtPacked, }, hourlyWorkspaceDefinedLeaves: [], }, pm: { type: "paid_leave", providedAtPacked: halvedPL.providedAtPacked, }, }, }); } else { await cursor.update({ ...value, record: undefined, kind: { type: "day_whole", data: { type: value.record.hasWorked ? "worked" : "skipped", hourlyPaidLeave: hourlyPL && { hours: hourlyPL.hours, providedAtPacked: hourlyPL.providedAtPacked, }, hourlyWorkspaceDefinedLeaves: [], }, }, }); } break; } case "day_off": { await cursor.update({ ...value, record: undefined, kind: { type: "day_whole", data: { type: "day_off", }, }, }); break; } case "paid_leave": { await cursor.update({ ...value, record: undefined, kind: { type: "day_whole", data: { type: "paid_leave", providedAtPacked: value.record.providedAtPacked, }, }, }); break; } case "workspace_defined_leave": { await cursor.update({ ...value, record: undefined, kind: { type: "day_whole", data: { type: "workspace_defined_leave", leaveId: value.record.leave_id, }, }, }); } } } };
-
-
-
@@ -5,6 +5,41 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport { type DBSchema, type IDBPDatabase } from "idb"; type RecordKind = | { type: "worked"; hourlyPaidLeave?: { hours: number; providedAtPacked?: number; }; hourlyWorkspaceDefinedLeaves: { hours: number; leaveId: string; }[]; } | { type: "skipped"; hourlyPaidLeave?: { hours: number; providedAtPacked?: number; }; hourlyWorkspaceDefinedLeaves: { hours: number; leaveId: string; }[]; } | { type: "day_off"; } | { type: "paid_leave"; providedAtPacked?: number; } | { type: "workspace_defined_leave"; leaveId: string; }; export interface YamoriDB extends DBSchema { workspaces: { key: string;
-
@@ -71,46 +106,15 @@ workerId: string;workspaceId: string; datePacked: number; note?: string; record?: kind?: | { type: "working_day"; hasWorked: boolean; timeOffs: ( | { type: "halved_paid_leave"; providedAtPacked?: number; } | { type: "hourly_paid_leave"; hours: number; providedAtPacked?: number; } )[]; type: "day_whole"; data: RecordKind; } | { type: "day_off"; } | { type: "paid_leave"; providedAtPacked?: number; } | { /** * @deprecated */ type: "legal_leave"; leave_id: string; } | { /** * @deprecated */ type: "special_leave"; leave_id: string; } | { type: "workspace_defined_leave"; leave_id: string; type: "day_halved"; am: RecordKind; pm: RecordKind; }; }; indexes: {
-
-
-
@@ -23,10 +23,12 @@ year: 2020,month: 1, day: 1, }), record: { type: "working_day", hasWorked: true, timeOffs: [], kind: { type: "day_whole", data: { type: "worked", hourlyWorkspaceDefinedLeaves: [], }, }, }, {
-
-
-
@@ -8,7 +8,6 @@ import { WorkRecordBatchWriteInputSchema } from "@yamori/proto/yamori/work_record/v1/work_record_batch_write_input_pb.js";import { WorkRecordBatchWriteMaskSchema } from "@yamori/proto/yamori/work_record/v1/work_record_batch_write_mask_pb.js"; import { WorkRecordReadMaskSchema } from "@yamori/proto/yamori/work_record/v1/work_record_read_mask_pb.js"; import { WorkRecordFilterSchema } from "@yamori/proto/yamori/work_record/v1/work_record_filter_pb.js"; import { TimeOffSchema } from "@yamori/proto/yamori/work_record/v1/time_off_pb.js"; import { type IDBPObjectStore, type StoreNames } from "idb"; import { packDate, unpackDate } from "../../../helpers";
-
@@ -68,11 +67,17 @@ value.note = message.note;break; case WorkRecordBatchWriteInputSchema.field.dayOff.number: if (message.record.case === "dayOff") { value.record = { type: "day_off", value.kind = { type: "day_whole", data: { type: "day_off", }, }; } else if (value.record?.type === "day_off") { value.record = undefined; } else if ( value.kind?.type === "day_whole" && value.kind.data.type === "day_off" ) { value.kind = undefined; } break; case WorkRecordBatchWriteInputSchema.field.workingDay.number:
-
@@ -81,63 +86,87 @@ if (!paidLeaveProvisions && message.record.value.timeOffs.length > 0) {paidLeaveProvisions = await getPaidLeaveProvisions(); } value.record = { type: "working_day", hasWorked: message.record.value.hasWorkerWorked, timeOffs: await Promise.all( message.record.value.timeOffs.map< Promise< Extract< YamoriDB["workRecords"]["value"]["record"], { type: "working_day" } >["timeOffs"][number] > >(async (timeOff) => { const providedAt = timeOff.kind.value?.providedAt && packDate(timeOff.kind.value.providedAt); const provision = providedAt ? paidLeaveProvisions?.find( (provision) => provision.providedAtPacked === providedAt, ) : paidLeaveProvisions ?.filter( (provision) => provision.providedAtPacked <= value.datePacked && provision.expiresAtPacked > value.datePacked && provision.remainingDays > 0, ) .sort((a, b) => a.expiresAtPacked - b.expiresAtPacked)[0]; const timeOffs = message.record.value.timeOffs .map<{ kind: (typeof message.record.value.timeOffs)[number]["kind"]; provision: YamoriDB["paidLeaveProvision"]["value"]; } | null>((timeOff) => { if (!timeOff.kind.case) { return null; } if (!provision) { if (timeOff.kind.value?.providedAt) { throw new NoPaidLeaveProvidedAtError(timeOff.kind.value.providedAt); } const providedAtPacked = timeOff.kind.value.providedAt && packDate(timeOff.kind.value.providedAt); const provision = providedAtPacked ? paidLeaveProvisions?.find( (provision) => provision.providedAtPacked === providedAtPacked, ) : paidLeaveProvisions ?.filter( (provision) => provision.providedAtPacked <= value.datePacked && provision.expiresAtPacked > value.datePacked && provision.remainingDays > 0, ) .sort((a, b) => a.expiresAtPacked - b.expiresAtPacked)[0]; throw new NoPaidLeaveAvailableError(unpackDate(value.datePacked)); if (!provision) { if (timeOff.kind.value.providedAt) { throw new NoPaidLeaveProvidedAtError(timeOff.kind.value.providedAt); } switch (timeOff.kind.case) { case "hourlyPaidLeave": // TODO: 時間単位年休に対応 return { type: "hourly_paid_leave", hours: timeOff.kind.value.hours, providedAtPacked: provision.providedAtPacked, }; case "halvedPaidLeave": return { type: "halved_paid_leave", providedAtPacked: provision.providedAtPacked, }; default: throw new Error(`Unsupported time_off type: ${timeOff.kind.case}`); } }), ), throw new NoPaidLeaveAvailableError(unpackDate(value.datePacked)); } return { kind: timeOff.kind, provision }; }) .filter((t): t is NonNullable<typeof t> => !!t); const halvedPaidLeave = timeOffs.find( ({ kind }) => kind.case === "halvedPaidLeave", ); const hourlyPaidLeave = timeOffs.find( ({ kind }) => kind.case === "hourlyPaidLeave", ); const record: Extract< YamoriDB["workRecords"]["value"]["kind"], { type: "day_whole" } >["data"] = { type: message.record.value.hasWorkerWorked ? "worked" : "skipped", hourlyPaidLeave: hourlyPaidLeave && { hours: ( hourlyPaidLeave.kind as Extract< typeof hourlyPaidLeave.kind, { case: "hourlyPaidLeave" } > ).value.hours, providedAtPacked: hourlyPaidLeave.provision.providedAtPacked, }, hourlyWorkspaceDefinedLeaves: [], }; } else if (value.record?.type === "working_day") { value.record = undefined; value.kind = halvedPaidLeave ? { type: "day_halved", am: record, pm: { type: "paid_leave", providedAtPacked: halvedPaidLeave.provision.providedAtPacked, }, } : { type: "day_whole", data: record, }; } else if ( (value.kind?.type === "day_whole" && (value.kind.data.type === "worked" || value.kind.data.type === "skipped")) || (value.kind?.type === "day_halved" && (value.kind.am.type === "worked" || value.kind.pm.type === "skipped") && value.kind.pm.type === "paid_leave") ) { value.kind = undefined; } break; case WorkRecordBatchWriteInputSchema.field.paidLeave.number:
-
@@ -169,22 +198,34 @@throw new NoPaidLeaveAvailableError(unpackDate(value.datePacked)); } value.record = { type: "paid_leave", providedAtPacked: provision.providedAtPacked, value.kind = { type: "day_whole", data: { type: "paid_leave", providedAtPacked: provision.providedAtPacked, }, }; } else if (value.record?.type === "paid_leave") { value.record = undefined; } else if ( value.kind?.type === "day_whole" && value.kind.data.type === "paid_leave" ) { value.kind = undefined; } break; case WorkRecordBatchWriteInputSchema.field.workspaceDefinedLeaveId.number: if (message.record.case === "workspaceDefinedLeaveId") { value.record = { type: "workspace_defined_leave", leave_id: message.record.value.value, value.kind = { type: "day_whole", data: { type: "workspace_defined_leave", leaveId: message.record.value.value, }, }; } else if (value.record?.type === "workspace_defined_leave") { value.record = undefined; } else if ( value.kind?.type === "day_whole" && value.kind.data.type === "workspace_defined_leave" ) { value.kind = undefined; } break; }
-
@@ -251,7 +292,7 @@ case WorkRecordSchema.field.note.number:init.note = entry.note; break; case WorkRecordSchema.field.dayOff.number: if (entry.record?.type === "day_off") { if (entry.kind?.type === "day_whole" && entry.kind.data.type === "day_off") { init.record = { case: "dayOff", value: {},
-
@@ -259,64 +300,109 @@ };} break; case WorkRecordSchema.field.paidLeave.number: if (entry.record?.type === "paid_leave") { if (entry.kind?.type === "day_whole" && entry.kind.data.type === "paid_leave") { init.record = { case: "paidLeave", value: { providedAt: typeof entry.record.providedAtPacked === "number" ? unpackDate(entry.record.providedAtPacked) typeof entry.kind.data.providedAtPacked === "number" ? unpackDate(entry.kind.data.providedAtPacked) : undefined, }, }; } break; case WorkRecordSchema.field.workingDay.number: if (entry.record?.type === "working_day") { init.record = { case: "workingDay", value: { hasWorkerWorked: entry.record.hasWorked, timeOffs: entry.record.timeOffs .map<proto.MessageInitShape<typeof TimeOffSchema> | null>((timeOff) => { switch (timeOff.type) { case "hourly_paid_leave": return { if (entry.kind?.type === "day_whole") { if (entry.kind.data.type === "worked" || entry.kind.data.type === "skipped") { init.record = { case: "workingDay", value: { hasWorkerWorked: entry.kind.data.type === "worked", timeOffs: entry.kind.data.hourlyPaidLeave ? [ { kind: { case: "hourlyPaidLeave", value: { hours: timeOff.hours, hours: entry.kind.data.hourlyPaidLeave.hours, providedAt: typeof timeOff.providedAtPacked === "number" ? unpackDate(timeOff.providedAtPacked) typeof entry.kind.data.hourlyPaidLeave.providedAtPacked === "number" ? unpackDate( entry.kind.data.hourlyPaidLeave.providedAtPacked, ) : undefined, }, }, }; case "halved_paid_leave": return { }, ] : [], }, }; } } else if (entry.kind?.type === "day_halved") { if ( (entry.kind.am.type === "worked" || entry.kind.am.type === "skipped") && entry.kind.pm.type === "paid_leave" ) { init.record = { case: "workingDay", value: { hasWorkerWorked: entry.kind.am.type === "worked", timeOffs: entry.kind.am.hourlyPaidLeave ? [ { kind: { case: "halvedPaidLeave", value: { providedAt: typeof entry.kind.pm.providedAtPacked === "number" ? unpackDate(entry.kind.pm.providedAtPacked) : undefined, }, }, }, { kind: { case: "hourlyPaidLeave", value: { hours: entry.kind.am.hourlyPaidLeave.hours, providedAt: typeof entry.kind.am.hourlyPaidLeave.providedAtPacked === "number" ? unpackDate( entry.kind.am.hourlyPaidLeave.providedAtPacked, ) : undefined, }, }, }, ] : [ { kind: { case: "halvedPaidLeave", value: { providedAt: typeof timeOff.providedAtPacked === "number" ? unpackDate(timeOff.providedAtPacked) typeof entry.kind.pm.providedAtPacked === "number" ? unpackDate(entry.kind.pm.providedAtPacked) : undefined, }, }, }; default: return null; } }) .filter((t): t is NonNullable<typeof t> => !!t), }, }; }, ], }, }; } } break; case WorkRecordSchema.field.workspaceDefinedLeave.number: if (entry.record?.type === "workspace_defined_leave") { const id = entry.record.leave_id; if ( entry.kind?.type === "day_whole" && entry.kind.data.type === "workspace_defined_leave" ) { const id = entry.kind.data.leaveId; const found = workspace.leaveDefinitions.find((def) => def.id === id);
-
-
-
@@ -229,24 +229,25 @@let remainings = provision.amountDays; for (const record of records) { switch (record.record?.type) { case "paid_leave": if (record.record.providedAtPacked === provision.providedAtPacked) { remainings -= 1; } break; case "working_day": for (const timeOff of record.record.timeOffs) { // TODO: 時間単位年休の対応 switch (timeOff.type) { case "halved_paid_leave": if (timeOff.providedAtPacked === provision.providedAtPacked) { remainings -= 0.5; } break; } } break; if (!record.kind) { continue; } const kinds = record.kind.type === "day_whole" ? [record.kind.data] : [record.kind.am, record.kind.pm]; for (const kind of kinds) { // TODO: 時間単位年休の対応 if ( kind.type !== "paid_leave" || kind.providedAtPacked !== provision.providedAtPacked ) { continue; } remainings -= record.kind.type === "day_whole" ? 1 : 0.5; } }
-