chore: fix 273+ analysis issues and repair corrupted core files
This commit is contained in:
@@ -3,8 +3,6 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:krow_core/src/services/auth/auth_token_provider.dart';
|
||||
import 'package:krow_core/src/services/auth/firebase_auth_service.dart';
|
||||
import 'package:krow_core/src/services/auth/firebase_auth_token_provider.dart';
|
||||
|
||||
import '../core.dart';
|
||||
|
||||
@@ -1,118 +1,50 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
||||
import 'dart:developer' as developer;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Global BLoC observer for centralized logging and monitoring.
|
||||
///
|
||||
/// This observer provides visibility into all BLoC lifecycle events across
|
||||
/// the entire application, enabling centralized logging, debugging, and
|
||||
/// error monitoring.
|
||||
///
|
||||
/// **Features:**
|
||||
/// - Logs BLoC creation and disposal
|
||||
/// - Logs all events and state changes
|
||||
/// - Captures and logs errors with stack traces
|
||||
/// - Ready for integration with monitoring services (Sentry, Firebase Crashlytics)
|
||||
///
|
||||
/// **Setup:**
|
||||
/// Register this observer in your app's main.dart before runApp():
|
||||
/// ```dart
|
||||
/// void main() {
|
||||
/// Bloc.observer = CoreBlocObserver();
|
||||
/// runApp(MyApp());
|
||||
/// }
|
||||
/// ```
|
||||
/// A BLoC observer that logs state changes and optionally events.
|
||||
class CoreBlocObserver extends BlocObserver {
|
||||
|
||||
CoreBlocObserver({
|
||||
this.logStateChanges = false,
|
||||
this.logEvents = true,
|
||||
/// Creates a [CoreBlocObserver].
|
||||
const CoreBlocObserver({
|
||||
this.logEvents = false,
|
||||
this.logStateChanges = true,
|
||||
});
|
||||
/// Whether to log state changes (can be verbose in production)
|
||||
final bool logStateChanges;
|
||||
|
||||
/// Whether to log events
|
||||
/// Whether to log individual BLoC events.
|
||||
final bool logEvents;
|
||||
|
||||
@override
|
||||
void onCreate(BlocBase bloc) {
|
||||
super.onCreate(bloc);
|
||||
developer.log(
|
||||
'Created: ${bloc.runtimeType}',
|
||||
name: 'BlocObserver',
|
||||
);
|
||||
}
|
||||
/// Whether to log BLoC state transitions.
|
||||
final bool logStateChanges;
|
||||
|
||||
@override
|
||||
void onEvent(Bloc bloc, Object? event) {
|
||||
void onEvent(Bloc<dynamic, dynamic> bloc, Object? event) {
|
||||
super.onEvent(bloc, event);
|
||||
if (logEvents) {
|
||||
developer.log(
|
||||
'Event: ${event.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
'onEvent -- ${bloc.runtimeType}: $event',
|
||||
name: 'BLOC_EVENT',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onChange(BlocBase bloc, Change change) {
|
||||
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
|
||||
super.onChange(bloc, change);
|
||||
if (logStateChanges) {
|
||||
developer.log(
|
||||
'State: ${change.currentState.runtimeType}’ ${change.nextState.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
'onChange -- ${bloc.runtimeType}: ${change.currentState} -> ${change.nextState}',
|
||||
name: 'BLOC_STATE',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
|
||||
super.onError(bloc, error, stackTrace);
|
||||
|
||||
// Log error to console
|
||||
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
|
||||
developer.log(
|
||||
'ERROR in ${bloc.runtimeType}',
|
||||
name: 'BlocObserver',
|
||||
'onError -- ${bloc.runtimeType}: $error',
|
||||
name: 'BLOC_ERROR',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
// TODO: Send to monitoring service
|
||||
// Example integrations:
|
||||
//
|
||||
// Sentry:
|
||||
// Sentry.captureException(
|
||||
// error,
|
||||
// stackTrace: stackTrace,
|
||||
// hint: Hint.withMap({'bloc': bloc.runtimeType.toString()}),
|
||||
// );
|
||||
//
|
||||
// Firebase Crashlytics:
|
||||
// FirebaseCrashlytics.instance.recordError(
|
||||
// error,
|
||||
// stackTrace,
|
||||
// reason: 'BLoC Error in ${bloc.runtimeType}',
|
||||
// );
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose(BlocBase bloc) {
|
||||
super.onClose(bloc);
|
||||
developer.log(
|
||||
'Closed: ${bloc.runtimeType}',
|
||||
name: 'BlocObserver',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTransition(Bloc bloc, Transition transition) {
|
||||
super.onTransition(bloc, transition);
|
||||
if (logStateChanges) {
|
||||
developer.log(
|
||||
'Transition: ${transition.event.runtimeType}’ ${transition.nextState.runtimeType}',
|
||||
name: bloc.runtimeType.toString(),
|
||||
);
|
||||
}
|
||||
super.onError(bloc, error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
||||
Container(
|
||||
height: 2,
|
||||
width: 40,
|
||||
color: UiColors.white.withOpacity(0.3),
|
||||
color: UiColors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -125,7 +125,7 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.6),
|
||||
color: UiColors.black.withValues(alpha: 0.6),
|
||||
blurRadius: 40,
|
||||
spreadRadius: 10,
|
||||
),
|
||||
@@ -241,12 +241,12 @@ class _WebFrameContentState extends State<_WebFrameContent> {
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.mutedForeground.withOpacity(0.3),
|
||||
color: UiColors.mutedForeground.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: UiColors.white.withOpacity(0.7), width: 2),
|
||||
border: Border.all(color: UiColors.white.withValues(alpha: 0.7), width: 2),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.2),
|
||||
color: UiColors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/billing_connector_repository.dart';
|
||||
|
||||
/// Implementation of [BillingConnectorRepository].
|
||||
class BillingConnectorRepositoryImpl implements BillingConnectorRepository {
|
||||
BillingConnectorRepositoryImpl({dc.DataConnectService? service})
|
||||
: _service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@override
|
||||
Future<List<BusinessBankAccount>> getBankAccounts({
|
||||
required String businessId,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<
|
||||
dc.GetAccountsByOwnerIdData,
|
||||
dc.GetAccountsByOwnerIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.getAccountsByOwnerId(ownerId: businessId)
|
||||
.execute();
|
||||
|
||||
return result.data.accounts.map(_mapBankAccount).toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> getCurrentBillAmount({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<
|
||||
dc.ListInvoicesByBusinessIdData,
|
||||
dc.ListInvoicesByBusinessIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listInvoicesByBusinessId(businessId: businessId)
|
||||
.execute();
|
||||
|
||||
return result.data.invoices
|
||||
.map(_mapInvoice)
|
||||
.where((Invoice i) => i.status == InvoiceStatus.open)
|
||||
.fold<double>(
|
||||
0.0,
|
||||
(double sum, Invoice item) => sum + item.totalAmount,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Invoice>> getInvoiceHistory({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<
|
||||
dc.ListInvoicesByBusinessIdData,
|
||||
dc.ListInvoicesByBusinessIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listInvoicesByBusinessId(businessId: businessId)
|
||||
.limit(20)
|
||||
.execute();
|
||||
|
||||
return result.data.invoices
|
||||
.map(_mapInvoice)
|
||||
.where((Invoice i) => i.status == InvoiceStatus.paid)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Invoice>> getPendingInvoices({required String businessId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<
|
||||
dc.ListInvoicesByBusinessIdData,
|
||||
dc.ListInvoicesByBusinessIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listInvoicesByBusinessId(businessId: businessId)
|
||||
.execute();
|
||||
|
||||
return result.data.invoices
|
||||
.map(_mapInvoice)
|
||||
.where(
|
||||
(Invoice i) =>
|
||||
i.status != InvoiceStatus.paid &&
|
||||
i.status != InvoiceStatus.disputed &&
|
||||
i.status != InvoiceStatus.open,
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<InvoiceItem>> getSpendingBreakdown({
|
||||
required String businessId,
|
||||
required BillingPeriod period,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime start;
|
||||
final DateTime end;
|
||||
|
||||
if (period == BillingPeriod.week) {
|
||||
final int daysFromMonday = now.weekday - DateTime.monday;
|
||||
final DateTime monday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
).subtract(Duration(days: daysFromMonday));
|
||||
start = monday;
|
||||
end = monday.add(
|
||||
const Duration(days: 6, hours: 23, minutes: 59, seconds: 59),
|
||||
);
|
||||
} else {
|
||||
start = DateTime(now.year, now.month, 1);
|
||||
end = DateTime(now.year, now.month + 1, 0, 23, 59, 59);
|
||||
}
|
||||
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndDatesSummaryData,
|
||||
dc.ListShiftRolesByBusinessAndDatesSummaryVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listShiftRolesByBusinessAndDatesSummary(
|
||||
businessId: businessId,
|
||||
start: _service.toTimestamp(start),
|
||||
end: _service.toTimestamp(end),
|
||||
)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles>
|
||||
shiftRoles = result.data.shiftRoles;
|
||||
if (shiftRoles.isEmpty) {
|
||||
return <InvoiceItem>[];
|
||||
}
|
||||
|
||||
final Map<String, _RoleSummary> summary = <String, _RoleSummary>{};
|
||||
for (final dc.ListShiftRolesByBusinessAndDatesSummaryShiftRoles role
|
||||
in shiftRoles) {
|
||||
final String roleId = role.roleId;
|
||||
final String roleName = role.role.name;
|
||||
final double hours = role.hours ?? 0.0;
|
||||
final double totalValue = role.totalValue ?? 0.0;
|
||||
|
||||
final _RoleSummary? existing = summary[roleId];
|
||||
if (existing == null) {
|
||||
summary[roleId] = _RoleSummary(
|
||||
roleId: roleId,
|
||||
roleName: roleName,
|
||||
totalHours: hours,
|
||||
totalValue: totalValue,
|
||||
);
|
||||
} else {
|
||||
summary[roleId] = existing.copyWith(
|
||||
totalHours: existing.totalHours + hours,
|
||||
totalValue: existing.totalValue + totalValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return summary.values
|
||||
.map(
|
||||
(_RoleSummary item) => InvoiceItem(
|
||||
id: item.roleId,
|
||||
invoiceId: item.roleId,
|
||||
staffId: item.roleName,
|
||||
workHours: item.totalHours,
|
||||
rate: item.totalHours > 0 ? item.totalValue / item.totalHours : 0,
|
||||
amount: item.totalValue,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> approveInvoice({required String id}) async {
|
||||
return _service.run(() async {
|
||||
await _service.connector
|
||||
.updateInvoice(id: id)
|
||||
.status(dc.InvoiceStatus.APPROVED)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disputeInvoice({
|
||||
required String id,
|
||||
required String reason,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
await _service.connector
|
||||
.updateInvoice(id: id)
|
||||
.status(dc.InvoiceStatus.DISPUTED)
|
||||
.disputeReason(reason)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
// --- MAPPERS ---
|
||||
|
||||
Invoice _mapInvoice(dynamic invoice) {
|
||||
List<InvoiceWorker> workers = <InvoiceWorker>[];
|
||||
|
||||
// Try to get workers from denormalized 'roles' field first
|
||||
final List<dynamic> rolesData = invoice.roles is List
|
||||
? invoice.roles
|
||||
: <dynamic>[];
|
||||
if (rolesData.isNotEmpty) {
|
||||
workers = rolesData.map((dynamic r) {
|
||||
final Map<String, dynamic> role = r as Map<String, dynamic>;
|
||||
|
||||
// Handle various possible key naming conventions in the JSON data
|
||||
final String name =
|
||||
role['name'] ?? role['staffName'] ?? role['fullName'] ?? 'Unknown';
|
||||
final String roleTitle =
|
||||
role['role'] ?? role['roleName'] ?? role['title'] ?? 'Staff';
|
||||
final double amount =
|
||||
(role['amount'] as num?)?.toDouble() ??
|
||||
(role['totalValue'] as num?)?.toDouble() ??
|
||||
0.0;
|
||||
final double hours =
|
||||
(role['hours'] as num?)?.toDouble() ??
|
||||
(role['workHours'] as num?)?.toDouble() ??
|
||||
(role['totalHours'] as num?)?.toDouble() ??
|
||||
0.0;
|
||||
final double rate =
|
||||
(role['rate'] as num?)?.toDouble() ??
|
||||
(role['hourlyRate'] as num?)?.toDouble() ??
|
||||
0.0;
|
||||
|
||||
final dynamic checkInVal =
|
||||
role['checkInTime'] ?? role['startTime'] ?? role['check_in_time'];
|
||||
final dynamic checkOutVal =
|
||||
role['checkOutTime'] ?? role['endTime'] ?? role['check_out_time'];
|
||||
|
||||
return InvoiceWorker(
|
||||
name: name,
|
||||
role: roleTitle,
|
||||
amount: amount,
|
||||
hours: hours,
|
||||
rate: rate,
|
||||
checkIn: _service.toDateTime(checkInVal),
|
||||
checkOut: _service.toDateTime(checkOutVal),
|
||||
breakMinutes: role['breakMinutes'] ?? role['break_minutes'] ?? 0,
|
||||
avatarUrl:
|
||||
role['avatarUrl'] ?? role['photoUrl'] ?? role['staffPhoto'],
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
// Fallback: If roles is empty, try to get workers from shift applications.
|
||||
// Only when the invoice type has a 'shift' field (e.g. getInvoiceById); listInvoicesByBusinessId
|
||||
// generated type has shiftId but no shift getter, so we guard with try/catch.
|
||||
else {
|
||||
try {
|
||||
final dynamic shift = (invoice as dynamic).shift;
|
||||
if (shift != null && shift.applications_on_shift != null) {
|
||||
final List<dynamic> apps = shift.applications_on_shift;
|
||||
workers = apps.map((dynamic app) {
|
||||
final String name = app.staff?.fullName ?? 'Unknown';
|
||||
final String roleTitle = app.shiftRole?.role?.name ?? 'Staff';
|
||||
final double amount =
|
||||
(app.shiftRole?.totalValue as num?)?.toDouble() ?? 0.0;
|
||||
final double hours = (app.shiftRole?.hours as num?)?.toDouble() ?? 0.0;
|
||||
|
||||
// Calculate rate if not explicitly provided
|
||||
double rate = 0.0;
|
||||
if (hours > 0) {
|
||||
rate = amount / hours;
|
||||
}
|
||||
|
||||
// Map break type to minutes
|
||||
int breakMin = 0;
|
||||
final String? breakType = app.shiftRole?.breakType?.toString();
|
||||
if (breakType != null) {
|
||||
if (breakType.contains('10')) {
|
||||
breakMin = 10;
|
||||
} else if (breakType.contains('15')) {
|
||||
breakMin = 15;
|
||||
} else if (breakType.contains('30')) {
|
||||
breakMin = 30;
|
||||
} else if (breakType.contains('45')) {
|
||||
breakMin = 45;
|
||||
} else if (breakType.contains('60')) {
|
||||
breakMin = 60;
|
||||
}
|
||||
}
|
||||
|
||||
return InvoiceWorker(
|
||||
name: name,
|
||||
role: roleTitle,
|
||||
amount: amount,
|
||||
hours: hours,
|
||||
rate: rate,
|
||||
checkIn: _service.toDateTime(app.checkInTime),
|
||||
checkOut: _service.toDateTime(app.checkOutTime),
|
||||
breakMinutes: breakMin,
|
||||
avatarUrl: app.staff?.photoUrl,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
} catch (_) {
|
||||
// Invoice type has no 'shift' getter (e.g. ListInvoicesByBusinessIdInvoices). Skip.
|
||||
}
|
||||
}
|
||||
|
||||
return Invoice(
|
||||
id: invoice.id,
|
||||
eventId: invoice.orderId,
|
||||
businessId: invoice.businessId,
|
||||
status: _mapInvoiceStatus(invoice.status.stringValue),
|
||||
totalAmount: invoice.amount,
|
||||
workAmount: invoice.amount,
|
||||
addonsAmount: invoice.otherCharges ?? 0,
|
||||
invoiceNumber: invoice.invoiceNumber,
|
||||
issueDate: _service.toDateTime(invoice.issueDate)!,
|
||||
title: invoice.order?.eventName,
|
||||
clientName: invoice.business?.businessName,
|
||||
locationAddress:
|
||||
invoice.order?.teamHub?.hubName ?? invoice.order?.teamHub?.address,
|
||||
staffCount:
|
||||
invoice.staffCount ?? (workers.isNotEmpty ? workers.length : 0),
|
||||
totalHours: _calculateTotalHours(rolesData),
|
||||
workers: workers,
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateTotalHours(List<dynamic> roles) {
|
||||
return roles.fold<double>(0.0, (double sum, dynamic role) {
|
||||
final dynamic hours = role['hours'] ?? role['workHours'] ?? role['totalHours'];
|
||||
if (hours is num) {
|
||||
return sum + hours.toDouble();
|
||||
}
|
||||
return sum;
|
||||
});
|
||||
}
|
||||
|
||||
BusinessBankAccount _mapBankAccount(dynamic account) {
|
||||
return BusinessBankAccountAdapter.fromPrimitives(
|
||||
id: account.id,
|
||||
bank: account.bank,
|
||||
last4: account.last4,
|
||||
isPrimary: account.isPrimary ?? false,
|
||||
expiryTime: _service.toDateTime(account.expiryTime),
|
||||
);
|
||||
}
|
||||
|
||||
InvoiceStatus _mapInvoiceStatus(String status) {
|
||||
switch (status) {
|
||||
case 'PAID':
|
||||
return InvoiceStatus.paid;
|
||||
case 'OVERDUE':
|
||||
return InvoiceStatus.overdue;
|
||||
case 'DISPUTED':
|
||||
return InvoiceStatus.disputed;
|
||||
case 'APPROVED':
|
||||
return InvoiceStatus.verified;
|
||||
default:
|
||||
return InvoiceStatus.open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _RoleSummary {
|
||||
const _RoleSummary({
|
||||
required this.roleId,
|
||||
required this.roleName,
|
||||
required this.totalHours,
|
||||
required this.totalValue,
|
||||
});
|
||||
|
||||
final String roleId;
|
||||
final String roleName;
|
||||
final double totalHours;
|
||||
final double totalValue;
|
||||
|
||||
_RoleSummary copyWith({double? totalHours, double? totalValue}) {
|
||||
return _RoleSummary(
|
||||
roleId: roleId,
|
||||
roleName: roleName,
|
||||
totalHours: totalHours ?? this.totalHours,
|
||||
totalValue: totalValue ?? this.totalValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,801 +0,0 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/shifts_connector_repository.dart';
|
||||
|
||||
/// Implementation of [ShiftsConnectorRepository].
|
||||
///
|
||||
/// Handles shift-related data operations by interacting with Data Connect.
|
||||
class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
||||
/// Creates a new [ShiftsConnectorRepositoryImpl].
|
||||
ShiftsConnectorRepositoryImpl({dc.DataConnectService? service})
|
||||
: _service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getMyShifts({
|
||||
required String staffId,
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final dc.GetApplicationsByStaffIdVariablesBuilder query = _service
|
||||
.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(_service.toTimestamp(start))
|
||||
.dayEnd(_service.toTimestamp(end));
|
||||
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
response = await query.execute();
|
||||
return _mapApplicationsToShifts(response.data.applications);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getAvailableShifts({
|
||||
required String staffId,
|
||||
String? query,
|
||||
String? type,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
// First, fetch all available shift roles for the vendor/business
|
||||
// Use the session owner ID (vendorId)
|
||||
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||
if (vendorId == null || vendorId.isEmpty) return <Shift>[];
|
||||
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByVendorIdData,
|
||||
dc.ListShiftRolesByVendorIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.listShiftRolesByVendorId(vendorId: vendorId)
|
||||
.execute();
|
||||
|
||||
final List<dc.ListShiftRolesByVendorIdShiftRoles> allShiftRoles =
|
||||
response.data.shiftRoles;
|
||||
|
||||
// Fetch current applications to filter out already booked shifts
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
myAppsResponse = await _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
final Set<String> appliedShiftIds = myAppsResponse.data.applications
|
||||
.map((dc.GetApplicationsByStaffIdApplications a) => a.shiftId)
|
||||
.toSet();
|
||||
|
||||
final List<Shift> mappedShifts = <Shift>[];
|
||||
for (final dc.ListShiftRolesByVendorIdShiftRoles sr in allShiftRoles) {
|
||||
if (appliedShiftIds.contains(sr.shiftId)) continue;
|
||||
|
||||
final DateTime? shiftDate = _service.toDateTime(sr.shift.date);
|
||||
final DateTime? startDt = _service.toDateTime(sr.startTime);
|
||||
final DateTime? endDt = _service.toDateTime(sr.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
|
||||
|
||||
// Normalise orderType to uppercase for consistent checks in the UI.
|
||||
// RECURRING → groups shifts into Multi-Day cards.
|
||||
// PERMANENT → groups shifts into Long Term cards.
|
||||
final String orderTypeStr = sr.shift.order.orderType.stringValue
|
||||
.toUpperCase();
|
||||
|
||||
final dc.ListShiftRolesByVendorIdShiftRolesShiftOrder order =
|
||||
sr.shift.order;
|
||||
final DateTime? startDate = _service.toDateTime(order.startDate);
|
||||
final DateTime? endDate = _service.toDateTime(order.endDate);
|
||||
|
||||
final String startTime = startDt != null
|
||||
? DateFormat('HH:mm').format(startDt)
|
||||
: '';
|
||||
final String endTime = endDt != null
|
||||
? DateFormat('HH:mm').format(endDt)
|
||||
: '';
|
||||
|
||||
final List<ShiftSchedule>? schedules = _generateSchedules(
|
||||
orderType: orderTypeStr,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
recurringDays: order.recurringDays,
|
||||
permanentDays: order.permanentDays,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
);
|
||||
|
||||
final String title = sr.role.name;
|
||||
|
||||
mappedShifts.add(
|
||||
Shift(
|
||||
id: sr.shiftId,
|
||||
roleId: sr.roleId,
|
||||
title: title,
|
||||
clientName: sr.shift.order.business.businessName,
|
||||
logoUrl: null,
|
||||
hourlyRate: sr.role.costPerHour,
|
||||
location: sr.shift.location ?? '',
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
||||
description: sr.shift.description,
|
||||
durationDays: sr.shift.durationDays ?? schedules?.length,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
latitude: sr.shift.latitude,
|
||||
longitude: sr.shift.longitude,
|
||||
// orderId + orderType power the grouping and type-badge logic in
|
||||
// FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType.
|
||||
orderId: sr.shift.orderId,
|
||||
orderType: orderTypeStr,
|
||||
startDate: startDate?.toIso8601String(),
|
||||
endDate: endDate?.toIso8601String(),
|
||||
recurringDays: sr.shift.order.recurringDays,
|
||||
permanentDays: sr.shift.order.permanentDays,
|
||||
schedules: schedules,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: sr.isBreakPaid ?? false,
|
||||
breakTime: sr.breakType?.stringValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (query != null && query.isNotEmpty) {
|
||||
final String lowerQuery = query.toLowerCase();
|
||||
return mappedShifts.where((Shift s) {
|
||||
return s.title.toLowerCase().contains(lowerQuery) ||
|
||||
s.clientName.toLowerCase().contains(lowerQuery);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return mappedShifts;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getPendingAssignments({required String staffId}) async {
|
||||
return _service.run(() async {
|
||||
// Current schema doesn't have a specific "pending assignment" query that differs from confirmed
|
||||
// unless we filter by status. In the old repo it was returning an empty list.
|
||||
return <Shift>[];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Shift?> getShiftDetails({
|
||||
required String shiftId,
|
||||
required String staffId,
|
||||
String? roleId,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
if (roleId != null && roleId.isNotEmpty) {
|
||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
||||
roleResult = await _service.connector
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
|
||||
.execute();
|
||||
final dc.GetShiftRoleByIdShiftRole? sr = roleResult.data.shiftRole;
|
||||
if (sr == null) return null;
|
||||
|
||||
final DateTime? startDt = _service.toDateTime(sr.startTime);
|
||||
final DateTime? endDt = _service.toDateTime(sr.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(sr.createdAt);
|
||||
|
||||
bool hasApplied = false;
|
||||
String status = 'open';
|
||||
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
appsResponse = await _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
|
||||
.data
|
||||
.applications
|
||||
.where(
|
||||
(dc.GetApplicationsByStaffIdApplications a) =>
|
||||
a.shiftId == shiftId && a.shiftRole.roleId == roleId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (app != null) {
|
||||
hasApplied = true;
|
||||
final String s = app.status.stringValue;
|
||||
status = _mapApplicationStatus(s);
|
||||
}
|
||||
|
||||
final String title = sr.role.name;
|
||||
|
||||
return Shift(
|
||||
id: sr.shiftId,
|
||||
roleId: sr.roleId,
|
||||
title: title,
|
||||
clientName: sr.shift.order.business.businessName,
|
||||
logoUrl: sr.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: sr.role.costPerHour,
|
||||
location: sr.shift.location ?? sr.shift.order.teamHub.hubName,
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: startDt?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: status,
|
||||
description: sr.shift.description,
|
||||
durationDays: null,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
hasApplied: hasApplied,
|
||||
totalValue: sr.totalValue,
|
||||
latitude: sr.shift.latitude,
|
||||
longitude: sr.shift.longitude,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: sr.isBreakPaid ?? false,
|
||||
breakTime: sr.breakType?.stringValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result =
|
||||
await _service.connector.getShiftById(id: shiftId).execute();
|
||||
final dc.GetShiftByIdShift? s = result.data.shift;
|
||||
if (s == null) return null;
|
||||
|
||||
int? required;
|
||||
int? filled;
|
||||
Break? breakInfo;
|
||||
|
||||
try {
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByShiftIdData,
|
||||
dc.ListShiftRolesByShiftIdVariables
|
||||
>
|
||||
rolesRes = await _service.connector
|
||||
.listShiftRolesByShiftId(shiftId: shiftId)
|
||||
.execute();
|
||||
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
||||
required = 0;
|
||||
filled = 0;
|
||||
for (dc.ListShiftRolesByShiftIdShiftRoles r
|
||||
in rolesRes.data.shiftRoles) {
|
||||
required = (required ?? 0) + r.count;
|
||||
filled = (filled ?? 0) + (r.assigned ?? 0);
|
||||
}
|
||||
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
|
||||
rolesRes.data.shiftRoles.first;
|
||||
breakInfo = BreakAdapter.fromData(
|
||||
isPaid: firstRole.isBreakPaid ?? false,
|
||||
breakTime: firstRole.breakType?.stringValue,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
final DateTime? startDt = _service.toDateTime(s.startTime);
|
||||
final DateTime? endDt = _service.toDateTime(s.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(s.createdAt);
|
||||
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
clientName: s.order.business.businessName,
|
||||
logoUrl: null,
|
||||
hourlyRate: s.cost ?? 0.0,
|
||||
location: s.location ?? '',
|
||||
locationAddress: s.locationAddress ?? '',
|
||||
date: startDt?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: s.status?.stringValue ?? 'OPEN',
|
||||
description: s.description,
|
||||
durationDays: s.durationDays,
|
||||
requiredSlots: required,
|
||||
filledSlots: filled,
|
||||
latitude: s.latitude,
|
||||
longitude: s.longitude,
|
||||
breakInfo: breakInfo,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> applyForShift({
|
||||
required String shiftId,
|
||||
required String staffId,
|
||||
bool isInstantBook = false,
|
||||
String? roleId,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final String targetRoleId = roleId ?? '';
|
||||
if (targetRoleId.isEmpty) throw Exception('Missing role id.');
|
||||
|
||||
// 1. Fetch the initial shift to determine order type
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables>
|
||||
shiftResult = await _service.connector
|
||||
.getShiftById(id: shiftId)
|
||||
.execute();
|
||||
final dc.GetShiftByIdShift? initialShift = shiftResult.data.shift;
|
||||
if (initialShift == null) throw Exception('Shift not found');
|
||||
|
||||
final dc.EnumValue<dc.OrderType> orderTypeEnum =
|
||||
initialShift.order.orderType;
|
||||
final bool isMultiDay =
|
||||
orderTypeEnum is dc.Known<dc.OrderType> &&
|
||||
(orderTypeEnum.value == dc.OrderType.RECURRING ||
|
||||
orderTypeEnum.value == dc.OrderType.PERMANENT);
|
||||
final List<_TargetShiftRole> targets = [];
|
||||
|
||||
if (isMultiDay) {
|
||||
// 2. Fetch all shifts for this order to apply to all of them for the same role
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByBusinessAndOrderData,
|
||||
dc.ListShiftRolesByBusinessAndOrderVariables
|
||||
>
|
||||
allRolesRes = await _service.connector
|
||||
.listShiftRolesByBusinessAndOrder(
|
||||
businessId: initialShift.order.businessId,
|
||||
orderId: initialShift.orderId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
for (final role in allRolesRes.data.shiftRoles) {
|
||||
if (role.roleId == targetRoleId) {
|
||||
targets.add(
|
||||
_TargetShiftRole(
|
||||
shiftId: role.shiftId,
|
||||
roleId: role.roleId,
|
||||
count: role.count,
|
||||
assigned: role.assigned ?? 0,
|
||||
shiftFilled: role.shift.filled ?? 0,
|
||||
date: _service.toDateTime(role.shift.date),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single shift application
|
||||
final QueryResult<dc.GetShiftRoleByIdData, dc.GetShiftRoleByIdVariables>
|
||||
roleResult = await _service.connector
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
|
||||
.execute();
|
||||
final dc.GetShiftRoleByIdShiftRole? role = roleResult.data.shiftRole;
|
||||
if (role == null) throw Exception('Shift role not found');
|
||||
|
||||
targets.add(
|
||||
_TargetShiftRole(
|
||||
shiftId: shiftId,
|
||||
roleId: targetRoleId,
|
||||
count: role.count,
|
||||
assigned: role.assigned ?? 0,
|
||||
shiftFilled: initialShift.filled ?? 0,
|
||||
date: _service.toDateTime(initialShift.date),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (targets.isEmpty) {
|
||||
throw Exception('No valid shifts found to apply for.');
|
||||
}
|
||||
|
||||
int appliedCount = 0;
|
||||
final List<String> errors = [];
|
||||
|
||||
for (final target in targets) {
|
||||
try {
|
||||
await _applyToSingleShiftRole(target: target, staffId: staffId);
|
||||
appliedCount++;
|
||||
} catch (e) {
|
||||
// For multi-shift apply, we might want to continue even if some fail due to conflicts
|
||||
if (targets.length == 1) rethrow;
|
||||
errors.add('Shift on ${target.date}: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedCount == 0 && targets.length > 1) {
|
||||
throw Exception('Failed to apply for any shifts: ${errors.join(", ")}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _applyToSingleShiftRole({
|
||||
required _TargetShiftRole target,
|
||||
required String staffId,
|
||||
}) async {
|
||||
// Validate daily limit
|
||||
if (target.date != null) {
|
||||
final DateTime dayStartUtc = DateTime.utc(
|
||||
target.date!.year,
|
||||
target.date!.month,
|
||||
target.date!.day,
|
||||
);
|
||||
final DateTime dayEndUtc = dayStartUtc
|
||||
.add(const Duration(days: 1))
|
||||
.subtract(const Duration(microseconds: 1));
|
||||
|
||||
final QueryResult<
|
||||
dc.VaidateDayStaffApplicationData,
|
||||
dc.VaidateDayStaffApplicationVariables
|
||||
>
|
||||
validationResponse = await _service.connector
|
||||
.vaidateDayStaffApplication(staffId: staffId)
|
||||
.dayStart(_service.toTimestamp(dayStartUtc))
|
||||
.dayEnd(_service.toTimestamp(dayEndUtc))
|
||||
.execute();
|
||||
|
||||
// if (validationResponse.data.applications.isNotEmpty) {
|
||||
// throw Exception('The user already has a shift that day.');
|
||||
// }
|
||||
}
|
||||
|
||||
// Check for existing application
|
||||
final QueryResult<
|
||||
dc.GetApplicationByStaffShiftAndRoleData,
|
||||
dc.GetApplicationByStaffShiftAndRoleVariables
|
||||
>
|
||||
existingAppRes = await _service.connector
|
||||
.getApplicationByStaffShiftAndRole(
|
||||
staffId: staffId,
|
||||
shiftId: target.shiftId,
|
||||
roleId: target.roleId,
|
||||
)
|
||||
.execute();
|
||||
|
||||
if (existingAppRes.data.applications.isNotEmpty) {
|
||||
throw Exception('Application already exists.');
|
||||
}
|
||||
|
||||
if (target.assigned >= target.count) {
|
||||
throw Exception('This shift is full.');
|
||||
}
|
||||
|
||||
String? createdAppId;
|
||||
try {
|
||||
final OperationResult<
|
||||
dc.CreateApplicationData,
|
||||
dc.CreateApplicationVariables
|
||||
>
|
||||
createRes = await _service.connector
|
||||
.createApplication(
|
||||
shiftId: target.shiftId,
|
||||
staffId: staffId,
|
||||
roleId: target.roleId,
|
||||
status: dc.ApplicationStatus.CONFIRMED,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
)
|
||||
.execute();
|
||||
|
||||
createdAppId = createRes.data.application_insert.id;
|
||||
|
||||
await _service.connector
|
||||
.updateShiftRole(shiftId: target.shiftId, roleId: target.roleId)
|
||||
.assigned(target.assigned + 1)
|
||||
.execute();
|
||||
|
||||
await _service.connector
|
||||
.updateShift(id: target.shiftId)
|
||||
.filled(target.shiftFilled + 1)
|
||||
.execute();
|
||||
} catch (e) {
|
||||
// Simple rollback attempt (not guaranteed)
|
||||
if (createdAppId != null) {
|
||||
await _service.connector.deleteApplication(id: createdAppId).execute();
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> acceptShift({required String shiftId, required String staffId}) {
|
||||
return _updateApplicationStatus(
|
||||
shiftId,
|
||||
staffId,
|
||||
dc.ApplicationStatus.CONFIRMED,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> declineShift({
|
||||
required String shiftId,
|
||||
required String staffId,
|
||||
}) {
|
||||
return _updateApplicationStatus(
|
||||
shiftId,
|
||||
staffId,
|
||||
dc.ApplicationStatus.REJECTED,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getCancelledShifts({required String staffId}) async {
|
||||
return _service.run(() async {
|
||||
// Logic would go here to fetch by REJECTED status if needed
|
||||
return <Shift>[];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getHistoryShifts({required String staffId}) async {
|
||||
return _service.run(() async {
|
||||
final QueryResult<
|
||||
dc.ListCompletedApplicationsByStaffIdData,
|
||||
dc.ListCompletedApplicationsByStaffIdVariables
|
||||
>
|
||||
response = await _service.connector
|
||||
.listCompletedApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final List<Shift> shifts = <Shift>[];
|
||||
for (final dc.ListCompletedApplicationsByStaffIdApplications app
|
||||
in response.data.applications) {
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
|
||||
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(app.createdAt);
|
||||
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null
|
||||
? DateFormat('HH:mm').format(startDt)
|
||||
: '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: 'completed', // Hardcoded as checked out implies completion
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
latitude: app.shift.latitude,
|
||||
longitude: app.shift.longitude,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: app.shiftRole.isBreakPaid ?? false,
|
||||
breakTime: app.shiftRole.breakType?.stringValue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return shifts;
|
||||
});
|
||||
}
|
||||
|
||||
// --- PRIVATE HELPERS ---
|
||||
|
||||
List<Shift> _mapApplicationsToShifts(List<dynamic> apps) {
|
||||
return apps.map((app) {
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
|
||||
final DateTime? shiftDate = _service.toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _service.toDateTime(app.createdAt);
|
||||
|
||||
final bool hasCheckIn = app.checkInTime != null;
|
||||
final bool hasCheckOut = app.checkOutTime != null;
|
||||
|
||||
String status;
|
||||
if (hasCheckOut) {
|
||||
status = 'completed';
|
||||
} else if (hasCheckIn) {
|
||||
status = 'checked_in';
|
||||
} else {
|
||||
status = _mapApplicationStatus(app.status.stringValue);
|
||||
}
|
||||
|
||||
return Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: status,
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
latitude: app.shift.latitude,
|
||||
longitude: app.shift.longitude,
|
||||
breakInfo: BreakAdapter.fromData(
|
||||
isPaid: app.shiftRole.isBreakPaid ?? false,
|
||||
breakTime: app.shiftRole.breakType?.stringValue,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
String _mapApplicationStatus(String status) {
|
||||
switch (status) {
|
||||
case 'CONFIRMED':
|
||||
return 'confirmed';
|
||||
case 'PENDING':
|
||||
return 'pending';
|
||||
case 'CHECKED_OUT':
|
||||
return 'completed';
|
||||
case 'REJECTED':
|
||||
return 'cancelled';
|
||||
default:
|
||||
return 'open';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateApplicationStatus(
|
||||
String shiftId,
|
||||
String staffId,
|
||||
dc.ApplicationStatus newStatus,
|
||||
) async {
|
||||
return _service.run(() async {
|
||||
// First try to find the application
|
||||
final QueryResult<
|
||||
dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables
|
||||
>
|
||||
appsResponse = await _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplications? app = appsResponse
|
||||
.data
|
||||
.applications
|
||||
.where(
|
||||
(dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId,
|
||||
)
|
||||
.firstOrNull;
|
||||
|
||||
if (app != null) {
|
||||
await _service.connector
|
||||
.updateApplicationStatus(id: app.id)
|
||||
.status(newStatus)
|
||||
.execute();
|
||||
} else if (newStatus == dc.ApplicationStatus.REJECTED) {
|
||||
// If declining but no app found, create a rejected application
|
||||
final QueryResult<
|
||||
dc.ListShiftRolesByShiftIdData,
|
||||
dc.ListShiftRolesByShiftIdVariables
|
||||
>
|
||||
rolesRes = await _service.connector
|
||||
.listShiftRolesByShiftId(shiftId: shiftId)
|
||||
.execute();
|
||||
|
||||
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
||||
final dc.ListShiftRolesByShiftIdShiftRoles firstRole =
|
||||
rolesRes.data.shiftRoles.first;
|
||||
await _service.connector
|
||||
.createApplication(
|
||||
shiftId: shiftId,
|
||||
staffId: staffId,
|
||||
roleId: firstRole.id,
|
||||
status: dc.ApplicationStatus.REJECTED,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
} else {
|
||||
throw Exception("Application not found for shift $shiftId");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates a list of [ShiftSchedule] for RECURRING or PERMANENT orders.
|
||||
List<ShiftSchedule>? _generateSchedules({
|
||||
required String orderType,
|
||||
required DateTime? startDate,
|
||||
required DateTime? endDate,
|
||||
required List<String>? recurringDays,
|
||||
required List<String>? permanentDays,
|
||||
required String startTime,
|
||||
required String endTime,
|
||||
}) {
|
||||
if (orderType != 'RECURRING' && orderType != 'PERMANENT') return null;
|
||||
if (startDate == null || endDate == null) return null;
|
||||
|
||||
final List<String>? daysToInclude = orderType == 'RECURRING'
|
||||
? recurringDays
|
||||
: permanentDays;
|
||||
if (daysToInclude == null || daysToInclude.isEmpty) return null;
|
||||
|
||||
final List<ShiftSchedule> schedules = <ShiftSchedule>[];
|
||||
final Set<int> targetWeekdayIndex = daysToInclude
|
||||
.map((String day) {
|
||||
switch (day.toUpperCase()) {
|
||||
case 'MONDAY':
|
||||
return DateTime.monday;
|
||||
case 'TUESDAY':
|
||||
return DateTime.tuesday;
|
||||
case 'WEDNESDAY':
|
||||
return DateTime.wednesday;
|
||||
case 'THURSDAY':
|
||||
return DateTime.thursday;
|
||||
case 'FRIDAY':
|
||||
return DateTime.friday;
|
||||
case 'SATURDAY':
|
||||
return DateTime.saturday;
|
||||
case 'SUNDAY':
|
||||
return DateTime.sunday;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
})
|
||||
.where((int idx) => idx != -1)
|
||||
.toSet();
|
||||
|
||||
DateTime current = startDate;
|
||||
while (current.isBefore(endDate) ||
|
||||
current.isAtSameMomentAs(endDate) ||
|
||||
// Handle cases where the time component might differ slightly by checking date equality
|
||||
(current.year == endDate.year &&
|
||||
current.month == endDate.month &&
|
||||
current.day == endDate.day)) {
|
||||
if (targetWeekdayIndex.contains(current.weekday)) {
|
||||
schedules.add(
|
||||
ShiftSchedule(
|
||||
date: current.toIso8601String(),
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
current = current.add(const Duration(days: 1));
|
||||
|
||||
// Safety break to prevent infinite loops if dates are messed up
|
||||
if (schedules.length > 365) break;
|
||||
}
|
||||
|
||||
return schedules;
|
||||
}
|
||||
}
|
||||
|
||||
class _TargetShiftRole {
|
||||
final String shiftId;
|
||||
final String roleId;
|
||||
final int count;
|
||||
final int assigned;
|
||||
final int shiftFilled;
|
||||
final DateTime? date;
|
||||
|
||||
_TargetShiftRole({
|
||||
required this.shiftId,
|
||||
required this.roleId,
|
||||
required this.count,
|
||||
required this.assigned,
|
||||
required this.shiftFilled,
|
||||
this.date,
|
||||
});
|
||||
}
|
||||
@@ -17,4 +17,4 @@ class Accessibility extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[id, name];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:krow_domain/krow_domain.dart'
|
||||
AppException,
|
||||
BaseApiService,
|
||||
ClientSession,
|
||||
NetworkException,
|
||||
PasswordMismatchException,
|
||||
SignInFailedException,
|
||||
SignUpFailedException,
|
||||
@@ -111,19 +110,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
} on AppException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
// Map common Firebase-originated errors from the V2 API response
|
||||
// to domain exceptions.
|
||||
if (e is AppException) rethrow;
|
||||
|
||||
// Extract error code if available from the API response
|
||||
final String errorMessage = e.toString();
|
||||
if (errorMessage.contains('EMAIL_EXISTS') ||
|
||||
errorMessage.contains('email-already-in-use')) {
|
||||
throw AccountExistsException(technicalMessage: errorMessage);
|
||||
} else if (errorMessage.contains('WEAK_PASSWORD') ||
|
||||
errorMessage.contains('weak-password')) {
|
||||
throw WeakPasswordException(technicalMessage: errorMessage);
|
||||
} else if (errorMessage.contains('network-request-failed')) {
|
||||
throw NetworkException(technicalMessage: errorMessage);
|
||||
}
|
||||
throw SignUpFailedException(technicalMessage: 'Unexpected error: $e');
|
||||
_throwSignUpError('SIGN_UP_ERROR', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
class ClientIntroPage extends StatelessWidget {
|
||||
class ClientIntroPage extends StatefulWidget {
|
||||
const ClientIntroPage({super.key});
|
||||
|
||||
@override
|
||||
State<ClientIntroPage> createState() => _ClientIntroPageState();
|
||||
}
|
||||
|
||||
class _ClientIntroPageState extends State<ClientIntroPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future<void>.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted && Modular.to.path == ClientPaths.root) {
|
||||
Modular.to.toClientGetStartedPage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
@@ -11,14 +11,14 @@ import 'package:client_home/src/presentation/blocs/client_home_state.dart';
|
||||
///
|
||||
/// Shows an error message with a retry button when data fails to load.
|
||||
class ClientHomeErrorState extends StatelessWidget {
|
||||
/// The current home state containing error information.
|
||||
final ClientHomeState state;
|
||||
|
||||
/// Creates a [ClientHomeErrorState].
|
||||
const ClientHomeErrorState({
|
||||
required this.state,
|
||||
super.key,
|
||||
});
|
||||
/// The current home state containing error information.
|
||||
final ClientHomeState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -8,14 +8,14 @@ import 'package:client_home/src/presentation/widgets/dashboard_widget_builder.da
|
||||
///
|
||||
/// Shows visible dashboard widgets in a vertical scrollable list with dividers.
|
||||
class ClientHomeNormalModeBody extends StatelessWidget {
|
||||
/// The current home state.
|
||||
final ClientHomeState state;
|
||||
|
||||
/// Creates a [ClientHomeNormalModeBody].
|
||||
const ClientHomeNormalModeBody({
|
||||
required this.state,
|
||||
super.key,
|
||||
});
|
||||
/// The current home state.
|
||||
final ClientHomeState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ class CoverageBloc extends Bloc<CoverageEvent, CoverageState>
|
||||
Emitter<CoverageState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(CoverageLoading());
|
||||
final CoverageReport report = await _getCoverageReportUseCase.call(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the coverage report BLoC.
|
||||
|
||||
@@ -24,7 +24,7 @@ class DailyOpsBloc extends Bloc<DailyOpsEvent, DailyOpsState>
|
||||
Emitter<DailyOpsState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(DailyOpsLoading());
|
||||
final DailyOpsReport report = await _getDailyOpsReportUseCase.call(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the daily operations report BLoC.
|
||||
|
||||
@@ -24,7 +24,7 @@ class ForecastBloc extends Bloc<ForecastEvent, ForecastState>
|
||||
Emitter<ForecastState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(ForecastLoading());
|
||||
final ForecastReport report = await _getForecastReportUseCase.call(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the forecast report BLoC.
|
||||
|
||||
@@ -24,7 +24,7 @@ class NoShowBloc extends Bloc<NoShowEvent, NoShowState>
|
||||
Emitter<NoShowState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(NoShowLoading());
|
||||
final NoShowReport report = await _getNoShowReportUseCase.call(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the no-show report BLoC.
|
||||
|
||||
@@ -25,7 +25,7 @@ class PerformanceBloc extends Bloc<PerformanceEvent, PerformanceState>
|
||||
Emitter<PerformanceState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(PerformanceLoading());
|
||||
final PerformanceReport report = await _getPerformanceReportUseCase.call(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the performance report BLoC.
|
||||
|
||||
@@ -24,7 +24,7 @@ class SpendBloc extends Bloc<SpendEvent, SpendState>
|
||||
Emitter<SpendState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(SpendLoading());
|
||||
final SpendReport report = await _getSpendReportUseCase.call(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Base state for the spend report BLoC.
|
||||
|
||||
@@ -26,7 +26,7 @@ class ReportsSummaryBloc
|
||||
Emitter<ReportsSummaryState> emit,
|
||||
) async {
|
||||
await handleError(
|
||||
emit: emit,
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
emit(ReportsSummaryLoading());
|
||||
final ReportSummary summary = await _getReportsSummaryUseCase.call(
|
||||
|
||||
@@ -71,7 +71,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -95,7 +95,7 @@ class _CoverageReportPageState extends State<CoverageReportPage> {
|
||||
context.t.client_reports.coverage_report
|
||||
.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -203,7 +203,7 @@ class _CoverageSummaryCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
@@ -214,7 +214,7 @@ class _CoverageSummaryCard extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
color: color.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 16, color: color),
|
||||
|
||||
@@ -101,7 +101,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -126,7 +126,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
|
||||
context.t.client_reports.daily_ops_report
|
||||
.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -155,7 +155,7 @@ class _DailyOpsReportPageState extends State<DailyOpsReportPage> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.06),
|
||||
color: UiColors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
@@ -390,7 +390,7 @@ class _OpsStatCard extends StatelessWidget {
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
@@ -451,7 +451,7 @@ class _ShiftListItem extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.02),
|
||||
color: UiColors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
@@ -497,7 +497,7 @@ class _ShiftListItem extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
|
||||
@@ -119,7 +119,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -141,7 +141,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
|
||||
Text(
|
||||
context.t.client_reports.forecast_report.subtitle,
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -213,7 +213,7 @@ class _ForecastReportPageState extends State<ForecastReportPage> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
@@ -289,7 +289,7 @@ class _MetricCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 8,
|
||||
),
|
||||
],
|
||||
@@ -469,7 +469,7 @@ class _ForecastChart extends StatelessWidget {
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: UiColors.tagPending.withOpacity(0.5),
|
||||
color: UiColors.tagPending.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -74,7 +74,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.15),
|
||||
color: UiColors.white.withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -97,7 +97,7 @@ class _NoShowReportPageState extends State<NoShowReportPage> {
|
||||
Text(
|
||||
context.t.client_reports.no_show_report.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withOpacity(0.6),
|
||||
color: UiColors.white.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -225,7 +225,7 @@ class _SummaryChip extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.06),
|
||||
color: UiColors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -302,7 +302,7 @@ class _WorkerCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -157,7 +157,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -182,7 +182,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
context.t.client_reports.performance_report
|
||||
.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -212,7 +212,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -270,7 +270,7 @@ class _PerformanceReportPageState extends State<PerformanceReportPage> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
@@ -387,7 +387,7 @@ class _KpiRow extends StatelessWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: kpi.iconColor.withOpacity(0.1),
|
||||
color: kpi.iconColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(kpi.icon, size: 18, color: kpi.iconColor),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart';
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart';
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -81,7 +81,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -104,7 +104,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
|
||||
Text(
|
||||
context.t.client_reports.spend_report.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -164,7 +164,7 @@ class _SpendReportPageState extends State<SpendReportPage> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -358,7 +358,7 @@ class _SpendStatCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.06),
|
||||
color: UiColors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
@@ -392,7 +392,7 @@ class _SpendStatCard extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: themeColor.withOpacity(0.1),
|
||||
color: themeColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
@@ -424,7 +424,7 @@ class _SpendByCategoryCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.04),
|
||||
color: UiColors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:client_reports/src/presentation/widgets/reports_page/report_card.dart';
|
||||
import 'package:client_reports/src/presentation/widgets/reports_page/report_card.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -44,7 +44,7 @@ class ReportCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.02),
|
||||
color: UiColors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -51,7 +51,7 @@ class ReportsHeader extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
@@ -84,7 +84,7 @@ class ReportsHeader extends StatelessWidget {
|
||||
height: 44,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
color: UiColors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TabBar(
|
||||
|
||||
@@ -176,104 +176,4 @@ class _QuickLinkItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationsSettingsCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
|
||||
builder: (BuildContext context, ClientSettingsState state) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
),
|
||||
color: UiColors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
context.t.client_settings.preferences.title,
|
||||
style: UiTypography.footnote1b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
_NotificationToggle(
|
||||
icon: UiIcons.bell,
|
||||
title: context.t.client_settings.preferences.push,
|
||||
value: state.pushEnabled,
|
||||
onChanged: (bool val) =>
|
||||
ReadContext(context).read<ClientSettingsBloc>().add(
|
||||
ClientSettingsNotificationToggled(
|
||||
type: 'push',
|
||||
isEnabled: val,
|
||||
),
|
||||
),
|
||||
),
|
||||
_NotificationToggle(
|
||||
icon: UiIcons.mail,
|
||||
title: context.t.client_settings.preferences.email,
|
||||
value: state.emailEnabled,
|
||||
onChanged: (bool val) =>
|
||||
ReadContext(context).read<ClientSettingsBloc>().add(
|
||||
ClientSettingsNotificationToggled(
|
||||
type: 'email',
|
||||
isEnabled: val,
|
||||
),
|
||||
),
|
||||
),
|
||||
_NotificationToggle(
|
||||
icon: UiIcons.phone,
|
||||
title: context.t.client_settings.preferences.sms,
|
||||
value: state.smsEnabled,
|
||||
onChanged: (bool val) =>
|
||||
ReadContext(context).read<ClientSettingsBloc>().add(
|
||||
ClientSettingsNotificationToggled(
|
||||
type: 'sms',
|
||||
isEnabled: val,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationToggle extends StatelessWidget {
|
||||
|
||||
const _NotificationToggle({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 20, color: UiColors.iconSecondary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Text(title, style: UiTypography.footnote1m.textPrimary),
|
||||
],
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: value,
|
||||
activeColor: UiColors.primary,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class SettingsProfileHeader extends StatelessWidget {
|
||||
final String businessName = session?.businessName ?? 'Your Company';
|
||||
final String email = session?.email ?? 'client@example.com';
|
||||
// V2 session does not include a photo URL; show letter avatar.
|
||||
final String? photoUrl = null;
|
||||
const String? photoUrl = null;
|
||||
final String avatarLetter = businessName.trim().isNotEmpty
|
||||
? businessName.trim()[0].toUpperCase()
|
||||
: 'C';
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import 'dart:async';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// A simple introductory page that displays the KROW logo.
|
||||
class IntroPage extends StatelessWidget {
|
||||
/// A simple introductory page that displays the KROW logo and navigates
|
||||
/// to the get started page after a short delay.
|
||||
class IntroPage extends StatefulWidget {
|
||||
const IntroPage({super.key});
|
||||
|
||||
@override
|
||||
State<IntroPage> createState() => _IntroPageState();
|
||||
}
|
||||
|
||||
class _IntroPageState extends State<IntroPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer(const Duration(seconds: 2), () {
|
||||
if (Modular.to.path == StaffPaths.root) {
|
||||
Modular.to.toGetStartedPage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -12,3 +31,4 @@ class IntroPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,12 +142,14 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||
(state.status == AuthStatus.loading &&
|
||||
state.verificationId != null);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
BlocProvider.of<AuthBloc>(
|
||||
context,
|
||||
).add(AuthResetRequested(mode: widget.mode));
|
||||
return true;
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
onPopInvokedWithResult: (bool didPop, dynamic result) {
|
||||
if (didPop) {
|
||||
BlocProvider.of<AuthBloc>(
|
||||
context,
|
||||
).add(AuthResetRequested(mode: widget.mode));
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: UiAppBar(
|
||||
|
||||
@@ -13,8 +13,7 @@ class _GetStartedBackgroundState extends State<GetStartedBackground> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Column(
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
// Logo
|
||||
@@ -113,7 +112,6 @@ class _GetStartedBackgroundState extends State<GetStartedBackground> {
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinput/pinput.dart';
|
||||
@@ -50,7 +49,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
||||
}
|
||||
|
||||
Future<void> _listenForSmsCode() async {
|
||||
final res = await _smartAuth.getSmsCode();
|
||||
final SmsCodeResult res = await _smartAuth.getSmsCode();
|
||||
if (res.code != null && mounted) {
|
||||
_controller.text = res.code!;
|
||||
_onChanged(_controller.text);
|
||||
@@ -68,7 +67,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultPinTheme = PinTheme(
|
||||
final PinTheme defaultPinTheme = PinTheme(
|
||||
width: 45,
|
||||
height: 56,
|
||||
textStyle: UiTypography.headline3m,
|
||||
@@ -81,7 +80,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
||||
),
|
||||
);
|
||||
|
||||
final focusedPinTheme = defaultPinTheme.copyWith(
|
||||
final PinTheme focusedPinTheme = defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(
|
||||
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
||||
@@ -90,7 +89,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
||||
),
|
||||
);
|
||||
|
||||
final submittedPinTheme = defaultPinTheme.copyWith(
|
||||
final PinTheme submittedPinTheme = defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(
|
||||
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
|
||||
@@ -99,7 +98,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
||||
),
|
||||
);
|
||||
|
||||
final errorPinTheme = defaultPinTheme.copyWith(
|
||||
final PinTheme errorPinTheme = defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
border: Border.all(color: UiColors.textError, width: 2),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -20,14 +20,14 @@ class PendingPaymentCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
UiColors.primary.withOpacity(0.08),
|
||||
UiColors.primary.withOpacity(0.04),
|
||||
UiColors.primary.withValues(alpha: 0.08),
|
||||
UiColors.primary.withValues(alpha: 0.04),
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.primary.withOpacity(0.12)),
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.12)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
||||
@@ -23,7 +23,7 @@ class DocumentSelectedCard extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(UiConstants.space2),
|
||||
),
|
||||
child: const Center(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports, unused_element, unused_field, duplicate_ignore
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -53,7 +53,7 @@ class AttireGrid extends StatelessWidget {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? UiColors.primary.withOpacity(0.1)
|
||||
? UiColors.primary.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: UiConstants.radiusSm,
|
||||
border: Border.all(
|
||||
@@ -141,7 +141,7 @@ class AttireGrid extends StatelessWidget {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: hasPhoto
|
||||
? UiColors.primary.withOpacity(0.05)
|
||||
? UiColors.primary.withValues(alpha: 0.05)
|
||||
: UiColors.white,
|
||||
border: Border.all(
|
||||
color: hasPhoto ? UiColors.primary : UiColors.border,
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:google_places_flutter/model/prediction.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../blocs/personal_info_bloc.dart';
|
||||
import '../blocs/personal_info_event.dart';
|
||||
import '../blocs/personal_info_state.dart';
|
||||
import '../widgets/preferred_locations_page/places_search_field.dart';
|
||||
import '../widgets/preferred_locations_page/locations_list.dart';
|
||||
import '../widgets/preferred_locations_page/empty_locations_state.dart';
|
||||
import 'package:staff_profile_info/src/presentation/blocs/personal_info_bloc.dart';
|
||||
import 'package:staff_profile_info/src/presentation/blocs/personal_info_state.dart';
|
||||
import 'package:staff_profile_info/src/presentation/blocs/personal_info_event.dart';
|
||||
|
||||
/// The maximum number of preferred locations a staff member can add.
|
||||
const int _kMaxLocations = 5;
|
||||
|
||||
/// Uber-style Preferred Locations editing page.
|
||||
/// Page for staff members to manage their preferred work locations.
|
||||
///
|
||||
/// Allows staff to search for US locations using the Google Places API,
|
||||
/// add them as chips (max 5), and save back to their profile.
|
||||
/// Allows searching for and adding multiple locations to the profile.
|
||||
class PreferredLocationsPage extends StatefulWidget {
|
||||
/// Creates a [PreferredLocationsPage].
|
||||
const PreferredLocationsPage({super.key});
|
||||
@@ -30,211 +20,112 @@ class PreferredLocationsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PreferredLocationsPageState extends State<PreferredLocationsPage> {
|
||||
late final TextEditingController _searchController;
|
||||
late final FocusNode _searchFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController = TextEditingController();
|
||||
_searchFocusNode = FocusNode();
|
||||
}
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onLocationSelected(Prediction prediction, PersonalInfoBloc bloc) {
|
||||
final String description = prediction.description ?? '';
|
||||
if (description.isEmpty) return;
|
||||
|
||||
bloc.add(PersonalInfoLocationAdded(location: description));
|
||||
|
||||
// Clear search field after selection
|
||||
void _onAddLocation(String location, PersonalInfoBloc bloc) {
|
||||
if (location.trim().isEmpty) return;
|
||||
bloc.add(PersonalInfoLocationAdded(location: location));
|
||||
_searchController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
}
|
||||
|
||||
void _removeLocation(String location, PersonalInfoBloc bloc) {
|
||||
void _onRemoveLocation(String location, PersonalInfoBloc bloc) {
|
||||
bloc.add(PersonalInfoLocationRemoved(location: location));
|
||||
}
|
||||
|
||||
void _save(BuildContext context, PersonalInfoBloc bloc, PersonalInfoState state) {
|
||||
bloc.add(const PersonalInfoFormSubmitted());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||
// Access the same PersonalInfoBloc singleton managed by the module.
|
||||
final PersonalInfoBloc bloc = Modular.get<PersonalInfoBloc>();
|
||||
|
||||
return BlocProvider<PersonalInfoBloc>.value(
|
||||
value: bloc,
|
||||
child: BlocConsumer<PersonalInfoBloc, PersonalInfoState>(
|
||||
listener: (BuildContext context, PersonalInfoState state) {
|
||||
if (state.status == PersonalInfoStatus.saved) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: i18n.preferred_locations.save_success,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
} else if (state.status == PersonalInfoStatus.error) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, PersonalInfoState state) {
|
||||
final List<String> locations = _currentLocations(state);
|
||||
final bool atMax = locations.length >= _kMaxLocations;
|
||||
final bool isSaving = state.status == PersonalInfoStatus.saving;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
appBar: UiAppBar(
|
||||
title: i18n.preferred_locations.title,
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
// ” Description
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space5,
|
||||
UiConstants.space3,
|
||||
),
|
||||
child: Text(
|
||||
i18n.preferred_locations.description,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
// ” Search autocomplete field
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: PlacesSearchField(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
hint: i18n.preferred_locations.search_hint,
|
||||
enabled: !atMax && !isSaving,
|
||||
onSelected: (Prediction p) => _onLocationSelected(p, bloc),
|
||||
),
|
||||
),
|
||||
|
||||
// ” "Max reached" banner
|
||||
if (atMax)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
UiConstants.space2,
|
||||
UiConstants.space5,
|
||||
0,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.info,
|
||||
size: 14,
|
||||
color: UiColors.textWarning,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
i18n.preferred_locations.max_reached,
|
||||
style: UiTypography.footnote1r.textWarning,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
|
||||
// ” Section label
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Text(
|
||||
i18n.preferred_locations.added_label,
|
||||
style: UiTypography.titleUppercase3m.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
|
||||
// Locations list / empty state
|
||||
Expanded(
|
||||
child: locations.isEmpty
|
||||
? EmptyLocationsState(message: i18n.preferred_locations.empty_state)
|
||||
: LocationsList(
|
||||
locations: locations,
|
||||
isSaving: isSaving,
|
||||
removeTooltip: i18n.preferred_locations.remove_tooltip,
|
||||
onRemove: (String loc) => _removeLocation(loc, bloc),
|
||||
),
|
||||
),
|
||||
|
||||
// Save button
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: UiButton.primary(
|
||||
text: isSaving ? null : i18n.preferred_locations.save_button,
|
||||
fullWidth: true,
|
||||
onPressed: isSaving ? null : () => _save(context, bloc, state),
|
||||
child: isSaving
|
||||
? const SizedBox(
|
||||
height: UiConstants.iconMd,
|
||||
width: UiConstants.iconMd,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
UiColors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSaving)
|
||||
Container(
|
||||
color: UiColors.black.withValues(alpha: 0.3),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _currentLocations(PersonalInfoState state) {
|
||||
final dynamic raw = state.formValues['preferredLocations'];
|
||||
final dynamic raw = state.personalInfo?.preferredLocations;
|
||||
if (raw is List<String>) return raw;
|
||||
if (raw is List) return raw.map((dynamic e) => e.toString()).toList();
|
||||
return <String>[];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffOnboardingPersonalInfoEn i18n =
|
||||
Translations.of(context).staff.onboarding.personal_info;
|
||||
|
||||
return BlocProvider<PersonalInfoBloc>.value(
|
||||
value: Modular.get<PersonalInfoBloc>(),
|
||||
child: BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
||||
builder: (BuildContext context, PersonalInfoState state) {
|
||||
final PersonalInfoBloc bloc = BlocProvider.of<PersonalInfoBloc>(context);
|
||||
final List<String> locations = _currentLocations(state);
|
||||
|
||||
return Scaffold(
|
||||
appBar: UiAppBar(
|
||||
title: i18n.preferred_locations.title,
|
||||
showBackButton: true,
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
i18n.preferred_locations.description,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
|
||||
// Search field (Mock autocomplete)
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: i18n.preferred_locations.search_hint,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(UiIcons.add),
|
||||
onPressed: () => _onAddLocation(_searchController.text, bloc),
|
||||
),
|
||||
),
|
||||
onSubmitted: (String val) => _onAddLocation(val, bloc),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
itemCount: locations.length,
|
||||
separatorBuilder: (BuildContext context, int index) => const Divider(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final String loc = locations[index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(UiIcons.mapPin, color: UiColors.primary),
|
||||
title: Text(loc, style: UiTypography.body2m.textPrimary),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(UiIcons.close, size: 20),
|
||||
onPressed: () => _onRemoveLocation(loc, bloc),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: UiButton.primary(
|
||||
text: i18n.save_button,
|
||||
onPressed: state.status == PersonalInfoStatus.loading
|
||||
? null
|
||||
: () => bloc.add(const PersonalInfoFormSubmitted()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,4 @@ class PersonalInfoForm extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/mobile/packages/features/staff/shifts/analyze_output.txt
Normal file
BIN
apps/mobile/packages/features/staff/shifts/analyze_output.txt
Normal file
Binary file not shown.
@@ -221,25 +221,15 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
color: UiColors.primary.withValues(alpha: 0.09),
|
||||
),
|
||||
),
|
||||
child: widget.shift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
child: Image.network(
|
||||
widget.shift.logoUrl!,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: Icon(
|
||||
UiIcons.briefcase,
|
||||
color: UiColors.primary,
|
||||
size: UiConstants.iconMd,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
UiIcons.briefcase,
|
||||
color: UiColors.primary,
|
||||
size: UiConstants.iconMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
// Consensed Details
|
||||
|
||||
@@ -2,8 +2,6 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_shifts/src/presentation/widgets/available_order_card.dart';
|
||||
@@ -34,124 +32,15 @@ class FindShiftsTab extends StatefulWidget {
|
||||
class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
String _searchQuery = '';
|
||||
String _jobType = 'all';
|
||||
double? _maxDistance; // miles
|
||||
Position? _currentPosition;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
|
||||
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initLocation() async {
|
||||
try {
|
||||
final LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.always ||
|
||||
permission == LocationPermission.whileInUse) {
|
||||
final Position pos = await Geolocator.getCurrentPosition();
|
||||
if (mounted) {
|
||||
setState(() => _currentPosition = pos);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
double _calculateDistance(double lat, double lng) {
|
||||
if (_currentPosition == null) return -1;
|
||||
final double distMeters = Geolocator.distanceBetween(
|
||||
_currentPosition!.latitude,
|
||||
_currentPosition!.longitude,
|
||||
lat,
|
||||
lng,
|
||||
);
|
||||
return distMeters / 1609.34; // meters to miles
|
||||
}
|
||||
|
||||
void _showDistanceFilter() {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: UiColors.bgPopup,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setModalState) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
context.t.staff_shifts.find_shifts.radius_filter_title,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_maxDistance == null
|
||||
? context.t.staff_shifts.find_shifts.unlimited_distance
|
||||
: context.t.staff_shifts.find_shifts.within_miles(
|
||||
miles: _maxDistance!.round().toString(),
|
||||
),
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
Slider(
|
||||
value: _maxDistance ?? 100,
|
||||
min: 5,
|
||||
max: 100,
|
||||
divisions: 19,
|
||||
activeColor: UiColors.primary,
|
||||
onChanged: (double val) {
|
||||
setModalState(() => _maxDistance = val);
|
||||
setState(() => _maxDistance = val);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: UiButton.secondary(
|
||||
text: context.t.staff_shifts.find_shifts.clear,
|
||||
onPressed: () {
|
||||
setModalState(() => _maxDistance = null);
|
||||
setState(() => _maxDistance = null);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UiButton.primary(
|
||||
text: context.t.staff_shifts.find_shifts.apply,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime today = DateTime(now.year, now.month, now.day);
|
||||
final DateTime tomorrow = today.add(const Duration(days: 1));
|
||||
final DateTime d = DateTime(date.year, date.month, date.day);
|
||||
if (d == today) return 'Today';
|
||||
if (d == tomorrow) return 'Tomorrow';
|
||||
return DateFormat('EEE, MMM d').format(date);
|
||||
}
|
||||
|
||||
/// Builds a filter tab chip.
|
||||
Widget _buildFilterTab(String id, String label) {
|
||||
final bool isSelected = _jobType == id;
|
||||
|
||||
Reference in New Issue
Block a user