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.
///
/// This is the starting point for all order creation flows.
void toCreateOrder() {
pushNamed(ClientPaths.createOrder);
void toCreateOrder({Object? arguments}) {
navigate(ClientPaths.createOrder, arguments: arguments);
}
/// Pushes the rapid order creation flow.
///
/// Quick shift creation with simplified inputs for urgent needs.
void toCreateOrderRapid() {
pushNamed(ClientPaths.createOrderRapid);
void toCreateOrderRapid({Object? arguments}) {
pushNamed(ClientPaths.createOrderRapid, arguments: arguments);
}
/// Pushes the one-time order creation flow.
///
/// Create a shift that occurs once at a specific date and time.
void toCreateOrderOneTime() {
pushNamed(ClientPaths.createOrderOneTime);
void toCreateOrderOneTime({Object? arguments}) {
pushNamed(ClientPaths.createOrderOneTime, arguments: arguments);
}
/// Pushes the recurring order creation flow.
///
/// Create shifts that repeat on a defined schedule (daily, weekly, etc.).
void toCreateOrderRecurring() {
pushNamed(ClientPaths.createOrderRecurring);
void toCreateOrderRecurring({Object? arguments}) {
pushNamed(ClientPaths.createOrderRecurring, arguments: arguments);
}
/// Pushes the permanent order creation flow.
///
/// Create a long-term or permanent staffing position.
void toCreateOrderPermanent() {
pushNamed(ClientPaths.createOrderPermanent);
void toCreateOrderPermanent({Object? arguments}) {
pushNamed(ClientPaths.createOrderPermanent, arguments: arguments);
}
// ==========================================================================
// VIEW ORDER
// ==========================================================================
/// Navigates to the order details page to a specific date.
void toOrdersSpecificDate(DateTime date) {
navigate(
ClientPaths.orders,
arguments: <String, DateTime>{'initialDate': date},
);
}
}

View File

@@ -29,7 +29,7 @@ extension StaffNavigator on IModularNavigator {
// ==========================================================================
/// Navigates to the root get started/authentication screen.
///
///
/// This effectively logs out the user by navigating to root.
/// Used when signing out or session expires.
void toInitialPage() {
@@ -37,7 +37,7 @@ extension StaffNavigator on IModularNavigator {
}
/// Navigates to the get started page.
///
///
/// This is the landing page for unauthenticated users, offering login/signup options.
void toGetStartedPage() {
navigate(StaffPaths.getStarted);
@@ -64,7 +64,7 @@ extension StaffNavigator on IModularNavigator {
/// This is typically called after successful phone verification for new
/// staff members. Uses pushReplacement to prevent going back to verification.
void toProfileSetup() {
pushReplacementNamed(StaffPaths.profileSetup);
pushNamed(StaffPaths.profileSetup);
}
// ==========================================================================
@@ -76,7 +76,7 @@ extension StaffNavigator on IModularNavigator {
/// This is the main landing page for authenticated staff members.
/// Displays shift cards, quick actions, and notifications.
void toStaffHome() {
pushNamed(StaffPaths.home);
pushNamedAndRemoveUntil(StaffPaths.home, (_) => false);
}
/// Navigates to the staff main shell.
@@ -84,7 +84,7 @@ extension StaffNavigator on IModularNavigator {
/// This is the container with bottom navigation. Navigates to home tab
/// by default. Usually you'd navigate to a specific tab instead.
void toStaffMain() {
navigate('${StaffPaths.main}/home/');
pushNamedAndRemoveUntil('${StaffPaths.main}/home/', (_) => false);
}
// ==========================================================================
@@ -113,31 +113,28 @@ extension StaffNavigator on IModularNavigator {
if (refreshAvailable == true) {
args['refreshAvailable'] = true;
}
navigate(
StaffPaths.shifts,
arguments: args.isEmpty ? null : args,
);
navigate(StaffPaths.shifts, arguments: args.isEmpty ? null : args);
}
/// Navigates to the Payments tab.
///
/// View payment history, earnings breakdown, and tax information.
void toPayments() {
navigate(StaffPaths.payments);
pushNamedAndRemoveUntil(StaffPaths.payments, (_) => false);
}
/// Navigates to the Clock In tab.
///
/// Access time tracking interface for active shifts.
void toClockIn() {
navigate(StaffPaths.clockIn);
pushNamedAndRemoveUntil(StaffPaths.clockIn, (_) => false);
}
/// Navigates to the Profile tab.
///
/// Manage personal information, documents, and preferences.
void toProfile() {
navigate(StaffPaths.profile);
pushNamedAndRemoveUntil(StaffPaths.profile, (_) => false);
}
// ==========================================================================
@@ -155,22 +152,7 @@ extension StaffNavigator on IModularNavigator {
/// The shift object is passed as an argument and can be retrieved
/// in the details page.
void toShiftDetails(Shift shift) {
navigate(
StaffPaths.shiftDetails(shift.id),
arguments: shift,
);
}
/// Pushes the shift details page (alternative method).
///
/// Same as [toShiftDetails] but using pushNamed instead of navigate.
/// Use this when you want to add the details page to the stack rather
/// than replacing the current route.
void pushShiftDetails(Shift shift) {
pushNamed(
StaffPaths.shiftDetails(shift.id),
arguments: shift,
);
navigate(StaffPaths.shiftDetails(shift.id), arguments: shift);
}
// ==========================================================================

View File

@@ -104,7 +104,7 @@
"client_authentication": {
"get_started_page": {
"title": "Take Control of Your\nShifts and Events",
"subtitle": "Streamline your operations with powerful tools to manage schedules, track performance, and keep your team on the same 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",
"create_account_button": "Create Account"
},
@@ -452,7 +452,7 @@
},
"empty_states": {
"no_shifts_today": "No shifts scheduled for today",
"find_shifts_cta": "Find shifts ",
"find_shifts_cta": "Find shifts \u2192",
"no_shifts_tomorrow": "No shifts for tomorrow",
"no_recommended_shifts": "No recommended shifts"
},
@@ -462,7 +462,7 @@
"amount": "$amount"
},
"recommended_card": {
"act_now": " ACT NOW",
"act_now": "\u2022 ACT NOW",
"one_day": "One Day",
"today": "Today",
"applied_for": "Applied for $title",
@@ -695,7 +695,7 @@
"eta_label": "$min min",
"locked_desc": "Most app features are locked while commute mode is on. You'll be able to clock in once you arrive.",
"turn_off": "Turn Off Commute Mode",
"arrived_title": "You've Arrived! 🎉",
"arrived_title": "You've Arrived! \ud83c\udf89",
"arrived_desc": "You're at the shift location. Ready to clock in?"
},
"swipe": {
@@ -967,16 +967,16 @@
"required": "REQUIRED",
"add_photo": "Add Photo",
"added": "Added",
"pending": " Pending verification"
"pending": "\u23f3 Pending verification"
},
"attestation": "I certify that I own these items and will wear them to my shifts. I understand that items are pending manager verification at my first shift.",
"actions": {
"save": "Save Attire"
},
"validation": {
"select_required": " Select all required items",
"upload_required": " Upload photos of required items",
"accept_attestation": " Accept attestation"
"select_required": "\u2713 Select all required items",
"upload_required": "\u2713 Upload photos of required items",
"accept_attestation": "\u2713 Accept attestation"
}
},
"staff_shifts": {
@@ -1095,8 +1095,18 @@
},
"card": {
"cancelled": "CANCELLED",
"compensation": " 4hr compensation"
"compensation": "\u2022 4hr compensation"
}
},
"find_shifts": {
"search_hint": "Search jobs, location...",
"filter_all": "All Jobs",
"filter_one_day": "One Day",
"filter_multi_day": "Multi-Day",
"filter_long_term": "Long Term",
"no_jobs_title": "No jobs available",
"no_jobs_subtitle": "Check back later",
"application_submitted": "Shift application submitted!"
}
},
"staff_time_card": {
@@ -1218,11 +1228,11 @@
},
"total_spend": {
"label": "Total Spend",
"badge": " 8% vs last week"
"badge": "\u2193 8% vs last week"
},
"fill_rate": {
"label": "Fill Rate",
"badge": " 2% improvement"
"badge": "\u2191 2% improvement"
},
"avg_fill_time": {
"label": "Avg Fill Time",
@@ -1364,9 +1374,9 @@
"target_prefix": "Target: ",
"target_hours": "$hours hrs",
"target_percent": "$percent%",
"met": " Met",
"close": " Close",
"miss": " Miss"
"met": "\u2713 Met",
"close": "\u2192 Close",
"miss": "\u2717 Miss"
},
"additional_metrics_title": "ADDITIONAL METRICS",
"additional_metrics": {

View File

@@ -6,7 +6,6 @@
/// They will implement interfaces defined in feature packages once those are created.
library;
export 'src/data_connect_module.dart';
export 'src/session/client_session_store.dart';
@@ -45,10 +44,6 @@ export 'src/connectors/hubs/data/repositories/hubs_connector_repository_impl.dar
export 'src/connectors/billing/domain/repositories/billing_connector_repository.dart';
export 'src/connectors/billing/data/repositories/billing_connector_repository_impl.dart';
// Export Home Connector
export 'src/connectors/home/domain/repositories/home_connector_repository.dart';
export 'src/connectors/home/data/repositories/home_connector_repository_impl.dart';
// Export Coverage Connector
export 'src/connectors/coverage/domain/repositories/coverage_connector_repository.dart';
export 'src/connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
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.
class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
/// Creates a new [ShiftsConnectorRepositoryImpl].
ShiftsConnectorRepositoryImpl({
dc.DataConnectService? service,
}) : _service = service ?? dc.DataConnectService.instance;
ShiftsConnectorRepositoryImpl({dc.DataConnectService? service})
: _service = service ?? dc.DataConnectService.instance;
final dc.DataConnectService _service;
@@ -23,12 +22,17 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
required DateTime end,
}) async {
return _service.run(() async {
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service.connector
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service
.connector
.getApplicationsByStaffId(staffId: staffId)
.dayStart(_service.toTimestamp(start))
.dayEnd(_service.toTimestamp(end));
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await query.execute();
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
response = await query.execute();
return _mapApplicationsToShifts(response.data.applications);
});
}
@@ -45,18 +49,28 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
if (vendorId == null || vendorId.isEmpty) return <Shift>[];
final QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> response = await _service.connector
final QueryResult<
dc.ListShiftRolesByVendorIdData,
dc.ListShiftRolesByVendorIdVariables
>
response = await _service.connector
.listShiftRolesByVendorId(vendorId: vendorId)
.execute();
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles = response.data.shiftRoles;
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles =
response.data.shiftRoles;
// Fetch current applications to filter out already booked shifts
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> myAppsResponse = await _service.connector
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
myAppsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final Set<String> appliedShiftIds =
myAppsResponse.data.applications.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId).toSet();
final Set<String> appliedShiftIds = myAppsResponse.data.applications
.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId)
.toSet();
final List<Shift> mappedShifts = <Shift>[];
for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) {
@@ -67,6 +81,34 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final DateTime? endDt = _service.toDateTime(sr.endTime);
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
// Normalise orderType to uppercase for consistent checks in the UI.
// RECURRING → groups shifts into Multi-Day cards.
// PERMANENT → groups shifts into Long Term cards.
final String orderTypeStr = sr.shift.order.orderType.stringValue
.toUpperCase();
final dc.ListShiftRolesByVendorIdShiftRolesShiftOrder order =
sr.shift.order;
final DateTime? startDate = _service.toDateTime(order.startDate);
final DateTime? endDate = _service.toDateTime(order.endDate);
final String startTime = startDt != null
? DateFormat('HH:mm').format(startDt)
: '';
final String endTime = endDt != null
? DateFormat('HH:mm').format(endDt)
: '';
final List<ShiftSchedule>? schedules = _generateSchedules(
orderType: orderTypeStr,
startDate: startDate,
endDate: endDate,
recurringDays: order.recurringDays,
permanentDays: order.permanentDays,
startTime: startTime,
endTime: endTime,
);
mappedShifts.add(
Shift(
id: sr.shiftId,
@@ -78,16 +120,25 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
location: sr.shift.location ?? '',
locationAddress: sr.shift.locationAddress ?? '',
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
startTime: startTime,
endTime: endTime,
createdDate: createdDt?.toIso8601String() ?? '',
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
description: sr.shift.description,
durationDays: sr.shift.durationDays,
durationDays: sr.shift.durationDays ?? schedules?.length,
requiredSlots: sr.count,
filledSlots: sr.assigned ?? 0,
latitude: sr.shift.latitude,
longitude: sr.shift.longitude,
// orderId + orderType power the grouping and type-badge logic in
// FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType.
orderId: sr.shift.orderId,
orderType: orderTypeStr,
startDate: startDate?.toIso8601String(),
endDate: endDate?.toIso8601String(),
recurringDays: sr.shift.order.recurringDays,
permanentDays: sr.shift.order.permanentDays,
schedules: schedules,
breakInfo: BreakAdapter.fromData(
isPaid: sr.isBreakPaid ?? false,
breakTime: sr.breakType?.stringValue,
@@ -125,7 +176,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
}) async {
return _service.run(() async {
if (roleId != null && roleId.isNotEmpty) {
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
roleResult = await _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
.execute();
final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole;
@@ -137,13 +189,22 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
bool hasApplied = false;
String status = 'open';
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
appsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId)
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
.data
.applications
.where(
(dc.GetApplicationsByStaffIdApplications a) =>
a.shiftId == shiftId && a.shiftRole.roleId == roleId,
)
.firstOrNull;
if (app != null) {
@@ -181,7 +242,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
);
}
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result = await _service.connector.getShiftById(id: shiftId).execute();
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result =
await _service.connector.getShiftById(id: shiftId).execute();
final dc.GetShiftByIdShift? s = result.data.shift;
if (s == null) return null;
@@ -190,17 +252,23 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
Break? breakInfo;
try {
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
final QueryResult<
dc.ListShiftRolesByShiftIdData,
dc.ListShiftRolesByShiftIdVariables
>
rolesRes = await _service.connector
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0;
filled = 0;
for (dc.ListShiftRolesByShiftIdShiftRoles r in rolesRes.data.shiftRoles) {
for (dc.ListShiftRolesByShiftIdShiftRoles r
in rolesRes.data.shiftRoles) {
required = (required ?? 0) + r.count;
filled = (filled ?? 0) + (r.assigned ?? 0);
}
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
rolesRes.data.shiftRoles.first;
breakInfo = BreakAdapter.fromData(
isPaid: firstRole.isBreakPaid ?? false,
breakTime: firstRole.breakType?.stringValue,
@@ -247,89 +315,188 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final String targetRoleId = roleId ?? '';
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables> roleResult = await _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
// 1. Fetch the initial shift to determine order type
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
shiftResult = await _service.connector
.getShiftById(id: shiftId)
.execute();
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
if (role == null) throw Exception('Shift role not found');
final dc.GetShiftByIdShift? initialShift = shiftResult.data.shift;
if (initialShift == null) throw Exception('Shift not found');
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult = await _service.connector.getShiftById(id: shiftId).execute();
final dc.GetShiftByIdShift? shift = shiftResult.data.shift;
if (shift == null) throw Exception('Shift not found');
final dc.EnumValue<dc.OrderType> orderTypeEnum =
initialShift.order.orderType;
final bool isMultiDay =
orderTypeEnum is dc.Known<dc.OrderType> &&
(orderTypeEnum.value == dc.OrderType.RECURRING ||
orderTypeEnum.value == dc.OrderType.PERMANENT);
final List<_TargetShiftRole> targets = [];
// Validate daily limit
final DateTime? shiftDate = _service.toDateTime(shift.date);
if (shiftDate != null) {
final DateTime dayStartUtc = DateTime.utc(shiftDate.year, shiftDate.month, shiftDate.day);
final DateTime dayEndUtc = dayStartUtc.add(const Duration(days: 1)).subtract(const Duration(microseconds: 1));
final QueryResult<dc.VaidateDayStaffApplicationData, dc.VaidateDayStaffApplicationVariables> validationResponse = await _service.connector
.vaidateDayStaffApplication(staffId: staffId)
.dayStart(_service.toTimestamp(dayStartUtc))
.dayEnd(_service.toTimestamp(dayEndUtc))
if (isMultiDay) {
// 2. Fetch all shifts for this order to apply to all of them for the same role
final QueryResult<
dc.ListShiftRolesByBusinessAndOrderData,
dc.ListShiftRolesByBusinessAndOrderVariables
>
allRolesRes = await _service.connector
.listShiftRolesByBusinessAndOrder(
businessId: initialShift.order.businessId,
orderId: initialShift.orderId,
)
.execute();
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,
for (final role in allRolesRes.data.shiftRoles) {
if (role.roleId == targetRoleId) {
targets.add(
_TargetShiftRole(
shiftId: role.shiftId,
roleId: role.roleId,
count: role.count,
assigned: role.assigned ?? 0,
shiftFilled: role.shift.filled ?? 0,
date: _service.toDateTime(role.shift.date),
),
);
}
}
} else {
// Single shift application
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
roleResult = await _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
.execute();
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
if (role == null) throw Exception('Shift role not found');
targets.add(
_TargetShiftRole(
shiftId: shiftId,
roleId: targetRoleId,
)
.execute();
if (existingAppRes.data.applications.isNotEmpty) {
throw Exception('Application already exists.');
count: role.count,
assigned: role.assigned ?? 0,
shiftFilled: initialShift.filled ?? 0,
date: _service.toDateTime(initialShift.date),
),
);
}
if ((role.assigned ?? 0) >= role.count) {
throw Exception('This shift is full.');
if (targets.isEmpty) {
throw Exception('No valid shifts found to apply for.');
}
final int currentAssigned = role.assigned ?? 0;
final int currentFilled = shift.filled ?? 0;
int appliedCount = 0;
final List<String> errors = [];
String? createdAppId;
try {
final OperationResult<dc.CreateApplicationData, dc.CreateApplicationVariables> createRes = await _service.connector.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: targetRoleId,
status: dc.ApplicationStatus.CONFIRMED, // Matches existing logic
origin: dc.ApplicationOrigin.STAFF,
).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();
for (final target in targets) {
try {
await _applyToSingleShiftRole(target: target, staffId: staffId);
appliedCount++;
} catch (e) {
// For multi-shift apply, we might want to continue even if some fail due to conflicts
if (targets.length == 1) rethrow;
errors.add('Shift on ${target.date}: ${e.toString()}');
}
rethrow;
}
if (appliedCount == 0 && targets.length > 1) {
throw Exception('Failed to apply for any shifts: ${errors.join(", ")}');
}
});
}
@override
Future<void> acceptShift({
required String shiftId,
Future<void> _applyToSingleShiftRole({
required _TargetShiftRole target,
required String staffId,
}) {
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.CONFIRMED);
}) async {
// 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
@@ -337,7 +504,11 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
required String shiftId,
required String staffId,
}) {
return _updateApplicationStatus(shiftId, staffId, dc.ApplicationStatus.REJECTED);
return _updateApplicationStatus(
shiftId,
staffId,
dc.ApplicationStatus.REJECTED,
);
}
@override
@@ -351,18 +522,24 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
@override
Future<List<Shift>> getHistoryShifts({required String staffId}) async {
return _service.run(() async {
final QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await _service.connector
final QueryResult<
dc.ListCompletedApplicationsByStaffIdData,
dc.ListCompletedApplicationsByStaffIdVariables
>
response = await _service.connector
.listCompletedApplicationsByStaffId(staffId: staffId)
.execute();
final List<Shift> shifts = <Shift>[];
for (final dc.ListCompletedApplicationsByStaffIdApplications app in response.data.applications) {
for (final dc.ListCompletedApplicationsByStaffIdApplications app
in response.data.applications) {
final String roleName = app.shiftRole.role.name;
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
final String orderName =
(app.shift.order.eventName ?? '').trim().isNotEmpty
? app.shift.order.eventName!
: app.shift.order.business.businessName;
final String title = '$roleName - $orderName';
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
@@ -379,7 +556,9 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
location: app.shift.location ?? '',
locationAddress: app.shift.order.teamHub.hubName,
date: shiftDate?.toIso8601String() ?? '',
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
startTime: startDt != null
? DateFormat('HH:mm').format(startDt)
: '',
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
createdDate: createdDt?.toIso8601String() ?? '',
status: 'completed', // Hardcoded as checked out implies completion
@@ -406,7 +585,8 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
List<Shift> _mapApplicationsToShifts(List<dynamic> apps) {
return apps.map((app) {
final String roleName = app.shiftRole.role.name;
final String orderName = (app.shift.order.eventName ?? '').trim().isNotEmpty
final String orderName =
(app.shift.order.eventName ?? '').trim().isNotEmpty
? app.shift.order.eventName!
: app.shift.order.business.businessName;
final String title = '$roleName - $orderName';
@@ -418,7 +598,7 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
final bool hasCheckIn = app.checkInTime != null;
final bool hasCheckOut = app.checkOutTime != null;
String status;
if (hasCheckOut) {
status = 'completed';
@@ -479,12 +659,20 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
) async {
return _service.run(() async {
// First try to find the application
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> appsResponse = await _service.connector
final QueryResult<
dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables
>
appsResponse = await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.execute();
final dc.GetApplicationsByStaffIdApplications? app = appsResponse.data.applications
.where((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId)
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
.data
.applications
.where(
(dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId,
)
.firstOrNull;
if (app != null) {
@@ -494,24 +682,116 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
.execute();
} else if (newStatus == dc.ApplicationStatus.REJECTED) {
// If declining but no app found, create a rejected application
final QueryResult<dc.ListShiftRolesByShiftIdData, dc.ListShiftRolesByShiftIdVariables> rolesRes = await _service.connector
final QueryResult<
dc.ListShiftRolesByShiftIdData,
dc.ListShiftRolesByShiftIdVariables
>
rolesRes = await _service.connector
.listShiftRolesByShiftId(shiftId: shiftId)
.execute();
if (rolesRes.data.shiftRoles.isNotEmpty) {
final dc.ListShiftRolesByShiftIdShiftRoles firstRole = rolesRes.data.shiftRoles.first;
await _service.connector.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: firstRole.id,
status: dc.ApplicationStatus.REJECTED,
origin: dc.ApplicationOrigin.STAFF,
).execute();
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
rolesRes.data.shiftRoles.first;
await _service.connector
.createApplication(
shiftId: shiftId,
staffId: staffId,
roleId: firstRole.id,
status: dc.ApplicationStatus.REJECTED,
origin: dc.ApplicationOrigin.STAFF,
)
.execute();
}
} else {
throw Exception("Application not found for shift $shiftId");
}
});
}
/// Generates a list of [ShiftSchedule] for RECURRING or PERMANENT orders.
List<ShiftSchedule>? _generateSchedules({
required String orderType,
required DateTime? startDate,
required DateTime? endDate,
required List<String>? recurringDays,
required List<String>? permanentDays,
required String startTime,
required String endTime,
}) {
if (orderType != 'RECURRING' && orderType != 'PERMANENT') return null;
if (startDate == null || endDate == null) return null;
final List<String>? daysToInclude = orderType == 'RECURRING'
? recurringDays
: permanentDays;
if (daysToInclude == null || daysToInclude.isEmpty) return null;
final List<ShiftSchedule> schedules = <ShiftSchedule>[];
final Set<int> targetWeekdayIndex = daysToInclude
.map((String day) {
switch (day.toUpperCase()) {
case 'MONDAY':
return DateTime.monday;
case 'TUESDAY':
return DateTime.tuesday;
case 'WEDNESDAY':
return DateTime.wednesday;
case 'THURSDAY':
return DateTime.thursday;
case 'FRIDAY':
return DateTime.friday;
case 'SATURDAY':
return DateTime.saturday;
case 'SUNDAY':
return DateTime.sunday;
default:
return -1;
}
})
.where((int idx) => idx != -1)
.toSet();
DateTime current = startDate;
while (current.isBefore(endDate) ||
current.isAtSameMomentAs(endDate) ||
// Handle cases where the time component might differ slightly by checking date equality
(current.year == endDate.year &&
current.month == endDate.month &&
current.day == endDate.day)) {
if (targetWeekdayIndex.contains(current.weekday)) {
schedules.add(
ShiftSchedule(
date: current.toIso8601String(),
startTime: startTime,
endTime: endTime,
),
);
}
current = current.add(const Duration(days: 1));
// Safety break to prevent infinite loops if dates are messed up
if (schedules.length > 365) break;
}
return schedules;
}
}
class _TargetShiftRole {
final String shiftId;
final String roleId;
final int count;
final int assigned;
final int shiftFilled;
final DateTime? date;
_TargetShiftRole({
required this.shiftId,
required this.roleId,
required this.count,
required this.assigned,
required this.shiftFilled,
this.date,
});
}

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/billing/domain/repositories/billing_connector_repository.dart';
import 'connectors/billing/data/repositories/billing_connector_repository_impl.dart';
import 'connectors/home/domain/repositories/home_connector_repository.dart';
import 'connectors/home/data/repositories/home_connector_repository_impl.dart';
import 'connectors/coverage/domain/repositories/coverage_connector_repository.dart';
import 'connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
import 'services/data_connect_service.dart';
@@ -32,9 +30,6 @@ class DataConnectModule extends Module {
i.addLazySingleton<BillingConnectorRepository>(
BillingConnectorRepositoryImpl.new,
);
i.addLazySingleton<HomeConnectorRepository>(
HomeConnectorRepositoryImpl.new,
);
i.addLazySingleton<CoverageConnectorRepository>(
CoverageConnectorRepositoryImpl.new,
);

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/billing/domain/repositories/billing_connector_repository.dart';
import '../connectors/billing/data/repositories/billing_connector_repository_impl.dart';
import '../connectors/home/domain/repositories/home_connector_repository.dart';
import '../connectors/home/data/repositories/home_connector_repository_impl.dart';
import '../connectors/coverage/domain/repositories/coverage_connector_repository.dart';
import '../connectors/coverage/data/repositories/coverage_connector_repository_impl.dart';
import '../connectors/staff/domain/repositories/staff_connector_repository.dart';
@@ -39,7 +37,6 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
ShiftsConnectorRepository? _shiftsRepository;
HubsConnectorRepository? _hubsRepository;
BillingConnectorRepository? _billingRepository;
HomeConnectorRepository? _homeRepository;
CoverageConnectorRepository? _coverageRepository;
StaffConnectorRepository? _staffRepository;
@@ -63,14 +60,11 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
return _billingRepository ??= BillingConnectorRepositoryImpl(service: this);
}
/// Gets the home connector repository.
HomeConnectorRepository getHomeRepository() {
return _homeRepository ??= HomeConnectorRepositoryImpl(service: this);
}
/// Gets the coverage connector repository.
CoverageConnectorRepository getCoverageRepository() {
return _coverageRepository ??= CoverageConnectorRepositoryImpl(service: this);
return _coverageRepository ??= CoverageConnectorRepositoryImpl(
service: this,
);
}
/// Gets the staff connector repository.
@@ -84,14 +78,14 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
/// Helper to get the current staff ID from the session.
Future<String> getStaffId() async {
String? staffId = dc.StaffSessionStore.instance.session?.ownerId;
String? staffId = dc.StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
// Attempt to recover session if user is signed in
final user = auth.currentUser;
if (user != null) {
await _loadSession(user.uid);
staffId = dc.StaffSessionStore.instance.session?.ownerId;
staffId = dc.StaffSessionStore.instance.session?.staff?.id;
}
}
@@ -128,12 +122,14 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
// Load Staff Session if applicable
if (role == 'STAFF' || role == 'BOTH') {
final response = await connector.getStaffByUserId(userId: userId).execute();
final response = await connector
.getStaffByUserId(userId: userId)
.execute();
if (response.data.staffs.isNotEmpty) {
final s = response.data.staffs.first;
dc.StaffSessionStore.instance.setSession(
dc.StaffSession(
ownerId: s.id,
ownerId: s.ownerId,
staff: domain.Staff(
id: s.id,
authProviderId: s.userId,
@@ -151,7 +147,9 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
// Load Client Session if applicable
if (role == 'BUSINESS' || role == 'BOTH') {
final response = await connector.getBusinessesByUserId(userId: userId).execute();
final response = await connector
.getBusinessesByUserId(userId: userId)
.execute();
if (response.data.businesses.isNotEmpty) {
final b = response.data.businesses.first;
dc.ClientSessionStore.instance.setSession(
@@ -173,16 +171,23 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
}
}
/// Converts a Data Connect [Timestamp] to a Dart [DateTime].
/// Converts a Data Connect [Timestamp] to a Dart [DateTime] in local time.
///
/// Firebase Data Connect always stores and returns timestamps in UTC.
/// Calling [toLocal] ensures the result reflects the device's timezone so
/// that shift dates, start/end times, and formatted strings are correct for
/// the end user.
DateTime? toDateTime(dynamic timestamp) {
if (timestamp == null) return null;
if (timestamp is fdc.Timestamp) {
return timestamp.toDateTime();
return timestamp.toDateTime().toLocal();
}
return null;
}
/// Converts a Dart [DateTime] to a Data Connect [Timestamp].
///
/// Converts the [DateTime] to UTC before creating the [Timestamp].
fdc.Timestamp toTimestamp(DateTime dateTime) {
final DateTime utc = dateTime.toUtc();
final int millis = utc.millisecondsSinceEpoch;
@@ -225,7 +230,6 @@ class DataConnectService with DataErrorHandler, SessionHandlerMixin {
_shiftsRepository = null;
_hubsRepository = null;
_billingRepository = null;
_homeRepository = null;
_coverageRepository = null;
_staffRepository = null;

View File

@@ -28,6 +28,9 @@ class UiIcons {
/// Calendar icon for shifts or schedules
static const IconData calendar = _IconLib.calendar;
/// Calender check icon for shifts or schedules
static const IconData calendarCheck = _IconLib.calendarCheck;
/// Briefcase icon for jobs
static const IconData briefcase = _IconLib.briefcase;

View File

@@ -221,6 +221,14 @@ class UiTypography {
color: UiColors.textPrimary,
);
/// Headline 4 Bold - Font: Instrument Sans, Size: 20, Height: 1.5 (#121826)
static final TextStyle headline4b = _primaryBase.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
height: 1.5,
color: UiColors.textPrimary,
);
/// Headline 5 Regular - Font: Instrument Sans, Size: 18, Height: 1.5 (#121826)
static final TextStyle headline5r = _primaryBase.copyWith(
fontWeight: FontWeight.w400,

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 '../ui_icons.dart';
import 'ui_icon_button.dart';
/// A custom AppBar for the Krow UI design system.
///
/// This widget provides a consistent look and feel for top app bars across the application.
class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
const UiAppBar({
super.key,
this.title,
@@ -14,11 +16,12 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
this.leading,
this.actions,
this.height = kToolbarHeight,
this.centerTitle = true,
this.centerTitle = false,
this.onLeadingPressed,
this.showBackButton = true,
this.bottom,
});
/// The title text to display in the app bar.
final String? title;
@@ -52,17 +55,19 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return AppBar(
title: titleWidget ??
(title != null
? Text(
title!,
)
: null),
leading: leading ??
title:
titleWidget ??
(title != null ? Text(title!, style: UiTypography.headline4b) : null),
leading:
leading ??
(showBackButton
? IconButton(
icon: const Icon(UiIcons.chevronLeft, size: 20),
onPressed: onLeadingPressed ?? () => Navigator.of(context).pop(),
? UiIconButton(
icon: UiIcons.chevronLeft,
onTap: onLeadingPressed ?? () => Navigator.of(context).pop(),
backgroundColor: UiColors.transparent,
iconColor: UiColors.iconThird,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
)
: null),
actions: actions,
@@ -72,5 +77,6 @@ class UiAppBar extends StatelessWidget implements PreferredSizeWidget {
}
@override
Size get preferredSize => Size.fromHeight(height + (bottom?.preferredSize.height ?? 0.0));
Size get preferredSize =>
Size.fromHeight(height + (bottom?.preferredSize.height ?? 0.0));
}

View File

@@ -29,7 +29,6 @@ enum UiChipVariant {
/// A custom chip widget with supports for different sizes, themes, and icons.
class UiChip extends StatelessWidget {
/// Creates a [UiChip].
const UiChip({
super.key,
@@ -42,6 +41,7 @@ class UiChip extends StatelessWidget {
this.onTrailingIconTap,
this.isSelected = false,
});
/// The text label to display.
final String label;
@@ -99,7 +99,7 @@ class UiChip extends StatelessWidget {
padding: padding,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: UiConstants.radiusFull,
borderRadius: UiConstants.radiusMd,
border: _getBorder(),
),
child: content,

View File

@@ -16,6 +16,8 @@ class UiIconButton extends StatelessWidget {
required this.iconColor,
this.useBlur = false,
this.onTap,
this.shape = BoxShape.circle,
this.borderRadius,
});
/// Creates a primary variant icon button with solid background.
@@ -25,6 +27,8 @@ class UiIconButton extends StatelessWidget {
this.size = 40,
this.iconSize = 20,
this.onTap,
this.shape = BoxShape.circle,
this.borderRadius,
}) : backgroundColor = UiColors.primary,
iconColor = UiColors.white,
useBlur = false;
@@ -36,6 +40,8 @@ class UiIconButton extends StatelessWidget {
this.size = 40,
this.iconSize = 20,
this.onTap,
this.shape = BoxShape.circle,
this.borderRadius,
}) : backgroundColor = UiColors.primary.withAlpha(96),
iconColor = UiColors.primary,
useBlur = true;
@@ -60,13 +66,23 @@ class UiIconButton extends StatelessWidget {
/// Callback when the button is tapped.
final VoidCallback? onTap;
/// The shape of the button (circle or rectangle).
final BoxShape shape;
/// The border radius for rectangle shape.
final BorderRadius? borderRadius;
@override
/// Builds the icon button UI.
Widget build(BuildContext context) {
final Widget button = Container(
width: size,
height: size,
decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle),
decoration: BoxDecoration(
color: backgroundColor,
shape: shape,
borderRadius: shape == BoxShape.rectangle ? borderRadius : null,
),
child: Icon(icon, color: iconColor, size: iconSize),
);

View File

@@ -34,14 +34,15 @@ export 'src/entities/shifts/break/break.dart';
export 'src/adapters/shifts/break/break_adapter.dart';
// Orders & Requests
export 'src/entities/orders/order_type.dart';
export 'src/entities/orders/one_time_order.dart';
export 'src/entities/orders/one_time_order_position.dart';
export 'src/entities/orders/recurring_order.dart';
export 'src/entities/orders/recurring_order_position.dart';
export 'src/entities/orders/permanent_order.dart';
export 'src/entities/orders/permanent_order_position.dart';
export 'src/entities/orders/order_type.dart';
export 'src/entities/orders/order_item.dart';
export 'src/entities/orders/reorder_data.dart';
// Skills & Certs
export 'src/entities/skills/skill.dart';

View File

@@ -1,46 +1,51 @@
import 'package:equatable/equatable.dart';
/// Summary of a completed shift role used for reorder suggestions.
/// Summary of a completed order used for reorder suggestions.
class ReorderItem extends Equatable {
const ReorderItem({
required this.orderId,
required this.title,
required this.location,
required this.hourlyRate,
required this.hours,
required this.totalCost,
required this.workers,
required this.type,
this.hourlyRate = 0,
this.hours = 0,
});
/// Parent order id for the completed shift.
/// Unique identifier of the order.
final String orderId;
/// Display title (role + shift title).
/// Display title of the order (e.g., event name or first shift title).
final String title;
/// Location from the shift.
/// Location of the order (e.g., first shift location).
final String location;
/// Hourly rate from the role.
final double hourlyRate;
/// Total calculated cost for the order.
final double totalCost;
/// Total hours for the shift role.
final double hours;
/// Worker count for the shift role.
/// Total number of workers required for the order.
final int workers;
/// Order type (e.g., ONE_TIME).
/// The type of order (e.g., ONE_TIME, RECURRING).
final String type;
/// Average or primary hourly rate (optional, for display).
final double hourlyRate;
/// Total hours for the order (optional, for display).
final double hours;
@override
List<Object?> get props => <Object?>[
orderId,
title,
location,
hourlyRate,
hours,
totalCost,
workers,
type,
hourlyRate,
hours,
];
}

View File

@@ -1,5 +1,7 @@
import 'package:equatable/equatable.dart';
import 'order_type.dart';
/// Represents a customer's view of an order or shift.
///
/// This entity captures the details necessary for the dashboard/view orders screen,
@@ -9,6 +11,7 @@ class OrderItem extends Equatable {
const OrderItem({
required this.id,
required this.orderId,
required this.orderType,
required this.title,
required this.clientName,
required this.status,
@@ -20,6 +23,7 @@ class OrderItem extends Equatable {
required this.filled,
required this.workersNeeded,
required this.hourlyRate,
required this.eventName,
this.hours = 0,
this.totalValue = 0,
this.confirmedApps = const <Map<String, dynamic>>[],
@@ -31,6 +35,9 @@ class OrderItem extends Equatable {
/// Parent order identifier.
final String orderId;
/// The type of order (e.g., ONE_TIME, PERMANENT).
final OrderType orderType;
/// Title or name of the role.
final String title;
@@ -70,6 +77,9 @@ class OrderItem extends Equatable {
/// Total value for the shift role.
final double totalValue;
/// Name of the event.
final String eventName;
/// List of confirmed worker applications.
final List<Map<String, dynamic>> confirmedApps;
@@ -77,6 +87,7 @@ class OrderItem extends Equatable {
List<Object?> get props => <Object?>[
id,
orderId,
orderType,
title,
clientName,
status,
@@ -90,6 +101,7 @@ class OrderItem extends Equatable {
hourlyRate,
hours,
totalValue,
eventName,
confirmedApps,
];
}

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).
///
/// This entity defines the identity and display metadata (keys) for the order type.
/// UI-specific properties like colors and icons are handled by the presentation layer.
class OrderType extends Equatable {
/// A long-term or permanent staffing position.
permanent,
const OrderType({
required this.id,
required this.titleKey,
required this.descriptionKey,
});
/// Unique identifier for the order type.
final String id;
/// Shifts that repeat on a defined schedule.
recurring,
/// Translation key for the title.
final String titleKey;
/// A quickly created shift.
rapid;
/// Translation key for the description.
final String descriptionKey;
@override
List<Object?> get props => <Object?>[id, titleKey, descriptionKey];
/// Creates an [OrderType] from a string value (typically from the backend).
static OrderType fromString(String value) {
switch (value.toUpperCase()) {
case 'ONE_TIME':
return OrderType.oneTime;
case 'PERMANENT':
return OrderType.permanent;
case 'RECURRING':
return OrderType.recurring;
case 'RAPID':
return OrderType.rapid;
default:
return OrderType.oneTime;
}
}
}

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.orderId,
this.orderType,
this.startDate,
this.endDate,
this.recurringDays,
this.permanentDays,
this.schedules,
});
@@ -68,6 +72,10 @@ class Shift extends Equatable {
final Break? breakInfo;
final String? orderId;
final String? orderType;
final String? startDate;
final String? endDate;
final List<String>? recurringDays;
final List<String>? permanentDays;
final List<ShiftSchedule>? schedules;
@override
@@ -103,6 +111,10 @@ class Shift extends Equatable {
breakInfo,
orderId,
orderType,
startDate,
endDate,
recurringDays,
permanentDays,
schedules,
];
}

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/src/core/ref.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/home_repository_interface.dart';
/// Implementation of [HomeRepositoryInterface] that delegates to [dc.HomeConnectorRepository].
///
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// connector repository from the data_connect package.
/// Implementation of [HomeRepositoryInterface] that directly interacts with the Data Connect SDK.
class HomeRepositoryImpl implements HomeRepositoryInterface {
HomeRepositoryImpl({dc.DataConnectService? service})
: _service = service ?? dc.DataConnectService.instance;
HomeRepositoryImpl({
dc.HomeConnectorRepository? connectorRepository,
dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getHomeRepository(),
_service = service ?? dc.DataConnectService.instance;
final dc.HomeConnectorRepository _connectorRepository;
final dc.DataConnectService _service;
@override
Future<HomeDashboardData> getDashboardData() async {
final String businessId = await _service.getBusinessId();
return _connectorRepository.getDashboardData(businessId: businessId);
return _service.run(() async {
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
@@ -39,7 +111,8 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
return await _service.run(() async {
final String businessId = await _service.getBusinessId();
final QueryResult<dc.GetBusinessByIdData, dc.GetBusinessByIdVariables> businessResult = await _service.connector
final QueryResult<dc.GetBusinessByIdData, dc.GetBusinessByIdVariables>
businessResult = await _service.connector
.getBusinessById(id: businessId)
.execute();
@@ -69,8 +142,67 @@ class HomeRepositoryImpl implements HomeRepositoryInterface {
@override
Future<List<ReorderItem>> getRecentReorders() async {
final String businessId = await _service.getBusinessId();
return _connectorRepository.getRecentReorders(businessId: businessId);
return _service.run(() async {
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:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
/// A widget that displays quick actions for the client.
class ActionsWidget extends StatelessWidget {
/// Creates an [ActionsWidget].
const ActionsWidget({
super.key,
required this.onRapidPressed,
required this.onCreateOrderPressed,
this.subtitle,
});
/// Callback when RAPID is pressed.
final VoidCallback onRapidPressed;
/// Callback when Create Order is pressed.
final VoidCallback onCreateOrderPressed;
const ActionsWidget({super.key, this.subtitle});
/// Optional subtitle for the section.
final String? subtitle;
@@ -40,7 +31,7 @@ class ActionsWidget extends StatelessWidget {
iconColor: UiColors.textError,
textColor: UiColors.textError,
subtitleColor: UiColors.textError.withValues(alpha: 0.8),
onTap: onRapidPressed,
onTap: () => Modular.to.toCreateOrderRapid(),
),
),
Expanded(
@@ -54,7 +45,7 @@ class ActionsWidget extends StatelessWidget {
iconColor: UiColors.primary,
textColor: UiColors.textPrimary,
subtitleColor: UiColors.textSecondary,
onTap: onCreateOrderPressed,
onTap: () => Modular.to.toCreateOrder(),
),
),
],

View File

@@ -9,14 +9,12 @@ import '../widgets/draggable_widget_wrapper.dart';
import '../widgets/live_activity_widget.dart';
import '../widgets/reorder_widget.dart';
import '../widgets/spending_widget.dart';
import 'client_home_sheets.dart';
/// A widget that builds dashboard content based on widget ID.
///
/// This widget encapsulates the logic for rendering different dashboard
/// widgets based on their unique identifiers and current state.
class DashboardWidgetBuilder extends StatelessWidget {
/// Creates a [DashboardWidgetBuilder].
const DashboardWidgetBuilder({
required this.id,
@@ -24,6 +22,7 @@ class DashboardWidgetBuilder extends StatelessWidget {
required this.isEditMode,
super.key,
});
/// The unique identifier for the widget to build.
final String id;
@@ -62,39 +61,9 @@ class DashboardWidgetBuilder extends StatelessWidget {
switch (id) {
case 'actions':
return ActionsWidget(
onRapidPressed: () => Modular.to.toCreateOrderRapid(),
onCreateOrderPressed: () => Modular.to.toCreateOrder(),
subtitle: subtitle,
);
return ActionsWidget(subtitle: subtitle);
case 'reorder':
return ReorderWidget(
orders: state.reorderItems,
onReorderPressed: (Map<String, dynamic> data) {
ClientHomeSheets.showOrderFormSheet(
context,
data,
onSubmit: (Map<String, dynamic> submittedData) {
final String? dateStr =
submittedData['date']?.toString();
if (dateStr == null || dateStr.isEmpty) {
return;
}
final DateTime? initialDate = DateTime.tryParse(dateStr);
if (initialDate == null) {
return;
}
Modular.to.navigate(
ClientPaths.orders,
arguments: <String, dynamic>{
'initialDate': initialDate.toIso8601String(),
},
);
},
);
},
subtitle: subtitle,
);
return ReorderWidget(orders: state.reorderItems, subtitle: subtitle);
case 'spending':
return SpendingWidget(
weeklySpending: state.dashboardData.weeklySpending,

View File

@@ -1,24 +1,18 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
/// A widget that allows clients to reorder recent shifts.
class ReorderWidget extends StatelessWidget {
/// Creates a [ReorderWidget].
const ReorderWidget({
super.key,
required this.orders,
required this.onReorderPressed,
this.subtitle,
});
const ReorderWidget({super.key, required this.orders, this.subtitle});
/// Recent completed orders for reorder.
final List<ReorderItem> orders;
/// Callback when a reorder button is pressed.
final Function(Map<String, dynamic> shiftData) onReorderPressed;
/// Optional subtitle for the section.
final String? subtitle;
@@ -55,8 +49,7 @@ class ReorderWidget extends StatelessWidget {
const SizedBox(width: UiConstants.space3),
itemBuilder: (BuildContext context, int index) {
final ReorderItem order = recentOrders[index];
final double totalCost =
order.hourlyRate * order.hours * order.workers;
final double totalCost = order.totalCost;
return Container(
width: 260,
@@ -155,15 +148,17 @@ class ReorderWidget extends StatelessWidget {
leadingIcon: UiIcons.zap,
iconSize: 12,
fullWidth: true,
onPressed: () => onReorderPressed(<String, dynamic>{
'orderId': order.orderId,
'title': order.title,
'location': order.location,
'hourlyRate': order.hourlyRate,
'hours': order.hours,
'workers': order.workers,
'type': order.type,
}),
onPressed: () =>
_handleReorderPressed(context, <String, dynamic>{
'orderId': order.orderId,
'title': order.title,
'location': order.location,
'hourlyRate': order.hourlyRate,
'hours': order.hours,
'workers': order.workers,
'type': order.type,
'totalCost': order.totalCost,
}),
),
],
),
@@ -174,10 +169,34 @@ class ReorderWidget extends StatelessWidget {
],
);
}
void _handleReorderPressed(BuildContext context, Map<String, dynamic> data) {
// Override start date with today's date as requested
final Map<String, dynamic> populatedData = Map<String, dynamic>.from(data)
..['startDate'] = DateTime.now();
final String? typeStr = populatedData['type']?.toString();
if (typeStr == null || typeStr.isEmpty) {
return;
}
final OrderType orderType = OrderType.fromString(typeStr);
switch (orderType) {
case OrderType.recurring:
Modular.to.toCreateOrderRecurring(arguments: populatedData);
break;
case OrderType.permanent:
Modular.to.toCreateOrderPermanent(arguments: populatedData);
break;
case OrderType.oneTime:
default:
Modular.to.toCreateOrderOneTime(arguments: populatedData);
break;
}
}
}
class _Badge extends StatelessWidget {
const _Badge({
required this.icon,
required this.text,

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_recurring_order_usecase.dart';
import 'domain/usecases/create_rapid_order_usecase.dart';
import 'domain/usecases/get_order_types_usecase.dart';
import 'presentation/blocs/client_create_order_bloc.dart';
import 'presentation/blocs/one_time_order_bloc.dart';
import 'presentation/blocs/permanent_order_bloc.dart';
import 'presentation/blocs/recurring_order_bloc.dart';
import 'presentation/blocs/rapid_order_bloc.dart';
import 'domain/usecases/get_order_details_for_reorder_usecase.dart';
import 'presentation/blocs/index.dart';
import 'presentation/pages/create_order_page.dart';
import 'presentation/pages/one_time_order_page.dart';
import 'presentation/pages/permanent_order_page.dart';
@@ -32,17 +28,18 @@ class ClientCreateOrderModule extends Module {
@override
void binds(Injector i) {
// Repositories
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(ClientCreateOrderRepositoryImpl.new);
i.addLazySingleton<ClientCreateOrderRepositoryInterface>(
ClientCreateOrderRepositoryImpl.new,
);
// UseCases
i.addLazySingleton(GetOrderTypesUseCase.new);
i.addLazySingleton(CreateOneTimeOrderUseCase.new);
i.addLazySingleton(CreatePermanentOrderUseCase.new);
i.addLazySingleton(CreateRecurringOrderUseCase.new);
i.addLazySingleton(CreateRapidOrderUseCase.new);
i.addLazySingleton(GetOrderDetailsForReorderUseCase.new);
// BLoCs
i.add<ClientCreateOrderBloc>(ClientCreateOrderBloc.new);
i.add<RapidOrderBloc>(RapidOrderBloc.new);
i.add<OneTimeOrderBloc>(OneTimeOrderBloc.new);
i.add<PermanentOrderBloc>(PermanentOrderBloc.new);
@@ -56,19 +53,31 @@ class ClientCreateOrderModule extends Module {
child: (BuildContext context) => const ClientCreateOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRapid),
ClientPaths.childRoute(
ClientPaths.createOrder,
ClientPaths.createOrderRapid,
),
child: (BuildContext context) => const RapidOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderOneTime),
ClientPaths.childRoute(
ClientPaths.createOrder,
ClientPaths.createOrderOneTime,
),
child: (BuildContext context) => const OneTimeOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRecurring),
ClientPaths.childRoute(
ClientPaths.createOrder,
ClientPaths.createOrderRecurring,
),
child: (BuildContext context) => const RecurringOrderPage(),
);
r.child(
ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderPermanent),
ClientPaths.childRoute(
ClientPaths.createOrder,
ClientPaths.createOrderPermanent,
),
child: (BuildContext context) => const PermanentOrderPage(),
);
}

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:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
@@ -11,46 +11,13 @@ import '../../domain/repositories/client_create_order_repository_interface.dart'
///
/// It follows the KROW Clean Architecture by keeping the data layer focused
/// on delegation and data mapping, without business logic.
class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface {
ClientCreateOrderRepositoryImpl({
required dc.DataConnectService service,
}) : _service = service;
class ClientCreateOrderRepositoryImpl
implements ClientCreateOrderRepositoryInterface {
ClientCreateOrderRepositoryImpl({required dc.DataConnectService service})
: _service = service;
final dc.DataConnectService _service;
@override
Future<List<domain.OrderType>> getOrderTypes() {
return Future<List<domain.OrderType>>.value(const <domain.OrderType>[
domain.OrderType(
id: 'one-time',
titleKey: 'client_create_order.types.one_time',
descriptionKey: 'client_create_order.types.one_time_desc',
),
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
// domain.OrderType(
// id: 'rapid',
// titleKey: 'client_create_order.types.rapid',
// descriptionKey: 'client_create_order.types.rapid_desc',
// ),
domain.OrderType(
id: 'recurring',
titleKey: 'client_create_order.types.recurring',
descriptionKey: 'client_create_order.types.recurring_desc',
),
// domain.OrderType(
// id: 'permanent',
// titleKey: 'client_create_order.types.permanent',
// descriptionKey: 'client_create_order.types.permanent_desc',
// ),
domain.OrderType(
id: 'permanent',
titleKey: 'client_create_order.types.permanent',
descriptionKey: 'client_create_order.types.permanent_desc',
),
]);
}
@override
Future<void> createOneTimeOrder(domain.OneTimeOrder order) async {
return _service.run(() async {
@@ -69,19 +36,19 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
order.date.month,
order.date.day,
);
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
await _service.connector
.createOrder(
businessId: businessId,
orderType: dc.OrderType.ONE_TIME,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.execute();
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
orderResult = await _service.connector
.createOrder(
businessId: businessId,
orderType: dc.OrderType.ONE_TIME,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.execute();
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 double shiftCost = _calculateShiftCost(order);
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
await _service.connector
.createShift(title: shiftTitle, orderId: orderId)
.date(orderTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute();
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
shiftResult = await _service.connector
.createShift(title: shiftTitle, orderId: orderId)
.date(orderTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute();
final String shiftId = shiftResult.data.shift_insert.id;
for (final domain.OneTimeOrderPosition position in order.positions) {
final DateTime start = _parseTime(order.date, position.startTime);
final DateTime end = _parseTime(order.date, position.endTime);
final DateTime normalizedEnd = end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final DateTime normalizedEnd = end.isBefore(start)
? end.add(const Duration(days: 1))
: end;
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
final double rate = order.roleRates[position.role] ?? 0;
final double totalValue = rate * hours * position.count;
@@ -139,7 +108,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
await _service.connector
.updateOrder(id: orderId, teamHubId: hub.id)
.shifts(fdc.AnyValue(<String>[shiftId]))
.shifts(AnyValue(<String>[shiftId]))
.execute();
});
}
@@ -162,74 +131,78 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
order.startDate.month,
order.startDate.day,
);
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final fdc.Timestamp startTimestamp = orderTimestamp;
final fdc.Timestamp endTimestamp = _service.toTimestamp(order.endDate);
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
final Timestamp endTimestamp = _service.toTimestamp(order.endDate);
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
await _service.connector
.createOrder(
businessId: businessId,
orderType: dc.OrderType.RECURRING,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.startDate(startTimestamp)
.endDate(endTimestamp)
.recurringDays(order.recurringDays)
.execute();
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
orderResult = await _service.connector
.createOrder(
businessId: businessId,
orderType: dc.OrderType.RECURRING,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.startDate(startTimestamp)
.endDate(endTimestamp)
.recurringDays(order.recurringDays)
.execute();
final String orderId = orderResult.data.order_insert.id;
// NOTE: Recurring orders are limited to 30 days of generated shifts.
// Future shifts beyond 30 days should be created by a scheduled job.
final DateTime maxEndDate = orderDateOnly.add(const Duration(days: 29));
final DateTime effectiveEndDate =
order.endDate.isAfter(maxEndDate) ? maxEndDate : order.endDate;
final DateTime effectiveEndDate = order.endDate.isAfter(maxEndDate)
? maxEndDate
: order.endDate;
final Set<String> selectedDays = Set<String>.from(order.recurringDays);
final int workersNeeded = order.positions.fold<int>(
0,
(int sum, domain.RecurringOrderPosition position) => sum + position.count,
(int sum, domain.RecurringOrderPosition position) =>
sum + position.count,
);
final double shiftCost = _calculateRecurringShiftCost(order);
final List<String> shiftIds = <String>[];
for (DateTime day = orderDateOnly;
!day.isAfter(effectiveEndDate);
day = day.add(const Duration(days: 1))) {
for (
DateTime day = orderDateOnly;
!day.isAfter(effectiveEndDate);
day = day.add(const Duration(days: 1))
) {
final String dayLabel = _weekdayLabel(day);
if (!selectedDays.contains(dayLabel)) {
continue;
}
final String shiftTitle = 'Shift ${_formatDate(day)}';
final fdc.Timestamp dayTimestamp = _service.toTimestamp(
final Timestamp dayTimestamp = _service.toTimestamp(
DateTime(day.year, day.month, day.day),
);
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
await _service.connector
.createShift(title: shiftTitle, orderId: orderId)
.date(dayTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute();
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
shiftResult = await _service.connector
.createShift(title: shiftTitle, orderId: orderId)
.date(dayTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute();
final String shiftId = shiftResult.data.shift_insert.id;
shiftIds.add(shiftId);
@@ -237,8 +210,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
for (final domain.RecurringOrderPosition position in order.positions) {
final DateTime start = _parseTime(day, position.startTime);
final DateTime end = _parseTime(day, position.endTime);
final DateTime normalizedEnd =
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final DateTime normalizedEnd = end.isBefore(start)
? end.add(const Duration(days: 1))
: end;
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
final double rate = order.roleRates[position.role] ?? 0;
final double totalValue = rate * hours * position.count;
@@ -261,7 +235,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
await _service.connector
.updateOrder(id: orderId, teamHubId: hub.id)
.shifts(fdc.AnyValue(shiftIds))
.shifts(AnyValue(shiftIds))
.execute();
});
}
@@ -284,23 +258,23 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
order.startDate.month,
order.startDate.day,
);
final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final fdc.Timestamp startTimestamp = orderTimestamp;
final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly);
final Timestamp startTimestamp = _service.toTimestamp(order.startDate);
final fdc.OperationResult<dc.CreateOrderData, dc.CreateOrderVariables> orderResult =
await _service.connector
.createOrder(
businessId: businessId,
orderType: dc.OrderType.PERMANENT,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.startDate(startTimestamp)
.permanentDays(order.permanentDays)
.execute();
final OperationResult<dc.CreateOrderData, dc.CreateOrderVariables>
orderResult = await _service.connector
.createOrder(
businessId: businessId,
orderType: dc.OrderType.PERMANENT,
teamHubId: hub.id,
)
.vendorId(vendorId)
.eventName(order.eventName)
.status(dc.OrderStatus.POSTED)
.date(orderTimestamp)
.startDate(startTimestamp)
.permanentDays(order.permanentDays)
.execute();
final String orderId = orderResult.data.order_insert.id;
@@ -316,38 +290,40 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
final double shiftCost = _calculatePermanentShiftCost(order);
final List<String> shiftIds = <String>[];
for (DateTime day = orderDateOnly;
!day.isAfter(maxEndDate);
day = day.add(const Duration(days: 1))) {
for (
DateTime day = orderDateOnly;
!day.isAfter(maxEndDate);
day = day.add(const Duration(days: 1))
) {
final String dayLabel = _weekdayLabel(day);
if (!selectedDays.contains(dayLabel)) {
continue;
}
final String shiftTitle = 'Shift ${_formatDate(day)}';
final fdc.Timestamp dayTimestamp = _service.toTimestamp(
final Timestamp dayTimestamp = _service.toTimestamp(
DateTime(day.year, day.month, day.day),
);
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
await _service.connector
.createShift(title: shiftTitle, orderId: orderId)
.date(dayTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute();
final OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
shiftResult = await _service.connector
.createShift(title: shiftTitle, orderId: orderId)
.date(dayTimestamp)
.location(hub.name)
.locationAddress(hub.address)
.latitude(hub.latitude)
.longitude(hub.longitude)
.placeId(hub.placeId)
.city(hub.city)
.state(hub.state)
.street(hub.street)
.country(hub.country)
.status(dc.ShiftStatus.OPEN)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute();
final String shiftId = shiftResult.data.shift_insert.id;
shiftIds.add(shiftId);
@@ -355,8 +331,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
for (final domain.OneTimeOrderPosition position in order.positions) {
final DateTime start = _parseTime(day, position.startTime);
final DateTime end = _parseTime(day, position.endTime);
final DateTime normalizedEnd =
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final DateTime normalizedEnd = end.isBefore(start)
? end.add(const Duration(days: 1))
: end;
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
final double rate = order.roleRates[position.role] ?? 0;
final double totalValue = rate * hours * position.count;
@@ -379,7 +356,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
await _service.connector
.updateOrder(id: orderId, teamHubId: hub.id)
.shifts(fdc.AnyValue(shiftIds))
.shifts(AnyValue(shiftIds))
.execute();
});
}
@@ -396,13 +373,76 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
throw UnimplementedError('Reorder functionality is not yet implemented.');
}
@override
Future<domain.ReorderData> getOrderDetailsForReorder(String orderId) async {
return _service.run(() async {
final String businessId = await _service.getBusinessId();
final QueryResult<
dc.ListShiftRolesByBusinessAndOrderData,
dc.ListShiftRolesByBusinessAndOrderVariables
>
result = await _service.connector
.listShiftRolesByBusinessAndOrder(
businessId: businessId,
orderId: orderId,
)
.execute();
final List<dc.ListShiftRolesByBusinessAndOrderShiftRoles> shiftRoles =
result.data.shiftRoles;
if (shiftRoles.isEmpty) {
throw Exception('Order not found or has no roles.');
}
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrder order =
shiftRoles.first.shift.order;
final domain.OrderType orderType = _mapOrderType(order.orderType);
final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub
teamHub = order.teamHub;
return domain.ReorderData(
orderId: orderId,
eventName: order.eventName ?? '',
vendorId: order.vendorId ?? '',
orderType: orderType,
hub: domain.OneTimeOrderHubDetails(
id: teamHub.id,
name: teamHub.hubName,
address: teamHub.address,
placeId: teamHub.placeId,
latitude: 0, // Not available in this query
longitude: 0,
),
positions: shiftRoles.map((
dc.ListShiftRolesByBusinessAndOrderShiftRoles role,
) {
return domain.ReorderPosition(
roleId: role.roleId,
count: role.count,
startTime: _formatTimestamp(role.startTime),
endTime: _formatTimestamp(role.endTime),
lunchBreak: _formatBreakDuration(role.breakType),
);
}).toList(),
startDate: order.startDate?.toDateTime(),
endDate: order.endDate?.toDateTime(),
recurringDays: order.recurringDays ?? const <String>[],
permanentDays: order.permanentDays ?? const <String>[],
);
});
}
double _calculateShiftCost(domain.OneTimeOrder order) {
double total = 0;
for (final domain.OneTimeOrderPosition position in order.positions) {
final DateTime start = _parseTime(order.date, position.startTime);
final DateTime end = _parseTime(order.date, position.endTime);
final DateTime normalizedEnd =
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final DateTime normalizedEnd = end.isBefore(start)
? end.add(const Duration(days: 1))
: end;
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
final double rate = order.roleRates[position.role] ?? 0;
total += rate * hours * position.count;
@@ -415,8 +455,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
for (final domain.RecurringOrderPosition position in order.positions) {
final DateTime start = _parseTime(order.startDate, position.startTime);
final DateTime end = _parseTime(order.startDate, position.endTime);
final DateTime normalizedEnd =
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final DateTime normalizedEnd = end.isBefore(start)
? end.add(const Duration(days: 1))
: end;
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
final double rate = order.roleRates[position.role] ?? 0;
total += rate * hours * position.count;
@@ -429,8 +470,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
for (final domain.OneTimeOrderPosition position in order.positions) {
final DateTime start = _parseTime(order.startDate, position.startTime);
final DateTime end = _parseTime(order.startDate, position.endTime);
final DateTime normalizedEnd =
end.isBefore(start) ? end.add(const Duration(days: 1)) : end;
final DateTime normalizedEnd = end.isBefore(start)
? end.add(const Duration(days: 1))
: end;
final double hours = normalizedEnd.difference(start).inMinutes / 60.0;
final double rate = order.roleRates[position.role] ?? 0;
total += rate * hours * position.count;
@@ -506,4 +548,49 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
final String day = dateTime.day.toString().padLeft(2, '0');
return '$year-$month-$day';
}
String _formatTimestamp(Timestamp? value) {
if (value == null) return '';
try {
return DateFormat('HH:mm').format(value.toDateTime());
} catch (_) {
return '';
}
}
String _formatBreakDuration(dc.EnumValue<dc.BreakDuration>? breakType) {
if (breakType is dc.Known<dc.BreakDuration>) {
switch (breakType.value) {
case dc.BreakDuration.MIN_10:
return 'MIN_10';
case dc.BreakDuration.MIN_15:
return 'MIN_15';
case dc.BreakDuration.MIN_30:
return 'MIN_30';
case dc.BreakDuration.MIN_45:
return 'MIN_45';
case dc.BreakDuration.MIN_60:
return 'MIN_60';
case dc.BreakDuration.NO_BREAK:
return 'NO_BREAK';
}
}
return 'NO_BREAK';
}
domain.OrderType _mapOrderType(dc.EnumValue<dc.OrderType>? orderType) {
if (orderType is dc.Known<dc.OrderType>) {
switch (orderType.value) {
case dc.OrderType.ONE_TIME:
return domain.OrderType.oneTime;
case dc.OrderType.RECURRING:
return domain.OrderType.recurring;
case dc.OrderType.PERMANENT:
return domain.OrderType.permanent;
case dc.OrderType.RAPID:
return domain.OrderType.oneTime;
}
}
return domain.OrderType.oneTime;
}
}

View File

@@ -3,15 +3,11 @@ import 'package:krow_domain/krow_domain.dart';
/// Interface for the Client Create Order repository.
///
/// This repository is responsible for:
/// 1. Retrieving available order types for the client.
/// 2. Submitting different types of staffing orders (Rapid, One-Time).
/// 1. Submitting different types of staffing orders (Rapid, One-Time, Recurring, Permanent).
///
/// It follows the KROW Clean Architecture by defining the contract in the
/// domain layer, to be implemented in the data layer.
abstract interface class ClientCreateOrderRepositoryInterface {
/// Retrieves the list of available order types (e.g., Rapid, One-Time, Recurring).
Future<List<OrderType>> getOrderTypes();
/// Submits a one-time staffing order with specific details.
///
/// [order] contains the date, location, and required positions.
@@ -33,4 +29,9 @@ abstract interface class ClientCreateOrderRepositoryInterface {
/// [previousOrderId] is the ID of the order to reorder.
/// [newDate] is the new date for the order.
Future<void> reorder(String previousOrderId, DateTime newDate);
/// Fetches the details of an existing order to be used as a template for a new order.
///
/// returns [ReorderData] containing the order details and positions.
Future<ReorderData> getOrderDetailsForReorder(String orderId);
}

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:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/one_time_order_arguments.dart';
import '../../domain/usecases/create_one_time_order_usecase.dart';
import 'one_time_order_event.dart';
import 'one_time_order_state.dart';
/// BLoC for managing the multi-step one-time order creation form.
class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
with BlocErrorHandler<OneTimeOrderState>, SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._service)
: super(OneTimeOrderState.initial()) {
with
BlocErrorHandler<OneTimeOrderState>,
SafeBloc<OneTimeOrderEvent, OneTimeOrderState> {
OneTimeOrderBloc(
this._createOneTimeOrderUseCase,
this._getOrderDetailsForReorderUseCase,
this._service,
) : super(OneTimeOrderState.initial()) {
on<OneTimeOrderVendorsLoaded>(_onVendorsLoaded);
on<OneTimeOrderVendorChanged>(_onVendorChanged);
on<OneTimeOrderHubsLoaded>(_onHubsLoaded);
@@ -23,18 +30,22 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
on<OneTimeOrderPositionRemoved>(_onPositionRemoved);
on<OneTimeOrderPositionUpdated>(_onPositionUpdated);
on<OneTimeOrderSubmitted>(_onSubmitted);
on<OneTimeOrderInitialized>(_onInitialized);
_loadVendors();
_loadHubs();
}
final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase;
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
final dc.DataConnectService _service;
Future<void> _loadVendors() async {
final List<Vendor>? vendors = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListVendorsData, void> result =
await _service.connector.listVendors().execute();
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
.connector
.listVendors()
.execute();
return result.data.vendors
.map(
(dc.ListVendorsVendors vendor) => Vendor(
@@ -53,11 +64,19 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
}
}
Future<void> _loadRolesForVendor(String vendorId, Emitter<OneTimeOrderState> emit) async {
Future<void> _loadRolesForVendor(
String vendorId,
Emitter<OneTimeOrderState> emit,
) async {
final List<OneTimeOrderRoleOption>? roles = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
result = await _service.connector.listRolesByVendorId(vendorId: vendorId).execute();
final fdc.QueryResult<
dc.ListRolesByVendorIdData,
dc.ListRolesByVendorIdVariables
>
result = await _service.connector
.listRolesByVendorId(vendorId: vendorId)
.execute();
return result.data.roles
.map(
(dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption(
@@ -68,7 +87,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
)
.toList();
},
onError: (_) => emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
onError: (_) =>
emit(state.copyWith(roles: const <OneTimeOrderRoleOption>[])),
);
if (roles != null) {
@@ -80,7 +100,10 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
final List<OneTimeOrderHubOption>? hubs = await handleErrorWithResult(
action: () async {
final String businessId = await _service.getBusinessId();
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
final fdc.QueryResult<
dc.ListTeamHubsByOwnerIdData,
dc.ListTeamHubsByOwnerIdVariables
>
result = await _service.connector
.listTeamHubsByOwnerId(ownerId: businessId)
.execute();
@@ -102,7 +125,8 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
)
.toList();
},
onError: (_) => add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
onError: (_) =>
add(const OneTimeOrderHubsLoaded(<OneTimeOrderHubOption>[])),
);
if (hubs != null) {
@@ -114,13 +138,11 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
OneTimeOrderVendorsLoaded event,
Emitter<OneTimeOrderState> emit,
) async {
final Vendor? selectedVendor =
event.vendors.isNotEmpty ? event.vendors.first : null;
final Vendor? selectedVendor = event.vendors.isNotEmpty
? event.vendors.first
: null;
emit(
state.copyWith(
vendors: event.vendors,
selectedVendor: selectedVendor,
),
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);
@@ -139,8 +161,9 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
OneTimeOrderHubsLoaded event,
Emitter<OneTimeOrderState> emit,
) {
final OneTimeOrderHubOption? selectedHub =
event.hubs.isNotEmpty ? event.hubs.first : null;
final OneTimeOrderHubOption? selectedHub = event.hubs.isNotEmpty
? event.hubs.first
: null;
emit(
state.copyWith(
hubs: event.hubs,
@@ -154,12 +177,7 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
OneTimeOrderHubChanged event,
Emitter<OneTimeOrderState> emit,
) {
emit(
state.copyWith(
selectedHub: event.hub,
location: event.hub.name,
),
);
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
}
void _onEventNameChanged(
@@ -260,4 +278,74 @@ class OneTimeOrderBloc extends Bloc<OneTimeOrderEvent, OneTimeOrderState>
),
);
}
Future<void> _onInitialized(
OneTimeOrderInitialized event,
Emitter<OneTimeOrderState> emit,
) async {
final Map<String, dynamic> data = event.data;
final String title = data['title']?.toString() ?? '';
final DateTime? startDate = data['startDate'] as DateTime?;
final String? orderId = data['orderId']?.toString();
emit(state.copyWith(eventName: title, date: startDate ?? DateTime.now()));
if (orderId == null || orderId.isEmpty) return;
emit(state.copyWith(status: OneTimeOrderStatus.loading));
await handleError(
emit: emit.call,
action: () async {
final ReorderData orderDetails =
await _getOrderDetailsForReorderUseCase(orderId);
// Map positions
final List<OneTimeOrderPosition> positions = orderDetails.positions.map(
(ReorderPosition role) {
return OneTimeOrderPosition(
role: role.roleId,
count: role.count,
startTime: role.startTime,
endTime: role.endTime,
lunchBreak: role.lunchBreak,
);
},
).toList();
// Update state with order details
final Vendor? selectedVendor = state.vendors
.where((Vendor v) => v.id == orderDetails.vendorId)
.firstOrNull;
final OneTimeOrderHubOption? selectedHub = state.hubs
.where(
(OneTimeOrderHubOption h) =>
h.placeId == orderDetails.hub.placeId,
)
.firstOrNull;
emit(
state.copyWith(
eventName: orderDetails.eventName.isNotEmpty
? orderDetails.eventName
: title,
positions: positions,
selectedVendor: selectedVendor,
selectedHub: selectedHub,
location: selectedHub?.name ?? '',
status: OneTimeOrderStatus.initial,
),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);
}
},
onError: (String errorKey) => state.copyWith(
status: OneTimeOrderStatus.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -81,3 +81,11 @@ class OneTimeOrderPositionUpdated extends OneTimeOrderEvent {
class OneTimeOrderSubmitted extends OneTimeOrderEvent {
const OneTimeOrderSubmitted();
}
class OneTimeOrderInitialized extends OneTimeOrderEvent {
const OneTimeOrderInitialized(this.data);
final Map<String, dynamic> data;
@override
List<Object?> get props => <Object?>[data];
}

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:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/usecases/create_permanent_order_usecase.dart';
import 'permanent_order_event.dart';
import 'permanent_order_state.dart';
/// BLoC for managing the permanent order creation form.
class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
with BlocErrorHandler<PermanentOrderState>, SafeBloc<PermanentOrderEvent, PermanentOrderState> {
PermanentOrderBloc(this._createPermanentOrderUseCase, this._service)
: super(PermanentOrderState.initial()) {
with
BlocErrorHandler<PermanentOrderState>,
SafeBloc<PermanentOrderEvent, PermanentOrderState> {
PermanentOrderBloc(
this._createPermanentOrderUseCase,
this._getOrderDetailsForReorderUseCase,
this._service,
) : super(PermanentOrderState.initial()) {
on<PermanentOrderVendorsLoaded>(_onVendorsLoaded);
on<PermanentOrderVendorChanged>(_onVendorChanged);
on<PermanentOrderHubsLoaded>(_onHubsLoaded);
@@ -23,12 +30,14 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
on<PermanentOrderPositionRemoved>(_onPositionRemoved);
on<PermanentOrderPositionUpdated>(_onPositionUpdated);
on<PermanentOrderSubmitted>(_onSubmitted);
on<PermanentOrderInitialized>(_onInitialized);
_loadVendors();
_loadHubs();
}
final CreatePermanentOrderUseCase _createPermanentOrderUseCase;
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
final dc.DataConnectService _service;
static const List<String> _dayLabels = <String>[
@@ -44,8 +53,10 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
Future<void> _loadVendors() async {
final List<domain.Vendor>? vendors = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListVendorsData, void> result =
await _service.connector.listVendors().execute();
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
.connector
.listVendors()
.execute();
return result.data.vendors
.map(
(dc.ListVendorsVendors vendor) => domain.Vendor(
@@ -70,10 +81,13 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
) async {
final List<PermanentOrderRoleOption>? roles = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
result = await _service.connector
.listRolesByVendorId(vendorId: vendorId)
.execute();
final fdc.QueryResult<
dc.ListRolesByVendorIdData,
dc.ListRolesByVendorIdVariables
>
result = await _service.connector
.listRolesByVendorId(vendorId: vendorId)
.execute();
return result.data.roles
.map(
(dc.ListRolesByVendorIdRoles role) => PermanentOrderRoleOption(
@@ -84,7 +98,8 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
)
.toList();
},
onError: (_) => emit(state.copyWith(roles: const <PermanentOrderRoleOption>[])),
onError: (_) =>
emit(state.copyWith(roles: const <PermanentOrderRoleOption>[])),
);
if (roles != null) {
@@ -96,10 +111,13 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
final List<PermanentOrderHubOption>? hubs = await handleErrorWithResult(
action: () async {
final String businessId = await _service.getBusinessId();
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
result = await _service.connector
.listTeamHubsByOwnerId(ownerId: businessId)
.execute();
final fdc.QueryResult<
dc.ListTeamHubsByOwnerIdData,
dc.ListTeamHubsByOwnerIdVariables
>
result = await _service.connector
.listTeamHubsByOwnerId(ownerId: businessId)
.execute();
return result.data.teamHubs
.map(
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => PermanentOrderHubOption(
@@ -118,7 +136,8 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
)
.toList();
},
onError: (_) => add(const PermanentOrderHubsLoaded(<PermanentOrderHubOption>[])),
onError: (_) =>
add(const PermanentOrderHubsLoaded(<PermanentOrderHubOption>[])),
);
if (hubs != null) {
@@ -130,13 +149,11 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
PermanentOrderVendorsLoaded event,
Emitter<PermanentOrderState> emit,
) async {
final domain.Vendor? selectedVendor =
event.vendors.isNotEmpty ? event.vendors.first : null;
final domain.Vendor? selectedVendor = event.vendors.isNotEmpty
? event.vendors.first
: null;
emit(
state.copyWith(
vendors: event.vendors,
selectedVendor: selectedVendor,
),
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);
@@ -155,8 +172,9 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
PermanentOrderHubsLoaded event,
Emitter<PermanentOrderState> emit,
) {
final PermanentOrderHubOption? selectedHub =
event.hubs.isNotEmpty ? event.hubs.first : null;
final PermanentOrderHubOption? selectedHub = event.hubs.isNotEmpty
? event.hubs.first
: null;
emit(
state.copyWith(
hubs: event.hubs,
@@ -170,12 +188,7 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
PermanentOrderHubChanged event,
Emitter<PermanentOrderState> emit,
) {
emit(
state.copyWith(
selectedHub: event.hub,
location: event.hub.name,
),
);
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
}
void _onEventNameChanged(
@@ -225,7 +238,12 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
} else {
days.add(label);
}
emit(state.copyWith(permanentDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
emit(
state.copyWith(
permanentDays: _sortDays(days),
autoSelectedDayIndex: autoIndex,
),
);
}
void _onPositionAdded(
@@ -324,6 +342,80 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
);
}
Future<void> _onInitialized(
PermanentOrderInitialized event,
Emitter<PermanentOrderState> emit,
) async {
final Map<String, dynamic> data = event.data;
final String title = data['title']?.toString() ?? '';
final DateTime? startDate = data['startDate'] as DateTime?;
final String? orderId = data['orderId']?.toString();
emit(
state.copyWith(eventName: title, startDate: startDate ?? DateTime.now()),
);
if (orderId == null || orderId.isEmpty) return;
emit(state.copyWith(status: PermanentOrderStatus.loading));
await handleError(
emit: emit.call,
action: () async {
final domain.ReorderData orderDetails =
await _getOrderDetailsForReorderUseCase(orderId);
// Map positions
final List<PermanentOrderPosition> positions = orderDetails.positions
.map((domain.ReorderPosition role) {
return PermanentOrderPosition(
role: role.roleId,
count: role.count,
startTime: role.startTime,
endTime: role.endTime,
lunchBreak: role.lunchBreak,
);
})
.toList();
// Update state with order details
final domain.Vendor? selectedVendor = state.vendors
.where((domain.Vendor v) => v.id == orderDetails.vendorId)
.firstOrNull;
final PermanentOrderHubOption? selectedHub = state.hubs
.where(
(PermanentOrderHubOption h) =>
h.placeId == orderDetails.hub.placeId,
)
.firstOrNull;
emit(
state.copyWith(
eventName: orderDetails.eventName.isNotEmpty
? orderDetails.eventName
: title,
positions: positions,
selectedVendor: selectedVendor,
selectedHub: selectedHub,
location: selectedHub?.name ?? '',
status: PermanentOrderStatus.initial,
startDate: startDate ?? orderDetails.startDate ?? DateTime.now(),
permanentDays: orderDetails.permanentDays,
),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);
}
},
onError: (String errorKey) => state.copyWith(
status: PermanentOrderStatus.failure,
errorMessage: errorKey,
),
);
}
static List<String> _sortDays(List<String> days) {
days.sort(
(String a, String b) =>

View File

@@ -98,3 +98,11 @@ class PermanentOrderPositionUpdated extends PermanentOrderEvent {
class PermanentOrderSubmitted extends PermanentOrderEvent {
const PermanentOrderSubmitted();
}
class PermanentOrderInitialized extends PermanentOrderEvent {
const PermanentOrderInitialized(this.data);
final Map<String, dynamic> data;
@override
List<Object?> get props => <Object?>[data];
}

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:krow_core/core.dart';
import '../../domain/arguments/rapid_order_arguments.dart';
import '../../domain/usecases/create_rapid_order_usecase.dart';
import 'rapid_order_event.dart';
import 'rapid_order_state.dart';

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:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/usecases/create_recurring_order_usecase.dart';
import 'recurring_order_event.dart';
import 'recurring_order_state.dart';
/// BLoC for managing the recurring order creation form.
class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
with BlocErrorHandler<RecurringOrderState>, SafeBloc<RecurringOrderEvent, RecurringOrderState> {
RecurringOrderBloc(this._createRecurringOrderUseCase, this._service)
: super(RecurringOrderState.initial()) {
with
BlocErrorHandler<RecurringOrderState>,
SafeBloc<RecurringOrderEvent, RecurringOrderState> {
RecurringOrderBloc(
this._createRecurringOrderUseCase,
this._getOrderDetailsForReorderUseCase,
this._service,
) : super(RecurringOrderState.initial()) {
on<RecurringOrderVendorsLoaded>(_onVendorsLoaded);
on<RecurringOrderVendorChanged>(_onVendorChanged);
on<RecurringOrderHubsLoaded>(_onHubsLoaded);
@@ -24,12 +31,14 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
on<RecurringOrderPositionRemoved>(_onPositionRemoved);
on<RecurringOrderPositionUpdated>(_onPositionUpdated);
on<RecurringOrderSubmitted>(_onSubmitted);
on<RecurringOrderInitialized>(_onInitialized);
_loadVendors();
_loadHubs();
}
final CreateRecurringOrderUseCase _createRecurringOrderUseCase;
final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase;
final dc.DataConnectService _service;
static const List<String> _dayLabels = <String>[
@@ -45,8 +54,10 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
Future<void> _loadVendors() async {
final List<domain.Vendor>? vendors = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListVendorsData, void> result =
await _service.connector.listVendors().execute();
final fdc.QueryResult<dc.ListVendorsData, void> result = await _service
.connector
.listVendors()
.execute();
return result.data.vendors
.map(
(dc.ListVendorsVendors vendor) => domain.Vendor(
@@ -71,10 +82,13 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
) async {
final List<RecurringOrderRoleOption>? roles = await handleErrorWithResult(
action: () async {
final QueryResult<dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables>
result = await _service.connector
.listRolesByVendorId(vendorId: vendorId)
.execute();
final fdc.QueryResult<
dc.ListRolesByVendorIdData,
dc.ListRolesByVendorIdVariables
>
result = await _service.connector
.listRolesByVendorId(vendorId: vendorId)
.execute();
return result.data.roles
.map(
(dc.ListRolesByVendorIdRoles role) => RecurringOrderRoleOption(
@@ -85,7 +99,8 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
)
.toList();
},
onError: (_) => emit(state.copyWith(roles: const <RecurringOrderRoleOption>[])),
onError: (_) =>
emit(state.copyWith(roles: const <RecurringOrderRoleOption>[])),
);
if (roles != null) {
@@ -97,10 +112,13 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
final List<RecurringOrderHubOption>? hubs = await handleErrorWithResult(
action: () async {
final String businessId = await _service.getBusinessId();
final QueryResult<dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables>
result = await _service.connector
.listTeamHubsByOwnerId(ownerId: businessId)
.execute();
final fdc.QueryResult<
dc.ListTeamHubsByOwnerIdData,
dc.ListTeamHubsByOwnerIdVariables
>
result = await _service.connector
.listTeamHubsByOwnerId(ownerId: businessId)
.execute();
return result.data.teamHubs
.map(
(dc.ListTeamHubsByOwnerIdTeamHubs hub) => RecurringOrderHubOption(
@@ -119,7 +137,8 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
)
.toList();
},
onError: (_) => add(const RecurringOrderHubsLoaded(<RecurringOrderHubOption>[])),
onError: (_) =>
add(const RecurringOrderHubsLoaded(<RecurringOrderHubOption>[])),
);
if (hubs != null) {
@@ -131,13 +150,11 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
RecurringOrderVendorsLoaded event,
Emitter<RecurringOrderState> emit,
) async {
final domain.Vendor? selectedVendor =
event.vendors.isNotEmpty ? event.vendors.first : null;
final domain.Vendor? selectedVendor = event.vendors.isNotEmpty
? event.vendors.first
: null;
emit(
state.copyWith(
vendors: event.vendors,
selectedVendor: selectedVendor,
),
state.copyWith(vendors: event.vendors, selectedVendor: selectedVendor),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);
@@ -156,8 +173,9 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
RecurringOrderHubsLoaded event,
Emitter<RecurringOrderState> emit,
) {
final RecurringOrderHubOption? selectedHub =
event.hubs.isNotEmpty ? event.hubs.first : null;
final RecurringOrderHubOption? selectedHub = event.hubs.isNotEmpty
? event.hubs.first
: null;
emit(
state.copyWith(
hubs: event.hubs,
@@ -171,12 +189,7 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
RecurringOrderHubChanged event,
Emitter<RecurringOrderState> emit,
) {
emit(
state.copyWith(
selectedHub: event.hub,
location: event.hub.name,
),
);
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
}
void _onEventNameChanged(
@@ -242,7 +255,12 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
} else {
days.add(label);
}
emit(state.copyWith(recurringDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
emit(
state.copyWith(
recurringDays: _sortDays(days),
autoSelectedDayIndex: autoIndex,
),
);
}
void _onPositionAdded(
@@ -343,6 +361,81 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
);
}
Future<void> _onInitialized(
RecurringOrderInitialized event,
Emitter<RecurringOrderState> emit,
) async {
final Map<String, dynamic> data = event.data;
final String title = data['title']?.toString() ?? '';
final DateTime? startDate = data['startDate'] as DateTime?;
final String? orderId = data['orderId']?.toString();
emit(
state.copyWith(eventName: title, startDate: startDate ?? DateTime.now()),
);
if (orderId == null || orderId.isEmpty) return;
emit(state.copyWith(status: RecurringOrderStatus.loading));
await handleError(
emit: emit.call,
action: () async {
final domain.ReorderData orderDetails =
await _getOrderDetailsForReorderUseCase(orderId);
// Map positions
final List<RecurringOrderPosition> positions = orderDetails.positions
.map((domain.ReorderPosition role) {
return RecurringOrderPosition(
role: role.roleId,
count: role.count,
startTime: role.startTime,
endTime: role.endTime,
lunchBreak: role.lunchBreak,
);
})
.toList();
// Update state with order details
final domain.Vendor? selectedVendor = state.vendors
.where((domain.Vendor v) => v.id == orderDetails.vendorId)
.firstOrNull;
final RecurringOrderHubOption? selectedHub = state.hubs
.where(
(RecurringOrderHubOption h) =>
h.placeId == orderDetails.hub.placeId,
)
.firstOrNull;
emit(
state.copyWith(
eventName: orderDetails.eventName.isNotEmpty
? orderDetails.eventName
: title,
positions: positions,
selectedVendor: selectedVendor,
selectedHub: selectedHub,
location: selectedHub?.name ?? '',
status: RecurringOrderStatus.initial,
startDate: startDate ?? orderDetails.startDate ?? DateTime.now(),
endDate: orderDetails.endDate ?? DateTime.now(),
recurringDays: orderDetails.recurringDays,
),
);
if (selectedVendor != null) {
await _loadRolesForVendor(selectedVendor.id, emit);
}
},
onError: (String errorKey) => state.copyWith(
status: RecurringOrderStatus.failure,
errorMessage: errorKey,
),
);
}
static List<String> _sortDays(List<String> days) {
days.sort(
(String a, String b) =>

View File

@@ -107,3 +107,11 @@ class RecurringOrderPositionUpdated extends RecurringOrderEvent {
class RecurringOrderSubmitted extends RecurringOrderEvent {
const RecurringOrderSubmitted();
}
class RecurringOrderInitialized extends RecurringOrderEvent {
const RecurringOrderInitialized(this.data);
final Map<String, dynamic> data;
@override
List<Object?> get props => <Object?>[data];
}

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_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/rapid_order_bloc.dart';
import '../blocs/rapid_order/rapid_order_bloc.dart';
import '../widgets/rapid_order/rapid_order_view.dart';
/// Rapid Order Flow Page - Emergency staffing requests.

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}) {
switch (id) {
case 'rapid':
return const OrderTypeUiMetadata(
return OrderTypeUiMetadata(
icon: UiIcons.zap,
backgroundColor: UiColors.tagPending,
borderColor: UiColors.separatorSpecial,
iconBackgroundColor: UiColors.textWarning,
iconColor: UiColors.white,
textColor: UiColors.textWarning,
descriptionColor: UiColors.textWarning,
backgroundColor: UiColors.iconError.withAlpha(24),
borderColor: UiColors.iconError,
iconBackgroundColor: UiColors.iconError.withAlpha(24),
iconColor: UiColors.iconError,
textColor: UiColors.iconError,
descriptionColor: UiColors.iconError,
);
case 'one-time':
return const OrderTypeUiMetadata(
return OrderTypeUiMetadata(
icon: UiIcons.calendar,
backgroundColor: UiColors.tagInProgress,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
backgroundColor: UiColors.primary.withAlpha(24),
borderColor: UiColors.primary,
iconBackgroundColor: UiColors.primary.withAlpha(24),
iconColor: UiColors.primary,
textColor: UiColors.primary,
descriptionColor: UiColors.primary,
);
case 'recurring':
return const OrderTypeUiMetadata(
icon: UiIcons.rotateCcw,
backgroundColor: UiColors.tagSuccess,
borderColor: UiColors.switchActive,
iconBackgroundColor: UiColors.textSuccess,
iconColor: UiColors.white,
case 'permanent':
return OrderTypeUiMetadata(
icon: UiIcons.users,
backgroundColor: UiColors.textSuccess.withAlpha(24),
borderColor: UiColors.textSuccess,
iconBackgroundColor: UiColors.textSuccess.withAlpha(24),
iconColor: UiColors.textSuccess,
textColor: UiColors.textSuccess,
descriptionColor: UiColors.textSuccess,
);
case 'permanent':
return const OrderTypeUiMetadata(
icon: UiIcons.briefcase,
backgroundColor: UiColors.tagRefunded,
borderColor: UiColors.primaryInverse,
iconBackgroundColor: UiColors.primary,
iconColor: UiColors.white,
textColor: UiColors.textLink,
descriptionColor: UiColors.textLink,
case 'recurring':
return OrderTypeUiMetadata(
icon: UiIcons.rotateCcw,
backgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
borderColor: const Color.fromARGB(255, 170, 10, 223),
iconBackgroundColor: const Color.fromARGB(255, 170, 10, 223).withAlpha(24),
iconColor: const Color.fromARGB(255, 170, 10, 223),
textColor: const Color.fromARGB(255, 170, 10, 223),
descriptionColor: const Color.fromARGB(255, 170, 10, 223),
);
default:
return const OrderTypeUiMetadata(

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(
color: backgroundColor,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: borderColor, width: 2),
border: Border.all(color: borderColor, width: 0.75),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -73,8 +73,7 @@ class OrderTypeCard extends StatelessWidget {
),
child: Icon(icon, color: iconColor, size: 24),
),
Text(title, style: UiTypography.body2b.copyWith(color: textColor)),
const SizedBox(height: UiConstants.space1),
Text(title, style: UiTypography.body1b.copyWith(color: textColor)),
Expanded(
child: Text(
description,

View File

@@ -5,9 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import '../../blocs/rapid_order_bloc.dart';
import '../../blocs/rapid_order_event.dart';
import '../../blocs/rapid_order_state.dart';
import '../../blocs/rapid_order/rapid_order_bloc.dart';
import '../../blocs/rapid_order/rapid_order_event.dart';
import '../../blocs/rapid_order/rapid_order_state.dart';
import 'rapid_order_example_card.dart';
import 'rapid_order_header.dart';
import 'rapid_order_success_view.dart';
@@ -295,7 +295,6 @@ class _RapidOrderActions extends StatelessWidget {
onPressed: isSubmitting || isMessageEmpty
? null
: () {
print('RapidOrder send pressed');
BlocProvider.of<RapidOrderBloc>(
context,
).add(const RapidOrderSubmitted());

View File

@@ -15,15 +15,17 @@ dependencies:
equatable: ^2.0.5
intl: 0.20.2
design_system:
path: ../../../design_system
path: ../../../../design_system
core_localization:
path: ../../../core_localization
path: ../../../../core_localization
krow_domain:
path: ../../../domain
path: ../../../../domain
krow_core:
path: ../../../core
path: ../../../../core
krow_data_connect:
path: ../../../data_connect
path: ../../../../data_connect
client_orders_common:
path: ../orders_common
firebase_data_connect: ^0.2.2+2
firebase_auth: ^6.1.4

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