Files
krow-worker-app/lib/widgets/shift_card.dart
2026-09-17 22:26:11 +05:30

171 lines
6.1 KiB
Dart

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),
],
),
),
),
);
}
}