Initial commit: Krow Worker App
This commit is contained in:
170
lib/widgets/shift_card.dart
Normal file
170
lib/widgets/shift_card.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user