first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
// Helper method
import 'package:flutter/material.dart';
import 'package:iconly/iconly.dart';
import '../../constant.dart';
Widget buildDateSelector({required String prefix, required String date, required ThemeData theme}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
spacing: 5,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
text: '$prefix: ',
style: TextStyle(fontWeight: FontWeight.w500),
children: [
TextSpan(text: date),
],
),
style: theme.textTheme.bodyMedium,
),
Icon(
IconlyLight.calendar,
color: kPeraColor,
),
],
),
],
);
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
enum BorderType {
rect,
rRect,
oval,
}
class CustomDottedBorder extends StatelessWidget {
final Color color;
final BorderType borderType;
final Radius radius;
final EdgeInsets padding;
final Widget child;
const CustomDottedBorder({
super.key,
required this.color,
this.borderType = BorderType.rRect,
this.radius = const Radius.circular(8),
this.padding = const EdgeInsets.all(6),
required this.child,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _DottedBorderPainter(
color: color,
borderType: borderType,
radius: radius,
),
child: Padding(
padding: padding,
child: child,
),
);
}
}
class _DottedBorderPainter extends CustomPainter {
final Color color;
final BorderType borderType;
final Radius radius;
_DottedBorderPainter({
required this.color,
required this.borderType,
required this.radius,
});
@override
void paint(Canvas canvas, Size size) {
final strokeWidth = 1.5;
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final adjustedRect = Rect.fromLTWH(
strokeWidth / 2,
strokeWidth / 2,
size.width - strokeWidth,
size.height - strokeWidth,
);
final path = Path();
switch (borderType) {
case BorderType.rect:
path.addRect(adjustedRect);
break;
case BorderType.rRect:
path.addRRect(RRect.fromRectAndRadius(adjustedRect, radius));
break;
case BorderType.oval:
path.addOval(adjustedRect);
break;
}
final dashPath = Path();
const dashWidth = 4.0;
const dashSpace = 4.0;
for (final pathMetric in path.computeMetrics()) {
var distance = 0.0;
while (distance < pathMetric.length) {
final next = distance + dashWidth;
dashPath.addPath(
pathMetric.extractPath(distance, next),
Offset.zero,
);
distance += dashWidth + dashSpace;
}
}
canvas.drawPath(dashPath, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true; // Repaint if color or size changes
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/constant.dart';
Widget globalDottedLine({double? height, double? width, Color? borderColor, int? generatedLine}) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
child: Row(
spacing: 2,
children: List.generate(generatedLine ?? 80, (index) {
return Container(
height: height ?? 1,
width: width ?? 4,
color: borderColor ?? kBorderColor,
);
}),
),
);
}

View File

@@ -0,0 +1,331 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:mobile_pos/constant.dart';
class EmptyWidget extends StatelessWidget {
const EmptyWidget({
super.key,
this.message,
});
final TextSpan? message;
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 260),
child: AspectRatio(
aspectRatio: 1,
child: Image.asset("assets/empty_placeholder.png"),
),
),
),
if (message != null) ...[
const SizedBox.square(dimension: 12),
Text.rich(
message!,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 20,
),
),
]
],
),
);
}
}
// Avatar Widget
class CircleAvatarWidget extends StatelessWidget {
final String? name;
final Size? size;
const CircleAvatarWidget({super.key, this.name, this.size});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: size?.height ?? 50,
width: size?.width ?? 50,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: kMainColor50,
),
clipBehavior: Clip.antiAlias,
child: Text(
(name != null && name!.length >= 2) ? name!.substring(0, 2) : (name != null ? name! : ''),
style: theme.textTheme.titleMedium?.copyWith(
color: kMainColor,
fontWeight: FontWeight.w500,
),
),
);
}
}
class EmptyWidgetUpdated extends StatelessWidget {
const EmptyWidgetUpdated({
super.key,
this.message,
});
final TextSpan? message;
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
'assets/empty_image.svg',
width: 319,
height: 250,
placeholderBuilder: (BuildContext context) => CircularProgressIndicator(),
),
if (message != null) ...[
const SizedBox.square(dimension: 12),
Text.rich(
message!,
textAlign: TextAlign.center,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 20,
),
),
]
],
),
);
}
}
class PermitDenyWidget extends StatefulWidget {
const PermitDenyWidget({
super.key,
this.message,
});
final TextSpan? message;
@override
State<PermitDenyWidget> createState() => _PermitDenyWidgetState();
}
class _PermitDenyWidgetState extends State<PermitDenyWidget> with TickerProviderStateMixin {
// Track drag offsets
double _dragX = 0;
double _dragY = 0;
// Animation controller for fade-in & reset bounce
late AnimationController _controller;
late Animation<double> _opacityAnimation;
// Animation controller for bounce-back effect after drag ends
late AnimationController _bounceController;
late Animation<double> _bounceAnimationX;
late Animation<double> _bounceAnimationY;
// Limits for rotation angles (radians)
static const double maxRotationX = 0.15; // ~8.6 degrees
static const double maxRotationY = 0.15;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_opacityAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
_controller.forward();
_bounceController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_bounceAnimationX =
Tween<double>(begin: 0, end: 0).animate(CurvedAnimation(parent: _bounceController, curve: Curves.elasticOut))
..addListener(() {
setState(() {
_dragX = _bounceAnimationX.value;
});
});
_bounceAnimationY =
Tween<double>(begin: 0, end: 0).animate(CurvedAnimation(parent: _bounceController, curve: Curves.elasticOut))
..addListener(() {
setState(() {
_dragY = _bounceAnimationY.value;
});
});
}
@override
void dispose() {
_controller.dispose();
_bounceController.dispose();
super.dispose();
}
void _onPanUpdate(DragUpdateDetails details) {
if (_bounceController.isAnimating) _bounceController.stop();
setState(() {
_dragX += details.delta.dx;
_dragY += details.delta.dy;
_dragX = _dragX.clamp(-100, 100);
_dragY = _dragY.clamp(-100, 100);
});
}
void _onPanEnd(DragEndDetails details) {
// Animate back to center with bounce
_bounceAnimationX = Tween<double>(begin: _dragX, end: 0).animate(
CurvedAnimation(parent: _bounceController, curve: Curves.elasticOut),
)..addListener(() {
setState(() {
_dragX = _bounceAnimationX.value;
});
});
_bounceAnimationY = Tween<double>(begin: _dragY, end: 0).animate(
CurvedAnimation(parent: _bounceController, curve: Curves.elasticOut),
)..addListener(() {
setState(() {
_dragY = _bounceAnimationY.value;
});
});
_bounceController.forward(from: 0);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final rotationY = (_dragX / 100) * maxRotationY;
final rotationX = -(_dragY / 100) * maxRotationX;
final dragDistance = (_dragX.abs() + _dragY.abs()) / 200;
final scale = 1 - (dragDistance * 0.07);
// Add a glowing border on drag to emphasize interaction
final glowColor = theme.colorScheme.primary.withOpacity(0.4 * dragDistance);
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: FadeTransition(
opacity: _opacityAnimation,
child: GestureDetector(
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001) // perspective
..rotateX(rotationX)
..rotateY(rotationY)
..scale(scale),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
theme.colorScheme.surface,
theme.colorScheme.surfaceVariant.withOpacity(0.9),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 25,
offset: const Offset(0, 15),
),
BoxShadow(
color: glowColor,
blurRadius: 30,
spreadRadius: 5,
),
],
border: Border.all(
color: glowColor,
width: dragDistance > 0 ? 2 : 0,
),
),
padding: const EdgeInsets.all(28),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: SvgPicture.asset(
'assets/empty_image.svg',
width: 320,
height: 260,
placeholderBuilder: (context) => const CircularProgressIndicator(),
),
),
const SizedBox(height: 20),
Text.rich(
widget.message ??
TextSpan(
text: "You don't have the necessary permissions.",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 22,
color: theme.colorScheme.onBackground,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.1),
offset: const Offset(0, 1),
blurRadius: 2,
),
],
),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
"Please contact your administrator to request access.",
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onBackground.withOpacity(0.6),
),
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
class KeyValueRow extends StatelessWidget {
const KeyValueRow({
super.key,
required this.title,
this.titleFlex = 1,
this.titleStyle,
this.titleMaxLines,
this.titleOverflow,
required this.description,
this.descriptionFlex = 1,
this.descriptionStyle,
this.descriptionMaxLines,
this.descriptionOverflow,
this.centerSpace = 8,
this.bottomSpace = 8,
});
final String title;
final int titleFlex;
final TextStyle? titleStyle;
final int? titleMaxLines;
final TextOverflow? titleOverflow;
final String description;
final int descriptionFlex;
final TextStyle? descriptionStyle;
final int? descriptionMaxLines;
final TextOverflow? descriptionOverflow;
final double centerSpace;
final double bottomSpace;
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _titleStyle = titleStyle ??
_theme.textTheme.bodyMedium?.copyWith(
color: Color(0xff4B5563),
fontSize: 14,
fontWeight: FontWeight.w500,
);
final _descriptionStyle = descriptionStyle ??
_titleStyle?.copyWith(
color: Color(0xff121535),
fontSize: 14,
fontWeight: FontWeight.w500,
);
return Padding(
padding: EdgeInsets.only(bottom: bottomSpace),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: titleFlex,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
title,
maxLines: titleMaxLines,
overflow: titleOverflow,
style: _titleStyle,
),
),
Text(':', style: _titleStyle),
],
),
),
SizedBox(width: centerSpace),
Expanded(
flex: descriptionFlex,
child: Text(
description,
maxLines: descriptionMaxLines,
overflow: descriptionOverflow,
style: _descriptionStyle,
),
)
],
),
);
}
}

