Changes
1 changed files (+261/-97)
-
-
@@ -2,17 +2,34 @@ // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>// SPDX-License-Identifier: AGPL-3.0-only import * as proto from "@bufbuild/protobuf"; import { Button, type ButtonProps, DropdownMenu } from "@radix-ui/themes"; import { Button, type ButtonProps, Dialog, DropdownMenu, Flex } from "@radix-ui/themes"; import { WriteWorkRecordRequestSchema } from "@yamori/proto/yamori/worker/v1/write_work_record_request_pb.js"; import { WriteWorkRecordResponseSchema } from "@yamori/proto/yamori/worker/v1/write_work_record_response_pb.js"; import { LeaveSchema } from "@yamori/proto/yamori/work_record/v1/leave_pb.js"; import { GetRequestSchema } from "@yamori/proto/yamori/workspace/v1/get_request_pb.js"; import { GetResponseSchema } from "@yamori/proto/yamori/workspace/v1/get_response_pb.js"; import { type DateSchema } from "@yamori/proto/yamori/type/v1/date_pb.js"; import { type WorkerSchema } from "@yamori/proto/yamori/worker/v1/worker_pb.js"; import { type WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { type FC } from "react"; import { WorkspaceSchema } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { isBefore } from "date-fns"; import { useMemo, useState, type FC } from "react"; import { useMethodMutation } from "../../../contexts/Service.tsx"; import { useMethodMutation, useMethodQuery } from "../../../contexts/Service.tsx"; import { useToast } from "../../../contexts/Toast.tsx"; import { IllegalMessageError } from "../../../errors/IllegalMessageError.ts"; import { fromProtoDate } from "../../../helpers.ts"; const dateFormatter = new Intl.DateTimeFormat(navigator.language, { dateStyle: "medium", }); class CannotApplyLeaveToDates { constructor( public readonly leave: proto.MessageShape<typeof LeaveSchema>, public readonly availableSince: Date, public readonly dates: readonly Date[], ) {} } export interface ActionTarget { worker: proto.MessageShape<typeof WorkerSchema>;
-
@@ -37,6 +54,40 @@ ...rest}) => { const toast = useToast(); const leaveDefinitions = useMethodQuery({ service: "yamori.workspace.v1.WorkspaceService", method: "Get", request: { schema: GetRequestSchema, data: { workspaceId: workspaceID, readMask: { fields: [WorkspaceSchema.field.leaveDefinitions.number], leaveDefinitionsMask: { fields: [ LeaveSchema.field.id.number, LeaveSchema.field.revisions.number, LeaveSchema.field.displayName.number, ], }, }, }, }, response: { schema: GetResponseSchema, }, mapResponse(resp) { switch (resp.result.case) { case "ok": return resp.result.value.leaveDefinitions; case undefined: throw new IllegalMessageError(resp); default: throw resp.result.value; } }, }); const isWorkRecordWritable = targets.length > 0 && targets.every((target) => target.worker.writeWorkRecordKey);
-
@@ -70,111 +121,224 @@ },}, }); const dates = useMemo(() => { return targets .map(({ dates }) => dates) .flat() .map((d) => fromProtoDate(d)); }, [targets]); const [canceledDialog, setCanceledDialog] = useState<CannotApplyLeaveToDates | null>( null, ); return ( <DropdownMenu.Root> <DropdownMenu.Trigger> <Button {...rest} loading={writeRecord.isPending}> 操作 <DropdownMenu.TriggerIcon /> </Button> </DropdownMenu.Trigger> <DropdownMenu.Content> <DropdownMenu.Label>記録書き込み</DropdownMenu.Label> <DropdownMenu.Item disabled={!isWorkRecordWritable} onSelect={async () => { try { for (const target of targets) { await writeRecord.mutateAsync({ workspaceId: workspaceID, workerId: target.worker.id, writeWorkRecordKey: target.worker.writeWorkRecordKey, workRecord: { dates: target.dates, record: { case: "workingDay", value: { hasWorkerWorked: true, <Dialog.Root open={!!canceledDialog} onOpenChange={(open) => { if (!open) { setCanceledDialog(null); } }} > <DropdownMenu.Root> <DropdownMenu.Trigger> <Button {...rest} loading={writeRecord.isPending}> 操作 <DropdownMenu.TriggerIcon /> </Button> </DropdownMenu.Trigger> <DropdownMenu.Content> <DropdownMenu.Label>記録書き込み</DropdownMenu.Label> <DropdownMenu.Item disabled={!isWorkRecordWritable} onSelect={async () => { try { for (const target of targets) { await writeRecord.mutateAsync({ workspaceId: workspaceID, workerId: target.worker.id, writeWorkRecordKey: target.worker.writeWorkRecordKey, workRecord: { dates: target.dates, record: { case: "workingDay", value: { hasWorkerWorked: true, }, }, }, }, }); } toast.open({ severity: "success", title: "勤怠記録を更新しました", }); } finally { onWorkRecordMutated?.(); } toast.open({ severity: "success", title: "勤怠記録を更新しました", }); } finally { onWorkRecordMutated?.(); } }} > 出勤 </DropdownMenu.Item> <DropdownMenu.Item disabled={!isWorkRecordWritable} onSelect={async () => { try { for (const target of targets) { await writeRecord.mutateAsync({ workspaceId: workspaceID, workerId: target.worker.id, writeWorkRecordKey: target.worker.writeWorkRecordKey, workRecord: { dates: target.dates, record: { case: "dayOff", value: {}, }} > 出勤 </DropdownMenu.Item> <DropdownMenu.Item disabled={!isWorkRecordWritable} onSelect={async () => { try { for (const target of targets) { await writeRecord.mutateAsync({ workspaceId: workspaceID, workerId: target.worker.id, writeWorkRecordKey: target.worker.writeWorkRecordKey, workRecord: { dates: target.dates, record: { case: "dayOff", value: {}, }, }, }, }); } toast.open({ severity: "success", title: "勤怠記録を更新しました", }); } finally { onWorkRecordMutated?.(); } toast.open({ severity: "success", title: "勤怠記録を更新しました", }); } finally { onWorkRecordMutated?.(); } }} > 休日 </DropdownMenu.Item> <DropdownMenu.Item disabled={!isWorkRecordWritable} onSelect={async () => { try { for (const target of targets) { await writeRecord.mutateAsync({ workspaceId: workspaceID, workerId: target.worker.id, writeWorkRecordKey: target.worker.writeWorkRecordKey, workRecord: { dates: target.dates, record: { case: "workingDay", value: { hasWorkerWorked: false, }} > 休日 </DropdownMenu.Item> <DropdownMenu.Item disabled={!isWorkRecordWritable} onSelect={async () => { try { for (const target of targets) { await writeRecord.mutateAsync({ workspaceId: workspaceID, workerId: target.worker.id, writeWorkRecordKey: target.worker.writeWorkRecordKey, workRecord: { dates: target.dates, record: { case: "workingDay", value: { hasWorkerWorked: false, }, }, }, }, }); } toast.open({ severity: "success", title: "勤怠記録を更新しました", }); } finally { onWorkRecordMutated?.(); } }} > 欠勤 </DropdownMenu.Item> <DropdownMenu.Sub> <DropdownMenu.SubTrigger>その他休暇休業</DropdownMenu.SubTrigger> <DropdownMenu.SubContent> {leaveDefinitions.data ? ( leaveDefinitions.data.map((def) => { if (!def.id) { return null; } toast.open({ severity: "success", title: "勤怠記録を更新しました", }); } finally { onWorkRecordMutated?.(); } }} > 欠勤 </DropdownMenu.Item> </DropdownMenu.Content> </DropdownMenu.Root> return ( <DropdownMenu.Item key={def.id?.value} disabled={!isWorkRecordWritable} onSelect={async () => { const firstAvailableAt = def.revisions[0]?.startAt && fromProtoDate(def.revisions[0].startAt); const datesTheLeaveNotAvailableFor = firstAvailableAt ? dates.filter((date) => isBefore(date, firstAvailableAt)) : []; if (firstAvailableAt && datesTheLeaveNotAvailableFor.length > 0) { setCanceledDialog( new CannotApplyLeaveToDates( def, firstAvailableAt, datesTheLeaveNotAvailableFor, ), ); return; } try { for (const target of targets) { await writeRecord.mutateAsync({ workspaceId: workspaceID, workerId: target.worker.id, writeWorkRecordKey: target.worker.writeWorkRecordKey, workRecord: { dates: target.dates, record: { case: "workspaceDefinedLeaveId", value: { value: def.id!.value, }, }, }, }); } toast.open({ severity: "success", title: "勤怠記録を更新しました", }); } finally { onWorkRecordMutated?.(); } }} > {def.displayName} </DropdownMenu.Item> ); }) ) : leaveDefinitions.isError ? ( <DropdownMenu.Item disabled>取得できませんでした</DropdownMenu.Item> ) : ( <DropdownMenu.Item disabled>取得中...</DropdownMenu.Item> )} </DropdownMenu.SubContent> </DropdownMenu.Sub> </DropdownMenu.Content> </DropdownMenu.Root> <Dialog.Content> {canceledDialog && ( <> <Dialog.Title>この休暇は設定できません</Dialog.Title> <Dialog.Description> 「{canceledDialog.leave.displayName}」の運用開始日 ( {dateFormatter.format(canceledDialog.availableSince)}) よりも前の日付が範囲に含まれているため、この休暇を設定することはできません。 </Dialog.Description> <ul> {canceledDialog.dates.map((d) => ( <li key={+d}>{dateFormatter.format(d)}</li> ))} </ul> <Flex mt="3" justify="end" align="center"> <Dialog.Close> <Button>閉じる</Button> </Dialog.Close> </Flex> </> )} </Dialog.Content> </Dialog.Root> ); };
-