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:
Achintha Isuru
2026-03-16 15:59:22 -04:00
parent 641dfac73d
commit 4834266986
159 changed files with 6857 additions and 3937 deletions

View File

@@ -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 ## Standard Workflow
Follow these steps in order for every feature implementation: 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 - Create barrel file exporting the domain public API
### 4. Data Layer ### 4. Data Layer
- Create models with `fromJson`/`toJson` methods - Implement repository classes using `ApiService` with `V2ApiEndpoints` — NOT DataConnectService
- Implement repository classes using `DataConnectService` - Parse V2 API JSON responses into domain entities via `Entity.fromJson()`
- Map errors to domain `Failure` types - Map errors to domain `Failure` types
- Create barrel file for data layer - Create barrel file for data layer

View File

@@ -16,8 +16,13 @@ export 'src/routing/routing.dart';
export 'src/services/api_service/api_service.dart'; export 'src/services/api_service/api_service.dart';
export 'src/services/api_service/dio_client.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 // Core API Services
export 'src/services/api_service/core_api_services/core_api_endpoints.dart'; 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_service.dart';
export 'src/services/api_service/core_api_services/file_upload/file_upload_response.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'; export 'src/services/api_service/core_api_services/signed_url/signed_url_service.dart';

View File

@@ -13,4 +13,10 @@ class AppConfig {
static const String coreApiBaseUrl = String.fromEnvironment( static const String coreApiBaseUrl = String.fromEnvironment(
'CORE_API_BASE_URL', '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',
);
} }

View File

