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`. 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`). 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 | | Flavor | Entry point | API URL (auto) | Web base-href |
|--------|-------------|----------------|---------|---------------| |--------|-------------|----------------|---------------|
| **dev** | `lib/config/main_dev.dart` | `https://demo.venbait.in/api/v1` | `https://bharatconsumerproducts.com/erp/login` | `/erp/` | | **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` | — | `/` | | **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/` | | **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 ### Web builds
```bash ```bash
# Dev # 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 # UAT
flutter build web -t lib/config/main_uat.dart --release --base-href / flutter build web -t lib/config/main_uat.dart --release --base-href /
# Prod # 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 ### Android APK / AAB

View File

@ -17,20 +17,6 @@ class Environment {
Flavor.prod => 'production', 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) { static String get envFileName => switch (flavor) {
Flavor.dev => '.env.development', Flavor.dev => '.env.development',
Flavor.uat => '.env.uat', Flavor.uat => '.env.uat',

View File

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

View File

@ -1,57 +1,189 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
/// ERP typography Figtree (Google Font). /// ERP typography Figtree design tokens.
/// Weights: Regular 400, Medium 500, SemiBold 600. ///
/// | 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 { class AppTypography {
AppTypography._(); AppTypography._();
static const FontWeight regular = FontWeight.w400; static const FontWeight regular = FontWeight.w400;
static const FontWeight medium = FontWeight.w500; static const FontWeight medium = FontWeight.w500;
static const FontWeight semiBold = FontWeight.w600; static const FontWeight semiBold = FontWeight.w600;
static const FontWeight bold = FontWeight.w700;
static String get fontFamily => GoogleFonts.figtree().fontFamily ?? 'Figtree'; static String get fontFamily => GoogleFonts.figtree().fontFamily ?? 'Figtree';
static TextTheme textTheme(ColorScheme colorScheme) { // Design tokens
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,
);
}
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 onSurface = colorScheme.onSurface;
final onSurfaceVariant = colorScheme.onSurfaceVariant; final onSurfaceVariant = colorScheme.onSurfaceVariant;
return TextTheme( return TextTheme(
// Display displayLarge: heading1(color: onSurface),
displayLarge: token(size: 57, weight: semiBold, color: onSurface), displayMedium: heading2(color: onSurface),
displayMedium: token(size: 45, weight: semiBold, color: onSurface), displaySmall: heading3(color: onSurface),
displaySmall: token(size: 36, weight: semiBold, color: onSurface), headlineLarge: heading4(color: onSurface),
// Headline headlineMedium: heading5(color: onSurface),
headlineLarge: token(size: 20, weight: semiBold, color: onSurface), headlineSmall: heading6(color: onSurface),
headlineMedium: token(size: 16, weight: regular, color: onSurface), titleLarge: heading5(color: onSurface),
headlineSmall: token(size: 18, weight: semiBold, color: onSurface), titleMedium: label1(color: onSurface),
// Title titleSmall: label2(color: onSurface),
titleLarge: token(size: 22, weight: semiBold, color: onSurface), bodyLarge: body1(color: onSurface),
titleMedium: token(size: 16, weight: medium, color: onSurface), bodyMedium: body2(color: onSurface),
titleSmall: token(size: 14, weight: medium, color: onSurface), bodySmall: body3(color: onSurfaceVariant),
// Body labelLarge: label2(color: onSurface),
bodyLarge: token(size: 16, weight: regular, color: onSurface), labelMedium: label3(color: onSurfaceVariant),
bodyMedium: token(size: 16, weight: regular, color: onSurface), labelSmall: caption1(color: onSurfaceVariant),
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), static TextStyle _token(
labelSmall: token(size: 10, weight: regular, color: onSurfaceVariant), 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( style: theme.textTheme.bodySmall?.copyWith(
color: visual.color, color: visual.color,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 12,
), ),
), ),
), ),
@ -374,7 +373,6 @@ class _AlertCard extends StatelessWidget {
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: visual.color, color: visual.color,
fontWeight: FontWeight.w600, 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:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../../../core/constants/enums.dart';
import '../../../../core/constants/route_constants.dart'; import '../../../../core/constants/route_constants.dart';
import '../../../../core/errors/failure.dart'; import '../../../../core/errors/failure.dart';
import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/formatters.dart';
import '../../../../core/utils/validators.dart'; import '../../../../core/utils/validators.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/user_management_models.dart' show FilterOptionModel; import '../../../../shared/models/user_management_models.dart' show FilterOptionModel;
import '../../../../shared/providers/permissions_provider.dart';
import '../../../../shared/utils/navigation_utils.dart'; import '../../../../shared/utils/navigation_utils.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_date_popup.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_sticky_form_layout.dart';
import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/error_view.dart';
import '../../../master_data/presentation/widgets/master_quick_add.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 '../../data/repositories/asset_repository_impl.dart';
import '../providers/asset_categories_provider.dart'; import '../providers/asset_categories_provider.dart';
import '../providers/asset_form_lookups_provider.dart'; import '../providers/asset_form_lookups_provider.dart';
@ -486,7 +490,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
if (mounted) { if (mounted) {
showAppToastFromSnackBar( showAppToastFromSnackBar(
context, context,
SnackBar(content: Text(e.toString())), SnackBar(content: Text(errorDisplayMessage(e))),
); );
} }
} finally { } finally {
@ -781,12 +785,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
children: [ children: [
FormRowFour( FormRowFour(
children: [ children: [
_optionalLookupDropdown( _vendorDropdown(lookups.vendors),
label: 'Vendor',
value: _vendorId,
options: lookups.vendors,
onChanged: (v) => setState(() => _vendorId = v),
),
_optionalLookupDropdown( _optionalLookupDropdown(
label: 'Purchase Order', label: 'Purchase Order',
value: _poId, value: _poId,
@ -1336,6 +1335,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
searchHint: 'Search ${label.toLowerCase()}...', searchHint: 'Search ${label.toLowerCase()}...',
isDense: true, isDense: true,
enabled: fieldEnabled, enabled: fieldEnabled,
openInSidePanel: true,
options: dropdownOptions, options: dropdownOptions,
refreshLookups: () { refreshLookups: () {
ref.invalidate(assetFormLookupsProvider); ref.invalidate(assetFormLookupsProvider);
@ -1371,6 +1371,7 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
searchHint: 'Search subcategory...', searchHint: 'Search subcategory...',
isDense: true, isDense: true,
enabled: hasCategory, enabled: hasCategory,
openInSidePanel: true,
options: options, options: options,
initialValues: {'item_category_id': _categoryId}, initialValues: {'item_category_id': _categoryId},
refreshLookups: () { refreshLookups: () {
@ -1395,7 +1396,9 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
value: _dropdownValue(_categoryId, categoryIds), value: _dropdownValue(_categoryId, categoryIds),
searchHint: 'Search category...', searchHint: 'Search category...',
isDense: true, isDense: true,
openInSidePanel: true,
initialValues: const {'category_type': 'ASSET'}, initialValues: const {'category_type': 'ASSET'},
readOnlyFields: const {'category_type'},
options: activeCategories options: activeCategories
.map( .map(
(c) => AppDropdownOption( (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) { Widget _locationDropdown(List<FilterOptionModel> locations) {
final locationIds = locations final locationIds = locations
.map((location) => int.tryParse(location.id)) .map((location) => int.tryParse(location.id))
.whereType<int>() .whereType<int>()
.toList(); .toList();
return AppSearchableDropdown<int>( return MasterQuickAddDropdown<int>(
masterId: 'locations',
label: 'Location *', label: 'Location *',
value: _dropdownValue(_locationId, locationIds), value: _dropdownValue(_locationId, locationIds),
searchHint: 'Search plant or warehouse...', searchHint: 'Search plant or warehouse...',
isDense: true, isDense: true,
openInSidePanel: true,
options: locations options: locations
.map( .map(
(location) => AppDropdownOption( (location) => AppDropdownOption(
@ -1451,6 +1494,11 @@ class _AssetFormScreenState extends ConsumerState<AssetFormScreen> {
) )
.where((option) => option.value != 0) .where((option) => option.value != 0)
.toList(), .toList(),
refreshLookups: () async {
ref.invalidate(assetFormLookupsProvider);
await ref.read(assetFormLookupsProvider.future);
},
parseCreatedId: int.tryParse,
onChanged: (v) => setState(() => _locationId = v), onChanged: (v) => setState(() => _locationId = v),
validator: (v) => v == null ? 'Location is required' : null, 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 '../../../../core/utils/formatters.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_date_popup.dart'; import '../../../../shared/widgets/app_date_popup.dart';
import '../../../../shared/widgets/app_dropdown.dart'; import '../../../../shared/widgets/app_dropdown.dart';
@ -156,7 +157,7 @@ class _SubmitMaintenanceLogPanelState
if (mounted) Navigator.of(context, rootNavigator: true).pop(true); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showSidePanelSnackBar(context, e.toString()); showSidePanelApiError(context, e);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); 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); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showSidePanelSnackBar(context, e.toString()); showSidePanelApiError(context, e);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);
@ -620,7 +620,7 @@ class _LogServiceVisitPanelState extends ConsumerState<LogServiceVisitPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showSidePanelSnackBar(context, e.toString()); showSidePanelApiError(context, e);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);
@ -1073,7 +1073,7 @@ class _AddInsurancePanelState extends ConsumerState<AddInsurancePanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showSidePanelSnackBar(context, e.toString()); showSidePanelApiError(context, e);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import '../providers/grn_provider.dart';
import '../widgets/grn_attachments_card.dart'; import '../widgets/grn_attachments_card.dart';
import '../widgets/grn_line_items_editor.dart'; import '../widgets/grn_line_items_editor.dart';
import '../widgets/grn_status_chip.dart'; import '../widgets/grn_status_chip.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
class GrnDetailScreen extends ConsumerStatefulWidget { class GrnDetailScreen extends ConsumerStatefulWidget {
@ -134,7 +135,10 @@ class _GrnDetailScreenState extends ConsumerState<GrnDetailScreen> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} }
} finally { } finally {
if (mounted) setState(() => _isWorking = false); if (mounted) setState(() => _isWorking = false);
@ -202,7 +206,10 @@ class _GrnDetailScreenState extends ConsumerState<GrnDetailScreen> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} }
} finally { } finally {
if (mounted) setState(() => _isDownloadingPdf = false); 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/constants/route_constants.dart';
import '../../../../core/errors/failure.dart'; import '../../../../core/errors/failure.dart';
import '../../../../core/network/api_handler.dart';
import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/formatters.dart';
import '../../../../core/utils/validators.dart'; import '../../../../core/utils/validators.dart';
import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/grn_model.dart';
import '../../../../shared/models/purchase_order_model.dart'; import '../../../../shared/models/purchase_order_model.dart';
import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/models/user_management_models.dart';
import '../../../../shared/utils/navigation_utils.dart'; import '../../../../shared/utils/navigation_utils.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_date_popup.dart'; import '../../../../shared/widgets/app_date_popup.dart';
import '../../../../shared/widgets/app_dropdown.dart'; import '../../../../shared/widgets/app_dropdown.dart';
@ -301,9 +301,10 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
}); });
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
final message = showAppToastFromSnackBar(
e is Failure ? validationErrorMessage(e) : e.toString(); context,
showAppToastFromSnackBar(context, SnackBar(content: Text(message))); SnackBar(content: Text(errorDisplayMessage(e))),
);
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);
} }
@ -463,8 +464,11 @@ class _GrnFormScreenState extends ConsumerState<GrnFormScreen> {
} }
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
showAppToastFromSnackBar(context, showAppToastFromSnackBar(
SnackBar(content: Text(e.toString())), 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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 '../../../../core/utils/formatters.dart';
import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/grn_model.dart';
import '../../../../shared/utils/file_download_helper.dart'; import '../../../../shared/utils/file_download_helper.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_confirmation_dialog.dart'; import '../../../../shared/widgets/app_confirmation_dialog.dart';
import '../providers/grn_provider.dart'; import '../providers/grn_provider.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
@ -95,10 +94,9 @@ class _GrnAttachmentsCardState extends ConsumerState<GrnAttachmentsCard> {
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
final message = showAppToastFromSnackBar(
e is Failure ? validationErrorMessage(e) : e.toString(); context,
showAppToastFromSnackBar(context, SnackBar(content: Text(errorDisplayMessage(e))),
SnackBar(content: Text(message)),
); );
} finally { } finally {
if (mounted) setState(() => _isUploading = false); if (mounted) setState(() => _isUploading = false);
@ -118,8 +116,9 @@ class _GrnAttachmentsCardState extends ConsumerState<GrnAttachmentsCard> {
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
showAppToastFromSnackBar(context, showAppToastFromSnackBar(
SnackBar(content: Text(e.toString())), context,
SnackBar(content: Text(errorDisplayMessage(e))),
); );
} finally { } finally {
if (mounted) setState(() => _busyAttachmentId = null); if (mounted) setState(() => _busyAttachmentId = null);
@ -150,10 +149,9 @@ class _GrnAttachmentsCardState extends ConsumerState<GrnAttachmentsCard> {
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
final message = showAppToastFromSnackBar(
e is Failure ? validationErrorMessage(e) : e.toString(); context,
showAppToastFromSnackBar(context, SnackBar(content: Text(errorDisplayMessage(e))),
SnackBar(content: Text(message)),
); );
} finally { } finally {
if (mounted) setState(() => _busyAttachmentId = null); if (mounted) setState(() => _busyAttachmentId = null);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../shared/models/purchase_order_model.dart'; import '../../../../shared/models/purchase_order_model.dart';
import '../../../../shared/widgets/app_status_chip.dart'; import '../../../../shared/widgets/app_status_chip.dart';
@ -101,13 +102,16 @@ class _PoBadge extends StatelessWidget {
widthFactor: 1, widthFactor: 1,
child: Text( child: Text(
label, label,
style: TextStyle( style: (compact
color: color, ? AppTypography.caption1(
fontSize: compact ? 11 : 12, weight: AppTypography.semiBold,
fontWeight: FontWeight.w600, color: color,
height: 1, )
letterSpacing: 0.1, : 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/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../shared/widgets/app_data_table.dart'; import '../../../../shared/widgets/app_data_table.dart';
import '../../../../shared/widgets/app_table_action_icon.dart'; import '../../../../shared/widgets/app_table_action_icon.dart';
@ -278,7 +279,7 @@ class UserTableUserCell extends StatelessWidget {
children: [ children: [
AppTableCell.text( AppTableCell.text(
user.fullName, user.fullName,
style: const TextStyle(fontWeight: FontWeight.w600), style: AppTypography.label2(weight: AppTypography.semiBold),
), ),
AppTableCell.text( AppTableCell.text(
user.email, user.email,
@ -325,8 +326,9 @@ class UserAvatarChip extends StatelessWidget {
child: Text( child: Text(
display.length > 2 ? display.substring(0, 2) : display, display.length > 2 ? display.substring(0, 2) : display,
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontFamily,
color: color, color: color,
fontWeight: FontWeight.w700, fontWeight: AppTypography.bold,
fontSize: radius * 0.6, fontSize: radius * 0.6,
), ),
), ),

View File

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

View File

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

View File

@ -86,7 +86,7 @@ class _UserProfileScreenState extends ConsumerState<UserProfileScreen> {
context, context,
SnackBar( SnackBar(
content: Text( 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( showAppToastFromSnackBar(
context, context,
SnackBar( SnackBar(
content: Text(result.failure?.message ?? result.failure.toString()), content: Text(
result.failure?.message ?? 'Unable to update profile.',
),
), ),
); );
return; return;

View File

@ -18,6 +18,7 @@ import '../../../../shared/widgets/app_segmented_tab_bar.dart';
import '../providers/vendors_provider.dart'; import '../providers/vendors_provider.dart';
import '../widgets/vendor_form_panel.dart'; import '../widgets/vendor_form_panel.dart';
import '../widgets/vendor_sub_resource_panels.dart'; import '../widgets/vendor_sub_resource_panels.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
class VendorDetailScreen extends ConsumerStatefulWidget { class VendorDetailScreen extends ConsumerStatefulWidget {
@ -209,7 +210,10 @@ class _VendorDetailScreenState extends ConsumerState<VendorDetailScreen>
} }
} catch (e) { } catch (e) {
if (mounted) { 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); if (mounted) context.go(RouteConstants.vendors);
} catch (e) { } catch (e) {
if (mounted) { 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); await ref.read(vendorDetailProvider(vendorId).notifier).deleteAddress(addressId);
} catch (e) { } catch (e) {
if (context.mounted) { 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); await ref.read(vendorDetailProvider(vendorId).notifier).deleteContact(contactId);
} catch (e) { } catch (e) {
if (context.mounted) { 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) { } catch (e) {
if (context.mounted) { 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); .deleteBankDetail(bankDetailId);
} catch (e) { } catch (e) {
if (context.mounted) { 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 '../../../master_data/presentation/widgets/master_quick_add.dart';
import '../providers/vendor_lookups_provider.dart'; import '../providers/vendor_lookups_provider.dart';
import '../providers/vendors_provider.dart'; import '../providers/vendors_provider.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
Future<String?> openVendorFormPanel( Future<String?> openVendorFormPanel(
@ -160,8 +161,9 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(savedId); if (mounted) Navigator.of(context, rootNavigator: true).pop(savedId);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showAppToastFromSnackBar(context, showAppToastFromSnackBar(
SnackBar(content: Text(e.toString())), context,
SnackBar(content: Text(errorDisplayMessage(e))),
); );
} }
} finally { } 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_side_panel.dart';
import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/app_text_field.dart';
import '../providers/vendors_provider.dart'; import '../providers/vendors_provider.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/app_toast.dart';
Future<bool?> openVendorAddressPanel( Future<bool?> openVendorAddressPanel(
@ -144,7 +145,10 @@ class _VendorAddressPanelState extends ConsumerState<VendorAddressPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);
@ -285,7 +289,10 @@ class _VendorContactPanelState extends ConsumerState<VendorContactPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);
@ -436,7 +443,10 @@ class _VendorBankDetailPanelState extends ConsumerState<VendorBankDetailPanel> {
if (mounted) Navigator.of(context, rootNavigator: true).pop(true); if (mounted) Navigator.of(context, rootNavigator: true).pop(true);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showAppToastFromSnackBar(context, SnackBar(content: Text(e.toString()))); showAppToastFromSnackBar(
context,
SnackBar(content: Text(errorDisplayMessage(e))),
);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);

View File

@ -5,6 +5,21 @@ import '../../core/network/api_handler.dart';
import 'app_side_panel.dart'; import 'app_side_panel.dart';
import 'app_toast.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}) { void showAccessDeniedSnackBar(BuildContext context, {String? message}) {
showAppToast( showAppToast(
context, context,
@ -14,12 +29,7 @@ void showAccessDeniedSnackBar(BuildContext context, {String? message}) {
} }
void showSidePanelApiError(BuildContext context, Object error) { void showSidePanelApiError(BuildContext context, Object error) {
final message = error is Failure showSidePanelSnackBar(context, errorDisplayMessage(error));
? (error is ValidationFailure
? validationErrorMessage(error)
: error.message)
: 'Something went wrong. Please try again.';
showSidePanelSnackBar(context, message);
} }
void showApiFailureSnackBar(BuildContext context, Failure failure) { void showApiFailureSnackBar(BuildContext context, Failure failure) {
@ -35,9 +45,15 @@ void showApiFailureSnackBar(BuildContext context, Failure failure) {
showAppToast( showAppToast(
context, context,
failure is ValidationFailure errorDisplayMessage(failure),
? validationErrorMessage(failure) type: AppToastType.error,
: failure.message, );
}
void showApiErrorToast(BuildContext context, Object error) {
showAppToast(
context,
errorDisplayMessage(error),
type: AppToastType.error, type: AppToastType.error,
); );
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/constants/enums.dart'; import '../../core/constants/enums.dart';
import '../../core/theme/app_typography.dart';
/// Uniform width for status chips inside data tables (fits "Partially Received"). /// Uniform width for status chips inside data tables (fits "Partially Received").
const double kTableStatusChipWidth = 132; const double kTableStatusChipWidth = 132;
@ -38,13 +39,16 @@ class TableStatusBadge extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: (compact
color: color, ? AppTypography.caption1(
fontSize: compact ? 11 : 12, weight: AppTypography.semiBold,
fontWeight: FontWeight.w600, color: color,
height: 1, )
letterSpacing: 0.1, : AppTypography.label3(
), weight: AppTypography.semiBold,
color: color,
))
.copyWith(height: 1, letterSpacing: 0.1),
), ),
), ),
); );
@ -86,11 +90,15 @@ class AppStatusChip extends StatelessWidget {
return Chip( return Chip(
label: Text( label: Text(
label, label,
style: TextStyle( style: compact
color: color, ? AppTypography.caption1(
fontSize: compact ? 11 : 12, weight: AppTypography.semiBold,
fontWeight: FontWeight.w600, color: color,
), )
: AppTypography.label3(
weight: AppTypography.semiBold,
color: color,
),
), ),
backgroundColor: color.withValues(alpha: 0.12), backgroundColor: color.withValues(alpha: 0.12),
side: BorderSide(color: color.withValues(alpha: 0.3)), 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/app_constants.dart';
import '../../core/constants/route_constants.dart'; import '../../core/constants/route_constants.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/theme/app_typography.dart';
import '../../core/theme/theme_provider.dart'; import '../../core/theme/theme_provider.dart';
import '../../modules/settings/presentation/providers/settings_provider.dart'; import '../../modules/settings/presentation/providers/settings_provider.dart';
import '../models/user_model.dart'; import '../models/user_model.dart';
@ -175,9 +176,9 @@ class _TopNavMenuLabel extends StatelessWidget {
children: [ children: [
Text( Text(
label, label,
style: TextStyle( style: AppTypography.label2(
weight: selected ? AppTypography.semiBold : AppTypography.medium,
color: color, color: color,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
), ),
), ),
if (showChevron) ...[ if (showChevron) ...[

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../core/theme/app_typography.dart';
import '../../core/utils/media_url.dart'; import '../../core/utils/media_url.dart';
/// Circular user avatar with network image fallback to name initial. /// Circular user avatar with network image fallback to name initial.
@ -24,9 +25,10 @@ class UserAvatar extends StatelessWidget {
final fallback = Text( final fallback = Text(
initial, initial,
style: theme.textTheme.titleSmall?.copyWith( style: TextStyle(
fontFamily: AppTypography.fontFamily,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
fontWeight: FontWeight.w700, fontWeight: AppTypography.bold,
fontSize: radius * 0.85, 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> <IfModule mod_rewrite.c>
RewriteEngine On RewriteEngine On
RewriteBase /erp/ RewriteBase /
RewriteRule ^index\.html$ - [L] RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /erp/index.html [L] RewriteRule . /index.html [L]
</IfModule> </IfModule>

View File

@ -17,7 +17,7 @@
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF">
<script> <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 () { (function () {
var hash = window.location.hash; var hash = window.location.hash;
if (!hash || hash.charAt(1) !== '/') return; if (!hash || hash.charAt(1) !== '/') return;