View File

@@ -0,0 +1,97 @@
class PaymentsTransaction {
num? id;
String? platform;
String? transactionType;
num? amount;
String? date;
String? invoiceNo;
num? referenceId;
num? paymentTypeId;
TransactionMeta? meta;
PaymentType? paymentType;
PaymentsTransaction({
this.id,
this.platform,
this.transactionType,
this.amount,
this.date,
this.invoiceNo,
this.referenceId,
this.paymentTypeId,
this.meta,
this.paymentType,
});
PaymentsTransaction.fromJson(dynamic json) {
id = json['id'];
platform = json['platform'];
transactionType = json['transaction_type'];
amount = num.tryParse(json['amount'].toString());
date = json['date'];
invoiceNo = json['invoice_no'];
referenceId = json['reference_id'];
paymentTypeId = json['payment_type_id'];
meta = json['meta'] != null ? TransactionMeta.fromJson(json['meta']) : null;
paymentType = json['payment_type'] != null ? PaymentType.fromJson(json['payment_type']) : null;
}
}
class TransactionMeta {
String? chequeNumber;
String? status;
TransactionMeta({this.chequeNumber, this.status});
TransactionMeta.fromJson(dynamic json) {
chequeNumber = json['cheque_number'];
status = json['status'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {};
data['cheque_number'] = chequeNumber;
data['status'] = status;
return data;
}
}
class PaymentType {
PaymentType({
this.id,
this.name,
this.paymentTypeMeta,
});
PaymentType.fromJson(dynamic json) {
id = json['id'];
name = json['name'];
paymentTypeMeta = json['meta'] != null ? PaymentTypeMeta.fromJson(json['meta']) : null;
}
num? id;
String? name;
PaymentTypeMeta? paymentTypeMeta;
}
class PaymentTypeMeta {
PaymentTypeMeta({
this.accountNumber,
this.ifscCode,
this.holderName,
this.bankName,
this.upiId,
});
PaymentTypeMeta.fromJson(dynamic json) {
accountNumber = json['account_number'];
ifscCode = json['routing_number']; // proper IFSC code
holderName = json['account_holder'];
bankName = json['bank_name'];
upiId = json['upi_id'];
}
String? accountNumber;
String? ifscCode;
String? holderName;
String? bankName;
String? upiId;
}

View File

@@ -0,0 +1,352 @@
// ignore_for_file: library_private_types_in_public_api, unused_result
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hugeicons/hugeicons.dart';
import 'package:nb_utils/nb_utils.dart';
import '../../Screens/cash and bank/bank account/provider/bank_account_provider.dart';
import '../../constant.dart';
import '../../generated/l10n.dart' as lang;
import 'model/payment_transaction_model.dart';
class PaymentEntry {
String? type;
final TextEditingController amountController = TextEditingController();
final TextEditingController chequeNumberController = TextEditingController();
final GlobalKey<FormFieldState> typeKey = GlobalKey<FormFieldState>();
final GlobalKey<FormFieldState> amountKey = GlobalKey<FormFieldState>();
PaymentEntry({this.type});
void dispose() {
amountController.dispose();
chequeNumberController.dispose();
}
Map<String, dynamic> toJson() {
return {
'type': type,
'amount': num.tryParse(amountController.text) ?? 0,
'cheque_number': chequeNumberController.text,
};
}
}
class MultiPaymentWidget extends ConsumerStatefulWidget {
final TextEditingController totalAmountController;
final bool showChequeOption;
final bool showWalletOption;
final bool hideAddButton;
final bool disableDropdown;
final VoidCallback? onPaymentListChanged;
final List<PaymentsTransaction>? initialTransactions;
const MultiPaymentWidget({
super.key,
required this.totalAmountController,
this.showChequeOption = false,
this.showWalletOption = false,
this.hideAddButton = false,
this.disableDropdown = false,
this.onPaymentListChanged,
this.initialTransactions,
});
@override
MultiPaymentWidgetState createState() => MultiPaymentWidgetState();
}
class MultiPaymentWidgetState extends ConsumerState<MultiPaymentWidget> {
List<PaymentEntry> _paymentEntries = [];
bool _isSyncing = false;
/// Public method to get payment entries
/// This can be accessed via a GlobalKey
List<PaymentEntry> getPaymentEntries() {
return _paymentEntries;
}
@override
void initState() {
super.initState();
_initializePaymentEntries();
// This listener syncs from TOTAL -> PAYMENT (if 1 row)
widget.totalAmountController.addListener(_onTotalAmountSync);
// This listener syncs from PAYMENT -> TOTAL (for all cases)
_paymentEntries[0].amountController.addListener(_calculateTotalsFromPayments);
}
void _initializePaymentEntries() {
if (widget.initialTransactions != null && widget.initialTransactions!.isNotEmpty) {
for (var trans in widget.initialTransactions!) {
String type = 'Cash';
if (trans.transactionType?.toLowerCase().contains('cheque') ?? false) {
type = 'Cheque';
} else if (trans.paymentTypeId != null) {
type = trans.paymentTypeId.toString();
} else if (trans.transactionType?.toLowerCase().contains('cash') ?? false) {
type = 'Cash';
}
PaymentEntry entry = PaymentEntry(type: type);
entry.amountController.text = trans.amount?.toString() ?? '0';
entry.amountController.addListener(_calculateTotalsFromPayments);
if (type == 'Cheque') {
entry.chequeNumberController.text = trans.meta?.chequeNumber ?? '';
}
_paymentEntries.add(entry);
}
} else {
_paymentEntries = [PaymentEntry(type: 'Cash')];
_paymentEntries[0].amountController.addListener(_calculateTotalsFromPayments);
}
}
@override
void dispose() {
widget.totalAmountController.removeListener(_onTotalAmountSync);
for (var entry in _paymentEntries) {
entry.amountController.removeListener(_calculateTotalsFromPayments);
entry.dispose();
}
super.dispose();
}
// Listener for the main "Total Amount" field
void _onTotalAmountSync() {
if (_isSyncing || _paymentEntries.length != 1) return;
_isSyncing = true;
final totalText = widget.totalAmountController.text;
if (_paymentEntries[0].amountController.text != totalText) {
_paymentEntries[0].amountController.text = totalText;
}
setState(() {});
_isSyncing = false;
}
// Listener for all payment amount fields
void _calculateTotalsFromPayments() {
if (_isSyncing) return;
_isSyncing = true;
double total = 0.0;
for (var entry in _paymentEntries) {
total += double.tryParse(entry.amountController.text) ?? 0.0;
}
setState(() {
if (mounted) {
// Only update parent if value is different to avoid infinite loop
if (widget.totalAmountController.text != total.toStringAsFixed(2)) {
widget.totalAmountController.text = total.toStringAsFixed(2);
}
}
});
_isSyncing = false;
}
// Add listener when adding a new row
void _addPaymentRow() {
final newEntry = PaymentEntry();
newEntry.amountController.addListener(_calculateTotalsFromPayments);
setState(() {
_paymentEntries.add(newEntry);
});
widget.onPaymentListChanged?.call();
_calculateTotalsFromPayments();
}
// Remove listener when removing a row
void _removePaymentRow(int index) {
if (_paymentEntries.length > 1) {
final entry = _paymentEntries[index];
entry.amountController.removeListener(_calculateTotalsFromPayments);
entry.dispose();
setState(() {
_paymentEntries.removeAt(index);
});
widget.onPaymentListChanged?.call();
_calculateTotalsFromPayments();
} else {
EasyLoading.showError('At least one payment method is required');
}
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
final _lang = lang.S.of(context);
final bankListAsync = ref.watch(bankListProvider);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
///________PaymentType__________________________________
Text(
lang.S.of(context).paymentTypes,
style: _theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 10),
// Build dynamic payment rows
bankListAsync.when(
data: (bankData) {
List<DropdownMenuItem<String>> paymentTypeItems = [
DropdownMenuItem(
value: 'Cash',
child: Text(lang.S.of(context).cash),
),
if (widget.showWalletOption)
const DropdownMenuItem(
value: 'wallet',
child: Text("Wallet"),
),
if (widget.showChequeOption)
const DropdownMenuItem(
value: 'Cheque',
child: Text("Cheque"),
),
...(bankData.data?.map((bank) => DropdownMenuItem(
value: bank.id.toString(),
child: Text(bank.name ?? 'Unknown Bank'),
)) ??
[]),
];
return Column(
children: [
..._paymentEntries.asMap().entries.map((entry) {
int index = entry.key;
PaymentEntry payment = entry.value;
return _buildPaymentRow(payment, index, paymentTypeItems,
readonly: widget.hideAddButton, disableDropdown: widget.disableDropdown);
}),
if (!widget.hideAddButton) const SizedBox(height: 4),
// "Add Payment" Button
if (!widget.hideAddButton)
SizedBox(
width: double.infinity,
child: TextButton.icon(
icon: const Icon(Icons.add),
label: Text(_lang.addPayment),
onPressed: _addPaymentRow,
style: TextButton.styleFrom(
foregroundColor: kMainColor,
side: const BorderSide(color: kMainColor),
),
),
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Text('Error loading banks: $err'),
),
],
);
}
Widget _buildPaymentRow(PaymentEntry payment, int index, List<DropdownMenuItem<String>> paymentTypeItems,
{bool readonly = false, bool disableDropdown = false}) {
return Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Payment Type Dropdown
Expanded(
flex: 3,
child: DropdownButtonFormField<String>(
isExpanded: true,
icon: Icon(
Icons.keyboard_arrow_down,
color: kGreyTextColor,
),
key: payment.typeKey,
value: payment.type,
hint: Text(lang.S.of(context).selectType),
items: paymentTypeItems,
onChanged: disableDropdown
? null
: (value) {
setState(() {
payment.type = value;
});
},
validator: (value) {
if (value == null) {
return 'Required';
}
return null;
},
),
),
const SizedBox(width: 10),
// Amount Field
Expanded(
flex: 2,
child: TextFormField(
key: payment.amountKey,
readOnly: readonly,
controller: payment.amountController,
decoration: kInputDecoration.copyWith(labelText: lang.S.of(context).amount, hintText: 'Ex: 10'),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
validator: (value) {
if (value.isEmptyOrNull) {
return 'Required';
}
if ((double.tryParse(value!) ?? 0) < 0) {
return 'Invalid';
}
return null;
},
onChanged: (val) {},
),
),
// Remove Button
if (_paymentEntries.length > 1)
IconButton(
icon: HugeIcon(
icon: HugeIcons.strokeRoundedDelete02,
color: Colors.red,
),
onPressed: () => _removePaymentRow(index),
),
],
),
// Conditional Cheque Number field
if (payment.type == 'Cheque')
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: TextFormField(
controller: payment.chequeNumberController,
decoration: kInputDecoration.copyWith(
labelText: lang.S.of(context).chequeNumber,
hintText: 'Ex: 12345689',
),
),
),
const SizedBox(height: 15),
],
);
}
}

View File

@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../constant.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../constant.dart';
class PageNavigationListView extends StatefulWidget {
const PageNavigationListView({
super.key,
this.header,
this.footer,
required this.navTiles,
this.onTap,
});
final Widget? header;
final Widget? footer;
final List<PageNavigationNavTile> navTiles;
final void Function(PageNavigationNavTile value)? onTap;
@override
State<PageNavigationListView> createState() => _PageNavigationListViewState();
}
class _PageNavigationListViewState extends State<PageNavigationListView> {
PageNavigationNavTile? selectedChildTile;
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return ListView(
physics: const ClampingScrollPhysics(),
children: [
if (widget.header != null) widget.header!,
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 0),
child: ListView.separated(
shrinkWrap: true,
primary: false,
itemCount: widget.navTiles.length,
itemBuilder: (context, index) {
final _navTile = widget.navTiles[index];
// =============================
// EXPANSION TILE
// =============================
if (_navTile.type == PageNavigationListTileType.expansion) {
return Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 4),
leading: SvgPicture.asset(
_navTile.svgIconPath,
height: 36,
width: 36,
),
title: Text(_navTile.title),
children: (_navTile.children ?? []).map((child) {
final isSelected = selectedChildTile == child;
return ListTile(
leading: SizedBox(),
onTap: () {
setState(() => selectedChildTile = child);
widget.onTap?.call(child);
},
title: Text(
child.title,
style: _theme.textTheme.bodyMedium?.copyWith(
color: isSelected ? kMainColor : kTitleColor,
),
),
contentPadding: EdgeInsetsDirectional.only(start: 22),
visualDensity: const VisualDensity(
vertical: -4,
horizontal: -2,
),
);
}).toList(),
),
);
}
// =============================
// NORMAL TILE (unchanged)
// =============================
return ListTile(
onTap: () => widget.onTap?.call(_navTile),
leading: SvgPicture.asset(
_navTile.svgIconPath,
height: 36,
width: 36,
),
title: Text(_navTile.title),
trailing: (_navTile.hideTrailing ?? false)
? null
: _navTile.trailing ??
const Icon(
Icons.arrow_forward_ios,
size: 20,
color: kGreyTextColor,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
);
},
separatorBuilder: (_, __) => const Divider(height: 1.5),
),
),
if (widget.footer != null) widget.footer!,
],
);
}
}
class PageNavigationNavTile<T> {
final String title;
final Widget? trailing;
final Color? color;
final String svgIconPath;
final PageNavigationListTileType type;
final Widget? route;
final bool? hideTrailing;
final T? value;
final List<PageNavigationNavTile<T>>? children;
const PageNavigationNavTile({
required this.title,
this.trailing,
this.color,
required this.svgIconPath,
this.type = PageNavigationListTileType.navigation,
this.route,
this.value,
this.hideTrailing,
this.children,
}) : assert(
type != PageNavigationListTileType.navigation || value == null,
'value cannot be assigned in navigation type',
);
}
enum PageNavigationListTileType { navigation, tool, function, expansion }
// class PageNavigationListView extends StatelessWidget {
// const PageNavigationListView({
// super.key,
// this.header,
// this.footer,
// required this.navTiles,
// this.onTap,
// });
//
// final Widget? header;
// final Widget? footer;
// final List<PageNavigationNavTile> navTiles;
// final void Function(PageNavigationNavTile value)? onTap;
//
// @override
// Widget build(BuildContext context) {
// final _theme = Theme.of(context);
//
// return ListView(
// physics: const ClampingScrollPhysics(),
// children: [
// // Header
// if (header != null) header!,
//
// // Nav Items
// Padding(
// padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 0),
// child: ListView.separated(
// shrinkWrap: true,
// primary: false,
// itemCount: navTiles.length,
// itemBuilder: (context, index) {
// final _navTile = navTiles[index];
//
// return Material(
// color: Colors.transparent,
// child: ListTile(
// onTap: () => onTap?.call(_navTile),
// leading: SvgPicture.asset(
// _navTile.svgIconPath,
// height: 36,
// width: 36,
// ),
// title: Text(_navTile.title),
// titleTextStyle: _theme.textTheme.bodyLarge,
// trailing: (_navTile.hideTrailing ?? false)
// ? null
// : _navTile.trailing ??
// const Icon(
// Icons.arrow_forward_ios,
// size: 20,
// color: kGreyTextColor,
// ),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(4),
// ),
// tileColor: _theme.colorScheme.primaryContainer,
// contentPadding: const EdgeInsets.symmetric(horizontal: 4),
// visualDensity: const VisualDensity(
// vertical: -1,
// horizontal: -2,
// ),
// ),
// );
// },
// separatorBuilder: (c, i) => const Divider(height: 1.5),
// ),
// ),
//
// // Footer
// if (footer != null) footer!,
// ],
// );
// }
// }
//
// class PageNavigationNavTile<T> {
// final String title;
// final Widget? trailing;
// final Color? color;
// final String svgIconPath;
// final PageNavigationListTileType type;
// final Widget? route;
// final bool? hideTrailing;
// final T? value;
//
// const PageNavigationNavTile({
// required this.title,
// this.trailing,
// this.color,
// required this.svgIconPath,
// this.type = PageNavigationListTileType.navigation,
// this.route,
// this.value,
// this.hideTrailing,
// }) : assert(
// type != PageNavigationListTileType.navigation || value == null,
// 'value cannot be assigned in navigation type',
// );
// }
//
// enum PageNavigationListTileType { navigation, tool, function }

