first commit
This commit is contained in:
33
lib/widgets/build_date_selector/build_date_selector.dart
Normal file
33
lib/widgets/build_date_selector/build_date_selector.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
105
lib/widgets/dotted_border/custom_dotted_border.dart
Normal file
105
lib/widgets/dotted_border/custom_dotted_border.dart
Normal 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
|
||||
}
|
||||
}
|
||||
19
lib/widgets/dotted_border/global_dotted_border.dart
Normal file
19
lib/widgets/dotted_border/global_dotted_border.dart
Normal 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,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
331
lib/widgets/empty_widget/_empty_widget.dart
Normal file
331
lib/widgets/empty_widget/_empty_widget.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
90
lib/widgets/key_values/key_values_widget.dart
Normal file
90
lib/widgets/key_values/key_values_widget.dart
Normal 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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
352
lib/widgets/multipal payment mathods/multi_payment_widget.dart
Normal file
352
lib/widgets/multipal payment mathods/multi_payment_widget.dart
Normal 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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
251
lib/widgets/page_navigation_list/_page_navigation_list.dart
Normal file
251
lib/widgets/page_navigation_list/_page_navigation_list.dart
Normal 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 }
|
||||
0
lib/widgets/payment_utils.dart
Normal file
0
lib/widgets/payment_utils.dart
Normal file
66
lib/widgets/universal_image.dart
Normal file
66
lib/widgets/universal_image.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
137
lib/widgets/view_modal_shet/view_modal_bottom_shet.dart
Normal file
137
lib/widgets/view_modal_shet/view_modal_bottom_shet.dart
Normal 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,
|
||||
),
|
||||
)
|
||||
]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user