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`.
|
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
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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 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 {
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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. You’ll need to sign in again afterward.',
|
'Enter your current password and choose a new one. You’ll 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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() ?? '');
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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) ...[
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user