Skip to content

Commit 467088b

Browse files
authored
Merge pull request #344 from yn1323/claude/fix-shift-board-monday-DSkGt
fix: シフトボード一覧(PC)を月曜始まりの固定週グリッドに
2 parents 5c470f3 + eb6ac71 commit 467088b

File tree

5 files changed

+284
-94
lines changed

5 files changed

+284
-94
lines changed

src/components/features/Shift/ShiftForm/__mocks__/storyData.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ export const mockDates = [
2626
"2026-01-27",
2727
];
2828

29+
// 水曜開始 2 週間(月曜起算で先頭 月火 / 末尾 火〜日 が期間外)
30+
export const mockDatesMidWeekStart = [
31+
"2026-01-21",
32+
"2026-01-22",
33+
"2026-01-23",
34+
"2026-01-24",
35+
"2026-01-25",
36+
"2026-01-26",
37+
"2026-01-27",
38+
"2026-01-28",
39+
"2026-01-29",
40+
"2026-01-30",
41+
"2026-01-31",
42+
"2026-02-01",
43+
"2026-02-02",
44+
"2026-02-03",
45+
];
46+
2947
export const mockTimeRange: TimeRange = { start: 9, end: 22, unit: 30 };
3048

3149
export const mockHolidays = ["2026-02-11"];

src/components/features/Shift/ShiftForm/pc/OverviewView/index.stories.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
2-
import { JotaiStoryWrapper, mockHolidays } from "../../__mocks__/storyData";
2+
import { JotaiStoryWrapper, mockDatesMidWeekStart, mockHolidays } from "../../__mocks__/storyData";
33
import { OverviewView } from ".";
44

55
const meta = {
@@ -43,3 +43,21 @@ export const WithHolidays: Story = {
4343
</JotaiStoryWrapper>
4444
),
4545
};
46+
47+
// 水曜開始 2 週間。月曜起算で先頭の月火と末尾の火〜日が期間外セルになる
48+
export const MidWeekStart: Story = {
49+
render: () => (
50+
<JotaiStoryWrapper overrides={{ initialViewMode: "overview", dates: mockDatesMidWeekStart }}>
51+
<OverviewView />
52+
</JotaiStoryWrapper>
53+
),
54+
};
55+
56+
// 日曜起算で同じ期間を表示するケース
57+
export const SundayStart: Story = {
58+
render: () => (
59+
<JotaiStoryWrapper overrides={{ initialViewMode: "overview", dates: mockDatesMidWeekStart }}>
60+
<OverviewView weekStart="sun" />
61+
</JotaiStoryWrapper>
62+
),
63+
};

src/components/features/Shift/ShiftForm/pc/OverviewView/index.tsx

Lines changed: 101 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@ import { useCallback, useMemo, useState } from "react";
55
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
66
import { selectedDateAtom, shiftConfigAtom, shiftsAtom, viewModeAtom } from "../../stores";
77
import type { ShiftData, StaffType } from "../../types";
8-
import { getWeekdayLabel } from "../../utils/dateUtils";
8+
import { buildWeeklyGrid, getWeekdayLabel, type WeekStart } from "../../utils/dateUtils";
99
import { timeToMinutes } from "../../utils/timeConversion";
1010

1111
type DateInfo = {
1212
iso: string;
1313
label: string;
1414
wk: string;
15-
weekIdx: number;
15+
inRange: boolean;
1616
};
1717

18-
const buildDateInfos = (dates: string[]): DateInfo[] =>
19-
dates.map((iso, i) => {
20-
const d = dayjs(iso);
21-
return {
22-
iso,
23-
label: `${d.month() + 1}/${d.date()}`,
24-
wk: getWeekdayLabel(iso),
25-
weekIdx: Math.floor(i / 7),
26-
};
27-
});
18+
const toDateInfo = (cell: { iso: string; inRange: boolean }): DateInfo => {
19+
const d = dayjs(cell.iso);
20+
return {
21+
iso: cell.iso,
22+
label: `${d.month() + 1}/${d.date()}`,
23+
wk: getWeekdayLabel(cell.iso),
24+
inRange: cell.inRange,
25+
};
26+
};
2827

