Merge pull request #458 from Oloodi/408-feature-implement-paidunpaid-breaks---client-app-frontend-development
Recurring and Permanent order is fully integrated to client/staff applications
This commit is contained in:
@@ -137,35 +137,47 @@ extension ClientNavigator on IModularNavigator {
|
||||
/// Pushes the order creation flow entry page.
|
||||
///
|
||||
/// This is the starting point for all order creation flows.
|
||||
void toCreateOrder() {
|
||||
pushNamed(ClientPaths.createOrder);
|
||||
void toCreateOrder({Object? arguments}) {
|
||||
navigate(ClientPaths.createOrder, arguments: arguments);
|
||||
}
|
||||
|
||||
/// Pushes the rapid order creation flow.
|
||||
///
|
||||
/// Quick shift creation with simplified inputs for urgent needs.
|
||||
void toCreateOrderRapid() {
|
||||
pushNamed(ClientPaths.createOrderRapid);
|
||||
void toCreateOrderRapid({Object? arguments}) {
|
||||
pushNamed(ClientPaths.createOrderRapid, arguments: arguments);
|
||||
}
|
||||
|
||||
/// Pushes the one-time order creation flow.
|
||||
///
|
||||
/// Create a shift that occurs once at a specific date and time.
|
||||
void toCreateOrderOneTime() {
|
||||
pushNamed(ClientPaths.createOrderOneTime);
|
||||
void toCreateOrderOneTime({Object? arguments}) {
|
||||
pushNamed(ClientPaths.createOrderOneTime, arguments: arguments);
|
||||
}
|
||||
|
||||
/// Pushes the recurring order creation flow.
|
||||
///
|
||||
/// Create shifts that repeat on a defined schedule (daily, weekly, etc.).
|
||||
void toCreateOrderRecurring() {
|
||||
pushNamed(ClientPaths.createOrderRecurring);
|
||||
void toCreateOrderRecurring({Object? arguments}) {
|
||||
pushNamed(ClientPaths.createOrderRecurring, arguments: arguments);
|
||||
}
|
||||
|
||||
/// Pushes the permanent order creation flow.
|
||||
///
|
||||
/// Create a long-term or permanent staffing position.
|
||||
void toCreateOrderPermanent() {
|
||||
pushNamed(ClientPaths.createOrderPermanent);
|
||||
void toCreateOrderPermanent({Object? arguments}) {
|
||||
pushNamed(ClientPaths.createOrderPermanent, arguments: arguments);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// VIEW ORDER
|
||||
// ==========================================================================
|
||||
|
||||
/// Navigates to the order details page to a specific date.
|
||||
void toOrdersSpecificDate(DateTime date) {
|
||||
navigate(
|
||||
ClientPaths.orders,
|
||||
arguments: <String, DateTime>{'initialDate': date},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ extension StaffNavigator on IModularNavigator {
|
||||
/// This is typically called after successful phone verification for new
|
||||
/// staff members. Uses pushReplacement to prevent going back to verification.
|
||||
void toProfileSetup() {
|
||||
pushReplacementNamed(StaffPaths.profileSetup);
|
||||
pushNamed(StaffPaths.profileSetup);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
@@ -76,7 +76,7 @@ extension StaffNavigator on IModularNavigator {
|
||||
/// This is the main landing page for authenticated staff members.
|
||||
/// Displays shift cards, quick actions, and notifications.
|
||||
void toStaffHome() {
|
||||
pushNamed(StaffPaths.home);
|
||||
pushNamedAndRemoveUntil(StaffPaths.home, (_) => false);
|
||||
}
|
||||
|
||||
/// Navigates to the staff main shell.
|
||||
@@ -84,7 +84,7 @@ extension StaffNavigator on IModularNavigator {
|
||||
/// This is the container with bottom navigation. Navigates to home tab
|
||||
/// by default. Usually you'd navigate to a specific tab instead.
|
||||
void toStaffMain() {
|
||||
navigate('${StaffPaths.main}/home/');
|
||||
pushNamedAndRemoveUntil('${StaffPaths.main}/home/', (_) => false);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
@@ -113,31 +113,28 @@ extension StaffNavigator on IModularNavigator {
|
||||
if (refreshAvailable == true) {
|
||||
args['refreshAvailable'] = true;
|
||||
}
|
||||
navigate(
|
||||
StaffPaths.shifts,
|
||||
arguments: args.isEmpty ? null : args,
|
||||
);
|
||||
navigate(StaffPaths.shifts, arguments: args.isEmpty ? null : args);
|
||||
}
|
||||
|
||||
/// Navigates to the Payments tab.
|
||||
///
|
||||
/// View payment history, earnings breakdown, and tax information.
|
||||
void toPayments() {
|
||||
navigate(StaffPaths.payments);
|
||||
pushNamedAndRemoveUntil(StaffPaths.payments, (_) => false);
|
||||
}
|
||||
|
||||
/// Navigates to the Clock In tab.
|
||||
///
|
||||
/// Access time tracking interface for active shifts.
|
||||
void toClockIn() {
|
||||
navigate(StaffPaths.clockIn);
|
||||
pushNamedAndRemoveUntil(StaffPaths.clockIn, (_) => false);
|
||||
}
|
||||
|
||||
/// Navigates to the Profile tab.
|
||||
///
|
||||
/// Manage personal information, documents, and preferences.
|
||||
void toProfile() {
|
||||
navigate(StaffPaths.profile);
|
||||
pushNamedAndRemoveUntil(StaffPaths.profile, (_) => false);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
@@ -155,22 +152,7 @@ extension StaffNavigator on IModularNavigator {
|
||||
/// The shift object is passed as an argument and can be retrieved
|
||||
/// in the details page.
|
||||
void toShiftDetails(Shift shift) {
|
||||
navigate(
|
||||
StaffPaths.shiftDetails(shift.id),
|
||||
arguments: shift,
|
||||
);
|
||||
}
|
||||
|
||||
/// Pushes the shift details page (alternative method).
|
||||
///
|
||||
/// Same as [toShiftDetails] but using pushNamed instead of navigate.
|
||||
/// Use this when you want to add the details page to the stack rather
|
||||
/// than replacing the current route.
|
||||
void pushShiftDetails(Shift shift) {
|
||||
pushNamed(
|
||||
StaffPaths.shiftDetails(shift.id),
|
||||
arguments: shift,
|
||||
);
|
||||
navigate(StaffPaths.shiftDetails(shift.id), arguments: shift);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"client_authentication": {
|
||||
"get_started_page": {
|
||||
"title": "Take Control of Your\nShifts and Events",
|
||||
"subtitle": "Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same page—all in one place",
|
||||
"subtitle": "Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same page\u2014all in one place",
|
||||
"sign_in_button": "Sign In",
|
||||
"create_account_button": "Create Account"
|
||||
},
|
||||
@@ -452,7 +452,7 @@
|
||||
},
|
||||
"empty_states": {
|
||||
"no_shifts_today": "No shifts scheduled for today",
|
||||
"find_shifts_cta": "Find shifts →",
|
||||
"find_shifts_cta": "Find shifts \u2192",
|
||||
"no_shifts_tomorrow": "No shifts for tomorrow",
|
||||
"no_recommended_shifts": "No recommended shifts"
|
||||
},
|
||||
@@ -462,7 +462,7 @@
|
||||
"amount": "$amount"
|
||||
},
|
||||
"recommended_card": {
|
||||
"act_now": "• ACT NOW",
|
||||
"act_now": "\u2022 ACT NOW",
|
||||
"one_day": "One Day",
|
||||
"today": "Today",
|
||||
"applied_for": "Applied for $title",
|
||||
@@ -695,7 +695,7 @@
|
||||
"eta_label": "$min min",
|
||||
"locked_desc": "Most app features are locked while commute mode is on. You'll be able to clock in once you arrive.",
|
||||
"turn_off": "Turn Off Commute Mode",
|
||||
"arrived_title": "You've Arrived! 🎉",
|
||||
"arrived_title": "You've Arrived! \ud83c\udf89",
|
||||
"arrived_desc": "You're at the shift location. Ready to clock in?"
|
||||
},
|
||||
"swipe": {
|
||||
@@ -967,16 +967,16 @@
|
||||
"required": "REQUIRED",
|
||||
"add_photo": "Add Photo",
|
||||
"added": "Added",
|
||||
"pending": "⏳ Pending verification"
|
||||
"pending": "\u23f3 Pending verification"
|
||||
},
|
||||
"attestation": "I certify that I own these items and will wear them to my shifts. I understand that items are pending manager verification at my first shift.",
|
||||
"actions": {
|
||||
"save": "Save Attire"
|
||||
},
|
||||
"validation": {
|
||||
"select_required": "✓ Select all required items",
|
||||
"upload_required": "✓ Upload photos of required items",
|
||||
"accept_attestation": "✓ Accept attestation"
|
||||
"select_required": "\u2713 Select all required items",
|
||||
"upload_required": "\u2713 Upload photos of required items",
|
||||
"accept_attestation": "\u2713 Accept attestation"
|
||||
}
|
||||
},
|
||||
"staff_shifts": {
|
||||
@@ -1095,8 +1095,18 @@
|
||||
},
|
||||
"card": {
|
||||
"cancelled": "CANCELLED",
|
||||
"compensation": "• 4hr compensation"
|
||||
"compensation": "\u2022 4hr compensation"
|
||||
}
|
||||
},
|
||||
"find_shifts": {
|
||||
"search_hint": "Search jobs, location...",
|
||||
"filter_all": "All Jobs",
|
||||
"filter_one_day": "One Day",
|
||||
"filter_multi_day": "Multi-Day",
|
||||
"filter_long_term": "Long Term",
|
||||
"no_jobs_title": "No jobs available",
|
||||
"no_jobs_subtitle": "Check back later",
|
||||
"application_submitted": "Shift application submitted!"
|
||||
}
|
||||
},
|
||||
"staff_time_card": {
|
||||
@@ -1218,11 +1228,11 @@
|
||||
},
|
||||
"total_spend": {
|
||||
"label": "Total Spend",
|
||||
"badge": "↓ 8% vs last week"
|
||||
"badge": "\u2193 8% vs last week"
|
||||
},
|
||||
"fill_rate": {
|
||||
"label": "Fill Rate",
|
||||
"badge": "↑ 2% improvement"
|
||||
"badge": "\u2191 2% improvement"
|
||||
},
|
||||
"avg_fill_time": {
|
||||
"label": "Avg Fill Time",
|
||||
@@ -1364,9 +1374,9 @@
|
||||
"target_prefix": "Target: ",
|
||||
"target_hours": "$hours hrs",
|
||||
"target_percent": "$percent%",
|
||||
"met": "✓ Met",
|
||||
"close": "→ Close",
|
||||
"miss": "✗ Miss"
|
||||
"met": "\u2713 Met",
|
||||
"close": "\u2192 Close",
|
||||
"miss": "\u2717 Miss"
|
||||
},
|
||||
"additional_metrics_title": "ADDITIONAL METRICS",
|
||||
"additional_metrics": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@
|
||||
/// They will implement interfaces defined in feature packages once those are created.
|
||||
library;
|
||||
|
||||
|
||||
export 'src/data_connect_module.dart';
|
||||
export 'src/session/client_session_store.dart';
|
||||
|
||||
@@ -45,10 +44,6 @@ export 'src/connectors/hubs/data/repositories/hubs_connector_repository_impl.dar
|
||||
export 'src/connectors/billing/domain/repositories/billing_connector_repository.dart';
|
||||
export 'src/connectors/billing/data/repositories/billing_connector_repository_impl.dart';
|
||||
|
||||
// Export Home Connector
|
||||
export 'src/connectors/home/domain/repositories/home_connector_repository.dart';
|
||||
export 'src/connectors/home/data/repositories/home_connector_repository_impl.dart';
|
||||
|
||||
// Export Coverage Connector
|
||||
export 'src/connectors/coverage/domain/repositories/coverage_connector_repository.dart';
|
||||
export 'src/connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
|
||||
@@ -1,113 +0,0 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
||||
import 'package:firebase_data_connect/src/core/ref.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/home_connector_repository.dart';
|
||||
|
||||
/// Implementation of [HomeConnectorRepository].
|
||||
class HomeConnectorRepositoryImpl implements HomeConnectorRepository {
|
||||
HomeConnectorRepositoryImpl({
|
||||
dc.DataConnectService? service,
|
||||
}) : _service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@override
|
||||
Future<HomeDashboardData> getDashboardData({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final DateTime now = DateTime.now();
|
||||
final int daysFromMonday = now.weekday - DateTime.monday;
|
||||
final DateTime monday = DateTime(now.year, now.month, now.day).subtract(Duration(days: daysFromMonday));
|
||||
final DateTime weekRangeStart = monday;
|
||||
final DateTime weekRangeEnd = monday.add(const Duration(days: 13, hours: 23, minutes: 59, seconds: 59));
|
||||
|
||||
final QueryResult<dc.GetCompletedShiftsByBusinessIdData, dc.GetCompletedShiftsByBusinessIdVariables> completedResult = await _service.connector
|
||||
.getCompletedShiftsByBusinessId(
|
||||
businessId: businessId,
|
||||
dateFrom: _service.toTimestamp(weekRangeStart),
|
||||
dateTo: _service.toTimestamp(weekRangeEnd),
|
||||
)
|
||||
.execute();
|
||||
|
||||
double weeklySpending = 0.0;
|
||||
double next7DaysSpending = 0.0;
|
||||
int weeklyShifts = 0;
|
||||
int next7DaysScheduled = 0;
|
||||
|
||||
for (final dc.GetCompletedShiftsByBusinessIdShifts shift in completedResult.data.shifts) {
|
||||
final DateTime? shiftDate = _service.toDateTime(shift.date);
|
||||
if (shiftDate == null) continue;
|
||||
|
||||
final int offset = shiftDate.difference(weekRangeStart).inDays;
|
||||
if (offset < 0 || offset > 13) continue;
|
||||
|
||||
final double cost = shift.cost ?? 0.0;
|
||||
if (offset <= 6) {
|
||||
weeklySpending += cost;
|
||||
weeklyShifts += 1;
|
||||
} else {
|
||||
next7DaysSpending += cost;
|
||||
next7DaysScheduled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(now.year, now.month, now.day);
|
||||
final DateTime end = start.add(const Duration(hours: 23, minutes: 59, seconds: 59));
|
||||
|
||||
final QueryResult<dc.ListShiftRolesByBusinessAndDateRangeData, dc.ListShiftRolesByBusinessAndDateRangeVariables> result = await _service.connector
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
int totalNeeded = 0;
|
||||
int totalFilled = 0;
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole in result.data.shiftRoles) {
|
||||
totalNeeded += shiftRole.count;
|
||||
totalFilled += shiftRole.assigned ?? 0;
|
||||
}
|
||||
|
||||
return HomeDashboardData(
|
||||
weeklySpending: weeklySpending,
|
||||
next7DaysSpending: next7DaysSpending,
|
||||
weeklyShifts: weeklyShifts,
|
||||
next7DaysScheduled: next7DaysScheduled,
|
||||
totalNeeded: totalNeeded,
|
||||
totalFilled: totalFilled,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ReorderItem>> getRecentReorders({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start = now.subtract(const Duration(days: 30));
|
||||
|
||||
final QueryResult<dc.ListShiftRolesByBusinessDateRangeCompletedOrdersData, dc.ListShiftRolesByBusinessDateRangeCompletedOrdersVariables> result = await _service.connector
|
||||
.listShiftRolesByBusinessDateRangeCompletedOrders(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(now),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return result.data.shiftRoles.map((dc.ListShiftRolesByBusinessDateRangeCompletedOrdersShiftRoles shiftRole) {
|
||||
final String location = shiftRole.shift.location ?? shiftRole.shift.locationAddress ?? '';
|
||||
final String type = shiftRole.shift.order.orderType.stringValue;
|
||||
return ReorderItem(
|
||||
orderId: shiftRole.shift.order.id,
|
||||
title: '${shiftRole.role.name} - ${shiftRole.shift.title}',
|
||||
location: location,
|
||||
hourlyRate: shiftRole.role.costPerHour,
|
||||
hours: shiftRole.hours ?? 0,
|
||||
workers: shiftRole.count,
|
||||
type: type,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for home connector operations.
|
||||
///
|
||||
/// This acts as a buffer layer between the domain repository and the Data Connect SDK.
|
||||
abstract interface class HomeConnectorRepository {
|
||||
/// Fetches dashboard data for a business.
|
||||
Future<HomeDashboardData> getDashboardData({required String businessId});
|
||||
|
||||
/// Fetches recent reorder items for a business.
|
||||
Future<List<ReorderItem>> getRecentReorders({required String businessId});
|
||||
}
|
||||
@@ -10,9 +10,8 @@ import '../../domain/repositories/shifts_connector_repository.dart';
|
||||
/// Handles shift-related data operations by interacting with Data Connect.
|
||||
class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
/// Creates a new [ShiftsConnectorRepositoryImpl].
|
||||
ShiftsConnectorRepositoryImpl({
|
||||
dc.DataConnectService? service,
|
||||
}) : _service = service ?? dc.DataConnectService.instance;
|
||||
ShiftsConnectorRepositoryImpl({dc.DataConnectService? service})
|
||||
: _service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@@ -23,12 +22,17 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
required DateTime end,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service.connector
|
||||
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service
|
||||
.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(_service.toTimestamp(start))
|
||||
.dayEnd(_service.toTimestamp(end));
|
||||
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await query.execute();
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
response = await query.execute();
|
||||
return _mapApplicationsToShifts(response.data.applications);
|
||||
});
|
||||
}
|
||||
@@ -45,18 +49,28 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||
if (vendorId == null || vendorId.isEmpty) return <Shift>[];
|
||||
|
||||
final QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> response = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByVendorIdData,
|
||||
dc.ListShiftRolesByVendorIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.listShiftRolesByVendorId(vendorId: vendorId)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles = response.data.shiftRoles;
|
||||
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles =
|
||||
response.data.shiftRoles;
|
||||
|
||||
// Fetch current applications to filter out already booked shifts
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> myAppsResponse = await _service.connector
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
myAppsResponse = await _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
final Set<String> appliedShiftIds =
|
||||
myAppsResponse.data.applications.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId).toSet();
|
||||
final Set<String> appliedShiftIds = myAppsResponse.data.applications
|
||||
.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId)
|
||||
.toSet();
|
||||
|
||||
final List<Shift> mappedShifts = <Shift>[];
|
||||
for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) {
|
||||
@@ -67,6 +81,34 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
final DateTime? endDt = _service.toDateTime(sr.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
|
||||
|
||||
// Normalise orderType to uppercase for consistent checks in the UI.
|
||||
// RECURRING → groups shifts into Multi-Day cards.
|
||||
// PERMANENT → groups shifts into Long Term cards.
|
||||
final String orderTypeStr = sr.shift.order.orderType.stringValue
|
||||
.toUpperCase();
|
||||
|
||||
final dc.ListShiftRolesByVendorIdShiftRolesShiftOrder order =
|
||||
sr.shift.order;
|
||||
final DateTime? startDate = _service.toDateTime(order.startDate);
|
||||
final DateTime? endDate = _service.toDateTime(order.endDate);
|
||||
|
||||
final String startTime = startDt != null
|
||||
? DateFormat('HH:mm').format(startDt)
|
||||
: '';
|
||||
final String endTime = endDt != null
|
||||
? DateFormat('HH:mm').format(endDt)
|
||||
: '';
|
||||
|
||||
final List<ShiftSchedule>? schedules = _generateSchedules(
|
||||
orderType: orderTypeStr,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
recurringDays: order.recurringDays,
|
||||
permanentDays: order.permanentDays,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
);
|
||||
|
||||
mappedShifts.add(
|
||||
Shift(
|
||||
id: sr.shiftId,
|
||||
@@ -78,16 +120,25 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
location: sr.shift.location ?? '',
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
||||
description: sr.shift.description,
|
||||
durationDays: sr.shift.durationDays,
|
||||
durationDays: sr.shift.durationDays ?? schedules?.length,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
latitude: sr.shift.latitude,
|
||||
longitude: sr.shift.longitude,
|
||||
// orderId + orderType power the grouping and type-badge logic in
|
||||
// FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType.
|
||||
orderId: sr.shift.orderId,
|
||||
orderType: orderTypeStr,
|
||||
startDate: startDate?.toIso8601String(),
|
||||
endDate: endDate?.toIso8601String(),
|
||||
recurringDays: sr.shift.order.recurringDays,
|
||||
permanentDays: sr.shift.order.permanentDays,
|
||||
schedules: schedules,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: sr.isBreakPaid ?? false,
|
||||
breakTime: sr.breakType?.stringValue,
|
||||
@@ -125,7 +176,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
if (roleId != null && roleId.isNotEmpty) {
|
||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
|
||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
||||
roleResult = await _service.connector
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
|
||||
.execute();
|
||||
final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole;
|
||||
@@ -138,12 +190,21 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
bool hasApplied = false;
|
||||
String status = 'open';
|
||||
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
appsResponse = await _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
|
||||
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId)
|
||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
|
||||
.data
|
||||
.applications
|
||||
.where(
|
||||
(dc.GetApplicationsByStaffIdApplications a) =>
|
||||
a.shiftId == shiftId && a.shiftRole.roleId == roleId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (app != null) {
|
||||
@@ -181,7 +242,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
);
|
||||
}
|
||||
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result = await _service.connector.getShiftById(id: shiftId).execute();
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result =
|
||||
await _service.connector.getShiftById(id: shiftId).execute();
|
||||
final dc.GetShiftByIdShift? s = result.data.shift;
|
||||
if (s == null) return null;
|
||||
|
||||
@@ -190,17 +252,23 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
Break? breakInfo;
|
||||
|
||||
try {
|
||||
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByShiftIdData,
|
||||
dc.ListShiftRolesByShiftIdVariables
|
||||
>
|
||||
rolesRes = await _service.connector
|
||||
.listShiftRolesByShiftId(shiftId: shiftId)
|
||||
.execute();
|
||||
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
||||
required = 0;
|
||||
filled = 0;
|
||||
for (dc.ListShiftRolesByShiftIdShiftRoles r in rolesRes.data.shiftRoles) {
|
||||
for (dc.ListShiftRolesByShiftIdShiftRoles r
|
||||
in rolesRes.data.shiftRoles) {
|
||||
required = (required ?? 0) + r.count;
|
||||
filled = (filled ?? 0) + (r.assigned ?? 0);
|
||||
}
|
||||
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
|
||||
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
|
||||
rolesRes.data.shiftRoles.first;
|
||||
breakInfo = BreakAdapter.fromData(
|
||||
isPaid: firstRole.isBreakPaid ?? false,
|
||||
breakTime: firstRole.breakType?.stringValue,
|
||||
@@ -247,23 +315,114 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
final String targetRoleId = roleId ?? '';
|
||||
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
|
||||
|
||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
|
||||
// 1. Fetch the initial shift to determine order type
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
|
||||
shiftResult = await _service.connector
|
||||
.getShiftById(id: shiftId)
|
||||
.execute();
|
||||
final dc.GetShiftByIdShift? initialShift = shiftResult.data.shift;
|
||||
if (initialShift == null) throw Exception('Shift not found');
|
||||
|
||||
final dc.EnumValue<dc.OrderType> orderTypeEnum =
|
||||
initialShift.order.orderType;
|
||||
final bool isMultiDay =
|
||||
orderTypeEnum is dc.Known<dc.OrderType> &&
|
||||
(orderTypeEnum.value == dc.OrderType.RECURRING ||
|
||||
orderTypeEnum.value == dc.OrderType.PERMANENT);
|
||||
final List<_TargetShiftRole> targets = [];
|
||||
|
||||
if (isMultiDay) {
|
||||
// 2. Fetch all shifts for this order to apply to all of them for the same role
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndOrderData,
|
||||
dc.ListShiftRolesByBusinessAndOrderVariables
|
||||
>
|
||||
allRolesRes = await _service.connector
|
||||
.listShiftRolesByBusinessAndOrder(
|
||||
businessId: initialShift.order.businessId,
|
||||
orderId: initialShift.orderId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
for (final role in allRolesRes.data.shiftRoles) {
|
||||
if (role.roleId == targetRoleId) {
|
||||
targets.add(
|
||||
_TargetShiftRole(
|
||||
shiftId: role.shiftId,
|
||||
roleId: role.roleId,
|
||||
count: role.count,
|
||||
assigned: role.assigned ?? 0,
|
||||
shiftFilled: role.shift.filled ?? 0,
|
||||
date: _service.toDateTime(role.shift.date),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single shift application
|
||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
||||
roleResult = await _service.connector
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
|
||||
.execute();
|
||||
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
|
||||
if (role == null) throw Exception('Shift role not found');
|
||||
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult = await _service.connector.getShiftById(id: shiftId).execute();
|
||||
final dc.GetShiftByIdShift? shift = shiftResult.data.shift;
|
||||
if (shift == null) throw Exception('Shift not found');
|
||||
targets.add(
|
||||
_TargetShiftRole(
|
||||
shiftId: shiftId,
|
||||
roleId: targetRoleId,
|
||||
count: role.count,
|
||||
assigned: role.assigned ?? 0,
|
||||
shiftFilled: initialShift.filled ?? 0,
|
||||
date: _service.toDateTime(initialShift.date),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (targets.isEmpty) {
|
||||
throw Exception('No valid shifts found to apply for.');
|
||||
}
|
||||
|
||||
int appliedCount = 0;
|
||||
final List<String> errors = [];
|
||||
|
||||
for (final target in targets) {
|
||||
try {
|
||||
await _applyToSingleShiftRole(target: target, staffId: staffId);
|
||||
appliedCount++;
|
||||
} catch (e) {
|
||||
// For multi-shift apply, we might want to continue even if some fail due to conflicts
|
||||
if (targets.length == 1) rethrow;
|
||||
errors.add('Shift on ${target.date}: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedCount == 0 && targets.length > 1) {
|
||||
throw Exception('Failed to apply for any shifts: ${errors.join(", ")}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applyToSingleShiftRole({
|
||||
required _TargetShiftRole target,
|
||||
required String staffId,
|
||||
}) async {
|
||||
// Validate daily limit
|
||||
final DateTime? shiftDate = _service.toDateTime(shift.date);
|
||||
if (shiftDate != null) {
|
||||
final DateTime dayStartUtc = DateTime.utc(shiftDate.year, shiftDate.month, shiftDate.day);
|
||||
final DateTime dayEndUtc = dayStartUtc.add(const Duration(days: 1)).subtract(const Duration(microseconds: 1));
|
||||
if (target.date != null) {
|
||||
final DateTime dayStartUtc = DateTime.utc(
|
||||
target.date!.year,
|
||||
target.date!.month,
|
||||
target.date!.day,
|
||||
);
|
||||
final DateTime dayEndUtc = dayStartUtc
|
||||
.add(const Duration(days: 1))
|
||||
.subtract(const Duration(microseconds: 1));
|
||||
|
||||
final QueryResult<dc.VaidateDayStaffApplicationData, dc.VaidateDayStaffApplicationVariables> validationResponse = await _service.connector
|
||||
final QueryResult<
|
||||
dc.VaidateDayStaffApplicationData,
|
||||
dc.VaidateDayStaffApplicationVariables
|
||||
>
|
||||
validationResponse = await _service.connector
|
||||
.vaidateDayStaffApplication(staffId: staffId)
|
||||
.dayStart(_service.toTimestamp(dayStartUtc))
|
||||
.dayEnd(_service.toTimestamp(dayEndUtc))
|
||||
@@ -275,44 +434,52 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
}
|
||||
|
||||
// Check for existing application
|
||||
final QueryResult<dc.GetApplicationByStaffShiftAndRoleData, dc.GetApplicationByStaffShiftAndRoleVariables> existingAppRes = await _service.connector
|
||||
final QueryResult<
|
||||
dc.GetApplicationByStaffShiftAndRoleData,
|
||||
dc.GetApplicationByStaffShiftAndRoleVariables
|
||||
>
|
||||
existingAppRes = await _service.connector
|
||||
.getApplicationByStaffShiftAndRole(
|
||||
staffId: staffId,
|
||||
shiftId: shiftId,
|
||||
roleId: targetRoleId,
|
||||
shiftId: target.shiftId,
|
||||
roleId: target.roleId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
if (existingAppRes.data.applications.isNotEmpty) {
|
||||
throw Exception('Application already exists.');
|
||||
}
|
||||
|
||||
if ((role.assigned ?? 0) >= role.count) {
|
||||
if (target.assigned >= target.count) {
|
||||
throw Exception('This shift is full.');
|
||||
}
|
||||
|
||||
final int currentAssigned = role.assigned ?? 0;
|
||||
final int currentFilled = shift.filled ?? 0;
|
||||
|
||||
String? createdAppId;
|
||||
try {
|
||||
final OperationResult<dc.CreateApplicationData, dc.CreateApplicationVariables> createRes = await _service.connector.createApplication(
|
||||
shiftId: shiftId,
|
||||
final OperationResult<
|
||||
dc.CreateApplicationData,
|
||||
dc.CreateApplicationVariables
|
||||
>
|
||||
createRes = await _service.connector
|
||||
.createApplication(
|
||||
shiftId: target.shiftId,
|
||||
staffId: staffId,
|
||||
roleId: targetRoleId,
|
||||
status: dc.ApplicationStatus.CONFIRMED, // Matches existing logic
|
||||
roleId: target.roleId,
|
||||
status: dc.ApplicationStatus.CONFIRMED,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
).execute();
|
||||
)
|
||||
.execute();
|
||||
|
||||
createdAppId = createRes.data.application_insert.id;
|
||||
|
||||
await _service.connector
|
||||
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
|
||||
.assigned(currentAssigned + 1)
|
||||
.updateShiftRole(shiftId: target.shiftId, roleId: target.roleId)
|
||||
.assigned(target.assigned + 1)
|
||||
.execute();
|
||||
|
||||
await _service.connector
|
||||
.updateShift(id: shiftId)
|
||||
.filled(currentFilled + 1)
|
||||
.updateShift(id: target.shiftId)
|
||||
.filled(target.shiftFilled + 1)
|
||||
.execute();
|
||||
} catch (e) {
|
||||
// Simple rollback attempt (not guaranteed)
|
||||
@@ -321,15 +488,15 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> acceptShift({
|
||||
required String shiftId,
|
||||
required String staffId,
|
||||
}) {
|
||||
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.CONFIRMED);
|
||||
Future<void> acceptShift({required String shiftId, required String staffId}) {
|
||||
return _updateApplicationStatus(
|
||||
shiftId,
|
||||
staffId,
|
||||
dc.ApplicationStatus.CONFIRMED,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -337,7 +504,11 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
required String shiftId,
|
||||
required String staffId,
|
||||
}) {
|
||||
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.REJECTED);
|
||||
return _updateApplicationStatus(
|
||||
shiftId,
|
||||
staffId,
|
||||
dc.ApplicationStatus.REJECTED,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -351,14 +522,20 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
@override
|
||||
Future<List<Shift>> getHistoryShifts({required String staffId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListCompletedApplicationsByStaffIdData,
|
||||
dc.ListCompletedApplicationsByStaffIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.listCompletedApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final List<Shift> shifts = <Shift>[];
|
||||
for (final dc.ListCompletedApplicationsByStaffIdApplications app in response.data.applications) {
|
||||
for (final dc.ListCompletedApplicationsByStaffIdApplications app
|
||||
in response.data.applications) {
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
@@ -379,7 +556,9 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
startTime: startDt != null
|
||||
? DateFormat('HH:mm').format(startDt)
|
||||
: '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: 'completed', // Hardcoded as checked out implies completion
|
||||
@@ -406,7 +585,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
List<Shift> _mapApplicationsToShifts(List<dynamic> apps) {
|
||||
return apps.map((app) {
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
@@ -479,12 +659,20 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
) async {
|
||||
return _service.run(() async {
|
||||
// First try to find the application
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
appsResponse = await _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
|
||||
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId)
|
||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
|
||||
.data
|
||||
.applications
|
||||
.where(
|
||||
(dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (app != null) {
|
||||
@@ -494,24 +682,116 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
.execute();
|
||||
} else if (newStatus == dc.ApplicationStatus.REJECTED) {
|
||||
// If declining but no app found, create a rejected application
|
||||
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByShiftIdData,
|
||||
dc.ListShiftRolesByShiftIdVariables
|
||||
>
|
||||
rolesRes = await _service.connector
|
||||
.listShiftRolesByShiftId(shiftId: shiftId)
|
||||
.execute();
|
||||
|
||||
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
||||
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
|
||||
await _service.connector.createApplication(
|
||||
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
|
||||
rolesRes.data.shiftRoles.first;
|
||||
await _service.connector
|
||||
.createApplication(
|
||||
shiftId: shiftId,
|
||||
staffId: staffId,
|
||||
roleId: firstRole.id,
|
||||
status: dc.ApplicationStatus.REJECTED,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
).execute();
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
} else {
|
||||
throw Exception("Application not found for shift $shiftId");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates a list of [ShiftSchedule] for RECURRING or PERMANENT orders.
|
||||
List<ShiftSchedule>? _generateSchedules({
|
||||
required String orderType,
|
||||
required DateTime? startDate,
|
||||
required DateTime? endDate,
|
||||
required List<String>? recurringDays,
|
||||
required List<String>? permanentDays,
|
||||
required String startTime,
|
||||
required String endTime,
|
||||
}) {
|
||||
if (orderType != 'RECURRING' && orderType != 'PERMANENT') return null;
|
||||
if (startDate == null || endDate == null) return null;
|
||||
|
||||
final List<String>? daysToInclude = orderType == 'RECURRING'
|
||||
? recurringDays
|
||||
: permanentDays;
|
||||
if (daysToInclude == null || daysToInclude.isEmpty) return null;
|
||||
|
||||
final List<ShiftSchedule> schedules = <ShiftSchedule>[];
|
||||
final Set<int> targetWeekdayIndex = daysToInclude
|
||||
.map((String day) {
|
||||
switch (day.toUpperCase()) {
|
||||
case 'MONDAY':
|
||||
return DateTime.monday;
|
||||
case 'TUESDAY':
|
||||
return DateTime.tuesday;
|
||||
case 'WEDNESDAY':
|
||||
return DateTime.wednesday;
|
||||
case 'THURSDAY':
|
||||
return DateTime.thursday;
|
||||
case 'FRIDAY':
|
||||
return DateTime.friday;
|
||||
case 'SATURDAY':
|
||||
return DateTime.saturday;
|
||||
case 'SUNDAY':
|
||||
return DateTime.sunday;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
})
|
||||
.where((int idx) => idx != -1)
|
||||
.toSet();
|
||||
|
||||
DateTime current = startDate;
|
||||
while (current.isBefore(endDate) ||
|
||||
current.isAtSameMomentAs(endDate) ||
|
||||
// Handle cases where the time component might differ slightly by checking date equality
|
||||
(current.year == endDate.year &&
|
||||
current.month == endDate.month &&
|
||||
current.day == endDate.day)) {
|
||||
if (targetWeekdayIndex.contains(current.weekday)) {
|
||||
schedules.add(
|
||||
ShiftSchedule(
|
||||
date: current.toIso8601String(),
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
current = current.add(const Duration(days: 1));
|
||||
|
||||
// Safety break to prevent infinite loops if dates are messed up
|
||||
if (schedules.length > 365) break;
|
||||
}
|
||||
|
||||
return schedules;
|
||||
}
|
||||
}
|
||||
|
||||
class _TargetShiftRole {
|
||||
final String shiftId;
|
||||
final String roleId;
|
||||
final int count;
|
||||
final int assigned;
|
||||
final int shiftFilled;
|
||||
final DateTime? date;
|
||||
|
||||
_TargetShiftRole({
|
||||
required this.shiftId,
|
||||
required this.roleId,
|
||||
required this.count,
|
||||
required this.assigned,
|
||||
required this.shiftFilled,
|
||||
this.date,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import 'connectors/hubs/domain/repositories/hubs_connector_repository.dart';
|
||||
import 'connectors/hubs/data/repositories/hubs_connector_repository_impl.dart';
|
||||
import 'connectors/billing/domain/repositories/billing_connector_repository.dart';
|
||||
import 'connectors/billing/data/repositories/billing_connector_repository_impl.dart';
|
||||
import 'connectors/home/domain/repositories/home_connector_repository.dart';
|
||||
import 'connectors/home/data/repositories/home_connector_repository_impl.dart';
|
||||
import 'connectors/coverage/domain/repositories/coverage_connector_repository.dart';
|
||||
import 'connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
|
||||
import 'services/data_connect_service.dart';
|
||||
@@ -32,9 +30,6 @@ class DataConnectModule extends Module {
|
||||
i.addLazySingleton<BillingConnectorRepository>(
|
||||
BillingConnectorRepositoryImpl.new,
|
||||
);
|
||||
i.addLazySingleton<HomeConnectorRepository>(
|
||||
HomeConnectorRepositoryImpl.new,
|
||||
);
|
||||
i.addLazySingleton<CoverageConnectorRepository>(
|
||||
CoverageConnectorRepositoryImpl.new,
|
||||
);
|
||||
|
||||
@@ -13,8 +13,6 @@ import '../connectors/hubs/domain/repositories/hubs_connector_repository.dart';
|
||||
import '../connectors/hubs/data/repositories/hubs_connector_repository_impl.dart';
|
||||
import '../connectors/billing/domain/repositories/billing_connector_repository.dart';
|
||||
import '../connectors/billing/data/repositories/billing_connector_repository_impl.dart';
|
||||
import '../connectors/home/domain/repositories/home_connector_repository.dart';
|
||||
import '../connectors/home/data/repositories/home_connector_repository_impl.dart';
|
||||
import '../connectors/coverage/domain/repositories/coverage_connector_repository.dart';
|
||||
import '../connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
|
||||
import '../connectors/staff/domain/repositories/staff_connector_repository.dart';
|
||||
@@ -39,7 +37,6 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
ShiftsConnectorRepository? _shiftsRepository;
|
||||
HubsConnectorRepository? _hubsRepository;
|
||||
BillingConnectorRepository? _billingRepository;
|
||||
HomeConnectorRepository? _homeRepository;
|
||||
CoverageConnectorRepository? _coverageRepository;
|
||||
StaffConnectorRepository? _staffRepository;
|
||||
|
||||
@@ -63,14 +60,11 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
return _billingRepository ??= BillingConnectorRepositoryImpl(service: this);
|
||||
}
|
||||
|
||||
/// Gets the home connector repository.
|
||||
HomeConnectorRepository getHomeRepository() {
|
||||
return _homeRepository ??= HomeConnectorRepositoryImpl(service: this);
|
||||
}
|
||||
|
||||
/// Gets the coverage connector repository.
|
||||
CoverageConnectorRepository getCoverageRepository() {
|
||||
return _coverageRepository ??= CoverageConnectorRepositoryImpl(service: this);
|
||||
return _coverageRepository ??= CoverageConnectorRepositoryImpl(
|
||||
service: this,
|
||||
);
|
||||
}
|
||||
|
||||
/// Gets the staff connector repository.
|
||||
@@ -84,14 +78,14 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
|
||||
/// Helper to get the current staff ID from the session.
|
||||
Future<String> getStaffId() async {
|
||||
String? staffId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||
String? staffId = dc.StaffSessionStore.instance.session?.staff?.id;
|
||||
|
||||
if (staffId == null || staffId.isEmpty) {
|
||||
// Attempt to recover session if user is signed in
|
||||
final user = auth.currentUser;
|
||||
if (user != null) {
|
||||
await _loadSession(user.uid);
|
||||
staffId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||
staffId = dc.StaffSessionStore.instance.session?.staff?.id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +122,14 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
|
||||
// Load Staff Session if applicable
|
||||
if (role == 'STAFF' || role == 'BOTH') {
|
||||
final response = await connector.getStaffByUserId(userId: userId).execute();
|
||||
final response = await connector
|
||||
.getStaffByUserId(userId: userId)
|
||||
.execute();
|
||||
if (response.data.staffs.isNotEmpty) {
|
||||
final s = response.data.staffs.first;
|
||||
dc.StaffSessionStore.instance.setSession(
|
||||
dc.StaffSession(
|
||||
ownerId: s.id,
|
||||
ownerId: s.ownerId,
|
||||
staff: domain.Staff(
|
||||
id: s.id,
|
||||
authProviderId: s.userId,
|
||||
@@ -151,7 +147,9 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
|
||||
// Load Client Session if applicable
|
||||
if (role == 'BUSINESS' || role == 'BOTH') {
|
||||
final response = await connector.getBusinessesByUserId(userId: userId).execute();
|
||||
final response = await connector
|
||||
.getBusinessesByUserId(userId: userId)
|
||||
.execute();
|
||||
if (response.data.businesses.isNotEmpty) {
|
||||
final b = response.data.businesses.first;
|
||||
dc.ClientSessionStore.instance.setSession(
|
||||
@@ -173,16 +171,23 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a Data Connect [Timestamp] to a Dart [DateTime].
|
||||
/// Converts a Data Connect [Timestamp] to a Dart [DateTime] in local time.
|
||||
///
|
||||
/// Firebase Data Connect always stores and returns timestamps in UTC.
|
||||
/// Calling [toLocal] ensures the result reflects the device's timezone so
|
||||
/// that shift dates, start/end times, and formatted strings are correct for
|
||||
/// the end user.
|
||||
DateTime? toDateTime(dynamic timestamp) {
|
||||
if (timestamp == null) return null;
|
||||
if (timestamp is fdc.Timestamp) {
|
||||
return timestamp.toDateTime();
|
||||
return timestamp.toDateTime().toLocal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Converts a Dart [DateTime] to a Data Connect [Timestamp].
|
||||
///
|
||||
/// Converts the [DateTime] to UTC before creating the [Timestamp].
|
||||
fdc.Timestamp toTimestamp(DateTime dateTime) {
|
||||
final DateTime utc = dateTime.toUtc();
|
||||
final int millis = utc.millisecondsSinceEpoch;
|
||||
@@ -225,7 +230,6 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
||||
_shiftsRepository = null;
|
||||
_hubsRepository = null;
|
||||
_billingRepository = null;
|
||||
_homeRepository = null;
|
||||
_coverageRepository = null;
|
||||
_staffRepository = null;
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ class UiIcons {
|
||||
/// Calendar icon for shifts or schedules
|
||||
static const IconData calendar = _IconLib.calendar;
|
||||
|
||||
/// Calender check icon for shifts or schedules
|
||||
static const IconData calendarCheck = _IconLib.calendarCheck;
|
||||
|
||||
/// Briefcase icon for jobs
|
||||
static const IconData briefcase = _IconLib.briefcase;
|
||||
|
||||
|
||||
@@ -221,6 +221,14 @@ class UiTypography {
|
||||
color: UiColors.textPrimary,
|
||||
);
|
||||
|
||||
/// Headline 4 Bold - Font: Instrument Sans, Size: 20, Height: 1.5 (#121826)
|
||||
static final TextStyle headline4b = _primaryBase.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
height: 1.5,
|
||||
color: UiColors.textPrimary,
|
||||
);
|
||||
|
||||
/// Headline 5 Regular - Font: Instrument Sans, Size: 18, Height: 1.5 (#121826)
|
||||
static final TextStyle headline5r = _primaryBase.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:design_system/src/ui_typography.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../ui_icons.dart';
|
||||
import 'ui_icon_button.dart';
|
||||
|
||||
/// A custom AppBar for the Krow UI design system.
|
||||
///
|
||||
/// This widget provides a consistent look and feel for top app bars across the application.
|
||||
class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
const UiAppBar({
|
||||
super.key,
|
||||
this.title,
|
||||
@@ -14,11 +16,12 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
this.leading,
|
||||
this.actions,
|
||||
this.height = kToolbarHeight,
|
||||
this.centerTitle = true,
|
||||
this.centerTitle = false,
|
||||
this.onLeadingPressed,
|
||||
this.showBackButton = true,
|
||||
this.bottom,
|
||||
});
|
||||
|
||||
/// The title text to display in the app bar.
|
||||
final String? title;
|
||||
|
||||
@@ -52,17 +55,19 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: titleWidget ??
|
||||
(title != null
|
||||
? Text(
|
||||
title!,
|
||||
)
|
||||
: null),
|
||||
leading: leading ??
|
||||
title:
|
||||
titleWidget ??
|
||||
(title != null ? Text(title!, style: UiTypography.headline4b) : null),
|
||||
leading:
|
||||
leading ??
|
||||
(showBackButton
|
||||
? IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, size: 20),
|
||||
onPressed: onLeadingPressed ?? () => Navigator.of(context).pop(),
|
||||
? UiIconButton(
|
||||
icon: UiIcons.chevronLeft,
|
||||
onTap: onLeadingPressed ?? () => Navigator.of(context).pop(),
|
||||
backgroundColor: UiColors.transparent,
|
||||
iconColor: UiColors.iconThird,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
)
|
||||
: null),
|
||||
actions: actions,
|
||||
@@ -72,5 +77,6 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(height + (bottom?.preferredSize.height ?? 0.0));
|
||||
Size get preferredSize =>
|
||||
Size.fromHeight(height + (bottom?.preferredSize.height ?? 0.0));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ enum UiChipVariant {
|
||||
|
||||
/// A custom chip widget with supports for different sizes, themes, and icons.
|
||||
class UiChip extends StatelessWidget {
|
||||
|
||||
/// Creates a [UiChip].
|
||||
const UiChip({
|
||||
super.key,
|
||||
@@ -42,6 +41,7 @@ class UiChip extends StatelessWidget {
|
||||
this.onTrailingIconTap,
|
||||
this.isSelected = false,
|
||||
});
|
||||
|
||||
/// The text label to display.
|
||||
final String label;
|
||||
|
||||
@@ -99,7 +99,7 @@ class UiChip extends StatelessWidget {
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: _getBorder(),
|
||||
),
|
||||
child: content,
|
||||
|
||||
@@ -16,6 +16,8 @@ class UiIconButton extends StatelessWidget {
|
||||
required this.iconColor,
|
||||
this.useBlur = false,
|
||||
this.onTap,
|
||||
this.shape = BoxShape.circle,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
/// Creates a primary variant icon button with solid background.
|
||||
@@ -25,6 +27,8 @@ class UiIconButton extends StatelessWidget {
|
||||
this.size = 40,
|
||||
this.iconSize = 20,
|
||||
this.onTap,
|
||||
this.shape = BoxShape.circle,
|
||||
this.borderRadius,
|
||||
}) : backgroundColor = UiColors.primary,
|
||||
iconColor = UiColors.white,
|
||||
useBlur = false;
|
||||
@@ -36,6 +40,8 @@ class UiIconButton extends StatelessWidget {
|
||||
this.size = 40,
|
||||
this.iconSize = 20,
|
||||
this.onTap,
|
||||
this.shape = BoxShape.circle,
|
||||
this.borderRadius,
|
||||
}) : backgroundColor = UiColors.primary.withAlpha(96),
|
||||
iconColor = UiColors.primary,
|
||||
useBlur = true;
|
||||
@@ -60,13 +66,23 @@ class UiIconButton extends StatelessWidget {
|
||||
/// Callback when the button is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// The shape of the button (circle or rectangle).
|
||||
final BoxShape shape;
|
||||
|
||||
/// The border radius for rectangle shape.
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
@override
|
||||
/// Builds the icon button UI.
|
||||
Widget build(BuildContext context) {
|
||||
final Widget button = Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
shape: shape,
|
||||
borderRadius: shape == BoxShape.rectangle ? borderRadius : null,
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: iconSize),
|
||||
);
|
||||
|
||||
|
||||
@@ -34,14 +34,15 @@ export 'src/entities/shifts/break/break.dart';
|
||||
export 'src/adapters/shifts/break/break_adapter.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';
|
||||
export 'src/entities/orders/recurring_order.dart';
|
||||
export 'src/entities/orders/recurring_order_position.dart';
|
||||
export 'src/entities/orders/permanent_order.dart';
|
||||
export 'src/entities/orders/permanent_order_position.dart';
|
||||
export 'src/entities/orders/order_type.dart';
|
||||
export 'src/entities/orders/order_item.dart';
|
||||
export 'src/entities/orders/reorder_data.dart';
|
||||
|
||||
// Skills & Certs
|
||||
export 'src/entities/skills/skill.dart';
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Summary of a completed shift role used for reorder suggestions.
|
||||
/// Summary of a completed order used for reorder suggestions.
|
||||
class ReorderItem extends Equatable {
|
||||
const ReorderItem({
|
||||
required this.orderId,
|
||||
required this.title,
|
||||
required this.location,
|
||||
required this.hourlyRate,
|
||||
required this.hours,
|
||||
required this.totalCost,
|
||||
required this.workers,
|
||||
required this.type,
|
||||
this.hourlyRate = 0,
|
||||
this.hours = 0,
|
||||
});
|
||||
|
||||
/// Parent order id for the completed shift.
|
||||
/// Unique identifier of the order.
|
||||
final String orderId;
|
||||
|
||||
/// Display title (role + shift title).
|
||||
/// Display title of the order (e.g., event name or first shift title).
|
||||
final String title;
|
||||
|
||||
/// Location from the shift.
|
||||
/// Location of the order (e.g., first shift location).
|
||||
final String location;
|
||||
|
||||
/// Hourly rate from the role.
|
||||
final double hourlyRate;
|
||||
/// Total calculated cost for the order.
|
||||
final double totalCost;
|
||||
|
||||
/// Total hours for the shift role.
|
||||
final double hours;
|
||||
|
||||
/// Worker count for the shift role.
|
||||
/// Total number of workers required for the order.
|
||||
final int workers;
|
||||
|
||||
/// Order type (e.g., ONE_TIME).
|
||||
/// The type of order (e.g., ONE_TIME, RECURRING).
|
||||
final String type;
|
||||
|
||||
/// Average or primary hourly rate (optional, for display).
|
||||
final double hourlyRate;
|
||||
|
||||
/// Total hours for the order (optional, for display).
|
||||
final double hours;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
orderId,
|
||||
title,
|
||||
location,
|
||||
hourlyRate,
|
||||
hours,
|
||||
totalCost,
|
||||
workers,
|
||||
type,
|
||||
hourlyRate,
|
||||
hours,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'order_type.dart';
|
||||
|
||||
/// Represents a customer's view of an order or shift.
|
||||
///
|
||||
/// This entity captures the details necessary for the dashboard/view orders screen,
|
||||
@@ -9,6 +11,7 @@ class OrderItem extends Equatable {
|
||||
const OrderItem({
|
||||
required this.id,
|
||||
required this.orderId,
|
||||
required this.orderType,
|
||||
required this.title,
|
||||
required this.clientName,
|
||||
required this.status,
|
||||
@@ -20,6 +23,7 @@ class OrderItem extends Equatable {
|
||||
required this.filled,
|
||||
required this.workersNeeded,
|
||||
required this.hourlyRate,
|
||||
required this.eventName,
|
||||
this.hours = 0,
|
||||
this.totalValue = 0,
|
||||
this.confirmedApps = const <Map<String, dynamic>>[],
|
||||
@@ -31,6 +35,9 @@ class OrderItem extends Equatable {
|
||||
/// Parent order identifier.
|
||||
final String orderId;
|
||||
|
||||
/// The type of order (e.g., ONE_TIME, PERMANENT).
|
||||
final OrderType orderType;
|
||||
|
||||
/// Title or name of the role.
|
||||
final String title;
|
||||
|
||||
@@ -70,6 +77,9 @@ class OrderItem extends Equatable {
|
||||
/// Total value for the shift role.
|
||||
final double totalValue;
|
||||
|
||||
/// Name of the event.
|
||||
final String eventName;
|
||||
|
||||
/// List of confirmed worker applications.
|
||||
final List<Map<String, dynamic>> confirmedApps;
|
||||
|
||||
@@ -77,6 +87,7 @@ class OrderItem extends Equatable {
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
orderId,
|
||||
orderType,
|
||||
title,
|
||||
clientName,
|
||||
status,
|
||||
@@ -90,6 +101,7 @@ class OrderItem extends Equatable {
|
||||
hourlyRate,
|
||||
hours,
|
||||
totalValue,
|
||||
eventName,
|
||||
confirmedApps,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
/// Defines the type of an order.
|
||||
enum OrderType {
|
||||
/// A single occurrence shift.
|
||||
oneTime,
|
||||
|
||||
/// 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 {
|
||||
/// A long-term or permanent staffing position.
|
||||
permanent,
|
||||
|
||||
const OrderType({
|
||||
required this.id,
|
||||
required this.titleKey,
|
||||
required this.descriptionKey,
|
||||
});
|
||||
/// Unique identifier for the order type.
|
||||
final String id;
|
||||
/// Shifts that repeat on a defined schedule.
|
||||
recurring,
|
||||
|
||||
/// Translation key for the title.
|
||||
final String titleKey;
|
||||
/// A quickly created shift.
|
||||
rapid;
|
||||
|
||||
/// Translation key for the description.
|
||||
final String descriptionKey;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, titleKey, descriptionKey];
|
||||
/// Creates an [OrderType] from a string value (typically from the backend).
|
||||
static OrderType fromString(String value) {
|
||||
switch (value.toUpperCase()) {
|
||||
case 'ONE_TIME':
|
||||
return OrderType.oneTime;
|
||||
case 'PERMANENT':
|
||||
return OrderType.permanent;
|
||||
case 'RECURRING':
|
||||
return OrderType.recurring;
|
||||
case 'RAPID':
|
||||
return OrderType.rapid;
|
||||
default:
|
||||
return OrderType.oneTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'one_time_order.dart';
|
||||
import 'order_type.dart';
|
||||
|
||||
/// Represents the full details of an order retrieved for reordering.
|
||||
class ReorderData extends Equatable {
|
||||
const ReorderData({
|
||||
required this.orderId,
|
||||
required this.orderType,
|
||||
required this.eventName,
|
||||
required this.vendorId,
|
||||
required this.hub,
|
||||
required this.positions,
|
||||
this.date,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.recurringDays = const <String>[],
|
||||
this.permanentDays = const <String>[],
|
||||
});
|
||||
|
||||
final String orderId;
|
||||
final OrderType orderType;
|
||||
final String eventName;
|
||||
final String? vendorId;
|
||||
final OneTimeOrderHubDetails hub;
|
||||
final List<ReorderPosition> positions;
|
||||
|
||||
// One-time specific
|
||||
final DateTime? date;
|
||||
|
||||
// Recurring/Permanent specific
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final List<String> recurringDays;
|
||||
final List<String> permanentDays;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
orderId,
|
||||
orderType,
|
||||
eventName,
|
||||
vendorId,
|
||||
hub,
|
||||
positions,
|
||||
date,
|
||||
startDate,
|
||||
endDate,
|
||||
recurringDays,
|
||||
permanentDays,
|
||||
];
|
||||
}
|
||||
|
||||
class ReorderPosition extends Equatable {
|
||||
const ReorderPosition({
|
||||
required this.roleId,
|
||||
required this.count,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.lunchBreak = 'NO_BREAK',
|
||||
});
|
||||
|
||||
final String roleId;
|
||||
final int count;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final String lunchBreak;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
roleId,
|
||||
count,
|
||||
startTime,
|
||||
endTime,
|
||||
lunchBreak,
|
||||
];
|
||||
}
|
||||
@@ -34,6 +34,10 @@ class Shift extends Equatable {
|
||||
this.breakInfo,
|
||||
this.orderId,
|
||||
this.orderType,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
this.recurringDays,
|
||||
this.permanentDays,
|
||||
this.schedules,
|
||||
});
|
||||
|
||||
@@ -68,6 +72,10 @@ class Shift extends Equatable {
|
||||
final Break? breakInfo;
|
||||
final String? orderId;
|
||||
final String? orderType;
|
||||
final String? startDate;
|
||||
final String? endDate;
|
||||
final List<String>? recurringDays;
|
||||
final List<String>? permanentDays;
|
||||
final List<ShiftSchedule>? schedules;
|
||||
|
||||
@override
|
||||
@@ -103,6 +111,10 @@ class Shift extends Equatable {
|
||||
breakInfo,
|
||||
orderId,
|
||||
orderType,
|
||||
startDate,
|
||||
endDate,
|
||||
recurringDays,
|
||||
permanentDays,
|
||||
schedules,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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 fetches the list of supported staffing order types
|
||||
/// from the [ClientCreateOrderRepositoryInterface].
|
||||
class GetOrderTypesUseCase implements NoInputUseCase<List<OrderType>> {
|
||||
/// Creates a [GetOrderTypesUseCase].
|
||||
///
|
||||
/// Requires a [ClientCreateOrderRepositoryInterface] to interact with the data layer.
|
||||
const GetOrderTypesUseCase(this._repository);
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<OrderType>> call() {
|
||||
return _repository.getOrderTypes();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.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<ClientCreateOrderEvent, ClientCreateOrderState>
|
||||
with BlocErrorHandler<ClientCreateOrderState> {
|
||||
ClientCreateOrderBloc(this._getOrderTypesUseCase)
|
||||
: super(const ClientCreateOrderInitial()) {
|
||||
on<ClientCreateOrderTypesRequested>(_onTypesRequested);
|
||||
}
|
||||
final GetOrderTypesUseCase _getOrderTypesUseCase;
|
||||
|
||||
Future<void> _onTypesRequested(
|
||||
ClientCreateOrderTypesRequested event,
|
||||
Emitter<ClientCreateOrderState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final List<OrderType> types = await _getOrderTypesUseCase();
|
||||
emit(ClientCreateOrderLoadSuccess(types));
|
||||
},
|
||||
onError: (String errorKey) => ClientCreateOrderLoadFailure(errorKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class ClientCreateOrderEvent extends Equatable {
|
||||
const ClientCreateOrderEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class ClientCreateOrderTypesRequested extends ClientCreateOrderEvent {
|
||||
const ClientCreateOrderTypesRequested();
|
||||
}
|
||||
|
||||
class ClientCreateOrderTypeSelected extends ClientCreateOrderEvent {
|
||||
const ClientCreateOrderTypeSelected(this.typeId);
|
||||
final String typeId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[typeId];
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the [ClientCreateOrderBloc].
|
||||
abstract class ClientCreateOrderState extends Equatable {
|
||||
const ClientCreateOrderState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
const ClientCreateOrderLoadSuccess(this.orderTypes);
|
||||
|
||||
/// The list of available order types retrieved from the domain.
|
||||
final List<OrderType> orderTypes;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[orderTypes];
|
||||
}
|
||||
|
||||
/// State representing a failure to load order types.
|
||||
class ClientCreateOrderLoadFailure extends ClientCreateOrderState {
|
||||
const ClientCreateOrderLoadFailure(this.error);
|
||||
|
||||
final String error;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[error];
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import '../blocs/client_create_order_bloc.dart';
|
||||
import '../blocs/client_create_order_event.dart';
|
||||
import '../widgets/create_order/create_order_view.dart';
|
||||
|
||||
/// Main entry page for the client create order flow.
|
||||
///
|
||||
/// This page initializes the [ClientCreateOrderBloc] and displays the [CreateOrderView].
|
||||
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
|
||||
/// delegating its state and UI to other components.
|
||||
class ClientCreateOrderPage extends StatelessWidget {
|
||||
/// Creates a [ClientCreateOrderPage].
|
||||
const ClientCreateOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ClientCreateOrderBloc>(
|
||||
create: (BuildContext context) =>
|
||||
Modular.get<ClientCreateOrderBloc>()
|
||||
..add(const ClientCreateOrderTypesRequested()),
|
||||
child: const CreateOrderView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import '../blocs/one_time_order_bloc.dart';
|
||||
import '../widgets/one_time_order/one_time_order_view.dart';
|
||||
|
||||
/// Page for creating a one-time staffing order.
|
||||
/// Users can specify the date, location, and multiple staff positions required.
|
||||
///
|
||||
/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView].
|
||||
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
|
||||
/// delegating its state and UI to other components.
|
||||
class OneTimeOrderPage extends StatelessWidget {
|
||||
/// Creates a [OneTimeOrderPage].
|
||||
const OneTimeOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<OneTimeOrderBloc>(
|
||||
create: (BuildContext context) => Modular.get<OneTimeOrderBloc>(),
|
||||
child: const OneTimeOrderView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import '../blocs/permanent_order_bloc.dart';
|
||||
import '../widgets/permanent_order/permanent_order_view.dart';
|
||||
|
||||
/// Page for creating a permanent staffing order.
|
||||
class PermanentOrderPage extends StatelessWidget {
|
||||
/// Creates a [PermanentOrderPage].
|
||||
const PermanentOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<PermanentOrderBloc>(
|
||||
create: (BuildContext context) => Modular.get<PermanentOrderBloc>(),
|
||||
child: const PermanentOrderView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import '../blocs/recurring_order_bloc.dart';
|
||||
import '../widgets/recurring_order/recurring_order_view.dart';
|
||||
|
||||
/// Page for creating a recurring staffing order.
|
||||
class RecurringOrderPage extends StatelessWidget {
|
||||
/// Creates a [RecurringOrderPage].
|
||||
const RecurringOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RecurringOrderBloc>(
|
||||
create: (BuildContext context) => Modular.get<RecurringOrderBloc>(),
|
||||
child: const RecurringOrderView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
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_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../blocs/client_create_order_bloc.dart';
|
||||
import '../../blocs/client_create_order_state.dart';
|
||||
import '../../ui_entities/order_type_ui_metadata.dart';
|
||||
import '../order_type_card.dart';
|
||||
|
||||
/// 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') {
|
||||
return t.client_create_order.types.rapid_desc;
|
||||
} else if (key == 'client_create_order.types.one_time') {
|
||||
return t.client_create_order.types.one_time;
|
||||
} else if (key == 'client_create_order.types.one_time_desc') {
|
||||
return t.client_create_order.types.one_time_desc;
|
||||
} else if (key == 'client_create_order.types.recurring') {
|
||||
return t.client_create_order.types.recurring;
|
||||
} else if (key == 'client_create_order.types.recurring_desc') {
|
||||
return t.client_create_order.types.recurring_desc;
|
||||
} else if (key == 'client_create_order.types.permanent') {
|
||||
return t.client_create_order.types.permanent;
|
||||
} else if (key == 'client_create_order.types.permanent_desc') {
|
||||
return t.client_create_order.types.permanent_desc;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/// The main content of the Create Order page.
|
||||
class CreateOrderView extends StatelessWidget {
|
||||
/// Creates a [CreateOrderView].
|
||||
const CreateOrderView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
title: t.client_create_order.title,
|
||||
onLeadingPressed: () => Modular.to.toClientHome(),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space6),
|
||||
child: Text(
|
||||
t.client_create_order.section_title,
|
||||
style: UiTypography.footnote1m.copyWith(
|
||||
color: UiColors.textDescription,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<ClientCreateOrderBloc, ClientCreateOrderState>(
|
||||
builder:
|
||||
(BuildContext context, ClientCreateOrderState state) {
|
||||
if (state is ClientCreateOrderLoadSuccess) {
|
||||
return GridView.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: UiConstants.space4,
|
||||
crossAxisSpacing: UiConstants.space4,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: state.orderTypes.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
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(
|
||||
key: type.descriptionKey,
|
||||
),
|
||||
backgroundColor: ui.backgroundColor,
|
||||
borderColor: ui.borderColor,
|
||||
iconBackgroundColor: ui.iconBackgroundColor,
|
||||
iconColor: ui.iconColor,
|
||||
textColor: ui.textColor,
|
||||
descriptionColor: ui.descriptionColor,
|
||||
onTap: () {
|
||||
switch (type.id) {
|
||||
case 'rapid':
|
||||
Modular.to.toCreateOrderRapid();
|
||||
break;
|
||||
case 'one-time':
|
||||
Modular.to.toCreateOrderOneTime();
|
||||
break;
|
||||
case 'recurring':
|
||||
Modular.to.toCreateOrderRecurring();
|
||||
break;
|
||||
case 'permanent':
|
||||
Modular.to.toCreateOrderPermanent();
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
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_core/core.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 'one_time_order_date_picker.dart';
|
||||
import 'one_time_order_event_name_input.dart';
|
||||
import 'one_time_order_header.dart';
|
||||
import 'one_time_order_position_card.dart';
|
||||
import 'one_time_order_section_header.dart';
|
||||
import 'one_time_order_success_view.dart';
|
||||
|
||||
/// The main content of the One-Time Order page.
|
||||
class OneTimeOrderView extends StatelessWidget {
|
||||
/// Creates a [OneTimeOrderView].
|
||||
const OneTimeOrderView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsClientCreateOrderOneTimeEn labels =
|
||||
t.client_create_order.one_time;
|
||||
|
||||
return BlocConsumer<OneTimeOrderBloc, OneTimeOrderState>(
|
||||
listener: (BuildContext context, OneTimeOrderState state) {
|
||||
if (state.status == OneTimeOrderStatus.failure &&
|
||||
state.errorMessage != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage!),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, OneTimeOrderState state) {
|
||||
if (state.status == OneTimeOrderStatus.success) {
|
||||
return OneTimeOrderSuccessView(
|
||||
title: labels.success_title,
|
||||
message: labels.success_message,
|
||||
buttonLabel: labels.back_to_orders,
|
||||
onDone: () => Modular.to.pushNamedAndRemoveUntil(
|
||||
ClientPaths.orders,
|
||||
(_) => false,
|
||||
arguments: <String, dynamic>{
|
||||
'initialDate': state.date.toIso8601String(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.vendors.isEmpty &&
|
||||
state.status != OneTimeOrderStatus.loading) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
OneTimeOrderHeader(
|
||||
title: labels.title,
|
||||
subtitle: labels.subtitle,
|
||||
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.search,
|
||||
size: 64,
|
||||
color: UiColors.iconInactive,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'No Vendors Available',
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'There are no staffing vendors associated with your account.',
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
OneTimeOrderHeader(
|
||||
title: labels.title,
|
||||
subtitle: labels.subtitle,
|
||||
onBack: () => Modular.to.navigate(ClientPaths.createOrder),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
_OneTimeOrderForm(state: state),
|
||||
if (state.status == OneTimeOrderStatus.loading)
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
),
|
||||
),
|
||||
_BottomActionButton(
|
||||
label: state.status == OneTimeOrderStatus.loading
|
||||
? labels.creating
|
||||
: labels.create_order,
|
||||
isLoading: state.status == OneTimeOrderStatus.loading,
|
||||
onPressed: state.isValid
|
||||
? () => BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(const OneTimeOrderSubmitted())
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: <Widget>[
|
||||
Text(
|
||||
labels.create_your_order,
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
OneTimeOrderEventNameInput(
|
||||
label: 'ORDER NAME',
|
||||
value: state.eventName,
|
||||
onChanged: (String value) => BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(OneTimeOrderEventNameChanged(value)),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Vendor Selection
|
||||
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<Vendor>(
|
||||
isExpanded: true,
|
||||
value: state.selectedVendor,
|
||||
icon: const Icon(
|
||||
UiIcons.chevronDown,
|
||||
size: 18,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onChanged: (Vendor? vendor) {
|
||||
if (vendor != null) {
|
||||
BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(OneTimeOrderVendorChanged(vendor));
|
||||
}
|
||||
},
|
||||
items: state.vendors.map((Vendor vendor) {
|
||||
return DropdownMenuItem<Vendor>(
|
||||
value: vendor,
|
||||
child: Text(
|
||||
vendor.name,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
OneTimeOrderDatePicker(
|
||||
label: labels.date_label,
|
||||
value: state.date,
|
||||
onChanged: (DateTime date) => BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(OneTimeOrderDateChanged(date)),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
Text('HUB', style: UiTypography.footnote2r.textSecondary),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<OneTimeOrderHubOption>(
|
||||
isExpanded: true,
|
||||
value: state.selectedHub,
|
||||
icon: const Icon(
|
||||
UiIcons.chevronDown,
|
||||
size: 18,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
onChanged: (OneTimeOrderHubOption? hub) {
|
||||
if (hub != null) {
|
||||
BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(OneTimeOrderHubChanged(hub));
|
||||
}
|
||||
},
|
||||
items: state.hubs.map((OneTimeOrderHubOption hub) {
|
||||
return DropdownMenuItem<OneTimeOrderHubOption>(
|
||||
value: hub,
|
||||
child: Text(
|
||||
hub.name,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
OneTimeOrderSectionHeader(
|
||||
title: labels.positions_title,
|
||||
actionLabel: labels.add_position,
|
||||
onAction: () => BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(const OneTimeOrderPositionAdded()),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// Positions List
|
||||
...state.positions.asMap().entries.map((
|
||||
MapEntry<int, OneTimeOrderPosition> entry,
|
||||
) {
|
||||
final int index = entry.key;
|
||||
final OneTimeOrderPosition position = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
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,
|
||||
roles: state.roles,
|
||||
onUpdated: (OneTimeOrderPosition updated) {
|
||||
BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(OneTimeOrderPositionUpdated(index, updated));
|
||||
},
|
||||
onRemoved: () {
|
||||
BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
).add(OneTimeOrderPositionRemoved(index));
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.space5,
|
||||
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.white,
|
||||
border: Border(top: BorderSide(color: UiColors.border)),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: UiButton.primary(
|
||||
text: label,
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
size: UiButtonSize.large,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,100 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
||||
import 'package:firebase_data_connect/src/core/ref.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/home_repository_interface.dart';
|
||||
|
||||
/// Implementation of [HomeRepositoryInterface] that delegates to [dc.HomeConnectorRepository].
|
||||
///
|
||||
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
|
||||
/// connector repository from the data_connect package.
|
||||
/// Implementation of [HomeRepositoryInterface] that directly interacts with the Data Connect SDK.
|
||||
class HomeRepositoryImpl implements HomeRepositoryInterface {
|
||||
HomeRepositoryImpl({dc.DataConnectService? service})
|
||||
: _service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
HomeRepositoryImpl({
|
||||
dc.HomeConnectorRepository? connectorRepository,
|
||||
dc.DataConnectService? service,
|
||||
}) : _connectorRepository = connectorRepository ??
|
||||
dc.DataConnectService.instance.getHomeRepository(),
|
||||
_service = service ?? dc.DataConnectService.instance;
|
||||
final dc.HomeConnectorRepository _connectorRepository;
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@override
|
||||
Future<HomeDashboardData> getDashboardData() async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.getDashboardData(businessId: businessId);
|
||||
final DateTime now = DateTime.now();
|
||||
final int daysFromMonday = now.weekday - DateTime.monday;
|
||||
final DateTime monday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).subtract(Duration(days: daysFromMonday));
|
||||
final DateTime weekRangeStart = monday;
|
||||
final DateTime weekRangeEnd = monday.add(
|
||||
const Duration(days: 13, hours: 23, minutes: 59, seconds: 59),
|
||||
);
|
||||
|
||||
final QueryResult<
|
||||
dc.GetCompletedShiftsByBusinessIdData,
|
||||
dc.GetCompletedShiftsByBusinessIdVariables
|
||||
>
|
||||
completedResult = await _service.connector
|
||||
.getCompletedShiftsByBusinessId(
|
||||
businessId: businessId,
|
||||
dateFrom: _service.toTimestamp(weekRangeStart),
|
||||
dateTo: _service.toTimestamp(weekRangeEnd),
|
||||
)
|
||||
.execute();
|
||||
|
||||
double weeklySpending = 0.0;
|
||||
double next7DaysSpending = 0.0;
|
||||
int weeklyShifts = 0;
|
||||
int next7DaysScheduled = 0;
|
||||
|
||||
for (final dc.GetCompletedShiftsByBusinessIdShifts shift
|
||||
in completedResult.data.shifts) {
|
||||
final DateTime? shiftDate = _service.toDateTime(shift.date);
|
||||
if (shiftDate == null) continue;
|
||||
|
||||
final int offset = shiftDate.difference(weekRangeStart).inDays;
|
||||
if (offset < 0 || offset > 13) continue;
|
||||
|
||||
final double cost = shift.cost ?? 0.0;
|
||||
if (offset <= 6) {
|
||||
weeklySpending += cost;
|
||||
weeklyShifts += 1;
|
||||
} else {
|
||||
next7DaysSpending += cost;
|
||||
next7DaysScheduled += 1;
|
||||
}
|
||||
}
|
||||
|
||||
final DateTime start = DateTime(now.year, now.month, now.day);
|
||||
final DateTime end = start.add(
|
||||
const Duration(hours: 23, minutes: 59, seconds: 59),
|
||||
);
|
||||
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDateRangeData,
|
||||
dc.ListShiftRolesByBusinessAndDateRangeVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listShiftRolesByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
int totalNeeded = 0;
|
||||
int totalFilled = 0;
|
||||
for (final dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole
|
||||
in result.data.shiftRoles) {
|
||||
totalNeeded += shiftRole.count;
|
||||
totalFilled += shiftRole.assigned ?? 0;
|
||||
}
|
||||
|
||||
return HomeDashboardData(
|
||||
weeklySpending: weeklySpending,
|
||||
next7DaysSpending: next7DaysSpending,
|
||||
weeklyShifts: weeklyShifts,
|
||||
next7DaysScheduled: next7DaysScheduled,
|
||||
totalNeeded: totalNeeded,
|
||||
totalFilled: totalFilled,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -39,7 +111,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
|
||||
|
||||
return await _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final QueryResult<dc.GetBusinessByIdData, dc.GetBusinessByIdVariables> businessResult = await _service.connector
|
||||
final QueryResult<dc.GetBusinessByIdData, dc.GetBusinessByIdVariables>
|
||||
businessResult = await _service.connector
|
||||
.getBusinessById(id: businessId)
|
||||
.execute();
|
||||
|
||||
@@ -69,8 +142,67 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
|
||||
|
||||
@override
|
||||
Future<List<ReorderItem>> getRecentReorders() async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.getRecentReorders(businessId: businessId);
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start = now.subtract(const Duration(days: 30));
|
||||
|
||||
final QueryResult<
|
||||
dc.ListCompletedOrdersByBusinessAndDateRangeData,
|
||||
dc.ListCompletedOrdersByBusinessAndDateRangeVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listCompletedOrdersByBusinessAndDateRange(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(now),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return result.data.orders.map((
|
||||
dc.ListCompletedOrdersByBusinessAndDateRangeOrders order,
|
||||
) {
|
||||
final String title =
|
||||
order.eventName ??
|
||||
(order.shifts_on_order.isNotEmpty
|
||||
? order.shifts_on_order[0].title
|
||||
: 'Order');
|
||||
|
||||
final String location = order.shifts_on_order.isNotEmpty
|
||||
? (order.shifts_on_order[0].location ??
|
||||
order.shifts_on_order[0].locationAddress ??
|
||||
'')
|
||||
: '';
|
||||
|
||||
int totalWorkers = 0;
|
||||
double totalHours = 0;
|
||||
double totalRate = 0;
|
||||
int roleCount = 0;
|
||||
|
||||
for (final dc.ListCompletedOrdersByBusinessAndDateRangeOrdersShiftsOnOrder
|
||||
shift
|
||||
in order.shifts_on_order) {
|
||||
for (final dc.ListCompletedOrdersByBusinessAndDateRangeOrdersShiftsOnOrderShiftRolesOnShift
|
||||
role
|
||||
in shift.shiftRoles_on_shift) {
|
||||
totalWorkers += role.count;
|
||||
totalHours += role.hours ?? 0;
|
||||
totalRate += role.role.costPerHour;
|
||||
roleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return ReorderItem(
|
||||
orderId: order.id,
|
||||
title: title,
|
||||
location: location,
|
||||
totalCost: order.total ?? 0.0,
|
||||
workers: totalWorkers,
|
||||
type: order.orderType.stringValue,
|
||||
hourlyRate: roleCount > 0 ? totalRate / roleCount : 0.0,
|
||||
hours: totalHours,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
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';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// A widget that displays quick actions for the client.
|
||||
class ActionsWidget extends StatelessWidget {
|
||||
/// Creates an [ActionsWidget].
|
||||
const ActionsWidget({
|
||||
super.key,
|
||||
required this.onRapidPressed,
|
||||
required this.onCreateOrderPressed,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
/// Callback when RAPID is pressed.
|
||||
final VoidCallback onRapidPressed;
|
||||
|
||||
/// Callback when Create Order is pressed.
|
||||
final VoidCallback onCreateOrderPressed;
|
||||
const ActionsWidget({super.key, this.subtitle});
|
||||
|
||||
/// Optional subtitle for the section.
|
||||
final String? subtitle;
|
||||
@@ -40,7 +31,7 @@ class ActionsWidget extends StatelessWidget {
|
||||
iconColor: UiColors.textError,
|
||||
textColor: UiColors.textError,
|
||||
subtitleColor: UiColors.textError.withValues(alpha: 0.8),
|
||||
onTap: onRapidPressed,
|
||||
onTap: () => Modular.to.toCreateOrderRapid(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -54,7 +45,7 @@ class ActionsWidget extends StatelessWidget {
|
||||
iconColor: UiColors.primary,
|
||||
textColor: UiColors.textPrimary,
|
||||
subtitleColor: UiColors.textSecondary,
|
||||
onTap: onCreateOrderPressed,
|
||||
onTap: () => Modular.to.toCreateOrder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -9,14 +9,12 @@ import '../widgets/draggable_widget_wrapper.dart';
|
||||
import '../widgets/live_activity_widget.dart';
|
||||
import '../widgets/reorder_widget.dart';
|
||||
import '../widgets/spending_widget.dart';
|
||||
import 'client_home_sheets.dart';
|
||||
|
||||
/// A widget that builds dashboard content based on widget ID.
|
||||
///
|
||||
/// This widget encapsulates the logic for rendering different dashboard
|
||||
/// widgets based on their unique identifiers and current state.
|
||||
class DashboardWidgetBuilder extends StatelessWidget {
|
||||
|
||||
/// Creates a [DashboardWidgetBuilder].
|
||||
const DashboardWidgetBuilder({
|
||||
required this.id,
|
||||
@@ -24,6 +22,7 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
||||
required this.isEditMode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The unique identifier for the widget to build.
|
||||
final String id;
|
||||
|
||||
@@ -62,39 +61,9 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
||||
|
||||
switch (id) {
|
||||
case 'actions':
|
||||
return ActionsWidget(
|
||||
onRapidPressed: () => Modular.to.toCreateOrderRapid(),
|
||||
onCreateOrderPressed: () => Modular.to.toCreateOrder(),
|
||||
subtitle: subtitle,
|
||||
);
|
||||
return ActionsWidget(subtitle: subtitle);
|
||||
case 'reorder':
|
||||
return ReorderWidget(
|
||||
orders: state.reorderItems,
|
||||
onReorderPressed: (Map<String, dynamic> data) {
|
||||
ClientHomeSheets.showOrderFormSheet(
|
||||
context,
|
||||
data,
|
||||
onSubmit: (Map<String, dynamic> submittedData) {
|
||||
final String? dateStr =
|
||||
submittedData['date']?.toString();
|
||||
if (dateStr == null || dateStr.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final DateTime? initialDate = DateTime.tryParse(dateStr);
|
||||
if (initialDate == null) {
|
||||
return;
|
||||
}
|
||||
Modular.to.navigate(
|
||||
ClientPaths.orders,
|
||||
arguments: <String, dynamic>{
|
||||
'initialDate': initialDate.toIso8601String(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
subtitle: subtitle,
|
||||
);
|
||||
return ReorderWidget(orders: state.reorderItems, subtitle: subtitle);
|
||||
case 'spending':
|
||||
return SpendingWidget(
|
||||
weeklySpending: state.dashboardData.weeklySpending,
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
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';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// A widget that allows clients to reorder recent shifts.
|
||||
class ReorderWidget extends StatelessWidget {
|
||||
|
||||
/// Creates a [ReorderWidget].
|
||||
const ReorderWidget({
|
||||
super.key,
|
||||
required this.orders,
|
||||
required this.onReorderPressed,
|
||||
this.subtitle,
|
||||
});
|
||||
const ReorderWidget({super.key, required this.orders, this.subtitle});
|
||||
|
||||
/// Recent completed orders for reorder.
|
||||
final List<ReorderItem> orders;
|
||||
|
||||
/// Callback when a reorder button is pressed.
|
||||
final Function(Map<String, dynamic> shiftData) onReorderPressed;
|
||||
|
||||
/// Optional subtitle for the section.
|
||||
final String? subtitle;
|
||||
|
||||
@@ -55,8 +49,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final ReorderItem order = recentOrders[index];
|
||||
final double totalCost =
|
||||
order.hourlyRate * order.hours * order.workers;
|
||||
final double totalCost = order.totalCost;
|
||||
|
||||
return Container(
|
||||
width: 260,
|
||||
@@ -155,7 +148,8 @@ class ReorderWidget extends StatelessWidget {
|
||||
leadingIcon: UiIcons.zap,
|
||||
iconSize: 12,
|
||||
fullWidth: true,
|
||||
onPressed: () => onReorderPressed(<String, dynamic>{
|
||||
onPressed: () =>
|
||||
_handleReorderPressed(context, <String, dynamic>{
|
||||
'orderId': order.orderId,
|
||||
'title': order.title,
|
||||
'location': order.location,
|
||||
@@ -163,6 +157,7 @@ class ReorderWidget extends StatelessWidget {
|
||||
'hours': order.hours,
|
||||
'workers': order.workers,
|
||||
'type': order.type,
|
||||
'totalCost': order.totalCost,
|
||||
}),
|
||||
),
|
||||
],
|
||||
@@ -174,10 +169,34 @@ class ReorderWidget extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _handleReorderPressed(BuildContext context, Map<String, dynamic> data) {
|
||||
// Override start date with today's date as requested
|
||||
final Map<String, dynamic> populatedData = Map<String, dynamic>.from(data)
|
||||
..['startDate'] = DateTime.now();
|
||||
|
||||
final String? typeStr = populatedData['type']?.toString();
|
||||
if (typeStr == null || typeStr.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final OrderType orderType = OrderType.fromString(typeStr);
|
||||
switch (orderType) {
|
||||
case OrderType.recurring:
|
||||
Modular.to.toCreateOrderRecurring(arguments: populatedData);
|
||||
break;
|
||||
case OrderType.permanent:
|
||||
Modular.to.toCreateOrderPermanent(arguments: populatedData);
|
||||
break;
|
||||
case OrderType.oneTime:
|
||||
default:
|
||||
Modular.to.toCreateOrderOneTime(arguments: populatedData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Badge extends StatelessWidget {
|
||||
|
||||
const _Badge({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
|
||||
@@ -8,12 +8,8 @@ import 'domain/usecases/create_one_time_order_usecase.dart';
|
||||
import 'domain/usecases/create_permanent_order_usecase.dart';
|
||||
import 'domain/usecases/create_recurring_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/permanent_order_bloc.dart';
|
||||
import 'presentation/blocs/recurring_order_bloc.dart';
|
||||
import 'presentation/blocs/rapid_order_bloc.dart';
|
||||
import 'domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||
import 'presentation/blocs/index.dart';
|
||||
import 'presentation/pages/create_order_page.dart';
|
||||
import 'presentation/pages/one_time_order_page.dart';
|
||||
import 'presentation/pages/permanent_order_page.dart';
|
||||
@@ -32,17 +28,18 @@ class ClientCreateOrderModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(ClientCreateOrderRepositoryImpl.new);
|
||||
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
|
||||
ClientCreateOrderRepositoryImpl.new,
|
||||
);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(GetOrderTypesUseCase.new);
|
||||
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
|
||||
i.addLazySingleton(CreatePermanentOrderUseCase.new);
|
||||
i.addLazySingleton(CreateRecurringOrderUseCase.new);
|
||||
i.addLazySingleton(CreateRapidOrderUseCase.new);
|
||||
i.addLazySingleton(GetOrderDetailsForReorderUseCase.new);
|
||||
|
||||
// BLoCs
|
||||
i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
|
||||
i.add<RapidOrderBloc>(RapidOrderBloc.new);
|
||||
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
|
||||
i.add<PermanentOrderBloc>(PermanentOrderBloc.new);
|
||||
@@ -56,19 +53,31 @@ class ClientCreateOrderModule extends Module {
|
||||
child: (BuildContext context) => const ClientCreateOrderPage(),
|
||||
);
|
||||
r.child(
|
||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRapid),
|
||||
ClientPaths.childRoute(
|
||||
ClientPaths.createOrder,
|
||||
ClientPaths.createOrderRapid,
|
||||
),
|
||||
child: (BuildContext context) => const RapidOrderPage(),
|
||||
);
|
||||
r.child(
|
||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderOneTime),
|
||||
ClientPaths.childRoute(
|
||||
ClientPaths.createOrder,
|
||||
ClientPaths.createOrderOneTime,
|
||||
),
|
||||
child: (BuildContext context) => const OneTimeOrderPage(),
|
||||
);
|
||||
r.child(
|
||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRecurring),
|
||||
ClientPaths.childRoute(
|
||||
ClientPaths.createOrder,
|
||||
ClientPaths.createOrderRecurring,
|
||||
),
|
||||
child: (BuildContext context) => const RecurringOrderPage(),
|
||||
);
|
||||
r.child(
|
||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderPermanent),
|
||||
ClientPaths.childRoute(
|
||||
ClientPaths.createOrder,
|
||||
ClientPaths.createOrderPermanent,
|
||||
),
|
||||
child: (BuildContext context) => const PermanentOrderPage(),
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
@@ -11,46 +11,13 @@ import '../../domain/repositories/client_create_order_repository_interface.dart'
|
||||
///
|
||||
/// It follows the KROW Clean Architecture by keeping the data layer focused
|
||||
/// on delegation and data mapping, without business logic.
|
||||
class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface {
|
||||
ClientCreateOrderRepositoryImpl({
|
||||
required dc.DataConnectService service,
|
||||
}) : _service = service;
|
||||
class ClientCreateOrderRepositoryImpl
|
||||
implements ClientCreateOrderRepositoryInterface {
|
||||
ClientCreateOrderRepositoryImpl({required dc.DataConnectService service})
|
||||
: _service = service;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@override
|
||||
Future<List<domain.OrderType>> getOrderTypes() {
|
||||
return Future<List<domain.OrderType>>.value(const <domain.OrderType>[
|
||||
domain.OrderType(
|
||||
id: 'one-time',
|
||||
titleKey: 'client_create_order.types.one_time',
|
||||
descriptionKey: 'client_create_order.types.one_time_desc',
|
||||
),
|
||||
|
||||
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||
// domain.OrderType(
|
||||
// id: 'rapid',
|
||||
// titleKey: 'client_create_order.types.rapid',
|
||||
// descriptionKey: 'client_create_order.types.rapid_desc',
|
||||
// ),
|
||||
domain.OrderType(
|
||||
id: 'recurring',
|
||||
titleKey: 'client_create_order.types.recurring',
|
||||
descriptionKey: 'client_create_order.types.recurring_desc',
|
||||
),
|
||||
// domain.OrderType(
|
||||
// id: 'permanent',
|
||||
// titleKey: 'client_create_order.types.permanent',
|
||||
// descriptionKey: 'client_create_order.types.permanent_desc',
|
||||
// ),
|
||||
domain.OrderType(
|
||||
id: 'permanent',
|
||||
titleKey: 'client_create_order.types.permanent',
|
||||
descriptionKey: 'client_create_order.types.permanent_desc',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
|
||||
return _service.run(() async {
|
||||
@@ -69,9 +36,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
order.date.month,
|
||||
order.date.day,
|
||||
);
|
||||
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
|
||||
await _service.connector
|
||||
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||
orderResult = await _service.connector
|
||||
.createOrder(
|
||||
businessId: businessId,
|
||||
orderType: dc.OrderType.ONE_TIME,
|
||||
@@ -92,8 +59,8 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}';
|
||||
final double shiftCost = _calculateShiftCost(order);
|
||||
|
||||
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
|
||||
await _service.connector
|
||||
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||
shiftResult = await _service.connector
|
||||
.createShift(title: shiftTitle, orderId: orderId)
|
||||
.date(orderTimestamp)
|
||||
.location(hub.name)
|
||||
@@ -117,7 +84,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.date, position.startTime);
|
||||
final DateTime end = _parseTime(order.date, position.endTime);
|
||||
final DateTime normalizedEnd = end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
final double totalValue = rate * hours * position.count;
|
||||
@@ -139,7 +108,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
|
||||
await _service.connector
|
||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||
.shifts(fdc.AnyValue(<String>[shiftId]))
|
||||
.shifts(AnyValue(<String>[shiftId]))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -162,12 +131,12 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
order.startDate.month,
|
||||
order.startDate.day,
|
||||
);
|
||||
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final fdc.Timestamp startTimestamp = orderTimestamp;
|
||||
final fdc.Timestamp endTimestamp = _service.toTimestamp(order.endDate);
|
||||
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
|
||||
final Timestamp endTimestamp = _service.toTimestamp(order.endDate);
|
||||
|
||||
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
|
||||
await _service.connector
|
||||
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||
orderResult = await _service.connector
|
||||
.createOrder(
|
||||
businessId: businessId,
|
||||
orderType: dc.OrderType.RECURRING,
|
||||
@@ -187,32 +156,36 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
// NOTE: Recurring orders are limited to 30 days of generated shifts.
|
||||
// Future shifts beyond 30 days should be created by a scheduled job.
|
||||
final DateTime maxEndDate = orderDateOnly.add(const Duration(days: 29));
|
||||
final DateTime effectiveEndDate =
|
||||
order.endDate.isAfter(maxEndDate) ? maxEndDate : order.endDate;
|
||||
final DateTime effectiveEndDate = order.endDate.isAfter(maxEndDate)
|
||||
? maxEndDate
|
||||
: order.endDate;
|
||||
|
||||
final Set<String> selectedDays = Set<String>.from(order.recurringDays);
|
||||
final int workersNeeded = order.positions.fold<int>(
|
||||
0,
|
||||
(int sum, domain.RecurringOrderPosition position) => sum + position.count,
|
||||
(int sum, domain.RecurringOrderPosition position) =>
|
||||
sum + position.count,
|
||||
);
|
||||
final double shiftCost = _calculateRecurringShiftCost(order);
|
||||
|
||||
final List<String> shiftIds = <String>[];
|
||||
for (DateTime day = orderDateOnly;
|
||||
for (
|
||||
DateTime day = orderDateOnly;
|
||||
!day.isAfter(effectiveEndDate);
|
||||
day = day.add(const Duration(days: 1))) {
|
||||
day = day.add(const Duration(days: 1))
|
||||
) {
|
||||
final String dayLabel = _weekdayLabel(day);
|
||||
if (!selectedDays.contains(dayLabel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
||||
final fdc.Timestamp dayTimestamp = _service.toTimestamp(
|
||||
final Timestamp dayTimestamp = _service.toTimestamp(
|
||||
DateTime(day.year, day.month, day.day),
|
||||
);
|
||||
|
||||
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
|
||||
await _service.connector
|
||||
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||
shiftResult = await _service.connector
|
||||
.createShift(title: shiftTitle, orderId: orderId)
|
||||
.date(dayTimestamp)
|
||||
.location(hub.name)
|
||||
@@ -237,8 +210,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
for (final domain.RecurringOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(day, position.startTime);
|
||||
final DateTime end = _parseTime(day, position.endTime);
|
||||
final DateTime normalizedEnd =
|
||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
final double totalValue = rate * hours * position.count;
|
||||
@@ -261,7 +235,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
|
||||
await _service.connector
|
||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||
.shifts(fdc.AnyValue(shiftIds))
|
||||
.shifts(AnyValue(shiftIds))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -284,11 +258,11 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
order.startDate.month,
|
||||
order.startDate.day,
|
||||
);
|
||||
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final fdc.Timestamp startTimestamp = orderTimestamp;
|
||||
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
|
||||
|
||||
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
|
||||
await _service.connector
|
||||
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||
orderResult = await _service.connector
|
||||
.createOrder(
|
||||
businessId: businessId,
|
||||
orderType: dc.OrderType.PERMANENT,
|
||||
@@ -316,21 +290,23 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
final double shiftCost = _calculatePermanentShiftCost(order);
|
||||
|
||||
final List<String> shiftIds = <String>[];
|
||||
for (DateTime day = orderDateOnly;
|
||||
for (
|
||||
DateTime day = orderDateOnly;
|
||||
!day.isAfter(maxEndDate);
|
||||
day = day.add(const Duration(days: 1))) {
|
||||
day = day.add(const Duration(days: 1))
|
||||
) {
|
||||
final String dayLabel = _weekdayLabel(day);
|
||||
if (!selectedDays.contains(dayLabel)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
||||
final fdc.Timestamp dayTimestamp = _service.toTimestamp(
|
||||
final Timestamp dayTimestamp = _service.toTimestamp(
|
||||
DateTime(day.year, day.month, day.day),
|
||||
);
|
||||
|
||||
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
|
||||
await _service.connector
|
||||
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||
shiftResult = await _service.connector
|
||||
.createShift(title: shiftTitle, orderId: orderId)
|
||||
.date(dayTimestamp)
|
||||
.location(hub.name)
|
||||
@@ -355,8 +331,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(day, position.startTime);
|
||||
final DateTime end = _parseTime(day, position.endTime);
|
||||
final DateTime normalizedEnd =
|
||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
final double totalValue = rate * hours * position.count;
|
||||
@@ -379,7 +356,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
|
||||
await _service.connector
|
||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||
.shifts(fdc.AnyValue(shiftIds))
|
||||
.shifts(AnyValue(shiftIds))
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
@@ -396,13 +373,76 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
throw UnimplementedError('Reorder functionality is not yet implemented.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ReorderData> getOrderDetailsForReorder(String orderId) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndOrderData,
|
||||
dc.ListShiftRolesByBusinessAndOrderVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listShiftRolesByBusinessAndOrder(
|
||||
businessId: businessId,
|
||||
orderId: orderId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListShiftRolesByBusinessAndOrderShiftRoles> shiftRoles =
|
||||
result.data.shiftRoles;
|
||||
|
||||
if (shiftRoles.isEmpty) {
|
||||
throw Exception('Order not found or has no roles.');
|
||||
}
|
||||
|
||||
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrder order =
|
||||
shiftRoles.first.shift.order;
|
||||
|
||||
final domain.OrderType orderType = _mapOrderType(order.orderType);
|
||||
|
||||
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub
|
||||
teamHub = order.teamHub;
|
||||
|
||||
return domain.ReorderData(
|
||||
orderId: orderId,
|
||||
eventName: order.eventName ?? '',
|
||||
vendorId: order.vendorId ?? '',
|
||||
orderType: orderType,
|
||||
hub: domain.OneTimeOrderHubDetails(
|
||||
id: teamHub.id,
|
||||
name: teamHub.hubName,
|
||||
address: teamHub.address,
|
||||
placeId: teamHub.placeId,
|
||||
latitude: 0, // Not available in this query
|
||||
longitude: 0,
|
||||
),
|
||||
positions: shiftRoles.map((
|
||||
dc.ListShiftRolesByBusinessAndOrderShiftRoles role,
|
||||
) {
|
||||
return domain.ReorderPosition(
|
||||
roleId: role.roleId,
|
||||
count: role.count,
|
||||
startTime: _formatTimestamp(role.startTime),
|
||||
endTime: _formatTimestamp(role.endTime),
|
||||
lunchBreak: _formatBreakDuration(role.breakType),
|
||||
);
|
||||
}).toList(),
|
||||
startDate: order.startDate?.toDateTime(),
|
||||
endDate: order.endDate?.toDateTime(),
|
||||
recurringDays: order.recurringDays ?? const <String>[],
|
||||
permanentDays: order.permanentDays ?? const <String>[],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
double _calculateShiftCost(domain.OneTimeOrder order) {
|
||||
double total = 0;
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.date, position.startTime);
|
||||
final DateTime end = _parseTime(order.date, position.endTime);
|
||||
final DateTime normalizedEnd =
|
||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
total += rate * hours * position.count;
|
||||
@@ -415,8 +455,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
for (final domain.RecurringOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.startDate, position.startTime);
|
||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||
final DateTime normalizedEnd =
|
||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
total += rate * hours * position.count;
|
||||
@@ -429,8 +470,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||
final DateTime start = _parseTime(order.startDate, position.startTime);
|
||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||
final DateTime normalizedEnd =
|
||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
||||
final DateTime normalizedEnd = end.isBefore(start)
|
||||
? end.add(const Duration(days: 1))
|
||||
: end;
|
||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||
final double rate = order.roleRates[position.role] ?? 0;
|
||||
total += rate * hours * position.count;
|
||||
@@ -506,4 +548,49 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
||||
final String day = dateTime.day.toString().padLeft(2, '0');
|
||||
return '$year-$month-$day';
|
||||
}
|
||||
|
||||
String _formatTimestamp(Timestamp? value) {
|
||||
if (value == null) return '';
|
||||
try {
|
||||
return DateFormat('HH:mm').format(value.toDateTime());
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatBreakDuration(dc.EnumValue<dc.BreakDuration>? breakType) {
|
||||
if (breakType is dc.Known<dc.BreakDuration>) {
|
||||
switch (breakType.value) {
|
||||
case dc.BreakDuration.MIN_10:
|
||||
return 'MIN_10';
|
||||
case dc.BreakDuration.MIN_15:
|
||||
return 'MIN_15';
|
||||
case dc.BreakDuration.MIN_30:
|
||||
return 'MIN_30';
|
||||
case dc.BreakDuration.MIN_45:
|
||||
return 'MIN_45';
|
||||
case dc.BreakDuration.MIN_60:
|
||||
return 'MIN_60';
|
||||
case dc.BreakDuration.NO_BREAK:
|
||||
return 'NO_BREAK';
|
||||
}
|
||||
}
|
||||
return 'NO_BREAK';
|
||||
}
|
||||
|
||||
domain.OrderType _mapOrderType(dc.EnumValue<dc.OrderType>? orderType) {
|
||||
if (orderType is dc.Known<dc.OrderType>) {
|
||||
switch (orderType.value) {
|
||||
case dc.OrderType.ONE_TIME:
|
||||
return domain.OrderType.oneTime;
|
||||
case dc.OrderType.RECURRING:
|
||||
return domain.OrderType.recurring;
|
||||
case dc.OrderType.PERMANENT:
|
||||
return domain.OrderType.permanent;
|
||||
case dc.OrderType.RAPID:
|
||||
return domain.OrderType.oneTime;
|
||||
}
|
||||
}
|
||||
return domain.OrderType.oneTime;
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,11 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
/// Interface for the Client Create Order repository.
|
||||
///
|
||||
/// This repository is responsible for:
|
||||
/// 1. Retrieving available order types for the client.
|
||||
/// 2. Submitting different types of staffing orders (Rapid, One-Time).
|
||||
/// 1. Submitting different types of staffing orders (Rapid, One-Time, Recurring, Permanent).
|
||||
///
|
||||
/// It follows the KROW Clean Architecture by defining the contract in the
|
||||
/// domain layer, to be implemented in the data layer.
|
||||
abstract interface class ClientCreateOrderRepositoryInterface {
|
||||
/// Retrieves the list of available order types (e.g., Rapid, One-Time, Recurring).
|
||||
Future<List<OrderType>> getOrderTypes();
|
||||
|
||||
/// Submits a one-time staffing order with specific details.
|
||||
///
|
||||
/// [order] contains the date, location, and required positions.
|
||||
@@ -33,4 +29,9 @@ abstract interface class ClientCreateOrderRepositoryInterface {
|
||||
/// [previousOrderId] is the ID of the order to reorder.
|
||||
/// [newDate] is the new date for the order.
|
||||
Future<void> reorder(String previousOrderId, DateTime newDate);
|
||||
|
||||
/// Fetches the details of an existing order to be used as a template for a new order.
|
||||
///
|
||||
/// returns [ReorderData] containing the order details and positions.
|
||||
Future<ReorderData> getOrderDetailsForReorder(String orderId);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/client_create_order_repository_interface.dart';
|
||||
|
||||
/// Use case for fetching order details for reordering.
|
||||
class GetOrderDetailsForReorderUseCase implements UseCase<String, ReorderData> {
|
||||
const GetOrderDetailsForReorderUseCase(this._repository);
|
||||
final ClientCreateOrderRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<ReorderData> call(String orderId) {
|
||||
return _repository.getOrderDetailsForReorder(orderId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export 'one_time_order/index.dart';
|
||||
export 'rapid_order/index.dart';
|
||||
export 'recurring_order/index.dart';
|
||||
export 'permanent_order/index.dart';
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'one_time_order_bloc.dart';
|
||||
export 'one_time_order_event.dart';
|
||||
export 'one_time_order_state.dart';
|
||||
@@ -1,18 +1,25 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:client_create_order/src/domain/arguments/one_time_order_arguments.dart';
|
||||
import 'package:client_create_order/src/domain/usecases/create_one_time_order_usecase.dart';
|
||||
import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
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<OneTimeOrderEvent, OneTimeOrderState>
|
||||
with BlocErrorHandler<OneTimeOrderState>, SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||
OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._service)
|
||||
: super(OneTimeOrderState.initial()) {
|
||||
with
|
||||
BlocErrorHandler<OneTimeOrderState>,
|
||||
SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||
OneTimeOrderBloc(
|
||||
this._createOneTimeOrderUseCase,
|
||||
this._getOrderDetailsForReorderUseCase,
|
||||
this._service,
|
||||
) : super(OneTimeOrderState.initial()) {
|
||||
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
|
||||
on<OneTimeOrderVendorChanged>(_onVendorChanged);
|
||||
on<OneTimeOrderHubsLoaded>(_onHubsLoaded);
|
||||
@@ -23,18 +30,22 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
|
||||
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
|
||||
on<OneTimeOrderSubmitted>(_onSubmitted);
|
||||
on<OneTimeOrderInitialized>(_onInitialized);
|
||||
|
||||
_loadVendors();
|
||||
_loadHubs();
|
||||
}
|
||||
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
|
||||
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
Future<void> _loadVendors() async {
|
||||
final List<Vendor>? vendors = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final QueryResult<dc.ListVendorsData, void> result =
|
||||
await _service.connector.listVendors().execute();
|
||||
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
|
||||
.connector
|
||||
.listVendors()
|
||||
.execute();
|
||||
return result.data.vendors
|
||||
.map(
|
||||
(dc.ListVendorsVendors vendor) => Vendor(
|
||||
@@ -53,11 +64,19 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadRolesForVendor(String vendorId, Emitter<OneTimeOrderState> emit) async {
|
||||
Future<void> _loadRolesForVendor(
|
||||
String vendorId,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) async {
|
||||
final List<OneTimeOrderRoleOption>? roles = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
|
||||
result = await _service.connector.listRolesByVendorId(vendorId: vendorId).execute();
|
||||
final fdc.QueryResult<
|
||||
dc.ListRolesByVendorIdData,
|
||||
dc.ListRolesByVendorIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listRolesByVendorId(vendorId: vendorId)
|
||||
.execute();
|
||||
return result.data.roles
|
||||
.map(
|
||||
(dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption(
|
||||
@@ -68,7 +87,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
onError: (_) => emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
|
||||
onError: (_) =>
|
||||
emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
|
||||
);
|
||||
|
||||
if (roles != null) {
|
||||
@@ -80,7 +100,10 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
|
||||
final fdc.QueryResult<
|
||||
dc.ListTeamHubsByOwnerIdData,
|
||||
dc.ListTeamHubsByOwnerIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||
.execute();
|
||||
@@ -102,7 +125,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
onError: (_) => add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
|
||||
onError: (_) =>
|
||||
add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
|
||||
);
|
||||
|
||||
if (hubs != null) {
|
||||
@@ -114,13 +138,11 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
OneTimeOrderVendorsLoaded event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) async {
|
||||
final Vendor? selectedVendor =
|
||||
event.vendors.isNotEmpty ? event.vendors.first : null;
|
||||
final Vendor? selectedVendor = event.vendors.isNotEmpty
|
||||
? event.vendors.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
vendors: event.vendors,
|
||||
selectedVendor: selectedVendor,
|
||||
),
|
||||
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
||||
);
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
@@ -139,8 +161,9 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
OneTimeOrderHubsLoaded event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) {
|
||||
final OneTimeOrderHubOption? selectedHub =
|
||||
event.hubs.isNotEmpty ? event.hubs.first : null;
|
||||
final OneTimeOrderHubOption? selectedHub = event.hubs.isNotEmpty
|
||||
? event.hubs.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
hubs: event.hubs,
|
||||
@@ -154,12 +177,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
OneTimeOrderHubChanged event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedHub: event.hub,
|
||||
location: event.hub.name,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
|
||||
}
|
||||
|
||||
void _onEventNameChanged(
|
||||
@@ -260,4 +278,74 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onInitialized(
|
||||
OneTimeOrderInitialized event,
|
||||
Emitter<OneTimeOrderState> emit,
|
||||
) async {
|
||||
final Map<String, dynamic> data = event.data;
|
||||
final String title = data['title']?.toString() ?? '';
|
||||
final DateTime? startDate = data['startDate'] as DateTime?;
|
||||
final String? orderId = data['orderId']?.toString();
|
||||
|
||||
emit(state.copyWith(eventName: title, date: startDate ?? DateTime.now()));
|
||||
|
||||
if (orderId == null || orderId.isEmpty) return;
|
||||
|
||||
emit(state.copyWith(status: OneTimeOrderStatus.loading));
|
||||
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final ReorderData orderDetails =
|
||||
await _getOrderDetailsForReorderUseCase(orderId);
|
||||
|
||||
// Map positions
|
||||
final List<OneTimeOrderPosition> positions = orderDetails.positions.map(
|
||||
(ReorderPosition role) {
|
||||
return OneTimeOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.count,
|
||||
startTime: role.startTime,
|
||||
endTime: role.endTime,
|
||||
lunchBreak: role.lunchBreak,
|
||||
);
|
||||
},
|
||||
).toList();
|
||||
|
||||
// Update state with order details
|
||||
final Vendor? selectedVendor = state.vendors
|
||||
.where((Vendor v) => v.id == orderDetails.vendorId)
|
||||
.firstOrNull;
|
||||
|
||||
final OneTimeOrderHubOption? selectedHub = state.hubs
|
||||
.where(
|
||||
(OneTimeOrderHubOption h) =>
|
||||
h.placeId == orderDetails.hub.placeId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
eventName: orderDetails.eventName.isNotEmpty
|
||||
? orderDetails.eventName
|
||||
: title,
|
||||
positions: positions,
|
||||
selectedVendor: selectedVendor,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
status: OneTimeOrderStatus.initial,
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: OneTimeOrderStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,3 +81,11 @@ class OneTimeOrderPositionUpdated extends OneTimeOrderEvent {
|
||||
class OneTimeOrderSubmitted extends OneTimeOrderEvent {
|
||||
const OneTimeOrderSubmitted();
|
||||
}
|
||||
|
||||
class OneTimeOrderInitialized extends OneTimeOrderEvent {
|
||||
const OneTimeOrderInitialized(this.data);
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[data];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'permanent_order_bloc.dart';
|
||||
export 'permanent_order_event.dart';
|
||||
export 'permanent_order_state.dart';
|
||||
@@ -1,17 +1,24 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:client_create_order/src/domain/usecases/create_permanent_order_usecase.dart';
|
||||
import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import '../../domain/usecases/create_permanent_order_usecase.dart';
|
||||
|
||||
import 'permanent_order_event.dart';
|
||||
import 'permanent_order_state.dart';
|
||||
|
||||
/// BLoC for managing the permanent order creation form.
|
||||
class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
with BlocErrorHandler<PermanentOrderState>, SafeBloc<PermanentOrderEvent, PermanentOrderState> {
|
||||
PermanentOrderBloc(this._createPermanentOrderUseCase, this._service)
|
||||
: super(PermanentOrderState.initial()) {
|
||||
with
|
||||
BlocErrorHandler<PermanentOrderState>,
|
||||
SafeBloc<PermanentOrderEvent, PermanentOrderState> {
|
||||
PermanentOrderBloc(
|
||||
this._createPermanentOrderUseCase,
|
||||
this._getOrderDetailsForReorderUseCase,
|
||||
this._service,
|
||||
) : super(PermanentOrderState.initial()) {
|
||||
on<PermanentOrderVendorsLoaded>(_onVendorsLoaded);
|
||||
on<PermanentOrderVendorChanged>(_onVendorChanged);
|
||||
on<PermanentOrderHubsLoaded>(_onHubsLoaded);
|
||||
@@ -23,12 +30,14 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
on<PermanentOrderPositionRemoved>(_onPositionRemoved);
|
||||
on<PermanentOrderPositionUpdated>(_onPositionUpdated);
|
||||
on<PermanentOrderSubmitted>(_onSubmitted);
|
||||
on<PermanentOrderInitialized>(_onInitialized);
|
||||
|
||||
_loadVendors();
|
||||
_loadHubs();
|
||||
}
|
||||
|
||||
final CreatePermanentOrderUseCase _createPermanentOrderUseCase;
|
||||
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
static const List<String> _dayLabels = <String>[
|
||||
@@ -44,8 +53,10 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
Future<void> _loadVendors() async {
|
||||
final List<domain.Vendor>? vendors = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final QueryResult<dc.ListVendorsData, void> result =
|
||||
await _service.connector.listVendors().execute();
|
||||
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
|
||||
.connector
|
||||
.listVendors()
|
||||
.execute();
|
||||
return result.data.vendors
|
||||
.map(
|
||||
(dc.ListVendorsVendors vendor) => domain.Vendor(
|
||||
@@ -70,7 +81,10 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
) async {
|
||||
final List<PermanentOrderRoleOption>? roles = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
|
||||
final fdc.QueryResult<
|
||||
dc.ListRolesByVendorIdData,
|
||||
dc.ListRolesByVendorIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listRolesByVendorId(vendorId: vendorId)
|
||||
.execute();
|
||||
@@ -84,7 +98,8 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
onError: (_) => emit(state.copyWith(roles: const <PermanentOrderRoleOption>[])),
|
||||
onError: (_) =>
|
||||
emit(state.copyWith(roles: const <PermanentOrderRoleOption>[])),
|
||||
);
|
||||
|
||||
if (roles != null) {
|
||||
@@ -96,7 +111,10 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
final List<PermanentOrderHubOption>? hubs = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
|
||||
final fdc.QueryResult<
|
||||
dc.ListTeamHubsByOwnerIdData,
|
||||
dc.ListTeamHubsByOwnerIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||
.execute();
|
||||
@@ -118,7 +136,8 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
onError: (_) => add(const PermanentOrderHubsLoaded(<PermanentOrderHubOption>[])),
|
||||
onError: (_) =>
|
||||
add(const PermanentOrderHubsLoaded(<PermanentOrderHubOption>[])),
|
||||
);
|
||||
|
||||
if (hubs != null) {
|
||||
@@ -130,13 +149,11 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
PermanentOrderVendorsLoaded event,
|
||||
Emitter<PermanentOrderState> emit,
|
||||
) async {
|
||||
final domain.Vendor? selectedVendor =
|
||||
event.vendors.isNotEmpty ? event.vendors.first : null;
|
||||
final domain.Vendor? selectedVendor = event.vendors.isNotEmpty
|
||||
? event.vendors.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
vendors: event.vendors,
|
||||
selectedVendor: selectedVendor,
|
||||
),
|
||||
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
||||
);
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
@@ -155,8 +172,9 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
PermanentOrderHubsLoaded event,
|
||||
Emitter<PermanentOrderState> emit,
|
||||
) {
|
||||
final PermanentOrderHubOption? selectedHub =
|
||||
event.hubs.isNotEmpty ? event.hubs.first : null;
|
||||
final PermanentOrderHubOption? selectedHub = event.hubs.isNotEmpty
|
||||
? event.hubs.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
hubs: event.hubs,
|
||||
@@ -170,12 +188,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
PermanentOrderHubChanged event,
|
||||
Emitter<PermanentOrderState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedHub: event.hub,
|
||||
location: event.hub.name,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
|
||||
}
|
||||
|
||||
void _onEventNameChanged(
|
||||
@@ -225,7 +238,12 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
} else {
|
||||
days.add(label);
|
||||
}
|
||||
emit(state.copyWith(permanentDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
|
||||
emit(
|
||||
state.copyWith(
|
||||
permanentDays: _sortDays(days),
|
||||
autoSelectedDayIndex: autoIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPositionAdded(
|
||||
@@ -324,6 +342,80 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onInitialized(
|
||||
PermanentOrderInitialized event,
|
||||
Emitter<PermanentOrderState> emit,
|
||||
) async {
|
||||
final Map<String, dynamic> data = event.data;
|
||||
final String title = data['title']?.toString() ?? '';
|
||||
final DateTime? startDate = data['startDate'] as DateTime?;
|
||||
final String? orderId = data['orderId']?.toString();
|
||||
|
||||
emit(
|
||||
state.copyWith(eventName: title, startDate: startDate ?? DateTime.now()),
|
||||
);
|
||||
|
||||
if (orderId == null || orderId.isEmpty) return;
|
||||
|
||||
emit(state.copyWith(status: PermanentOrderStatus.loading));
|
||||
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final domain.ReorderData orderDetails =
|
||||
await _getOrderDetailsForReorderUseCase(orderId);
|
||||
|
||||
// Map positions
|
||||
final List<PermanentOrderPosition> positions = orderDetails.positions
|
||||
.map((domain.ReorderPosition role) {
|
||||
return PermanentOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.count,
|
||||
startTime: role.startTime,
|
||||
endTime: role.endTime,
|
||||
lunchBreak: role.lunchBreak,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Update state with order details
|
||||
final domain.Vendor? selectedVendor = state.vendors
|
||||
.where((domain.Vendor v) => v.id == orderDetails.vendorId)
|
||||
.firstOrNull;
|
||||
|
||||
final PermanentOrderHubOption? selectedHub = state.hubs
|
||||
.where(
|
||||
(PermanentOrderHubOption h) =>
|
||||
h.placeId == orderDetails.hub.placeId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
eventName: orderDetails.eventName.isNotEmpty
|
||||
? orderDetails.eventName
|
||||
: title,
|
||||
positions: positions,
|
||||
selectedVendor: selectedVendor,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
status: PermanentOrderStatus.initial,
|
||||
startDate: startDate ?? orderDetails.startDate ?? DateTime.now(),
|
||||
permanentDays: orderDetails.permanentDays,
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: PermanentOrderStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<String> _sortDays(List<String> days) {
|
||||
days.sort(
|
||||
(String a, String b) =>
|
||||
@@ -98,3 +98,11 @@ class PermanentOrderPositionUpdated extends PermanentOrderEvent {
|
||||
class PermanentOrderSubmitted extends PermanentOrderEvent {
|
||||
const PermanentOrderSubmitted();
|
||||
}
|
||||
|
||||
class PermanentOrderInitialized extends PermanentOrderEvent {
|
||||
const PermanentOrderInitialized(this.data);
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[data];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'rapid_order_bloc.dart';
|
||||
export 'rapid_order_event.dart';
|
||||
export 'rapid_order_state.dart';
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:client_create_order/src/domain/arguments/rapid_order_arguments.dart';
|
||||
import 'package:client_create_order/src/domain/usecases/create_rapid_order_usecase.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.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';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'recurring_order_bloc.dart';
|
||||
export 'recurring_order_event.dart';
|
||||
export 'recurring_order_state.dart';
|
||||
@@ -1,17 +1,24 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:client_create_order/src/domain/usecases/create_recurring_order_usecase.dart';
|
||||
import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import '../../domain/usecases/create_recurring_order_usecase.dart';
|
||||
|
||||
import 'recurring_order_event.dart';
|
||||
import 'recurring_order_state.dart';
|
||||
|
||||
/// BLoC for managing the recurring order creation form.
|
||||
class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
with BlocErrorHandler<RecurringOrderState>, SafeBloc<RecurringOrderEvent, RecurringOrderState> {
|
||||
RecurringOrderBloc(this._createRecurringOrderUseCase, this._service)
|
||||
: super(RecurringOrderState.initial()) {
|
||||
with
|
||||
BlocErrorHandler<RecurringOrderState>,
|
||||
SafeBloc<RecurringOrderEvent, RecurringOrderState> {
|
||||
RecurringOrderBloc(
|
||||
this._createRecurringOrderUseCase,
|
||||
this._getOrderDetailsForReorderUseCase,
|
||||
this._service,
|
||||
) : super(RecurringOrderState.initial()) {
|
||||
on<RecurringOrderVendorsLoaded>(_onVendorsLoaded);
|
||||
on<RecurringOrderVendorChanged>(_onVendorChanged);
|
||||
on<RecurringOrderHubsLoaded>(_onHubsLoaded);
|
||||
@@ -24,12 +31,14 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
on<RecurringOrderPositionRemoved>(_onPositionRemoved);
|
||||
on<RecurringOrderPositionUpdated>(_onPositionUpdated);
|
||||
on<RecurringOrderSubmitted>(_onSubmitted);
|
||||
on<RecurringOrderInitialized>(_onInitialized);
|
||||
|
||||
_loadVendors();
|
||||
_loadHubs();
|
||||
}
|
||||
|
||||
final CreateRecurringOrderUseCase _createRecurringOrderUseCase;
|
||||
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
static const List<String> _dayLabels = <String>[
|
||||
@@ -45,8 +54,10 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
Future<void> _loadVendors() async {
|
||||
final List<domain.Vendor>? vendors = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final QueryResult<dc.ListVendorsData, void> result =
|
||||
await _service.connector.listVendors().execute();
|
||||
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
|
||||
.connector
|
||||
.listVendors()
|
||||
.execute();
|
||||
return result.data.vendors
|
||||
.map(
|
||||
(dc.ListVendorsVendors vendor) => domain.Vendor(
|
||||
@@ -71,7 +82,10 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
) async {
|
||||
final List<RecurringOrderRoleOption>? roles = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
|
||||
final fdc.QueryResult<
|
||||
dc.ListRolesByVendorIdData,
|
||||
dc.ListRolesByVendorIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listRolesByVendorId(vendorId: vendorId)
|
||||
.execute();
|
||||
@@ -85,7 +99,8 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
onError: (_) => emit(state.copyWith(roles: const <RecurringOrderRoleOption>[])),
|
||||
onError: (_) =>
|
||||
emit(state.copyWith(roles: const <RecurringOrderRoleOption>[])),
|
||||
);
|
||||
|
||||
if (roles != null) {
|
||||
@@ -97,7 +112,10 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
final List<RecurringOrderHubOption>? hubs = await handleErrorWithResult(
|
||||
action: () async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
|
||||
final fdc.QueryResult<
|
||||
dc.ListTeamHubsByOwnerIdData,
|
||||
dc.ListTeamHubsByOwnerIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||
.execute();
|
||||
@@ -119,7 +137,8 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
onError: (_) => add(const RecurringOrderHubsLoaded(<RecurringOrderHubOption>[])),
|
||||
onError: (_) =>
|
||||
add(const RecurringOrderHubsLoaded(<RecurringOrderHubOption>[])),
|
||||
);
|
||||
|
||||
if (hubs != null) {
|
||||
@@ -131,13 +150,11 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
RecurringOrderVendorsLoaded event,
|
||||
Emitter<RecurringOrderState> emit,
|
||||
) async {
|
||||
final domain.Vendor? selectedVendor =
|
||||
event.vendors.isNotEmpty ? event.vendors.first : null;
|
||||
final domain.Vendor? selectedVendor = event.vendors.isNotEmpty
|
||||
? event.vendors.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
vendors: event.vendors,
|
||||
selectedVendor: selectedVendor,
|
||||
),
|
||||
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
||||
);
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
@@ -156,8 +173,9 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
RecurringOrderHubsLoaded event,
|
||||
Emitter<RecurringOrderState> emit,
|
||||
) {
|
||||
final RecurringOrderHubOption? selectedHub =
|
||||
event.hubs.isNotEmpty ? event.hubs.first : null;
|
||||
final RecurringOrderHubOption? selectedHub = event.hubs.isNotEmpty
|
||||
? event.hubs.first
|
||||
: null;
|
||||
emit(
|
||||
state.copyWith(
|
||||
hubs: event.hubs,
|
||||
@@ -171,12 +189,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
RecurringOrderHubChanged event,
|
||||
Emitter<RecurringOrderState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedHub: event.hub,
|
||||
location: event.hub.name,
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
|
||||
}
|
||||
|
||||
void _onEventNameChanged(
|
||||
@@ -242,7 +255,12 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
} else {
|
||||
days.add(label);
|
||||
}
|
||||
emit(state.copyWith(recurringDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
|
||||
emit(
|
||||
state.copyWith(
|
||||
recurringDays: _sortDays(days),
|
||||
autoSelectedDayIndex: autoIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPositionAdded(
|
||||
@@ -343,6 +361,81 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onInitialized(
|
||||
RecurringOrderInitialized event,
|
||||
Emitter<RecurringOrderState> emit,
|
||||
) async {
|
||||
final Map<String, dynamic> data = event.data;
|
||||
final String title = data['title']?.toString() ?? '';
|
||||
final DateTime? startDate = data['startDate'] as DateTime?;
|
||||
final String? orderId = data['orderId']?.toString();
|
||||
|
||||
emit(
|
||||
state.copyWith(eventName: title, startDate: startDate ?? DateTime.now()),
|
||||
);
|
||||
|
||||
if (orderId == null || orderId.isEmpty) return;
|
||||
|
||||
emit(state.copyWith(status: RecurringOrderStatus.loading));
|
||||
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
final domain.ReorderData orderDetails =
|
||||
await _getOrderDetailsForReorderUseCase(orderId);
|
||||
|
||||
// Map positions
|
||||
final List<RecurringOrderPosition> positions = orderDetails.positions
|
||||
.map((domain.ReorderPosition role) {
|
||||
return RecurringOrderPosition(
|
||||
role: role.roleId,
|
||||
count: role.count,
|
||||
startTime: role.startTime,
|
||||
endTime: role.endTime,
|
||||
lunchBreak: role.lunchBreak,
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Update state with order details
|
||||
final domain.Vendor? selectedVendor = state.vendors
|
||||
.where((domain.Vendor v) => v.id == orderDetails.vendorId)
|
||||
.firstOrNull;
|
||||
|
||||
final RecurringOrderHubOption? selectedHub = state.hubs
|
||||
.where(
|
||||
(RecurringOrderHubOption h) =>
|
||||
h.placeId == orderDetails.hub.placeId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
eventName: orderDetails.eventName.isNotEmpty
|
||||
? orderDetails.eventName
|
||||
: title,
|
||||
positions: positions,
|
||||
selectedVendor: selectedVendor,
|
||||
selectedHub: selectedHub,
|
||||
location: selectedHub?.name ?? '',
|
||||
status: RecurringOrderStatus.initial,
|
||||
startDate: startDate ?? orderDetails.startDate ?? DateTime.now(),
|
||||
endDate: orderDetails.endDate ?? DateTime.now(),
|
||||
recurringDays: orderDetails.recurringDays,
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedVendor != null) {
|
||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: RecurringOrderStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<String> _sortDays(List<String> days) {
|
||||
days.sort(
|
||||
(String a, String b) =>
|
||||
@@ -107,3 +107,11 @@ class RecurringOrderPositionUpdated extends RecurringOrderEvent {
|
||||
class RecurringOrderSubmitted extends RecurringOrderEvent {
|
||||
const RecurringOrderSubmitted();
|
||||
}
|
||||
|
||||
class RecurringOrderInitialized extends RecurringOrderEvent {
|
||||
const RecurringOrderInitialized(this.data);
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[data];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/create_order/create_order_view.dart';
|
||||
|
||||
/// Main entry page for the client create order flow.
|
||||
///
|
||||
/// This page displays the [CreateOrderView].
|
||||
/// It follows the Krow Clean Architecture by being a [StatelessWidget] and
|
||||
/// delegating its UI to other components.
|
||||
class ClientCreateOrderPage extends StatelessWidget {
|
||||
/// Creates a [ClientCreateOrderPage].
|
||||
const ClientCreateOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const CreateOrderView();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../blocs/one_time_order/one_time_order_bloc.dart';
|
||||
import '../blocs/one_time_order/one_time_order_event.dart';
|
||||
import '../blocs/one_time_order/one_time_order_state.dart';
|
||||
|
||||
/// Page for creating a one-time staffing order.
|
||||
/// Users can specify the date, location, and multiple staff positions required.
|
||||
///
|
||||
/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView]
|
||||
/// from the common orders package. It follows the Krow Clean Architecture by being
|
||||
/// a [StatelessWidget] and mapping local BLoC state to generic UI models.
|
||||
class OneTimeOrderPage extends StatelessWidget {
|
||||
/// Creates a [OneTimeOrderPage].
|
||||
const OneTimeOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<OneTimeOrderBloc>(
|
||||
create: (BuildContext context) {
|
||||
final OneTimeOrderBloc bloc = Modular.get<OneTimeOrderBloc>();
|
||||
final dynamic args = Modular.args.data;
|
||||
if (args is Map<String, dynamic>) {
|
||||
bloc.add(OneTimeOrderInitialized(args));
|
||||
}
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<OneTimeOrderBloc, OneTimeOrderState>(
|
||||
builder: (BuildContext context, OneTimeOrderState state) {
|
||||
final OneTimeOrderBloc bloc = BlocProvider.of<OneTimeOrderBloc>(
|
||||
context,
|
||||
);
|
||||
|
||||
return OneTimeOrderView(
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
selectedVendor: state.selectedVendor,
|
||||
vendors: state.vendors,
|
||||
date: state.date,
|
||||
selectedHub: state.selectedHub != null
|
||||
? _mapHub(state.selectedHub!)
|
||||
: null,
|
||||
hubs: state.hubs.map(_mapHub).toList(),
|
||||
positions: state.positions.map(_mapPosition).toList(),
|
||||
roles: state.roles.map(_mapRole).toList(),
|
||||
isValid: state.isValid,
|
||||
onEventNameChanged: (String val) =>
|
||||
bloc.add(OneTimeOrderEventNameChanged(val)),
|
||||
onVendorChanged: (Vendor val) =>
|
||||
bloc.add(OneTimeOrderVendorChanged(val)),
|
||||
onDateChanged: (DateTime val) =>
|
||||
bloc.add(OneTimeOrderDateChanged(val)),
|
||||
onHubChanged: (OrderHubUiModel val) {
|
||||
final OneTimeOrderHubOption originalHub = state.hubs.firstWhere(
|
||||
(OneTimeOrderHubOption h) => h.id == val.id,
|
||||
);
|
||||
bloc.add(OneTimeOrderHubChanged(originalHub));
|
||||
},
|
||||
onPositionAdded: () => bloc.add(const OneTimeOrderPositionAdded()),
|
||||
onPositionUpdated: (int index, OrderPositionUiModel val) {
|
||||
final OneTimeOrderPosition original = state.positions[index];
|
||||
final OneTimeOrderPosition updated = original.copyWith(
|
||||
role: val.role,
|
||||
count: val.count,
|
||||
startTime: val.startTime,
|
||||
endTime: val.endTime,
|
||||
lunchBreak: val.lunchBreak,
|
||||
);
|
||||
bloc.add(OneTimeOrderPositionUpdated(index, updated));
|
||||
},
|
||||
onPositionRemoved: (int index) =>
|
||||
bloc.add(OneTimeOrderPositionRemoved(index)),
|
||||
onSubmit: () => bloc.add(const OneTimeOrderSubmitted()),
|
||||
onDone: () => Modular.to.toOrdersSpecificDate(state.date),
|
||||
onBack: () => Modular.to.pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OrderFormStatus _mapStatus(OneTimeOrderStatus status) {
|
||||
switch (status) {
|
||||
case OneTimeOrderStatus.initial:
|
||||
return OrderFormStatus.initial;
|
||||
case OneTimeOrderStatus.loading:
|
||||
return OrderFormStatus.loading;
|
||||
case OneTimeOrderStatus.success:
|
||||
return OrderFormStatus.success;
|
||||
case OneTimeOrderStatus.failure:
|
||||
return OrderFormStatus.failure;
|
||||
}
|
||||
}
|
||||
|
||||
OrderHubUiModel _mapHub(OneTimeOrderHubOption hub) {
|
||||
return OrderHubUiModel(
|
||||
id: hub.id,
|
||||
name: hub.name,
|
||||
address: hub.address,
|
||||
placeId: hub.placeId,
|
||||
latitude: hub.latitude,
|
||||
longitude: hub.longitude,
|
||||
city: hub.city,
|
||||
state: hub.state,
|
||||
street: hub.street,
|
||||
country: hub.country,
|
||||
zipCode: hub.zipCode,
|
||||
);
|
||||
}
|
||||
|
||||
OrderRoleUiModel _mapRole(OneTimeOrderRoleOption role) {
|
||||
return OrderRoleUiModel(
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
costPerHour: role.costPerHour,
|
||||
);
|
||||
}
|
||||
|
||||
OrderPositionUiModel _mapPosition(OneTimeOrderPosition pos) {
|
||||
return OrderPositionUiModel(
|
||||
role: pos.role,
|
||||
count: pos.count,
|
||||
startTime: pos.startTime,
|
||||
endTime: pos.endTime,
|
||||
lunchBreak: pos.lunchBreak,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' hide PermanentOrderPosition;
|
||||
import '../blocs/permanent_order/permanent_order_bloc.dart';
|
||||
import '../blocs/permanent_order/permanent_order_event.dart';
|
||||
import '../blocs/permanent_order/permanent_order_state.dart';
|
||||
|
||||
/// Page for creating a permanent staffing order.
|
||||
class PermanentOrderPage extends StatelessWidget {
|
||||
/// Creates a [PermanentOrderPage].
|
||||
const PermanentOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<PermanentOrderBloc>(
|
||||
create: (BuildContext context) {
|
||||
final PermanentOrderBloc bloc = Modular.get<PermanentOrderBloc>();
|
||||
final dynamic args = Modular.args.data;
|
||||
if (args is Map<String, dynamic>) {
|
||||
bloc.add(PermanentOrderInitialized(args));
|
||||
}
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<PermanentOrderBloc, PermanentOrderState>(
|
||||
builder: (BuildContext context, PermanentOrderState state) {
|
||||
final PermanentOrderBloc bloc = BlocProvider.of<PermanentOrderBloc>(
|
||||
context,
|
||||
);
|
||||
|
||||
return PermanentOrderView(
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
selectedVendor: state.selectedVendor,
|
||||
vendors: state.vendors,
|
||||
startDate: state.startDate,
|
||||
permanentDays: state.permanentDays,
|
||||
selectedHub: state.selectedHub != null
|
||||
? _mapHub(state.selectedHub!)
|
||||
: null,
|
||||
hubs: state.hubs.map(_mapHub).toList(),
|
||||
positions: state.positions.map(_mapPosition).toList(),
|
||||
roles: state.roles.map(_mapRole).toList(),
|
||||
isValid: state.isValid,
|
||||
onEventNameChanged: (String val) =>
|
||||
bloc.add(PermanentOrderEventNameChanged(val)),
|
||||
onVendorChanged: (Vendor val) =>
|
||||
bloc.add(PermanentOrderVendorChanged(val)),
|
||||
onStartDateChanged: (DateTime val) =>
|
||||
bloc.add(PermanentOrderStartDateChanged(val)),
|
||||
onDayToggled: (int index) =>
|
||||
bloc.add(PermanentOrderDayToggled(index)),
|
||||
onHubChanged: (OrderHubUiModel val) {
|
||||
final PermanentOrderHubOption originalHub = state.hubs.firstWhere(
|
||||
(PermanentOrderHubOption h) => h.id == val.id,
|
||||
);
|
||||
bloc.add(PermanentOrderHubChanged(originalHub));
|
||||
},
|
||||
onPositionAdded: () =>
|
||||
bloc.add(const PermanentOrderPositionAdded()),
|
||||
onPositionUpdated: (int index, OrderPositionUiModel val) {
|
||||
final PermanentOrderPosition original = state.positions[index];
|
||||
final PermanentOrderPosition updated = original.copyWith(
|
||||
role: val.role,
|
||||
count: val.count,
|
||||
startTime: val.startTime,
|
||||
endTime: val.endTime,
|
||||
lunchBreak: val.lunchBreak,
|
||||
);
|
||||
bloc.add(PermanentOrderPositionUpdated(index, updated));
|
||||
},
|
||||
onPositionRemoved: (int index) =>
|
||||
bloc.add(PermanentOrderPositionRemoved(index)),
|
||||
onSubmit: () => bloc.add(const PermanentOrderSubmitted()),
|
||||
onDone: () {
|
||||
final DateTime initialDate = _firstPermanentShiftDate(
|
||||
state.startDate,
|
||||
state.permanentDays,
|
||||
);
|
||||
|
||||
// Navigate to orders page with the initial date set to the first recurring shift date
|
||||
Modular.to.toOrdersSpecificDate(initialDate);
|
||||
},
|
||||
onBack: () => Modular.to.pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _firstPermanentShiftDate(
|
||||
DateTime startDate,
|
||||
List<String> permanentDays,
|
||||
) {
|
||||
final DateTime start = DateTime(
|
||||
startDate.year,
|
||||
startDate.month,
|
||||
startDate.day,
|
||||
);
|
||||
final DateTime end = start.add(const Duration(days: 29));
|
||||
final Set<String> selected = permanentDays.toSet();
|
||||
for (
|
||||
DateTime day = start;
|
||||
!day.isAfter(end);
|
||||
day = day.add(const Duration(days: 1))
|
||||
) {
|
||||
if (selected.contains(_weekdayLabel(day))) {
|
||||
return day;
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
String _weekdayLabel(DateTime date) {
|
||||
switch (date.weekday) {
|
||||
case DateTime.monday:
|
||||
return 'MON';
|
||||
case DateTime.tuesday:
|
||||
return 'TUE';
|
||||
case DateTime.wednesday:
|
||||
return 'WED';
|
||||
case DateTime.thursday:
|
||||
return 'THU';
|
||||
case DateTime.friday:
|
||||
return 'FRI';
|
||||
case DateTime.saturday:
|
||||
return 'SAT';
|
||||
case DateTime.sunday:
|
||||
return 'SUN';
|
||||
default:
|
||||
return 'SUN';
|
||||
}
|
||||
}
|
||||
|
||||
OrderFormStatus _mapStatus(PermanentOrderStatus status) {
|
||||
switch (status) {
|
||||
case PermanentOrderStatus.initial:
|
||||
return OrderFormStatus.initial;
|
||||
case PermanentOrderStatus.loading:
|
||||
return OrderFormStatus.loading;
|
||||
case PermanentOrderStatus.success:
|
||||
return OrderFormStatus.success;
|
||||
case PermanentOrderStatus.failure:
|
||||
return OrderFormStatus.failure;
|
||||
}
|
||||
}
|
||||
|
||||
OrderHubUiModel _mapHub(PermanentOrderHubOption hub) {
|
||||
return OrderHubUiModel(
|
||||
id: hub.id,
|
||||
name: hub.name,
|
||||
address: hub.address,
|
||||
placeId: hub.placeId,
|
||||
latitude: hub.latitude,
|
||||
longitude: hub.longitude,
|
||||
city: hub.city,
|
||||
state: hub.state,
|
||||
street: hub.street,
|
||||
country: hub.country,
|
||||
zipCode: hub.zipCode,
|
||||
);
|
||||
}
|
||||
|
||||
OrderRoleUiModel _mapRole(PermanentOrderRoleOption role) {
|
||||
return OrderRoleUiModel(
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
costPerHour: role.costPerHour,
|
||||
);
|
||||
}
|
||||
|
||||
OrderPositionUiModel _mapPosition(PermanentOrderPosition pos) {
|
||||
return OrderPositionUiModel(
|
||||
role: pos.role,
|
||||
count: pos.count,
|
||||
startTime: pos.startTime,
|
||||
endTime: pos.endTime,
|
||||
lunchBreak: pos.lunchBreak ?? 'NO_BREAK',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import '../blocs/rapid_order_bloc.dart';
|
||||
import '../blocs/rapid_order/rapid_order_bloc.dart';
|
||||
import '../widgets/rapid_order/rapid_order_view.dart';
|
||||
|
||||
/// Rapid Order Flow Page - Emergency staffing requests.
|
||||
@@ -0,0 +1,196 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:client_orders_common/client_orders_common.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart' hide RecurringOrderPosition;
|
||||
import '../blocs/recurring_order/recurring_order_bloc.dart';
|
||||
import '../blocs/recurring_order/recurring_order_event.dart';
|
||||
import '../blocs/recurring_order/recurring_order_state.dart';
|
||||
|
||||
/// Page for creating a recurring staffing order.
|
||||
class RecurringOrderPage extends StatelessWidget {
|
||||
/// Creates a [RecurringOrderPage].
|
||||
const RecurringOrderPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RecurringOrderBloc>(
|
||||
create: (BuildContext context) {
|
||||
final RecurringOrderBloc bloc = Modular.get<RecurringOrderBloc>();
|
||||
final dynamic args = Modular.args.data;
|
||||
if (args is Map<String, dynamic>) {
|
||||
bloc.add(RecurringOrderInitialized(args));
|
||||
}
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<RecurringOrderBloc, RecurringOrderState>(
|
||||
builder: (BuildContext context, RecurringOrderState state) {
|
||||
final RecurringOrderBloc bloc = BlocProvider.of<RecurringOrderBloc>(
|
||||
context,
|
||||
);
|
||||
|
||||
return RecurringOrderView(
|
||||
status: _mapStatus(state.status),
|
||||
errorMessage: state.errorMessage,
|
||||
eventName: state.eventName,
|
||||
selectedVendor: state.selectedVendor,
|
||||
vendors: state.vendors,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
recurringDays: state.recurringDays,
|
||||
selectedHub: state.selectedHub != null
|
||||
? _mapHub(state.selectedHub!)
|
||||
: null,
|
||||
hubs: state.hubs.map(_mapHub).toList(),
|
||||
positions: state.positions.map(_mapPosition).toList(),
|
||||
roles: state.roles.map(_mapRole).toList(),
|
||||
isValid: state.isValid,
|
||||
onEventNameChanged: (String val) =>
|
||||
bloc.add(RecurringOrderEventNameChanged(val)),
|
||||
onVendorChanged: (Vendor val) =>
|
||||
bloc.add(RecurringOrderVendorChanged(val)),
|
||||
onStartDateChanged: (DateTime val) =>
|
||||
bloc.add(RecurringOrderStartDateChanged(val)),
|
||||
onEndDateChanged: (DateTime val) =>
|
||||
bloc.add(RecurringOrderEndDateChanged(val)),
|
||||
onDayToggled: (int index) =>
|
||||
bloc.add(RecurringOrderDayToggled(index)),
|
||||
onHubChanged: (OrderHubUiModel val) {
|
||||
final RecurringOrderHubOption originalHub = state.hubs.firstWhere(
|
||||
(RecurringOrderHubOption h) => h.id == val.id,
|
||||
);
|
||||
bloc.add(RecurringOrderHubChanged(originalHub));
|
||||
},
|
||||
onPositionAdded: () =>
|
||||
bloc.add(const RecurringOrderPositionAdded()),
|
||||
onPositionUpdated: (int index, OrderPositionUiModel val) {
|
||||
final RecurringOrderPosition original = state.positions[index];
|
||||
final RecurringOrderPosition updated = original.copyWith(
|
||||
role: val.role,
|
||||
count: val.count,
|
||||
startTime: val.startTime,
|
||||
endTime: val.endTime,
|
||||
lunchBreak: val.lunchBreak,
|
||||
);
|
||||
bloc.add(RecurringOrderPositionUpdated(index, updated));
|
||||
},
|
||||
onPositionRemoved: (int index) =>
|
||||
bloc.add(RecurringOrderPositionRemoved(index)),
|
||||
onSubmit: () => bloc.add(const RecurringOrderSubmitted()),
|
||||
onDone: () {
|
||||
final DateTime maxEndDate = state.startDate.add(
|
||||
const Duration(days: 29),
|
||||
);
|
||||
final DateTime effectiveEndDate =
|
||||
state.endDate.isAfter(maxEndDate)
|
||||
? maxEndDate
|
||||
: state.endDate;
|
||||
final DateTime initialDate = _firstRecurringShiftDate(
|
||||
state.startDate,
|
||||
effectiveEndDate,
|
||||
state.recurringDays,
|
||||
);
|
||||
|
||||
// Navigate to orders page with the initial date set to the first recurring shift date
|
||||
Modular.to.toOrdersSpecificDate(initialDate);
|
||||
},
|
||||
onBack: () => Modular.to.pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _firstRecurringShiftDate(
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
List<String> recurringDays,
|
||||
) {
|
||||
final DateTime start = DateTime(
|
||||
startDate.year,
|
||||
startDate.month,
|
||||
startDate.day,
|
||||
);
|
||||
final DateTime end = DateTime(endDate.year, endDate.month, endDate.day);
|
||||
final Set<String> selected = recurringDays.toSet();
|
||||
for (
|
||||
DateTime day = start;
|
||||
!day.isAfter(end);
|
||||
day = day.add(const Duration(days: 1))
|
||||
) {
|
||||
if (selected.contains(_weekdayLabel(day))) {
|
||||
return day;
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
String _weekdayLabel(DateTime date) {
|
||||
switch (date.weekday) {
|
||||
case DateTime.monday:
|
||||
return 'MON';
|
||||
case DateTime.tuesday:
|
||||
return 'TUE';
|
||||
case DateTime.wednesday:
|
||||
return 'WED';
|
||||
case DateTime.thursday:
|
||||
return 'THU';
|
||||
case DateTime.friday:
|
||||
return 'FRI';
|
||||
case DateTime.saturday:
|
||||
return 'SAT';
|
||||
case DateTime.sunday:
|
||||
return 'SUN';
|
||||
default:
|
||||
return 'SUN';
|
||||
}
|
||||
}
|
||||
|
||||
OrderFormStatus _mapStatus(RecurringOrderStatus status) {
|
||||
switch (status) {
|
||||
case RecurringOrderStatus.initial:
|
||||
return OrderFormStatus.initial;
|
||||
case RecurringOrderStatus.loading:
|
||||
return OrderFormStatus.loading;
|
||||
case RecurringOrderStatus.success:
|
||||
return OrderFormStatus.success;
|
||||
case RecurringOrderStatus.failure:
|
||||
return OrderFormStatus.failure;
|
||||
}
|
||||
}
|
||||
|
||||
OrderHubUiModel _mapHub(RecurringOrderHubOption hub) {
|
||||
return OrderHubUiModel(
|
||||
id: hub.id,
|
||||
name: hub.name,
|
||||
address: hub.address,
|
||||
placeId: hub.placeId,
|
||||
latitude: hub.latitude,
|
||||
longitude: hub.longitude,
|
||||
city: hub.city,
|
||||
state: hub.state,
|
||||
street: hub.street,
|
||||
country: hub.country,
|
||||
zipCode: hub.zipCode,
|
||||
);
|
||||
}
|
||||
|
||||
OrderRoleUiModel _mapRole(RecurringOrderRoleOption role) {
|
||||
return OrderRoleUiModel(
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
costPerHour: role.costPerHour,
|
||||
);
|
||||
}
|
||||
|
||||
OrderPositionUiModel _mapPosition(RecurringOrderPosition pos) {
|
||||
return OrderPositionUiModel(
|
||||
role: pos.role,
|
||||
count: pos.count,
|
||||
startTime: pos.startTime,
|
||||
endTime: pos.endTime,
|
||||
lunchBreak: pos.lunchBreak ?? 'NO_BREAK',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
class UiOrderType {
|
||||
const UiOrderType({
|
||||
required this.id,
|
||||
required this.titleKey,
|
||||
required this.descriptionKey,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String titleKey;
|
||||
final String descriptionKey;
|
||||
}
|
||||
|
||||
/// Order type constants for the create order feature
|
||||
const List<UiOrderType> orderTypes = <UiOrderType>[
|
||||
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
|
||||
// UiOrderType(
|
||||
// id: 'rapid',
|
||||
// titleKey: 'client_create_order.types.rapid',
|
||||
// descriptionKey: 'client_create_order.types.rapid_desc',
|
||||
// ),
|
||||
UiOrderType(
|
||||
id: 'one-time',
|
||||
titleKey: 'client_create_order.types.one_time',
|
||||
descriptionKey: 'client_create_order.types.one_time_desc',
|
||||
),
|
||||
|
||||
UiOrderType(
|
||||
id: 'recurring',
|
||||
titleKey: 'client_create_order.types.recurring',
|
||||
descriptionKey: 'client_create_order.types.recurring_desc',
|
||||
),
|
||||
UiOrderType(
|
||||
id: 'permanent',
|
||||
titleKey: 'client_create_order.types.permanent',
|
||||
descriptionKey: 'client_create_order.types.permanent_desc',
|
||||
),
|
||||
];
|
||||
@@ -18,44 +18,44 @@ class OrderTypeUiMetadata {
|
||||
factory OrderTypeUiMetadata.fromId({required String id}) {
|
||||
switch (id) {
|
||||
case 'rapid':
|
||||
return const OrderTypeUiMetadata(
|
||||
return OrderTypeUiMetadata(
|
||||
icon: UiIcons.zap,
|
||||
backgroundColor: UiColors.tagPending,
|
||||
borderColor: UiColors.separatorSpecial,
|
||||
iconBackgroundColor: UiColors.textWarning,
|
||||
iconColor: UiColors.white,
|
||||
textColor: UiColors.textWarning,
|
||||
descriptionColor: UiColors.textWarning,
|
||||
backgroundColor: UiColors.iconError.withAlpha(24),
|
||||
borderColor: UiColors.iconError,
|
||||
iconBackgroundColor: UiColors.iconError.withAlpha(24),
|
||||
iconColor: UiColors.iconError,
|
||||
textColor: UiColors.iconError,
|
||||
descriptionColor: UiColors.iconError,
|
||||
);
|
||||
case 'one-time':
|
||||
return const OrderTypeUiMetadata(
|
||||
return OrderTypeUiMetadata(
|
||||
icon: UiIcons.calendar,
|
||||
backgroundColor: UiColors.tagInProgress,
|
||||
borderColor: UiColors.primaryInverse,
|
||||
iconBackgroundColor: UiColors.primary,
|
||||
iconColor: UiColors.white,
|
||||
textColor: UiColors.textLink,
|
||||
descriptionColor: UiColors.textLink,
|
||||
backgroundColor: UiColors.primary.withAlpha(24),
|
||||
borderColor: UiColors.primary,
|
||||
iconBackgroundColor: UiColors.primary.withAlpha(24),
|
||||
iconColor: UiColors.primary,
|
||||
textColor: UiColors.primary,
|
||||
descriptionColor: UiColors.primary,
|
||||
);
|
||||
case 'recurring':
|
||||
return const OrderTypeUiMetadata(
|
||||
icon: UiIcons.rotateCcw,
|
||||
backgroundColor: UiColors.tagSuccess,
|
||||
borderColor: UiColors.switchActive,
|
||||
iconBackgroundColor: UiColors.textSuccess,
|
||||
iconColor: UiColors.white,
|
||||
case 'permanent':
|
||||
return OrderTypeUiMetadata(
|
||||
icon: UiIcons.users,
|
||||
backgroundColor: UiColors.textSuccess.withAlpha(24),
|
||||
borderColor: UiColors.textSuccess,
|
||||
iconBackgroundColor: UiColors.textSuccess.withAlpha(24),
|
||||
iconColor: UiColors.textSuccess,
|
||||
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,
|
||||
case 'recurring':
|
||||
return OrderTypeUiMetadata(
|
||||
icon: UiIcons.rotateCcw,
|
||||
backgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
|
||||
borderColor: const Color.fromARGB(255, 170, 10, 223),
|
||||
iconBackgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
|
||||
iconColor: const Color.fromARGB(255, 170, 10, 223),
|
||||
textColor: const Color.fromARGB(255, 170, 10, 223),
|
||||
descriptionColor: const Color.fromARGB(255, 170, 10, 223),
|
||||
);
|
||||
default:
|
||||
return const OrderTypeUiMetadata(
|
||||
@@ -0,0 +1,112 @@
|
||||
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';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../utils/constants/order_types.dart';
|
||||
import '../../utils/ui_entities/order_type_ui_metadata.dart';
|
||||
import '../order_type_card.dart';
|
||||
|
||||
/// 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') {
|
||||
return t.client_create_order.types.rapid_desc;
|
||||
} else if (key == 'client_create_order.types.one_time') {
|
||||
return t.client_create_order.types.one_time;
|
||||
} else if (key == 'client_create_order.types.one_time_desc') {
|
||||
return t.client_create_order.types.one_time_desc;
|
||||
} else if (key == 'client_create_order.types.recurring') {
|
||||
return t.client_create_order.types.recurring;
|
||||
} else if (key == 'client_create_order.types.recurring_desc') {
|
||||
return t.client_create_order.types.recurring_desc;
|
||||
} else if (key == 'client_create_order.types.permanent') {
|
||||
return t.client_create_order.types.permanent;
|
||||
} else if (key == 'client_create_order.types.permanent_desc') {
|
||||
return t.client_create_order.types.permanent_desc;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/// The main content of the Create Order page.
|
||||
class CreateOrderView extends StatelessWidget {
|
||||
/// Creates a [CreateOrderView].
|
||||
const CreateOrderView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
title: t.client_create_order.title,
|
||||
onLeadingPressed: () => Modular.to.toClientHome(),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space6),
|
||||
child: Text(
|
||||
t.client_create_order.section_title,
|
||||
style: UiTypography.body2m.textDescription,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: UiConstants.space4,
|
||||
crossAxisSpacing: UiConstants.space4,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: orderTypes.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final UiOrderType type = orderTypes[index];
|
||||
final OrderTypeUiMetadata ui = OrderTypeUiMetadata.fromId(
|
||||
id: type.id,
|
||||
);
|
||||
|
||||
return OrderTypeCard(
|
||||
icon: ui.icon,
|
||||
title: _getTranslation(key: type.titleKey),
|
||||
description: _getTranslation(key: type.descriptionKey),
|
||||
backgroundColor: ui.backgroundColor,
|
||||
borderColor: ui.borderColor,
|
||||
iconBackgroundColor: ui.iconBackgroundColor,
|
||||
iconColor: ui.iconColor,
|
||||
textColor: ui.textColor,
|
||||
descriptionColor: ui.descriptionColor,
|
||||
onTap: () {
|
||||
switch (type.id) {
|
||||
case 'rapid':
|
||||
Modular.to.toCreateOrderRapid();
|
||||
break;
|
||||
case 'one-time':
|
||||
Modular.to.toCreateOrderOneTime();
|
||||
break;
|
||||
case 'recurring':
|
||||
Modular.to.toCreateOrderRecurring();
|
||||
break;
|
||||
case 'permanent':
|
||||
Modular.to.toCreateOrderPermanent();
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class OrderTypeCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
border: Border.all(color: borderColor, width: 0.75),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -73,8 +73,7 @@ class OrderTypeCard extends StatelessWidget {
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
),
|
||||
Text(title, style: UiTypography.body2b.copyWith(color: textColor)),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(title, style: UiTypography.body1b.copyWith(color: textColor)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
@@ -5,9 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../../blocs/rapid_order_bloc.dart';
|
||||
import '../../blocs/rapid_order_event.dart';
|
||||
import '../../blocs/rapid_order_state.dart';
|
||||
import '../../blocs/rapid_order/rapid_order_bloc.dart';
|
||||
import '../../blocs/rapid_order/rapid_order_event.dart';
|
||||
import '../../blocs/rapid_order/rapid_order_state.dart';
|
||||
import 'rapid_order_example_card.dart';
|
||||
import 'rapid_order_header.dart';
|
||||
import 'rapid_order_success_view.dart';
|
||||
@@ -295,7 +295,6 @@ class _RapidOrderActions extends StatelessWidget {
|
||||
onPressed: isSubmitting || isMessageEmpty
|
||||
? null
|
||||
: () {
|
||||
print('RapidOrder send pressed');
|
||||
BlocProvider.of<RapidOrderBloc>(
|
||||
context,
|
||||
).add(const RapidOrderSubmitted());
|
||||
@@ -15,15 +15,17 @@ dependencies:
|
||||
equatable: ^2.0.5
|
||||
intl: 0.20.2
|
||||
design_system:
|
||||
path: ../../../design_system
|
||||
path: ../../../../design_system
|
||||
core_localization:
|
||||
path: ../../../core_localization
|
||||
path: ../../../../core_localization
|
||||
krow_domain:
|
||||
path: ../../../domain
|
||||
path: ../../../../domain
|
||||
krow_core:
|
||||
path: ../../../core
|
||||
path: ../../../../core
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
path: ../../../../data_connect
|
||||
client_orders_common:
|
||||
path: ../orders_common
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
|
||||
45
apps/mobile/packages/features/client/orders/orders_common/.gitignore
vendored
Normal file
45
apps/mobile/packages/features/client/orders/orders_common/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
@@ -0,0 +1,45 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
- platform: android
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
- platform: ios
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
- platform: linux
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
- platform: macos
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
- platform: web
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
- platform: windows
|
||||
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,3 @@
|
||||
# orders
|
||||
|
||||
A new Flutter project.
|
||||
@@ -0,0 +1 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
14
apps/mobile/packages/features/client/orders/orders_common/android/.gitignore
vendored
Normal file
14
apps/mobile/packages/features/client/orders/orders_common/android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.orders"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.orders"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="orders"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.orders
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
Binary file not shown.
|
After Width: | Height: | Size: 442 B |
Binary file not shown.
|
After Width: | Height: | Size: 721 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user