feat: enhance ShiftDetailsPage with manager contact details and shift information display

This commit is contained in:
Achintha Isuru
2026-01-25 16:28:30 -05:00
parent 8e429dda03
commit d37e1f7093
2 changed files with 541 additions and 97 deletions

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:intl/intl.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
@@ -30,6 +32,12 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
bool _showDetails = true; bool _showDetails = true;
bool _isApplying = false; bool _isApplying = false;
// Mock Managers
final List<Map<String, String>> _managers = [
{'name': 'John Smith', 'phone': '+1 123 456 7890'},
{'name': 'Jane Doe', 'phone': '+1 123 456 7890'},
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -41,24 +49,59 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
_shift = widget.shift!; _shift = widget.shift!;
setState(() => _isLoading = false); setState(() => _isLoading = false);
} else { } else {
// Simulate fetch or logic to handle missing data
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
if (mounted) { if (mounted) {
// Mock data from POC if needed, but assuming shift is always passed in this context // Fallback mock shift
// based on ShiftsPage navigation. setState(() {
// If generic fetch needed, we would use a Repo/Bloc here. _shift = Shift(
// For now, stop loading. id: widget.shiftId,
setState(() => _isLoading = false); title: 'Event Server',
clientName: 'Grand Hotel',
logoUrl: null,
hourlyRate: 25.0,
date: DateFormat('yyyy-MM-dd').format(DateTime.now()),
startTime: '16:00',
endTime: '22:00',
location: 'Downtown',
locationAddress: '123 Main St, New York, NY',
status: 'open',
createdDate: DateTime.now().toIso8601String(),
description: 'Provide exceptional customer service. Respond to guest requests or concerns promptly and professionally.',
);
_isLoading = false;
});
} }
} }
} }
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mma').format(dt).toLowerCase();
} catch (e) {
return time;
}
}
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
return DateFormat('MMMM d').format(date);
} catch (e) {
return dateStr;
}
}
double _calculateHours(String start, String end) { double _calculateHours(String start, String end) {
try { try {
final startParts = start.split(':').map(int.parse).toList(); final startParts = start.split(':').map(int.parse).toList();
final endParts = end.split(':').map(int.parse).toList(); final endParts = end.split(':').map(int.parse).toList();
double h = double h = (endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60;
(endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60;
if (h < 0) h += 24; if (h < 0) h += 24;
return h; return h;
} catch (e) { } catch (e) {
@@ -66,31 +109,6 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
} }
} }
Widget _buildTag(IconData icon, String label, Color bg, Color activeColor) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: activeColor),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: activeColor,
),
),
],
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_isLoading) {
@@ -109,7 +127,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: AppColors.krowMuted), icon: const Icon(LucideIcons.chevronLeft, color: AppColors.krowMuted),
onPressed: () => Modular.to.pop(), onPressed: () => Modular.to.pop(),
), ),
bottom: PreferredSize( bottom: PreferredSize(
@@ -124,7 +142,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Pending Badge (Mock logic) // Pending Badge
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Container( child: Container(
@@ -234,14 +252,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
Row( Row(
children: [ children: [
_buildTag( _buildTag(
UiIcons.zap, LucideIcons.zap,
'Immediate start', 'Immediate start',
AppColors.krowBlue.withOpacity(0.1), AppColors.krowBlue.withOpacity(0.1),
AppColors.krowBlue, AppColors.krowBlue,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildTag( _buildTag(
UiIcons.star, LucideIcons.star,
'No experience', 'No experience',
AppColors.krowYellow.withOpacity(0.3), AppColors.krowYellow.withOpacity(0.3),
AppColors.krowCharcoal, AppColors.krowCharcoal,
@@ -250,7 +268,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Additional Details // Additional Details Collapsible
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
@@ -278,8 +296,8 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
Icon( Icon(
_showDetails _showDetails
? UiIcons.chevronUp ? LucideIcons.chevronUp
: UiIcons.chevronDown, : LucideIcons.chevronDown,
color: AppColors.krowMuted, color: AppColors.krowMuted,
size: 20, size: 20,
), ),
@@ -287,76 +305,457 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
), ),
), ),
if (_showDetails && _shift.description != null) if (_showDetails)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Divider(height: 1, color: AppColors.krowBorder), _buildDetailRow('Tips', 'Yes', true),
const SizedBox(height: 16), _buildDetailRow('Travel Time', 'Yes', true),
Text( _buildDetailRow('Meal Provided', 'No', false),
_shift.description!, _buildDetailRow('Parking Available', 'Yes', true),
style: const TextStyle( _buildDetailRow('Gas Compensation', 'No', false),
color: AppColors.krowCharcoal,
height: 1.5,
),
),
], ],
), ),
), ),
], ],
), ),
), ),
const SizedBox(height: 16),
// Date & Duration Grid
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'START',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColors.krowMuted,
),
),
const SizedBox(height: 8),
Text(
_formatDate(_shift.date),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
const Text(
'Date',
style: TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
const SizedBox(height: 12),
Text(
_formatTime(_shift.startTime),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
const Text(
'Time',
style: TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'DURATION',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColors.krowMuted,
),
),
const SizedBox(height: 8),
Text(
'${hours.toStringAsFixed(0)} hours',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
const Text(
'Shift duration',
style: TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
const SizedBox(height: 12),
const Text(
'1 hour',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
const Text(
'Break duration',
style: TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
// Location
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'LOCATION',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColors.krowMuted,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_shift.location,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
Text(
_shift.locationAddress,
style: const TextStyle(
fontSize: 14,
color: AppColors.krowMuted,
),
),
],
),
),
OutlinedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
_shift.locationAddress,
),
duration: const Duration(seconds: 3),
),
);
},
icon: const Icon(LucideIcons.navigation, size: 14),
label: const Text('Get direction'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.krowCharcoal,
side: const BorderSide(
color: AppColors.krowBorder,
),
textStyle: const TextStyle(fontSize: 12),
),
),
],
),
const SizedBox(height: 16),
Container(
height: 160,
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFF1F3F5),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Icon(
LucideIcons.map,
color: AppColors.krowMuted,
size: 48,
),
),
),
],
),
),
const SizedBox(height: 16),
// Manager Contact
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'MANAGER CONTACT DETAILS',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColors.krowMuted,
),
),
const SizedBox(height: 16),
..._managers
.map(
(manager) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
AppColors.krowBlue,
Color(0xFF0830B8),
],
),
borderRadius: BorderRadius.circular(
8,
),
),
child: const Center(
child: Icon(
LucideIcons.user,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
manager['name']!,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
),
Text(
manager['phone']!,
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
],
),
OutlinedButton.icon(
onPressed: () {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(manager['phone']!),
duration: const Duration(seconds: 3),
),
);
},
icon: const Icon(
LucideIcons.phone,
size: 14,
color: Color(0xFF059669),
),
label: const Text(
'Call',
style: TextStyle(
color: Color(0xFF059669),
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFA7F3D0),
),
backgroundColor: const Color(0xFFECFDF5),
textStyle: const TextStyle(fontSize: 12),
),
),
],
),
),
)
.toList(),
],
),
),
const SizedBox(height: 16),
// Additional Info
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'ADDITIONAL INFO',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: AppColors.krowMuted,
),
),
const SizedBox(height: 12),
Text(
_shift.description ??
'Providing Exceptional Customer Service.',
style: const TextStyle(
fontSize: 14,
color: AppColors.krowMuted,
height: 1.5,
),
),
],
),
),
], ],
), ),
), ),
// Action Button // Bottom Actions
Align( Positioned(
alignment: Alignment.bottomCenter, bottom: 0,
left: 0,
right: 0,
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(20),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
border: Border(top: BorderSide(color: AppColors.krowBorder)), border: Border(top: BorderSide(color: AppColors.krowBorder)),
), ),
child: SafeArea( child: SafeArea(
child: SizedBox( top: false,
width: double.infinity, child: Column(
child: ElevatedButton( children: [
onPressed: _isApplying ? null : () { SizedBox(
setState(() { width: double.infinity,
_isApplying = true; height: 56,
}); child: ElevatedButton(
// Simulate Apply onPressed: () async {
Future.delayed(const Duration(seconds: 1), () { setState(() => _isApplying = true);
if (mounted) { await Future.delayed(const Duration(seconds: 1));
setState(() => _isApplying = false); if (mounted) {
Modular.to.pop(); setState(() => _isApplying = false);
} Modular.to.pop();
}); ScaffoldMessenger.of(context).showSnackBar(
}, const SnackBar(
style: ElevatedButton.styleFrom( content: Text('Shift Accepted!'),
backgroundColor: AppColors.krowBlue, backgroundColor: Color(0xFF10B981),
foregroundColor: Colors.white, ),
padding: const EdgeInsets.symmetric(vertical: 16), );
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), }
), },
child: _isApplying style: ElevatedButton.styleFrom(
? const SizedBox( backgroundColor: AppColors.krowBlue,
height: 20, shape: RoundedRectangleBorder(
width: 20, borderRadius: BorderRadius.circular(12),
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2) ),
) elevation: 0,
: const Text( ),
'Apply Now', child: _isApplying
style: TextStyle( ? const SizedBox(
fontSize: 16, width: 24,
fontWeight: FontWeight.bold, height: 24,
), child: CircularProgressIndicator(
), color: Colors.white,
), ),
)
: const Text(
'Accept shift',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
height: 48,
child: TextButton(
onPressed: () => Modular.to.pop(),
child: const Text(
'Decline shift',
style: TextStyle(
color: Color(0xFFEF4444),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
),
],
), ),
), ),
), ),
@@ -365,4 +764,51 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
), ),
); );
} }
Widget _buildTag(IconData icon, String label, Color bg, Color text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(icon, size: 14, color: text),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
color: text,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildDetailRow(String label, String value, bool isPositive) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(fontSize: 14, color: AppColors.krowMuted),
),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isPositive ? const Color(0xFF059669) : AppColors.krowMuted,
),
),
],
),
);
}
} }

View File

@@ -415,12 +415,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
if (filteredJobs.isEmpty) if (filteredJobs.isEmpty)
_buildEmptyState(LucideIcons.search, "No jobs available", "Check back later", null, null) _buildEmptyState(LucideIcons.search, "No jobs available", "Check back later", null, null)
else else
...filteredJobs.map((shift) => GestureDetector( ...filteredJobs.map((shift) => MyShiftCard(
onTap: () => Modular.to.pushNamed('details/${shift.id}', arguments: shift), shift: shift,
child: Padding( onAccept: () {},
padding: const EdgeInsets.only(bottom: 12), onDecline: () {},
child: MyShiftCard(shift: shift),
),
)), )),
], ],