@@ -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,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
174178type 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