@@ -22,6 +22,9 @@ import {
2222 Activity ,
2323 Plus ,
2424 X ,
25+ Check ,
26+ CheckSquare ,
27+ Trash2 ,
2528} from "lucide-react" ;
2629import { useState } from "react" ;
2730import type { MaintenanceRecord , MaintenanceType , Location , FluidType , BatteryType } from "~/types/db" ;
@@ -46,6 +49,12 @@ interface MaintenanceListProps {
4649 userLocations ?: Location [ ] ;
4750 onEdit : ( record : MaintenanceRecord ) => void ;
4851 onAdd ?: ( ) => void ;
52+ /**
53+ * When provided, enables the bulk-select UI. The parent receives every
54+ * underlying record id (parent + bundled children) for the selected groups
55+ * and is responsible for confirming and performing the deletion.
56+ */
57+ onBulkDelete ?: ( recordIds : number [ ] ) => void ;
4958}
5059
5160interface GroupedMaintenanceRecord {
@@ -199,9 +208,13 @@ function getCollapsedMetric(group: GroupedMaintenanceRecord, currencyCode?: stri
199208 return null ;
200209}
201210
202- export function MaintenanceList ( { records, currencyCode, userLocations, onEdit, onAdd } : MaintenanceListProps ) {
211+ export function MaintenanceList ( { records, currencyCode, userLocations, onEdit, onAdd, onBulkDelete } : MaintenanceListProps ) {
203212 const [ expandedGroupId , setExpandedGroupId ] = useState < string | null > ( null ) ;
204213 const [ filter , setFilter ] = useState < "all" | "maintenance" | "fuel" > ( "all" ) ;
214+ const [ selectionMode , setSelectionMode ] = useState ( false ) ;
215+ const [ selectedGroupIds , setSelectedGroupIds ] = useState < Set < string > > ( new Set ( ) ) ;
216+
217+ const bulkSelectEnabled = typeof onBulkDelete === "function" ;
205218
206219 const dateFormatter = new Intl . DateTimeFormat ( "de-CH" , {
207220 dateStyle : "medium" ,
@@ -232,35 +245,123 @@ export function MaintenanceList({ records, currencyCode, userLocations, onEdit,
232245 setExpandedGroupId ( expandedGroupId === id ? null : id ) ;
233246 } ;
234247
248+ const toggleSelectGroup = ( id : string ) => {
249+ setSelectedGroupIds ( ( prev ) => {
250+ const next = new Set ( prev ) ;
251+ if ( next . has ( id ) ) {
252+ next . delete ( id ) ;
253+ } else {
254+ next . add ( id ) ;
255+ }
256+ return next ;
257+ } ) ;
258+ } ;
259+
260+ const exitSelectionMode = ( ) => {
261+ setSelectionMode ( false ) ;
262+ setSelectedGroupIds ( new Set ( ) ) ;
263+ } ;
264+
265+ const enterSelectionMode = ( ) => {
266+ setSelectionMode ( true ) ;
267+ setExpandedGroupId ( null ) ;
268+ } ;
269+
270+ const handleBulkDelete = ( ) => {
271+ if ( ! onBulkDelete ) return ;
272+ const ids : number [ ] = [ ] ;
273+ for ( const group of groupedRecords ) {
274+ if ( ! selectedGroupIds . has ( group . id ) ) continue ;
275+ for ( const record of group . originalRecords ) {
276+ ids . push ( record . id ) ;
277+ }
278+ }
279+ if ( ids . length === 0 ) return ;
280+ onBulkDelete ( ids ) ;
281+ exitSelectionMode ( ) ;
282+ } ;
283+
284+ const selectedCount = selectedGroupIds . size ;
285+
235286 return (
236287 < div className = "space-y-6" >
237288 < div
238- className = "sticky z-20 -mx-4 flex items-center justify-between gap-3 border-b border-gray-100 bg-white px-4 pb-3 pt-3 dark:border-navy-700 dark:bg-navy-800"
289+ className = { clsx (
290+ "sticky z-20 -mx-4 flex items-center justify-between gap-3 border-b px-4 pb-3 pt-3" ,
291+ selectionMode
292+ ? "border-primary/30 bg-primary/10 dark:bg-primary/15"
293+ : "border-gray-100 bg-white dark:border-navy-700 dark:bg-navy-800" ,
294+ ) }
239295 style = { { top : "var(--app-header-h, 4rem)" } }
240296 >
241- < div className = "flex items-center gap-2 overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden" >
242- < FilterChip
243- label = "Alle"
244- icon = { Layers }
245- active = { filter === "all" }
246- onClick = { ( ) => setFilter ( "all" ) }
247- />
248- < FilterChip
249- label = "Wartung"
250- icon = { Wrench }
251- active = { filter === "maintenance" }
252- onClick = { ( ) => setFilter ( "maintenance" ) }
253- />
254- < FilterChip
255- label = "Tanken"
256- icon = { Fuel }
257- active = { filter === "fuel" }
258- onClick = { ( ) => setFilter ( "fuel" ) }
259- />
260- </ div >
261- < div className = "shrink-0 font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/45 dark:text-navy-500" >
262- { groupedRecords . length } { groupedRecords . length === 1 ? 'Eintrag' : 'Einträge' }
263- </ div >
297+ { selectionMode ? (
298+ < >
299+ < div className = "flex items-center gap-2 text-sm font-semibold text-primary dark:text-primary-light" >
300+ < CheckSquare className = "h-4 w-4" aria-hidden = "true" />
301+ < span aria-live = "polite" >
302+ { selectedCount } { selectedCount === 1 ? "ausgewählt" : "ausgewählt" }
303+ </ span >
304+ </ div >
305+ < div className = "flex items-center gap-2" >
306+ < button
307+ type = "button"
308+ onClick = { exitSelectionMode }
309+ className = "inline-flex items-center gap-1 rounded-sm px-2.5 py-1.5 text-xs font-semibold text-base-content/70 transition-colors hover:bg-base-200 dark:text-navy-300 dark:hover:bg-navy-700"
310+ >
311+ < X className = "h-3.5 w-3.5" aria-hidden = "true" />
312+ Abbrechen
313+ </ button >
314+ < button
315+ type = "button"
316+ onClick = { handleBulkDelete }
317+ disabled = { selectedCount === 0 }
318+ className = "inline-flex items-center gap-1 rounded-sm bg-error px-2.5 py-1.5 text-xs font-semibold text-error-content shadow-sm transition-colors hover:bg-error/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-error/40 disabled:cursor-not-allowed disabled:opacity-50"
319+ >
320+ < Trash2 className = "h-3.5 w-3.5" aria-hidden = "true" />
321+ Löschen
322+ </ button >
323+ </ div >
324+ </ >
325+ ) : (
326+ < >
327+ < div className = "flex items-center gap-2 overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden" >
328+ < FilterChip
329+ label = "Alle"
330+ icon = { Layers }
331+ active = { filter === "all" }
332+ onClick = { ( ) => setFilter ( "all" ) }
333+ />
334+ < FilterChip
335+ label = "Wartung"
336+ icon = { Wrench }
337+ active = { filter === "maintenance" }
338+ onClick = { ( ) => setFilter ( "maintenance" ) }
339+ />
340+ < FilterChip
341+ label = "Tanken"
342+ icon = { Fuel }
343+ active = { filter === "fuel" }
344+ onClick = { ( ) => setFilter ( "fuel" ) }
345+ />
346+ </ div >
347+ < div className = "flex shrink-0 items-center gap-3" >
348+ < span className = "font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/45 dark:text-navy-500" >
349+ { groupedRecords . length } { groupedRecords . length === 1 ? 'Eintrag' : 'Einträge' }
350+ </ span >
351+ { bulkSelectEnabled && groupedRecords . length > 0 && (
352+ < button
353+ type = "button"
354+ onClick = { enterSelectionMode }
355+ className = "inline-flex items-center gap-1 rounded-sm border border-base-300 px-2 py-1 text-[11px] font-semibold text-base-content/70 transition-colors hover:bg-base-200 dark:border-navy-700 dark:text-navy-300 dark:hover:bg-navy-700"
356+ aria-label = "Mehrere Einträge auswählen"
357+ >
358+ < CheckSquare className = "h-3.5 w-3.5" aria-hidden = "true" />
359+ Auswählen
360+ </ button >
361+ ) }
362+ </ div >
363+ </ >
364+ ) }
264365 </ div >
265366
266367 { groupedRecords . length === 0 ? (
@@ -316,7 +417,8 @@ export function MaintenanceList({ records, currencyCode, userLocations, onEdit,
316417 < ul className = "space-y-2" >
317418 { recordsByYear . get ( year ) ! . map ( ( group ) => {
318419 const Icon = getIconForType ( group . type ) ;
319- const isExpanded = expandedGroupId === group . id ;
420+ const isExpanded = ! selectionMode && expandedGroupId === group . id ;
421+ const isSelected = selectedGroupIds . has ( group . id ) ;
320422 const tone = TYPE_TONE [ group . type ] ?? TYPE_TONE . general ;
321423 const typeLabel = maintenanceTypeLabels [ group . type ] || group . type ;
322424
@@ -339,14 +441,36 @@ export function MaintenanceList({ records, currencyCode, userLocations, onEdit,
339441 const showTypeTag = ! summaryRepeatsType ;
340442
341443 return (
342- < li key = { group . id } className = "rounded-sm transition-colors hover:bg-base-200/50 dark:hover:bg-navy-700/30" >
444+ < li
445+ key = { group . id }
446+ className = { clsx (
447+ "rounded-sm transition-colors" ,
448+ isSelected
449+ ? "bg-primary/10 dark:bg-primary/15"
450+ : "hover:bg-base-200/50 dark:hover:bg-navy-700/30" ,
451+ ) }
452+ >
343453 < button
344454 type = "button"
345- onClick = { ( ) => toggleExpand ( group . id ) }
346- aria-expanded = { isExpanded }
347- aria-controls = { `maintenance-details-${ group . id } ` }
455+ onClick = { ( ) => ( selectionMode ? toggleSelectGroup ( group . id ) : toggleExpand ( group . id ) ) }
456+ aria-expanded = { selectionMode ? undefined : isExpanded }
457+ aria-pressed = { selectionMode ? isSelected : undefined }
458+ aria-controls = { selectionMode ? undefined : `maintenance-details-${ group . id } ` }
348459 className = "group flex w-full cursor-pointer items-start gap-3 py-2.5 pl-0 text-left"
349460 >
461+ { selectionMode && (
462+ < div
463+ className = { clsx (
464+ "mt-1.5 grid h-5 w-5 shrink-0 place-items-center rounded-sm border transition-colors" ,
465+ isSelected
466+ ? "border-primary bg-primary text-primary-content"
467+ : "border-base-300 bg-base-100 dark:border-navy-600 dark:bg-navy-900" ,
468+ ) }
469+ aria-hidden = "true"
470+ >
471+ { isSelected && < Check className = "h-3.5 w-3.5" strokeWidth = { 3 } /> }
472+ </ div >
473+ ) }
350474 < div className = { clsx (
351475 "mt-0.5 grid h-10 w-10 place-items-center rounded-sm shrink-0 transition-transform group-hover:scale-105" ,
352476 tone . bg ,
@@ -366,7 +490,9 @@ export function MaintenanceList({ records, currencyCode, userLocations, onEdit,
366490 { formatNumber ( group . odo ) }
367491 </ span >
368492 < span className = "font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/55" > km</ span >
369- < ChevronDown className = { clsx ( "h-4 w-4 self-center text-base-content/45 transition-transform dark:text-navy-400" , isExpanded && "rotate-180" ) } aria-hidden = "true" />
493+ { ! selectionMode && (
494+ < ChevronDown className = { clsx ( "h-4 w-4 self-center text-base-content/45 transition-transform dark:text-navy-400" , isExpanded && "rotate-180" ) } aria-hidden = "true" />
495+ ) }
370496 </ div >
371497 </ div >
372498
0 commit comments