chore: fix 273+ analysis issues and repair corrupted core files

This commit is contained in:
2026-03-20 21:05:23 +05:30
parent d3159bc2ae
commit 39263a4af5
59 changed files with 265 additions and 3268 deletions

View File

@@ -25,7 +25,7 @@ void main() async {
);
// Register global BLoC observer for centralized error logging
Bloc.observer = CoreBlocObserver(
Bloc.observer = const CoreBlocObserver(
logEvents: true,
logStateChanges: false, // Set to true for verbose debugging
);

View File

@@ -23,7 +23,7 @@ void main() async {
await const BackgroundTaskService().initialize(backgroundGeofenceDispatcher);
// Register global BLoC observer for centralized error logging
Bloc.observer = CoreBlocObserver(
Bloc.observer = const CoreBlocObserver(
logEvents: true,
logStateChanges: false, // Set to true for verbose debugging
);

View File

@@ -3,6 +3,9 @@
appId: com.krowwithus.staff
---
- launchApp
- extendedWaitUntil:
visible: "(?i).*(Home|Profile|Log In).*"
timeout: 10000
- assertVisible: "Profile"
- tapOn: "Profile"
- assertVisible: "Personal Info"

View File

@@ -3,6 +3,9 @@
appId: com.krowwithus.staff
---
- launchApp
- extendedWaitUntil:
visible: "(?i).*(Home|Shifts|Welcome back|Log In).*"
timeout: 10000
- assertVisible: "Shifts"
- tapOn: "Shifts"
- assertVisible: "Find Shifts"

View File

@@ -34,12 +34,12 @@ appId: com.krowwithus.staff
# Case A: shifts are available — list renders
- assertVisible:
text: "(?i).*(Available|Apply Now|shift|hours).*"
text: "(?i).*(Available|Apply Now|Book Shift|shift|hours|jobs).*"
optional: true
# Case B: no shifts — empty state copy is shown (not a blank screen)
- assertVisible:
text: "(?i).*(No shifts available|No available shifts|Check back|Nothing available|no open shifts).*"
text: "(?i).*(No shifts available|No available shifts|No jobs available|Check back|Nothing available|no open shifts).*"
optional: true
# Entry assertion: incomplete-profile banner (if applicable)

View File

@@ -29,20 +29,20 @@ appId: com.krowwithus.staff
visible: "Shifts"
timeout: 10000
# If jobs exist, APPLY NOW should be present (optional)
# If jobs exist, BOOK SHIFT should be present (optional)
- tapOn:
text: "APPLY NOW"
text: "BOOK SHIFT"
optional: true
- extendedWaitUntil:
visible: "Applying"
visible: "Booking order.*"
timeout: 10000
optional: true
- assertVisible:
text: "Applying"
text: "Booking order.*"
optional: true
# Otherwise, empty state may be visible (optional)
- assertVisible:
text: "No shifts found"
text: "No jobs available"
optional: true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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