diff --git a/apps/mobile-client/.keep b/apps/mobile-client/.keep deleted file mode 100644 index 8b137891..00000000 --- a/apps/mobile-client/.keep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/mobile-staff/.keep b/apps/mobile-staff/.keep deleted file mode 100644 index 8b137891..00000000 --- a/apps/mobile-staff/.keep +++ /dev/null @@ -1 +0,0 @@ - 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 97aed906..81167fa3 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 @@ -245,6 +245,58 @@ "recurring_desc": "Ongoing Weekly / Monthly Coverage", "permanent": "Permanent", "permanent_desc": "Long-Term Staffing Placement" + }, + "rapid": { + "title": "RAPID Order", + "subtitle": "Emergency staffing in minutes", + "urgent_badge": "URGENT", + "tell_us": "Tell us what you need", + "need_staff": "Need staff urgently?", + "type_or_speak": "Type or speak what you need. I'll handle the rest", + "example": "Example: ", + "hint": "Type or speak... (e.g., \"Need 5 cooks ASAP until 5am\")", + "speak": "Speak", + "listening": "Listening...", + "send": "Send Message", + "sending": "Sending...", + "success_title": "Request Sent!", + "success_message": "We're finding available workers for you right now. You'll be notified as they accept.", + "back_to_orders": "Back to Orders" + }, + "one_time": { + "title": "One-Time Order", + "subtitle": "Single event or shift request", + "create_your_order": "Create Your Order", + "date_label": "Date", + "date_hint": "Select date", + "location_label": "Location", + "location_hint": "Enter address", + "positions_title": "Positions", + "add_position": "Add Position", + "position_number": "Position $number", + "remove": "Remove", + "select_role": "Select role", + "start_label": "Start", + "end_label": "End", + "workers_label": "Workers", + "lunch_break_label": "Lunch Break", + "different_location": "Use different location for this position", + "different_location_title": "Different Location", + "different_location_hint": "Enter different address", + "create_order": "Create Order", + "creating": "Creating...", + "success_title": "Order Created!", + "success_message": "Your shift request has been posted. Workers will start applying soon." + }, + "recurring": { + "title": "Recurring Order", + "subtitle": "Ongoing weekly/monthly coverage", + "placeholder": "Recurring Order Flow (Work in Progress)" + }, + "permanent": { + "title": "Permanent Order", + "subtitle": "Long-term staffing placement", + "placeholder": "Permanent Order Flow (Work in Progress)" } } } 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 1ec7d32d..d207dc0b 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 @@ -245,6 +245,58 @@ "recurring_desc": "Cobertura Continua Semanal / Mensual", "permanent": "Permanente", "permanent_desc": "Colocación de Personal a Largo Plazo" + }, + "rapid": { + "title": "Orden RÁPIDA", + "subtitle": "Personal de emergencia en minutos", + "urgent_badge": "URGENTE", + "tell_us": "Dinos qué necesitas", + "need_staff": "¿Necesitas personal urgentemente?", + "type_or_speak": "Escribe o habla lo que necesitas. Yo me encargo del resto", + "example": "Ejemplo: ", + "hint": "Escribe o habla... (ej., \"Necesito 5 cocineros YA hasta las 5am\")", + "speak": "Hablar", + "listening": "Escuchando...", + "send": "Enviar Mensaje", + "sending": "Enviando...", + "success_title": "¡Solicitud Enviada!", + "success_message": "Estamos encontrando trabajadores disponibles para ti ahora mismo. Te notificaremos cuando acepten.", + "back_to_orders": "Volver a Órdenes" + }, + "one_time": { + "title": "Orden Única Vez", + "subtitle": "Evento único o petición de turno", + "create_your_order": "Crea Tu Orden", + "date_label": "Fecha", + "date_hint": "Seleccionar fecha", + "location_label": "Ubicación", + "location_hint": "Ingresar dirección", + "positions_title": "Posiciones", + "add_position": "Añadir Posición", + "position_number": "Posición $number", + "remove": "Eliminar", + "select_role": "Seleccionar rol", + "start_label": "Inicio", + "end_label": "Fin", + "workers_label": "Trabajadores", + "lunch_break_label": "Descanso para Almuerzo", + "different_location": "Usar ubicación diferente para esta posición", + "different_location_title": "Ubicación Diferente", + "different_location_hint": "Ingresar dirección diferente", + "create_order": "Crear Orden", + "creating": "Creando...", + "success_title": "¡Orden Creada!", + "success_message": "Tu solicitud de turno ha sido publicada. Los trabajadores comenzarán a postularse pronto." + }, + "recurring": { + "title": "Orden Recurrente", + "subtitle": "Cobertura continua semanal/mensual", + "placeholder": "Flujo de Orden Recurrente (Trabajo en Progreso)" + }, + "permanent": { + "title": "Orden Permanente", + "subtitle": "Colocación de personal a largo plazo", + "placeholder": "Flujo de Orden Permanente (Trabajo en Progreso)" } } } 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 c7184fdb..445db229 100644 --- a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart +++ b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart @@ -5,6 +5,7 @@ /// /// TODO: These mocks currently do not implement any specific interfaces. /// They will implement interfaces defined in feature packages once those are created. +library; export 'src/mocks/auth_repository_mock.dart'; export 'src/mocks/staff_repository_mock.dart'; @@ -15,6 +16,7 @@ export 'src/mocks/rating_repository_mock.dart'; export 'src/mocks/support_repository_mock.dart'; export 'src/mocks/home_repository_mock.dart'; export 'src/mocks/business_repository_mock.dart'; +export 'src/mocks/order_repository_mock.dart'; export 'src/data_connect_module.dart'; // Export the generated Data Connect SDK diff --git a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart index 553eac80..5d1036f1 100644 --- a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart +++ b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart @@ -2,6 +2,7 @@ import 'package:flutter_modular/flutter_modular.dart'; import 'mocks/auth_repository_mock.dart'; import 'mocks/business_repository_mock.dart'; import 'mocks/home_repository_mock.dart'; +import 'mocks/order_repository_mock.dart'; /// A module that provides Data Connect dependencies, including mocks. class DataConnectModule extends Module { @@ -11,5 +12,6 @@ class DataConnectModule extends Module { i.addLazySingleton(AuthRepositoryMock.new); i.addLazySingleton(HomeRepositoryMock.new); i.addLazySingleton(BusinessRepositoryMock.new); + i.addLazySingleton(OrderRepositoryMock.new); } } diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart index 3895c0b6..6ed624ef 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/business_repository_mock.dart @@ -15,7 +15,7 @@ class BusinessRepositoryMock { Future> getHubs(String businessId) async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ const Hub( id: 'hub_1', businessId: 'biz_1', 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 44159611..0cdd03c2 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 @@ -19,7 +19,7 @@ class EventRepositoryMock { Future> getEventShifts(String eventId) async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ const EventShift( id: 'shift_1', eventId: 'event_1', @@ -31,7 +31,7 @@ class EventRepositoryMock { Future> getStaffAssignments(String staffId) async { await Future.delayed(const Duration(milliseconds: 500)); - return [ + return [ const Assignment( id: 'assign_1', positionId: 'pos_1', @@ -43,10 +43,10 @@ class EventRepositoryMock { Future> getUpcomingEvents() async { await Future.delayed(const Duration(milliseconds: 800)); - return [_mockEvent]; + return [_mockEvent]; } - static final _mockEvent = Event( + static final Event _mockEvent = Event( id: 'event_1', businessId: 'biz_1', hubId: 'hub_1', diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart index 050cf7e5..0711463a 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/financial_repository_mock.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; class FinancialRepositoryMock { Future> getInvoices(String businessId) async { await Future.delayed(const Duration(milliseconds: 500)); - return [ + return [ const Invoice( id: 'inv_1', eventId: 'event_1', @@ -19,7 +19,7 @@ class FinancialRepositoryMock { Future> getStaffPayments(String staffId) async { await Future.delayed(const Duration(milliseconds: 500)); - return [ + return [ StaffPayment( id: 'pay_1', staffId: staffId, diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart new file mode 100644 index 00000000..8e7979ea --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/mocks/order_repository_mock.dart @@ -0,0 +1,44 @@ +import 'package:krow_domain/krow_domain.dart'; + +/// Mock implementation of order-related data operations. +/// +/// This class simulates backend responses for order types and order creation. +/// It is used by the feature-level repository implementations. +class OrderRepositoryMock { + /// Returns a list of available [OrderType]s. + Future> getOrderTypes() async { + await Future.delayed(const Duration(milliseconds: 500)); + return const [ + OrderType( + id: 'rapid', + titleKey: 'client_create_order.types.rapid', + descriptionKey: 'client_create_order.types.rapid_desc', + ), + OrderType( + id: 'one-time', + titleKey: 'client_create_order.types.one_time', + descriptionKey: 'client_create_order.types.one_time_desc', + ), + OrderType( + id: 'recurring', + titleKey: 'client_create_order.types.recurring', + descriptionKey: 'client_create_order.types.recurring_desc', + ), + OrderType( + id: 'permanent', + titleKey: 'client_create_order.types.permanent', + descriptionKey: 'client_create_order.types.permanent_desc', + ), + ]; + } + + /// Simulates creating a one-time order. + Future createOneTimeOrder(OneTimeOrder order) async { + await Future.delayed(const Duration(milliseconds: 800)); + } + + /// Simulates creating a rapid order. + Future createRapidOrder(String description) async { + await Future.delayed(const Duration(seconds: 1)); + } +} diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart index eedb0efb..f679fa11 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/rating_repository_mock.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; class RatingRepositoryMock { Future> getStaffRatings(String staffId) async { await Future.delayed(const Duration(milliseconds: 400)); - return [ + return [ const StaffRating( id: 'rate_1', staffId: 'staff_1', diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart index a808733c..f60187da 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/skill_repository_mock.dart @@ -8,7 +8,7 @@ class SkillRepositoryMock { Future> getAllSkills() async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ const Skill( id: 'skill_1', categoryId: 'cat_1', @@ -26,7 +26,7 @@ class SkillRepositoryMock { Future> getStaffSkills(String staffId) async { await Future.delayed(const Duration(milliseconds: 400)); - return [ + return [ const StaffSkill( id: 'staff_skill_1', staffId: 'staff_1', diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart index b40479ee..c032464f 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/staff_repository_mock.dart @@ -9,7 +9,7 @@ class StaffRepositoryMock { Future> getMemberships(String userId) async { await Future.delayed(const Duration(milliseconds: 300)); - return [ + return [ Membership( id: 'mem_1', userId: userId, diff --git a/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart b/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart index 346eb8d1..0722052e 100644 --- a/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart +++ b/apps/mobile/packages/data_connect/lib/src/mocks/support_repository_mock.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; class SupportRepositoryMock { Future> getTags() async { await Future.delayed(const Duration(milliseconds: 200)); - return [ + return [ const Tag(id: 'tag_1', label: 'Urgent'), const Tag(id: 'tag_2', label: 'VIP Event'), ]; @@ -12,7 +12,7 @@ class SupportRepositoryMock { Future> getWorkingAreas() async { await Future.delayed(const Duration(milliseconds: 200)); - return [ + return [ const WorkingArea( id: 'area_1', name: 'Central London', diff --git a/apps/mobile/packages/design_system/lib/src/ui_icons.dart b/apps/mobile/packages/design_system/lib/src/ui_icons.dart index 99e8b1f9..60b6fb02 100644 --- a/apps/mobile/packages/design_system/lib/src/ui_icons.dart +++ b/apps/mobile/packages/design_system/lib/src/ui_icons.dart @@ -48,6 +48,9 @@ class UiIcons { /// Plus/Add icon static const IconData add = _IconLib.plus; + /// Minus icon + static const IconData minus = _IconLib.minus; + /// Edit icon static const IconData edit = _IconLib.edit2; diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 63e416fc..07c99633 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -4,6 +4,7 @@ /// It is pure Dart and has no dependencies on Flutter or Firebase. /// /// Note: Repository Interfaces are now located in their respective Feature packages. +library; // Users & Membership export 'src/entities/users/user.dart'; @@ -26,6 +27,11 @@ export 'src/entities/events/event_shift_position.dart'; export 'src/entities/events/assignment.dart'; export 'src/entities/events/work_session.dart'; +// Orders & Requests +export 'src/entities/orders/order_type.dart'; +export 'src/entities/orders/one_time_order.dart'; +export 'src/entities/orders/one_time_order_position.dart'; + // Skills & Certs export 'src/entities/skills/skill.dart'; export 'src/entities/skills/skill_category.dart'; diff --git a/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart b/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart index 81ebf648..196c9eb8 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/biz_contract.dart @@ -4,6 +4,15 @@ import 'package:equatable/equatable.dart'; /// /// Can be between a business and the platform, or a business and staff. class BizContract extends Equatable { + + const BizContract({ + required this.id, + required this.businessId, + required this.name, + required this.startDate, + this.endDate, + required this.contentUrl, + }); /// Unique identifier. final String id; @@ -22,15 +31,6 @@ class BizContract extends Equatable { /// URL to the document content (PDF/HTML). final String contentUrl; - const BizContract({ - required this.id, - required this.businessId, - required this.name, - required this.startDate, - this.endDate, - required this.contentUrl, - }); - @override - List get props => [id, businessId, name, startDate, endDate, contentUrl]; + List get props => [id, businessId, name, startDate, endDate, contentUrl]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/business/business.dart b/apps/mobile/packages/domain/lib/src/entities/business/business.dart index a719d748..c03e75c9 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/business.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/business.dart @@ -19,6 +19,14 @@ enum BusinessStatus { /// /// This is the top-level organizational entity in the system. class Business extends Equatable { + + const Business({ + required this.id, + required this.name, + required this.registrationNumber, + required this.status, + this.avatar, + }); /// Unique identifier for the business. final String id; @@ -34,14 +42,6 @@ class Business extends Equatable { /// URL to the business logo. final String? avatar; - const Business({ - required this.id, - required this.name, - required this.registrationNumber, - required this.status, - this.avatar, - }); - @override - List get props => [id, name, registrationNumber, status, avatar]; + List get props => [id, name, registrationNumber, status, avatar]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart b/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart index b9f62bd0..328cb39c 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/business_setting.dart @@ -2,6 +2,15 @@ import 'package:equatable/equatable.dart'; /// Represents payroll and operational configuration for a [Business]. class BusinessSetting extends Equatable { + + const BusinessSetting({ + required this.id, + required this.businessId, + required this.prefix, + required this.overtimeEnabled, + this.clockInRequirement, + this.clockOutRequirement, + }); /// Unique identifier for the settings record. final String id; @@ -20,17 +29,8 @@ class BusinessSetting extends Equatable { /// Requirement method for clocking out. final String? clockOutRequirement; - const BusinessSetting({ - required this.id, - required this.businessId, - required this.prefix, - required this.overtimeEnabled, - this.clockInRequirement, - this.clockOutRequirement, - }); - @override - List get props => [ + List get props => [ id, businessId, prefix, diff --git a/apps/mobile/packages/domain/lib/src/entities/business/hub.dart b/apps/mobile/packages/domain/lib/src/entities/business/hub.dart index 400d3bfe..4070a28a 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/hub.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/hub.dart @@ -14,6 +14,15 @@ enum HubStatus { /// Represents a branch location or operational unit within a [Business]. class Hub extends Equatable { + + const Hub({ + required this.id, + required this.businessId, + required this.name, + required this.address, + this.nfcTagId, + required this.status, + }); /// Unique identifier. final String id; @@ -32,15 +41,6 @@ class Hub extends Equatable { /// Operational status. final HubStatus status; - const Hub({ - required this.id, - required this.businessId, - required this.name, - required this.address, - this.nfcTagId, - required this.status, - }); - @override - List get props => [id, businessId, name, address, nfcTagId, status]; + List get props => [id, businessId, name, address, nfcTagId, status]; } diff --git a/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart b/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart index 0e8f523e..3c6891bc 100644 --- a/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart +++ b/apps/mobile/packages/domain/lib/src/entities/business/hub_department.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Used for more granular organization of staff and events (e.g. "Kitchen", "Service"). class HubDepartment extends Equatable { + + const HubDepartment({ + required this.id, + required this.hubId, + required this.name, + }); /// Unique identifier. final String id; @@ -13,12 +19,6 @@ class HubDepartment extends Equatable { /// Name of the department. final String name; - const HubDepartment({ - required this.id, - required this.hubId, - required this.name, - }); - @override - List get props => [id, hubId, name]; + List get props => [id, hubId, name]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart b/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart index 26795977..197281a5 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/assignment.dart @@ -26,6 +26,15 @@ enum AssignmentStatus { /// Represents the link between a [Staff] member and an [EventShiftPosition]. class Assignment extends Equatable { + + const Assignment({ + required this.id, + required this.positionId, + required this.staffId, + required this.status, + this.clockIn, + this.clockOut, + }); /// Unique identifier. final String id; @@ -44,15 +53,6 @@ class Assignment extends Equatable { /// Actual timestamp when staff clocked out. final DateTime? clockOut; - const Assignment({ - required this.id, - required this.positionId, - required this.staffId, - required this.status, - this.clockIn, - this.clockOut, - }); - @override - List get props => [id, positionId, staffId, status, clockIn, clockOut]; + List get props => [id, positionId, staffId, status, clockIn, clockOut]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event.dart b/apps/mobile/packages/domain/lib/src/entities/events/event.dart index 717fb24a..d7def36f 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/event.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/event.dart @@ -34,6 +34,16 @@ enum EventStatus { /// /// This is the central entity for scheduling work. An Event contains [EventShift]s. class Event extends Equatable { + + const Event({ + required this.id, + required this.businessId, + required this.hubId, + required this.name, + required this.date, + required this.status, + required this.contractType, + }); /// Unique identifier. final String id; @@ -55,16 +65,6 @@ class Event extends Equatable { /// Type of employment contract (e.g., 'freelance', 'permanent'). final String contractType; - const Event({ - required this.id, - required this.businessId, - required this.hubId, - required this.name, - required this.date, - required this.status, - required this.contractType, - }); - @override - List get props => [id, businessId, hubId, name, date, status, contractType]; + List get props => [id, businessId, hubId, name, date, status, contractType]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart b/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart index d5218e19..32a025e3 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/event_shift.dart @@ -4,6 +4,13 @@ import 'package:equatable/equatable.dart'; /// /// An Event can have multiple shifts (e.g. "Morning Shift", "Evening Shift"). class EventShift extends Equatable { + + const EventShift({ + required this.id, + required this.eventId, + required this.name, + required this.address, + }); /// Unique identifier. final String id; @@ -16,13 +23,6 @@ class EventShift extends Equatable { /// Specific address for this shift (if different from Hub). final String address; - const EventShift({ - required this.id, - required this.eventId, - required this.name, - required this.address, - }); - @override - List get props => [id, eventId, name, address]; + List get props => [id, eventId, name, address]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart b/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart index abceb7b9..8eb226f0 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/event_shift_position.dart @@ -4,6 +4,17 @@ import 'package:equatable/equatable.dart'; /// /// Defines the requirement for a specific [Skill], the quantity needed, and the pay. class EventShiftPosition extends Equatable { + + const EventShiftPosition({ + required this.id, + required this.shiftId, + required this.skillId, + required this.count, + required this.rate, + required this.startTime, + required this.endTime, + required this.breakDurationMinutes, + }); /// Unique identifier. final String id; @@ -28,19 +39,8 @@ class EventShiftPosition extends Equatable { /// Deducted break duration in minutes. final int breakDurationMinutes; - const EventShiftPosition({ - required this.id, - required this.shiftId, - required this.skillId, - required this.count, - required this.rate, - required this.startTime, - required this.endTime, - required this.breakDurationMinutes, - }); - @override - List get props => [ + List get props => [ id, shiftId, skillId, diff --git a/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart b/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart index 319606bd..ef06a323 100644 --- a/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart +++ b/apps/mobile/packages/domain/lib/src/entities/events/work_session.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Derived from [Assignment] clock-in/out times, used for payroll. class WorkSession extends Equatable { + + const WorkSession({ + required this.id, + required this.assignmentId, + required this.startTime, + this.endTime, + required this.breakDurationMinutes, + }); /// Unique identifier. final String id; @@ -19,14 +27,6 @@ class WorkSession extends Equatable { /// Verified break duration. final int breakDurationMinutes; - const WorkSession({ - required this.id, - required this.assignmentId, - required this.startTime, - this.endTime, - required this.breakDurationMinutes, - }); - @override - List get props => [id, assignmentId, startTime, endTime, breakDurationMinutes]; + List get props => [id, assignmentId, startTime, endTime, breakDurationMinutes]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart index 7775d775..2dc06f9c 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/invoice.dart @@ -26,6 +26,16 @@ enum InvoiceStatus { /// Represents a bill sent to a [Business] for services rendered. class Invoice extends Equatable { + + const Invoice({ + required this.id, + required this.eventId, + required this.businessId, + required this.status, + required this.totalAmount, + required this.workAmount, + required this.addonsAmount, + }); /// Unique identifier. final String id; @@ -47,18 +57,8 @@ class Invoice extends Equatable { /// Total amount for addons/extras. final double addonsAmount; - const Invoice({ - required this.id, - required this.eventId, - required this.businessId, - required this.status, - required this.totalAmount, - required this.workAmount, - required this.addonsAmount, - }); - @override - List get props => [ + List get props => [ id, eventId, businessId, diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart index 17d7afc4..1d0a8035 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_decline.dart @@ -2,6 +2,13 @@ import 'package:equatable/equatable.dart'; /// Represents a reason or log for a declined [Invoice]. class InvoiceDecline extends Equatable { + + const InvoiceDecline({ + required this.id, + required this.invoiceId, + required this.reason, + required this.declinedAt, + }); /// Unique identifier. final String id; @@ -14,13 +21,6 @@ class InvoiceDecline extends Equatable { /// When the decline happened. final DateTime declinedAt; - const InvoiceDecline({ - required this.id, - required this.invoiceId, - required this.reason, - required this.declinedAt, - }); - @override - List get props => [id, invoiceId, reason, declinedAt]; + List get props => [id, invoiceId, reason, declinedAt]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart index b290d7b1..e661334a 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/invoice_item.dart @@ -4,6 +4,15 @@ import 'package:equatable/equatable.dart'; /// /// Corresponds to the work done by one [Staff] member. class InvoiceItem extends Equatable { + + const InvoiceItem({ + required this.id, + required this.invoiceId, + required this.staffId, + required this.workHours, + required this.rate, + required this.amount, + }); /// Unique identifier. final String id; @@ -22,15 +31,6 @@ class InvoiceItem extends Equatable { /// Total line item amount (workHours * rate). final double amount; - const InvoiceItem({ - required this.id, - required this.invoiceId, - required this.staffId, - required this.workHours, - required this.rate, - required this.amount, - }); - @override - List get props => [id, invoiceId, staffId, workHours, rate, amount]; + List get props => [id, invoiceId, staffId, workHours, rate, amount]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart index ed8cd75c..bd890a77 100644 --- a/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart +++ b/apps/mobile/packages/domain/lib/src/entities/financial/staff_payment.dart @@ -17,6 +17,15 @@ enum PaymentStatus { /// Represents a payout to a [Staff] member for a completed [Assignment]. class StaffPayment extends Equatable { + + const StaffPayment({ + required this.id, + required this.staffId, + required this.assignmentId, + required this.amount, + required this.status, + this.paidAt, + }); /// Unique identifier. final String id; @@ -35,15 +44,6 @@ class StaffPayment extends Equatable { /// When the payment was successfully processed. final DateTime? paidAt; - const StaffPayment({ - required this.id, - required this.staffId, - required this.assignmentId, - required this.amount, - required this.status, - this.paidAt, - }); - @override - List get props => [id, staffId, assignmentId, amount, status, paidAt]; + List get props => [id, staffId, assignmentId, amount, status, paidAt]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart b/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart index 124f7d65..681e7a22 100644 --- a/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart +++ b/apps/mobile/packages/domain/lib/src/entities/home/home_dashboard_data.dart @@ -5,6 +5,16 @@ import 'package:equatable/equatable.dart'; /// This entity provides aggregated metrics such as spending and shift counts /// for both the current week and the upcoming 7 days. class HomeDashboardData extends Equatable { + + /// Creates a [HomeDashboardData] instance. + const HomeDashboardData({ + required this.weeklySpending, + required this.next7DaysSpending, + required this.weeklyShifts, + required this.next7DaysScheduled, + required this.totalNeeded, + required this.totalFilled, + }); /// Total spending for the current week. final double weeklySpending; @@ -23,18 +33,8 @@ class HomeDashboardData extends Equatable { /// Total workers filled for today's shifts. final int totalFilled; - /// Creates a [HomeDashboardData] instance. - const HomeDashboardData({ - required this.weeklySpending, - required this.next7DaysSpending, - required this.weeklyShifts, - required this.next7DaysScheduled, - required this.totalNeeded, - required this.totalFilled, - }); - @override - List get props => [ + List get props => [ weeklySpending, next7DaysSpending, weeklyShifts, diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart new file mode 100644 index 00000000..7fb15c9a --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; +import 'one_time_order_position.dart'; + +/// Represents a customer's request for a single event or shift. +/// +/// Encapsulates the date, primary location, and a list of specific [OneTimeOrderPosition] requirements. +class OneTimeOrder extends Equatable { + + const OneTimeOrder({ + required this.date, + required this.location, + required this.positions, + }); + /// The specific date for the shift or event. + final DateTime date; + + /// The primary location where the work will take place. + final String location; + + /// The list of positions and headcounts required for this order. + final List positions; + + @override + List get props => [date, location, positions]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart new file mode 100644 index 00000000..b8a09b7e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/one_time_order_position.dart @@ -0,0 +1,62 @@ +import 'package:equatable/equatable.dart'; + +/// Represents a specific position requirement within a [OneTimeOrder]. +/// +/// Defines the role, headcount, and scheduling details for a single staffing requirement. +class OneTimeOrderPosition extends Equatable { + + const OneTimeOrderPosition({ + required this.role, + required this.count, + required this.startTime, + required this.endTime, + this.lunchBreak = 30, + this.location, + }); + /// The job role or title required. + final String role; + + /// The number of workers required for this position. + final int count; + + /// The scheduled start time (e.g., "09:00 AM"). + final String startTime; + + /// The scheduled end time (e.g., "05:00 PM"). + final String endTime; + + /// The duration of the lunch break in minutes. Defaults to 30. + final int lunchBreak; + + /// Optional specific location for this position, if different from the order's main location. + final String? location; + + @override + List get props => [ + role, + count, + startTime, + endTime, + lunchBreak, + location, + ]; + + /// Creates a copy of this position with the given fields replaced. + OneTimeOrderPosition copyWith({ + String? role, + int? count, + String? startTime, + String? endTime, + int? lunchBreak, + String? location, + }) { + return OneTimeOrderPosition( + role: role ?? this.role, + count: count ?? this.count, + startTime: startTime ?? this.startTime, + endTime: endTime ?? this.endTime, + lunchBreak: lunchBreak ?? this.lunchBreak, + location: location ?? this.location, + ); + } +} diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart b/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart new file mode 100644 index 00000000..e1448be7 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/order_type.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +/// Represents a type of order that can be created (e.g., Rapid, One-Time). +/// +/// This entity defines the identity and display metadata (keys) for the order type. +/// UI-specific properties like colors and icons are handled by the presentation layer. +class OrderType extends Equatable { + + const OrderType({ + required this.id, + required this.titleKey, + required this.descriptionKey, + }); + /// Unique identifier for the order type. + final String id; + + /// Translation key for the title. + final String titleKey; + + /// Translation key for the description. + final String descriptionKey; + + @override + List get props => [id, titleKey, descriptionKey]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart b/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart index 22169d82..263b5550 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/accessibility.dart @@ -4,17 +4,17 @@ import 'package:equatable/equatable.dart'; /// /// Can apply to Staff (needs) or Events (provision). class Accessibility extends Equatable { + + const Accessibility({ + required this.id, + required this.name, + }); /// Unique identifier. final String id; /// Description (e.g. "Wheelchair Access"). final String name; - const Accessibility({ - required this.id, - required this.name, - }); - @override - List get props => [id, name]; + List get props => [id, name]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart index 04b74224..91ff1f5b 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart @@ -2,6 +2,15 @@ import 'package:equatable/equatable.dart'; /// Represents bank account details for payroll. class BankAccount extends Equatable { + + const BankAccount({ + required this.id, + required this.userId, + required this.bankName, + required this.accountNumber, + required this.accountName, + this.sortCode, + }); /// Unique identifier. final String id; @@ -20,15 +29,6 @@ class BankAccount extends Equatable { /// Sort code (if applicable). final String? sortCode; - const BankAccount({ - required this.id, - required this.userId, - required this.bankName, - required this.accountNumber, - required this.accountName, - this.sortCode, - }); - @override - List get props => [id, userId, bankName, accountNumber, accountName, sortCode]; + List get props => [id, userId, bankName, accountNumber, accountName, sortCode]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart b/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart index 99ffe704..d9e8fcd2 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/emergency_contact.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Critical for staff safety during shifts. class EmergencyContact extends Equatable { + + const EmergencyContact({ + required this.name, + required this.relationship, + required this.phone, + }); /// Full name of the contact. final String name; @@ -13,12 +19,6 @@ class EmergencyContact extends Equatable { /// Phone number. final String phone; - const EmergencyContact({ - required this.name, - required this.relationship, - required this.phone, - }); - @override - List get props => [name, relationship, phone]; + List get props => [name, relationship, phone]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart b/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart index 40276a20..5aeb8131 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/schedule.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Defines recurring availability (e.g., "Mondays 9-5"). class Schedule extends Equatable { + + const Schedule({ + required this.id, + required this.staffId, + required this.dayOfWeek, + required this.startTime, + required this.endTime, + }); /// Unique identifier. final String id; @@ -19,14 +27,6 @@ class Schedule extends Equatable { /// End time of availability. final DateTime endTime; - const Schedule({ - required this.id, - required this.staffId, - required this.dayOfWeek, - required this.startTime, - required this.endTime, - }); - @override - List get props => [id, staffId, dayOfWeek, startTime, endTime]; + List get props => [id, staffId, dayOfWeek, startTime, endTime]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart index 1c4ea3af..1f56eecb 100644 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart +++ b/apps/mobile/packages/domain/lib/src/entities/ratings/business_staff_preference.dart @@ -11,6 +11,13 @@ enum PreferenceType { /// Represents a business's specific preference for a staff member. class BusinessStaffPreference extends Equatable { + + const BusinessStaffPreference({ + required this.id, + required this.businessId, + required this.staffId, + required this.type, + }); /// Unique identifier. final String id; @@ -23,13 +30,6 @@ class BusinessStaffPreference extends Equatable { /// Whether they are a favorite or blocked. final PreferenceType type; - const BusinessStaffPreference({ - required this.id, - required this.businessId, - required this.staffId, - required this.type, - }); - @override - List get props => [id, businessId, staffId, type]; + List get props => [id, businessId, staffId, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart index 317b6dd6..d42e46f0 100644 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart +++ b/apps/mobile/packages/domain/lib/src/entities/ratings/penalty_log.dart @@ -4,6 +4,15 @@ import 'package:equatable/equatable.dart'; /// /// Penalties are issued for no-shows, cancellations, or poor conduct. class PenaltyLog extends Equatable { + + const PenaltyLog({ + required this.id, + required this.staffId, + required this.assignmentId, + required this.reason, + required this.points, + required this.issuedAt, + }); /// Unique identifier. final String id; @@ -22,15 +31,6 @@ class PenaltyLog extends Equatable { /// When the penalty was issued. final DateTime issuedAt; - const PenaltyLog({ - required this.id, - required this.staffId, - required this.assignmentId, - required this.reason, - required this.points, - required this.issuedAt, - }); - @override - List get props => [id, staffId, assignmentId, reason, points, issuedAt]; + List get props => [id, staffId, assignmentId, reason, points, issuedAt]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart b/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart index 635dcc0b..b51a44ae 100644 --- a/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart +++ b/apps/mobile/packages/domain/lib/src/entities/ratings/staff_rating.dart @@ -2,6 +2,15 @@ import 'package:equatable/equatable.dart'; /// Represents a rating given to a staff member by a client. class StaffRating extends Equatable { + + const StaffRating({ + required this.id, + required this.staffId, + required this.eventId, + required this.businessId, + required this.rating, + this.comment, + }); /// Unique identifier. final String id; @@ -20,15 +29,6 @@ class StaffRating extends Equatable { /// Optional feedback text. final String? comment; - const StaffRating({ - required this.id, - required this.staffId, - required this.eventId, - required this.businessId, - required this.rating, - this.comment, - }); - @override - List get props => [id, staffId, eventId, businessId, rating, comment]; + List get props => [id, staffId, eventId, businessId, rating, comment]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart b/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart index 362832c0..fd6065f8 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/certificate.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Examples: "Food Hygiene Level 2", "SIA Badge". class Certificate extends Equatable { + + const Certificate({ + required this.id, + required this.name, + required this.isRequired, + }); /// Unique identifier. final String id; @@ -13,12 +19,6 @@ class Certificate extends Equatable { /// Whether this certificate is mandatory for platform access or specific roles. final bool isRequired; - const Certificate({ - required this.id, - required this.name, - required this.isRequired, - }); - @override - List get props => [id, name, isRequired]; + List get props => [id, name, isRequired]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart index a5d11320..f61b68e7 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/skill.dart @@ -5,6 +5,13 @@ import 'package:equatable/equatable.dart'; /// Examples: "Waiter", "Security Guard", "Bartender". /// Linked to a [SkillCategory]. class Skill extends Equatable { + + const Skill({ + required this.id, + required this.categoryId, + required this.name, + required this.basePrice, + }); /// Unique identifier. final String id; @@ -17,13 +24,6 @@ class Skill extends Equatable { /// Default hourly rate suggested for this skill. final double basePrice; - const Skill({ - required this.id, - required this.categoryId, - required this.name, - required this.basePrice, - }); - @override - List get props => [id, categoryId, name, basePrice]; + List get props => [id, categoryId, name, basePrice]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart index 063dedb8..091fce05 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/skill_category.dart @@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart'; /// Represents a broad category of skills (e.g. "Hospitality", "Logistics"). class SkillCategory extends Equatable { + + const SkillCategory({ + required this.id, + required this.name, + }); /// Unique identifier. final String id; /// Display name. final String name; - const SkillCategory({ - required this.id, - required this.name, - }); - @override - List get props => [id, name]; + List get props => [id, name]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart b/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart index a92b8bd2..eca88467 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/skill_kit.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Examples: "Black Shirt" (Uniform), "Safety Boots" (Equipment). class SkillKit extends Equatable { + + const SkillKit({ + required this.id, + required this.skillId, + required this.name, + required this.isRequired, + required this.type, + }); /// Unique identifier. final String id; @@ -19,14 +27,6 @@ class SkillKit extends Equatable { /// Type of kit ('uniform' or 'equipment'). final String type; - const SkillKit({ - required this.id, - required this.skillId, - required this.name, - required this.isRequired, - required this.type, - }); - @override - List get props => [id, skillId, name, isRequired, type]; + List get props => [id, skillId, name, isRequired, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart b/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart index da54471f..b868c9d7 100644 --- a/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart +++ b/apps/mobile/packages/domain/lib/src/entities/skills/staff_skill.dart @@ -26,6 +26,15 @@ enum StaffSkillStatus { /// Represents a staff member's qualification in a specific [Skill]. class StaffSkill extends Equatable { + + const StaffSkill({ + required this.id, + required this.staffId, + required this.skillId, + required this.level, + required this.experienceYears, + required this.status, + }); /// Unique identifier. final String id; @@ -44,15 +53,6 @@ class StaffSkill extends Equatable { /// Verification status. final StaffSkillStatus status; - const StaffSkill({ - required this.id, - required this.staffId, - required this.skillId, - required this.level, - required this.experienceYears, - required this.status, - }); - @override - List get props => [id, staffId, skillId, level, experienceYears, status]; + List get props => [id, staffId, skillId, level, experienceYears, status]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/addon.dart b/apps/mobile/packages/domain/lib/src/entities/support/addon.dart index 9a78353f..fd85edba 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/addon.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/addon.dart @@ -2,6 +2,13 @@ import 'package:equatable/equatable.dart'; /// Represents a financial addon/bonus/deduction applied to an Invoice or Payment. class Addon extends Equatable { + + const Addon({ + required this.id, + required this.name, + required this.amount, + required this.type, + }); /// Unique identifier. final String id; @@ -14,13 +21,6 @@ class Addon extends Equatable { /// Type ('credit' or 'debit'). final String type; - const Addon({ - required this.id, - required this.name, - required this.amount, - required this.type, - }); - @override - List get props => [id, name, amount, type]; + List get props => [id, name, amount, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/media.dart b/apps/mobile/packages/domain/lib/src/entities/support/media.dart index 8b298b61..329cdca6 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/media.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/media.dart @@ -4,6 +4,12 @@ import 'package:equatable/equatable.dart'; /// /// Used for avatars, certificates, or event photos. class Media extends Equatable { + + const Media({ + required this.id, + required this.url, + required this.type, + }); /// Unique identifier. final String id; @@ -13,12 +19,6 @@ class Media extends Equatable { /// MIME type or general type (image, pdf). final String type; - const Media({ - required this.id, - required this.url, - required this.type, - }); - @override - List get props => [id, url, type]; + List get props => [id, url, type]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/tag.dart b/apps/mobile/packages/domain/lib/src/entities/support/tag.dart index 62deacaa..44d4db9d 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/tag.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/tag.dart @@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart'; /// Represents a descriptive tag used for categorizing events or staff. class Tag extends Equatable { + + const Tag({ + required this.id, + required this.label, + }); /// Unique identifier. final String id; /// Text label. final String label; - const Tag({ - required this.id, - required this.label, - }); - @override - List get props => [id, label]; + List get props => [id, label]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart b/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart index cc044b4c..aa5d8d56 100644 --- a/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart +++ b/apps/mobile/packages/domain/lib/src/entities/support/working_area.dart @@ -2,6 +2,14 @@ import 'package:equatable/equatable.dart'; /// Represents a geographical area where a [Staff] member is willing to work. class WorkingArea extends Equatable { + + const WorkingArea({ + required this.id, + required this.name, + required this.centerLat, + required this.centerLng, + required this.radiusKm, + }); /// Unique identifier. final String id; @@ -17,14 +25,6 @@ class WorkingArea extends Equatable { /// Radius in Kilometers. final double radiusKm; - const WorkingArea({ - required this.id, - required this.name, - required this.centerLat, - required this.centerLng, - required this.radiusKm, - }); - @override - List get props => [id, name, centerLat, centerLng, radiusKm]; + List get props => [id, name, centerLat, centerLng, radiusKm]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart b/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart index fc7b8099..2f3bcf34 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/biz_member.dart @@ -4,6 +4,13 @@ import 'package:equatable/equatable.dart'; /// /// Grants a user access to business-level operations. class BizMember extends Equatable { + + const BizMember({ + required this.id, + required this.businessId, + required this.userId, + required this.role, + }); /// Unique identifier for this membership. final String id; @@ -16,13 +23,6 @@ class BizMember extends Equatable { /// The role within the business. final String role; - const BizMember({ - required this.id, - required this.businessId, - required this.userId, - required this.role, - }); - @override - List get props => [id, businessId, userId, role]; + List get props => [id, businessId, userId, role]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart b/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart index 0ef66a18..a6bd7a7f 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/hub_member.dart @@ -4,6 +4,13 @@ import 'package:equatable/equatable.dart'; /// /// Grants a user access to specific [Hub] operations, distinct from [BizMember]. class HubMember extends Equatable { + + const HubMember({ + required this.id, + required this.hubId, + required this.userId, + required this.role, + }); /// Unique identifier for this membership. final String id; @@ -16,13 +23,6 @@ class HubMember extends Equatable { /// The role within the hub. final String role; - const HubMember({ - required this.id, - required this.hubId, - required this.userId, - required this.role, - }); - @override - List get props => [id, hubId, userId, role]; + List get props => [id, hubId, userId, role]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/membership.dart b/apps/mobile/packages/domain/lib/src/entities/users/membership.dart index be5a0587..c09ea2ae 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/membership.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/membership.dart @@ -4,6 +4,14 @@ import 'package:equatable/equatable.dart'; /// /// Allows a [User] to be a member of either a [Business] or a [Hub]. class Membership extends Equatable { + + const Membership({ + required this.id, + required this.userId, + required this.memberableId, + required this.memberableType, + required this.role, + }); /// Unique identifier for the membership record. final String id; @@ -19,14 +27,6 @@ class Membership extends Equatable { /// The role within that organization (e.g., 'manager', 'viewer'). final String role; - const Membership({ - required this.id, - required this.userId, - required this.memberableId, - required this.memberableType, - required this.role, - }); - @override - List get props => [id, userId, memberableId, memberableType, role]; + List get props => [id, userId, memberableId, memberableType, role]; } \ No newline at end of file diff --git a/apps/mobile/packages/domain/lib/src/entities/users/staff.dart b/apps/mobile/packages/domain/lib/src/entities/users/staff.dart index 29f417cb..f3bc2bf0 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/staff.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/staff.dart @@ -29,6 +29,18 @@ enum StaffStatus { /// Contains all personal and professional details of a staff member. /// Linked to a [User] via [authProviderId]. class Staff extends Equatable { + + const Staff({ + required this.id, + required this.authProviderId, + required this.name, + required this.email, + this.phone, + required this.status, + this.address, + this.avatar, + this.livePhoto, + }); /// Unique identifier for the staff profile. final String id; @@ -56,20 +68,8 @@ class Staff extends Equatable { /// URL to a verified live photo for identity verification. final String? livePhoto; - const Staff({ - required this.id, - required this.authProviderId, - required this.name, - required this.email, - this.phone, - required this.status, - this.address, - this.avatar, - this.livePhoto, - }); - @override - List get props => [ + List get props => [ id, authProviderId, name, diff --git a/apps/mobile/packages/domain/lib/src/entities/users/user.dart b/apps/mobile/packages/domain/lib/src/entities/users/user.dart index bc1b3e11..fc300f59 100644 --- a/apps/mobile/packages/domain/lib/src/entities/users/user.dart +++ b/apps/mobile/packages/domain/lib/src/entities/users/user.dart @@ -5,6 +5,13 @@ import 'package:equatable/equatable.dart'; /// This entity corresponds to the Firebase Auth user record and acts as the /// linkage between the authentication system and the specific [Staff] or Client profiles. class User extends Equatable { + + const User({ + required this.id, + required this.email, + this.phone, + required this.role, + }); /// The unique identifier from the authentication provider (e.g., Firebase UID). final String id; @@ -18,13 +25,6 @@ class User extends Equatable { /// This determines the initial routing and permissions. final String role; - const User({ - required this.id, - required this.email, - this.phone, - required this.role, - }); - @override - List get props => [id, email, phone, role]; + List get props => [id, email, phone, role]; } \ No newline at end of file diff --git a/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart b/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart index 826ffc4b..dc353045 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/create_order_module.dart @@ -1,16 +1,46 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'data/repositories_impl/client_create_order_repository_impl.dart'; +import 'domain/repositories/client_create_order_repository_interface.dart'; +import 'domain/usecases/create_one_time_order_usecase.dart'; +import 'domain/usecases/create_rapid_order_usecase.dart'; +import 'domain/usecases/get_order_types_usecase.dart'; import 'presentation/blocs/client_create_order_bloc.dart'; +import 'presentation/blocs/one_time_order_bloc.dart'; +import 'presentation/blocs/rapid_order_bloc.dart'; import 'presentation/pages/create_order_page.dart'; import 'presentation/pages/one_time_order_page.dart'; import 'presentation/pages/permanent_order_page.dart'; import 'presentation/pages/rapid_order_page.dart'; import 'presentation/pages/recurring_order_page.dart'; +/// Module for the Client Create Order feature. +/// +/// This module orchestrates the dependency injection for the create order feature, +/// connecting the domain use cases with their data layer implementations and +/// presentation layer BLoCs. class ClientCreateOrderModule extends Module { + @override + List get imports => [DataConnectModule()]; + @override void binds(Injector i) { + // Repositories + i.addLazySingleton( + () => ClientCreateOrderRepositoryImpl( + orderMock: i.get()), + ); + + // UseCases + i.addLazySingleton(GetOrderTypesUseCase.new); + i.addLazySingleton(CreateOneTimeOrderUseCase.new); + i.addLazySingleton(CreateRapidOrderUseCase.new); + + // BLoCs i.addSingleton(ClientCreateOrderBloc.new); + i.add(RapidOrderBloc.new); + i.add(OneTimeOrderBloc.new); } @override diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart new file mode 100644 index 00000000..e0f7d843 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -0,0 +1,33 @@ +import 'package:krow_data_connect/krow_data_connect.dart' hide OrderType; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/repositories/client_create_order_repository_interface.dart'; + +/// Implementation of [ClientCreateOrderRepositoryInterface]. +/// +/// This implementation delegates all data access to the Data Connect layer, +/// specifically using [OrderRepositoryMock] for now as per the platform's mocking strategy. +class ClientCreateOrderRepositoryImpl + implements ClientCreateOrderRepositoryInterface { + + /// Creates a [ClientCreateOrderRepositoryImpl]. + /// + /// Requires an [OrderRepositoryMock] from the Data Connect shared package. + ClientCreateOrderRepositoryImpl({required OrderRepositoryMock orderMock}) + : _orderMock = orderMock; + final OrderRepositoryMock _orderMock; + + @override + Future> getOrderTypes() { + return _orderMock.getOrderTypes(); + } + + @override + Future createOneTimeOrder(OneTimeOrder order) { + return _orderMock.createOneTimeOrder(order); + } + + @override + Future createRapidOrder(String description) { + return _orderMock.createRapidOrder(description); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart new file mode 100644 index 00000000..08db06db --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/one_time_order_arguments.dart @@ -0,0 +1,13 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// Represents the arguments required for the [CreateOneTimeOrderUseCase]. +class OneTimeOrderArguments extends UseCaseArgument { + + const OneTimeOrderArguments({required this.order}); + /// The order details to be created. + final OneTimeOrder order; + + @override + List get props => [order]; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart new file mode 100644 index 00000000..58212905 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/arguments/rapid_order_arguments.dart @@ -0,0 +1,12 @@ +import 'package:krow_core/core.dart'; + +/// Represents the arguments required for the [CreateRapidOrderUseCase]. +class RapidOrderArguments extends UseCaseArgument { + + const RapidOrderArguments({required this.description}); + /// The text description of the urgent staffing need. + final String description; + + @override + List get props => [description]; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart new file mode 100644 index 00000000..895fdd64 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart @@ -0,0 +1,16 @@ +import 'package:krow_domain/krow_domain.dart'; + +/// Interface for the Client Create Order repository. +/// +/// This repository handles the retrieval of available order types and the +/// submission of different types of staffing orders (Rapid, One-Time, etc.). +abstract interface class ClientCreateOrderRepositoryInterface { + /// Retrieves the list of available order types. + Future> getOrderTypes(); + + /// Submits a one-time staffing order. + Future createOneTimeOrder(OneTimeOrder order); + + /// Submits a rapid (urgent) staffing order with a text description. + Future createRapidOrder(String description); +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart new file mode 100644 index 00000000..23c92224 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_one_time_order_usecase.dart @@ -0,0 +1,20 @@ +import 'package:krow_core/core.dart'; +import '../arguments/one_time_order_arguments.dart'; +import '../repositories/client_create_order_repository_interface.dart'; + +/// Use case for creating a one-time staffing order. +/// +/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit +/// a [OneTimeOrder] provided via [OneTimeOrderArguments]. +class CreateOneTimeOrderUseCase + implements UseCase { + + /// Creates a [CreateOneTimeOrderUseCase]. + const CreateOneTimeOrderUseCase(this._repository); + final ClientCreateOrderRepositoryInterface _repository; + + @override + Future call(OneTimeOrderArguments input) { + return _repository.createOneTimeOrder(input.order); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart new file mode 100644 index 00000000..3d2d1f0c --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/create_rapid_order_usecase.dart @@ -0,0 +1,19 @@ +import 'package:krow_core/core.dart'; +import '../arguments/rapid_order_arguments.dart'; +import '../repositories/client_create_order_repository_interface.dart'; + +/// Use case for creating a rapid (urgent) staffing order. +/// +/// This use case uses the [ClientCreateOrderRepositoryInterface] to submit +/// a text-based urgent request via [RapidOrderArguments]. +class CreateRapidOrderUseCase implements UseCase { + + /// Creates a [CreateRapidOrderUseCase]. + const CreateRapidOrderUseCase(this._repository); + final ClientCreateOrderRepositoryInterface _repository; + + @override + Future call(RapidOrderArguments input) { + return _repository.createRapidOrder(input.description); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart new file mode 100644 index 00000000..9473369f --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/domain/usecases/get_order_types_usecase.dart @@ -0,0 +1,19 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/client_create_order_repository_interface.dart'; + +/// Use case for retrieving the available order types for a client. +/// +/// This use case interacts with the [ClientCreateOrderRepositoryInterface] to +/// fetch the list of staffing order types (e.g., Rapid, One-Time). +class GetOrderTypesUseCase implements NoInputUseCase> { + + /// Creates a [GetOrderTypesUseCase]. + const GetOrderTypesUseCase(this._repository); + final ClientCreateOrderRepositoryInterface _repository; + + @override + Future> call() { + return _repository.getOrderTypes(); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart index 60f08bab..794cdfd3 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_bloc.dart @@ -1,69 +1,24 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:design_system/design_system.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/usecases/get_order_types_usecase.dart'; import 'client_create_order_event.dart'; import 'client_create_order_state.dart'; +/// BLoC for managing the list of available order types. class ClientCreateOrderBloc extends Bloc { - ClientCreateOrderBloc() : super(const ClientCreateOrderInitial()) { + + ClientCreateOrderBloc(this._getOrderTypesUseCase) + : super(const ClientCreateOrderInitial()) { on(_onTypesRequested); } + final GetOrderTypesUseCase _getOrderTypesUseCase; - void _onTypesRequested( + Future _onTypesRequested( ClientCreateOrderTypesRequested event, Emitter emit, - ) { - // In a real app, this might come from a repository or config - final List types = [ - const CreateOrderType( - id: 'rapid', - icon: UiIcons.zap, - titleKey: 'client_create_order.types.rapid', - descriptionKey: 'client_create_order.types.rapid_desc', - backgroundColor: UiColors.tagError, // Red-ish background - borderColor: UiColors.destructive, // Red border - iconBackgroundColor: UiColors.tagError, - iconColor: UiColors.destructive, - textColor: UiColors.destructive, - descriptionColor: UiColors.textError, - ), - const CreateOrderType( - id: 'one-time', - icon: UiIcons.calendar, - titleKey: 'client_create_order.types.one_time', - descriptionKey: 'client_create_order.types.one_time_desc', - backgroundColor: UiColors.tagInProgress, // Blue-ish - borderColor: UiColors.primary, - iconBackgroundColor: UiColors.tagInProgress, - iconColor: UiColors.primary, - textColor: UiColors.primary, - descriptionColor: UiColors.primary, - ), - const CreateOrderType( - id: 'recurring', - icon: UiIcons.rotateCcw, - titleKey: 'client_create_order.types.recurring', - descriptionKey: 'client_create_order.types.recurring_desc', - backgroundColor: UiColors.tagRefunded, // Indigo-ish (Purple sub) - borderColor: UiColors.primary, // No purple, use primary or mix - iconBackgroundColor: UiColors.tagRefunded, - iconColor: UiColors.primary, - textColor: UiColors.primary, - descriptionColor: UiColors.textSecondary, - ), - const CreateOrderType( - id: 'permanent', - icon: UiIcons.briefcase, - titleKey: 'client_create_order.types.permanent', - descriptionKey: 'client_create_order.types.permanent_desc', - backgroundColor: UiColors.tagSuccess, // Green - borderColor: UiColors.textSuccess, - iconBackgroundColor: UiColors.tagSuccess, - iconColor: UiColors.textSuccess, - textColor: UiColors.textSuccess, - descriptionColor: UiColors.textSuccess, - ), - ]; + ) async { + final List types = await _getOrderTypesUseCase(); emit(ClientCreateOrderLoadSuccess(types)); } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart index 33ed40eb..6b16d110 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_event.dart @@ -4,7 +4,7 @@ abstract class ClientCreateOrderEvent extends Equatable { const ClientCreateOrderEvent(); @override - List get props => []; + List get props => []; } class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent { @@ -12,10 +12,10 @@ class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent { } class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent { - final String typeId; const ClientCreateOrderTypeSelected(this.typeId); + final String typeId; @override - List get props => [typeId]; + List get props => [typeId]; } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart index d977c4c3..a58f89cd 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/client_create_order_state.dart @@ -1,63 +1,26 @@ import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; - -/// Represents an available order type. -class CreateOrderType extends Equatable { - final String id; - final IconData icon; - final String titleKey; // Key for translation - final String descriptionKey; // Key for translation - final Color backgroundColor; - final Color borderColor; - final Color iconBackgroundColor; - final Color iconColor; - final Color textColor; - final Color descriptionColor; - - const CreateOrderType({ - required this.id, - required this.icon, - required this.titleKey, - required this.descriptionKey, - required this.backgroundColor, - required this.borderColor, - required this.iconBackgroundColor, - required this.iconColor, - required this.textColor, - required this.descriptionColor, - }); - - @override - List get props => [ - id, - icon, - titleKey, - descriptionKey, - backgroundColor, - borderColor, - iconBackgroundColor, - iconColor, - textColor, - descriptionColor, - ]; -} +import 'package:krow_domain/krow_domain.dart'; +/// Base state for the [ClientCreateOrderBloc]. abstract class ClientCreateOrderState extends Equatable { const ClientCreateOrderState(); @override - List get props => []; + List get props => []; } +/// Initial state when order types haven't been loaded yet. class ClientCreateOrderInitial extends ClientCreateOrderState { const ClientCreateOrderInitial(); } +/// State representing successfully loaded order types from the repository. class ClientCreateOrderLoadSuccess extends ClientCreateOrderState { - final List orderTypes; const ClientCreateOrderLoadSuccess(this.orderTypes); + /// The list of available order types retrieved from the domain. + final List orderTypes; @override - List get props => [orderTypes]; + List get props => [orderTypes]; } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart new file mode 100644 index 00000000..8d603b10 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_bloc.dart @@ -0,0 +1,93 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/arguments/one_time_order_arguments.dart'; +import '../../domain/usecases/create_one_time_order_usecase.dart'; +import 'one_time_order_event.dart'; +import 'one_time_order_state.dart'; + +/// BLoC for managing the multi-step one-time order creation form. +class OneTimeOrderBloc extends Bloc { + + OneTimeOrderBloc(this._createOneTimeOrderUseCase) + : super(OneTimeOrderState.initial()) { + on(_onDateChanged); + on(_onLocationChanged); + on(_onPositionAdded); + on(_onPositionRemoved); + on(_onPositionUpdated); + on(_onSubmitted); + } + final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; + + void _onDateChanged( + OneTimeOrderDateChanged event, + Emitter emit, + ) { + emit(state.copyWith(date: event.date)); + } + + void _onLocationChanged( + OneTimeOrderLocationChanged event, + Emitter emit, + ) { + emit(state.copyWith(location: event.location)); + } + + void _onPositionAdded( + OneTimeOrderPositionAdded event, + Emitter emit, + ) { + final List newPositions = + List.from(state.positions) + ..add(const OneTimeOrderPosition( + role: '', + count: 1, + startTime: '', + endTime: '', + )); + emit(state.copyWith(positions: newPositions)); + } + + void _onPositionRemoved( + OneTimeOrderPositionRemoved event, + Emitter emit, + ) { + if (state.positions.length > 1) { + final List newPositions = + List.from(state.positions) + ..removeAt(event.index); + emit(state.copyWith(positions: newPositions)); + } + } + + void _onPositionUpdated( + OneTimeOrderPositionUpdated event, + Emitter emit, + ) { + final List newPositions = + List.from(state.positions); + newPositions[event.index] = event.position; + emit(state.copyWith(positions: newPositions)); + } + + Future _onSubmitted( + OneTimeOrderSubmitted event, + Emitter emit, + ) async { + emit(state.copyWith(status: OneTimeOrderStatus.loading)); + try { + final OneTimeOrder order = OneTimeOrder( + date: state.date, + location: state.location, + positions: state.positions, + ); + await _createOneTimeOrderUseCase(OneTimeOrderArguments(order: order)); + emit(state.copyWith(status: OneTimeOrderStatus.success)); + } catch (e) { + emit(state.copyWith( + status: OneTimeOrderStatus.failure, + errorMessage: e.toString(), + )); + } + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart new file mode 100644 index 00000000..749bbb2e --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_event.dart @@ -0,0 +1,50 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +abstract class OneTimeOrderEvent extends Equatable { + const OneTimeOrderEvent(); + + @override + List get props => []; +} + +class OneTimeOrderDateChanged extends OneTimeOrderEvent { + const OneTimeOrderDateChanged(this.date); + final DateTime date; + + @override + List get props => [date]; +} + +class OneTimeOrderLocationChanged extends OneTimeOrderEvent { + const OneTimeOrderLocationChanged(this.location); + final String location; + + @override + List get props => [location]; +} + +class OneTimeOrderPositionAdded extends OneTimeOrderEvent { + const OneTimeOrderPositionAdded(); +} + +class OneTimeOrderPositionRemoved extends OneTimeOrderEvent { + const OneTimeOrderPositionRemoved(this.index); + final int index; + + @override + List get props => [index]; +} + +class OneTimeOrderPositionUpdated extends OneTimeOrderEvent { + const OneTimeOrderPositionUpdated(this.index, this.position); + final int index; + final OneTimeOrderPosition position; + + @override + List get props => [index, position]; +} + +class OneTimeOrderSubmitted extends OneTimeOrderEvent { + const OneTimeOrderSubmitted(); +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart new file mode 100644 index 00000000..2ef862f6 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/one_time_order_state.dart @@ -0,0 +1,59 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +enum OneTimeOrderStatus { initial, loading, success, failure } + +class OneTimeOrderState extends Equatable { + const OneTimeOrderState({ + required this.date, + required this.location, + required this.positions, + this.status = OneTimeOrderStatus.initial, + this.errorMessage, + }); + + factory OneTimeOrderState.initial() { + return OneTimeOrderState( + date: DateTime.now(), + location: '', + positions: const [ + OneTimeOrderPosition( + role: '', + count: 1, + startTime: '', + endTime: '', + ), + ], + ); + } + final DateTime date; + final String location; + final List positions; + final OneTimeOrderStatus status; + final String? errorMessage; + + OneTimeOrderState copyWith({ + DateTime? date, + String? location, + List? positions, + OneTimeOrderStatus? status, + String? errorMessage, + }) { + return OneTimeOrderState( + date: date ?? this.date, + location: location ?? this.location, + positions: positions ?? this.positions, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [ + date, + location, + positions, + status, + errorMessage, + ]; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart new file mode 100644 index 00000000..3574faf0 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_bloc.dart @@ -0,0 +1,89 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../domain/arguments/rapid_order_arguments.dart'; +import '../../domain/usecases/create_rapid_order_usecase.dart'; +import 'rapid_order_event.dart'; +import 'rapid_order_state.dart'; + +/// BLoC for managing the rapid (urgent) order creation flow. +class RapidOrderBloc extends Bloc { + + RapidOrderBloc(this._createRapidOrderUseCase) + : super( + const RapidOrderInitial( + examples: [ + '"We had a call out. Need 2 cooks ASAP"', + '"Need 5 bartenders ASAP until 5am"', + '"Emergency! Need 3 servers right now till midnight"', + ], + ), + ) { + on(_onMessageChanged); + on(_onVoiceToggled); + on(_onSubmitted); + on(_onExampleSelected); + } + final CreateRapidOrderUseCase _createRapidOrderUseCase; + + void _onMessageChanged( + RapidOrderMessageChanged event, + Emitter emit, + ) { + if (state is RapidOrderInitial) { + emit((state as RapidOrderInitial).copyWith(message: event.message)); + } + } + + Future _onVoiceToggled( + RapidOrderVoiceToggled event, + Emitter emit, + ) async { + if (state is RapidOrderInitial) { + final RapidOrderInitial currentState = state as RapidOrderInitial; + final bool newListeningState = !currentState.isListening; + + emit(currentState.copyWith(isListening: newListeningState)); + + // Simulate voice recognition + if (newListeningState) { + await Future.delayed(const Duration(seconds: 2)); + if (state is RapidOrderInitial) { + emit( + (state as RapidOrderInitial).copyWith( + message: 'Need 2 servers for a banquet right now.', + isListening: false, + ), + ); + } + } + } + } + + Future _onSubmitted( + RapidOrderSubmitted event, + Emitter emit, + ) async { + final RapidOrderState currentState = state; + if (currentState is RapidOrderInitial) { + final String message = currentState.message; + emit(const RapidOrderSubmitting()); + + try { + await _createRapidOrderUseCase( + RapidOrderArguments(description: message)); + emit(const RapidOrderSuccess()); + } catch (e) { + emit(RapidOrderFailure(e.toString())); + } + } + } + + void _onExampleSelected( + RapidOrderExampleSelected event, + Emitter emit, + ) { + if (state is RapidOrderInitial) { + final String cleanedExample = event.example.replaceAll('"', ''); + emit((state as RapidOrderInitial).copyWith(message: cleanedExample)); + } + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart new file mode 100644 index 00000000..b2875f77 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_event.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +abstract class RapidOrderEvent extends Equatable { + const RapidOrderEvent(); + + @override + List get props => []; +} + +class RapidOrderMessageChanged extends RapidOrderEvent { + + const RapidOrderMessageChanged(this.message); + final String message; + + @override + List get props => [message]; +} + +class RapidOrderVoiceToggled extends RapidOrderEvent { + const RapidOrderVoiceToggled(); +} + +class RapidOrderSubmitted extends RapidOrderEvent { + const RapidOrderSubmitted(); +} + +class RapidOrderExampleSelected extends RapidOrderEvent { + + const RapidOrderExampleSelected(this.example); + final String example; + + @override + List get props => [example]; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart new file mode 100644 index 00000000..4129ed4b --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/rapid_order_state.dart @@ -0,0 +1,52 @@ +import 'package:equatable/equatable.dart'; + +abstract class RapidOrderState extends Equatable { + const RapidOrderState(); + + @override + List get props => []; +} + +class RapidOrderInitial extends RapidOrderState { + + const RapidOrderInitial({ + this.message = '', + this.isListening = false, + required this.examples, + }); + final String message; + final bool isListening; + final List examples; + + @override + List get props => [message, isListening, examples]; + + RapidOrderInitial copyWith({ + String? message, + bool? isListening, + List? examples, + }) { + return RapidOrderInitial( + message: message ?? this.message, + isListening: isListening ?? this.isListening, + examples: examples ?? this.examples, + ); + } +} + +class RapidOrderSubmitting extends RapidOrderState { + const RapidOrderSubmitting(); +} + +class RapidOrderSuccess extends RapidOrderState { + const RapidOrderSuccess(); +} + +class RapidOrderFailure extends RapidOrderState { + + const RapidOrderFailure(this.error); + final String error; + + @override + List get props => [error]; +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart index 2687f435..42c91202 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/create_order_page.dart @@ -3,14 +3,15 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../blocs/client_create_order_bloc.dart'; import '../blocs/client_create_order_event.dart'; import '../blocs/client_create_order_state.dart'; import '../navigation/client_create_order_navigator.dart'; +import '../widgets/order_type_card.dart'; -/// One-time helper to map keys to translations since they are dynamic in BLoC state -String _getTranslation(String key) { - // Safe mapping - explicit keys expected +/// Helper to map keys to localized strings. +String _getTranslation({required String key}) { if (key == 'client_create_order.types.rapid') { return t.client_create_order.types.rapid; } else if (key == 'client_create_order.types.rapid_desc') { @@ -31,7 +32,10 @@ String _getTranslation(String key) { return key; } +/// Main entry page for the client create order flow. +/// Allows the user to select the type of order they want to create. class ClientCreateOrderPage extends StatelessWidget { + /// Creates a [ClientCreateOrderPage]. const ClientCreateOrderPage({super.key}); @override @@ -50,22 +54,10 @@ class _CreateOrderView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - title: Text( - t.client_create_order.title, - style: UiTypography.headline3m.textPrimary, - ), + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: t.client_create_order.title, + onLeadingPressed: () => Modular.to.pop(), ), body: SafeArea( child: Padding( @@ -75,7 +67,7 @@ class _CreateOrderView extends StatelessWidget { ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ Padding( padding: const EdgeInsets.only(bottom: UiConstants.space6), child: Text( @@ -102,19 +94,22 @@ class _CreateOrderView extends StatelessWidget { ), itemCount: state.orderTypes.length, itemBuilder: (BuildContext context, int index) { - final CreateOrderType type = state.orderTypes[index]; - return _OrderTypeCard( - icon: type.icon, - title: _getTranslation(type.titleKey), + final OrderType type = state.orderTypes[index]; + final _OrderTypeUiMetadata ui = + _OrderTypeUiMetadata.fromId(id: type.id); + + return OrderTypeCard( + icon: ui.icon, + title: _getTranslation(key: type.titleKey), description: _getTranslation( - type.descriptionKey, + key: type.descriptionKey, ), - backgroundColor: type.backgroundColor, - borderColor: type.borderColor, - iconBackgroundColor: type.iconBackgroundColor, - iconColor: type.iconColor, - textColor: type.textColor, - descriptionColor: type.descriptionColor, + backgroundColor: ui.backgroundColor, + borderColor: ui.borderColor, + iconBackgroundColor: ui.iconBackgroundColor, + iconColor: ui.iconColor, + textColor: ui.textColor, + descriptionColor: ui.descriptionColor, onTap: () { switch (type.id) { case 'rapid': @@ -147,68 +142,92 @@ class _CreateOrderView extends StatelessWidget { } } -class _OrderTypeCard extends StatelessWidget { - final IconData icon; - final String title; - final String description; - final Color backgroundColor; - final Color borderColor; - final Color iconBackgroundColor; - final Color iconColor; - final Color textColor; - final Color descriptionColor; - final VoidCallback onTap; - - const _OrderTypeCard({ +/// Metadata for styling order type cards based on their ID. +class _OrderTypeUiMetadata { + const _OrderTypeUiMetadata({ required this.icon, - required this.title, - required this.description, required this.backgroundColor, required this.borderColor, required this.iconBackgroundColor, required this.iconColor, required this.textColor, required this.descriptionColor, - required this.onTap, }); - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(UiConstants.space5), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: borderColor, width: 2), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - width: 48, - height: 48, - margin: const EdgeInsets.only(bottom: UiConstants.space3), - decoration: BoxDecoration( - color: iconBackgroundColor, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - child: Icon(icon, color: iconColor, size: 24), - ), - Text( - title, - style: UiTypography.body2b.copyWith(color: textColor), - ), - const SizedBox(height: UiConstants.space1), - Text( - description, - style: UiTypography.footnote1r.copyWith(color: descriptionColor), - ), - ], - ), - ), - ); + /// Factory to get metadata based on order type ID. + factory _OrderTypeUiMetadata.fromId({required String id}) { + switch (id) { + case 'rapid': + return const _OrderTypeUiMetadata( + icon: UiIcons.zap, + backgroundColor: UiColors.tagPending, + borderColor: UiColors.separatorSpecial, + iconBackgroundColor: UiColors.textWarning, + iconColor: UiColors.white, + textColor: UiColors.textWarning, + descriptionColor: UiColors.textWarning, + ); + case 'one-time': + return const _OrderTypeUiMetadata( + icon: UiIcons.calendar, + backgroundColor: UiColors.tagInProgress, + borderColor: UiColors.primaryInverse, + iconBackgroundColor: UiColors.primary, + iconColor: UiColors.white, + textColor: UiColors.textLink, + descriptionColor: UiColors.textLink, + ); + case 'recurring': + return const _OrderTypeUiMetadata( + icon: UiIcons.rotateCcw, + backgroundColor: UiColors.tagSuccess, + borderColor: UiColors.switchActive, + iconBackgroundColor: UiColors.textSuccess, + iconColor: UiColors.white, + textColor: UiColors.textSuccess, + descriptionColor: UiColors.textSuccess, + ); + case 'permanent': + return const _OrderTypeUiMetadata( + icon: UiIcons.briefcase, + backgroundColor: UiColors.tagRefunded, + borderColor: UiColors.primaryInverse, + iconBackgroundColor: UiColors.primary, + iconColor: UiColors.white, + textColor: UiColors.textLink, + descriptionColor: UiColors.textLink, + ); + default: + return const _OrderTypeUiMetadata( + icon: UiIcons.help, + backgroundColor: UiColors.bgSecondary, + borderColor: UiColors.border, + iconBackgroundColor: UiColors.iconSecondary, + iconColor: UiColors.white, + textColor: UiColors.textPrimary, + descriptionColor: UiColors.textSecondary, + ); + } } + + /// Icon for the order type. + final IconData icon; + + /// Background color for the card. + final Color backgroundColor; + + /// Border color for the card. + final Color borderColor; + + /// Background color for the icon. + final Color iconBackgroundColor; + + /// Color for the icon. + final Color iconColor; + + /// Color for the title text. + final Color textColor; + + /// Color for the description text. + final Color descriptionColor; } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart index a7187324..96995b2e 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/one_time_order_page.dart @@ -1,32 +1,208 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../blocs/one_time_order_bloc.dart'; +import '../blocs/one_time_order_event.dart'; +import '../blocs/one_time_order_state.dart'; +import '../widgets/one_time_order/one_time_order_date_picker.dart'; +import '../widgets/one_time_order/one_time_order_location_input.dart'; +import '../widgets/one_time_order/one_time_order_position_card.dart'; +import '../widgets/one_time_order/one_time_order_section_header.dart'; +import '../widgets/one_time_order/one_time_order_success_view.dart'; +/// Page for creating a one-time staffing order. +/// Users can specify the date, location, and multiple staff positions required. class OneTimeOrderPage extends StatelessWidget { + /// Creates a [OneTimeOrderPage]. const OneTimeOrderPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: - Text('One-Time Order', style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), - ), - body: Center( - child: Text('One-Time Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), - ), + return BlocProvider( + create: (BuildContext context) => Modular.get(), + child: const _OneTimeOrderView(), ); } } + +class _OneTimeOrderView extends StatelessWidget { + const _OneTimeOrderView(); + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + + return BlocBuilder( + builder: (BuildContext context, OneTimeOrderState state) { + if (state.status == OneTimeOrderStatus.success) { + return OneTimeOrderSuccessView( + title: labels.success_title, + message: labels.success_message, + buttonLabel: 'Done', + onDone: () => Modular.to.pop(), + ); + } + + return Scaffold( + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: labels.title, + onLeadingPressed: () => Modular.to.pop(), + ), + body: Stack( + children: [ + _OneTimeOrderForm(state: state), + if (state.status == OneTimeOrderStatus.loading) + const Center(child: CircularProgressIndicator()), + ], + ), + bottomNavigationBar: _BottomActionButton( + label: labels.create_order, + isLoading: state.status == OneTimeOrderStatus.loading, + onPressed: () => BlocProvider.of(context) + .add(const OneTimeOrderSubmitted()), + ), + ); + }, + ); + } +} + +class _OneTimeOrderForm extends StatelessWidget { + const _OneTimeOrderForm({required this.state}); + final OneTimeOrderState state; + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + + return ListView( + padding: const EdgeInsets.all(UiConstants.space5), + children: [ + OneTimeOrderSectionHeader(title: labels.create_your_order), + const SizedBox(height: UiConstants.space4), + + OneTimeOrderDatePicker( + label: labels.date_label, + value: state.date, + onChanged: (DateTime date) => + BlocProvider.of(context) + .add(OneTimeOrderDateChanged(date)), + ), + const SizedBox(height: UiConstants.space4), + + OneTimeOrderLocationInput( + label: labels.location_label, + value: state.location, + onChanged: (String location) => + BlocProvider.of(context) + .add(OneTimeOrderLocationChanged(location)), + ), + const SizedBox(height: UiConstants.space6), + + OneTimeOrderSectionHeader( + title: labels.positions_title, + actionLabel: labels.add_position, + onAction: () => BlocProvider.of(context) + .add(const OneTimeOrderPositionAdded()), + ), + const SizedBox(height: UiConstants.space4), + + // Positions List + ...state.positions + .asMap() + .entries + .map((MapEntry entry) { + final int index = entry.key; + final OneTimeOrderPosition position = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space4), + child: OneTimeOrderPositionCard( + index: index, + position: position, + isRemovable: state.positions.length > 1, + positionLabel: labels.positions_title, + roleLabel: labels.select_role, + workersLabel: labels.workers_label, + startLabel: labels.start_label, + endLabel: labels.end_label, + lunchLabel: labels.lunch_break_label, + onUpdated: (OneTimeOrderPosition updated) { + BlocProvider.of(context).add( + OneTimeOrderPositionUpdated(index, updated), + ); + }, + onRemoved: () { + BlocProvider.of(context) + .add(OneTimeOrderPositionRemoved(index)); + }, + ), + ); + }), + const SizedBox(height: 100), // Space for bottom button + ], + ); + } +} + +class _BottomActionButton extends StatelessWidget { + const _BottomActionButton({ + required this.label, + required this.onPressed, + this.isLoading = false, + }); + final String label; + final VoidCallback onPressed; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only( + left: UiConstants.space5, + right: UiConstants.space5, + top: UiConstants.space4, + bottom: MediaQuery.of(context).padding.bottom + UiConstants.space4, + ), + decoration: BoxDecoration( + color: UiColors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -4), + ), + ], + ), + child: isLoading + ? const UiButton( + buttonBuilder: _dummyBuilder, + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: UiColors.primary, strokeWidth: 2), + ), + ) + : UiButton.primary( + text: label, + onPressed: onPressed, + size: UiButtonSize.large, + ), + ); + } + + static Widget _dummyBuilder( + BuildContext context, + VoidCallback? onPressed, + ButtonStyle? style, + Widget child, + ) { + return Center(child: child); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart index cdde6387..fd38a142 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -1,31 +1,39 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; +/// Permanent Order Page - Long-term staffing placement. +/// Placeholder for future implementation. class PermanentOrderPage extends StatelessWidget { + /// Creates a [PermanentOrderPage]. const PermanentOrderPage({super.key}); @override Widget build(BuildContext context) { + final TranslationsClientCreateOrderPermanentEn labels = + t.client_create_order.permanent; + return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: - Text('Permanent Order', style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: labels.title, + onLeadingPressed: () => Modular.to.pop(), ), body: Center( - child: Text('Permanent Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), + child: Padding( + padding: const EdgeInsets.all(UiConstants.space6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), + ), ), ); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart index 187202d5..0f0bb874 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/rapid_order_page.dart @@ -1,31 +1,326 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:intl/intl.dart'; +import '../blocs/rapid_order_bloc.dart'; +import '../blocs/rapid_order_event.dart'; +import '../blocs/rapid_order_state.dart'; +import '../widgets/rapid_order/rapid_order_example_card.dart'; +import '../widgets/rapid_order/rapid_order_header.dart'; +import '../widgets/rapid_order/rapid_order_success_view.dart'; +/// Rapid Order Flow Page - Emergency staffing requests. +/// Features voice recognition simulation and quick example selection. class RapidOrderPage extends StatelessWidget { + /// Creates a [RapidOrderPage]. const RapidOrderPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: Text('Rapid Order', style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), + return BlocProvider( + create: (BuildContext context) => Modular.get(), + child: const _RapidOrderView(), + ); + } +} + +class _RapidOrderView extends StatelessWidget { + const _RapidOrderView(); + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderRapidEn labels = + t.client_create_order.rapid; + + return BlocBuilder( + builder: (BuildContext context, RapidOrderState state) { + if (state is RapidOrderSuccess) { + return RapidOrderSuccessView( + title: labels.success_title, + message: labels.success_message, + buttonLabel: labels.back_to_orders, + onDone: () => Modular.to.pop(), + ); + } + + return const _RapidOrderForm(); + }, + ); + } +} + +class _RapidOrderForm extends StatefulWidget { + const _RapidOrderForm(); + + @override + State<_RapidOrderForm> createState() => _RapidOrderFormState(); +} + +class _RapidOrderFormState extends State<_RapidOrderForm> { + final TextEditingController _messageController = TextEditingController(); + + @override + void dispose() { + _messageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderRapidEn labels = + t.client_create_order.rapid; + final DateTime now = DateTime.now(); + final String dateStr = DateFormat('EEE, MMM dd, yyyy').format(now); + final String timeStr = DateFormat('h:mm a').format(now); + + return BlocListener( + listener: (BuildContext context, RapidOrderState state) { + if (state is RapidOrderInitial) { + if (_messageController.text != state.message) { + _messageController.text = state.message; + _messageController.selection = TextSelection.fromPosition( + TextPosition(offset: _messageController.text.length), + ); + } + } + }, + child: Scaffold( + backgroundColor: UiColors.bgPrimary, + body: Column( + children: [ + RapidOrderHeader( + title: labels.title, + subtitle: labels.subtitle, + date: dateStr, + time: timeStr, + onBack: () => Modular.to.pop(), + ), + + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + labels.tell_us, + style: UiTypography.headline3m.textPrimary, + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.destructive, + borderRadius: UiConstants.radiusSm, + ), + child: Text( + labels.urgent_badge, + style: UiTypography.footnote2b.copyWith( + color: UiColors.white, + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space4), + + // Main Card + Container( + padding: const EdgeInsets.all(UiConstants.space6), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: BlocBuilder( + builder: (BuildContext context, RapidOrderState state) { + final RapidOrderInitial? initialState = + state is RapidOrderInitial ? state : null; + final bool isSubmitting = + state is RapidOrderSubmitting; + + return Column( + children: [ + // Icon + _AnimatedZapIcon(), + const SizedBox(height: UiConstants.space4), + Text( + labels.need_staff, + style: UiTypography.headline2m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), + Text( + labels.type_or_speak, + textAlign: TextAlign.center, + style: UiTypography.body2r.textSecondary, + ), + const SizedBox(height: UiConstants.space6), + + // Examples + if (initialState != null) + ...initialState.examples + .asMap() + .entries + .map((MapEntry entry) { + final int index = entry.key; + final String example = entry.value; + final bool isHighlighted = index == 0; + + return Padding( + padding: const EdgeInsets.only( + bottom: UiConstants.space2), + child: RapidOrderExampleCard( + example: example, + isHighlighted: isHighlighted, + label: labels.example, + onTap: () => + BlocProvider.of( + context) + .add( + RapidOrderExampleSelected(example), + ), + ), + ); + }), + const SizedBox(height: UiConstants.space4), + + // Input + TextField( + controller: _messageController, + maxLines: 4, + onChanged: (String value) { + BlocProvider.of(context).add( + RapidOrderMessageChanged(value), + ); + }, + decoration: InputDecoration( + hintText: labels.hint, + hintStyle: UiTypography.body2r.copyWith( + color: UiColors.textPlaceholder, + ), + border: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + borderSide: const BorderSide( + color: UiColors.border, + ), + ), + contentPadding: + const EdgeInsets.all(UiConstants.space4), + ), + ), + const SizedBox(height: UiConstants.space4), + + // Actions + _RapidOrderActions( + labels: labels, + isSubmitting: isSubmitting, + isListening: initialState?.isListening ?? false, + isMessageEmpty: initialState != null && + initialState.message.trim().isEmpty, + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ], ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), - ), - body: Center( - child: Text('Rapid Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), ), ); } } + +class _AnimatedZapIcon extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + width: 64, + height: 64, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.destructive, + UiColors.destructive.withValues(alpha: 0.85), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: UiConstants.radiusLg, + boxShadow: [ + BoxShadow( + color: UiColors.destructive.withValues(alpha: 0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + UiIcons.zap, + color: UiColors.white, + size: 32, + ), + ); + } +} + +class _RapidOrderActions extends StatelessWidget { + const _RapidOrderActions({ + required this.labels, + required this.isSubmitting, + required this.isListening, + required this.isMessageEmpty, + }); + final TranslationsClientCreateOrderRapidEn labels; + final bool isSubmitting; + final bool isListening; + final bool isMessageEmpty; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: UiButton.secondary( + text: isListening ? labels.listening : labels.speak, + leadingIcon: UiIcons.bell, // Placeholder for mic + onPressed: () => BlocProvider.of(context).add( + const RapidOrderVoiceToggled(), + ), + style: OutlinedButton.styleFrom( + backgroundColor: isListening + ? UiColors.destructive.withValues(alpha: 0.05) + : null, + side: isListening + ? const BorderSide(color: UiColors.destructive) + : null, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: UiButton.primary( + text: isSubmitting ? labels.sending : labels.send, + trailingIcon: UiIcons.arrowRight, + onPressed: isSubmitting || isMessageEmpty + ? null + : () => BlocProvider.of(context).add( + const RapidOrderSubmitted(), + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart index 98092ea6..64324b46 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -1,31 +1,39 @@ +import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; +/// Recurring Order Page - Ongoing weekly/monthly coverage. +/// Placeholder for future implementation. class RecurringOrderPage extends StatelessWidget { + /// Creates a [RecurringOrderPage]. const RecurringOrderPage({super.key}); @override Widget build(BuildContext context) { + final TranslationsClientCreateOrderRecurringEn labels = + t.client_create_order.recurring; + return Scaffold( - backgroundColor: UiColors.background, - appBar: AppBar( - title: - Text('Recurring Order', style: UiTypography.headline3m.textPrimary), - leading: IconButton( - icon: const Icon(UiIcons.chevronLeft, color: UiColors.iconSecondary), - onPressed: () => Modular.to.pop(), - ), - backgroundColor: UiColors.white, - elevation: 0, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: UiColors.border, height: 1.0), - ), + backgroundColor: UiColors.bgPrimary, + appBar: UiAppBar( + title: labels.title, + onLeadingPressed: () => Modular.to.pop(), ), body: Center( - child: Text('Recurring Order Flow (WIP)', - style: UiTypography.body1r.textSecondary), + child: Padding( + padding: const EdgeInsets.all(UiConstants.space6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + labels.subtitle, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + ], + ), + ), ), ); } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart new file mode 100644 index 00000000..5b32274d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart @@ -0,0 +1,68 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// A date picker field for the one-time order form. +class OneTimeOrderDatePicker extends StatelessWidget { + /// The label text to display above the field. + final String label; + + /// The currently selected date. + final DateTime value; + + /// Callback when a new date is selected. + final ValueChanged onChanged; + + /// Creates a [OneTimeOrderDatePicker]. + const OneTimeOrderDatePicker({ + required this.label, + required this.value, + required this.onChanged, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: UiTypography.footnote1m.textSecondary), + const SizedBox(height: UiConstants.space2), + InkWell( + onTap: () async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: value, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null) { + onChanged(picked); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3 + 2, + ), + decoration: BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + ), + child: Row( + children: [ + const Icon(UiIcons.calendar, + size: 20, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space3), + Text( + DateFormat('EEEE, MMM d, yyyy').format(value), + style: UiTypography.body1r.textPrimary, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart new file mode 100644 index 00000000..3f93da9d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart @@ -0,0 +1,34 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A location input field for the one-time order form. +class OneTimeOrderLocationInput extends StatelessWidget { + /// The label text to display above the field. + final String label; + + /// The current location value. + final String value; + + /// Callback when the location text changes. + final ValueChanged onChanged; + + /// Creates a [OneTimeOrderLocationInput]. + const OneTimeOrderLocationInput({ + required this.label, + required this.value, + required this.onChanged, + super.key, + }); + + @override + Widget build(BuildContext context) { + return UiTextField( + label: label, + hintText: 'Select Branch/Location', + controller: TextEditingController(text: value) + ..selection = TextSelection.collapsed(offset: value.length), + onChanged: onChanged, + prefixIcon: UiIcons.mapPin, + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart new file mode 100644 index 00000000..a605ea5c --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart @@ -0,0 +1,294 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A card widget for editing a specific position in a one-time order. +class OneTimeOrderPositionCard extends StatelessWidget { + /// The index of the position in the list. + final int index; + + /// The position entity data. + final OneTimeOrderPosition position; + + /// Whether this position can be removed (usually if there's more than one). + final bool isRemovable; + + /// Callback when the position data is updated. + final ValueChanged onUpdated; + + /// Callback when the position is removed. + final VoidCallback onRemoved; + + /// Label for positions (e.g., "Position"). + final String positionLabel; + + /// Label for the role selection. + final String roleLabel; + + /// Label for the worker count. + final String workersLabel; + + /// Label for the start time. + final String startLabel; + + /// Label for the end time. + final String endLabel; + + /// Label for the lunch break. + final String lunchLabel; + + /// Creates a [OneTimeOrderPositionCard]. + const OneTimeOrderPositionCard({ + required this.index, + required this.position, + required this.isRemovable, + required this.onUpdated, + required this.onRemoved, + required this.positionLabel, + required this.roleLabel, + required this.workersLabel, + required this.startLabel, + required this.endLabel, + required this.lunchLabel, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$positionLabel #${index + 1}', + style: UiTypography.body1b.textPrimary, + ), + if (isRemovable) + IconButton( + icon: const Icon(UiIcons.delete, + size: 20, color: UiColors.destructive), + onPressed: onRemoved, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + visualDensity: VisualDensity.compact, + ), + ], + ), + const Divider(height: UiConstants.space6), + + // Role (Dropdown) + _LabelField( + label: roleLabel, + child: DropdownButtonFormField( + value: position.role.isEmpty ? null : position.role, + items: ['Server', 'Bartender', 'Cook', 'Busser', 'Host'] + .map((String role) => DropdownMenuItem( + value: role, + child: + Text(role, style: UiTypography.body1r.textPrimary), + )) + .toList(), + onChanged: (String? val) { + if (val != null) { + onUpdated(position.copyWith(role: val)); + } + }, + decoration: _inputDecoration(UiIcons.briefcase), + ), + ), + const SizedBox(height: UiConstants.space4), + + // Count (Counter) + _LabelField( + label: workersLabel, + child: Row( + children: [ + _CounterButton( + icon: UiIcons.minus, + onPressed: position.count > 1 + ? () => onUpdated( + position.copyWith(count: position.count - 1)) + : null, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4), + child: Text('${position.count}', + style: UiTypography.headline3m.textPrimary), + ), + _CounterButton( + icon: UiIcons.add, + onPressed: () => + onUpdated(position.copyWith(count: position.count + 1)), + ), + ], + ), + ), + const SizedBox(height: UiConstants.space4), + + // Start/End Time + Row( + children: [ + Expanded( + child: _LabelField( + label: startLabel, + child: InkWell( + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 9, minute: 0), + ); + if (picked != null) { + onUpdated(position.copyWith( + startTime: picked.format(context))); + } + }, + child: Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: _boxDecoration(), + child: Text( + position.startTime.isEmpty + ? '--:--' + : position.startTime, + style: UiTypography.body1r.textPrimary, + ), + ), + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: _LabelField( + label: endLabel, + child: InkWell( + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: const TimeOfDay(hour: 17, minute: 0), + ); + if (picked != null) { + onUpdated( + position.copyWith(endTime: picked.format(context))); + } + }, + child: Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: _boxDecoration(), + child: Text( + position.endTime.isEmpty ? '--:--' : position.endTime, + style: UiTypography.body1r.textPrimary, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space4), + + // Lunch Break + _LabelField( + label: lunchLabel, + child: DropdownButtonFormField( + value: position.lunchBreak, + items: [0, 30, 45, 60] + .map((int mins) => DropdownMenuItem( + value: mins, + child: Text('${mins}m', + style: UiTypography.body1r.textPrimary), + )) + .toList(), + onChanged: (int? val) { + if (val != null) { + onUpdated(position.copyWith(lunchBreak: val)); + } + }, + decoration: _inputDecoration(UiIcons.clock), + ), + ), + ], + ), + ); + } + + InputDecoration _inputDecoration(IconData icon) => InputDecoration( + prefixIcon: Icon(icon, size: 18, color: UiColors.iconSecondary), + contentPadding: + const EdgeInsets.symmetric(horizontal: UiConstants.space3), + border: OutlineInputBorder( + borderRadius: UiConstants.radiusLg, + borderSide: const BorderSide(color: UiColors.border), + ), + ); + + BoxDecoration _boxDecoration() => BoxDecoration( + border: Border.all(color: UiColors.border), + borderRadius: UiConstants.radiusLg, + ); +} + +class _LabelField extends StatelessWidget { + const _LabelField({required this.label, required this.child}); + final String label; + final Widget child; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: UiTypography.footnote1m.textSecondary), + const SizedBox(height: UiConstants.space1), + child, + ], + ); + } +} + +class _CounterButton extends StatelessWidget { + const _CounterButton({required this.icon, this.onPressed}); + final IconData icon; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + border: Border.all( + color: onPressed != null + ? UiColors.border + : UiColors.border.withOpacity(0.5)), + borderRadius: UiConstants.radiusLg, + color: onPressed != null ? UiColors.white : UiColors.background, + ), + child: Icon( + icon, + size: 16, + color: onPressed != null + ? UiColors.iconPrimary + : UiColors.iconSecondary.withOpacity(0.5), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart new file mode 100644 index 00000000..29c8df31 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart @@ -0,0 +1,42 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A header widget for sections in the one-time order form. +class OneTimeOrderSectionHeader extends StatelessWidget { + /// The title text for the section. + final String title; + + /// Optional label for an action button on the right. + final String? actionLabel; + + /// Callback when the action button is tapped. + final VoidCallback? onAction; + + /// Creates a [OneTimeOrderSectionHeader]. + const OneTimeOrderSectionHeader({ + required this.title, + this.actionLabel, + this.onAction, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: UiTypography.headline4m.textPrimary), + if (actionLabel != null && onAction != null) + TextButton.icon( + onPressed: onAction, + icon: const Icon(UiIcons.add, size: 16, color: UiColors.primary), + label: Text(actionLabel!, style: UiTypography.body2b.textPrimary), + style: TextButton.styleFrom( + padding: + const EdgeInsets.symmetric(horizontal: UiConstants.space2), + ), + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart new file mode 100644 index 00000000..ea704758 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart @@ -0,0 +1,71 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A view to display when a one-time order has been successfully created. +class OneTimeOrderSuccessView extends StatelessWidget { + /// The title of the success message. + final String title; + + /// The body of the success message. + final String message; + + /// Label for the completion button. + final String buttonLabel; + + /// Callback when the completion button is tapped. + final VoidCallback onDone; + + /// Creates a [OneTimeOrderSuccessView]. + const OneTimeOrderSuccessView({ + required this.title, + required this.message, + required this.buttonLabel, + required this.onDone, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: UiColors.white, + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: const BoxDecoration( + color: UiColors.tagSuccess, + shape: BoxShape.circle, + ), + child: const Icon(UiIcons.check, + size: 50, color: UiColors.textSuccess), + ), + const SizedBox(height: UiConstants.space8), + Text( + title, + style: UiTypography.headline2m.textPrimary, + textAlign: TextAlign.center, + ), + const SizedBox(height: UiConstants.space4), + Text( + message, + style: UiTypography.body1r.textSecondary, + textAlign: TextAlign.center, + ), + const SizedBox(height: UiConstants.space10), + UiButton.primary( + text: buttonLabel, + onPressed: onDone, + size: UiButtonSize.large, + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart new file mode 100644 index 00000000..8b450b99 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/order_type_card.dart @@ -0,0 +1,95 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A card widget representing an order type in the creation flow. +class OrderTypeCard extends StatelessWidget { + /// Icon to display at the top of the card. + final IconData icon; + + /// Main title of the order type. + final String title; + + /// Brief description of what this order type entails. + final String description; + + /// Background color of the card. + final Color backgroundColor; + + /// Color of the card's border. + final Color borderColor; + + /// Background color for the icon container. + final Color iconBackgroundColor; + + /// Color of the icon itself. + final Color iconColor; + + /// Color of the title text. + final Color textColor; + + /// Color of the description text. + final Color descriptionColor; + + /// Callback when the card is tapped. + final VoidCallback onTap; + + /// Creates an [OrderTypeCard]. + const OrderTypeCard({ + required this.icon, + required this.title, + required this.description, + required this.backgroundColor, + required this.borderColor, + required this.iconBackgroundColor, + required this.iconColor, + required this.textColor, + required this.descriptionColor, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(UiConstants.space5), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + border: Border.all(color: borderColor, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + margin: const EdgeInsets.only(bottom: UiConstants.space3), + decoration: BoxDecoration( + color: iconBackgroundColor, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Icon(icon, color: iconColor, size: 24), + ), + Text( + title, + style: UiTypography.body2b.copyWith(color: textColor), + ), + const SizedBox(height: UiConstants.space1), + Expanded( + child: Text( + description, + style: + UiTypography.footnote1r.copyWith(color: descriptionColor), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart new file mode 100644 index 00000000..3bde4479 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_example_card.dart @@ -0,0 +1,61 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A card displaying an example message for a rapid order. +class RapidOrderExampleCard extends StatelessWidget { + /// The example text. + final String example; + + /// Whether this is the first (highlighted) example. + final bool isHighlighted; + + /// The label for the example prefix (e.g., "Example:"). + final String label; + + /// Callback when the card is tapped. + final VoidCallback onTap; + + /// Creates a [RapidOrderExampleCard]. + const RapidOrderExampleCard({ + required this.example, + required this.isHighlighted, + required this.label, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space4, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + color: isHighlighted + ? UiColors.accent.withValues(alpha: 0.15) + : UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all( + color: isHighlighted ? UiColors.accent : UiColors.border, + ), + ), + child: RichText( + text: TextSpan( + style: UiTypography.body2r.textPrimary, + children: [ + TextSpan( + text: label, + style: UiTypography.body2b.textPrimary, + ), + TextSpan(text: ' $example'), + ], + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart new file mode 100644 index 00000000..4d7a3848 --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_header.dart @@ -0,0 +1,122 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A header widget for the rapid order flow with a gradient background. +class RapidOrderHeader extends StatelessWidget { + /// The title of the page. + final String title; + + /// The subtitle or description. + final String subtitle; + + /// The formatted current date. + final String date; + + /// The formatted current time. + final String time; + + /// Callback when the back button is pressed. + final VoidCallback onBack; + + /// Creates a [RapidOrderHeader]. + const RapidOrderHeader({ + required this.title, + required this.subtitle, + required this.date, + required this.time, + required this.onBack, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + UiConstants.space5, + bottom: UiConstants.space5, + left: UiConstants.space5, + right: UiConstants.space5, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.destructive, + UiColors.destructive.withValues(alpha: 0.85), + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + GestureDetector( + onTap: onBack, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.2), + borderRadius: UiConstants.radiusMd, + ), + child: const Icon( + UiIcons.chevronLeft, + color: UiColors.white, + size: 24, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + UiIcons.zap, + color: UiColors.accent, + size: 18, + ), + const SizedBox(width: UiConstants.space2), + Text( + title, + style: UiTypography.headline3m.copyWith( + color: UiColors.white, + ), + ), + ], + ), + Text( + subtitle, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + date, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.9), + ), + ), + Text( + time, + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.9), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart new file mode 100644 index 00000000..3ea9ad4d --- /dev/null +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_success_view.dart @@ -0,0 +1,108 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A view to display when a rapid order has been successfully created. +class RapidOrderSuccessView extends StatelessWidget { + /// The title of the success message. + final String title; + + /// The body of the success message. + final String message; + + /// Label for the completion button. + final String buttonLabel; + + /// Callback when the completion button is tapped. + final VoidCallback onDone; + + /// Creates a [RapidOrderSuccessView]. + const RapidOrderSuccessView({ + required this.title, + required this.message, + required this.buttonLabel, + required this.onDone, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + UiColors.primary, + UiColors.primary.withValues(alpha: 0.85), + ], + ), + ), + child: SafeArea( + child: Center( + child: Container( + margin: + const EdgeInsets.symmetric(horizontal: UiConstants.space10), + padding: const EdgeInsets.all(UiConstants.space8), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, + height: 64, + decoration: const BoxDecoration( + color: UiColors.accent, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + UiIcons.zap, + color: UiColors.textPrimary, + size: 32, + ), + ), + ), + const SizedBox(height: UiConstants.space6), + Text( + title, + style: UiTypography.headline1m.textPrimary, + ), + const SizedBox(height: UiConstants.space3), + Text( + message, + textAlign: TextAlign.center, + style: UiTypography.body2r.copyWith( + color: UiColors.textSecondary, + height: 1.5, + ), + ), + const SizedBox(height: UiConstants.space8), + SizedBox( + width: double.infinity, + child: UiButton.primary( + text: buttonLabel, + onPressed: onDone, + size: UiButtonSize.large, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/create_order/pubspec.lock b/apps/mobile/packages/features/client/create_order/pubspec.lock index 2a4056b0..41d3237a 100644 --- a/apps/mobile/packages/features/client/create_order/pubspec.lock +++ b/apps/mobile/packages/features/client/create_order/pubspec.lock @@ -300,7 +300,7 @@ packages: source: hosted version: "4.1.2" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" @@ -324,12 +324,19 @@ packages: source: hosted version: "0.7.2" krow_core: - dependency: transitive + 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: diff --git a/apps/mobile/packages/features/client/create_order/pubspec.yaml b/apps/mobile/packages/features/client/create_order/pubspec.yaml index 34cc1051..6ff66afe 100644 --- a/apps/mobile/packages/features/client/create_order/pubspec.yaml +++ b/apps/mobile/packages/features/client/create_order/pubspec.yaml @@ -12,12 +12,17 @@ dependencies: flutter_bloc: ^8.1.3 flutter_modular: ^6.3.2 equatable: ^2.0.5 + intl: 0.20.2 design_system: path: ../../../design_system core_localization: path: ../../../core_localization krow_domain: path: ../../../domain + krow_core: + path: ../../../core + krow_data_connect: + path: ../../../data_connect dev_dependencies: flutter_test: