feat: add ShiftAssignmentCard widget and StaffShifts module

- Implemented ShiftAssignmentCard widget for displaying shift assignments with client details, pay calculation, and confirmation actions.
- Created StaffShiftsModule to manage dependencies, routes, and use cases related to staff shifts.
- Added necessary dependencies in pubspec.yaml and generated pubspec.lock.
This commit is contained in:
Achintha Isuru
2026-01-25 16:09:11 -05:00
parent 533a545da7
commit 8e429dda03
29 changed files with 2992 additions and 13 deletions

View File

@@ -662,6 +662,48 @@
"upload_required": "✓ Upload photos of required items",
"accept_attestation": "✓ Accept attestation"
}
},
"staff_shifts": {
"title": "Shifts",
"tabs": {
"my_shifts": "My Shifts",
"find_work": "Find Work"
},
"list": {
"no_shifts": "No shifts found",
"pending_offers": "PENDING OFFERS",
"available_jobs": "$count AVAILABLE JOBS",
"search_hint": "Search jobs..."
},
"filter": {
"all": "All Jobs",
"one_day": "One Day",
"multi_day": "Multi Day",
"long_term": "Long Term"
},
"status": {
"confirmed": "CONFIRMED",
"act_now": "ACT NOW",
"swap_requested": "SWAP REQUESTED",
"completed": "COMPLETED",
"no_show": "NO SHOW",
"pending_warning": "Please confirm assignment"
},
"action": {
"decline": "Decline",
"confirm": "Confirm",
"request_swap": "Request Swap"
},
"details": {
"additional": "ADDITIONAL DETAILS",
"days": "$days Days",
"exp_total": "(exp.total \\$$amount)",
"pending_time": "Pending $time ago"
},
"tags": {
"immediate_start": "Immediate start",
"no_experience": "No experience"
}
}
}

View File

@@ -661,5 +661,47 @@
"upload_required": "✓ Subir fotos de artículos requeridos",
"accept_attestation": "✓ Aceptar certificación"
}
},
"staff_shifts": {
"title": "Shifts",
"tabs": {
"my_shifts": "My Shifts",
"find_work": "Find Work"
},
"list": {
"no_shifts": "No shifts found",
"pending_offers": "PENDING OFFERS",
"available_jobs": "$count AVAILABLE JOBS",
"search_hint": "Search jobs..."
},
"filter": {
"all": "All Jobs",
"one_day": "One Day",
"multi_day": "Multi Day",
"long_term": "Long Term"
},
"status": {
"confirmed": "CONFIRMED",
"act_now": "ACT NOW",
"swap_requested": "SWAP REQUESTED",
"completed": "COMPLETED",
"no_show": "NO SHOW",
"pending_warning": "Please confirm assignment"
},
"action": {
"decline": "Decline",
"confirm": "Confirm",
"request_swap": "Request Swap"
},
"details": {
"additional": "ADDITIONAL DETAILS",
"days": "$days Days",
"exp_total": "(exp.total \\$$amount)",
"pending_time": "Pending $time ago"
},
"tags": {
"immediate_start": "Immediate start",
"no_experience": "No experience"
}
}
}

View File

@@ -8,6 +8,7 @@
library;
export 'src/mocks/auth_repository_mock.dart';
export 'src/mocks/shifts_repository_mock.dart';
export 'src/mocks/staff_repository_mock.dart';
export 'src/mocks/profile_repository_mock.dart';
export 'src/mocks/event_repository_mock.dart';

View File

