Merge pull request #421 from Oloodi/feature/centralized-data-error-handling

Pull Request: feature(staff): Refactor Shift Cards & Integrate Google Maps
This commit is contained in:
Achintha Isuru
2026-02-16 10:53:32 -05:00
committed by GitHub
7 changed files with 507 additions and 417 deletions

View File

@@ -1,3 +1,4 @@
{ {
"GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU" "GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU",
} "GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0"
}

View File

@@ -6,4 +6,7 @@ class AppConfig {
/// The Google Places API key used for address autocomplete functionality. /// The Google Places API key used for address autocomplete functionality.
static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY'); static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY');
/// The Google Maps Static API key used for location preview images.
static const String googleMapsApiKey = String.fromEnvironment('GOOGLE_MAPS_API_KEY');
} }

View File

@@ -1,5 +1,5 @@
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
@@ -10,6 +10,7 @@ import 'package:krow_domain/krow_domain.dart';
import '../blocs/shift_details/shift_details_bloc.dart'; import '../blocs/shift_details/shift_details_bloc.dart';
import '../blocs/shift_details/shift_details_event.dart'; import '../blocs/shift_details/shift_details_event.dart';
import '../blocs/shift_details/shift_details_state.dart'; import '../blocs/shift_details/shift_details_state.dart';
import '../widgets/shift_location_map.dart';
class ShiftDetailsPage extends StatefulWidget { class ShiftDetailsPage extends StatefulWidget {
final String shiftId; final String shiftId;
@@ -65,10 +66,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
Widget _buildStatCard(IconData icon, String value, String label) { Widget _buildStatCard(IconData icon, String value, String label) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.background, color: UiColors.background,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(color: UiColors.border), border: Border.all(color: UiColors.border),
), ),
child: Column( child: Column(
@@ -80,12 +81,12 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
color: UiColors.white, color: UiColors.white,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(icon, size: 20, color: UiColors.iconSecondary), child: Icon(icon, size: 20, color: UiColors.textSecondary),
), ),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
Text( Text(
value, value,
style: UiTypography.title1m.textPrimary, style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
), ),
Text( Text(
label, label,
@@ -98,21 +99,22 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
Widget _buildTimeBox(String label, String time) { Widget _buildTimeBox(String label, String time) {
return Container( return Container(
padding: const EdgeInsets.all(UiConstants.space4), padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.background, color: UiColors.background,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
), ),
child: Column( child: Column(
children: [ children: [
Text( Text(
label, label,
style: UiTypography.titleUppercase4b.textSecondary, style: UiTypography.footnote2b.copyWith(
color: UiColors.textSecondary, letterSpacing: 0.5),
), ),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Text( Text(
_formatTime(time), _formatTime(time),
style: UiTypography.headline2m.textPrimary, style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
), ),
], ],
), ),
@@ -267,45 +269,49 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
// Worker Capacity / Open Slots // Stats Row (New)
if ((displayShift.requiredSlots ?? 0) > 0) Row(
Container( children: [
padding: const EdgeInsets.all(UiConstants.space4), Expanded(
decoration: BoxDecoration( child: _buildStatCard(
color: UiColors.success.withValues(alpha: 0.1), UiIcons.dollar,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), "\$${estimatedTotal.toStringAsFixed(0)}",
"Total",
),
), ),
child: Row( const SizedBox(width: UiConstants.space4),
children: [ Expanded(
const Icon( child: _buildStatCard(
UiIcons.users, UiIcons.dollar,
size: 16, "\$${displayShift.hourlyRate.toStringAsFixed(0)}",
color: UiColors.success, "Hourly Rate",
), ),
const SizedBox(width: UiConstants.space2),
Text(
i18n.slots_remaining(count: openSlots),
style: UiTypography.footnote1m.textSuccess,
),
],
), ),
), const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildStatCard(
UiIcons.clock,
"${duration.toStringAsFixed(1)}",
"Hours",
),
),
],
),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
// Time Section // Time Section (New)
Row( Row(
children: [ children: [
Expanded( Expanded(
child: _buildTimeBox( child: _buildTimeBox(
i18n.start_time, "CLOCK IN TIME",
displayShift.startTime, displayShift.startTime,
), ),
), ),
const SizedBox(width: UiConstants.space4), const SizedBox(width: UiConstants.space4),
Expanded( Expanded(
child: _buildTimeBox( child: _buildTimeBox(
i18n.end_time, "CLOCK OUT TIME",
displayShift.endTime, displayShift.endTime,
), ),
), ),
@@ -313,97 +319,79 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
const SizedBox(height: UiConstants.space6), const SizedBox(height: UiConstants.space6),
// Quick Info Grid
Row(
children: [
Expanded(
child: _buildStatCard(
UiIcons.dollar,
"\$${displayShift.hourlyRate.toStringAsFixed(0)}/hr",
i18n.base_rate,
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildStatCard(
UiIcons.clock,
i18n.hours_label(count: duration.toInt()),
i18n.duration,
),
),
const SizedBox(width: UiConstants.space4),
Expanded(
child: _buildStatCard(
UiIcons.wallet,
"\$${estimatedTotal.toStringAsFixed(0)}",
i18n.est_total,
),
),
],
),
const SizedBox(height: UiConstants.space8),
// Location Section // Location Section (New with Map)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
i18n.location, "LOCATION",
style: UiTypography.titleUppercase4b.textSecondary, style: UiTypography.titleUppercase4b.textSecondary,
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
Container( Row(
padding: const EdgeInsets.all(UiConstants.space4), mainAxisAlignment:
decoration: BoxDecoration( MainAxisAlignment.spaceBetween,
color: UiColors.white, children: [
borderRadius: BorderRadius.circular(UiConstants.radiusBase), Expanded(
border: Border.all(color: UiColors.border), child: Text(
), displayShift.location.isEmpty
child: Column( ? "TBD"
children: [ : displayShift.location,
Row( style: UiTypography.title1m.textPrimary,
children: [ overflow: TextOverflow.ellipsis,
const Icon( ),
UiIcons.mapPin, ),
color: UiColors.primary, const SizedBox(width: UiConstants.space3),
size: 20, OutlinedButton.icon(
), onPressed: () {
const SizedBox(width: UiConstants.space3), ScaffoldMessenger.of(
Expanded( context,
child: Column( ).showSnackBar(
crossAxisAlignment: SnackBar(
CrossAxisAlignment.start, content: Text(
children: [ displayShift!.locationAddress.isNotEmpty
Text( ? displayShift!.locationAddress
displayShift.location, : displayShift!.location,
style: UiTypography.body2b.textPrimary, ),
), duration: const Duration(
Text( seconds: 3,
displayShift.locationAddress,
style: UiTypography.body3r.textSecondary,
),
],
), ),
), ),
], );
},
icon: const Icon(
UiIcons.navigation,
size: UiConstants.iconXs,
), ),
const SizedBox(height: UiConstants.space4), label: const Text(
const Divider(), "Get direction",
const SizedBox(height: UiConstants.space2),
TextButton.icon(
onPressed: () {},
icon: const Icon(
UiIcons.arrowRight,
size: 16,
),
label: Text(i18n.open_in_maps),
style: TextButton.styleFrom(
foregroundColor: UiColors.primary,
padding: EdgeInsets.zero,
),
), ),
], style: OutlinedButton.styleFrom(
), foregroundColor:
UiColors.textPrimary,
side: const BorderSide(
color: UiColors.border,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
),
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
vertical: 0,
),
minimumSize: const Size(0, 32),
),
),
],
),
const SizedBox(height: UiConstants.space3),
ShiftLocationMap(
shift: displayShift,
height: 160,
borderRadius: UiConstants.radiusBase,
), ),
], ],
), ),
@@ -579,40 +567,21 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
final i18n = Translations.of(context).staff_shifts.shift_details; final i18n = Translations.of(context).staff_shifts.shift_details;
if (status == 'confirmed') { if (status == 'confirmed') {
return Row( return SizedBox(
children: [ width: double.infinity,
Expanded( child: ElevatedButton(
child: ElevatedButton( onPressed: () => Modular.to.toClockIn(),
onPressed: () => _openCancelDialog(context), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: UiColors.success,
backgroundColor: UiColors.destructive, foregroundColor: UiColors.white,
foregroundColor: UiColors.white, padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
elevation: 0,
),
child: Text(i18n.cancel_shift, style: UiTypography.body2b.white),
), ),
elevation: 0,
), ),
const SizedBox(width: UiConstants.space4), child: Text(i18n.clock_in, style: UiTypography.body2b.white),
Expanded( ),
child: ElevatedButton(
onPressed: () => Modular.to.toClockIn(),
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.success,
foregroundColor: UiColors.white,
padding: const EdgeInsets.symmetric(vertical: UiConstants.space4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
elevation: 0,
),
child: Text(i18n.clock_in, style: UiTypography.body2b.white),
),
),
],
); );
} }
@@ -693,32 +662,4 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
return const SizedBox(); return const SizedBox();
} }
void _openCancelDialog(BuildContext context) {
final i18n = Translations.of(context).staff_shifts.shift_details.cancel_dialog;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(i18n.title),
content: Text(i18n.message),
actions: [
TextButton(
onPressed: () => Modular.to.pop(),
child: Text(Translations.of(context).common.cancel),
),
TextButton(
onPressed: () {
Modular.to.pop();
BlocProvider.of<ShiftDetailsBloc>(context).add(
DeclineShiftDetailsEvent(widget.shiftId),
);
},
style: TextButton.styleFrom(
foregroundColor: UiColors.destructive,
),
child: Text(Translations.of(context).common.ok),
),
],
),
);
}
} }

