This commit is contained in:
Surendiran 2026-07-24 14:01:50 +05:30
parent b3a0366848
commit 167bd99e0a
39 changed files with 559 additions and 344 deletions

View File

@ -46,25 +46,25 @@ dart run build_runner build --delete-conflicting-outputs
Each environment has its own entry point in `lib/config/` that sets `Environment.flavor`.
API URL and `.env` file are picked automatically (see `lib/core/config/environment.dart`).
| Flavor | Entry point | API URL (auto) | Web URL | Web base-href |
|--------|-------------|----------------|---------|---------------|
| **dev** | `lib/config/main_dev.dart` | `https://demo.venbait.in/api/v1` | `https://bharatconsumerproducts.com/erp/login` | `/erp/` |
| **uat** | `lib/config/main_uat.dart` | `https://uat-api.bharaterp.com/api/v1` | — | `/` |
| **prod** | `lib/config/main_prod.dart` | `https://api.bharaterp.com/api/v1` | — | `/app/` |
| Flavor | Entry point | API URL (auto) | Web base-href |
|--------|-------------|----------------|---------------|
| **dev** | `lib/config/main_dev.dart` | `https://demo.venbait.in/api/v1` | `/` |
| **uat** | `lib/config/main_uat.dart` | `https://uat-api.bharaterp.com/api/v1` | `/` |
| **prod** | `lib/config/main_prod.dart` | `https://api.bharaterp.com/api/v1` | `/` |
Dev web uses **path URLs** (no `index.html#`). Build with `--base-href /erp/` and deploy `web/.htaccess` for Apache SPA fallback.
Dev web uses **path URLs** (no `index.html#`). Build with `--base-href /` and deploy `web/.htaccess` for Apache/LiteSpeed SPA fallback.
### Web builds
```bash
# Dev
flutter build web -t lib/config/main_dev.dart --release --base-href /erp/
flutter build web -t lib/config/main_dev.dart --release --base-href /
# UAT
flutter build web -t lib/config/main_uat.dart --release --base-href /
# Prod
flutter build web -t lib/config/main_prod.dart --release --base-href /app/
flutter build web -t lib/config/main_prod.dart --release --base-href /
```
### Android APK / AAB

View File

@ -17,20 +17,6 @@ class Environment {
Flavor.prod => 'production',
};
/// Web deploy path (`--base-href` should match this value).
static String get baseHref => switch (flavor) {
Flavor.dev => '/erp/',
Flavor.uat => '/',
Flavor.prod => '/app/',
};
/// Public web app URL (path-based routing no `index.html#`).
static String get webAppUrl => switch (flavor) {
Flavor.dev => 'https://bharatconsumerproducts.com/erp',
Flavor.uat => 'https://uat.bharaterp.com',
Flavor.prod => 'https://app.bharaterp.com',
};
static String get envFileName => switch (flavor) {
Flavor.dev => '.env.development',
Flavor.uat => '.env.uat',

View File

@ -201,10 +201,10 @@ class AppTheme {
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: textTheme.bodyMedium,
labelStyle: textTheme.labelLarge,
hintStyle: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant),
helperStyle: textTheme.bodySmall,
errorStyle: textTheme.bodySmall?.copyWith(color: colorScheme.error),
errorStyle: textTheme.labelSmall?.copyWith(color: colorScheme.error),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(

View File

@ -1,57 +1,189 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// ERP typography Figtree (Google Font).
/// Weights: Regular 400, Medium 500, SemiBold 600.
/// ERP typography Figtree design tokens.
///
/// | Token | Sizes | Weights |
/// |--------------|-------|----------------------------------|
/// | Heading 16 | 5620 | Medium, SemiBold, Bold |
/// | Body 14 | 1812 | Regular, Medium |
/// | Label 13 | 1612 | Regular, Medium, SemiBold, Bold |
/// | Caption 12 | 109 | Medium, SemiBold |
class AppTypography {
AppTypography._();
static const FontWeight regular = FontWeight.w400;
static const FontWeight medium = FontWeight.w500;
static const FontWeight semiBold = FontWeight.w600;
static const FontWeight bold = FontWeight.w700;
static String get fontFamily => GoogleFonts.figtree().fontFamily ?? 'Figtree';
static TextTheme textTheme(ColorScheme colorScheme) {
TextStyle token({
required double size,
FontWeight weight = regular,
Color? color,
double? height,
}) {
return GoogleFonts.figtree(
fontSize: size,
fontWeight: weight,
letterSpacing: 0,
height: height,
color: color ?? colorScheme.onSurface,
);
}
// Design tokens
static TextStyle heading1({
FontWeight weight = semiBold,
Color? color,
double? height,
}) =>
_token(56, weight, color, height);
static TextStyle heading2({
FontWeight weight = semiBold,
Color? color,
double? height,
}) =>
_token(48, weight, color, height);
static TextStyle heading3({
FontWeight weight = semiBold,
Color? color,
double? height,
}) =>
_token(40, weight, color, height);
static TextStyle heading4({
FontWeight weight = semiBold,
Color? color,
double? height,
}) =>
_token(32, weight, color, height);
static TextStyle heading5({
FontWeight weight = semiBold,
Color? color,
double? height,
}) =>
_token(24, weight, color, height);
static TextStyle heading6({
FontWeight weight = semiBold,
Color? color,
double? height,
}) =>
_token(20, weight, color, height);
static TextStyle body1({
FontWeight weight = regular,
Color? color,
double? height,
}) =>
_token(18, weight, color, height);
static TextStyle body2({
FontWeight weight = regular,
Color? color,
double? height,
}) =>
_token(16, weight, color, height);
static TextStyle body3({
FontWeight weight = regular,
Color? color,
double? height,
}) =>
_token(14, weight, color, height);
static TextStyle body4({
FontWeight weight = regular,
Color? color,
double? height,
}) =>
_token(12, weight, color, height);
static TextStyle label1({
FontWeight weight = medium,
Color? color,
double? height,
}) =>
_token(16, weight, color, height);
static TextStyle label2({
FontWeight weight = medium,
Color? color,
double? height,
}) =>
_token(14, weight, color, height);
static TextStyle label3({
FontWeight weight = medium,
Color? color,
double? height,
}) =>
_token(12, weight, color, height);
static TextStyle caption1({
FontWeight weight = medium,
Color? color,
double? height,
}) =>
_token(10, weight, color, height);
static TextStyle caption2({
FontWeight weight = medium,
Color? color,
double? height,
}) =>
_token(9, weight, color, height);
// Material TextTheme mapping
/// Maps design tokens onto Material slots used across the app.
///
/// | Material slot | Token | Default weight |
/// |-----------------|-----------|----------------|
/// | displayLarge | Heading-1 | SemiBold |
/// | displayMedium | Heading-2 | SemiBold |
/// | displaySmall | Heading-3 | SemiBold |
/// | headlineLarge | Heading-4 | SemiBold |
/// | headlineMedium | Heading-5 | SemiBold |
/// | headlineSmall | Heading-6 | SemiBold |
/// | titleLarge | Heading-5 | SemiBold |
/// | titleMedium | Label-1 | Medium |
/// | titleSmall | Label-2 | Medium |
/// | bodyLarge | Body-1 | Regular |
/// | bodyMedium | Body-2 | Regular |
/// | bodySmall | Body-3 | Regular |
/// | labelLarge | Label-2 | Medium |
/// | labelMedium | Label-3 | Medium |
/// | labelSmall | Caption-1 | Medium |
///
/// Use [body4], [caption2], or weight variants via the named helpers above.
static TextTheme textTheme(ColorScheme colorScheme) {
final onSurface = colorScheme.onSurface;
final onSurfaceVariant = colorScheme.onSurfaceVariant;
return TextTheme(
// Display
displayLarge: token(size: 57, weight: semiBold, color: onSurface),
displayMedium: token(size: 45, weight: semiBold, color: onSurface),
displaySmall: token(size: 36, weight: semiBold, color: onSurface),
// Headline
headlineLarge: token(size: 20, weight: semiBold, color: onSurface),
headlineMedium: token(size: 16, weight: regular, color: onSurface),
headlineSmall: token(size: 18, weight: semiBold, color: onSurface),
// Title
titleLarge: token(size: 22, weight: semiBold, color: onSurface),
titleMedium: token(size: 16, weight: medium, color: onSurface),
titleSmall: token(size: 14, weight: medium, color: onSurface),
// Body
bodyLarge: token(size: 16, weight: regular, color: onSurface),
bodyMedium: token(size: 16, weight: regular, color: onSurface),
bodySmall: token(size: 12, weight: regular, color: onSurfaceVariant),
// Label
labelLarge: token(size: 14, weight: medium, color: onSurface),
labelMedium: token(size: 12, weight: medium, color: onSurfaceVariant),
labelSmall: token(size: 10, weight: regular, color: onSurfaceVariant),
displayLarge: heading1(color: onSurface),
displayMedium: heading2(color: onSurface),
displaySmall: heading3(color: onSurface),
headlineLarge: heading4(color: onSurface),
headlineMedium: heading5(color: onSurface),
headlineSmall: heading6(color: onSurface),
titleLarge: heading5(color: onSurface),
titleMedium: label1(color: onSurface),
titleSmall: label2(color: onSurface),
bodyLarge: body1(color: onSurface),
bodyMedium: body2(color: onSurface),
bodySmall: body3(color: onSurfaceVariant),
labelLarge: label2(color: onSurface),
labelMedium: label3(color: onSurfaceVariant),
labelSmall: caption1(color: onSurfaceVariant),
);
}
static TextStyle _token(
double size,
FontWeight weight,
Color? color,
double? height,
) {
return GoogleFonts.figtree(
fontSize: size,
fontWeight: weight,
letterSpacing: 0,
height: height,
color: color,
);
}
}

View File

@ -346,7 +346,6 @@ class _AlertCard extends StatelessWidget {
style: theme.textTheme.bodySmall?.copyWith(
color: visual.color,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
@ -374,7 +373,6 @@ class _AlertCard extends StatelessWidget {
style: theme.textTheme.bodySmall?.copyWith(
color: visual.color,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),

View File

@ -5,12 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import '../../../../core/constants/enums.dart';
import '../../../../core/constants/route_constants.dart';
import '../../../../core/errors/failure.dart';
import '../../../../core/utils/formatters.dart';
import '../../../../core/utils/validators.dart';
import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/user_management_models.dart' show FilterOptionModel;
import '../../../../shared/providers/permissions_provider.dart';
import '../../../../shared/utils/navigation_utils.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_date_popup.dart';
@ -22,8 +24,10 @@ import '../../../../shared/widgets/app_side_panel.dart';
import '../../../../shared/widgets/app_sticky_form_layout.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_toast.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/error_view.dart';
import '../../../master_data/presentation/widgets/master_quick_add.dart';
import '../../../vendors/presentation/widgets/vendor_form_panel.dart';
import '../../data/repositories/asset_repository_impl.dart';
import '../providers/asset_categories_provider.dart';
import '../providers/asset_form_lookups_provider.dart';
@ -486,7 +490,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
if (mounted) {
showAppToastFromSnackBar(
context,
SnackBar(content: Text(e.toString())),
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
@ -781,12 +785,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
children: [
FormRowFour(
children: [
_optionalLookupDropdown(
label: 'Vendor',
value: _vendorId,
options: lookups.vendors,
onChanged: (v) => setState(() => _vendorId = v),
),
_vendorDropdown(lookups.vendors),
_optionalLookupDropdown(
label: 'Purchase Order',
value: _poId,
@ -1336,6 +1335,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
searchHint: 'Search ${label.toLowerCase()}...',
isDense: true,
enabled: fieldEnabled,
openInSidePanel: true,
options: dropdownOptions,
refreshLookups: () {
ref.invalidate(assetFormLookupsProvider);
@ -1371,6 +1371,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
searchHint: 'Search subcategory...',
isDense: true,
enabled: hasCategory,
openInSidePanel: true,
options: options,
initialValues: {'item_category_id': _categoryId},
refreshLookups: () {
@ -1395,7 +1396,9 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
value: _dropdownValue(_categoryId, categoryIds),
searchHint: 'Search category...',
isDense: true,
openInSidePanel: true,
initialValues: const {'category_type': 'ASSET'},
readOnlyFields: const {'category_type'},
options: activeCategories
.map(
(c) => AppDropdownOption(
@ -1432,16 +1435,56 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
);
}
Widget _vendorDropdown(List<FilterOptionModel> vendors) {
final vendorIds = vendors
.map((vendor) => int.tryParse(vendor.id))
.whereType<int>()
.toList();
final canCreateVendor = ref.can('vendors', PermissionAction.create);
return AppSearchableDropdown<int?>(
label: 'Vendor',
value: _dropdownValue(_vendorId, vendorIds),
hint: 'None',
searchHint: 'Search vendor...',
isDense: true,
options: <AppDropdownOption<int?>>[
const AppDropdownOption<int?>(value: null, label: 'None'),
...vendors
.map(
(vendor) => AppDropdownOption<int?>(
value: int.tryParse(vendor.id),
label: vendor.name,
),
)
.where((option) => option.value != null && option.value! > 0),
],
onChanged: (v) => setState(() => _vendorId = v),
addNewLabel: canCreateVendor ? 'Add vendor' : null,
onAddNew: !canCreateVendor
? null
: () async {
final createdId = await openVendorFormPanel(context, ref);
if (!mounted || createdId == null) return;
ref.invalidate(assetFormLookupsProvider);
await ref.read(assetFormLookupsProvider.future);
if (!mounted) return;
setState(() => _vendorId = int.tryParse(createdId));
},
);
}
Widget _locationDropdown(List<FilterOptionModel> locations) {
final locationIds = locations
.map((location) => int.tryParse(location.id))
.whereType<int>()
.toList();
return AppSearchableDropdown<int>(
return MasterQuickAddDropdown<int>(
masterId: 'locations',
label: 'Location *',
value: _dropdownValue(_locationId, locationIds),
searchHint: 'Search plant or warehouse...',
isDense: true,
openInSidePanel: true,
options: locations
.map(
(location) => AppDropdownOption(
@ -1451,6 +1494,11 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
)
.where((option) => option.value != 0)
.toList(),
refreshLookups: () async {
ref.invalidate(assetFormLookupsProvider);
await ref.read(assetFormLookupsProvider.future);
},
parseCreatedId: int.tryParse,
onChanged: (v) => setState(() => _locationId = v),
validator: (v) => v == null ? 'Location is required' : null,
);

View File

@ -4,6 +4,7 @@ import 'package:intl/intl.dart';
import '../../../../core/utils/formatters.dart';
import '../../../../shared/models/asset_model.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_date_popup.dart';
import '../../../../shared/widgets/app_dropdown.dart';
@ -156,7 +157,7 @@ class _SubmitMaintenanceLogPanelState
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) {
if (mounted) {
showSidePanelSnackBar(context, e.toString());
showSidePanelApiError(context, e);
}
} finally {
if (mounted) setState(() => _isSubmitting = false);

View File

@ -207,7 +207,7 @@ class _AddAmcPanelState extends ConsumerState<AddAmcPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) {
if (mounted) {
showSidePanelSnackBar(context, e.toString());
showSidePanelApiError(context, e);
}
} finally {
if (mounted) setState(() => _isSubmitting = false);
@ -620,7 +620,7 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) {
if (mounted) {
showSidePanelSnackBar(context, e.toString());
showSidePanelApiError(context, e);
}
} finally {
if (mounted) setState(() => _isSubmitting = false);
@ -1073,7 +1073,7 @@ class _AddInsurancePanelState extends ConsumerState<AddInsurancePanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) {
if (mounted) {
showSidePanelSnackBar(context, e.toString());
showSidePanelApiError(context, e);
}
} finally {
if (mounted) setState(() => _isSubmitting = false);

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../core/constants/route_constants.dart';
import '../../../../core/utils/validators.dart';
@ -78,7 +78,7 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
if (result.failure != null) {
setState(
() => _errorMessage =
result.failure?.message ?? result.failure.toString(),
result.failure?.message ?? 'Unable to change password.',
);
return;
}
@ -108,13 +108,9 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
return InputDecoration(
labelText: label,
floatingLabelBehavior: FloatingLabelBehavior.auto,
labelStyle: GoogleFonts.inter(
fontSize: 14.5,
color: colors.onSurfaceVariant,
),
floatingLabelStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
labelStyle: AppTypography.label2(color: colors.onSurfaceVariant),
floatingLabelStyle: AppTypography.label3(
weight: AppTypography.semiBold,
color: colors.primary,
),
prefixIcon: Icon(icon, color: colors.iconMuted, size: 18),
@ -212,9 +208,8 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
Expanded(
child: Text(
'Change Password',
style: GoogleFonts.manrope(
fontSize: 18,
fontWeight: FontWeight.w700,
style: AppTypography.body1(
weight: AppTypography.bold,
color: colors.headingColor,
),
),
@ -285,21 +280,17 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
const SizedBox(height: 20),
Text(
'Update your password',
style: GoogleFonts.manrope(
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
style: AppTypography.heading5(
weight: AppTypography.bold,
color: colors.headingColor,
),
),
const SizedBox(height: 5),
Text(
'Enter your current password and choose a new one. Youll need to sign in again afterward.',
style: GoogleFonts.inter(
fontSize: 13.5,
height: 1.45,
style: AppTypography.body3(
color: colors.subtitleColor,
),
).copyWith(height: 1.45),
),
const SizedBox(height: 26),
TextFormField(
@ -312,8 +303,8 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
v,
fieldName: 'Current password',
),
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -338,8 +329,8 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
AutofillHints.newPassword
],
validator: Validators.password,
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -368,8 +359,8 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
}
return Validators.password(v);
},
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -390,8 +381,7 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
const SizedBox(height: 12),
Text(
_errorMessage!,
style: GoogleFonts.inter(
fontSize: 13,
style: AppTypography.body3(
color: colors.colorScheme.error,
),
),
@ -415,10 +405,8 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
borderRadius:
BorderRadius.circular(12),
),
textStyle: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
textStyle: AppTypography.label1(
weight: AppTypography.bold,
),
),
),
@ -446,11 +434,9 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
),
child: Text(
'SECURE ACCESS',
style: GoogleFonts.inter(
fontSize: 11.5,
letterSpacing: 0.6,
style: AppTypography.caption1(
color: colors.onSurfaceVariant,
),
).copyWith(letterSpacing: 0.6),
),
),
Expanded(
@ -473,8 +459,7 @@ class _ChangePasswordScreenState extends ConsumerState<ChangePasswordScreen> {
const SizedBox(width: 6),
Text(
'Your data is protected and encrypted',
style: GoogleFonts.inter(
fontSize: 12,
style: AppTypography.body4(
color: colors.onSurfaceVariant,
),
),

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/constants/route_constants.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../core/utils/validators.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
@ -47,7 +48,10 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
setState(() => _isLoading = false);
if (result.failure != null) {
setState(() => _errorMessage = result.failure.toString());
setState(
() => _errorMessage =
result.failure?.message ?? 'Unable to send reset email.',
);
return;
}
@ -86,7 +90,9 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
const SizedBox(height: 12),
Text(
_errorMessage!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
style: AppTypography.body3(
color: Theme.of(context).colorScheme.error,
),
textAlign: TextAlign.center,
),
],
@ -94,7 +100,9 @@ class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
const SizedBox(height: 12),
Text(
_successMessage!,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
style: AppTypography.body3(
color: Theme.of(context).colorScheme.primary,
),
textAlign: TextAlign.center,
),
],

View File

@ -3,7 +3,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../core/config/dev_config.dart';
import '../../../../core/constants/enums.dart';
@ -171,7 +171,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
setState(() => _isForgotLoading = false);
if (result.failure != null) {
setState(() => _forgotErrorMessage = result.failure.toString());
setState(
() => _forgotErrorMessage =
result.failure?.message ?? 'Unable to send reset email.',
);
return;
}
@ -213,7 +216,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (result.failure != null) {
setState(
() => _resetErrorMessage =
result.failure?.message ?? result.failure.toString(),
result.failure?.message ?? 'Unable to reset password.',
);
return;
}
@ -295,13 +298,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return InputDecoration(
labelText: label,
floatingLabelBehavior: FloatingLabelBehavior.auto,
labelStyle: GoogleFonts.inter(
fontSize: 14.5,
color: colors.onSurfaceVariant,
),
floatingLabelStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
labelStyle: AppTypography.label2(color: colors.onSurfaceVariant),
floatingLabelStyle: AppTypography.label3(
weight: AppTypography.semiBold,
color: colors.primary,
),
prefixIcon: Icon(icon, color: colors.iconMuted, size: 18),
@ -387,11 +386,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
'SECURE ACCESS',
style: GoogleFonts.inter(
fontSize: 11.5,
letterSpacing: 0.6,
color: colors.onSurfaceVariant,
),
style: AppTypography.caption1(color: colors.onSurfaceVariant)
.copyWith(letterSpacing: 0.6),
),
),
Expanded(child: Divider(color: colors.outlineSoft, height: 1)),
@ -409,10 +405,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(width: 6),
Text(
'Your data is protected and encrypted',
style: GoogleFonts.inter(
fontSize: 12,
color: colors.onSurfaceVariant,
),
style: AppTypography.body4(color: colors.onSurfaceVariant),
),
],
),
@ -430,10 +423,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
child: Text(
'Back to Sign In',
style: GoogleFonts.inter(
fontSize: 13.5,
fontWeight: FontWeight.w600,
),
style: AppTypography.body3(weight: AppTypography.semiBold),
),
),
);
@ -456,11 +446,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
textStyle: AppTypography.label1(weight: AppTypography.bold),
),
),
),
@ -483,20 +469,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 20),
Text(
'Welcome',
style: GoogleFonts.manrope(
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
style: AppTypography.heading5(
weight: AppTypography.bold,
color: colors.headingColor,
),
),
const SizedBox(height: 5),
Text(
'Sign in to access your BCPL workspace.',
style: GoogleFonts.inter(
fontSize: 13.5,
color: colors.subtitleColor,
),
style: AppTypography.body3(color: colors.subtitleColor),
),
const SizedBox(height: 26),
TextFormField(
@ -504,8 +485,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
validator: Validators.email,
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -520,8 +501,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
obscureText: _obscurePassword,
autofillHints: const [AutofillHints.password],
validator: (v) => Validators.required(v, fieldName: 'Password'),
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -561,10 +542,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
Text(
'Remember me',
style: GoogleFonts.inter(
fontSize: 13,
color: colors.labelColor,
),
style: AppTypography.body3(color: colors.labelColor),
),
const Spacer(),
TextButton(
@ -577,9 +555,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
child: Text(
'Forgot password?',
style: GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
style: AppTypography.body3(
weight: AppTypography.semiBold,
),
),
),
@ -603,10 +580,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text(
'Login API unavailable? Browse all screens without signing in:',
textAlign: TextAlign.center,
style: GoogleFonts.inter(
fontSize: 12,
color: colors.subtitleColor,
),
style: AppTypography.body4(color: colors.subtitleColor),
),
const SizedBox(height: 12),
Theme(
@ -653,21 +627,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 20),
Text(
'Forgot Password',
style: GoogleFonts.manrope(
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
style: AppTypography.heading5(
weight: AppTypography.bold,
color: colors.headingColor,
),
),
const SizedBox(height: 5),
Text(
'Enter your registered email address to receive password reset instructions.',
style: GoogleFonts.inter(
fontSize: 13.5,
height: 1.45,
color: colors.subtitleColor,
),
style: AppTypography.body3(color: colors.subtitleColor)
.copyWith(height: 1.45),
),
const SizedBox(height: 26),
TextFormField(
@ -675,8 +644,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
validator: Validators.email,
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -689,20 +658,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 12),
Text(
_forgotErrorMessage!,
style: GoogleFonts.inter(
fontSize: 13,
color: colors.colorScheme.error,
),
style: AppTypography.body3(color: colors.colorScheme.error),
),
],
if (_forgotSuccessMessage != null) ...[
const SizedBox(height: 12),
Text(
_forgotSuccessMessage!,
style: GoogleFonts.inter(
fontSize: 13,
color: colors.primary,
),
style: AppTypography.body3(color: colors.primary),
),
],
const SizedBox(height: 18),
@ -741,21 +704,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 20),
Text(
'Reset Password',
style: GoogleFonts.manrope(
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
style: AppTypography.heading5(
weight: AppTypography.bold,
color: colors.headingColor,
),
),
const SizedBox(height: 5),
Text(
'Choose a new password for your account.',
style: GoogleFonts.inter(
fontSize: 13.5,
height: 1.45,
color: colors.subtitleColor,
),
style: AppTypography.body3(color: colors.subtitleColor)
.copyWith(height: 1.45),
),
const SizedBox(height: 26),
TextFormField(
@ -763,8 +721,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
obscureText: _obscureNewPassword,
autofillHints: const [AutofillHints.newPassword],
validator: Validators.password,
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -798,8 +756,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
return Validators.password(v);
},
style: GoogleFonts.inter(
fontSize: 14.5,
style: AppTypography.body3(
weight: AppTypography.medium,
color: colors.headingColor,
),
decoration: _fieldDecoration(
@ -827,20 +785,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
const SizedBox(height: 12),
Text(
_resetErrorMessage!,
style: GoogleFonts.inter(
fontSize: 13,
color: colors.colorScheme.error,
),
style: AppTypography.body3(color: colors.colorScheme.error),
),
],
if ((_resetToken ?? '').isEmpty) ...[
const SizedBox(height: 12),
Text(
'This reset link is invalid or has expired. Request a new one from Forgot Password.',
style: GoogleFonts.inter(
fontSize: 13,
color: colors.colorScheme.error,
),
style: AppTypography.body3(color: colors.colorScheme.error),
),
],
const SizedBox(height: 18),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_typography.dart';
import 'login_colors.dart';
/// BCPL wordmark with blue and green swooshes, matching the login design.
@ -26,8 +27,9 @@ class BcplLogo extends StatelessWidget {
child: Text(
'BCPL',
style: TextStyle(
fontFamily: AppTypography.fontFamily,
fontSize: height * 0.58,
fontWeight: FontWeight.w800,
fontWeight: AppTypography.bold,
fontStyle: FontStyle.italic,
letterSpacing: -0.5,
color: colors.headingColor,

View File

@ -3,6 +3,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'login_colors.dart';
import '../../../../core/theme/app_typography.dart';
/// Orbiting ERP module illustration for the login brand panel.
class LoginHeroIllustration extends StatefulWidget {
@ -200,12 +201,10 @@ class _Hub extends StatelessWidget {
),
child: Text(
'ERP',
style: TextStyle(
style: AppTypography.body2(
weight: AppTypography.bold,
color: colors.panelText,
fontWeight: FontWeight.w800,
fontSize: 17,
letterSpacing: 0.5,
),
).copyWith(letterSpacing: 0.5),
),
);
}

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../../core/theme/app_typography.dart';
import 'login_colors.dart';
import 'login_hero_illustration.dart';
@ -40,24 +40,26 @@ class LoginHeroPanel extends StatelessWidget {
SizedBox(height: compact ? 20 : 28),
Text(
'Smart. Integrated.\nEfficient.',
style: GoogleFonts.manrope(
fontSize: compact ? 28 : 40,
fontWeight: FontWeight.w800,
height: 1.12,
letterSpacing: -0.5,
color: colors.panelText,
),
style: (compact
? AppTypography.heading4(
weight: AppTypography.bold,
color: colors.panelText,
)
: AppTypography.heading3(
weight: AppTypography.bold,
color: colors.panelText,
))
.copyWith(height: 1.12),
),
const SizedBox(height: 14),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Text(
'One workspace for inventory, sales, purchases, accounts and reporting — built for teams who move fast.',
style: GoogleFonts.inter(
fontSize: compact ? 13.5 : 15,
height: 1.6,
color: colors.panelTextDim,
),
style: (compact
? AppTypography.body3(color: colors.panelTextDim)
: AppTypography.body2(color: colors.panelTextDim))
.copyWith(height: 1.6),
),
),
SizedBox(height: compact ? 16 : 24),
@ -89,20 +91,15 @@ class _BrandMark extends StatelessWidget {
children: [
Text(
'BCPL',
style: GoogleFonts.manrope(
fontWeight: FontWeight.w800,
fontSize: 20,
letterSpacing: 0.5,
style: AppTypography.heading6(
weight: AppTypography.bold,
color: colors.panelText,
),
).copyWith(letterSpacing: 0.5),
),
Text(
'BHARAT ERP',
style: GoogleFonts.inter(
fontSize: 11,
letterSpacing: 1.5,
color: colors.panelTextDim,
),
style: AppTypography.caption1(color: colors.panelTextDim)
.copyWith(letterSpacing: 1.5),
),
],
);
@ -170,9 +167,8 @@ class _FeatureItem extends StatelessWidget {
const SizedBox(width: 10),
Text(
title,
style: GoogleFonts.inter(
fontSize: 12.5,
fontWeight: FontWeight.w600,
style: AppTypography.label3(
weight: AppTypography.semiBold,
color: colors.panelText,
),
),

View File

@ -18,6 +18,7 @@ import '../providers/grn_provider.dart';
import '../widgets/grn_attachments_card.dart';
import '../widgets/grn_line_items_editor.dart';
import '../widgets/grn_status_chip.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart';
class GrnDetailScreen extends ConsumerStatefulWidget {
@ -134,7 +135,10 @@ class _GrnDetailScreenState extends ConsumerState<GrnDetailScreen> {
}
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isWorking = false);
@ -202,7 +206,10 @@ class _GrnDetailScreenState extends ConsumerState<GrnDetailScreen> {
}
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isDownloadingPdf = false);

View File

@ -5,13 +5,13 @@ import 'package:go_router/go_router.dart';
import '../../../../core/constants/route_constants.dart';
import '../../../../core/errors/failure.dart';
import '../../../../core/network/api_handler.dart';
import '../../../../core/utils/formatters.dart';
import '../../../../core/utils/validators.dart';
import '../../../../shared/models/grn_model.dart';
import '../../../../shared/models/purchase_order_model.dart';
import '../../../../shared/models/user_management_models.dart';
import '../../../../shared/utils/navigation_utils.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_date_popup.dart';
import '../../../../shared/widgets/app_dropdown.dart';
@ -301,9 +301,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
});
} catch (e) {
if (!mounted) return;
final message =
e is Failure ? validationErrorMessage(e) : e.toString();
showAppToastFromSnackBar(context, SnackBar(content: Text(message)));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
@ -463,8 +464,11 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
}
} catch (e) {
if (!mounted) return;
showAppToastFromSnackBar(context,
SnackBar(content: Text(e.toString())),
showAppToastFromSnackBar(
context,
SnackBar(
content: Text(errorDisplayMessage(e)),
),
);
}
},

View File

@ -2,11 +2,10 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failure.dart';
import '../../../../core/network/api_handler.dart';
import '../../../../core/utils/formatters.dart';
import '../../../../shared/models/grn_model.dart';
import '../../../../shared/utils/file_download_helper.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_confirmation_dialog.dart';
import '../providers/grn_provider.dart';
import '../../../../shared/widgets/app_toast.dart';
@ -95,10 +94,9 @@ class _GrnAttachmentsCardState extends ConsumerState<GrnAttachmentsCard> {
);
} catch (e) {
if (!mounted) return;
final message =
e is Failure ? validationErrorMessage(e) : e.toString();
showAppToastFromSnackBar(context,
SnackBar(content: Text(message)),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _isUploading = false);
@ -118,8 +116,9 @@ class _GrnAttachmentsCardState extends ConsumerState<GrnAttachmentsCard> {
);
} catch (e) {
if (!mounted) return;
showAppToastFromSnackBar(context,
SnackBar(content: Text(e.toString())),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _busyAttachmentId = null);
@ -150,10 +149,9 @@ class _GrnAttachmentsCardState extends ConsumerState<GrnAttachmentsCard> {
);
} catch (e) {
if (!mounted) return;
final message =
e is Failure ? validationErrorMessage(e) : e.toString();
showAppToastFromSnackBar(context,
SnackBar(content: Text(message)),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _busyAttachmentId = null);

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../shared/models/grn_model.dart';
import '../../../../shared/widgets/app_status_chip.dart';
@ -31,11 +32,15 @@ class GrnStatusChip extends StatelessWidget {
return Chip(
label: Text(
label,
style: TextStyle(
color: color,
fontSize: compact ? 11 : 12,
fontWeight: FontWeight.w600,
),
style: compact
? AppTypography.caption1(
weight: AppTypography.semiBold,
color: color,
)
: AppTypography.label3(
weight: AppTypography.semiBold,
color: color,
),
),
backgroundColor: color.withValues(alpha: 0.12),
side: BorderSide(color: color.withValues(alpha: 0.3)),

View File

@ -142,7 +142,6 @@ class _MasterAppTile extends StatelessWidget {
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
height: 1.2,
fontSize: 12,
),
),
],

View File

@ -22,6 +22,7 @@ class MasterFormPanel extends ConsumerStatefulWidget {
this.recordId,
this.initialValues,
this.formSessionId,
this.readOnlyFields,
});
final String masterId;
@ -33,6 +34,9 @@ class MasterFormPanel extends ConsumerStatefulWidget {
/// Unique per open so create forms always start empty.
final String? formSessionId;
/// Field keys rendered as read-only (in addition to [MasterFieldDef.readOnly]).
final Set<String>? readOnlyFields;
bool get isEditing => recordId != null;
@override
@ -105,6 +109,10 @@ class _MasterFormPanelState extends ConsumerState<MasterFormPanel> {
);
}
bool _isFieldReadOnly(MasterFieldDef field) =>
field.readOnly ||
(widget.readOnlyFields?.contains(field.key) ?? false);
Widget _buildField(
BuildContext context, {
required MasterFieldDef field,
@ -152,7 +160,7 @@ class _MasterFormPanelState extends ConsumerState<MasterFormPanel> {
);
case MasterFieldType.dropdown:
if (field.readOnly) {
if (_isFieldReadOnly(field)) {
final display = field.key == 'gst_rate_id'
? (gstRateDisplayFromValues(formState.values) ?? '')
: (value?.toString() ?? '');

View File

@ -37,6 +37,7 @@ class MasterQuickAddDropdown<T> extends ConsumerStatefulWidget {
this.refreshLookups,
this.addNewLabel,
this.openInSidePanel = false,
this.readOnlyFields,
});
final String masterId;
@ -59,6 +60,9 @@ class MasterQuickAddDropdown<T> extends ConsumerStatefulWidget {
/// When true, Quick Add opens [MasterFormPanel] in a side panel popup.
final bool openInSidePanel;
/// Field keys locked as read-only in the Quick Add form (side panel or inline).
final Set<String>? readOnlyFields;
@override
ConsumerState<MasterQuickAddDropdown<T>> createState() =>
_MasterQuickAddDropdownState<T>();
@ -117,6 +121,7 @@ class _MasterQuickAddDropdownState<T>
masterId: widget.masterId,
initialValues: widget.initialValues,
formSessionId: sessionId,
readOnlyFields: widget.readOnlyFields,
),
width: 560,
);

View File

@ -22,6 +22,7 @@ import '../providers/purchase_orders_provider.dart';
import '../../data/repositories/purchase_order_repository_impl.dart';
import '../widgets/po_status_chip.dart';
import '../widgets/purchase_order_line_items_editor.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart';
class PurchaseOrderDetailScreen extends ConsumerStatefulWidget {
@ -171,7 +172,10 @@ class _PurchaseOrderDetailScreenState
}
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isWorking = false);
@ -209,10 +213,9 @@ class _PurchaseOrderDetailScreenState
}
} catch (e) {
if (mounted) {
final message = e is Failure ? e.message : e.toString();
showAppToastFromSnackBar(
context,
SnackBar(content: Text(message)),
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
@ -293,7 +296,10 @@ class _PurchaseOrderDetailScreenState
}
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isWorking = false);
@ -342,7 +348,10 @@ class _PurchaseOrderDetailScreenState
}
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isWorking = false);
@ -362,7 +371,10 @@ class _PurchaseOrderDetailScreenState
);
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isDownloadingPdf = false);
@ -1400,7 +1412,7 @@ class _AmountSummaryCard extends StatelessWidget {
Text(
CurrencyFormatter.format(order.totalAmount),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: theme.colorScheme.primary,
),
),

View File

@ -1041,7 +1041,7 @@ class _AmountSummaryCard extends StatelessWidget {
Text(
CurrencyFormatter.format(totals.grandTotal),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: theme.colorScheme.primary,
),
),
@ -1159,9 +1159,8 @@ class _SummaryInputRow extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
),
errorMaxLines: 2,
errorStyle: theme.textTheme.bodySmall?.copyWith(
errorStyle: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.error,
fontSize: 11,
),
),
),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../shared/models/purchase_order_model.dart';
import '../../../../shared/widgets/app_status_chip.dart';
@ -101,13 +102,16 @@ class _PoBadge extends StatelessWidget {
widthFactor: 1,
child: Text(
label,
style: TextStyle(
color: color,
fontSize: compact ? 11 : 12,
fontWeight: FontWeight.w600,
height: 1,
letterSpacing: 0.1,
),
style: (compact
? AppTypography.caption1(
weight: AppTypography.semiBold,
color: color,
)
: AppTypography.label3(
weight: AppTypography.semiBold,
color: color,
))
.copyWith(height: 1, letterSpacing: 0.1),
),
),
);