2928
const dayColor = (iso: string, holidays: string[]): string => {
3029
const day = dayjs(iso).day();
@@ -45,22 +44,23 @@ const shiftHours = (range: [string, string] | null): number => {
4544
return minutes / 60;
4645
};
4746

48-
export const OverviewView = () => {
47+
type OverviewViewProps = {
48+
weekStart?: WeekStart;
49+
};
50+
51+
export const OverviewView = ({ weekStart = "mon" }: OverviewViewProps) => {
4952
const config = useAtomValue(shiftConfigAtom);
5053
const shifts = useAtomValue(shiftsAtom);
5154
const setSelectedDate = useSetAtom(selectedDateAtom);
5255
const setViewMode = useSetAtom(viewModeAtom);
5356
const { dates, holidays, isReadOnly, staffs } = config;
5457

55-
const dateInfos = useMemo(() => buildDateInfos(dates), [dates]);
56-
const weekCount = Math.max(1, Math.ceil(dateInfos.length / 7));
58+
const weeks = useMemo<DateInfo[][]>(
59+
() => buildWeeklyGrid(dates, weekStart).map((week) => week.map(toDateInfo)),
60+
[dates, weekStart],
61+
);
5762

58-
const initialOpen = useMemo(() => {
59-
const o: Record<number, boolean> = {};
60-
for (let i = 0; i < weekCount; i++) o[i] = true;
61-
return o;
62-
}, [weekCount]);
63-
const [open, setOpen] = useState(initialOpen);
63+
const [open, setOpen] = useState<Record<number, boolean>>({});
6464

6565
const lookup = useMemo(() => {
6666
const map = new Map<string, [string, string]>();
@@ -83,13 +83,12 @@ export const OverviewView = () => {
8383
return (
8484
<Box bg="gray.50" h="100%" overflow="auto" px={5} py={5}>
8585
<Stack gap={3}>
86-
{Array.from({ length: weekCount }).map((_, wi) => {
87-
const wkDates = dateInfos.filter((d) => d.weekIdx === wi);
86+
{weeks.map((wkDates, wi) => {
8887
if (wkDates.length === 0) return null;
89-
const isOpen = !!open[wi];
88+
const isOpen = open[wi] !== false;
9089
return (
9190
<WeekCard
92-
key={wi}
91+
key={wkDates[0].iso}
9392
wkDates={wkDates}
9493
staffs={staffs}
9594
lookup={lookup}
@@ -117,59 +116,64 @@ type WeekCardProps = {
117116
isReadOnly: boolean;
118117
};
119118

120-
const WeekCard = ({ wkDates, staffs, lookup, holidays, isOpen, onToggle, onDateClick, isReadOnly }: WeekCardProps) => (
121-
<Box
122-
bg="white"
123-
borderRadius="xl"
124-
borderWidth="1px"
125-
borderColor={isOpen ? "teal.200" : "gray.200"}
126-
overflow="hidden"
127-
boxShadow="0 1px 2px rgba(0,0,0,0.03)"
128-
transition="all 120ms"
129-
>
130-
<Flex
131-
align="center"
132-
gap={3}
133-
px={5}
134-
py={3}
135-
bg={isOpen ? "teal.50" : "white"}
136-
cursor="pointer"
137-
onClick={onToggle}
138-
borderBottomWidth={isOpen ? "1px" : "0"}
139-
borderColor="teal.200"
119+
const WeekCard = ({ wkDates, staffs, lookup, holidays, isOpen, onToggle, onDateClick, isReadOnly }: WeekCardProps) => {
120+
const inRangeDates = wkDates.filter((d) => d.inRange);
121+
const rangeLabel =
122+
inRangeDates.length > 0 ? `${inRangeDates[0].label}${inRangeDates[inRangeDates.length - 1].label}` : "";
123+
return (
124+
<Box
125+
bg="white"
126+
borderRadius="xl"
127+
borderWidth="1px"
128+
borderColor={isOpen ? "teal.200" : "gray.200"}
129+
overflow="hidden"
130+
boxShadow="0 1px 2px rgba(0,0,0,0.03)"
131+
transition="all 120ms"
140132
>
141133
<Flex
142-
w="28px"
143-
h="28px"
144-
borderRadius="md"
145-
bg={isOpen ? "teal.600" : "gray.100"}
146-
color={isOpen ? "white" : "gray.500"}
147134
align="center"
148-
justify="center"
149-
flexShrink={0}
135+
gap={3}
136+
px={5}
137+
py={3}
138+
bg={isOpen ? "teal.50" : "white"}
139+
cursor="pointer"
140+
onClick={onToggle}
141+
borderBottomWidth={isOpen ? "1px" : "0"}
142+
borderColor="teal.200"
150143
>
151-
{isOpen ? <LuChevronDown size={16} /> : <LuChevronRight size={16} />}
144+
<Flex
145+
w="28px"
146+
h="28px"
147+
borderRadius="md"
148+
bg={isOpen ? "teal.600" : "gray.100"}
149+
color={isOpen ? "white" : "gray.500"}
150+
align="center"
151+
justify="center"
152+
flexShrink={0}
153+
>
154+
{isOpen ? <LuChevronDown size={16} /> : <LuChevronRight size={16} />}
155+
</Flex>
156+
<Box fontSize="15px" fontWeight={700} color="gray.800" style={{ fontVariantNumeric: "tabular-nums" }}>
157+
{rangeLabel}
158+
</Box>
159+
<Box fontSize="12px" color="gray.500">
160+
({inRangeDates.length}日)
161+
</Box>
152162
</Flex>
153-
<Box fontSize="15px" fontWeight={700} color="gray.800" style={{ fontVariantNumeric: "tabular-nums" }}>
154-
{wkDates[0].label}{wkDates[wkDates.length - 1].label}
155-
</Box>
156-
<Box fontSize="12px" color="gray.500">
157-
({wkDates.length}日)
158-
</Box>
159-
</Flex>
160163

161-
{isOpen && (
162-
<WeekTable
163-
staffs={staffs}
164-
wkDates={wkDates}
165-
lookup={lookup}
166-
holidays={holidays}
167-
onDateClick={onDateClick}
168-
isReadOnly={isReadOnly}
169-
/>
170-
)}
171-
</Box>
172-
);
164+
{isOpen && (
165+
<WeekTable
166+
staffs={staffs}
167+
wkDates={wkDates}
168+
lookup={lookup}
169+
holidays={holidays}
170+
onDateClick={onDateClick}
171+
isReadOnly={isReadOnly}
172+
/>
173+
)}
174+
</Box>
175+
);
176+
};
173177

174178
type WeekTableProps = {
175179
staffs: StaffType[];
@@ -204,26 +208,30 @@ const WeekTable = ({ staffs, wkDates, lookup, holidays, onDateClick, isReadOnly
204208
>
205209
スタッフ
206210
</Box>
207-
{wkDates.map((d) => (
208-
<Box
209-
as="th"
210-
key={d.iso}
211-
onClick={isReadOnly ? undefined : () => onDateClick(d.iso)}
212-
style={{
213-
padding: "10px 4px",
214-
fontWeight: 600,
215-
textAlign: "center",
216-
cursor: isReadOnly ? "default" : "pointer",
217-
}}
218-
>
219-
<Box fontSize="12px" color="gray.700" fontWeight={600} style={{ fontVariantNumeric: "tabular-nums" }}>
220-
{d.label}
221-
</Box>
222-
<Box fontSize="10px" fontWeight={600} mt="2px" style={{ color: dayColor(d.iso, holidays) }}>
223-
{d.wk}
211+
{wkDates.map((d) => {
212+
const isClickable = !isReadOnly && d.inRange;
213+
return (
214+
<Box
215+
as="th"
216+
key={d.iso}
217+
onClick={isClickable ? () => onDateClick(d.iso) : undefined}
218+
style={{
219+
padding: "10px 4px",
220+
fontWeight: 600,
221+
textAlign: "center",
222+
cursor: isClickable ? "pointer" : "default",
223+
opacity: d.inRange ? 1 : 0.35,
224+
}}
225+
>
226+
<Box fontSize="12px" color="gray.700" fontWeight={600} style={{ fontVariantNumeric: "tabular-nums" }}>
227+
{d.label}
228+
</Box>
229+
<Box fontSize="10px" fontWeight={600} mt="2px" style={{ color: dayColor(d.iso, holidays) }}>
230+
{d.wk}
231+
</Box>
224232
</Box>
225-
</Box>
226-
))}
233+
);
234+
})}
227235
<Box
228236
as="th"
229237
style={{
@@ -257,7 +265,7 @@ const WeekTable = ({ staffs, wkDates, lookup, holidays, onDateClick, isReadOnly
257265
</Flex>
258266
</Box>
259267
{wkDates.map((d) => {
260-
const asn = lookup.get(`${s.id}-${d.iso}`) ?? null;
268+
const asn = d.inRange ? (lookup.get(`${s.id}-${d.iso}`) ?? null) : null;
261269
if (asn) total += shiftHours(asn);
262270
return (
263271
<Box as="td" key={d.iso} style={{ padding: "8px 4px", textAlign: "center", verticalAlign: "middle" }}>
@@ -272,7 +280,7 @@ const WeekTable = ({ staffs, wkDates, lookup, holidays, onDateClick, isReadOnly
272280
{asn[0]}{asn[1]}
273281
</Box>
274282
) : (
275-
<Box as="span" color="gray.300" fontSize="12px">
283+
<Box as="span" color={d.inRange ? "gray.300" : "gray.200"} fontSize="12px">
276284
277285
</Box>
278286
)}

0 commit comments

Comments
 (0)