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) {
|
DateTime? _toDateTime(dynamic t) {
|
||||||
if (t == null) return null;
|
if (t == null) return null;
|
||||||
if (t is DateTime) return t;
|
|
||||||
if (t is String) return DateTime.tryParse(t);
|
|
||||||
|
|
||||||
// Data Connect Timestamp handling
|
|
||||||
try {
|
try {
|
||||||
if (t is Timestamp) {
|
return DateTime.tryParse(t.toJson() as String);
|
||||||
return t.toDateTime();
|
} 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
|
@override
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.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';
|
||||||
import '../blocs/shifts/shifts_bloc.dart';
|
import '../blocs/shifts/shifts_bloc.dart';
|
||||||
import '../widgets/my_shift_card.dart';
|
import '../widgets/tabs/my_shifts_tab.dart';
|
||||||
import '../widgets/shift_assignment_card.dart';
|
import '../widgets/tabs/find_shifts_tab.dart';
|
||||||
|
import '../widgets/tabs/history_shifts_tab.dart';
|
||||||
// Shim to match POC styles locally
|
import '../styles/shifts_styles.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShiftsPage extends StatefulWidget {
|
class ShiftsPage extends StatefulWidget {
|
||||||
final String? initialTab;
|
final String? initialTab;
|
||||||
@@ -31,15 +19,6 @@ class ShiftsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _ShiftsPageState extends State<ShiftsPage> {
|
class _ShiftsPageState extends State<ShiftsPage> {
|
||||||
late String _activeTab;
|
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>();
|
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _bloc,
|
value: _bloc,
|
||||||
child: BlocBuilder<ShiftsBloc, ShiftsState>(
|
child: BlocBuilder<ShiftsBloc, ShiftsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final List<Shift> myShifts = (state is ShiftsLoaded) ? state.myShifts : [];
|
final List<Shift> myShifts = (state is ShiftsLoaded)
|
||||||
final List<Shift> availableJobs = (state is ShiftsLoaded) ? state.availableShifts : [];
|
? state.myShifts
|
||||||
final List<Shift> pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : [];
|
: [];
|
||||||
final List<Shift> cancelledShifts = (state is ShiftsLoaded) ? state.cancelledShifts : [];
|
final List<Shift> availableJobs = (state is ShiftsLoaded)
|
||||||
final List<Shift> historyShifts = (state is ShiftsLoaded) ? state.historyShifts : [];
|
? 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
|
// Note: "filteredJobs" logic moved to FindShiftsTab
|
||||||
final filteredJobs = availableJobs.where((s) {
|
// Note: Calendar logic moved to MyShiftsTab
|
||||||
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();
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.krowBackground,
|
backgroundColor: AppColors.krowBackground,
|
||||||
@@ -161,326 +77,58 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
20,
|
20,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
const Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
"Shifts",
|
||||||
children: [
|
style: TextStyle(
|
||||||
const Text(
|
fontSize: 24,
|
||||||
"Shifts",
|
fontWeight: FontWeight.bold,
|
||||||
style: TextStyle(
|
color: Colors.white,
|
||||||
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 SizedBox(height: 16),
|
|
||||||
// Tabs
|
// Tabs
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTab("myshifts", "My Shifts", UiIcons.calendar, myShifts.length),
|
_buildTab(
|
||||||
|
"myshifts",
|
||||||
|
"My Shifts",
|
||||||
|
UiIcons.calendar,
|
||||||
|
myShifts.length,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
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),
|
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
|
// Body Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: state is ShiftsLoading
|
child: state is ShiftsLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: SingleChildScrollView(
|
: _buildTabContent(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
myShifts,
|
||||||
child: Column(
|
pendingAssignments,
|
||||||
children: [
|
cancelledShifts,
|
||||||
const SizedBox(height: 20),
|
availableJobs,
|
||||||
if (_activeTab == 'myshifts') ...[
|
historyShifts,
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -490,62 +138,33 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDateStr(String dateStr) {
|
Widget _buildTabContent(
|
||||||
try {
|
List<Shift> myShifts,
|
||||||
final date = DateTime.parse(dateStr);
|
List<Shift> pendingAssignments,
|
||||||
final now = DateTime.now();
|
List<Shift> cancelledShifts,
|
||||||
if (_isSameDay(date, now)) return "Today";
|
List<Shift> availableJobs,
|
||||||
final tomorrow = now.add(const Duration(days: 1));
|
List<Shift> historyShifts,
|
||||||
if (_isSameDay(date, tomorrow)) return "Tomorrow";
|
) {
|
||||||
return DateFormat('EEE, MMM d').format(date);
|
switch (_activeTab) {
|
||||||
} catch (_) {
|
case 'myshifts':
|
||||||
return dateStr;
|
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) {
|
Widget _buildTab(String id, String label, IconData icon, int count) {
|
||||||
final isActive = _activeTab == id;
|
final isActive = _activeTab == id;
|
||||||
return Expanded(
|
return Expanded(
|
||||||
@@ -554,112 +173,57 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isActive ? Colors.white : Colors.white.withAlpha((0.2 * 255).round()),
|
color: isActive
|
||||||
borderRadius: BorderRadius.circular(8),
|
? Colors.white
|
||||||
|
: Colors.white.withAlpha((0.2 * 255).round()),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14, color: isActive ? AppColors.krowBlue : Colors.white),
|
Icon(
|
||||||
const SizedBox(width: 6),
|
icon,
|
||||||
Flexible(child: Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isActive ? AppColors.krowBlue : Colors.white), overflow: TextOverflow.ellipsis)),
|
size: 14,
|
||||||
const SizedBox(width: 4),
|
color: isActive ? AppColors.krowBlue : Colors.white,
|
||||||
Container(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
const SizedBox(width: 6),
|
||||||
constraints: const BoxConstraints(minWidth: 18),
|
Flexible(
|
||||||
decoration: BoxDecoration(
|
child: Text(
|
||||||
color: isActive ? AppColors.krowBlue.withAlpha((0.1 * 255).round()) : Colors.white.withAlpha((0.2 * 255).round()),
|
label,
|
||||||
borderRadius: BorderRadius.circular(999),
|
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