second commit

This commit is contained in:
2026-07-29 11:41:53 +05:30
parent fcccf22bac
commit d72522e737
211 changed files with 19260 additions and 0 deletions

View 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;
}
}

View 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')}';
}
}

View 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);
}
}