View File

@@ -4,14 +4,22 @@ import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart'; // For modular navigation
class MyShiftCard extends StatefulWidget { class MyShiftCard extends StatefulWidget {
final Shift shift; final Shift shift;
final bool historyMode;
final VoidCallback? onAccept;
final VoidCallback? onDecline;
final VoidCallback? onRequestSwap;
const MyShiftCard({ const MyShiftCard({
super.key, super.key,
required this.shift, required this.shift,
this.historyMode = false,
this.onAccept,
this.onDecline,
this.onRequestSwap,
}); });
@override @override
@@ -19,12 +27,14 @@ class MyShiftCard extends StatefulWidget {
} }
class _MyShiftCardState extends State<MyShiftCard> { class _MyShiftCardState extends State<MyShiftCard> {
String _formatTime(String time) { String _formatTime(String time) {
if (time.isEmpty) return ''; if (time.isEmpty) return '';
try { try {
final parts = time.split(':'); final parts = time.split(':');
final hour = int.parse(parts[0]); final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]); final minute = int.parse(parts[1]);
// Date doesn't matter for time formatting
final dt = DateTime(2022, 1, 1, hour, minute); final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mm a').format(dt); return DateFormat('h:mm a').format(dt);
} catch (e) { } catch (e) {
@@ -65,13 +75,18 @@ class _MyShiftCardState extends State<MyShiftCard> {
} }
String _getShiftType() { String _getShiftType() {
if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) { // Handling potential localization key availability
return t.staff_shifts.filter.long_term; try {
if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) {
return t.staff_shifts.filter.long_term;
}
if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) {
return t.staff_shifts.filter.multi_day;
}
return t.staff_shifts.filter.one_day;
} catch (_) {
return "One Day";
} }
if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) {
return t.staff_shifts.filter.multi_day;
}
return t.staff_shifts.filter.one_day;
} }
@override @override
@@ -86,36 +101,44 @@ class _MyShiftCardState extends State<MyShiftCard> {
String statusText = ''; String statusText = '';
IconData? statusIcon; IconData? statusIcon;
if (status == 'confirmed') { // Fallback localization if keys missing
statusText = t.staff_shifts.status.confirmed; try {
statusColor = UiColors.textLink; if (status == 'confirmed') {
statusBg = UiColors.primary; statusText = t.staff_shifts.status.confirmed;
} else if (status == 'checked_in') { statusColor = UiColors.textLink;
statusText = 'Checked in'; statusBg = UiColors.primary;
statusColor = UiColors.textSuccess; } else if (status == 'checked_in') {
statusBg = UiColors.iconSuccess; statusText = 'Checked in';
} else if (status == 'pending' || status == 'open') { statusColor = UiColors.textSuccess;
statusText = t.staff_shifts.status.act_now; statusBg = UiColors.iconSuccess;
statusColor = UiColors.destructive; } else if (status == 'pending' || status == 'open') {
statusBg = UiColors.destructive; statusText = t.staff_shifts.status.act_now;
} else if (status == 'swap') { statusColor = UiColors.destructive;
statusText = t.staff_shifts.status.swap_requested; statusBg = UiColors.destructive;
statusColor = UiColors.textWarning; } else if (status == 'swap') {
statusBg = UiColors.textWarning; statusText = t.staff_shifts.status.swap_requested;
statusIcon = UiIcons.swap; statusColor = UiColors.textWarning;
} else if (status == 'completed') { statusBg = UiColors.textWarning;
statusText = t.staff_shifts.status.completed; statusIcon = UiIcons.swap;
statusColor = UiColors.textSuccess; } else if (status == 'completed') {
statusBg = UiColors.iconSuccess; statusText = t.staff_shifts.status.completed;
} else if (status == 'no_show') { statusColor = UiColors.textSuccess;
statusText = t.staff_shifts.status.no_show; statusBg = UiColors.iconSuccess;
statusColor = UiColors.destructive; } else if (status == 'no_show') {
statusBg = UiColors.destructive; statusText = t.staff_shifts.status.no_show;
statusColor = UiColors.destructive;
statusBg = UiColors.destructive;
}
} catch (_) {
statusText = status?.toUpperCase() ?? "";
} }
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Modular.to.pushShiftDetails(widget.shift); Modular.to.pushNamed(
StaffPaths.shiftDetails(widget.shift.id),
arguments: widget.shift,
);
}, },
child: Container( child: Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3), margin: const EdgeInsets.only(bottom: UiConstants.space3),
@@ -131,246 +154,245 @@ class _MyShiftCardState extends State<MyShiftCard> {
), ),
], ],
), ),
child: Column( child: Padding(
children: [ padding: const EdgeInsets.all(UiConstants.space4),
// Collapsed Content child: Column(
Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.all(UiConstants.space4), children: [
child: Column( // Status Badge
crossAxisAlignment: CrossAxisAlignment.start, if (statusText.isNotEmpty)
children: [ Padding(
// Status Badge padding: const EdgeInsets.only(bottom: UiConstants.space2),
if (statusText.isNotEmpty) child: Row(
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Row(
children: [
if (statusIcon != null)
Padding(
padding: const EdgeInsets.only(right: UiConstants.space2),
child: Icon(
statusIcon,
size: UiConstants.iconXs,
color: statusColor,
),
)
else
Container(
width: UiConstants.radiusMdValue,
height: UiConstants.radiusMdValue,
margin: const EdgeInsets.only(right: UiConstants.space2),
decoration: BoxDecoration(
color: statusBg,
shape: BoxShape.circle,
),
),
Text(
statusText,
style: UiTypography.footnote2b.copyWith(
color: statusColor,
letterSpacing: 0.5,
),
),
// Shift Type Badge for available/pending shifts
if (status == 'open' || status == 'pending') ...[
const SizedBox(width: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: 2,
),
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1),
borderRadius: UiConstants.radiusSm,
),
child: Text(
_getShiftType(),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
),
],
],
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Logo if (statusIcon != null)
Container( Padding(
width: 44, padding: const EdgeInsets.only(right: UiConstants.space2),
height: 44, child: Icon(
decoration: BoxDecoration( statusIcon,
gradient: LinearGradient( size: UiConstants.iconXs,
colors: [ color: statusColor,
UiColors.primary.withValues(alpha: 0.09),
UiColors.primary.withValues(alpha: 0.03),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(UiConstants.radiusBase), )
border: Border.all( else
color: UiColors.primary.withValues(alpha: 0.09), Container(
width: 8,
height: 8,
margin: const EdgeInsets.only(right: UiConstants.space2),
decoration: BoxDecoration(
color: statusBg,
shape: BoxShape.circle,
), ),
), ),
child: widget.shift.logoUrl != null Text(
? ClipRRect( statusText,
borderRadius: BorderRadius.circular(UiConstants.radiusBase), style: UiTypography.footnote2b.copyWith(
child: Image.network( color: statusColor,
widget.shift.logoUrl!, letterSpacing: 0.5,
fit: BoxFit.contain, ),
),
)
: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: UiConstants.iconMd,
),
),
), ),
const SizedBox(width: UiConstants.space3), // Shift Type Badge
if (status == 'open' || status == 'pending') ...[
const SizedBox(width: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: 2,
),
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.1),
borderRadius: UiConstants.radiusSm,
),
child: Text(
_getShiftType(),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
),
],
],
),
),
// Details Row(
Expanded( crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, // Logo
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
UiColors.primary.withValues(alpha: 0.09),
UiColors.primary.withValues(alpha: 0.03),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: UiColors.primary.withValues(alpha: 0.09),
),
),
child: widget.shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
child: Image.network(
widget.shift.logoUrl!,
fit: BoxFit.contain,
),
)
: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: UiConstants.iconMd,
),
),
),
const SizedBox(width: UiConstants.space3),
// Consensed Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Expanded(
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Column(
children: [ crossAxisAlignment:
Expanded( CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: Text(
CrossAxisAlignment.start, widget.shift.title,
children: [ style: UiTypography.body2m.textPrimary,
Text( overflow: TextOverflow.ellipsis,
widget.shift.title,
style: UiTypography.body2m.textPrimary,
overflow: TextOverflow.ellipsis,
),
Text(
widget.shift.clientName,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
),
],
), ),
Text(
widget.shift.clientName,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: UiConstants.space2),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"\$${estimatedTotal.toStringAsFixed(0)}",
style: UiTypography.title1m.textPrimary,
), ),
const SizedBox(width: UiConstants.space2), Text(
Column( "\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h",
crossAxisAlignment: CrossAxisAlignment.end, style: UiTypography.footnote2r.textSecondary,
children: [
Text(
"\$${estimatedTotal.toStringAsFixed(0)}",
style: UiTypography.title1m.textPrimary,
),
Text(
"\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h",
style: UiTypography.footnote2r.textSecondary,
),
],
), ),
], ],
), ),
const SizedBox(height: UiConstants.space2), ],
),
const SizedBox(height: UiConstants.space2),
// Date & Time - Multi-Day or Single Day // Date & Time
if (widget.shift.durationDays != null && if (widget.shift.durationDays != null &&
widget.shift.durationDays! > 1) ...[ widget.shift.durationDays! > 1) ...[
// Multi-Day Schedule Display Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(
children: [ children: [
Row( const Icon(
children: [ UiIcons.clock,
const Icon( size: UiConstants.iconXs,
UiIcons.clock, color: UiColors.primary,
size: UiConstants.iconXs,
color: UiColors.primary,
),
const SizedBox(width: UiConstants.space1),
Text(
t.staff_shifts.details.days(
days: widget.shift.durationDays!,
),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
],
), ),
const SizedBox(height: UiConstants.space1), const SizedBox(width: UiConstants.space1),
Text( Text(
"Showing first schedule...", t.staff_shifts.details.days(
style: UiTypography.footnote2r.copyWith( days: widget.shift.durationDays!,
),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary, color: UiColors.primary,
), ),
), ),
], ],
), ),
] else ...[ const SizedBox(height: UiConstants.space1),
// Single Day Display Padding(
Row( padding: const EdgeInsets.only(bottom: 2),
children: [ child: Text(
const Icon( '${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} ${_formatTime(widget.shift.endTime)}',
UiIcons.calendar, style: UiTypography.footnote2r.copyWith(color: UiColors.primary),
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
), ),
const SizedBox(width: UiConstants.space1), ),
if (widget.shift.durationDays! > 1)
Text( Text(
_formatDate(widget.shift.date), '... +${widget.shift.durationDays! - 1} more days',
style: UiTypography.footnote1r.textSecondary, style: UiTypography.footnote2r.copyWith(color: UiColors.primary.withOpacity(0.7)),
), )
const SizedBox(width: UiConstants.space3), ],
const Icon( ),
UiIcons.clock, ] else ...[
size: UiConstants.iconXs, Row(
color: UiColors.iconSecondary, children: [
), const Icon(
const SizedBox(width: UiConstants.space1), UiIcons.calendar,
Text( size: UiConstants.iconXs,
"${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}", color: UiColors.iconSecondary,
style: UiTypography.footnote1r.textSecondary, ),
), const SizedBox(width: UiConstants.space1),
], Text(
_formatDate(widget.shift.date),
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
"${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}",
style: UiTypography.footnote1r.textSecondary,
), ),
], ],
const SizedBox(height: UiConstants.space1), ),
],
const SizedBox(height: UiConstants.space1),
// Location // Location
Row( Row(
children: [ children: [
const Icon( const Icon(
UiIcons.mapPin, UiIcons.mapPin,
size: UiConstants.iconXs, size: UiConstants.iconXs,
color: UiColors.iconSecondary, color: UiColors.iconSecondary,
), ),
const SizedBox(width: UiConstants.space1), const SizedBox(width: UiConstants.space1),
Expanded( Expanded(
child: Text( child: Text(
widget.shift.locationAddress.isNotEmpty widget.shift.locationAddress.isNotEmpty
? widget.shift.locationAddress ? widget.shift.locationAddress
: widget.shift.location, : widget.shift.location,
style: UiTypography.footnote1r.textSecondary, style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
),
],
), ),
], ],
), ),
), ],
], ),
), ),
], ],
), ),
), ],
], ),
), ),
), ),
); );

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:krow_core/core.dart'; // Import AppConfig from krow_core
class ShiftLocationMap extends StatelessWidget {
final Shift shift;
final double height;
final double borderRadius;
const ShiftLocationMap({
super.key,
required this.shift,
this.height = 120,
this.borderRadius = 8,
});
@override
Widget build(BuildContext context) {
if (AppConfig.googleMapsApiKey.isEmpty) {
return _buildPlaceholder(context, "Config Map Key");
}
final String mapUrl = _generateStaticMapUrl();
return Container(
height: height,
width: double.infinity,
decoration: BoxDecoration(
color: UiColors.background,
borderRadius: BorderRadius.circular(borderRadius),
),
clipBehavior: Clip.antiAlias,
child: Image.network(
mapUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholder(context, "Map unavailable");
},
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
),
);
}
String _generateStaticMapUrl() {
// Base URL
const String baseUrl = "https://maps.googleapis.com/maps/api/staticmap";
// Parameters
String center;
if (shift.latitude != null && shift.longitude != null) {
center = "${shift.latitude},${shift.longitude}";
} else {
center = Uri.encodeComponent(shift.locationAddress.isNotEmpty
? shift.locationAddress
: shift.location);
}
// Construct URL
// scale=2 for retina displays
return "$baseUrl?center=$center&zoom=15&size=600x300&maptype=roadmap&markers=color:red%7C$center&key=${AppConfig.googleMapsApiKey}&scale=2";
}
Widget _buildPlaceholder(BuildContext context, String message) {
return Container(
height: height,
width: double.infinity,
decoration: BoxDecoration(
color: UiColors.secondary,
borderRadius: BorderRadius.circular(borderRadius),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
UiIcons.mapPin,
size: 32,
color: UiColors.iconSecondary,
),
if (message.isNotEmpty) ...[
const SizedBox(height: UiConstants.space2),
Text(
message,
style: UiTypography.footnote2r.textSecondary,
),
],
],
),
),
);
}
}

View File

@@ -2,6 +2,8 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../blocs/shifts/shifts_bloc.dart';
import '../my_shift_card.dart'; import '../my_shift_card.dart';
import '../shared/empty_state_view.dart'; import '../shared/empty_state_view.dart';
@@ -171,6 +173,14 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
padding: const EdgeInsets.only(bottom: UiConstants.space3), padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: MyShiftCard( child: MyShiftCard(
shift: shift, shift: shift,
onAccept: () {
context.read<ShiftsBloc>().add(AcceptShiftEvent(shift.id));
UiSnackbar.show(
context,
message: "Shift application submitted!", // Todo: Localization
type: UiSnackbarType.success,
);
},
), ),
), ),
), ),

View File

@@ -382,7 +382,17 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
...visibleMyShifts.map( ...visibleMyShifts.map(
(shift) => Padding( (shift) => Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3), padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: MyShiftCard(shift: shift), child: MyShiftCard(
shift: shift,
onDecline: () => _declineShift(shift.id),
onRequestSwap: () {
UiSnackbar.show(
context,
message: "Swap functionality coming soon!", // Todo: Localization
type: UiSnackbarType.message,
);
},
),
), ),
), ),
], ],