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:
Achintha Isuru
2026-02-22 21:35:57 -05:00
committed by GitHub
280 changed files with 10422 additions and 3920 deletions

View File

@@ -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},
);
} }
} }

View File

@@ -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,
);
} }
// ========================================================================== // ==========================================================================

View File

@@ -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 pageall 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": {

View File

@@ -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';

View File

@@ -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();
});
}
}

View File

@@ -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});
}

View File

@@ -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,
});
}

View File

@@ -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,
); );

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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));
} }

View File

@@ -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,

View File

@@ -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),
); );

View File

@@ -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';

View File

@@ -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,
]; ];
} }

View File

@@ -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,
]; ];
} }

View File

@@ -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;
}
}
} }

View File

@@ -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,
];
}

View File

@@ -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,
]; ];
} }

View File

@@ -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();
}
}

View File

@@ -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),
);
}
}

View File

@@ -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];
}

View File

@@ -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];
}

View File

@@ -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(),
);
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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());
},
),
),
],
),
),
),
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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();
});
} }
} }

View File

@@ -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(),
), ),
), ),
], ],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(),
); );
} }

View File

@@ -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;
}
} }

View File

@@ -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);
} }

View File

@@ -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);
}
}

View File

@@ -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';

View File

@@ -0,0 +1,3 @@
export 'one_time_order_bloc.dart';
export 'one_time_order_event.dart';
export 'one_time_order_state.dart';

View File

@@ -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,
),
);
}
} }

View File

@@ -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];
}

View File

@@ -0,0 +1,3 @@
export 'permanent_order_bloc.dart';
export 'permanent_order_event.dart';
export 'permanent_order_state.dart';

View File

@@ -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) =>

View File

@@ -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];
}

View File

@@ -0,0 +1,3 @@
export 'rapid_order_bloc.dart';
export 'rapid_order_event.dart';
export 'rapid_order_state.dart';

View File

@@ -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';

View File

@@ -0,0 +1,3 @@
export 'recurring_order_bloc.dart';
export 'recurring_order_event.dart';
export 'recurring_order_state.dart';

View File

@@ -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) =>

View File

@@ -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];
}

View File

@@ -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();
}
}

View File

@@ -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,
);
}
}

View File

@@ -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',
);
}
}

View File

@@ -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.

View File

@@ -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',
);
}
}

View File

@@ -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',
),
];

View File

@@ -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(

View File

@@ -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;
}
},
);
},
),
),
],
),
),
),
);
}
}

View File

@@ -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,

View File

@@ -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());

View File

@@ -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

View 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

View File

@@ -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'

View File

@@ -0,0 +1,3 @@
# orders
A new Flutter project.

View File

@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

View 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

View File

@@ -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 = "../.."
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
package com.example.orders
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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