feat: Add required and filled slots to Shift entity and update ShiftDetailsPage for capacity display

This commit is contained in:
Achintha Isuru
2026-01-31 21:48:42 -05:00
parent 144976de00
commit 820f475c51
3 changed files with 349 additions and 207 deletions

View File

@@ -24,6 +24,8 @@ class Shift extends Equatable {
final double? longitude;
final String? status;
final int? durationDays; // For multi-day shifts
final int? requiredSlots;
final int? filledSlots;
const Shift({
required this.id,
@@ -49,6 +51,8 @@ class Shift extends Equatable {
this.longitude,
this.status,
this.durationDays,
this.requiredSlots,
this.filledSlots,
});
@override
@@ -76,6 +80,8 @@ class Shift extends Equatable {
longitude,
status,
durationDays,
requiredSlots,
filledSlots,
];
}

View File

@@ -127,6 +127,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
status: _mapStatus(status),
description: shift.description,
durationDays: shift.durationDays,
requiredSlots: shift.requiredSlots,
filledSlots: shift.filledSlots,
));
}
}
@@ -182,6 +184,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
status: s.status?.stringValue.toLowerCase() ?? 'open',
description: s.description,
durationDays: s.durationDays,
requiredSlots: null, // Basic list doesn't fetch detailed role stats yet
filledSlots: null,
));
}
@@ -210,6 +214,20 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
final s = result.data.shift;
if (s == null) return null;
int? required;
int? filled;
try {
final rolesRes = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for(var r in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
}
} catch (_) {}
final startDt = _toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt);
@@ -229,6 +247,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
status: s.status?.stringValue ?? 'OPEN',
description: s.description,
durationDays: s.durationDays,
requiredSlots: required,
filledSlots: filled,
);
} catch (e) {
return null;

View File

@@ -3,22 +3,16 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic
import 'package:intl/intl.dart';
import 'package:intl/intl.dart';
import '../blocs/shift_details/shift_details_bloc.dart';
import '../blocs/shift_details/shift_details_event.dart';
import '../blocs/shift_details/shift_details_state.dart';
import '../styles/shifts_styles.dart';
import '../widgets/my_shift_card.dart';
class ShiftDetailsPage extends StatelessWidget {
final String shiftId;
final Shift? shift;
const ShiftDetailsPage({
super.key,
required this.shiftId,
this.shift,
});
const ShiftDetailsPage({super.key, required this.shiftId, this.shift});
String _formatTime(String time) {
if (time.isEmpty) return '';
@@ -33,6 +27,16 @@ class ShiftDetailsPage extends StatelessWidget {
}
}
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
return DateFormat('EEEE, MMMM d, y').format(date);
} catch (e) {
return dateStr;
}
}
double _calculateDuration(Shift shift) {
if (shift.startTime.isEmpty || shift.endTime.isEmpty) {
return 0;
@@ -70,9 +74,7 @@ class ShiftDetailsPage extends StatelessWidget {
const SizedBox(height: 8),
Text(
value,
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
style: UiTypography.title1m.copyWith(color: UiColors.textPrimary),
),
Text(
label,
@@ -118,9 +120,9 @@ class ShiftDetailsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => Modular.get<ShiftDetailsBloc>()
..add(LoadShiftDetailsEvent(shiftId)),
return BlocProvider<ShiftDetailsBloc>(
create: (_) =>
Modular.get<ShiftDetailsBloc>()..add(LoadShiftDetailsEvent(shiftId)),
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
listener: (context, state) {
if (state is ShiftActionSuccess) {
@@ -163,213 +165,322 @@ class ShiftDetailsPage extends StatelessWidget {
final duration = _calculateDuration(displayShift);
final estimatedTotal = (displayShift.hourlyRate) * duration;
final openSlots =
(displayShift.requiredSlots ?? 0) -
(displayShift.filledSlots ?? 0);
return Scaffold(
backgroundColor: AppColors.krowBackground,
appBar: AppBar(
title: const Text("Shift Details"),
backgroundColor: Colors.white,
foregroundColor: AppColors.krowCharcoal,
elevation: 0.5,
appBar: UiAppBar(
title: displayShift.title,
showBackButton: true,
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
MyShiftCard(
shift: displayShift,
// No direct actions on the card, handled by page buttons
),
const SizedBox(height: 24),
// Stats Row
Row(
children: [
Expanded(
child: _buildStatCard(
UiIcons.dollar,
"\$${estimatedTotal.toStringAsFixed(0)}",
"Total",
),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Vendor Section
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"VENDOR",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: UiColors.textSecondary,
letterSpacing: 0.5,
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
UiIcons.dollar,
"\$${displayShift.hourlyRate.toInt()}",
"Hourly Rate",
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
UiIcons.clock,
"${duration.toInt()}",
"Hours",
),
),
],
),
const SizedBox(height: 24),
// In/Out Time
Row(
children: [
Expanded(
child: _buildTimeBox(
"CLOCK IN TIME",
displayShift.startTime,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildTimeBox(
"CLOCK OUT TIME",
displayShift.endTime,
),
),
],
),
const SizedBox(height: 24),
// Location
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"LOCATION",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: UiColors.textSecondary,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
displayShift.location.isEmpty
? "TBD"
: displayShift.location,
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
),
OutlinedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(displayShift!.locationAddress),
duration: const Duration(seconds: 3),
),
const SizedBox(height: 8),
Row(
children: [
Container(
width: 24,
height: 24,
child: displayShift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(
6,
),
child: Image.network(
displayShift.logoUrl!,
fit: BoxFit.cover,
),
)
: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: 20,
),
),
);
},
icon: const Icon(UiIcons.navigation, size: 14),
label: const Text(
"Get direction",
style: TextStyle(fontSize: 12),
),
style: OutlinedButton.styleFrom(
foregroundColor: UiColors.textPrimary,
side: const BorderSide(color: UiColors.border),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
minimumSize: const Size(0, 32),
),
),
],
),
const SizedBox(height: 12),
Container(
height: 128,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Icon(
UiIcons.mapPin,
color: UiColors.iconSecondary,
size: 32,
const SizedBox(width: 8),
Text(
displayShift.clientName,
style: UiTypography.headline5m.copyWith(
color: UiColors.textPrimary,
),
),
// Placeholder for Map
),
],
),
const SizedBox(height: 24),
],
),
],
),
const SizedBox(height: 24),
// Additional Info
if (displayShift.description != null) ...[
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"ADDITIONAL INFO",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: UiColors.textSecondary,
letterSpacing: 0.5,
),
// Date Section
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"SHIFT DATE",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: UiColors.textSecondary,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Row(
children: [
const Icon(
UiIcons.calendar,
size: 20,
color: UiColors.primary,
),
const SizedBox(width: 8),
Text(
_formatDate(displayShift.date),
style: UiTypography.headline5m.copyWith(
color: UiColors.textPrimary,
),
const SizedBox(height: 8),
Text(
displayShift.description!,
style: UiTypography.body2m.copyWith(
color: UiColors.textPrimary,
),
),
],
),
],
),
const SizedBox(height: 24),
// Worker Capacity / Open Slots
if ((displayShift.requiredSlots ?? 0) > 0)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF0FDF4), // green-50
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFBBF7D0),
), // green-200
),
child: Row(
children: [
const Icon(
Icons.people_alt_outlined,
size: 20,
color: Color(0xFF15803D),
), // green-700, using Material Icon as generic fallback
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"$openSlots spots remaining",
style: UiTypography.body2b.copyWith(
color: const Color(0xFF15803D),
),
),
Text(
"${displayShift.filledSlots ?? 0} filled out of ${displayShift.requiredSlots}",
style: UiTypography.body3r.copyWith(
color: const Color(0xFF166534),
),
),
],
),
],
),
SizedBox(
width: 60,
child: LinearProgressIndicator(
value: (displayShift.requiredSlots! > 0)
? (displayShift.filledSlots ?? 0) /
displayShift.requiredSlots!
: 0,
backgroundColor: Colors.white,
color: const Color(0xFF15803D),
minHeight: 6,
borderRadius: BorderRadius.circular(3),
),
),
],
),
),
const SizedBox(height: 24),
// Stats Grid
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
crossAxisSpacing: 12,
childAspectRatio: 0.85,
children: [
_buildStatCard(
UiIcons.dollar,
"\$${estimatedTotal.toStringAsFixed(0)}",
"Total Pay",
),
_buildStatCard(
UiIcons.dollar,
"\$${displayShift.hourlyRate.toInt()}",
"Per Hour",
),
_buildStatCard(
UiIcons.clock,
"${duration.toInt()}h",
"Duration",
),
],
),
const SizedBox(height: 24),
// Shift Timing
Row(
children: [
Expanded(
child: _buildTimeBox(
"START TIME",
displayShift.startTime,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildTimeBox(
"END TIME",
displayShift.endTime,
),
),
],
),
const SizedBox(height: 24),
// Location
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"LOCATION",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: UiColors.textSecondary,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayShift.location.isEmpty
? "TBD"
: displayShift.location,
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
),
Text(
displayShift.location.isEmpty
? "TBD"
: displayShift.locationAddress,
style: UiTypography.title1m.copyWith(
color: UiColors.textPrimary,
),
),
],
),
],
),
const SizedBox(height: 24),
// Additional Info
if (displayShift.description != null) ...[
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"ADDITIONAL INFO",
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: UiColors.textSecondary,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Text(
displayShift.description!,
style: UiTypography.body2m.copyWith(
color: UiColors.textPrimary,
),
),
],
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () =>
_declineShift(context, displayShift!.id),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFEF4444),
side: const BorderSide(
color: Color(0xFFEF4444),
),
padding: const EdgeInsets.symmetric(
vertical: 16,
),
),
child: const Text("Decline"),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () =>
_bookShift(context, displayShift!.id),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
),
child: const Text("Book Shift"),
),
),
],
),
SizedBox(
height: MediaQuery.of(context).padding.bottom + 10,
),
],
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => _declineShift(context, displayShift!.id),
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFFEF4444),
side: const BorderSide(color: Color(0xFFEF4444)),
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text("Decline"),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: () => _bookShift(context, displayShift!.id),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF10B981),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text("Book Shift"),
),
),
],
),
SizedBox(height: MediaQuery.of(context).padding.bottom + 10),
],
),
),
],
),
);
},
@@ -392,7 +503,9 @@ class ShiftDetailsPage extends StatelessWidget {
TextButton(
onPressed: () {
Navigator.of(ctx).pop();
BlocProvider.of<ShiftDetailsBloc>(context).add(BookShiftDetailsEvent(id));
BlocProvider.of<ShiftDetailsBloc>(
context,
).add(BookShiftDetailsEvent(id));
},
style: TextButton.styleFrom(
foregroundColor: const Color(0xFF10B981),
@@ -410,7 +523,8 @@ class ShiftDetailsPage extends StatelessWidget {
builder: (ctx) => AlertDialog(
title: const Text('Decline Shift'),
content: const Text(
'Are you sure you want to decline this shift? It will be hidden from your available jobs.'),
'Are you sure you want to decline this shift? It will be hidden from your available jobs.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
@@ -419,7 +533,9 @@ class ShiftDetailsPage extends StatelessWidget {
TextButton(
onPressed: () {
Navigator.of(ctx).pop();
BlocProvider.of<ShiftDetailsBloc>(context).add(DeclineShiftDetailsEvent(id));
BlocProvider.of<ShiftDetailsBloc>(
context,
).add(DeclineShiftDetailsEvent(id));
},
style: TextButton.styleFrom(
foregroundColor: const Color(0xFFEF4444),