From 8e429dda031e2d17762e1a5e9c73d882626b70f8 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 25 Jan 2026 16:09:11 -0500 Subject: [PATCH] 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. --- .../lib/src/l10n/en.i18n.json | 42 ++ .../lib/src/l10n/es.i18n.json | 42 ++ .../data_connect/lib/krow_data_connect.dart | 1 + .../lib/src/mocks/event_repository_mock.dart | 10 +- .../lib/src/mocks/shifts_repository_mock.dart | 89 +++ .../packages/domain/lib/krow_domain.dart | 7 +- .../domain/lib/src/entities/shifts/shift.dart | 91 +++ .../staff/shifts/analysis_options.yaml | 5 + .../shifts_repository_impl.dart | 70 ++ .../get_available_shifts_arguments.dart | 19 + .../shifts_repository_interface.dart | 28 + .../get_available_shifts_usecase.dart | 19 + .../usecases/get_my_shifts_usecase.dart | 18 + .../get_pending_assignments_usecase.dart | 18 + .../blocs/shifts/shifts_bloc.dart | 83 +++ .../blocs/shifts/shifts_event.dart | 21 + .../blocs/shifts/shifts_state.dart | 57 ++ .../navigation/shifts_navigator.dart | 10 + .../pages/shift_details_page.dart | 368 ++++++++++ .../src/presentation/pages/shifts_page.dart | 618 +++++++++++++++++ .../presentation/widgets/my_shift_card.dart | 412 +++++++++++ .../widgets/shift_assignment_card.dart | 242 +++++++ .../shifts/lib/src/staff_shifts_module.dart | 31 + .../staff/shifts/lib/staff_shifts.dart | 4 + .../features/staff/shifts/pubspec.lock | 650 ++++++++++++++++++ .../features/staff/shifts/pubspec.yaml | 33 + .../staff_main/lib/src/staff_main_module.dart | 6 +- .../features/staff/staff_main/pubspec.yaml | 4 +- apps/mobile/pubspec.lock | 7 + 29 files changed, 2992 insertions(+), 13 deletions(-) create mode 100644 apps/mobile/packages/data_connect/lib/src/mocks/shifts_repository_mock.dart create mode 100644 apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart create mode 100644 apps/mobile/packages/features/staff/shifts/analysis_options.yaml create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_available_shifts_arguments.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_available_shifts_usecase.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_pending_assignments_usecase.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart create mode 100644 apps/mobile/packages/features/staff/shifts/pubspec.lock create mode 100644 apps/mobile/packages/features/staff/shifts/pubspec.yaml diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index 1f529e83..514f70cb 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -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" + } } } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 6023c314..21177038 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -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" + } } } diff --git a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart index 01ff1773..429cf0f1 100644 --- a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart +++ b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart @@ -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'; diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart index 0cdd03c2..4bbfb69e 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/event_repository_mock.dart @@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart'; // TODO: Implement EventRepositoryInterface once defined in a feature package. class EventRepositoryMock { Future applyForPosition(String positionId, String staffId) async { - await Future.delayed(const Duration(milliseconds: 600)); + await Future.delayed(const Duration(milliseconds: 600)); return Assignment( id: 'assign_1', positionId: positionId, @@ -13,12 +13,12 @@ class EventRepositoryMock { } Future getEvent(String id) async { - await Future.delayed(const Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); return _mockEvent; } Future> getEventShifts(String eventId) async { - await Future.delayed(const Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); return [ const EventShift( id: 'shift_1', @@ -30,7 +30,7 @@ class EventRepositoryMock { } Future> getStaffAssignments(String staffId) async { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); return [ const Assignment( id: 'assign_1', @@ -42,7 +42,7 @@ class EventRepositoryMock { } Future> getUpcomingEvents() async { - await Future.delayed(const Duration(milliseconds: 800)); + await Future.delayed(const Duration(milliseconds: 800)); return [_mockEvent]; } diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/shifts_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/shifts_repository_mock.dart new file mode 100644 index 00000000..eb0c3e92 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/mocks/shifts_repository_mock.dart @@ -0,0 +1,89 @@ +import 'package:krow_domain/krow_domain.dart'; +import 'package:intl/intl.dart'; + +// Mock Implementation for now. +class ShiftsRepositoryMock { + + Future> 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> 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> 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 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: [], + ); + } +} diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 8028872a..c2183140 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -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'; diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart new file mode 100644 index 00000000..4998c45b --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -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? 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 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 get props => [name, phone, avatar]; +} diff --git a/apps/mobile/packages/features/staff/shifts/analysis_options.yaml b/apps/mobile/packages/features/staff/shifts/analysis_options.yaml new file mode 100644 index 00000000..f41560b9 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Add project specific rules here diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart new file mode 100644 index 00000000..1c54242b --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -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> getMyShifts() async { + return _mock.getMyShifts(); + } + + @override + Future> 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> getPendingAssignments() async { + return _mock.getPendingAssignments(); + } + + @override + Future getShiftDetails(String shiftId) async { + return _mock.getShiftDetails(shiftId); + } + + @override + Future applyForShift(String shiftId) async { + await Future.delayed(const Duration(milliseconds: 500)); + } + + @override + Future acceptShift(String shiftId) async { + await Future.delayed(const Duration(milliseconds: 500)); + } + + @override + Future declineShift(String shiftId) async { + await Future.delayed(const Duration(milliseconds: 500)); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_available_shifts_arguments.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_available_shifts_arguments.dart new file mode 100644 index 00000000..69098abb --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/arguments/get_available_shifts_arguments.dart @@ -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 get props => [query, type]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart new file mode 100644 index 00000000..e0c36133 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart @@ -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> getMyShifts(); + + /// Retrieves available shifts matching the given [query] and [type]. + Future> getAvailableShifts(String query, String type); + + /// Retrieves shifts that are pending acceptance by the user. + Future> getPendingAssignments(); + + /// Retrieves detailed information for a specific shift by [shiftId]. + Future getShiftDetails(String shiftId); + + /// Applies for a specific open shift. + Future applyForShift(String shiftId); + + /// Accepts a pending shift assignment. + Future acceptShift(String shiftId); + + /// Declines a pending shift assignment. + Future declineShift(String shiftId); +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_available_shifts_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_available_shifts_usecase.dart new file mode 100644 index 00000000..54d0269e --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_available_shifts_usecase.dart @@ -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> { + final ShiftsRepositoryInterface repository; + + GetAvailableShiftsUseCase(this.repository); + + @override + Future> call(GetAvailableShiftsArguments arguments) async { + return repository.getAvailableShifts(arguments.query, arguments.type); + } +} + diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart new file mode 100644 index 00000000..5b9f172d --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_my_shifts_usecase.dart @@ -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> { + final ShiftsRepositoryInterface repository; + + GetMyShiftsUseCase(this.repository); + + @override + Future> call() async { + return repository.getMyShifts(); + } +} + diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_pending_assignments_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_pending_assignments_usecase.dart new file mode 100644 index 00000000..e4747c36 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/get_pending_assignments_usecase.dart @@ -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> { + final ShiftsRepositoryInterface repository; + + GetPendingAssignmentsUseCase(this.repository); + + @override + Future> call() async { + return repository.getPendingAssignments(); + } +} + diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart new file mode 100644 index 00000000..9b33b7c4 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart @@ -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 { + final GetMyShiftsUseCase getMyShifts; + final GetAvailableShiftsUseCase getAvailableShifts; + final GetPendingAssignmentsUseCase getPendingAssignments; + + ShiftsBloc({ + required this.getMyShifts, + required this.getAvailableShifts, + required this.getPendingAssignments, + }) : super(ShiftsInitial()) { + on(_onLoadShifts); + on(_onFilterAvailableShifts); + } + + Future _onLoadShifts( + LoadShiftsEvent event, + Emitter 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 _onFilterAvailableShifts( + FilterAvailableShiftsEvent event, + Emitter 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 + } + } + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart new file mode 100644 index 00000000..41e01253 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart @@ -0,0 +1,21 @@ +part of 'shifts_bloc.dart'; + +@immutable +sealed class ShiftsEvent extends Equatable { + const ShiftsEvent(); + + @override + List get props => []; +} + +class LoadShiftsEvent extends ShiftsEvent {} + +class FilterAvailableShiftsEvent extends ShiftsEvent { + final String? query; + final String? jobType; + + const FilterAvailableShiftsEvent({this.query, this.jobType}); + + @override + List get props => [query, jobType]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart new file mode 100644 index 00000000..c6051cea --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_state.dart @@ -0,0 +1,57 @@ +part of 'shifts_bloc.dart'; + +@immutable +sealed class ShiftsState extends Equatable { + const ShiftsState(); + + @override + List get props => []; +} + +class ShiftsInitial extends ShiftsState {} + +class ShiftsLoading extends ShiftsState {} + +class ShiftsLoaded extends ShiftsState { + final List myShifts; + final List pendingShifts; + final List 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? myShifts, + List? pendingShifts, + List? 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 get props => [myShifts, pendingShifts, availableShifts, searchQuery, jobType]; +} + +class ShiftsError extends ShiftsState { + final String message; + + const ShiftsError(this.message); + + @override + List get props => [message]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart new file mode 100644 index 00000000..4832055b --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart @@ -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 +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart new file mode 100644 index 00000000..65d31a36 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -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 createState() => _ShiftDetailsPageState(); +} + +class _ShiftDetailsPageState extends State { + 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, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart new file mode 100644 index 00000000..350c4855 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shifts_page.dart @@ -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 createState() => _ShiftsPageState(); +} + +class _ShiftsPageState extends State { + 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(); + + @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 _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( + builder: (context, state) { + final List myShifts = (state is ShiftsLoaded) ? state.myShifts : []; + final List availableJobs = (state is ShiftsLoaded) ? state.availableShifts : []; + final List pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : []; + final List 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"), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart new file mode 100644 index 00000000..e9e99b35 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -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 createState() => _MyShiftCardState(); +} + +class _MyShiftCardState extends State { + 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), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart new file mode 100644 index 00000000..d3c813ff --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_assignment_card.dart @@ -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), + ), + ), + ], + ), + ], + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart new file mode 100644 index 00000000..95c428fc --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart @@ -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(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)); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart new file mode 100644 index 00000000..28ae0ac4 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart @@ -0,0 +1,4 @@ +library staff_shifts; + +export 'src/staff_shifts_module.dart'; + diff --git a/apps/mobile/packages/features/staff/shifts/pubspec.lock b/apps/mobile/packages/features/staff/shifts/pubspec.lock new file mode 100644 index 00000000..a2cdf2f8 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/pubspec.lock @@ -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" diff --git a/apps/mobile/packages/features/staff/shifts/pubspec.yaml b/apps/mobile/packages/features/staff/shifts/pubspec.yaml new file mode 100644 index 00000000..64467b03 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/pubspec.yaml @@ -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 diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index 33ed2fe5..551b4f69 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -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: >[ - ChildRoute( + ModuleRoute( StaffMainRoutes.shifts, - child: (BuildContext context) => - const PlaceholderPage(title: 'Shifts'), + module: StaffShiftsModule(), ), ChildRoute( StaffMainRoutes.payments, diff --git a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml index 2d6f04a3..8d1d349e 100644 --- a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml +++ b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml @@ -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 diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 18fc5b20..254b32d0 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -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: