feat(staff): Refactor Shift Cards & Integrate Google Maps
Refactors MyShiftCard to match prototype design with expandable details, bold typography, and Google Static Maps integration. Updates AppConfig for API keys.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU"
|
"GOOGLE_PLACES_API_KEY": "AIzaSyAS9yTf4q51_CNSZ7mbmeS9V3l_LZR80lU",
|
||||||
}
|
"GOOGLE_MAPS_API_KEY": "AIzaSyAyRS9I4xxoVPAX91RJvWJHszB3ZY3-IC0"
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,40 +579,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 +674,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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,27 +4,39 @@ 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 'shift_location_map.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
|
||||||
State<MyShiftCard> createState() => _MyShiftCardState();
|
State<MyShiftCard> createState() => _MyShiftCardState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MyShiftCardState extends State<MyShiftCard> {
|
class _MyShiftCardState extends State<MyShiftCard> with TickerProviderStateMixin {
|
||||||
|
bool _isExpanded = false;
|
||||||
|
|
||||||
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 +77,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,38 +103,43 @@ 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;
|
// Assuming t.staff_shifts.status.* exists as per previous file content
|
||||||
statusColor = UiColors.textLink;
|
try {
|
||||||
statusBg = UiColors.primary;
|
if (status == 'confirmed') {
|
||||||
} else if (status == 'checked_in') {
|
statusText = t.staff_shifts.status.confirmed;
|
||||||
statusText = 'Checked in';
|
statusColor = UiColors.textLink;
|
||||||
statusColor = UiColors.textSuccess;
|
statusBg = UiColors.primary;
|
||||||
statusBg = UiColors.iconSuccess;
|
} else if (status == 'checked_in') {
|
||||||
} else if (status == 'pending' || status == 'open') {
|
statusText = 'Checked in';
|
||||||
statusText = t.staff_shifts.status.act_now;
|
statusColor = UiColors.textSuccess;
|
||||||
statusColor = UiColors.destructive;
|
statusBg = UiColors.iconSuccess;
|
||||||
statusBg = UiColors.destructive;
|
} else if (status == 'pending' || status == 'open') {
|
||||||
} else if (status == 'swap') {
|
statusText = t.staff_shifts.status.act_now;
|
||||||
statusText = t.staff_shifts.status.swap_requested;
|
statusColor = UiColors.destructive;
|
||||||
statusColor = UiColors.textWarning;
|
statusBg = UiColors.destructive;
|
||||||
statusBg = UiColors.textWarning;
|
} else if (status == 'swap') {
|
||||||
statusIcon = UiIcons.swap;
|
statusText = t.staff_shifts.status.swap_requested;
|
||||||
} else if (status == 'completed') {
|
statusColor = UiColors.textWarning;
|
||||||
statusText = t.staff_shifts.status.completed;
|
statusBg = UiColors.textWarning;
|
||||||
statusColor = UiColors.textSuccess;
|
statusIcon = UiIcons.swap;
|
||||||
statusBg = UiColors.iconSuccess;
|
} else if (status == 'completed') {
|
||||||
} else if (status == 'no_show') {
|
statusText = t.staff_shifts.status.completed;
|
||||||
statusText = t.staff_shifts.status.no_show;
|
statusColor = UiColors.textSuccess;
|
||||||
statusColor = UiColors.destructive;
|
statusBg = UiColors.iconSuccess;
|
||||||
statusBg = UiColors.destructive;
|
} else if (status == 'no_show') {
|
||||||
|
statusText = t.staff_shifts.status.no_show;
|
||||||
|
statusColor = UiColors.destructive;
|
||||||
|
statusBg = UiColors.destructive;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
statusText = status?.toUpperCase() ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () => setState(() => _isExpanded = !_isExpanded),
|
||||||
Modular.to.pushShiftDetails(widget.shift);
|
child: AnimatedContainer(
|
||||||
},
|
duration: const Duration(milliseconds: 300),
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.white,
|
color: UiColors.white,
|
||||||
@@ -156,8 +178,8 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
width: UiConstants.radiusMdValue,
|
width: 8,
|
||||||
height: UiConstants.radiusMdValue,
|
height: 8,
|
||||||
margin: const EdgeInsets.only(right: UiConstants.space2),
|
margin: const EdgeInsets.only(right: UiConstants.space2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: statusBg,
|
color: statusBg,
|
||||||
@@ -304,12 +326,20 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space1),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Text(
|
// Mock loop for demo purposes, as we don't have all schedule dates in the model
|
||||||
"Showing first schedule...",
|
// In real app, we might need to fetch schedule or iterate if model changes
|
||||||
style: UiTypography.footnote2r.copyWith(
|
Padding(
|
||||||
color: UiColors.primary,
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
),
|
child: Text(
|
||||||
|
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} – ${_formatTime(widget.shift.endTime)}',
|
||||||
|
style: UiTypography.footnote2r.copyWith(color: UiColors.primary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.shift.durationDays! > 1)
|
||||||
|
Text(
|
||||||
|
'... +${widget.shift.durationDays! - 1} more days',
|
||||||
|
style: UiTypography.footnote2r.copyWith(color: UiColors.primary.withOpacity(0.7)),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
@@ -370,9 +400,336 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Expanded Content
|
||||||
|
AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: _isExpanded
|
||||||
|
? Column(
|
||||||
|
children: [
|
||||||
|
const Divider(height: 1, color: UiColors.border),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Stats Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.dollar,
|
||||||
|
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||||
|
"Total",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.dollar,
|
||||||
|
"\$${widget.shift.hourlyRate}",
|
||||||
|
"Hourly Rate",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
UiIcons.clock,
|
||||||
|
"${duration}",
|
||||||
|
"Hours",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
|
||||||
|
// In/Out Time
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
"CLOCK IN TIME",
|
||||||
|
widget.shift.startTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeBox(
|
||||||
|
"CLOCK OUT TIME",
|
||||||
|
widget.shift.endTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
|
||||||
|
// Location
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"LOCATION",
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
letterSpacing: 0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.shift.location.isEmpty
|
||||||
|
? "TBD"
|
||||||
|
: widget.shift.location,
|
||||||
|
style: UiTypography.title1m.textPrimary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
widget.shift.locationAddress ??
|
||||||
|
widget.shift.location,
|
||||||
|
),
|
||||||
|
duration: const Duration(
|
||||||
|
seconds: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.navigation,
|
||||||
|
size: UiConstants.iconXs,
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
"Get direction",
|
||||||
|
),
|
||||||
|
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: widget.shift,
|
||||||
|
height: 128,
|
||||||
|
borderRadius: UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
|
||||||
|
// Additional Info
|
||||||
|
if (widget.shift.description != null) ...[
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"ADDITIONAL INFO",
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
letterSpacing: 0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
widget.shift.description!,
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space5),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
if (!widget.historyMode)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: UiConstants.space2),
|
||||||
|
child: _buildActions(status),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildActions(String? status) {
|
||||||
|
if (status == 'confirmed') {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: widget.onRequestSwap,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.swap,
|
||||||
|
size: UiConstants.iconSm,
|
||||||
|
),
|
||||||
|
label: const Text("Request Swap"),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: UiColors.primary,
|
||||||
|
side: const BorderSide(
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
UiConstants.radiusBase,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (status == 'swap') {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.tagPending,
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.textWarning,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.swap,
|
||||||
|
size: UiConstants.iconSm,
|
||||||
|
color: UiColors.textWarning,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
"Swap Pending",
|
||||||
|
style: UiTypography.body2b.copyWith(
|
||||||
|
color: UiColors.textWarning,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// status == 'open' || status == 'pending' or others
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: widget.onAccept,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: widget.onAccept == null
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)
|
||||||
|
) // Loading state if callback null? or just Text
|
||||||
|
: const Text(
|
||||||
|
"Book Shift",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatCard(IconData icon, String value, String label) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.background,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 20, color: UiColors.textSecondary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeBox(String label, String time) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.background,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.footnote2b.copyWith(
|
||||||
|
color: UiColors.textSecondary, letterSpacing: 0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
_formatTime(time),
|
||||||
|
style: UiTypography.title1m.copyWith(fontWeight: FontWeight.w700).textPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user