@@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart';
// TODO: Implement EventRepositoryInterface once defined in a feature package.
class EventRepositoryMock {
Future<Assignment> applyForPosition(String positionId, String staffId) async {
await Future.delayed(const Duration(milliseconds: 600));
await Future<void>.delayed(const Duration(milliseconds: 600));
return Assignment(
id: 'assign_1',
positionId: positionId,
@@ -13,12 +13,12 @@ class EventRepositoryMock {
}
Future<Event?> getEvent(String id) async {
await Future.delayed(const Duration(milliseconds: 300));
await Future<void>.delayed(const Duration(milliseconds: 300));
return _mockEvent;
}
Future<List<EventShift>> getEventShifts(String eventId) async {
await Future.delayed(const Duration(milliseconds: 300));
await Future<void>.delayed(const Duration(milliseconds: 300));
return <EventShift>[
const EventShift(
id: 'shift_1',
@@ -30,7 +30,7 @@ class EventRepositoryMock {
}
Future<List<Assignment>> getStaffAssignments(String staffId) async {
await Future.delayed(const Duration(milliseconds: 500));
await Future<void>.delayed(const Duration(milliseconds: 500));
return <Assignment>[
const Assignment(
id: 'assign_1',
@@ -42,7 +42,7 @@ class EventRepositoryMock {
}
Future<List<Event>> getUpcomingEvents() async {
await Future.delayed(const Duration(milliseconds: 800));
await Future<void>.delayed(const Duration(milliseconds: 800));
return <Event>[_mockEvent];
}

View File

@@ -0,0 +1,89 @@
import 'package:krow_domain/krow_domain.dart';
import 'package:intl/intl.dart';
// Mock Implementation for now.
class ShiftsRepositoryMock {
Future<List<Shift>> getMyShifts() async {
await Future.delayed(const Duration(milliseconds: 500));
return [
Shift(
id: 'm1',
title: 'Warehouse Assistant',
clientName: 'Amazon',
logoUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Amazon_2024.svg/500px-Amazon_2024.svg.png',
hourlyRate: 22.5,
date: DateFormat('yyyy-MM-dd').format(DateTime.now().add(const Duration(days: 1))),
startTime: '09:00',
endTime: '17:00',
location: 'Logistics Park',
locationAddress: '456 Industrial Way',
status: 'confirmed',
createdDate: DateTime.now().toIso8601String(),
description: 'Standard warehouse duties. Safety boots required.',
),
];
}
Future<List<Shift>> getAvailableShifts() async {
await Future.delayed(const Duration(milliseconds: 500));
return [
Shift(
id: 'a1',
title: 'Bartender',
clientName: 'Club Luxe',
logoUrl: null,
hourlyRate: 30.0,
date: DateFormat('yyyy-MM-dd').format(DateTime.now().add(const Duration(days: 3))),
startTime: '20:00',
endTime: '02:00',
location: 'City Center',
locationAddress: '789 Nightlife Blvd',
status: 'open',
createdDate: DateTime.now().toIso8601String(),
description: 'Experience mixing cocktails required.',
),
// Add more mocks if needed
];
}
Future<List<Shift>> getPendingAssignments() async {
await Future.delayed(const Duration(milliseconds: 500));
return [
Shift(
id: 'p1',
title: 'Event Server',
clientName: 'Grand Hotel',
logoUrl: null,
hourlyRate: 25.0,
date: DateFormat('yyyy-MM-dd').format(DateTime.now().add(const Duration(days: 2))),
startTime: '16:00',
endTime: '22:00',
location: 'Downtown',
locationAddress: '123 Main St',
status: 'pending',
createdDate: DateTime.now().toIso8601String(),
),
];
}
Future<Shift?> getShiftDetails(String shiftId) async {
await Future.delayed(const Duration(milliseconds: 500));
return Shift(
id: shiftId,
title: 'Event Server',
clientName: 'Grand Hotel',
logoUrl: null,
hourlyRate: 25.0,
date: DateFormat('yyyy-MM-dd').format(DateTime.now()),
startTime: '16:00',
endTime: '22:00',
location: 'Downtown',
locationAddress: '123 Main St, New York, NY',
status: 'open',
createdDate: DateTime.now().toIso8601String(),
description: 'Provide exceptional customer service. Respond to guest requests or concerns promptly and professionally.',
managers: [],
);
}
}

View File

@@ -18,16 +18,17 @@ export 'src/entities/business/business.dart';
export 'src/entities/business/business_setting.dart';
export 'src/entities/business/hub.dart';
export 'src/entities/business/hub_department.dart';
export 'src/entities/business/biz_contract.dart';
export 'src/entities/business/vendor.dart';
// Events & Shifts
// Events & Assignments
export 'src/entities/events/event.dart';
export 'src/entities/events/event_shift.dart';
export 'src/entities/events/event_shift_position.dart';
export 'src/entities/events/assignment.dart';
export 'src/entities/events/work_session.dart';
// Shifts
export 'src/entities/shifts/shift.dart';
// Orders & Requests
export 'src/entities/orders/order_type.dart';
export 'src/entities/orders/one_time_order.dart';

View File

@@ -0,0 +1,91 @@
import 'package:equatable/equatable.dart';
class Shift extends Equatable {
final String id;
final String title;
final String clientName;
final String? logoUrl;
final double hourlyRate;
final String location;
final String locationAddress;
final String date;
final String startTime;
final String endTime;
final String createdDate;
final bool? tipsAvailable;
final bool? travelTime;
final bool? mealProvided;
final bool? parkingAvailable;
final bool? gasCompensation;
final String? description;
final String? instructions;
final List<ShiftManager>? managers;
final double? latitude;
final double? longitude;
final String? status;
final int? durationDays; // For multi-day shifts
const Shift({
required this.id,
required this.title,
required this.clientName,
this.logoUrl,
required this.hourlyRate,
required this.location,
required this.locationAddress,
required this.date,
required this.startTime,
required this.endTime,
required this.createdDate,
this.tipsAvailable,
this.travelTime,
this.mealProvided,
this.parkingAvailable,
this.gasCompensation,
this.description,
this.instructions,
this.managers,
this.latitude,
this.longitude,
this.status,
this.durationDays,
});
@override
List<Object?> get props => [
id,
title,
clientName,
logoUrl,
hourlyRate,
location,
locationAddress,
date,
startTime,
endTime,
createdDate,
tipsAvailable,
travelTime,
mealProvided,
parkingAvailable,
gasCompensation,
description,
instructions,
managers,
latitude,
longitude,
status,
durationDays,
];
}
class ShiftManager extends Equatable {
final String name;
final String phone;
final String? avatar;
const ShiftManager({required this.name, required this.phone, this.avatar});
@override
List<Object?> get props => [name, phone, avatar];
}

View File

@@ -0,0 +1,5 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
# Add project specific rules here

View File

@@ -0,0 +1,70 @@
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/shifts_repository_interface.dart';
/// Implementation of [ShiftsRepositoryInterface] that delegates to [ShiftsRepositoryMock].
///
/// This class resides in the data layer and handles the communication with
/// the external data sources (currently mocks).
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
final ShiftsRepositoryMock _mock;
ShiftsRepositoryImpl({ShiftsRepositoryMock? mock}) : _mock = mock ?? ShiftsRepositoryMock();
@override
Future<List<Shift>> getMyShifts() async {
return _mock.getMyShifts();
}
@override
Future<List<Shift>> getAvailableShifts(String query, String type) async {
// Delegates to mock. Logic kept here temporarily as per architecture constraints
// on data_connect modifications, mimicking a query capable datasource.
var shifts = await _mock.getAvailableShifts();
// Simple in-memory filtering for mock adapter
if (query.isNotEmpty) {
shifts = shifts.where((s) =>
s.title.toLowerCase().contains(query.toLowerCase()) ||
s.clientName.toLowerCase().contains(query.toLowerCase())
).toList();
}
if (type != 'all') {
if (type == 'one-day') {
shifts = shifts.where((s) => !s.title.contains('Multi-Day') && !s.title.contains('Long Term')).toList();
} else if (type == 'multi-day') {
shifts = shifts.where((s) => s.title.contains('Multi-Day')).toList();
} else if (type == 'long-term') {
shifts = shifts.where((s) => s.title.contains('Long Term')).toList();
}
}
return shifts;
}
@override
Future<List<Shift>> getPendingAssignments() async {
return _mock.getPendingAssignments();
}
@override
Future<Shift?> getShiftDetails(String shiftId) async {
return _mock.getShiftDetails(shiftId);
}
@override
Future<void> applyForShift(String shiftId) async {
await Future.delayed(const Duration(milliseconds: 500));
}
@override
Future<void> acceptShift(String shiftId) async {
await Future.delayed(const Duration(milliseconds: 500));
}
@override
Future<void> declineShift(String shiftId) async {
await Future.delayed(const Duration(milliseconds: 500));
}
}

View File

@@ -0,0 +1,19 @@
import 'package:krow_core/core.dart';
/// Arguments for [GetAvailableShiftsUseCase].
class GetAvailableShiftsArguments extends UseCaseArgument {
/// The search query to filter shifts.
final String query;
/// The job type filter (e.g., 'all', 'one-day', 'multi-day', 'long-term').
final String type;
/// Creates a [GetAvailableShiftsArguments] instance.
const GetAvailableShiftsArguments({
this.query = '',
this.type = 'all',
});
@override
List<Object?> get props => [query, type];
}

View File

@@ -0,0 +1,28 @@
import 'package:krow_domain/krow_domain.dart';
/// Interface for the Shifts Repository.
///
/// Defines the contract for accessing and modifying shift-related data.
/// Implementations of this interface should reside in the data layer.
abstract interface class ShiftsRepositoryInterface {
/// Retrieves the list of shifts assigned to the current user.
Future<List<Shift>> getMyShifts();
/// Retrieves available shifts matching the given [query] and [type].
Future<List<Shift>> getAvailableShifts(String query, String type);
/// Retrieves shifts that are pending acceptance by the user.
Future<List<Shift>> getPendingAssignments();
/// Retrieves detailed information for a specific shift by [shiftId].
Future<Shift?> getShiftDetails(String shiftId);
/// Applies for a specific open shift.
Future<void> applyForShift(String shiftId);
/// Accepts a pending shift assignment.
Future<void> acceptShift(String shiftId);
/// Declines a pending shift assignment.
Future<void> declineShift(String shiftId);
}

View File

@@ -0,0 +1,19 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/shifts_repository_interface.dart';
import '../arguments/get_available_shifts_arguments.dart';
/// Use case for retrieving available shifts with filters.
///
/// This use case delegates to [ShiftsRepositoryInterface].
class GetAvailableShiftsUseCase extends UseCase<GetAvailableShiftsArguments, List<Shift>> {
final ShiftsRepositoryInterface repository;
GetAvailableShiftsUseCase(this.repository);
@override
Future<List<Shift>> call(GetAvailableShiftsArguments arguments) async {
return repository.getAvailableShifts(arguments.query, arguments.type);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/shifts_repository_interface.dart';
/// Use case for retrieving the user's assigned shifts.
///
/// This use case delegates to [ShiftsRepositoryInterface].
class GetMyShiftsUseCase extends NoInputUseCase<List<Shift>> {
final ShiftsRepositoryInterface repository;
GetMyShiftsUseCase(this.repository);
@override
Future<List<Shift>> call() async {
return repository.getMyShifts();
}
}

View File

@@ -0,0 +1,18 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/shifts_repository_interface.dart';
/// Use case for retrieving pending shift assignments.
///
/// This use case delegates to [ShiftsRepositoryInterface].
class GetPendingAssignmentsUseCase extends NoInputUseCase<List<Shift>> {
final ShiftsRepositoryInterface repository;
GetPendingAssignmentsUseCase(this.repository);
@override
Future<List<Shift>> call() async {
return repository.getPendingAssignments();
}
}

View File

@@ -0,0 +1,83 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:meta/meta.dart';
import '../../../domain/usecases/get_available_shifts_usecase.dart';
import '../../../domain/arguments/get_available_shifts_arguments.dart';
import '../../../domain/usecases/get_my_shifts_usecase.dart';
import '../../../domain/usecases/get_pending_assignments_usecase.dart';
part 'shifts_event.dart';
part 'shifts_state.dart';
class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
final GetMyShiftsUseCase getMyShifts;
final GetAvailableShiftsUseCase getAvailableShifts;
final GetPendingAssignmentsUseCase getPendingAssignments;
ShiftsBloc({
required this.getMyShifts,
required this.getAvailableShifts,
required this.getPendingAssignments,
}) : super(ShiftsInitial()) {
on<LoadShiftsEvent>(_onLoadShifts);
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
}
Future<void> _onLoadShifts(
LoadShiftsEvent event,
Emitter<ShiftsState> emit,
) async {
if (state is! ShiftsLoaded) {
emit(ShiftsLoading());
}
// Determine what to load based on current tab?
// Or load all for simplicity as per prototype logic which had them all in memory.
try {
final myShiftsResult = await getMyShifts();
final pendingResult = await getPendingAssignments();
// Initial available with defaults
final availableResult = await getAvailableShifts(const GetAvailableShiftsArguments());
emit(ShiftsLoaded(
myShifts: myShiftsResult,
pendingShifts: pendingResult,
availableShifts: availableResult,
searchQuery: '',
jobType: 'all',
));
} catch (_) {
emit(const ShiftsError('Failed to load shifts'));
}
}
Future<void> _onFilterAvailableShifts(
FilterAvailableShiftsEvent event,
Emitter<ShiftsState> emit,
) async {
final currentState = state;
if (currentState is ShiftsLoaded) {
// Optimistic update or loading indicator?
// Since it's filtering, we can just reload available.
try {
final result = await getAvailableShifts(GetAvailableShiftsArguments(
query: event.query ?? currentState.searchQuery,
type: event.jobType ?? currentState.jobType,
));
emit(currentState.copyWith(
availableShifts: result,
searchQuery: event.query ?? currentState.searchQuery,
jobType: event.jobType ?? currentState.jobType,
));
} catch (_) {
// Error handling if filter fails
}
}
}
}

View File

@@ -0,0 +1,21 @@
part of 'shifts_bloc.dart';
@immutable
sealed class ShiftsEvent extends Equatable {
const ShiftsEvent();
@override
List<Object?> get props => [];
}
class LoadShiftsEvent extends ShiftsEvent {}
class FilterAvailableShiftsEvent extends ShiftsEvent {
final String? query;
final String? jobType;
const FilterAvailableShiftsEvent({this.query, this.jobType});
@override
List<Object?> get props => [query, jobType];
}

View File

@@ -0,0 +1,57 @@
part of 'shifts_bloc.dart';
@immutable
sealed class ShiftsState extends Equatable {
const ShiftsState();
@override
List<Object> get props => [];
}
class ShiftsInitial extends ShiftsState {}
class ShiftsLoading extends ShiftsState {}
class ShiftsLoaded extends ShiftsState {
final List<Shift> myShifts;
final List<Shift> pendingShifts;
final List<Shift> availableShifts;
final String searchQuery;
final String jobType;
const ShiftsLoaded({
required this.myShifts,
required this.pendingShifts,
required this.availableShifts,
required this.searchQuery,
required this.jobType,
});
ShiftsLoaded copyWith({
List<Shift>? myShifts,
List<Shift>? pendingShifts,
List<Shift>? availableShifts,
String? searchQuery,
String? jobType,
}) {
return ShiftsLoaded(
myShifts: myShifts ?? this.myShifts,
pendingShifts: pendingShifts ?? this.pendingShifts,
availableShifts: availableShifts ?? this.availableShifts,
searchQuery: searchQuery ?? this.searchQuery,
jobType: jobType ?? this.jobType,
);
}
@override
List<Object> get props => [myShifts, pendingShifts, availableShifts, searchQuery, jobType];
}
class ShiftsError extends ShiftsState {
final String message;
const ShiftsError(this.message);
@override
List<Object> get props => [message];
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
extension ShiftsNavigator on IModularNavigator {
void pushShiftDetails(Shift shift) {
pushNamed('/shifts/details/${shift.id}', arguments: shift);
}
// Example for going back or internal navigation if needed
}

View File

@@ -0,0 +1,368 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.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; // 121826
static const Color krowMuted = UiColors.textSecondary; // 6A7382
static const Color krowBorder = UiColors.border; // E3E6E9
static const Color krowBackground = UiColors.background; // FAFBFC
static const Color white = Colors.white;
}
class ShiftDetailsPage extends StatefulWidget {
final String shiftId;
final Shift? shift;
const ShiftDetailsPage({super.key, required this.shiftId, this.shift});
@override
State<ShiftDetailsPage> createState() => _ShiftDetailsPageState();
}
class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
late Shift _shift;
bool _isLoading = true;
bool _showDetails = true;
bool _isApplying = false;
@override
void initState() {
super.initState();
_loadShift();
}
void _loadShift() async {
if (widget.shift != null) {
_shift = widget.shift!;
setState(() => _isLoading = false);
} else {
// Simulate fetch or logic to handle missing data
await Future.delayed(const Duration(milliseconds: 500));
if (mounted) {
// Mock data from POC if needed, but assuming shift is always passed in this context
// based on ShiftsPage navigation.
// If generic fetch needed, we would use a Repo/Bloc here.
// For now, stop loading.
setState(() => _isLoading = false);
}
}
}
double _calculateHours(String start, String end) {
try {
final startParts = start.split(':').map(int.parse).toList();
final endParts = end.split(':').map(int.parse).toList();
double h =
(endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60;
if (h < 0) h += 24;
return h;
} catch (e) {
return 0;
}
}
Widget _buildTag(IconData icon, String label, Color bg, Color activeColor) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: activeColor),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: activeColor,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Scaffold(
backgroundColor: AppColors.krowBackground,
body: Center(child: CircularProgressIndicator()),
);
}
final hours = _calculateHours(_shift.startTime, _shift.endTime);
final totalPay = _shift.hourlyRate * hours;
return Scaffold(
backgroundColor: AppColors.krowBackground,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: AppColors.krowMuted),
onPressed: () => Modular.to.pop(),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: AppColors.krowBorder, height: 1.0),
),
),
body: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 120),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Pending Badge (Mock logic)
Align(
alignment: Alignment.centerRight,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.krowYellow.withOpacity(0.3),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Pending 6h ago',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.krowCharcoal,
),
),
),
),
const SizedBox(height: 16),
// Header
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: _shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
_shift.logoUrl!,
fit: BoxFit.contain,
),
)
: Center(
child: Text(
_shift.clientName.isNotEmpty ? _shift.clientName[0] : 'K',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.krowBlue,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
_shift.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'\$${_shift.hourlyRate.toStringAsFixed(0)}/h',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
Text(
'(exp.total \$${totalPay.toStringAsFixed(0)})',
style: const TextStyle(
fontSize: 12,
color: AppColors.krowMuted,
),
),
],
),
],
),
Text(
_shift.clientName,
style: const TextStyle(color: AppColors.krowMuted),
),
],
),
),
],
),
const SizedBox(height: 16),
// Tags
Row(
children: [
_buildTag(
UiIcons.zap,
'Immediate start',
AppColors.krowBlue.withOpacity(0.1),
AppColors.krowBlue,
),
const SizedBox(width: 8),
_buildTag(
UiIcons.star,
'No experience',
AppColors.krowYellow.withOpacity(0.3),
AppColors.krowCharcoal,
),
],
),
const SizedBox(height: 24),
// Additional Details
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.krowBorder),
),
child: Column(
children: [
InkWell(
onTap: () =>
setState(() => _showDetails = !_showDetails),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'ADDITIONAL DETAILS',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
color: AppColors.krowMuted,
),
),
Icon(
_showDetails
? UiIcons.chevronUp
: UiIcons.chevronDown,
color: AppColors.krowMuted,
size: 20,
),
],
),
),
),
if (_showDetails && _shift.description != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(height: 1, color: AppColors.krowBorder),
const SizedBox(height: 16),
Text(
_shift.description!,
style: const TextStyle(
color: AppColors.krowCharcoal,
height: 1.5,
),
),
],
),
),
],
),
),
],
),
),
// Action Button
Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: AppColors.krowBorder)),
),
child: SafeArea(
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isApplying ? null : () {
setState(() {
_isApplying = true;
});
// Simulate Apply
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() => _isApplying = false);
Modular.to.pop();
}
});
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.krowBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: _isApplying
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)
)
: const Text(
'Apply Now',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,618 @@
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;
}
class ShiftsPage extends StatefulWidget {
final String? initialTab;
const ShiftsPage({super.key, this.initialTab});
@override
State<ShiftsPage> createState() => _ShiftsPageState();
}
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
void initState() {
super.initState();
_activeTab = widget.initialTab ?? 'myshifts';
_bloc.add(LoadShiftsEvent());
}
@override
void didUpdateWidget(ShiftsPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialTab != null && widget.initialTab != _activeTab) {
setState(() {
_activeTab = widget.initialTab!;
});
}
}
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) {
// TODO: Implement Bloc event
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Shift confirmed! (Placeholder)')),
);
}
void _declineShift(String id) {
// TODO: Implement Bloc event
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Shift declined. (Placeholder)')),
);
}
@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> historyShifts = []; // Not in state yet, placeholder
// Filter logic from POC
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.title.contains('Long Term') && !s.title.contains('Multi-Day');
}
if (_jobType == 'multi-day') return s.title.contains('Multi-Day');
if (_jobType == 'long-term') return s.title.contains('Long Term');
return true;
}).toList();
final calendarDays = _getCalendarDays();
final weekStartDate = calendarDays.first;
final weekEndDate = calendarDays.last;
final visibleMyShifts = myShifts.where((s) {
// Primitive check if shift date string compare
// In real app use DateTime logic
final sDateStr = s.date;
final wStartStr = DateFormat('yyyy-MM-dd').format(weekStartDate);
final wEndStr = DateFormat('yyyy-MM-dd').format(weekEndDate);
return sDateStr.compareTo(wStartStr) >= 0 &&
sDateStr.compareTo(wEndStr) <= 0;
}).toList();
return Scaffold(
backgroundColor: AppColors.krowBackground,
body: Column(
children: [
// Header (Blue)
Container(
color: AppColors.krowBlue,
padding: EdgeInsets.fromLTRB(
20,
MediaQuery.of(context).padding.top + 20,
20,
24,
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Shifts",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Row(
children: [
_buildDemoButton("Demo: Cancel <4hr", const Color(0xFFEF4444), () {
setState(() => _cancelledShiftDemo = 'lastMinute');
_showCancelledModal('lastMinute');
}),
const SizedBox(width: 8),
_buildDemoButton("Demo: Cancel >4hr", const Color(0xFFF59E0B), () {
setState(() => _cancelledShiftDemo = 'advance');
_showCancelledModal('advance');
}),
],
),
],
),
const SizedBox(height: 16),
// Tabs
Row(
children: [
_buildTab("myshifts", "My Shifts", LucideIcons.calendar, myShifts.length),
const SizedBox(width: 8),
_buildTab("find", "Find Shifts", LucideIcons.search, filteredJobs.length),
const SizedBox(width: 8),
_buildTab("history", "History", LucideIcons.clock, historyShifts.length),
],
),
],
),
),
// Calendar Selector
if (_activeTab == 'myshifts')
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () => setState(() => _weekOffset--),
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(LucideIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal),
),
),
Text(
DateFormat('MMMM yyyy').format(weekStartDate),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.krowCharcoal,
),
),
InkWell(
onTap: () => setState(() => _weekOffset++),
borderRadius: BorderRadius.circular(20),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(LucideIcons.chevronRight, size: 20, color: AppColors.krowCharcoal),
),
),
],
),
),
// 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) => s.date == dateStr);
return GestureDetector(
onTap: () => setState(() => _selectedDate = date),
child: Container(
width: 44,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? AppColors.krowBlue : Colors.white,
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: isSelected ? AppColors.krowBlue : AppColors.krowBorder,
width: 1,
),
),
child: Column(
children: [
Text(
date.day.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.white : AppColors.krowCharcoal,
),
),
const SizedBox(height: 2),
Text(
DateFormat('E').format(date),
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: isSelected ? Colors.white.withOpacity(0.8) : AppColors.krowMuted,
),
),
if (hasShifts)
Container(
margin: const EdgeInsets.only(top: 4),
width: 6,
height: 6,
decoration: BoxDecoration(
color: isSelected ? Colors.white : AppColors.krowBlue,
shape: BoxShape.circle,
),
),
],
),
),
);
}).toList(),
),
],
),
),
if (_activeTab == 'myshifts')
const Divider(height: 1, color: AppColors.krowBorder),
// Body Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
if (_activeTab == 'find') ...[
// Search & Filter
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
height: 48,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.krowBorder),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: TextField(
onChanged: (val) => setState(() => _searchQuery = val), // Local filter for now
decoration: const InputDecoration(
prefixIcon: Icon(LucideIcons.search, size: 20, color: AppColors.krowMuted),
border: InputBorder.none,
hintText: "Search jobs...",
hintStyle: TextStyle(color: AppColors.krowMuted, fontSize: 14),
contentPadding: EdgeInsets.symmetric(vertical: 12),
),
),
),
),
Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFF1F3F5),
borderRadius: BorderRadius.circular(999),
),
child: Row(
children: [
_buildFilterTab('all', 'All Jobs'),
_buildFilterTab('one-day', 'One Day'),
_buildFilterTab('multi-day', 'Multi-Day'),
_buildFilterTab('long-term', 'Long Term'),
],
),
),
],
if (_activeTab == 'myshifts') ...[
if (pendingAssignments.isNotEmpty) ...[
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(width: 8, height: 8, decoration: const BoxDecoration(color: Color(0xFFF59E0B), shape: BoxShape.circle)),
const SizedBox(width: 8),
const Text("Awaiting Confirmation", style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFFD97706)
)),
],
),
),
),
...pendingAssignments.map((shift) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: ShiftAssignmentCard(
shift: shift,
onConfirm: () => _confirmShift(shift.id),
onDecline: () => _declineShift(shift.id),
),
)),
],
// Cancelled Shifts Demo (Visual only as per POC)
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: const Text("Cancelled Shifts", style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowMuted
)),
),
),
_buildCancelledCard(
title: "Annual Tech Conference", client: "TechCorp Inc.", pay: "\$200", rate: "\$25/hr · 8h",
date: "Today", time: "10:00 AM - 6:00 PM", address: "123 Convention Center Dr", isLastMinute: true,
onTap: () => setState(() => _cancelledShiftDemo = 'lastMinute')
),
const SizedBox(height: 12),
_buildCancelledCard(
title: "Morning Catering Setup", client: "EventPro Services", pay: "\$120", rate: "\$20/hr · 6h",
date: "Tomorrow", time: "8:00 AM - 2:00 PM", address: "456 Grand Ballroom Ave", isLastMinute: false,
onTap: () => setState(() => _cancelledShiftDemo = 'advance')
),
const SizedBox(height: 24),
// Confirmed Shifts
if (visibleMyShifts.isNotEmpty) ...[
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: const Text("Confirmed Shifts", style: TextStyle(
fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowMuted
)),
),
),
...visibleMyShifts.map((shift) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: MyShiftCard(shift: shift),
)),
],
],
if (_activeTab == 'find') ...[
if (filteredJobs.isEmpty)
_buildEmptyState(LucideIcons.search, "No jobs available", "Check back later", null, null)
else
...filteredJobs.map((shift) => GestureDetector(
onTap: () => Modular.to.pushNamed('details/${shift.id}', arguments: shift),
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: MyShiftCard(shift: shift),
),
)),
],
if (_activeTab == 'history')
_buildEmptyState(LucideIcons.clock, "No shift history", "Completed shifts appear here", null, null),
],
),
),
),
],
),
);
},
),
);
}
Widget _buildFilterTab(String id, String label) {
final isSelected = _jobType == id;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _jobType = id),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppColors.krowBlue : Colors.transparent,
borderRadius: BorderRadius.circular(999),
boxShadow: isSelected ? [BoxShadow(color: AppColors.krowBlue.withOpacity(0.2), blurRadius: 4, offset: const Offset(0, 2))] : null,
),
child: Text(label, textAlign: TextAlign.center, style: TextStyle(
fontSize: 11, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : AppColors.krowMuted
)),
),
),
);
}
Widget _buildTab(String id, String label, IconData icon, int count) {
final isActive = _activeTab == id;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _activeTab = id),
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),
),
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),
),
child: Center(child: Text("$count", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: isActive ? AppColors.krowBlue : Colors.white))),
),
],
),
),
),
);
}
Widget _buildDemoButton(String label, Color color, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
child: Text(label, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: 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(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.krowBlue.withAlpha((0.15 * 255).round()), AppColors.krowBlue.withAlpha((0.08 * 255).round())]), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.krowBlue.withAlpha((0.15 * 255).round()))), 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))]),
])),
]),
])),
);
}
void _showCancelledModal(String type) {
final isLastMinute = type == 'lastMinute';
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row(
children: [
const Icon(LucideIcons.xCircle, color: Color(0xFFEF4444)),
const SizedBox(width: 8),
const Text("Shift Cancelled"),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"We're sorry, but the following shift has been cancelled by the client:",
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Annual Tech Conference", style: TextStyle(fontWeight: FontWeight.bold)),
Text("Today, 10:00 AM - 6:00 PM"),
],
),
),
const SizedBox(height: 16),
if (isLastMinute)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFECFDF5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF10B981)),
),
child: const Row(
children: [
Icon(LucideIcons.checkCircle, color: Color(0xFF10B981), size: 16),
SizedBox(width: 8),
Expanded(
child: Text(
"You are eligible for 4hr cancellation compensation.",
style: TextStyle(
fontSize: 12, color: Color(0xFF065F46), fontWeight: FontWeight.w500),
),
),
],
),
)
else
const Text(
"Reduced schedule at the venue. No compensation is due as this was cancelled more than 4 hours in advance.",
style: TextStyle(fontSize: 12, color: AppColors.krowMuted),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Close"),
),
],
),
);
}
}