View File

@ -1,6 +1,7 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../shared/widgets/app_data_table.dart';
import '../../../../shared/widgets/app_table_action_icon.dart';
@ -278,7 +279,7 @@ class UserTableUserCell extends StatelessWidget {
children: [
AppTableCell.text(
user.fullName,
style: const TextStyle(fontWeight: FontWeight.w600),
style: AppTypography.label2(weight: AppTypography.semiBold),
),
AppTableCell.text(
user.email,
@ -325,8 +326,9 @@ class UserAvatarChip extends StatelessWidget {
child: Text(
display.length > 2 ? display.substring(0, 2) : display,
style: TextStyle(
fontFamily: AppTypography.fontFamily,
color: color,
fontWeight: FontWeight.w700,
fontWeight: AppTypography.bold,
fontSize: radius * 0.6,
),
),

View File

@ -260,7 +260,9 @@ class UsersListNotifier extends AsyncNotifier<UsersListState> {
if (result.failure != null) {
final current = state.valueOrNull;
if (current != null) {
state = AsyncData(current.copyWith(actionError: result.failure.toString()));
state = AsyncData(
current.copyWith(actionError: result.failure!.message),
);
}
return false;
}
@ -281,7 +283,9 @@ class UsersListNotifier extends AsyncNotifier<UsersListState> {
if (result.failure != null) {
final current = state.valueOrNull;
if (current != null) {
state = AsyncData(current.copyWith(actionError: result.failure.toString()));
state = AsyncData(
current.copyWith(actionError: result.failure!.message),
);
}
return false;
}

View File

@ -13,6 +13,7 @@ import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/error_view.dart';
import '../../../master_data/presentation/widgets/master_quick_add.dart';
import '../providers/users_provider.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart';
class UserFormScreen extends ConsumerStatefulWidget {
@ -144,8 +145,9 @@ class _UserFormScreenState extends ConsumerState<UserFormScreen> {
context.pop();
} catch (e) {
if (!mounted) return;
showAppToastFromSnackBar(context,
SnackBar(content: Text(e.toString())),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _isSubmitting = false);

View File

@ -86,7 +86,7 @@ class _UserProfileScreenState extends ConsumerState<UserProfileScreen> {
context,
SnackBar(
content: Text(
uploadResult.failure?.message ?? uploadResult.failure.toString(),
uploadResult.failure?.message ?? 'Unable to upload profile picture.',
),
),
);
@ -131,7 +131,9 @@ class _UserProfileScreenState extends ConsumerState<UserProfileScreen> {
showAppToastFromSnackBar(
context,
SnackBar(
content: Text(result.failure?.message ?? result.failure.toString()),
content: Text(
result.failure?.message ?? 'Unable to update profile.',
),
),
);
return;

View File

@ -18,6 +18,7 @@ import '../../../../shared/widgets/app_segmented_tab_bar.dart';
import '../providers/vendors_provider.dart';
import '../widgets/vendor_form_panel.dart';
import '../widgets/vendor_sub_resource_panels.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart';
class VendorDetailScreen extends ConsumerStatefulWidget {
@ -209,7 +210,10 @@ class _VendorDetailScreenState extends ConsumerState<VendorDetailScreen>
}
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
}
}
@ -229,7 +233,10 @@ class _VendorDetailScreenState extends ConsumerState<VendorDetailScreen>
if (mounted) context.go(RouteConstants.vendors);
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
}
}
@ -482,7 +489,10 @@ class _AddressesTab extends ConsumerWidget {
await ref.read(vendorDetailProvider(vendorId).notifier).deleteAddress(addressId);
} catch (e) {
if (context.mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
}
}
@ -582,7 +592,10 @@ class _ContactsTab extends ConsumerWidget {
await ref.read(vendorDetailProvider(vendorId).notifier).deleteContact(contactId);
} catch (e) {
if (context.mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
}
}
@ -725,7 +738,10 @@ class _BankDetailsTab extends ConsumerWidget {
);
} catch (e) {
if (context.mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
}
}
@ -750,7 +766,10 @@ class _BankDetailsTab extends ConsumerWidget {
.deleteBankDetail(bankDetailId);
} catch (e) {
if (context.mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
}
}

View File

@ -16,6 +16,7 @@ import '../../../../shared/widgets/error_view.dart';
import '../../../master_data/presentation/widgets/master_quick_add.dart';
import '../providers/vendor_lookups_provider.dart';
import '../providers/vendors_provider.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart';
Future<String?> openVendorFormPanel(
@ -160,8 +161,9 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(savedId);
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context,
SnackBar(content: Text(e.toString())),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {

View File

@ -9,6 +9,7 @@ import '../../../../shared/widgets/app_form_toggle_field.dart';
import '../../../../shared/widgets/app_side_panel.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../providers/vendors_provider.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart';
Future<bool?> openVendorAddressPanel(
@ -144,7 +145,10 @@ class _VendorAddressPanelState extends ConsumerState<VendorAddressPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isSubmitting = false);
@ -285,7 +289,10 @@ class _VendorContactPanelState extends ConsumerState<VendorContactPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isSubmitting = false);
@ -436,7 +443,10 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) {
if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString())));
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
}
} finally {
if (mounted) setState(() => _isSubmitting = false);

View File

@ -5,6 +5,21 @@ import '../../core/network/api_handler.dart';
import 'app_side_panel.dart';
import 'app_toast.dart';
/// User-facing text from a [Failure] or other thrown error.
/// Prefer this over [Object.toString] for toasts (never show `Failure.server(...)`).
String errorDisplayMessage(
Object error, {
String fallback = 'Something went wrong. Please try again.',
}) {
if (error is Failure) {
return error is ValidationFailure
? validationErrorMessage(error)
: error.message;
}
final text = error.toString().trim();
return text.isEmpty ? fallback : text;
}
void showAccessDeniedSnackBar(BuildContext context, {String? message}) {
showAppToast(
context,
@ -14,12 +29,7 @@ void showAccessDeniedSnackBar(BuildContext context, {String? message}) {
}
void showSidePanelApiError(BuildContext context, Object error) {
final message = error is Failure
? (error is ValidationFailure
? validationErrorMessage(error)
: error.message)
: 'Something went wrong. Please try again.';
showSidePanelSnackBar(context, message);
showSidePanelSnackBar(context, errorDisplayMessage(error));
}
void showApiFailureSnackBar(BuildContext context, Failure failure) {
@ -35,9 +45,15 @@ void showApiFailureSnackBar(BuildContext context, Failure failure) {
showAppToast(
context,
failure is ValidationFailure
? validationErrorMessage(failure)
: failure.message,
errorDisplayMessage(failure),
type: AppToastType.error,
);
}
void showApiErrorToast(BuildContext context, Object error) {
showAppToast(
context,
errorDisplayMessage(error),
type: AppToastType.error,
);
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../core/constants/enums.dart';
import '../../core/theme/app_typography.dart';
/// Uniform width for status chips inside data tables (fits "Partially Received").
const double kTableStatusChipWidth = 132;
@ -38,13 +39,16 @@ class TableStatusBadge extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: color,
fontSize: compact ? 11 : 12,
fontWeight: FontWeight.w600,
height: 1,
letterSpacing: 0.1,
),
style: (compact
? AppTypography.caption1(
weight: AppTypography.semiBold,
color: color,
)
: AppTypography.label3(
weight: AppTypography.semiBold,
color: color,
))
.copyWith(height: 1, letterSpacing: 0.1),
),
),
);
@ -86,11 +90,15 @@ class AppStatusChip extends StatelessWidget {
return Chip(
label: Text(
label,
style: TextStyle(
color: color,
fontSize: compact ? 11 : 12,
fontWeight: FontWeight.w600,
),
style: compact
? AppTypography.caption1(
weight: AppTypography.semiBold,
color: color,
)
: AppTypography.label3(
weight: AppTypography.semiBold,
color: color,
),
),
backgroundColor: color.withValues(alpha: 0.12),
side: BorderSide(color: color.withValues(alpha: 0.3)),

View File

@ -8,6 +8,7 @@ import '../../core/config/dev_config.dart';
import '../../core/constants/app_constants.dart';
import '../../core/constants/route_constants.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_typography.dart';
import '../../core/theme/theme_provider.dart';
import '../../modules/settings/presentation/providers/settings_provider.dart';
import '../models/user_model.dart';
@ -175,9 +176,9 @@ class _TopNavMenuLabel extends StatelessWidget {
children: [
Text(
label,
style: TextStyle(
style: AppTypography.label2(
weight: selected ? AppTypography.semiBold : AppTypography.medium,
color: color,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
),
),
if (showChevron) ...[

View File

@ -1,11 +1,10 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import '../../core/errors/failure.dart';
import '../../core/network/api_handler.dart';
import '../../core/utils/formatters.dart';
import '../models/entity_attachment_model.dart';
import '../utils/file_download_helper.dart';
import 'api_feedback.dart';
import 'app_confirmation_dialog.dart';
import 'app_toast.dart';
@ -107,10 +106,9 @@ class _EntityAttachmentsCardState extends State<EntityAttachmentsCard> {
);
} catch (e) {
if (!mounted) return;
final message =
e is Failure ? validationErrorMessage(e) : e.toString();
showAppToastFromSnackBar(context,
SnackBar(content: Text(message)),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _isUploading = false);
@ -128,8 +126,9 @@ class _EntityAttachmentsCardState extends State<EntityAttachmentsCard> {
);
} catch (e) {
if (!mounted) return;
showAppToastFromSnackBar(context,
SnackBar(content: Text(e.toString())),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _busyAttachmentId = null);
@ -158,10 +157,9 @@ class _EntityAttachmentsCardState extends State<EntityAttachmentsCard> {
);
} catch (e) {
if (!mounted) return;
final message =
e is Failure ? validationErrorMessage(e) : e.toString();
showAppToastFromSnackBar(context,
SnackBar(content: Text(message)),
showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally {
if (mounted) setState(() => _busyAttachmentId = null);

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/utils/validators.dart';
import '../../core/theme/app_typography.dart';
import '../../modules/assets/data/repositories/asset_repository_impl.dart';
import '../../modules/master_data/data/repositories/master_repository_impl.dart';
import '../../modules/master_data/domain/entities/master_definition.dart';
@ -241,7 +242,7 @@ class _MasterInlineQuickAddFormState
return CheckboxListTile(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(field.label, style: const TextStyle(fontSize: 13)),
title: Text(field.label, style: AppTypography.body3()),
value: _values[field.key] == true,
onChanged: _submitting
? null

View File

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../core/theme/app_typography.dart';
import '../../core/utils/media_url.dart';
/// Circular user avatar with network image fallback to name initial.
@ -24,9 +25,10 @@ class UserAvatar extends StatelessWidget {
final fallback = Text(
initial,
style: theme.textTheme.titleSmall?.copyWith(
style: TextStyle(
fontFamily: AppTypography.fontFamily,
color: theme.colorScheme.primary,
fontWeight: FontWeight.w700,
fontWeight: AppTypography.bold,
fontSize: radius * 0.85,
),
);

View File

@ -1,9 +1,9 @@
# SPA fallback for Flutter web path URL strategy (dev: /erp/).
# SPA fallback for Flutter web path URL strategy (root host).
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /erp/
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /erp/index.html [L]
RewriteRule . /index.html [L]
</IfModule>

View File

@ -17,7 +17,7 @@
<base href="$FLUTTER_BASE_HREF">
<script>
// Migrate legacy hash URLs (…/index.html#/login) to path URLs (…/erp/login).
// Migrate legacy hash URLs (…/index.html#/login) to path URLs (…/login).
(function () {
var hash = window.location.hash;
if (!hash || hash.charAt(1) !== '/') return;