@@ -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]. /// Extracts [ApiResponse] from a successful [Response].
ApiResponse _handleResponse(Response<dynamic> response) { ApiResponse _handleResponse(Response<dynamic> response) {
return ApiResponse( return ApiResponse(
@@ -96,6 +115,9 @@ class ApiService implements BaseApiService {
} }
/// Extracts [ApiResponse] from a [DioException]. /// Extracts [ApiResponse] from a [DioException].
///
/// Supports both legacy error format and V2 API error envelope
/// (`{ code, message, details, requestId }`).
ApiResponse _handleError(DioException e) { ApiResponse _handleError(DioException e) {
if (e.response?.data is Map<String, dynamic>) { if (e.response?.data is Map<String, dynamic>) {
final Map<String, dynamic> body = final Map<String, dynamic> body =
@@ -106,7 +128,7 @@ class ApiService implements BaseApiService {
e.response?.statusCode?.toString() ?? e.response?.statusCode?.toString() ??
'error', 'error',
message: body['message']?.toString() ?? e.message ?? 'Error occurred', message: body['message']?.toString() ?? e.message ?? 'Error occurred',
data: body['data'], data: body['data'] ?? body['details'],
errors: _parseErrors(body['errors']), errors: _parseErrors(body['errors']),
); );
} }

View File

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

View File

@@ -1,8 +1,9 @@
import 'package:dio/dio.dart'; 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/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 /// A custom Dio client for the KROW project that includes basic configuration,
/// and an [AuthInterceptor]. /// [AuthInterceptor], and [IdempotencyInterceptor].
class DioClient extends DioMixin implements Dio { class DioClient extends DioMixin implements Dio {
DioClient([BaseOptions? baseOptions]) { DioClient([BaseOptions? baseOptions]) {
options = options =
@@ -18,10 +19,11 @@ class DioClient extends DioMixin implements Dio {
// Add interceptors // Add interceptors
interceptors.addAll(<Interceptor>[ interceptors.addAll(<Interceptor>[
AuthInterceptor(), AuthInterceptor(),
IdempotencyInterceptor(),
LogInterceptor( LogInterceptor(
requestBody: true, requestBody: true,
responseBody: true, responseBody: true,
), // Added for better debugging ),
]); ]);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -32,3 +32,4 @@ dependencies:
flutter_local_notifications: ^21.0.0 flutter_local_notifications: ^21.0.0
shared_preferences: ^2.5.4 shared_preferences: ^2.5.4
workmanager: ^0.9.0+3 workmanager: ^0.9.0+3
uuid: ^4.5.1

View File

@@ -6,6 +6,21 @@
/// Note: Repository Interfaces are now located in their respective Feature packages. /// Note: Repository Interfaces are now located in their respective Feature packages.
library; 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 // Core
export 'src/core/services/api_services/api_response.dart'; export 'src/core/services/api_services/api_response.dart';
export 'src/core/services/api_services/base_api_service.dart'; export 'src/core/services/api_services/base_api_service.dart';
@@ -22,124 +37,90 @@ export 'src/core/models/device_location.dart';
// Users & Membership // Users & Membership
export 'src/entities/users/user.dart'; export 'src/entities/users/user.dart';
export 'src/entities/users/staff.dart'; export 'src/entities/users/staff.dart';
export 'src/entities/users/membership.dart';
export 'src/entities/users/biz_member.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 // Business & Organization
export 'src/entities/business/business.dart'; export 'src/entities/business/business.dart';
export 'src/entities/business/business_setting.dart';
export 'src/entities/business/hub.dart'; export 'src/entities/business/hub.dart';
export 'src/entities/business/hub_department.dart';
export 'src/entities/business/vendor.dart'; export 'src/entities/business/vendor.dart';
export 'src/entities/business/cost_center.dart'; export 'src/entities/business/cost_center.dart';
export 'src/entities/business/vendor_role.dart';
// Events & Assignments export 'src/entities/business/hub_manager.dart';
export 'src/entities/events/event.dart'; export 'src/entities/business/team_member.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';
// Shifts // Shifts
export 'src/entities/shifts/shift.dart'; export 'src/entities/shifts/shift.dart';
export 'src/adapters/shifts/shift_adapter.dart'; export 'src/entities/shifts/today_shift.dart';
export 'src/entities/shifts/break/break.dart'; export 'src/entities/shifts/assigned_shift.dart';
export 'src/adapters/shifts/break/break_adapter.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 // Orders
export 'src/entities/orders/one_time_order.dart';
export 'src/entities/orders/one_time_order_position.dart';
export 'src/entities/orders/recurring_order.dart';
export 'src/entities/orders/recurring_order_position.dart';
export 'src/entities/orders/permanent_order.dart';
export 'src/entities/orders/permanent_order_position.dart';
export 'src/entities/orders/order_type.dart';
export 'src/entities/orders/order_item.dart'; export 'src/entities/orders/order_item.dart';
export 'src/entities/orders/reorder_data.dart'; export 'src/entities/orders/assigned_worker_summary.dart';
export 'src/entities/orders/order_preview.dart';
// Skills & Certs export 'src/entities/orders/recent_order.dart';
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';
// Financial & Payroll // Financial & Payroll
export 'src/entities/benefits/benefit.dart'; export 'src/entities/benefits/benefit.dart';
export 'src/entities/financial/invoice.dart'; export 'src/entities/financial/invoice.dart';
export 'src/entities/financial/time_card.dart'; export 'src/entities/financial/billing_account.dart';
export 'src/entities/financial/invoice_item.dart'; export 'src/entities/financial/current_bill.dart';
export 'src/entities/financial/invoice_decline.dart'; export 'src/entities/financial/savings.dart';
export 'src/entities/financial/staff_payment.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/payment_summary.dart';
export 'src/entities/financial/billing_period.dart'; export 'src/entities/financial/staff_payment.dart';
export 'src/entities/financial/bank_account/bank_account.dart'; export 'src/entities/financial/payment_chart_point.dart';
export 'src/entities/financial/bank_account/business_bank_account.dart'; export 'src/entities/financial/time_card.dart';
export 'src/entities/financial/bank_account/staff_bank_account.dart';
export 'src/adapters/financial/bank_account/bank_account_adapter.dart';
// Profile // Profile
export 'src/entities/profile/staff_document.dart'; export 'src/entities/profile/staff_personal_info.dart';
export 'src/entities/profile/document_verification_status.dart'; export 'src/entities/profile/profile_section_status.dart';
export 'src/entities/profile/staff_certificate.dart'; export 'src/entities/profile/profile_completion.dart';
export 'src/entities/profile/compliance_type.dart'; export 'src/entities/profile/profile_document.dart';
export 'src/entities/profile/staff_certificate_status.dart'; export 'src/entities/profile/certificate.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/emergency_contact.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/accessibility.dart';
export 'src/entities/profile/schedule.dart';
// Support & Config // Ratings
export 'src/entities/support/addon.dart'; export 'src/entities/ratings/staff_rating.dart';
export 'src/entities/support/tag.dart';
export 'src/entities/support/media.dart';
export 'src/entities/support/working_area.dart';
// Home // Home
export 'src/entities/home/home_dashboard_data.dart'; export 'src/entities/home/client_dashboard.dart';
export 'src/entities/home/reorder_item.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 // Clock-In & Availability
export 'src/adapters/availability/availability_adapter.dart';
export 'src/entities/clock_in/attendance_status.dart'; export 'src/entities/clock_in/attendance_status.dart';
export 'src/adapters/clock_in/clock_in_adapter.dart'; export 'src/entities/availability/availability_day.dart';
export 'src/entities/availability/availability_slot.dart'; export 'src/entities/availability/time_slot.dart';
export 'src/entities/availability/day_availability.dart';
// Coverage // Coverage
export 'src/entities/coverage_domain/coverage_shift.dart'; export 'src/entities/coverage_domain/shift_with_workers.dart';
export 'src/entities/coverage_domain/coverage_worker.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/coverage_stats.dart';
export 'src/entities/coverage_domain/core_team_member.dart';
// Adapters // Reports
export 'src/adapters/profile/emergency_contact_adapter.dart'; export 'src/entities/reports/report_summary.dart';
export 'src/adapters/profile/experience_adapter.dart'; export 'src/entities/reports/daily_ops_report.dart';
export 'src/entities/profile/experience_skill.dart'; export 'src/entities/reports/spend_data_point.dart';
export 'src/adapters/profile/bank_account_adapter.dart'; export 'src/entities/reports/coverage_report.dart';
export 'src/adapters/profile/tax_form_adapter.dart'; export 'src/entities/reports/forecast_report.dart';
export 'src/adapters/financial/payment_adapter.dart'; export 'src/entities/reports/performance_report.dart';
export 'src/entities/reports/no_show_report.dart';
// Exceptions // Exceptions
export 'src/exceptions/app_exception.dart'; 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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,4 +27,11 @@ abstract class BaseApiService {
dynamic data, dynamic data,
Map<String, dynamic>? params, Map<String, dynamic>? params,
}); });
/// Performs a DELETE request to the specified [endpoint].
Future<ApiResponse> delete(
String endpoint, {
dynamic data,
Map<String, dynamic>? params,
});
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,73 @@
import 'package:equatable/equatable.dart'; 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 { class Benefit extends Equatable {
/// Creates a [Benefit]. /// Creates a [Benefit] instance.
const Benefit({ const Benefit({
required this.benefitId,
required this.benefitType,
required this.title, required this.title,
required this.entitlementHours, required this.status,
required this.usedHours, 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; final String title;
/// The total entitlement in hours. /// Current benefit status.
final double entitlementHours; final BenefitStatus status;
/// The hours used so far. /// Hours tracked so far.
final double usedHours; final int trackedHours;
/// The hours remaining. /// Target hours to accrue.
double get remainingHours => entitlementHours - usedHours; 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 @override
List<Object?> get props => [title, entitlementHours, usedHours]; List<Object?> get props => <Object?>[
benefitId,
benefitType,
title,
status,
trackedHours,
targetHours,
];
} }

View File

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

View File

@@ -1,47 +1,111 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
/// The operating status of a [Business]. import 'package:krow_domain/src/entities/enums/business_status.dart';
enum BusinessStatus {
/// Business created but not yet approved.
pending,
/// Fully active and operational. /// A client company registered on the platform.
active,
/// Temporarily suspended (e.g. for non-payment).
suspended,
/// Permanently inactive.
inactive,
}
/// Represents a Client Company / Business.
/// ///
/// This is the top-level organizational entity in the system. /// Maps to the V2 `businesses` table.
class Business extends Equatable { class Business extends Equatable {
/// Creates a [Business] instance.
const Business({ const Business({
required this.id, required this.id,
required this.name, required this.tenantId,
required this.registrationNumber, required this.slug,
required this.businessName,
required this.status, 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; final String id;
/// Tenant this business belongs to.
final String tenantId;
/// URL-safe slug.
final String slug;
/// Display name of the business. /// Display name of the business.
final String name; final String businessName;
/// Legal registration or tax number. /// Current account status.
final String registrationNumber;
/// Current operating status.
final BusinessStatus status; final BusinessStatus status;
/// URL to the business logo. /// Primary contact name.
final String? avatar; 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 @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,
];
} }

View File

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

View File

@@ -1,22 +1,37 @@
import 'package:equatable/equatable.dart'; 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 { class CostCenter extends Equatable {
/// Creates a [CostCenter] instance.
const CostCenter({ const CostCenter({
required this.id, required this.costCenterId,
required this.name, required this.name,
this.code,
}); });
/// Unique identifier. /// Deserialises a [CostCenter] from a V2 API JSON map.
final String id; 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; final String name;
/// Optional alphanumeric code associated with this cost center. /// Serialises this [CostCenter] to a JSON map.
final String? code; Map<String, dynamic> toJson() {
return <String, dynamic>{
'costCenterId': costCenterId,
'name': name,
};
}
@override @override
List<Object?> get props => <Object?>[id, name, code]; List<Object?> get props => <Object?>[costCenterId, name];
} }

View File

@@ -1,51 +1,107 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'cost_center.dart'; /// 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.hubId,
required this.name,
this.fullAddress,
this.latitude,
this.longitude,
this.nfcTagId,
this.city,
this.state,
this.zipCode,
this.costCenterId,
this.costCenterName,
});
/// The status of a [Hub]. /// Deserialises a [Hub] from a V2 API JSON map.
enum HubStatus { factory Hub.fromJson(Map<String, dynamic> json) {
/// Fully operational. return Hub(
active, hubId: json['hubId'] as String,
name: json['name'] as String,
/// Closed or inactive. fullAddress: json['fullAddress'] as String?,
inactive, latitude: json['latitude'] != null
? double.parse(json['latitude'].toString())
/// Not yet ready for operations. : null,
underConstruction, 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?,
);
} }
/// Represents a branch location or operational unit within a [Business]. /// Unique identifier (clock_point id).
class Hub extends Equatable { final String hubId;
const Hub({
required this.id,
required this.businessId,
required this.name,
required this.address,
this.nfcTagId,
required this.status,
this.costCenter,
});
/// Unique identifier.
final String id;
/// The parent [Business]. /// Display label for the hub.
final String businessId;
/// Display name of the hub (e.g. "Downtown Branch").
final String name; final String name;
/// Physical address of this hub. /// Full street address.
final String 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; final String? nfcTagId;
/// Operational status. /// City from metadata.
final HubStatus status; final String? city;
/// Assigned cost center for this hub. /// State from metadata.
final CostCenter? costCenter; 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 @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,
];
} }

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,86 @@
import 'package:equatable/equatable.dart'; 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 { 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 id;
final String name;
/// A map of role names to hourly rates. /// Tenant this vendor belongs to.
final Map<String, double> rates; 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 @override
List<Object?> get props => <Object?>[id, name, rates]; List<Object?> get props => <Object?>[
id,
tenantId,
slug,
companyName,
status,
contactName,
contactEmail,
contactPhone,
];
} }

View File

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

View File

@@ -1,27 +1,56 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
/// Simple entity to hold attendance state import 'package:krow_domain/src/entities/enums/attendance_status_type.dart';
class AttendanceStatus extends Equatable {
/// 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({ const AttendanceStatus({
this.isCheckedIn = false,
this.checkInTime,
this.checkOutTime,
this.activeShiftId, this.activeShiftId,
this.activeApplicationId, required this.attendanceStatus,
this.clockInAt,
}); });
final bool isCheckedIn;
final DateTime? checkInTime; /// Deserialises from the V2 API JSON response.
final DateTime? checkOutTime; 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? 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 @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
isCheckedIn,
checkInTime,
checkOutTime,
activeShiftId, activeShiftId,
activeApplicationId, attendanceStatus,
clockInAt,
]; ];
} }

View File

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

View File

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

View File

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

View File

@@ -1,45 +1,69 @@
import 'package:equatable/equatable.dart'; 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 { class CoverageStats extends Equatable {
/// Creates a [CoverageStats]. /// Creates a [CoverageStats] instance.
const CoverageStats({ const CoverageStats({
required this.totalNeeded, required this.totalPositionsNeeded,
required this.totalConfirmed, required this.totalPositionsConfirmed,
required this.checkedIn, required this.totalWorkersCheckedIn,
required this.enRoute, required this.totalWorkersEnRoute,
required this.late, required this.totalWorkersLate,
required this.totalCoveragePercentage,
}); });
/// The total number of workers needed. /// Deserialises a [CoverageStats] from a V2 API JSON map.
final int totalNeeded; 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. /// Total positions that need to be filled.
final int totalConfirmed; final int totalPositionsNeeded;
/// The number of workers who have checked in. /// Total positions that have been confirmed.
final int checkedIn; final int totalPositionsConfirmed;
/// The number of workers en route. /// Workers who have checked in.
final int enRoute; final int totalWorkersCheckedIn;
/// The number of late workers. /// Workers en route (accepted but not checked in).
final int late; final int totalWorkersEnRoute;
/// Calculates the overall coverage percentage. /// Workers marked as late / no-show.
int get coveragePercent { final int totalWorkersLate;
if (totalNeeded == 0) return 0;
return ((totalConfirmed / totalNeeded) * 100).round(); /// 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 @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
totalNeeded, totalPositionsNeeded,
totalConfirmed, totalPositionsConfirmed,
checkedIn, totalWorkersCheckedIn,
enRoute, totalWorkersEnRoute,
late, totalWorkersLate,
totalCoveragePercentage,
]; ];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
/// Defines the period for billing calculations.
enum BillingPeriod {
/// Weekly billing period.
week,
/// Monthly billing period.
month,
}

View File

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

View File

@@ -1,148 +1,88 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
/// The workflow status of an [Invoice]. import 'package:krow_domain/src/entities/enums/invoice_status.dart';
enum InvoiceStatus {
/// Generated but not yet sent/finalized.
open,
/// Client has disputed a line item. /// An invoice issued to a business for services rendered.
disputed, ///
/// Returned by `GET /client/billing/invoices/*`.
class Invoice extends Equatable {
/// Creates an [Invoice] instance.
const Invoice({
required this.invoiceId,
required this.invoiceNumber,
required this.amountCents,
required this.status,
this.dueDate,
this.paymentDate,
this.vendorId,
this.vendorName,
});
/// Dispute has been handled. /// Deserialises an [Invoice] from a V2 API JSON map.
resolved, factory Invoice.fromJson(Map<String, dynamic> json) {
return Invoice(
/// Invoice accepted by client. invoiceId: json['invoiceId'] as String,
verified, invoiceNumber: json['invoiceNumber'] as String,
amountCents: (json['amountCents'] as num).toInt(),
/// Payment received. status: InvoiceStatus.fromJson(json['status'] as String?),
paid, dueDate: json['dueDate'] != null
? DateTime.parse(json['dueDate'] as String)
/// Payment reconciled in accounting. : null,
reconciled, paymentDate: json['paymentDate'] != null
? DateTime.parse(json['paymentDate'] as String)
/// Payment not received by due date. : null,
overdue, vendorId: json['vendorId'] as String?,
vendorName: json['vendorName'] as String?,
);
} }
/// Represents a bill sent to a [Business] for services rendered.
class Invoice extends Equatable {
const Invoice({
required this.id,
required this.eventId,
required this.businessId,
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 [],
});
/// Unique identifier. /// Unique identifier.
final String id; final String invoiceId;
/// 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;
/// Human-readable invoice number. /// Human-readable invoice number.
final String? invoiceNumber; final String invoiceNumber;
/// Date when the invoice was issued. /// Total amount in cents.
final DateTime? issueDate; final int amountCents;
/// Human-readable title (e.g. event name). /// Current invoice lifecycle status.
final String? title; final InvoiceStatus status;
/// Name of the client business. /// When payment is due.
final String? clientName; final DateTime? dueDate;
/// Address of the event/location. /// When the invoice was paid (history endpoint).
final String? locationAddress; final DateTime? paymentDate;
/// Number of staff worked. /// Vendor ID associated with this invoice.
final int? staffCount; final String? vendorId;
/// Total hours worked. /// Vendor company name.
final double? totalHours; final String? vendorName;
/// List of workers associated with this invoice. /// Serialises this [Invoice] to a JSON map.
final List<InvoiceWorker> workers; 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 @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
id, invoiceId,
eventId,
businessId,
status,
totalAmount,
workAmount,
addonsAmount,
invoiceNumber, invoiceNumber,
issueDate, amountCents,
title, status,
clientName, dueDate,
locationAddress, paymentDate,
staffCount, vendorId,
totalHours, vendorName,
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,
]; ];
} }

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,29 @@
import 'package:equatable/equatable.dart'; 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 { class PaymentSummary extends Equatable {
/// Creates a [PaymentSummary] instance.
const PaymentSummary({required this.totalEarningsCents});
const PaymentSummary({ /// Deserialises a [PaymentSummary] from a V2 API JSON map.
required this.weeklyEarnings, factory PaymentSummary.fromJson(Map<String, dynamic> json) {
required this.monthlyEarnings, return PaymentSummary(
required this.pendingEarnings, totalEarningsCents: (json['totalEarningsCents'] as num).toInt(),
required this.totalEarnings, );
}); }
final double weeklyEarnings;
final double monthlyEarnings; /// Total earnings in cents for the queried period.
final double pendingEarnings; final int totalEarningsCents;
final double totalEarnings;
/// Serialises this [PaymentSummary] to a JSON map.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'totalEarningsCents': totalEarningsCents,
};
}
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[totalEarningsCents];
weeklyEarnings,
monthlyEarnings,
pendingEarnings,
totalEarnings,
];
} }