View File

@@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
class MyShiftCard extends StatefulWidget {
final Shift shift;
final bool historyMode;
final VoidCallback? onAccept;
final VoidCallback? onDecline;
final VoidCallback? onRequestSwap;
final int index;
const MyShiftCard({
super.key,
required this.shift,
this.historyMode = false,
this.onAccept,
this.onDecline,
this.onRequestSwap,
this.index = 0,
});
@override
State<MyShiftCard> createState() => _MyShiftCardState();
}
class _MyShiftCardState extends State<MyShiftCard> {
bool _isExpanded = false;
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return time;
}
}
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final d = DateTime(date.year, date.month, date.day);
if (d == today) return 'Today';
if (d == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
} catch (e) {
return dateStr;
}
}
double _calculateDuration() {
if (widget.shift.startTime.isEmpty || widget.shift.endTime.isEmpty) {
return 0;
}
try {
final s = widget.shift.startTime.split(':').map(int.parse).toList();
final e = widget.shift.endTime.split(':').map(int.parse).toList();
double hours = ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60;
if (hours < 0) hours += 24;
return hours.roundToDouble();
} catch (_) {
return 0;
}
}
String _getShiftType() {
// Check title for type indicators (for mock data)
if (widget.shift.title.contains('Long Term')) return t.staff_shifts.filter.long_term;
if (widget.shift.title.contains('Multi-Day')) return t.staff_shifts.filter.multi_day;
return t.staff_shifts.filter.one_day;
}
@override
Widget build(BuildContext context) {
// ignore: unused_local_variable
final duration = _calculateDuration();
// Status Logic
String? status = widget.shift.status;
Color statusColor = UiColors.primary;
Color statusBg = UiColors.primary;
String statusText = '';
IconData? statusIcon;
if (status == 'confirmed') {
statusText = t.staff_shifts.status.confirmed;
statusColor = UiColors.textLink;
statusBg = UiColors.primary;
} else if (status == 'pending' || status == 'open') {
statusText = t.staff_shifts.status.act_now;
statusColor = UiColors.destructive;
statusBg = UiColors.destructive;
} else if (status == 'swap') {
statusText = t.staff_shifts.status.swap_requested;
statusColor = UiColors.textWarning;
statusBg = UiColors.textWarning;
statusIcon = UiIcons.swap;
} else if (status == 'completed') {
statusText = t.staff_shifts.status.completed;
statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess;
} else if (status == 'no_show') {
statusText = t.staff_shifts.status.no_show;
statusColor = UiColors.destructive;
statusBg = UiColors.destructive;
}
return GestureDetector(
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
// Collapsed Content
Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status Badge
if (statusText.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
if (statusIcon != null)
Padding(
padding: const EdgeInsets.only(right: 6),
child: Icon(
statusIcon,
size: 12,
color: statusColor,
),
)
else
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: statusBg,
shape: BoxShape.circle,
),
),
Text(
statusText,
style: UiTypography.display3r.copyWith(
color: statusColor,
letterSpacing: 0.5,
),
),
// Shift Type Badge for available/pending shifts
if (status == 'open' || status == 'pending') ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: UiColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_getShiftType(),
style: UiTypography.display3r.copyWith(
color: UiColors.primary,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
// Main Content
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Date/Time Column
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
_formatDate(widget.shift.date),
style: UiTypography.display2m.copyWith(
color: UiColors.textPrimary,
),
),
if (widget.shift.durationDays != null) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: UiColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
t.staff_shifts.details.days(days: widget.shift.durationDays!),
style: UiTypography.display3r.copyWith(
color: UiColors.primary,
fontWeight: FontWeight.w600,
),
),
),
],
],
),
const SizedBox(height: 4),
Text(
'${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}',
style: UiTypography.body2r.copyWith(
color: UiColors.textSecondary,
),
),
const SizedBox(height: 12),
Text(
widget.shift.title,
style: UiTypography.body2m.copyWith(
color: UiColors.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
const Icon(
UiIcons.mapPin,
size: 12,
color: UiColors.iconSecondary,
),
const SizedBox(width: 4),
Text(
widget.shift.clientName,
style: UiTypography.display3r.copyWith(
color: UiColors.textSecondary,
),
),
],
),
],
),
),
// Logo Box
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: UiColors.background,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: UiColors.border),
),
child: widget.shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
widget.shift.logoUrl!,
fit: BoxFit.cover,
),
)
: Center(
child: Text(
widget.shift.clientName.isNotEmpty
? widget.shift.clientName[0]
: 'K',
style: UiTypography.title1m.textLink,
),
),
),
],
),
],
),
),
// Expanded Actions
AnimatedCrossFade(
firstChild: const SizedBox(height: 0),
secondChild: Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: UiColors.border),
),
),
child: Column(
children: [
// Warning for Pending
if (status == 'pending' || status == 'open')
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
color: UiColors.accent.withOpacity(0.1),
child: Row(
children: [
const Icon(
UiIcons.warning,
size: 14,
color: UiColors.textWarning,
),
const SizedBox(width: 8),
Text(
t.staff_shifts.status.pending_warning,
style: UiTypography.display3r.copyWith(
color: UiColors.textWarning,
fontWeight: FontWeight.w500,
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
if (status == 'pending' || status == 'open') ...[
Expanded(
child: OutlinedButton(
onPressed: widget.onDecline,
style: OutlinedButton.styleFrom(
foregroundColor: UiColors.destructive,
side: const BorderSide(color: UiColors.border),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(t.staff_shifts.action.decline),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: widget.onAccept,
style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary,
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(t.staff_shifts.action.confirm),
),
),
] else if (status == 'confirmed') ...[
Expanded(
child: OutlinedButton.icon(
onPressed: widget.onRequestSwap,
icon: const Icon(UiIcons.swap, size: 16),
label: Text(t.staff_shifts.action.request_swap),
style: OutlinedButton.styleFrom(
foregroundColor: UiColors.textPrimary,
side: const BorderSide(color: UiColors.border),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
],
),
),
],
),
),
crossFadeState: _isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
class ShiftAssignmentCard extends StatelessWidget {
final Shift shift;
final VoidCallback onConfirm;
final VoidCallback onDecline;
final bool isConfirming;
const ShiftAssignmentCard({
super.key,
required this.shift,
required this.onConfirm,
required this.onDecline,
this.isConfirming = false,
});
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return time;
}
}
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final d = DateTime(date.year, date.month, date.day);
if (d == today) return 'Today';
if (d == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
} catch (e) {
return dateStr;
}
}
double _calculateHours(String start, String end) {
if (start.isEmpty || end.isEmpty) return 0;
try {
final s = start.split(':').map(int.parse).toList();
final e = end.split(':').map(int.parse).toList();
return ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60;
} catch (_) {
return 0;
}
}
@override
Widget build(BuildContext context) {
final hours = _calculateHours(shift.startTime, shift.endTime);
final totalPay = shift.hourlyRate * hours;
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: UiColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: UiColors.secondary,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
shift.clientName.isNotEmpty
? shift.clientName[0]
: 'K',
style: UiTypography.body2b.copyWith(
color: UiColors.textSecondary,
),
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
shift.title,
style: UiTypography.body2b.copyWith(
color: UiColors.textPrimary,
),
),
Text(
shift.clientName,
style: UiTypography.display3r.copyWith(
color: UiColors.textSecondary,
),
),
],
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"\$${totalPay.toStringAsFixed(0)}",
style: UiTypography.display2m.copyWith(
color: UiColors.textPrimary,
),
),
Text(
"\$${shift.hourlyRate}/hr · ${hours}h",
style: UiTypography.display3r.copyWith(
color: UiColors.textSecondary,
),
),
],
),
],
),
),
// Details
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
UiIcons.calendar,
size: 14,
color: UiColors.iconSecondary,
),
const SizedBox(width: 6),
Text(
_formatDate(shift.date),
style: UiTypography.display3r.copyWith(
color: UiColors.textSecondary,
),
),
const SizedBox(width: 16),
const Icon(
UiIcons.clock,
size: 14,
color: UiColors.iconSecondary,
),
const SizedBox(width: 6),
Text(
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
style: UiTypography.display3r.copyWith(
color: UiColors.textSecondary,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
const Icon(
UiIcons.mapPin,
size: 14,
color: UiColors.iconSecondary,
),
const SizedBox(width: 6),
Expanded(
child: Text(
shift.location,
style: UiTypography.display3r.copyWith(
color: UiColors.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
if (isConfirming) ...[
const Divider(height: 1),
Row(
children: [
Expanded(
child: TextButton(
onPressed: onDecline,
style: TextButton.styleFrom(
foregroundColor: UiColors.destructive,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(t.staff_shifts.action.decline),
),
),
Container(width: 1, height: 48, color: UiColors.border),
Expanded(
child: TextButton(
onPressed: onConfirm,
style: TextButton.styleFrom(
foregroundColor: UiColors.primary,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(t.staff_shifts.action.confirm),
),
),
],
),
],
],
),
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter_modular/flutter_modular.dart';
import 'domain/repositories/shifts_repository_interface.dart';
import 'data/repositories_impl/shifts_repository_impl.dart';
import 'domain/usecases/get_my_shifts_usecase.dart';
import 'domain/usecases/get_available_shifts_usecase.dart';
import 'domain/usecases/get_pending_assignments_usecase.dart';
import 'presentation/blocs/shifts/shifts_bloc.dart';
import 'presentation/pages/shifts_page.dart';
import 'presentation/pages/shift_details_page.dart';
class StaffShiftsModule extends Module {
@override
void binds(Injector i) {
// Repository
i.add<ShiftsRepositoryInterface>(ShiftsRepositoryImpl.new);
// UseCases
i.add(GetMyShiftsUseCase.new);
i.add(GetAvailableShiftsUseCase.new);
i.add(GetPendingAssignmentsUseCase.new);
// Bloc
i.add(ShiftsBloc.new);
}
@override
void routes(RouteManager r) {
r.child('/', child: (_) => const ShiftsPage());
r.child('/details/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data));
}
}

View File

@@ -0,0 +1,4 @@
library staff_shifts;
export 'src/staff_shifts_module.dart';

View File

@@ -0,0 +1,650 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
auto_injector:
dependency: transitive
description:
name: auto_injector
sha256: "1fc2624898e92485122eb2b1698dd42511d7ff6574f84a3a8606fc4549a1e8f8"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
core_localization:
dependency: "direct main"
description:
path: "../../../core_localization"
relative: true
source: path
version: "0.0.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csv:
dependency: transitive
description:
name: csv
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
url: "https://pub.dev"
source: hosted
version: "6.0.0"
design_system:
dependency: "direct main"
description:
path: "../../../design_system"
relative: true
source: path
version: "0.0.1"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev"
source: hosted
version: "2.1.5"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_localizations:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_modular:
dependency: "direct main"
description:
name: flutter_modular
sha256: "33a63d9fe61429d12b3dfa04795ed890f17d179d3d38e988ba7969651fcd5586"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
font_awesome_flutter:
dependency: transitive
description:
name: font_awesome_flutter
sha256: b9011df3a1fa02993630b8fb83526368cf2206a711259830325bab2f1d2a4eb0
url: "https://pub.dev"
source: hosted
version: "10.12.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: transitive
description:
name: google_fonts
sha256: "6996212014b996eaa17074e02b1b925b212f5e053832d9048970dc27255a8fb3"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
hooks:
dependency: transitive
description:
name: hooks
sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
krow_core:
dependency: "direct main"
description:
path: "../../../core"
relative: true
source: path
version: "0.0.1"
krow_data_connect:
dependency: "direct main"
description:
path: "../../../data_connect"
relative: true
source: path
version: "0.0.1"
krow_domain:
dependency: "direct main"
description:
path: "../../../domain"
relative: true
source: path
version: "0.0.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lucide_icons:
dependency: transitive
description:
name: lucide_icons
sha256: ad24d0fd65707e48add30bebada7d90bff2a1bba0a72d6e9b19d44246b0e83c4
url: "https://pub.dev"
source: hosted
version: "0.257.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
modular_core:
dependency: transitive
description:
name: modular_core
sha256: "1db0420a0dfb8a2c6dca846e7cbaa4ffeb778e247916dbcb27fb25aa566e5436"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "7fd0c4d8ac8980011753b9bdaed2bf15111365924cdeeeaeb596214ea2b03537"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: transitive
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
result_dart:
dependency: transitive
description:
name: result_dart
sha256: "0666b21fbdf697b3bdd9986348a380aa204b3ebe7c146d8e4cdaa7ce735e6054"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
url: "https://pub.dev"
source: hosted
version: "2.4.18"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
slang:
dependency: transitive
description:
name: slang
sha256: "13e3b6f07adc51ab751e7889647774d294cbce7a3382f81d9e5029acfe9c37b2"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
slang_flutter:
dependency: transitive
description:
name: slang_flutter
sha256: "0a4545cca5404d6b7487cf61cf1fe56c52daeb08de56a7574ee8381fbad035a0"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.7"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.4"

View File

@@ -0,0 +1,33 @@
name: staff_shifts
description: A new Flutter package project.
version: 0.0.1
publish_to: 'none'
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_modular: ^6.3.2
flutter_bloc: ^8.1.3
equatable: ^2.0.5
intl: ^0.20.2
# Internal packages
krow_core:
path: ../../../core
design_system:
path: ../../../design_system
krow_domain:
path: ../../../domain
krow_data_connect:
path: ../../../data_connect
core_localization:
path: ../../../core_localization
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0

View File

@@ -10,6 +10,7 @@ import 'package:staff_tax_forms/staff_tax_forms.dart';
import 'package:staff_documents/staff_documents.dart';
import 'package:staff_certificates/staff_certificates.dart';
import 'package:staff_attire/staff_attire.dart';
import 'package:staff_shifts/staff_shifts.dart';
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
@@ -28,10 +29,9 @@ class StaffMainModule extends Module {
'/',
child: (BuildContext context) => const StaffMainPage(),
children: <ParallelRoute<dynamic>>[
ChildRoute<dynamic>(
ModuleRoute<dynamic>(
StaffMainRoutes.shifts,
child: (BuildContext context) =>
const PlaceholderPage(title: 'Shifts'),
module: StaffShiftsModule(),
),
ChildRoute<dynamic>(
StaffMainRoutes.payments,

View File

@@ -43,8 +43,8 @@ dependencies:
path: ../profile_sections/compliance/certificates
staff_attire:
path: ../profile_sections/onboarding/attire
# staff_shifts:
# path: ../shifts
staff_shifts:
path: ../shifts
# staff_payments:
# path: ../payments

View File

@@ -1100,6 +1100,13 @@ packages:
relative: true
source: path
version: "0.0.1"
staff_shifts:
dependency: transitive
description:
path: "packages/features/staff/shifts"
relative: true
source: path
version: "0.0.1"
staff_tax_forms:
dependency: transitive
description: