Merge branch 'dev' into 493-implement-rapid-order-creation-voice-text-in-client-mobile-app
This commit is contained in:
@@ -73,11 +73,15 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
|
||||
final String eventName =
|
||||
shiftRole.shift.order.eventName ?? shiftRole.shift.title;
|
||||
|
||||
final order = shiftRole.shift.order;
|
||||
final String? hubManagerId = order.hubManagerId;
|
||||
final String? hubManagerName = order.hubManager?.user?.fullName;
|
||||
|
||||
return domain.OrderItem(
|
||||
id: _shiftRoleKey(shiftRole.shiftId, shiftRole.roleId),
|
||||
orderId: shiftRole.shift.order.id,
|
||||
orderId: order.id,
|
||||
orderType: domain.OrderType.fromString(
|
||||
shiftRole.shift.order.orderType.stringValue,
|
||||
order.orderType.stringValue,
|
||||
),
|
||||
title: shiftRole.role.name,
|
||||
eventName: eventName,
|
||||
@@ -94,6 +98,8 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
|
||||
hours: hours,
|
||||
totalValue: totalValue,
|
||||
confirmedApps: const <Map<String, dynamic>>[],
|
||||
hubManagerId: hubManagerId,
|
||||
hubManagerName: hubManagerName,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
|
||||
@@ -193,6 +193,8 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState>
|
||||
hours: order.hours,
|
||||
totalValue: order.totalValue,
|
||||
confirmedApps: confirmed,
|
||||
hubManagerId: order.hubManagerId,
|
||||
hubManagerName: order.hubManagerName,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -692,7 +692,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
_buildSectionHeader('VENDOR'),
|
||||
_buildSectionHeader(t.client_orders_common.select_vendor),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space3,
|
||||
@@ -742,7 +742,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
_buildSectionHeader('ORDER NAME'),
|
||||
_buildSectionHeader(t.client_orders_common.order_name),
|
||||
UiTextField(
|
||||
controller: _orderNameController,
|
||||
hintText: t.client_view_orders.order_edit_sheet.order_name_hint,
|
||||
@@ -750,7 +750,7 @@ class OrderEditSheetState extends State<OrderEditSheet> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
_buildSectionHeader('HUB'),
|
||||
_buildSectionHeader(t.client_orders_common.hub),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space3,
|
||||
|
||||
@@ -108,6 +108,43 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the edit icon should be shown.
|
||||
/// Hidden for completed orders and for past orders (shift has ended).
|
||||
bool _canEditOrder(OrderItem order) {
|
||||
if (order.status == 'COMPLETED') return false;
|
||||
if (order.date.isEmpty) return true;
|
||||
try {
|
||||
final DateTime orderDate = DateTime.parse(order.date);
|
||||
final String endTime = order.endTime.trim();
|
||||
final DateTime endDateTime;
|
||||
if (endTime.isEmpty) {
|
||||
// No end time: use end of day so orders today remain editable
|
||||
endDateTime = DateTime(
|
||||
orderDate.year,
|
||||
orderDate.month,
|
||||
orderDate.day,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
);
|
||||
} else {
|
||||
final List<String> endParts = endTime.split(':');
|
||||
final int hour = endParts.isNotEmpty ? int.parse(endParts[0]) : 0;
|
||||
final int minute = endParts.length > 1 ? int.parse(endParts[1]) : 0;
|
||||
endDateTime = DateTime(
|
||||
orderDate.year,
|
||||
orderDate.month,
|
||||
orderDate.day,
|
||||
hour,
|
||||
minute,
|
||||
);
|
||||
}
|
||||
return endDateTime.isAfter(DateTime.now());
|
||||
} catch (_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final OrderItem order = widget.order;
|
||||
@@ -291,13 +328,14 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
|
||||
// Actions
|
||||
Row(
|
||||
children: <Widget>[
|
||||
_buildHeaderIconButton(
|
||||
icon: UiIcons.edit,
|
||||
color: UiColors.primary,
|
||||
bgColor: UiColors.primary.withValues(alpha: 0.08),
|
||||
onTap: () => _openEditSheet(order: order),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
if (_canEditOrder(order))
|
||||
_buildHeaderIconButton(
|
||||
icon: UiIcons.edit,
|
||||
color: UiColors.primary,
|
||||
bgColor: UiColors.primary.withValues(alpha: 0.08),
|
||||
onTap: () => _openEditSheet(order: order),
|
||||
),
|
||||
if (_canEditOrder(order)) const SizedBox(width: UiConstants.space2),
|
||||
if (order.confirmedApps.isNotEmpty)
|
||||
_buildHeaderIconButton(
|
||||
icon: _expanded
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class TestPhoneNumbers {
|
||||
static const List<String> values = <String>[
|
||||
'+15145912311', // Test User 1
|
||||
'+15145912311', // Test User 1
|
||||
'+15557654321', // Demo / Mariana
|
||||
'+15555551234', // Test User 2
|
||||
];
|
||||
|
||||
static bool isTestNumber(String phoneNumber) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -42,7 +43,7 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text('Map View (GPS)', style: UiTypography.body2r.textSecondary),
|
||||
Text(context.t.staff.clock_in.map_view_gps, style: UiTypography.body2r.textSecondary),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -123,10 +123,12 @@ class HomeRepositoryImpl
|
||||
|
||||
return response.data.benefitsDatas.map((data) {
|
||||
final plan = data.vendorBenefitPlan;
|
||||
final total = plan.total?.toDouble() ?? 0.0;
|
||||
final remaining = data.current.toDouble();
|
||||
return Benefit(
|
||||
title: plan.title,
|
||||
entitlementHours: plan.total?.toDouble() ?? 0.0,
|
||||
usedHours: data.current.toDouble(),
|
||||
entitlementHours: total,
|
||||
usedHours: (total - remaining).clamp(0.0, total),
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ class BenefitsOverviewPage extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
child: Text(
|
||||
t.staff.home.benefits.overview.subtitle,
|
||||
t.staff.home.benefits.overview.empty_state,
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -143,21 +143,17 @@ class BenefitsOverviewPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
benefit.title,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
const Icon(UiIcons.info, size: 18, color: Color(0xFFE2E8F0)),
|
||||
],
|
||||
Text(
|
||||
benefit.title,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getSubtitle(benefit.title),
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
_buildStatsRow(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -231,6 +227,56 @@ class BenefitsOverviewPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsRow() {
|
||||
final i18n = t.staff.home.benefits.overview;
|
||||
return Row(
|
||||
children: [
|
||||
_buildStatChip(
|
||||
i18n.entitlement,
|
||||
'${benefit.entitlementHours.toInt()}',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatChip(
|
||||
i18n.used,
|
||||
'${benefit.usedHours.toInt()}',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatChip(
|
||||
i18n.remaining,
|
||||
'${benefit.remainingHours.toInt()}',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatChip(String label, String value) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.footnote2r.textTertiary.copyWith(
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$value ${t.staff.home.benefits.overview.hours}',
|
||||
style: UiTypography.footnote2b.textPrimary.copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getSubtitle(String title) {
|
||||
final i18n = t.staff.home.benefits.overview;
|
||||
if (title.toLowerCase().contains('sick')) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -55,18 +57,44 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
|
||||
|
||||
Future<void> _pickFile() async {
|
||||
final String? path = await _filePicker.pickFile(
|
||||
allowedExtensions: <String>['pdf', 'jpg', 'png'],
|
||||
allowedExtensions: <String>['pdf'],
|
||||
);
|
||||
|
||||
if (path != null) {
|
||||
final String? error = _validatePdfFile(context, path);
|
||||
if (error != null && mounted) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: error,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedFilePath = path;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? _validatePdfFile(BuildContext context, String path) {
|
||||
final File file = File(path);
|
||||
if (!file.existsSync()) return context.t.common.file_not_found;
|
||||
final String ext = path.split('.').last.toLowerCase();
|
||||
if (ext != 'pdf') {
|
||||
return context.t.staff_documents.upload.pdf_banner;
|
||||
}
|
||||
final int size = file.lengthSync();
|
||||
if (size > _kMaxFileSizeBytes) {
|
||||
return context.t.staff_documents.upload.pdf_banner;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _selectDate() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
@@ -117,12 +145,8 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t
|
||||
.staff_documents
|
||||
.upload
|
||||
.instructions, // Reusing instructions logic
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
_PdfFileTypesBanner(
|
||||
message: t.staff_documents.upload.pdf_banner,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
|
||||
@@ -135,7 +159,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "e.g. Food Handler Permit",
|
||||
hintText: t.staff_certificates.upload_modal.name_hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
@@ -193,7 +217,7 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
TextField(
|
||||
controller: _issuerController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "e.g. Department of Health",
|
||||
hintText: t.staff_certificates.upload_modal.issuer_hint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
@@ -247,19 +271,33 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
(_selectedFilePath != null &&
|
||||
state.isAttested &&
|
||||
_nameController.text.isNotEmpty)
|
||||
? () =>
|
||||
BlocProvider.of<CertificateUploadCubit>(
|
||||
? () {
|
||||
final String? err =
|
||||
_validatePdfFile(context, _selectedFilePath!);
|
||||
if (err != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
).uploadCertificate(
|
||||
UploadCertificateParams(
|
||||
certificationType: _selectedType!,
|
||||
name: _nameController.text,
|
||||
filePath: _selectedFilePath!,
|
||||
expiryDate: _selectedExpiryDate,
|
||||
issuer: _issuerController.text,
|
||||
certificateNumber: _numberController.text,
|
||||
message: err,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(
|
||||
UiConstants.space4,
|
||||
),
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
BlocProvider.of<CertificateUploadCubit>(
|
||||
context,
|
||||
).uploadCertificate(
|
||||
UploadCertificateParams(
|
||||
certificationType: _selectedType!,
|
||||
name: _nameController.text,
|
||||
filePath: _selectedFilePath!,
|
||||
expiryDate: _selectedExpiryDate,
|
||||
issuer: _issuerController.text,
|
||||
certificateNumber: _numberController.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
@@ -291,6 +329,42 @@ class _CertificateUploadPageState extends State<CertificateUploadPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Banner displaying accepted file types and size limit for PDF upload.
|
||||
class _PdfFileTypesBanner extends StatelessWidget {
|
||||
const _PdfFileTypesBanner({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagActive,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FileSelector extends StatelessWidget {
|
||||
const _FileSelector({this.selectedFilePath, required this.onTap});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -37,18 +39,44 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
||||
String? _selectedFilePath;
|
||||
final FilePickerService _filePicker = Modular.get<FilePickerService>();
|
||||
|
||||
static const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
|
||||
|
||||
Future<void> _pickFile() async {
|
||||
final String? path = await _filePicker.pickFile(
|
||||
allowedExtensions: <String>['pdf'],
|
||||
);
|
||||
|
||||
if (path != null) {
|
||||
final String? error = _validatePdfFile(context, path);
|
||||
if (error != null && mounted) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: error,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedFilePath = path;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? _validatePdfFile(BuildContext context, String path) {
|
||||
final File file = File(path);
|
||||
if (!file.existsSync()) return context.t.common.file_not_found;
|
||||
final String ext = path.split('.').last.toLowerCase();
|
||||
if (ext != 'pdf') {
|
||||
return context.t.staff_documents.upload.pdf_banner;
|
||||
}
|
||||
final int size = file.lengthSync();
|
||||
if (size > _kMaxFileSizeBytes) {
|
||||
return context.t.staff_documents.upload.pdf_banner;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<DocumentUploadCubit>(
|
||||
@@ -82,9 +110,8 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
t.staff_documents.upload.instructions,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
_PdfFileTypesBanner(
|
||||
message: t.staff_documents.upload.pdf_banner,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
DocumentFileSelector(
|
||||
@@ -116,9 +143,22 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
||||
isUploading:
|
||||
state.status == DocumentUploadStatus.uploading,
|
||||
canSubmit: _selectedFilePath != null && state.isAttested,
|
||||
onSubmit: () => BlocProvider.of<DocumentUploadCubit>(
|
||||
context,
|
||||
).uploadDocument(widget.document.id, _selectedFilePath!),
|
||||
onSubmit: () {
|
||||
final String? err =
|
||||
_validatePdfFile(context, _selectedFilePath!);
|
||||
if (err != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: err,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
return;
|
||||
}
|
||||
BlocProvider.of<DocumentUploadCubit>(
|
||||
context,
|
||||
).uploadDocument(widget.document.id, _selectedFilePath!);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -130,3 +170,39 @@ class _DocumentUploadPageState extends State<DocumentUploadPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Banner displaying accepted file types and size limit for PDF upload.
|
||||
class _PdfFileTypesBanner extends StatelessWidget {
|
||||
const _PdfFileTypesBanner({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagActive,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.info, size: 20, color: UiColors.primary),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class DocumentsPage extends StatelessWidget {
|
||||
: t.staff_documents.list.error(
|
||||
message: state.errorMessage!,
|
||||
))
|
||||
: t.staff_documents.list.error(message: 'Unknown'),
|
||||
: t.staff_documents.list.error(message: t.staff_documents.list.unknown),
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body1m.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -31,6 +33,12 @@ class AttireCapturePage extends StatefulWidget {
|
||||
State<AttireCapturePage> createState() => _AttireCapturePageState();
|
||||
}
|
||||
|
||||
/// Maximum file size for attire upload (10MB).
|
||||
const int _kMaxFileSizeBytes = 10 * 1024 * 1024;
|
||||
|
||||
/// Allowed file extensions for attire upload.
|
||||
const Set<String> _kAllowedExtensions = <String>{'jpeg', 'jpg', 'png'};
|
||||
|
||||
class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
String? _selectedLocalPath;
|
||||
|
||||
@@ -57,6 +65,16 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
final GalleryService service = Modular.get<GalleryService>();
|
||||
final String? path = await service.pickImage();
|
||||
if (path != null && context.mounted) {
|
||||
final String? error = _validateFile(path);
|
||||
if (error != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: error,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedLocalPath = path;
|
||||
});
|
||||
@@ -84,6 +102,16 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
final CameraService service = Modular.get<CameraService>();
|
||||
final String? path = await service.takePhoto();
|
||||
if (path != null && context.mounted) {
|
||||
final String? error = _validateFile(path);
|
||||
if (error != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: error,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedLocalPath = path;
|
||||
});
|
||||
@@ -105,7 +133,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Gallery'),
|
||||
title: Text(t.common.gallery),
|
||||
onTap: () {
|
||||
Modular.to.pop();
|
||||
_onGallery(context);
|
||||
@@ -113,7 +141,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Camera'),
|
||||
title: Text(t.common.camera),
|
||||
onTap: () {
|
||||
Modular.to.pop();
|
||||
_onCamera(context);
|
||||
@@ -128,17 +156,33 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
void _showAttestationWarning(BuildContext context) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Please attest that you own this item.',
|
||||
message: t.staff_profile_attire.capture.attest_please,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
}
|
||||
|
||||
/// Validates file format (JPEG, JPG, PNG) and size (max 10MB).
|
||||
/// Returns an error message if invalid, or null if valid.
|
||||
String? _validateFile(String path) {
|
||||
final File file = File(path);
|
||||
if (!file.existsSync()) return t.common.file_not_found;
|
||||
final String ext = path.split('.').last.toLowerCase();
|
||||
if (!_kAllowedExtensions.contains(ext)) {
|
||||
return t.staff_profile_attire.upload_file_types_banner;
|
||||
}
|
||||
final int size = file.lengthSync();
|
||||
if (size > _kMaxFileSizeBytes) {
|
||||
return t.staff_profile_attire.capture.file_size_exceeds;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _showError(BuildContext context, String message) {
|
||||
debugPrint(message);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Could not access camera or gallery. Please try again.',
|
||||
message: t.staff_profile_attire.capture.could_not_access_media,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
@@ -150,6 +194,17 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
);
|
||||
if (_selectedLocalPath == null) return;
|
||||
|
||||
final String? error = _validateFile(_selectedLocalPath!);
|
||||
if (error != null) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: error,
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.all(UiConstants.space4),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await cubit.uploadPhoto(widget.item.id, _selectedLocalPath!);
|
||||
if (context.mounted && cubit.state.status == AttireCaptureStatus.success) {
|
||||
setState(() {
|
||||
@@ -160,10 +215,10 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
|
||||
String _getStatusText(bool hasUploadedPhoto) {
|
||||
return switch (widget.item.verificationStatus) {
|
||||
AttireVerificationStatus.approved => 'Approved',
|
||||
AttireVerificationStatus.rejected => 'Rejected',
|
||||
AttireVerificationStatus.pending => 'Pending Verification',
|
||||
_ => hasUploadedPhoto ? 'Pending Verification' : 'Not Uploaded',
|
||||
AttireVerificationStatus.approved => t.staff_profile_attire.capture.approved,
|
||||
AttireVerificationStatus.rejected => t.staff_profile_attire.capture.rejected,
|
||||
AttireVerificationStatus.pending => t.staff_profile_attire.capture.pending_verification,
|
||||
_ => hasUploadedPhoto ? t.staff_profile_attire.capture.pending_verification : t.staff_profile_attire.capture.not_uploaded,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,7 +262,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
if (state.status == AttireCaptureStatus.success) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Attire image submitted for verification',
|
||||
message: t.staff_profile_attire.capture.attire_submitted,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Modular.to.toAttire();
|
||||
@@ -225,6 +280,10 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_FileTypesBanner(
|
||||
message: t.staff_profile_attire.upload_file_types_banner,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
ImagePreviewSection(
|
||||
selectedLocalPath: _selectedLocalPath,
|
||||
currentPhotoUrl: currentPhotoUrl,
|
||||
@@ -268,3 +327,43 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Banner displaying accepted file types and size limit for attire upload.
|
||||
class _FileTypesBanner extends StatelessWidget {
|
||||
const _FileTypesBanner({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: UiConstants.space3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagActive,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
UiIcons.info,
|
||||
size: 20,
|
||||
color: UiColors.primary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class AttirePage extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'No items found for this filter.',
|
||||
context.t.staff_profile_attire.capture.no_items_filter,
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -28,7 +29,7 @@ class ImagePreviewSection extends StatelessWidget {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Review the attire item',
|
||||
context.t.staff_profile_attire.capture.review_attire_item,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
@@ -42,7 +43,7 @@ class ImagePreviewSection extends StatelessWidget {
|
||||
if (currentPhotoUrl != null) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Text('Your Uploaded Photo', style: UiTypography.body1b.textPrimary),
|
||||
Text(context.t.staff_profile_attire.capture.your_uploaded_photo, style: UiTypography.body1b.textPrimary),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
AttireImagePreview(imageUrl: currentPhotoUrl),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
@@ -56,7 +57,7 @@ class ImagePreviewSection extends StatelessWidget {
|
||||
AttireImagePreview(imageUrl: referenceImageUrl),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Example of the item that you need to upload.',
|
||||
context.t.staff_profile_attire.capture.example_upload_hint,
|
||||
style: UiTypography.body1b.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -77,7 +78,7 @@ class ReferenceExample extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Text('Reference Example', style: UiTypography.body2b.textSecondary),
|
||||
Text(context.t.staff_profile_attire.capture.reference_example, style: UiTypography.body2b.textSecondary),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
|
||||
@@ -317,7 +317,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
children: [
|
||||
const Icon(UiIcons.warning, color: UiColors.error),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(child: Text("Eligibility Requirements")),
|
||||
Expanded(child: Text(context.t.staff_shifts.shift_details.eligibility_requirements)),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
|
||||
@@ -214,6 +214,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
),
|
||||
)
|
||||
: _buildTabContent(
|
||||
state,
|
||||
myShifts,
|
||||
pendingAssignments,
|
||||
cancelledShifts,
|
||||
@@ -232,6 +233,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}
|
||||
|
||||
Widget _buildTabContent(
|
||||
ShiftsState state,
|
||||
List<Shift> myShifts,
|
||||
List<Shift> pendingAssignments,
|
||||
List<Shift> cancelledShifts,
|
||||
@@ -252,7 +254,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
if (availableLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return FindShiftsTab(availableJobs: availableJobs);
|
||||
return FindShiftsTab(
|
||||
availableJobs: availableJobs,
|
||||
profileComplete: state.profileComplete ?? true,
|
||||
);
|
||||
case ShiftTabType.history:
|
||||
if (historyLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
||||
@@ -117,7 +117,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
statusColor = UiColors.textLink;
|
||||
statusBg = UiColors.primary;
|
||||
} else if (status == 'checked_in') {
|
||||
statusText = 'Checked in';
|
||||
statusText = context.t.staff_shifts.my_shift_card.checked_in;
|
||||
statusColor = UiColors.textSuccess;
|
||||
statusBg = UiColors.iconSuccess;
|
||||
} else if (status == 'pending' || status == 'open') {
|
||||
@@ -487,20 +487,22 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_isSubmitted ? 'SUBMITTED' : 'READY TO SUBMIT',
|
||||
_isSubmitted
|
||||
? context.t.staff_shifts.my_shift_card.submitted
|
||||
: context.t.staff_shifts.my_shift_card.ready_to_submit,
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: _isSubmitted ? UiColors.textSuccess : UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (!_isSubmitted)
|
||||
UiButton.secondary(
|
||||
text: 'Submit for Approval',
|
||||
text: context.t.staff_shifts.my_shift_card.submit_for_approval,
|
||||
size: UiButtonSize.small,
|
||||
onPressed: () {
|
||||
setState(() => _isSubmitted = true);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Timesheet submitted for client approval',
|
||||
message: context.t.staff_shifts.my_shift_card.timesheet_submitted,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
@@ -124,7 +125,7 @@ class ShiftLocationSection extends StatelessWidget {
|
||||
if (context.mounted) {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Could not open maps',
|
||||
message: context.t.staff_shifts.shift_location.could_not_open_maps,
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../blocs/shifts/shifts_bloc.dart';
|
||||
import '../my_shift_card.dart';
|
||||
import '../shared/empty_state_view.dart';
|
||||
@@ -12,7 +13,15 @@ import 'package:geolocator/geolocator.dart';
|
||||
class FindShiftsTab extends StatefulWidget {
|
||||
final List<Shift> availableJobs;
|
||||
|
||||
const FindShiftsTab({super.key, required this.availableJobs});
|
||||
/// Whether the worker's profile is complete. When false, shows incomplete
|
||||
/// profile banner and disables apply actions.
|
||||
final bool profileComplete;
|
||||
|
||||
const FindShiftsTab({
|
||||
super.key,
|
||||
required this.availableJobs,
|
||||
this.profileComplete = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
||||
@@ -305,6 +314,15 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Incomplete profile banner
|
||||
if (!widget.profileComplete) ...[
|
||||
_IncompleteProfileBanner(
|
||||
title: context.t.staff_shifts.find_shifts.incomplete_profile_banner_title,
|
||||
message: context.t.staff_shifts.find_shifts.incomplete_profile_banner_message,
|
||||
ctaText: context.t.staff_shifts.find_shifts.incomplete_profile_cta,
|
||||
onCtaPressed: () => Modular.to.toProfile(),
|
||||
),
|
||||
],
|
||||
// Search and Filters
|
||||
Container(
|
||||
color: UiColors.white,
|
||||
@@ -440,20 +458,24 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
onAccept: () {
|
||||
context.read<ShiftsBloc>().add(
|
||||
AcceptShiftEvent(shift.id),
|
||||
);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.application_submitted,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
onAccept: widget.profileComplete
|
||||
? () {
|
||||
BlocProvider.of<ShiftsBloc>(
|
||||
context,
|
||||
).add(
|
||||
AcceptShiftEvent(shift.id),
|
||||
);
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: context
|
||||
.t
|
||||
.staff_shifts
|
||||
.find_shifts
|
||||
.application_submitted,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -466,3 +488,54 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Banner shown when the worker's profile is incomplete.
|
||||
class _IncompleteProfileBanner extends StatelessWidget {
|
||||
const _IncompleteProfileBanner({
|
||||
required this.title,
|
||||
required this.message,
|
||||
required this.ctaText,
|
||||
required this.onCtaPressed,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String message;
|
||||
final String ctaText;
|
||||
final VoidCallback onCtaPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.all(UiConstants.space5),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagPending,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(
|
||||
color: UiColors.textWarning.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: UiTypography.body2b.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
message,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
UiButton.primary(
|
||||
text: ctaText,
|
||||
onPressed: onCtaPressed,
|
||||
size: UiButtonSize.small,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,28 +104,28 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
void _confirmShift(String id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.title),
|
||||
content: Text(t.staff_shifts.my_shifts_tab.confirm_dialog.message),
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(context.t.staff_shifts.my_shifts_tab.confirm_dialog.title),
|
||||
content: Text(context.t.staff_shifts.my_shifts_tab.confirm_dialog.message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(context.t.common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<ShiftsBloc>().add(AcceptShiftEvent(id));
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.staff_shifts.my_shifts_tab.confirm_dialog.success,
|
||||
message: context.t.staff_shifts.my_shifts_tab.confirm_dialog.success,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.success,
|
||||
),
|
||||
child: Text(t.staff_shifts.shift_details.accept_shift),
|
||||
child: Text(context.t.staff_shifts.shift_details.accept_shift),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -135,30 +135,30 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
void _declineShift(String id) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.staff_shifts.my_shifts_tab.decline_dialog.title),
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(context.t.staff_shifts.my_shifts_tab.decline_dialog.title),
|
||||
content: Text(
|
||||
t.staff_shifts.my_shifts_tab.decline_dialog.message,
|
||||
context.t.staff_shifts.my_shifts_tab.decline_dialog.message,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(context.t.common.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(ctx).pop();
|
||||
context.read<ShiftsBloc>().add(DeclineShiftEvent(id));
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.staff_shifts.my_shifts_tab.decline_dialog.success,
|
||||
message: context.t.staff_shifts.my_shifts_tab.decline_dialog.success,
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
),
|
||||
child: Text(t.staff_shifts.shift_details.decline),
|
||||
child: Text(context.t.staff_shifts.shift_details.decline),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -169,9 +169,9 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
final now = DateTime.now();
|
||||
if (_isSameDay(date, now)) return t.staff_shifts.my_shifts_tab.date.today;
|
||||
if (_isSameDay(date, now)) return context.t.staff_shifts.my_shifts_tab.date.today;
|
||||
final tomorrow = now.add(const Duration(days: 1));
|
||||
if (_isSameDay(date, tomorrow)) return t.staff_shifts.my_shifts_tab.date.tomorrow;
|
||||
if (_isSameDay(date, tomorrow)) return context.t.staff_shifts.my_shifts_tab.date.tomorrow;
|
||||
return DateFormat('EEE, MMM d').format(date);
|
||||
} catch (_) {
|
||||
return dateStr;
|
||||
@@ -338,7 +338,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
if (widget.pendingAssignments.isNotEmpty) ...[
|
||||
_buildSectionHeader(
|
||||
t.staff_shifts.my_shifts_tab.sections.awaiting,
|
||||
context.t.staff_shifts.my_shifts_tab.sections.awaiting,
|
||||
UiColors.textWarning,
|
||||
),
|
||||
...widget.pendingAssignments.map(
|
||||
@@ -356,7 +356,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
],
|
||||
|
||||
if (visibleCancelledShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader(t.staff_shifts.my_shifts_tab.sections.cancelled, UiColors.textSecondary),
|
||||
_buildSectionHeader(context.t.staff_shifts.my_shifts_tab.sections.cancelled, UiColors.textSecondary),
|
||||
...visibleCancelledShifts.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
@@ -378,7 +378,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
|
||||
// Confirmed Shifts
|
||||
if (visibleMyShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader(t.staff_shifts.my_shifts_tab.sections.confirmed, UiColors.textSecondary),
|
||||
_buildSectionHeader(context.t.staff_shifts.my_shifts_tab.sections.confirmed, UiColors.textSecondary),
|
||||
...visibleMyShifts.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
@@ -388,7 +388,7 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
onRequestSwap: () {
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: "Swap functionality coming soon!", // Todo: Localization
|
||||
message: context.t.staff_shifts.my_shifts_tab.swap_coming_soon,
|
||||
type: UiSnackbarType.message,
|
||||
);
|
||||
},
|
||||
@@ -402,8 +402,8 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
widget.cancelledShifts.isEmpty)
|
||||
EmptyStateView(
|
||||
icon: UiIcons.calendar,
|
||||
title: t.staff_shifts.my_shifts_tab.empty.title,
|
||||
subtitle: t.staff_shifts.my_shifts_tab.empty.subtitle,
|
||||
title: context.t.staff_shifts.my_shifts_tab.empty.title,
|
||||
subtitle: context.t.staff_shifts.my_shifts_tab.empty.subtitle,
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space32),
|
||||
@@ -472,13 +472,13 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
t.staff_shifts.my_shifts_tab.card.cancelled,
|
||||
context.t.staff_shifts.my_shifts_tab.card.cancelled,
|
||||
style: UiTypography.footnote2b.textError,
|
||||
),
|
||||
if (isLastMinute) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
t.staff_shifts.my_shifts_tab.card.compensation,
|
||||
context.t.staff_shifts.my_shifts_tab.card.compensation,
|
||||
style: UiTypography.footnote2m.textSuccess,
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user