feat: Add shifts styles, empty state view, and tabs for finding, history, and my shifts
- Introduced AppColors for consistent color usage across the app. - Implemented EmptyStateView widget for displaying empty states with icons and messages. - Created FindShiftsTab for searching and filtering available jobs. - Developed HistoryShiftsTab to display completed shifts with an empty state. - Added MyShiftsTab for managing user shifts, including confirmation and decline functionalities.
This commit is contained in:
@@ -51,28 +51,15 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
|
||||
DateTime? _toDateTime(dynamic t) {
|
||||
if (t == null) return null;
|
||||
if (t is DateTime) return t;
|
||||
if (t is String) return DateTime.tryParse(t);
|
||||
|
||||
// Data Connect Timestamp handling
|
||||
try {
|
||||
if (t is Timestamp) {
|
||||
return t.toDateTime();
|
||||
return DateTime.tryParse(t.toJson() as String);
|
||||
} catch (_) {
|
||||
try {
|
||||
return DateTime.tryParse(t.toString());
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
// Fallback for any object that might have a toDate or similar
|
||||
if (t.runtimeType.toString().contains('Timestamp')) {
|
||||
return (t as dynamic).toDate();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
return DateTime.tryParse(t.toString());
|
||||
} catch (_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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';
|
||||
import '../blocs/shifts/shifts_bloc.dart';
|
||||
import '../widgets/my_shift_card.dart';
|
||||
import '../widgets/shift_assignment_card.dart';
|
||||
|
||||
// Shim to match POC styles locally
|
||||
class AppColors {
|
||||
static const Color krowBlue = UiColors.primary;
|
||||
static const Color krowYellow = Color(0xFFFFED4A);
|
||||
static const Color krowCharcoal = UiColors.textPrimary;
|
||||
static const Color krowMuted = UiColors.textSecondary;
|
||||
static const Color krowBorder = UiColors.border;
|
||||
static const Color krowBackground = UiColors.background;
|
||||
static const Color white = Colors.white;
|
||||
static const Color black = Colors.black;
|
||||
}
|
||||
import '../widgets/tabs/my_shifts_tab.dart';
|
||||
import '../widgets/tabs/find_shifts_tab.dart';
|
||||
import '../widgets/tabs/history_shifts_tab.dart';
|
||||
import '../styles/shifts_styles.dart';
|
||||
|
||||
class ShiftsPage extends StatefulWidget {
|
||||
final String? initialTab;
|
||||
@@ -31,15 +19,6 @@ class ShiftsPage extends StatefulWidget {
|
||||
|
||||
class _ShiftsPageState extends State<ShiftsPage> {
|
||||
late String _activeTab;
|
||||
String _searchQuery = '';
|
||||
// ignore: unused_field
|
||||
String? _cancelledShiftDemo; // 'lastMinute' or 'advance'
|
||||
String _jobType = 'all'; // all, one-day, multi-day, long-term
|
||||
|
||||
// Calendar State
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
int _weekOffset = 0;
|
||||
|
||||
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
|
||||
|
||||
@override
|
||||
@@ -59,93 +38,30 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
List<DateTime> _getCalendarDays() {
|
||||
final now = DateTime.now();
|
||||
int reactDayIndex = now.weekday == 7 ? 0 : now.weekday;
|
||||
int daysSinceFriday = (reactDayIndex + 2) % 7;
|
||||
final start = now
|
||||
.subtract(Duration(days: daysSinceFriday))
|
||||
.add(Duration(days: _weekOffset * 7));
|
||||
final startDate = DateTime(start.year, start.month, start.day);
|
||||
return List.generate(7, (index) => startDate.add(Duration(days: index)));
|
||||
}
|
||||
|
||||
bool _isSameDay(DateTime a, DateTime b) {
|
||||
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||
}
|
||||
|
||||
void _confirmShift(String id) {
|
||||
_bloc.add(AcceptShiftEvent(id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift confirmed!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _declineShift(String id) {
|
||||
_bloc.add(DeclineShiftEvent(id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift declined.'),
|
||||
backgroundColor: Color(0xFFEF4444),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: BlocBuilder<ShiftsBloc, ShiftsState>(
|
||||
builder: (context, state) {
|
||||
final List<Shift> myShifts = (state is ShiftsLoaded) ? state.myShifts : [];
|
||||
final List<Shift> availableJobs = (state is ShiftsLoaded) ? state.availableShifts : [];
|
||||
final List<Shift> pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : [];
|
||||
final List<Shift> cancelledShifts = (state is ShiftsLoaded) ? state.cancelledShifts : [];
|
||||
final List<Shift> historyShifts = (state is ShiftsLoaded) ? state.historyShifts : [];
|
||||
final List<Shift> myShifts = (state is ShiftsLoaded)
|
||||
? state.myShifts
|
||||
: [];
|
||||
final List<Shift> availableJobs = (state is ShiftsLoaded)
|
||||
? state.availableShifts
|
||||
: [];
|
||||
final List<Shift> pendingAssignments = (state is ShiftsLoaded)
|
||||
? state.pendingShifts
|
||||
: [];
|
||||
final List<Shift> cancelledShifts = (state is ShiftsLoaded)
|
||||
? state.cancelledShifts
|
||||
: [];
|
||||
final List<Shift> historyShifts = (state is ShiftsLoaded)
|
||||
? state.historyShifts
|
||||
: [];
|
||||
|
||||
// Filter logic
|
||||
final filteredJobs = availableJobs.where((s) {
|
||||
final matchesSearch =
|
||||
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
|
||||
if (_jobType == 'all') return true;
|
||||
if (_jobType == 'one-day') {
|
||||
return s.durationDays == null || s.durationDays! <= 1;
|
||||
}
|
||||
if (_jobType == 'multi-day') return s.durationDays != null && s.durationDays! > 1;
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
final calendarDays = _getCalendarDays();
|
||||
final weekStartDate = calendarDays.first;
|
||||
final weekEndDate = calendarDays.last;
|
||||
|
||||
final visibleMyShifts = myShifts.where((s) {
|
||||
try {
|
||||
final date = DateTime.parse(s.date);
|
||||
return date.isAfter(weekStartDate.subtract(const Duration(seconds: 1))) &&
|
||||
date.isBefore(weekEndDate.add(const Duration(days: 1)));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
final visibleCancelledShifts = cancelledShifts.where((s) {
|
||||
try {
|
||||
final date = DateTime.parse(s.date);
|
||||
return date.isAfter(weekStartDate.subtract(const Duration(seconds: 1))) &&
|
||||
date.isBefore(weekEndDate.add(const Duration(days: 1)));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
// Note: "filteredJobs" logic moved to FindShiftsTab
|
||||
// Note: Calendar logic moved to MyShiftsTab
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.krowBackground,
|
||||
@@ -161,326 +77,58 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
20,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 16,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Shifts",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(UiIcons.user, size: 20, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
const Text(
|
||||
"Shifts",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Tabs
|
||||
Row(
|
||||
children: [
|
||||
_buildTab("myshifts", "My Shifts", UiIcons.calendar, myShifts.length),
|
||||
_buildTab(
|
||||
"myshifts",
|
||||
"My Shifts",
|
||||
UiIcons.calendar,
|
||||
myShifts.length,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildTab("find", "Find Shifts", UiIcons.search, filteredJobs.length),
|
||||
_buildTab(
|
||||
"find",
|
||||
"Find Shifts",
|
||||
UiIcons.search,
|
||||
availableJobs.length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs.
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildTab("history", "History", UiIcons.clock, historyShifts.length),
|
||||
_buildTab(
|
||||
"history",
|
||||
"History",
|
||||
UiIcons.clock,
|
||||
historyShifts.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Calendar Selector
|
||||
if (_activeTab == 'myshifts')
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal),
|
||||
onPressed: () => setState(() => _weekOffset--),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMMM yyyy').format(weekStartDate),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.chevronRight, size: 20, color: AppColors.krowCharcoal),
|
||||
onPressed: () => setState(() => _weekOffset++),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Days Grid
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: calendarDays.map((date) {
|
||||
final isSelected = _isSameDay(date, _selectedDate);
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
||||
final hasShifts = myShifts.any((s) {
|
||||
try {
|
||||
return _isSameDay(DateTime.parse(s.date), date);
|
||||
} catch (_) { return false; }
|
||||
});
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedDate = date),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.krowBlue : AppColors.krowBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
date.day.toString().padLeft(2, '0'),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected ? Colors.white : AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected ? Colors.white.withOpacity(0.8) : AppColors.krowMuted,
|
||||
),
|
||||
),
|
||||
if (hasShifts && !isSelected)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.krowBlue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (_activeTab == 'myshifts')
|
||||
const Divider(height: 1, color: AppColors.krowBorder),
|
||||
|
||||
// Search and Filters for Find Tab (Fixed at top)
|
||||
if (_activeTab == 'find')
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(UiIcons.search, size: 20, color: Color(0xFF94A3B8)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (v) => setState(() => _searchQuery = v),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Search jobs, location...",
|
||||
hintStyle: TextStyle(
|
||||
color: Color(0xFF94A3B8),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: const Icon(UiIcons.filter, size: 18, color: Color(0xFF64748B)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Filter Tabs
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterTab('all', 'All Jobs'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterTab('one-day', 'One Day'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterTab('multi-day', 'Multi-Day'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterTab('long-term', 'Long Term'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Body Content
|
||||
Expanded(
|
||||
child: state is ShiftsLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
if (_activeTab == 'myshifts') ...[
|
||||
if (pendingAssignments.isNotEmpty) ...[
|
||||
_buildSectionHeader("Awaiting Confirmation", const Color(0xFFF59E0B)),
|
||||
...pendingAssignments.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: ShiftAssignmentCard(
|
||||
shift: shift,
|
||||
onConfirm: () => _confirmShift(shift.id),
|
||||
onDecline: () => _declineShift(shift.id),
|
||||
isConfirming: true,
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
if (visibleCancelledShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader("Cancelled Shifts", AppColors.krowMuted),
|
||||
...visibleCancelledShifts.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _buildCancelledCard(
|
||||
title: shift.title,
|
||||
client: shift.clientName,
|
||||
pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}",
|
||||
rate: "\$${shift.hourlyRate}/hr · 8h",
|
||||
date: _formatDateStr(shift.date),
|
||||
time: "${shift.startTime} - ${shift.endTime}",
|
||||
address: shift.locationAddress,
|
||||
isLastMinute: true,
|
||||
onTap: () {}
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Confirmed Shifts
|
||||
if (visibleMyShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader("Confirmed Shifts", AppColors.krowMuted),
|
||||
...visibleMyShifts.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(shift: shift),
|
||||
)),
|
||||
],
|
||||
|
||||
if (visibleMyShifts.isEmpty && pendingAssignments.isEmpty && cancelledShifts.isEmpty)
|
||||
_buildEmptyState(UiIcons.calendar, "No shifts this week", "Try finding new jobs in the Find tab", null, null),
|
||||
],
|
||||
|
||||
if (_activeTab == 'find') ...[
|
||||
if (filteredJobs.isEmpty)
|
||||
_buildEmptyState(UiIcons.search, "No jobs available", "Check back later", null, null)
|
||||
else
|
||||
...filteredJobs.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
onAccept: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Booked!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
},
|
||||
onDecline: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Declined'),
|
||||
backgroundColor: Color(0xFFEF4444),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
|
||||
if (_activeTab == 'history') ...[
|
||||
if (historyShifts.isEmpty)
|
||||
_buildEmptyState(UiIcons.clock, "No shift history", "Completed shifts appear here", null, null)
|
||||
else
|
||||
...historyShifts.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
historyMode: true,
|
||||
),
|
||||
)),
|
||||
],
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: state is ShiftsLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _buildTabContent(
|
||||
myShifts,
|
||||
pendingAssignments,
|
||||
cancelledShifts,
|
||||
availableJobs,
|
||||
historyShifts,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -490,62 +138,33 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateStr(String dateStr) {
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
final now = DateTime.now();
|
||||
if (_isSameDay(date, now)) return "Today";
|
||||
final tomorrow = now.add(const Duration(days: 1));
|
||||
if (_isSameDay(date, tomorrow)) return "Tomorrow";
|
||||
return DateFormat('EEE, MMM d').format(date);
|
||||
} catch (_) {
|
||||
return dateStr;
|
||||
Widget _buildTabContent(
|
||||
List<Shift> myShifts,
|
||||
List<Shift> pendingAssignments,
|
||||
List<Shift> cancelledShifts,
|
||||
List<Shift> availableJobs,
|
||||
List<Shift> historyShifts,
|
||||
) {
|
||||
switch (_activeTab) {
|
||||
case 'myshifts':
|
||||
return MyShiftsTab(
|
||||
myShifts: myShifts,
|
||||
pendingAssignments: pendingAssignments,
|
||||
cancelledShifts: cancelledShifts,
|
||||
);
|
||||
case 'find':
|
||||
return FindShiftsTab(
|
||||
availableJobs: availableJobs,
|
||||
);
|
||||
case 'history':
|
||||
return HistoryShiftsTab(
|
||||
historyShifts: historyShifts,
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title, Color dotColor) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(width: 8, height: 8, decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 8),
|
||||
Text(title, style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: dotColor == AppColors.krowMuted ? AppColors.krowMuted : dotColor
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterTab(String id, String label) {
|
||||
final isSelected = _jobType == id;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _jobType = id),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.krowBlue : const Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.white : const Color(0xFF64748B),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(String id, String label, IconData icon, int count) {
|
||||
final isActive = _activeTab == id;
|
||||
return Expanded(
|
||||
@@ -554,112 +173,57 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.white : Colors.white.withAlpha((0.2 * 255).round()),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: isActive
|
||||
? Colors.white
|
||||
: Colors.white.withAlpha((0.2 * 255).round()),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: isActive ? AppColors.krowBlue : Colors.white),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(child: Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isActive ? AppColors.krowBlue : Colors.white), overflow: TextOverflow.ellipsis)),
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? AppColors.krowBlue.withAlpha((0.1 * 255).round()) : Colors.white.withAlpha((0.2 * 255).round()),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
Icon(
|
||||
icon,
|
||||
size: 14,
|
||||
color: isActive ? AppColors.krowBlue : Colors.white,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isActive ? AppColors.krowBlue : Colors.white,
|
||||
),
|
||||
child: Center(child: Text("$count", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: isActive ? AppColors.krowBlue : Colors.white))),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
constraints: const BoxConstraints(minWidth: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? AppColors.krowBlue.withAlpha((0.1 * 255).round())
|
||||
: Colors.white.withAlpha((0.2 * 255).round()),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"$count",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isActive ? AppColors.krowBlue : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(IconData icon, String title, String subtitle, String? actionLabel, VoidCallback? onAction) {
|
||||
return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 64), child: Column(children: [
|
||||
Container(width: 64, height: 64, decoration: BoxDecoration(color: const Color(0xFFF1F3F5), borderRadius: BorderRadius.circular(12)), child: Icon(icon, size: 32, color: AppColors.krowMuted)),
|
||||
const SizedBox(height: 16),
|
||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.krowCharcoal)),
|
||||
const SizedBox(height: 4),
|
||||
Text(subtitle, style: const TextStyle(fontSize: 14, color: AppColors.krowMuted)),
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: onAction, style: ElevatedButton.styleFrom(backgroundColor: AppColors.krowBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), child: Text(actionLabel)),
|
||||
]
|
||||
])));
|
||||
}
|
||||
|
||||
Widget _buildCancelledCard({required String title, required String client, required String pay, required String rate, required String date, required String time, required String address, required bool isLastMinute, required VoidCallback onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.krowBorder)
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFEF4444), shape: BoxShape.circle)),
|
||||
const SizedBox(width: 6),
|
||||
const Text("CANCELLED", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Color(0xFFEF4444))),
|
||||
if (isLastMinute) ...[
|
||||
const SizedBox(width: 4),
|
||||
const Text("• 4hr compensation", style: TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF10B981)))
|
||||
]
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.krowBlue.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(child: Icon(LucideIcons.briefcase, color: AppColors.krowBlue, size: 20))
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowCharcoal)),
|
||||
Text(client, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))
|
||||
])),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
Text(pay, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.krowCharcoal)),
|
||||
Text(rate, style: const TextStyle(fontSize: 10, color: AppColors.krowMuted))
|
||||
])
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
const Icon(LucideIcons.calendar, size: 12, color: AppColors.krowMuted),
|
||||
const SizedBox(width: 4),
|
||||
Text(date, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted)),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(LucideIcons.clock, size: 12, color: AppColors.krowMuted),
|
||||
const SizedBox(width: 4),
|
||||
Text(time, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Row(children: [
|
||||
const Icon(LucideIcons.mapPin, size: 12, color: AppColors.krowMuted),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(child: Text(address, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted), overflow: TextOverflow.ellipsis))
|
||||
]),
|
||||
])),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
class AppColors {
|
||||
static const Color krowBlue = UiColors.primary;
|
||||
static const Color krowYellow = Color(0xFFFFED4A);
|
||||
static const Color krowCharcoal = UiColors.textPrimary;
|
||||
static const Color krowMuted = UiColors.textSecondary;
|
||||
static const Color krowBorder = UiColors.border;
|
||||
static const Color krowBackground = UiColors.background;
|
||||
static const Color white = Colors.white;
|
||||
static const Color black = Colors.black;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../styles/shifts_styles.dart';
|
||||
|
||||
class EmptyStateView extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const EmptyStateView({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 64),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F3F5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, size: 32, color: AppColors.krowMuted),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.krowMuted),
|
||||
),
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: onAction,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.krowBlue,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(actionLabel!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../styles/shifts_styles.dart';
|
||||
import '../my_shift_card.dart';
|
||||
import '../shared/empty_state_view.dart';
|
||||
|
||||
class FindShiftsTab extends StatefulWidget {
|
||||
final List<Shift> availableJobs;
|
||||
|
||||
const FindShiftsTab({
|
||||
super.key,
|
||||
required this.availableJobs,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
||||
}
|
||||
|
||||
class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
String _searchQuery = '';
|
||||
String _jobType = 'all';
|
||||
|
||||
Widget _buildFilterTab(String id, String label) {
|
||||
final isSelected = _jobType == id;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _jobType = id),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.krowBlue : const Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isSelected ? Colors.white : const Color(0xFF64748B),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Filter logic
|
||||
final filteredJobs = widget.availableJobs.where((s) {
|
||||
final matchesSearch =
|
||||
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
|
||||
if (_jobType == 'all') return true;
|
||||
if (_jobType == 'one-day') {
|
||||
return s.durationDays == null || s.durationDays! <= 1;
|
||||
}
|
||||
if (_jobType == 'multi-day')
|
||||
return s.durationDays != null && s.durationDays! > 1;
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Search and Filters
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.search,
|
||||
size: 20,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (v) =>
|
||||
setState(() => _searchQuery = v),
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: "Search jobs, location...",
|
||||
hintStyle: TextStyle(
|
||||
color: Color(0xFF94A3B8),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.filter,
|
||||
size: 18,
|
||||
color: Color(0xFF64748B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Filter Tabs
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterTab('all', 'All Jobs'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterTab('one-day', 'One Day'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterTab('multi-day', 'Multi-Day'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterTab('long-term', 'Long Term'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: filteredJobs.isEmpty
|
||||
? EmptyStateView(
|
||||
icon: UiIcons.search,
|
||||
title: "No jobs available",
|
||||
subtitle: "Check back later",
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
...filteredJobs.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
onAccept: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Booked!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
},
|
||||
onDecline: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Declined'),
|
||||
backgroundColor: Color(0xFFEF4444),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../my_shift_card.dart';
|
||||
import '../shared/empty_state_view.dart';
|
||||
|
||||
class HistoryShiftsTab extends StatelessWidget {
|
||||
final List<Shift> historyShifts;
|
||||
|
||||
const HistoryShiftsTab({
|
||||
super.key,
|
||||
required this.historyShifts,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (historyShifts.isEmpty) {
|
||||
return EmptyStateView(
|
||||
icon: UiIcons.clock,
|
||||
title: "No shift history",
|
||||
subtitle: "Completed shifts appear here",
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
...historyShifts.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
historyMode: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../blocs/shifts/shifts_bloc.dart';
|
||||
import '../my_shift_card.dart';
|
||||
import '../shift_assignment_card.dart';
|
||||
import '../shared/empty_state_view.dart';
|
||||
import '../../styles/shifts_styles.dart';
|
||||
|
||||
class MyShiftsTab extends StatefulWidget {
|
||||
final List<Shift> myShifts;
|
||||
final List<Shift> pendingAssignments;
|
||||
final List<Shift> cancelledShifts;
|
||||
|
||||
const MyShiftsTab({
|
||||
super.key,
|
||||
required this.myShifts,
|
||||
required this.pendingAssignments,
|
||||
required this.cancelledShifts,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MyShiftsTab> createState() => _MyShiftsTabState();
|
||||
}
|
||||
|
||||
class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
int _weekOffset = 0;
|
||||
|
||||
List<DateTime> _getCalendarDays() {
|
||||
final now = DateTime.now();
|
||||
int reactDayIndex = now.weekday == 7 ? 0 : now.weekday;
|
||||
int daysSinceFriday = (reactDayIndex + 2) % 7;
|
||||
final start = now
|
||||
.subtract(Duration(days: daysSinceFriday))
|
||||
.add(Duration(days: _weekOffset * 7));
|
||||
final startDate = DateTime(start.year, start.month, start.day);
|
||||
return List.generate(7, (index) => startDate.add(Duration(days: index)));
|
||||
}
|
||||
|
||||
bool _isSameDay(DateTime a, DateTime b) {
|
||||
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||
}
|
||||
|
||||
void _confirmShift(String id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Accept Shift'),
|
||||
content: const Text(
|
||||
'Are you sure you want to accept this shift?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.read<ShiftsBloc>().add(AcceptShiftEvent(id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift confirmed!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF10B981),
|
||||
),
|
||||
child: const Text('Accept'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _declineShift(String id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Decline Shift'),
|
||||
content: const Text(
|
||||
'Are you sure you want to decline this shift? This action cannot be undone.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
context.read<ShiftsBloc>().add(DeclineShiftEvent(id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift declined.'),
|
||||
backgroundColor: Color(0xFFEF4444),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFEF4444),
|
||||
),
|
||||
child: const Text('Decline'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateStr(String dateStr) {
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
final now = DateTime.now();
|
||||
if (_isSameDay(date, now)) return "Today";
|
||||
final tomorrow = now.add(const Duration(days: 1));
|
||||
if (_isSameDay(date, tomorrow)) return "Tomorrow";
|
||||
return DateFormat('EEE, MMM d').format(date);
|
||||
} catch (_) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final calendarDays = _getCalendarDays();
|
||||
final weekStartDate = calendarDays.first;
|
||||
final weekEndDate = calendarDays.last;
|
||||
|
||||
final visibleMyShifts = widget.myShifts.where((s) {
|
||||
try {
|
||||
final date = DateTime.parse(s.date);
|
||||
return date.isAfter(
|
||||
weekStartDate.subtract(const Duration(seconds: 1)),
|
||||
) &&
|
||||
date.isBefore(weekEndDate.add(const Duration(days: 1)));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
final visibleCancelledShifts = widget.cancelledShifts.where((s) {
|
||||
try {
|
||||
final date = DateTime.parse(s.date);
|
||||
return date.isAfter(
|
||||
weekStartDate.subtract(const Duration(seconds: 1)),
|
||||
) &&
|
||||
date.isBefore(weekEndDate.add(const Duration(days: 1)));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Calendar Selector
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
UiIcons.chevronLeft,
|
||||
size: 20,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
onPressed: () => setState(() => _weekOffset--),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMMM yyyy').format(weekStartDate),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
UiIcons.chevronRight,
|
||||
size: 20,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
onPressed: () => setState(() => _weekOffset++),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Days Grid
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: calendarDays.map((date) {
|
||||
final isSelected = _isSameDay(date, _selectedDate);
|
||||
// ignore: unused_local_variable
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
||||
final hasShifts = widget.myShifts.any((s) {
|
||||
try {
|
||||
return _isSameDay(DateTime.parse(s.date), date);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedDate = date),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColors.krowBlue
|
||||
: AppColors.krowBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
date.day.toString().padLeft(2, '0'),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected
|
||||
? Colors.white.withOpacity(0.8)
|
||||
: AppColors.krowMuted,
|
||||
),
|
||||
),
|
||||
if (hasShifts && !isSelected)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.krowBlue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, color: AppColors.krowBorder),
|
||||
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
if (widget.pendingAssignments.isNotEmpty) ...[
|
||||
_buildSectionHeader(
|
||||
"Awaiting Confirmation",
|
||||
const Color(0xFFF59E0B),
|
||||
),
|
||||
...widget.pendingAssignments.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: ShiftAssignmentCard(
|
||||
shift: shift,
|
||||
onConfirm: () => _confirmShift(shift.id),
|
||||
onDecline: () => _declineShift(shift.id),
|
||||
isConfirming: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
if (visibleCancelledShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader(
|
||||
"Cancelled Shifts",
|
||||
AppColors.krowMuted,
|
||||
),
|
||||
...visibleCancelledShifts.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _buildCancelledCard(
|
||||
title: shift.title,
|
||||
client: shift.clientName,
|
||||
pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}",
|
||||
rate: "\$${shift.hourlyRate}/hr · 8h",
|
||||
date: _formatDateStr(shift.date),
|
||||
time: "${shift.startTime} - ${shift.endTime}",
|
||||
address: shift.locationAddress,
|
||||
isLastMinute: true,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Confirmed Shifts
|
||||
if (visibleMyShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader(
|
||||
"Confirmed Shifts",
|
||||
AppColors.krowMuted,
|
||||
),
|
||||
...visibleMyShifts.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(shift: shift),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (visibleMyShifts.isEmpty &&
|
||||
widget.pendingAssignments.isEmpty &&
|
||||
widget.cancelledShifts.isEmpty)
|
||||
const EmptyStateView(
|
||||
icon: UiIcons.calendar,
|
||||
title: "No shifts this week",
|
||||
subtitle: "Try finding new jobs in the Find tab",
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title, Color dotColor) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: dotColor == AppColors.krowMuted
|
||||
? AppColors.krowMuted
|
||||
: dotColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Widget _buildCancelledCard({
|
||||
required String title,
|
||||
required String client,
|
||||
required String pay,
|
||||
required String rate,
|
||||
required String date,
|
||||
required String time,
|
||||
required String address,
|
||||
required bool isLastMinute,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: AppColors.krowBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFEF4444),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
"CANCELLED",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFEF4444),
|
||||
),
|
||||
),
|
||||
if (isLastMinute) ...[
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
"• 4hr compensation",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF10B981),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.krowBlue.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
LucideIcons.briefcase,
|
||||
color: AppColors.krowBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
client,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
pay,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
rate,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.calendar,
|
||||
size: 12,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
date,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 12,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
time,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.mapPin,
|
||||
size: 12,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
address,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.krowMuted,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user