Changes
3 changed files (+219/-18)
-
-
@@ -80,6 +80,24 @@ },], }, ], paidLeaveProvisionTables: [ { id: { value: "pt-foo" }, displayName: "テストテーブル1", currentRevision: { firstProvisionAmountDays: 10, subsequentProvisionAmountDays: [11, 12, 13, 14, 15], }, }, { id: { value: "pt-bar" }, displayName: "テストテーブル2", currentRevision: { firstProvisionAmountDays: 8, subsequentProvisionAmountDays: [9, 9, 10, 11, 12], }, }, ], }, error = { case: "systemError",
-
-
-
@@ -8,6 +8,7 @@import { withMockedBackend } from "../../../../../.storybook/decorators/withMockedBackend.tsx"; import { withInmemoryRouter } from "../../../../../.storybook/decorators/withInmemoryRouter.tsx"; import { Create } from "../../../../mocks/yamori/worker/v1/worker_service.ts"; import { Get } from "../../../../mocks/yamori/workspace/v1/workspace_service.ts"; import { Page } from "./page.tsx";
-
@@ -30,11 +31,11 @@type Story = StoryObj<typeof Page>; export const Success: Story = { decorators: [withMockedBackend([Create()])], decorators: [withMockedBackend([Create(), Get()])], }; export const SlowLoad: Story = { decorators: [withMockedBackend([Create({ delayMs: 2_000 })])], decorators: [withMockedBackend([Create({ delayMs: 2_000 }), Get({ delayMs: 1_000 })])], }; export const NoCapability: Story = {
-
@@ -43,14 +44,24 @@ workspace: create(WorkspaceSchema, {id: { value: "ws-foo" }, }), }, decorators: [withMockedBackend([Get()])], }; export const SystemError: Story = { decorators: [withMockedBackend([Create({ delayMs: 1_000, failureRate: 1 })])], decorators: [withMockedBackend([Create({ delayMs: 1_000, failureRate: 1 }), Get()])], }; export const TableLoadError: Story = { decorators: [withMockedBackend([Create(), Get({ delayMs: 1_000, failureRate: 1 })])], }; export const Flaky: Story = { decorators: [withMockedBackend([Create({ delayMs: 1_000, failureRate: 0.5 })])], decorators: [ withMockedBackend([ Create({ delayMs: 1_000, failureRate: 0.5 }), Get({ delayMs: 1_000, failureRate: 0.5 }), ]), ], }; export const CapabilityError: Story = {
-
@@ -65,6 +76,7 @@ path: "worker_add_key",}, }, }), Get(), ]), ], };
-
@@ -81,6 +93,7 @@ typeName: "yamori.workspace.v1.Workspace",}, }, }), Get(), ]), ], };
-
@@ -97,6 +110,7 @@ path: "display_name",}, }, }), Get(), ]), ], };
-
-
-
@@ -3,38 +3,76 @@ // SPDX-License-Identifier: AGPL-3.0-onlyimport "../../../../polyfill.ts"; import * as proto from "@bufbuild/protobuf"; import { TZDate } from "@date-fns/tz"; import { CheckCircledIcon } from "@radix-ui/react-icons"; import { Button, Container, Flex, TextField } from "@radix-ui/themes"; import { type Workspace } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { Button, Container, Flex, Inset, RadioCards, Spinner, Text, TextField, } from "@radix-ui/themes"; import { PaidLeaveProvisionTableSchema } from "@yamori/proto/yamori/paid_leave_provision/v1/paid_leave_provision_table_pb.js"; import { type Workspace, WorkspaceSchema, } from "@yamori/proto/yamori/workspace/v1/workspace_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 { CreateRequestSchema } from "@yamori/proto/yamori/worker/v1/create_request_pb.js"; import { CreateResponseSchema } from "@yamori/proto/yamori/worker/v1/create_response_pb.js"; import { type FC, use } from "react"; import { useForm } from "react-hook-form"; import { format } from "date-fns"; import { type FC, use, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import * as Empty from "../../../../components/Empty.ts"; import { ManagedErrorCallout } from "../../../../components/ErrorCallout.ts"; import * as FormField from "../../../../components/FormField.ts"; import { useMethodMutation } from "../../../../contexts/Service.tsx"; import { PaidLeaveProvisionTableView } from "../../../../components/PaidLeaveProvisionTableView.tsx"; import { useMethodMutation, useMethodQuery } from "../../../../contexts/Service.tsx"; import { NavigationContext } from "../../../../contexts/Router.tsx"; import { useToast } from "../../../../contexts/Toast.tsx"; import { IllegalMessageError } from "../../../../errors/IllegalMessageError.ts"; import { toProtoDate } from "../../../../helpers.ts"; import { Layout } from "../../Layout.tsx"; interface BodyProps { workspace: Workspace; provisionTables: proto.MessageShape<typeof PaidLeaveProvisionTableSchema>[]; } const Body: FC<BodyProps> = ({ workspace }) => { const Body: FC<BodyProps> = ({ workspace, provisionTables }) => { const navigation = use(NavigationContext); const toast = useToast(); const sortedTables = useMemo(() => { return [...provisionTables].sort( (a, b) => (b.currentRevision?.firstProvisionAmountDays ?? 0) - (a.currentRevision?.firstProvisionAmountDays ?? 0), ); }, [provisionTables]); const firstProvisionAtDefaultValue = useMemo<string>(() => { return format(new Date(), "yyyy-MM-dd"); }, []); const form = useForm<{ displayName: string; provisionTableId: string; firstProvisionAt: string; }>({ defaultValues: { displayName: "", provisionTableId: sortedTables[0]?.id?.value, firstProvisionAt: firstProvisionAtDefaultValue, }, mode: "onBlur", });
-
@@ -98,13 +136,21 @@return ( <Flex asChild direction="column" gap="3" mt="2"> <form onSubmit={form.handleSubmit(({ displayName }) => { creation.mutate({ displayName, workspaceId: workspace.id, workerAddKey: workspace.workerAddKey, }); })} onSubmit={form.handleSubmit( ({ displayName, firstProvisionAt, provisionTableId }) => { creation.mutate({ displayName, workspaceId: workspace.id, workerAddKey: workspace.workerAddKey, firstPaidLeaveProvisionAt: toProtoDate( new TZDate(firstProvisionAt, "Asia/Tokyo"), ), paidLeaveProvisionTableId: { value: provisionTableId, }, }); }, )} > {creation.error ? ( <ManagedErrorCallout
-
@@ -143,6 +189,72 @@ <FormField.Description error={form.formState.errors.displayName?.message}>労働者を表示する際に利用される名前です。前後の空白は自動的に取り除かれます。 </FormField.Description> </FormField.Root> <FormField.Root mt="4"> <FormField.Label htmlFor="first_provision_at_id"> 年次有給休暇初回付与日 </FormField.Label> <TextField.Root id="first_provision_at_id" type="date" disabled={creation.isPending} color={form.formState.errors.firstProvisionAt ? "red" : undefined} aria-invalid={!!form.formState.errors.firstProvisionAt} {...form.register("firstProvisionAt", { required: "初回付与日は必須です", })} style={{ alignSelf: "flex-start" }} /> <FormField.Description error={form.formState.errors.firstProvisionAt?.message}> この労働者に初めて年次有給休暇が付与される日付です。 以降の付与はこの日から 1 年後、 2 年後... となります。 </FormField.Description> </FormField.Root> <FormField.Root mt="4"> <FormField.Label>有給休暇付与日数</FormField.Label> <Controller control={form.control} name="provisionTableId" render={({ field }) => { return ( <RadioCards.Root {...field} disabled={creation.isPending} columns={{ initial: "1", sm: "2", md: "3", }} onValueChange={(value) => { field.onChange(value); }} > {sortedTables.map((table) => { if (!table.id) { return null; } return ( <RadioCards.Item key={table.id.value} value={table.id.value}> <Flex width="100%" direction="column" gap="2"> <Text weight="bold">{table.displayName}</Text> <Inset side="x"> <PaidLeaveProvisionTableView paidLeaveProvisionTable={table} size="1" /> </Inset> </Flex> </RadioCards.Item> ); })} </RadioCards.Root> ); }} /> <FormField.Description> 年次有給休暇の自動付与を行う際に参照する付与日数テーブルです。 </FormField.Description> </FormField.Root> <Button mt="5" loading={creation.isPending}> 作成 </Button>
-
@@ -156,10 +268,67 @@ workspace: Workspace;} export const Page: FC<PageProps> = ({ workspace }) => { const tables = useMethodQuery({ service: "yamori.workspace.v1.WorkspaceService", method: "Get", request: { schema: GetRequestSchema, data: { workspaceId: workspace.id, readMask: { fields: [WorkspaceSchema.field.paidLeaveProvisionTables.number], paidLeaveProvisionTableMask: { fields: [ PaidLeaveProvisionTableSchema.field.id.number, PaidLeaveProvisionTableSchema.field.displayName.number, PaidLeaveProvisionTableSchema.field.currentRevision.number, ], }, }, }, }, response: { schema: GetResponseSchema, }, mapResponse(resp) { switch (resp.result.case) { case "ok": return resp.result.value.paidLeaveProvisionTables; case undefined: throw new IllegalMessageError(resp); default: throw resp.result.value; } }, }); return ( <Layout workspace={workspace} title="労働者登録"> <Container p="2" size="2"> <Body workspace={workspace} /> {tables.isLoading && ( <Flex mt="5" justify="center" align="center" gap="2"> <Spinner /> <Text size="2" color="gray"> 付与日数テーブルの一覧を取得中... </Text> </Flex> )} {tables.isLoadingError && ( <ManagedErrorCallout title="付与日数テーブルの一覧取得に失敗しました" error={tables.error} actions={ <Button size="1" loading={tables.isFetching} onClick={() => void tables.refetch()} > 再取得 </Button> } /> )} {tables.data && <Body workspace={workspace} provisionTables={tables.data} />} </Container> </Layout> );
-