Fix build errors: localization syntax, key paths, and ViewOrderCard widget

This commit is contained in:
2026-02-14 16:26:10 +05:30
parent 097481d26a
commit 4e1a41ebff
25 changed files with 420 additions and 207 deletions

View File

@@ -238,7 +238,7 @@ class AuthRepositoryImpl
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user;
return user != null && user.userRole == 'BUSINESS';
return user != null && (user.userRole == 'BUSINESS' || user.userRole == 'BOTH');
}
/// Creates Business and User entities in PostgreSQL for a Firebase user.
@@ -261,18 +261,28 @@ class AuthRepositoryImpl
final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert;
onBusinessCreated(businessData.id);
// Create User entity in PostgreSQL
// Check if User entity already exists in PostgreSQL
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> userResult =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUser.uid).execute());
final dc.GetUserByIdUser? existingUser = userResult.data.user;
final OperationResult<dc.CreateUserData, dc.CreateUserVariables> createUserResponse =
await executeProtected(() => _dataConnect.createUser(
id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
.email(email)
.userRole('BUSINESS')
.execute());
final dc.CreateUserUserInsert newUserData = createUserResponse.data.user_insert;
if (existingUser != null) {
// User exists (likely in another app like STAFF). Update role to BOTH.
await executeProtected(() => _dataConnect.updateUser(
id: firebaseUser.uid,
)
.userRole('BOTH')
.execute());
} else {
// Create new User entity in PostgreSQL
await executeProtected(() => _dataConnect.createUser(
id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
.email(email)
.userRole('BUSINESS')
.execute());
}
return _getUserProfile(
firebaseUserId: firebaseUser.uid,
@@ -331,11 +341,11 @@ class AuthRepositoryImpl
technicalMessage: 'Firebase UID $firebaseUserId not found in users table',
);
}
if (requireBusinessRole && user.userRole != 'BUSINESS') {
if (requireBusinessRole && user.userRole != 'BUSINESS' && user.userRole != 'BOTH') {
await _firebaseAuth.signOut();
dc.ClientSessionStore.instance.clear();
throw UnauthorizedAppException(
technicalMessage: 'User role is ${user.userRole}, expected BUSINESS',
technicalMessage: 'User role is ${user.userRole}, expected BUSINESS or BOTH',
);
}

View File

@@ -27,94 +27,108 @@ class ClientGetStartedPage extends StatelessWidget {
),
SafeArea(
child: Column(
children: <Widget>[
const SizedBox(height: UiConstants.space10),
// Logo
Center(
child: Image.asset(
UiImageAssets.logoBlue,
height: 40,
fit: BoxFit.contain,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
children: <Widget>[
const SizedBox(height: UiConstants.space10),
// Logo
Center(
child: Image.asset(
UiImageAssets.logoBlue,
height: 40,
fit: BoxFit.contain,
),
),
const Spacer(),
// Content Cards Area (Keeping prototype layout)
Container(
height: 300,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
),
child: Stack(
children: <Widget>[
// Representative cards from prototype
Positioned(
top: 20,
left: 0,
right: 20,
child: _ShiftOrderCard(),
),
Positioned(
bottom: 40,
right: 0,
left: 40,
child: _WorkerProfileCard(),
),
Positioned(
top: 60,
right: 10,
child: _CalendarCard(),
),
],
),
),
const Spacer(),
// Bottom Content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
vertical: UiConstants.space10,
),
child: Column(
children: <Widget>[
Text(
t.client_authentication.get_started_page.title,
textAlign: TextAlign.center,
style: UiTypography.displayM,
),
const SizedBox(height: UiConstants.space3),
Text(
t.client_authentication.get_started_page
.subtitle,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space8),
// Sign In Button
UiButton.primary(
text: t.client_authentication.get_started_page
.sign_in_button,
onPressed: () => Modular.to.toClientSignIn(),
fullWidth: true,
),
const SizedBox(height: UiConstants.space3),
// Create Account Button
UiButton.secondary(
text: t.client_authentication.get_started_page
.create_account_button,
onPressed: () => Modular.to.toClientSignUp(),
fullWidth: true,
),
],
),
),
],
),
),
),
),
const Spacer(),
// Content Cards Area (Keeping prototype layout)
Container(
height: 300,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
),
child: Stack(
children: <Widget>[
// Representative cards from prototype
Positioned(
top: 20,
left: 0,
right: 20,
child: _ShiftOrderCard(),
),
Positioned(
bottom: 40,
right: 0,
left: 40,
child: _WorkerProfileCard(),
),
Positioned(top: 60, right: 10, child: _CalendarCard()),
],
),
),
const Spacer(),
// Bottom Content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
vertical: UiConstants.space10,
),
child: Column(
children: <Widget>[
Text(
t.client_authentication.get_started_page.title,
textAlign: TextAlign.center,
style: UiTypography.displayM,
),
const SizedBox(height: UiConstants.space3),
Text(
t.client_authentication.get_started_page.subtitle,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space8),
// Sign In Button
UiButton.primary(
text: t
.client_authentication
.get_started_page
.sign_in_button,
onPressed: () => Modular.to.toClientSignIn(),
fullWidth: true,
),
const SizedBox(height: UiConstants.space3),
// Create Account Button
UiButton.secondary(
text: t
.client_authentication
.get_started_page
.create_account_button,
onPressed: () => Modular.to.toClientSignUp(),
fullWidth: true,
),
],
),
),
],
);
},
),
),
],

