bug fix
This commit is contained in:
parent
b3a0366848
commit
167bd99e0a
16
README.md
16
README.md
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 1–6 | 56→20 | Medium, SemiBold, Bold |
|
||||
/// | Body 1–4 | 18→12 | Regular, Medium |
|
||||
/// | Label 1–3 | 16→12 | Regular, Medium, SemiBold, Bold |
|
||||
/// | Caption 1–2 | 10→9 | 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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. You’ll 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,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -142,7 +142,6 @@ class _MasterAppTile extends StatelessWidget {
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -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() ?? '');
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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) ...[
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user