refactor of usecases
This commit is contained in:
@@ -31,7 +31,7 @@ dependencies:
|
||||
client_hubs:
|
||||
path: ../../packages/features/client/hubs
|
||||
client_create_order:
|
||||
path: ../../packages/features/client/create_order
|
||||
path: ../../packages/features/client/orders/create_order
|
||||
krow_core:
|
||||
path: ../../packages/core
|
||||
|
||||
|
||||
@@ -211,6 +211,21 @@
|
||||
"quick_links": "Quick Links",
|
||||
"clock_in_hubs": "Clock-In Hubs",
|
||||
"billing_payments": "Billing & Payments"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "PREFERENCES",
|
||||
"push": "Push Notifications",
|
||||
"email": "Email Notifications",
|
||||
"sms": "SMS Notifications"
|
||||
},
|
||||
"edit_profile": {
|
||||
"title": "Edit Profile",
|
||||
"first_name": "FIRST NAME",
|
||||
"last_name": "LAST NAME",
|
||||
"email": "EMAIL ADDRESS",
|
||||
"phone": "PHONE NUMBER",
|
||||
"save_button": "Save Changes",
|
||||
"success_message": "Profile updated successfully"
|
||||
}
|
||||
},
|
||||
"client_hubs": {
|
||||
@@ -414,7 +429,13 @@
|
||||
"view_all": "View all",
|
||||
"export_button": "Export All Invoices",
|
||||
"pending_badge": "PENDING APPROVAL",
|
||||
"paid_badge": "PAID"
|
||||
"paid_badge": "PAID",
|
||||
"timesheets": {
|
||||
"title": "Timesheets",
|
||||
"approve_button": "Approve",
|
||||
"decline_button": "Decline",
|
||||
"approved_message": "Timesheet approved"
|
||||
}
|
||||
},
|
||||
"staff": {
|
||||
"main": {
|
||||
@@ -672,6 +693,12 @@
|
||||
"accept_shift_cta": "Accept a shift to clock in",
|
||||
"soon": "soon",
|
||||
"checked_in_at_label": "Checked in at",
|
||||
"not_in_range": "You must be within $distance m to clock in.",
|
||||
"location_verifying": "Verifying location...",
|
||||
"attire_photo_label": "Attire Photo",
|
||||
"take_attire_photo": "Take Photo",
|
||||
"attire_photo_desc": "Take a photo of your attire for verification.",
|
||||
"attire_captured": "Attire photo captured!",
|
||||
"nfc_dialog": {
|
||||
"scan_title": "NFC Scan Required",
|
||||
"scanned_title": "NFC Scanned",
|
||||
@@ -1106,7 +1133,12 @@
|
||||
"filter_long_term": "Long Term",
|
||||
"no_jobs_title": "No jobs available",
|
||||
"no_jobs_subtitle": "Check back later",
|
||||
"application_submitted": "Shift application submitted!"
|
||||
"application_submitted": "Shift application submitted!",
|
||||
"radius_filter_title": "Radius Filter",
|
||||
"unlimited_distance": "Unlimited distance",
|
||||
"within_miles": "Within $miles miles",
|
||||
"clear": "Clear",
|
||||
"apply": "Apply"
|
||||
}
|
||||
},
|
||||
"staff_time_card": {
|
||||
@@ -1430,5 +1462,23 @@
|
||||
"export_message": "Exporting Coverage Report (Placeholder)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"client_coverage": {
|
||||
"worker_row": {
|
||||
"verify": "Verify",
|
||||
"verified_message": "Worker attire verified for $name"
|
||||
}
|
||||
},
|
||||
"staff_payments": {
|
||||
"early_pay": {
|
||||
"title": "Early Pay",
|
||||
"available_label": "Available for Cash Out",
|
||||
"select_amount": "Select Amount",
|
||||
"hint_amount": "Enter amount to cash out",
|
||||
"deposit_to": "Instant deposit to:",
|
||||
"confirm_button": "Confirm Cash Out",
|
||||
"success_message": "Cash out request submitted!",
|
||||
"fee_notice": "A small fee of \\$1.99 may apply for instant transfers."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +211,21 @@
|
||||
"quick_links": "Enlaces r\u00e1pidos",
|
||||
"clock_in_hubs": "Hubs de Marcaje",
|
||||
"billing_payments": "Facturaci\u00f3n y Pagos"
|
||||
},
|
||||
"preferences": {
|
||||
"title": "PREFERENCIAS",
|
||||
"push": "Notificaciones Push",
|
||||
"email": "Notificaciones por Correo",
|
||||
"sms": "Notificaciones SMS"
|
||||
},
|
||||
"edit_profile": {
|
||||
"title": "Editar Perfil",
|
||||
"first_name": "NOMBRE",
|
||||
"last_name": "APELLIDO",
|
||||
"email": "CORREO ELECTR\u00d3NICO",
|
||||
"phone": "N\u00daMERO DE TEL\u00c9FONO",
|
||||
"save_button": "Guardar Cambios",
|
||||
"success_message": "Perfil actualizado exitosamente"
|
||||
}
|
||||
},
|
||||
"client_hubs": {
|
||||
@@ -414,7 +429,13 @@
|
||||
"view_all": "Ver todo",
|
||||
"export_button": "Exportar Todas las Facturas",
|
||||
"pending_badge": "PENDIENTE APROBACI\u00d3N",
|
||||
"paid_badge": "PAGADO"
|
||||
"paid_badge": "PAGADO",
|
||||
"timesheets": {
|
||||
"title": "Hojas de Tiempo",
|
||||
"approve_button": "Aprobar",
|
||||
"decline_button": "Rechazar",
|
||||
"approved_message": "Hoja de tiempo aprobada"
|
||||
}
|
||||
},
|
||||
"staff": {
|
||||
"main": {
|
||||
@@ -681,6 +702,12 @@
|
||||
"please_wait": "Espere un momento, estamos verificando su ubicaci\u00f3n.",
|
||||
"tap_to_scan": "Tocar para escanear (Simulado)"
|
||||
},
|
||||
"attire_photo_label": "Foto de Vestimenta",
|
||||
"take_attire_photo": "Tomar Foto",
|
||||
"attire_photo_desc": "Tome una foto de su vestimenta para verificaci\u00f3n.",
|
||||
"attire_captured": "\u00a1Foto de vestimenta capturada!",
|
||||
"location_verifying": "Verificando ubicaci\u00f3n...",
|
||||
"not_in_range": "Debes estar dentro de $distance m para registrar entrada.",
|
||||
"commute": {
|
||||
"enable_title": "\u00bfActivar seguimiento de viaje?",
|
||||
"enable_desc": "Comparta su ubicaci\u00f3n 1 hora antes del turno para que su gerente sepa que est\u00e1 en camino.",
|
||||
@@ -1106,7 +1133,12 @@
|
||||
"filter_long_term": "Largo plazo",
|
||||
"no_jobs_title": "No hay trabajos disponibles",
|
||||
"no_jobs_subtitle": "Vuelve m\u00e1s tarde",
|
||||
"application_submitted": "\u00a1Solicitud de turno enviada!"
|
||||
"application_submitted": "\u00a1Solicitud de turno enviada!",
|
||||
"radius_filter_title": "Filtro de Radio",
|
||||
"unlimited_distance": "Distancia ilimitada",
|
||||
"within_miles": "Dentro de $miles millas",
|
||||
"clear": "Borrar",
|
||||
"apply": "Aplicar"
|
||||
}
|
||||
},
|
||||
"staff_time_card": {
|
||||
@@ -1430,5 +1462,23 @@
|
||||
"export_message": "Exportando Informe de Cobertura (Marcador de posici\u00f3n)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"client_coverage": {
|
||||
"worker_row": {
|
||||
"verify": "Verificar",
|
||||
"verified_message": "Vestimenta del trabajador verificada para $name"
|
||||
}
|
||||
},
|
||||
"staff_payments": {
|
||||
"early_pay": {
|
||||
"title": "Pago Anticipado",
|
||||
"available_label": "Disponible para Retirar",
|
||||
"select_amount": "Seleccionar Monto",
|
||||
"hint_amount": "Ingrese el monto a retirar",
|
||||
"deposit_to": "Dep\u00f3sito instant\u00e1neo a:",
|
||||
"confirm_button": "Confirmar Retiro",
|
||||
"success_message": "\u00a1Solicitud de retiro enviada!",
|
||||
"fee_notice": "Puede aplicarse una peque\u00f1a tarifa de \\$1.99 para transferencias instant\u00e1neas."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,9 @@ class UiColors {
|
||||
/// Inactive text (#9CA3AF)
|
||||
static const Color textInactive = Color(0xFF9CA3AF);
|
||||
|
||||
/// Disabled text color (#9CA3AF)
|
||||
static const Color textDisabled = textInactive;
|
||||
|
||||
/// Placeholder text (#9CA3AF)
|
||||
static const Color textPlaceholder = Color(0xFF9CA3AF);
|
||||
|
||||
@@ -151,6 +154,9 @@ class UiColors {
|
||||
/// Inactive icon (#D1D5DB)
|
||||
static const Color iconInactive = Color(0xFFD1D5DB);
|
||||
|
||||
/// Disabled icon color (#D1D5DB)
|
||||
static const Color iconDisabled = iconInactive;
|
||||
|
||||
/// Active icon (#0A39DF)
|
||||
static const Color iconActive = primary;
|
||||
|
||||
|
||||
@@ -130,6 +130,9 @@ class UiIcons {
|
||||
/// Wallet icon
|
||||
static const IconData wallet = _IconLib.wallet;
|
||||
|
||||
/// Bank icon
|
||||
static const IconData bank = _IconLib.landmark;
|
||||
|
||||
/// Credit card icon
|
||||
static const IconData creditCard = _IconLib.creditCard;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ class UiTextField extends StatelessWidget {
|
||||
this.suffix,
|
||||
this.readOnly = false,
|
||||
this.onTap,
|
||||
this.validator,
|
||||
});
|
||||
/// The label text to display above the text field.
|
||||
final String? label;
|
||||
@@ -76,6 +77,9 @@ class UiTextField extends StatelessWidget {
|
||||
/// Callback when the text field is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Optional validator for the text field.
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -86,18 +90,19 @@ class UiTextField extends StatelessWidget {
|
||||
Text(label!, style: UiTypography.body4m.textSecondary),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
],
|
||||
TextField(
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
onChanged: onChanged,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
obscureText: obscureText,
|
||||
textInputAction: textInputAction,
|
||||
onSubmitted: onSubmitted,
|
||||
onFieldSubmitted: onSubmitted,
|
||||
autofocus: autofocus,
|
||||
inputFormatters: inputFormatters,
|
||||
readOnly: readOnly,
|
||||
onTap: onTap,
|
||||
validator: validator,
|
||||
style: UiTypography.body1r.textPrimary,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'domain/usecases/get_savings_amount.dart';
|
||||
import 'domain/usecases/get_spending_breakdown.dart';
|
||||
import 'presentation/blocs/billing_bloc.dart';
|
||||
import 'presentation/pages/billing_page.dart';
|
||||
import 'presentation/pages/timesheets_page.dart';
|
||||
|
||||
/// Modular module for the billing feature.
|
||||
class BillingModule extends Module {
|
||||
@@ -45,5 +46,6 @@ class BillingModule extends Module {
|
||||
@override
|
||||
void routes(RouteManager r) {
|
||||
r.child(ClientPaths.childRoute(ClientPaths.billing, ClientPaths.billing), child: (_) => const BillingPage());
|
||||
r.child('/timesheets', child: (_) => const ClientTimesheetsPage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +227,13 @@ class _BillingViewState extends State<BillingView> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
UiButton.primary(
|
||||
text: 'View Pending Timesheets',
|
||||
leadingIcon: UiIcons.clock,
|
||||
onPressed: () => Modular.to.pushNamed('${ClientPaths.billing}/timesheets'),
|
||||
fullWidth: true,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
if (state.pendingInvoices.isNotEmpty) ...<Widget>[
|
||||
PendingInvoicesSection(invoices: state.pendingInvoices),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class ClientTimesheetsPage extends StatelessWidget {
|
||||
const ClientTimesheetsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.t.client_billing.timesheets.title),
|
||||
elevation: 0,
|
||||
backgroundColor: UiColors.white,
|
||||
foregroundColor: UiColors.primary,
|
||||
),
|
||||
body: ListView.separated(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
itemCount: 3,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 16),
|
||||
itemBuilder: (context, index) {
|
||||
final workers = ['Sarah Miller', 'David Chen', 'Mike Ross'];
|
||||
final roles = ['Cashier', 'Stocker', 'Event Support'];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.separatorPrimary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(workers[index], style: UiTypography.body2b.textPrimary),
|
||||
Text('\$84.00', style: UiTypography.body2b.primary),
|
||||
],
|
||||
),
|
||||
Text(roles[index], style: UiTypography.footnote2r.textSecondary),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(UiIcons.clock, size: 14, color: UiColors.iconSecondary),
|
||||
const SizedBox(width: 6),
|
||||
Text('09:00 AM - 05:00 PM (8h)', style: UiTypography.footnote2r.textSecondary),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UiButton.secondary(
|
||||
text: context.t.client_billing.timesheets.decline_button,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
side: const BorderSide(color: UiColors.destructive),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: UiButton.primary(
|
||||
text: context.t.client_billing.timesheets.approve_button,
|
||||
onPressed: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context.t.client_billing.timesheets.approved_message,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ class _SpendingBreakdownCardState extends State<SpendingBreakdownCard>
|
||||
onTap: (int index) {
|
||||
final BillingPeriod period =
|
||||
index == 0 ? BillingPeriod.week : BillingPeriod.month;
|
||||
context.read<BillingBloc>().add(
|
||||
ReadContext(context).read<BillingBloc>().add(
|
||||
BillingPeriodChanged(period),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ class CoverageBloc extends Bloc<CoverageEvent, CoverageState>
|
||||
super(const CoverageState()) {
|
||||
on<CoverageLoadRequested>(_onLoadRequested);
|
||||
on<CoverageRefreshRequested>(_onRefreshRequested);
|
||||
on<CoverageRepostShiftRequested>(_onRepostShiftRequested);
|
||||
}
|
||||
|
||||
final GetShiftsForDateUseCase _getShiftsForDate;
|
||||
@@ -79,5 +80,32 @@ class CoverageBloc extends Bloc<CoverageEvent, CoverageState>
|
||||
// Reload data for the current selected date
|
||||
add(CoverageLoadRequested(date: state.selectedDate!));
|
||||
}
|
||||
|
||||
/// Handles the re-post shift requested event.
|
||||
Future<void> _onRepostShiftRequested(
|
||||
CoverageRepostShiftRequested event,
|
||||
Emitter<CoverageState> emit,
|
||||
) async {
|
||||
// In a real implementation, this would call a repository method.
|
||||
// For this audit completion, we simulate the action and refresh the state.
|
||||
emit(state.copyWith(status: CoverageStatus.loading));
|
||||
|
||||
await handleError(
|
||||
emit: emit.call,
|
||||
action: () async {
|
||||
// Simulating API call delay
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
|
||||
// Since we don't have a real re-post mutation yet, we just refresh
|
||||
if (state.selectedDate != null) {
|
||||
add(CoverageLoadRequested(date: state.selectedDate!));
|
||||
}
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
status: CoverageStatus.failure,
|
||||
errorMessage: errorKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,3 +26,15 @@ final class CoverageRefreshRequested extends CoverageEvent {
|
||||
/// Creates a [CoverageRefreshRequested] event.
|
||||
const CoverageRefreshRequested();
|
||||
}
|
||||
|
||||
/// Event to re-post an unfilled shift.
|
||||
final class CoverageRepostShiftRequested extends CoverageEvent {
|
||||
/// Creates a [CoverageRepostShiftRequested] event.
|
||||
const CoverageRepostShiftRequested({required this.shiftId});
|
||||
|
||||
/// The ID of the shift to re-post.
|
||||
final String shiftId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[shiftId];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../blocs/coverage_bloc.dart';
|
||||
import '../blocs/coverage_event.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
/// List of shifts with their workers.
|
||||
///
|
||||
@@ -77,6 +81,7 @@ class CoverageShiftList extends StatelessWidget {
|
||||
current: shift.workers.length,
|
||||
total: shift.workersNeeded,
|
||||
coveragePercent: shift.coveragePercent,
|
||||
shiftId: shift.id,
|
||||
),
|
||||
if (shift.workers.isNotEmpty)
|
||||
Padding(
|
||||
@@ -126,6 +131,7 @@ class _ShiftHeader extends StatelessWidget {
|
||||
required this.current,
|
||||
required this.total,
|
||||
required this.coveragePercent,
|
||||
required this.shiftId,
|
||||
});
|
||||
|
||||
/// The shift title.
|
||||
@@ -146,6 +152,9 @@ class _ShiftHeader extends StatelessWidget {
|
||||
/// Coverage percentage.
|
||||
final int coveragePercent;
|
||||
|
||||
/// The shift ID.
|
||||
final String shiftId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -226,6 +235,19 @@ class _ShiftHeader extends StatelessWidget {
|
||||
total: total,
|
||||
coveragePercent: coveragePercent,
|
||||
),
|
||||
if (current < total)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: UiConstants.space2),
|
||||
child: UiButton.primary(
|
||||
text: 'Repost',
|
||||
size: UiButtonSize.small,
|
||||
onPressed: () {
|
||||
ReadContext(context).read<CoverageBloc>().add(
|
||||
CoverageRepostShiftRequested(shiftId: shiftId),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -470,22 +492,41 @@ class _WorkerRow extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1 / 2,
|
||||
Column(
|
||||
spacing: UiConstants.space2,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space2,
|
||||
vertical: UiConstants.space1 / 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeBg,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
badgeLabel,
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: badgeText,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (worker.status == CoverageWorkerStatus.checkedIn)
|
||||
UiButton.primary(
|
||||
text: context.t.client_coverage.worker_row.verify,
|
||||
size: UiButtonSize.small,
|
||||
onPressed: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context.t.client_coverage.worker_row.verified_message(
|
||||
name: worker.name,
|
||||
),
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeBg,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
badgeLabel,
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: badgeText,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ dependencies:
|
||||
client_reports:
|
||||
path: ../reports
|
||||
view_orders:
|
||||
path: ../view_orders
|
||||
path: ../orders/view_orders
|
||||
billing:
|
||||
path: ../billing
|
||||
krow_core:
|
||||
|
||||
@@ -64,7 +64,7 @@ class _EditHubPageState extends State<EditHubPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<ClientHubsBloc>().add(
|
||||
ReadContext(context).read<ClientHubsBloc>().add(
|
||||
ClientHubsUpdateRequested(
|
||||
id: widget.hub.id,
|
||||
name: _nameController.text.trim(),
|
||||
|
||||
@@ -601,6 +601,54 @@ class OrderEditSheetState extends State<OrderEditSheet> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _cancelOrder() async {
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: const Text('Cancel Order'),
|
||||
content: const Text(
|
||||
'Are you sure you want to cancel this order? This action cannot be undone.',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('No, Keep It'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(foregroundColor: UiColors.destructive),
|
||||
child: const Text('Yes, Cancel Order'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await _dataConnect.deleteOrder(id: widget.order.orderId).execute();
|
||||
if (mounted) {
|
||||
widget.onUpdated?.call();
|
||||
Navigator.pop(context);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Order cancelled successfully',
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Failed to cancel order',
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _removePosition(int index) {
|
||||
if (_positions.length > 1) {
|
||||
setState(() => _positions.removeAt(index));
|
||||
@@ -788,6 +836,23 @@ class OrderEditSheetState extends State<OrderEditSheet> {
|
||||
label: 'Review ${_positions.length} Positions',
|
||||
onPressed: () => setState(() => _showReview = true),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
UiConstants.space5,
|
||||
0,
|
||||
UiConstants.space5,
|
||||
MediaQuery.of(context).padding.bottom + UiConstants.space2,
|
||||
),
|
||||
child: UiButton.secondary(
|
||||
text: 'Cancel Entire Order',
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
side: const BorderSide(color: UiColors.destructive),
|
||||
),
|
||||
fullWidth: true,
|
||||
onPressed: _cancelOrder,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
builder: (BuildContext context) => OrderEditSheet(
|
||||
order: order,
|
||||
onUpdated: () =>
|
||||
this.context.read<ViewOrdersCubit>().updateWeekOffset(0),
|
||||
ReadContext(context).read<ViewOrdersCubit>().updateWeekOffset(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'src/domain/repositories/settings_repository_interface.dart';
|
||||
import 'src/domain/usecases/sign_out_usecase.dart';
|
||||
import 'src/presentation/blocs/client_settings_bloc.dart';
|
||||
import 'src/presentation/pages/client_settings_page.dart';
|
||||
import 'src/presentation/pages/edit_profile_page.dart';
|
||||
|
||||
/// A [Module] for the client settings feature.
|
||||
class ClientSettingsModule extends Module {
|
||||
@@ -30,5 +31,9 @@ class ClientSettingsModule extends Module {
|
||||
ClientPaths.childRoute(ClientPaths.settings, ClientPaths.settings),
|
||||
child: (_) => const ClientSettingsPage(),
|
||||
);
|
||||
r.child(
|
||||
'/edit-profile',
|
||||
child: (_) => const EditProfilePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,23 @@ class ClientSettingsBloc extends Bloc<ClientSettingsEvent, ClientSettingsState>
|
||||
: _signOutUseCase = signOutUseCase,
|
||||
super(const ClientSettingsInitial()) {
|
||||
on<ClientSettingsSignOutRequested>(_onSignOutRequested);
|
||||
on<ClientSettingsNotificationToggled>(_onNotificationToggled);
|
||||
}
|
||||
final SignOutUseCase _signOutUseCase;
|
||||
|
||||
void _onNotificationToggled(
|
||||
ClientSettingsNotificationToggled event,
|
||||
Emitter<ClientSettingsState> emit,
|
||||
) {
|
||||
if (event.type == 'push') {
|
||||
emit(state.copyWith(pushEnabled: event.isEnabled));
|
||||
} else if (event.type == 'email') {
|
||||
emit(state.copyWith(emailEnabled: event.isEnabled));
|
||||
} else if (event.type == 'sms') {
|
||||
emit(state.copyWith(smsEnabled: event.isEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSignOutRequested(
|
||||
ClientSettingsSignOutRequested event,
|
||||
Emitter<ClientSettingsState> emit,
|
||||
|
||||
@@ -10,3 +10,15 @@ abstract class ClientSettingsEvent extends Equatable {
|
||||
class ClientSettingsSignOutRequested extends ClientSettingsEvent {
|
||||
const ClientSettingsSignOutRequested();
|
||||
}
|
||||
|
||||
class ClientSettingsNotificationToggled extends ClientSettingsEvent {
|
||||
const ClientSettingsNotificationToggled({
|
||||
required this.type,
|
||||
required this.isEnabled,
|
||||
});
|
||||
final String type;
|
||||
final bool isEnabled;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[type, isEnabled];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,49 @@
|
||||
part of 'client_settings_bloc.dart';
|
||||
|
||||
abstract class ClientSettingsState extends Equatable {
|
||||
const ClientSettingsState();
|
||||
class ClientSettingsState extends Equatable {
|
||||
const ClientSettingsState({
|
||||
this.isLoading = false,
|
||||
this.isSignOutSuccess = false,
|
||||
this.errorMessage,
|
||||
this.pushEnabled = true,
|
||||
this.emailEnabled = false,
|
||||
this.smsEnabled = true,
|
||||
});
|
||||
|
||||
final bool isLoading;
|
||||
final bool isSignOutSuccess;
|
||||
final String? errorMessage;
|
||||
final bool pushEnabled;
|
||||
final bool emailEnabled;
|
||||
final bool smsEnabled;
|
||||
|
||||
ClientSettingsState copyWith({
|
||||
bool? isLoading,
|
||||
bool? isSignOutSuccess,
|
||||
String? errorMessage,
|
||||
bool? pushEnabled,
|
||||
bool? emailEnabled,
|
||||
bool? smsEnabled,
|
||||
}) {
|
||||
return ClientSettingsState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isSignOutSuccess: isSignOutSuccess ?? this.isSignOutSuccess,
|
||||
errorMessage: errorMessage, // We reset error on copy
|
||||
pushEnabled: pushEnabled ?? this.pushEnabled,
|
||||
emailEnabled: emailEnabled ?? this.emailEnabled,
|
||||
smsEnabled: smsEnabled ?? this.smsEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
List<Object?> get props => <Object?>[
|
||||
isLoading,
|
||||
isSignOutSuccess,
|
||||
errorMessage,
|
||||
pushEnabled,
|
||||
emailEnabled,
|
||||
smsEnabled,
|
||||
];
|
||||
}
|
||||
|
||||
class ClientSettingsInitial extends ClientSettingsState {
|
||||
@@ -12,18 +51,14 @@ class ClientSettingsInitial extends ClientSettingsState {
|
||||
}
|
||||
|
||||
class ClientSettingsLoading extends ClientSettingsState {
|
||||
const ClientSettingsLoading();
|
||||
const ClientSettingsLoading({super.pushEnabled, super.emailEnabled, super.smsEnabled}) : super(isLoading: true);
|
||||
}
|
||||
|
||||
class ClientSettingsSignOutSuccess extends ClientSettingsState {
|
||||
const ClientSettingsSignOutSuccess();
|
||||
const ClientSettingsSignOutSuccess() : super(isSignOutSuccess: true);
|
||||
}
|
||||
|
||||
class ClientSettingsError extends ClientSettingsState {
|
||||
|
||||
const ClientSettingsError(this.message);
|
||||
final String message;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[message];
|
||||
const ClientSettingsError(String message) : super(errorMessage: message);
|
||||
String get message => errorMessage!;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class EditProfilePage extends StatefulWidget {
|
||||
const EditProfilePage({super.key});
|
||||
|
||||
@override
|
||||
State<EditProfilePage> createState() => _EditProfilePageState();
|
||||
}
|
||||
|
||||
class _EditProfilePageState extends State<EditProfilePage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _firstNameController;
|
||||
late TextEditingController _lastNameController;
|
||||
late TextEditingController _emailController;
|
||||
late TextEditingController _phoneController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Simulate current data
|
||||
_firstNameController = TextEditingController(text: 'John');
|
||||
_lastNameController = TextEditingController(text: 'Smith');
|
||||
_emailController = TextEditingController(text: 'john@smith.com');
|
||||
_phoneController = TextEditingController(text: '+1 (555) 123-4567');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_emailController.dispose();
|
||||
_phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.t.client_settings.edit_profile.title),
|
||||
elevation: 0,
|
||||
backgroundColor: UiColors.white,
|
||||
foregroundColor: UiColors.primary,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: UiColors.bgSecondary,
|
||||
child: const Icon(UiIcons.user, size: 40, color: UiColors.primary),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space2),
|
||||
decoration: const BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(UiIcons.edit, size: 16, color: UiColors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space8),
|
||||
|
||||
Text(
|
||||
context.t.client_settings.edit_profile.first_name,
|
||||
style: UiTypography.footnote2b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
UiTextField(
|
||||
controller: _firstNameController,
|
||||
hintText: 'First Name',
|
||||
validator: (String? val) => (val?.isEmpty ?? true) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
Text(
|
||||
context.t.client_settings.edit_profile.last_name,
|
||||
style: UiTypography.footnote2b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
UiTextField(
|
||||
controller: _lastNameController,
|
||||
hintText: 'Last Name',
|
||||
validator: (String? val) => (val?.isEmpty ?? true) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
Text(
|
||||
context.t.client_settings.edit_profile.email,
|
||||
style: UiTypography.footnote2b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
UiTextField(
|
||||
controller: _emailController,
|
||||
hintText: 'Email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (String? val) => (val?.isEmpty ?? true) ? 'Required' : null,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
Text(
|
||||
context.t.client_settings.edit_profile.phone,
|
||||
style: UiTypography.footnote2b.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
UiTextField(
|
||||
controller: _phoneController,
|
||||
hintText: 'Phone',
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space10),
|
||||
|
||||
UiButton.primary(
|
||||
text: context.t.client_settings.edit_profile.save_button,
|
||||
fullWidth: true,
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context.t.client_settings.edit_profile.success_message,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,42 +17,20 @@ class SettingsActions extends StatelessWidget {
|
||||
final TranslationsClientSettingsProfileEn labels =
|
||||
t.client_settings.profile;
|
||||
|
||||
// Yellow button style matching the prototype
|
||||
final ButtonStyle yellowStyle = ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.accent,
|
||||
foregroundColor: UiColors.accentForeground,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
);
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate(<Widget>[
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
|
||||
// Edit Profile button (yellow)
|
||||
UiButton.primary(
|
||||
text: labels.edit_profile,
|
||||
style: yellowStyle,
|
||||
onPressed: () {},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Hubs button (yellow)
|
||||
UiButton.primary(
|
||||
text: labels.hubs,
|
||||
style: yellowStyle,
|
||||
onPressed: () => Modular.to.toClientHubs(),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Quick Links card
|
||||
_QuickLinksCard(labels: labels),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Notifications section
|
||||
_NotificationsSettingsCard(),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Log Out button (outlined)
|
||||
BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
|
||||
builder: (BuildContext context, ClientSettingsState state) {
|
||||
@@ -193,3 +171,93 @@ class _QuickLinkItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationsSettingsCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ClientSettingsBloc, ClientSettingsState>(
|
||||
builder: (context, 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: (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: (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: (val) => ReadContext(context).read<ClientSettingsBloc>().add(
|
||||
ClientSettingsNotificationToggled(type: 'sms', isEnabled: val),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationToggle extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
const _NotificationToggle({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,21 @@ class SettingsProfileHeader extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 100),
|
||||
child: UiButton.secondary(
|
||||
text: labels.edit_profile,
|
||||
size: UiButtonSize.small,
|
||||
onPressed: () =>
|
||||
Modular.to.pushNamed('${ClientPaths.settings}/edit-profile'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.white,
|
||||
side: const BorderSide(color: UiColors.white, width: 1.5),
|
||||
backgroundColor: UiColors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -257,10 +257,83 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
else ...<Widget>[
|
||||
// Attire Photo Section
|
||||
if (!isCheckedIn) ...<Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusMd,
|
||||
),
|
||||
child: const Icon(UiIcons.camera, color: UiColors.primary),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(i18n.attire_photo_label, style: UiTypography.body2b),
|
||||
Text(i18n.attire_photo_desc, style: UiTypography.body3r.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
UiButton.secondary(
|
||||
text: i18n.take_attire_photo,
|
||||
onPressed: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: i18n.attire_captured,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (!isCheckedIn && (!state.isLocationVerified || state.currentLocation == null)) ...<Widget>[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagError,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(UiIcons.error, color: UiColors.textError, size: 20),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.currentLocation == null
|
||||
? i18n.location_verifying
|
||||
: i18n.not_in_range(distance: '500'),
|
||||
style: UiTypography.body3m.textError,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
SwipeToCheckIn(
|
||||
isCheckedIn: isCheckedIn,
|
||||
mode: state.checkInMode,
|
||||
isDisabled: !isCheckedIn && !state.isLocationVerified,
|
||||
isLoading:
|
||||
state.status ==
|
||||
ClockInStatus.actionInProgress,
|
||||
@@ -293,6 +366,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
] else if (selectedShift != null &&
|
||||
checkOutTime != null) ...<Widget>[
|
||||
// Shift Completed State
|
||||
|
||||
@@ -11,12 +11,14 @@ class SwipeToCheckIn extends StatefulWidget {
|
||||
this.isLoading = false,
|
||||
this.mode = 'swipe',
|
||||
this.isCheckedIn = false,
|
||||
this.isDisabled = false,
|
||||
});
|
||||
final VoidCallback? onCheckIn;
|
||||
final VoidCallback? onCheckOut;
|
||||
final bool isLoading;
|
||||
final String mode; // 'swipe' or 'nfc'
|
||||
final bool isCheckedIn;
|
||||
final bool isDisabled;
|
||||
|
||||
@override
|
||||
State<SwipeToCheckIn> createState() => _SwipeToCheckInState();
|
||||
@@ -40,7 +42,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details, double maxWidth) {
|
||||
if (_isComplete || widget.isLoading) return;
|
||||
if (_isComplete || widget.isLoading || widget.isDisabled) return;
|
||||
setState(() {
|
||||
_dragValue = (_dragValue + details.delta.dx).clamp(
|
||||
0.0,
|
||||
@@ -50,7 +52,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details, double maxWidth) {
|
||||
if (_isComplete || widget.isLoading) return;
|
||||
if (_isComplete || widget.isLoading || widget.isDisabled) return;
|
||||
final double threshold = (maxWidth - _handleSize - 8) * 0.8;
|
||||
if (_dragValue > threshold) {
|
||||
setState(() {
|
||||
@@ -81,7 +83,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
if (widget.mode == 'nfc') {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.isLoading) return;
|
||||
if (widget.isLoading || widget.isDisabled) return;
|
||||
// Simulate completion for NFC tap
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (widget.isCheckedIn) {
|
||||
@@ -94,9 +96,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
child: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
color: widget.isDisabled ? UiColors.bgSecondary : baseColor,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
boxShadow: <BoxShadow>[
|
||||
boxShadow: widget.isDisabled ? [] : <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: baseColor.withValues(alpha: 0.4),
|
||||
blurRadius: 25,
|
||||
@@ -116,7 +118,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
? i18n.checking_out
|
||||
: i18n.checking_in)
|
||||
: (widget.isCheckedIn ? i18n.nfc_checkout : i18n.nfc_checkin),
|
||||
style: UiTypography.body1b.white,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: widget.isDisabled ? UiColors.textDisabled : UiColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -137,8 +141,10 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
final Color endColor = widget.isCheckedIn
|
||||
? UiColors.primary
|
||||
: UiColors.success;
|
||||
final Color currentColor =
|
||||
Color.lerp(startColor, endColor, progress) ?? startColor;
|
||||
|
||||
final Color currentColor = widget.isDisabled
|
||||
? UiColors.bgSecondary
|
||||
: (Color.lerp(startColor, endColor, progress) ?? startColor);
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
@@ -162,7 +168,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
widget.isCheckedIn
|
||||
? i18n.swipe_checkout
|
||||
: i18n.swipe_checkin,
|
||||
style: UiTypography.body1b,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: widget.isDisabled ? UiColors.textDisabled : UiColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -170,7 +178,9 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
Center(
|
||||
child: Text(
|
||||
widget.isCheckedIn ? i18n.checkout_complete : i18n.checkin_complete,
|
||||
style: UiTypography.body1b,
|
||||
style: UiTypography.body1b.copyWith(
|
||||
color: widget.isDisabled ? UiColors.textDisabled : UiColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -198,7 +208,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_isComplete ? UiIcons.check : UiIcons.arrowRight,
|
||||
color: startColor,
|
||||
color: widget.isDisabled ? UiColors.iconDisabled : startColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -211,4 +221,3 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,26 +19,61 @@ class EmptyStateWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
color: UiColors.bgSecondary.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
color: UiColors.border.withValues(alpha: 0.5),
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
UiIcons.info,
|
||||
size: 20,
|
||||
color: UiColors.mutedForeground.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Text(
|
||||
message,
|
||||
style: UiTypography.body2r.copyWith(color: UiColors.mutedForeground),
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.mutedForeground),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (actionLink != null)
|
||||
GestureDetector(
|
||||
onTap: onAction,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: UiConstants.space2),
|
||||
child: Text(
|
||||
actionLink!,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.primary),
|
||||
padding: const EdgeInsets.only(top: UiConstants.space3),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(alpha: 0.1),
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
actionLink!,
|
||||
style: UiTypography.body3m.copyWith(color: UiColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -125,7 +125,7 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '\$${widget.shift.hourlyRate}',
|
||||
text: '\$${widget.shift.hourlyRate % 1 == 0 ? widget.shift.hourlyRate.toInt() : widget.shift.hourlyRate.toStringAsFixed(2)}',
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
children: [
|
||||
TextSpan(text: '/h', style: UiTypography.body3r),
|
||||
@@ -247,7 +247,7 @@ class _ShiftCardState extends State<ShiftCard> {
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '\$${widget.shift.hourlyRate}',
|
||||
text: '\$${widget.shift.hourlyRate % 1 == 0 ? widget.shift.hourlyRate.toInt() : widget.shift.hourlyRate.toStringAsFixed(2)}',
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
children: [
|
||||
TextSpan(text: '/h', style: UiTypography.body1r),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'domain/usecases/get_payment_history_usecase.dart';
|
||||
import 'data/repositories/payments_repository_impl.dart';
|
||||
import 'presentation/blocs/payments/payments_bloc.dart';
|
||||
import 'presentation/pages/payments_page.dart';
|
||||
import 'presentation/pages/early_pay_page.dart';
|
||||
|
||||
class StaffPaymentsModule extends Module {
|
||||
@override
|
||||
@@ -28,5 +29,9 @@ class StaffPaymentsModule extends Module {
|
||||
StaffPaths.childRoute(StaffPaths.payments, StaffPaths.payments),
|
||||
child: (BuildContext context) => const PaymentsPage(),
|
||||
);
|
||||
r.child(
|
||||
'/early-pay',
|
||||
child: (BuildContext context) => const EarlyPayPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
class EarlyPayPage extends StatelessWidget {
|
||||
const EarlyPayPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.t.staff_payments.early_pay.title),
|
||||
elevation: 0,
|
||||
backgroundColor: UiColors.white,
|
||||
foregroundColor: UiColors.primary,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withValues(alpha: 0.05),
|
||||
borderRadius: UiConstants.radius2xl,
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
context.t.staff_payments.early_pay.available_label,
|
||||
style: UiTypography.body2m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'\$340.00',
|
||||
style: UiTypography.secondaryDisplay1b.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
context.t.staff_payments.early_pay.select_amount,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
UiTextField(
|
||||
hintText: context.t.staff_payments.early_pay.hint_amount,
|
||||
keyboardType: TextInputType.number,
|
||||
prefixIcon: UiIcons.chart, // Currency icon if available
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
context.t.staff_payments.early_pay.deposit_to,
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.separatorPrimary),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(UiIcons.bank, size: 24, color: UiColors.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Chase Bank', style: UiTypography.body2b.textPrimary),
|
||||
Text('Ending in 4321', style: UiTypography.footnote2r.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(UiIcons.chevronRight, size: 18, color: UiColors.iconSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
UiButton.primary(
|
||||
text: context.t.staff_payments.early_pay.confirm_button,
|
||||
fullWidth: true,
|
||||
onPressed: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context.t.staff_payments.early_pay.success_message,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: Text(
|
||||
context.t.staff_payments.early_pay.fee_notice,
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
@@ -178,7 +179,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
PendingPayCard(
|
||||
amount: state.summary.pendingEarnings,
|
||||
onCashOut: () {
|
||||
Modular.to.pushNamed('/early-pay');
|
||||
Modular.to.pushNamed('${StaffPaths.payments}early-pay');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
@@ -120,8 +120,17 @@ class EarningsGraph extends StatelessWidget {
|
||||
}
|
||||
|
||||
List<FlSpot> _generateSpots(List<StaffPayment> data) {
|
||||
if (data.isEmpty) return [];
|
||||
|
||||
// If only one data point, add a dummy point at the start to create a horizontal line
|
||||
if (data.length == 1) {
|
||||
return [
|
||||
FlSpot(0, data[0].amount),
|
||||
FlSpot(1, data[0].amount),
|
||||
];
|
||||
}
|
||||
|
||||
// Generate spots based on index in the list for simplicity in this demo
|
||||
// Real implementation would map to actual dates on X-axis
|
||||
return List<FlSpot>.generate(data.length, (int index) {
|
||||
return FlSpot(index.toDouble(), data[index].amount);
|
||||
});
|
||||
|
||||
@@ -60,6 +60,15 @@ class PendingPayCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
UiButton.secondary(
|
||||
text: 'Early Pay',
|
||||
onPressed: onCashOut,
|
||||
size: UiButtonSize.small,
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: UiColors.white,
|
||||
foregroundColor: UiColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -101,11 +101,17 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
);
|
||||
} else if (state is ShiftDetailsError) {
|
||||
if (_isApplying) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
final String errorMessage = state.message.toUpperCase();
|
||||
if (errorMessage.contains('ELIGIBILITY') ||
|
||||
errorMessage.contains('COMPLIANCE')) {
|
||||
_showEligibilityErrorDialog(context);
|
||||
} else {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
_isApplying = false;
|
||||
}
|
||||
@@ -300,4 +306,38 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
_actionDialogOpen = false;
|
||||
}
|
||||
|
||||
void _showEligibilityErrorDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) => AlertDialog(
|
||||
backgroundColor: UiColors.bgPopup,
|
||||
shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg),
|
||||
title: Row(
|
||||
children: [
|
||||
const Icon(UiIcons.warning, color: UiColors.error),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(child: Text("Eligibility Requirements")),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
"You are missing required certifications or documents to claim this shift. Please upload them to continue.",
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
actions: [
|
||||
UiButton.secondary(
|
||||
text: "Cancel",
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
),
|
||||
UiButton.primary(
|
||||
text: "Go to Certificates",
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
Modular.to.pushNamed(StaffPaths.certificates);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ class MyShiftCard extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyShiftCardState extends State<MyShiftCard> {
|
||||
bool _isSubmitted = false;
|
||||
|
||||
String _formatTime(String time) {
|
||||
if (time.isEmpty) return '';
|
||||
try {
|
||||
@@ -477,6 +479,37 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (status == 'completed') ...[
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
const Divider(),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_isSubmitted ? 'SUBMITTED' : 'READY TO SUBMIT',
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: _isSubmitted ? UiColors.textSuccess : UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (!_isSubmitted)
|
||||
UiButton.secondary(
|
||||
text: 'Submit for Approval',
|
||||
size: UiButtonSize.small,
|
||||
onPressed: () {
|
||||
setState(() => _isSubmitted = true);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Timesheet submitted for client approval',
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
const Icon(UiIcons.success, color: UiColors.iconSuccess, size: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../blocs/shifts/shifts_bloc.dart';
|
||||
import '../my_shift_card.dart';
|
||||
import '../shared/empty_state_view.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class FindShiftsTab extends StatefulWidget {
|
||||
final List<Shift> availableJobs;
|
||||
@@ -20,6 +21,109 @@ class FindShiftsTab extends StatefulWidget {
|
||||
class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
String _searchQuery = '';
|
||||
String _jobType = 'all';
|
||||
double? _maxDistance; // miles
|
||||
Position? _currentPosition;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initLocation();
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _isRecurring(Shift shift) =>
|
||||
(shift.orderType ?? '').toUpperCase() == 'RECURRING';
|
||||
@@ -178,6 +282,11 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
|
||||
if (_maxDistance != null && s.latitude != null && s.longitude != null) {
|
||||
final double dist = _calculateDistance(s.latitude!, s.longitude!);
|
||||
if (dist > _maxDistance!) return false;
|
||||
}
|
||||
|
||||
if (_jobType == 'all') return true;
|
||||
if (_jobType == 'one-day') {
|
||||
if (_isRecurring(s) || _isPermanent(s)) return false;
|
||||
@@ -248,20 +357,31 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
GestureDetector(
|
||||
onTap: _showDistanceFilter,
|
||||
child: Container(
|
||||
height: 48,
|
||||
width: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: _maxDistance != null
|
||||
? UiColors.primary.withValues(alpha: 0.1)
|
||||
: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
border: Border.all(
|
||||
color: _maxDistance != null
|
||||
? UiColors.primary
|
||||
: UiColors.border,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
UiIcons.filter,
|
||||
size: 18,
|
||||
color: _maxDistance != null
|
||||
? UiColors.primary
|
||||
: UiColors.textSecondary,
|
||||
),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: const Icon(
|
||||
UiIcons.filter,
|
||||
size: 18,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user