second commit
This commit is contained in:
59
lib/core/utils/extensions.dart
Normal file
59
lib/core/utils/extensions.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_dimens.dart';
|
||||
|
||||
extension BuildContextX on BuildContext {
|
||||
ThemeData get theme => Theme.of(this);
|
||||
TextTheme get text => Theme.of(this).textTheme;
|
||||
ColorScheme get colors => Theme.of(this).colorScheme;
|
||||
|
||||
Size get screen => MediaQuery.sizeOf(this);
|
||||
double get screenWidth => MediaQuery.sizeOf(this).width;
|
||||
double get screenHeight => MediaQuery.sizeOf(this).height;
|
||||
|
||||
bool get isCompact => AppBreakpoints.isCompact(screenWidth);
|
||||
bool get isMedium => AppBreakpoints.isMedium(screenWidth);
|
||||
bool get isExpanded => AppBreakpoints.isExpanded(screenWidth);
|
||||
|
||||
/// Picks a value appropriate to the current breakpoint.
|
||||
T responsive<T>({required T compact, T? medium, required T expanded}) {
|
||||
if (isCompact) return compact;
|
||||
if (isExpanded) return expanded;
|
||||
return medium ?? expanded;
|
||||
}
|
||||
|
||||
void showSnack(String message, {Color? background}) {
|
||||
ScaffoldMessenger.of(this)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: background,
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
extension DoubleX on double {
|
||||
/// Rounds to two decimals to avoid floating point drift in money maths.
|
||||
double get asMoney => (this * 100).roundToDouble() / 100;
|
||||
|
||||
/// Indian retail round-off to the nearest rupee.
|
||||
double get roundedToRupee => roundToDouble();
|
||||
}
|
||||
|
||||
extension StringX on String {
|
||||
String get capitalized =>
|
||||
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
|
||||
|
||||
bool containsIgnoreCase(String other) =>
|
||||
toLowerCase().contains(other.toLowerCase());
|
||||
}
|
||||
|
||||
extension IterableX<T> on Iterable<T> {
|
||||
T? firstWhereOrNull(bool Function(T) test) {
|
||||
for (final element in this) {
|
||||
if (test(element)) return element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
67
lib/core/utils/formatters.dart
Normal file
67
lib/core/utils/formatters.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../constants/app_constants.dart';
|
||||
|
||||
/// Currency, date and number formatting helpers.
|
||||
class Formatters {
|
||||
const Formatters._();
|
||||
|
||||
static final NumberFormat _currency = NumberFormat.currency(
|
||||
locale: AppConstants.locale,
|
||||
symbol: AppConstants.currencySymbol,
|
||||
decimalDigits: 2,
|
||||
);
|
||||
|
||||
static final NumberFormat _compactCurrency = NumberFormat.compactCurrency(
|
||||
locale: AppConstants.locale,
|
||||
symbol: AppConstants.currencySymbol,
|
||||
decimalDigits: 1,
|
||||
);
|
||||
|
||||
static final DateFormat _time = DateFormat('hh:mm a');
|
||||
static final DateFormat _date = DateFormat('dd MMM yyyy');
|
||||
static final DateFormat _dateTime = DateFormat('dd MMM yyyy, hh:mm a');
|
||||
static final DateFormat _receiptStamp = DateFormat('dd/MM/yyyy HH:mm:ss');
|
||||
|
||||
static String money(num value) => _currency.format(value);
|
||||
static String moneyCompact(num value) => _compactCurrency.format(value);
|
||||
|
||||
/// Amount without the symbol — used where the symbol is styled separately.
|
||||
static String amount(num value) => value.toStringAsFixed(2);
|
||||
|
||||
static String time(DateTime dt) => _time.format(dt);
|
||||
static String date(DateTime dt) => _date.format(dt);
|
||||
static String dateTime(DateTime dt) => _dateTime.format(dt);
|
||||
static String receiptStamp(DateTime dt) => _receiptStamp.format(dt);
|
||||
|
||||
static String percent(double fraction) =>
|
||||
'${(fraction * 100).toStringAsFixed(fraction * 100 % 1 == 0 ? 0 : 1)}%';
|
||||
|
||||
/// `9876543210` -> `98765 43210`
|
||||
static String mobile(String raw) {
|
||||
final digits = raw.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length != 10) return raw;
|
||||
return '${digits.substring(0, 5)} ${digits.substring(5)}';
|
||||
}
|
||||
|
||||
/// Masks all but the last four digits for on-screen privacy.
|
||||
static String maskedMobile(String raw) {
|
||||
final digits = raw.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length < 4) return raw;
|
||||
return '${'\u2022' * (digits.length - 4)}${digits.substring(digits.length - 4)}';
|
||||
}
|
||||
|
||||
static String initials(String name) {
|
||||
final parts = name.trim().split(RegExp(r'\s+')).where((p) => p.isNotEmpty);
|
||||
if (parts.isEmpty) return '?';
|
||||
if (parts.length == 1) return parts.first.substring(0, 1).toUpperCase();
|
||||
return (parts.first.substring(0, 1) + parts.last.substring(0, 1))
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
static String invoiceNumber(int sequence, DateTime date) {
|
||||
final y = date.year.toString().substring(2);
|
||||
final m = date.month.toString().padLeft(2, '0');
|
||||
return 'INV-$y$m-${sequence.toString().padLeft(5, '0')}';
|
||||
}
|
||||
}
|
||||
56
lib/core/utils/validators.dart
Normal file
56
lib/core/utils/validators.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import '../constants/app_constants.dart';
|
||||
|
||||
/// Form field validators returning a message, or null when valid.
|
||||
class Validators {
|
||||
const Validators._();
|
||||
|
||||
static final RegExp _emailRe =
|
||||
RegExp(r'^[\w.+-]+@([\w-]+\.)+[A-Za-z]{2,}$');
|
||||
static final RegExp _mobileRe = RegExp(r'^[6-9]\d{9}$');
|
||||
|
||||
static String? mobile(String? value) {
|
||||
final v = (value ?? '').replaceAll(RegExp(r'\D'), '');
|
||||
if (v.isEmpty) return 'Mobile number is required';
|
||||
if (v.length != AppConstants.mobileNumberLength) {
|
||||
return 'Enter all ${AppConstants.mobileNumberLength} digits';
|
||||
}
|
||||
if (!_mobileRe.hasMatch(v)) return 'Enter a valid Indian mobile number';
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? name(String? value) {
|
||||
final v = (value ?? '').trim();
|
||||
if (v.isEmpty) return 'Customer name is required';
|
||||
if (v.length < 2) return 'Name is too short';
|
||||
if (v.length > 60) return 'Name is too long';
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? emailOptional(String? value) {
|
||||
final v = (value ?? '').trim();
|
||||
if (v.isEmpty) return null;
|
||||
if (!_emailRe.hasMatch(v)) return 'Enter a valid email address';
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? dobOptional(DateTime? value) {
|
||||
if (value == null) return null;
|
||||
final now = DateTime.now();
|
||||
if (value.isAfter(now)) return 'Date of birth cannot be in the future';
|
||||
if (now.year - value.year > 120) return 'Enter a valid date of birth';
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? positiveAmount(String? value) {
|
||||
final v = double.tryParse((value ?? '').trim());
|
||||
if (v == null) return 'Enter a valid amount';
|
||||
if (v <= 0) return 'Amount must be greater than zero';
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool isLikelyBarcode(String value) {
|
||||
final v = value.trim();
|
||||
return v.length >= AppConstants.minBarcodeLength &&
|
||||
RegExp(r'^\d+$').hasMatch(v);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user