View File

View File

@@ -0,0 +1,66 @@
import 'dart:io';
import 'package:flutter/material.dart';
class UniversalImage extends StatelessWidget {
final String? imagePath;
final double? height;
final double? width;
final BoxFit fit;
final Widget? placeholder;
const UniversalImage({
super.key,
required this.imagePath,
this.height,
this.width,
this.fit = BoxFit.contain,
this.placeholder,
});
@override
Widget build(BuildContext context) {
if (imagePath == null || imagePath!.isEmpty) {
return _placeholder();
}
/// Network Image
if (imagePath!.startsWith('http')) {
return Image.network(
imagePath!,
height: height,
width: width,
fit: fit,
errorBuilder: (_, __, ___) => _placeholder(),
);
}
/// Local File Image
if (imagePath!.startsWith('/')) {
return Image.file(
File(imagePath!),
height: height,
width: width,
fit: fit,
errorBuilder: (_, __, ___) => _placeholder(),
);
}
/// Asset Image
return Image.asset(
imagePath!,
height: height,
width: width,
fit: fit,
errorBuilder: (_, __, ___) => _placeholder(),
);
}
Widget _placeholder() {
return placeholder ??
SizedBox(
height: height,
width: width,
child: const Icon(Icons.image_not_supported, size: 40),
);
}
}

