import 'package:flutter/material.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../core/formatters.dart'; import '../core/theme.dart'; import '../models/shift.dart'; import 'common.dart'; /// The one card shape in the app. Identity on top, a hairline split, then the /// money band — with a status bar welded to the bottom edge. class ShiftCard extends StatelessWidget { const ShiftCard({ super.key, required this.shift, this.onTap, this.trailingAction, this.showDate = false, }); final Shift shift; final VoidCallback? onTap; /// Optional action shown in the money band, e.g. an Apply button. final Widget? trailingAction; final bool showDate; (String, ChipTone) get _status => switch (shift.status) { ShiftStatus.open => ('Open', ChipTone.blue), ShiftStatus.booked => (Fmt.untilStart(shift.start), ChipTone.yellow), ShiftStatus.active => ('On shift', ChipTone.yellow), ShiftStatus.completed => ('Completed', ChipTone.neutral), ShiftStatus.cancelled => ('Cancelled', ChipTone.muted), ShiftStatus.noShow => ('No show', ChipTone.danger), }; Color get _railColor => switch (shift.status) { ShiftStatus.active => KrowPalette.yellow, ShiftStatus.cancelled || ShiftStatus.noShow => const Color(0xFFCFDDD3), _ => KrowPalette.blue, }; @override Widget build(BuildContext context) { final (statusLabel, statusTone) = _status; final unpaid = shift.status == ShiftStatus.cancelled || shift.status == ShiftStatus.noShow; return Material( color: Colors.white, borderRadius: BorderRadius.circular(18), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(18), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(18), border: Border.all(color: KrowPalette.line), ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ BusinessBadge(shift.logoLetter), const SizedBox(width: 13), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( shift.role, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: -0.5, height: 1.15, ), ), const SizedBox(height: 3), Text( shift.venue, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12.5, color: KrowPalette.mute, ), ), ], ), ), const SizedBox(width: 8), StatusChip(statusLabel, tone: statusTone), ], ), const SizedBox(height: 12), Wrap( spacing: 7, runSpacing: 7, children: [ if (showDate) MetaPill( LucideIcons.calendar, Fmt.dayShort(shift.start), ), MetaPill( LucideIcons.clock, Fmt.window(shift.start, shift.end), ), MetaPill( LucideIcons.mapPin, '${shift.distanceMiles} mi', ), ], ), ], ), ), const Divider(height: 1), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: Row( children: [ Expanded( child: Text( unpaid ? 'No pay generated' : '${Fmt.money(shift.rate)}/hr · ' '${Fmt.duration(shift.scheduledDuration)}', style: const TextStyle( fontSize: 11.5, fontWeight: FontWeight.w600, letterSpacing: 0.2, color: Color(0xFF949FA5), ), ), ), if (trailingAction != null) trailingAction! else Text( Fmt.money(shift.pay), style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: -0.7, color: unpaid ? const Color(0xFF949FA5) : KrowPalette.ink, ), ), ], ), ), Container(height: 4, color: _railColor), ], ), ), ), ); } }