View File

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

View File

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

View File

@@ -1,76 +1,88 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
/// Status of a staff payout. import 'package:krow_domain/src/entities/enums/payment_status.dart';
enum PaymentStatus {
/// Payout calculated but not processed.
pending,
/// Submitted to banking provider. /// A single payment record for a staff member.
processing, ///
/// 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.shiftName,
this.location,
this.hourlyRateCents,
this.minutesWorked,
});
/// Successfully transferred to staff. /// Deserialises a [PaymentRecord] from a V2 API JSON map.
paid, factory PaymentRecord.fromJson(Map<String, dynamic> json) {
return PaymentRecord(
/// Transfer failed. paymentId: json['paymentId'] as String,
failed, amountCents: (json['amountCents'] as num).toInt(),
date: DateTime.parse(json['date'] as String),
/// Status unknown. status: PaymentStatus.fromJson(json['status'] as String?),
unknown, 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,
);
} }
/// 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,
required this.status,
this.paidAt,
this.shiftTitle,
this.shiftLocation,
this.locationAddress,
this.hoursWorked,
this.hourlyRate,
this.workedTime,
});
/// Unique identifier. /// Unique identifier.
final String id; final String paymentId;
/// The recipient [Staff]. /// Payment amount in cents.
final String staffId; final int amountCents;
/// The [Assignment] being paid for. /// Date the payment was processed or created.
final String assignmentId; final DateTime date;
/// Amount to be paid. /// Payment processing status.
final double amount;
/// Processing status.
final PaymentStatus status; final PaymentStatus status;
/// When the payment was successfully processed. /// Title of the associated shift.
final DateTime? paidAt; final String? shiftName;
/// Title of the shift worked. /// Location/hub name.
final String? shiftTitle; final String? location;
/// Location/hub name of the shift. /// Hourly pay rate in cents.
final String? shiftLocation; final int? hourlyRateCents;
/// Address of the shift location. /// Total minutes worked for this payment.
final String? locationAddress; final int? minutesWorked;
/// Number of hours worked. /// Serialises this [PaymentRecord] to a JSON map.
final double? hoursWorked; Map<String, dynamic> toJson() {
return <String, dynamic>{
/// Hourly rate for the shift. 'paymentId': paymentId,
final double? hourlyRate; 'amountCents': amountCents,
'date': date.toIso8601String(),
/// Work session duration or status. 'status': status.toJson(),
final String? workedTime; 'shiftName': shiftName,
'location': location,
'hourlyRateCents': hourlyRateCents,
'minutesWorked': minutesWorked,
};
}
@override @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,
];
} }