View File

@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import '../../constant.dart';
void viewModalSheet({
required BuildContext context,
required Map<String, String> item,
String? description,
bool? showImage,
String? image,
String? descriptionTitle,
}) {
final _theme = Theme.of(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
isDismissible: false,
builder: (BuildContext context) {
return SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsetsDirectional.only(start: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'View Details',
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.close, size: 18),
)
],
),
),
Divider(color: kBorderColor, height: 1),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showImage == true) ...[
SizedBox(height: 15),
image != null
? Center(
child: Container(
height: 120,
width: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(image),
),
),
),
)
: Center(
child: Image.asset(
height: 120,
width: 120,
fit: BoxFit.cover,
'assets/hrm/image_icon.jpg',
),
),
SizedBox(height: 21),
],
Column(
children: item.entries.map((entry) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
flex: 2,
child: Text(
'${entry.key} ',
style: _theme.textTheme.bodyLarge?.copyWith(
color: kNeutral800,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(width: 8),
Flexible(
fit: FlexFit.tight,
flex: 4,
child: Text(
': ${entry.value}',
style: _theme.textTheme.bodyLarge,
),
),
],
),
);
}).toList(),
),
if (description != null) ...[
SizedBox(height: 16),
Text.rich(
TextSpan(
text: descriptionTitle ?? 'Description : ',
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
children: [
TextSpan(
text: description,
style: _theme.textTheme.bodyLarge?.copyWith(
color: kNeutral800,
),
)
]),
),
],
],
),
),
],
),
);
},
);
}