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_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:krow_domain/krow_domain.dart';
@@ -30,6 +32,12 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
bool _showDetails = true;
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
void initState() {
super.initState();
@@ -41,24 +49,59 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
_shift = widget.shift!;
setState(() => _isLoading = false);
} else {
// Simulate fetch or logic to handle missing data
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
// Mock data from POC if needed, but assuming shift is always passed in this context
// based on ShiftsPage navigation.
// If generic fetch needed, we would use a Repo/Bloc here.
// For now, stop loading.
setState(() => _isLoading = false);
// Fallback mock shift
setState(() {
_shift = Shift(
id: widget.shiftId,
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) {
try {
final startParts = start.split(':').map(int.parse).toList();
final endParts = end.split(':').map(int.parse).toList();
double h =
(endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60;
double h = (endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60;
if (h < 0) h += 24;
return h;
} 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
Widget build(BuildContext context) {
if (_isLoading) {
@@ -109,7 +127,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: AppColors.krowMuted),
icon: const Icon(LucideIcons.chevronLeft, color: AppColors.krowMuted),
onPressed: () => Modular.to.pop(),
),
bottom: PreferredSize(
@@ -124,7 +142,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Pending Badge (Mock logic)
// Pending Badge
Align(
alignment: Alignment.centerRight,
child: Container(
@@ -234,14 +252,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
Row(
children: [
_buildTag(
UiIcons.zap,
LucideIcons.zap,
'Immediate start',
AppColors.krowBlue.withOpacity(0.1),
AppColors.krowBlue,
),
const SizedBox(width: 8),
_buildTag(
UiIcons.star,
LucideIcons.star,
'No experience',
AppColors.krowYellow.withOpacity(0.3),
AppColors.krowCharcoal,
@@ -250,7 +268,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
),
const SizedBox(height: 24),
// Additional Details
// Additional Details Collapsible
Container(
decoration: BoxDecoration(
color: Colors.white,
@@ -278,8 +296,8 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
),
Icon(
_showDetails
? UiIcons.chevronUp
: UiIcons.chevronDown,
? LucideIcons.chevronUp
: LucideIcons.chevronDown,
color: AppColors.krowMuted,
size: 20,
),
@@ -287,76 +305,457 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
),
),
),
if (_showDetails && _shift.description != null)
if (_showDetails)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1, color: AppColors.krowBorder),
const SizedBox(height: 16),
Text(
_shift.description!,
style: const TextStyle(
color: AppColors.krowCharcoal,
height: 1.5,
),
),
_buildDetailRow('Tips', 'Yes', true),
_buildDetailRow('Travel Time', 'Yes', true),
_buildDetailRow('Meal Provided', 'No', false),
_buildDetailRow('Parking Available', 'Yes', true),
_buildDetailRow('Gas Compensation', 'No', false),
],
),
),
],
),
),
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
Align(
alignment: Alignment.bottomCenter,
// Bottom Actions
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: AppColors.krowBorder)),
),
child: SafeArea(
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isApplying ? null : () {
setState(() {
_isApplying = true;
});
// Simulate Apply
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() => _isApplying = false);
Modular.to.pop();
}
});
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.krowBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: _isApplying
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)
)
: const Text(
'Apply Now',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
top: false,
child: Column(
children: [
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () async {
setState(() => _isApplying = true);
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
setState(() => _isApplying = false);
Modular.to.pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Shift Accepted!'),
backgroundColor: Color(0xFF10B981),
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.krowBlue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: _isApplying
? const SizedBox(
width: 24,
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)
_buildEmptyState(LucideIcons.search, "No jobs available", "Check back later", null, null)
else
...filteredJobs.map((shift) => GestureDetector(
onTap: () => Modular.to.pushNamed('details/${shift.id}', arguments: shift),
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: MyShiftCard(shift: shift),
),
...filteredJobs.map((shift) => MyShiftCard(
shift: shift,
onAccept: () {},
onDecline: () {},
)),
],