@@ -5,26 +5,25 @@ import { useCallback, useMemo, useState } from "react";
55import { LuChevronDown , LuChevronRight } from "react-icons/lu" ;
66import { selectedDateAtom , shiftConfigAtom , shiftsAtom , viewModeAtom } from "../../stores" ;
77import type { ShiftData , StaffType } from "../../types" ;
8- import { getWeekdayLabel } from "../../utils/dateUtils" ;
8+ import { buildWeeklyGrid , getWeekdayLabel , type WeekStart } from "../../utils/dateUtils" ;
99import { timeToMinutes } from "../../utils/timeConversion" ;
1010
1111type 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
2928const dayColor = ( iso : string , holidays : string [ ] ) : string => {
3029 const day = dayjs ( iso ) . day ( ) ;
@@ -45,21 +44,27 @@ 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
5863 const initialOpen = useMemo ( ( ) => {
5964 const o : Record < number , boolean > = { } ;
60- for ( let i = 0 ; i < weekCount ; i ++ ) o [ i ] = true ;
65+ for ( let i = 0 ; i < weeks . length ; i ++ ) o [ i ] = true ;
6166 return o ;
62- } , [ weekCount ] ) ;
67+ } , [ weeks . length ] ) ;
6368 const [ open , setOpen ] = useState ( initialOpen ) ;
6469
6570 const lookup = useMemo ( ( ) => {
@@ -83,13 +88,12 @@ export const OverviewView = () => {
8388 return (
8489 < Box bg = "gray.50" h = "100%" overflow = "auto" px = { 5 } py = { 5 } >
8590 < Stack gap = { 3 } >
86- { Array . from ( { length : weekCount } ) . map ( ( _ , wi ) => {
87- const wkDates = dateInfos . filter ( ( d ) => d . weekIdx === wi ) ;
91+ { weeks . map ( ( wkDates , wi ) => {
8892 if ( wkDates . length === 0 ) return null ;
8993 const isOpen = ! ! open [ wi ] ;
9094 return (
9195 < WeekCard
92- key = { wi }
96+ key = { wkDates [ 0 ] . iso }
9397 wkDates = { wkDates }
9498 staffs = { staffs }
9599 lookup = { lookup }
@@ -117,59 +121,64 @@ type WeekCardProps = {
117121 isReadOnly : boolean ;
118122} ;
119123
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"
124+ const WeekCard = ( { wkDates, staffs, lookup, holidays, isOpen, onToggle, onDateClick, isReadOnly } : WeekCardProps ) => {
125+ const inRangeDates = wkDates . filter ( ( d ) => d . inRange ) ;
126+ const rangeLabel =
127+ inRangeDates . length > 0 ? `${ inRangeDates [ 0 ] . label } – ${ inRangeDates [ inRangeDates . length - 1 ] . label } ` : "" ;
128+ return (
129+ < Box
130+ bg = "white"
131+ borderRadius = "xl"
132+ borderWidth = "1px"
133+ borderColor = { isOpen ? "teal.200" : "gray.200" }
134+ overflow = "hidden"
135+ boxShadow = "0 1px 2px rgba(0,0,0,0.03)"
136+ transition = "all 120ms"
140137 >
141138 < Flex
142- w = "28px"
143- h = "28px"
144- borderRadius = "md"
145- bg = { isOpen ? "teal.600" : "gray.100" }
146- color = { isOpen ? "white" : "gray.500" }
147139 align = "center"
148- justify = "center"
149- flexShrink = { 0 }
140+ gap = { 3 }
141+ px = { 5 }
142+ py = { 3 }
143+ bg = { isOpen ? "teal.50" : "white" }
144+ cursor = "pointer"
145+ onClick = { onToggle }
146+ borderBottomWidth = { isOpen ? "1px" : "0" }
147+ borderColor = "teal.200"
150148 >
151- { isOpen ? < LuChevronDown size = { 16 } /> : < LuChevronRight size = { 16 } /> }
149+ < Flex
150+ w = "28px"
151+ h = "28px"
152+ borderRadius = "md"
153+ bg = { isOpen ? "teal.600" : "gray.100" }
154+ color = { isOpen ? "white" : "gray.500" }
155+ align = "center"
156+ justify = "center"
157+ flexShrink = { 0 }
158+ >
159+ { isOpen ? < LuChevronDown size = { 16 } /> : < LuChevronRight size = { 16 } /> }
160+ </ Flex >
161+ < Box fontSize = "15px" fontWeight = { 700 } color = "gray.800" style = { { fontVariantNumeric : "tabular-nums" } } >
162+ { rangeLabel }
163+ </ Box >
164+ < Box fontSize = "12px" color = "gray.500" >
165+ ({ inRangeDates . length } 日)
166+ </ Box >
152167 </ 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 >
160168
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- ) ;
169+ { isOpen && (
170+ < WeekTable
171+ staffs = { staffs }
172+ wkDates = { wkDates }
173+ lookup = { lookup }
174+ holidays = { holidays }
175+ onDateClick = { onDateClick }
176+ isReadOnly = { isReadOnly }
177+ />
178+ ) }
179+ </ Box >
180+ ) ;
181+ } ;
173182
174183type WeekTableProps = {
175184 staffs : StaffType [ ] ;
@@ -204,26 +213,30 @@ const WeekTable = ({ staffs, wkDates, lookup, holidays, onDateClick, isReadOnly
204213 >
205214 スタッフ
206215 </ 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 }
216+ { wkDates . map ( ( d ) => {
217+ const isClickable = ! isReadOnly && d . inRange ;
218+ return (
219+ < Box
220+ as = "th"
221+ key = { d . iso }
222+ onClick = { isClickable ? ( ) => onDateClick ( d . iso ) : undefined }
223+ style = { {
224+ padding : "10px 4px" ,
225+ fontWeight : 600 ,
226+ textAlign : "center" ,
227+ cursor : isClickable ? "pointer" : "default" ,
228+ opacity : d . inRange ? 1 : 0.35 ,
229+ } }
230+ >
231+ < Box fontSize = "12px" color = "gray.700" fontWeight = { 600 } style = { { fontVariantNumeric : "tabular-nums" } } >
232+ { d . label }
233+ </ Box >
234+ < Box fontSize = "10px" fontWeight = { 600 } mt = "2px" style = { { color : dayColor ( d . iso , holidays ) } } >
235+ { d . wk }
236+ </ Box >
224237 </ Box >
225- </ Box >
226- ) ) }
238+ ) ;
239+ } ) }
227240 < Box
228241 as = "th"
229242 style = { {
@@ -257,7 +270,7 @@ const WeekTable = ({ staffs, wkDates, lookup, holidays, onDateClick, isReadOnly
257270 </ Flex >
258271 </ Box >
259272 { wkDates . map ( ( d ) => {
260- const asn = lookup . get ( `${ s . id } -${ d . iso } ` ) ?? null ;
273+ const asn = d . inRange ? ( lookup . get ( `${ s . id } -${ d . iso } ` ) ?? null ) : null ;
261274 if ( asn ) total += shiftHours ( asn ) ;
262275 return (
263276 < Box as = "td" key = { d . iso } style = { { padding : "8px 4px" , textAlign : "center" , verticalAlign : "middle" } } >
@@ -272,7 +285,7 @@ const WeekTable = ({ staffs, wkDates, lookup, holidays, onDateClick, isReadOnly
272285 { asn [ 0 ] } –{ asn [ 1 ] }
273286 </ Box >
274287 ) : (
275- < Box as = "span" color = "gray.300" fontSize = "12px" >
288+ < Box as = "span" color = { d . inRange ? "gray.300" : "gray.200" } fontSize = "12px" >
276289 —
277290 </ Box >
278291 ) }
0 commit comments