View File

@@ -151,10 +151,12 @@ class CoverageRepositoryImpl implements CoverageRepository {
shiftId: app.shiftId,
roleId: app.roleId,
title: app.shiftRole.role.name,
location: app.shiftRole.shift.location ?? '',
startTime: '00:00',
workersNeeded: 0,
date: date,
location: app.shiftRole.shift.location ??
app.shiftRole.shift.locationAddress ??
'',
startTime: _formatTime(app.shiftRole.startTime) ?? '00:00',
workersNeeded: app.shiftRole.count,
date: app.shiftRole.shift.date?.toDateTime() ?? date,
workers: <CoverageWorker>[],
);

View File

@@ -63,6 +63,8 @@ class OneTimeOrderSuccessView extends StatelessWidget {
color: UiColors.accent,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
UiIcons.check,

View File

@@ -72,6 +72,12 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
TextPosition(offset: _messageController.text.length),
);
}
} else if (state is RapidOrderFailure) {
UiSnackbar.show(
context,
message: translateErrorKey(state.error),
type: UiSnackbarType.error,
);
}
},
child: Scaffold(

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
@@ -47,7 +48,7 @@ class CoverageWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"TODAY'S COVERAGE",
t.client_home.dashboard.todays_coverage,
style: UiTypography.footnote1b.copyWith(
color: UiColors.textPrimary,
letterSpacing: 0.5,
@@ -64,8 +65,8 @@ class CoverageWidget extends StatelessWidget {
color: backgroundColor,
borderRadius: UiConstants.radiusLg,
),
child: Text(
'$coveragePercent% Covered',
child: Text(
t.client_home.dashboard.percent_covered(percent: coveragePercent),
style: UiTypography.footnote2b.copyWith(color: textColor),
),
),
@@ -81,7 +82,7 @@ class CoverageWidget extends StatelessWidget {
child: _MetricCard(
icon: UiIcons.target,
iconColor: UiColors.primary,
label: 'Needed',
label: t.client_home.dashboard.metric_needed,
value: '$totalNeeded',
),
),
@@ -91,7 +92,7 @@ class CoverageWidget extends StatelessWidget {
child: _MetricCard(
icon: UiIcons.success,
iconColor: UiColors.iconSuccess,
label: 'Filled',
label: t.client_home.dashboard.metric_filled,
value: '$totalConfirmed',
valueColor: UiColors.textSuccess,
),
@@ -101,7 +102,7 @@ class CoverageWidget extends StatelessWidget {
child: _MetricCard(
icon: UiIcons.error,
iconColor: UiColors.iconError,
label: 'Open',
label: t.client_home.dashboard.metric_open,
value: '${totalNeeded - totalConfirmed}',
valueColor: UiColors.textError,
),

View File

@@ -65,6 +65,8 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub;
bool _showSuccess = false;
Map<String, dynamic>? _submitData;
bool _isSubmitting = false;
String? _errorMessage;
@override
void initState() {
@@ -190,7 +192,25 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
}
Future<void> _handleSubmit() async {
await _submitNewOrder();
if (_isSubmitting) return;
setState(() {
_isSubmitting = true;
_errorMessage = null;
});
try {
await _submitNewOrder();
} catch (e) {
if (!mounted) return;
setState(() {
_isSubmitting = false;
_errorMessage = 'Failed to create order. Please try again.';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(_errorMessage!)),
);
}
}
Future<void> _submitNewOrder() async {
@@ -296,6 +316,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
'date': _dateController.text,
};
_showSuccess = true;
_isSubmitting = false;
});
}
@@ -770,7 +791,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
UiButton.primary(
text: widget.initialData != null ? 'Update Order' : 'Post Order',
onPressed: widget.isLoading ? null : _handleSubmit,
onPressed: (widget.isLoading || _isSubmitting) ? null : _handleSubmit,
),
SizedBox(height: MediaQuery.of(context).padding.bottom + UiConstants.space5),
],

