feat: add entities for staff personal info, reports, shifts, and user sessions
- Implemented StaffPersonalInfo entity for staff profile data. - Created ReportSummary entity for summarizing report metrics. - Added SpendReport and SpendDataPoint entities for spend reporting. - Introduced AssignedShift, CancelledShift, CompletedShift, OpenShift, PendingAssignment, ShiftDetail, TodayShift entities for shift management. - Developed ClientSession and StaffSession entities for user session management.
This commit is contained in:
@@ -67,6 +67,84 @@ If any of these files are missing or unreadable, notify the user before proceedi
|
||||
}
|
||||
```
|
||||
|
||||
## V2 API Migration Rules (Active Migration)
|
||||
|
||||
The mobile apps are migrating from Firebase Data Connect (direct DB) to V2 REST API. Follow these rules for ALL new and migrated features:
|
||||
|
||||
### Backend Access
|
||||
- **Use `ApiService.get/post/put/delete`** for ALL backend calls — NEVER use Data Connect connectors
|
||||
- Import `ApiService` from `package:krow_core/core.dart`
|
||||
- Use `V2ApiEndpoints` from `package:krow_core/core.dart` for endpoint URLs
|
||||
- V2 API docs are at `docs/BACKEND/API_GUIDES/V2/` — check response shapes before writing code
|
||||
|
||||
### Domain Entities
|
||||
- Domain entities live in `packages/domain/lib/src/entities/` with `fromJson`/`toJson` directly on the class
|
||||
- No separate DTO or adapter layer — entities are self-serializing
|
||||
- Entities are shared across all features via `package:krow_domain/krow_domain.dart`
|
||||
- When migrating: check if the entity already exists and update its `fromJson` to match V2 API response shape
|
||||
|
||||
### Feature Structure
|
||||
- **RepoImpl lives in the feature package** at `data/repositories/`
|
||||
- **Feature-level domain layer is optional** — only add `domain/` when the feature has use cases, validators, or feature-specific interfaces
|
||||
- **Simple features** (read-only, no business logic) = just `data/` + `presentation/`
|
||||
- Do NOT import from `packages/data_connect/` — it is deprecated
|
||||
|
||||
### Status & Type Enums
|
||||
All status/type fields from the V2 API must use Dart enums, NOT raw strings. Parse at the `fromJson` boundary with a safe fallback:
|
||||
```dart
|
||||
enum ShiftStatus {
|
||||
open, assigned, active, completed, cancelled;
|
||||
|
||||
static ShiftStatus fromJson(String value) {
|
||||
switch (value) {
|
||||
case 'OPEN': return ShiftStatus.open;
|
||||
case 'ASSIGNED': return ShiftStatus.assigned;
|
||||
case 'ACTIVE': return ShiftStatus.active;
|
||||
case 'COMPLETED': return ShiftStatus.completed;
|
||||
case 'CANCELLED': return ShiftStatus.cancelled;
|
||||
default: return ShiftStatus.open;
|
||||
}
|
||||
}
|
||||
|
||||
String toJson() {
|
||||
switch (this) {
|
||||
case ShiftStatus.open: return 'OPEN';
|
||||
case ShiftStatus.assigned: return 'ASSIGNED';
|
||||
case ShiftStatus.active: return 'ACTIVE';
|
||||
case ShiftStatus.completed: return 'COMPLETED';
|
||||
case ShiftStatus.cancelled: return 'CANCELLED';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Place shared enums (used by multiple entities) in `packages/domain/lib/src/entities/enums/`. Feature-specific enums can live in the entity file.
|
||||
|
||||
### RepoImpl Pattern
|
||||
```dart
|
||||
class FeatureRepositoryImpl implements FeatureRepositoryInterface {
|
||||
FeatureRepositoryImpl({required ApiService apiService})
|
||||
: _apiService = apiService;
|
||||
|
||||
final ApiService _apiService;
|
||||
|
||||
Future<List<Shift>> getShifts() async {
|
||||
final ApiResponse response = await _apiService.get(V2ApiEndpoints.staffShiftsAssigned);
|
||||
final List<dynamic> items = response.data['shifts'] as List<dynamic>;
|
||||
return items.map((dynamic json) => Shift.fromJson(json as Map<String, dynamic>)).toList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DI Registration
|
||||
```dart
|
||||
// Inject ApiService (available from CoreModule)
|
||||
i.add<FeatureRepositoryImpl>(() => FeatureRepositoryImpl(
|
||||
apiService: i.get<ApiService>(),
|
||||
));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Standard Workflow
|
||||
|
||||
Follow these steps in order for every feature implementation:
|
||||
@@ -91,8 +169,8 @@ Follow these steps in order for every feature implementation:
|
||||
- Create barrel file exporting the domain public API
|
||||
|
||||
### 4. Data Layer
|
||||
- Create models with `fromJson`/`toJson` methods
|
||||
- Implement repository classes using `DataConnectService`
|
||||
- Implement repository classes using `ApiService` with `V2ApiEndpoints` — NOT DataConnectService
|
||||
- Parse V2 API JSON responses into domain entities via `Entity.fromJson()`
|
||||
- Map errors to domain `Failure` types
|
||||
- Create barrel file for data layer
|
||||
|
||||
|
||||
@@ -16,8 +16,13 @@ export 'src/routing/routing.dart';
|
||||
export 'src/services/api_service/api_service.dart';
|
||||
export 'src/services/api_service/dio_client.dart';
|
||||
|
||||
// API Mixins
|
||||
export 'src/services/api_service/mixins/api_error_handler.dart';
|
||||
export 'src/services/api_service/mixins/session_handler_mixin.dart';
|
||||
|
||||
// Core API Services
|
||||
export 'src/services/api_service/core_api_services/core_api_endpoints.dart';
|
||||
export 'src/services/api_service/core_api_services/v2_api_endpoints.dart';
|
||||
export 'src/services/api_service/core_api_services/file_upload/file_upload_service.dart';
|
||||
export 'src/services/api_service/core_api_services/file_upload/file_upload_response.dart';
|
||||
export 'src/services/api_service/core_api_services/signed_url/signed_url_service.dart';
|
||||
|
||||
@@ -13,4 +13,10 @@ class AppConfig {
|
||||
static const String coreApiBaseUrl = String.fromEnvironment(
|
||||
'CORE_API_BASE_URL',
|
||||
);
|
||||
|
||||
/// The base URL for the V2 Unified API gateway.
|
||||
static const String v2ApiBaseUrl = String.fromEnvironment(
|
||||
'V2_API_BASE_URL',
|
||||
defaultValue: 'https://krow-api-v2-933560802882.us-central1.run.app',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,6 +86,25 @@ class ApiService implements BaseApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a DELETE request to the specified [endpoint].
|
||||
@override
|
||||
Future<ApiResponse> delete(
|
||||
String endpoint, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? params,
|
||||
}) async {
|
||||
try {
|
||||
final Response<dynamic> response = await _dio.delete<dynamic>(
|
||||
endpoint,
|
||||
data: data,
|
||||
queryParameters: params,
|
||||
);
|
||||
return _handleResponse(response);
|
||||
} on DioException catch (e) {
|
||||
return _handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts [ApiResponse] from a successful [Response].
|
||||
ApiResponse _handleResponse(Response<dynamic> response) {
|
||||
return ApiResponse(
|
||||
@@ -96,6 +115,9 @@ class ApiService implements BaseApiService {
|
||||
}
|
||||
|
||||
/// Extracts [ApiResponse] from a [DioException].
|
||||
///
|
||||
/// Supports both legacy error format and V2 API error envelope
|
||||
/// (`{ code, message, details, requestId }`).
|
||||
ApiResponse _handleError(DioException e) {
|
||||
if (e.response?.data is Map<String, dynamic>) {
|
||||
final Map<String, dynamic> body =
|
||||
@@ -106,7 +128,7 @@ class ApiService implements BaseApiService {
|
||||
e.response?.statusCode?.toString() ??
|
||||
'error',
|
||||
message: body['message']?.toString() ?? e.message ?? 'Error occurred',
|
||||
data: body['data'],
|
||||
data: body['data'] ?? body['details'],
|
||||
errors: _parseErrors(body['errors']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
/// Constants for V2 Unified API endpoints.
|
||||
///
|
||||
/// All mobile read/write operations go through the V2 gateway which proxies
|
||||
/// to the internal Query API and Command API services.
|
||||
class V2ApiEndpoints {
|
||||
V2ApiEndpoints._();
|
||||
|
||||
/// The base URL for the V2 Unified API gateway.
|
||||
static const String baseUrl = AppConfig.v2ApiBaseUrl;
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Client email/password sign-in.
|
||||
static const String clientSignIn = '$baseUrl/auth/client/sign-in';
|
||||
|
||||
/// Client business registration.
|
||||
static const String clientSignUp = '$baseUrl/auth/client/sign-up';
|
||||
|
||||
/// Client sign-out.
|
||||
static const String clientSignOut = '$baseUrl/auth/client/sign-out';
|
||||
|
||||
/// Start staff phone verification (SMS).
|
||||
static const String staffPhoneStart = '$baseUrl/auth/staff/phone/start';
|
||||
|
||||
/// Complete staff phone verification.
|
||||
static const String staffPhoneVerify = '$baseUrl/auth/staff/phone/verify';
|
||||
|
||||
/// Generic sign-out.
|
||||
static const String signOut = '$baseUrl/auth/sign-out';
|
||||
|
||||
/// Get current session data.
|
||||
static const String session = '$baseUrl/auth/session';
|
||||
|
||||
// ── Staff Read ────────────────────────────────────────────────────────
|
||||
|
||||
/// Staff session data.
|
||||
static const String staffSession = '$baseUrl/staff/session';
|
||||
|
||||
/// Staff dashboard overview.
|
||||
static const String staffDashboard = '$baseUrl/staff/dashboard';
|
||||
|
||||
/// Staff profile completion status.
|
||||
static const String staffProfileCompletion =
|
||||
'$baseUrl/staff/profile-completion';
|
||||
|
||||
/// Staff availability schedule.
|
||||
static const String staffAvailability = '$baseUrl/staff/availability';
|
||||
|
||||
/// Today's shifts for clock-in.
|
||||
static const String staffClockInShiftsToday =
|
||||
'$baseUrl/staff/clock-in/shifts/today';
|
||||
|
||||
/// Current clock-in status.
|
||||
static const String staffClockInStatus = '$baseUrl/staff/clock-in/status';
|
||||
|
||||
/// Payments summary.
|
||||
static const String staffPaymentsSummary = '$baseUrl/staff/payments/summary';
|
||||
|
||||
/// Payments history.
|
||||
static const String staffPaymentsHistory = '$baseUrl/staff/payments/history';
|
||||
|
||||
/// Payments chart data.
|
||||
static const String staffPaymentsChart = '$baseUrl/staff/payments/chart';
|
||||
|
||||
/// Assigned shifts.
|
||||
static const String staffShiftsAssigned = '$baseUrl/staff/shifts/assigned';
|
||||
|
||||
/// Open shifts available to apply.
|
||||
static const String staffShiftsOpen = '$baseUrl/staff/shifts/open';
|
||||
|
||||
/// Pending shift assignments.
|
||||
static const String staffShiftsPending = '$baseUrl/staff/shifts/pending';
|
||||
|
||||
/// Cancelled shifts.
|
||||
static const String staffShiftsCancelled = '$baseUrl/staff/shifts/cancelled';
|
||||
|
||||
/// Completed shifts.
|
||||
static const String staffShiftsCompleted = '$baseUrl/staff/shifts/completed';
|
||||
|
||||
/// Shift details by ID.
|
||||
static String staffShiftDetails(String shiftId) =>
|
||||
'$baseUrl/staff/shifts/$shiftId';
|
||||
|
||||
/// Staff profile sections overview.
|
||||
static const String staffProfileSections = '$baseUrl/staff/profile/sections';
|
||||
|
||||
/// Personal info.
|
||||
static const String staffPersonalInfo = '$baseUrl/staff/profile/personal-info';
|
||||
|
||||
/// Industries/experience.
|
||||
static const String staffIndustries = '$baseUrl/staff/profile/industries';
|
||||
|
||||
/// Skills.
|
||||
static const String staffSkills = '$baseUrl/staff/profile/skills';
|
||||
|
||||
/// Documents.
|
||||
static const String staffDocuments = '$baseUrl/staff/profile/documents';
|
||||
|
||||
/// Attire items.
|
||||
static const String staffAttire = '$baseUrl/staff/profile/attire';
|
||||
|
||||
/// Tax forms.
|
||||
static const String staffTaxForms = '$baseUrl/staff/profile/tax-forms';
|
||||
|
||||
/// Emergency contacts.
|
||||
static const String staffEmergencyContacts =
|
||||
'$baseUrl/staff/profile/emergency-contacts';
|
||||
|
||||
/// Certificates.
|
||||
static const String staffCertificates = '$baseUrl/staff/profile/certificates';
|
||||
|
||||
/// Bank accounts.
|
||||
static const String staffBankAccounts = '$baseUrl/staff/profile/bank-accounts';
|
||||
|
||||
/// Benefits.
|
||||
static const String staffBenefits = '$baseUrl/staff/profile/benefits';
|
||||
|
||||
/// Time card.
|
||||
static const String staffTimeCard = '$baseUrl/staff/profile/time-card';
|
||||
|
||||
/// Privacy settings.
|
||||
static const String staffPrivacy = '$baseUrl/staff/profile/privacy';
|
||||
|
||||
/// FAQs.
|
||||
static const String staffFaqs = '$baseUrl/staff/faqs';
|
||||
|
||||
/// FAQs search.
|
||||
static const String staffFaqsSearch = '$baseUrl/staff/faqs/search';
|
||||
|
||||
// ── Staff Write ───────────────────────────────────────────────────────
|
||||
|
||||
/// Staff profile setup.
|
||||
static const String staffProfileSetup = '$baseUrl/staff/profile/setup';
|
||||
|
||||
/// Clock in.
|
||||
static const String staffClockIn = '$baseUrl/staff/clock-in';
|
||||
|
||||
/// Clock out.
|
||||
static const String staffClockOut = '$baseUrl/staff/clock-out';
|
||||
|
||||
/// Quick-set availability.
|
||||
static const String staffAvailabilityQuickSet =
|
||||
'$baseUrl/staff/availability/quick-set';
|
||||
|
||||
/// Apply for a shift.
|
||||
static String staffShiftApply(String shiftId) =>
|
||||
'$baseUrl/staff/shifts/$shiftId/apply';
|
||||
|
||||
/// Accept a shift.
|
||||
static String staffShiftAccept(String shiftId) =>
|
||||
'$baseUrl/staff/shifts/$shiftId/accept';
|
||||
|
||||
/// Decline a shift.
|
||||
static String staffShiftDecline(String shiftId) =>
|
||||
'$baseUrl/staff/shifts/$shiftId/decline';
|
||||
|
||||
/// Request a shift swap.
|
||||
static String staffShiftRequestSwap(String shiftId) =>
|
||||
'$baseUrl/staff/shifts/$shiftId/request-swap';
|
||||
|
||||
/// Update emergency contact by ID.
|
||||
static String staffEmergencyContactUpdate(String contactId) =>
|
||||
'$baseUrl/staff/profile/emergency-contacts/$contactId';
|
||||
|
||||
/// Update tax form by type.
|
||||
static String staffTaxFormUpdate(String formType) =>
|
||||
'$baseUrl/staff/profile/tax-forms/$formType';
|
||||
|
||||
/// Submit tax form by type.
|
||||
static String staffTaxFormSubmit(String formType) =>
|
||||
'$baseUrl/staff/profile/tax-forms/$formType/submit';
|
||||
|
||||
/// Upload staff profile photo.
|
||||
static const String staffProfilePhoto = '$baseUrl/staff/profile/photo';
|
||||
|
||||
/// Upload document by ID.
|
||||
static String staffDocumentUpload(String documentId) =>
|
||||
'$baseUrl/staff/profile/documents/$documentId/upload';
|
||||
|
||||
/// Upload attire by ID.
|
||||
static String staffAttireUpload(String documentId) =>
|
||||
'$baseUrl/staff/profile/attire/$documentId/upload';
|
||||
|
||||
/// Delete certificate by ID.
|
||||
static String staffCertificateDelete(String certificateId) =>
|
||||
'$baseUrl/staff/profile/certificates/$certificateId';
|
||||
|
||||
// ── Client Read ───────────────────────────────────────────────────────
|
||||
|
||||
/// Client session data.
|
||||
static const String clientSession = '$baseUrl/client/session';
|
||||
|
||||
/// Client dashboard.
|
||||
static const String clientDashboard = '$baseUrl/client/dashboard';
|
||||
|
||||
/// Client reorders.
|
||||
static const String clientReorders = '$baseUrl/client/reorders';
|
||||
|
||||
/// Billing accounts.
|
||||
static const String clientBillingAccounts = '$baseUrl/client/billing/accounts';
|
||||
|
||||
/// Pending invoices.
|
||||
static const String clientBillingInvoicesPending =
|
||||
'$baseUrl/client/billing/invoices/pending';
|
||||
|
||||
/// Invoice history.
|
||||
static const String clientBillingInvoicesHistory =
|
||||
'$baseUrl/client/billing/invoices/history';
|
||||
|
||||
/// Current bill.
|
||||
static const String clientBillingCurrentBill =
|
||||
'$baseUrl/client/billing/current-bill';
|
||||
|
||||
/// Savings data.
|
||||
static const String clientBillingSavings = '$baseUrl/client/billing/savings';
|
||||
|
||||
/// Spend breakdown.
|
||||
static const String clientBillingSpendBreakdown =
|
||||
'$baseUrl/client/billing/spend-breakdown';
|
||||
|
||||
/// Coverage overview.
|
||||
static const String clientCoverage = '$baseUrl/client/coverage';
|
||||
|
||||
/// Coverage stats.
|
||||
static const String clientCoverageStats = '$baseUrl/client/coverage/stats';
|
||||
|
||||
/// Core team.
|
||||
static const String clientCoverageCoreTeam =
|
||||
'$baseUrl/client/coverage/core-team';
|
||||
|
||||
/// Hubs list.
|
||||
static const String clientHubs = '$baseUrl/client/hubs';
|
||||
|
||||
/// Cost centers.
|
||||
static const String clientCostCenters = '$baseUrl/client/cost-centers';
|
||||
|
||||
/// Vendors.
|
||||
static const String clientVendors = '$baseUrl/client/vendors';
|
||||
|
||||
/// Vendor roles by ID.
|
||||
static String clientVendorRoles(String vendorId) =>
|
||||
'$baseUrl/client/vendors/$vendorId/roles';
|
||||
|
||||
/// Hub managers by ID.
|
||||
static String clientHubManagers(String hubId) =>
|
||||
'$baseUrl/client/hubs/$hubId/managers';
|
||||
|
||||
/// Team members.
|
||||
static const String clientTeamMembers = '$baseUrl/client/team-members';
|
||||
|
||||
/// View orders.
|
||||
static const String clientOrdersView = '$baseUrl/client/orders/view';
|
||||
|
||||
/// Order reorder preview.
|
||||
static String clientOrderReorderPreview(String orderId) =>
|
||||
'$baseUrl/client/orders/$orderId/reorder-preview';
|
||||
|
||||
/// Reports summary.
|
||||
static const String clientReportsSummary = '$baseUrl/client/reports/summary';
|
||||
|
||||
/// Daily ops report.
|
||||
static const String clientReportsDailyOps =
|
||||
'$baseUrl/client/reports/daily-ops';
|
||||
|
||||
/// Spend report.
|
||||
static const String clientReportsSpend = '$baseUrl/client/reports/spend';
|
||||
|
||||
/// Coverage report.
|
||||
static const String clientReportsCoverage =
|
||||
'$baseUrl/client/reports/coverage';
|
||||
|
||||
/// Forecast report.
|
||||
static const String clientReportsForecast =
|
||||
'$baseUrl/client/reports/forecast';
|
||||
|
||||
/// Performance report.
|
||||
static const String clientReportsPerformance =
|
||||
'$baseUrl/client/reports/performance';
|
||||
|
||||
/// No-show report.
|
||||
static const String clientReportsNoShow = '$baseUrl/client/reports/no-show';
|
||||
|
||||
// ── Client Write ──────────────────────────────────────────────────────
|
||||
|
||||
/// Create one-time order.
|
||||
static const String clientOrdersOneTime = '$baseUrl/client/orders/one-time';
|
||||
|
||||
/// Create recurring order.
|
||||
static const String clientOrdersRecurring =
|
||||
'$baseUrl/client/orders/recurring';
|
||||
|
||||
/// Create permanent order.
|
||||
static const String clientOrdersPermanent =
|
||||
'$baseUrl/client/orders/permanent';
|
||||
|
||||
/// Edit order by ID.
|
||||
static String clientOrderEdit(String orderId) =>
|
||||
'$baseUrl/client/orders/$orderId/edit';
|
||||
|
||||
/// Cancel order by ID.
|
||||
static String clientOrderCancel(String orderId) =>
|
||||
'$baseUrl/client/orders/$orderId/cancel';
|
||||
|
||||
/// Create hub.
|
||||
static const String clientHubCreate = '$baseUrl/client/hubs';
|
||||
|
||||
/// Update hub by ID.
|
||||
static String clientHubUpdate(String hubId) =>
|
||||
'$baseUrl/client/hubs/$hubId';
|
||||
|
||||
/// Delete hub by ID.
|
||||
static String clientHubDelete(String hubId) =>
|
||||
'$baseUrl/client/hubs/$hubId';
|
||||
|
||||
/// Assign NFC to hub.
|
||||
static String clientHubAssignNfc(String hubId) =>
|
||||
'$baseUrl/client/hubs/$hubId/assign-nfc';
|
||||
|
||||
/// Assign managers to hub.
|
||||
static String clientHubAssignManagers(String hubId) =>
|
||||
'$baseUrl/client/hubs/$hubId/managers';
|
||||
|
||||
/// Approve invoice.
|
||||
static String clientInvoiceApprove(String invoiceId) =>
|
||||
'$baseUrl/client/billing/invoices/$invoiceId/approve';
|
||||
|
||||
/// Dispute invoice.
|
||||
static String clientInvoiceDispute(String invoiceId) =>
|
||||
'$baseUrl/client/billing/invoices/$invoiceId/dispute';
|
||||
|
||||
/// Submit coverage review.
|
||||
static const String clientCoverageReviews =
|
||||
'$baseUrl/client/coverage/reviews';
|
||||
|
||||
/// Cancel late worker assignment.
|
||||
static String clientCoverageCancelLateWorker(String assignmentId) =>
|
||||
'$baseUrl/client/coverage/late-workers/$assignmentId/cancel';
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:krow_core/src/services/api_service/inspectors/auth_interceptor.dart';
|
||||
import 'package:krow_core/src/services/api_service/inspectors/idempotency_interceptor.dart';
|
||||
|
||||
/// A custom Dio client for the KROW project that includes basic configuration
|
||||
/// and an [AuthInterceptor].
|
||||
/// A custom Dio client for the KROW project that includes basic configuration,
|
||||
/// [AuthInterceptor], and [IdempotencyInterceptor].
|
||||
class DioClient extends DioMixin implements Dio {
|
||||
DioClient([BaseOptions? baseOptions]) {
|
||||
options =
|
||||
@@ -18,10 +19,11 @@ class DioClient extends DioMixin implements Dio {
|
||||
// Add interceptors
|
||||
interceptors.addAll(<Interceptor>[
|
||||
AuthInterceptor(),
|
||||
IdempotencyInterceptor(),
|
||||
LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
), // Added for better debugging
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// A Dio interceptor that adds an `Idempotency-Key` header to write requests.
|
||||
///
|
||||
/// The V2 API requires an idempotency key for all POST, PUT, and DELETE
|
||||
/// requests to prevent duplicate operations. A unique UUID v4 is generated
|
||||
/// per request automatically.
|
||||
class IdempotencyInterceptor extends Interceptor {
|
||||
/// The UUID generator instance.
|
||||
static const Uuid _uuid = Uuid();
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) {
|
||||
final String method = options.method.toUpperCase();
|
||||
if (method == 'POST' || method == 'PUT' || method == 'DELETE') {
|
||||
options.headers['Idempotency-Key'] = _uuid.v4();
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Mixin to handle API layer errors and map them to domain exceptions.
|
||||
///
|
||||
/// Use this in repository implementations to wrap [ApiService] calls.
|
||||
/// It catches [DioException], [SocketException], etc., and throws
|
||||
/// the appropriate [AppException] subclass.
|
||||
mixin ApiErrorHandler {
|
||||
/// Executes a Future and maps low-level exceptions to [AppException].
|
||||
///
|
||||
/// [timeout] defaults to 30 seconds.
|
||||
Future<T> executeProtected<T>(
|
||||
Future<T> Function() action, {
|
||||
Duration timeout = const Duration(seconds: 30),
|
||||
}) async {
|
||||
try {
|
||||
return await action().timeout(timeout);
|
||||
} on TimeoutException {
|
||||
debugPrint(
|
||||
'ApiErrorHandler: Request timed out after ${timeout.inSeconds}s',
|
||||
);
|
||||
throw ServiceUnavailableException(
|
||||
technicalMessage: 'Request timed out after ${timeout.inSeconds}s',
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
throw _mapDioException(e);
|
||||
} on SocketException catch (e) {
|
||||
throw NetworkException(
|
||||
technicalMessage: 'SocketException: ${e.message}',
|
||||
);
|
||||
} catch (e) {
|
||||
// If it's already an AppException, rethrow it.
|
||||
if (e is AppException) rethrow;
|
||||
|
||||
final String errorStr = e.toString().toLowerCase();
|
||||
if (_isNetworkRelated(errorStr)) {
|
||||
debugPrint('ApiErrorHandler: Network-related error: $e');
|
||||
throw NetworkException(technicalMessage: e.toString());
|
||||
}
|
||||
|
||||
debugPrint('ApiErrorHandler: Unhandled exception caught: $e');
|
||||
throw UnknownException(technicalMessage: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a [DioException] to the appropriate [AppException].
|
||||
AppException _mapDioException(DioException e) {
|
||||
switch (e.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
debugPrint('ApiErrorHandler: Dio timeout: ${e.type}');
|
||||
return ServiceUnavailableException(
|
||||
technicalMessage: 'Dio ${e.type}: ${e.message}',
|
||||
);
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
debugPrint('ApiErrorHandler: Connection error: ${e.message}');
|
||||
return NetworkException(
|
||||
technicalMessage: 'Connection error: ${e.message}',
|
||||
);
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
final int? statusCode = e.response?.statusCode;
|
||||
final String body = e.response?.data?.toString() ?? '';
|
||||
debugPrint(
|
||||
'ApiErrorHandler: Bad response $statusCode: $body',
|
||||
);
|
||||
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
return NotAuthenticatedException(
|
||||
technicalMessage: 'HTTP $statusCode: $body',
|
||||
);
|
||||
}
|
||||
if (statusCode == 404) {
|
||||
return ServerException(
|
||||
technicalMessage: 'HTTP 404: Not found — $body',
|
||||
);
|
||||
}
|
||||
if (statusCode == 429) {
|
||||
return ServiceUnavailableException(
|
||||
technicalMessage: 'Rate limited (429): $body',
|
||||
);
|
||||
}
|
||||
if (statusCode != null && statusCode >= 500) {
|
||||
return ServiceUnavailableException(
|
||||
technicalMessage: 'HTTP $statusCode: $body',
|
||||
);
|
||||
}
|
||||
return ServerException(
|
||||
technicalMessage: 'HTTP $statusCode: $body',
|
||||
);
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return UnknownException(
|
||||
technicalMessage: 'Request cancelled',
|
||||
);
|
||||
|
||||
case DioExceptionType.badCertificate:
|
||||
return NetworkException(
|
||||
technicalMessage: 'Bad certificate: ${e.message}',
|
||||
);
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
if (e.error is SocketException) {
|
||||
return NetworkException(
|
||||
technicalMessage: 'Socket error: ${e.error}',
|
||||
);
|
||||
}
|
||||
return UnknownException(
|
||||
technicalMessage: 'Unknown Dio error: ${e.message}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if an error string is network-related.
|
||||
bool _isNetworkRelated(String errorStr) {
|
||||
return errorStr.contains('socketexception') ||
|
||||
errorStr.contains('network') ||
|
||||
errorStr.contains('offline') ||
|
||||
errorStr.contains('connection failed') ||
|
||||
errorStr.contains('unavailable') ||
|
||||
errorStr.contains('handshake') ||
|
||||
errorStr.contains('clientexception') ||
|
||||
errorStr.contains('failed host lookup') ||
|
||||
errorStr.contains('connection error') ||
|
||||
errorStr.contains('terminated') ||
|
||||
errorStr.contains('connectexception');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
/// Enum representing the current session state.
|
||||
enum SessionStateType { loading, authenticated, unauthenticated, error }
|
||||
|
||||
/// Data class for session state.
|
||||
class SessionState {
|
||||
/// Creates a [SessionState].
|
||||
SessionState({required this.type, this.userId, this.errorMessage});
|
||||
|
||||
/// Creates a loading state.
|
||||
factory SessionState.loading() =>
|
||||
SessionState(type: SessionStateType.loading);
|
||||
|
||||
/// Creates an authenticated state.
|
||||
factory SessionState.authenticated({required String userId}) =>
|
||||
SessionState(type: SessionStateType.authenticated, userId: userId);
|
||||
|
||||
/// Creates an unauthenticated state.
|
||||
factory SessionState.unauthenticated() =>
|
||||
SessionState(type: SessionStateType.unauthenticated);
|
||||
|
||||
/// Creates an error state.
|
||||
factory SessionState.error(String message) =>
|
||||
SessionState(type: SessionStateType.error, errorMessage: message);
|
||||
|
||||
/// The type of session state.
|
||||
final SessionStateType type;
|
||||
|
||||
/// The current user ID (if authenticated).
|
||||
final String? userId;
|
||||
|
||||
/// Error message (if error occurred).
|
||||
final String? errorMessage;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SessionState(type: $type, userId: $userId, error: $errorMessage)';
|
||||
}
|
||||
|
||||
/// Mixin for handling Firebase Auth session management, token refresh,
|
||||
/// and state emissions.
|
||||
///
|
||||
/// Implementors must provide [auth] and [fetchUserRole]. The role fetch
|
||||
/// should call `GET /auth/session` via [ApiService] instead of querying
|
||||
/// Data Connect directly.
|
||||
mixin SessionHandlerMixin {
|
||||
/// Stream controller for session state changes.
|
||||
final StreamController<SessionState> _sessionStateController =
|
||||
StreamController<SessionState>.broadcast();
|
||||
|
||||
/// Last emitted session state (for late subscribers).
|
||||
SessionState? _lastSessionState;
|
||||
|
||||
/// Public stream for listening to session state changes.
|
||||
/// Late subscribers will immediately receive the last emitted state.
|
||||
Stream<SessionState> get onSessionStateChanged {
|
||||
return _createStreamWithLastState();
|
||||
}
|
||||
|
||||
/// Creates a stream that emits the last state before subscribing to new events.
|
||||
Stream<SessionState> _createStreamWithLastState() async* {
|
||||
if (_lastSessionState != null) {
|
||||
yield _lastSessionState!;
|
||||
}
|
||||
yield* _sessionStateController.stream;
|
||||
}
|
||||
|
||||
/// Last token refresh timestamp to avoid excessive checks.
|
||||
DateTime? _lastTokenRefreshTime;
|
||||
|
||||
/// Subscription to auth state changes.
|
||||
StreamSubscription<firebase_auth.User?>? _authStateSubscription;
|
||||
|
||||
/// Minimum interval between token refresh checks.
|
||||
static const Duration _minRefreshCheckInterval = Duration(seconds: 2);
|
||||
|
||||
/// Time before token expiry to trigger a refresh.
|
||||
static const Duration _refreshThreshold = Duration(minutes: 5);
|
||||
|
||||
/// Firebase Auth instance (to be provided by implementing class).
|
||||
firebase_auth.FirebaseAuth get auth;
|
||||
|
||||
/// List of allowed roles for this app (set during initialization).
|
||||
List<String> _allowedRoles = <String>[];
|
||||
|
||||
/// Initialize the auth state listener (call once on app startup).
|
||||
void initializeAuthListener({
|
||||
List<String> allowedRoles = const <String>[],
|
||||
}) {
|
||||
_allowedRoles = allowedRoles;
|
||||
|
||||
_authStateSubscription?.cancel();
|
||||
|
||||
_authStateSubscription = auth.authStateChanges().listen(
|
||||
(firebase_auth.User? user) async {
|
||||
if (user == null) {
|
||||
handleSignOut();
|
||||
} else {
|
||||
await _handleSignIn(user);
|
||||
}
|
||||
},
|
||||
onError: (Object error) {
|
||||
_emitSessionState(SessionState.error(error.toString()));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates if user has one of the allowed roles.
|
||||
Future<bool> validateUserRole(
|
||||
String userId,
|
||||
List<String> allowedRoles,
|
||||
) async {
|
||||
try {
|
||||
final String? userRole = await fetchUserRole(userId);
|
||||
return userRole != null && allowedRoles.contains(userRole);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to validate user role: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches user role from the backend.
|
||||
///
|
||||
/// Implementors should call `GET /auth/session` via [ApiService] and
|
||||
/// extract the role from the response.
|
||||
Future<String?> fetchUserRole(String userId);
|
||||
|
||||
/// Ensures the Firebase auth token is valid and refreshes if needed.
|
||||
/// Retries up to 3 times with exponential backoff before emitting error.
|
||||
Future<void> ensureSessionValid() async {
|
||||
final firebase_auth.User? user = auth.currentUser;
|
||||
if (user == null) return;
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
if (_lastTokenRefreshTime != null) {
|
||||
final Duration timeSinceLastCheck = now.difference(
|
||||
_lastTokenRefreshTime!,
|
||||
);
|
||||
if (timeSinceLastCheck < _minRefreshCheckInterval) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const int maxRetries = 3;
|
||||
int retryCount = 0;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
final firebase_auth.IdTokenResult idToken =
|
||||
await user.getIdTokenResult();
|
||||
final DateTime? expiryTime = idToken.expirationTime;
|
||||
|
||||
if (expiryTime == null) return;
|
||||
|
||||
final Duration timeUntilExpiry = expiryTime.difference(now);
|
||||
if (timeUntilExpiry <= _refreshThreshold) {
|
||||
await user.getIdTokenResult();
|
||||
}
|
||||
|
||||
_lastTokenRefreshTime = now;
|
||||
return;
|
||||
} catch (e) {
|
||||
retryCount++;
|
||||
debugPrint(
|
||||
'Token validation error (attempt $retryCount/$maxRetries): $e',
|
||||
);
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
_emitSessionState(
|
||||
SessionState.error(
|
||||
'Token validation failed after $maxRetries attempts: $e',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final Duration backoffDuration = Duration(
|
||||
seconds: 1 << (retryCount - 1),
|
||||
);
|
||||
debugPrint(
|
||||
'Retrying token validation in ${backoffDuration.inSeconds}s',
|
||||
);
|
||||
await Future<void>.delayed(backoffDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle user sign-in event.
|
||||
Future<void> _handleSignIn(firebase_auth.User user) async {
|
||||
try {
|
||||
_emitSessionState(SessionState.loading());
|
||||
|
||||
if (_allowedRoles.isNotEmpty) {
|
||||
final String? userRole = await fetchUserRole(user.uid);
|
||||
|
||||
if (userRole == null) {
|
||||
_emitSessionState(SessionState.unauthenticated());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_allowedRoles.contains(userRole)) {
|
||||
await auth.signOut();
|
||||
_emitSessionState(SessionState.unauthenticated());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final firebase_auth.IdTokenResult idToken =
|
||||
await user.getIdTokenResult();
|
||||
if (idToken.expirationTime != null &&
|
||||
DateTime.now().difference(idToken.expirationTime!) <
|
||||
const Duration(minutes: 5)) {
|
||||
await user.getIdTokenResult();
|
||||
}
|
||||
|
||||
_emitSessionState(SessionState.authenticated(userId: user.uid));
|
||||
} catch (e) {
|
||||
_emitSessionState(SessionState.error(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle user sign-out event.
|
||||
void handleSignOut() {
|
||||
_emitSessionState(SessionState.unauthenticated());
|
||||
}
|
||||
|
||||
/// Emit session state update.
|
||||
void _emitSessionState(SessionState state) {
|
||||
_lastSessionState = state;
|
||||
if (!_sessionStateController.isClosed) {
|
||||
_sessionStateController.add(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispose session handler resources.
|
||||
Future<void> disposeSessionHandler() async {
|
||||
await _authStateSubscription?.cancel();
|
||||
await _sessionStateController.close();
|
||||
}
|
||||
}
|
||||
@@ -32,3 +32,4 @@ dependencies:
|
||||
flutter_local_notifications: ^21.0.0
|
||||
shared_preferences: ^2.5.4
|
||||
workmanager: ^0.9.0+3
|
||||
uuid: ^4.5.1
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
/// Note: Repository Interfaces are now located in their respective Feature packages.
|
||||
library;
|
||||
|
||||
// Enums (shared status/type enums aligned with V2 CHECK constraints)
|
||||
export 'src/entities/enums/account_type.dart';
|
||||
export 'src/entities/enums/application_status.dart';
|
||||
export 'src/entities/enums/assignment_status.dart';
|
||||
export 'src/entities/enums/attendance_status_type.dart';
|
||||
export 'src/entities/enums/availability_status.dart';
|
||||
export 'src/entities/enums/benefit_status.dart';
|
||||
export 'src/entities/enums/business_status.dart';
|
||||
export 'src/entities/enums/invoice_status.dart';
|
||||
export 'src/entities/enums/onboarding_status.dart';
|
||||
export 'src/entities/enums/order_type.dart';
|
||||
export 'src/entities/enums/payment_status.dart';
|
||||
export 'src/entities/enums/shift_status.dart';
|
||||
export 'src/entities/enums/staff_status.dart';
|
||||
|
||||
// Core
|
||||
export 'src/core/services/api_services/api_response.dart';
|
||||
export 'src/core/services/api_services/base_api_service.dart';
|
||||
@@ -22,124 +37,90 @@ export 'src/core/models/device_location.dart';
|
||||
// Users & Membership
|
||||
export 'src/entities/users/user.dart';
|
||||
export 'src/entities/users/staff.dart';
|
||||
export 'src/entities/users/membership.dart';
|
||||
export 'src/entities/users/biz_member.dart';
|
||||
export 'src/entities/users/hub_member.dart';
|
||||
export 'src/entities/users/staff_session.dart';
|
||||
export 'src/entities/users/client_session.dart';
|
||||
|
||||
// Business & Organization
|
||||
export 'src/entities/business/business.dart';
|
||||
export 'src/entities/business/business_setting.dart';
|
||||
export 'src/entities/business/hub.dart';
|
||||
export 'src/entities/business/hub_department.dart';
|
||||
export 'src/entities/business/vendor.dart';
|
||||
export 'src/entities/business/cost_center.dart';
|
||||
|
||||
// Events & Assignments
|
||||
export 'src/entities/events/event.dart';
|
||||
export 'src/entities/events/event_shift.dart';
|
||||
export 'src/entities/events/event_shift_position.dart';
|
||||
export 'src/entities/events/assignment.dart';
|
||||
export 'src/entities/events/work_session.dart';
|
||||
export 'src/entities/business/vendor_role.dart';
|
||||
export 'src/entities/business/hub_manager.dart';
|
||||
export 'src/entities/business/team_member.dart';
|
||||
|
||||
// Shifts
|
||||
export 'src/entities/shifts/shift.dart';
|
||||
export 'src/adapters/shifts/shift_adapter.dart';
|
||||
export 'src/entities/shifts/break/break.dart';
|
||||
export 'src/adapters/shifts/break/break_adapter.dart';
|
||||
export 'src/entities/shifts/today_shift.dart';
|
||||
export 'src/entities/shifts/assigned_shift.dart';
|
||||
export 'src/entities/shifts/open_shift.dart';
|
||||
export 'src/entities/shifts/pending_assignment.dart';
|
||||
export 'src/entities/shifts/cancelled_shift.dart';
|
||||
export 'src/entities/shifts/completed_shift.dart';
|
||||
export 'src/entities/shifts/shift_detail.dart';
|
||||
|
||||
// Orders & Requests
|
||||
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';
|
||||
// Orders
|
||||
export 'src/entities/orders/order_item.dart';
|
||||
export 'src/entities/orders/reorder_data.dart';
|
||||
|
||||
// Skills & Certs
|
||||
export 'src/entities/skills/skill.dart';
|
||||
export 'src/entities/skills/skill_category.dart';
|
||||
export 'src/entities/skills/staff_skill.dart';
|
||||
export 'src/entities/skills/certificate.dart';
|
||||
export 'src/entities/skills/skill_kit.dart';
|
||||
export 'src/entities/orders/assigned_worker_summary.dart';
|
||||
export 'src/entities/orders/order_preview.dart';
|
||||
export 'src/entities/orders/recent_order.dart';
|
||||
|
||||
// Financial & Payroll
|
||||
export 'src/entities/benefits/benefit.dart';
|
||||
export 'src/entities/financial/invoice.dart';
|
||||
export 'src/entities/financial/time_card.dart';
|
||||
export 'src/entities/financial/invoice_item.dart';
|
||||
export 'src/entities/financial/invoice_decline.dart';
|
||||
export 'src/entities/financial/staff_payment.dart';
|
||||
export 'src/entities/financial/billing_account.dart';
|
||||
export 'src/entities/financial/current_bill.dart';
|
||||
export 'src/entities/financial/savings.dart';
|
||||
export 'src/entities/financial/spend_item.dart';
|
||||
export 'src/entities/financial/bank_account.dart';
|
||||
export 'src/entities/financial/payment_summary.dart';
|
||||
export 'src/entities/financial/billing_period.dart';
|
||||
export 'src/entities/financial/bank_account/bank_account.dart';
|
||||
export 'src/entities/financial/bank_account/business_bank_account.dart';
|
||||
export 'src/entities/financial/bank_account/staff_bank_account.dart';
|
||||
export 'src/adapters/financial/bank_account/bank_account_adapter.dart';
|
||||
export 'src/entities/financial/staff_payment.dart';
|
||||
export 'src/entities/financial/payment_chart_point.dart';
|
||||
export 'src/entities/financial/time_card.dart';
|
||||
|
||||
// Profile
|
||||
export 'src/entities/profile/staff_document.dart';
|
||||
export 'src/entities/profile/document_verification_status.dart';
|
||||
export 'src/entities/profile/staff_certificate.dart';
|
||||
export 'src/entities/profile/compliance_type.dart';
|
||||
export 'src/entities/profile/staff_certificate_status.dart';
|
||||
export 'src/entities/profile/staff_certificate_validation_status.dart';
|
||||
export 'src/entities/profile/attire_item.dart';
|
||||
export 'src/entities/profile/attire_verification_status.dart';
|
||||
export 'src/entities/profile/relationship_type.dart';
|
||||
export 'src/entities/profile/industry.dart';
|
||||
export 'src/entities/profile/tax_form.dart';
|
||||
|
||||
// Ratings & Penalties
|
||||
export 'src/entities/ratings/staff_rating.dart';
|
||||
export 'src/entities/ratings/penalty_log.dart';
|
||||
export 'src/entities/ratings/business_staff_preference.dart';
|
||||
|
||||
// Staff Profile
|
||||
export 'src/entities/profile/staff_personal_info.dart';
|
||||
export 'src/entities/profile/profile_section_status.dart';
|
||||
export 'src/entities/profile/profile_completion.dart';
|
||||
export 'src/entities/profile/profile_document.dart';
|
||||
export 'src/entities/profile/certificate.dart';
|
||||
export 'src/entities/profile/emergency_contact.dart';
|
||||
export 'src/entities/profile/tax_form.dart';
|
||||
export 'src/entities/profile/privacy_settings.dart';
|
||||
export 'src/entities/profile/attire_checklist.dart';
|
||||
export 'src/entities/profile/accessibility.dart';
|
||||
export 'src/entities/profile/schedule.dart';
|
||||
|
||||
// Support & Config
|
||||
export 'src/entities/support/addon.dart';
|
||||
export 'src/entities/support/tag.dart';
|
||||
export 'src/entities/support/media.dart';
|
||||
export 'src/entities/support/working_area.dart';
|
||||
// Ratings
|
||||
export 'src/entities/ratings/staff_rating.dart';
|
||||
|
||||
// Home
|
||||
export 'src/entities/home/home_dashboard_data.dart';
|
||||
export 'src/entities/home/reorder_item.dart';
|
||||
export 'src/entities/home/client_dashboard.dart';
|
||||
export 'src/entities/home/spending_summary.dart';
|
||||
export 'src/entities/home/coverage_metrics.dart';
|
||||
export 'src/entities/home/live_activity_metrics.dart';
|
||||
export 'src/entities/home/staff_dashboard.dart';
|
||||
|
||||
// Availability
|
||||
export 'src/adapters/availability/availability_adapter.dart';
|
||||
// Clock-In & Availability
|
||||
export 'src/entities/clock_in/attendance_status.dart';
|
||||
export 'src/adapters/clock_in/clock_in_adapter.dart';
|
||||
export 'src/entities/availability/availability_slot.dart';
|
||||
export 'src/entities/availability/day_availability.dart';
|
||||
export 'src/entities/availability/availability_day.dart';
|
||||
export 'src/entities/availability/time_slot.dart';
|
||||
|
||||
// Coverage
|
||||
export 'src/entities/coverage_domain/coverage_shift.dart';
|
||||
export 'src/entities/coverage_domain/coverage_worker.dart';
|
||||
export 'src/entities/coverage_domain/shift_with_workers.dart';
|
||||
export 'src/entities/coverage_domain/assigned_worker.dart';
|
||||
export 'src/entities/coverage_domain/time_range.dart';
|
||||
export 'src/entities/coverage_domain/coverage_stats.dart';
|
||||
export 'src/entities/coverage_domain/core_team_member.dart';
|
||||
|
||||
// Adapters
|
||||
export 'src/adapters/profile/emergency_contact_adapter.dart';
|
||||
export 'src/adapters/profile/experience_adapter.dart';
|
||||
export 'src/entities/profile/experience_skill.dart';
|
||||
export 'src/adapters/profile/bank_account_adapter.dart';
|
||||
export 'src/adapters/profile/tax_form_adapter.dart';
|
||||
export 'src/adapters/financial/payment_adapter.dart';
|
||||
// Reports
|
||||
export 'src/entities/reports/report_summary.dart';
|
||||
export 'src/entities/reports/daily_ops_report.dart';
|
||||
export 'src/entities/reports/spend_data_point.dart';
|
||||
export 'src/entities/reports/coverage_report.dart';
|
||||
export 'src/entities/reports/forecast_report.dart';
|
||||
export 'src/entities/reports/performance_report.dart';
|
||||
export 'src/entities/reports/no_show_report.dart';
|
||||
|
||||
// Exceptions
|
||||
export 'src/exceptions/app_exception.dart';
|
||||
|
||||
// Reports
|
||||
export 'src/entities/reports/daily_ops_report.dart';
|
||||
export 'src/entities/reports/spend_report.dart';
|
||||
export 'src/entities/reports/coverage_report.dart';
|
||||
export 'src/entities/reports/forecast_report.dart';
|
||||
export 'src/entities/reports/no_show_report.dart';
|
||||
export 'src/entities/reports/performance_report.dart';
|
||||
export 'src/entities/reports/reports_summary.dart';
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import '../../entities/availability/availability_slot.dart';
|
||||
|
||||
/// Adapter for [AvailabilitySlot] domain entity.
|
||||
class AvailabilityAdapter {
|
||||
static const Map<String, Map<String, String>> _slotDefinitions = <String, Map<String, String>>{
|
||||
'MORNING': <String, String>{
|
||||
'id': 'morning',
|
||||
'label': 'Morning',
|
||||
'timeRange': '4:00 AM - 12:00 PM',
|
||||
},
|
||||
'AFTERNOON': <String, String>{
|
||||
'id': 'afternoon',
|
||||
'label': 'Afternoon',
|
||||
'timeRange': '12:00 PM - 6:00 PM',
|
||||
},
|
||||
'EVENING': <String, String>{
|
||||
'id': 'evening',
|
||||
'label': 'Evening',
|
||||
'timeRange': '6:00 PM - 12:00 AM',
|
||||
},
|
||||
};
|
||||
|
||||
/// Converts a backend slot name (e.g. 'MORNING') to a Domain [AvailabilitySlot].
|
||||
static AvailabilitySlot fromPrimitive(String slotName, {bool isAvailable = false}) {
|
||||
final Map<String, String> def = _slotDefinitions[slotName.toUpperCase()] ?? _slotDefinitions['MORNING']!;
|
||||
return AvailabilitySlot(
|
||||
id: def['id']!,
|
||||
label: def['label']!,
|
||||
timeRange: def['timeRange']!,
|
||||
isAvailable: isAvailable,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import '../../entities/clock_in/attendance_status.dart';
|
||||
|
||||
/// Adapter for Clock In related data.
|
||||
class ClockInAdapter {
|
||||
|
||||
/// Converts primitive attendance data to [AttendanceStatus].
|
||||
static AttendanceStatus toAttendanceStatus({
|
||||
required String status,
|
||||
DateTime? checkInTime,
|
||||
DateTime? checkOutTime,
|
||||
String? activeShiftId,
|
||||
String? activeApplicationId,
|
||||
}) {
|
||||
final bool isCheckedIn = status == 'CHECKED_IN' || status == 'LATE'; // Assuming LATE is also checked in?
|
||||
|
||||
// Statuses that imply active attendance: CHECKED_IN, LATE.
|
||||
// Statuses that imply completed: CHECKED_OUT.
|
||||
|
||||
return AttendanceStatus(
|
||||
isCheckedIn: isCheckedIn,
|
||||
checkInTime: checkInTime,
|
||||
checkOutTime: checkOutTime,
|
||||
activeShiftId: activeShiftId,
|
||||
activeApplicationId: activeApplicationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import '../../../entities/financial/bank_account/business_bank_account.dart';
|
||||
|
||||
/// Adapter for [BusinessBankAccount] to map data layer values to domain entity.
|
||||
class BusinessBankAccountAdapter {
|
||||
/// Maps primitive values to [BusinessBankAccount].
|
||||
static BusinessBankAccount fromPrimitives({
|
||||
required String id,
|
||||
required String bank,
|
||||
required String last4,
|
||||
required bool isPrimary,
|
||||
DateTime? expiryTime,
|
||||
}) {
|
||||
return BusinessBankAccount(
|
||||
id: id,
|
||||
bankName: bank,
|
||||
last4: last4,
|
||||
isPrimary: isPrimary,
|
||||
expiryTime: expiryTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import '../../entities/financial/staff_payment.dart';
|
||||
|
||||
/// Adapter for Payment related data.
|
||||
class PaymentAdapter {
|
||||
|
||||
/// Converts string status to [PaymentStatus].
|
||||
static PaymentStatus toPaymentStatus(String status) {
|
||||
switch (status) {
|
||||
case 'PAID':
|
||||
return PaymentStatus.paid;
|
||||
case 'PENDING':
|
||||
return PaymentStatus.pending;
|
||||
case 'FAILED':
|
||||
return PaymentStatus.failed;
|
||||
default:
|
||||
return PaymentStatus.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import '../../entities/financial/time_card.dart';
|
||||
|
||||
/// Adapter for [TimeCard] to map data layer values to domain entity.
|
||||
class TimeCardAdapter {
|
||||
/// Maps primitive values to [TimeCard].
|
||||
static TimeCard fromPrimitives({
|
||||
required String id,
|
||||
required String shiftTitle,
|
||||
required String clientName,
|
||||
required DateTime date,
|
||||
required String startTime,
|
||||
required String endTime,
|
||||
required double totalHours,
|
||||
required double hourlyRate,
|
||||
required double totalPay,
|
||||
required String status,
|
||||
String? location,
|
||||
}) {
|
||||
return TimeCard(
|
||||
id: id,
|
||||
shiftTitle: shiftTitle,
|
||||
clientName: clientName,
|
||||
date: date,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
totalHours: totalHours,
|
||||
hourlyRate: hourlyRate,
|
||||
totalPay: totalPay,
|
||||
status: _stringToStatus(status),
|
||||
location: location,
|
||||
);
|
||||
}
|
||||
|
||||
static TimeCardStatus _stringToStatus(String status) {
|
||||
switch (status.toUpperCase()) {
|
||||
case 'CHECKED_OUT':
|
||||
case 'COMPLETED':
|
||||
return TimeCardStatus.approved; // Assuming completed = approved for now
|
||||
case 'PAID':
|
||||
return TimeCardStatus.paid; // If this status exists
|
||||
case 'DISPUTED':
|
||||
return TimeCardStatus.disputed;
|
||||
case 'CHECKED_IN':
|
||||
case 'CONFIRMED':
|
||||
default:
|
||||
return TimeCardStatus.pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import '../../entities/financial/bank_account/staff_bank_account.dart';
|
||||
|
||||
/// Adapter for [StaffBankAccount] to map data layer values to domain entity.
|
||||
class BankAccountAdapter {
|
||||
/// Maps primitive values to [StaffBankAccount].
|
||||
static StaffBankAccount fromPrimitives({
|
||||
required String id,
|
||||
required String userId,
|
||||
required String bankName,
|
||||
required String? type,
|
||||
String? accountNumber,
|
||||
String? last4,
|
||||
String? sortCode,
|
||||
bool? isPrimary,
|
||||
}) {
|
||||
return StaffBankAccount(
|
||||
id: id,
|
||||
userId: userId,
|
||||
bankName: bankName,
|
||||
accountNumber: accountNumber ?? '',
|
||||
accountName: '', // Not provided by backend
|
||||
last4: last4,
|
||||
sortCode: sortCode,
|
||||
type: _stringToType(type),
|
||||
isPrimary: isPrimary ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
static StaffBankAccountType _stringToType(String? value) {
|
||||
if (value == null) return StaffBankAccountType.checking;
|
||||
try {
|
||||
// Assuming backend enum names match or are uppercase
|
||||
return StaffBankAccountType.values.firstWhere(
|
||||
(StaffBankAccountType e) => e.name.toLowerCase() == value.toLowerCase(),
|
||||
orElse: () => StaffBankAccountType.other,
|
||||
);
|
||||
} catch (_) {
|
||||
return StaffBankAccountType.other;
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts domain type to string for backend.
|
||||
static String typeToString(StaffBankAccountType type) {
|
||||
switch (type) {
|
||||
case StaffBankAccountType.checking:
|
||||
return 'CHECKING';
|
||||
case StaffBankAccountType.savings:
|
||||
return 'SAVINGS';
|
||||
default:
|
||||
return 'CHECKING';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import '../../entities/profile/emergency_contact.dart';
|
||||
|
||||
/// Adapter for [EmergencyContact] to map data layer values to domain entity.
|
||||
class EmergencyContactAdapter {
|
||||
/// Maps primitive values to [EmergencyContact].
|
||||
static EmergencyContact fromPrimitives({
|
||||
required String id,
|
||||
required String name,
|
||||
required String phone,
|
||||
String? relationship,
|
||||
}) {
|
||||
return EmergencyContact(
|
||||
id: id,
|
||||
name: name,
|
||||
phone: phone,
|
||||
relationship: EmergencyContact.stringToRelationshipType(relationship),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/// Adapter for Experience data (skills/industries) to map data layer values to domain models.
|
||||
class ExperienceAdapter {
|
||||
/// Converts a dynamic list (from backend AnyValue) to List<String>.
|
||||
///
|
||||
/// Handles nulls and converts elements to Strings.
|
||||
static List<String> fromDynamicList(dynamic data) {
|
||||
if (data == null) return <String>[];
|
||||
|
||||
if (data is List) {
|
||||
return data
|
||||
.where((dynamic e) => e != null)
|
||||
.map((dynamic e) => e.toString())
|
||||
.toList();
|
||||
}
|
||||
|
||||
return <String>[];
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import '../../entities/profile/tax_form.dart';
|
||||
|
||||
/// Adapter for [TaxForm] to map data layer values to domain entity.
|
||||
class TaxFormAdapter {
|
||||
/// Maps primitive values to [TaxForm].
|
||||
static TaxForm fromPrimitives({
|
||||
required String id,
|
||||
required String type,
|
||||
required String title,
|
||||
String? subtitle,
|
||||
String? description,
|
||||
required String status,
|
||||
String? staffId,
|
||||
dynamic formData,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
final TaxFormType formType = _stringToType(type);
|
||||
final TaxFormStatus formStatus = _stringToStatus(status);
|
||||
final Map<String, dynamic> formDetails =
|
||||
formData is Map ? Map<String, dynamic>.from(formData) : <String, dynamic>{};
|
||||
|
||||
if (formType == TaxFormType.i9) {
|
||||
return I9TaxForm(
|
||||
id: id,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
description: description,
|
||||
status: formStatus,
|
||||
staffId: staffId,
|
||||
formData: formDetails,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
} else {
|
||||
return W4TaxForm(
|
||||
id: id,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
description: description,
|
||||
status: formStatus,
|
||||
staffId: staffId,
|
||||
formData: formDetails,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static TaxFormType _stringToType(String? value) {
|
||||
if (value == null) return TaxFormType.i9;
|
||||
try {
|
||||
return TaxFormType.values.firstWhere(
|
||||
(TaxFormType e) => e.name.toLowerCase() == value.toLowerCase(),
|
||||
orElse: () => TaxFormType.i9,
|
||||
);
|
||||
} catch (_) {
|
||||
return TaxFormType.i9;
|
||||
}
|
||||
}
|
||||
|
||||
static TaxFormStatus _stringToStatus(String? value) {
|
||||
if (value == null) return TaxFormStatus.notStarted;
|
||||
try {
|
||||
final String normalizedValue = value.replaceAll('_', '').toLowerCase();
|
||||
// map DRAFT to inProgress
|
||||
if (normalizedValue == 'draft') return TaxFormStatus.inProgress;
|
||||
|
||||
return TaxFormStatus.values.firstWhere(
|
||||
(TaxFormStatus e) {
|
||||
// Handle differences like not_started vs notStarted if any,
|
||||
// but standardizing to lowercase is a good start.
|
||||
// The enum names are camelCase in Dart, but might be SNAKE_CASE from backend.
|
||||
final String normalizedEnum = e.name.toLowerCase();
|
||||
return normalizedValue == normalizedEnum;
|
||||
},
|
||||
orElse: () => TaxFormStatus.notStarted,
|
||||
);
|
||||
} catch (_) {
|
||||
return TaxFormStatus.notStarted;
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts domain [TaxFormType] to string for backend.
|
||||
static String typeToString(TaxFormType type) {
|
||||
return type.name.toUpperCase();
|
||||
}
|
||||
|
||||
/// Converts domain [TaxFormStatus] to string for backend.
|
||||
static String statusToString(TaxFormStatus status) {
|
||||
switch (status) {
|
||||
case TaxFormStatus.notStarted:
|
||||
return 'NOT_STARTED';
|
||||
case TaxFormStatus.inProgress:
|
||||
return 'DRAFT';
|
||||
case TaxFormStatus.submitted:
|
||||
return 'SUBMITTED';
|
||||
case TaxFormStatus.approved:
|
||||
return 'APPROVED';
|
||||
case TaxFormStatus.rejected:
|
||||
return 'REJECTED';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import '../../../entities/shifts/break/break.dart';
|
||||
|
||||
/// Adapter for Break related data.
|
||||
class BreakAdapter {
|
||||
/// Maps break data to a Break entity.
|
||||
///
|
||||
/// [isPaid] whether the break is paid.
|
||||
/// [breakTime] the string representation of the break duration (e.g., 'MIN_10', 'MIN_30').
|
||||
static Break fromData({
|
||||
required bool isPaid,
|
||||
required String? breakTime,
|
||||
}) {
|
||||
return Break(
|
||||
isBreakPaid: isPaid,
|
||||
duration: _parseDuration(breakTime),
|
||||
);
|
||||
}
|
||||
|
||||
static BreakDuration _parseDuration(String? breakTime) {
|
||||
if (breakTime == null) return BreakDuration.none;
|
||||
|
||||
switch (breakTime.toUpperCase()) {
|
||||
case 'MIN_10':
|
||||
return BreakDuration.ten;
|
||||
case 'MIN_15':
|
||||
return BreakDuration.fifteen;
|
||||
case 'MIN_20':
|
||||
return BreakDuration.twenty;
|
||||
case 'MIN_30':
|
||||
return BreakDuration.thirty;
|
||||
case 'MIN_45':
|
||||
return BreakDuration.fortyFive;
|
||||
case 'MIN_60':
|
||||
return BreakDuration.sixty;
|
||||
default:
|
||||
return BreakDuration.none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../entities/shifts/shift.dart';
|
||||
|
||||
/// Adapter for Shift related data.
|
||||
class ShiftAdapter {
|
||||
/// Maps application data to a Shift entity.
|
||||
///
|
||||
/// This method handles the common mapping logic used across different
|
||||
/// repositories when converting application data from Data Connect to
|
||||
/// domain Shift entities.
|
||||
static Shift fromApplicationData({
|
||||
required String shiftId,
|
||||
required String roleId,
|
||||
required String roleName,
|
||||
required String businessName,
|
||||
String? companyLogoUrl,
|
||||
required double costPerHour,
|
||||
String? shiftLocation,
|
||||
required String teamHubName,
|
||||
DateTime? shiftDate,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
DateTime? createdAt,
|
||||
required String status,
|
||||
String? description,
|
||||
int? durationDays,
|
||||
required int count,
|
||||
int? assigned,
|
||||
String? eventName,
|
||||
bool hasApplied = false,
|
||||
}) {
|
||||
final String orderName = (eventName ?? '').trim().isNotEmpty
|
||||
? eventName!
|
||||
: businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
|
||||
return Shift(
|
||||
id: shiftId,
|
||||
roleId: roleId,
|
||||
title: title,
|
||||
clientName: businessName,
|
||||
logoUrl: companyLogoUrl,
|
||||
hourlyRate: costPerHour,
|
||||
location: shiftLocation ?? '',
|
||||
locationAddress: teamHubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startTime != null ? DateFormat('HH:mm').format(startTime) : '',
|
||||
endTime: endTime != null ? DateFormat('HH:mm').format(endTime) : '',
|
||||
createdDate: createdAt?.toIso8601String() ?? '',
|
||||
status: status,
|
||||
description: description,
|
||||
durationDays: durationDays,
|
||||
requiredSlots: count,
|
||||
filledSlots: assigned ?? 0,
|
||||
hasApplied: hasApplied,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,11 @@ abstract class BaseApiService {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? params,
|
||||
});
|
||||
|
||||
/// Performs a DELETE request to the specified [endpoint].
|
||||
Future<ApiResponse> delete(
|
||||
String endpoint, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? params,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/availability/time_slot.dart';
|
||||
import 'package:krow_domain/src/entities/enums/availability_status.dart';
|
||||
|
||||
/// Availability for a single calendar date.
|
||||
///
|
||||
/// Returned by `GET /staff/availability`. The backend generates one entry
|
||||
/// per date in the requested range by projecting the staff member's
|
||||
/// recurring weekly availability pattern.
|
||||
class AvailabilityDay extends Equatable {
|
||||
/// Creates an [AvailabilityDay].
|
||||
const AvailabilityDay({
|
||||
required this.date,
|
||||
required this.dayOfWeek,
|
||||
required this.availabilityStatus,
|
||||
this.slots = const <TimeSlot>[],
|
||||
});
|
||||
|
||||
/// Deserialises from the V2 API JSON response.
|
||||
factory AvailabilityDay.fromJson(Map<String, dynamic> json) {
|
||||
final dynamic rawSlots = json['slots'];
|
||||
final List<TimeSlot> parsedSlots = rawSlots is List<dynamic>
|
||||
? rawSlots
|
||||
.map((dynamic e) =>
|
||||
TimeSlot.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: <TimeSlot>[];
|
||||
|
||||
return AvailabilityDay(
|
||||
date: json['date'] as String,
|
||||
dayOfWeek: json['dayOfWeek'] as int,
|
||||
availabilityStatus:
|
||||
AvailabilityStatus.fromJson(json['availabilityStatus'] as String?),
|
||||
slots: parsedSlots,
|
||||
);
|
||||
}
|
||||
|
||||
/// ISO date string (`YYYY-MM-DD`).
|
||||
final String date;
|
||||
|
||||
/// Day of week (0 = Sunday, 6 = Saturday).
|
||||
final int dayOfWeek;
|
||||
|
||||
/// Availability status for this day.
|
||||
final AvailabilityStatus availabilityStatus;
|
||||
|
||||
/// Time slots when the worker is available (relevant for `PARTIAL`).
|
||||
final List<TimeSlot> slots;
|
||||
|
||||
/// Whether the worker has any availability on this day.
|
||||
bool get isAvailable => availabilityStatus != AvailabilityStatus.unavailable;
|
||||
|
||||
/// Creates a copy with the given fields replaced.
|
||||
AvailabilityDay copyWith({
|
||||
String? date,
|
||||
int? dayOfWeek,
|
||||
AvailabilityStatus? availabilityStatus,
|
||||
List<TimeSlot>? slots,
|
||||
}) {
|
||||
return AvailabilityDay(
|
||||
date: date ?? this.date,
|
||||
dayOfWeek: dayOfWeek ?? this.dayOfWeek,
|
||||
availabilityStatus: availabilityStatus ?? this.availabilityStatus,
|
||||
slots: slots ?? this.slots,
|
||||
);
|
||||
}
|
||||
|
||||
/// Serialises to JSON.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'date': date,
|
||||
'dayOfWeek': dayOfWeek,
|
||||
'availabilityStatus': availabilityStatus.toJson(),
|
||||
'slots': slots.map((TimeSlot s) => s.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
date,
|
||||
dayOfWeek,
|
||||
availabilityStatus,
|
||||
slots,
|
||||
];
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a specific time slot within a day (e.g., Morning, Afternoon, Evening).
|
||||
class AvailabilitySlot extends Equatable {
|
||||
|
||||
const AvailabilitySlot({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.timeRange,
|
||||
this.isAvailable = true,
|
||||
});
|
||||
final String id;
|
||||
final String label;
|
||||
final String timeRange;
|
||||
final bool isAvailable;
|
||||
|
||||
AvailabilitySlot copyWith({
|
||||
String? id,
|
||||
String? label,
|
||||
String? timeRange,
|
||||
bool? isAvailable,
|
||||
}) {
|
||||
return AvailabilitySlot(
|
||||
id: id ?? this.id,
|
||||
label: label ?? this.label,
|
||||
timeRange: timeRange ?? this.timeRange,
|
||||
isAvailable: isAvailable ?? this.isAvailable,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, label, timeRange, isAvailable];
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'availability_slot.dart';
|
||||
|
||||
/// Represents availability configuration for a specific date.
|
||||
class DayAvailability extends Equatable {
|
||||
|
||||
const DayAvailability({
|
||||
required this.date,
|
||||
this.isAvailable = false,
|
||||
this.slots = const <AvailabilitySlot>[],
|
||||
});
|
||||
final DateTime date;
|
||||
final bool isAvailable;
|
||||
final List<AvailabilitySlot> slots;
|
||||
|
||||
DayAvailability copyWith({
|
||||
DateTime? date,
|
||||
bool? isAvailable,
|
||||
List<AvailabilitySlot>? slots,
|
||||
}) {
|
||||
return DayAvailability(
|
||||
date: date ?? this.date,
|
||||
isAvailable: isAvailable ?? this.isAvailable,
|
||||
slots: slots ?? this.slots,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[date, isAvailable, slots];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A time range within a day of availability.
|
||||
///
|
||||
/// Embedded inside [AvailabilityDay.slots]. Times are stored as `HH:MM`
|
||||
/// strings because the backend stores them in a JSONB array and they
|
||||
/// are timezone-agnostic display values.
|
||||
class TimeSlot extends Equatable {
|
||||
/// Creates a [TimeSlot].
|
||||
const TimeSlot({
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
});
|
||||
|
||||
/// Deserialises from a JSON map inside the availability slots array.
|
||||
factory TimeSlot.fromJson(Map<String, dynamic> json) {
|
||||
return TimeSlot(
|
||||
startTime: json['startTime'] as String? ?? '00:00',
|
||||
endTime: json['endTime'] as String? ?? '00:00',
|
||||
);
|
||||
}
|
||||
|
||||
/// Start time in `HH:MM` format.
|
||||
final String startTime;
|
||||
|
||||
/// End time in `HH:MM` format.
|
||||
final String endTime;
|
||||
|
||||
/// Serialises to JSON.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[startTime, endTime];
|
||||
}
|
||||
@@ -1,26 +1,73 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a staff member's benefit balance.
|
||||
import 'package:krow_domain/src/entities/enums/benefit_status.dart';
|
||||
|
||||
/// A benefit accrued by a staff member (e.g. sick leave, vacation).
|
||||
///
|
||||
/// Returned by `GET /staff/profile/benefits`.
|
||||
class Benefit extends Equatable {
|
||||
/// Creates a [Benefit].
|
||||
/// Creates a [Benefit] instance.
|
||||
const Benefit({
|
||||
required this.benefitId,
|
||||
required this.benefitType,
|
||||
required this.title,
|
||||
required this.entitlementHours,
|
||||
required this.usedHours,
|
||||
required this.status,
|
||||
required this.trackedHours,
|
||||
required this.targetHours,
|
||||
});
|
||||
|
||||
/// The title of the benefit (e.g., Sick Leave, Holiday, Vacation).
|
||||
/// Deserialises a [Benefit] from a V2 API JSON map.
|
||||
factory Benefit.fromJson(Map<String, dynamic> json) {
|
||||
return Benefit(
|
||||
benefitId: json['benefitId'] as String,
|
||||
benefitType: json['benefitType'] as String,
|
||||
title: json['title'] as String,
|
||||
status: BenefitStatus.fromJson(json['status'] as String?),
|
||||
trackedHours: (json['trackedHours'] as num).toInt(),
|
||||
targetHours: (json['targetHours'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier.
|
||||
final String benefitId;
|
||||
|
||||
/// Type code (e.g. SICK_LEAVE, VACATION).
|
||||
final String benefitType;
|
||||
|
||||
/// Human-readable title.
|
||||
final String title;
|
||||
|
||||
/// The total entitlement in hours.
|
||||
final double entitlementHours;
|
||||
/// Current benefit status.
|
||||
final BenefitStatus status;
|
||||
|
||||
/// The hours used so far.
|
||||
final double usedHours;
|
||||
/// Hours tracked so far.
|
||||
final int trackedHours;
|
||||
|
||||
/// The hours remaining.
|
||||
double get remainingHours => entitlementHours - usedHours;
|
||||
/// Target hours to accrue.
|
||||
final int targetHours;
|
||||
|
||||
/// Remaining hours to reach the target.
|
||||
int get remainingHours => targetHours - trackedHours;
|
||||
|
||||
/// Serialises this [Benefit] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'benefitId': benefitId,
|
||||
'benefitType': benefitType,
|
||||
'title': title,
|
||||
'status': status.toJson(),
|
||||
'trackedHours': trackedHours,
|
||||
'targetHours': targetHours,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [title, entitlementHours, usedHours];
|
||||
List<Object?> get props => <Object?>[
|
||||
benefitId,
|
||||
benefitType,
|
||||
title,
|
||||
status,
|
||||
trackedHours,
|
||||
targetHours,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a legal or service contract.
|
||||
///
|
||||
/// Can be between a business and the platform, or a business and staff.
|
||||
class BizContract extends Equatable {
|
||||
|
||||
const BizContract({
|
||||
required this.id,
|
||||
required this.businessId,
|
||||
required this.name,
|
||||
required this.startDate,
|
||||
this.endDate,
|
||||
required this.contentUrl,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Business] party to the contract.
|
||||
final String businessId;
|
||||
|
||||
/// Descriptive name of the contract.
|
||||
final String name;
|
||||
|
||||
/// Valid from date.
|
||||
final DateTime startDate;
|
||||
|
||||
/// Valid until date (null if indefinite).
|
||||
final DateTime? endDate;
|
||||
|
||||
/// URL to the document content (PDF/HTML).
|
||||
final String contentUrl;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, businessId, name, startDate, endDate, contentUrl];
|
||||
}
|
||||
@@ -1,47 +1,111 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// The operating status of a [Business].
|
||||
enum BusinessStatus {
|
||||
/// Business created but not yet approved.
|
||||
pending,
|
||||
import 'package:krow_domain/src/entities/enums/business_status.dart';
|
||||
|
||||
/// Fully active and operational.
|
||||
active,
|
||||
|
||||
/// Temporarily suspended (e.g. for non-payment).
|
||||
suspended,
|
||||
|
||||
/// Permanently inactive.
|
||||
inactive,
|
||||
}
|
||||
|
||||
/// Represents a Client Company / Business.
|
||||
/// A client company registered on the platform.
|
||||
///
|
||||
/// This is the top-level organizational entity in the system.
|
||||
/// Maps to the V2 `businesses` table.
|
||||
class Business extends Equatable {
|
||||
|
||||
/// Creates a [Business] instance.
|
||||
const Business({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.registrationNumber,
|
||||
required this.tenantId,
|
||||
required this.slug,
|
||||
required this.businessName,
|
||||
required this.status,
|
||||
this.avatar,
|
||||
this.contactName,
|
||||
this.contactEmail,
|
||||
this.contactPhone,
|
||||
this.metadata = const <String, dynamic>{},
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
/// Unique identifier for the business.
|
||||
|
||||
/// Deserialises a [Business] from a V2 API JSON map.
|
||||
factory Business.fromJson(Map<String, dynamic> json) {
|
||||
return Business(
|
||||
id: json['id'] as String,
|
||||
tenantId: json['tenantId'] as String,
|
||||
slug: json['slug'] as String,
|
||||
businessName: json['businessName'] as String,
|
||||
status: BusinessStatus.fromJson(json['status'] as String?),
|
||||
contactName: json['contactName'] as String?,
|
||||
contactEmail: json['contactEmail'] as String?,
|
||||
contactPhone: json['contactPhone'] as String?,
|
||||
metadata: json['metadata'] is Map
|
||||
? Map<String, dynamic>.from(json['metadata'] as Map<dynamic, dynamic>)
|
||||
: const <String, dynamic>{},
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'] as String)
|
||||
: null,
|
||||
updatedAt: json['updatedAt'] != null
|
||||
? DateTime.parse(json['updatedAt'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// Tenant this business belongs to.
|
||||
final String tenantId;
|
||||
|
||||
/// URL-safe slug.
|
||||
final String slug;
|
||||
|
||||
/// Display name of the business.
|
||||
final String name;
|
||||
final String businessName;
|
||||
|
||||
/// Legal registration or tax number.
|
||||
final String registrationNumber;
|
||||
|
||||
/// Current operating status.
|
||||
/// Current account status.
|
||||
final BusinessStatus status;
|
||||
|
||||
/// URL to the business logo.
|
||||
final String? avatar;
|
||||
/// Primary contact name.
|
||||
final String? contactName;
|
||||
|
||||
/// Primary contact email.
|
||||
final String? contactEmail;
|
||||
|
||||
/// Primary contact phone.
|
||||
final String? contactPhone;
|
||||
|
||||
/// Flexible metadata bag.
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
/// When the record was created.
|
||||
final DateTime? createdAt;
|
||||
|
||||
/// When the record was last updated.
|
||||
final DateTime? updatedAt;
|
||||
|
||||
/// Serialises this [Business] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'id': id,
|
||||
'tenantId': tenantId,
|
||||
'slug': slug,
|
||||
'businessName': businessName,
|
||||
'status': status.toJson(),
|
||||
'contactName': contactName,
|
||||
'contactEmail': contactEmail,
|
||||
'contactPhone': contactPhone,
|
||||
'metadata': metadata,
|
||||
'createdAt': createdAt?.toIso8601String(),
|
||||
'updatedAt': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, name, registrationNumber, status, avatar];
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
tenantId,
|
||||
slug,
|
||||
businessName,
|
||||
status,
|
||||
contactName,
|
||||
contactEmail,
|
||||
contactPhone,
|
||||
metadata,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents payroll and operational configuration for a [Business].
|
||||
class BusinessSetting extends Equatable {
|
||||
|
||||
const BusinessSetting({
|
||||
required this.id,
|
||||
required this.businessId,
|
||||
required this.prefix,
|
||||
required this.overtimeEnabled,
|
||||
this.clockInRequirement,
|
||||
this.clockOutRequirement,
|
||||
});
|
||||
/// Unique identifier for the settings record.
|
||||
final String id;
|
||||
|
||||
/// The [Business] these settings apply to.
|
||||
final String businessId;
|
||||
|
||||
/// Prefix for generated invoices (e.g., "INV-").
|
||||
final String prefix;
|
||||
|
||||
/// Whether overtime calculations are applied.
|
||||
final bool overtimeEnabled;
|
||||
|
||||
/// Requirement method for clocking in (e.g. "qr_code", "geo_fence").
|
||||
final String? clockInRequirement;
|
||||
|
||||
/// Requirement method for clocking out.
|
||||
final String? clockOutRequirement;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
businessId,
|
||||
prefix,
|
||||
overtimeEnabled,
|
||||
clockInRequirement,
|
||||
clockOutRequirement,
|
||||
];
|
||||
}
|
||||
@@ -1,22 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a financial cost center used for billing and tracking.
|
||||
/// A financial cost center used for billing and tracking.
|
||||
///
|
||||
/// Returned by `GET /client/cost-centers`.
|
||||
class CostCenter extends Equatable {
|
||||
/// Creates a [CostCenter] instance.
|
||||
const CostCenter({
|
||||
required this.id,
|
||||
required this.costCenterId,
|
||||
required this.name,
|
||||
this.code,
|
||||
});
|
||||
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
/// Deserialises a [CostCenter] from a V2 API JSON map.
|
||||
factory CostCenter.fromJson(Map<String, dynamic> json) {
|
||||
return CostCenter(
|
||||
costCenterId: json['costCenterId'] as String,
|
||||
name: json['name'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// Display name of the cost center.
|
||||
/// Unique identifier.
|
||||
final String costCenterId;
|
||||
|
||||
/// Display name.
|
||||
final String name;
|
||||
|
||||
/// Optional alphanumeric code associated with this cost center.
|
||||
final String? code;
|
||||
/// Serialises this [CostCenter] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'costCenterId': costCenterId,
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, name, code];
|
||||
List<Object?> get props => <Object?>[costCenterId, name];
|
||||
}
|
||||
|
||||
@@ -1,51 +1,107 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'cost_center.dart';
|
||||
|
||||
/// The status of a [Hub].
|
||||
enum HubStatus {
|
||||
/// Fully operational.
|
||||
active,
|
||||
|
||||
/// Closed or inactive.
|
||||
inactive,
|
||||
|
||||
/// Not yet ready for operations.
|
||||
underConstruction,
|
||||
}
|
||||
|
||||
/// Represents a branch location or operational unit within a [Business].
|
||||
/// A physical clock-point location (hub) belonging to a business.
|
||||
///
|
||||
/// Maps to the V2 `clock_points` table; returned by `GET /client/hubs`.
|
||||
class Hub extends Equatable {
|
||||
/// Creates a [Hub] instance.
|
||||
const Hub({
|
||||
required this.id,
|
||||
required this.businessId,
|
||||
required this.hubId,
|
||||
required this.name,
|
||||
required this.address,
|
||||
this.fullAddress,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.nfcTagId,
|
||||
required this.status,
|
||||
this.costCenter,
|
||||
this.city,
|
||||
this.state,
|
||||
this.zipCode,
|
||||
this.costCenterId,
|
||||
this.costCenterName,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The parent [Business].
|
||||
final String businessId;
|
||||
/// Deserialises a [Hub] from a V2 API JSON map.
|
||||
factory Hub.fromJson(Map<String, dynamic> json) {
|
||||
return Hub(
|
||||
hubId: json['hubId'] as String,
|
||||
name: json['name'] as String,
|
||||
fullAddress: json['fullAddress'] as String?,
|
||||
latitude: json['latitude'] != null
|
||||
? double.parse(json['latitude'].toString())
|
||||
: null,
|
||||
longitude: json['longitude'] != null
|
||||
? double.parse(json['longitude'].toString())
|
||||
: null,
|
||||
nfcTagId: json['nfcTagId'] as String?,
|
||||
city: json['city'] as String?,
|
||||
state: json['state'] as String?,
|
||||
zipCode: json['zipCode'] as String?,
|
||||
costCenterId: json['costCenterId'] as String?,
|
||||
costCenterName: json['costCenterName'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Display name of the hub (e.g. "Downtown Branch").
|
||||
/// Unique identifier (clock_point id).
|
||||
final String hubId;
|
||||
|
||||
/// Display label for the hub.
|
||||
final String name;
|
||||
|
||||
/// Physical address of this hub.
|
||||
final String address;
|
||||
/// Full street address.
|
||||
final String? fullAddress;
|
||||
|
||||
/// Unique identifier of the NFC tag assigned to this hub.
|
||||
/// GPS latitude.
|
||||
final double? latitude;
|
||||
|
||||
/// GPS longitude.
|
||||
final double? longitude;
|
||||
|
||||
/// NFC tag UID assigned to this hub.
|
||||
final String? nfcTagId;
|
||||
|
||||
/// Operational status.
|
||||
final HubStatus status;
|
||||
/// City from metadata.
|
||||
final String? city;
|
||||
|
||||
/// Assigned cost center for this hub.
|
||||
final CostCenter? costCenter;
|
||||
/// State from metadata.
|
||||
final String? state;
|
||||
|
||||
/// Zip code from metadata.
|
||||
final String? zipCode;
|
||||
|
||||
/// Associated cost center ID.
|
||||
final String? costCenterId;
|
||||
|
||||
/// Associated cost center name.
|
||||
final String? costCenterName;
|
||||
|
||||
/// Serialises this [Hub] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'hubId': hubId,
|
||||
'name': name,
|
||||
'fullAddress': fullAddress,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'nfcTagId': nfcTagId,
|
||||
'city': city,
|
||||
'state': state,
|
||||
'zipCode': zipCode,
|
||||
'costCenterId': costCenterId,
|
||||
'costCenterName': costCenterName,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, businessId, name, address, nfcTagId, status, costCenter];
|
||||
List<Object?> get props => <Object?>[
|
||||
hubId,
|
||||
name,
|
||||
fullAddress,
|
||||
latitude,
|
||||
longitude,
|
||||
nfcTagId,
|
||||
city,
|
||||
state,
|
||||
zipCode,
|
||||
costCenterId,
|
||||
costCenterName,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a department within a [Hub].
|
||||
///
|
||||
/// Used for more granular organization of staff and events (e.g. "Kitchen", "Service").
|
||||
class HubDepartment extends Equatable {
|
||||
|
||||
const HubDepartment({
|
||||
required this.id,
|
||||
required this.hubId,
|
||||
required this.name,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Hub] this department belongs to.
|
||||
final String hubId;
|
||||
|
||||
/// Name of the department.
|
||||
final String name;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, hubId, name];
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A manager assigned to a hub (clock point).
|
||||
///
|
||||
/// Returned by `GET /client/hubs/:id/managers`.
|
||||
class HubManager extends Equatable {
|
||||
/// Creates a [HubManager] instance.
|
||||
const HubManager({
|
||||
required this.managerAssignmentId,
|
||||
required this.businessMembershipId,
|
||||
required this.managerId,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
/// Deserialises a [HubManager] from a V2 API JSON map.
|
||||
factory HubManager.fromJson(Map<String, dynamic> json) {
|
||||
return HubManager(
|
||||
managerAssignmentId: json['managerAssignmentId'] as String,
|
||||
businessMembershipId: json['businessMembershipId'] as String,
|
||||
managerId: json['managerId'] as String,
|
||||
name: json['name'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// Primary key of the hub_managers row.
|
||||
final String managerAssignmentId;
|
||||
|
||||
/// Business membership ID of the manager.
|
||||
final String businessMembershipId;
|
||||
|
||||
/// User ID of the manager.
|
||||
final String managerId;
|
||||
|
||||
/// Display name of the manager.
|
||||
final String name;
|
||||
|
||||
/// Serialises this [HubManager] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'managerAssignmentId': managerAssignmentId,
|
||||
'businessMembershipId': businessMembershipId,
|
||||
'managerId': managerId,
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
managerAssignmentId,
|
||||
businessMembershipId,
|
||||
managerId,
|
||||
name,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A member of a business team (business membership + user).
|
||||
///
|
||||
/// Returned by `GET /client/team-members`.
|
||||
class TeamMember extends Equatable {
|
||||
/// Creates a [TeamMember] instance.
|
||||
const TeamMember({
|
||||
required this.businessMembershipId,
|
||||
required this.userId,
|
||||
required this.name,
|
||||
this.email,
|
||||
this.role,
|
||||
});
|
||||
|
||||
/// Deserialises a [TeamMember] from a V2 API JSON map.
|
||||
factory TeamMember.fromJson(Map<String, dynamic> json) {
|
||||
return TeamMember(
|
||||
businessMembershipId: json['businessMembershipId'] as String,
|
||||
userId: json['userId'] as String,
|
||||
name: json['name'] as String,
|
||||
email: json['email'] as String?,
|
||||
role: json['role'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Business membership primary key.
|
||||
final String businessMembershipId;
|
||||
|
||||
/// User ID.
|
||||
final String userId;
|
||||
|
||||
/// Display name.
|
||||
final String name;
|
||||
|
||||
/// Email address.
|
||||
final String? email;
|
||||
|
||||
/// Business role (owner, manager, member, viewer).
|
||||
final String? role;
|
||||
|
||||
/// Serialises this [TeamMember] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'businessMembershipId': businessMembershipId,
|
||||
'userId': userId,
|
||||
'name': name,
|
||||
'email': email,
|
||||
'role': role,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
businessMembershipId,
|
||||
userId,
|
||||
name,
|
||||
email,
|
||||
role,
|
||||
];
|
||||
}
|
||||
@@ -1,15 +1,86 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a staffing vendor.
|
||||
import 'package:krow_domain/src/entities/enums/business_status.dart';
|
||||
|
||||
/// A staffing vendor that supplies workers to businesses.
|
||||
///
|
||||
/// Maps to the V2 `vendors` table.
|
||||
class Vendor extends Equatable {
|
||||
const Vendor({required this.id, required this.name, required this.rates});
|
||||
/// Creates a [Vendor] instance.
|
||||
const Vendor({
|
||||
required this.id,
|
||||
required this.tenantId,
|
||||
required this.slug,
|
||||
required this.companyName,
|
||||
required this.status,
|
||||
this.contactName,
|
||||
this.contactEmail,
|
||||
this.contactPhone,
|
||||
});
|
||||
|
||||
/// Deserialises a [Vendor] from a V2 API JSON map.
|
||||
factory Vendor.fromJson(Map<String, dynamic> json) {
|
||||
return Vendor(
|
||||
id: json['id'] as String? ?? json['vendorId'] as String,
|
||||
tenantId: json['tenantId'] as String? ?? '',
|
||||
slug: json['slug'] as String? ?? '',
|
||||
companyName: json['companyName'] as String? ??
|
||||
json['vendorName'] as String? ??
|
||||
'',
|
||||
status: BusinessStatus.fromJson(json['status'] as String?),
|
||||
contactName: json['contactName'] as String?,
|
||||
contactEmail: json['contactEmail'] as String?,
|
||||
contactPhone: json['contactPhone'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
/// A map of role names to hourly rates.
|
||||
final Map<String, double> rates;
|
||||
/// Tenant this vendor belongs to.
|
||||
final String tenantId;
|
||||
|
||||
/// URL-safe slug.
|
||||
final String slug;
|
||||
|
||||
/// Display name of the vendor company.
|
||||
final String companyName;
|
||||
|
||||
/// Current account status.
|
||||
final BusinessStatus status;
|
||||
|
||||
/// Primary contact name.
|
||||
final String? contactName;
|
||||
|
||||
/// Primary contact email.
|
||||
final String? contactEmail;
|
||||
|
||||
/// Primary contact phone.
|
||||
final String? contactPhone;
|
||||
|
||||
/// Serialises this [Vendor] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'id': id,
|
||||
'tenantId': tenantId,
|
||||
'slug': slug,
|
||||
'companyName': companyName,
|
||||
'status': status.toJson(),
|
||||
'contactName': contactName,
|
||||
'contactEmail': contactEmail,
|
||||
'contactPhone': contactPhone,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, name, rates];
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
tenantId,
|
||||
slug,
|
||||
companyName,
|
||||
status,
|
||||
contactName,
|
||||
contactEmail,
|
||||
contactPhone,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A role available through a vendor with its billing rate.
|
||||
///
|
||||
/// Returned by `GET /client/vendors/:id/roles`.
|
||||
class VendorRole extends Equatable {
|
||||
/// Creates a [VendorRole] instance.
|
||||
const VendorRole({
|
||||
required this.roleId,
|
||||
required this.roleCode,
|
||||
required this.roleName,
|
||||
required this.hourlyRateCents,
|
||||
});
|
||||
|
||||
/// Deserialises a [VendorRole] from a V2 API JSON map.
|
||||
factory VendorRole.fromJson(Map<String, dynamic> json) {
|
||||
return VendorRole(
|
||||
roleId: json['roleId'] as String,
|
||||
roleCode: json['roleCode'] as String,
|
||||
roleName: json['roleName'] as String,
|
||||
hourlyRateCents: (json['hourlyRateCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier from the roles catalog.
|
||||
final String roleId;
|
||||
|
||||
/// Short code for the role (e.g. BARISTA).
|
||||
final String roleCode;
|
||||
|
||||
/// Human-readable role name.
|
||||
final String roleName;
|
||||
|
||||
/// Billing rate in cents per hour.
|
||||
final int hourlyRateCents;
|
||||
|
||||
/// Serialises this [VendorRole] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'roleId': roleId,
|
||||
'roleCode': roleCode,
|
||||
'roleName': roleName,
|
||||
'hourlyRateCents': hourlyRateCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[roleId, roleCode, roleName, hourlyRateCents];
|
||||
}
|
||||
@@ -1,27 +1,56 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Simple entity to hold attendance state
|
||||
class AttendanceStatus extends Equatable {
|
||||
import 'package:krow_domain/src/entities/enums/attendance_status_type.dart';
|
||||
|
||||
/// Current clock-in / attendance status of the staff member.
|
||||
///
|
||||
/// Returned by `GET /staff/clock-in/status`. When no open session exists
|
||||
/// the API returns `attendanceStatus: 'NOT_CLOCKED_IN'` with null IDs.
|
||||
class AttendanceStatus extends Equatable {
|
||||
/// Creates an [AttendanceStatus].
|
||||
const AttendanceStatus({
|
||||
this.isCheckedIn = false,
|
||||
this.checkInTime,
|
||||
this.checkOutTime,
|
||||
this.activeShiftId,
|
||||
this.activeApplicationId,
|
||||
required this.attendanceStatus,
|
||||
this.clockInAt,
|
||||
});
|
||||
final bool isCheckedIn;
|
||||
final DateTime? checkInTime;
|
||||
final DateTime? checkOutTime;
|
||||
|
||||
/// Deserialises from the V2 API JSON response.
|
||||
factory AttendanceStatus.fromJson(Map<String, dynamic> json) {
|
||||
return AttendanceStatus(
|
||||
activeShiftId: json['activeShiftId'] as String?,
|
||||
attendanceStatus:
|
||||
AttendanceStatusType.fromJson(json['attendanceStatus'] as String?),
|
||||
clockInAt: json['clockInAt'] != null
|
||||
? DateTime.parse(json['clockInAt'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// The shift id of the currently active attendance session, if any.
|
||||
final String? activeShiftId;
|
||||
final String? activeApplicationId;
|
||||
|
||||
/// Attendance session status.
|
||||
final AttendanceStatusType attendanceStatus;
|
||||
|
||||
/// Timestamp of clock-in, if currently clocked in.
|
||||
final DateTime? clockInAt;
|
||||
|
||||
/// Whether the worker is currently clocked in.
|
||||
bool get isClockedIn => attendanceStatus == AttendanceStatusType.open;
|
||||
|
||||
/// Serialises to JSON.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'activeShiftId': activeShiftId,
|
||||
'attendanceStatus': attendanceStatus.toJson(),
|
||||
'clockInAt': clockInAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
isCheckedIn,
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
activeShiftId,
|
||||
activeApplicationId,
|
||||
attendanceStatus,
|
||||
clockInAt,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/enums/assignment_status.dart';
|
||||
|
||||
/// A worker assigned to a coverage shift.
|
||||
///
|
||||
/// Nested within [ShiftWithWorkers].
|
||||
class AssignedWorker extends Equatable {
|
||||
/// Creates an [AssignedWorker] instance.
|
||||
const AssignedWorker({
|
||||
required this.assignmentId,
|
||||
required this.staffId,
|
||||
required this.fullName,
|
||||
required this.status,
|
||||
this.checkInAt,
|
||||
});
|
||||
|
||||
/// Deserialises an [AssignedWorker] from a V2 API JSON map.
|
||||
factory AssignedWorker.fromJson(Map<String, dynamic> json) {
|
||||
return AssignedWorker(
|
||||
assignmentId: json['assignmentId'] as String,
|
||||
staffId: json['staffId'] as String,
|
||||
fullName: json['fullName'] as String,
|
||||
status: AssignmentStatus.fromJson(json['status'] as String?),
|
||||
checkInAt: json['checkInAt'] != null
|
||||
? DateTime.parse(json['checkInAt'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Assignment ID.
|
||||
final String assignmentId;
|
||||
|
||||
/// Staff member ID.
|
||||
final String staffId;
|
||||
|
||||
/// Worker display name.
|
||||
final String fullName;
|
||||
|
||||
/// Assignment status.
|
||||
final AssignmentStatus status;
|
||||
|
||||
/// When the worker clocked in (null if not yet).
|
||||
final DateTime? checkInAt;
|
||||
|
||||
/// Serialises this [AssignedWorker] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'assignmentId': assignmentId,
|
||||
'staffId': staffId,
|
||||
'fullName': fullName,
|
||||
'status': status.toJson(),
|
||||
'checkInAt': checkInAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
assignmentId,
|
||||
staffId,
|
||||
fullName,
|
||||
status,
|
||||
checkInAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A staff member on the business's core team (favorites).
|
||||
///
|
||||
/// Returned by `GET /client/coverage/core-team`.
|
||||
class CoreTeamMember extends Equatable {
|
||||
/// Creates a [CoreTeamMember] instance.
|
||||
const CoreTeamMember({
|
||||
required this.staffId,
|
||||
required this.fullName,
|
||||
this.primaryRole,
|
||||
required this.averageRating,
|
||||
required this.ratingCount,
|
||||
required this.favorite,
|
||||
});
|
||||
|
||||
/// Deserialises a [CoreTeamMember] from a V2 API JSON map.
|
||||
factory CoreTeamMember.fromJson(Map<String, dynamic> json) {
|
||||
return CoreTeamMember(
|
||||
staffId: json['staffId'] as String,
|
||||
fullName: json['fullName'] as String,
|
||||
primaryRole: json['primaryRole'] as String?,
|
||||
averageRating: (json['averageRating'] as num).toDouble(),
|
||||
ratingCount: (json['ratingCount'] as num).toInt(),
|
||||
favorite: json['favorite'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Staff member ID.
|
||||
final String staffId;
|
||||
|
||||
/// Display name.
|
||||
final String fullName;
|
||||
|
||||
/// Primary role code.
|
||||
final String? primaryRole;
|
||||
|
||||
/// Average review rating (0-5).
|
||||
final double averageRating;
|
||||
|
||||
/// Total number of reviews.
|
||||
final int ratingCount;
|
||||
|
||||
/// Whether this staff is favorited by the business.
|
||||
final bool favorite;
|
||||
|
||||
/// Serialises this [CoreTeamMember] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'staffId': staffId,
|
||||
'fullName': fullName,
|
||||
'primaryRole': primaryRole,
|
||||
'averageRating': averageRating,
|
||||
'ratingCount': ratingCount,
|
||||
'favorite': favorite,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
staffId,
|
||||
fullName,
|
||||
primaryRole,
|
||||
averageRating,
|
||||
ratingCount,
|
||||
favorite,
|
||||
];
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'coverage_worker.dart';
|
||||
|
||||
/// Domain entity representing a shift in the coverage view.
|
||||
///
|
||||
/// This is a feature-specific domain entity that encapsulates shift information
|
||||
/// including scheduling details and assigned workers.
|
||||
class CoverageShift extends Equatable {
|
||||
/// Creates a [CoverageShift].
|
||||
const CoverageShift({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.location,
|
||||
required this.startTime,
|
||||
required this.workersNeeded,
|
||||
required this.date,
|
||||
required this.workers,
|
||||
});
|
||||
|
||||
/// The unique identifier for the shift.
|
||||
final String id;
|
||||
|
||||
/// The title or role of the shift.
|
||||
final String title;
|
||||
|
||||
/// The location where the shift takes place.
|
||||
final String location;
|
||||
|
||||
/// The start time of the shift (e.g., "16:00").
|
||||
final String startTime;
|
||||
|
||||
/// The number of workers needed for this shift.
|
||||
final int workersNeeded;
|
||||
|
||||
/// The date of the shift.
|
||||
final DateTime date;
|
||||
|
||||
/// The list of workers assigned to this shift.
|
||||
final List<CoverageWorker> workers;
|
||||
|
||||
/// Calculates the coverage percentage for this shift.
|
||||
int get coveragePercent {
|
||||
if (workersNeeded == 0) return 0;
|
||||
return ((workers.length / workersNeeded) * 100).round();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
title,
|
||||
location,
|
||||
startTime,
|
||||
workersNeeded,
|
||||
date,
|
||||
workers,
|
||||
];
|
||||
}
|
||||
@@ -1,45 +1,69 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Domain entity representing coverage statistics.
|
||||
/// Aggregated coverage statistics for a specific date.
|
||||
///
|
||||
/// Aggregates coverage metrics for a specific date.
|
||||
/// Returned by `GET /client/coverage/stats`.
|
||||
class CoverageStats extends Equatable {
|
||||
/// Creates a [CoverageStats].
|
||||
/// Creates a [CoverageStats] instance.
|
||||
const CoverageStats({
|
||||
required this.totalNeeded,
|
||||
required this.totalConfirmed,
|
||||
required this.checkedIn,
|
||||
required this.enRoute,
|
||||
required this.late,
|
||||
required this.totalPositionsNeeded,
|
||||
required this.totalPositionsConfirmed,
|
||||
required this.totalWorkersCheckedIn,
|
||||
required this.totalWorkersEnRoute,
|
||||
required this.totalWorkersLate,
|
||||
required this.totalCoveragePercentage,
|
||||
});
|
||||
|
||||
/// The total number of workers needed.
|
||||
final int totalNeeded;
|
||||
/// Deserialises a [CoverageStats] from a V2 API JSON map.
|
||||
factory CoverageStats.fromJson(Map<String, dynamic> json) {
|
||||
return CoverageStats(
|
||||
totalPositionsNeeded: (json['totalPositionsNeeded'] as num).toInt(),
|
||||
totalPositionsConfirmed: (json['totalPositionsConfirmed'] as num).toInt(),
|
||||
totalWorkersCheckedIn: (json['totalWorkersCheckedIn'] as num).toInt(),
|
||||
totalWorkersEnRoute: (json['totalWorkersEnRoute'] as num).toInt(),
|
||||
totalWorkersLate: (json['totalWorkersLate'] as num).toInt(),
|
||||
totalCoveragePercentage:
|
||||
(json['totalCoveragePercentage'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// The total number of confirmed workers.
|
||||
final int totalConfirmed;
|
||||
/// Total positions that need to be filled.
|
||||
final int totalPositionsNeeded;
|
||||
|
||||
/// The number of workers who have checked in.
|
||||
final int checkedIn;
|
||||
/// Total positions that have been confirmed.
|
||||
final int totalPositionsConfirmed;
|
||||
|
||||
/// The number of workers en route.
|
||||
final int enRoute;
|
||||
/// Workers who have checked in.
|
||||
final int totalWorkersCheckedIn;
|
||||
|
||||
/// The number of late workers.
|
||||
final int late;
|
||||
/// Workers en route (accepted but not checked in).
|
||||
final int totalWorkersEnRoute;
|
||||
|
||||
/// Calculates the overall coverage percentage.
|
||||
int get coveragePercent {
|
||||
if (totalNeeded == 0) return 0;
|
||||
return ((totalConfirmed / totalNeeded) * 100).round();
|
||||
/// Workers marked as late / no-show.
|
||||
final int totalWorkersLate;
|
||||
|
||||
/// Overall coverage percentage (0-100).
|
||||
final int totalCoveragePercentage;
|
||||
|
||||
/// Serialises this [CoverageStats] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'totalPositionsNeeded': totalPositionsNeeded,
|
||||
'totalPositionsConfirmed': totalPositionsConfirmed,
|
||||
'totalWorkersCheckedIn': totalWorkersCheckedIn,
|
||||
'totalWorkersEnRoute': totalWorkersEnRoute,
|
||||
'totalWorkersLate': totalWorkersLate,
|
||||
'totalCoveragePercentage': totalCoveragePercentage,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
totalNeeded,
|
||||
totalConfirmed,
|
||||
checkedIn,
|
||||
enRoute,
|
||||
late,
|
||||
totalPositionsNeeded,
|
||||
totalPositionsConfirmed,
|
||||
totalWorkersCheckedIn,
|
||||
totalWorkersEnRoute,
|
||||
totalWorkersLate,
|
||||
totalCoveragePercentage,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Worker status enum matching ApplicationStatus from Data Connect.
|
||||
enum CoverageWorkerStatus {
|
||||
/// Application is pending approval.
|
||||
pending,
|
||||
|
||||
/// Application has been accepted.
|
||||
accepted,
|
||||
|
||||
/// Application has been rejected.
|
||||
rejected,
|
||||
|
||||
/// Worker has confirmed attendance.
|
||||
confirmed,
|
||||
|
||||
/// Worker has checked in.
|
||||
checkedIn,
|
||||
|
||||
/// Worker has checked out.
|
||||
checkedOut,
|
||||
|
||||
/// Worker is late.
|
||||
late,
|
||||
|
||||
/// Worker did not show up.
|
||||
noShow,
|
||||
|
||||
/// Shift is completed.
|
||||
completed,
|
||||
}
|
||||
|
||||
/// Domain entity representing a worker in the coverage view.
|
||||
///
|
||||
/// This entity tracks worker status including check-in information.
|
||||
class CoverageWorker extends Equatable {
|
||||
/// Creates a [CoverageWorker].
|
||||
const CoverageWorker({
|
||||
required this.name,
|
||||
required this.status,
|
||||
this.checkInTime,
|
||||
});
|
||||
|
||||
/// The name of the worker.
|
||||
final String name;
|
||||
|
||||
/// The status of the worker.
|
||||
final CoverageWorkerStatus status;
|
||||
|
||||
/// The time the worker checked in, if applicable.
|
||||
final String? checkInTime;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[name, status, checkInTime];
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'assigned_worker.dart';
|
||||
import 'time_range.dart';
|
||||
|
||||
/// A shift in the coverage view with its assigned workers.
|
||||
///
|
||||
/// Returned by `GET /client/coverage`.
|
||||
class ShiftWithWorkers extends Equatable {
|
||||
/// Creates a [ShiftWithWorkers] instance.
|
||||
const ShiftWithWorkers({
|
||||
required this.shiftId,
|
||||
required this.roleName,
|
||||
required this.timeRange,
|
||||
required this.requiredWorkerCount,
|
||||
required this.assignedWorkerCount,
|
||||
this.assignedWorkers = const <AssignedWorker>[],
|
||||
});
|
||||
|
||||
/// Deserialises a [ShiftWithWorkers] from a V2 API JSON map.
|
||||
factory ShiftWithWorkers.fromJson(Map<String, dynamic> json) {
|
||||
final dynamic workersRaw = json['assignedWorkers'];
|
||||
final List<AssignedWorker> workersList = workersRaw is List
|
||||
? workersRaw
|
||||
.map((dynamic e) =>
|
||||
AssignedWorker.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: const <AssignedWorker>[];
|
||||
|
||||
return ShiftWithWorkers(
|
||||
shiftId: json['shiftId'] as String,
|
||||
roleName: json['roleName'] as String? ?? '',
|
||||
timeRange: TimeRange.fromJson(json['timeRange'] as Map<String, dynamic>),
|
||||
requiredWorkerCount: (json['requiredWorkerCount'] as num).toInt(),
|
||||
assignedWorkerCount: (json['assignedWorkerCount'] as num).toInt(),
|
||||
assignedWorkers: workersList,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shift ID.
|
||||
final String shiftId;
|
||||
|
||||
/// Role name for this shift.
|
||||
final String roleName;
|
||||
|
||||
/// Start and end time range.
|
||||
final TimeRange timeRange;
|
||||
|
||||
/// Total workers required.
|
||||
final int requiredWorkerCount;
|
||||
|
||||
/// Workers currently assigned.
|
||||
final int assignedWorkerCount;
|
||||
|
||||
/// List of assigned workers with their statuses.
|
||||
final List<AssignedWorker> assignedWorkers;
|
||||
|
||||
/// Serialises this [ShiftWithWorkers] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'shiftId': shiftId,
|
||||
'roleName': roleName,
|
||||
'timeRange': timeRange.toJson(),
|
||||
'requiredWorkerCount': requiredWorkerCount,
|
||||
'assignedWorkerCount': assignedWorkerCount,
|
||||
'assignedWorkers':
|
||||
assignedWorkers.map((AssignedWorker w) => w.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
shiftId,
|
||||
roleName,
|
||||
timeRange,
|
||||
requiredWorkerCount,
|
||||
assignedWorkerCount,
|
||||
assignedWorkers,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A time range with start and end timestamps.
|
||||
///
|
||||
/// Used within [ShiftWithWorkers] for shift time windows.
|
||||
class TimeRange extends Equatable {
|
||||
/// Creates a [TimeRange] instance.
|
||||
const TimeRange({
|
||||
required this.startsAt,
|
||||
required this.endsAt,
|
||||
});
|
||||
|
||||
/// Deserialises a [TimeRange] from a V2 API JSON map.
|
||||
factory TimeRange.fromJson(Map<String, dynamic> json) {
|
||||
return TimeRange(
|
||||
startsAt: DateTime.parse(json['startsAt'] as String),
|
||||
endsAt: DateTime.parse(json['endsAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Start timestamp.
|
||||
final DateTime startsAt;
|
||||
|
||||
/// End timestamp.
|
||||
final DateTime endsAt;
|
||||
|
||||
/// Serialises this [TimeRange] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'startsAt': startsAt.toIso8601String(),
|
||||
'endsAt': endsAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[startsAt, endsAt];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/// Type of bank account.
|
||||
///
|
||||
/// Used by both staff bank accounts and client billing accounts.
|
||||
enum AccountType {
|
||||
/// Checking account.
|
||||
checking('CHECKING'),
|
||||
|
||||
/// Savings account.
|
||||
savings('SAVINGS'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const AccountType(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static AccountType fromJson(String? value) {
|
||||
if (value == null) return AccountType.checking;
|
||||
for (final AccountType type in AccountType.values) {
|
||||
if (type.value == value) return type;
|
||||
}
|
||||
return AccountType.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/// Status of a worker's application to a shift.
|
||||
///
|
||||
/// Maps to the `status` CHECK constraint in the V2 `applications` table.
|
||||
enum ApplicationStatus {
|
||||
/// Application submitted, awaiting review.
|
||||
pending('PENDING'),
|
||||
|
||||
/// Application confirmed / approved.
|
||||
confirmed('CONFIRMED'),
|
||||
|
||||
/// Worker has checked in for the shift.
|
||||
checkedIn('CHECKED_IN'),
|
||||
|
||||
/// Worker is late for check-in.
|
||||
late_('LATE'),
|
||||
|
||||
/// Worker did not show up.
|
||||
noShow('NO_SHOW'),
|
||||
|
||||
/// Application / attendance completed.
|
||||
completed('COMPLETED'),
|
||||
|
||||
/// Application rejected.
|
||||
rejected('REJECTED'),
|
||||
|
||||
/// Application cancelled.
|
||||
cancelled('CANCELLED'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const ApplicationStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static ApplicationStatus fromJson(String? value) {
|
||||
if (value == null) return ApplicationStatus.unknown;
|
||||
for (final ApplicationStatus status in ApplicationStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return ApplicationStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/// Status of a worker's assignment to a shift.
|
||||
///
|
||||
/// Maps to the `status` CHECK constraint in the V2 `assignments` table.
|
||||
enum AssignmentStatus {
|
||||
/// Worker has been assigned but not yet accepted.
|
||||
assigned('ASSIGNED'),
|
||||
|
||||
/// Worker accepted the assignment.
|
||||
accepted('ACCEPTED'),
|
||||
|
||||
/// Worker requested to swap this assignment.
|
||||
swapRequested('SWAP_REQUESTED'),
|
||||
|
||||
/// Worker has checked in.
|
||||
checkedIn('CHECKED_IN'),
|
||||
|
||||
/// Worker has checked out.
|
||||
checkedOut('CHECKED_OUT'),
|
||||
|
||||
/// Assignment completed.
|
||||
completed('COMPLETED'),
|
||||
|
||||
/// Assignment cancelled.
|
||||
cancelled('CANCELLED'),
|
||||
|
||||
/// Worker did not show up.
|
||||
noShow('NO_SHOW'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const AssignmentStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static AssignmentStatus fromJson(String? value) {
|
||||
if (value == null) return AssignmentStatus.unknown;
|
||||
for (final AssignmentStatus status in AssignmentStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return AssignmentStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/// Attendance session status for clock-in tracking.
|
||||
///
|
||||
/// Maps to the `status` CHECK constraint in the V2 `attendance_events` table.
|
||||
enum AttendanceStatusType {
|
||||
/// Worker has not clocked in yet.
|
||||
notClockedIn('NOT_CLOCKED_IN'),
|
||||
|
||||
/// Attendance session is open (worker is clocked in).
|
||||
open('OPEN'),
|
||||
|
||||
/// Attendance session is closed (worker clocked out).
|
||||
closed('CLOSED'),
|
||||
|
||||
/// Attendance record is disputed.
|
||||
disputed('DISPUTED'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const AttendanceStatusType(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static AttendanceStatusType fromJson(String? value) {
|
||||
if (value == null) return AttendanceStatusType.notClockedIn;
|
||||
for (final AttendanceStatusType status in AttendanceStatusType.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return AttendanceStatusType.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/// Availability status for a calendar day.
|
||||
///
|
||||
/// Used by the staff availability feature to indicate whether a worker
|
||||
/// is available on a given date.
|
||||
enum AvailabilityStatus {
|
||||
/// Worker is available for the full day.
|
||||
available('AVAILABLE'),
|
||||
|
||||
/// Worker is not available.
|
||||
unavailable('UNAVAILABLE'),
|
||||
|
||||
/// Worker is available for partial time slots.
|
||||
partial('PARTIAL'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const AvailabilityStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static AvailabilityStatus fromJson(String? value) {
|
||||
if (value == null) return AvailabilityStatus.unavailable;
|
||||
for (final AvailabilityStatus status in AvailabilityStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return AvailabilityStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/// Status of a staff benefit accrual.
|
||||
///
|
||||
/// Used by the benefits feature to track whether a benefit is currently
|
||||
/// active, paused, or pending activation.
|
||||
enum BenefitStatus {
|
||||
/// Benefit is active and accruing.
|
||||
active('ACTIVE'),
|
||||
|
||||
/// Benefit is inactive / paused.
|
||||
inactive('INACTIVE'),
|
||||
|
||||
/// Benefit is pending activation.
|
||||
pending('PENDING'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const BenefitStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static BenefitStatus fromJson(String? value) {
|
||||
if (value == null) return BenefitStatus.unknown;
|
||||
for (final BenefitStatus status in BenefitStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return BenefitStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/// Account status of a business or vendor.
|
||||
///
|
||||
/// Maps to the `status` CHECK constraint in the V2 `businesses` and
|
||||
/// `vendors` tables.
|
||||
enum BusinessStatus {
|
||||
/// Account is active.
|
||||
active('ACTIVE'),
|
||||
|
||||
/// Account is inactive / suspended.
|
||||
inactive('INACTIVE'),
|
||||
|
||||
/// Account has been archived.
|
||||
archived('ARCHIVED'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const BusinessStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static BusinessStatus fromJson(String? value) {
|
||||
if (value == null) return BusinessStatus.unknown;
|
||||
for (final BusinessStatus status in BusinessStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return BusinessStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/// Lifecycle status of an invoice.
|
||||
///
|
||||
/// Maps to the `status` CHECK constraint in the V2 `invoices` table.
|
||||
enum InvoiceStatus {
|
||||
/// Invoice created but not yet sent.
|
||||
draft('DRAFT'),
|
||||
|
||||
/// Invoice sent, awaiting payment.
|
||||
pending('PENDING'),
|
||||
|
||||
/// Invoice under review.
|
||||
pendingReview('PENDING_REVIEW'),
|
||||
|
||||
/// Invoice approved for payment.
|
||||
approved('APPROVED'),
|
||||
|
||||
/// Invoice paid.
|
||||
paid('PAID'),
|
||||
|
||||
/// Invoice overdue.
|
||||
overdue('OVERDUE'),
|
||||
|
||||
/// Invoice disputed by the client.
|
||||
disputed('DISPUTED'),
|
||||
|
||||
/// Invoice voided / cancelled.
|
||||
void_('VOID'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const InvoiceStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static InvoiceStatus fromJson(String? value) {
|
||||
if (value == null) return InvoiceStatus.unknown;
|
||||
for (final InvoiceStatus status in InvoiceStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return InvoiceStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// Onboarding progress status for a staff member.
|
||||
///
|
||||
/// Maps to the `onboarding_status` CHECK constraint in the V2 `staffs` table.
|
||||
enum OnboardingStatus {
|
||||
/// Onboarding not yet started.
|
||||
pending('PENDING'),
|
||||
|
||||
/// Onboarding in progress.
|
||||
inProgress('IN_PROGRESS'),
|
||||
|
||||
/// Onboarding completed.
|
||||
completed('COMPLETED'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const OnboardingStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static OnboardingStatus fromJson(String? value) {
|
||||
if (value == null) return OnboardingStatus.unknown;
|
||||
for (final OnboardingStatus status in OnboardingStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return OnboardingStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/// Type of order placed by a business client.
|
||||
///
|
||||
/// Maps to the `order_type` CHECK constraint in the V2 `orders` table.
|
||||
enum OrderType {
|
||||
/// A single occurrence order.
|
||||
oneTime('ONE_TIME'),
|
||||
|
||||
/// A recurring/repeating order.
|
||||
recurring('RECURRING'),
|
||||
|
||||
/// A permanent/ongoing order.
|
||||
permanent('PERMANENT'),
|
||||
|
||||
/// A rapid-fill order.
|
||||
rapid('RAPID'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const OrderType(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static OrderType fromJson(String? value) {
|
||||
if (value == null) return OrderType.unknown;
|
||||
for (final OrderType type in OrderType.values) {
|
||||
if (type.value == value) return type;
|
||||
}
|
||||
return OrderType.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/// Payment processing status.
|
||||
///
|
||||
/// Maps to the `payment_status` CHECK constraint in the V2 schema.
|
||||
enum PaymentStatus {
|
||||
/// Payment not yet processed.
|
||||
pending('PENDING'),
|
||||
|
||||
/// Payment is being processed.
|
||||
processing('PROCESSING'),
|
||||
|
||||
/// Payment completed successfully.
|
||||
paid('PAID'),
|
||||
|
||||
/// Payment processing failed.
|
||||
failed('FAILED'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const PaymentStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static PaymentStatus fromJson(String? value) {
|
||||
if (value == null) return PaymentStatus.unknown;
|
||||
for (final PaymentStatus status in PaymentStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return PaymentStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/// Lifecycle status of a shift.
|
||||
///
|
||||
/// Maps to the `status` CHECK constraint in the V2 `shifts` table.
|
||||
enum ShiftStatus {
|
||||
/// Shift created but not yet published.
|
||||
draft('DRAFT'),
|
||||
|
||||
/// Open for applications.
|
||||
open('OPEN'),
|
||||
|
||||
/// Awaiting worker confirmation.
|
||||
pendingConfirmation('PENDING_CONFIRMATION'),
|
||||
|
||||
/// All roles filled and confirmed.
|
||||
assigned('ASSIGNED'),
|
||||
|
||||
/// Currently in progress.
|
||||
active('ACTIVE'),
|
||||
|
||||
/// Shift finished.
|
||||
completed('COMPLETED'),
|
||||
|
||||
/// Shift cancelled.
|
||||
cancelled('CANCELLED'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const ShiftStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static ShiftStatus fromJson(String? value) {
|
||||
if (value == null) return ShiftStatus.unknown;
|
||||
for (final ShiftStatus status in ShiftStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return ShiftStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/// Account status of a staff member.
|
||||
///
|
||||
/// Maps to the `status` CHECK constraint in the V2 `staffs` table.
|
||||
enum StaffStatus {
|
||||
/// Staff account is active.
|
||||
active('ACTIVE'),
|
||||
|
||||
/// Staff has been invited but not yet onboarded.
|
||||
invited('INVITED'),
|
||||
|
||||
/// Staff account is inactive / suspended.
|
||||
inactive('INACTIVE'),
|
||||
|
||||
/// Staff account has been blocked.
|
||||
blocked('BLOCKED'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const StaffStatus(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static StaffStatus fromJson(String? value) {
|
||||
if (value == null) return StaffStatus.unknown;
|
||||
for (final StaffStatus status in StaffStatus.values) {
|
||||
if (status.value == value) return status;
|
||||
}
|
||||
return StaffStatus.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// The status of a staff [Assignment].
|
||||
enum AssignmentStatus {
|
||||
/// Staff member has been assigned but hasn't confirmed.
|
||||
assigned,
|
||||
|
||||
/// Staff member has accepted the assignment.
|
||||
confirmed,
|
||||
|
||||
/// Work is currently in progress (Clocked In).
|
||||
ongoing,
|
||||
|
||||
/// Work completed successfully (Clocked Out).
|
||||
completed,
|
||||
|
||||
/// Staff rejected the assignment offer.
|
||||
declinedByStaff,
|
||||
|
||||
/// Staff canceled after accepting.
|
||||
canceledByStaff,
|
||||
|
||||
/// Staff did not show up.
|
||||
noShowed,
|
||||
}
|
||||
|
||||
/// Represents the link between a [Staff] member and an [EventShiftPosition].
|
||||
class Assignment extends Equatable {
|
||||
|
||||
const Assignment({
|
||||
required this.id,
|
||||
required this.positionId,
|
||||
required this.staffId,
|
||||
required this.status,
|
||||
this.clockIn,
|
||||
this.clockOut,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The job position being filled.
|
||||
final String positionId;
|
||||
|
||||
/// The staff member filling the position.
|
||||
final String staffId;
|
||||
|
||||
/// Current status of the assignment.
|
||||
final AssignmentStatus status;
|
||||
|
||||
/// Actual timestamp when staff clocked in.
|
||||
final DateTime? clockIn;
|
||||
|
||||
/// Actual timestamp when staff clocked out.
|
||||
final DateTime? clockOut;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, positionId, staffId, status, clockIn, clockOut];
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// The workflow status of an [Event].
|
||||
enum EventStatus {
|
||||
/// Created but incomplete.
|
||||
draft,
|
||||
|
||||
/// Waiting for approval or publication.
|
||||
pending,
|
||||
|
||||
/// Published and staff have been assigned.
|
||||
assigned,
|
||||
|
||||
/// Fully confirmed and ready to start.
|
||||
confirmed,
|
||||
|
||||
/// Currently in progress.
|
||||
active,
|
||||
|
||||
/// Work has finished.
|
||||
finished,
|
||||
|
||||
/// All post-event processes (invoicing) complete.
|
||||
completed,
|
||||
|
||||
/// Archived.
|
||||
closed,
|
||||
|
||||
/// Flagged for administrative review.
|
||||
underReview,
|
||||
}
|
||||
|
||||
/// Represents a Job Posting or Event.
|
||||
///
|
||||
/// This is the central entity for scheduling work. An Event contains [EventShift]s.
|
||||
class Event extends Equatable {
|
||||
|
||||
const Event({
|
||||
required this.id,
|
||||
required this.businessId,
|
||||
required this.hubId,
|
||||
required this.name,
|
||||
required this.date,
|
||||
required this.status,
|
||||
required this.contractType,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Business] hosting the event.
|
||||
final String businessId;
|
||||
|
||||
/// The [Hub] location.
|
||||
final String hubId;
|
||||
|
||||
/// Title of the event.
|
||||
final String name;
|
||||
|
||||
/// Date of the event.
|
||||
final DateTime date;
|
||||
|
||||
/// Current workflow status.
|
||||
final EventStatus status;
|
||||
|
||||
/// Type of employment contract (e.g., 'freelance', 'permanent').
|
||||
final String contractType;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, businessId, hubId, name, date, status, contractType];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a specific time block or "shift" within an [Event].
|
||||
///
|
||||
/// An Event can have multiple shifts (e.g. "Morning Shift", "Evening Shift").
|
||||
class EventShift extends Equatable {
|
||||
|
||||
const EventShift({
|
||||
required this.id,
|
||||
required this.eventId,
|
||||
required this.name,
|
||||
required this.address,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Event] this shift belongs to.
|
||||
final String eventId;
|
||||
|
||||
/// Descriptive name (e.g. "Setup Crew").
|
||||
final String name;
|
||||
|
||||
/// Specific address for this shift (if different from Hub).
|
||||
final String address;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, eventId, name, address];
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a specific job opening within a [EventShift].
|
||||
///
|
||||
/// Defines the requirement for a specific [Skill], the quantity needed, and the pay.
|
||||
class EventShiftPosition extends Equatable {
|
||||
|
||||
const EventShiftPosition({
|
||||
required this.id,
|
||||
required this.shiftId,
|
||||
required this.skillId,
|
||||
required this.count,
|
||||
required this.rate,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.breakDurationMinutes,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [EventShift] this position is part of.
|
||||
final String shiftId;
|
||||
|
||||
/// The [Skill] required for this position.
|
||||
final String skillId;
|
||||
|
||||
/// Number of staff needed.
|
||||
final int count;
|
||||
|
||||
/// Hourly pay rate.
|
||||
final double rate;
|
||||
|
||||
/// Start time of this specific position.
|
||||
final DateTime startTime;
|
||||
|
||||
/// End time of this specific position.
|
||||
final DateTime endTime;
|
||||
|
||||
/// Deducted break duration in minutes.
|
||||
final int breakDurationMinutes;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
shiftId,
|
||||
skillId,
|
||||
count,
|
||||
rate,
|
||||
startTime,
|
||||
endTime,
|
||||
breakDurationMinutes,
|
||||
];
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a verified record of time worked.
|
||||
///
|
||||
/// Derived from [Assignment] clock-in/out times, used for payroll.
|
||||
class WorkSession extends Equatable {
|
||||
|
||||
const WorkSession({
|
||||
required this.id,
|
||||
required this.assignmentId,
|
||||
required this.startTime,
|
||||
this.endTime,
|
||||
required this.breakDurationMinutes,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Assignment] this session belongs to.
|
||||
final String assignmentId;
|
||||
|
||||
/// Verified start time.
|
||||
final DateTime startTime;
|
||||
|
||||
/// Verified end time.
|
||||
final DateTime? endTime;
|
||||
|
||||
/// Verified break duration.
|
||||
final int breakDurationMinutes;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, assignmentId, startTime, endTime, breakDurationMinutes];
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/enums/account_type.dart';
|
||||
|
||||
/// A bank account belonging to a staff member.
|
||||
///
|
||||
/// Returned by `GET /staff/profile/bank-accounts`.
|
||||
class BankAccount extends Equatable {
|
||||
/// Creates a [BankAccount] instance.
|
||||
const BankAccount({
|
||||
required this.accountId,
|
||||
required this.bankName,
|
||||
required this.providerReference,
|
||||
this.last4,
|
||||
required this.isPrimary,
|
||||
required this.accountType,
|
||||
});
|
||||
|
||||
/// Deserialises a [BankAccount] from a V2 API JSON map.
|
||||
factory BankAccount.fromJson(Map<String, dynamic> json) {
|
||||
return BankAccount(
|
||||
accountId: json['accountId'] as String,
|
||||
bankName: json['bankName'] as String,
|
||||
providerReference: json['providerReference'] as String,
|
||||
last4: json['last4'] as String?,
|
||||
isPrimary: json['isPrimary'] as bool,
|
||||
accountType: AccountType.fromJson(json['accountType'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier.
|
||||
final String accountId;
|
||||
|
||||
/// Name of the bank / payment provider.
|
||||
final String bankName;
|
||||
|
||||
/// External provider reference.
|
||||
final String providerReference;
|
||||
|
||||
/// Last 4 digits of the account number.
|
||||
final String? last4;
|
||||
|
||||
/// Whether this is the primary payout account.
|
||||
final bool isPrimary;
|
||||
|
||||
/// Account type.
|
||||
final AccountType accountType;
|
||||
|
||||
/// Serialises this [BankAccount] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'accountId': accountId,
|
||||
'bankName': bankName,
|
||||
'providerReference': providerReference,
|
||||
'last4': last4,
|
||||
'isPrimary': isPrimary,
|
||||
'accountType': accountType.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
accountId,
|
||||
bankName,
|
||||
providerReference,
|
||||
last4,
|
||||
isPrimary,
|
||||
accountType,
|
||||
];
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Abstract base class for all types of bank accounts.
|
||||
abstract class BankAccount extends Equatable {
|
||||
/// Creates a [BankAccount].
|
||||
const BankAccount({
|
||||
required this.id,
|
||||
required this.bankName,
|
||||
required this.isPrimary,
|
||||
this.last4,
|
||||
});
|
||||
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// Name of the bank or provider.
|
||||
final String bankName;
|
||||
|
||||
/// Whether this is the primary payment method.
|
||||
final bool isPrimary;
|
||||
|
||||
/// Last 4 digits of the account/card.
|
||||
final String? last4;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, bankName, isPrimary, last4];
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'bank_account.dart';
|
||||
|
||||
/// Domain model representing a business bank account or payment method.
|
||||
class BusinessBankAccount extends BankAccount {
|
||||
/// Creates a [BusinessBankAccount].
|
||||
const BusinessBankAccount({
|
||||
required super.id,
|
||||
required super.bankName,
|
||||
required String last4,
|
||||
required super.isPrimary,
|
||||
this.expiryTime,
|
||||
}) : super(last4: last4);
|
||||
|
||||
/// Expiration date if applicable.
|
||||
final DateTime? expiryTime;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
...super.props,
|
||||
expiryTime,
|
||||
];
|
||||
|
||||
/// Getter for non-nullable last4 in Business context.
|
||||
@override
|
||||
String get last4 => super.last4!;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'bank_account.dart';
|
||||
|
||||
/// Type of staff bank account.
|
||||
enum StaffBankAccountType {
|
||||
/// Checking account.
|
||||
checking,
|
||||
|
||||
/// Savings account.
|
||||
savings,
|
||||
|
||||
/// Other type.
|
||||
other,
|
||||
}
|
||||
|
||||
/// Domain entity representing a staff's bank account.
|
||||
class StaffBankAccount extends BankAccount {
|
||||
/// Creates a [StaffBankAccount].
|
||||
const StaffBankAccount({
|
||||
required super.id,
|
||||
required this.userId,
|
||||
required super.bankName,
|
||||
required this.accountNumber,
|
||||
required this.accountName,
|
||||
required super.isPrimary,
|
||||
super.last4,
|
||||
this.sortCode,
|
||||
this.type = StaffBankAccountType.checking,
|
||||
});
|
||||
|
||||
/// User identifier.
|
||||
final String userId;
|
||||
|
||||
/// Full account number.
|
||||
final String accountNumber;
|
||||
|
||||
/// Name of the account holder.
|
||||
final String accountName;
|
||||
|
||||
/// Sort code (optional).
|
||||
final String? sortCode;
|
||||
|
||||
/// Account type.
|
||||
final StaffBankAccountType type;
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
<Object?>[...super.props, userId, accountNumber, accountName, sortCode, type];
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/enums/account_type.dart';
|
||||
|
||||
/// A billing/bank account belonging to a business.
|
||||
///
|
||||
/// Returned by `GET /client/billing/accounts`.
|
||||
class BillingAccount extends Equatable {
|
||||
/// Creates a [BillingAccount] instance.
|
||||
const BillingAccount({
|
||||
required this.accountId,
|
||||
required this.bankName,
|
||||
required this.providerReference,
|
||||
this.last4,
|
||||
required this.isPrimary,
|
||||
required this.accountType,
|
||||
this.routingNumberMasked,
|
||||
});
|
||||
|
||||
/// Deserialises a [BillingAccount] from a V2 API JSON map.
|
||||
factory BillingAccount.fromJson(Map<String, dynamic> json) {
|
||||
return BillingAccount(
|
||||
accountId: json['accountId'] as String,
|
||||
bankName: json['bankName'] as String,
|
||||
providerReference: json['providerReference'] as String,
|
||||
last4: json['last4'] as String?,
|
||||
isPrimary: json['isPrimary'] as bool,
|
||||
accountType: AccountType.fromJson(json['accountType'] as String?),
|
||||
routingNumberMasked: json['routingNumberMasked'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier.
|
||||
final String accountId;
|
||||
|
||||
/// Name of the bank / payment provider.
|
||||
final String bankName;
|
||||
|
||||
/// External provider reference (e.g. Stripe account ID).
|
||||
final String providerReference;
|
||||
|
||||
/// Last 4 digits of the account number.
|
||||
final String? last4;
|
||||
|
||||
/// Whether this is the primary billing account.
|
||||
final bool isPrimary;
|
||||
|
||||
/// Account type.
|
||||
final AccountType accountType;
|
||||
|
||||
/// Masked routing number.
|
||||
final String? routingNumberMasked;
|
||||
|
||||
/// Serialises this [BillingAccount] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'accountId': accountId,
|
||||
'bankName': bankName,
|
||||
'providerReference': providerReference,
|
||||
'last4': last4,
|
||||
'isPrimary': isPrimary,
|
||||
'accountType': accountType.toJson(),
|
||||
'routingNumberMasked': routingNumberMasked,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
accountId,
|
||||
bankName,
|
||||
providerReference,
|
||||
last4,
|
||||
isPrimary,
|
||||
accountType,
|
||||
routingNumberMasked,
|
||||
];
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/// Defines the period for billing calculations.
|
||||
enum BillingPeriod {
|
||||
/// Weekly billing period.
|
||||
week,
|
||||
|
||||
/// Monthly billing period.
|
||||
month,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// The current outstanding bill for a business.
|
||||
///
|
||||
/// Returned by `GET /client/billing/current-bill`.
|
||||
class CurrentBill extends Equatable {
|
||||
/// Creates a [CurrentBill] instance.
|
||||
const CurrentBill({required this.currentBillCents});
|
||||
|
||||
/// Deserialises a [CurrentBill] from a V2 API JSON map.
|
||||
factory CurrentBill.fromJson(Map<String, dynamic> json) {
|
||||
return CurrentBill(
|
||||
currentBillCents: (json['currentBillCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Outstanding bill amount in cents.
|
||||
final int currentBillCents;
|
||||
|
||||
/// Serialises this [CurrentBill] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'currentBillCents': currentBillCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[currentBillCents];
|
||||
}
|
||||
@@ -1,148 +1,88 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// The workflow status of an [Invoice].
|
||||
enum InvoiceStatus {
|
||||
/// Generated but not yet sent/finalized.
|
||||
open,
|
||||
import 'package:krow_domain/src/entities/enums/invoice_status.dart';
|
||||
|
||||
/// Client has disputed a line item.
|
||||
disputed,
|
||||
|
||||
/// Dispute has been handled.
|
||||
resolved,
|
||||
|
||||
/// Invoice accepted by client.
|
||||
verified,
|
||||
|
||||
/// Payment received.
|
||||
paid,
|
||||
|
||||
/// Payment reconciled in accounting.
|
||||
reconciled,
|
||||
|
||||
/// Payment not received by due date.
|
||||
overdue,
|
||||
}
|
||||
|
||||
/// Represents a bill sent to a [Business] for services rendered.
|
||||
/// An invoice issued to a business for services rendered.
|
||||
///
|
||||
/// Returned by `GET /client/billing/invoices/*`.
|
||||
class Invoice extends Equatable {
|
||||
|
||||
/// Creates an [Invoice] instance.
|
||||
const Invoice({
|
||||
required this.id,
|
||||
required this.eventId,
|
||||
required this.businessId,
|
||||
required this.invoiceId,
|
||||
required this.invoiceNumber,
|
||||
required this.amountCents,
|
||||
required this.status,
|
||||
required this.totalAmount,
|
||||
required this.workAmount,
|
||||
required this.addonsAmount,
|
||||
this.invoiceNumber,
|
||||
this.issueDate,
|
||||
this.title,
|
||||
this.clientName,
|
||||
this.locationAddress,
|
||||
this.staffCount,
|
||||
this.totalHours,
|
||||
this.workers = const [],
|
||||
this.dueDate,
|
||||
this.paymentDate,
|
||||
this.vendorId,
|
||||
this.vendorName,
|
||||
});
|
||||
|
||||
/// Deserialises an [Invoice] from a V2 API JSON map.
|
||||
factory Invoice.fromJson(Map<String, dynamic> json) {
|
||||
return Invoice(
|
||||
invoiceId: json['invoiceId'] as String,
|
||||
invoiceNumber: json['invoiceNumber'] as String,
|
||||
amountCents: (json['amountCents'] as num).toInt(),
|
||||
status: InvoiceStatus.fromJson(json['status'] as String?),
|
||||
dueDate: json['dueDate'] != null
|
||||
? DateTime.parse(json['dueDate'] as String)
|
||||
: null,
|
||||
paymentDate: json['paymentDate'] != null
|
||||
? DateTime.parse(json['paymentDate'] as String)
|
||||
: null,
|
||||
vendorId: json['vendorId'] as String?,
|
||||
vendorName: json['vendorName'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Event] this invoice covers.
|
||||
final String eventId;
|
||||
|
||||
/// The [Business] being billed.
|
||||
final String businessId;
|
||||
|
||||
/// Current payment/approval status.
|
||||
final InvoiceStatus status;
|
||||
|
||||
/// Grand total amount.
|
||||
final double totalAmount;
|
||||
|
||||
/// Total amount for labor costs.
|
||||
final double workAmount;
|
||||
|
||||
/// Total amount for addons/extras.
|
||||
final double addonsAmount;
|
||||
final String invoiceId;
|
||||
|
||||
/// Human-readable invoice number.
|
||||
final String? invoiceNumber;
|
||||
final String invoiceNumber;
|
||||
|
||||
/// Date when the invoice was issued.
|
||||
final DateTime? issueDate;
|
||||
/// Total amount in cents.
|
||||
final int amountCents;
|
||||
|
||||
/// Human-readable title (e.g. event name).
|
||||
final String? title;
|
||||
/// Current invoice lifecycle status.
|
||||
final InvoiceStatus status;
|
||||
|
||||
/// Name of the client business.
|
||||
final String? clientName;
|
||||
/// When payment is due.
|
||||
final DateTime? dueDate;
|
||||
|
||||
/// Address of the event/location.
|
||||
final String? locationAddress;
|
||||
/// When the invoice was paid (history endpoint).
|
||||
final DateTime? paymentDate;
|
||||
|
||||
/// Number of staff worked.
|
||||
final int? staffCount;
|
||||
/// Vendor ID associated with this invoice.
|
||||
final String? vendorId;
|
||||
|
||||
/// Total hours worked.
|
||||
final double? totalHours;
|
||||
/// Vendor company name.
|
||||
final String? vendorName;
|
||||
|
||||
/// List of workers associated with this invoice.
|
||||
final List<InvoiceWorker> workers;
|
||||
/// Serialises this [Invoice] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'invoiceId': invoiceId,
|
||||
'invoiceNumber': invoiceNumber,
|
||||
'amountCents': amountCents,
|
||||
'status': status.toJson(),
|
||||
'dueDate': dueDate?.toIso8601String(),
|
||||
'paymentDate': paymentDate?.toIso8601String(),
|
||||
'vendorId': vendorId,
|
||||
'vendorName': vendorName,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
eventId,
|
||||
businessId,
|
||||
status,
|
||||
totalAmount,
|
||||
workAmount,
|
||||
addonsAmount,
|
||||
invoiceId,
|
||||
invoiceNumber,
|
||||
issueDate,
|
||||
title,
|
||||
clientName,
|
||||
locationAddress,
|
||||
staffCount,
|
||||
totalHours,
|
||||
workers,
|
||||
];
|
||||
}
|
||||
|
||||
/// Represents a worker entry in an [Invoice].
|
||||
class InvoiceWorker extends Equatable {
|
||||
const InvoiceWorker({
|
||||
required this.name,
|
||||
required this.role,
|
||||
required this.amount,
|
||||
required this.hours,
|
||||
required this.rate,
|
||||
this.checkIn,
|
||||
this.checkOut,
|
||||
this.breakMinutes = 0,
|
||||
this.avatarUrl,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String role;
|
||||
final double amount;
|
||||
final double hours;
|
||||
final double rate;
|
||||
final DateTime? checkIn;
|
||||
final DateTime? checkOut;
|
||||
final int breakMinutes;
|
||||
final String? avatarUrl;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
role,
|
||||
amount,
|
||||
hours,
|
||||
rate,
|
||||
checkIn,
|
||||
checkOut,
|
||||
breakMinutes,
|
||||
avatarUrl,
|
||||
amountCents,
|
||||
status,
|
||||
dueDate,
|
||||
paymentDate,
|
||||
vendorId,
|
||||
vendorName,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a reason or log for a declined [Invoice].
|
||||
class InvoiceDecline extends Equatable {
|
||||
|
||||
const InvoiceDecline({
|
||||
required this.id,
|
||||
required this.invoiceId,
|
||||
required this.reason,
|
||||
required this.declinedAt,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Invoice] that was declined.
|
||||
final String invoiceId;
|
||||
|
||||
/// Reason provided by the client.
|
||||
final String reason;
|
||||
|
||||
/// When the decline happened.
|
||||
final DateTime declinedAt;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, invoiceId, reason, declinedAt];
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a line item in an [Invoice].
|
||||
///
|
||||
/// Corresponds to the work done by one [Staff] member.
|
||||
class InvoiceItem extends Equatable {
|
||||
|
||||
const InvoiceItem({
|
||||
required this.id,
|
||||
required this.invoiceId,
|
||||
required this.staffId,
|
||||
required this.workHours,
|
||||
required this.rate,
|
||||
required this.amount,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
|
||||
/// The [Invoice] this item belongs to.
|
||||
final String invoiceId;
|
||||
|
||||
/// The [Staff] member whose work is being billed.
|
||||
final String staffId;
|
||||
|
||||
/// Total billed hours.
|
||||
final double workHours;
|
||||
|
||||
/// Hourly rate applied.
|
||||
final double rate;
|
||||
|
||||
/// Total line item amount (workHours * rate).
|
||||
final double amount;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, invoiceId, staffId, workHours, rate, amount];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A single data point in the staff payment chart.
|
||||
///
|
||||
/// Returned by `GET /staff/payments/chart`.
|
||||
class PaymentChartPoint extends Equatable {
|
||||
/// Creates a [PaymentChartPoint] instance.
|
||||
const PaymentChartPoint({
|
||||
required this.bucket,
|
||||
required this.amountCents,
|
||||
});
|
||||
|
||||
/// Deserialises a [PaymentChartPoint] from a V2 API JSON map.
|
||||
factory PaymentChartPoint.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentChartPoint(
|
||||
bucket: DateTime.parse(json['bucket'] as String),
|
||||
amountCents: (json['amountCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Time bucket start (day, week, or month).
|
||||
final DateTime bucket;
|
||||
|
||||
/// Aggregated payment amount in cents for this bucket.
|
||||
final int amountCents;
|
||||
|
||||
/// Serialises this [PaymentChartPoint] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'bucket': bucket.toIso8601String(),
|
||||
'amountCents': amountCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[bucket, amountCents];
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Summary of staff earnings.
|
||||
/// Aggregated payment summary for a staff member over a date range.
|
||||
///
|
||||
/// Returned by `GET /staff/payments/summary`.
|
||||
class PaymentSummary extends Equatable {
|
||||
/// Creates a [PaymentSummary] instance.
|
||||
const PaymentSummary({required this.totalEarningsCents});
|
||||
|
||||
const PaymentSummary({
|
||||
required this.weeklyEarnings,
|
||||
required this.monthlyEarnings,
|
||||
required this.pendingEarnings,
|
||||
required this.totalEarnings,
|
||||
});
|
||||
final double weeklyEarnings;
|
||||
final double monthlyEarnings;
|
||||
final double pendingEarnings;
|
||||
final double totalEarnings;
|
||||
/// Deserialises a [PaymentSummary] from a V2 API JSON map.
|
||||
factory PaymentSummary.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentSummary(
|
||||
totalEarningsCents: (json['totalEarningsCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Total earnings in cents for the queried period.
|
||||
final int totalEarningsCents;
|
||||
|
||||
/// Serialises this [PaymentSummary] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'totalEarningsCents': totalEarningsCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
weeklyEarnings,
|
||||
monthlyEarnings,
|
||||
pendingEarnings,
|
||||
totalEarnings,
|
||||
];
|
||||
List<Object?> get props => <Object?>[totalEarningsCents];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Accumulated savings for a business.
|
||||
///
|
||||
/// Returned by `GET /client/billing/savings`.
|
||||
class Savings extends Equatable {
|
||||
/// Creates a [Savings] instance.
|
||||
const Savings({required this.savingsCents});
|
||||
|
||||
/// Deserialises a [Savings] from a V2 API JSON map.
|
||||
factory Savings.fromJson(Map<String, dynamic> json) {
|
||||
return Savings(
|
||||
savingsCents: (json['savingsCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Total savings amount in cents.
|
||||
final int savingsCents;
|
||||
|
||||
/// Serialises this [Savings] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'savingsCents': savingsCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[savingsCents];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A single category in the spend breakdown.
|
||||
///
|
||||
/// Returned by `GET /client/billing/spend-breakdown`.
|
||||
class SpendItem extends Equatable {
|
||||
/// Creates a [SpendItem] instance.
|
||||
const SpendItem({
|
||||
required this.category,
|
||||
required this.amountCents,
|
||||
required this.percentage,
|
||||
});
|
||||
|
||||
/// Deserialises a [SpendItem] from a V2 API JSON map.
|
||||
factory SpendItem.fromJson(Map<String, dynamic> json) {
|
||||
return SpendItem(
|
||||
category: json['category'] as String,
|
||||
amountCents: (json['amountCents'] as num).toInt(),
|
||||
percentage: (json['percentage'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Role/category name.
|
||||
final String category;
|
||||
|
||||
/// Total spend in cents for this category.
|
||||
final int amountCents;
|
||||
|
||||
/// Percentage of total spend.
|
||||
final double percentage;
|
||||
|
||||
/// Serialises this [SpendItem] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'category': category,
|
||||
'amountCents': amountCents,
|
||||
'percentage': percentage,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[category, amountCents, percentage];
|
||||
}
|
||||
@@ -1,76 +1,88 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Status of a staff payout.
|
||||
enum PaymentStatus {
|
||||
/// Payout calculated but not processed.
|
||||
pending,
|
||||
import 'package:krow_domain/src/entities/enums/payment_status.dart';
|
||||
|
||||
/// Submitted to banking provider.
|
||||
processing,
|
||||
|
||||
/// Successfully transferred to staff.
|
||||
paid,
|
||||
|
||||
/// Transfer failed.
|
||||
failed,
|
||||
|
||||
/// Status unknown.
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// Represents a payout to a [Staff] member for a completed [Assignment].
|
||||
class StaffPayment extends Equatable {
|
||||
|
||||
const StaffPayment({
|
||||
required this.id,
|
||||
required this.staffId,
|
||||
required this.assignmentId,
|
||||
required this.amount,
|
||||
/// A single payment record for a staff member.
|
||||
///
|
||||
/// Returned by `GET /staff/payments/history`.
|
||||
class PaymentRecord extends Equatable {
|
||||
/// Creates a [PaymentRecord] instance.
|
||||
const PaymentRecord({
|
||||
required this.paymentId,
|
||||
required this.amountCents,
|
||||
required this.date,
|
||||
required this.status,
|
||||
this.paidAt,
|
||||
this.shiftTitle,
|
||||
this.shiftLocation,
|
||||
this.locationAddress,
|
||||
this.hoursWorked,
|
||||
this.hourlyRate,
|
||||
this.workedTime,
|
||||
this.shiftName,
|
||||
this.location,
|
||||
this.hourlyRateCents,
|
||||
this.minutesWorked,
|
||||
});
|
||||
|
||||
/// Deserialises a [PaymentRecord] from a V2 API JSON map.
|
||||
factory PaymentRecord.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentRecord(
|
||||
paymentId: json['paymentId'] as String,
|
||||
amountCents: (json['amountCents'] as num).toInt(),
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
status: PaymentStatus.fromJson(json['status'] as String?),
|
||||
shiftName: json['shiftName'] as String?,
|
||||
location: json['location'] as String?,
|
||||
hourlyRateCents: json['hourlyRateCents'] != null
|
||||
? (json['hourlyRateCents'] as num).toInt()
|
||||
: null,
|
||||
minutesWorked: json['minutesWorked'] != null
|
||||
? (json['minutesWorked'] as num).toInt()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
final String paymentId;
|
||||
|
||||
/// The recipient [Staff].
|
||||
final String staffId;
|
||||
/// Payment amount in cents.
|
||||
final int amountCents;
|
||||
|
||||
/// The [Assignment] being paid for.
|
||||
final String assignmentId;
|
||||
/// Date the payment was processed or created.
|
||||
final DateTime date;
|
||||
|
||||
/// Amount to be paid.
|
||||
final double amount;
|
||||
|
||||
/// Processing status.
|
||||
/// Payment processing status.
|
||||
final PaymentStatus status;
|
||||
|
||||
/// When the payment was successfully processed.
|
||||
final DateTime? paidAt;
|
||||
/// Title of the associated shift.
|
||||
final String? shiftName;
|
||||
|
||||
/// Title of the shift worked.
|
||||
final String? shiftTitle;
|
||||
/// Location/hub name.
|
||||
final String? location;
|
||||
|
||||
/// Location/hub name of the shift.
|
||||
final String? shiftLocation;
|
||||
/// Hourly pay rate in cents.
|
||||
final int? hourlyRateCents;
|
||||
|
||||
/// Address of the shift location.
|
||||
final String? locationAddress;
|
||||
/// Total minutes worked for this payment.
|
||||
final int? minutesWorked;
|
||||
|
||||
/// Number of hours worked.
|
||||
final double? hoursWorked;
|
||||
|
||||
/// Hourly rate for the shift.
|
||||
final double? hourlyRate;
|
||||
|
||||
/// Work session duration or status.
|
||||
final String? workedTime;
|
||||
/// Serialises this [PaymentRecord] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'paymentId': paymentId,
|
||||
'amountCents': amountCents,
|
||||
'date': date.toIso8601String(),
|
||||
'status': status.toJson(),
|
||||
'shiftName': shiftName,
|
||||
'location': location,
|
||||
'hourlyRateCents': hourlyRateCents,
|
||||
'minutesWorked': minutesWorked,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, staffId, assignmentId, amount, status, paidAt, shiftTitle, shiftLocation, locationAddress, hoursWorked, hourlyRate, workedTime];
|
||||
List<Object?> get props => <Object?>[
|
||||
paymentId,
|
||||
amountCents,
|
||||
date,
|
||||
status,
|
||||
shiftName,
|
||||
location,
|
||||
hourlyRateCents,
|
||||
minutesWorked,
|
||||
];
|
||||
}
|
||||
@@ -1,78 +1,88 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Status of a time card.
|
||||
enum TimeCardStatus {
|
||||
/// Waiting for approval or payment.
|
||||
pending,
|
||||
/// Approved by manager.
|
||||
approved,
|
||||
/// Payment has been issued.
|
||||
paid,
|
||||
/// Disputed by staff or client.
|
||||
disputed;
|
||||
|
||||
/// Whether the card is approved.
|
||||
bool get isApproved => this == TimeCardStatus.approved;
|
||||
/// Whether the card is paid.
|
||||
bool get isPaid => this == TimeCardStatus.paid;
|
||||
/// Whether the card is disputed.
|
||||
bool get isDisputed => this == TimeCardStatus.disputed;
|
||||
/// Whether the card is pending.
|
||||
bool get isPending => this == TimeCardStatus.pending;
|
||||
}
|
||||
|
||||
/// Represents a time card for a staff member.
|
||||
class TimeCard extends Equatable {
|
||||
|
||||
/// Creates a [TimeCard].
|
||||
const TimeCard({
|
||||
required this.id,
|
||||
required this.shiftTitle,
|
||||
required this.clientName,
|
||||
/// A single time-card entry for a completed shift.
|
||||
///
|
||||
/// Returned by `GET /staff/profile/time-card`.
|
||||
class TimeCardEntry extends Equatable {
|
||||
/// Creates a [TimeCardEntry] instance.
|
||||
const TimeCardEntry({
|
||||
required this.date,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.totalHours,
|
||||
required this.hourlyRate,
|
||||
required this.totalPay,
|
||||
required this.status,
|
||||
required this.shiftName,
|
||||
this.location,
|
||||
this.clockInAt,
|
||||
this.clockOutAt,
|
||||
required this.minutesWorked,
|
||||
this.hourlyRateCents,
|
||||
required this.totalPayCents,
|
||||
});
|
||||
/// Unique identifier of the time card (often matches Application ID).
|
||||
final String id;
|
||||
/// Title of the shift.
|
||||
final String shiftTitle;
|
||||
/// Name of the client business.
|
||||
final String clientName;
|
||||
|
||||
/// Deserialises a [TimeCardEntry] from a V2 API JSON map.
|
||||
factory TimeCardEntry.fromJson(Map<String, dynamic> json) {
|
||||
return TimeCardEntry(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
shiftName: json['shiftName'] as String,
|
||||
location: json['location'] as String?,
|
||||
clockInAt: json['clockInAt'] != null
|
||||
? DateTime.parse(json['clockInAt'] as String)
|
||||
: null,
|
||||
clockOutAt: json['clockOutAt'] != null
|
||||
? DateTime.parse(json['clockOutAt'] as String)
|
||||
: null,
|
||||
minutesWorked: (json['minutesWorked'] as num).toInt(),
|
||||
hourlyRateCents: json['hourlyRateCents'] != null
|
||||
? (json['hourlyRateCents'] as num).toInt()
|
||||
: null,
|
||||
totalPayCents: (json['totalPayCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Date of the shift.
|
||||
final DateTime date;
|
||||
/// Actual or scheduled start time.
|
||||
final String startTime;
|
||||
/// Actual or scheduled end time.
|
||||
final String endTime;
|
||||
/// Total hours worked.
|
||||
final double totalHours;
|
||||
/// Hourly pay rate.
|
||||
final double hourlyRate;
|
||||
/// Total pay amount.
|
||||
final double totalPay;
|
||||
/// Current status of the time card.
|
||||
final TimeCardStatus status;
|
||||
/// Location name.
|
||||
|
||||
/// Title of the shift.
|
||||
final String shiftName;
|
||||
|
||||
/// Location/hub name.
|
||||
final String? location;
|
||||
|
||||
/// Clock-in timestamp.
|
||||
final DateTime? clockInAt;
|
||||
|
||||
/// Clock-out timestamp.
|
||||
final DateTime? clockOutAt;
|
||||
|
||||
/// Total minutes worked (regular + overtime).
|
||||
final int minutesWorked;
|
||||
|
||||
/// Hourly pay rate in cents.
|
||||
final int? hourlyRateCents;
|
||||
|
||||
/// Gross pay in cents.
|
||||
final int totalPayCents;
|
||||
|
||||
/// Serialises this [TimeCardEntry] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'date': date.toIso8601String(),
|
||||
'shiftName': shiftName,
|
||||
'location': location,
|
||||
'clockInAt': clockInAt?.toIso8601String(),
|
||||
'clockOutAt': clockOutAt?.toIso8601String(),
|
||||
'minutesWorked': minutesWorked,
|
||||
'hourlyRateCents': hourlyRateCents,
|
||||
'totalPayCents': totalPayCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
shiftTitle,
|
||||
clientName,
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
totalHours,
|
||||
hourlyRate,
|
||||
totalPay,
|
||||
status,
|
||||
shiftName,
|
||||
location,
|
||||
clockInAt,
|
||||
clockOutAt,
|
||||
minutesWorked,
|
||||
hourlyRateCents,
|
||||
totalPayCents,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'coverage_metrics.dart';
|
||||
import 'live_activity_metrics.dart';
|
||||
import 'spending_summary.dart';
|
||||
|
||||
/// Client dashboard data aggregating key business metrics.
|
||||
///
|
||||
/// Returned by `GET /client/dashboard`.
|
||||
class ClientDashboard extends Equatable {
|
||||
/// Creates a [ClientDashboard] instance.
|
||||
const ClientDashboard({
|
||||
required this.userName,
|
||||
required this.businessName,
|
||||
required this.businessId,
|
||||
required this.spending,
|
||||
required this.coverage,
|
||||
required this.liveActivity,
|
||||
});
|
||||
|
||||
/// Deserialises a [ClientDashboard] from a V2 API JSON map.
|
||||
factory ClientDashboard.fromJson(Map<String, dynamic> json) {
|
||||
return ClientDashboard(
|
||||
userName: json['userName'] as String,
|
||||
businessName: json['businessName'] as String,
|
||||
businessId: json['businessId'] as String,
|
||||
spending:
|
||||
SpendingSummary.fromJson(json['spending'] as Map<String, dynamic>),
|
||||
coverage:
|
||||
CoverageMetrics.fromJson(json['coverage'] as Map<String, dynamic>),
|
||||
liveActivity: LiveActivityMetrics.fromJson(
|
||||
json['liveActivity'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
/// Display name of the logged-in user.
|
||||
final String userName;
|
||||
|
||||
/// Name of the business.
|
||||
final String businessName;
|
||||
|
||||
/// Business ID.
|
||||
final String businessId;
|
||||
|
||||
/// Spending summary.
|
||||
final SpendingSummary spending;
|
||||
|
||||
/// Today's coverage metrics.
|
||||
final CoverageMetrics coverage;
|
||||
|
||||
/// Live activity metrics.
|
||||
final LiveActivityMetrics liveActivity;
|
||||
|
||||
/// Serialises this [ClientDashboard] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'userName': userName,
|
||||
'businessName': businessName,
|
||||
'businessId': businessId,
|
||||
'spending': spending.toJson(),
|
||||
'coverage': coverage.toJson(),
|
||||
'liveActivity': liveActivity.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
userName,
|
||||
businessName,
|
||||
businessId,
|
||||
spending,
|
||||
coverage,
|
||||
liveActivity,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Today's coverage metrics nested in [ClientDashboard].
|
||||
class CoverageMetrics extends Equatable {
|
||||
/// Creates a [CoverageMetrics] instance.
|
||||
const CoverageMetrics({
|
||||
required this.neededWorkersToday,
|
||||
required this.filledWorkersToday,
|
||||
required this.openPositionsToday,
|
||||
});
|
||||
|
||||
/// Deserialises a [CoverageMetrics] from a V2 API JSON map.
|
||||
factory CoverageMetrics.fromJson(Map<String, dynamic> json) {
|
||||
return CoverageMetrics(
|
||||
neededWorkersToday: (json['neededWorkersToday'] as num).toInt(),
|
||||
filledWorkersToday: (json['filledWorkersToday'] as num).toInt(),
|
||||
openPositionsToday: (json['openPositionsToday'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Workers needed today.
|
||||
final int neededWorkersToday;
|
||||
|
||||
/// Workers filled today.
|
||||
final int filledWorkersToday;
|
||||
|
||||
/// Open (unfilled) positions today.
|
||||
final int openPositionsToday;
|
||||
|
||||
/// Serialises this [CoverageMetrics] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'neededWorkersToday': neededWorkersToday,
|
||||
'filledWorkersToday': filledWorkersToday,
|
||||
'openPositionsToday': openPositionsToday,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
<Object?>[neededWorkersToday, filledWorkersToday, openPositionsToday];
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Entity representing dashboard data for the home screen.
|
||||
///
|
||||
/// This entity provides aggregated metrics such as spending and shift counts
|
||||
/// for both the current week and the upcoming 7 days.
|
||||
class HomeDashboardData extends Equatable {
|
||||
|
||||
/// Creates a [HomeDashboardData] instance.
|
||||
const HomeDashboardData({
|
||||
required this.weeklySpending,
|
||||
required this.next7DaysSpending,
|
||||
required this.weeklyShifts,
|
||||
required this.next7DaysScheduled,
|
||||
required this.totalNeeded,
|
||||
required this.totalFilled,
|
||||
});
|
||||
/// Total spending for the current week.
|
||||
final double weeklySpending;
|
||||
|
||||
/// Projected spending for the next 7 days.
|
||||
final double next7DaysSpending;
|
||||
|
||||
/// Total shifts scheduled for the current week.
|
||||
final int weeklyShifts;
|
||||
|
||||
/// Shifts scheduled for the next 7 days.
|
||||
final int next7DaysScheduled;
|
||||
|
||||
/// Total workers needed for today's shifts.
|
||||
final int totalNeeded;
|
||||
|
||||
/// Total workers filled for today's shifts.
|
||||
final int totalFilled;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
weeklySpending,
|
||||
next7DaysSpending,
|
||||
weeklyShifts,
|
||||
next7DaysScheduled,
|
||||
totalNeeded,
|
||||
totalFilled,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Live activity metrics nested in [ClientDashboard].
|
||||
class LiveActivityMetrics extends Equatable {
|
||||
/// Creates a [LiveActivityMetrics] instance.
|
||||
const LiveActivityMetrics({
|
||||
required this.lateWorkersToday,
|
||||
required this.checkedInWorkersToday,
|
||||
required this.averageShiftCostCents,
|
||||
});
|
||||
|
||||
/// Deserialises a [LiveActivityMetrics] from a V2 API JSON map.
|
||||
factory LiveActivityMetrics.fromJson(Map<String, dynamic> json) {
|
||||
return LiveActivityMetrics(
|
||||
lateWorkersToday: (json['lateWorkersToday'] as num).toInt(),
|
||||
checkedInWorkersToday: (json['checkedInWorkersToday'] as num).toInt(),
|
||||
averageShiftCostCents:
|
||||
(json['averageShiftCostCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Workers marked late/no-show today.
|
||||
final int lateWorkersToday;
|
||||
|
||||
/// Workers who have checked in today.
|
||||
final int checkedInWorkersToday;
|
||||
|
||||
/// Average shift cost in cents.
|
||||
final int averageShiftCostCents;
|
||||
|
||||
/// Serialises this [LiveActivityMetrics] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'lateWorkersToday': lateWorkersToday,
|
||||
'checkedInWorkersToday': checkedInWorkersToday,
|
||||
'averageShiftCostCents': averageShiftCostCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
<Object?>[lateWorkersToday, checkedInWorkersToday, averageShiftCostCents];
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// 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.totalCost,
|
||||
required this.workers,
|
||||
required this.type,
|
||||
this.hourlyRate = 0,
|
||||
this.hours = 0,
|
||||
});
|
||||
|
||||
/// Unique identifier of the order.
|
||||
final String orderId;
|
||||
|
||||
/// Display title of the order (e.g., event name or first shift title).
|
||||
final String title;
|
||||
|
||||
/// Location of the order (e.g., first shift location).
|
||||
final String location;
|
||||
|
||||
/// Total calculated cost for the order.
|
||||
final double totalCost;
|
||||
|
||||
/// Total number of workers required for the order.
|
||||
final int workers;
|
||||
|
||||
/// 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,
|
||||
totalCost,
|
||||
workers,
|
||||
type,
|
||||
hourlyRate,
|
||||
hours,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Spending summary nested in [ClientDashboard].
|
||||
class SpendingSummary extends Equatable {
|
||||
/// Creates a [SpendingSummary] instance.
|
||||
const SpendingSummary({
|
||||
required this.weeklySpendCents,
|
||||
required this.projectedNext7DaysCents,
|
||||
});
|
||||
|
||||
/// Deserialises a [SpendingSummary] from a V2 API JSON map.
|
||||
factory SpendingSummary.fromJson(Map<String, dynamic> json) {
|
||||
return SpendingSummary(
|
||||
weeklySpendCents: (json['weeklySpendCents'] as num).toInt(),
|
||||
projectedNext7DaysCents:
|
||||
(json['projectedNext7DaysCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Total spend this week in cents.
|
||||
final int weeklySpendCents;
|
||||
|
||||
/// Projected spend for the next 7 days in cents.
|
||||
final int projectedNext7DaysCents;
|
||||
|
||||
/// Serialises this [SpendingSummary] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'weeklySpendCents': weeklySpendCents,
|
||||
'projectedNext7DaysCents': projectedNext7DaysCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
<Object?>[weeklySpendCents, projectedNext7DaysCents];
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../benefits/benefit.dart';
|
||||
|
||||
/// Staff dashboard data with shifts and benefits overview.
|
||||
///
|
||||
/// Returned by `GET /staff/dashboard`.
|
||||
class StaffDashboard extends Equatable {
|
||||
/// Creates a [StaffDashboard] instance.
|
||||
const StaffDashboard({
|
||||
required this.staffName,
|
||||
this.todaysShifts = const <Map<String, dynamic>>[],
|
||||
this.tomorrowsShifts = const <Map<String, dynamic>>[],
|
||||
this.recommendedShifts = const <Map<String, dynamic>>[],
|
||||
this.benefits = const <Benefit>[],
|
||||
});
|
||||
|
||||
/// Deserialises a [StaffDashboard] from a V2 API JSON map.
|
||||
factory StaffDashboard.fromJson(Map<String, dynamic> json) {
|
||||
final dynamic benefitsRaw = json['benefits'];
|
||||
final List<Benefit> benefitsList = benefitsRaw is List
|
||||
? benefitsRaw
|
||||
.map((dynamic e) => Benefit.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: const <Benefit>[];
|
||||
|
||||
return StaffDashboard(
|
||||
staffName: json['staffName'] as String,
|
||||
todaysShifts: _castShiftList(json['todaysShifts']),
|
||||
tomorrowsShifts: _castShiftList(json['tomorrowsShifts']),
|
||||
recommendedShifts: _castShiftList(json['recommendedShifts']),
|
||||
benefits: benefitsList,
|
||||
);
|
||||
}
|
||||
|
||||
/// Display name of the staff member.
|
||||
final String staffName;
|
||||
|
||||
/// Shifts assigned for today.
|
||||
final List<Map<String, dynamic>> todaysShifts;
|
||||
|
||||
/// Shifts assigned for tomorrow.
|
||||
final List<Map<String, dynamic>> tomorrowsShifts;
|
||||
|
||||
/// Recommended open shifts.
|
||||
final List<Map<String, dynamic>> recommendedShifts;
|
||||
|
||||
/// Active benefits.
|
||||
final List<Benefit> benefits;
|
||||
|
||||
/// Serialises this [StaffDashboard] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'staffName': staffName,
|
||||
'todaysShifts': todaysShifts,
|
||||
'tomorrowsShifts': tomorrowsShifts,
|
||||
'recommendedShifts': recommendedShifts,
|
||||
'benefits': benefits.map((Benefit b) => b.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> _castShiftList(dynamic raw) {
|
||||
if (raw is List) {
|
||||
return raw
|
||||
.map((dynamic e) =>
|
||||
Map<String, dynamic>.from(e as Map<dynamic, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
return const <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
staffName,
|
||||
todaysShifts,
|
||||
tomorrowsShifts,
|
||||
recommendedShifts,
|
||||
benefits,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/enums/application_status.dart';
|
||||
|
||||
/// Summary of a worker assigned to an order line item.
|
||||
///
|
||||
/// Nested within [OrderItem].
|
||||
class AssignedWorkerSummary extends Equatable {
|
||||
/// Creates an [AssignedWorkerSummary] instance.
|
||||
const AssignedWorkerSummary({
|
||||
this.applicationId,
|
||||
this.workerName,
|
||||
this.role,
|
||||
this.confirmationStatus,
|
||||
});
|
||||
|
||||
/// Deserialises an [AssignedWorkerSummary] from a V2 API JSON map.
|
||||
factory AssignedWorkerSummary.fromJson(Map<String, dynamic> json) {
|
||||
return AssignedWorkerSummary(
|
||||
applicationId: json['applicationId'] as String?,
|
||||
workerName: json['workerName'] as String?,
|
||||
role: json['role'] as String?,
|
||||
confirmationStatus: json['confirmationStatus'] != null
|
||||
? ApplicationStatus.fromJson(json['confirmationStatus'] as String?)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Application ID for this worker assignment.
|
||||
final String? applicationId;
|
||||
|
||||
/// Display name of the worker.
|
||||
final String? workerName;
|
||||
|
||||
/// Role the worker is assigned to.
|
||||
final String? role;
|
||||
|
||||
/// Confirmation status of the assignment.
|
||||
final ApplicationStatus? confirmationStatus;
|
||||
|
||||
/// Serialises this [AssignedWorkerSummary] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'applicationId': applicationId,
|
||||
'workerName': workerName,
|
||||
'role': role,
|
||||
'confirmationStatus': confirmationStatus?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
applicationId,
|
||||
workerName,
|
||||
role,
|
||||
confirmationStatus,
|
||||
];
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'one_time_order_position.dart';
|
||||
|
||||
/// Represents a customer's request for a single event or shift.
|
||||
///
|
||||
/// Encapsulates the date, primary location, and a list of specific [OneTimeOrderPosition] requirements.
|
||||
class OneTimeOrder extends Equatable {
|
||||
|
||||
const OneTimeOrder({
|
||||
required this.date,
|
||||
required this.location,
|
||||
required this.positions,
|
||||
this.hub,
|
||||
this.eventName,
|
||||
this.vendorId,
|
||||
this.hubManagerId,
|
||||
this.roleRates = const <String, double>{},
|
||||
});
|
||||
/// The specific date for the shift or event.
|
||||
final DateTime date;
|
||||
|
||||
/// The primary location where the work will take place.
|
||||
final String location;
|
||||
|
||||
/// The list of positions and headcounts required for this order.
|
||||
final List<OneTimeOrderPosition> positions;
|
||||
|
||||
/// Selected hub details for this order.
|
||||
final OneTimeOrderHubDetails? hub;
|
||||
|
||||
/// Optional order name.
|
||||
final String? eventName;
|
||||
|
||||
/// Selected vendor id for this order.
|
||||
final String? vendorId;
|
||||
|
||||
/// Optional hub manager id.
|
||||
final String? hubManagerId;
|
||||
|
||||
/// Role hourly rates keyed by role id.
|
||||
final Map<String, double> roleRates;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
date,
|
||||
location,
|
||||
positions,
|
||||
hub,
|
||||
eventName,
|
||||
vendorId,
|
||||
hubManagerId,
|
||||
roleRates,
|
||||
];
|
||||
}
|
||||
|
||||
/// Minimal hub details used during order creation.
|
||||
class OneTimeOrderHubDetails extends Equatable {
|
||||
const OneTimeOrderHubDetails({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.address,
|
||||
this.placeId,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.city,
|
||||
this.state,
|
||||
this.street,
|
||||
this.country,
|
||||
this.zipCode,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String address;
|
||||
final String? placeId;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? street;
|
||||
final String? country;
|
||||
final String? zipCode;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
name,
|
||||
address,
|
||||
placeId,
|
||||
latitude,
|
||||
longitude,
|
||||
city,
|
||||
state,
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
];
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a specific position requirement within a [OneTimeOrder].
|
||||
///
|
||||
/// Defines the role, headcount, and scheduling details for a single staffing requirement.
|
||||
class OneTimeOrderPosition extends Equatable {
|
||||
|
||||
const OneTimeOrderPosition({
|
||||
required this.role,
|
||||
required this.count,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.lunchBreak = 'NO_BREAK',
|
||||
this.location,
|
||||
});
|
||||
/// The job role or title required.
|
||||
final String role;
|
||||
|
||||
/// The number of workers required for this position.
|
||||
final int count;
|
||||
|
||||
/// The scheduled start time (e.g., "09:00 AM").
|
||||
final String startTime;
|
||||
|
||||
/// The scheduled end time (e.g., "05:00 PM").
|
||||
final String endTime;
|
||||
|
||||
/// The break duration enum value (e.g., NO_BREAK, MIN_15, MIN_30).
|
||||
final String lunchBreak;
|
||||
|
||||
/// Optional specific location for this position, if different from the order's main location.
|
||||
final String? location;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
role,
|
||||
count,
|
||||
startTime,
|
||||
endTime,
|
||||
lunchBreak,
|
||||
location,
|
||||
];
|
||||
|
||||
/// Creates a copy of this position with the given fields replaced.
|
||||
OneTimeOrderPosition copyWith({
|
||||
String? role,
|
||||
int? count,
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
String? lunchBreak,
|
||||
String? location,
|
||||
}) {
|
||||
return OneTimeOrderPosition(
|
||||
role: role ?? this.role,
|
||||
count: count ?? this.count,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
lunchBreak: lunchBreak ?? this.lunchBreak,
|
||||
location: location ?? this.location,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +1,137 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'order_type.dart';
|
||||
import 'package:krow_domain/src/entities/enums/order_type.dart';
|
||||
import 'package:krow_domain/src/entities/enums/shift_status.dart';
|
||||
|
||||
/// Represents a customer's view of an order or shift.
|
||||
import 'assigned_worker_summary.dart';
|
||||
|
||||
/// A line item within an order, representing a role needed for a shift.
|
||||
///
|
||||
/// This entity captures the details necessary for the dashboard/view orders screen,
|
||||
/// including status and worker assignments.
|
||||
/// Returned by `GET /client/orders/view`.
|
||||
class OrderItem extends Equatable {
|
||||
/// Creates an [OrderItem].
|
||||
/// Creates an [OrderItem] instance.
|
||||
const OrderItem({
|
||||
required this.id,
|
||||
required this.itemId,
|
||||
required this.orderId,
|
||||
required this.orderType,
|
||||
required this.title,
|
||||
required this.clientName,
|
||||
required this.status,
|
||||
required this.roleName,
|
||||
required this.date,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.location,
|
||||
required this.locationAddress,
|
||||
required this.filled,
|
||||
required this.workersNeeded,
|
||||
required this.hourlyRate,
|
||||
required this.eventName,
|
||||
this.hours = 0,
|
||||
this.totalValue = 0,
|
||||
this.confirmedApps = const <Map<String, dynamic>>[],
|
||||
this.hubManagerId,
|
||||
this.hubManagerName,
|
||||
required this.startsAt,
|
||||
required this.endsAt,
|
||||
required this.requiredWorkerCount,
|
||||
required this.filledCount,
|
||||
required this.hourlyRateCents,
|
||||
required this.totalCostCents,
|
||||
this.locationName,
|
||||
required this.status,
|
||||
this.workers = const <AssignedWorkerSummary>[],
|
||||
});
|
||||
|
||||
/// Unique identifier of the order.
|
||||
final String id;
|
||||
/// Deserialises an [OrderItem] from a V2 API JSON map.
|
||||
factory OrderItem.fromJson(Map<String, dynamic> json) {
|
||||
final dynamic workersRaw = json['workers'];
|
||||
final List<AssignedWorkerSummary> workersList = workersRaw is List
|
||||
? workersRaw
|
||||
.map((dynamic e) => AssignedWorkerSummary.fromJson(
|
||||
e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: const <AssignedWorkerSummary>[];
|
||||
|
||||
/// Parent order identifier.
|
||||
return OrderItem(
|
||||
itemId: json['itemId'] as String,
|
||||
orderId: json['orderId'] as String,
|
||||
orderType: OrderType.fromJson(json['orderType'] as String?),
|
||||
roleName: json['roleName'] as String,
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
startsAt: DateTime.parse(json['startsAt'] as String),
|
||||
endsAt: DateTime.parse(json['endsAt'] as String),
|
||||
requiredWorkerCount: (json['requiredWorkerCount'] as num).toInt(),
|
||||
filledCount: (json['filledCount'] as num).toInt(),
|
||||
hourlyRateCents: (json['hourlyRateCents'] as num).toInt(),
|
||||
totalCostCents: (json['totalCostCents'] as num).toInt(),
|
||||
locationName: json['locationName'] as String?,
|
||||
status: ShiftStatus.fromJson(json['status'] as String?),
|
||||
workers: workersList,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shift-role ID (primary key).
|
||||
final String itemId;
|
||||
|
||||
/// Parent order ID.
|
||||
final String orderId;
|
||||
|
||||
/// The type of order (e.g., ONE_TIME, PERMANENT).
|
||||
/// Order type (ONE_TIME, RECURRING, PERMANENT, RAPID).
|
||||
final OrderType orderType;
|
||||
|
||||
/// Title or name of the role.
|
||||
final String title;
|
||||
/// Name of the role.
|
||||
final String roleName;
|
||||
|
||||
/// Name of the client company.
|
||||
final String clientName;
|
||||
/// Shift date.
|
||||
final DateTime date;
|
||||
|
||||
/// status of the order (e.g., 'open', 'filled', 'completed').
|
||||
final String status;
|
||||
/// Shift start time.
|
||||
final DateTime startsAt;
|
||||
|
||||
/// Date of the shift (ISO format).
|
||||
final String date;
|
||||
/// Shift end time.
|
||||
final DateTime endsAt;
|
||||
|
||||
/// Start time of the shift.
|
||||
final String startTime;
|
||||
/// Total workers required.
|
||||
final int requiredWorkerCount;
|
||||
|
||||
/// End time of the shift.
|
||||
final String endTime;
|
||||
/// Workers currently assigned/filled.
|
||||
final int filledCount;
|
||||
|
||||
/// Location name.
|
||||
final String location;
|
||||
/// Billing rate in cents per hour.
|
||||
final int hourlyRateCents;
|
||||
|
||||
/// Full address of the location.
|
||||
final String locationAddress;
|
||||
/// Total cost in cents.
|
||||
final int totalCostCents;
|
||||
|
||||
/// Number of workers currently filled.
|
||||
final int filled;
|
||||
/// Location/hub name.
|
||||
final String? locationName;
|
||||
|
||||
/// Total number of workers required.
|
||||
final int workersNeeded;
|
||||
/// Shift status.
|
||||
final ShiftStatus status;
|
||||
|
||||
/// Hourly pay rate.
|
||||
final double hourlyRate;
|
||||
/// Assigned workers for this line item.
|
||||
final List<AssignedWorkerSummary> workers;
|
||||
|
||||
/// Total hours for the shift role.
|
||||
final double hours;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Optional ID of the assigned hub manager.
|
||||
final String? hubManagerId;
|
||||
|
||||
/// Optional Name of the assigned hub manager.
|
||||
final String? hubManagerName;
|
||||
/// Serialises this [OrderItem] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'itemId': itemId,
|
||||
'orderId': orderId,
|
||||
'orderType': orderType.toJson(),
|
||||
'roleName': roleName,
|
||||
'date': date.toIso8601String(),
|
||||
'startsAt': startsAt.toIso8601String(),
|
||||
'endsAt': endsAt.toIso8601String(),
|
||||
'requiredWorkerCount': requiredWorkerCount,
|
||||
'filledCount': filledCount,
|
||||
'hourlyRateCents': hourlyRateCents,
|
||||
'totalCostCents': totalCostCents,
|
||||
'locationName': locationName,
|
||||
'status': status.toJson(),
|
||||
'workers': workers.map((AssignedWorkerSummary w) => w.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
orderId,
|
||||
orderType,
|
||||
title,
|
||||
clientName,
|
||||
status,
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
location,
|
||||
locationAddress,
|
||||
filled,
|
||||
workersNeeded,
|
||||
hourlyRate,
|
||||
hours,
|
||||
totalValue,
|
||||
eventName,
|
||||
confirmedApps,
|
||||
hubManagerId,
|
||||
hubManagerName,
|
||||
];
|
||||
itemId,
|
||||
orderId,
|
||||
orderType,
|
||||
roleName,
|
||||
date,
|
||||
startsAt,
|
||||
endsAt,
|
||||
requiredWorkerCount,
|
||||
filledCount,
|
||||
hourlyRateCents,
|
||||
totalCostCents,
|
||||
locationName,
|
||||
status,
|
||||
workers,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A preview of an order for reordering purposes.
|
||||
///
|
||||
/// Returned by `GET /client/orders/:id/reorder-preview`.
|
||||
class OrderPreview extends Equatable {
|
||||
/// Creates an [OrderPreview] instance.
|
||||
const OrderPreview({
|
||||
required this.orderId,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.startsAt,
|
||||
this.endsAt,
|
||||
this.locationName,
|
||||
this.locationAddress,
|
||||
this.metadata = const <String, dynamic>{},
|
||||
this.shifts = const <OrderPreviewShift>[],
|
||||
});
|
||||
|
||||
/// Deserialises an [OrderPreview] from a V2 API JSON map.
|
||||
factory OrderPreview.fromJson(Map<String, dynamic> json) {
|
||||
final dynamic shiftsRaw = json['shifts'];
|
||||
final List<OrderPreviewShift> shiftsList = shiftsRaw is List
|
||||
? shiftsRaw
|
||||
.map((dynamic e) =>
|
||||
OrderPreviewShift.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: const <OrderPreviewShift>[];
|
||||
|
||||
return OrderPreview(
|
||||
orderId: json['orderId'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
startsAt: json['startsAt'] != null
|
||||
? DateTime.parse(json['startsAt'] as String)
|
||||
: null,
|
||||
endsAt: json['endsAt'] != null
|
||||
? DateTime.parse(json['endsAt'] as String)
|
||||
: null,
|
||||
locationName: json['locationName'] as String?,
|
||||
locationAddress: json['locationAddress'] as String?,
|
||||
metadata: json['metadata'] is Map
|
||||
? Map<String, dynamic>.from(json['metadata'] as Map<dynamic, dynamic>)
|
||||
: const <String, dynamic>{},
|
||||
shifts: shiftsList,
|
||||
);
|
||||
}
|
||||
|
||||
/// Order ID.
|
||||
final String orderId;
|
||||
|
||||
/// Order title.
|
||||
final String title;
|
||||
|
||||
/// Order description.
|
||||
final String? description;
|
||||
|
||||
/// Order start time.
|
||||
final DateTime? startsAt;
|
||||
|
||||
/// Order end time.
|
||||
final DateTime? endsAt;
|
||||
|
||||
/// Location name.
|
||||
final String? locationName;
|
||||
|
||||
/// Location address.
|
||||
final String? locationAddress;
|
||||
|
||||
/// Flexible metadata bag.
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
/// Shifts with their roles from the original order.
|
||||
final List<OrderPreviewShift> shifts;
|
||||
|
||||
/// Serialises this [OrderPreview] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'orderId': orderId,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'startsAt': startsAt?.toIso8601String(),
|
||||
'endsAt': endsAt?.toIso8601String(),
|
||||
'locationName': locationName,
|
||||
'locationAddress': locationAddress,
|
||||
'metadata': metadata,
|
||||
'shifts': shifts.map((OrderPreviewShift s) => s.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
orderId,
|
||||
title,
|
||||
description,
|
||||
startsAt,
|
||||
endsAt,
|
||||
locationName,
|
||||
locationAddress,
|
||||
metadata,
|
||||
shifts,
|
||||
];
|
||||
}
|
||||
|
||||
/// A shift within a reorder preview.
|
||||
class OrderPreviewShift extends Equatable {
|
||||
/// Creates an [OrderPreviewShift] instance.
|
||||
const OrderPreviewShift({
|
||||
required this.shiftId,
|
||||
required this.shiftCode,
|
||||
required this.title,
|
||||
required this.startsAt,
|
||||
required this.endsAt,
|
||||
this.roles = const <OrderPreviewRole>[],
|
||||
});
|
||||
|
||||
/// Deserialises an [OrderPreviewShift] from a V2 API JSON map.
|
||||
factory OrderPreviewShift.fromJson(Map<String, dynamic> json) {
|
||||
final dynamic rolesRaw = json['roles'];
|
||||
final List<OrderPreviewRole> rolesList = rolesRaw is List
|
||||
? rolesRaw
|
||||
.map((dynamic e) =>
|
||||
OrderPreviewRole.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: const <OrderPreviewRole>[];
|
||||
|
||||
return OrderPreviewShift(
|
||||
shiftId: json['shiftId'] as String,
|
||||
shiftCode: json['shiftCode'] as String,
|
||||
title: json['title'] as String,
|
||||
startsAt: DateTime.parse(json['startsAt'] as String),
|
||||
endsAt: DateTime.parse(json['endsAt'] as String),
|
||||
roles: rolesList,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shift ID.
|
||||
final String shiftId;
|
||||
|
||||
/// Shift code.
|
||||
final String shiftCode;
|
||||
|
||||
/// Shift title.
|
||||
final String title;
|
||||
|
||||
/// Shift start time.
|
||||
final DateTime startsAt;
|
||||
|
||||
/// Shift end time.
|
||||
final DateTime endsAt;
|
||||
|
||||
/// Roles in this shift.
|
||||
final List<OrderPreviewRole> roles;
|
||||
|
||||
/// Serialises this [OrderPreviewShift] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'shiftId': shiftId,
|
||||
'shiftCode': shiftCode,
|
||||
'title': title,
|
||||
'startsAt': startsAt.toIso8601String(),
|
||||
'endsAt': endsAt.toIso8601String(),
|
||||
'roles': roles.map((OrderPreviewRole r) => r.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
<Object?>[shiftId, shiftCode, title, startsAt, endsAt, roles];
|
||||
}
|
||||
|
||||
/// A role within a reorder preview shift.
|
||||
class OrderPreviewRole extends Equatable {
|
||||
/// Creates an [OrderPreviewRole] instance.
|
||||
const OrderPreviewRole({
|
||||
required this.roleId,
|
||||
required this.roleCode,
|
||||
required this.roleName,
|
||||
required this.workersNeeded,
|
||||
required this.payRateCents,
|
||||
required this.billRateCents,
|
||||
});
|
||||
|
||||
/// Deserialises an [OrderPreviewRole] from a V2 API JSON map.
|
||||
factory OrderPreviewRole.fromJson(Map<String, dynamic> json) {
|
||||
return OrderPreviewRole(
|
||||
roleId: json['roleId'] as String,
|
||||
roleCode: json['roleCode'] as String,
|
||||
roleName: json['roleName'] as String,
|
||||
workersNeeded: (json['workersNeeded'] as num).toInt(),
|
||||
payRateCents: (json['payRateCents'] as num).toInt(),
|
||||
billRateCents: (json['billRateCents'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Role ID.
|
||||
final String roleId;
|
||||
|
||||
/// Role code.
|
||||
final String roleCode;
|
||||
|
||||
/// Role name.
|
||||
final String roleName;
|
||||
|
||||
/// Workers needed for this role.
|
||||
final int workersNeeded;
|
||||
|
||||
/// Pay rate in cents per hour.
|
||||
final int payRateCents;
|
||||
|
||||
/// Bill rate in cents per hour.
|
||||
final int billRateCents;
|
||||
|
||||
/// Serialises this [OrderPreviewRole] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'roleId': roleId,
|
||||
'roleCode': roleCode,
|
||||
'roleName': roleName,
|
||||
'workersNeeded': workersNeeded,
|
||||
'payRateCents': payRateCents,
|
||||
'billRateCents': billRateCents,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
roleId,
|
||||
roleCode,
|
||||
roleName,
|
||||
workersNeeded,
|
||||
payRateCents,
|
||||
billRateCents,
|
||||
];
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/// Defines the type of an order.
|
||||
enum OrderType {
|
||||
/// A single occurrence shift.
|
||||
oneTime,
|
||||
|
||||
/// A long-term or permanent staffing position.
|
||||
permanent,
|
||||
|
||||
/// Shifts that repeat on a defined schedule.
|
||||
recurring,
|
||||
|
||||
/// A quickly created shift.
|
||||
rapid;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'one_time_order.dart';
|
||||
import 'one_time_order_position.dart';
|
||||
|
||||
/// Represents a customer's request for permanent/ongoing staffing.
|
||||
class PermanentOrder extends Equatable {
|
||||
const PermanentOrder({
|
||||
required this.startDate,
|
||||
required this.permanentDays,
|
||||
required this.positions,
|
||||
this.hub,
|
||||
this.eventName,
|
||||
this.vendorId,
|
||||
this.hubManagerId,
|
||||
this.roleRates = const <String, double>{},
|
||||
});
|
||||
|
||||
final DateTime startDate;
|
||||
|
||||
/// List of days (e.g., ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'])
|
||||
final List<String> permanentDays;
|
||||
|
||||
final List<OneTimeOrderPosition> positions;
|
||||
final OneTimeOrderHubDetails? hub;
|
||||
final String? eventName;
|
||||
final String? vendorId;
|
||||
final String? hubManagerId;
|
||||
final Map<String, double> roleRates;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
startDate,
|
||||
permanentDays,
|
||||
positions,
|
||||
hub,
|
||||
eventName,
|
||||
vendorId,
|
||||
hubManagerId,
|
||||
roleRates,
|
||||
];
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a specific position requirement within a [PermanentOrder].
|
||||
class PermanentOrderPosition extends Equatable {
|
||||
const PermanentOrderPosition({
|
||||
required this.role,
|
||||
required this.count,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.lunchBreak = 'NO_BREAK',
|
||||
this.location,
|
||||
});
|
||||
|
||||
/// The job role or title required.
|
||||
final String role;
|
||||
|
||||
/// The number of workers required for this position.
|
||||
final int count;
|
||||
|
||||
/// The scheduled start time (e.g., "09:00 AM").
|
||||
final String startTime;
|
||||
|
||||
/// The scheduled end time (e.g., "05:00 PM").
|
||||
final String endTime;
|
||||
|
||||
/// The break duration enum value (e.g., NO_BREAK, MIN_15, MIN_30).
|
||||
final String lunchBreak;
|
||||
|
||||
/// Optional specific location for this position, if different from the order's main location.
|
||||
final String? location;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
role,
|
||||
count,
|
||||
startTime,
|
||||
endTime,
|
||||
lunchBreak,
|
||||
location,
|
||||
];
|
||||
|
||||
/// Creates a copy of this position with the given fields replaced.
|
||||
PermanentOrderPosition copyWith({
|
||||
String? role,
|
||||
int? count,
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
String? lunchBreak,
|
||||
String? location,
|
||||
}) {
|
||||
return PermanentOrderPosition(
|
||||
role: role ?? this.role,
|
||||
count: count ?? this.count,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
lunchBreak: lunchBreak ?? this.lunchBreak,
|
||||
location: location ?? this.location,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/enums/order_type.dart';
|
||||
|
||||
/// A recently completed order available for reordering.
|
||||
///
|
||||
/// Returned by `GET /client/reorders`.
|
||||
class RecentOrder extends Equatable {
|
||||
/// Creates a [RecentOrder] instance.
|
||||
const RecentOrder({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.date,
|
||||
this.hubName,
|
||||
required this.positionCount,
|
||||
required this.orderType,
|
||||
});
|
||||
|
||||
/// Deserialises a [RecentOrder] from a V2 API JSON map.
|
||||
factory RecentOrder.fromJson(Map<String, dynamic> json) {
|
||||
return RecentOrder(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
date: json['date'] != null
|
||||
? DateTime.parse(json['date'] as String)
|
||||
: null,
|
||||
hubName: json['hubName'] as String?,
|
||||
positionCount: (json['positionCount'] as num).toInt(),
|
||||
orderType: OrderType.fromJson(json['orderType'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
/// Order ID.
|
||||
final String id;
|
||||
|
||||
/// Order title.
|
||||
final String title;
|
||||
|
||||
/// Order date.
|
||||
final DateTime? date;
|
||||
|
||||
/// Hub/location name.
|
||||
final String? hubName;
|
||||
|
||||
/// Number of positions in the order.
|
||||
final int positionCount;
|
||||
|
||||
/// Type of order (ONE_TIME, RECURRING, PERMANENT, RAPID).
|
||||
final OrderType orderType;
|
||||
|
||||
/// Serialises this [RecentOrder] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'id': id,
|
||||
'title': title,
|
||||
'date': date?.toIso8601String(),
|
||||
'hubName': hubName,
|
||||
'positionCount': positionCount,
|
||||
'orderType': orderType.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, title, date, hubName, positionCount, orderType];
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'recurring_order_position.dart';
|
||||
|
||||
/// Represents a recurring staffing request spanning a date range.
|
||||
class RecurringOrder extends Equatable {
|
||||
const RecurringOrder({
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.recurringDays,
|
||||
required this.location,
|
||||
required this.positions,
|
||||
this.hub,
|
||||
this.eventName,
|
||||
this.vendorId,
|
||||
this.hubManagerId,
|
||||
this.roleRates = const <String, double>{},
|
||||
});
|
||||
|
||||
/// Start date for the recurring schedule.
|
||||
final DateTime startDate;
|
||||
|
||||
/// End date for the recurring schedule.
|
||||
final DateTime endDate;
|
||||
|
||||
/// Days of the week to repeat on (e.g., ["S", "M", ...]).
|
||||
final List<String> recurringDays;
|
||||
|
||||
/// The primary location where the work will take place.
|
||||
final String location;
|
||||
|
||||
/// The list of positions and headcounts required for this order.
|
||||
final List<RecurringOrderPosition> positions;
|
||||
|
||||
/// Selected hub details for this order.
|
||||
final RecurringOrderHubDetails? hub;
|
||||
|
||||
/// Optional order name.
|
||||
final String? eventName;
|
||||
|
||||
/// Selected vendor id for this order.
|
||||
final String? vendorId;
|
||||
|
||||
/// Optional hub manager id.
|
||||
final String? hubManagerId;
|
||||
|
||||
/// Role hourly rates keyed by role id.
|
||||
final Map<String, double> roleRates;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
startDate,
|
||||
endDate,
|
||||
recurringDays,
|
||||
location,
|
||||
positions,
|
||||
hub,
|
||||
eventName,
|
||||
vendorId,
|
||||
hubManagerId,
|
||||
roleRates,
|
||||
];
|
||||
}
|
||||
|
||||
/// Minimal hub details used during recurring order creation.
|
||||
class RecurringOrderHubDetails extends Equatable {
|
||||
const RecurringOrderHubDetails({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.address,
|
||||
this.placeId,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.city,
|
||||
this.state,
|
||||
this.street,
|
||||
this.country,
|
||||
this.zipCode,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String address;
|
||||
final String? placeId;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? street;
|
||||
final String? country;
|
||||
final String? zipCode;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
name,
|
||||
address,
|
||||
placeId,
|
||||
latitude,
|
||||
longitude,
|
||||
city,
|
||||
state,
|
||||
street,
|
||||
country,
|
||||
zipCode,
|
||||
];
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Represents a specific position requirement within a [RecurringOrder].
|
||||
class RecurringOrderPosition extends Equatable {
|
||||
const RecurringOrderPosition({
|
||||
required this.role,
|
||||
required this.count,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
this.lunchBreak = 'NO_BREAK',
|
||||
this.location,
|
||||
});
|
||||
|
||||
/// The job role or title required.
|
||||
final String role;
|
||||
|
||||
/// The number of workers required for this position.
|
||||
final int count;
|
||||
|
||||
/// The scheduled start time (e.g., "09:00 AM").
|
||||
final String startTime;
|
||||
|
||||
/// The scheduled end time (e.g., "05:00 PM").
|
||||
final String endTime;
|
||||
|
||||
/// The break duration enum value (e.g., NO_BREAK, MIN_15, MIN_30).
|
||||
final String lunchBreak;
|
||||
|
||||
/// Optional specific location for this position, if different from the order's main location.
|
||||
final String? location;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
role,
|
||||
count,
|
||||
startTime,
|
||||
endTime,
|
||||
lunchBreak,
|
||||
location,
|
||||
];
|
||||
|
||||
/// Creates a copy of this position with the given fields replaced.
|
||||
RecurringOrderPosition copyWith({
|
||||
String? role,
|
||||
int? count,
|
||||
String? startTime,
|
||||
String? endTime,
|
||||
String? lunchBreak,
|
||||
String? location,
|
||||
}) {
|
||||
return RecurringOrderPosition(
|
||||
role: role ?? this.role,
|
||||
count: count ?? this.count,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
lunchBreak: lunchBreak ?? this.lunchBreak,
|
||||
location: location ?? this.location,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
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,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Status of an attire checklist item.
|
||||
enum AttireItemStatus {
|
||||
/// Photo has not been uploaded yet.
|
||||
notUploaded,
|
||||
|
||||
/// Upload is pending review.
|
||||
pending,
|
||||
|
||||
/// Photo has been verified/approved.
|
||||
verified,
|
||||
|
||||
/// Photo was rejected.
|
||||
rejected,
|
||||
|
||||
/// Document has expired.
|
||||
expired,
|
||||
}
|
||||
|
||||
/// An attire checklist item for a staff member.
|
||||
///
|
||||
/// Returned by `GET /staff/profile/attire`. Joins the `documents` catalog
|
||||
/// (filtered to ATTIRE type) with the staff-specific `staff_documents` record.
|
||||
class AttireChecklist extends Equatable {
|
||||
/// Creates an [AttireChecklist] instance.
|
||||
const AttireChecklist({
|
||||
required this.documentId,
|
||||
required this.name,
|
||||
this.description = '',
|
||||
this.mandatory = true,
|
||||
this.staffDocumentId,
|
||||
this.photoUri,
|
||||
required this.status,
|
||||
this.verificationStatus,
|
||||
});
|
||||
|
||||
/// Deserialises an [AttireChecklist] from the V2 API JSON response.
|
||||
factory AttireChecklist.fromJson(Map<String, dynamic> json) {
|
||||
return AttireChecklist(
|
||||
documentId: json['documentId'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String? ?? '',
|
||||
mandatory: json['mandatory'] as bool? ?? true,
|
||||
staffDocumentId: json['staffDocumentId'] as String?,
|
||||
photoUri: json['photoUri'] as String?,
|
||||
status: _parseStatus(json['status'] as String?),
|
||||
verificationStatus: json['verificationStatus'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Catalog document definition ID (UUID).
|
||||
final String documentId;
|
||||
|
||||
/// Human-readable attire item name.
|
||||
final String name;
|
||||
|
||||
/// Description of the attire requirement.
|
||||
final String description;
|
||||
|
||||
/// Whether this attire item is mandatory.
|
||||
final bool mandatory;
|
||||
|
||||
/// Staff-specific document record ID, or null if not uploaded.
|
||||
final String? staffDocumentId;
|
||||
|
||||
/// URI to the uploaded attire photo.
|
||||
final String? photoUri;
|
||||
|
||||
/// Current status of the attire item.
|
||||
final AttireItemStatus status;
|
||||
|
||||
/// Detailed verification status string (from metadata).
|
||||
final String? verificationStatus;
|
||||
|
||||
/// Whether a photo has been uploaded.
|
||||
bool get isUploaded => staffDocumentId != null;
|
||||
|
||||
/// Serialises this [AttireChecklist] to a JSON map.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'documentId': documentId,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'mandatory': mandatory,
|
||||
'staffDocumentId': staffDocumentId,
|
||||
'photoUri': photoUri,
|
||||
'status': _statusToString(status),
|
||||
'verificationStatus': verificationStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
documentId,
|
||||
name,
|
||||
description,
|
||||
mandatory,
|
||||
staffDocumentId,
|
||||
photoUri,
|
||||
status,
|
||||
verificationStatus,
|
||||
];
|
||||
|
||||
/// Parses a status string from the API.
|
||||
static AttireItemStatus _parseStatus(String? value) {
|
||||
switch (value?.toUpperCase()) {
|
||||
case 'NOT_UPLOADED':
|
||||
return AttireItemStatus.notUploaded;
|
||||
case 'PENDING':
|
||||
return AttireItemStatus.pending;
|
||||
case 'VERIFIED':
|
||||
return AttireItemStatus.verified;
|
||||
case 'REJECTED':
|
||||
return AttireItemStatus.rejected;
|
||||
case 'EXPIRED':
|
||||
return AttireItemStatus.expired;
|
||||
default:
|
||||
return AttireItemStatus.notUploaded;
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an [AttireItemStatus] to its API string.
|
||||
static String _statusToString(AttireItemStatus status) {
|
||||
switch (status) {
|
||||
case AttireItemStatus.notUploaded:
|
||||
return 'NOT_UPLOADED';
|
||||
case AttireItemStatus.pending:
|
||||
return 'PENDING';
|
||||
case AttireItemStatus.verified:
|
||||
return 'VERIFIED';
|
||||
case AttireItemStatus.rejected:
|
||||
return 'REJECTED';
|
||||
case AttireItemStatus.expired:
|
||||
return 'EXPIRED';
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user