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.
|
/// Pushes the order creation flow entry page.
|
||||||
///
|
///
|
||||||
/// This is the starting point for all order creation flows.
|
/// This is the starting point for all order creation flows.
|
||||||
void toCreateOrder() {
|
void toCreateOrder({Object? arguments}) {
|
||||||
pushNamed(ClientPaths.createOrder);
|
navigate(ClientPaths.createOrder, arguments: arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the rapid order creation flow.
|
/// Pushes the rapid order creation flow.
|
||||||
///
|
///
|
||||||
/// Quick shift creation with simplified inputs for urgent needs.
|
/// Quick shift creation with simplified inputs for urgent needs.
|
||||||
void toCreateOrderRapid() {
|
void toCreateOrderRapid({Object? arguments}) {
|
||||||
pushNamed(ClientPaths.createOrderRapid);
|
pushNamed(ClientPaths.createOrderRapid, arguments: arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the one-time order creation flow.
|
/// Pushes the one-time order creation flow.
|
||||||
///
|
///
|
||||||
/// Create a shift that occurs once at a specific date and time.
|
/// Create a shift that occurs once at a specific date and time.
|
||||||
void toCreateOrderOneTime() {
|
void toCreateOrderOneTime({Object? arguments}) {
|
||||||
pushNamed(ClientPaths.createOrderOneTime);
|
pushNamed(ClientPaths.createOrderOneTime, arguments: arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the recurring order creation flow.
|
/// Pushes the recurring order creation flow.
|
||||||
///
|
///
|
||||||
/// Create shifts that repeat on a defined schedule (daily, weekly, etc.).
|
/// Create shifts that repeat on a defined schedule (daily, weekly, etc.).
|
||||||
void toCreateOrderRecurring() {
|
void toCreateOrderRecurring({Object? arguments}) {
|
||||||
pushNamed(ClientPaths.createOrderRecurring);
|
pushNamed(ClientPaths.createOrderRecurring, arguments: arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the permanent order creation flow.
|
/// Pushes the permanent order creation flow.
|
||||||
///
|
///
|
||||||
/// Create a long-term or permanent staffing position.
|
/// Create a long-term or permanent staffing position.
|
||||||
void toCreateOrderPermanent() {
|
void toCreateOrderPermanent({Object? arguments}) {
|
||||||
pushNamed(ClientPaths.createOrderPermanent);
|
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},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ extension StaffNavigator on IModularNavigator {
|
|||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
/// Navigates to the root get started/authentication screen.
|
/// Navigates to the root get started/authentication screen.
|
||||||
///
|
///
|
||||||
/// This effectively logs out the user by navigating to root.
|
/// This effectively logs out the user by navigating to root.
|
||||||
/// Used when signing out or session expires.
|
/// Used when signing out or session expires.
|
||||||
void toInitialPage() {
|
void toInitialPage() {
|
||||||
@@ -37,7 +37,7 @@ extension StaffNavigator on IModularNavigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the get started page.
|
/// Navigates to the get started page.
|
||||||
///
|
///
|
||||||
/// This is the landing page for unauthenticated users, offering login/signup options.
|
/// This is the landing page for unauthenticated users, offering login/signup options.
|
||||||
void toGetStartedPage() {
|
void toGetStartedPage() {
|
||||||
navigate(StaffPaths.getStarted);
|
navigate(StaffPaths.getStarted);
|
||||||
@@ -64,7 +64,7 @@ extension StaffNavigator on IModularNavigator {
|
|||||||
/// This is typically called after successful phone verification for new
|
/// This is typically called after successful phone verification for new
|
||||||
/// staff members. Uses pushReplacement to prevent going back to verification.
|
/// staff members. Uses pushReplacement to prevent going back to verification.
|
||||||
void toProfileSetup() {
|
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.
|
/// This is the main landing page for authenticated staff members.
|
||||||
/// Displays shift cards, quick actions, and notifications.
|
/// Displays shift cards, quick actions, and notifications.
|
||||||
void toStaffHome() {
|
void toStaffHome() {
|
||||||
pushNamed(StaffPaths.home);
|
pushNamedAndRemoveUntil(StaffPaths.home, (_) => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the staff main shell.
|
/// 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
|
/// This is the container with bottom navigation. Navigates to home tab
|
||||||
/// by default. Usually you'd navigate to a specific tab instead.
|
/// by default. Usually you'd navigate to a specific tab instead.
|
||||||
void toStaffMain() {
|
void toStaffMain() {
|
||||||
navigate('${StaffPaths.main}/home/');
|
pushNamedAndRemoveUntil('${StaffPaths.main}/home/', (_) => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
@@ -113,31 +113,28 @@ extension StaffNavigator on IModularNavigator {
|
|||||||
if (refreshAvailable == true) {
|
if (refreshAvailable == true) {
|
||||||
args['refreshAvailable'] = true;
|
args['refreshAvailable'] = true;
|
||||||
}
|
}
|
||||||
navigate(
|
navigate(StaffPaths.shifts, arguments: args.isEmpty ? null : args);
|
||||||
StaffPaths.shifts,
|
|
||||||
arguments: args.isEmpty ? null : args,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the Payments tab.
|
/// Navigates to the Payments tab.
|
||||||
///
|
///
|
||||||
/// View payment history, earnings breakdown, and tax information.
|
/// View payment history, earnings breakdown, and tax information.
|
||||||
void toPayments() {
|
void toPayments() {
|
||||||
navigate(StaffPaths.payments);
|
pushNamedAndRemoveUntil(StaffPaths.payments, (_) => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the Clock In tab.
|
/// Navigates to the Clock In tab.
|
||||||
///
|
///
|
||||||
/// Access time tracking interface for active shifts.
|
/// Access time tracking interface for active shifts.
|
||||||
void toClockIn() {
|
void toClockIn() {
|
||||||
navigate(StaffPaths.clockIn);
|
pushNamedAndRemoveUntil(StaffPaths.clockIn, (_) => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the Profile tab.
|
/// Navigates to the Profile tab.
|
||||||
///
|
///
|
||||||
/// Manage personal information, documents, and preferences.
|
/// Manage personal information, documents, and preferences.
|
||||||
void toProfile() {
|
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
|
/// The shift object is passed as an argument and can be retrieved
|
||||||
/// in the details page.
|
/// in the details page.
|
||||||
void toShiftDetails(Shift shift) {
|
void toShiftDetails(Shift shift) {
|
||||||
navigate(
|
navigate(StaffPaths.shiftDetails(shift.id), arguments: shift);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
"client_authentication": {
|
"client_authentication": {
|
||||||
"get_started_page": {
|
"get_started_page": {
|
||||||
"title": "Take Control of Your\nShifts and Events",
|
"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",
|
"sign_in_button": "Sign In",
|
||||||
"create_account_button": "Create Account"
|
"create_account_button": "Create Account"
|
||||||
},
|
},
|
||||||
@@ -452,7 +452,7 @@
|
|||||||
},
|
},
|
||||||
"empty_states": {
|
"empty_states": {
|
||||||
"no_shifts_today": "No shifts scheduled for today",
|
"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_shifts_tomorrow": "No shifts for tomorrow",
|
||||||
"no_recommended_shifts": "No recommended shifts"
|
"no_recommended_shifts": "No recommended shifts"
|
||||||
},
|
},
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
"amount": "$amount"
|
"amount": "$amount"
|
||||||
},
|
},
|
||||||
"recommended_card": {
|
"recommended_card": {
|
||||||
"act_now": "• ACT NOW",
|
"act_now": "\u2022 ACT NOW",
|
||||||
"one_day": "One Day",
|
"one_day": "One Day",
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"applied_for": "Applied for $title",
|
"applied_for": "Applied for $title",
|
||||||
@@ -695,7 +695,7 @@
|
|||||||
"eta_label": "$min min",
|
"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.",
|
"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",
|
"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?"
|
"arrived_desc": "You're at the shift location. Ready to clock in?"
|
||||||
},
|
},
|
||||||
"swipe": {
|
"swipe": {
|
||||||
@@ -967,16 +967,16 @@
|
|||||||
"required": "REQUIRED",
|
"required": "REQUIRED",
|
||||||
"add_photo": "Add Photo",
|
"add_photo": "Add Photo",
|
||||||
"added": "Added",
|
"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.",
|
"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": {
|
"actions": {
|
||||||
"save": "Save Attire"
|
"save": "Save Attire"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"select_required": "✓ Select all required items",
|
"select_required": "\u2713 Select all required items",
|
||||||
"upload_required": "✓ Upload photos of required items",
|
"upload_required": "\u2713 Upload photos of required items",
|
||||||
"accept_attestation": "✓ Accept attestation"
|
"accept_attestation": "\u2713 Accept attestation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"staff_shifts": {
|
"staff_shifts": {
|
||||||
@@ -1095,8 +1095,18 @@
|
|||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"cancelled": "CANCELLED",
|
"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": {
|
"staff_time_card": {
|
||||||
@@ -1218,11 +1228,11 @@
|
|||||||
},
|
},
|
||||||
"total_spend": {
|
"total_spend": {
|
||||||
"label": "Total Spend",
|
"label": "Total Spend",
|
||||||
"badge": "↓ 8% vs last week"
|
"badge": "\u2193 8% vs last week"
|
||||||
},
|
},
|
||||||
"fill_rate": {
|
"fill_rate": {
|
||||||
"label": "Fill Rate",
|
"label": "Fill Rate",
|
||||||
"badge": "↑ 2% improvement"
|
"badge": "\u2191 2% improvement"
|
||||||
},
|
},
|
||||||
"avg_fill_time": {
|
"avg_fill_time": {
|
||||||
"label": "Avg Fill Time",
|
"label": "Avg Fill Time",
|
||||||
@@ -1364,9 +1374,9 @@
|
|||||||
"target_prefix": "Target: ",
|
"target_prefix": "Target: ",
|
||||||
"target_hours": "$hours hrs",
|
"target_hours": "$hours hrs",
|
||||||
"target_percent": "$percent%",
|
"target_percent": "$percent%",
|
||||||
"met": "✓ Met",
|
"met": "\u2713 Met",
|
||||||
"close": "→ Close",
|
"close": "\u2192 Close",
|
||||||
"miss": "✗ Miss"
|
"miss": "\u2717 Miss"
|
||||||
},
|
},
|
||||||
"additional_metrics_title": "ADDITIONAL METRICS",
|
"additional_metrics_title": "ADDITIONAL METRICS",
|
||||||
"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.
|
/// They will implement interfaces defined in feature packages once those are created.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
|
||||||
export 'src/data_connect_module.dart';
|
export 'src/data_connect_module.dart';
|
||||||
export 'src/session/client_session_store.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/domain/repositories/billing_connector_repository.dart';
|
||||||
export 'src/connectors/billing/data/repositories/billing_connector_repository_impl.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 Coverage Connector
|
||||||
export 'src/connectors/coverage/domain/repositories/coverage_connector_repository.dart';
|
export 'src/connectors/coverage/domain/repositories/coverage_connector_repository.dart';
|
||||||
export 'src/connectors/coverage/data/repositories/coverage_connector_repository_impl.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.
|
/// Handles shift-related data operations by interacting with Data Connect.
|
||||||
class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||||
/// Creates a new [ShiftsConnectorRepositoryImpl].
|
/// Creates a new [ShiftsConnectorRepositoryImpl].
|
||||||
ShiftsConnectorRepositoryImpl({
|
ShiftsConnectorRepositoryImpl({dc.DataConnectService? service})
|
||||||
dc.DataConnectService? service,
|
: _service = service ?? dc.DataConnectService.instance;
|
||||||
}) : _service = service ?? dc.DataConnectService.instance;
|
|
||||||
|
|
||||||
final dc.DataConnectService _service;
|
final dc.DataConnectService _service;
|
||||||
|
|
||||||
@@ -23,12 +22,17 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
required DateTime end,
|
required DateTime end,
|
||||||
}) async {
|
}) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service.connector
|
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service
|
||||||
|
.connector
|
||||||
.getApplicationsByStaffId(staffId: staffId)
|
.getApplicationsByStaffId(staffId: staffId)
|
||||||
.dayStart(_service.toTimestamp(start))
|
.dayStart(_service.toTimestamp(start))
|
||||||
.dayEnd(_service.toTimestamp(end));
|
.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);
|
return _mapApplicationsToShifts(response.data.applications);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -45,18 +49,28 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
|
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||||
if (vendorId == null || vendorId.isEmpty) return <Shift>[];
|
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)
|
.listShiftRolesByVendorId(vendorId: vendorId)
|
||||||
.execute();
|
.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
|
// 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)
|
.getApplicationsByStaffId(staffId: staffId)
|
||||||
.execute();
|
.execute();
|
||||||
final Set<String> appliedShiftIds =
|
final Set<String> appliedShiftIds = myAppsResponse.data.applications
|
||||||
myAppsResponse.data.applications.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId).toSet();
|
.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
final List<Shift> mappedShifts = <Shift>[];
|
final List<Shift> mappedShifts = <Shift>[];
|
||||||
for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) {
|
for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) {
|
||||||
@@ -67,6 +81,34 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
final DateTime? endDt = _service.toDateTime(sr.endTime);
|
final DateTime? endDt = _service.toDateTime(sr.endTime);
|
||||||
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
|
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(
|
mappedShifts.add(
|
||||||
Shift(
|
Shift(
|
||||||
id: sr.shiftId,
|
id: sr.shiftId,
|
||||||
@@ -78,16 +120,25 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
location: sr.shift.location ?? '',
|
location: sr.shift.location ?? '',
|
||||||
locationAddress: sr.shift.locationAddress ?? '',
|
locationAddress: sr.shift.locationAddress ?? '',
|
||||||
date: shiftDate?.toIso8601String() ?? '',
|
date: shiftDate?.toIso8601String() ?? '',
|
||||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
startTime: startTime,
|
||||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
endTime: endTime,
|
||||||
createdDate: createdDt?.toIso8601String() ?? '',
|
createdDate: createdDt?.toIso8601String() ?? '',
|
||||||
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
||||||
description: sr.shift.description,
|
description: sr.shift.description,
|
||||||
durationDays: sr.shift.durationDays,
|
durationDays: sr.shift.durationDays ?? schedules?.length,
|
||||||
requiredSlots: sr.count,
|
requiredSlots: sr.count,
|
||||||
filledSlots: sr.assigned ?? 0,
|
filledSlots: sr.assigned ?? 0,
|
||||||
latitude: sr.shift.latitude,
|
latitude: sr.shift.latitude,
|
||||||
longitude: sr.shift.longitude,
|
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(
|
breakInfo: BreakAdapter.fromData(
|
||||||
isPaid: sr.isBreakPaid ?? false,
|
isPaid: sr.isBreakPaid ?? false,
|
||||||
breakTime: sr.breakType?.stringValue,
|
breakTime: sr.breakType?.stringValue,
|
||||||
@@ -125,7 +176,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
if (roleId != null && roleId.isNotEmpty) {
|
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)
|
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
|
||||||
.execute();
|
.execute();
|
||||||
final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole;
|
final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole;
|
||||||
@@ -137,13 +189,22 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
|
|
||||||
bool hasApplied = false;
|
bool hasApplied = false;
|
||||||
String status = 'open';
|
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)
|
.getApplicationsByStaffId(staffId: staffId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
|
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
|
||||||
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId)
|
.data
|
||||||
|
.applications
|
||||||
|
.where(
|
||||||
|
(dc.GetApplicationsByStaffIdApplications a) =>
|
||||||
|
a.shiftId == shiftId && a.shiftRole.roleId == roleId,
|
||||||
|
)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|
||||||
if (app != null) {
|
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;
|
final dc.GetShiftByIdShift? s = result.data.shift;
|
||||||
if (s == null) return null;
|
if (s == null) return null;
|
||||||
|
|
||||||
@@ -190,17 +252,23 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
Break? breakInfo;
|
Break? breakInfo;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
|
final QueryResult<
|
||||||
|
dc.ListShiftRolesByShiftIdData,
|
||||||
|
dc.ListShiftRolesByShiftIdVariables
|
||||||
|
>
|
||||||
|
rolesRes = await _service.connector
|
||||||
.listShiftRolesByShiftId(shiftId: shiftId)
|
.listShiftRolesByShiftId(shiftId: shiftId)
|
||||||
.execute();
|
.execute();
|
||||||
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
||||||
required = 0;
|
required = 0;
|
||||||
filled = 0;
|
filled = 0;
|
||||||
for (dc.ListShiftRolesByShiftIdShiftRoles r in rolesRes.data.shiftRoles) {
|
for (dc.ListShiftRolesByShiftIdShiftRoles r
|
||||||
|
in rolesRes.data.shiftRoles) {
|
||||||
required = (required ?? 0) + r.count;
|
required = (required ?? 0) + r.count;
|
||||||
filled = (filled ?? 0) + (r.assigned ?? 0);
|
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(
|
breakInfo = BreakAdapter.fromData(
|
||||||
isPaid: firstRole.isBreakPaid ?? false,
|
isPaid: firstRole.isBreakPaid ?? false,
|
||||||
breakTime: firstRole.breakType?.stringValue,
|
breakTime: firstRole.breakType?.stringValue,
|
||||||
@@ -247,89 +315,188 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
final String targetRoleId = roleId ?? '';
|
final String targetRoleId = roleId ?? '';
|
||||||
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
|
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
|
||||||
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
|
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
|
||||||
|
shiftResult = await _service.connector
|
||||||
|
.getShiftById(id: shiftId)
|
||||||
.execute();
|
.execute();
|
||||||
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
|
final dc.GetShiftByIdShift? initialShift = shiftResult.data.shift;
|
||||||
if (role == null) throw Exception('Shift role not found');
|
if (initialShift == null) throw Exception('Shift not found');
|
||||||
|
|
||||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult = await _service.connector.getShiftById(id: shiftId).execute();
|
final dc.EnumValue<dc.OrderType> orderTypeEnum =
|
||||||
final dc.GetShiftByIdShift? shift = shiftResult.data.shift;
|
initialShift.order.orderType;
|
||||||
if (shift == null) throw Exception('Shift not found');
|
final bool isMultiDay =
|
||||||
|
orderTypeEnum is dc.Known<dc.OrderType> &&
|
||||||
|
(orderTypeEnum.value == dc.OrderType.RECURRING ||
|
||||||
|
orderTypeEnum.value == dc.OrderType.PERMANENT);
|
||||||
|
final List<_TargetShiftRole> targets = [];
|
||||||
|
|
||||||
// Validate daily limit
|
if (isMultiDay) {
|
||||||
final DateTime? shiftDate = _service.toDateTime(shift.date);
|
// 2. Fetch all shifts for this order to apply to all of them for the same role
|
||||||
if (shiftDate != null) {
|
final QueryResult<
|
||||||
final DateTime dayStartUtc = DateTime.utc(shiftDate.year, shiftDate.month, shiftDate.day);
|
dc.ListShiftRolesByBusinessAndOrderData,
|
||||||
final DateTime dayEndUtc = dayStartUtc.add(const Duration(days: 1)).subtract(const Duration(microseconds: 1));
|
dc.ListShiftRolesByBusinessAndOrderVariables
|
||||||
|
>
|
||||||
final QueryResult<dc.VaidateDayStaffApplicationData, dc.VaidateDayStaffApplicationVariables> validationResponse = await _service.connector
|
allRolesRes = await _service.connector
|
||||||
.vaidateDayStaffApplication(staffId: staffId)
|
.listShiftRolesByBusinessAndOrder(
|
||||||
.dayStart(_service.toTimestamp(dayStartUtc))
|
businessId: initialShift.order.businessId,
|
||||||
.dayEnd(_service.toTimestamp(dayEndUtc))
|
orderId: initialShift.orderId,
|
||||||
|
)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (validationResponse.data.applications.isNotEmpty) {
|
|
||||||
throw Exception('The user already has a shift that day.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing application
|
for (final role in allRolesRes.data.shiftRoles) {
|
||||||
final QueryResult<dc.GetApplicationByStaffShiftAndRoleData, dc.GetApplicationByStaffShiftAndRoleVariables> existingAppRes = await _service.connector
|
if (role.roleId == targetRoleId) {
|
||||||
.getApplicationByStaffShiftAndRole(
|
targets.add(
|
||||||
staffId: staffId,
|
_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');
|
||||||
|
|
||||||
|
targets.add(
|
||||||
|
_TargetShiftRole(
|
||||||
shiftId: shiftId,
|
shiftId: shiftId,
|
||||||
roleId: targetRoleId,
|
roleId: targetRoleId,
|
||||||
)
|
count: role.count,
|
||||||
.execute();
|
assigned: role.assigned ?? 0,
|
||||||
if (existingAppRes.data.applications.isNotEmpty) {
|
shiftFilled: initialShift.filled ?? 0,
|
||||||
throw Exception('Application already exists.');
|
date: _service.toDateTime(initialShift.date),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((role.assigned ?? 0) >= role.count) {
|
if (targets.isEmpty) {
|
||||||
throw Exception('This shift is full.');
|
throw Exception('No valid shifts found to apply for.');
|
||||||
}
|
}
|
||||||
|
|
||||||
final int currentAssigned = role.assigned ?? 0;
|
int appliedCount = 0;
|
||||||
final int currentFilled = shift.filled ?? 0;
|
final List<String> errors = [];
|
||||||
|
|
||||||
String? createdAppId;
|
for (final target in targets) {
|
||||||
try {
|
try {
|
||||||
final OperationResult<dc.CreateApplicationData, dc.CreateApplicationVariables> createRes = await _service.connector.createApplication(
|
await _applyToSingleShiftRole(target: target, staffId: staffId);
|
||||||
shiftId: shiftId,
|
appliedCount++;
|
||||||
staffId: staffId,
|
} catch (e) {
|
||||||
roleId: targetRoleId,
|
// For multi-shift apply, we might want to continue even if some fail due to conflicts
|
||||||
status: dc.ApplicationStatus.CONFIRMED, // Matches existing logic
|
if (targets.length == 1) rethrow;
|
||||||
origin: dc.ApplicationOrigin.STAFF,
|
errors.add('Shift on ${target.date}: ${e.toString()}');
|
||||||
).execute();
|
|
||||||
|
|
||||||
createdAppId = createRes.data.application_insert.id;
|
|
||||||
|
|
||||||
await _service.connector
|
|
||||||
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
|
|
||||||
.assigned(currentAssigned + 1)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await _service.connector
|
|
||||||
.updateShift(id: shiftId)
|
|
||||||
.filled(currentFilled + 1)
|
|
||||||
.execute();
|
|
||||||
} catch (e) {
|
|
||||||
// Simple rollback attempt (not guaranteed)
|
|
||||||
if (createdAppId != null) {
|
|
||||||
await _service.connector.deleteApplication(id: createdAppId).execute();
|
|
||||||
}
|
}
|
||||||
rethrow;
|
}
|
||||||
|
|
||||||
|
if (appliedCount == 0 && targets.length > 1) {
|
||||||
|
throw Exception('Failed to apply for any shifts: ${errors.join(", ")}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _applyToSingleShiftRole({
|
||||||
Future<void> acceptShift({
|
required _TargetShiftRole target,
|
||||||
required String shiftId,
|
|
||||||
required String staffId,
|
required String staffId,
|
||||||
}) {
|
}) async {
|
||||||
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.CONFIRMED);
|
// Validate daily limit
|
||||||
|
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
|
||||||
|
.vaidateDayStaffApplication(staffId: staffId)
|
||||||
|
.dayStart(_service.toTimestamp(dayStartUtc))
|
||||||
|
.dayEnd(_service.toTimestamp(dayEndUtc))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (validationResponse.data.applications.isNotEmpty) {
|
||||||
|
throw Exception('The user already has a shift that day.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing application
|
||||||
|
final QueryResult<
|
||||||
|
dc.GetApplicationByStaffShiftAndRoleData,
|
||||||
|
dc.GetApplicationByStaffShiftAndRoleVariables
|
||||||
|
>
|
||||||
|
existingAppRes = await _service.connector
|
||||||
|
.getApplicationByStaffShiftAndRole(
|
||||||
|
staffId: staffId,
|
||||||
|
shiftId: target.shiftId,
|
||||||
|
roleId: target.roleId,
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (existingAppRes.data.applications.isNotEmpty) {
|
||||||
|
throw Exception('Application already exists.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.assigned >= target.count) {
|
||||||
|
throw Exception('This shift is full.');
|
||||||
|
}
|
||||||
|
|
||||||
|
String? createdAppId;
|
||||||
|
try {
|
||||||
|
final OperationResult<
|
||||||
|
dc.CreateApplicationData,
|
||||||
|
dc.CreateApplicationVariables
|
||||||
|
>
|
||||||
|
createRes = await _service.connector
|
||||||
|
.createApplication(
|
||||||
|
shiftId: target.shiftId,
|
||||||
|
staffId: staffId,
|
||||||
|
roleId: target.roleId,
|
||||||
|
status: dc.ApplicationStatus.CONFIRMED,
|
||||||
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
createdAppId = createRes.data.application_insert.id;
|
||||||
|
|
||||||
|
await _service.connector
|
||||||
|
.updateShiftRole(shiftId: target.shiftId, roleId: target.roleId)
|
||||||
|
.assigned(target.assigned + 1)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await _service.connector
|
||||||
|
.updateShift(id: target.shiftId)
|
||||||
|
.filled(target.shiftFilled + 1)
|
||||||
|
.execute();
|
||||||
|
} catch (e) {
|
||||||
|
// Simple rollback attempt (not guaranteed)
|
||||||
|
if (createdAppId != null) {
|
||||||
|
await _service.connector.deleteApplication(id: createdAppId).execute();
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> acceptShift({required String shiftId, required String staffId}) {
|
||||||
|
return _updateApplicationStatus(
|
||||||
|
shiftId,
|
||||||
|
staffId,
|
||||||
|
dc.ApplicationStatus.CONFIRMED,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -337,7 +504,11 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
required String shiftId,
|
required String shiftId,
|
||||||
required String staffId,
|
required String staffId,
|
||||||
}) {
|
}) {
|
||||||
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.REJECTED);
|
return _updateApplicationStatus(
|
||||||
|
shiftId,
|
||||||
|
staffId,
|
||||||
|
dc.ApplicationStatus.REJECTED,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -351,18 +522,24 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
@override
|
@override
|
||||||
Future<List<Shift>> getHistoryShifts({required String staffId}) async {
|
Future<List<Shift>> getHistoryShifts({required String staffId}) async {
|
||||||
return _service.run(() 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)
|
.listCompletedApplicationsByStaffId(staffId: staffId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final List<Shift> shifts = <Shift>[];
|
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 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.eventName!
|
||||||
: app.shift.order.business.businessName;
|
: app.shift.order.business.businessName;
|
||||||
final String title = '$roleName - $orderName';
|
final String title = '$roleName - $orderName';
|
||||||
|
|
||||||
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
|
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
|
||||||
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
|
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
|
||||||
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
|
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
|
||||||
@@ -379,7 +556,9 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
location: app.shift.location ?? '',
|
location: app.shift.location ?? '',
|
||||||
locationAddress: app.shift.order.teamHub.hubName,
|
locationAddress: app.shift.order.teamHub.hubName,
|
||||||
date: shiftDate?.toIso8601String() ?? '',
|
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) : '',
|
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||||
createdDate: createdDt?.toIso8601String() ?? '',
|
createdDate: createdDt?.toIso8601String() ?? '',
|
||||||
status: 'completed', // Hardcoded as checked out implies completion
|
status: 'completed', // Hardcoded as checked out implies completion
|
||||||
@@ -406,7 +585,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
List<Shift> _mapApplicationsToShifts(List<dynamic> apps) {
|
List<Shift> _mapApplicationsToShifts(List<dynamic> apps) {
|
||||||
return apps.map((app) {
|
return apps.map((app) {
|
||||||
final String roleName = app.shiftRole.role.name;
|
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.eventName!
|
||||||
: app.shift.order.business.businessName;
|
: app.shift.order.business.businessName;
|
||||||
final String title = '$roleName - $orderName';
|
final String title = '$roleName - $orderName';
|
||||||
@@ -418,7 +598,7 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
|
|
||||||
final bool hasCheckIn = app.checkInTime != null;
|
final bool hasCheckIn = app.checkInTime != null;
|
||||||
final bool hasCheckOut = app.checkOutTime != null;
|
final bool hasCheckOut = app.checkOutTime != null;
|
||||||
|
|
||||||
String status;
|
String status;
|
||||||
if (hasCheckOut) {
|
if (hasCheckOut) {
|
||||||
status = 'completed';
|
status = 'completed';
|
||||||
@@ -479,12 +659,20 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
) async {
|
) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
// First try to find the application
|
// 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)
|
.getApplicationsByStaffId(staffId: staffId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
|
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
|
||||||
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId)
|
.data
|
||||||
|
.applications
|
||||||
|
.where(
|
||||||
|
(dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId,
|
||||||
|
)
|
||||||
.firstOrNull;
|
.firstOrNull;
|
||||||
|
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
@@ -494,24 +682,116 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
} else if (newStatus == dc.ApplicationStatus.REJECTED) {
|
} else if (newStatus == dc.ApplicationStatus.REJECTED) {
|
||||||
// If declining but no app found, create a rejected application
|
// 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)
|
.listShiftRolesByShiftId(shiftId: shiftId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
||||||
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
|
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
|
||||||
await _service.connector.createApplication(
|
rolesRes.data.shiftRoles.first;
|
||||||
shiftId: shiftId,
|
await _service.connector
|
||||||
staffId: staffId,
|
.createApplication(
|
||||||
roleId: firstRole.id,
|
shiftId: shiftId,
|
||||||
status: dc.ApplicationStatus.REJECTED,
|
staffId: staffId,
|
||||||
origin: dc.ApplicationOrigin.STAFF,
|
roleId: firstRole.id,
|
||||||
).execute();
|
status: dc.ApplicationStatus.REJECTED,
|
||||||
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Exception("Application not found for shift $shiftId");
|
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/hubs/data/repositories/hubs_connector_repository_impl.dart';
|
||||||
import 'connectors/billing/domain/repositories/billing_connector_repository.dart';
|
import 'connectors/billing/domain/repositories/billing_connector_repository.dart';
|
||||||
import 'connectors/billing/data/repositories/billing_connector_repository_impl.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/domain/repositories/coverage_connector_repository.dart';
|
||||||
import 'connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
|
import 'connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
|
||||||
import 'services/data_connect_service.dart';
|
import 'services/data_connect_service.dart';
|
||||||
@@ -32,9 +30,6 @@ class DataConnectModule extends Module {
|
|||||||
i.addLazySingleton<BillingConnectorRepository>(
|
i.addLazySingleton<BillingConnectorRepository>(
|
||||||
BillingConnectorRepositoryImpl.new,
|
BillingConnectorRepositoryImpl.new,
|
||||||
);
|
);
|
||||||
i.addLazySingleton<HomeConnectorRepository>(
|
|
||||||
HomeConnectorRepositoryImpl.new,
|
|
||||||
);
|
|
||||||
i.addLazySingleton<CoverageConnectorRepository>(
|
i.addLazySingleton<CoverageConnectorRepository>(
|
||||||
CoverageConnectorRepositoryImpl.new,
|
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/hubs/data/repositories/hubs_connector_repository_impl.dart';
|
||||||
import '../connectors/billing/domain/repositories/billing_connector_repository.dart';
|
import '../connectors/billing/domain/repositories/billing_connector_repository.dart';
|
||||||
import '../connectors/billing/data/repositories/billing_connector_repository_impl.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/domain/repositories/coverage_connector_repository.dart';
|
||||||
import '../connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
|
import '../connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
|
||||||
import '../connectors/staff/domain/repositories/staff_connector_repository.dart';
|
import '../connectors/staff/domain/repositories/staff_connector_repository.dart';
|
||||||
@@ -39,7 +37,6 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
|||||||
ShiftsConnectorRepository? _shiftsRepository;
|
ShiftsConnectorRepository? _shiftsRepository;
|
||||||
HubsConnectorRepository? _hubsRepository;
|
HubsConnectorRepository? _hubsRepository;
|
||||||
BillingConnectorRepository? _billingRepository;
|
BillingConnectorRepository? _billingRepository;
|
||||||
HomeConnectorRepository? _homeRepository;
|
|
||||||
CoverageConnectorRepository? _coverageRepository;
|
CoverageConnectorRepository? _coverageRepository;
|
||||||
StaffConnectorRepository? _staffRepository;
|
StaffConnectorRepository? _staffRepository;
|
||||||
|
|
||||||
@@ -63,14 +60,11 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
|||||||
return _billingRepository ??= BillingConnectorRepositoryImpl(service: this);
|
return _billingRepository ??= BillingConnectorRepositoryImpl(service: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the home connector repository.
|
|
||||||
HomeConnectorRepository getHomeRepository() {
|
|
||||||
return _homeRepository ??= HomeConnectorRepositoryImpl(service: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the coverage connector repository.
|
/// Gets the coverage connector repository.
|
||||||
CoverageConnectorRepository getCoverageRepository() {
|
CoverageConnectorRepository getCoverageRepository() {
|
||||||
return _coverageRepository ??= CoverageConnectorRepositoryImpl(service: this);
|
return _coverageRepository ??= CoverageConnectorRepositoryImpl(
|
||||||
|
service: this,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the staff connector repository.
|
/// Gets the staff connector repository.
|
||||||
@@ -84,14 +78,14 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
|||||||
|
|
||||||
/// Helper to get the current staff ID from the session.
|
/// Helper to get the current staff ID from the session.
|
||||||
Future<String> getStaffId() async {
|
Future<String> getStaffId() async {
|
||||||
String? staffId = dc.StaffSessionStore.instance.session?.ownerId;
|
String? staffId = dc.StaffSessionStore.instance.session?.staff?.id;
|
||||||
|
|
||||||
if (staffId == null || staffId.isEmpty) {
|
if (staffId == null || staffId.isEmpty) {
|
||||||
// Attempt to recover session if user is signed in
|
// Attempt to recover session if user is signed in
|
||||||
final user = auth.currentUser;
|
final user = auth.currentUser;
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
await _loadSession(user.uid);
|
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
|
// Load Staff Session if applicable
|
||||||
if (role == 'STAFF' || role == 'BOTH') {
|
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) {
|
if (response.data.staffs.isNotEmpty) {
|
||||||
final s = response.data.staffs.first;
|
final s = response.data.staffs.first;
|
||||||
dc.StaffSessionStore.instance.setSession(
|
dc.StaffSessionStore.instance.setSession(
|
||||||
dc.StaffSession(
|
dc.StaffSession(
|
||||||
ownerId: s.id,
|
ownerId: s.ownerId,
|
||||||
staff: domain.Staff(
|
staff: domain.Staff(
|
||||||
id: s.id,
|
id: s.id,
|
||||||
authProviderId: s.userId,
|
authProviderId: s.userId,
|
||||||
@@ -151,7 +147,9 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
|||||||
|
|
||||||
// Load Client Session if applicable
|
// Load Client Session if applicable
|
||||||
if (role == 'BUSINESS' || role == 'BOTH') {
|
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) {
|
if (response.data.businesses.isNotEmpty) {
|
||||||
final b = response.data.businesses.first;
|
final b = response.data.businesses.first;
|
||||||
dc.ClientSessionStore.instance.setSession(
|
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) {
|
DateTime? toDateTime(dynamic timestamp) {
|
||||||
if (timestamp == null) return null;
|
if (timestamp == null) return null;
|
||||||
if (timestamp is fdc.Timestamp) {
|
if (timestamp is fdc.Timestamp) {
|
||||||
return timestamp.toDateTime();
|
return timestamp.toDateTime().toLocal();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a Dart [DateTime] to a Data Connect [Timestamp].
|
/// Converts a Dart [DateTime] to a Data Connect [Timestamp].
|
||||||
|
///
|
||||||
|
/// Converts the [DateTime] to UTC before creating the [Timestamp].
|
||||||
fdc.Timestamp toTimestamp(DateTime dateTime) {
|
fdc.Timestamp toTimestamp(DateTime dateTime) {
|
||||||
final DateTime utc = dateTime.toUtc();
|
final DateTime utc = dateTime.toUtc();
|
||||||
final int millis = utc.millisecondsSinceEpoch;
|
final int millis = utc.millisecondsSinceEpoch;
|
||||||
@@ -225,7 +230,6 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
|
|||||||
_shiftsRepository = null;
|
_shiftsRepository = null;
|
||||||
_hubsRepository = null;
|
_hubsRepository = null;
|
||||||
_billingRepository = null;
|
_billingRepository = null;
|
||||||
_homeRepository = null;
|
|
||||||
_coverageRepository = null;
|
_coverageRepository = null;
|
||||||
_staffRepository = null;
|
_staffRepository = null;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ class UiIcons {
|
|||||||
/// Calendar icon for shifts or schedules
|
/// Calendar icon for shifts or schedules
|
||||||
static const IconData calendar = _IconLib.calendar;
|
static const IconData calendar = _IconLib.calendar;
|
||||||
|
|
||||||
|
/// Calender check icon for shifts or schedules
|
||||||
|
static const IconData calendarCheck = _IconLib.calendarCheck;
|
||||||
|
|
||||||
/// Briefcase icon for jobs
|
/// Briefcase icon for jobs
|
||||||
static const IconData briefcase = _IconLib.briefcase;
|
static const IconData briefcase = _IconLib.briefcase;
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,14 @@ class UiTypography {
|
|||||||
color: UiColors.textPrimary,
|
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)
|
/// Headline 5 Regular - Font: Instrument Sans, Size: 18, Height: 1.5 (#121826)
|
||||||
static final TextStyle headline5r = _primaryBase.copyWith(
|
static final TextStyle headline5r = _primaryBase.copyWith(
|
||||||
fontWeight: FontWeight.w400,
|
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 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../ui_icons.dart';
|
import '../ui_icons.dart';
|
||||||
|
import 'ui_icon_button.dart';
|
||||||
|
|
||||||
/// A custom AppBar for the Krow UI design system.
|
/// A custom AppBar for the Krow UI design system.
|
||||||
///
|
///
|
||||||
/// This widget provides a consistent look and feel for top app bars across the application.
|
/// This widget provides a consistent look and feel for top app bars across the application.
|
||||||
class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
|
||||||
const UiAppBar({
|
const UiAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
this.title,
|
this.title,
|
||||||
@@ -14,11 +16,12 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.leading,
|
this.leading,
|
||||||
this.actions,
|
this.actions,
|
||||||
this.height = kToolbarHeight,
|
this.height = kToolbarHeight,
|
||||||
this.centerTitle = true,
|
this.centerTitle = false,
|
||||||
this.onLeadingPressed,
|
this.onLeadingPressed,
|
||||||
this.showBackButton = true,
|
this.showBackButton = true,
|
||||||
this.bottom,
|
this.bottom,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The title text to display in the app bar.
|
/// The title text to display in the app bar.
|
||||||
final String? title;
|
final String? title;
|
||||||
|
|
||||||
@@ -52,17 +55,19 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
title: titleWidget ??
|
title:
|
||||||
(title != null
|
titleWidget ??
|
||||||
? Text(
|
(title != null ? Text(title!, style: UiTypography.headline4b) : null),
|
||||||
title!,
|
leading:
|
||||||
)
|
leading ??
|
||||||
: null),
|
|
||||||
leading: leading ??
|
|
||||||
(showBackButton
|
(showBackButton
|
||||||
? IconButton(
|
? UiIconButton(
|
||||||
icon: const Icon(UiIcons.chevronLeft, size: 20),
|
icon: UiIcons.chevronLeft,
|
||||||
onPressed: onLeadingPressed ?? () => Navigator.of(context).pop(),
|
onTap: onLeadingPressed ?? () => Navigator.of(context).pop(),
|
||||||
|
backgroundColor: UiColors.transparent,
|
||||||
|
iconColor: UiColors.iconThird,
|
||||||
|
shape: BoxShape.rectangle,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
)
|
)
|
||||||
: null),
|
: null),
|
||||||
actions: actions,
|
actions: actions,
|
||||||
@@ -72,5 +77,6 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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.
|
/// A custom chip widget with supports for different sizes, themes, and icons.
|
||||||
class UiChip extends StatelessWidget {
|
class UiChip extends StatelessWidget {
|
||||||
|
|
||||||
/// Creates a [UiChip].
|
/// Creates a [UiChip].
|
||||||
const UiChip({
|
const UiChip({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -42,6 +41,7 @@ class UiChip extends StatelessWidget {
|
|||||||
this.onTrailingIconTap,
|
this.onTrailingIconTap,
|
||||||
this.isSelected = false,
|
this.isSelected = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The text label to display.
|
/// The text label to display.
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ class UiChip extends StatelessWidget {
|
|||||||
padding: padding,
|
padding: padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: UiConstants.radiusFull,
|
borderRadius: UiConstants.radiusMd,
|
||||||
border: _getBorder(),
|
border: _getBorder(),
|
||||||
),
|
),
|
||||||
child: content,
|
child: content,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class UiIconButton extends StatelessWidget {
|
|||||||
required this.iconColor,
|
required this.iconColor,
|
||||||
this.useBlur = false,
|
this.useBlur = false,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.shape = BoxShape.circle,
|
||||||
|
this.borderRadius,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Creates a primary variant icon button with solid background.
|
/// Creates a primary variant icon button with solid background.
|
||||||
@@ -25,6 +27,8 @@ class UiIconButton extends StatelessWidget {
|
|||||||
this.size = 40,
|
this.size = 40,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.shape = BoxShape.circle,
|
||||||
|
this.borderRadius,
|
||||||
}) : backgroundColor = UiColors.primary,
|
}) : backgroundColor = UiColors.primary,
|
||||||
iconColor = UiColors.white,
|
iconColor = UiColors.white,
|
||||||
useBlur = false;
|
useBlur = false;
|
||||||
@@ -36,6 +40,8 @@ class UiIconButton extends StatelessWidget {
|
|||||||
this.size = 40,
|
this.size = 40,
|
||||||
this.iconSize = 20,
|
this.iconSize = 20,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.shape = BoxShape.circle,
|
||||||
|
this.borderRadius,
|
||||||
}) : backgroundColor = UiColors.primary.withAlpha(96),
|
}) : backgroundColor = UiColors.primary.withAlpha(96),
|
||||||
iconColor = UiColors.primary,
|
iconColor = UiColors.primary,
|
||||||
useBlur = true;
|
useBlur = true;
|
||||||
@@ -60,13 +66,23 @@ class UiIconButton extends StatelessWidget {
|
|||||||
/// Callback when the button is tapped.
|
/// Callback when the button is tapped.
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
/// The shape of the button (circle or rectangle).
|
||||||
|
final BoxShape shape;
|
||||||
|
|
||||||
|
/// The border radius for rectangle shape.
|
||||||
|
final BorderRadius? borderRadius;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Builds the icon button UI.
|
/// Builds the icon button UI.
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Widget button = Container(
|
final Widget button = Container(
|
||||||
width: size,
|
width: size,
|
||||||
height: 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),
|
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';
|
export 'src/adapters/shifts/break/break_adapter.dart';
|
||||||
|
|
||||||
// Orders & Requests
|
// Orders & Requests
|
||||||
export 'src/entities/orders/order_type.dart';
|
|
||||||
export 'src/entities/orders/one_time_order.dart';
|
export 'src/entities/orders/one_time_order.dart';
|
||||||
export 'src/entities/orders/one_time_order_position.dart';
|
export 'src/entities/orders/one_time_order_position.dart';
|
||||||
export 'src/entities/orders/recurring_order.dart';
|
export 'src/entities/orders/recurring_order.dart';
|
||||||
export 'src/entities/orders/recurring_order_position.dart';
|
export 'src/entities/orders/recurring_order_position.dart';
|
||||||
export 'src/entities/orders/permanent_order.dart';
|
export 'src/entities/orders/permanent_order.dart';
|
||||||
export 'src/entities/orders/permanent_order_position.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/order_item.dart';
|
||||||
|
export 'src/entities/orders/reorder_data.dart';
|
||||||
|
|
||||||
// Skills & Certs
|
// Skills & Certs
|
||||||
export 'src/entities/skills/skill.dart';
|
export 'src/entities/skills/skill.dart';
|
||||||
|
|||||||
@@ -1,46 +1,51 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
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 {
|
class ReorderItem extends Equatable {
|
||||||
const ReorderItem({
|
const ReorderItem({
|
||||||
required this.orderId,
|
required this.orderId,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.location,
|
required this.location,
|
||||||
required this.hourlyRate,
|
required this.totalCost,
|
||||||
required this.hours,
|
|
||||||
required this.workers,
|
required this.workers,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
this.hourlyRate = 0,
|
||||||
|
this.hours = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Parent order id for the completed shift.
|
/// Unique identifier of the order.
|
||||||
final String orderId;
|
final String orderId;
|
||||||
|
|
||||||
/// Display title (role + shift title).
|
/// Display title of the order (e.g., event name or first shift title).
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
/// Location from the shift.
|
/// Location of the order (e.g., first shift location).
|
||||||
final String location;
|
final String location;
|
||||||
|
|
||||||
/// Hourly rate from the role.
|
/// Total calculated cost for the order.
|
||||||
final double hourlyRate;
|
final double totalCost;
|
||||||
|
|
||||||
/// Total hours for the shift role.
|
/// Total number of workers required for the order.
|
||||||
final double hours;
|
|
||||||
|
|
||||||
/// Worker count for the shift role.
|
|
||||||
final int workers;
|
final int workers;
|
||||||
|
|
||||||
/// Order type (e.g., ONE_TIME).
|
/// The type of order (e.g., ONE_TIME, RECURRING).
|
||||||
final String type;
|
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
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
orderId,
|
orderId,
|
||||||
title,
|
title,
|
||||||
location,
|
location,
|
||||||
hourlyRate,
|
totalCost,
|
||||||
hours,
|
|
||||||
workers,
|
workers,
|
||||||
type,
|
type,
|
||||||
|
hourlyRate,
|
||||||
|
hours,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import 'order_type.dart';
|
||||||
|
|
||||||
/// Represents a customer's view of an order or shift.
|
/// Represents a customer's view of an order or shift.
|
||||||
///
|
///
|
||||||
/// This entity captures the details necessary for the dashboard/view orders screen,
|
/// This entity captures the details necessary for the dashboard/view orders screen,
|
||||||
@@ -9,6 +11,7 @@ class OrderItem extends Equatable {
|
|||||||
const OrderItem({
|
const OrderItem({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.orderId,
|
required this.orderId,
|
||||||
|
required this.orderType,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.clientName,
|
required this.clientName,
|
||||||
required this.status,
|
required this.status,
|
||||||
@@ -20,6 +23,7 @@ class OrderItem extends Equatable {
|
|||||||
required this.filled,
|
required this.filled,
|
||||||
required this.workersNeeded,
|
required this.workersNeeded,
|
||||||
required this.hourlyRate,
|
required this.hourlyRate,
|
||||||
|
required this.eventName,
|
||||||
this.hours = 0,
|
this.hours = 0,
|
||||||
this.totalValue = 0,
|
this.totalValue = 0,
|
||||||
this.confirmedApps = const <Map<String, dynamic>>[],
|
this.confirmedApps = const <Map<String, dynamic>>[],
|
||||||
@@ -31,6 +35,9 @@ class OrderItem extends Equatable {
|
|||||||
/// Parent order identifier.
|
/// Parent order identifier.
|
||||||
final String orderId;
|
final String orderId;
|
||||||
|
|
||||||
|
/// The type of order (e.g., ONE_TIME, PERMANENT).
|
||||||
|
final OrderType orderType;
|
||||||
|
|
||||||
/// Title or name of the role.
|
/// Title or name of the role.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
@@ -70,6 +77,9 @@ class OrderItem extends Equatable {
|
|||||||
/// Total value for the shift role.
|
/// Total value for the shift role.
|
||||||
final double totalValue;
|
final double totalValue;
|
||||||
|
|
||||||
|
/// Name of the event.
|
||||||
|
final String eventName;
|
||||||
|
|
||||||
/// List of confirmed worker applications.
|
/// List of confirmed worker applications.
|
||||||
final List<Map<String, dynamic>> confirmedApps;
|
final List<Map<String, dynamic>> confirmedApps;
|
||||||
|
|
||||||
@@ -77,6 +87,7 @@ class OrderItem extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
id,
|
id,
|
||||||
orderId,
|
orderId,
|
||||||
|
orderType,
|
||||||
title,
|
title,
|
||||||
clientName,
|
clientName,
|
||||||
status,
|
status,
|
||||||
@@ -90,6 +101,7 @@ class OrderItem extends Equatable {
|
|||||||
hourlyRate,
|
hourlyRate,
|
||||||
hours,
|
hours,
|
||||||
totalValue,
|
totalValue,
|
||||||
|
eventName,
|
||||||
confirmedApps,
|
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).
|
/// A long-term or permanent staffing position.
|
||||||
///
|
permanent,
|
||||||
/// This entity defines the identity and display metadata (keys) for the order type.
|
|
||||||
/// UI-specific properties like colors and icons are handled by the presentation layer.
|
|
||||||
class OrderType extends Equatable {
|
|
||||||
|
|
||||||
const OrderType({
|
/// Shifts that repeat on a defined schedule.
|
||||||
required this.id,
|
recurring,
|
||||||
required this.titleKey,
|
|
||||||
required this.descriptionKey,
|
|
||||||
});
|
|
||||||
/// Unique identifier for the order type.
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
/// Translation key for the title.
|
/// A quickly created shift.
|
||||||
final String titleKey;
|
rapid;
|
||||||
|
|
||||||
/// Translation key for the description.
|
/// Creates an [OrderType] from a string value (typically from the backend).
|
||||||
final String descriptionKey;
|
static OrderType fromString(String value) {
|
||||||
|
switch (value.toUpperCase()) {
|
||||||
@override
|
case 'ONE_TIME':
|
||||||
List<Object?> get props => <Object?>[id, titleKey, descriptionKey];
|
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.breakInfo,
|
||||||
this.orderId,
|
this.orderId,
|
||||||
this.orderType,
|
this.orderType,
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
this.recurringDays,
|
||||||
|
this.permanentDays,
|
||||||
this.schedules,
|
this.schedules,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,6 +72,10 @@ class Shift extends Equatable {
|
|||||||
final Break? breakInfo;
|
final Break? breakInfo;
|
||||||
final String? orderId;
|
final String? orderId;
|
||||||
final String? orderType;
|
final String? orderType;
|
||||||
|
final String? startDate;
|
||||||
|
final String? endDate;
|
||||||
|
final List<String>? recurringDays;
|
||||||
|
final List<String>? permanentDays;
|
||||||
final List<ShiftSchedule>? schedules;
|
final List<ShiftSchedule>? schedules;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -103,6 +111,10 @@ class Shift extends Equatable {
|
|||||||
breakInfo,
|
breakInfo,
|
||||||
orderId,
|
orderId,
|
||||||
orderType,
|
orderType,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
recurringDays,
|
||||||
|
permanentDays,
|
||||||
schedules,
|
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/firebase_data_connect.dart';
|
||||||
import 'package:firebase_data_connect/src/core/ref.dart';
|
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../domain/repositories/home_repository_interface.dart';
|
import '../../domain/repositories/home_repository_interface.dart';
|
||||||
|
|
||||||
/// Implementation of [HomeRepositoryInterface] that delegates to [dc.HomeConnectorRepository].
|
/// Implementation of [HomeRepositoryInterface] that directly interacts with the Data Connect SDK.
|
||||||
///
|
|
||||||
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
|
|
||||||
/// connector repository from the data_connect package.
|
|
||||||
class HomeRepositoryImpl implements HomeRepositoryInterface {
|
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;
|
final dc.DataConnectService _service;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<HomeDashboardData> getDashboardData() async {
|
Future<HomeDashboardData> getDashboardData() async {
|
||||||
final String businessId = await _service.getBusinessId();
|
return _service.run(() async {
|
||||||
return _connectorRepository.getDashboardData(businessId: businessId);
|
final String businessId = await _service.getBusinessId();
|
||||||
|
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
|
@override
|
||||||
@@ -39,7 +111,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
|
|||||||
|
|
||||||
return await _service.run(() async {
|
return await _service.run(() async {
|
||||||
final String businessId = await _service.getBusinessId();
|
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)
|
.getBusinessById(id: businessId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
@@ -69,8 +142,67 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ReorderItem>> getRecentReorders() async {
|
Future<List<ReorderItem>> getRecentReorders() async {
|
||||||
final String businessId = await _service.getBusinessId();
|
return _service.run(() async {
|
||||||
return _connectorRepository.getRecentReorders(businessId: businessId);
|
final String businessId = await _service.getBusinessId();
|
||||||
|
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:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.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.
|
/// A widget that displays quick actions for the client.
|
||||||
class ActionsWidget extends StatelessWidget {
|
class ActionsWidget extends StatelessWidget {
|
||||||
/// Creates an [ActionsWidget].
|
/// Creates an [ActionsWidget].
|
||||||
const ActionsWidget({
|
const ActionsWidget({super.key, this.subtitle});
|
||||||
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;
|
|
||||||
|
|
||||||
/// Optional subtitle for the section.
|
/// Optional subtitle for the section.
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
@@ -40,7 +31,7 @@ class ActionsWidget extends StatelessWidget {
|
|||||||
iconColor: UiColors.textError,
|
iconColor: UiColors.textError,
|
||||||
textColor: UiColors.textError,
|
textColor: UiColors.textError,
|
||||||
subtitleColor: UiColors.textError.withValues(alpha: 0.8),
|
subtitleColor: UiColors.textError.withValues(alpha: 0.8),
|
||||||
onTap: onRapidPressed,
|
onTap: () => Modular.to.toCreateOrderRapid(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -54,7 +45,7 @@ class ActionsWidget extends StatelessWidget {
|
|||||||
iconColor: UiColors.primary,
|
iconColor: UiColors.primary,
|
||||||
textColor: UiColors.textPrimary,
|
textColor: UiColors.textPrimary,
|
||||||
subtitleColor: UiColors.textSecondary,
|
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/live_activity_widget.dart';
|
||||||
import '../widgets/reorder_widget.dart';
|
import '../widgets/reorder_widget.dart';
|
||||||
import '../widgets/spending_widget.dart';
|
import '../widgets/spending_widget.dart';
|
||||||
import 'client_home_sheets.dart';
|
|
||||||
|
|
||||||
/// A widget that builds dashboard content based on widget ID.
|
/// A widget that builds dashboard content based on widget ID.
|
||||||
///
|
///
|
||||||
/// This widget encapsulates the logic for rendering different dashboard
|
/// This widget encapsulates the logic for rendering different dashboard
|
||||||
/// widgets based on their unique identifiers and current state.
|
/// widgets based on their unique identifiers and current state.
|
||||||
class DashboardWidgetBuilder extends StatelessWidget {
|
class DashboardWidgetBuilder extends StatelessWidget {
|
||||||
|
|
||||||
/// Creates a [DashboardWidgetBuilder].
|
/// Creates a [DashboardWidgetBuilder].
|
||||||
const DashboardWidgetBuilder({
|
const DashboardWidgetBuilder({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -24,6 +22,7 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
|||||||
required this.isEditMode,
|
required this.isEditMode,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The unique identifier for the widget to build.
|
/// The unique identifier for the widget to build.
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
@@ -62,39 +61,9 @@ class DashboardWidgetBuilder extends StatelessWidget {
|
|||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'actions':
|
case 'actions':
|
||||||
return ActionsWidget(
|
return ActionsWidget(subtitle: subtitle);
|
||||||
onRapidPressed: () => Modular.to.toCreateOrderRapid(),
|
|
||||||
onCreateOrderPressed: () => Modular.to.toCreateOrder(),
|
|
||||||
subtitle: subtitle,
|
|
||||||
);
|
|
||||||
case 'reorder':
|
case 'reorder':
|
||||||
return ReorderWidget(
|
return ReorderWidget(orders: state.reorderItems, subtitle: subtitle);
|
||||||
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,
|
|
||||||
);
|
|
||||||
case 'spending':
|
case 'spending':
|
||||||
return SpendingWidget(
|
return SpendingWidget(
|
||||||
weeklySpending: state.dashboardData.weeklySpending,
|
weeklySpending: state.dashboardData.weeklySpending,
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.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 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// A widget that allows clients to reorder recent shifts.
|
/// A widget that allows clients to reorder recent shifts.
|
||||||
class ReorderWidget extends StatelessWidget {
|
class ReorderWidget extends StatelessWidget {
|
||||||
|
|
||||||
/// Creates a [ReorderWidget].
|
/// Creates a [ReorderWidget].
|
||||||
const ReorderWidget({
|
const ReorderWidget({super.key, required this.orders, this.subtitle});
|
||||||
super.key,
|
|
||||||
required this.orders,
|
|
||||||
required this.onReorderPressed,
|
|
||||||
this.subtitle,
|
|
||||||
});
|
|
||||||
/// Recent completed orders for reorder.
|
/// Recent completed orders for reorder.
|
||||||
final List<ReorderItem> orders;
|
final List<ReorderItem> orders;
|
||||||
|
|
||||||
/// Callback when a reorder button is pressed.
|
|
||||||
final Function(Map<String, dynamic> shiftData) onReorderPressed;
|
|
||||||
|
|
||||||
/// Optional subtitle for the section.
|
/// Optional subtitle for the section.
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
||||||
@@ -55,8 +49,7 @@ class ReorderWidget extends StatelessWidget {
|
|||||||
const SizedBox(width: UiConstants.space3),
|
const SizedBox(width: UiConstants.space3),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final ReorderItem order = recentOrders[index];
|
final ReorderItem order = recentOrders[index];
|
||||||
final double totalCost =
|
final double totalCost = order.totalCost;
|
||||||
order.hourlyRate * order.hours * order.workers;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 260,
|
width: 260,
|
||||||
@@ -155,15 +148,17 @@ class ReorderWidget extends StatelessWidget {
|
|||||||
leadingIcon: UiIcons.zap,
|
leadingIcon: UiIcons.zap,
|
||||||
iconSize: 12,
|
iconSize: 12,
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
onPressed: () => onReorderPressed(<String, dynamic>{
|
onPressed: () =>
|
||||||
'orderId': order.orderId,
|
_handleReorderPressed(context, <String, dynamic>{
|
||||||
'title': order.title,
|
'orderId': order.orderId,
|
||||||
'location': order.location,
|
'title': order.title,
|
||||||
'hourlyRate': order.hourlyRate,
|
'location': order.location,
|
||||||
'hours': order.hours,
|
'hourlyRate': order.hourlyRate,
|
||||||
'workers': order.workers,
|
'hours': order.hours,
|
||||||
'type': order.type,
|
'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 {
|
class _Badge extends StatelessWidget {
|
||||||
|
|
||||||
const _Badge({
|
const _Badge({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.text,
|
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_permanent_order_usecase.dart';
|
||||||
import 'domain/usecases/create_recurring_order_usecase.dart';
|
import 'domain/usecases/create_recurring_order_usecase.dart';
|
||||||
import 'domain/usecases/create_rapid_order_usecase.dart';
|
import 'domain/usecases/create_rapid_order_usecase.dart';
|
||||||
import 'domain/usecases/get_order_types_usecase.dart';
|
import 'domain/usecases/get_order_details_for_reorder_usecase.dart';
|
||||||
import 'presentation/blocs/client_create_order_bloc.dart';
|
import 'presentation/blocs/index.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 'presentation/pages/create_order_page.dart';
|
import 'presentation/pages/create_order_page.dart';
|
||||||
import 'presentation/pages/one_time_order_page.dart';
|
import 'presentation/pages/one_time_order_page.dart';
|
||||||
import 'presentation/pages/permanent_order_page.dart';
|
import 'presentation/pages/permanent_order_page.dart';
|
||||||
@@ -32,17 +28,18 @@ class ClientCreateOrderModule extends Module {
|
|||||||
@override
|
@override
|
||||||
void binds(Injector i) {
|
void binds(Injector i) {
|
||||||
// Repositories
|
// Repositories
|
||||||
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(ClientCreateOrderRepositoryImpl.new);
|
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
|
||||||
|
ClientCreateOrderRepositoryImpl.new,
|
||||||
|
);
|
||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
i.addLazySingleton(GetOrderTypesUseCase.new);
|
|
||||||
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
|
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
|
||||||
i.addLazySingleton(CreatePermanentOrderUseCase.new);
|
i.addLazySingleton(CreatePermanentOrderUseCase.new);
|
||||||
i.addLazySingleton(CreateRecurringOrderUseCase.new);
|
i.addLazySingleton(CreateRecurringOrderUseCase.new);
|
||||||
i.addLazySingleton(CreateRapidOrderUseCase.new);
|
i.addLazySingleton(CreateRapidOrderUseCase.new);
|
||||||
|
i.addLazySingleton(GetOrderDetailsForReorderUseCase.new);
|
||||||
|
|
||||||
// BLoCs
|
// BLoCs
|
||||||
i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
|
|
||||||
i.add<RapidOrderBloc>(RapidOrderBloc.new);
|
i.add<RapidOrderBloc>(RapidOrderBloc.new);
|
||||||
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
|
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
|
||||||
i.add<PermanentOrderBloc>(PermanentOrderBloc.new);
|
i.add<PermanentOrderBloc>(PermanentOrderBloc.new);
|
||||||
@@ -56,19 +53,31 @@ class ClientCreateOrderModule extends Module {
|
|||||||
child: (BuildContext context) => const ClientCreateOrderPage(),
|
child: (BuildContext context) => const ClientCreateOrderPage(),
|
||||||
);
|
);
|
||||||
r.child(
|
r.child(
|
||||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRapid),
|
ClientPaths.childRoute(
|
||||||
|
ClientPaths.createOrder,
|
||||||
|
ClientPaths.createOrderRapid,
|
||||||
|
),
|
||||||
child: (BuildContext context) => const RapidOrderPage(),
|
child: (BuildContext context) => const RapidOrderPage(),
|
||||||
);
|
);
|
||||||
r.child(
|
r.child(
|
||||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderOneTime),
|
ClientPaths.childRoute(
|
||||||
|
ClientPaths.createOrder,
|
||||||
|
ClientPaths.createOrderOneTime,
|
||||||
|
),
|
||||||
child: (BuildContext context) => const OneTimeOrderPage(),
|
child: (BuildContext context) => const OneTimeOrderPage(),
|
||||||
);
|
);
|
||||||
r.child(
|
r.child(
|
||||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRecurring),
|
ClientPaths.childRoute(
|
||||||
|
ClientPaths.createOrder,
|
||||||
|
ClientPaths.createOrderRecurring,
|
||||||
|
),
|
||||||
child: (BuildContext context) => const RecurringOrderPage(),
|
child: (BuildContext context) => const RecurringOrderPage(),
|
||||||
);
|
);
|
||||||
r.child(
|
r.child(
|
||||||
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderPermanent),
|
ClientPaths.childRoute(
|
||||||
|
ClientPaths.createOrder,
|
||||||
|
ClientPaths.createOrderPermanent,
|
||||||
|
),
|
||||||
child: (BuildContext context) => const PermanentOrderPage(),
|
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:intl/intl.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
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
|
/// It follows the KROW Clean Architecture by keeping the data layer focused
|
||||||
/// on delegation and data mapping, without business logic.
|
/// on delegation and data mapping, without business logic.
|
||||||
class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface {
|
class ClientCreateOrderRepositoryImpl
|
||||||
ClientCreateOrderRepositoryImpl({
|
implements ClientCreateOrderRepositoryInterface {
|
||||||
required dc.DataConnectService service,
|
ClientCreateOrderRepositoryImpl({required dc.DataConnectService service})
|
||||||
}) : _service = service;
|
: _service = service;
|
||||||
|
|
||||||
final dc.DataConnectService _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
|
@override
|
||||||
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
|
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
|
||||||
return _service.run(() async {
|
return _service.run(() async {
|
||||||
@@ -69,19 +36,19 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
order.date.month,
|
order.date.month,
|
||||||
order.date.day,
|
order.date.day,
|
||||||
);
|
);
|
||||||
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||||
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
|
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||||
await _service.connector
|
orderResult = await _service.connector
|
||||||
.createOrder(
|
.createOrder(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
orderType: dc.OrderType.ONE_TIME,
|
orderType: dc.OrderType.ONE_TIME,
|
||||||
teamHubId: hub.id,
|
teamHubId: hub.id,
|
||||||
)
|
)
|
||||||
.vendorId(vendorId)
|
.vendorId(vendorId)
|
||||||
.eventName(order.eventName)
|
.eventName(order.eventName)
|
||||||
.status(dc.OrderStatus.POSTED)
|
.status(dc.OrderStatus.POSTED)
|
||||||
.date(orderTimestamp)
|
.date(orderTimestamp)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String orderId = orderResult.data.order_insert.id;
|
final String orderId = orderResult.data.order_insert.id;
|
||||||
|
|
||||||
@@ -92,32 +59,34 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}';
|
final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}';
|
||||||
final double shiftCost = _calculateShiftCost(order);
|
final double shiftCost = _calculateShiftCost(order);
|
||||||
|
|
||||||
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
|
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||||
await _service.connector
|
shiftResult = await _service.connector
|
||||||
.createShift(title: shiftTitle, orderId: orderId)
|
.createShift(title: shiftTitle, orderId: orderId)
|
||||||
.date(orderTimestamp)
|
.date(orderTimestamp)
|
||||||
.location(hub.name)
|
.location(hub.name)
|
||||||
.locationAddress(hub.address)
|
.locationAddress(hub.address)
|
||||||
.latitude(hub.latitude)
|
.latitude(hub.latitude)
|
||||||
.longitude(hub.longitude)
|
.longitude(hub.longitude)
|
||||||
.placeId(hub.placeId)
|
.placeId(hub.placeId)
|
||||||
.city(hub.city)
|
.city(hub.city)
|
||||||
.state(hub.state)
|
.state(hub.state)
|
||||||
.street(hub.street)
|
.street(hub.street)
|
||||||
.country(hub.country)
|
.country(hub.country)
|
||||||
.status(dc.ShiftStatus.OPEN)
|
.status(dc.ShiftStatus.OPEN)
|
||||||
.workersNeeded(workersNeeded)
|
.workersNeeded(workersNeeded)
|
||||||
.filled(0)
|
.filled(0)
|
||||||
.durationDays(1)
|
.durationDays(1)
|
||||||
.cost(shiftCost)
|
.cost(shiftCost)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String shiftId = shiftResult.data.shift_insert.id;
|
final String shiftId = shiftResult.data.shift_insert.id;
|
||||||
|
|
||||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||||
final DateTime start = _parseTime(order.date, position.startTime);
|
final DateTime start = _parseTime(order.date, position.startTime);
|
||||||
final DateTime end = _parseTime(order.date, position.endTime);
|
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 hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||||
final double rate = order.roleRates[position.role] ?? 0;
|
final double rate = order.roleRates[position.role] ?? 0;
|
||||||
final double totalValue = rate * hours * position.count;
|
final double totalValue = rate * hours * position.count;
|
||||||
@@ -139,7 +108,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
|
|
||||||
await _service.connector
|
await _service.connector
|
||||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||||
.shifts(fdc.AnyValue(<String>[shiftId]))
|
.shifts(AnyValue(<String>[shiftId]))
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -162,74 +131,78 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
order.startDate.month,
|
order.startDate.month,
|
||||||
order.startDate.day,
|
order.startDate.day,
|
||||||
);
|
);
|
||||||
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||||
final fdc.Timestamp startTimestamp = orderTimestamp;
|
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
|
||||||
final fdc.Timestamp endTimestamp = _service.toTimestamp(order.endDate);
|
final Timestamp endTimestamp = _service.toTimestamp(order.endDate);
|
||||||
|
|
||||||
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
|
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||||
await _service.connector
|
orderResult = await _service.connector
|
||||||
.createOrder(
|
.createOrder(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
orderType: dc.OrderType.RECURRING,
|
orderType: dc.OrderType.RECURRING,
|
||||||
teamHubId: hub.id,
|
teamHubId: hub.id,
|
||||||
)
|
)
|
||||||
.vendorId(vendorId)
|
.vendorId(vendorId)
|
||||||
.eventName(order.eventName)
|
.eventName(order.eventName)
|
||||||
.status(dc.OrderStatus.POSTED)
|
.status(dc.OrderStatus.POSTED)
|
||||||
.date(orderTimestamp)
|
.date(orderTimestamp)
|
||||||
.startDate(startTimestamp)
|
.startDate(startTimestamp)
|
||||||
.endDate(endTimestamp)
|
.endDate(endTimestamp)
|
||||||
.recurringDays(order.recurringDays)
|
.recurringDays(order.recurringDays)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String orderId = orderResult.data.order_insert.id;
|
final String orderId = orderResult.data.order_insert.id;
|
||||||
|
|
||||||
// NOTE: Recurring orders are limited to 30 days of generated shifts.
|
// NOTE: Recurring orders are limited to 30 days of generated shifts.
|
||||||
// Future shifts beyond 30 days should be created by a scheduled job.
|
// Future shifts beyond 30 days should be created by a scheduled job.
|
||||||
final DateTime maxEndDate = orderDateOnly.add(const Duration(days: 29));
|
final DateTime maxEndDate = orderDateOnly.add(const Duration(days: 29));
|
||||||
final DateTime effectiveEndDate =
|
final DateTime effectiveEndDate = order.endDate.isAfter(maxEndDate)
|
||||||
order.endDate.isAfter(maxEndDate) ? maxEndDate : order.endDate;
|
? maxEndDate
|
||||||
|
: order.endDate;
|
||||||
|
|
||||||
final Set<String> selectedDays = Set<String>.from(order.recurringDays);
|
final Set<String> selectedDays = Set<String>.from(order.recurringDays);
|
||||||
final int workersNeeded = order.positions.fold<int>(
|
final int workersNeeded = order.positions.fold<int>(
|
||||||
0,
|
0,
|
||||||
(int sum, domain.RecurringOrderPosition position) => sum + position.count,
|
(int sum, domain.RecurringOrderPosition position) =>
|
||||||
|
sum + position.count,
|
||||||
);
|
);
|
||||||
final double shiftCost = _calculateRecurringShiftCost(order);
|
final double shiftCost = _calculateRecurringShiftCost(order);
|
||||||
|
|
||||||
final List<String> shiftIds = <String>[];
|
final List<String> shiftIds = <String>[];
|
||||||
for (DateTime day = orderDateOnly;
|
for (
|
||||||
!day.isAfter(effectiveEndDate);
|
DateTime day = orderDateOnly;
|
||||||
day = day.add(const Duration(days: 1))) {
|
!day.isAfter(effectiveEndDate);
|
||||||
|
day = day.add(const Duration(days: 1))
|
||||||
|
) {
|
||||||
final String dayLabel = _weekdayLabel(day);
|
final String dayLabel = _weekdayLabel(day);
|
||||||
if (!selectedDays.contains(dayLabel)) {
|
if (!selectedDays.contains(dayLabel)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
||||||
final fdc.Timestamp dayTimestamp = _service.toTimestamp(
|
final Timestamp dayTimestamp = _service.toTimestamp(
|
||||||
DateTime(day.year, day.month, day.day),
|
DateTime(day.year, day.month, day.day),
|
||||||
);
|
);
|
||||||
|
|
||||||
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
|
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||||
await _service.connector
|
shiftResult = await _service.connector
|
||||||
.createShift(title: shiftTitle, orderId: orderId)
|
.createShift(title: shiftTitle, orderId: orderId)
|
||||||
.date(dayTimestamp)
|
.date(dayTimestamp)
|
||||||
.location(hub.name)
|
.location(hub.name)
|
||||||
.locationAddress(hub.address)
|
.locationAddress(hub.address)
|
||||||
.latitude(hub.latitude)
|
.latitude(hub.latitude)
|
||||||
.longitude(hub.longitude)
|
.longitude(hub.longitude)
|
||||||
.placeId(hub.placeId)
|
.placeId(hub.placeId)
|
||||||
.city(hub.city)
|
.city(hub.city)
|
||||||
.state(hub.state)
|
.state(hub.state)
|
||||||
.street(hub.street)
|
.street(hub.street)
|
||||||
.country(hub.country)
|
.country(hub.country)
|
||||||
.status(dc.ShiftStatus.OPEN)
|
.status(dc.ShiftStatus.OPEN)
|
||||||
.workersNeeded(workersNeeded)
|
.workersNeeded(workersNeeded)
|
||||||
.filled(0)
|
.filled(0)
|
||||||
.durationDays(1)
|
.durationDays(1)
|
||||||
.cost(shiftCost)
|
.cost(shiftCost)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String shiftId = shiftResult.data.shift_insert.id;
|
final String shiftId = shiftResult.data.shift_insert.id;
|
||||||
shiftIds.add(shiftId);
|
shiftIds.add(shiftId);
|
||||||
@@ -237,8 +210,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
for (final domain.RecurringOrderPosition position in order.positions) {
|
for (final domain.RecurringOrderPosition position in order.positions) {
|
||||||
final DateTime start = _parseTime(day, position.startTime);
|
final DateTime start = _parseTime(day, position.startTime);
|
||||||
final DateTime end = _parseTime(day, position.endTime);
|
final DateTime end = _parseTime(day, position.endTime);
|
||||||
final DateTime normalizedEnd =
|
final DateTime normalizedEnd = end.isBefore(start)
|
||||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
? end.add(const Duration(days: 1))
|
||||||
|
: end;
|
||||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||||
final double rate = order.roleRates[position.role] ?? 0;
|
final double rate = order.roleRates[position.role] ?? 0;
|
||||||
final double totalValue = rate * hours * position.count;
|
final double totalValue = rate * hours * position.count;
|
||||||
@@ -261,7 +235,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
|
|
||||||
await _service.connector
|
await _service.connector
|
||||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||||
.shifts(fdc.AnyValue(shiftIds))
|
.shifts(AnyValue(shiftIds))
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -284,23 +258,23 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
order.startDate.month,
|
order.startDate.month,
|
||||||
order.startDate.day,
|
order.startDate.day,
|
||||||
);
|
);
|
||||||
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
|
||||||
final fdc.Timestamp startTimestamp = orderTimestamp;
|
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
|
||||||
|
|
||||||
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
|
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
|
||||||
await _service.connector
|
orderResult = await _service.connector
|
||||||
.createOrder(
|
.createOrder(
|
||||||
businessId: businessId,
|
businessId: businessId,
|
||||||
orderType: dc.OrderType.PERMANENT,
|
orderType: dc.OrderType.PERMANENT,
|
||||||
teamHubId: hub.id,
|
teamHubId: hub.id,
|
||||||
)
|
)
|
||||||
.vendorId(vendorId)
|
.vendorId(vendorId)
|
||||||
.eventName(order.eventName)
|
.eventName(order.eventName)
|
||||||
.status(dc.OrderStatus.POSTED)
|
.status(dc.OrderStatus.POSTED)
|
||||||
.date(orderTimestamp)
|
.date(orderTimestamp)
|
||||||
.startDate(startTimestamp)
|
.startDate(startTimestamp)
|
||||||
.permanentDays(order.permanentDays)
|
.permanentDays(order.permanentDays)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String orderId = orderResult.data.order_insert.id;
|
final String orderId = orderResult.data.order_insert.id;
|
||||||
|
|
||||||
@@ -316,38 +290,40 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
final double shiftCost = _calculatePermanentShiftCost(order);
|
final double shiftCost = _calculatePermanentShiftCost(order);
|
||||||
|
|
||||||
final List<String> shiftIds = <String>[];
|
final List<String> shiftIds = <String>[];
|
||||||
for (DateTime day = orderDateOnly;
|
for (
|
||||||
!day.isAfter(maxEndDate);
|
DateTime day = orderDateOnly;
|
||||||
day = day.add(const Duration(days: 1))) {
|
!day.isAfter(maxEndDate);
|
||||||
|
day = day.add(const Duration(days: 1))
|
||||||
|
) {
|
||||||
final String dayLabel = _weekdayLabel(day);
|
final String dayLabel = _weekdayLabel(day);
|
||||||
if (!selectedDays.contains(dayLabel)) {
|
if (!selectedDays.contains(dayLabel)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
final String shiftTitle = 'Shift ${_formatDate(day)}';
|
||||||
final fdc.Timestamp dayTimestamp = _service.toTimestamp(
|
final Timestamp dayTimestamp = _service.toTimestamp(
|
||||||
DateTime(day.year, day.month, day.day),
|
DateTime(day.year, day.month, day.day),
|
||||||
);
|
);
|
||||||
|
|
||||||
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
|
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
|
||||||
await _service.connector
|
shiftResult = await _service.connector
|
||||||
.createShift(title: shiftTitle, orderId: orderId)
|
.createShift(title: shiftTitle, orderId: orderId)
|
||||||
.date(dayTimestamp)
|
.date(dayTimestamp)
|
||||||
.location(hub.name)
|
.location(hub.name)
|
||||||
.locationAddress(hub.address)
|
.locationAddress(hub.address)
|
||||||
.latitude(hub.latitude)
|
.latitude(hub.latitude)
|
||||||
.longitude(hub.longitude)
|
.longitude(hub.longitude)
|
||||||
.placeId(hub.placeId)
|
.placeId(hub.placeId)
|
||||||
.city(hub.city)
|
.city(hub.city)
|
||||||
.state(hub.state)
|
.state(hub.state)
|
||||||
.street(hub.street)
|
.street(hub.street)
|
||||||
.country(hub.country)
|
.country(hub.country)
|
||||||
.status(dc.ShiftStatus.OPEN)
|
.status(dc.ShiftStatus.OPEN)
|
||||||
.workersNeeded(workersNeeded)
|
.workersNeeded(workersNeeded)
|
||||||
.filled(0)
|
.filled(0)
|
||||||
.durationDays(1)
|
.durationDays(1)
|
||||||
.cost(shiftCost)
|
.cost(shiftCost)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final String shiftId = shiftResult.data.shift_insert.id;
|
final String shiftId = shiftResult.data.shift_insert.id;
|
||||||
shiftIds.add(shiftId);
|
shiftIds.add(shiftId);
|
||||||
@@ -355,8 +331,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||||
final DateTime start = _parseTime(day, position.startTime);
|
final DateTime start = _parseTime(day, position.startTime);
|
||||||
final DateTime end = _parseTime(day, position.endTime);
|
final DateTime end = _parseTime(day, position.endTime);
|
||||||
final DateTime normalizedEnd =
|
final DateTime normalizedEnd = end.isBefore(start)
|
||||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
? end.add(const Duration(days: 1))
|
||||||
|
: end;
|
||||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||||
final double rate = order.roleRates[position.role] ?? 0;
|
final double rate = order.roleRates[position.role] ?? 0;
|
||||||
final double totalValue = rate * hours * position.count;
|
final double totalValue = rate * hours * position.count;
|
||||||
@@ -379,7 +356,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
|
|
||||||
await _service.connector
|
await _service.connector
|
||||||
.updateOrder(id: orderId, teamHubId: hub.id)
|
.updateOrder(id: orderId, teamHubId: hub.id)
|
||||||
.shifts(fdc.AnyValue(shiftIds))
|
.shifts(AnyValue(shiftIds))
|
||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -396,13 +373,76 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
throw UnimplementedError('Reorder functionality is not yet implemented.');
|
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 _calculateShiftCost(domain.OneTimeOrder order) {
|
||||||
double total = 0;
|
double total = 0;
|
||||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||||
final DateTime start = _parseTime(order.date, position.startTime);
|
final DateTime start = _parseTime(order.date, position.startTime);
|
||||||
final DateTime end = _parseTime(order.date, position.endTime);
|
final DateTime end = _parseTime(order.date, position.endTime);
|
||||||
final DateTime normalizedEnd =
|
final DateTime normalizedEnd = end.isBefore(start)
|
||||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
? end.add(const Duration(days: 1))
|
||||||
|
: end;
|
||||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||||
final double rate = order.roleRates[position.role] ?? 0;
|
final double rate = order.roleRates[position.role] ?? 0;
|
||||||
total += rate * hours * position.count;
|
total += rate * hours * position.count;
|
||||||
@@ -415,8 +455,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
for (final domain.RecurringOrderPosition position in order.positions) {
|
for (final domain.RecurringOrderPosition position in order.positions) {
|
||||||
final DateTime start = _parseTime(order.startDate, position.startTime);
|
final DateTime start = _parseTime(order.startDate, position.startTime);
|
||||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||||
final DateTime normalizedEnd =
|
final DateTime normalizedEnd = end.isBefore(start)
|
||||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
? end.add(const Duration(days: 1))
|
||||||
|
: end;
|
||||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||||
final double rate = order.roleRates[position.role] ?? 0;
|
final double rate = order.roleRates[position.role] ?? 0;
|
||||||
total += rate * hours * position.count;
|
total += rate * hours * position.count;
|
||||||
@@ -429,8 +470,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
for (final domain.OneTimeOrderPosition position in order.positions) {
|
for (final domain.OneTimeOrderPosition position in order.positions) {
|
||||||
final DateTime start = _parseTime(order.startDate, position.startTime);
|
final DateTime start = _parseTime(order.startDate, position.startTime);
|
||||||
final DateTime end = _parseTime(order.startDate, position.endTime);
|
final DateTime end = _parseTime(order.startDate, position.endTime);
|
||||||
final DateTime normalizedEnd =
|
final DateTime normalizedEnd = end.isBefore(start)
|
||||||
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
|
? end.add(const Duration(days: 1))
|
||||||
|
: end;
|
||||||
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
|
||||||
final double rate = order.roleRates[position.role] ?? 0;
|
final double rate = order.roleRates[position.role] ?? 0;
|
||||||
total += rate * hours * position.count;
|
total += rate * hours * position.count;
|
||||||
@@ -506,4 +548,49 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
|
|||||||
final String day = dateTime.day.toString().padLeft(2, '0');
|
final String day = dateTime.day.toString().padLeft(2, '0');
|
||||||
return '$year-$month-$day';
|
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.
|
/// Interface for the Client Create Order repository.
|
||||||
///
|
///
|
||||||
/// This repository is responsible for:
|
/// This repository is responsible for:
|
||||||
/// 1. Retrieving available order types for the client.
|
/// 1. Submitting different types of staffing orders (Rapid, One-Time, Recurring, Permanent).
|
||||||
/// 2. Submitting different types of staffing orders (Rapid, One-Time).
|
|
||||||
///
|
///
|
||||||
/// It follows the KROW Clean Architecture by defining the contract in the
|
/// It follows the KROW Clean Architecture by defining the contract in the
|
||||||
/// domain layer, to be implemented in the data layer.
|
/// domain layer, to be implemented in the data layer.
|
||||||
abstract interface class ClientCreateOrderRepositoryInterface {
|
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.
|
/// Submits a one-time staffing order with specific details.
|
||||||
///
|
///
|
||||||
/// [order] contains the date, location, and required positions.
|
/// [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.
|
/// [previousOrderId] is the ID of the order to reorder.
|
||||||
/// [newDate] is the new date for the order.
|
/// [newDate] is the new date for the order.
|
||||||
Future<void> reorder(String previousOrderId, DateTime newDate);
|
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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../../domain/arguments/one_time_order_arguments.dart';
|
|
||||||
import '../../domain/usecases/create_one_time_order_usecase.dart';
|
|
||||||
import 'one_time_order_event.dart';
|
import 'one_time_order_event.dart';
|
||||||
import 'one_time_order_state.dart';
|
import 'one_time_order_state.dart';
|
||||||
|
|
||||||
/// BLoC for managing the multi-step one-time order creation form.
|
/// BLoC for managing the multi-step one-time order creation form.
|
||||||
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
||||||
with BlocErrorHandler<OneTimeOrderState>, SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
|
with
|
||||||
OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._service)
|
BlocErrorHandler<OneTimeOrderState>,
|
||||||
: super(OneTimeOrderState.initial()) {
|
SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
|
||||||
|
OneTimeOrderBloc(
|
||||||
|
this._createOneTimeOrderUseCase,
|
||||||
|
this._getOrderDetailsForReorderUseCase,
|
||||||
|
this._service,
|
||||||
|
) : super(OneTimeOrderState.initial()) {
|
||||||
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
|
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
|
||||||
on<OneTimeOrderVendorChanged>(_onVendorChanged);
|
on<OneTimeOrderVendorChanged>(_onVendorChanged);
|
||||||
on<OneTimeOrderHubsLoaded>(_onHubsLoaded);
|
on<OneTimeOrderHubsLoaded>(_onHubsLoaded);
|
||||||
@@ -23,18 +30,22 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
|
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
|
||||||
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
|
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
|
||||||
on<OneTimeOrderSubmitted>(_onSubmitted);
|
on<OneTimeOrderSubmitted>(_onSubmitted);
|
||||||
|
on<OneTimeOrderInitialized>(_onInitialized);
|
||||||
|
|
||||||
_loadVendors();
|
_loadVendors();
|
||||||
_loadHubs();
|
_loadHubs();
|
||||||
}
|
}
|
||||||
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
|
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
|
||||||
|
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
|
||||||
final dc.DataConnectService _service;
|
final dc.DataConnectService _service;
|
||||||
|
|
||||||
Future<void> _loadVendors() async {
|
Future<void> _loadVendors() async {
|
||||||
final List<Vendor>? vendors = await handleErrorWithResult(
|
final List<Vendor>? vendors = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final QueryResult<dc.ListVendorsData, void> result =
|
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
|
||||||
await _service.connector.listVendors().execute();
|
.connector
|
||||||
|
.listVendors()
|
||||||
|
.execute();
|
||||||
return result.data.vendors
|
return result.data.vendors
|
||||||
.map(
|
.map(
|
||||||
(dc.ListVendorsVendors vendor) => Vendor(
|
(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(
|
final List<OneTimeOrderRoleOption>? roles = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
|
final fdc.QueryResult<
|
||||||
result = await _service.connector.listRolesByVendorId(vendorId: vendorId).execute();
|
dc.ListRolesByVendorIdData,
|
||||||
|
dc.ListRolesByVendorIdVariables
|
||||||
|
>
|
||||||
|
result = await _service.connector
|
||||||
|
.listRolesByVendorId(vendorId: vendorId)
|
||||||
|
.execute();
|
||||||
return result.data.roles
|
return result.data.roles
|
||||||
.map(
|
.map(
|
||||||
(dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption(
|
(dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption(
|
||||||
@@ -68,7 +87,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
onError: (_) => emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
|
onError: (_) =>
|
||||||
|
emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (roles != null) {
|
if (roles != null) {
|
||||||
@@ -80,7 +100,10 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult(
|
final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final String businessId = await _service.getBusinessId();
|
final String businessId = await _service.getBusinessId();
|
||||||
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
|
final fdc.QueryResult<
|
||||||
|
dc.ListTeamHubsByOwnerIdData,
|
||||||
|
dc.ListTeamHubsByOwnerIdVariables
|
||||||
|
>
|
||||||
result = await _service.connector
|
result = await _service.connector
|
||||||
.listTeamHubsByOwnerId(ownerId: businessId)
|
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||||
.execute();
|
.execute();
|
||||||
@@ -102,7 +125,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
onError: (_) => add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
|
onError: (_) =>
|
||||||
|
add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hubs != null) {
|
if (hubs != null) {
|
||||||
@@ -114,13 +138,11 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
OneTimeOrderVendorsLoaded event,
|
OneTimeOrderVendorsLoaded event,
|
||||||
Emitter<OneTimeOrderState> emit,
|
Emitter<OneTimeOrderState> emit,
|
||||||
) async {
|
) async {
|
||||||
final Vendor? selectedVendor =
|
final Vendor? selectedVendor = event.vendors.isNotEmpty
|
||||||
event.vendors.isNotEmpty ? event.vendors.first : null;
|
? event.vendors.first
|
||||||
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
||||||
vendors: event.vendors,
|
|
||||||
selectedVendor: selectedVendor,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (selectedVendor != null) {
|
if (selectedVendor != null) {
|
||||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||||
@@ -139,8 +161,9 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
OneTimeOrderHubsLoaded event,
|
OneTimeOrderHubsLoaded event,
|
||||||
Emitter<OneTimeOrderState> emit,
|
Emitter<OneTimeOrderState> emit,
|
||||||
) {
|
) {
|
||||||
final OneTimeOrderHubOption? selectedHub =
|
final OneTimeOrderHubOption? selectedHub = event.hubs.isNotEmpty
|
||||||
event.hubs.isNotEmpty ? event.hubs.first : null;
|
? event.hubs.first
|
||||||
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
hubs: event.hubs,
|
hubs: event.hubs,
|
||||||
@@ -154,12 +177,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
|
|||||||
OneTimeOrderHubChanged event,
|
OneTimeOrderHubChanged event,
|
||||||
Emitter<OneTimeOrderState> emit,
|
Emitter<OneTimeOrderState> emit,
|
||||||
) {
|
) {
|
||||||
emit(
|
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
|
||||||
state.copyWith(
|
|
||||||
selectedHub: event.hub,
|
|
||||||
location: event.hub.name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEventNameChanged(
|
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 {
|
class OneTimeOrderSubmitted extends OneTimeOrderEvent {
|
||||||
const OneTimeOrderSubmitted();
|
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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
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_event.dart';
|
||||||
import 'permanent_order_state.dart';
|
import 'permanent_order_state.dart';
|
||||||
|
|
||||||
/// BLoC for managing the permanent order creation form.
|
/// BLoC for managing the permanent order creation form.
|
||||||
class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
||||||
with BlocErrorHandler<PermanentOrderState>, SafeBloc<PermanentOrderEvent, PermanentOrderState> {
|
with
|
||||||
PermanentOrderBloc(this._createPermanentOrderUseCase, this._service)
|
BlocErrorHandler<PermanentOrderState>,
|
||||||
: super(PermanentOrderState.initial()) {
|
SafeBloc<PermanentOrderEvent, PermanentOrderState> {
|
||||||
|
PermanentOrderBloc(
|
||||||
|
this._createPermanentOrderUseCase,
|
||||||
|
this._getOrderDetailsForReorderUseCase,
|
||||||
|
this._service,
|
||||||
|
) : super(PermanentOrderState.initial()) {
|
||||||
on<PermanentOrderVendorsLoaded>(_onVendorsLoaded);
|
on<PermanentOrderVendorsLoaded>(_onVendorsLoaded);
|
||||||
on<PermanentOrderVendorChanged>(_onVendorChanged);
|
on<PermanentOrderVendorChanged>(_onVendorChanged);
|
||||||
on<PermanentOrderHubsLoaded>(_onHubsLoaded);
|
on<PermanentOrderHubsLoaded>(_onHubsLoaded);
|
||||||
@@ -23,12 +30,14 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
on<PermanentOrderPositionRemoved>(_onPositionRemoved);
|
on<PermanentOrderPositionRemoved>(_onPositionRemoved);
|
||||||
on<PermanentOrderPositionUpdated>(_onPositionUpdated);
|
on<PermanentOrderPositionUpdated>(_onPositionUpdated);
|
||||||
on<PermanentOrderSubmitted>(_onSubmitted);
|
on<PermanentOrderSubmitted>(_onSubmitted);
|
||||||
|
on<PermanentOrderInitialized>(_onInitialized);
|
||||||
|
|
||||||
_loadVendors();
|
_loadVendors();
|
||||||
_loadHubs();
|
_loadHubs();
|
||||||
}
|
}
|
||||||
|
|
||||||
final CreatePermanentOrderUseCase _createPermanentOrderUseCase;
|
final CreatePermanentOrderUseCase _createPermanentOrderUseCase;
|
||||||
|
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
|
||||||
final dc.DataConnectService _service;
|
final dc.DataConnectService _service;
|
||||||
|
|
||||||
static const List<String> _dayLabels = <String>[
|
static const List<String> _dayLabels = <String>[
|
||||||
@@ -44,8 +53,10 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
Future<void> _loadVendors() async {
|
Future<void> _loadVendors() async {
|
||||||
final List<domain.Vendor>? vendors = await handleErrorWithResult(
|
final List<domain.Vendor>? vendors = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final QueryResult<dc.ListVendorsData, void> result =
|
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
|
||||||
await _service.connector.listVendors().execute();
|
.connector
|
||||||
|
.listVendors()
|
||||||
|
.execute();
|
||||||
return result.data.vendors
|
return result.data.vendors
|
||||||
.map(
|
.map(
|
||||||
(dc.ListVendorsVendors vendor) => domain.Vendor(
|
(dc.ListVendorsVendors vendor) => domain.Vendor(
|
||||||
@@ -70,10 +81,13 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
) async {
|
) async {
|
||||||
final List<PermanentOrderRoleOption>? roles = await handleErrorWithResult(
|
final List<PermanentOrderRoleOption>? roles = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
|
final fdc.QueryResult<
|
||||||
result = await _service.connector
|
dc.ListRolesByVendorIdData,
|
||||||
.listRolesByVendorId(vendorId: vendorId)
|
dc.ListRolesByVendorIdVariables
|
||||||
.execute();
|
>
|
||||||
|
result = await _service.connector
|
||||||
|
.listRolesByVendorId(vendorId: vendorId)
|
||||||
|
.execute();
|
||||||
return result.data.roles
|
return result.data.roles
|
||||||
.map(
|
.map(
|
||||||
(dc.ListRolesByVendorIdRoles role) => PermanentOrderRoleOption(
|
(dc.ListRolesByVendorIdRoles role) => PermanentOrderRoleOption(
|
||||||
@@ -84,7 +98,8 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
onError: (_) => emit(state.copyWith(roles: const <PermanentOrderRoleOption>[])),
|
onError: (_) =>
|
||||||
|
emit(state.copyWith(roles: const <PermanentOrderRoleOption>[])),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (roles != null) {
|
if (roles != null) {
|
||||||
@@ -96,10 +111,13 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
final List<PermanentOrderHubOption>? hubs = await handleErrorWithResult(
|
final List<PermanentOrderHubOption>? hubs = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final String businessId = await _service.getBusinessId();
|
final String businessId = await _service.getBusinessId();
|
||||||
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
|
final fdc.QueryResult<
|
||||||
result = await _service.connector
|
dc.ListTeamHubsByOwnerIdData,
|
||||||
.listTeamHubsByOwnerId(ownerId: businessId)
|
dc.ListTeamHubsByOwnerIdVariables
|
||||||
.execute();
|
>
|
||||||
|
result = await _service.connector
|
||||||
|
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||||
|
.execute();
|
||||||
return result.data.teamHubs
|
return result.data.teamHubs
|
||||||
.map(
|
.map(
|
||||||
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => PermanentOrderHubOption(
|
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => PermanentOrderHubOption(
|
||||||
@@ -118,7 +136,8 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
onError: (_) => add(const PermanentOrderHubsLoaded(<PermanentOrderHubOption>[])),
|
onError: (_) =>
|
||||||
|
add(const PermanentOrderHubsLoaded(<PermanentOrderHubOption>[])),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hubs != null) {
|
if (hubs != null) {
|
||||||
@@ -130,13 +149,11 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
PermanentOrderVendorsLoaded event,
|
PermanentOrderVendorsLoaded event,
|
||||||
Emitter<PermanentOrderState> emit,
|
Emitter<PermanentOrderState> emit,
|
||||||
) async {
|
) async {
|
||||||
final domain.Vendor? selectedVendor =
|
final domain.Vendor? selectedVendor = event.vendors.isNotEmpty
|
||||||
event.vendors.isNotEmpty ? event.vendors.first : null;
|
? event.vendors.first
|
||||||
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
||||||
vendors: event.vendors,
|
|
||||||
selectedVendor: selectedVendor,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (selectedVendor != null) {
|
if (selectedVendor != null) {
|
||||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||||
@@ -155,8 +172,9 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
PermanentOrderHubsLoaded event,
|
PermanentOrderHubsLoaded event,
|
||||||
Emitter<PermanentOrderState> emit,
|
Emitter<PermanentOrderState> emit,
|
||||||
) {
|
) {
|
||||||
final PermanentOrderHubOption? selectedHub =
|
final PermanentOrderHubOption? selectedHub = event.hubs.isNotEmpty
|
||||||
event.hubs.isNotEmpty ? event.hubs.first : null;
|
? event.hubs.first
|
||||||
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
hubs: event.hubs,
|
hubs: event.hubs,
|
||||||
@@ -170,12 +188,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
PermanentOrderHubChanged event,
|
PermanentOrderHubChanged event,
|
||||||
Emitter<PermanentOrderState> emit,
|
Emitter<PermanentOrderState> emit,
|
||||||
) {
|
) {
|
||||||
emit(
|
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
|
||||||
state.copyWith(
|
|
||||||
selectedHub: event.hub,
|
|
||||||
location: event.hub.name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEventNameChanged(
|
void _onEventNameChanged(
|
||||||
@@ -225,7 +238,12 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
|
|||||||
} else {
|
} else {
|
||||||
days.add(label);
|
days.add(label);
|
||||||
}
|
}
|
||||||
emit(state.copyWith(permanentDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
permanentDays: _sortDays(days),
|
||||||
|
autoSelectedDayIndex: autoIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPositionAdded(
|
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) {
|
static List<String> _sortDays(List<String> days) {
|
||||||
days.sort(
|
days.sort(
|
||||||
(String a, String b) =>
|
(String a, String b) =>
|
||||||
@@ -98,3 +98,11 @@ class PermanentOrderPositionUpdated extends PermanentOrderEvent {
|
|||||||
class PermanentOrderSubmitted extends PermanentOrderEvent {
|
class PermanentOrderSubmitted extends PermanentOrderEvent {
|
||||||
const PermanentOrderSubmitted();
|
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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_core/core.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_event.dart';
|
||||||
import 'rapid_order_state.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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
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_event.dart';
|
||||||
import 'recurring_order_state.dart';
|
import 'recurring_order_state.dart';
|
||||||
|
|
||||||
/// BLoC for managing the recurring order creation form.
|
/// BLoC for managing the recurring order creation form.
|
||||||
class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
||||||
with BlocErrorHandler<RecurringOrderState>, SafeBloc<RecurringOrderEvent, RecurringOrderState> {
|
with
|
||||||
RecurringOrderBloc(this._createRecurringOrderUseCase, this._service)
|
BlocErrorHandler<RecurringOrderState>,
|
||||||
: super(RecurringOrderState.initial()) {
|
SafeBloc<RecurringOrderEvent, RecurringOrderState> {
|
||||||
|
RecurringOrderBloc(
|
||||||
|
this._createRecurringOrderUseCase,
|
||||||
|
this._getOrderDetailsForReorderUseCase,
|
||||||
|
this._service,
|
||||||
|
) : super(RecurringOrderState.initial()) {
|
||||||
on<RecurringOrderVendorsLoaded>(_onVendorsLoaded);
|
on<RecurringOrderVendorsLoaded>(_onVendorsLoaded);
|
||||||
on<RecurringOrderVendorChanged>(_onVendorChanged);
|
on<RecurringOrderVendorChanged>(_onVendorChanged);
|
||||||
on<RecurringOrderHubsLoaded>(_onHubsLoaded);
|
on<RecurringOrderHubsLoaded>(_onHubsLoaded);
|
||||||
@@ -24,12 +31,14 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
on<RecurringOrderPositionRemoved>(_onPositionRemoved);
|
on<RecurringOrderPositionRemoved>(_onPositionRemoved);
|
||||||
on<RecurringOrderPositionUpdated>(_onPositionUpdated);
|
on<RecurringOrderPositionUpdated>(_onPositionUpdated);
|
||||||
on<RecurringOrderSubmitted>(_onSubmitted);
|
on<RecurringOrderSubmitted>(_onSubmitted);
|
||||||
|
on<RecurringOrderInitialized>(_onInitialized);
|
||||||
|
|
||||||
_loadVendors();
|
_loadVendors();
|
||||||
_loadHubs();
|
_loadHubs();
|
||||||
}
|
}
|
||||||
|
|
||||||
final CreateRecurringOrderUseCase _createRecurringOrderUseCase;
|
final CreateRecurringOrderUseCase _createRecurringOrderUseCase;
|
||||||
|
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
|
||||||
final dc.DataConnectService _service;
|
final dc.DataConnectService _service;
|
||||||
|
|
||||||
static const List<String> _dayLabels = <String>[
|
static const List<String> _dayLabels = <String>[
|
||||||
@@ -45,8 +54,10 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
Future<void> _loadVendors() async {
|
Future<void> _loadVendors() async {
|
||||||
final List<domain.Vendor>? vendors = await handleErrorWithResult(
|
final List<domain.Vendor>? vendors = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final QueryResult<dc.ListVendorsData, void> result =
|
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
|
||||||
await _service.connector.listVendors().execute();
|
.connector
|
||||||
|
.listVendors()
|
||||||
|
.execute();
|
||||||
return result.data.vendors
|
return result.data.vendors
|
||||||
.map(
|
.map(
|
||||||
(dc.ListVendorsVendors vendor) => domain.Vendor(
|
(dc.ListVendorsVendors vendor) => domain.Vendor(
|
||||||
@@ -71,10 +82,13 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
) async {
|
) async {
|
||||||
final List<RecurringOrderRoleOption>? roles = await handleErrorWithResult(
|
final List<RecurringOrderRoleOption>? roles = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
|
final fdc.QueryResult<
|
||||||
result = await _service.connector
|
dc.ListRolesByVendorIdData,
|
||||||
.listRolesByVendorId(vendorId: vendorId)
|
dc.ListRolesByVendorIdVariables
|
||||||
.execute();
|
>
|
||||||
|
result = await _service.connector
|
||||||
|
.listRolesByVendorId(vendorId: vendorId)
|
||||||
|
.execute();
|
||||||
return result.data.roles
|
return result.data.roles
|
||||||
.map(
|
.map(
|
||||||
(dc.ListRolesByVendorIdRoles role) => RecurringOrderRoleOption(
|
(dc.ListRolesByVendorIdRoles role) => RecurringOrderRoleOption(
|
||||||
@@ -85,7 +99,8 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
onError: (_) => emit(state.copyWith(roles: const <RecurringOrderRoleOption>[])),
|
onError: (_) =>
|
||||||
|
emit(state.copyWith(roles: const <RecurringOrderRoleOption>[])),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (roles != null) {
|
if (roles != null) {
|
||||||
@@ -97,10 +112,13 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
final List<RecurringOrderHubOption>? hubs = await handleErrorWithResult(
|
final List<RecurringOrderHubOption>? hubs = await handleErrorWithResult(
|
||||||
action: () async {
|
action: () async {
|
||||||
final String businessId = await _service.getBusinessId();
|
final String businessId = await _service.getBusinessId();
|
||||||
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
|
final fdc.QueryResult<
|
||||||
result = await _service.connector
|
dc.ListTeamHubsByOwnerIdData,
|
||||||
.listTeamHubsByOwnerId(ownerId: businessId)
|
dc.ListTeamHubsByOwnerIdVariables
|
||||||
.execute();
|
>
|
||||||
|
result = await _service.connector
|
||||||
|
.listTeamHubsByOwnerId(ownerId: businessId)
|
||||||
|
.execute();
|
||||||
return result.data.teamHubs
|
return result.data.teamHubs
|
||||||
.map(
|
.map(
|
||||||
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => RecurringOrderHubOption(
|
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => RecurringOrderHubOption(
|
||||||
@@ -119,7 +137,8 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
onError: (_) => add(const RecurringOrderHubsLoaded(<RecurringOrderHubOption>[])),
|
onError: (_) =>
|
||||||
|
add(const RecurringOrderHubsLoaded(<RecurringOrderHubOption>[])),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hubs != null) {
|
if (hubs != null) {
|
||||||
@@ -131,13 +150,11 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
RecurringOrderVendorsLoaded event,
|
RecurringOrderVendorsLoaded event,
|
||||||
Emitter<RecurringOrderState> emit,
|
Emitter<RecurringOrderState> emit,
|
||||||
) async {
|
) async {
|
||||||
final domain.Vendor? selectedVendor =
|
final domain.Vendor? selectedVendor = event.vendors.isNotEmpty
|
||||||
event.vendors.isNotEmpty ? event.vendors.first : null;
|
? event.vendors.first
|
||||||
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
|
||||||
vendors: event.vendors,
|
|
||||||
selectedVendor: selectedVendor,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (selectedVendor != null) {
|
if (selectedVendor != null) {
|
||||||
await _loadRolesForVendor(selectedVendor.id, emit);
|
await _loadRolesForVendor(selectedVendor.id, emit);
|
||||||
@@ -156,8 +173,9 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
RecurringOrderHubsLoaded event,
|
RecurringOrderHubsLoaded event,
|
||||||
Emitter<RecurringOrderState> emit,
|
Emitter<RecurringOrderState> emit,
|
||||||
) {
|
) {
|
||||||
final RecurringOrderHubOption? selectedHub =
|
final RecurringOrderHubOption? selectedHub = event.hubs.isNotEmpty
|
||||||
event.hubs.isNotEmpty ? event.hubs.first : null;
|
? event.hubs.first
|
||||||
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
hubs: event.hubs,
|
hubs: event.hubs,
|
||||||
@@ -171,12 +189,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
RecurringOrderHubChanged event,
|
RecurringOrderHubChanged event,
|
||||||
Emitter<RecurringOrderState> emit,
|
Emitter<RecurringOrderState> emit,
|
||||||
) {
|
) {
|
||||||
emit(
|
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
|
||||||
state.copyWith(
|
|
||||||
selectedHub: event.hub,
|
|
||||||
location: event.hub.name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEventNameChanged(
|
void _onEventNameChanged(
|
||||||
@@ -242,7 +255,12 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
|
|||||||
} else {
|
} else {
|
||||||
days.add(label);
|
days.add(label);
|
||||||
}
|
}
|
||||||
emit(state.copyWith(recurringDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
recurringDays: _sortDays(days),
|
||||||
|
autoSelectedDayIndex: autoIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPositionAdded(
|
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) {
|
static List<String> _sortDays(List<String> days) {
|
||||||
days.sort(
|
days.sort(
|
||||||
(String a, String b) =>
|
(String a, String b) =>
|
||||||
@@ -107,3 +107,11 @@ class RecurringOrderPositionUpdated extends RecurringOrderEvent {
|
|||||||
class RecurringOrderSubmitted extends RecurringOrderEvent {
|
class RecurringOrderSubmitted extends RecurringOrderEvent {
|
||||||
const RecurringOrderSubmitted();
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import '../blocs/rapid_order_bloc.dart';
|
import '../blocs/rapid_order/rapid_order_bloc.dart';
|
||||||
import '../widgets/rapid_order/rapid_order_view.dart';
|
import '../widgets/rapid_order/rapid_order_view.dart';
|
||||||
|
|
||||||
/// Rapid Order Flow Page - Emergency staffing requests.
|
/// 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}) {
|
factory OrderTypeUiMetadata.fromId({required String id}) {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'rapid':
|
case 'rapid':
|
||||||
return const OrderTypeUiMetadata(
|
return OrderTypeUiMetadata(
|
||||||
icon: UiIcons.zap,
|
icon: UiIcons.zap,
|
||||||
backgroundColor: UiColors.tagPending,
|
backgroundColor: UiColors.iconError.withAlpha(24),
|
||||||
borderColor: UiColors.separatorSpecial,
|
borderColor: UiColors.iconError,
|
||||||
iconBackgroundColor: UiColors.textWarning,
|
iconBackgroundColor: UiColors.iconError.withAlpha(24),
|
||||||
iconColor: UiColors.white,
|
iconColor: UiColors.iconError,
|
||||||
textColor: UiColors.textWarning,
|
textColor: UiColors.iconError,
|
||||||
descriptionColor: UiColors.textWarning,
|
descriptionColor: UiColors.iconError,
|
||||||
);
|
);
|
||||||
case 'one-time':
|
case 'one-time':
|
||||||
return const OrderTypeUiMetadata(
|
return OrderTypeUiMetadata(
|
||||||
icon: UiIcons.calendar,
|
icon: UiIcons.calendar,
|
||||||
backgroundColor: UiColors.tagInProgress,
|
backgroundColor: UiColors.primary.withAlpha(24),
|
||||||
borderColor: UiColors.primaryInverse,
|
borderColor: UiColors.primary,
|
||||||
iconBackgroundColor: UiColors.primary,
|
iconBackgroundColor: UiColors.primary.withAlpha(24),
|
||||||
iconColor: UiColors.white,
|
iconColor: UiColors.primary,
|
||||||
textColor: UiColors.textLink,
|
textColor: UiColors.primary,
|
||||||
descriptionColor: UiColors.textLink,
|
descriptionColor: UiColors.primary,
|
||||||
);
|
);
|
||||||
case 'recurring':
|
case 'permanent':
|
||||||
return const OrderTypeUiMetadata(
|
return OrderTypeUiMetadata(
|
||||||
icon: UiIcons.rotateCcw,
|
icon: UiIcons.users,
|
||||||
backgroundColor: UiColors.tagSuccess,
|
backgroundColor: UiColors.textSuccess.withAlpha(24),
|
||||||
borderColor: UiColors.switchActive,
|
borderColor: UiColors.textSuccess,
|
||||||
iconBackgroundColor: UiColors.textSuccess,
|
iconBackgroundColor: UiColors.textSuccess.withAlpha(24),
|
||||||
iconColor: UiColors.white,
|
iconColor: UiColors.textSuccess,
|
||||||
textColor: UiColors.textSuccess,
|
textColor: UiColors.textSuccess,
|
||||||
descriptionColor: UiColors.textSuccess,
|
descriptionColor: UiColors.textSuccess,
|
||||||
);
|
);
|
||||||
case 'permanent':
|
case 'recurring':
|
||||||
return const OrderTypeUiMetadata(
|
return OrderTypeUiMetadata(
|
||||||
icon: UiIcons.briefcase,
|
icon: UiIcons.rotateCcw,
|
||||||
backgroundColor: UiColors.tagRefunded,
|
backgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
|
||||||
borderColor: UiColors.primaryInverse,
|
borderColor: const Color.fromARGB(255, 170, 10, 223),
|
||||||
iconBackgroundColor: UiColors.primary,
|
iconBackgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
|
||||||
iconColor: UiColors.white,
|
iconColor: const Color.fromARGB(255, 170, 10, 223),
|
||||||
textColor: UiColors.textLink,
|
textColor: const Color.fromARGB(255, 170, 10, 223),
|
||||||
descriptionColor: UiColors.textLink,
|
descriptionColor: const Color.fromARGB(255, 170, 10, 223),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return const OrderTypeUiMetadata(
|
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(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
border: Border.all(color: borderColor, width: 2),
|
border: Border.all(color: borderColor, width: 0.75),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -73,8 +73,7 @@ class OrderTypeCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Icon(icon, color: iconColor, size: 24),
|
child: Icon(icon, color: iconColor, size: 24),
|
||||||
),
|
),
|
||||||
Text(title, style: UiTypography.body2b.copyWith(color: textColor)),
|
Text(title, style: UiTypography.body1b.copyWith(color: textColor)),
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
description,
|
description,
|
||||||
@@ -5,9 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import '../../blocs/rapid_order_bloc.dart';
|
import '../../blocs/rapid_order/rapid_order_bloc.dart';
|
||||||
import '../../blocs/rapid_order_event.dart';
|
import '../../blocs/rapid_order/rapid_order_event.dart';
|
||||||
import '../../blocs/rapid_order_state.dart';
|
import '../../blocs/rapid_order/rapid_order_state.dart';
|
||||||
import 'rapid_order_example_card.dart';
|
import 'rapid_order_example_card.dart';
|
||||||
import 'rapid_order_header.dart';
|
import 'rapid_order_header.dart';
|
||||||
import 'rapid_order_success_view.dart';
|
import 'rapid_order_success_view.dart';
|
||||||
@@ -295,7 +295,6 @@ class _RapidOrderActions extends StatelessWidget {
|
|||||||
onPressed: isSubmitting || isMessageEmpty
|
onPressed: isSubmitting || isMessageEmpty
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
print('RapidOrder send pressed');
|
|
||||||
BlocProvider.of<RapidOrderBloc>(
|
BlocProvider.of<RapidOrderBloc>(
|
||||||
context,
|
context,
|
||||||
).add(const RapidOrderSubmitted());
|
).add(const RapidOrderSubmitted());
|
||||||
@@ -15,15 +15,17 @@ dependencies:
|
|||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
intl: 0.20.2
|
intl: 0.20.2
|
||||||
design_system:
|
design_system:
|
||||||
path: ../../../design_system
|
path: ../../../../design_system
|
||||||
core_localization:
|
core_localization:
|
||||||
path: ../../../core_localization
|
path: ../../../../core_localization
|
||||||
krow_domain:
|
krow_domain:
|
||||||
path: ../../../domain
|
path: ../../../../domain
|
||||||
krow_core:
|
krow_core:
|
||||||
path: ../../../core
|
path: ../../../../core
|
||||||
krow_data_connect:
|
krow_data_connect:
|
||||||
path: ../../../data_connect
|
path: ../../../../data_connect
|
||||||
|
client_orders_common:
|
||||||
|
path: ../orders_common
|
||||||
firebase_data_connect: ^0.2.2+2
|
firebase_data_connect: ^0.2.2+2
|
||||||
firebase_auth: ^6.1.4
|
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