View File

@@ -78,7 +78,7 @@ class SpendingWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'This Week',
t.client_home.dashboard.spending.this_week,
style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
fontSize: 9,
@@ -93,7 +93,7 @@ class SpendingWidget extends StatelessWidget {
),
),
Text(
'$weeklyShifts shifts',
t.client_home.dashboard.spending.shifts_count(count: weeklyShifts),
style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.6),
fontSize: 9,
@@ -107,7 +107,7 @@ class SpendingWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
'Next 7 Days',
t.client_home.dashboard.spending.next_7_days,
style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.7),
fontSize: 9,
@@ -122,7 +122,7 @@ class SpendingWidget extends StatelessWidget {
),
),
Text(
'$next7DaysScheduled scheduled',
t.client_home.dashboard.spending.scheduled_count(count: next7DaysScheduled),
style: UiTypography.footnote2r.white.copyWith(
color: UiColors.white.withValues(alpha: 0.6),
fontSize: 9,

View File

@@ -227,23 +227,23 @@ class ClientHubsPage extends StatelessWidget {
}
Future<void> _confirmDeleteHub(BuildContext context, Hub hub) async {
final String hubName = hub.name.isEmpty ? 'this hub' : hub.name;
final String hubName = hub.name.isEmpty ? t.client_hubs.title : hub.name;
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Confirm Hub Deletion'),
title: Text(t.client_hubs.delete_dialog.title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Are you sure you want to delete "$hubName"?'),
Text(t.client_hubs.delete_dialog.message(hubName: hubName)),
const SizedBox(height: UiConstants.space2),
const Text('This action cannot be undone.'),
Text(t.client_hubs.delete_dialog.undo_warning),
const SizedBox(height: UiConstants.space2),
Text(
'Note that if there are any shifts/orders assigned to this hub we shouldn\'t be able to delete the hub.',
t.client_hubs.delete_dialog.dependency_warning,
style: UiTypography.footnote1r.copyWith(
color: UiColors.textSecondary,
),
@@ -253,7 +253,7 @@ class ClientHubsPage extends StatelessWidget {
actions: <Widget>[
TextButton(
onPressed: () => Modular.to.pop(),
child: const Text('Cancel'),
child: Text(t.client_hubs.delete_dialog.cancel),
),
TextButton(
onPressed: () {
@@ -265,7 +265,7 @@ class ClientHubsPage extends StatelessWidget {
style: TextButton.styleFrom(
foregroundColor: UiColors.destructive,
),
child: const Text('Delete'),
child: Text(t.client_hubs.delete_dialog.delete),
),
],
);

View File

@@ -89,8 +89,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
final DateTime tomorrow = today.add(const Duration(days: 1));
final DateTime checkDate = DateTime(date.year, date.month, date.day);
if (checkDate == today) return 'Today';
if (checkDate == tomorrow) return 'Tomorrow';
if (checkDate == today) return t.client_view_orders.card.today;
if (checkDate == tomorrow) return t.client_view_orders.card.tomorrow;
return DateFormat('EEE, MMM d').format(date);
} catch (_) {
return dateStr;
@@ -279,19 +279,19 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
_buildStatItem(
icon: UiIcons.dollar,
value: '\$${cost.round()}',
label: 'Total',
label: t.client_view_orders.card.total,
),
_buildStatDivider(),
_buildStatItem(
icon: UiIcons.clock,
value: hours.toStringAsFixed(1),
label: 'Hrs',
label: t.client_view_orders.card.hrs,
),
_buildStatDivider(),
_buildStatItem(
icon: UiIcons.users,
value: '${order.workersNeeded}',
label: 'Workers',
label: t.client_create_order.one_time.workers_label,
),
],
),
@@ -303,14 +303,14 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
children: <Widget>[
Expanded(
child: _buildTimeDisplay(
label: 'Clock In',
label: t.client_view_orders.card.clock_in,
time: _formatTime(timeStr: order.startTime),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _buildTimeDisplay(
label: 'Clock Out',
label: t.client_view_orders.card.clock_out,
time: _formatTime(timeStr: order.endTime),
),
),
@@ -341,8 +341,8 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
const SizedBox(width: UiConstants.space2),
Text(
coveragePercent == 100
? 'All Workers Confirmed'
: '${order.workersNeeded} Workers Needed',
? t.client_view_orders.card.all_confirmed
: t.client_view_orders.card.workers_needed(count: order.workersNeeded),
style: UiTypography.body2m.textPrimary,
),
],
@@ -378,7 +378,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
Padding(
padding: const EdgeInsets.only(left: 12),
child: Text(
'+${order.confirmedApps.length - 3} more',
t.client_view_orders.card.show_more_workers(count: order.confirmedApps.length - 3),
style: UiTypography.footnote2r.textSecondary,
),
),
@@ -408,13 +408,13 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'CONFIRMED WORKERS',
t.client_view_orders.card.confirmed_workers_title,
style: UiTypography.footnote2b.textSecondary,
),
GestureDetector(
onTap: () {},
child: Text(
'Message All',
t.client_view_orders.card.message_all,
style: UiTypography.footnote2b.copyWith(
color: UiColors.primary,
),
@@ -433,7 +433,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
child: TextButton(
onPressed: () {},
child: Text(
'Show ${order.confirmedApps.length - 5} more workers',
t.client_view_orders.card.show_more_workers(count: order.confirmedApps.length - 5),
style: UiTypography.body2m.copyWith(
color: UiColors.primary,
),
@@ -569,7 +569,7 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
borderRadius: UiConstants.radiusSm,
),
child: Text(
'Checked In',
t.client_view_orders.card.checked_in,
style: UiTypography.titleUppercase4m.copyWith(
color: UiColors.textSuccess,
),
@@ -615,16 +615,16 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Call'),
content: Text('Do you want to call $phone?'),
title: Text(t.client_view_orders.card.call_dialog.title),
content: Text(t.client_view_orders.card.call_dialog.message(phone: phone)),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
child: Text(t.common.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Call'),
child: Text(t.client_view_orders.card.call_dialog.title),
),
],
);