View File

@@ -1,78 +1,88 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
/// Status of a time card. /// A single time-card entry for a completed shift.
enum TimeCardStatus { ///
/// Waiting for approval or payment. /// Returned by `GET /staff/profile/time-card`.
pending, class TimeCardEntry extends Equatable {
/// Approved by manager. /// Creates a [TimeCardEntry] instance.
approved, const TimeCardEntry({
/// Payment has been issued. required this.date,
paid, required this.shiftName,
/// Disputed by staff or client. this.location,
disputed; this.clockInAt,
this.clockOutAt,
required this.minutesWorked,
this.hourlyRateCents,
required this.totalPayCents,
});
/// Whether the card is approved. /// Deserialises a [TimeCardEntry] from a V2 API JSON map.
bool get isApproved => this == TimeCardStatus.approved; factory TimeCardEntry.fromJson(Map<String, dynamic> json) {
/// Whether the card is paid. return TimeCardEntry(
bool get isPaid => this == TimeCardStatus.paid; date: DateTime.parse(json['date'] as String),
/// Whether the card is disputed. shiftName: json['shiftName'] as String,
bool get isDisputed => this == TimeCardStatus.disputed; location: json['location'] as String?,
/// Whether the card is pending. clockInAt: json['clockInAt'] != null
bool get isPending => this == TimeCardStatus.pending; ? 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(),
);
} }
/// 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,
required this.date,
required this.startTime,
required this.endTime,
required this.totalHours,
required this.hourlyRate,
required this.totalPay,
required this.status,
this.location,
});
/// 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;
/// Date of the shift. /// Date of the shift.
final DateTime date; final DateTime date;
/// Actual or scheduled start time.
final String startTime; /// Title of the shift.
/// Actual or scheduled end time. final String shiftName;
final String endTime;
/// Total hours worked. /// Location/hub name.
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.
final String? location; 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 @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
id,
shiftTitle,
clientName,
date, date,
startTime, shiftName,
endTime,
totalHours,
hourlyRate,
totalPay,
status,
location, location,
clockInAt,
clockOutAt,
minutesWorked,
hourlyRateCents,
totalPayCents,
]; ];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,117 +1,137 @@
import 'package:equatable/equatable.dart'; 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, /// Returned by `GET /client/orders/view`.
/// including status and worker assignments.
class OrderItem extends Equatable { class OrderItem extends Equatable {
/// Creates an [OrderItem]. /// Creates an [OrderItem] instance.
const OrderItem({ const OrderItem({
required this.id, required this.itemId,
required this.orderId, required this.orderId,
required this.orderType, required this.orderType,
required this.title, required this.roleName,
required this.clientName,
required this.status,
required this.date, required this.date,
required this.startTime, required this.startsAt,
required this.endTime, required this.endsAt,
required this.location, required this.requiredWorkerCount,
required this.locationAddress, required this.filledCount,
required this.filled, required this.hourlyRateCents,
required this.workersNeeded, required this.totalCostCents,
required this.hourlyRate, this.locationName,
required this.eventName, required this.status,
this.hours = 0, this.workers = const <AssignedWorkerSummary>[],
this.totalValue = 0,
this.confirmedApps = const <Map<String, dynamic>>[],
this.hubManagerId,
this.hubManagerName,
}); });
/// Unique identifier of the order. /// Deserialises an [OrderItem] from a V2 API JSON map.
final String id; 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; final String orderId;
/// The type of order (e.g., ONE_TIME, PERMANENT). /// Order type (ONE_TIME, RECURRING, PERMANENT, RAPID).
final OrderType orderType; final OrderType orderType;
/// Title or name of the role. /// Name of the role.
final String title; final String roleName;
/// Name of the client company. /// Shift date.
final String clientName; final DateTime date;
/// status of the order (e.g., 'open', 'filled', 'completed'). /// Shift start time.
final String status; final DateTime startsAt;
/// Date of the shift (ISO format). /// Shift end time.
final String date; final DateTime endsAt;
/// Start time of the shift. /// Total workers required.
final String startTime; final int requiredWorkerCount;
/// End time of the shift. /// Workers currently assigned/filled.
final String endTime; final int filledCount;
/// Location name. /// Billing rate in cents per hour.
final String location; final int hourlyRateCents;
/// Full address of the location. /// Total cost in cents.
final String locationAddress; final int totalCostCents;
/// Number of workers currently filled. /// Location/hub name.
final int filled; final String? locationName;
/// Total number of workers required. /// Shift status.
final int workersNeeded; final ShiftStatus status;
/// Hourly pay rate. /// Assigned workers for this line item.
final double hourlyRate; final List<AssignedWorkerSummary> workers;
/// Total hours for the shift role. /// Serialises this [OrderItem] to a JSON map.
final double hours; Map<String, dynamic> toJson() {
return <String, dynamic>{
/// Total value for the shift role. 'itemId': itemId,
final double totalValue; 'orderId': orderId,
'orderType': orderType.toJson(),
/// Name of the event. 'roleName': roleName,
final String eventName; 'date': date.toIso8601String(),
'startsAt': startsAt.toIso8601String(),
/// List of confirmed worker applications. 'endsAt': endsAt.toIso8601String(),
final List<Map<String, dynamic>> confirmedApps; 'requiredWorkerCount': requiredWorkerCount,
'filledCount': filledCount,
/// Optional ID of the assigned hub manager. 'hourlyRateCents': hourlyRateCents,
final String? hubManagerId; 'totalCostCents': totalCostCents,
'locationName': locationName,
/// Optional Name of the assigned hub manager. 'status': status.toJson(),
final String? hubManagerName; 'workers': workers.map((AssignedWorkerSummary w) => w.toJson()).toList(),
};
}
@override @override
List<Object?> get props => <Object?>[ List<Object?> get props => <Object?>[
id, itemId,
orderId, orderId,
orderType, orderType,
title, roleName,
clientName,
status,
date, date,
startTime, startsAt,
endTime, endsAt,
location, requiredWorkerCount,
locationAddress, filledCount,
filled, hourlyRateCents,
workersNeeded, totalCostCents,
hourlyRate, locationName,
hours, status,
totalValue, workers,
eventName,
confirmedApps,
hubManagerId,
hubManagerName,
]; ];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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