diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index 65d31a36..fdb92535 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -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 { bool _showDetails = true; bool _isApplying = false; + // Mock Managers + final List> _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 { _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 { } } - 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 { 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 { 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 { 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 { ), const SizedBox(height: 24), - // Additional Details + // Additional Details Collapsible Container( decoration: BoxDecoration( color: Colors.white, @@ -278,8 +296,8 @@ class _ShiftDetailsPageState extends State { ), Icon( _showDetails - ? UiIcons.chevronUp - : UiIcons.chevronDown, + ? LucideIcons.chevronUp + : LucideIcons.chevronDown, color: AppColors.krowMuted, size: 20, ), @@ -287,76 +305,457 @@ class _ShiftDetailsPageState extends State { ), ), ), - 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 { ), ); } + + 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, + ), + ), + ], + ), + ); + } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart index 350c4855..e89ded58 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart @@ -415,12 +415,10 @@ class _ShiftsPageState extends State { 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: () {}, )), ],