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,6 @@
class APIConfig {
static String domain = 'https://kulakpos.id/';
static String url = '${domain}api/v1';
static String registerUrl = '/sign-up';
static String businessCategoriesUrl = '/business-categories';
}

View File

@@ -0,0 +1,190 @@
import 'package:nb_utils/nb_utils.dart';
List<Map<String, String>> languageData = [
{"code": "ab", "name": "Abkhaz", "nativeName": "аҧсуа"},
{"code": "aa", "name": "Afar", "nativeName": "Afaraf"},
{"code": "af", "name": "Afrikaans", "nativeName": "Afrikaans"},
{"code": "ak", "name": "Akan", "nativeName": "Akan"},
{"code": "sq", "name": "Albanian", "nativeName": "Shqip"},
{"code": "am", "name": "Amharic", "nativeName": "አማርኛ"},
{"code": "ar", "name": "Arabic", "nativeName": "العربية"},
{"code": "an", "name": "Aragonese", "nativeName": "Aragonés"},
{"code": "hy", "name": "Armenian", "nativeName": "Հայերեն"},
{"code": "as", "name": "Assamese", "nativeName": "অসমীয়া"},
{"code": "av", "name": "Avaric", "nativeName": "авар мацӀ, магӀарул мацӀ"},
{"code": "ae", "name": "Avestan", "nativeName": "avesta"},
{"code": "ay", "name": "Aymara", "nativeName": "aymar aru"},
{"code": "az", "name": "Azerbaijani", "nativeName": "azərbaycan dili"},
{"code": "bm", "name": "Bambara", "nativeName": "bamanankan"},
{"code": "ba", "name": "Bashkir", "nativeName": "башҡорт теле"},
{"code": "eu", "name": "Basque", "nativeName": "euskara, euskera"},
{"code": "be", "name": "Belarusian", "nativeName": "Беларуская"},
{"code": "bn", "name": "Bengali", "nativeName": "বাংলা"},
{"code": "bh", "name": "Bihari", "nativeName": "भोजपुरी"},
{"code": "bi", "name": "Bislama", "nativeName": "Bislama"},
{"code": "bs", "name": "Bosnian", "nativeName": "bosanski jezik"},
{"code": "br", "name": "Breton", "nativeName": "brezhoneg"},
{"code": "bg", "name": "Bulgarian", "nativeName": "български език"},
{"code": "my", "name": "Burmese", "nativeName": "ဗမာစာ"},
{"code": "ca", "name": "Catalan; Valencian", "nativeName": "Català"},
{"code": "ch", "name": "Chamorro", "nativeName": "Chamoru"},
{"code": "ce", "name": "Chechen", "nativeName": "нохчийн мотт"},
{"code": "ny", "name": "Chichewa; Chewa; Nyanja", "nativeName": "chiCheŵa, chinyanja"},
{"code": "zh", "name": "Chinese", "nativeName": "中文 (Zhōngwén), 汉语, 漢語"},
{"code": "cv", "name": "Chuvash", "nativeName": "чӑваш чӗлхи"},
{"code": "kw", "name": "Cornish", "nativeName": "Kernewek"},
{"code": "co", "name": "Corsican", "nativeName": "corsu, lingua corsa"},
{"code": "cr", "name": "Cree", "nativeName": "ᓀᐦᐃᔭᐍᐏᐣ"},
{"code": "hr", "name": "Croatian", "nativeName": "hrvatski"},
{"code": "cs", "name": "Czech", "nativeName": "česky, čeština"},
{"code": "da", "name": "Danish", "nativeName": "dansk"},
{"code": "dv", "name": "Divehi; Dhivehi; Maldivian;", "nativeName": "ދިވެހި"},
{"code": "nl", "name": "Dutch", "nativeName": "Nederlands, Vlaams"},
{"code": "en", "name": "English", "nativeName": "English"},
{"code": "eo", "name": "Esperanto", "nativeName": "Esperanto"},
{"code": "et", "name": "Estonian", "nativeName": "eesti, eesti keel"},
{"code": "ee", "name": "Ewe", "nativeName": "Eʋegbe"},
{"code": "fo", "name": "Faroese", "nativeName": "føroyskt"},
{"code": "fj", "name": "Fijian", "nativeName": "vosa Vakaviti"},
{"code": "fi", "name": "Finnish", "nativeName": "suomi, suomen kieli"},
{"code": "fr", "name": "French", "nativeName": "français, langue française"},
{"code": "ff", "name": "Fula; Fulah; Pulaar; Pular", "nativeName": "Fulfulde, Pulaar, Pular"},
{"code": "gl", "name": "Galician", "nativeName": "Galego"},
{"code": "ka", "name": "Georgian", "nativeName": "ქართული"},
{"code": "de", "name": "German", "nativeName": "Deutsch"},
{"code": "el", "name": "Greek, Modern", "nativeName": "Ελληνικά"},
{"code": "gn", "name": "Guaraní", "nativeName": "Avañeẽ"},
{"code": "gu", "name": "Gujarati", "nativeName": "ગુજરાતી"},
{"code": "ht", "name": "Haitian; Haitian Creole", "nativeName": "Kreyòl ayisyen"},
{"code": "ha", "name": "Hausa", "nativeName": "Hausa, هَوُسَ"},
{"code": "he", "name": "Hebrew (modern)", "nativeName": "עברית"},
{"code": "hz", "name": "Herero", "nativeName": "Otjiherero"},
{"code": "hi", "name": "Hindi", "nativeName": "हिन्दी"},
{"code": "ho", "name": "Hiri Motu", "nativeName": "Hiri Motu"},
{"code": "hu", "name": "Hungarian", "nativeName": "Magyar"},
{"code": "ia", "name": "Interlingua", "nativeName": "Interlingua"},
{"code": "id", "name": "Indonesian", "nativeName": "Bahasa Indonesia"},
{"code": "ie", "name": "Interlingue", "nativeName": "Originally called Occidental; then Interlingue after WWII"},
{"code": "ga", "name": "Irish", "nativeName": "Gaeilge"},
{"code": "ig", "name": "Igbo", "nativeName": "Asụsụ Igbo"},
{"code": "ik", "name": "Inupiaq", "nativeName": "Iñupiaq, Iñupiatun"},
{"code": "io", "name": "Ido", "nativeName": "Ido"},
{"code": "is", "name": "Icelandic", "nativeName": "Íslenska"},
{"code": "it", "name": "Italian", "nativeName": "Italiano"},
{"code": "iu", "name": "Inuktitut", "nativeName": "ᐃᓄᒃᑎᑐᑦ"},
{"code": "ja", "name": "Japanese", "nativeName": "日本語 (にほんご/にっぽんご)"},
{"code": "jv", "name": "Javanese", "nativeName": "basa Jawa"},
{"code": "kl", "name": "Kalaallisut, Greenlandic", "nativeName": "kalaallisut, kalaallit oqaasii"},
{"code": "kn", "name": "Kannada", "nativeName": "ಕನ್ನಡ"},
{"code": "kr", "name": "Kanuri", "nativeName": "Kanuri"},
{"code": "ks", "name": "Kashmiri", "nativeName": "कश्मीरी, كشميري‎"},
{"code": "kk", "name": "Kazakh", "nativeName": "Қазақ тілі"},
{"code": "km", "name": "Khmer", "nativeName": "ភាសាខ្មែរ"},
{"code": "ki", "name": "Kikuyu, Gikuyu", "nativeName": "Gĩkũyũ"},
{"code": "rw", "name": "Kinyarwanda", "nativeName": "Ikinyarwanda"},
{"code": "ky", "name": "Kirghiz, Kyrgyz", "nativeName": "кыргыз тили"},
{"code": "kv", "name": "Komi", "nativeName": "коми кыв"},
{"code": "kg", "name": "Kongo", "nativeName": "KiKongo"},
{"code": "ko", "name": "Korean", "nativeName": "한국어 (韓國語), 조선말 (朝鮮語)"},
{"code": "ku", "name": "Kurdish", "nativeName": "Kurdî, كوردی‎"},
{"code": "kj", "name": "Kwanyama, Kuanyama", "nativeName": "Kuanyama"},
{"code": "la", "name": "Latin", "nativeName": "latine, lingua latina"},
{"code": "lb", "name": "Luxembourgish, Letzeburgesch", "nativeName": "Lëtzebuergesch"},
{"code": "lg", "name": "Luganda", "nativeName": "Luganda"},
{"code": "li", "name": "Limburgish, Limburgan, Limburger", "nativeName": "Limburgs"},
{"code": "ln", "name": "Lingala", "nativeName": "Lingála"},
{"code": "lo", "name": "Lao", "nativeName": "ພາສາລາວ"},
{"code": "lt", "name": "Lithuanian", "nativeName": "lietuvių kalba"},
{"code": "lu", "name": "Luba-Katanga", "nativeName": ""},
{"code": "lv", "name": "Latvian", "nativeName": "latviešu valoda"},
{"code": "gv", "name": "Manx", "nativeName": "Gaelg, Gailck"},
{"code": "mk", "name": "Macedonian", "nativeName": "македонски јазик"},
{"code": "mg", "name": "Malagasy", "nativeName": "Malagasy fiteny"},
{"code": "ms", "name": "Malay", "nativeName": "bahasa Melayu, بهاس ملايو‎"},
{"code": "ml", "name": "Malayalam", "nativeName": "മലയാളം"},
{"code": "mt", "name": "Maltese", "nativeName": "Malti"},
{"code": "mi", "name": "Māori", "nativeName": "te reo Māori"},
{"code": "mr", "name": "Marathi (Marāṭhī)", "nativeName": "मराठी"},
{"code": "mh", "name": "Marshallese", "nativeName": "Kajin M̧ajeļ"},
{"code": "mn", "name": "Mongolian", "nativeName": "монгол"},
{"code": "na", "name": "Nauru", "nativeName": "Ekakairũ Naoero"},
{"code": "nv", "name": "Navajo, Navaho", "nativeName": "Diné bizaad, Dinékʼehǰí"},
{"code": "nb", "name": "Norwegian Bokmål", "nativeName": "Norsk bokmål"},
{"code": "nd", "name": "North Ndebele", "nativeName": "isiNdebele"},
{"code": "ne", "name": "Nepali", "nativeName": "नेपाली"},
{"code": "ng", "name": "Ndonga", "nativeName": "Owambo"},
{"code": "nn", "name": "Norwegian Nynorsk", "nativeName": "Norsk nynorsk"},
{"code": "no", "name": "Norwegian", "nativeName": "Norsk"},
{"code": "ii", "name": "Nuosu", "nativeName": "ꆈꌠ꒿ Nuosuhxop"},
{"code": "nr", "name": "South Ndebele", "nativeName": "isiNdebele"},
{"code": "oc", "name": "Occitan", "nativeName": "Occitan"},
{"code": "oj", "name": "Ojibwe, Ojibwa", "nativeName": "ᐊᓂᔑᓈᐯᒧᐎᓐ"},
{"code": "om", "name": "Oromo", "nativeName": "Afaan Oromoo"},
{"code": "or", "name": "Oriya", "nativeName": "ଓଡ଼ିଆ"},
{"code": "os", "name": "Ossetian, Ossetic", "nativeName": "ирон æвзаг"},
{"code": "pa", "name": "Panjabi, Punjabi", "nativeName": "ਪੰਜਾਬੀ, پنجابی‎"},
{"code": "pi", "name": "Pāli", "nativeName": "पाऴि"},
{"code": "fa", "name": "Persian", "nativeName": "فارسی"},
{"code": "pl", "name": "Polish", "nativeName": "polski"},
{"code": "ps", "name": "Pashto, Pushto", "nativeName": "پښتو"},
{"code": "pt", "name": "Portuguese", "nativeName": "Português"},
{"code": "qu", "name": "Quechua", "nativeName": "Runa Simi, Kichwa"},
{"code": "rm", "name": "Romansh", "nativeName": "rumantsch grischun"},
{"code": "rn", "name": "Kirundi", "nativeName": "kiRundi"},
{"code": "ro", "name": "Romanian, Moldavian, Moldovan", "nativeName": "română"},
{"code": "ru", "name": "Russian", "nativeName": "русский язык"},
{"code": "sa", "name": "Sanskrit (Saṁskṛta)", "nativeName": "संस्कृतम्"},
{"code": "sc", "name": "Sardinian", "nativeName": "sardu"},
{"code": "sd", "name": "Sindhi", "nativeName": "सिन्धी, سنڌي، سندھی‎"},
{"code": "se", "name": "Northern Sami", "nativeName": "Davvisámegiella"},
{"code": "sm", "name": "Samoan", "nativeName": "gagana faa Samoa"},
{"code": "sg", "name": "Sango", "nativeName": "yângâ tî sängö"},
{"code": "sr", "name": "Serbian", "nativeName": "српски језик"},
{"code": "gd", "name": "Scottish Gaelic; Gaelic", "nativeName": "Gàidhlig"},
{"code": "sn", "name": "Shona", "nativeName": "chiShona"},
{"code": "si", "name": "Sinhala, Sinhalese", "nativeName": "සිංහල"},
{"code": "sk", "name": "Slovak", "nativeName": "slovenčina"},
{"code": "sl", "name": "Slovene", "nativeName": "slovenščina"},
{"code": "so", "name": "Somali", "nativeName": "Soomaaliga, af Soomaali"},
{"code": "st", "name": "Southern Sotho", "nativeName": "Sesotho"},
{"code": "es", "name": "Spanish; Castilian", "nativeName": "español, castellano"},
{"code": "su", "name": "Sundanese", "nativeName": "Basa Sunda"},
{"code": "sw", "name": "Swahili", "nativeName": "Kiswahili"},
{"code": "ss", "name": "Swati", "nativeName": "SiSwati"},
{"code": "sv", "name": "Swedish", "nativeName": "svenska"},
{"code": "ta", "name": "Tamil", "nativeName": "தமிழ்"},
{"code": "te", "name": "Telugu", "nativeName": "తెలుగు"},
{"code": "tg", "name": "Tajik", "nativeName": "тоҷикӣ, toğikī, تاجیکی‎"},
{"code": "th", "name": "Thai", "nativeName": "ไทย"},
{"code": "ti", "name": "Tigrinya", "nativeName": "ትግርኛ"},
{"code": "bo", "name": "Tibetan Standard, Tibetan, Central", "nativeName": "བོད་ཡིག"},
{"code": "tk", "name": "Turkmen", "nativeName": "Türkmen, Түркмен"},
{"code": "tl", "name": "Tagalog", "nativeName": "Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔"},
{"code": "tn", "name": "Tswana", "nativeName": "Setswana"},
{"code": "to", "name": "Tonga (Tonga Islands)", "nativeName": "faka Tonga"},
{"code": "tr", "name": "Turkish", "nativeName": "Türkçe"},
{"code": "ts", "name": "Tsonga", "nativeName": "Xitsonga"},
{"code": "tt", "name": "Tatar", "nativeName": "татарча, tatarça, تاتارچا‎"},
{"code": "tw", "name": "Twi", "nativeName": "Twi"},
{"code": "ty", "name": "Tahitian", "nativeName": "Reo Tahiti"},
{"code": "ug", "name": "Uighur, Uyghur", "nativeName": "Uyƣurqə, ئۇيغۇرچە‎"},
{"code": "uk", "name": "Ukrainian", "nativeName": "українська"},
{"code": "ur", "name": "Urdu", "nativeName": "اردو"},
{"code": "uz", "name": "Uzbek", "nativeName": "zbek, Ўзбек, أۇزبېك‎"},
{"code": "ve", "name": "Venda", "nativeName": "Tshivenḓa"},
{"code": "vi", "name": "Vietnamese", "nativeName": "Tiếng Việt"},
{"code": "vo", "name": "Volapük", "nativeName": "Volapük"},
{"code": "wa", "name": "Walloon", "nativeName": "Walon"},
{"code": "cy", "name": "Welsh", "nativeName": "Cymraeg"},
{"code": "wo", "name": "Wolof", "nativeName": "Wollof"},
{"code": "fy", "name": "Western Frisian", "nativeName": "Frysk"},
{"code": "xh", "name": "Xhosa", "nativeName": "isiXhosa"},
{"code": "yi", "name": "Yiddish", "nativeName": "ייִדיש"},
{"code": "yo", "name": "Yoruba", "nativeName": "Yorùbá"},
{"code": "za", "name": "Zhuang, Chuang", "nativeName": "Saɯ cueŋƅ, Saw cuengh"}
];
final rtlLang = ['ar', 'ar-bh', 'eg-ar', 'fa', 'prs', 'ps', 'ur'];
Future<String> getLanguageName() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('lang') ?? 'en';
}

View File

@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerWidget extends StatefulWidget {
final Function(String) onBarcodeFound;
const BarcodeScannerWidget({super.key, required this.onBarcodeFound});
@override
_BarcodeScannerWidgetState createState() => _BarcodeScannerWidgetState();
}
class _BarcodeScannerWidgetState extends State<BarcodeScannerWidget> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
final MobileScannerController controller = MobileScannerController(
torchEnabled: false,
returnImage: false,
);
@override
void initState() {
super.initState();
// Red Line Animation (Moves Up and Down)
_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
_animation = Tween<double>(begin: 50, end: 250).animate(_animationController);
}
@override
void dispose() {
_animationController.dispose();
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Container(
width: 320,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Title and Close Button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Scan Barcode",
style: TextStyle(color: Colors.white, fontSize: 18),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 10),
// Scanner Box
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Stack(
alignment: Alignment.center,
children: [
// Camera Scanner
SizedBox(
width: 300,
height: 300,
child: MobileScanner(
fit: BoxFit.cover,
controller: controller,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isNotEmpty) {
final Barcode barcode = barcodes.first;
debugPrint('Barcode found: ${barcode.rawValue}');
// Call the callback function with the barcode value
widget.onBarcodeFound(barcode.rawValue!);
// Close the scanner
Navigator.pop(context);
}
},
),
),
// Animated Red Line
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Positioned(
top: _animation.value,
left: 10,
right: 10,
child: Container(
height: 2,
width: 280,
color: Colors.red,
),
);
},
),
],
),
),
],
),
),
);
}
}
class BarCodeButton extends StatelessWidget {
const BarCodeButton({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
height: 48.0,
width: 100.0,
padding: const EdgeInsets.all(5.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: const Color(0xffD8D8D8)),
),
child: const Image(
image: AssetImage('images/barcode.png'),
),
);
}
}

View File

@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/constant.dart';
// ignore: must_be_immutable
class ButtonGlobal extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
var iconWidget;
final String buttontext;
final Color iconColor;
final Decoration? buttonDecoration;
// ignore: prefer_typing_uninitialized_variables
var onPressed;
// ignore: use_key_in_widget_constructors
ButtonGlobal({required this.iconWidget, required this.buttontext, required this.iconColor, this.buttonDecoration, required this.onPressed});
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
decoration: buttonDecoration ?? BoxDecoration(borderRadius: BorderRadius.circular(8), color: kMainColor),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
buttontext,
style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Colors.white),
),
const SizedBox(
width: 2,
),
Icon(
iconWidget,
color: iconColor,
),
],
),
),
);
}
}
// ignore: must_be_immutable
class ButtonGlobalWithoutIcon extends StatelessWidget {
final String buttontext;
final Decoration buttonDecoration;
// ignore: prefer_typing_uninitialized_variables
var onPressed;
final Color buttonTextColor;
// ignore: use_key_in_widget_constructors
ButtonGlobalWithoutIcon({required this.buttontext, required this.buttonDecoration, required this.onPressed, required this.buttonTextColor});
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: onPressed,
child: Container(
height: 50,
alignment: Alignment.center,
width: double.infinity,
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
decoration: buttonDecoration,
child: Text(
buttontext,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: Theme.of(context).textTheme.titleLarge?.copyWith(color: buttonTextColor),
),
),
);
}
}
///-----------------------name with logo------------------
class NameWithLogo extends StatelessWidget {
const NameWithLogo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: 75,
width: 66,
decoration: const BoxDecoration(image: DecorationImage(image: AssetImage(logo))),
),
const Text(
appsName,
style: TextStyle(color: kTitleColor, fontWeight: FontWeight.bold, fontSize: 28),
),
],
);
}
}
///-------------------update button--------------------------------
class UpdateButton extends StatelessWidget {
const UpdateButton({Key? key, required this.text, required this.onpressed}) : super(key: key);
final String text;
final VoidCallback onpressed;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
onTap: onpressed,
child: Container(
alignment: Alignment.center,
width: double.infinity,
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: kMainColor,
),
child: Text(
text,
style: theme.textTheme.titleMedium?.copyWith(color: kWhite),
),
),
);
}
}

View File

@@ -0,0 +1,48 @@
// import 'package:flutter/material.dart';
// import '../../model/business_info_model.dart' as business;
// import '../Screens/subscription/purchase_premium_plan_screen.dart';
//
// Future<void> checkSubscriptionAndNavigate(
// BuildContext context,
// String? subscriptionDate,
// String expireDate,
// business.EnrolledPlan? enrolledPlan,
// ) async {
// print('Subscription plan: Expire date : $expireDate');
// DateTime expireDate2 = DateTime.parse(expireDate);
// if (DateTime.now().isAfter(expireDate2)) {
// await navigateToPurchasePremiumPlanScreen(context, true, expireDate, enrolledPlan);
// }
// if (subscriptionDate == null || enrolledPlan == null) {
// await navigateToPurchasePremiumPlanScreen(context, true, expireDate, enrolledPlan);
// return;
// }
//
// DateTime parsedSubscriptionDate = DateTime.parse(subscriptionDate);
// num duration = enrolledPlan.duration ?? 0;
// DateTime expirationDate = parsedSubscriptionDate.add(Duration(days: duration.toInt()));
// num daysLeft = expirationDate.difference(DateTime.now()).inDays;
//
// if (daysLeft < 0) {
// await navigateToPurchasePremiumPlanScreen(context, true, expireDate, enrolledPlan);
// }
// }
//
// Future<void> navigateToPurchasePremiumPlanScreen(
// BuildContext context,
// bool isExpired,
// String expireDate,
// business.EnrolledPlan? enrolledPlan,
// ) async {
// await Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => PurchasePremiumPlanScreen(
// isExpired: true,
// isCameBack: true,
// enrolledPlan: enrolledPlan,
// willExpire: expireDate,
// ),
// ),
// );
// }

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constant.dart';
import 'internet_connection_notifier.dart';
class GlobalPopup extends ConsumerStatefulWidget {
final Widget child;
const GlobalPopup({super.key, required this.child});
@override
ConsumerState<GlobalPopup> createState() => _GlobalPopupState();
}
class _GlobalPopupState extends ConsumerState<GlobalPopup> {
@override
Widget build(BuildContext context) {
final internetStatus = ref.watch(internetConnectionProvider);
return Stack(
children: [
widget.child,
if (!internetStatus.isConnected && internetStatus.appLifecycleState == AppLifecycleState.resumed)
Positioned.fill(
child: Container(
padding: const EdgeInsets.all(20),
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.wifi_off, color: kMainColor, size: 100),
const SizedBox(height: 20),
const Text(
'No Internet Connection',
style: TextStyle(color: kTitleColor, fontSize: 24),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
final notifier = ref.read(internetConnectionProvider);
await notifier.checkConnection();
},
child: const Text("Try Again"),
),
],
),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../Screens/subscription/package_screen.dart';
import '../constant.dart';
import '../generated/l10n.dart' as lang;
import '../model/business_info_model.dart';
Widget goToPackagePagePopup({required BuildContext context, required EnrolledPlan? enrolledPlan}) {
return AlertDialog(
backgroundColor: kWhite,
surfaceTintColor: kWhite,
elevation: 0.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
contentPadding: const EdgeInsets.all(20),
titlePadding: const EdgeInsets.all(0),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const Spacer(),
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(
Icons.close,
color: kGreyTextColor,
)),
],
),
SvgPicture.asset(
'assets/upgradePlan.svg',
height: 198,
width: 238,
),
const SizedBox(height: 20),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
// lang.S.of(context).endYourFreePlan,
textAlign: TextAlign.center,
enrolledPlan?.plan?.subscriptionName != null ? 'End your ${enrolledPlan?.plan?.subscriptionName} plan?' : "No active plan!",
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: kTitleColor),
),
),
const SizedBox(height: 10),
Text(
enrolledPlan?.plan?.subscriptionName != null
? 'Your ${enrolledPlan?.plan?.subscriptionName} plan is almost done, buy your next plan Thanks.'
: 'You dont have an active plan! buy your next plan now, Thanks',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: kGreyTextColor),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
ElevatedButton(
child: Text(lang.S.of(context).upgradeNow),
onPressed: () {
Navigator.pop(context);
}),
const SizedBox(height: 5),
],
),
);
}

View File

@@ -0,0 +1,55 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
final internetConnectionProvider = ChangeNotifierProvider<InternetConnectionNotifier>((ref) {
return InternetConnectionNotifier();
});
class InternetConnectionNotifier extends ChangeNotifier with WidgetsBindingObserver {
bool _isConnected = true;
AppLifecycleState appLifecycleState = AppLifecycleState.resumed;
late final StreamSubscription<InternetStatus> _subscription;
bool get isConnected => _isConnected;
InternetConnectionNotifier() {
WidgetsBinding.instance.addObserver(this);
_init();
}
void _init() {
checkConnection();
_subscription = InternetConnection().onStatusChange.listen((status) {
print('Internet connection status: $status');
if (appLifecycleState != AppLifecycleState.paused) {
final wasConnected = _isConnected;
_isConnected = status == InternetStatus.connected;
notifyListeners();
}
});
}
Future<void> checkConnection() async {
final previous = _isConnected;
_isConnected = await InternetConnection().hasInternetAccess;
if (_isConnected != previous) notifyListeners();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
appLifecycleState = state;
notifyListeners();
if (state == AppLifecycleState.resumed) {
checkConnection();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_subscription.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
void showLicense({required BuildContext context}) {
showDialog(
context: context,
builder: (BuildContext context) {
return Padding(
padding: const EdgeInsets.all(30.0),
child: Center(
child: Container(
height: 180.0,
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(30)),
),
child: const Column(
children: [
Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
Text(
'Please Check Your Purchase Code',
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
SizedBox(
height: 10.0,
),
Text(
'Your purchase code is not valid. Please buy our product from envato to get a new purchase code',
maxLines: 6,
),
],
),
),
],
),
),
),
);
},
);
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
class ReturnedTagWidget extends StatelessWidget {
const ReturnedTagWidget({super.key, required this.show});
final bool show;
@override
Widget build(BuildContext context) {
return Visibility(
visible: show,
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.2),
borderRadius: const BorderRadius.all(
Radius.circular(2),
),
),
child: Text(
lang.S.of(context).returned,
style: const TextStyle(color: Colors.orange),
),
),
),
);
}
}

View File

@@ -0,0 +1,387 @@
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hugeicons/hugeicons.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/GlobalComponents/returned_tag_widget.dart';
import 'package:mobile_pos/model/sale_transaction_model.dart';
import 'package:nb_utils/nb_utils.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../PDF Invoice/sales_invoice_pdf.dart';
import '../Provider/profile_provider.dart';
import '../Screens/Loss_Profit/single_loss_profit_screen.dart';
import '../Screens/Sales/add_sales.dart';
import '../Screens/Sales/provider/sales_cart_provider.dart';
import '../Screens/invoice return/invoice_return_screen.dart';
import '../Screens/invoice_details/sales_invoice_details_screen.dart';
import '../constant.dart';
import '../core/theme/_app_colors.dart';
import '../currency.dart';
import '../generated/l10n.dart' as lang;
import '../model/business_info_model.dart' as bInfo;
import '../service/check_actions_when_no_branch.dart';
import '../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
Widget salesTransactionWidget({
required BuildContext context,
required SalesTransactionModel sale,
required bInfo.BusinessInformationModel businessInfo,
required WidgetRef ref,
bool? showProductQTY,
required bool advancePermission,
bool? fromLossProfit,
num? returnAmount,
bool? isFromSaleList,
}) {
final theme = Theme.of(context);
final _lang = l.S.of(context);
final printerData = ref.watch(thermalPrinterProvider);
return Column(
children: [
InkWell(
onTap: () {
if (fromLossProfit ?? false) {
SingleLossProfitScreen(
transactionModel: sale,
).launch(context);
} else {
SalesInvoiceDetails(
saleTransaction: sale,
businessInfo: businessInfo,
).launch(context);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
(showProductQTY ?? false)
? "${lang.S.of(context).totalProduct} : ${sale.salesDetails?.length.toString()}"
: sale.party?.name ?? '',
style: theme.textTheme.titleMedium?.copyWith(
fontSize: 15,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
Text(
'#${sale.invoiceNumber}',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
///_____Payment_Sttus________________________________________
getPaymentStatusBadge(
context: context, dueAmount: sale.dueAmount!, totalAmount: sale.totalAmount!),
///________Return_tag_________________________________________
ReturnedTagWidget(show: sale.salesReturns?.isNotEmpty ?? false),
],
),
Flexible(
child: Text(
DateFormat('dd MMM, yyyy').format(DateTime.parse(sale.saleDate ?? '')),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
color: kPeragrapColor,
),
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${lang.S.of(context).total} : $currency${formatPointNumber(sale.totalAmount ?? 0)}',
style: theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 4),
if (sale.dueAmount!.toInt() != 0)
Text(
'${lang.S.of(context).paid} : $currency${formatPointNumber(
(sale.totalAmount!.toDouble() - sale.dueAmount!.toDouble()),
)}',
style: theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (fromLossProfit ?? false) ...{
Flexible(
child: Text(
'${lang.S.of(context).profit} : $currency ${formatPointNumber(sale.detailsSumLossProfit ?? 0)}',
style: theme.textTheme.titleSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.w500,
),
).visible(!sale.detailsSumLossProfit!.isNegative),
),
Flexible(
child: Text(
'${lang.S.of(context).loss}: $currency ${formatPointNumber(sale.detailsSumLossProfit!.abs())}',
style: theme.textTheme.titleSmall?.copyWith(
color: Colors.redAccent,
fontWeight: FontWeight.w500,
),
).visible(sale.detailsSumLossProfit!.isNegative),
),
} else ...{
if (sale.dueAmount!.toInt() == 0)
Flexible(
child: Text(
(returnAmount != null)
? '${_lang.returnedAmount}: $currency${formatPointNumber(returnAmount)}'
: '${lang.S.of(context).paid} : $currency${formatPointNumber((sale.totalAmount!.toDouble() - sale.dueAmount!.toDouble()))}',
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
maxLines: 2,
),
),
if (sale.dueAmount!.toInt() != 0)
Flexible(
child: Text(
(returnAmount != null)
? '${_lang.returnedAmount}: $currency${formatPointNumber(returnAmount)}'
: '${lang.S.of(context).due}: $currency${formatPointNumber(sale.dueAmount ?? 0)}',
maxLines: 2,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
},
Row(
children: [
const SizedBox(width: 6),
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () =>
SalesInvoicePdf.generateSaleDocument(sale, businessInfo, context, showPreview: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedPdf02,
size: 22,
color: kPeraColor,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () async {
// PrintSalesTransactionModel model = PrintSalesTransactionModel(transitionModel: sale, personalInformationModel: businessInfo);
// await printerData.printSalesThermalInvoiceNow(
// transaction: model,
// productList: model.transitionModel!.salesDetails,
// context: context,
// );
SalesInvoiceDetails(
saleTransaction: sale,
businessInfo: businessInfo,
).launch(context);
},
icon: const Icon(
FeatherIcons.printer,
color: kPeraColor,
size: 22,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () => SalesInvoiceExcel.generateSaleDocument(sale, businessInfo, context),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedXls02,
size: 22,
color: kPeraColor,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () =>
SalesInvoicePdf.generateSaleDocument(sale, businessInfo, context, download: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedDownload01,
size: 22,
color: kPeraColor,
),
),
IconButton(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: () =>
SalesInvoicePdf.generateSaleDocument(sale, businessInfo, context, share: true),
icon: HugeIcon(
icon: HugeIcons.strokeRoundedShare08,
size: 22,
color: kPeraColor,
),
),
],
),
///________Sales_return_____________________________
if (isFromSaleList == true)
if (advancePermission)
PopupMenuButton(
offset: const Offset(0, 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0),
),
padding: EdgeInsets.zero,
itemBuilder: (BuildContext bc) => [
///________Sale Return___________________________________
PopupMenuItem(
child: GestureDetector(
onTap: () async {
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
if (!result) {
return;
}
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceReturnScreen(saleTransactionModel: sale),
),
);
Navigator.pop(bc);
},
child: Row(
children: [
Icon(
Icons.keyboard_return_outlined,
color: kGreyTextColor,
),
SizedBox(width: 10.0),
Text(
_lang.saleReturn,
style: TextStyle(color: kGreyTextColor),
),
],
),
),
),
PopupMenuItem(
onTap: () async {
ref.refresh(cartNotifier);
AddSalesScreen(
transitionModel: sale,
customerModel: null,
).launch(context);
},
child: Row(
children: [
Icon(
FeatherIcons.edit,
color: kGreyTextColor,
),
SizedBox(width: 10.0),
Text(
_lang.saleEdit,
style: TextStyle(color: kGreyTextColor),
),
],
),
// child:
//
// ///_________Sales_edit___________________________
// Visibility(
// visible: !(sale.salesReturns?.isNotEmpty ?? false),
// child: const Icon(
// FeatherIcons.edit,
// color: Colors.grey,
// ),
// ),
),
],
onSelected: (value) {
Navigator.pushNamed(context, '$value');
},
child: const Icon(
FeatherIcons.moreVertical,
color: kPeraColor,
),
),
],
)
],
),
],
),
),
),
Divider(height: 1, color: kLineColor),
],
);
}
Widget getPaymentStatusBadge({required num dueAmount, required num totalAmount, required BuildContext context}) {
String status;
Color textColor;
Color bgColor;
if (dueAmount <= 0) {
status = lang.S.of(context).paid;
textColor = const Color(0xff0dbf7d);
bgColor = const Color(0xff0dbf7d).withOpacity(0.1);
} else if (dueAmount >= totalAmount) {
status = lang.S.of(context).unPaid;
textColor = const Color(0xFFED1A3B);
bgColor = const Color(0xFFED1A3B).withOpacity(0.1);
} else {
status = lang.S.of(context).partialPaid;
textColor = const Color(0xFFFFA500);
bgColor = const Color(0xFFFFA500).withOpacity(0.1);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: bgColor,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
child: Text(
status,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: textColor,
),
),
);
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
// ignore: must_be_immutable
class TabButton extends StatelessWidget {
TabButton({
required this.title,
required this.text,
required this.background,
required this.press,
Key? key,
}) : super(key: key);
final Color background;
final Color text;
final String title;
// ignore: prefer_typing_uninitialized_variables
var press;
@override
Widget build(BuildContext context) {
return Container(
height: 40.0,
width: 100.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
color: background,
),
child: Center(
child: TextButton(
onPressed: press,
child: Text(
title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: text,
),
),
),
),
);
}
}
// ignore: must_be_immutable
class TabButtonSmall extends StatelessWidget {
TabButtonSmall({
required this.title,
required this.text,
required this.background,
required this.press,
Key? key,
}) : super(key: key);
final Color background;
final Color text;
final String title;
// ignore: prefer_typing_uninitialized_variables
var press;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: 40.0,
width: 90.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
color: background,
),
child: Center(
child: TextButton(
onPressed: press,
child: Text(
title,
style: theme.textTheme.bodyLarge?.copyWith(
color: text,
),
),
),
),
);
}
}
// ignore: must_be_immutable
class TabButtonBig extends StatelessWidget {
TabButtonBig({
required this.title,
required this.text,
required this.background,
required this.press,
Key? key,
}) : super(key: key);
final Color background;
final Color text;
final String title;
// ignore: prefer_typing_uninitialized_variables
var press;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: 40.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
color: background,
),
child: Center(
child: TextButton(
onPressed: press,
child: Text(
title,
style: theme.textTheme.bodyMedium?.copyWith(
color: text,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class UrlLauncher {
static Future<void> handleLaunchURL(BuildContext context, String url, bool isEmail) async {
try {
final parsedUrl = Uri.tryParse(url);
if (parsedUrl == null || !parsedUrl.hasScheme) {
throw const FormatException('Invalid URL format');
}
final launched = await launchUrl(
parsedUrl,
mode: LaunchMode.externalApplication,
);
if (!launched && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not launch ${isEmail ? 'Email' : 'Sms'}')),
);
}
} catch (e, stackTrace) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not launch the ${isEmail ? 'Email' : 'Sms'}')),
);
}
// Consider logging the error for debugging
debugPrint('URL Launch Error: $e\n$stackTrace');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,221 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/model/sale_transaction_model.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
import '../Screens/PDF/pdf.dart';
import '../http_client/customer_http_client_get.dart';
class PDFCommonFunctions {
//-------------------image
Future<dynamic> getNetworkImage(String imageURL) async {
if (imageURL.isEmpty) return null;
try {
final Uri uri = Uri.parse(imageURL);
final String fileExtension = uri.path.split('.').last.toLowerCase();
if (fileExtension == 'png' || fileExtension == 'jpg' || fileExtension == 'jpeg') {
final List<int> responseBytes = await http.readBytes(uri);
return Uint8List.fromList(responseBytes);
} else if (fileExtension == 'svg') {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final response = await clientGet.get(url: uri);
return response.body;
} else {
print('Unsupported image type: $fileExtension');
return null;
}
} catch (e) {
print('Error loading image: $e');
return null;
}
}
Future<Uint8List?> loadAssetImage(String path) async {
try {
final ByteData data = await rootBundle.load(path);
return data.buffer.asUint8List();
} catch (e) {
print('Error loading local image: $e');
return null;
}
}
int serialNumber = 1; // Initialize serial number
num getProductQuantity({required num detailsId, required SalesTransactionModel transactions}) {
num totalQuantity = transactions.salesDetails?.where((element) => element.id == detailsId).first.quantities ?? 0;
if (transactions.salesReturns?.isNotEmpty ?? false) {
for (var returns in transactions.salesReturns!) {
if (returns.salesReturnDetails?.isNotEmpty ?? false) {
for (var details in returns.salesReturnDetails!) {
if (details.saleDetailId == detailsId) {
totalQuantity += details.returnQty ?? 0;
}
}
}
}
}
return totalQuantity;
}
static Future<void> savePdfAndShowPdf(
{required BuildContext context, required String shopName, required String invoice, required pw.Document doc, bool? isShare, bool? download}) async {
if (Platform.isIOS) {
// EasyLoading.show(status: 'Generating PDF');
if (download ?? false) {
EasyLoading.show(status: 'Downloading...');
} else {
EasyLoading.show(status: 'Generating PDF');
}
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/${'$appsName-$shopName-$invoice'}.pdf');
final byteData = await doc.save();
try {
await file.writeAsBytes(byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));
EasyLoading.showSuccess('Done');
if (isShare ?? false) {
await SharePlus.instance.share(ShareParams(
files: [XFile(file.path)],
text: 'Here is your invoice PDF: ',
));
} else if (download ?? false) {
EasyLoading.showSuccess('Download successful! Check your Downloads folder');
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PDFViewerPage(path: file.path),
),
);
}
} on FileSystemException catch (err) {
EasyLoading.showError(err.message);
// handle error
}
}
if (Platform.isAndroid) {
var status = await Permission.storage.status;
if (status != PermissionStatus.granted) {
status = await Permission.storage.request();
}
if (true) {
if (download ?? false) {
EasyLoading.show(status: 'Downloading...');
} else {
EasyLoading.show(status: 'Generating PDF');
}
const downloadsFolderPath = '/storage/emulated/0/Download/';
Directory dir = Directory(downloadsFolderPath);
var file = File('${dir.path}/${'$appsName-$shopName-$invoice'}.pdf');
for (var i = 1; i < 20; i++) {
if (await file.exists()) {
try {
await file.delete();
break;
} catch (e) {
if (e.toString().contains('Cannot delete file')) {
file = File('${file.path.replaceAll(RegExp(r'\(\d+\)?'), '').replaceAll('.pdf', '')}($i).pdf');
}
}
} else {
break;
}
}
try {
final byteData = await doc.save();
await file.writeAsBytes(byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes));
EasyLoading.dismiss();
if (isShare ?? false) {
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)], text: 'Here is your invoice PDF: '));
} else if (download ?? false) {
EasyLoading.showSuccess('Download successful! Check your Downloads folder');
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PDFViewerPage(path: file.path),
),
);
}
} on FileSystemException catch (err) {
EasyLoading.showError(err.message);
}
}
}
}
String numberToWords(num amount) {
int taka = amount.floor();
int paisa = ((amount - taka) * 100).round();
String takaWords = _convertNumberToWords(taka);
String paisaWords = paisa > 0 ? ' and ${_convertNumberToWords(paisa)} Cents' : '';
return '$takaWords $paisaWords Only';
}
String _convertNumberToWords(int number) {
if (number == 0) return 'Zero';
final units = [
'',
'One',
'Two',
'Three',
'Four',
'Five',
'Six',
'Seven',
'Eight',
'Nine',
'Ten',
'Eleven',
'Twelve',
'Thirteen',
'Fourteen',
'Fifteen',
'Sixteen',
'Seventeen',
'Eighteen',
'Nineteen'
];
final tens = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'];
String convert(int n) {
if (n < 20) return units[n];
if (n < 100) {
return tens[n ~/ 10] + (n % 10 != 0 ? ' ' + units[n % 10] : '');
}
if (n < 1000) {
return units[n ~/ 100] + ' Hundred' + (n % 100 != 0 ? ' ' + convert(n % 100) : '');
}
if (n < 100000) {
return convert(n ~/ 1000) + ' Thousand' + (n % 1000 != 0 ? ' ' + convert(n % 1000) : '');
}
if (n < 10000000) {
return convert(n ~/ 100000) + ' Lakh' + (n % 100000 != 0 ? ' ' + convert(n % 100000) : '');
}
return convert(n ~/ 10000000) + ' Crore' + (n % 10000000 != 0 ? ' ' + convert(n % 10000000) : '');
}
return convert(number);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,592 @@
import 'dart:async';
import 'dart:io';
import 'package:excel/excel.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/PDF%20Invoice/universal_image_widget.dart';
import 'package:mobile_pos/Screens/all_transaction/model/transaction_model.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import 'package:mobile_pos/model/sale_transaction_model.dart';
import 'package:nb_utils/nb_utils.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import '../Screens/Products/add product/modle/create_product_model.dart';
import '../model/business_info_model.dart';
import '../model/subscription_report_model.dart';
import 'pdf_common_functions.dart';
class SubscriptionInvoicePdf {
static Future<void> generateSaleDocument(
List<SubscriptionReportModel> subscription, BusinessInformationModel personalInformation, BuildContext context,
{bool? share, bool? download, bool? showPreview}) async {
final pw.Document doc = pw.Document();
final _lang = l.S.of(context);
final String imageUrl =
'${APIConfig.domain}${(personalInformation.data?.showA4InvoiceLogo == 1) ? personalInformation.data?.a4InvoiceLogo : ''}';
dynamic imageData = await PDFCommonFunctions().getNetworkImage(imageUrl);
imageData ??= (personalInformation.data?.showA4InvoiceLogo == 1)
? await PDFCommonFunctions().loadAssetImage('images/logo.png')
: null;
final englishFont = pw.Font.ttf(await rootBundle.load('fonts/NotoSans/NotoSans-Regular.ttf'));
final englishBold = pw.Font.ttf(await rootBundle.load('fonts/NotoSans/NotoSans-Medium.ttf'));
final banglaFont = pw.Font.ttf(await rootBundle.load('assets/fonts/siyam_rupali_ansi.ttf'));
final arabicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Amiri-Regular.ttf'));
final hindiFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Hind-Regular.ttf'));
final frenchFont = pw.Font.ttf(await rootBundle.load('assets/fonts/GFSDidot-Regular.ttf'));
// Helper function
pw.Font getFont({bool bold = false}) {
switch (selectedLanguage) {
case 'en':
return bold ? englishBold : englishFont;
case 'bn':
// Bold not available, fallback to regular
return banglaFont;
case 'ar':
return arabicFont;
case 'hi':
return hindiFont;
case 'fr':
return frenchFont;
default:
return bold ? englishBold : englishFont;
}
}
getFontWithLangMatching(String data) {
String detectedLanguage = detectLanguageEnhanced(data);
if (detectedLanguage == 'en') {
return englishFont;
} else if (detectedLanguage == 'bn') {
return banglaFont;
} else if (detectedLanguage == 'ar') {
return arabicFont;
} else if (detectedLanguage == 'hi') {
return hindiFont;
} else if (detectedLanguage == 'fr') {
return frenchFont;
} else {
return englishFont;
}
}
final showWarranty = personalInformation.data?.showWarranty == 1 &&
(personalInformation.data?.warrantyVoidLabel != null || personalInformation.data?.warrantyVoid != null);
doc.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.letter.copyWith(marginBottom: 1.5 * PdfPageFormat.cm),
margin: pw.EdgeInsets.zero,
crossAxisAlignment: pw.CrossAxisAlignment.start,
header: (pw.Context context) {
return pw.Padding(
padding: const pw.EdgeInsets.all(20.0),
child: pw.Column(
children: [
pw.Row(mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [
pw.Container(
height: 54.12,
width: 200,
child: universalImage(
imageData,
w: 200,
h: 54.12,
),
),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
if (personalInformation.data?.meta?.showAddress == 1)
pw.SizedBox(
width: 200,
child: getLocalizedPdfText(
'${_lang.address}: ${personalInformation.data?.address ?? ''}',
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
),
),
),
if (personalInformation.data?.meta?.showPhoneNumber == 1)
pw.SizedBox(
width: 200,
child: getLocalizedPdfText(
'${_lang.mobile}: ${personalInformation.data?.phoneNumber ?? ''}',
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
),
),
),
if (personalInformation.data?.meta?.showEmail == 1)
pw.SizedBox(
width: 200,
child: getLocalizedPdfText(
'${_lang.emailText}: ${personalInformation.data?.invoiceEmail ?? ''}',
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
)),
),
//vat Name
if (personalInformation.data?.meta?.showVat == 1)
if (personalInformation.data?.vatNo != null && personalInformation.data?.meta?.showVat == 1)
pw.SizedBox(
width: 200,
child: getLocalizedPdfText(
'${personalInformation.data?.vatName ?? _lang.vatNumber}: ${personalInformation.data?.vatNo ?? ''}',
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
)),
),
],
),
]),
pw.SizedBox(height: 16.0),
pw.Center(
child: pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 19, vertical: 10),
decoration: pw.BoxDecoration(
borderRadius: pw.BorderRadius.circular(20),
border: pw.Border.all(color: PdfColors.black),
),
child: getLocalizedPdfText(
_lang.INVOICE,
pw.TextStyle(
fontWeight: pw.FontWeight.bold,
fontSize: 18,
color: PdfColors.black,
font: getFont(bold: true),
),
),
),
),
pw.SizedBox(height: 20),
pw.Row(mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [
pw.Column(crossAxisAlignment: pw.CrossAxisAlignment.start, children: [
//customer name
pw.Row(children: [
pw.SizedBox(
width: 60.0,
child: getLocalizedPdfText(
_lang.billTO,
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
)),
),
pw.SizedBox(
width: 10.0,
child: pw.Text(
':',
style: pw.Theme.of(context).defaultTextStyle.copyWith(color: PdfColors.black),
),
),
pw.SizedBox(
width: 100.0,
child: getLocalizedPdfTextWithLanguage(
personalInformation.data?.user?.name ?? '',
pw.TextStyle(
color: PdfColors.black,
font: getFontWithLangMatching(personalInformation.data?.user?.name ?? ''),
fontFallback: [englishFont],
)),
),
]),
//mobile
pw.Row(children: [
pw.SizedBox(
width: 60.0,
child: getLocalizedPdfText(
_lang.mobile,
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
)),
),
pw.SizedBox(
width: 10.0,
child: pw.Text(
':',
style: pw.Theme.of(context).defaultTextStyle.copyWith(color: PdfColors.black),
),
),
pw.SizedBox(
width: 100.0,
child: getLocalizedPdfText(personalInformation.data?.phoneNumber ?? 'n/a',
pw.TextStyle(font: getFont(), fontFallback: [englishFont])),
),
]),
//Address
pw.Row(children: [
pw.SizedBox(
width: 60.0,
child: getLocalizedPdfText(
_lang.address,
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
)),
),
pw.SizedBox(
width: 10.0,
child: pw.Text(
':',
style: pw.Theme.of(context).defaultTextStyle.copyWith(color: PdfColors.black),
),
),
pw.SizedBox(
width: 150.0,
child: getLocalizedPdfTextWithLanguage(
personalInformation.data?.address ?? '',
pw.TextStyle(
color: PdfColors.black,
font: getFontWithLangMatching(personalInformation.data?.address ?? ''),
fontFallback: [englishFont],
)),
),
]),
]),
]),
],
),
);
},
footer: (pw.Context context) {
return pw.Column(children: [
pw.Padding(
padding: const pw.EdgeInsets.all(10.0),
child: pw.Row(mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [
pw.Container(
alignment: pw.Alignment.centerRight,
margin: const pw.EdgeInsets.only(bottom: 3.0 * PdfPageFormat.mm),
padding: const pw.EdgeInsets.only(bottom: 3.0 * PdfPageFormat.mm),
child: pw.Column(children: [
pw.Container(
width: 120.0,
height: 2.0,
color: PdfColors.black,
),
pw.SizedBox(height: 4.0),
getLocalizedPdfText(
_lang.customerSignature,
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
))
]),
),
pw.Container(
alignment: pw.Alignment.centerRight,
margin: const pw.EdgeInsets.only(bottom: 3.0 * PdfPageFormat.mm),
padding: const pw.EdgeInsets.only(bottom: 3.0 * PdfPageFormat.mm),
child: pw.Column(children: [
pw.Container(
width: 120.0,
height: 2.0,
color: PdfColors.black,
),
pw.SizedBox(height: 4.0),
getLocalizedPdfText(
_lang.authorizedSignature,
pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
))
]),
),
]),
),
if (showWarranty)
pw.Padding(
padding: pw.EdgeInsets.symmetric(horizontal: 10),
child: pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(4),
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black),
),
child: pw.RichText(
text: pw.TextSpan(
children: [
if (personalInformation.data?.warrantyVoidLabel != null)
pw.TextSpan(
text: '${personalInformation.data!.warrantyVoidLabel!}- ',
style: pw.TextStyle(
color: PdfColors.black,
font: getFont(bold: true),
fontFallback: [englishFont],
),
),
if (personalInformation.data?.warrantyVoid != null)
pw.TextSpan(
text: personalInformation.data!.warrantyVoid!,
style: pw.TextStyle(
color: PdfColors.black,
font: getFont(),
fontFallback: [englishFont],
),
),
],
),
),
),
),
pw.SizedBox(height: 10),
pw.Padding(
padding: pw.EdgeInsets.symmetric(horizontal: 10),
child: pw.Center(
child: pw.Text(
'${personalInformation.data?.developByLevel ?? ''} ${personalInformation.data?.developBy ?? ''}',
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
),
),
]);
},
build: (pw.Context context) => <pw.Widget>[
pw.Padding(
padding: const pw.EdgeInsets.only(left: 20.0, right: 20.0, bottom: 20.0),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Main products table
pw.Table(
border: pw.TableBorder(
horizontalInside: pw.BorderSide(color: PdfColor.fromInt(0xffD9D9D9)),
verticalInside: pw.BorderSide(color: PdfColor.fromInt(0xffD9D9D9)),
left: pw.BorderSide(color: PdfColor.fromInt(0xffD9D9D9)),
right: pw.BorderSide(color: PdfColor.fromInt(0xffD9D9D9)),
top: pw.BorderSide(color: PdfColor.fromInt(0xffD9D9D9)),
bottom: pw.BorderSide(color: PdfColor.fromInt(0xffD9D9D9)),
),
columnWidths: <int, pw.TableColumnWidth>{
0: const pw.FlexColumnWidth(1),
1: const pw.FlexColumnWidth(2),
2: const pw.FlexColumnWidth(3),
3: const pw.FlexColumnWidth(2),
4: const pw.FlexColumnWidth(2),
5: const pw.FlexColumnWidth(3),
},
children: [
// Table header
pw.TableRow(
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: getLocalizedPdfText(
_lang.sl,
pw.TextStyle(
font: getFont(bold: true),
fontFallback: [englishFont],
),
textAlignment: pw.TextAlign.center,
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: getLocalizedPdfText(
_lang.date,
pw.TextStyle(
font: getFont(bold: true),
fontFallback: [englishFont],
fontWeight: pw.FontWeight.bold,
),
textAlignment: pw.TextAlign.left,
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: getLocalizedPdfText(
_lang.packageName,
pw.TextStyle(
font: getFont(bold: true),
fontFallback: [englishFont],
fontWeight: pw.FontWeight.bold,
),
textAlignment: pw.TextAlign.center,
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: getLocalizedPdfText(
_lang.started,
pw.TextStyle(
font: getFont(bold: true),
fontFallback: [englishFont],
fontWeight: pw.FontWeight.bold,
),
textAlignment: pw.TextAlign.center,
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: getLocalizedPdfText(
_lang.end,
pw.TextStyle(
font: getFont(bold: true),
fontFallback: [englishFont],
fontWeight: pw.FontWeight.bold,
),
textAlignment: pw.TextAlign.center,
),
),
pw.Padding(
padding: const pw.EdgeInsets.all(8),
child: getLocalizedPdfText(
_lang.paymentMethod,
pw.TextStyle(
font: getFont(bold: true),
fontFallback: [englishFont],
fontWeight: pw.FontWeight.bold,
),
textAlignment: pw.TextAlign.center,
),
),
],
),
// Table rows for products
for (int i = 0; i < subscription.length; i++)
pw.TableRow(
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(8.0),
child: pw.Text('${i + 1}', textAlign: pw.TextAlign.center),
),
pw.Padding(
padding: pw.EdgeInsets.all(8.0),
child: getLocalizedPdfTextWithLanguage(
textAlignment: pw.TextAlign.center,
subscription[i].startDate == null
? "N/A"
: DateFormat('dd MMM yyyy').format(subscription[i].startDate!),
pw.TextStyle(
font: getFont(),
fontFallback: [englishFont],
)),
),
pw.Padding(
padding: pw.EdgeInsets.all(8.0),
child: getLocalizedPdfTextWithLanguage(
textAlignment: pw.TextAlign.center,
subscription[i].name ?? 'n/a',
pw.TextStyle(
font: getFont(),
fontFallback: [englishFont],
)),
),
pw.Padding(
padding: pw.EdgeInsets.all(8.0),
child: getLocalizedPdfTextWithLanguage(
textAlignment: pw.TextAlign.center,
subscription[i].startDate == null
? "N/A"
: DateFormat('dd MMM yyyy').format(subscription[i].startDate!),
pw.TextStyle(
font: getFont(),
fontFallback: [englishFont],
)),
),
pw.Padding(
padding: pw.EdgeInsets.all(8.0),
child: getLocalizedPdfTextWithLanguage(
textAlignment: pw.TextAlign.center,
subscription[i].endDate == null
? "N/A"
: DateFormat('dd MMM yyyy').format(subscription[i].startDate!),
pw.TextStyle(
font: getFont(),
fontFallback: [englishFont],
)),
),
pw.SizedBox(height: 12),
pw.Padding(
padding: pw.EdgeInsets.all(8.0),
child: getLocalizedPdfTextWithLanguage(
textAlignment: pw.TextAlign.center,
subscription[i].paymentBy ?? 'n/a',
pw.TextStyle(
font: getFont(),
fontFallback: [englishFont],
)),
),
],
),
],
),
pw.SizedBox(height: 20.0),
if ((!personalInformation.data!.invoiceNote.isEmptyOrNull ||
!personalInformation.data!.invoiceNoteLevel.isEmptyOrNull) &&
personalInformation.data!.showNote == 1)
pw.RichText(
textAlign: pw.TextAlign.start,
text: pw.TextSpan(
text: '${personalInformation.data?.invoiceNoteLevel ?? ''}: ',
style: pw.TextStyle(
font: getFont(bold: true),
),
children: [
pw.TextSpan(
text: personalInformation.data?.invoiceNote ?? '',
style: pw.TextStyle(
font: getFont(bold: true),
))
])),
pw.SizedBox(height: 30),
if (personalInformation.data?.showGratitudeMsg == 1)
if (!personalInformation.data!.gratitudeMessage.isEmptyOrNull)
pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.only(bottom: 8.0),
child: pw.Center(
child: pw.Text(
personalInformation.data!.gratitudeMessage ?? '',
)),
),
pw.Padding(padding: const pw.EdgeInsets.all(10)),
],
),
),
],
),
);
if (showPreview == true) {
await Printing.layoutPdf(
name: personalInformation.data?.companyName ?? '',
usePrinterSettings: true,
dynamicLayout: true,
forceCustomPrintPaper: true,
onLayout: (PdfPageFormat format) async => doc.save());
} else {
await PDFCommonFunctions.savePdfAndShowPdf(
context: context,
shopName: personalInformation.data?.companyName ?? '',
invoice: '1',
doc: doc,
isShare: share,
download: download,
);
}
}
}

View File

@@ -0,0 +1,83 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:mobile_pos/constant.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
pw.Widget universalImage(dynamic data, {double? w, double? h}) {
try {
// Case 1: Uint8List → PNG/JPG
if (data is Uint8List) {
return pw.Image(
pw.MemoryImage(data),
width: w,
height: h,
fit: pw.BoxFit.cover,
);
}
// Case 2: SVG string
if (data is String && data.trim().startsWith("<svg")) {
return pw.SvgImage(
svg: data,
width: w,
height: h,
fit: pw.BoxFit.cover,
);
}
// Case 3: Base64 Image String (data:image/png;base64,...)
if (data is String && data.contains("base64")) {
try {
final base64Str = data.split(',').last;
final bytes = base64Decode(base64Str);
return pw.Image(
pw.MemoryImage(bytes),
width: w,
height: h,
fit: pw.BoxFit.cover,
);
} catch (e) {
// base64 decode failed
return pw.Container(width: w, height: h);
}
}
// Case 4: Unknown String → DO NOT convert to image
if (data is String) {
return pw.Container(
alignment: pw.Alignment.center,
width: w,
height: h,
padding: pw.EdgeInsets.all(8),
decoration: pw.BoxDecoration(
shape: pw.BoxShape.circle,
color: PdfColors.grey200,
),
child: pw.Text(
"No Image",
textAlign: pw.TextAlign.center,
),
);
}
// Unknown type
return pw.Container(width: w, height: h);
} catch (e) {
return pw.Container(
alignment: pw.Alignment.center,
width: w,
height: h,
padding: pw.EdgeInsets.all(8),
decoration: pw.BoxDecoration(
shape: pw.BoxShape.circle,
color: PdfColors.grey200,
),
child: pw.Text(
"Invalid Image",
textAlign: pw.TextAlign.center,
),
);
}
}

View File

@@ -0,0 +1,161 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:nb_utils/nb_utils.dart';
import '../Screens/Purchase/Repo/purchase_repo.dart';
import '../Screens/vat_&_tax/model/vat_model.dart';
final cartNotifierPurchaseNew = ChangeNotifierProvider((ref) => CartNotifierPurchase());
class CartNotifierPurchase extends ChangeNotifier {
List<CartProductModelPurchase> cartItemList = [];
TextEditingController discountTextControllerFlat = TextEditingController();
TextEditingController vatAmountController = TextEditingController();
TextEditingController shippingChargeController = TextEditingController();
///_________NEW_________________________________
num totalAmount = 0;
num discountAmount = 0;
num discountPercent = 0;
num totalPayableAmount = 0;
VatModel? selectedVat;
num vatAmount = 0;
bool isFullPaid = false;
num receiveAmount = 0;
num changeAmount = 0;
num dueAmount = 0;
num finalShippingCharge = 0;
void changeSelectedVat({VatModel? data}) {
if (data != null) {
selectedVat = data;
} else {
selectedVat = null;
vatAmount = 0;
vatAmountController.clear();
}
calculatePrice();
}
void calculateDiscount({
required String value,
bool? rebuilding,
String? selectedTaxType,
}) {
if (value.isEmpty) {
discountAmount = 0;
discountPercent = 0;
discountTextControllerFlat.clear();
} else {
num discountValue = num.tryParse(value) ?? 0;
if (selectedTaxType == null) {
EasyLoading.showError('Please select a discount type');
discountAmount = 0;
discountPercent = 0;
} else if (selectedTaxType == "Flat") {
discountAmount = discountValue;
if (discountAmount > totalAmount) {
discountTextControllerFlat.clear();
discountAmount = 0;
EasyLoading.showError('Enter a valid discount');
}
} else if (selectedTaxType == "Percent") {
discountPercent = discountValue;
discountAmount = (totalAmount * discountPercent) / 100;
if (discountAmount > totalAmount) {
discountAmount = totalAmount;
}
} else {
EasyLoading.showError('Invalid discount type selected');
discountAmount = 0;
}
}
if (rebuilding == false) return;
calculatePrice();
}
void updateProduct({required int index, required CartProductModelPurchase newProduct}) {
cartItemList[index] = newProduct;
calculatePrice();
}
void calculatePrice({String? receivedAmount, String? shippingCharge, bool? stopRebuild}) {
totalAmount = 0;
totalPayableAmount = 0;
dueAmount = 0;
for (var element in cartItemList) {
totalAmount += (element.quantities ?? 0) * (element.productPurchasePrice ?? 0);
}
totalPayableAmount = totalAmount;
if (discountAmount > totalAmount) {
calculateDiscount(value: discountAmount.toString(), rebuilding: false);
}
if (discountAmount >= 0) {
totalPayableAmount -= discountAmount;
}
if (selectedVat?.rate != null) {
vatAmount = (totalPayableAmount * selectedVat!.rate!) / 100;
vatAmountController.text = vatAmount.toStringAsFixed(2);
}
totalPayableAmount += vatAmount;
if (shippingCharge != null) {
finalShippingCharge = num.tryParse(shippingCharge) ?? 0;
}
totalPayableAmount += finalShippingCharge;
if (receivedAmount != null) {
receiveAmount = num.tryParse(receivedAmount) ?? 0;
}
changeAmount = totalPayableAmount < receiveAmount ? receiveAmount - totalPayableAmount : 0;
dueAmount = totalPayableAmount < receiveAmount ? 0 : totalPayableAmount - receiveAmount;
if (dueAmount <= 0) isFullPaid = true;
if (stopRebuild ?? false) return;
notifyListeners();
}
double getTotalAmount() {
double totalAmountOfCart = 0;
for (var element in cartItemList) {
totalAmountOfCart = totalAmountOfCart + ((element.productPurchasePrice ?? 0) * (element.quantities ?? 0));
}
return totalAmountOfCart;
}
void quantityIncrease(int index) {
cartItemList[index].quantities = (cartItemList[index].quantities ?? 0) + 1;
calculatePrice();
}
void quantityDecrease(int index) {
if ((cartItemList[index].quantities ?? 0) > 1) {
cartItemList[index].quantities = (cartItemList[index].quantities ?? 0) - 1;
}
calculatePrice();
}
void addToCartRiverPod({required CartProductModelPurchase cartItem, bool? fromEditSales, required bool isVariation}) {
if (!cartItemList
.any((element) => isVariation ? (element.productId == cartItem.productId && element.batchNumber == cartItem.batchNumber) : element.productId == cartItem.productId)) {
cartItemList.add(cartItem);
} else {
int index = cartItemList.indexWhere(
(element) => isVariation ? (element.productId == cartItem.productId && element.batchNumber == cartItem.batchNumber) : element.productId == cartItem.productId,
);
cartItemList[index] = cartItem;
}
(fromEditSales ?? false) ? null : calculatePrice();
}
void deleteToCart(int index) {
cartItemList.removeAt(index);
calculatePrice();
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
import '../Screens/Products/Repo/product_repo.dart';
ProductRepo productRepo = ProductRepo();
final productProvider = FutureProvider.autoDispose<List<Product>>((ref) => productRepo.fetchAllProducts());
final fetchProductDetails = FutureProvider.family.autoDispose<Product, String>((ref, id) {
return productRepo.fetchProductDetails(productID: id);
});

View File

@@ -0,0 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/model/business_info_model.dart';
import 'package:mobile_pos/model/dashboard_overview_model.dart';
import '../Repository/API/business_info_repo.dart';
import '../service/check_user_role_permission_provider.dart';
import '../model/todays_summary_model.dart';
final BusinessRepository businessRepository = BusinessRepository();
final businessInfoProvider = FutureProvider<BusinessInformationModel>((ref) async {
return await BusinessRepository().fetchBusinessData();
});
final getExpireDateProvider = FutureProvider.family<void, WidgetRef>(
(ref, widgetRef) => businessRepository.fetchSubscriptionExpireDate(ref: widgetRef));
final summaryInfoProvider = FutureProvider<TodaysSummaryModel>((ref) => businessRepository.fetchTodaySummaryData());
final dashboardInfoProvider = FutureProvider.family<DashboardOverviewModel, String>((ref, type) {
return businessRepository.dashboardData(type);
});

View File

@@ -0,0 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../Repository/API/business_category_repo.dart';
import '../model/business_category_model.dart';
BusinessCategoryRepository businessCategoryRepository = BusinessCategoryRepository();
final businessCategoryProvider = FutureProvider<List<BusinessCategory>>((ref) => businessCategoryRepository.getBusinessCategories());

View File

@@ -0,0 +1,203 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Purchase/Model/purchase_transaction_model.dart';
import 'package:mobile_pos/Screens/Purchase/Repo/purchase_repo.dart';
import 'package:mobile_pos/Screens/Sales/Repo/sales_repo.dart';
import 'package:mobile_pos/model/sale_transaction_model.dart';
import '../model/balance_sheet_model.dart' as bs;
import '../model/bill_wise_loss_profit_report_model.dart' as bwlprm;
import '../model/cashflow_model.dart' as cf;
import '../model/loss_profit_model.dart' as lpmodel;
import '../model/product_history_model.dart' as phlm;
import '../model/subscription_report_model.dart' as srm;
import '../model/tax_report_model.dart' as trm;
//------------sales-------------------------------------
final saleRepo = Provider<SaleRepo>((ref) => SaleRepo());
final salesTransactionProvider = FutureProvider.autoDispose<List<SalesTransactionModel>>((ref) {
final repo = ref.read(saleRepo);
return repo.fetchSalesList();
});
final filteredSaleProvider = FutureProvider.family.autoDispose<List<SalesTransactionModel>, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.fetchSalesList(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredSaleReturnedProvider = FutureProvider.family.autoDispose<List<SalesTransactionModel>, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.fetchSalesList(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
salesReturn: true,
);
},
);
//------------------purchase----------------------------------------
final purchaseRepo = Provider<PurchaseRepo>((ref) => PurchaseRepo());
final purchaseTransactionProvider = FutureProvider.autoDispose<List<PurchaseTransaction>>((ref) {
final repo = ref.read(purchaseRepo);
return repo.fetchPurchaseList();
});
final filterPurchaseProvider = FutureProvider.family.autoDispose<List<PurchaseTransaction>, FilterModel>(
(ref, filter) {
final repo = ref.read(purchaseRepo);
return repo.fetchPurchaseList(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filterPurchaseReturnProvider = FutureProvider.family.autoDispose<List<PurchaseTransaction>, FilterModel>(
(ref, filter) {
final repo = ref.read(purchaseRepo);
return repo.fetchPurchaseList(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
salesReturn: true,
);
},
);
final filteredLossProfitProvider = FutureProvider.family.autoDispose<lpmodel.LossProfitModel, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.getLossProfit(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredCashflowProvider = FutureProvider.family.autoDispose<cf.CashflowModel, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.getCashflow(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredBalanceSheetProvider = FutureProvider.family.autoDispose<bs.BalanceSheetModel, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.getBalanceSheet(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredSubscriptionReportProvider =
FutureProvider.family.autoDispose<List<srm.SubscriptionReportModel>, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.getSubscriptionReport(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredTaxReportReportProvider = FutureProvider.family.autoDispose<trm.TaxReportModel, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.getTaxReport(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredBillWiseLossProfitReportProvider =
FutureProvider.family.autoDispose<bwlprm.BillWiseLossProfitReportModel, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.getBillWiseLossProfitReport(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredProductSaleHistoryReportProvider =
FutureProvider.family.autoDispose<phlm.ProductHistoryListModel, FilterModel>(
(ref, filter) {
final repo = ref.read(saleRepo);
return repo.getProductSaleHistoryReport(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
final filteredProductSaleHistoryReportDetailsProvider =
FutureProvider.family.autoDispose<phlm.ProductHistoryDetailsModel, ({int productId, FilterModel filter})>(
(ref, arg) {
final repo = ref.read(saleRepo);
return repo.getProductSaleHistoryReportDetails(
productId: arg.productId,
type: arg.filter.duration,
fromDate: arg.filter.fromDate,
toDate: arg.filter.toDate,
);
},
);
final filteredProductPurchaseHistoryReportDetailsProvider =
FutureProvider.family.autoDispose<phlm.ProductHistoryDetailsModel, ({int productId, FilterModel filter})>(
(ref, arg) {
final repo = ref.read(saleRepo);
return repo.getProductPurchaseHistoryReportDetails(
productId: arg.productId,
type: arg.filter.duration,
fromDate: arg.filter.fromDate,
toDate: arg.filter.toDate,
);
},
);
class FilterModel {
final String? duration;
final String? fromDate;
final String? toDate;
FilterModel({
this.duration,
this.fromDate,
this.toDate,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is FilterModel && other.duration == duration && other.fromDate == fromDate && other.toDate == toDate;
}
@override
int get hashCode => duration.hashCode ^ fromDate.hashCode ^ toDate.hashCode;
}

View File

@@ -0,0 +1,28 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../Const/api_config.dart';
import '../../http_client/customer_http_client_get.dart';
import '../../model/business_category_model.dart';
import '../constant_functions.dart';
class BusinessCategoryRepository {
Future<List<BusinessCategory>> getBusinessCategories() async {
try {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final response = await clientGet.get(
url: Uri.parse('${APIConfig.url}${APIConfig.businessCategoriesUrl}'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body)['data'] as List;
return data.map((category) => BusinessCategory.fromJson(category)).toList();
} else {
throw Exception('Failed to fetch business categories');
}
} catch (error) {
throw Exception('Error fetching business categories: $error');
}
}
}

View File

@@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/model/dashboard_overview_model.dart';
import 'package:mobile_pos/model/todays_summary_model.dart';
import '../../http_client/customer_http_client_get.dart';
import '../../http_client/subscription_expire_provider.dart';
import '../../model/business_info_model.dart';
import '../../model/business_info_model_new.dart';
import '../constant_functions.dart';
class BusinessRepository {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
Future<BusinessInformationModel> fetchBusinessData() async {
final uri = Uri.parse('${APIConfig.url}/business');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body);
return BusinessInformationModel.fromJson(parsedData);
} else {
throw Exception('Failed to fetch business data');
}
}
Future<void> fetchSubscriptionExpireDate({required WidgetRef ref}) async {
final uri = Uri.parse('${APIConfig.url}/business');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body);
final BusinessInformationModel businessInformation = BusinessInformationModel.fromJson(parsedData);
ref.read(subscriptionProvider.notifier).updateSubscription(businessInformation.data?.willExpire);
// ref.read(subscriptionProvider.notifier).updateSubscription("2025-01-05");
} else {
throw Exception('Failed to fetch business data');
}
}
Future<BusinessInformationModel?> checkBusinessData() async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/business');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body);
return BusinessInformationModel.fromJson(parsedData); // Extract the "data" object from the response
} else {
return null;
}
}
Future<TodaysSummaryModel> fetchTodaySummaryData() async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
String date = DateFormat('yyyy-MM-dd').format(DateTime.now());
final uri = Uri.parse('${APIConfig.url}/summary?date=$date');
final response = await clientGet.get(url: uri);
print('------------dashboard------${response.statusCode}--------------');
if (response.statusCode == 200) {
print(response.body);
return TodaysSummaryModel.fromJson(jsonDecode(response.body)); // Extract the "data" object from the response
} else {
// await LogOutRepo().signOut();
throw Exception('Failed to fetch business data');
}
}
Future<DashboardOverviewModel> dashboardData(String type) async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
Uri uri;
if (type.startsWith('custom_date&')) {
final uriParams = Uri.splitQueryString(type.replaceFirst('custom_date&', ''));
final fromDate = uriParams['from_date'];
final toDate = uriParams['to_date'];
uri = Uri.parse('${APIConfig.url}/dashboard?duration=custom_date&from_date=$fromDate&to_date=$toDate');
} else {
uri = Uri.parse('${APIConfig.url}/dashboard?duration=$type');
}
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
return DashboardOverviewModel.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to fetch business data ${response.statusCode}');
}
}
}

View File

@@ -0,0 +1,144 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/Const/api_config.dart';
import '../../http_client/custome_http_client.dart';
import '../constant_functions.dart';
class BusinessUpdateRepository {
Future<bool> updateProfile({
required String id,
String? name,
required String categoryId,
required BuildContext context,
required WidgetRef ref,
String? phone,
String? address,
String? email,
String? vatNumber,
String? vatTitle,
String? invoiceNoteLevel,
String? invoiceNote,
String? gratitudeMessage,
String? warrantyLabelVoid,
String? warrantyVoid,
String? saleRoundingOption,
String? invoiceSize,
String? invoiceLanguage,
Map<String, int>? invoiceVisibilityMeta,
File? image,
File? invoiceLogo,
File? a4InvoiceLogo,
File? thermalInvoiceLogo,
File? invoiceScannerLogo,
}) async {
final uri = Uri.parse('${APIConfig.url}/business/$id');
final customHttpClient = CustomHttpClient(
client: http.Client(),
context: context,
ref: ref,
);
/// ---------- BASE FIELDS ----------
final fields = <String, String>{
'_method': 'PUT',
'business_category_id': categoryId,
'companyName': name ?? '',
'phoneNumber': phone ?? '',
'address': address ?? '',
'email': email ?? '',
'vat_no': vatNumber ?? '',
'vat_name': vatTitle ?? '',
'note_label': invoiceNoteLevel ?? '',
'note': invoiceNote ?? '',
'warranty_void_label': warrantyLabelVoid ?? '',
'warranty_void': warrantyVoid ?? '',
'gratitude_message': gratitudeMessage ?? '',
'sale_rounding_option': saleRoundingOption ?? 'none',
'invoice_size': invoiceSize ?? '2_inch_58mm',
'invoice_language': invoiceLanguage ?? 'english',
};
/// ---------- META FIELDS (numeric 0/1) ----------
fields['show_company_name'] = (invoiceVisibilityMeta?['show_company_name'] ?? 1).toString();
fields['show_phone_number'] = (invoiceVisibilityMeta?['show_phone_number'] ?? 1).toString();
fields['show_address'] = (invoiceVisibilityMeta?['show_address'] ?? 1).toString();
fields['show_email'] = (invoiceVisibilityMeta?['show_email'] ?? 1).toString();
fields['show_vat'] = (invoiceVisibilityMeta?['show_vat'] ?? 1).toString();
/// ---------- ROOT FIELDS (numeric 0/1) ----------
fields['show_note'] = (invoiceVisibilityMeta?['show_note'] ?? 1).toString();
fields['show_gratitude_msg'] = (invoiceVisibilityMeta?['show_gratitude_msg'] ?? 1).toString();
fields['show_invoice_scanner_logo'] = (invoiceVisibilityMeta?['show_invoice_scanner_logo'] ?? 1).toString();
fields['show_a4_invoice_logo'] = (invoiceVisibilityMeta?['show_a4_invoice_logo'] ?? 1).toString();
fields['show_thermal_invoice_logo'] = (invoiceVisibilityMeta?['show_thermal_invoice_logo'] ?? 1).toString();
fields['show_warranty'] = (invoiceVisibilityMeta?['show_warranty'] ?? 1).toString();
/// ---------- FILES ----------
final files = <String, File>{};
if (image != null) files['pictureUrl'] = image;
if (invoiceLogo != null) files['invoice_logo'] = invoiceLogo;
if (a4InvoiceLogo != null) files['a4_invoice_logo'] = a4InvoiceLogo;
if (thermalInvoiceLogo != null) files['thermal_invoice_logo'] = thermalInvoiceLogo;
if (invoiceScannerLogo != null) files['invoice_scanner_logo'] = invoiceScannerLogo;
try {
final response = await customHttpClient.uploadMultipleFiles(
url: uri,
fields: fields,
files: files,
);
final body = await response.stream.bytesToString();
final decoded = json.decode(body);
if (response.statusCode == 200) {
EasyLoading.showSuccess(decoded['message'] ?? 'Updated successfully');
return true;
} else {
EasyLoading.showError(decoded['message'] ?? 'Update failed. Status: ${response.statusCode}');
return false;
}
} catch (e, stackTrace) {
print('Error updating profile: $e');
print('Stack trace: $stackTrace');
EasyLoading.showError('Update failed: $e');
return false;
}
}
Future<bool> updateSalesSettings({
required String id,
required BuildContext context,
required WidgetRef ref,
String? saleRoundingOption,
}) async {
final uri = Uri.parse('${APIConfig.url}/business/$id');
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
var request = http.MultipartRequest('POST', uri)
..headers['Accept'] = 'application/json'
..headers['Authorization'] = await getAuthToken();
request.fields['_method'] = 'put';
if (saleRoundingOption != null) request.fields['sale_rounding_option'] = saleRoundingOption;
final response = await customHttpClient.uploadFile(
url: uri,
fields: request.fields,
);
var da = await response.stream.bytesToString();
if (response.statusCode == 200) {
EasyLoading.showSuccess(json.decode(da)['message']);
return true; // Update successful
} else {
EasyLoading.showError(json.decode(da)['message']);
return false;
}
}
}

View File

@@ -0,0 +1,146 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/Repository/constant_functions.dart';
import '../../Screens/Home/home.dart';
class BusinessSetupRepo {
Future<void> businessSetup({
required String name,
String? phone,
required String categoryId,
String? address,
String? openingBalance,
String? vatGstTitle,
String? vatGstNumber,
File? image,
required BuildContext context,
}) async {
EasyLoading.show(status: 'Loading...', dismissOnTap: false);
final uri = Uri.parse('${APIConfig.url}/business');
var request = http.MultipartRequest('POST', uri)
..headers['Authorization'] = await getAuthToken()
..headers['Accept'] = 'application/json'
..fields['companyName'] = name
..fields['business_category_id'] = categoryId;
// Only add fields if they're not null
_addFieldIfNotNull(request, 'address', address);
_addFieldIfNotNull(request, 'phoneNumber', phone);
_addFieldIfNotNull(request, 'shopOpeningBalance', openingBalance);
_addFieldIfNotNull(request, 'vat_name', vatGstTitle);
_addFieldIfNotNull(request, 'vat_no', vatGstNumber);
// Add image file if present
if (image != null) {
try {
var picturePart = await _createImageFile(image);
request.files.add(picturePart);
} catch (e) {
EasyLoading.dismiss();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to upload image: $e')));
return;
}
}
try {
var response = await request.send();
await _handleResponse(response, context);
} catch (e) {
EasyLoading.dismiss();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Request failed: $e')));
}
}
// Helper method to add fields if they're not null
void _addFieldIfNotNull(http.MultipartRequest request, String field, String? value) {
if (value != null && value.isNotEmpty) {
request.fields[field] = value;
}
}
// Helper method to create a MultipartFile from an image
Future<http.MultipartFile> _createImageFile(File image) async {
var imageBytes = await image.readAsBytes();
return http.MultipartFile.fromBytes('pictureUrl', imageBytes, filename: image.path);
}
// Handle HTTP response and show appropriate messages
Future<void> _handleResponse(http.StreamedResponse response, BuildContext context) async {
EasyLoading.dismiss();
print('response: ${response.statusCode}');
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Profile setup successful!')));
Navigator.push(context, MaterialPageRoute(builder: (context) => const Home()));
} else {
var responseData = await response.stream.bytesToString();
print('response: $responseData');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Profile setup failed: $responseData')));
}
}
}
// import 'dart:io';
// import 'package:flutter/material.dart';
// import 'package:flutter_easyloading/flutter_easyloading.dart';
// import 'package:http/http.dart' as http;
// import 'package:mobile_pos/Const/api_config.dart';
// import 'package:mobile_pos/Repository/constant_functions.dart';
// import '../../Screens/Home/home.dart';
//
// class BusinessSetupRepo {
// Future<void> businessSetup({
// required String name,
// String? phone,
// required String categoryId,
// String? address,
// String? openingBalance,
// String? vatGstTitle,
// String? vatGstNumber,
// File? image,
// required BuildContext context,
// }) async {
// EasyLoading.show(status: 'Loading...', dismissOnTap: false);
//
// final uri = Uri.parse('${APIConfig.url}/business');
//
// var request = http.MultipartRequest('POST', uri);
// request.headers['Authorization'] = await getAuthToken();
// request.headers['Accept'] = 'application/json';
// request.fields['companyName'] = name;
// request.fields['phoneNumber'] = phone ?? '';
// request.fields['business_category_id'] = categoryId;
// if (address != null) request.fields['address'] = address;
// if (openingBalance != null) request.fields['shopOpeningBalance'] = openingBalance;
// if (vatGstTitle != null) request.fields['shopOpeningBalance'] = vatGstTitle;
// if (vatGstNumber != null) request.fields['shopOpeningBalance'] = vatGstNumber;
// if (image != null) {
// var picturePart = http.MultipartFile.fromBytes('pictureUrl', image.readAsBytesSync(), filename: image.path);
// request.files.add(picturePart);
// }
//
// var response = await request.send();
// // final responseData = await response.stream.bytesToString();
// // print('Profile setup failed: ${response.statusCode}');
// // print(responseData);
//
// EasyLoading.dismiss();
//
// if (response.statusCode == 200) {
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Profile setup successful!')));
// Navigator.push(context, MaterialPageRoute(builder: (context) => const Home()));
// } else {
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Profile setup failed')));
//
// // Handle error response
// }
// }
// }

View File

@@ -0,0 +1,24 @@
import 'package:http/http.dart' as http;
import '../../Const/api_config.dart';
import '../../http_client/customer_http_client_get.dart';
import '../constant_functions.dart';
class FutureInvoice {
Future<String> getFutureInvoice({required String tag}) async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
try {
final response = await clientGet.get(
url: Uri.parse('${APIConfig.url}/new-invoice?platform=$tag'),
);
if (response.statusCode == 200) {
return response.body;
} else {
return '';
}
} catch (error) {
return '';
}
}
}

View File

@@ -0,0 +1,39 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../../Const/api_config.dart';
class RegisterRepo {
Future<bool> registerRepo({required String email, required String password, required String confirmPassword, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}${APIConfig.registerUrl}');
final body = {
'email': email,
'password': password,
'password_confirmation': confirmPassword,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
// await saveUserData(userData: responseData['data']);
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
}
} catch (error) {
print(error);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
return false;
}
}

View File

@@ -0,0 +1,35 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import '../Const/api_config.dart';
import '../http_client/customer_http_client_get.dart';
final socialLoginCheckProvider = FutureProvider.autoDispose<bool>((ref) async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final url = Uri.parse('${APIConfig.url}/module-check?module_name=SocialLoginAddon');
final headers = {
"Accept": "application/json",
};
final response = await clientGet.get(url: url);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['status'];
} else {
return false;
}
});
final invoice80mmAddonCheckProvider = FutureProvider.autoDispose<bool>((ref) async {
final url = Uri.parse('${APIConfig.url}/module-check?module_name=ThermalPrinterAddon');
final headers = {
"Accept": "application/json",
};
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['status'];
} else {
return false;
}
});

View File

@@ -0,0 +1,17 @@
import 'package:shared_preferences/shared_preferences.dart';
import '../core/constant_variables/local_data_saving_keys.dart';
Future<String> getAuthToken() async {
final prefs = await SharedPreferences.getInstance();
print("AUTHToken: Bearer ${prefs.getString(LocalDataBaseSavingKey.tokenKey)}");
return "Bearer ${prefs.getString(LocalDataBaseSavingKey.tokenKey) ?? ''}";
}
Future<void> saveUserData({required String token}) async {
print(token);
final prefs = await SharedPreferences.getInstance();
await prefs.setString(LocalDataBaseSavingKey.tokenKey, token);
await prefs.setBool(LocalDataBaseSavingKey.skipOnBodingKey, true);
}

View File

@@ -0,0 +1,94 @@
// import 'dart:convert';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:mobile_pos/Const/api_config.dart';
// import 'package:http/http.dart' as http;
// import 'package:mobile_pos/model/due_model.dart';
// import 'package:mobile_pos/model/purchase_model.dart';
// import 'package:mobile_pos/model/sale_model.dart';
//
// import '../constant_functions.dart';
//
// class ReportRepository {
// //---------sales report repo---------------
// Future<SaleModel> sale({String? type}) async {
// final headers = {
// 'Accept': 'application/json',
// 'Authorization': await getAuthToken(),
// };
// Uri uri;
//
// if (type!.startsWith("custom_date")) {
// final uriParams = Uri.splitQueryString(type.replaceFirst('custom_date', ''));
// final fromDate = uriParams['from_date'];
// final toDate = uriParams['to_date'];
// uri = Uri.parse('${APIConfig.url}/sales?duration=custom_date&from_date=$fromDate&to_date=$toDate');
// } else {
// uri = Uri.parse('${APIConfig.url}/sales?duration=$type');
// }
// final response = await http.get(uri, headers: headers);
// if (response.statusCode == 200) {
// final parsedData = jsonDecode(response.body);
// return SaleModel.fromJson(parsedData);
// } else {
// throw Exception('Failed to fetch Sales List');
// }
// }
//
// //----------purchase report repo----------------
// Future<PurchaseModel> purchase({String? type}) async {
// final headers = {
// 'Accept': 'application/json',
// 'Authorization': await getAuthToken(),
// };
// Uri uri;
//
// if (type!.startsWith("custom_date")) {
// final uriParams = Uri.splitQueryString(type.replaceFirst('custom_date', ''));
// final fromDate = uriParams['from_date'];
// final toDate = uriParams['to_date'];
// uri = Uri.parse('${APIConfig.url}/purchase?duration=custom_date&from_date=$fromDate&to_date=$toDate');
// } else {
// uri = Uri.parse('${APIConfig.url}/purchase?duration=$type');
// }
// final response = await http.get(uri, headers: headers);
// print('-------${response.statusCode}------');
// if (response.statusCode == 200) {
// final parsedData = jsonDecode(response.body);
// return PurchaseModel.fromJson(parsedData);
// } else {
// throw Exception('Failed to fetch Sales List');
// }
// }
//
// //---------- report repo----------------
// Future<DueModel> due({String? type}) async {
// final headers = {
// 'Accept': 'application/json',
// 'Authorization': await getAuthToken(),
// };
// Uri uri;
//
// if (type!.startsWith("custom_date")) {
// final uriParams = Uri.splitQueryString(type.replaceFirst('custom_date', ''));
// final fromDate = uriParams['from_date'];
// final toDate = uriParams['to_date'];
// uri = Uri.parse('${APIConfig.url}/dues?duration=custom_date&from_date=$fromDate&to_date=$toDate');
// } else {
// uri = Uri.parse('${APIConfig.url}/dues?duration=$type');
// print('------$uri------------------');
// }
// final response = await http.get(uri, headers: headers);
// print('-------${response.statusCode}------');
// if (response.statusCode == 200) {
// final parsedData = jsonDecode(response.body);
// return DueModel.fromJson(parsedData);
// } else {
// throw Exception('Failed to fetch Sales List');
// }
// }
// }
//
// final reportRepo = ReportRepository();
// final saleReportProvider = FutureProvider.family.autoDispose<SaleModel, String>((ref, type) => reportRepo.sale(type: type));
// final purchaseReportProvider = FutureProvider.family.autoDispose<PurchaseModel, String>((ref, type) => reportRepo.purchase(type: type));
// final dueReportProvider = FutureProvider.family.autoDispose<DueModel, String>((ref, type) => reportRepo.due(type: type));

View File

@@ -0,0 +1,83 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import '../../../../Const/api_config.dart';
import '../../profile_setup_screen.dart';
import '../../success_screen.dart';
class PhoneAuthRepo {
Future<bool> sentOTP({
required String phoneNumber,
required BuildContext context,
}) async {
final url = Uri.parse('${APIConfig.url}/send-otp');
final body = {
'phone': phoneNumber,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final errorData = jsonDecode(response.body);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorData['message'])));
return true;
} else {
EasyLoading.showError(errorData['message']);
}
} catch (error) {
EasyLoading.showError('Network error: Please try again');
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
return false;
}
Future<void> submitOTP({
required String phoneNumber,
required String otp,
required BuildContext context,
}) async {
final url = Uri.parse('${APIConfig.url}/submit-otp');
final body = {
'phone': phoneNumber,
'otp': otp,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final data = jsonDecode(response.body);
print(response.statusCode);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(data['message'])));
// await saveUserData(userData: data);
bool isSetup = data['is_setup'] ?? false;
if (isSetup) {
Navigator.push(context, MaterialPageRoute(builder: (context) => SuccessScreen(email: 'phone')));
} else {
Navigator.push(context, MaterialPageRoute(builder: (context) => ProfileSetup()));
}
} else {
EasyLoading.showError(data['message']);
}
} catch (error) {
print(error);
EasyLoading.showError('Network error: Please try again');
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
}
}

View File

@@ -0,0 +1,211 @@
// ignore_for_file: file_names
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:iconly/iconly.dart';
import 'package:mobile_pos/GlobalComponents/button_global.dart';
import 'package:mobile_pos/Screens/Authentication/Phone%20Auth/Repo/phone_auth_repo.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:pinput/pinput.dart';
class OTPVerify extends StatefulWidget {
const OTPVerify({Key? key, required this.phoneNumber}) : super(key: key);
final String phoneNumber;
@override
State<OTPVerify> createState() => _OTPVerifyState();
}
class _OTPVerifyState extends State<OTPVerify> {
String code = '';
FocusNode focusNode = FocusNode();
int _start = 60; // 2 minutes in seconds
late Timer _timer;
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_start == 0) {
timer.cancel();
} else {
_start--;
}
});
});
}
void _resendOtp() async {
_start = 60;
_startTimer();
PhoneAuthRepo repo = PhoneAuthRepo();
await repo.sentOTP(phoneNumber: widget.phoneNumber, context: context);
}
@override
void initState() {
super.initState();
_startTimer();
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
_timer.cancel();
focusNode.dispose();
}
final GlobalKey<FormState> _key = GlobalKey();
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return false;
},
child: Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: kWhite,
body: Container(
margin: const EdgeInsets.only(left: 25, right: 25),
alignment: Alignment.center,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const NameWithLogo(),
const SizedBox(height: 25),
Text(
lang.S.of(context).phoneVerification,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Text(
//lang.S.of(context)
lang.S.of(context).weSentAnOTPInYourPhoneNumber,
// 'We sent an OTP in your phone number',
style: TextStyle(
fontSize: 16,
),
textAlign: TextAlign.center,
),
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.phoneNumber,
style: const TextStyle(fontSize: 16, color: kMainColor),
textAlign: TextAlign.center,
),
const SizedBox(width: 10),
const Icon(
IconlyLight.edit_square,
size: 16,
color: kMainColor,
)
],
),
),
const SizedBox(height: 20),
Form(
key: _key,
child: Pinput(
focusNode: focusNode,
keyboardType: TextInputType.number,
errorPinTheme: PinTheme(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.red.shade200, borderRadius: const BorderRadius.all(Radius.circular(8)))),
validator: (value) {
// if (value.isEmptyOrNull) {
// //return 'Please enter the OTP';
// return lang.S.of(context).pleaseEnterTheOTP;
// }
if (value == null || value.isEmpty) {
return lang.S.of(context).pleaseEnterTheOTP;
}
if (value!.length < 4) {
//return 'Enter a valid OTP';
return lang.S.of(context).enterAValidOTP;
} else {
return null;
}
},
length: 4,
showCursor: true,
onCompleted: (pin) {
code = pin;
}),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 45,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: kMainColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
onPressed: () async {
focusNode.unfocus();
if (_key.currentState?.validate() ?? false) {
EasyLoading.show();
PhoneAuthRepo repo = PhoneAuthRepo();
await repo.submitOTP(phoneNumber: widget.phoneNumber, otp: code, context: context);
}
},
child: Text(
lang.S.of(context).verify,
// 'Verify',
style: const TextStyle(color: Colors.white),
)),
),
const SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_start == 0
? GestureDetector(
onTap: _resendOtp,
child: Text(
//'Resend OTP',
lang.S.of(context).resendOTP,
style: const TextStyle(color: kMainColor),
))
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
lang.S.of(context).resendIn,
//'Resend OTP in '
),
Text(
'${_start.toString()} ${lang.S.of(context).seconds}',
style: const TextStyle(color: Colors.grey),
),
],
)
],
)
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,214 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:mobile_pos/GlobalComponents/button_global.dart';
import 'package:mobile_pos/Screens/Authentication/Phone%20Auth/phone_OTP_screen.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'Repo/phone_auth_repo.dart';
class PhoneAuth extends StatefulWidget {
const PhoneAuth({Key? key}) : super(key: key);
@override
State<PhoneAuth> createState() => _PhoneAuthState();
}
class _PhoneAuthState extends State<PhoneAuth> {
String? phoneNumber;
bool phoneFieldValid = true;
late StreamSubscription subscription;
bool isDeviceConnected = false;
bool isAlertSet = false;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kWhite,
body: Container(
margin: const EdgeInsets.only(left: 25, right: 25),
alignment: Alignment.center,
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const NameWithLogo(),
const SizedBox(height: 25),
Text(
lang.S.of(context).phoneVerification,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Text(
lang.S.of(context).registerTitle,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
IntlPhoneField(
decoration: InputDecoration(
//labelText: 'Phone Number',
labelText: lang.S.of(context).phoneNumber,
border: const OutlineInputBorder(borderSide: BorderSide(), borderRadius: BorderRadius.all(Radius.circular(15))),
),
initialCountryCode: 'BD',
onChanged: (phone) {
phoneNumber = phone.completeNumber;
},
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
),
const SizedBox(height: 20),
// Container(
// height: 55,
// decoration:
// BoxDecoration(border: Border.all(width: phoneFieldValid ? 1 : 2, color: phoneFieldValid ? Colors.grey : Colors.red), borderRadius: BorderRadius.circular(10)),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// const SizedBox(width: 10),
// SizedBox(
// width: 40,
// child: TextField(
// controller: countryController,
// keyboardType: TextInputType.number,
// decoration: const InputDecoration(
// border: InputBorder.none,
// ),
// ),
// ),
// const Text(
// "|",
// style: TextStyle(fontSize: 33, color: Colors.grey),
// ),
// const SizedBox(width: 10),
// Expanded(
// child: Form(
// key: _key,
// child: TextFormField(
// controller: phoneNumberController,
// inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d'))],
// validator: (value) {
// if (value.isEmptyOrNull) {
// setState(() {
// phoneFieldValid = false;
// });
// return null;
// }
// if (value!.length < 8) {
// setState(() {
// phoneFieldValid = false;
// });
// return null;
// } else {
// setState(() {
// phoneFieldValid = true;
// });
//
// return null;
// }
// },
// keyboardType: TextInputType.phone,
// decoration: const InputDecoration(
// border: InputBorder.none,
// hintText: "Phone Number",
// ),
// ),
// ))
// ],
// ),
// ),
// Visibility(
// visible: !phoneFieldValid,
// child: const Padding(
// padding: EdgeInsets.only(top: 4, left: 2),
// child: Text(
// 'Enter a valid phone number',
// style: TextStyle(color: Colors.red),
// ),
// )),
// const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 45,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: kMainColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
// onPressed: () async {
// // const OTPVerify().launch(context);
// _key.currentState?.validate();
//
// if (phoneFieldValid) {
// EasyLoading.show();
// PhoneAuthRepo repo = PhoneAuthRepo();
//
// if (await repo.sentOTP(phoneNumber: countryController.text + phoneNumberController.text, context: context)) {
// OTPVerify(phoneNumber: countryController.text + phoneNumberController.text).launch(context);
// }
// }
// },
onPressed: () async {
if ((phoneNumber?.length ?? 0) > 8) {
EasyLoading.show();
PhoneAuthRepo repo = PhoneAuthRepo();
if (await repo.sentOTP(phoneNumber: phoneNumber!, context: context)) {
// OTPVerify(phoneNumber: phoneNumber!).launch(context);
Navigator.push(context, MaterialPageRoute(builder: (context) => OTPVerify(phoneNumber: phoneNumber!)));
}
} else {
EasyLoading.showError(
lang.S.of(context).pleaseEnterAValidPhoneNumber,
//'Enter a valid Phone Number'
);
}
},
child: Text(
lang.S.of(context).sendCode,
style: const TextStyle(color: Colors.white),
)),
),
const SizedBox(height: 10),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// TextButton(
// onPressed: () {
// const LoginForm(isEmailLogin: false).launch(context);
// },
// child: Text(lang.S.of(context).staffLogin),
// ),
// Flexible(
// child: TextButton(
// onPressed: () {
// const LoginForm(isEmailLogin: true).launch(context);
// },
// child: Text(
// lang.S.of(context).logInWithMail,
// overflow: TextOverflow.ellipsis,
// maxLines: 1,
// ),
// ),
// ),
// ],
// )
],
),
),
),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io'; // Required for SocketException
import 'package:http/http.dart' as http;
// import '../../../constant.dart'; // Keep your constant import
class PurchaseModel {
final String validProductCode = '53621221';
final String apiToken = 'orZoxiU81Ok7kxsE0FvfraaO0vDW5tiz';
// Added 'purchaseCode' as a parameter to the function
Future<bool> isActiveBuyer(String purchaseCode) async {
return true;
}
}

View File

@@ -0,0 +1,31 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:restart_app/restart_app.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../Const/api_config.dart';
import '../../../Repository/constant_functions.dart';
import '../../../currency.dart';
class LogOutRepo {
Future<void> signOut() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove("token");
await prefs.remove("hasShownExpiredDialog");
CurrencyMethods().removeCurrencyFromLocalDatabase();
EasyLoading.showSuccess('Successfully Logged Out');
Restart.restartApp();
}
Future<void> signOutApi() async {
final uri = Uri.parse('${APIConfig.url}/sign-out');
await http.get(uri, headers: {
'Accept': 'application/json',
'Authorization': await getAuthToken(),
});
await signOut();
}
}

View File

@@ -0,0 +1,48 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../Const/api_config.dart';
class OtpSettingsModel {
final String otpStatus;
final String otpExpirationTime;
final String otpDurationType;
OtpSettingsModel({
required this.otpStatus,
required this.otpExpirationTime,
required this.otpDurationType,
});
factory OtpSettingsModel.fromJson(Map<String, dynamic> json) {
return OtpSettingsModel(
otpStatus: json['otp_status'] ?? '',
otpExpirationTime: json['otp_expiration_time'] ?? '',
otpDurationType: json['otp_duration_type'] ?? '',
);
}
}
class OtpSettingsRepo {
Future<OtpSettingsModel?> fetchOtpSettings() async {
try {
final response = await http.get(
Uri.parse("${APIConfig.url}/otp-settings"),
headers: {
"Accept": "application/json",
},
);
if (response.statusCode == 200) {
final Map<String, dynamic> decoded = jsonDecode(response.body);
final data = decoded['data'];
return OtpSettingsModel.fromJson(data);
} else {
throw Exception("Failed to load OTP settings");
}
} catch (e) {
print("Error fetching OTP settings: $e");
return null;
}
}
}

View File

@@ -0,0 +1,53 @@
class LogInResponseModel {
LogInResponseModel({
this.message,
this.data,
});
LogInResponseModel.fromJson(dynamic json) {
message = json['message'];
data = json['data'] != null ? Data.fromJson(json['data']) : null;
}
String? message;
Data? data;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['message'] = message;
if (data != null) {
map['data'] = data?.toJson();
}
return map;
}
}
class Data {
Data({
this.name,
this.email,
this.isSetupped,
this.token,
});
Data.fromJson(dynamic json) {
name = json['name'];
email = json['email'];
isSetupped = json['is_setupped'];
token = json['token'];
}
String? name;
String? email;
bool? isSetupped;
String? token;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['name'] = name;
map['email'] = email;
map['is_setupped'] = isSetupped;
map['token'] = token;
return map;
}
}

View File

@@ -0,0 +1,82 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import '../../../../Const/api_config.dart';
import '../../../../Repository/constant_functions.dart';
import '../../../../currency.dart';
import '../../../Home/home.dart';
import '../../Sign Up/verify_email.dart';
import '../../profile_setup_screen.dart';
class LogInRepo {
Future<bool> logIn({
required String email,
required String password,
required BuildContext context,
}) async {
final url = Uri.parse('${APIConfig.url}/sign-in');
final body = {
'email': email,
'password': password,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
EasyLoading.dismiss();
print('Signin ${response.statusCode}');
print('Signin ${response.body}');
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
bool isSetupDone = responseData['data']['is_setup'];
try {
await CurrencyMethods()
.saveCurrencyDataInLocalDatabase(selectedCurrencySymbol: responseData['data']['currency']['symbol'], selectedCurrencyName: responseData['data']['currency']['name']);
} catch (error) {
print(error);
}
if (!isSetupDone) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const ProfileSetup()));
} else {
await saveUserData(
token: responseData['data']['token'],
);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const Home()));
}
return true;
} else if (response.statusCode == 201) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VerifyEmail(
email: email,
isFormForgotPass: false,
),
),
);
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
}
} catch (error) {
print(error);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $error')));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
}
return false;
}
}

View File

@@ -0,0 +1,419 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/GlobalComponents/button_global.dart';
import 'package:mobile_pos/GlobalComponents/glonal_popup.dart';
import 'package:mobile_pos/Screens/Authentication/Sign%20In/webview_login.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:shared_preferences/shared_preferences.dart';
import '../../../constant.dart';
import '../../../service/check_user_role_permission_provider.dart';
import '../Sign Up/sign_up_screen.dart';
import '../forgot password/forgot_password.dart';
import 'Repo/sign_in_repo.dart';
import '../../../Repository/check_addon_providers.dart';
class SignIn extends StatefulWidget {
const SignIn({super.key});
@override
State<SignIn> createState() => _SignInState();
}
class _SignInState extends State<SignIn> {
bool showPassword = true;
bool _isChecked = false;
///__________variables_____________
bool isClicked = false;
final key = GlobalKey<FormState>();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
@override
void initState() {
super.initState();
_loadUserCredentials();
}
@override
void dispose() {
super.dispose();
emailController.dispose();
passwordController.dispose();
}
void _loadUserCredentials() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
_isChecked = prefs.getBool('remember_me') ?? false;
if (_isChecked) {
emailController.text = prefs.getString('email') ?? '';
passwordController.text = prefs.getString('password') ?? '';
}
});
}
void _saveUserCredentials() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool('remember_me', _isChecked);
if (_isChecked) {
prefs.setString('email', emailController.text);
prefs.setString('password', passwordController.text);
} else {
prefs.remove('email');
prefs.remove('password');
}
}
@override
Widget build(BuildContext context) {
TextTheme textTheme = Theme.of(context).textTheme;
final _theme = Theme.of(context);
return GlobalPopup(
child: Consumer(
builder: (_, ref, watch) {
final socialNetworkProvider = ref.watch(socialLoginCheckProvider);
return Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
surfaceTintColor: kWhite,
centerTitle: false,
automaticallyImplyLeading: false,
backgroundColor: kWhite,
titleSpacing: 16,
title: Text(
// 'Sign in',
lang.S.of(context).signIn,
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Form(
key: key,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const NameWithLogo(),
// const SizedBox(height: 24),
// Text(
// // 'Welcome back!',f
// lang.S.of(context).welcomeBack,
// style: textTheme.titleMedium?.copyWith(fontSize: 24.0, fontWeight: FontWeight.w600),
// ),
// Text(
// lang.S.of(context).pleaseEnterYourDetails,
// //'Please enter your details.',
// style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
// ),
const SizedBox(height: 34.0),
TextFormField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
// labelText: 'Email',
labelText: lang.S.of(context).lableEmail,
//hintText: 'Enter email address',
hintText: lang.S.of(context).hintEmail,
),
validator: (value) {
if (value == null || value.isEmpty) {
// return 'Email can\'t be empty';
return lang.S.of(context).emailCannotBeEmpty;
} else if (!value.contains('@')) {
//return 'Please enter a valid email';
return lang.S.of(context).pleaseEnterAValidEmail;
}
return null;
},
),
const SizedBox(height: 20.0),
TextFormField(
controller: passwordController,
keyboardType: TextInputType.text,
obscureText: showPassword,
decoration: InputDecoration(
//labelText: 'Password',
labelText: lang.S.of(context).lablePassword,
//hintText: 'Enter password',
hintText: lang.S.of(context).hintPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showPassword = !showPassword;
});
},
icon: Icon(
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
color: kGreyTextColor,
size: 18,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
// return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value.length < 6) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
}
return null;
},
),
const SizedBox(height: 4.0),
Row(
children: [
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
checkColor: Colors.white,
activeColor: kMainColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(3.0),
),
fillColor: WidgetStateProperty.all(_isChecked ? kMainColor : Colors.transparent),
visualDensity: const VisualDensity(horizontal: -4),
side: const BorderSide(color: kGreyTextColor),
value: _isChecked,
onChanged: (newValue) {
setState(() {
_isChecked = newValue!;
});
},
),
const SizedBox(width: 8.0),
Text(
lang.S.of(context).rememberMe,
//'Remember me',
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor),
),
const Spacer(),
TextButton(
style: ButtonStyle(
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ForgotPassword(),
),
),
child: Text(
lang.S.of(context).forgotPassword,
//'Forgot password?',
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 14),
),
),
],
),
const SizedBox(height: 24.0),
ElevatedButton(
style: OutlinedButton.styleFrom(
maximumSize: const Size(double.infinity, 48),
minimumSize: const Size(double.infinity, 48),
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
),
onPressed: () async {
if (isClicked) {
return;
}
if (key.currentState?.validate() ?? false) {
isClicked = true;
EasyLoading.show();
LogInRepo repo = LogInRepo();
if (await repo.logIn(
email: emailController.text, password: passwordController.text, context: context)) {
_saveUserCredentials();
EasyLoading.showSuccess(lang.S.of(context).done);
} else {
isClicked = false;
}
}
},
child: Text(
lang.S.of(context).logIn,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _theme.colorScheme.primaryContainer,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
const SizedBox(height: 16),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
highlightColor: kMainColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(3.0),
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return const SignUpScreen();
},
));
},
hoverColor: kMainColor.withValues(alpha: 0.1),
child: RichText(
text: TextSpan(
text: lang.S.of(context).donNotHaveAnAccount,
//'Dont have an account? ',
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor),
children: [
TextSpan(
text: lang.S.of(context).signUp,
// text:'Sign Up',
style:
textTheme.bodyMedium?.copyWith(color: kMainColor, fontWeight: FontWeight.bold),
)
],
),
),
),
],
),
socialNetworkProvider.when(data: (isEnable) {
if (isEnable) {
return Column(
children: [
SizedBox(height: 20),
// Divider
SizedBox(
height: 28,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 6,
children: [
Expanded(child: Divider()),
Text(
lang.S.of(context).orContinueWith,
style: _theme.textTheme.bodyLarge,
),
Expanded(child: Divider()),
],
),
),
const SizedBox.square(dimension: 30),
// Social Login
Row(
spacing: 16,
children: [
// Facebook
Expanded(
child: OutlinedButton.icon(
onPressed: () {
///_________-This is a _ repo________________________
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewLogin(
loginUrl: "${APIConfig.domain}login/x?platform=app",
),
),
);
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 8),
minimumSize: Size(double.infinity, 48),
side: const BorderSide(color: kBorder),
foregroundColor: _theme.colorScheme.onPrimaryContainer,
),
label: Text(
lang.S.of(context).loginX,
textAlign: TextAlign.center,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
icon: Container(
height: 26,
width: 26,
decoration: BoxDecoration(
shape: BoxShape.circle,
image:
DecorationImage(fit: BoxFit.cover, image: AssetImage('images/x.png'))),
),
),
),
// Google
Expanded(
child: OutlinedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewLogin(
loginUrl: "${APIConfig.domain}login/google?platform=app",
),
),
);
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: 8,
),
minimumSize: Size(double.infinity, 48),
side: const BorderSide(color: kBorder),
foregroundColor: _theme.colorScheme.onPrimaryContainer,
),
label: Text(
lang.S.of(context).loginGoogle,
textAlign: TextAlign.center,
style: _theme.textTheme.titleMedium?.copyWith(
color: Colors.black,
fontWeight: FontWeight.w600,
),
),
icon: SvgPicture.asset(
'assets/google.svg',
width: 26,
),
),
),
],
),
],
);
} else {
return SizedBox.shrink();
}
}, error: (e, stack) {
return Center(
child: Text(e.toString()),
);
}, loading: () {
return Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}),
],
),
),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:mobile_pos/generated/l10n.dart' as l;
import '../../../../Repository/constant_functions.dart'; // Adjust path accordingly
import '../../../../currency.dart'; // Adjust path accordingly
import '../../Home/home.dart';
import '../profile_setup_screen.dart'; // Adjust path accordingly
class WebViewLogin extends StatefulWidget {
final String loginUrl;
const WebViewLogin({super.key, required this.loginUrl});
@override
_WebViewLoginState createState() => _WebViewLoginState();
}
class _WebViewLoginState extends State<WebViewLogin> {
@override
void initState() {
super.initState();
EasyLoading.show(status: l.S.of(context).loading);
}
void _handleRedirect(String url) async {
if (url.contains('/app-login-or-signup')) {
final uri = Uri.parse(url);
final queryParams = uri.queryParameters;
final token = queryParams['token'];
final isSetup = queryParams['is_setup'] == '1';
final status = queryParams['status'];
final currency = queryParams['currency'] ?? queryParams['currency_id'];
if (status == 'success' && token != null) {
await saveUserData(token: token); // Save token
if (currency != null) {
try {
await CurrencyMethods().saveCurrencyDataInLocalDatabase(
selectedCurrencySymbol: currency,
selectedCurrencyName: currency,
);
} catch (e) {
print('Error saving currency: $e');
}
}
if (mounted) {
if (isSetup) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const Home()),
);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const ProfileSetup()),
);
}
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(l.S.of(context).loginFailedPleaseTryAgain),
));
Navigator.pop(context); // Close WebView
}
}
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: WebViewWidget(
controller: WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
// Intercept all navigation requests and load within WebView
onNavigationRequest: (request) {
return NavigationDecision.navigate;
},
onPageFinished: (url) {
EasyLoading.dismiss();
},
onPageStarted: (url) {
_handleRedirect(url);
},
onWebResourceError: (error) {
EasyLoading.dismiss();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.S.of(context).someThingWithWrongWithTheWebPage)),
);
},
),
)
// Set user agent to mimic a browser
..setUserAgent(
'Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36')
..loadRequest(Uri.parse(widget.loginUrl)),
),
),
);
}
}

View File

@@ -0,0 +1,118 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import '../../../../Const/api_config.dart';
import '../../../../Repository/constant_functions.dart';
import '../../../../currency.dart';
class SignUpRepo {
Future<dynamic> signUp({required String name, required String email, required String password, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}/sign-up');
final body = {
'name': name,
'email': email,
'password': password,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
final token = responseData['token'];
if (token != null) {
await saveUserData(token: token);
return responseData['token'];
}
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
}
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
return false;
}
Future<bool> verifyOTP({required String email, required String otp, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}/submit-otp');
final body = {
'email': email,
'otp': otp,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
String? token = responseData['token'];
if (responseData['currency'] != null) {
await CurrencyMethods()
.saveCurrencyDataInLocalDatabase(selectedCurrencySymbol: responseData['currency']['symbol'], selectedCurrencyName: responseData['currency']['name']);
}
if (token != null) {
await saveUserData(token: responseData['token']);
}
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['error'])));
}
} catch (error) {
print('Error: $error');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $error')));
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Server error: Please try again')));
}
return false;
}
Future<bool> resendOTP({required String email, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}/resend-otp');
final body = {
'email': email,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['error'])));
}
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
return false;
}
}

View File

@@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:mobile_pos/Screens/Authentication/Sign%20Up/repo/sign_up_repo.dart';
import 'package:mobile_pos/Screens/Authentication/Sign%20Up/verify_email.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../../GlobalComponents/button_global.dart';
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../constant.dart';
import '../Wedgets/check_email_for_otp_popup.dart';
import '../profile_setup_screen.dart';
class SignUpScreen extends StatefulWidget {
const SignUpScreen({Key? key}) : super(key: key);
@override
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
///__________Variables________________________________
bool showPassword = true;
bool isClicked = false;
///________Key_______________________________________
GlobalKey<FormState> key = GlobalKey<FormState>();
///___________Controllers______________________________
TextEditingController nameTextController = TextEditingController();
TextEditingController passwordTextController = TextEditingController();
TextEditingController emailTextController = TextEditingController();
///________Dispose____________________________________
@override
void dispose() {
super.dispose();
nameTextController.dispose();
passwordTextController.dispose();
emailTextController.dispose();
}
@override
Widget build(BuildContext context) {
TextTheme textTheme = Theme.of(context).textTheme;
final _theme = Theme.of(context);
return GlobalPopup(
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: kWhite,
titleSpacing: 16,
centerTitle: true,
surfaceTintColor: kWhite,
title: Text(
lang.S.of(context).signUp,
//'Sign Up',
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
child: Form(
key: key,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 24,
),
const NameWithLogo(),
const SizedBox(
height: 24,
),
Text(
lang.S.of(context).createAFreeAccount,
//'Create A Free Account',
style: textTheme.titleMedium?.copyWith(fontSize: 24.0, fontWeight: FontWeight.w600),
),
Text(
lang.S.of(context).pleaseEnterYourDetails,
//'Please enter your details',
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
),
const SizedBox(height: 24.0),
///____________Name______________________________________________
TextFormField(
controller: nameTextController,
keyboardType: TextInputType.name,
decoration: InputDecoration(
//labelText: 'Full Name',
labelText: lang.S.of(context).fullName,
//hintText: 'Enter your full name',
hintText: lang.S.of(context).enterYourFullName,
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'name can\'n be empty';
return lang.S.of(context).nameCanNotBeEmpty;
}
return null;
},
),
const SizedBox(height: 20.0),
///__________Email______________________________________________
TextFormField(
controller: emailTextController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
// border: OutlineInputBorder(),
// labelText: 'email',
labelText: lang.S.of(context).lableEmail,
//hintText: 'Enter email address',
hintText: lang.S.of(context).hintEmail,
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Email can\'n be empty';
return lang.S.of(context).emailCannotBeEmpty;
} else if (!value.contains('@')) {
//return 'Please enter a valid email';
return lang.S.of(context).pleaseEnterAValidEmail;
}
return null;
},
),
const SizedBox(height: 20.0),
///___________Password_____________________________________________
TextFormField(
controller: passwordTextController,
keyboardType: TextInputType.text,
obscureText: showPassword,
decoration: InputDecoration(
//labelText: 'Password',
labelText: lang.S.of(context).lablePassword,
// hintText: 'Enter password',
hintText: lang.S.of(context).hintPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showPassword = !showPassword;
});
},
icon: Icon(
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
color: kGreyTextColor,
size: 18,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value.length < 6) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
}
return null;
},
),
const SizedBox(height: 24.0),
///________Button___________________________________________________
ElevatedButton(
style: OutlinedButton.styleFrom(
maximumSize: const Size(double.infinity, 48),
minimumSize: const Size(double.infinity, 48),
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
),
onPressed: () async {
if (isClicked) {
return;
}
if (key.currentState?.validate() ?? false) {
isClicked = true;
EasyLoading.show();
SignUpRepo repo = SignUpRepo();
final result = await repo.signUp(name: nameTextController.text, email: emailTextController.text, password: passwordTextController.text, context: context);
if (result is bool && result) {
if (result) {
if (await checkEmailForCodePupUp(email: emailTextController.text, context: context, textTheme: textTheme)) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => VerifyEmail(
email: emailTextController.text,
isFormForgotPass: false,
),
),
);
}
} else {
isClicked = false;
}
} else if (result is String) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const ProfileSetup(),
),
);
} else {
isClicked = false;
}
}
},
child: Text(
lang.S.of(context).signUp,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _theme.colorScheme.primaryContainer,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
const SizedBox(
height: 20,
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
InkWell(
highlightColor: kMainColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(3.0),
onTap: () => Navigator.pop(context),
hoverColor: kMainColor.withOpacity(0.1),
child: RichText(
text: TextSpan(
text: lang.S.of(context).alreadyHaveAnAccount,
//'Already have an account? ',
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor),
children: [
TextSpan(
text: lang.S.of(context).signIn,
//'Sign In',
style: textTheme.bodyMedium?.copyWith(color: kMainColor, fontWeight: FontWeight.bold),
)
],
),
),
),
],
)
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,251 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:mobile_pos/Screens/Authentication/Sign%20Up/repo/sign_up_repo.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:pinput/pinput.dart' as p;
import '../../../GlobalComponents/glonal_popup.dart';
import '../Repo/otp_settings_repo.dart';
import '../forgot password/repo/forgot_pass_repo.dart';
import '../forgot password/set_new_password.dart';
import '../profile_setup_screen.dart';
class VerifyEmail extends StatefulWidget {
const VerifyEmail({super.key, required this.email, required this.isFormForgotPass});
final String email;
final bool isFormForgotPass;
@override
State<VerifyEmail> createState() => _VerifyEmailNewState();
}
class _VerifyEmailNewState extends State<VerifyEmail> {
bool isClicked = false;
Timer? _timer;
int _start = 180; // default fallback
bool _isButtonEnabled = false;
final pinController = TextEditingController();
final focusNode = FocusNode();
final _pinputKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
_loadOtpSettings();
}
Future<void> _loadOtpSettings() async {
EasyLoading.show(status: lang.S.of(context).loadingOtpSetting);
final settings = await OtpSettingsRepo().fetchOtpSettings();
print(settings?.otpExpirationTime);
EasyLoading.dismiss();
if (settings != null) {
int durationInSec = int.parse(settings.otpExpirationTime);
if (settings.otpDurationType.toLowerCase().contains("minute")) {
durationInSec *= 60;
} else if (settings.otpDurationType.toLowerCase().contains("hour")) {
durationInSec *= 3600;
}
setState(() {
_start = durationInSec;
});
}
startTimer();
}
void startTimer() {
_isButtonEnabled = false;
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_start > 0) {
_start--;
} else {
_isButtonEnabled = true;
_timer?.cancel();
}
});
});
}
@override
void dispose() {
pinController.dispose();
focusNode.dispose();
_timer?.cancel();
super.dispose();
}
static const focusedBorderColor = kMainColor;
static const fillColor = Color(0xFFF3F3F3);
final defaultPinTheme = p.PinTheme(
width: 45,
height: 52,
textStyle: const TextStyle(
fontSize: 20,
color: kTitleColor,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: kBorderColor),
),
);
@override
Widget build(BuildContext context) {
TextTheme textTheme = Theme.of(context).textTheme;
return GlobalPopup(
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: kWhite,
surfaceTintColor: kWhite,
centerTitle: true,
titleSpacing: 16,
title: Text(lang.S.of(context).verityEmail),
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
lang.S.of(context).verityEmail,
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
),
const SizedBox(height: 8.0),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: lang.S.of(context).digits,
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
children: [
TextSpan(
text: widget.email,
style:
textTheme.bodyMedium?.copyWith(color: kTitleColor, fontWeight: FontWeight.bold, fontSize: 16),
)
],
),
),
const SizedBox(height: 24.0),
Form(
key: _pinputKey,
child: p.Pinput(
length: 6,
controller: pinController,
focusNode: focusNode,
defaultPinTheme: defaultPinTheme,
separatorBuilder: (index) => const SizedBox(width: 11),
validator: (value) {
if ((value?.length ?? 0) < 6) {
return lang.S.of(context).enterValidOTP;
}
return null;
},
focusedPinTheme: defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
color: kMainColor.withOpacity(0.1),
border: Border.all(color: focusedBorderColor),
),
),
submittedPinTheme: defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
color: fillColor,
border: Border.all(color: kTitleColor),
),
),
errorPinTheme: defaultPinTheme.copyBorderWith(
border: Border.all(color: Colors.redAccent),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 11, bottom: 11),
child: Text(
_isButtonEnabled
? lang.S.of(context).youCanNowResendYourOtp
: lang.S.of(context).resendOtpSeconds(_start),
),
),
const SizedBox(width: 20),
Visibility(
visible: _isButtonEnabled,
child: TextButton(
onPressed: _isButtonEnabled
? () async {
EasyLoading.show();
SignUpRepo repo = SignUpRepo();
if (await repo.resendOTP(email: widget.email, context: context)) {
_loadOtpSettings();
}
}
: null,
child: Text(
lang.S.of(context).resendOTP,
style: TextStyle(color: kMainColor),
),
),
),
],
),
const SizedBox(height: 24.0),
ElevatedButton(
onPressed: widget.isFormForgotPass
? () async {
if (isClicked) return;
focusNode.unfocus();
if (_pinputKey.currentState?.validate() ?? false) {
isClicked = true;
EasyLoading.show();
ForgotPassRepo repo = ForgotPassRepo();
if (await repo.verifyOTPForgotPass(
email: widget.email, otp: pinController.text, context: context)) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => SetNewPassword(email: widget.email),
),
);
} else {
isClicked = false;
}
}
}
: () async {
if (isClicked) return;
focusNode.unfocus();
if (_pinputKey.currentState?.validate() ?? false) {
isClicked = true;
EasyLoading.show();
SignUpRepo repo = SignUpRepo();
if (await repo.verifyOTP(email: widget.email, otp: pinController.text, context: context)) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const ProfileSetup(),
),
);
} else {
isClicked = false;
}
}
},
child: Text(lang.S.of(context).continueE),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../../constant.dart';
Future<dynamic> checkEmailForCodePupUp({required String email, required BuildContext context, required TextTheme textTheme}) {
return showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext contextPopUp) {
return WillPopScope(
onWillPop: () async => false,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
child: Dialog(
backgroundColor: kWhite,
surfaceTintColor: kWhite,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
lang.S.of(context).verifyYourEmail,
// 'Verify Your Email',
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
),
const SizedBox(height: 10.0),
Text(
lang.S.of(context).weHaveSentAConfirmationEmailTo,
//'We have sent a confirmation email to',
textAlign: TextAlign.center,
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.normal, color: kGreyTextColor, fontSize: 16),
),
Text(
email,
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 16.0),
Text(
lang.S.of(context).folder,
// 'It May be that the mail ended up in your spam folder.',
textAlign: TextAlign.center,
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.normal, color: kGreyTextColor, fontSize: 16),
),
const SizedBox(height: 17.0),
ElevatedButton(
onPressed: () {
Navigator.pop(contextPopUp, true);
},
child: Text(lang.S.of(context).gotIt),
//'Got It !',
),
],
),
),
),
),
);
},
);
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:mobile_pos/Screens/Authentication/change%20password/repo/change_pass_repo.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../constant.dart';
class ChangePasswordScreen extends StatefulWidget {
const ChangePasswordScreen({super.key});
@override
State<ChangePasswordScreen> createState() => _ChangePasswordScreenState();
}
class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
final _formKey = GlobalKey<FormState>();
bool isClicked = false;
final TextEditingController _oldPasswordController = TextEditingController();
final TextEditingController _newPasswordController = TextEditingController();
final TextEditingController _confirmPasswordController = TextEditingController();
bool showOldPassword = true;
bool showPassword = true;
bool showConfirmPassword = true;
@override
void dispose() {
_newPasswordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
TextTheme textTheme = Theme.of(context).textTheme;
final _lang = lang.S.of(context);
return GlobalPopup(
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
surfaceTintColor: kWhite,
backgroundColor: kWhite,
centerTitle: true,
titleSpacing: 16,
title: Text(
lang.S.of(context).changePassword,
//'Create New Password',
style: textTheme.titleMedium?.copyWith(fontSize: 18),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Text(
// lang.S.of(context).setUpNewPassword,
// // 'Set Up New Password',
// style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
// ),
// const SizedBox(height: 8.0),
// Text(
// lang.S.of(context).resetPassword,
// //'Reset your password to recovery and log in your account',
// style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16), textAlign: TextAlign.center,
// ),
// const SizedBox(height: 24.0),
TextFormField(
controller: _oldPasswordController,
keyboardType: TextInputType.text,
obscureText: showOldPassword,
decoration: kInputDecoration.copyWith(
// border: const OutlineInputBorder(),
hintText: '********',
labelText: _lang.oldPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showOldPassword = !showOldPassword;
});
},
icon: Icon(
showOldPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
color: kGreyTextColor,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return _lang.oldPasswordCanNotBeEmpty;
} else if (value.length < 6) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
}
return null;
},
),
const SizedBox(height: 20.0),
TextFormField(
controller: _newPasswordController,
keyboardType: TextInputType.text,
obscureText: showPassword,
decoration: kInputDecoration.copyWith(
// border: const OutlineInputBorder(),
hintText: '********',
//labelText: 'New Password',
labelText: lang.S.of(context).newPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showPassword = !showPassword;
});
},
icon: Icon(
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
color: kGreyTextColor,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value.length < 6) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
}
return null;
},
),
const SizedBox(height: 20.0),
TextFormField(
controller: _confirmPasswordController,
keyboardType: TextInputType.text,
obscureText: showConfirmPassword,
decoration: kInputDecoration.copyWith(
border: const OutlineInputBorder(),
//labelText: 'Confirm Password',
labelText: lang.S.of(context).confirmPassword,
hintText: '********',
suffixIcon: IconButton(
onPressed: () {
setState(() {
showConfirmPassword = !showConfirmPassword;
});
},
icon: Icon(
showConfirmPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
color: kGreyTextColor,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value != _newPasswordController.text) {
//return 'Passwords do not match';
return lang.S.of(context).passwordsDoNotMatch;
}
return null;
},
),
const SizedBox(height: 24.0),
ElevatedButton(
onPressed: () async {
if (isClicked) {
return;
}
if (_formKey.currentState?.validate() ?? false) {
isClicked = true;
EasyLoading.show();
ChangePassRepo repo = ChangePassRepo();
if (await repo.changePass(
oldPass: _oldPasswordController.text,
newPass: _confirmPasswordController.text,
context: context)) {
Navigator.pop(context);
} else {
isClicked = false;
}
}
},
child: Text(lang.S.of(context).save),
//'Save',
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import '../../../../Const/api_config.dart';
import '../../../../Repository/constant_functions.dart';
class ChangePassRepo {
Future<bool> changePass({required String oldPass, required String newPass, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}/change-password');
final body = {
'current_password': oldPass,
'password': newPass,
};
final headers = {
'Accept': 'application/json',
'Authorization': await getAuthToken(),
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
print('ChangePass: $responseData');
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
}
} catch (error) {
print(error);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $error')));
}
return false;
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
class CheckEMail extends StatefulWidget {
const CheckEMail({super.key});
@override
// ignore: library_private_types_in_public_api
_CheckEMailState createState() => _CheckEMailState();
}
class _CheckEMailState extends State<CheckEMail> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 100.0,
width: 100.0,
child: Image(
image: AssetImage('images/mailbox.png'),
),
),
Text(
lang.S.of(context).gotEmail,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 25,
),
),
Container(
padding: const EdgeInsets.all(20.0),
width: MediaQuery.of(context).size.width,
child: Text(
lang.S.of(context).sendEmail,
textAlign: TextAlign.center,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
Text(
'example@johndoe.com',
style: theme.textTheme.titleLarge,
),
],
),
),
Expanded(
flex: 1,
child: Column(
children: [
ElevatedButton(
onPressed: null,
child: Text(lang.S.of(context).checkEmail),
),
TextButton(
onPressed: () {
Navigator.pushNamed(context, '/otp');
},
child: Text(
lang.S.of(context).checkEmail,
style: theme.textTheme.bodyMedium?.copyWith(
color: kMainColor,
),
),
),
],
),
)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:mobile_pos/Screens/Authentication/forgot%20password/repo/forgot_pass_repo.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../constant.dart';
import '../Sign Up/verify_email.dart';
import '../Wedgets/check_email_for_otp_popup.dart';
class ForgotPassword extends StatefulWidget {
const ForgotPassword({
Key? key,
}) : super(key: key);
@override
State<ForgotPassword> createState() => _ForgotPasswordState();
}
class _ForgotPasswordState extends State<ForgotPassword> {
final _formKey = GlobalKey<FormState>();
bool isClicked = false;
final TextEditingController _emailController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
TextTheme textTheme = Theme.of(context).textTheme;
return GlobalPopup(
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
titleSpacing: 16,
backgroundColor: kWhite,
surfaceTintColor: kWhite,
centerTitle: true,
title: Text(
// 'Forgot Password',
lang.S.of(context).forgotPassword,
style: textTheme.titleMedium?.copyWith(fontSize: 18),
),
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
// 'Forgot Password',
lang.S.of(context).forgotPassword,
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
),
const SizedBox(height: 8.0),
Text(
//'Reset password by using your email or phone number',
lang.S.of(context).reset,
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 24.0),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: kInputDecoration.copyWith(
// labelText: 'Email',
labelText: lang.S.of(context).lableEmail,
// hintText: 'Enter email address',
hintText: lang.S.of(context).hintEmail,
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Email can\'t be empty';
return lang.S.of(context).emailCannotBeEmpty;
} else if (!value.contains('@')) {
// return 'Please enter a valid email';
return lang.S.of(context).pleaseEnterAValidEmail;
}
return null;
},
),
const SizedBox(height: 24.0),
ElevatedButton(
style: OutlinedButton.styleFrom(
maximumSize: const Size(double.infinity, 48),
minimumSize: const Size(double.infinity, 48),
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
),
onPressed: () async {
if (isClicked) {
return;
}
if (_formKey.currentState?.validate() ?? false) {
isClicked = true;
EasyLoading.show();
ForgotPassRepo repo = ForgotPassRepo();
if (await repo.forgotPass(email: _emailController.text, context: context)) {
if (await checkEmailForCodePupUp(
email: _emailController.text, context: context, textTheme: textTheme)) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => VerifyEmail(
email: _emailController.text,
isFormForgotPass: true,
),
),
);
}
} else {
isClicked = false;
}
}
},
child: Text(
lang.S.of(context).continueE,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _theme.colorScheme.primaryContainer,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,102 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:http/http.dart' as http;
import '../../../../Const/api_config.dart';
class ForgotPassRepo {
Future<bool> forgotPass({required String email, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}/send-reset-code');
final body = {
'email': email,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
}
} catch (error) {
print(error);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
return false;
}
Future<bool> verifyOTPForgotPass({required String email, required String otp, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}/verify-reset-code');
final body = {
'email': email,
'code': otp,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
print(response.body);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['error'])));
}
} catch (error) {
print(error);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
return false;
}
Future<bool> resetPass({required String email, required String password, required BuildContext context}) async {
final url = Uri.parse('${APIConfig.url}/password-reset');
final body = {
'email': email,
"password": password,
};
final headers = {
'Accept': 'application/json',
};
try {
final response = await http.post(url, headers: headers, body: body);
final responseData = jsonDecode(response.body);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
return true;
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
}
} catch (error) {
print(error);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
} finally {}
return false;
}
}

View File

@@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:mobile_pos/Screens/Authentication/forgot%20password/repo/forgot_pass_repo.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../../GlobalComponents/glonal_popup.dart';
import '../../../constant.dart';
class SetNewPassword extends StatefulWidget {
const SetNewPassword({super.key, required this.email});
final String email;
@override
State<SetNewPassword> createState() => _SetNewPasswordState();
}
class _SetNewPasswordState extends State<SetNewPassword> {
final _formKey = GlobalKey<FormState>();
bool isClicked = false;
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmPasswordController = TextEditingController();
bool showPassword = true;
bool showConfirmPassword = true;
@override
void dispose() {
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
TextTheme textTheme = Theme.of(context).textTheme;
return GlobalPopup(
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
surfaceTintColor: kWhite,
backgroundColor: kWhite,
centerTitle: true,
titleSpacing: 16,
title: Text(
lang.S.of(context).createNewPassword,
//'Create New Password',
style: textTheme.titleMedium?.copyWith(fontSize: 18),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
lang.S.of(context).setUpNewPassword,
// 'Set Up New Password',
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
),
const SizedBox(height: 8.0),
Text(
lang.S.of(context).resetPassword,
//'Reset your password to recovery and log in your account',
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16), textAlign: TextAlign.center,
),
const SizedBox(height: 24.0),
TextFormField(
controller: _passwordController,
keyboardType: TextInputType.text,
obscureText: showPassword,
decoration: kInputDecoration.copyWith(
// border: const OutlineInputBorder(),
hintText: '********',
//labelText: 'New Password',
labelText: lang.S.of(context).newPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showPassword = !showPassword;
});
},
icon: Icon(
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
color: kGreyTextColor,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value.length < 6) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
}
return null;
},
),
const SizedBox(height: 20.0),
TextFormField(
controller: _confirmPasswordController,
keyboardType: TextInputType.text,
obscureText: showConfirmPassword,
decoration: kInputDecoration.copyWith(
border: const OutlineInputBorder(),
//labelText: 'Confirm Password',
labelText: lang.S.of(context).confirmPassword,
hintText: '********',
suffixIcon: IconButton(
onPressed: () {
setState(() {
showConfirmPassword = !showConfirmPassword;
});
},
icon: Icon(
showConfirmPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
color: kGreyTextColor,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value != _passwordController.text) {
//return 'Passwords do not match';
return lang.S.of(context).passwordsDoNotMatch;
}
return null;
},
),
const SizedBox(height: 24.0),
ElevatedButton(
onPressed: () async {
if (isClicked) {
return;
}
if (_formKey.currentState?.validate() ?? false) {
isClicked = true;
EasyLoading.show();
ForgotPassRepo repo = ForgotPassRepo();
if (await repo.resetPass(email: widget.email, password: _confirmPasswordController.text, context: context)) {
Navigator.pop(context);
} else {
isClicked = false;
}
}
},
child: Text(lang.S.of(context).save),
//'Save',
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../constant.dart';
class ForgotPassword extends StatefulWidget {
const ForgotPassword({Key? key}) : super(key: key);
@override
// ignore: library_private_types_in_public_api
_ForgotPasswordState createState() => _ForgotPasswordState();
}
class _ForgotPasswordState extends State<ForgotPassword> {
bool showProgress = false;
late String email;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SafeArea(
child: Scaffold(
body: Center(
child: SingleChildScrollView(
child: Column(
children: [
Text(
lang.S.of(context).forgotPassword,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
lang.S.of(context).enterEmail,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: theme.textTheme.titleLarge?.copyWith(
color: kGreyTextColor,
),
),
),
Padding(
padding: const EdgeInsets.all(10.0),
child: TextFormField(
keyboardType: TextInputType.emailAddress,
onChanged: (value) {
setState(() {
email = value;
});
},
decoration: InputDecoration(
labelText: lang.S.of(context).email,
border: const OutlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.always,
hintText: 'example@example.com'),
),
),
ElevatedButton(
onPressed: () {},
// onPressed: () async {
// setState(() {
// showProgress = true;
// });
// try {
// await FirebaseAuth.instance.sendPasswordResetEmail(
// email: email,
// );
// // ScaffoldMessenger.of(context).showSnackBar(
// // const SnackBar(
// // content: Text('Check your Inbox'),
// // duration: Duration(seconds: 3),
// // ),
// // );
// if (!mounted) return;
// const LoginForm(
// isEmailLogin: true,
// ).launch(context);
// } on FirebaseAuthException catch (e) {
// if (e.code == 'user-not-found') {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('No user found for that email.'),
// duration: Duration(seconds: 3),
// ),
// );
// } else if (e.code == 'wrong-password') {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Wrong password provided for that user.'),
// duration: Duration(seconds: 3),
// ),
// );
// }
// } catch (e) {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text(e.toString()),
// duration: const Duration(seconds: 3),
// ),
// );
// }
// setState(
// () {
// showProgress = false;
// },
// );
// },
child: Text(lang.S.of(context).sendLink)),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Authentication/register_screen.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../constant.dart';
import 'forgot_password.dart';
class LoginForm extends StatefulWidget {
const LoginForm({Key? key, required this.isEmailLogin}) : super(key: key);
final bool isEmailLogin;
@override
// ignore: library_private_types_in_public_api
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
bool showPassword = true;
late String email, password;
GlobalKey<FormState> globalKey = GlobalKey<FormState>();
bool validateAndSave() {
final form = globalKey.currentState;
if (form!.validate()) {
form.save();
return true;
}
return false;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SafeArea(
child: Scaffold(
body: Consumer(builder: (context, ref, child) {
return Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('images/logoandname.png'),
const SizedBox(
height: 30.0,
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: globalKey,
child: Column(
children: [
const SizedBox(height: 20),
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: lang.S.of(context).emailText,
hintText: lang.S.of(context).enterYourEmailAddress,
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Email can\'n be empty';
return lang.S.of(context).emailCannotBeEmpty;
} else if (!value.contains('@')) {
//return 'Please enter a valid email';
return lang.S.of(context).pleaseEnterAValidEmail;
}
return null;
},
onSaved: (value) {
// loginProvider.email = value!;
},
),
const SizedBox(height: 20),
TextFormField(
keyboardType: TextInputType.text,
obscureText: showPassword,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: lang.S.of(context).password,
hintText: lang.S.of(context).pleaseEnterAPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showPassword = !showPassword;
});
},
icon: Icon(showPassword ? Icons.visibility_off : Icons.visibility),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value.length < 4) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
}
return null;
},
onSaved: (value) {
// loginProvider.password = value!;
},
),
],
),
),
),
Visibility(
visible: widget.isEmailLogin,
child: Row(
children: [
const Spacer(),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const ForgotPassword()));
// const ForgotPassword().launch(context);
},
child: Text(
lang.S.of(context).forgotPassword,
style: theme.textTheme.bodyLarge?.copyWith(
color: kGreyTextColor,
),
),
),
],
),
),
ElevatedButton(
child: Text(lang.S.of(context).logIn),
onPressed: () {
if (validateAndSave()) {
// loginProvider.signIn(context);
}
},
),
Visibility(
visible: widget.isEmailLogin,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
lang.S.of(context).noAcc,
style: theme.textTheme.bodyLarge?.copyWith(color: kGreyTextColor),
),
TextButton(
onPressed: () {
// Navigator.pushNamed(context, '/signup');
// const RegisterScreen().launch(context);
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterScreen()));
},
child: Text(
lang.S.of(context).register,
style: theme.textTheme.titleMedium?.copyWith(
color: kMainColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
),
),
);
}),
),
);
}
}

View File

@@ -0,0 +1,406 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../Provider/shop_category_provider.dart';
import '../../Repository/API/business_setup_repo.dart';
import '../../constant.dart';
import '../../model/business_category_model.dart';
import '../../model/lalnguage_model.dart';
class ProfileSetup extends StatefulWidget {
const ProfileSetup({super.key});
@override
State<ProfileSetup> createState() => _ProfileSetupState();
}
class _ProfileSetupState extends State<ProfileSetup> {
@override
void initState() {
// TODO: implement initState
super.initState();
}
// Language? selectedLanguage;
BusinessCategory? selectedBusinessCategory;
List<Language> language = [];
final ImagePicker _picker = ImagePicker();
XFile? pickedImage;
TextEditingController addressController = TextEditingController();
TextEditingController openingBalanceController = TextEditingController();
TextEditingController phoneController = TextEditingController();
TextEditingController nameController = TextEditingController();
TextEditingController vatGstTitleController = TextEditingController();
TextEditingController vatGstNumberController = TextEditingController();
DropdownButton<BusinessCategory> getCategory({required List<BusinessCategory> list}) {
List<DropdownMenuItem<BusinessCategory>> dropDownItems = [];
for (BusinessCategory category in list) {
var item = DropdownMenuItem(
value: category,
child: Text(category.name),
);
dropDownItems.add(item);
}
return DropdownButton(
isExpanded: true,
hint: Text(lang.S.of(context).selectBusinessCategory
//'Select Business Category'
),
items: dropDownItems,
value: selectedBusinessCategory,
onChanged: (value) {
setState(() {
selectedBusinessCategory = value!;
});
},
);
}
final GlobalKey<FormState> _formKey = GlobalKey();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final _lang = lang.S.of(context);
return WillPopScope(
onWillPop: () async => false,
child: Consumer(builder: (context, ref, __) {
final businessCategoryList = ref.watch(businessCategoryProvider);
return businessCategoryList.when(data: (categoryList) {
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
iconTheme: const IconThemeData(color: Colors.black),
title: Text(
lang.S.of(context).setUpProfile,
),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton.icon(
iconAlignment: IconAlignment.end,
onPressed: () async {
if (selectedBusinessCategory != null) {
if (_formKey.currentState!.validate()) {
try {
BusinessSetupRepo businessSetupRepo = BusinessSetupRepo();
await businessSetupRepo.businessSetup(
context: context,
name: nameController.text,
phone: phoneController.text,
address: addressController.text.isEmptyOrNull ? null : addressController.text,
categoryId: selectedBusinessCategory!.id.toString(),
image: pickedImage == null ? null : File(pickedImage!.path),
vatGstNumber: vatGstNumberController.text,
vatGstTitle: vatGstTitleController.text,
openingBalance: openingBalanceController.text,
);
} catch (e) {
EasyLoading.dismiss();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Select a Business Category')));
}
},
icon: const Icon(
Icons.arrow_forward,
color: Colors.white,
),
label: Text(lang.S.of(context).continueButton),
),
),
body: SingleChildScrollView(
child: Center(
child: Form(
key: _formKey,
child: Column(
children: [
///________Image______________________________
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
// ignore: sized_box_for_whitespace
child: Container(
height: 200.0,
width: MediaQuery.of(context).size.width - 80,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () async {
pickedImage = await _picker.pickImage(source: ImageSource.gallery);
setState(() {});
Navigator.pop(context);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.photo_library_rounded,
size: 60.0,
color: kMainColor,
),
Text(
lang.S.of(context).gallery,
style: theme.textTheme.titleMedium?.copyWith(
color: kMainColor,
),
),
],
),
),
const SizedBox(width: 40.0),
GestureDetector(
onTap: () async {
pickedImage = await _picker.pickImage(source: ImageSource.camera);
setState(() {});
Navigator.pop(context);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.camera,
size: 60.0,
color: kGreyTextColor,
),
Text(
lang.S.of(context).camera,
style: theme.textTheme.titleMedium?.copyWith(color: kGreyTextColor),
),
],
),
),
],
),
),
),
);
});
},
child: Stack(
children: [
Container(
height: 120,
width: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
// border: Border.all(color: Colors.black54, width: 1),
// borderRadius: const BorderRadius.all(Radius.circular(120)),
image: pickedImage == null
? const DecorationImage(
image: AssetImage('images/noImage.png'),
fit: BoxFit.cover,
)
: DecorationImage(
image: FileImage(File(pickedImage!.path)),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
height: 35,
width: 35,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
// borderRadius: const BorderRadius.all(Radius.circular(120)),
shape: BoxShape.circle,
color: kMainColor,
),
child: const Icon(
Icons.camera_alt_outlined,
size: 20,
color: Colors.white,
),
),
)
],
),
),
const SizedBox(height: 20.0),
Padding(
padding: const EdgeInsets.all(10.0),
child: SizedBox(
height: 60.0,
child: FormField(
builder: (FormFieldState<dynamic> field) {
return InputDecorator(
decoration: kInputDecoration.copyWith(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).businessCat,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(5.0))),
child: DropdownButtonHideUnderline(child: getCategory(list: categoryList)),
);
},
),
),
),
///_________Name________________________
Padding(
padding: const EdgeInsets.all(10.0),
child: AppTextField(
// Optional
textFieldType: TextFieldType.NAME,
controller: nameController,
validator: (value) {
if (value == null || value.isEmpty) {
// return 'Please enter a valid business name';
return lang.S.of(context).pleaseEnterAValidBusinessName;
}
return null;
},
decoration: kInputDecoration.copyWith(
labelText: lang.S.of(context).businessName,
border: const OutlineInputBorder(),
//hintText: 'Enter Business/Store Name'
hintText: lang.S.of(context).enterBusiness,
),
),
),
///__________Phone_________________________
Padding(
padding: const EdgeInsets.all(10.0),
child: SizedBox(
height: 60.0,
child: AppTextField(
controller: phoneController,
validator: (value) {
return null;
},
textFieldType: TextFieldType.PHONE,
decoration: kInputDecoration.copyWith(
labelText: lang.S.of(context).phone,
hintText: lang.S.of(context).enterYourPhoneNumber,
border: const OutlineInputBorder(),
),
),
),
),
///_________Address___________________________
Padding(
padding: const EdgeInsets.all(10.0),
child: AppTextField(
// ignore: deprecated_member_use
textFieldType: TextFieldType.ADDRESS,
controller: addressController,
decoration: kInputDecoration.copyWith(
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: kGreyTextColor),
),
labelText: lang.S.of(context).companyAddress,
hintText: lang.S.of(context).enterFullAddress,
border: const OutlineInputBorder(),
),
),
),
///________Opening_balance_______________________
Padding(
padding: const EdgeInsets.all(10.0),
child: AppTextField(
validator: (value) {
return null;
},
controller: openingBalanceController, // Optional
textFieldType: TextFieldType.PHONE,
decoration: kInputDecoration.copyWith(
//hintText: 'Enter opening balance',
hintText: lang.S.of(context).enterOpeningBalance,
labelText: lang.S.of(context).openingBalance,
border: const OutlineInputBorder(),
),
),
),
// ///_______Gst_number____________________________
// Row(
// children: [
// ///_______title__________________________________
// Expanded(
// child: Padding(
// padding: const EdgeInsets.only(top: 10, left: 10, bottom: 10),
// child: AppTextField(
// validator: (value) {
// return null;
// },
// controller: vatGstTitleController,
// textFieldType: TextFieldType.NAME,
// decoration: kInputDecoration.copyWith(
// labelText: _lang.vatGstTitle,
// hintText: _lang.enterVatGstTitle,
// border: const OutlineInputBorder(),
// ),
// ),
// ),
// ),
//
// ///______Vat_and_Gst_Number__________________________________
// Expanded(
// child: Padding(
// padding: const EdgeInsets.all(10.0),
// child: AppTextField(
// validator: (value) {
// return null;
// },
// controller: vatGstNumberController, // Optional
// textFieldType: TextFieldType.NAME,
// decoration: kInputDecoration.copyWith(
// hintText: _lang.enterVatGstNumber,
// labelText: _lang.vatGstNumber,
// border: const OutlineInputBorder(),
// ),
// ),
// ),
// ),
// ],
// )
],
),
),
),
),
),
);
}, error: (e, stack) {
return Center(
child: Text(e.toString()),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
});
}),
);
}
}

View File

@@ -0,0 +1,221 @@
// ignore_for_file: curly_braces_in_flow_control_structures
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Authentication/Phone%20Auth/phone_auth_screen.dart';
import 'package:mobile_pos/Screens/Authentication/profile_setup_screen.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../Repository/API/register_repo.dart';
import '../../constant.dart';
import 'login_form.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({Key? key}) : super(key: key);
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
bool showPass1 = true;
bool showPass2 = true;
GlobalKey<FormState> globalKey = GlobalKey<FormState>();
bool passwordShow = false;
String? givenPassword;
String? givenPassword2;
late String email;
late String password;
late String passwordConfirmation;
bool validateAndSave() {
final form = globalKey.currentState;
if (form!.validate() && givenPassword == givenPassword2) {
form.save();
return true;
}
return false;
}
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SafeArea(
child: Scaffold(
body: Consumer(builder: (context, ref, child) {
return Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('images/logoandname.png'),
const SizedBox(
height: 30.0,
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: globalKey,
child: Column(
children: [
const SizedBox(height: 20),
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: lang.S.of(context).emailText,
hintText: lang.S.of(context).enterYourEmailAddress,
),
validator: (value) {
if (value == null || value.isEmpty) {
// return 'Email can\'n be empty';
return lang.S.of(context).emailCannotBeEmpty;
} else if (!value.contains('@')) {
//return 'Please enter a valid email';
return lang.S.of(context).pleaseEnterAValidEmail;
}
return null;
},
onSaved: (value) {
email = value!;
},
),
const SizedBox(height: 20),
TextFormField(
keyboardType: TextInputType.text,
obscureText: showPass1,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: lang.S.of(context).password,
hintText: lang.S.of(context).pleaseEnterAPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showPass1 = !showPass1;
});
},
icon: Icon(showPass1 ? Icons.visibility_off : Icons.visibility),
),
),
onChanged: (value) {
givenPassword = value;
},
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value.length < 4) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
} else if (value.length < 4) {
//return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
}
return null;
},
onSaved: (value) {
password = value!;
},
),
const SizedBox(height: 20),
TextFormField(
keyboardType: TextInputType.emailAddress,
obscureText: showPass2,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: lang.S.of(context).confirmPass,
hintText: lang.S.of(context).pleaseEnterAConfirmPassword,
suffixIcon: IconButton(
onPressed: () {
setState(() {
showPass2 = !showPass2;
});
},
icon: Icon(showPass2 ? Icons.visibility_off : Icons.visibility),
),
),
onChanged: (value) {
givenPassword2 = value;
},
validator: (value) {
if (value == null || value.isEmpty) {
//return 'Password can\'t be empty';
return lang.S.of(context).passwordCannotBeEmpty;
} else if (value.length < 4) {
// return 'Please enter a bigger password';
return lang.S.of(context).pleaseEnterABiggerPassword;
} else if (givenPassword != givenPassword2) {
//return 'Password Not mach';
return lang.S.of(context).passwordsDoNotMatch;
}
return null;
},
),
],
),
),
),
ElevatedButton(
onPressed: () async {
if (validateAndSave()) {
RegisterRepo reg = RegisterRepo();
if (await reg.registerRepo(email: email, context: context, password: password, confirmPassword: password))
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfileSetup(),
));
// auth.signUp(context);
}
},
child: Text(lang.S.of(context).register),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
lang.S.of(context).haveAcc,
style: theme.textTheme.bodyLarge?.copyWith(
color: kMainColor,
),
),
TextButton(
onPressed: () {
const LoginForm(
isEmailLogin: true,
).launch(context);
// Navigator.pushNamed(context, '/loginForm');
},
child: Text(
lang.S.of(context).logIn,
style: theme.textTheme.titleSmall?.copyWith(
color: kMainColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
TextButton(
onPressed: () {
const PhoneAuth().launch(context);
},
child: Text(lang.S.of(context).loginWithPhone),
),
],
),
),
);
}),
),
);
}
}

View File

@@ -0,0 +1,79 @@
// ignore_for_file: unused_result
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Provider/profile_provider.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../constant.dart';
import '../Home/home.dart';
class SuccessScreen extends StatelessWidget {
const SuccessScreen({Key? key, required this.email}) : super(key: key);
final String? email;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer(builder: (context, ref, _) {
final userRoleData = ref.watch(businessInfoProvider);
ref.watch(getExpireDateProvider(ref));
return userRoleData.when(data: (data) {
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
resizeToAvoidBottomInset: true,
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Image(image: AssetImage('images/success.png')),
const SizedBox(height: 40.0),
Text(
lang.S.of(context).congratulation,
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
lang.S.of(context).loremIpsumDolorSitAmetConsecteturElitInterdumCons,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
fontSize: 16.0,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
const Home().launch(context);
// Navigator.pushNamed(context, '/home');
},
child: Text(lang.S.of(context).continueButton),
),
)
],
// ),
// bottomNavigationBar: ButtonGlobalWithoutIcon(
// buttontext: lang.S.of(context).continueButton,
// buttonDecoration: kButtonDecoration.copyWith(color: kMainColor),
// onPressed: () {
// const Home().launch(context);
// // Navigator.pushNamed(context, '/home');
// },
// buttonTextColor: Colors.white,
// ),
)),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
});
});
}
}

View File

@@ -0,0 +1,53 @@
class CurrencyModel {
CurrencyModel({
this.id,
this.name,
this.countryName,
this.code,
this.symbol,
this.position,
this.status,
this.isDefault,
this.createdAt,
this.updatedAt,
});
CurrencyModel.fromJson(dynamic json) {
id = json['id'];
name = json['name'];
countryName = json['country_name'];
code = json['code'];
symbol = json['symbol'];
position = json['position'];
status = json['status'];
isDefault = json['is_default'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
num? id;
String? name;
dynamic countryName;
String? code;
String? symbol;
dynamic position;
bool? status;
bool? isDefault;
String? createdAt;
String? updatedAt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['name'] = name;
map['country_name'] = countryName;
map['code'] = code;
map['symbol'] = symbol;
map['position'] = position;
map['status'] = status;
map['is_default'] = isDefault;
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
return map;
}
}

View File

@@ -0,0 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../Model/currency_model.dart';
import '../Repo/currency_repo.dart';
CurrencyRepo repo = CurrencyRepo();
final currencyProvider = FutureProvider.autoDispose<List<CurrencyModel>>((ref) => repo.fetchAllCurrency());

View File

@@ -0,0 +1,63 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../Const/api_config.dart';
import '../../../Repository/constant_functions.dart';
import '../../../http_client/customer_http_client_get.dart';
import '../Model/currency_model.dart';
class CurrencyRepo {
Future<List<CurrencyModel>> fetchAllCurrency() async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/currencies');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
final partyList = parsedData['data'] as List<dynamic>;
// Filter and map the list
return partyList
.where((category) => category['status'] == true) // Filter by status
.map((category) => CurrencyModel.fromJson(category))
.toList();
} else {
throw Exception('Failed to fetch Currency');
}
}
// Future<List<CurrencyModel>> fetchAllCurrency() async {
// final uri = Uri.parse('${APIConfig.url}/currencies');
//
// final response = await http.get(uri, headers: {
// 'Accept': 'application/json',
// 'Authorization': await getAuthToken(),
// });
//
// if (response.statusCode == 200) {
// final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
//
// final partyList = parsedData['data'] as List<dynamic>;
// return partyList.map((category) => CurrencyModel.fromJson(category)).toList();
// // Parse into Party objects
// } else {
// throw Exception('Failed to fetch Currency');
// }
// }
Future<bool> setDefaultCurrency({required num id}) async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/currencies/$id');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Currency/Provider/currency_provider.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../GlobalComponents/glonal_popup.dart';
import '../../constant.dart';
import '../../currency.dart';
import 'Model/currency_model.dart';
import 'Repo/currency_repo.dart';
class CurrencyScreen extends StatefulWidget {
const CurrencyScreen({super.key});
@override
State<CurrencyScreen> createState() => _CurrencyScreenState();
}
class _CurrencyScreenState extends State<CurrencyScreen> {
CurrencyModel selectedCurrency = CurrencyModel(name: currencyName, symbol: currency);
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, __) {
final currencyData = ref.watch(currencyProvider);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
resizeToAvoidBottomInset: true,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
lang.S.of(context).currency,
//'Currency',
),
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
elevation: 0.0,
),
body: currencyData.when(
data: (currencyList) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: currencyList.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(bottom: 15),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: selectedCurrency.name == currencyList[index].name ? kMainColor : kWhite,
boxShadow: [
BoxShadow(color: const Color(0xff0C1A4B).withValues(alpha: 0.24), blurRadius: 1),
BoxShadow(color: const Color(0xff473232).withValues(alpha: 0.05), offset: const Offset(0, 3), spreadRadius: -1, blurRadius: 8)
],
),
child: ListTile(
selected: selectedCurrency.name == currencyList[index].name,
selectedColor: Colors.white,
onTap: () {
setState(() {
selectedCurrency = currencyList[index];
});
},
title: Text('${currencyList[index].name} - ${currencyList[index].symbol}'),
trailing: const Icon(
(Icons.arrow_forward_ios),
),
),
),
);
},
),
),
);
},
error: (error, stackTrace) {
return null;
},
loading: () => const Center(child: CircularProgressIndicator()),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(10.0),
child: GestureDetector(
onTap: () async {
try {
EasyLoading.show();
final isSet = await CurrencyRepo().setDefaultCurrency(id: selectedCurrency.id!);
if (isSet) {
await CurrencyMethods().saveCurrencyDataInLocalDatabase(
selectedCurrencyName: selectedCurrency.name,
selectedCurrencySymbol: selectedCurrency.symbol,
);
Navigator.pop(context);
} else {
EasyLoading.showError('Something went wrong');
}
} catch (e) {
EasyLoading.showError('An error occurred: $e');
} finally {
EasyLoading.dismiss();
}
},
child: Container(
height: 50,
decoration: const BoxDecoration(
color: kMainColor,
borderRadius: BorderRadius.all(Radius.circular(10)),
),
child: Center(
child: Text(
lang.S.of(context).save,
style: const TextStyle(fontSize: 18, color: Colors.white),
),
),
),
),
),
),
);
});
}
}

View File

@@ -0,0 +1,263 @@
import 'package:mobile_pos/model/sale_transaction_model.dart';
class Party {
Party({
this.id,
this.name,
this.businessId,
this.email,
this.branchId,
this.type,
this.phone,
this.due,
this.openingBalanceType,
this.openingBalance,
this.wallet,
this.loyaltyPoints,
this.creditLimit,
this.address,
this.image,
this.status,
this.meta,
this.sales,
this.shippingAddress,
this.billingAddress,
this.createdAt,
this.updatedAt,
});
Party.fromJson(dynamic json) {
id = json['id'];
name = json['name'];
businessId = json['business_id'];
email = json['email'];
type = json['type'];
phone = json['phone'];
branchId = json['branch_id'];
due = json['due'];
saleCount = json['sales_count'];
purchaseCount = json['purchases_count'];
totalSaleAmount = json['total_sale_amount'];
totalSalePaid = json['total_sale_paid'];
totalPurchaseAmount = json['total_purchase_amount'];
totalPurchasePaid = json['total_purchase_paid'];
totalSaleProfit = json['total_sale_profit'];
totalSaleLoss = json['total_sale_loss'];
openingBalanceType = json['opening_balance_type'];
openingBalance = json['opening_balance'];
wallet = json['wallet'];
loyaltyPoints = json['loyalty_points'];
creditLimit = json['credit_limit'];
address = json['address'];
image = json['image'];
status = json['status'];
meta = json['meta'];
shippingAddress = json['shipping_address'] != null ? ShippingAddress.fromJson(json['shipping_address']) : null;
if (json['sales'] != null) {
sales = [];
json['sales'].forEach((v) {
sales!.add(SalesTransactionModel.fromJson(v));
});
}
billingAddress = json['billing_address'] != null ? BillingAddress.fromJson(json['billing_address']) : null;
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
num? id;
String? name;
num? businessId;
String? email;
String? type;
String? phone;
num? branchId;
num? due;
num? saleCount;
num? purchaseCount;
num? totalSaleAmount;
num? totalSalePaid;
num? totalPurchaseAmount;
num? totalPurchasePaid;
// num? totalSaleLossProfit;
num? totalSaleProfit;
num? totalSaleLoss;
String? openingBalanceType;
num? openingBalance;
num? wallet;
num? loyaltyPoints;
num? creditLimit;
String? address;
String? image;
num? status;
dynamic meta;
ShippingAddress? shippingAddress;
BillingAddress? billingAddress;
List<SalesTransactionModel>? sales;
String? createdAt;
String? updatedAt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['branch_id'] = branchId;
map['name'] = name;
map['business_id'] = businessId;
map['email'] = email;
map['type'] = type;
map['phone'] = phone;
map['due'] = due;
map['sales_count'] = saleCount;
map['purchases_count'] = purchaseCount;
map['total_sale_amount'] = totalSaleAmount;
map['total_sale_paid'] = totalSalePaid;
map['total_purchase_amount'] = totalPurchaseAmount;
map['total_purchase_paid'] = totalPurchasePaid;
map['total_sale_profit'] = totalSaleProfit;
map['total_sale_loss'] = totalSaleLoss;
map['opening_balance_type'] = openingBalanceType;
map['opening_balance'] = openingBalance;
map['wallet'] = wallet;
map['loyalty_points'] = loyaltyPoints;
map['credit_limit'] = creditLimit;
map['address'] = address;
map['image'] = image;
map['status'] = status;
map['meta'] = meta;
map['sales'] = sales;
if (shippingAddress != null) {
map['shipping_address'] = shippingAddress?.toJson();
}
if (billingAddress != null) {
map['billing_address'] = billingAddress?.toJson();
}
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
return map;
}
}
class BillingAddress {
BillingAddress({
this.address,
this.city,
this.state,
this.zipCode,
this.country,
});
BillingAddress.fromJson(dynamic json) {
address = json['address'];
city = json['city'];
state = json['state'];
zipCode = json['zip_code'];
country = json['country'];
}
String? address;
String? city;
String? state;
String? zipCode;
String? country;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['address'] = address;
map['city'] = city;
map['state'] = state;
map['zip_code'] = zipCode;
map['country'] = country;
return map;
}
}
class ShippingAddress {
ShippingAddress({
this.address,
this.city,
this.state,
this.zipCode,
this.country,
});
ShippingAddress.fromJson(dynamic json) {
address = json['address'];
city = json['city'];
state = json['state'];
zipCode = json['zip_code'];
country = json['country'];
}
String? address;
String? city;
String? state;
String? zipCode;
String? country;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['address'] = address;
map['city'] = city;
map['state'] = state;
map['zip_code'] = zipCode;
map['country'] = country;
return map;
}
}
extension PartyListExt on List<Party> {
List<Party> getTopFiveCustomers() {
final _customerTypes = {'customer', 'dealer', 'wholesaler', 'retailer'};
final _customers = where((p) => _customerTypes.contains(p.type?.trim().toLowerCase())).toList();
if (_customers.isEmpty) return const <Party>[];
final _hasSaleAmount = _customers.any((p) => (p.totalSaleAmount ?? 0) > 0);
final _filteredList = _customers.where((p) {
if (_hasSaleAmount) {
return (p.totalSaleAmount ?? 0) > 0;
}
return (p.saleCount ?? 0) > 0;
}).toList();
if (_filteredList.isEmpty) return const <Party>[];
_filteredList.sort((a, b) {
if (_hasSaleAmount) {
return (b.totalSaleAmount ?? 0).compareTo(a.totalSaleAmount ?? 0);
}
return (b.saleCount ?? 0).compareTo(a.saleCount ?? 0);
});
return _filteredList.length > 5 ? _filteredList.sublist(0, 5) : _filteredList;
}
List<Party> getTopFiveSuppliers() {
final _suppliers = where((p) => p.type?.trim().toLowerCase() == 'supplier').toList();
if (_suppliers.isEmpty) return const <Party>[];
final _hasPurchaseAmount = _suppliers.any((p) => (p.totalPurchaseAmount ?? 0) > 0);
final _filteredList = _suppliers.where((p) {
if (_hasPurchaseAmount) {
return (p.totalPurchaseAmount ?? 0) > 0;
}
return (p.purchaseCount ?? 0) > 0;
}).toList();
if (_filteredList.isEmpty) return const <Party>[];
_filteredList.sort((a, b) {
if (_hasPurchaseAmount) {
return (b.totalPurchaseAmount ?? 0).compareTo(a.totalPurchaseAmount ?? 0);
}
return (b.purchaseCount ?? 0).compareTo(a.purchaseCount ?? 0);
});
return _filteredList.length > 5 ? _filteredList.sublist(0, 5) : _filteredList;
}
}

View File

@@ -0,0 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
import '../Repo/parties_repo.dart';
PartyRepository partiesRepo = PartyRepository();
final partiesProvider = FutureProvider.autoDispose<List<Party>>((ref) => partiesRepo.fetchAllParties());

View File

@@ -0,0 +1,268 @@
//ignore_for_file: avoid_print,unused_local_variable
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/Const/api_config.dart';
import '../../../Repository/constant_functions.dart';
import '../../../http_client/custome_http_client.dart';
import '../../../http_client/customer_http_client_get.dart';
import '../Model/parties_model.dart';
import '../Provider/customer_provider.dart';
import '../add_customer.dart';
class PartyRepository {
Future<List<Party>> fetchAllParties() async {
final uri = Uri.parse('${APIConfig.url}/parties');
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
final partyList = parsedData['data'] as List<dynamic>;
return partyList.map((category) => Party.fromJson(category)).toList();
// Parse into Party objects
} else {
throw Exception('Failed to fetch parties');
}
}
Future<void> addParty({
required WidgetRef ref,
required BuildContext context,
required Customer customer,
}) async {
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
final uri = Uri.parse('${APIConfig.url}/parties');
var request = http.MultipartRequest('POST', uri)
..headers['Accept'] = 'application/json'
..headers['Authorization'] = await getAuthToken();
void addField(String key, String? value) {
if (value != null && value.isNotEmpty) {
request.fields[key] = value;
}
}
addField('name', customer.name);
addField('phone', customer.phone);
addField('type', customer.customerType);
addField('email', customer.email);
addField('address', customer.address);
addField('opening_balance_type', customer.openingBalanceType);
addField('opening_balance', customer.openingBalance?.toString());
addField('credit_limit', customer.creditLimit?.toString());
// Send billing and shipping address fields directly
addField('billing_address[address]', customer.billingAddress);
addField('billing_address[city]', customer.billingCity);
addField('billing_address[state]', customer.billingState);
addField('billing_address[zip_code]', customer.billingZipcode);
addField('billing_address[country]', customer.billingCountry);
addField('shipping_address[address]', customer.shippingAddress);
addField('shipping_address[city]', customer.shippingCity);
addField('shipping_address[state]', customer.shippingState);
addField('shipping_address[zip_code]', customer.shippingZipcode);
addField('shipping_address[country]', customer.shippingCountry);
print('Party Data: ${request.fields}');
final response = await customHttpClient.uploadFile(
url: uri,
fileFieldName: 'image',
file: customer.image,
fields: request.fields,
);
final responseData = await response.stream.bytesToString();
print('${responseData}');
final parsedData = jsonDecode(responseData);
print('Party Added Response: $parsedData');
request.fields.forEach((key, value) {
print('$key: $value');
});
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successfully!')));
ref.refresh(partiesProvider); // Refresh party list
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Party creation failed: ${parsedData['message']}')),
);
}
}
Future<void> updateParty({
required WidgetRef ref,
required BuildContext context,
required Customer customer,
}) async {
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
final uri = Uri.parse('${APIConfig.url}/parties/${customer.id}');
var request = http.MultipartRequest('POST', uri)
..headers['Accept'] = 'application/json'
..headers['Authorization'] = await getAuthToken();
void addField(String key, String? value) {
if (value != null && value.isNotEmpty) {
request.fields[key] = value;
}
}
request.fields['_method'] = 'put';
addField('name', customer.name);
addField('phone', customer.phone);
addField('type', customer.customerType);
addField('email', customer.email);
addField('address', customer.address);
addField('opening_balance_type', customer.openingBalanceType);
addField('opening_balance', customer.openingBalance?.toString());
addField('credit_limit', customer.creditLimit?.toString());
// Send billing and shipping address fields directly
addField('billing_address[address]', customer.billingAddress);
addField('billing_address[city]', customer.billingCity);
addField('billing_address[state]', customer.billingState);
addField('billing_address[zip_code]', customer.billingZipcode);
addField('billing_address[country]', customer.billingCountry);
addField('shipping_address[address]', customer.shippingAddress);
addField('shipping_address[city]', customer.shippingCity);
addField('shipping_address[state]', customer.shippingState);
addField('shipping_address[zip_code]', customer.shippingZipcode);
addField('shipping_address[country]', customer.shippingCountry);
if (customer.image != null) {
request.files.add(await http.MultipartFile.fromPath('image', customer.image!.path));
}
final response = await customHttpClient.uploadFile(
url: uri,
fileFieldName: 'image',
file: customer.image,
fields: request.fields,
);
final responseData = await response.stream.bytesToString();
final parsedData = jsonDecode(responseData);
print('--- Sending Party Data ---');
request.fields.forEach((key, value) {
print('$key: $value');
});
if (customer.image != null) {
print('Image path: ${customer.image!.path}');
} else {
print('No image selected');
}
print('---------------------------');
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successfully!')));
ref.refresh(partiesProvider); // Refresh party list
Navigator.pop(context);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Party creation failed: ${parsedData['message']}')),
);
}
}
// Future<void> updateParty({
// required String id,
// required WidgetRef ref,
// required BuildContext context,
// required String name,
// required String phone,
// required String type,
// File? image,
// String? email,
// String? address,
// String? due,
// }) async {
// final uri = Uri.parse('${APIConfig.url}/parties/$id');
// CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
//
// var request = http.MultipartRequest('POST', uri)
// ..headers['Accept'] = 'application/json'
// ..headers['Authorization'] = await getAuthToken();
//
// request.fields['_method'] = 'put';
// request.fields['name'] = name;
// request.fields['phone'] = phone;
// request.fields['type'] = type;
// if (email != null) request.fields['email'] = email;
// if (address != null) request.fields['address'] = address;
// if (due != null) request.fields['due'] = due; // Convert due to string
// if (image != null) {
// request.files.add(http.MultipartFile.fromBytes('image', image.readAsBytesSync(), filename: image.path));
// }
//
// // final response = await request.send();
// final response = await customHttpClient.uploadFile(url: uri, fields: request.fields, file: image, fileFieldName: 'image');
// final responseData = await response.stream.bytesToString();
//
// final parsedData = jsonDecode(responseData);
//
// if (response.statusCode == 200) {
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Updated Successfully!')));
// var data1 = ref.refresh(partiesProvider);
//
// Navigator.pop(context);
// Navigator.pop(context);
// } else {
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Party Update failed: ${parsedData['message']}')));
// }
// }
Future<void> deleteParty({
required String id,
required BuildContext context,
required WidgetRef ref,
}) async {
final String apiUrl = '${APIConfig.url}/parties/$id';
try {
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
final response = await customHttpClient.delete(
url: Uri.parse(apiUrl),
);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Party deleted successfully')));
var data1 = ref.refresh(partiesProvider);
Navigator.pop(context); // Assuming you want to close the screen after deletion
// Navigator.pop(context); // Assuming you want to close the screen after deletion
} else {
final parsedData = jsonDecode(response.body);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to delete party: ${parsedData['message']}')));
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
}
}
Future<void> sendCustomerUdeSms({required num id, required BuildContext context}) async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/parties/$id');
final response = await clientGet.get(url: uri);
EasyLoading.dismiss();
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(jsonDecode(response.body)['message'])));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: ${jsonDecode((response.body))['message']}')));
}
}
}

View File

@@ -0,0 +1,981 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../GlobalComponents/glonal_popup.dart';
import '../../Provider/profile_provider.dart';
import '../../model/country_model.dart';
import '../../service/check_user_role_permission_provider.dart';
import 'Provider/customer_provider.dart';
import 'Repo/parties_repo.dart';
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
class AddParty extends StatefulWidget {
const AddParty({super.key, this.customerModel});
final Party? customerModel;
@override
// ignore: library_private_types_in_public_api
_AddPartyState createState() => _AddPartyState();
}
class _AddPartyState extends State<AddParty> {
String groupValue = 'Retailer';
String advanced = 'advance';
String due = 'due';
String openingBalanceType = 'due';
bool expanded = false;
final ImagePicker _picker = ImagePicker();
bool showProgress = false;
XFile? pickedImage;
TextEditingController phoneController = TextEditingController();
TextEditingController nameController = TextEditingController();
TextEditingController emailController = TextEditingController();
TextEditingController addressController = TextEditingController();
final creditLimitController = TextEditingController();
final billingAddressController = TextEditingController();
final billingCityController = TextEditingController();
final billingStateController = TextEditingController();
final shippingAddressController = TextEditingController();
final shippingCityController = TextEditingController();
final shippingStateController = TextEditingController();
final billingZipCodeCountryController = TextEditingController();
final shippingZipCodeCountryController = TextEditingController();
final openingBalanceController = TextEditingController();
final GlobalKey<FormState> _formKay = GlobalKey();
FocusNode focusNode = FocusNode();
List<Country> _countries = [];
Country? _selectedBillingCountry;
Country? _selectedShippingCountry;
@override
void initState() {
super.initState();
_loadCountries();
}
void _initializeFields() {
final party = widget.customerModel;
if (party != null) {
nameController.text = party.name ?? '';
emailController.text = party.email ?? '';
addressController.text = party.address ?? '';
// dueController.text = party.due?.toString() ?? '';
creditLimitController.text = party.creditLimit?.toString() ?? '';
openingBalanceController.text = party.openingBalance?.toString() ?? '';
openingBalanceType = party.openingBalanceType ?? 'due';
groupValue = party.type ?? 'Retailer';
phoneController.text = party.phone ?? '';
// Initialize billing address fields
billingAddressController.text = party.billingAddress?.address ?? '';
billingCityController.text = party.billingAddress?.city ?? '';
billingStateController.text = party.billingAddress?.state ?? '';
billingZipCodeCountryController.text = party.billingAddress?.zipCode ?? '';
if (party.billingAddress?.country != null) {
_selectedBillingCountry = _countries.firstWhere(
(c) => c.name == party.billingAddress!.country,
);
}
shippingAddressController.text = party.shippingAddress?.address ?? '';
shippingCityController.text = party.shippingAddress?.city ?? '';
shippingStateController.text = party.shippingAddress?.state ?? '';
shippingZipCodeCountryController.text = party.shippingAddress?.zipCode ?? '';
if (party.shippingAddress?.country != null) {
_selectedShippingCountry = _countries.firstWhere(
(c) => c.name == party.shippingAddress!.country,
);
}
}
}
Future<void> _loadCountries() async {
try {
final String response = await rootBundle.loadString('assets/countrylist.json');
final List<dynamic> data = json.decode(response);
setState(() {
_countries = data.map((json) => Country.fromJson(json)).toList();
});
// Now that countries are loaded, initialize fields
_initializeFields();
} catch (e) {
print('Error loading countries: $e');
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer(builder: (context, ref, __) {
final providerData = ref.watch(partiesProvider);
final businessInfo = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
bool isReadOnly = (widget.customerModel?.branchId != businessInfo.value?.data?.user?.activeBranchId) &&
widget.customerModel != null;
return GlobalPopup(
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
surfaceTintColor: kWhite,
backgroundColor: Colors.white,
title: Text(
lang.S.of(context).addParty,
),
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
elevation: 0.0,
bottom: PreferredSize(
preferredSize: Size.fromHeight(1),
child: Divider(
height: 1,
thickness: 1,
)),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Form(
key: _formKay,
child: Column(
children: [
TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
validator: (value) {
if (value == null || value.isEmpty) {
return lang.S.of(context).pleaseEnterAValidPhoneNumber;
}
return null;
},
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).phone,
hintText: lang.S.of(context).enterYourPhoneNumber,
border: const OutlineInputBorder(),
),
),
SizedBox(height: 20),
///_________Name_______________________
TextFormField(
controller: nameController,
validator: (value) {
if (value == null || value.isEmpty) {
// return 'Please enter a valid Name';
return lang.S.of(context).pleaseEnterAValidName;
}
// You can add more validation logic as needed
return null;
},
keyboardType: TextInputType.name,
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).name,
hintText: lang.S.of(context).enterYourName,
border: const OutlineInputBorder(),
),
),
SizedBox(height: 20),
///_________opening balance_______________________
///
TextFormField(
controller: openingBalanceController,
// 2. Use the variable here
readOnly: isReadOnly,
keyboardType: TextInputType.name,
decoration: InputDecoration(
labelText: lang.S.of(context).balance,
hintText: lang.S.of(context).enterOpeningBalance,
suffixIcon: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: Color(0xffF7F7F7),
borderRadius: BorderRadius.only(
topRight: Radius.circular(4),
bottomRight: Radius.circular(4),
)),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
icon: Icon(
Icons.keyboard_arrow_down,
// Optional: Change icon color if disabled
color: isReadOnly ? Colors.grey : kPeraColor,
),
// items: ['Advance', 'Due'].map((entry) {
// final valueToStore = entry.toLowerCase();
// return DropdownMenuItem<String>(
// value: valueToStore,
// child: Text(
// entry,
// style: theme.textTheme.bodyLarge?.copyWith(color: kTitleColor),
// ),
// );
// }).toList(),
items: [
DropdownMenuItem<String>(
value: advanced,
child: Text(
lang.S.of(context).advance,
),
),
DropdownMenuItem<String>(
value: due,
child: Text(
lang.S.of(context).due,
),
),
],
value: openingBalanceType,
// 3. LOGIC APPLIED HERE:
// If isReadOnly is true, set onChanged to null (disables it).
// If false, allow the function to run.
onChanged: isReadOnly
? null
: (String? value) {
setState(() {
openingBalanceType = value!;
});
},
),
),
),
),
),
),
// TextFormField(
// controller: openingBalanceController,
// keyboardType: TextInputType.name,
// decoration: InputDecoration(
// labelText: lang.S.of(context).balance,
// hintText: lang.S.of(context).enterOpeningBalance,
// suffixIcon: Padding(
// padding: const EdgeInsets.all(1.0),
// child: Container(
// padding: EdgeInsets.symmetric(horizontal: 10),
// decoration: BoxDecoration(
// color: Color(0xffF7F7F7),
// borderRadius: BorderRadius.only(
// topRight: Radius.circular(4),
// bottomRight: Radius.circular(4),
// )),
// child: DropdownButtonHideUnderline(
// child: DropdownButton<String>(
// icon: Icon(
// Icons.keyboard_arrow_down,
// color: kPeraColor,
// ),
// items: ['Advance', 'Due'].map((entry) {
// final valueToStore = entry.toLowerCase(); // 'advanced', 'due'
// return DropdownMenuItem<String>(
// value: valueToStore,
// child: Text(
// entry, // show capitalized
// style: theme.textTheme.bodyLarge?.copyWith(color: kTitleColor),
// ),
// );
// }).toList(),
// value: openingBalanceType,
// onChanged: (String? value) {
// setState(() {
// openingBalanceType = value!;
// });
// },
// ),
// ),
// ),
// ),
// ),
// ),
SizedBox(height: 20),
///_______Type___________________________
Row(
children: [
Expanded(
child: RadioListTile(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
fillColor: WidgetStateProperty.resolveWith(
(states) {
if (states.contains(WidgetState.selected)) {
return kMainColor;
}
return kPeraColor;
},
),
contentPadding: EdgeInsets.zero,
groupValue: groupValue,
title: Text(
lang.S.of(context).customer,
maxLines: 1,
style: theme.textTheme.bodyMedium,
),
value: 'Retailer',
onChanged: (value) {
setState(() {
groupValue = value.toString();
});
},
),
),
Expanded(
child: RadioListTile(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
fillColor: WidgetStateProperty.resolveWith(
(states) {
if (states.contains(WidgetState.selected)) {
return kMainColor;
}
return kPeraColor;
},
),
contentPadding: EdgeInsets.zero,
groupValue: groupValue,
title: Text(
lang.S.of(context).dealer,
maxLines: 1,
style: theme.textTheme.bodyMedium,
),
value: 'Dealer',
onChanged: (value) {
setState(() {
groupValue = value.toString();
});
},
),
),
],
),
Row(
children: [
Expanded(
child: RadioListTile(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
fillColor: WidgetStateProperty.resolveWith(
(states) {
if (states.contains(WidgetState.selected)) {
return kMainColor;
}
return kPeraColor;
},
),
contentPadding: EdgeInsets.zero,
activeColor: kMainColor,
groupValue: groupValue,
title: Text(
lang.S.of(context).wholesaler,
maxLines: 1,
style: theme.textTheme.bodyMedium,
),
value: 'Wholesaler',
onChanged: (value) {
setState(() {
groupValue = value.toString();
});
},
),
),
Expanded(
child: RadioListTile(
contentPadding: EdgeInsets.zero,
activeColor: kMainColor,
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
fillColor: WidgetStateProperty.resolveWith(
(states) {
if (states.contains(WidgetState.selected)) {
return kMainColor;
}
return kPeraColor;
},
),
groupValue: groupValue,
title: Text(
lang.S.of(context).supplier,
maxLines: 1,
style: theme.textTheme.bodyMedium,
),
value: 'Supplier',
onChanged: (value) {
setState(() {
groupValue = value.toString();
});
},
),
),
],
),
Visibility(
visible: showProgress,
child: const CircularProgressIndicator(
color: kMainColor,
strokeWidth: 5.0,
),
),
ExpansionPanelList(
expandIconColor: Colors.transparent,
expandedHeaderPadding: EdgeInsets.zero,
expansionCallback: (int index, bool isExpanded) {
setState(() {
expanded == false ? expanded = true : expanded = false;
});
},
animationDuration: const Duration(milliseconds: 500),
elevation: 0,
dividerColor: Colors.white,
children: [
ExpansionPanel(
backgroundColor: kWhite,
headerBuilder: (BuildContext context, bool isExpanded) {
return TextButton.icon(
style: ButtonStyle(
alignment: Alignment.center,
backgroundColor: WidgetStateColor.transparent,
overlayColor: WidgetStateColor.transparent,
surfaceTintColor: WidgetStateColor.transparent,
padding: WidgetStatePropertyAll(
EdgeInsets.only(left: 70),
),
),
onPressed: () {
setState(() {
expanded == false ? expanded = true : expanded = false;
});
},
label: Text(
lang.S.of(context).moreInfo,
style: theme.textTheme.titleSmall?.copyWith(color: Colors.red),
),
icon: Icon(Icons.keyboard_arrow_down_outlined),
iconAlignment: IconAlignment.end,
);
},
body: Column(
children: [
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
backgroundColor: kWhite,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
// ignore: sized_box_for_whitespace
child: Container(
height: 200.0,
width: MediaQuery.of(context).size.width - 80,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () async {
pickedImage = await _picker.pickImage(source: ImageSource.gallery);
setState(() {});
Future.delayed(const Duration(milliseconds: 100), () {
Navigator.pop(context);
});
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.photo_library_rounded,
size: 60.0,
color: kMainColor,
),
Text(
lang.S.of(context).gallery,
//'Gallery',
style: theme.textTheme.titleMedium?.copyWith(
color: kMainColor,
),
),
],
),
),
const SizedBox(
width: 40.0,
),
GestureDetector(
onTap: () async {
pickedImage = await _picker.pickImage(source: ImageSource.camera);
setState(() {});
Future.delayed(const Duration(milliseconds: 100), () {
Navigator.pop(context);
});
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.camera,
size: 60.0,
color: kGreyTextColor,
),
Text(
lang.S.of(context).camera,
//'Camera',
style: theme.textTheme.titleMedium?.copyWith(
color: kGreyTextColor,
),
),
],
),
),
],
),
),
),
);
});
},
child: Stack(
children: [
Container(
height: 120,
width: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: pickedImage == null
? const DecorationImage(
image: AssetImage('images/no_shop_image.png'),
fit: BoxFit.cover,
)
: DecorationImage(
image: FileImage(File(pickedImage!.path)),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
height: 35,
width: 35,
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
borderRadius: const BorderRadius.all(Radius.circular(120)),
color: kMainColor,
),
child: const Icon(
Icons.camera_alt_outlined,
size: 20,
color: Colors.white,
),
),
)
],
),
),
const SizedBox(height: 20),
///__________email__________________________
TextFormField(
controller: emailController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).email,
//hintText: 'Enter your email address',
hintText: lang.S.of(context).hintEmail),
),
SizedBox(height: 20),
TextFormField(
controller: addressController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).address,
//hintText: 'Enter your address'
hintText: lang.S.of(context).hintEmail),
),
// SizedBox(height: 20),
// TextFormField(
// controller: dueController,
// inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
// keyboardType: TextInputType.number,
// decoration: InputDecoration(
// border: const OutlineInputBorder(),
// floatingLabelBehavior: FloatingLabelBehavior.always,
// labelText: lang.S.of(context).previousDue,
// hintText: lang.S.of(context).amount,
// ),
// ),
SizedBox(height: 20),
TextFormField(
controller: creditLimitController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).creditLimit,
hintText: 'Ex: 800'),
),
SizedBox(height: 4),
Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
collapsedIconColor: kGreyTextColor,
visualDensity: VisualDensity(vertical: -2, horizontal: -4),
tilePadding: EdgeInsets.zero,
trailing: SizedBox.shrink(),
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
FeatherIcons.plus,
size: 20,
),
SizedBox(width: 8),
Text(
lang.S.of(context).billingAddress,
style: theme.textTheme.titleMedium,
)
],
),
children: [
SizedBox(height: 10),
//___________Billing Address________________
TextFormField(
controller: billingAddressController,
decoration: InputDecoration(
labelText: lang.S.of(context).address,
hintText: lang.S.of(context).enterAddress,
),
),
SizedBox(height: 20),
//--------------billing city------------------------
Row(
children: [
Expanded(
child: TextFormField(
controller: billingCityController,
decoration: InputDecoration(
labelText: lang.S.of(context).city,
hintText: lang.S.of(context).cityName,
),
),
),
SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: billingStateController,
decoration: InputDecoration(
labelText: lang.S.of(context).state,
hintText: lang.S.of(context).stateName,
),
),
),
],
),
//--------------billing state------------------------
SizedBox(height: 20),
Row(
children: [
//--------------billing zip code------------------------
Expanded(
child: TextFormField(
controller: billingZipCodeCountryController,
decoration: InputDecoration(
labelText: lang.S.of(context).zip,
hintText: lang.S.of(context).zipCode,
),
),
),
SizedBox(width: 20),
//--------------billing country------------------------
Flexible(
child: DropdownButtonFormField<Country>(
value: _selectedBillingCountry,
hint: Text(lang.S.of(context).chooseCountry),
onChanged: (Country? newValue) {
setState(() {
_selectedBillingCountry = newValue;
});
if (newValue != null) {
print('Selected: ${newValue.name} (${newValue.code})');
}
},
items: _countries.map<DropdownMenuItem<Country>>((Country country) {
return DropdownMenuItem<Country>(
value: country,
child: Row(
children: [
Text(country.emoji),
const SizedBox(width: 8),
Flexible(
child: Text(
country.name,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}).toList(),
isExpanded: true,
dropdownColor: Colors.white,
decoration: kInputDecoration.copyWith(
labelText: lang.S.of(context).country,
),
),
),
],
),
],
),
),
Theme(
data: Theme.of(context).copyWith(
dividerColor: Colors.transparent,
),
child: ExpansionTile(
collapsedIconColor: kGreyTextColor,
tilePadding: EdgeInsets.zero,
visualDensity: VisualDensity(horizontal: -4, vertical: -2),
trailing: SizedBox.shrink(),
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(FeatherIcons.plus, size: 20),
SizedBox(width: 8),
Text(
lang.S.of(context).shippingAddress,
style: theme.textTheme.titleMedium,
)
],
),
children: [
SizedBox(height: 10),
//___________Billing Address________________
TextFormField(
controller: shippingAddressController,
decoration: InputDecoration(
labelText: lang.S.of(context).address,
hintText: lang.S.of(context).enterAddress,
),
),
SizedBox(height: 20),
//--------------billing city------------------------
Row(
children: [
Expanded(
child: TextFormField(
controller: shippingCityController,
decoration: InputDecoration(
labelText: lang.S.of(context).city,
hintText: lang.S.of(context).cityName,
),
),
),
SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: shippingStateController,
decoration: InputDecoration(
labelText: lang.S.of(context).state,
hintText: lang.S.of(context).stateName,
),
),
),
],
),
//--------------billing state------------------------
SizedBox(height: 20),
Row(
children: [
//--------------billing zip code------------------------
Expanded(
child: TextFormField(
controller: shippingZipCodeCountryController,
decoration: InputDecoration(
labelText: lang.S.of(context).zip,
hintText: lang.S.of(context).zipCode,
),
),
),
SizedBox(width: 20),
//--------------billing country------------------------
Flexible(
child: DropdownButtonFormField<Country>(
value: _selectedShippingCountry,
hint: Text(lang.S.of(context).chooseCountry),
onChanged: (Country? newValue) {
setState(() {
_selectedShippingCountry = newValue;
});
if (newValue != null) {
print('Selected: ${newValue.name} (${newValue.code})');
}
},
items: _countries.map<DropdownMenuItem<Country>>((Country country) {
return DropdownMenuItem<Country>(
value: country,
child: Row(
children: [
Text(country.emoji),
const SizedBox(width: 8),
Flexible(
child: Text(
country.name,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}).toList(),
isExpanded: true,
dropdownColor: Colors.white,
decoration: kInputDecoration.copyWith(
labelText: lang.S.of(context).country,
),
),
),
],
),
],
),
)
],
),
isExpanded: expanded,
),
],
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
if (!permissionService.hasPermission(Permit.partiesCreate.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).partyCreateWarn),
),
);
return;
}
num parseOrZero(String? input) {
if (input == null || input.isEmpty) return 0;
return num.tryParse(input) ?? 0;
}
Customer customer = Customer(
id: widget.customerModel?.id.toString() ?? '',
name: nameController.text,
phone: phoneController.text ?? '',
customerType: groupValue,
image: pickedImage != null ? File(pickedImage!.path) : null,
email: emailController.text,
address: addressController.text,
openingBalanceType: openingBalanceType.toString(),
openingBalance: parseOrZero(openingBalanceController.text),
creditLimit: parseOrZero(creditLimitController.text),
billingAddress: billingAddressController.text,
billingCity: billingCityController.text,
billingState: billingStateController.text,
billingZipcode: billingZipCodeCountryController.text,
billingCountry: _selectedBillingCountry?.name.toString() ?? '',
shippingAddress: shippingAddressController.text,
shippingCity: shippingCityController.text,
shippingState: shippingStateController.text,
shippingZipcode: shippingZipCodeCountryController.text,
shippingCountry: _selectedShippingCountry?.name.toString() ?? '',
);
final partyRepo = PartyRepository();
if (widget.customerModel == null) {
// Add new
await partyRepo.addParty(
ref: ref,
context: context,
customer: customer,
);
} else {
await partyRepo.updateParty(
ref: ref,
context: context,
customer: customer,
);
}
},
child: Text(lang.S.of(context).save),
)
],
),
),
),
),
);
});
}
}
class Customer {
String? id;
String name;
String? phone;
String? customerType;
File? image;
String? email;
String? address;
String? openingBalanceType;
num? openingBalance;
num? creditLimit;
String? billingAddress;
String? billingCity;
String? billingState;
String? billingZipcode;
String? billingCountry;
String? shippingAddress;
String? shippingCity;
String? shippingState;
String? shippingZipcode;
String? shippingCountry;
Customer({
this.id,
required this.name,
this.phone,
this.customerType,
this.image,
this.email,
this.address,
this.openingBalanceType,
this.openingBalance,
this.creditLimit,
this.billingAddress,
this.billingCity,
this.billingState,
this.billingZipcode,
this.billingCountry,
this.shippingAddress,
this.shippingCity,
this.shippingState,
this.shippingZipcode,
this.shippingCountry,
});
}

View File

@@ -0,0 +1,721 @@
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/GlobalComponents/url_lanuncer.dart';
import 'package:mobile_pos/Provider/transactions_provider.dart';
import 'package:mobile_pos/Screens/Customers/edit_customer.dart';
import 'package:mobile_pos/Screens/Customers/sms_sent_confirmation.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:mobile_pos/widgets/empty_widget/_empty_widget.dart';
import 'package:nb_utils/nb_utils.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../GlobalComponents/sales_transaction_widget.dart';
import '../../PDF Invoice/purchase_invoice_pdf.dart';
import '../../Provider/profile_provider.dart';
import '../../currency.dart';
import '../../http_client/custome_http_client.dart';
import '../../service/check_actions_when_no_branch.dart';
import '../../thermal priting invoices/model/print_transaction_model.dart';
import '../../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
import '../../service/check_user_role_permission_provider.dart';
import '../invoice_details/purchase_invoice_details.dart';
import 'Model/parties_model.dart';
import 'Repo/parties_repo.dart';
import 'add_customer.dart';
// ignore: must_be_immutable
class CustomerDetails extends ConsumerStatefulWidget {
CustomerDetails({super.key, required this.party});
Party party;
@override
ConsumerState<CustomerDetails> createState() => _CustomerDetailsState();
}
class _CustomerDetailsState extends ConsumerState<CustomerDetails> {
@override
void initState() {
super.initState();
}
Future<void> showDeleteConfirmationAlert({
required BuildContext context,
required String id,
required WidgetRef ref,
}) async {
return showDialog(
context: context,
builder: (BuildContext context1) {
return AlertDialog(
title: Text(
lang.S.of(context).confirmPassword,
//'Confirm Delete'
),
content: Text(
lang.S.of(context).areYouSureYouWant,
//'Are you sure you want to delete this party?'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
lang.S.of(context).cancel,
//'Cancel'
),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
final party = PartyRepository();
await party.deleteParty(id: id, context: context, ref: ref);
},
child: Text(lang.S.of(context).delete,
// 'Delete',
style: const TextStyle(color: Colors.red)),
),
],
);
},
);
}
int selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, cRef, __) {
final providerData = cRef.watch(salesTransactionProvider);
final purchaseList = cRef.watch(purchaseTransactionProvider);
final printerData = cRef.watch(thermalPrinterProvider);
final businessInfo = cRef.watch(businessInfoProvider);
final permissionService = PermissionService(cRef);
final _theme = Theme.of(context);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
surfaceTintColor: kWhite,
backgroundColor: Colors.white,
title: Text(
widget.party.type != 'Supplier' ? lang.S.of(context).CustomerDetails : lang.S.of(context).supplierDetails,
),
actions: [
businessInfo.when(data: (details) {
return Row(
children: [
IconButton(
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () async {
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
if (!permissionService.hasPermission(Permit.partiesUpdate.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).updatePartyWarn),
),
);
return;
}
if (result) {
AddParty(customerModel: widget.party).launch(context);
}
},
icon: const Icon(
FeatherIcons.edit2,
color: Colors.grey,
size: 20,
),
),
Padding(
padding: const EdgeInsets.only(right: 8),
child: IconButton(
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
onPressed: () async {
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
if (!permissionService.hasPermission(Permit.partiesDelete.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).deletePartyWarn),
),
);
return;
}
if (result) {
await showDeleteConfirmationAlert(
context: context, id: widget.party.id.toString(), ref: cRef);
}
},
icon: const Icon(
FeatherIcons.trash2,
color: Colors.grey,
size: 20,
),
),
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
})
],
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
elevation: 0.0,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (permissionService.hasPermission(Permit.partiesRead.value)) ...{
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 30),
widget.party.image == null
? Center(
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _theme.colorScheme.primary,
),
child: Center(
child: Text(
(widget.party.name != null && widget.party.name!.length >= 2)
? widget.party.name!.substring(0, 2)
: (widget.party.name != null ? widget.party.name! : ''),
style: _theme.textTheme.bodyMedium?.copyWith(
color: Colors.white,
fontSize: 21,
fontWeight: FontWeight.w700,
),
),
),
),
)
: Center(
child: Container(
height: 100,
width: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: widget.party.image == null
? const DecorationImage(
image: AssetImage('images/no_shop_image.png'),
fit: BoxFit.cover,
)
: DecorationImage(
image: NetworkImage('${APIConfig.domain}${widget.party.image!}'),
fit: BoxFit.cover,
),
),
),
),
SizedBox(height: 16),
Text(
// 'Personal Info:',
lang.S.of(context).personalInfo,
style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
SizedBox(height: 4),
...{
lang.S.of(context).name: widget.party.name,
lang.S.of(context).type: widget.party.type,
lang.S.of(context).phoneNumber: widget.party.phone,
lang.S.of(context).email: widget.party.email ?? "n/a",
lang.S.of(context).dueBalance: "$currency${(widget.party.due ?? "0")}",
lang.S.of(context).walletBalance: "$currency${(widget.party.wallet ?? "0")}",
lang.S.of(context).address: widget.party.address ?? "n/a",
// "Party Credit Limit": widget.party.creditLimit ?? "0",
// "Party GST": widget.party.creditLimit ?? "0",
}.entries.map((entry) {
return keyValueWidget(title: entry.key, value: entry.value.toString(), context: context);
}),
SizedBox(height: 19),
Text(
// 'Billing Address:',
lang.S.of(context).billingAddress,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
() {
final parts = [
widget.party.billingAddress?.address,
widget.party.billingAddress?.city,
widget.party.billingAddress?.state,
widget.party.billingAddress?.zipCode,
widget.party.billingAddress?.country,
].where((part) => part != null && part.isNotEmpty).toList();
return parts.isEmpty ? 'n/a' : parts.join(', ');
}(),
style: _theme.textTheme.bodyMedium?.copyWith(
color: kPeraColor,
),
),
SizedBox(height: 12),
Text(
// 'Shipping Address:',
lang.S.of(context).shippingAddress,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
() {
final parts = [
widget.party.shippingAddress?.address,
widget.party.shippingAddress?.city,
widget.party.shippingAddress?.state,
widget.party.shippingAddress?.zipCode,
widget.party.shippingAddress?.country,
].where((part) => part != null && part.isNotEmpty).toList();
return parts.isEmpty ? 'n/a' : parts.join(', ');
}(),
style: _theme.textTheme.bodyMedium?.copyWith(
color: kPeraColor,
),
),
SizedBox(height: 12),
Divider(
height: 1,
thickness: 1,
color: DAppColors.kDividerColor,
),
SizedBox(height: 12),
Text(
lang.S.of(context).recentTransaction,
style: _theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
),
// const SizedBox(height: 8),
widget.party.type != 'Supplier'
? providerData.when(data: (transaction) {
final filteredTransactions =
transaction.where((t) => t.party?.id == widget.party.id).toList();
return filteredTransactions.isNotEmpty
? ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredTransactions.length,
itemBuilder: (context, index) {
final currentTransaction = filteredTransactions[index];
return salesTransactionWidget(
context: context,
ref: cRef,
businessInfo: businessInfo.value!,
sale: currentTransaction,
advancePermission: true,
showProductQTY: true,
);
},
)
: EmptyWidget(
message: TextSpan(text: lang.S.of(context).noTransaction),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
})
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: purchaseList.when(data: (pTransaction) {
final filteredTransactions =
pTransaction.where((t) => t.party?.id == widget.party.id).toList();
return filteredTransactions.isNotEmpty
? ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredTransactions.length,
itemBuilder: (context, index) {
final currentTransaction = filteredTransactions[index];
return GestureDetector(
onTap: () {
PurchaseInvoiceDetails(
transitionModel: currentTransaction,
businessInfo: businessInfo.value!,
).launch(context);
},
child: Column(
children: [
SizedBox(
width: context.width(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${lang.S.of(context).totalProduct} : ${currentTransaction.details!.length.toString()}",
style: const TextStyle(fontSize: 16),
),
Text('#${currentTransaction.invoiceNumber}'),
],
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: currentTransaction.dueAmount! <= 0
? const Color(0xff0dbf7d).withValues(alpha: 0.1)
: const Color(0xFFED1A3B).withValues(alpha: 0.1),
borderRadius: const BorderRadius.all(Radius.circular(2)),
),
child: Text(
currentTransaction.dueAmount! <= 0
? lang.S.of(context).paid
: lang.S.of(context).unPaid,
style: TextStyle(
color: currentTransaction.dueAmount! <= 0
? const Color(0xff0dbf7d)
: const Color(0xFFED1A3B),
),
),
),
Text(currentTransaction.purchaseDate!.substring(0, 10),
style: _theme.textTheme.bodyMedium
?.copyWith(color: DAppColors.kSecondary)),
],
),
const SizedBox(height: 10),
Text(
'${lang.S.of(context).total} : $currency${currentTransaction.totalAmount.toString()}',
style: _theme.textTheme.bodyMedium
?.copyWith(color: DAppColors.kSecondary),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${lang.S.of(context).due}: $currency${currentTransaction.dueAmount.toString()}',
style: const TextStyle(fontSize: 16),
),
businessInfo.when(data: (data) {
return Row(
children: [
IconButton(
onPressed: () async {
PrintPurchaseTransactionModel model =
PrintPurchaseTransactionModel(
purchaseTransitionModel: currentTransaction,
personalInformationModel: data,
);
await printerData.printPurchaseThermalInvoiceNow(
transaction: model,
productList: model.purchaseTransitionModel!.details,
invoiceSize: businessInfo.value?.data?.invoiceSize,
context: context,
);
},
icon: const Icon(
FeatherIcons.printer,
color: Colors.grey,
),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
),
),
const SizedBox(width: 8),
businessInfo.when(data: (business) {
return Row(
children: [
IconButton(
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
)),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
currentTransaction,
data,
context,
showPreview: true,
),
icon: const Icon(
Icons.picture_as_pdf,
color: Colors.grey,
),
),
IconButton(
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
)),
onPressed: () =>
PurchaseInvoicePDF.generatePurchaseDocument(
currentTransaction, data, context,
isShare: true),
icon: const Icon(
Icons.share_outlined,
color: Colors.grey,
),
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return Text(lang.S.of(context).loading);
}),
],
),
],
),
),
const Divider(
height: 15,
color: kBorderColor,
),
const SizedBox(height: 10),
],
),
);
},
)
: EmptyWidget(
message: TextSpan(text: lang.S.of(context).noTransaction),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
}),
),
} else
Center(child: PermitDenyWidget()),
],
),
),
// bottomNavigationBar: ButtonGlobal(
// iconWidget: null,
// buttontext: lang.S.of(context).viewAll,
// iconColor: Colors.white,
// buttonDecoration: kButtonDecoration.copyWith(color: kMainColor),
// onPressed: () {
// Navigator.push(context, MaterialPageRoute(builder: (context)=>const CustomerAllTransactionScreen()));
// },
// ),
),
);
});
}
}
class ContactOptionsRow extends StatefulWidget {
final Party party;
const ContactOptionsRow({super.key, required this.party});
@override
State<ContactOptionsRow> createState() => _ContactOptionsRowState();
}
class _ContactOptionsRowState extends State<ContactOptionsRow> {
int selectedIndex = -1;
void _onButtonTap(int index) async {
setState(() {
selectedIndex = index;
});
if (index == 0) {
// Call functionality
if (widget.party.phone == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(lang.S.of(context).phoneNotAvail)),
);
return;
}
final Uri url = Uri.parse('tel:${widget.party.phone}');
bool t = await launchUrl(url);
if (!t) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(lang.S.of(context).notLaunch)),
);
}
} else if (index == 1) {
// SMS functionality
if (widget.party.type != 'Supplier') {
showDialog(
context: context,
builder: (context1) {
return SmsConfirmationPopup(
customerName: widget.party.name ?? '',
phoneNumber: widget.party.phone ?? '',
onCancel: () {
Navigator.pop(context1);
},
onSendSms: () {
UrlLauncher.handleLaunchURL(context, 'sms:${widget.party.phone}', false);
// EasyLoading.show(status: 'SMS Sending..');
// PartyRepository repo = PartyRepository();
// await repo.sendCustomerUdeSms(id: widget.party.id!, context: context);
},
);
},
);
} else {
if (widget.party.phone == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(lang.S.of(context).phoneNotAvail)),
);
return;
}
UrlLauncher.handleLaunchURL(
context,
'sms:${widget.party.phone}',
false,
);
}
} else if (index == 2) {
// Email functionality
if (widget.party.email == null || !RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(widget.party.email!)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid email address.')),
);
return;
}
UrlLauncher.handleLaunchURL(context, 'mailto:${widget.party.email}', true);
}
}
Widget _buildContactButton(int index, IconData icon, String label) {
final _theme = Theme.of(context);
return Expanded(
child: GestureDetector(
onTap: () => _onButtonTap(index),
child: Container(
padding: const EdgeInsets.all(8),
height: 90,
decoration: BoxDecoration(
color: selectedIndex == index ? kMainColor : kMainColor.withValues(alpha: 0.10),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 20,
color: selectedIndex == index ? kWhite : Colors.black,
),
const SizedBox(height: 8),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
label,
maxLines: 1,
style: _theme.textTheme.bodyMedium?.copyWith(
fontSize: 14,
color: selectedIndex == index ? kWhite : Colors.black,
),
),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildContactButton(0, FeatherIcons.phone, 'Call'),
const SizedBox(width: 18),
_buildContactButton(1, FeatherIcons.messageSquare, 'Message'),
const SizedBox(width: 18),
_buildContactButton(2, FeatherIcons.mail, 'Email'),
],
);
}
}
Widget keyValueWidget({required String title, required String value, required BuildContext context}) {
final _theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
fit: FlexFit.tight,
flex: 3,
child: Text(
'$title ',
style: _theme.textTheme.bodyMedium?.copyWith(
color: DAppColors.kNeutral700,
),
),
),
SizedBox(width: 8),
Flexible(
fit: FlexFit.tight,
flex: 4,
child: Text(
': $value',
style: _theme.textTheme.bodyMedium?.copyWith(
color: kTitleColor,
// fontSize: 15,
),
),
),
],
),
);
}

View File

@@ -0,0 +1,775 @@
// // ignore: import_of_legacy_library_into_null_safe
// // ignore_for_file: unused_result
// import 'dart:io';
//
// import 'package:flutter/material.dart';
// import 'package:flutter_easyloading/flutter_easyloading.dart';
// import 'package:flutter_feather_icons/flutter_feather_icons.dart';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:image_picker/image_picker.dart';
// import 'package:mobile_pos/Const/api_config.dart';
// import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
// import 'package:mobile_pos/constant.dart';
// import 'package:mobile_pos/generated/l10n.dart' as lang;
// import 'package:nb_utils/nb_utils.dart';
//
// import '../../GlobalComponents/glonal_popup.dart';
// import '../../http_client/custome_http_client.dart';
// import '../User Roles/Provider/check_user_role_permission_provider.dart';
// import 'Provider/customer_provider.dart';
// import 'Repo/parties_repo.dart';
//
// // ignore: must_be_immutable
// class EditCustomer extends StatefulWidget {
// EditCustomer({super.key, required this.customerModel});
//
// Party customerModel;
//
// @override
// // ignore: library_private_types_in_public_api
// _EditCustomerState createState() => _EditCustomerState();
// }
//
// class _EditCustomerState extends State<EditCustomer> {
// String groupValue = '';
// bool expanded = false;
// final ImagePicker _picker = ImagePicker();
// bool showProgress = false;
// XFile? pickedImage;
//
// @override
// void initState() {
// phoneController.text = widget.customerModel.phone ?? '';
// nameController.text = widget.customerModel.name ?? '';
// emailController.text = widget.customerModel.email ?? '';
// dueController.text = (widget.customerModel.due ?? 0).toString();
// addressController.text = widget.customerModel.address ?? '';
// groupValue = widget.customerModel.type ?? '';
// super.initState();
// }
//
// final GlobalKey<FormState> _formKay = GlobalKey();
//
// TextEditingController phoneController = TextEditingController();
// TextEditingController nameController = TextEditingController();
// TextEditingController emailController = TextEditingController();
// TextEditingController dueController = TextEditingController();
// TextEditingController addressController = TextEditingController();
//
// final partyCreditLimitController = TextEditingController();
// final partyGstController = TextEditingController();
// final billingAddressController = TextEditingController();
// final billingCityController = TextEditingController();
// final billingStateController = TextEditingController();
// final billingCountryController = TextEditingController();
// final shippingAddressController = TextEditingController();
// final shippingCityController = TextEditingController();
// final shippingStateController = TextEditingController();
// final shippingCountryController = TextEditingController();
// final billingZipCodeCountryController = TextEditingController();
// final shippingZipCodeCountryController = TextEditingController();
// final openingBalanceController = TextEditingController();
//
// FocusNode focusNode = FocusNode();
// String? selectedBillingCountry;
// String? selectedDShippingCountry;
// String? selectedBalanceType;
//
// @override
// Widget build(BuildContext context) {
// final theme = Theme.of(context);
//
// return Consumer(builder: (context, cRef, __) {
// final permissionService = PermissionService(cRef);
// return GlobalPopup(
// child: Scaffold(
// backgroundColor: Colors.white,
// appBar: AppBar(
// backgroundColor: Colors.white,
// title: Text(
// lang.S.of(context).updateContact,
// ),
// centerTitle: true,
// iconTheme: const IconThemeData(color: Colors.black),
// elevation: 0.0,
// ),
// body: Consumer(builder: (context, ref, __) {
// // ignore: unused_local_variable
// final customerData = ref.watch(partiesProvider);
//
// return SingleChildScrollView(
// child: Padding(
// padding: const EdgeInsets.all(16.0),
// child: Column(
// children: [
// Form(
// key: _formKay,
// child: Column(
// children: [
// ///_________Phone_______________________
// TextFormField(
// controller: phoneController,
// validator: (value) {
// if (value == null || value.isEmpty) {
// // return 'Please enter a valid phone number';
// return lang.S.of(context).pleaseEnterAValidPhoneNumber;
// }
// return null;
// },
// decoration: InputDecoration(
// floatingLabelBehavior: FloatingLabelBehavior.always,
// labelText: lang.S.of(context).phone,
// hintText: lang.S.of(context).enterYourPhoneNumber,
// border: const OutlineInputBorder(),
// ),
// ),
// SizedBox(height: 20),
//
// ///_________Name_______________________
// TextFormField(
// controller: nameController,
// validator: (value) {
// if (value == null || value.isEmpty) {
// // return 'Please enter a valid Name';
// return lang.S.of(context).pleaseEnterAValidName;
// }
// // You can add more validation logic as needed
// return null;
// },
// decoration: InputDecoration(
// floatingLabelBehavior: FloatingLabelBehavior.always,
// labelText: lang.S.of(context).name,
// hintText: lang.S.of(context).enterYourName,
// border: const OutlineInputBorder(),
// ),
// ),
// ],
// ),
// ),
// SizedBox(height: 20),
//
// ///_________opening balance_______________________
// // TextFormField(
// // controller: openingBalanceController,
// // keyboardType: TextInputType.name,
// // decoration: InputDecoration(
// // labelText: lang.S.of(context).openingBalance,
// // hintText: lang.S.of(context).enterOpeningBalance,
// // suffixIcon: Padding(
// // padding: const EdgeInsets.all(1.0),
// // child: Container(
// // padding: EdgeInsets.symmetric(horizontal: 10),
// // decoration: BoxDecoration(
// // color: kBackgroundColor,
// // borderRadius: BorderRadius.only(
// // topRight: Radius.circular(4),
// // bottomRight: Radius.circular(4),
// // )),
// // child: DropdownButtonHideUnderline(
// // child: DropdownButton(
// // icon: Icon(
// // Icons.keyboard_arrow_down,
// // color: kPeraColor,
// // ),
// // items: ['Advanced', 'Due'].map((entry) {
// // return DropdownMenuItem(value: entry, child: Text(entry, style: theme.textTheme.bodyLarge?.copyWith(color: kTitleColor)));
// // }).toList(),
// // value: selectedBalanceType ?? 'Advanced',
// // onChanged: (String? value) {
// // setState(() {
// // selectedBalanceType = value;
// // });
// // }),
// // ),
// // ),
// // )),
// // ),
// // SizedBox(height: 20),
// Row(
// children: [
// Expanded(
// child: RadioListTile(
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
// fillColor: WidgetStateProperty.resolveWith(
// (states) {
// if (states.contains(WidgetState.selected)) {
// return kMainColor;
// }
// return kPeraColor;
// },
// ),
// contentPadding: EdgeInsets.zero,
// groupValue: groupValue,
// title: Text(
// lang.S.of(context).retailer,
// maxLines: 1,
// style: theme.textTheme.bodySmall,
// ),
// value: 'Retailer',
// onChanged: (value) {
// if (widget.customerModel.type != 'Supplier') {
// setState(() {
// groupValue = value.toString();
// });
// }
// },
// // Change the color to indicate it's not selectable
// activeColor: widget.customerModel.type == 'Supplier' ? Colors.grey : kMainColor,
// ),
// ),
// Expanded(
// child: RadioListTile(
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
// fillColor: WidgetStateProperty.resolveWith(
// (states) {
// if (states.contains(WidgetState.selected)) {
// return kMainColor;
// }
// return kPeraColor;
// },
// ),
// contentPadding: EdgeInsets.zero,
// groupValue: groupValue,
// title: Text(
// lang.S.of(context).dealer,
// maxLines: 1,
// style: theme.textTheme.bodySmall,
// ),
// value: 'Dealer',
// onChanged: (value) {
// if (widget.customerModel.type != 'Supplier') {
// setState(() {
// groupValue = value.toString();
// });
// }
// },
// activeColor: widget.customerModel.type == 'Supplier' ? Colors.grey : kMainColor,
// ),
// ),
// ],
// ),
// Row(
// children: [
// Expanded(
// child: RadioListTile(
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
// fillColor: WidgetStateProperty.resolveWith(
// (states) {
// if (states.contains(WidgetState.selected)) {
// return kMainColor;
// }
// return kPeraColor;
// },
// ),
// contentPadding: EdgeInsets.zero,
// activeColor: kMainColor,
// groupValue: groupValue,
// title: Text(
// lang.S.of(context).wholesaler,
// maxLines: 1,
// style: theme.textTheme.bodySmall,
// ),
// value: 'Wholesaler',
// onChanged: (value) {
// if (widget.customerModel.type != 'Supplier') {
// setState(() {
// groupValue = value.toString();
// });
// }
// },
// ),
// ),
// Expanded(
// child: RadioListTile(
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
// fillColor: WidgetStateProperty.resolveWith(
// (states) {
// if (states.contains(WidgetState.selected)) {
// return kMainColor;
// }
// return kPeraColor;
// },
// ),
// contentPadding: EdgeInsets.zero,
// activeColor: kMainColor,
// groupValue: groupValue,
// title: Text(
// lang.S.of(context).supplier,
// maxLines: 1,
// style: theme.textTheme.bodySmall,
// ),
// value: 'Supplier',
// onChanged: (value) {
// if (widget.customerModel.type != 'Retailer' && widget.customerModel.type != 'Dealer' && widget.customerModel.type != 'Wholesaler') {
// setState(() {
// groupValue = value.toString();
// });
// }
// },
// ),
// ),
// ],
// ),
// Visibility(
// visible: showProgress,
// child: const CircularProgressIndicator(
// color: kMainColor,
// strokeWidth: 5.0,
// ),
// ),
// ExpansionPanelList(
// expandIconColor: Colors.red,
// expansionCallback: (int index, bool isExpanded) {},
// animationDuration: const Duration(seconds: 1),
// elevation: 0,
// dividerColor: Colors.white,
// children: [
// ExpansionPanel(
// backgroundColor: kWhite,
// headerBuilder: (BuildContext context, bool isExpanded) {
// return Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// TextButton(
// child: Text(
// lang.S.of(context).moreInfo,
// style: theme.textTheme.titleLarge?.copyWith(
// color: kMainColor,
// ),
// ),
// onPressed: () {
// setState(() {
// expanded == false ? expanded = true : expanded = false;
// });
// },
// ),
// ],
// );
// },
// body: Column(
// children: [
// GestureDetector(
// onTap: () {
// showDialog(
// context: context,
// builder: (BuildContext context) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// // ignore: sized_box_for_whitespace
// child: Container(
// height: 200.0,
// width: MediaQuery.of(context).size.width - 80,
// child: Center(
// child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// GestureDetector(
// onTap: () async {
// pickedImage = await _picker.pickImage(source: ImageSource.gallery);
// setState(() {});
// Navigator.pop(context);
// },
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// const Icon(
// Icons.photo_library_rounded,
// size: 60.0,
// color: kMainColor,
// ),
// Text(lang.S.of(context).gallery, style: theme.textTheme.titleLarge?.copyWith(color: kMainColor)),
// ],
// ),
// ),
// const SizedBox(
// width: 40.0,
// ),
// GestureDetector(
// onTap: () async {
// pickedImage = await _picker.pickImage(source: ImageSource.camera);
// setState(() {});
// Navigator.pop(context);
// },
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// const Icon(
// Icons.camera,
// size: 60.0,
// color: kGreyTextColor,
// ),
// Text(
// lang.S.of(context).camera,
// style: theme.textTheme.titleLarge?.copyWith(
// color: kGreyTextColor,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
// ),
// );
// });
// },
// child: Stack(
// children: [
// Container(
// height: 120,
// width: 120,
// decoration: BoxDecoration(
// border: Border.all(color: Colors.black54, width: 1),
// borderRadius: const BorderRadius.all(Radius.circular(120)),
// image: pickedImage == null
// ? widget.customerModel.image.isEmptyOrNull
// ? const DecorationImage(
// image: AssetImage('images/no_shop_image.png'),
// fit: BoxFit.cover,
// )
// : DecorationImage(
// image: NetworkImage('${APIConfig.domain}${widget.customerModel.image!}'),
// fit: BoxFit.cover,
// )
// : DecorationImage(
// image: FileImage(File(pickedImage!.path)),
// fit: BoxFit.cover,
// ),
// ),
// ),
// Positioned(
// bottom: 0,
// right: 0,
// child: Container(
// height: 35,
// width: 35,
// decoration: BoxDecoration(
// border: Border.all(color: Colors.white, width: 2),
// borderRadius: const BorderRadius.all(Radius.circular(120)),
// color: kMainColor,
// ),
// child: const Icon(
// Icons.camera_alt_outlined,
// size: 20,
// color: Colors.white,
// ),
// ),
// )
// ],
// ),
// ),
// const SizedBox(height: 20),
// TextFormField(
// controller: emailController,
// decoration: InputDecoration(
// labelText: lang.S.of(context).email,
// hintText: lang.S.of(context).hintEmail,
// ),
// ),
// SizedBox(height: 20),
// TextFormField(
// controller: addressController,
// decoration: InputDecoration(
// labelText: lang.S.of(context).address,
// hintText: lang.S.of(context).enterFullAddress,
// ),
// ),
// SizedBox(height: 20),
// TextFormField(
// readOnly: true,
// controller: dueController,
// decoration: InputDecoration(
// border: const OutlineInputBorder(),
// floatingLabelBehavior: FloatingLabelBehavior.always,
// labelText: lang.S.of(context).previousDue,
// ),
// ),
// // TextFormField(
// // readOnly: true,
// // controller: dueController,
// // decoration: InputDecoration(
// // border: const OutlineInputBorder(),
// // floatingLabelBehavior:
// // FloatingLabelBehavior.always,
// // labelText: lang.S.of(context).previousDue,
// // ),
// // ),
// // Row(
// // children: [
// // Expanded(
// // child: TextFormField(
// // controller: partyCreditLimitController,
// // decoration: InputDecoration(
// // border: const OutlineInputBorder(),
// // floatingLabelBehavior: FloatingLabelBehavior.always,
// // labelText: 'Party Credit Limit',
// // //hintText: 'Enter your address'
// // hintText: 'Ex: 800'),
// // ),
// // ),
// // SizedBox(width: 20),
// // Expanded(
// // child: TextFormField(
// // controller: partyGstController,
// // decoration: InputDecoration(
// // border: const OutlineInputBorder(),
// // floatingLabelBehavior: FloatingLabelBehavior.always,
// // labelText: 'Party Gst',
// // //hintText: 'Enter your address'
// // hintText: 'Ex: 800'),
// // ),
// // ),
// // ],
// // ),
// // SizedBox(height: 4),
// // Theme(
// // data: Theme.of(context).copyWith(
// // dividerColor: Colors.transparent,
// // ),
// // child: ExpansionTile(
// // visualDensity: VisualDensity(vertical: -2, horizontal: -4),
// // tilePadding: EdgeInsets.zero,
// // trailing: SizedBox.shrink(),
// // title: Row(
// // crossAxisAlignment: CrossAxisAlignment.center,
// // children: [
// // Icon(FeatherIcons.minus, size: 20, color: Colors.red),
// // SizedBox(width: 8),
// // Text(
// // 'Billing Address',
// // style: theme.textTheme.titleMedium?.copyWith(
// // color: kMainColor,
// // ),
// // )
// // ],
// // ),
// // children: [
// // SizedBox(height: 10),
// // //___________Billing Address________________
// // TextFormField(
// // controller: billingAddressController,
// // decoration: InputDecoration(
// // labelText: 'Address',
// // hintText: 'Enter Address',
// // ),
// // ),
// // SizedBox(height: 20),
// // //--------------billing city------------------------
// // TextFormField(
// // controller: billingCityController,
// // decoration: InputDecoration(
// // labelText: 'City',
// // hintText: 'Enter city',
// // ),
// // ),
// // SizedBox(height: 20),
// // //--------------billing state------------------------
// // TextFormField(
// // controller: billingStateController,
// // decoration: InputDecoration(
// // labelText: 'State',
// // hintText: 'Enter state',
// // ),
// // ),
// // SizedBox(height: 20),
// // Row(
// // children: [
// // //--------------billing zip code------------------------
// // Expanded(
// // child: TextFormField(
// // controller: billingZipCodeCountryController,
// // decoration: InputDecoration(
// // labelText: 'Zip Code',
// // hintText: 'Enter zip code',
// // ),
// // ),
// // ),
// // SizedBox(width: 20),
// // //--------------billing country------------------------
// // Expanded(
// // child: DropdownButtonFormField(
// // isExpanded: true,
// // hint: Text(
// // 'Select Country',
// // maxLines: 1,
// // style: theme.textTheme.bodyMedium?.copyWith(
// // color: kPeraColor,
// // ),
// // overflow: TextOverflow.ellipsis,
// // ),
// // icon: Icon(Icons.keyboard_arrow_down, color: kPeraColor),
// // items: ['Bangladesh', 'Pakisthan', 'Iran'].map((entry) {
// // return DropdownMenuItem(
// // value: entry,
// // child: Text(
// // entry,
// // style: theme.textTheme.bodyMedium?.copyWith(color: kPeraColor),
// // ),
// // );
// // }).toList(),
// // value: selectedDShippingCountry,
// // onChanged: (String? value) {
// // setState(() {
// // selectedBillingCountry = value;
// // });
// // }),
// // ),
// // ],
// // ),
// // ],
// // ),
// // ),
// // Theme(
// // data: Theme.of(context).copyWith(
// // dividerColor: Colors.transparent,
// // ),
// // child: ExpansionTile(
// // tilePadding: EdgeInsets.zero,
// // visualDensity: VisualDensity(horizontal: -4, vertical: -2),
// // trailing: SizedBox.shrink(),
// // title: Row(
// // crossAxisAlignment: CrossAxisAlignment.center,
// // children: [
// // Icon(FeatherIcons.plus, size: 20),
// // SizedBox(width: 8),
// // Text(
// // 'Shipping Address',
// // style: theme.textTheme.titleMedium,
// // )
// // ],
// // ),
// // children: [
// // SizedBox(height: 10),
// // //___________Billing Address________________
// // TextFormField(
// // controller: billingAddressController,
// // decoration: InputDecoration(
// // labelText: 'Address',
// // hintText: 'Enter Address',
// // ),
// // ),
// // SizedBox(height: 20),
// // //--------------billing city------------------------
// // TextFormField(
// // controller: billingCityController,
// // decoration: InputDecoration(
// // labelText: 'City',
// // hintText: 'Enter city',
// // ),
// // ),
// // SizedBox(height: 20),
// // //--------------billing state------------------------
// // TextFormField(
// // controller: billingStateController,
// // decoration: InputDecoration(
// // labelText: 'State',
// // hintText: 'Enter state',
// // ),
// // ),
// // SizedBox(height: 20),
// // Row(
// // children: [
// // //--------------billing zip code------------------------
// // Expanded(
// // child: TextFormField(
// // controller: billingZipCodeCountryController,
// // decoration: InputDecoration(
// // labelText: 'Zip Code',
// // hintText: 'Enter zip code',
// // ),
// // ),
// // ),
// // SizedBox(width: 20),
// // //--------------billing country------------------------
// // Expanded(
// // child: DropdownButtonFormField(
// // isExpanded: true,
// // hint: Text(
// // 'Select Country',
// // maxLines: 1,
// // style: theme.textTheme.bodyMedium?.copyWith(
// // color: kPeraColor,
// // ),
// // overflow: TextOverflow.ellipsis,
// // ),
// // icon: Icon(Icons.keyboard_arrow_down, color: kPeraColor),
// // items: ['Bangladesh', 'Pakisthan', 'Iran'].map((entry) {
// // return DropdownMenuItem(
// // value: entry,
// // child: Text(
// // entry,
// // style: theme.textTheme.bodyMedium?.copyWith(color: kPeraColor),
// // ),
// // );
// // }).toList(),
// // value: selectedDShippingCountry,
// // onChanged: (String? value) {
// // setState(() {
// // selectedBillingCountry = value;
// // });
// // }),
// // ),
// // ],
// // ),
// // ],
// // ),
// // )
// ],
// ),
// isExpanded: expanded,
// ),
// ],
// ),
// SizedBox(height: 20),
// ElevatedButton(
// onPressed: () async {
// if (!permissionService.hasPermission(Permit.partiesCreate.value)) {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// backgroundColor: Colors.red,
// content: Text('You do not have permission to update Party.'),
// ),
// );
// return;
// }
// if (_formKay.currentState!.validate()) {
// try {
// EasyLoading.show(
// status: lang.S.of(context).updating,
// // 'Updating...'
// );
// final party = PartyRepository();
// await party.updateParty(
// id: widget.customerModel.id.toString(),
// // Assuming id is a property in customerModel
// ref: ref,
// context: context,
// name: nameController.text,
// phone: phoneController.text,
// type: groupValue,
// image: pickedImage != null ? File(pickedImage!.path) : null,
// email: emailController.text,
// address: addressController.text,
// due: dueController.text,
// );
// EasyLoading.dismiss();
// } catch (e) {
// EasyLoading.dismiss();
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
// }
// }
// },
// child: Text(lang.S.of(context).update)),
// ],
// ),
// ),
// );
// }),
// ),
// );
// });
// }
// }

View File

@@ -0,0 +1,587 @@
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconly/iconly.dart';
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart';
import 'package:mobile_pos/Screens/Customers/Provider/customer_provider.dart';
import 'package:mobile_pos/Screens/Customers/add_customer.dart';
import 'package:mobile_pos/Screens/Customers/customer_details.dart';
import 'package:mobile_pos/Screens/Sales/add_sales.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:mobile_pos/widgets/empty_widget/_empty_widget.dart';
import 'package:nb_utils/nb_utils.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../Provider/profile_provider.dart';
import '../../currency.dart';
import '../../service/check_actions_when_no_branch.dart';
import '../../service/check_user_role_permission_provider.dart';
import 'Repo/parties_repo.dart';
// 1. Combine the screens into a single class with a parameter for mode
class PartyListScreen extends StatefulWidget {
// Use a boolean to determine the screen's purpose
final bool isSelectionMode;
const PartyListScreen({super.key, this.isSelectionMode = false});
@override
State<PartyListScreen> createState() => _PartyListScreenState();
}
class _PartyListScreenState extends State<PartyListScreen> {
late Color color;
bool _isRefreshing = false;
bool _isSearching = false;
final TextEditingController _searchController = TextEditingController();
Future<void> refreshData(WidgetRef ref) async {
if (_isRefreshing) return;
_isRefreshing = true;
ref.refresh(partiesProvider);
await Future.delayed(const Duration(seconds: 1));
_isRefreshing = false;
}
String? partyType;
// Define party types based on the mode
List<String> get availablePartyTypes {
if (widget.isSelectionMode) {
// For Sales/Selection mode, exclude 'Supplier'
return [
PartyType.customer,
PartyType.dealer,
PartyType.wholesaler,
];
} else {
// For General List/Management mode, include all
return [
PartyType.customer,
PartyType.supplier,
PartyType.dealer,
PartyType.wholesaler,
];
}
}
Future<void> showDeleteConfirmationAlert({
required BuildContext context,
required String id,
required WidgetRef ref,
}) async {
return showDialog(
context: context,
builder: (BuildContext context1) {
return AlertDialog(
title: Text(
lang.S.of(context).confirmPassword,
//'Confirm Delete'
),
content: Text(
lang.S.of(context).areYouSureYouWant,
//'Are you sure you want to delete this party?'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
lang.S.of(context).cancel,
//'Cancel'
),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
final party = PartyRepository();
await party.deleteParty(id: id, context: context, ref: ref);
},
child: Text(lang.S.of(context).delete,
// 'Delete',
style: const TextStyle(color: Colors.red)),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return Consumer(
builder: (context, ref, __) {
final providerData = ref.watch(partiesProvider);
final businessInfo = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
// Determine App Bar Title based on mode
final appBarTitle = widget.isSelectionMode
? lang.S.of(context).chooseCustomer // Sales title
: lang.S.of(context).partyList; // Management title
return businessInfo.when(data: (details) {
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
resizeToAvoidBottomInset: true,
appBar: AppBar(
backgroundColor: Colors.white,
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
elevation: 0.0,
actionsPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(
appBarTitle,
style: _theme.textTheme.titleMedium?.copyWith(color: Colors.black),
),
),
body: RefreshIndicator.adaptive(
onRefresh: () => refreshData(ref),
child: providerData.when(data: (partyList) {
// Permission check only required for the management view
if (!widget.isSelectionMode && !permissionService.hasPermission(Permit.partiesRead.value)) {
return const Center(child: PermitDenyWidget());
}
final filteredParties = partyList.where((c) {
final normalizedType = (c.type ?? '').toLowerCase();
// Filter out suppliers ONLY if in selection mode
if (widget.isSelectionMode && normalizedType == 'supplier') {
return false;
}
final nameMatches = !_isSearching || _searchController.text.isEmpty
? true
: (c.name ?? '').toLowerCase().contains(_searchController.text.toLowerCase());
final effectiveType = normalizedType == 'retailer' ? 'customer' : normalizedType;
final typeMatches = partyType == null || partyType!.isEmpty ? true : effectiveType == partyType;
return nameMatches && typeMatches;
}).toList();
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4),
child: TextFormField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: lang.S.of(context).search,
border: InputBorder.none,
hintStyle: TextStyle(color: Colors.grey[600]),
suffixIcon: Padding(
padding: const EdgeInsets.all(1.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: const Color(0xffF7F7F7),
borderRadius: const BorderRadius.only(
topRight: Radius.circular(8),
bottomRight: Radius.circular(8),
)),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
hint: Text(lang.S.of(context).selectType),
icon: partyType != null
? IconButton(
icon: Icon(
Icons.clear,
color: kMainColor,
size: 18,
),
onPressed: () {
setState(() {
partyType = null;
});
},
)
: const Icon(Icons.keyboard_arrow_down, color: kPeraColor),
value: partyType,
onChanged: (String? value) {
setState(() {
partyType = value;
});
},
// Use the list defined by the mode
items: availablePartyTypes.map((entry) {
final valueToStore = entry.toLowerCase();
return DropdownMenuItem<String>(
value: valueToStore,
child: Text(
getPartyTypeLabel(context, valueToStore),
style: _theme.textTheme.bodyLarge?.copyWith(color: kTitleColor),
),
);
}).toList(),
),
),
),
],
),
),
),
style: const TextStyle(color: Colors.black),
onChanged: (value) {
setState(() {
_isSearching = value.isNotEmpty;
});
},
),
),
// 3. Show Walk-In Customer ONLY in selection mode
if (widget.isSelectionMode)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
onTap: () {
AddSalesScreen(customerModel: null).launch(context);
ref.refresh(cartNotifier);
},
leading: SizedBox(
height: 40.0,
width: 40.0,
child: CircleAvatar(
backgroundColor: Colors.white,
child: ClipOval(
child: Image.asset(
'images/no_shop_image.png',
fit: BoxFit.cover,
width: 120.0,
height: 120.0,
),
),
),
),
title: Text(
lang.S.of(context).walkInCustomer,
style: _theme.textTheme.bodyMedium?.copyWith(
color: kTitleColor,
fontSize: 16.0,
),
),
subtitle: Text(
lang.S.of(context).guest,
style: _theme.textTheme.bodyLarge,
),
trailing: const Icon(
Icons.arrow_forward_ios_rounded,
size: 18,
color: Color(0xff4B5563),
),
),
filteredParties.isNotEmpty
? Expanded(
child: ListView.builder(
itemCount: filteredParties.length,
shrinkWrap: true,
physics:
const AlwaysScrollableScrollPhysics(), // Use AlwaysScrollableScrollPhysics for the main list
padding: const EdgeInsets.symmetric(horizontal: 16),
itemBuilder: (_, index) {
final item = filteredParties[index];
final normalizedType = (item.type ?? '').toLowerCase();
// Color logic (unchanged)
color = Colors.white;
if (normalizedType == 'retailer' || normalizedType == 'customer') {
color = const Color(0xFF56da87);
}
if (normalizedType == 'wholesaler') color = const Color(0xFF25a9e0);
if (normalizedType == 'dealer') color = const Color(0xFFff5f00);
if (normalizedType == 'supplier') color = const Color(0xFFA569BD);
// final effectiveDisplayType = normalizedType == 'retailer'
// ? 'Customer'
// : normalizedType == 'wholesaler'
// ? lang.S.of(context).wholesaler
// : normalizedType == 'dealer'
// ? lang.S.of(context).dealer
// : normalizedType == 'supplier'
// ? lang.S.of(context).supplier
// : item.type ?? '';
String effectiveDisplayType;
if (normalizedType == 'retailer') {
effectiveDisplayType = lang.S.of(context).customer;
} else if (normalizedType == 'wholesaler') {
effectiveDisplayType = lang.S.of(context).wholesaler;
} else if (normalizedType == 'dealer') {
effectiveDisplayType = lang.S.of(context).dealer;
} else if (normalizedType == 'supplier') {
effectiveDisplayType = lang.S.of(context).supplier;
} else {
effectiveDisplayType = item.type ?? '';
}
// Due/Advance/No Due Logic (from previous step)
String statusText;
Color statusColor;
num? statusAmount;
if (item.due != null && item.due! > 0) {
statusText = lang.S.of(context).due;
statusColor = const Color(0xFFff5f00);
statusAmount = item.due;
} else if (item.openingBalanceType?.toLowerCase() == 'advance' &&
item.wallet != null &&
item.wallet! > 0) {
statusText = lang.S.of(context).advance;
statusColor = DAppColors.kSecondary;
statusAmount = item.wallet;
} else {
statusText = lang.S.of(context).noDue;
statusColor = DAppColors.kSecondary;
statusAmount = null;
}
return ListTile(
visualDensity: const VisualDensity(vertical: -2),
contentPadding: EdgeInsets.zero,
onTap: () {
// 4. OnTap action based on mode
if (widget.isSelectionMode) {
// Selection Mode: Go to AddSalesScreen
AddSalesScreen(customerModel: item).launch(context);
ref.refresh(cartNotifier);
} else {
// Management Mode: Go to CustomerDetails
CustomerDetails(party: item).launch(context);
}
},
leading: item.image != null
? Container(
height: 40,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: DAppColors.kBorder, width: 0.3),
image: DecorationImage(
image: NetworkImage('${APIConfig.domain}${item.image ?? ''}'),
fit: BoxFit.cover,
),
),
)
: CircleAvatarWidget(name: item.name),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
item.name ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: kTitleColor,
fontSize: 16.0,
),
),
),
const SizedBox(width: 4),
Text(
statusAmount != null ? '$currency${statusAmount.toStringAsFixed(2)}' : '',
style: _theme.textTheme.bodyMedium?.copyWith(fontSize: 16.0),
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
effectiveDisplayType,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: color,
fontSize: 14.0,
),
),
),
const SizedBox(width: 4),
Text(
statusText,
style: _theme.textTheme.bodyMedium?.copyWith(
color: statusColor,
fontSize: 14.0,
),
),
],
),
trailing: PopupMenuButton(
offset: const Offset(0, 30),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0),
),
padding: EdgeInsets.zero,
itemBuilder: (BuildContext bc) => [
PopupMenuItem(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CustomerDetails(party: item),
),
),
child: Row(
children: [
Icon(
Icons.remove_red_eye,
color: kGreyTextColor,
size: 20,
),
SizedBox(width: 8.0),
Text(
lang.S.of(context).view,
style: TextStyle(color: kGreyTextColor),
),
],
),
),
PopupMenuItem(
onTap: () async {
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
if (!permissionService.hasPermission(Permit.partiesUpdate.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).updatePartyWarn),
),
);
return;
}
if (result) {
AddParty(customerModel: item).launch(context);
}
},
child: Row(
children: [
Icon(
IconlyBold.edit,
color: kGreyTextColor,
size: 20,
),
SizedBox(width: 8.0),
Text(
lang.S.of(context).edit,
style: TextStyle(color: kGreyTextColor),
),
],
),
),
PopupMenuItem(
onTap: () async {
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
if (!permissionService.hasPermission(Permit.partiesDelete.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).deletePartyWarn),
),
);
return;
}
if (result) {
await showDeleteConfirmationAlert(
context: context, id: item.id.toString(), ref: ref);
}
},
child: Row(
children: [
Icon(
IconlyBold.delete,
color: kGreyTextColor,
size: 20,
),
SizedBox(width: 8.0),
Text(
lang.S.of(context).delete,
style: TextStyle(color: kGreyTextColor),
),
],
),
),
],
onSelected: (value) {
Navigator.pushNamed(context, '$value');
},
child: const Icon(
FeatherIcons.moreVertical,
color: kGreyTextColor,
),
),
);
},
),
)
: Center(
child: EmptyWidget(
message: TextSpan(text: lang.S.of(context).noParty),
),
),
],
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
}),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: ElevatedButton.icon(
style: OutlinedButton.styleFrom(
maximumSize: const Size(double.infinity, 48),
minimumSize: const Size(double.infinity, 48),
disabledBackgroundColor: _theme.colorScheme.primary.withAlpha(15),
disabledForegroundColor: const Color(0xff567DF4).withOpacity(0.05),
),
onPressed: () async {
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
// Check logic based on business info (kept original logic)
if (result) {
if (details.data?.subscriptionDate != null && details.data?.enrolledPlan != null) {
Navigator.push(context, MaterialPageRoute(builder: (context) => const AddParty()));
} else if (!widget.isSelectionMode) {
// Allow navigation if not in selection mode and subscription check fails (or fix subscription check)
Navigator.push(context, MaterialPageRoute(builder: (context) => const AddParty()));
}
}
},
icon: const Icon(Icons.add, color: Colors.white),
iconAlignment: IconAlignment.start,
label: Text(
lang.S.of(context).addCustomer,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _theme.colorScheme.primaryContainer,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
});
},
);
}
}

View File

@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
class SmsConfirmationPopup extends StatefulWidget {
final String customerName;
final String phoneNumber;
final Function onSendSms;
final VoidCallback onCancel;
const SmsConfirmationPopup({
super.key,
required this.customerName,
required this.phoneNumber,
required this.onSendSms,
required this.onCancel,
});
@override
_SmsConfirmationPopupState createState() => _SmsConfirmationPopupState();
}
class _SmsConfirmationPopupState extends State<SmsConfirmationPopup> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 250),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
final scale = _animationController.value;
return Transform.scale(
scale: scale,
child: child,
);
},
child: Dialog(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
// 'Confirm SMS to ${widget.customerName}',
'${lang.S.of(context).confirmSMSTo} ${widget.customerName}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
Text(
//'An SMS will be sent to the following number: ${widget.phoneNumber}',
'${lang.S.of(context).anSMSWillBeSentToTheFollowingNumber} ${widget.phoneNumber}',
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.center,
),
const SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: widget.onCancel,
child: Text(
lang.S.of(context).cancel,
//'Cancel'
),
),
),
SizedBox(width: 15),
Flexible(
child: ElevatedButton(
style: const ButtonStyle(backgroundColor: MaterialStatePropertyAll(kMainColor)),
onPressed: () {
widget.onSendSms();
Navigator.pop(context);
},
child: Text(
lang.S.of(context).sendSMS,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// 'Send SMS',
style: const TextStyle(color: Colors.white),
),
),
),
],
),
],
),
),
),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_animationController.forward();
}
}

View File

@@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../currency.dart';
class CustomerAllTransactionScreen extends StatefulWidget {
const CustomerAllTransactionScreen({Key? key}) : super(key: key);
@override
State<CustomerAllTransactionScreen> createState() => _CustomerAllTransactionScreenState();
}
class _CustomerAllTransactionScreenState extends State<CustomerAllTransactionScreen> {
int currentIndex = 0;
bool isSearch = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
elevation: 2.0,
surfaceTintColor: kWhite,
automaticallyImplyLeading: isSearch ? false : true,
backgroundColor: kWhite,
title: isSearch
? TextFormField(
decoration: kInputDecoration.copyWith(
contentPadding: const EdgeInsets.only(left: 12, right: 5),
//hintText: 'Search Here.....',
hintText: lang.S.of(context).searchH,
),
)
: Text(
lang.S.of(context).transactions,
// 'Transactions'
),
actions: [
GestureDetector(
onTap: () {
setState(() {
isSearch = true;
});
},
child: const Padding(
padding: EdgeInsets.all(15.0),
child: Icon(
FeatherIcons.search,
color: kGreyTextColor,
),
),
)
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// SalesInvoiceDetails(
// businessInfo: personalData.value!,
// saleTransaction: transaction[index],
// ).launch(context);
},
child: Column(
children: [
Container(
// padding: const EdgeInsets.all(20),
width: context.width(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
lang.S.of(context).sale,
//"Sale",
style: const TextStyle(fontSize: 16),
),
const Text('#2145'),
],
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
// padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: const Color(0xff0dbf7d).withOpacity(0.1), borderRadius: const BorderRadius.all(Radius.circular(10))),
child: Text(
lang.S.of(context).paid,
style: const TextStyle(color: Color(0xff0dbf7d)),
),
),
const Text(
'30/08/2021',
style: TextStyle(color: Colors.grey),
),
],
),
const SizedBox(height: 10),
Text(
'${lang.S.of(context).total} : $currency 20000',
style: const TextStyle(color: Colors.grey),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${lang.S.of(context).due}: $currency 3000',
style: const TextStyle(fontSize: 16),
),
Row(
children: [
IconButton(
onPressed: () {},
icon: const Icon(
FeatherIcons.printer,
color: Colors.grey,
)),
IconButton(
onPressed: () {},
icon: const Icon(
Icons.picture_as_pdf,
color: Colors.grey,
)),
// IconButton(
// onPressed: () {},
// icon: const Icon(
// FeatherIcons.share,
// color: Colors.grey,
// ),
// ),
// IconButton(
// onPressed: () {},
// icon: const Icon(
// FeatherIcons.moreVertical,
// color: Colors.grey,
// )),
],
)
],
)
],
),
),
Container(
height: 0.5,
width: context.width(),
color: Colors.grey,
)
],
),
);
},
)
],
),
),
),
);
}
}

View File

@@ -0,0 +1,7 @@
class ChartData {
ChartData(this.x, this.y, this.y1);
final String x;
final double y;
final double y1;
}

View File

@@ -0,0 +1,484 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Screens/DashBoard/global_container.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/currency.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import '../../Provider/profile_provider.dart';
import '../../http_client/custome_http_client.dart';
import '../../widgets/build_date_selector/build_date_selector.dart';
import '../../widgets/empty_widget/_empty_widget.dart';
import '../../service/check_user_role_permission_provider.dart';
import 'numeric_axis.dart';
class DashboardScreen extends ConsumerStatefulWidget {
const DashboardScreen({super.key});
@override
ConsumerState<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final Map<String, String> dateOptions = {
'today': lang.S.current.today,
'yesterday': lang.S.current.yesterday,
'last_seven_days': lang.S.current.last7Days,
'last_thirty_days': lang.S.current.last30Days,
'current_month': lang.S.current.currentMonth,
'last_month': lang.S.current.lastMonth,
'current_year': lang.S.current.currentYear,
'custom_date': lang.S.current.customDate,
};
String selectedTime = 'today';
bool _isRefreshing = false; // Prevents multiple refresh calls
Future<void> refreshData(WidgetRef ref) async {
if (_isRefreshing) return; // Prevent duplicate refresh calls
_isRefreshing = true;
ref.refresh(dashboardInfoProvider(selectedTime.toLowerCase()));
await Future.delayed(const Duration(seconds: 1)); // Optional delay
_isRefreshing = false;
}
bool _showCustomDatePickers = false; // Track if custom date pickers should be shown
DateTime? fromDate;
DateTime? toDate;
String _getDateRangeString() {
if (selectedTime != 'custom_date') {
return selectedTime.toLowerCase();
} else if (fromDate != null && toDate != null) {
final formattedFrom = DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!);
final formattedTo = DateFormat('yyyy-MM-dd', 'en_US').format(toDate!);
return 'custom_date&from_date=$formattedFrom&to_date=$formattedTo';
} else {
return 'custom_date'; // fallback
}
}
Future<void> _selectedFormDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: DateTime(2021),
lastDate: DateTime.now(),
);
if (picked != null && picked != fromDate) {
setState(() {
fromDate = picked;
});
if (toDate != null) refreshData(ref);
}
}
Future<void> _selectToDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
firstDate: fromDate ?? DateTime(2021),
lastDate: DateTime.now(),
);
if (picked != null && picked != toDate) {
setState(() {
toDate = picked;
});
if (fromDate != null) refreshData(ref);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer(builder: (_, ref, watch) {
final dateRangeString = _getDateRangeString();
final dashboardInfo = ref.watch(dashboardInfoProvider(dateRangeString));
final permissionService = PermissionService(ref);
return dashboardInfo.when(data: (dashboard) {
final totalSales = dashboard.data!.sales!.fold<double>(
0,
(sum, item) => sum + (item.amount ?? 0),
);
final totalPurchase = dashboard.data!.purchases!.fold<double>(
0,
(sum, items) => sum + (items.amount ?? 0),
);
return Scaffold(
backgroundColor: kBackgroundColor,
appBar: AppBar(
backgroundColor: kWhite,
surfaceTintColor: kWhite,
title: Text(lang.S.of(context).dashboard),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: SizedBox(
width: 120,
height: 32,
child: DropdownButtonFormField2<String>(
isExpanded: true,
iconStyleData: IconStyleData(
icon: Icon(Icons.keyboard_arrow_down, color: kPeraColor, size: 20),
),
value: selectedTime,
items: dateOptions.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(
entry.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
color: kPeraColor,
fontWeight: FontWeight.w500,
),
),
);
}).toList(),
onChanged: (value) {
setState(() {
selectedTime = value!;
_showCustomDatePickers = selectedTime == 'custom_date';
if (_showCustomDatePickers) {
fromDate = DateTime.now().subtract(const Duration(days: 7));
toDate = DateTime.now();
}
if (selectedTime != 'custom_date') {
refreshData(ref);
}
});
},
dropdownStyleData: DropdownStyleData(
maxHeight: 500,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
scrollbarTheme: ScrollbarThemeData(
radius: const Radius.circular(40),
thickness: WidgetStateProperty.all<double>(6),
thumbVisibility: WidgetStateProperty.all<bool>(true),
),
),
menuItemStyleData: const MenuItemStyleData(padding: EdgeInsets.symmetric(horizontal: 6)),
),
),
)
],
bottom: _showCustomDatePickers
? PreferredSize(
preferredSize: const Size.fromHeight(50),
child: Column(
children: [
Divider(thickness: 1, color: kBottomBorder, height: 1),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => _selectedFormDate(context),
child: buildDateSelector(
prefix: 'From',
date:
fromDate != null ? DateFormat('dd MMMM yyyy').format(fromDate!) : 'Select Date',
theme: theme,
),
),
SizedBox(width: 5),
RotatedBox(
quarterTurns: 1,
child: Container(
height: 1,
width: 22,
color: kSubPeraColor,
),
),
SizedBox(width: 5),
GestureDetector(
onTap: () => _selectToDate(context),
child: buildDateSelector(
prefix: 'To',
date: toDate != null ? DateFormat('dd MMMM yyyy').format(toDate!) : 'Select Date',
theme: theme,
),
),
],
),
),
)
],
),
)
: null,
),
body: RefreshIndicator(
onRefresh: () => refreshData(ref),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (permissionService.hasPermission(Permit.dashboardRead.value)) ...{
Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 12),
decoration: BoxDecoration(
color: kMainColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lang.S.of(context).quickOver,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
color: kWhite,
),
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Flexible(
child: GlobalContainer(
minVerticalPadding: 0,
minTileHeight: 0,
titlePadding: EdgeInsets.zero,
// isShadow: true,
textColor: true,
title: lang.S.of(context).sales,
subtitle: '$currency${formatAmount(totalSales.toString())}',
),
),
Flexible(
child: GlobalContainer(
alainRight: true,
minVerticalPadding: 0,
minTileHeight: 0,
// isShadow: true,
textColor: true,
titlePadding: EdgeInsets.zero,
title: lang.S.of(context).purchased,
subtitle: '$currency${formatAmount(totalPurchase.toString())}',
),
),
],
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Flexible(
child: GlobalContainer(
minVerticalPadding: 0,
textColor: true,
minTileHeight: 0,
titlePadding: EdgeInsets.zero,
title: lang.S.of(context).income,
subtitle: '$currency${formatAmount(dashboard.data?.totalIncome.toString() ?? '0')}',
),
),
Flexible(
child: GlobalContainer(
alainRight: true,
minVerticalPadding: 0,
minTileHeight: 0,
textColor: true,
titlePadding: EdgeInsets.zero,
title: lang.S.of(context).expense,
subtitle:
'$currency${formatAmount(dashboard.data?.totalExpense.toString() ?? '0')}',
),
),
],
),
],
),
),
SizedBox(height: 16),
///---------------chart----------------------
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: kWhite),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lang.S.of(context).tranSacOver,
//'Sales & Purchase Overview',
style: theme.textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.bold, fontSize: 18, color: kTitleColor),
),
const SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.circle,
color: Colors.green,
size: 18,
),
const SizedBox(
width: 5,
),
RichText(
text: TextSpan(
text: '${lang.S.of(context).sales}: ',
//'Sales',
style: theme.textTheme.bodyMedium,
children: [
TextSpan(
text: '$currency${formatAmount(totalSales.toString())}',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.w600, color: kTitleColor)),
])),
const SizedBox(
width: 20,
),
const Icon(
Icons.circle,
color: kMainColor,
size: 18,
),
const SizedBox(
width: 5,
),
RichText(
text: TextSpan(
text: '${lang.S.of(context).purchase}: ',
//'Purchase',
style: theme.textTheme.bodyMedium,
children: [
TextSpan(
text: '$currency${formatAmount(totalPurchase.toString())}',
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(fontWeight: FontWeight.w600, color: kTitleColor)),
])),
],
),
const SizedBox(
height: 10,
),
SizedBox(
height: 250,
width: double.infinity,
child: DashboardChart(
model: dashboard,
)),
],
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: GlobalContainer(
title: lang.S.of(context).totalDue,
image: 'assets/duelist.svg',
subtitle: '$currency ${formatAmount(dashboard.data!.totalDue.toString())}')),
const SizedBox(
width: 12,
),
Expanded(
child: GlobalContainer(
title: lang.S.of(context).stockValue,
image: 'assets/h_stock.svg',
subtitle: "$currency${formatAmount(dashboard.data!.stockValue.toString())}"))
],
),
const SizedBox(height: 19),
///_________Items_Category________________________
Row(
children: [
Expanded(
child: GlobalContainer(
title: lang.S.of(context).item,
image: 'assets/totalItem.svg',
subtitle: formatAmount('${dashboard.data?.totalItems!.round().toString()}'))),
const SizedBox(
width: 12,
),
Expanded(
child: GlobalContainer(
title: lang.S.of(context).categories,
image: 'assets/purchaseLisst.svg',
subtitle: formatAmount('${dashboard.data?.totalCategories?.round().toString()}')))
],
),
const SizedBox(height: 21),
Text(
lang.S.of(context).profitLoss,
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, fontSize: 18),
),
///__________Total_Lass_and_Total_profit_____________________________________
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: GlobalContainer(
title: lang.S.of(context).profit,
image: 'assets/h_lossProfit.svg',
subtitle: '$currency ${formatAmount(dashboard.data!.totalProfit.toString())}')),
const SizedBox(width: 12),
Expanded(
child: GlobalContainer(
title: lang.S.of(context).loss,
image: 'assets/expense.svg',
subtitle: '$currency ${formatAmount(dashboard.data!.totalLoss!.abs().toString())}'))
],
),
} else
Center(child: PermitDenyWidget()),
],
),
),
),
),
);
}, error: (e, stack) {
print('--------------print-------${e.toString()}-----------------');
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
//'{No data found} $e',
'${lang.S.of(context).noDataFound} $e',
style: const TextStyle(color: kGreyTextColor, fontSize: 16, fontWeight: FontWeight.w500),
),
],
),
),
);
}, loading: () {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
});
});
}
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../constant.dart';
class GlobalContainer extends StatelessWidget {
final String title;
final String? image;
final String subtitle;
final double? minVerticalPadding;
final double? minTileHeight;
final EdgeInsets? titlePadding;
final bool? textColor;
final bool? alainRight;
const GlobalContainer({
super.key,
required this.title,
this.image,
required this.subtitle,
this.minVerticalPadding,
this.minTileHeight,
this.titlePadding,
this.textColor,
this.alainRight,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: textColor == true ? Colors.transparent : Colors.white),
child: ListTile(
minVerticalPadding: minVerticalPadding ?? 4,
minTileHeight: minTileHeight ?? 0,
contentPadding: titlePadding ?? EdgeInsets.symmetric(horizontal: 10, vertical: 2),
visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
leading: image != null
? SvgPicture.asset(
image!,
height: 40,
width: 40,
)
: null,
title: Text(
title,
textAlign: (alainRight ?? false) ? TextAlign.end : null,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: textColor == true ? Colors.white : Colors.black),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
subtitle,
textAlign: (alainRight ?? false) ? TextAlign.end : null,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: textColor == true ? Colors.white : Colors.black),
),
),
);
}
}

View File

@@ -0,0 +1,268 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/model/dashboard_overview_model.dart';
import 'chart_data.dart';
class DashboardChart extends StatefulWidget {
const DashboardChart({Key? key, required this.model}) : super(key: key);
final DashboardOverviewModel model;
@override
State<DashboardChart> createState() => _DashboardChartState();
}
class _DashboardChartState extends State<DashboardChart> {
List<ChartData> chartData = [];
@override
void initState() {
super.initState();
getData(widget.model);
}
void getData(DashboardOverviewModel model) {
chartData = [];
for (int i = 0; i < model.data!.sales!.length; i++) {
chartData.add(ChartData(
model.data!.sales![i].date!,
model.data!.sales![i].amount!.toDouble(),
model.data!.purchases![i].amount!.toDouble(),
));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
color: Colors.white,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: chartData.length * 50.0, // Adjust width based on the number of data points
child: Stack(
alignment: Alignment.topRight,
children: [
BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: _getMaxY(),
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _getBottomTitles(value, meta),
);
},
reservedSize: 42,
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false, reservedSize: 20),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: _getLeftTitles,
reservedSize: _getLeftTitleReservedSize(),
),
),
),
borderData: FlBorderData(
show: false,
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: Color(0xffD1D5DB),
dashArray: [4, 4],
strokeWidth: 1,
);
},
),
barGroups: _buildBarGroups(),
),
),
Column(
children: [
SizedBox(),
const Spacer(),
Padding(
padding: const EdgeInsets.only(bottom: 42),
child: CustomPaint(
size: Size(
chartData.length * 50.0 - _getLeftTitleReservedSize(), // Adjust to match the width of the BarChart exactly
0.1),
painter: DashedBarPainter(
barHeight: 1,
barColor: const Color(0xffD1D5DB),
dashWidth: 4,
dashSpace: 4,
),
),
),
],
),
],
),
),
),
),
),
);
}
double _getMaxY() {
double maxY = 0;
for (var data in chartData) {
maxY = maxY > data.y ? maxY : data.y;
maxY = maxY > data.y1 ? maxY : data.y1;
}
return maxY + 10;
}
double _getLeftTitleReservedSize() {
double maxY = _getMaxY();
if (maxY < 999) {
return 32;
} else if (maxY < 1000) {
return 35;
} else if (maxY < 10000) {
return 54;
} else {
return 50; // Add more cases if needed
}
}
List<BarChartGroupData> _buildBarGroups() {
return chartData.asMap().entries.map((entry) {
int index = entry.key;
ChartData data = entry.value;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: data.y,
color: Colors.green,
width: 6,
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
BarChartRodData(
toY: data.y1,
color: kMainColor,
width: 6,
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
],
barsSpace: 8,
);
}).toList();
}
Widget _getBottomTitles(double value, TitleMeta meta) {
const style = TextStyle(
color: Color(0xff4D4D4D),
fontSize: 12,
);
String text = chartData[value.toInt()].x;
return SideTitleWidget(
space: 8,
meta: TitleMeta(
min: meta.min,
max: meta.max,
parentAxisSize: meta.parentAxisSize,
axisPosition: meta.axisPosition,
appliedInterval: meta.appliedInterval,
sideTitles: meta.sideTitles,
formattedValue: meta.formattedValue,
axisSide: meta.axisSide,
rotationQuarterTurns: meta.rotationQuarterTurns,
),
child: Text(text, style: style),
);
}
Widget _getLeftTitles(double value, TitleMeta meta) {
// Skip the highest value (already handled in your code)
double maxY = _getMaxY();
if (value == maxY) {
return const SizedBox.shrink();
}
// Format the number
String formattedValue;
if (value >= 1e9) {
formattedValue = '${(value / 1e9).toStringAsFixed(1)}B';
} else if (value >= 1e6) {
formattedValue = '${(value / 1e6).toStringAsFixed(1)}M';
} else if (value >= 1e3) {
formattedValue = '${(value / 1e3).toStringAsFixed(1)}K';
} else {
formattedValue = value.toInt().toString();
}
return SideTitleWidget(
meta: meta,
child: Text(
formattedValue,
style: const TextStyle(
color: Colors.black,
fontSize: 12,
),
),
);
}
}
///---------------------------------dash line-------------------------------
class DashedBarPainter extends CustomPainter {
final double barHeight;
final Color barColor;
final double dashWidth;
final double dashSpace;
DashedBarPainter({
required this.barHeight,
required this.barColor,
this.dashWidth = 4.0,
this.dashSpace = 2.0,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = barColor
..style = PaintingStyle.stroke
..strokeWidth = barHeight;
final dashPath = Path();
for (double i = 0; i < size.width; i += dashWidth + dashSpace) {
dashPath.addRect(Rect.fromLTWH(i, 0, dashWidth, size.height));
}
canvas.drawPath(dashPath, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -0,0 +1,154 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class TestNumericAxisChart extends StatefulWidget {
const TestNumericAxisChart({Key? key}) : super(key: key);
@override
State<TestNumericAxisChart> createState() => _TestNumericAxisChartState();
}
class _TestNumericAxisChartState extends State<TestNumericAxisChart> {
final List<ChartData> chartData = [
ChartData('Sat', 20000, 15000),
ChartData('Sun', 10000, 25000),
ChartData('Mon', 5000, 5000),
ChartData('Tues', 45000, 35000),
ChartData('Wed', 25000, 30000),
ChartData('Thurs', 20000, 10000),
ChartData('Fri', 25000, 20000),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(16.0),
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: 50000,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: _getBottomTitles,
reservedSize: 42,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: _getLeftTitles,
reservedSize: 42,
),
),
),
borderData: FlBorderData(show: false),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: Color(0xffD1D5DB),
dashArray: [5, 5],
strokeWidth: 1,
);
},
),
barGroups: _buildBarGroups(),
),
),
),
),
);
}
List<BarChartGroupData> _buildBarGroups() {
return chartData.asMap().entries.map((entry) {
int index = entry.key;
ChartData data = entry.value;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
toY: data.y,
color: Colors.green,
width: 10,
borderRadius: BorderRadius.circular(0),
),
BarChartRodData(
toY: data.y1,
color: Colors.red,
width: 10,
borderRadius: BorderRadius.circular(0),
),
],
barsSpace: 10,
);
}).toList();
}
Widget _getBottomTitles(double value, TitleMeta meta) {
final style = const TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 12,
);
String text = chartData[value.toInt()].x;
return SideTitleWidget(
meta: TitleMeta(
min: meta.min,
max: meta.max,
parentAxisSize: meta.parentAxisSize,
axisPosition: meta.axisPosition,
appliedInterval: meta.appliedInterval,
sideTitles: meta.sideTitles,
formattedValue: meta.formattedValue,
axisSide: meta.axisSide,
rotationQuarterTurns: meta.rotationQuarterTurns,
),
space: 8,
child: Text(text, style: style),
);
}
Widget _getLeftTitles(double value, TitleMeta meta) {
return SideTitleWidget(
meta: TitleMeta(
min: meta.min,
max: meta.max,
parentAxisSize: meta.parentAxisSize,
axisPosition: meta.axisPosition,
appliedInterval: meta.appliedInterval,
sideTitles: meta.sideTitles,
formattedValue: meta.formattedValue,
axisSide: meta.axisSide,
rotationQuarterTurns: meta.rotationQuarterTurns,
),
child: Text(
value.toInt().toString(),
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 12,
),
),
);
}
}
class ChartData {
ChartData(this.x, this.y, this.y1);
final String x;
final double y;
final double y1;
}

View File

@@ -0,0 +1,78 @@
class DueCollectionInvoice {
DueCollectionInvoice({
this.id,
this.due,
this.name,
this.type,
this.salesDues,
});
DueCollectionInvoice.fromJson(dynamic json) {
id = json['id'];
due = json['due'];
name = json['name'];
type = json['type'];
if (json[json['type'] == 'Supplier' ? 'purchases_dues' : 'sales_dues'] != null) {
salesDues = [];
json[json['type'] == 'Supplier' ? 'purchases_dues' : 'sales_dues'].forEach((v) {
salesDues?.add(SalesDuesInvoice.fromJson(v));
});
}
}
num? id;
num? due;
String? name;
String? type;
List<SalesDuesInvoice>? salesDues;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['due'] = due;
map['name'] = name;
map['type'] = type;
if (salesDues != null) {
map['sales_dues'] = salesDues?.map((v) => v.toJson()).toList();
}
return map;
}
}
class SalesDuesInvoice {
SalesDuesInvoice({
this.id,
this.partyId,
this.dueAmount,
this.paidAmount,
this.totalAmount,
this.invoiceNumber,
});
SalesDuesInvoice.fromJson(dynamic json) {
id = json['id'];
partyId = json['party_id'];
dueAmount = json['dueAmount'];
paidAmount = json['paidAmount'];
totalAmount = json['totalAmount'];
invoiceNumber = json['invoiceNumber'];
}
num? id;
num? partyId;
num? dueAmount;
num? paidAmount;
num? totalAmount;
String? invoiceNumber;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['party_id'] = partyId;
map['dueAmount'] = dueAmount;
map['paidAmount'] = paidAmount;
map['totalAmount'] = totalAmount;
map['invoiceNumber'] = invoiceNumber;
return map;
}
}

View File

@@ -0,0 +1,144 @@
import '../../../model/sale_transaction_model.dart';
import '../../../widgets/multipal payment mathods/model/payment_transaction_model.dart';
import '../../Customers/Model/parties_model.dart';
class DueCollection {
DueCollection(
{this.id,
this.businessId,
this.partyId,
this.userId,
this.saleId,
this.purchaseId,
this.totalDue,
this.dueAmountAfterPay,
this.payDueAmount,
this.paymentTypeId,
this.paymentType,
this.paymentDate,
this.invoiceNumber,
this.createdAt,
this.updatedAt,
this.user,
this.party,
this.transactions,
this.branch});
DueCollection.fromJson(dynamic json) {
id = json['id'];
businessId = json['business_id'];
partyId = json['party_id'];
userId = json['user_id'];
saleId = json['sale_id'];
purchaseId = json['purchase_id'];
totalDue = json['totalDue'];
dueAmountAfterPay = json['dueAmountAfterPay'];
payDueAmount = json['payDueAmount'];
paymentTypeId = int.tryParse(json["payment_type_id"].toString());
// paymentType = json['paymentType'];
paymentDate = json['paymentDate'];
invoiceNumber = json['invoiceNumber'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
user = json['user'] != null ? User.fromJson(json['user']) : null;
party = json['party'] != null ? Party.fromJson(json['party']) : null;
paymentType = json['payment_type'] != null ? PaymentType.fromJson(json['payment_type']) : null;
branch = json['branch'] != null ? Branch.fromJson(json['branch']) : null;
// NEW: Parsing the transactions list
if (json['transactions'] != null) {
transactions = [];
json['transactions'].forEach((v) {
transactions?.add(PaymentsTransaction.fromJson(v));
});
}
}
num? id;
num? businessId;
num? partyId;
num? userId;
num? saleId;
num? purchaseId;
num? totalDue;
num? dueAmountAfterPay;
num? payDueAmount;
int? paymentTypeId;
PaymentType? paymentType;
String? invoiceNumber;
String? paymentDate;
String? createdAt;
String? updatedAt;
User? user;
Party? party;
Branch? branch;
List<PaymentsTransaction>? transactions; // NEW Variable
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['business_id'] = businessId;
map['party_id'] = partyId;
map['user_id'] = userId;
map['sale_id'] = saleId;
map['purchase_id'] = purchaseId;
map['totalDue'] = totalDue;
map['dueAmountAfterPay'] = dueAmountAfterPay;
map['payDueAmount'] = payDueAmount;
map['paymentType'] = paymentType;
map['paymentDate'] = paymentDate;
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
if (user != null) {
map['user'] = user?.toJson();
}
if (party != null) {
map['party'] = party?.toJson();
}
map['branch'] = branch;
return map;
}
}
class PaymentType {
int? id;
String? name;
PaymentType({required this.id, required this.name});
// Factory constructor to create an instance from a Map
factory PaymentType.fromJson(Map<String, dynamic> json) {
return PaymentType(
id: json['id'] as int,
name: json['name'] as String,
);
}
// Method to convert an instance to a Map
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
};
}
}
class Branch {
Branch({
this.id,
this.name,
this.phone,
this.address,
});
Branch.fromJson(dynamic json) {
id = json['id'];
name = json['name'];
phone = json['phone'];
address = json['address'];
}
num? id;
String? name;
String? phone;
String? address;
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Due%20Calculation/Model/due_collection_model.dart';
import '../../../Provider/transactions_provider.dart';
import '../Model/due_collection_invoice_model.dart';
import '../Repo/due_repo.dart';
//------------dues-------------------------------------
final dueRepo = Provider<DueRepo>((ref) => DueRepo());
final dueCollectionListProvider = FutureProvider.autoDispose<List<DueCollection>>((ref) {
final repo = ref.read(dueRepo);
return repo.fetchDueCollectionList();
});
final filteredDueProvider = FutureProvider.family.autoDispose<List<DueCollection>, FilterModel>(
(ref, filter) {
final repo = ref.read(dueRepo);
return repo.fetchDueCollectionList(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);
DueRepo repo = DueRepo();
final dueInvoiceListProvider =
FutureProvider.autoDispose.family<DueCollectionInvoice, int>((ref, id) => repo.fetchDueInvoiceList(id: id));

View File

@@ -0,0 +1,132 @@
// ignore_for_file: unused_local_variable
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import '../../../Const/api_config.dart';
import '../../../Provider/profile_provider.dart';
import '../../../Provider/transactions_provider.dart';
import '../../../Repository/constant_functions.dart';
import '../../../http_client/custome_http_client.dart';
import '../../../http_client/customer_http_client_get.dart';
import '../../Customers/Provider/customer_provider.dart';
import '../Model/due_collection_invoice_model.dart';
import '../Model/due_collection_model.dart';
import '../Providers/due_provider.dart';
class DueRepo {
Future<List<DueCollection>> fetchDueCollectionList({
String? type,
String? fromDate,
String? toDate,
}) async {
final client = CustomHttpClientGet(client: http.Client());
// Manually build query string to preserve order
final List<String> queryList = [];
if (type != null && type.isNotEmpty) {
queryList.add('duration=$type');
}
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
queryList.add('from_date=$fromDate');
queryList.add('to_date=$toDate');
}
final String queryString = queryList.join('&');
final Uri uri = Uri.parse('${APIConfig.url}/dues${queryString.isNotEmpty ? '?$queryString' : ''}');
print(uri);
final response = await client.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body) as Map<String, dynamic>;
final list = parsed['data'] as List<dynamic>;
return list.map((json) => DueCollection.fromJson(json)).toList();
} else {
throw Exception('Failed to fetch Due List. Status code: ${response.statusCode}');
}
}
Future<DueCollectionInvoice> fetchDueInvoiceList({required int id}) async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/invoices?party_id=$id');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body);
return DueCollectionInvoice.fromJson(parsedData['data']);
} else {
throw Exception('Failed to fetch Sales List');
}
}
Future<DueCollection?> dueCollect({
required WidgetRef ref,
required BuildContext context,
required num partyId,
required String? invoiceNumber,
required String paymentDate,
required List<Map<String, dynamic>> payments,
required num payDueAmount,
}) async {
final uri = Uri.parse('${APIConfig.url}/dues');
final requestBody = jsonEncode({
'party_id': partyId,
'invoiceNumber': invoiceNumber,
'paymentDate': paymentDate,
'payments': payments,
'payDueAmount': payDueAmount,
});
try {
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
var responseData = await customHttpClient.post(
url: uri,
headers: {
"Accept": 'application/json',
'Authorization': await getAuthToken(),
'Content-Type': 'application/json'
},
body: requestBody);
final parsedData = jsonDecode(responseData.body);
print("Print Due data: $parsedData");
if (responseData.statusCode == 200) {
EasyLoading.showSuccess('Collected successful!');
ref.refresh(partiesProvider);
ref.refresh(purchaseTransactionProvider);
ref.refresh(salesTransactionProvider);
ref.refresh(businessInfoProvider);
ref.refresh(getExpireDateProvider(ref));
// ref.refresh(dueInvoiceListProvider(partyId.round()));
ref.refresh(dueCollectionListProvider);
ref.refresh(summaryInfoProvider);
return DueCollection.fromJson(parsedData['data']);
// Navigator.pop(context);
// return PurchaseTransaction.fromJson(parsedData);
} else {
EasyLoading.dismiss().then(
(value) => ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Due creation failed: ${parsedData['message']}'))),
);
return null;
}
} catch (error) {
EasyLoading.dismiss().then(
(value) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error'))),
);
return null;
}
}
}

View File

@@ -0,0 +1,475 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Due%20Calculation/Model/due_collection_model.dart';
import 'package:mobile_pos/Screens/Due%20Calculation/Repo/due_repo.dart';
import 'package:mobile_pos/Screens/invoice_details/due_invoice_details.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../Provider/profile_provider.dart';
import '../../constant.dart';
import '../../currency.dart';
import '../../widgets/multipal payment mathods/multi_payment_widget.dart';
import '../Customers/Model/parties_model.dart';
import 'Model/due_collection_invoice_model.dart';
import 'Providers/due_provider.dart';
class DueCollectionScreen extends StatefulWidget {
const DueCollectionScreen({super.key, required this.customerModel});
@override
State<DueCollectionScreen> createState() => _DueCollectionScreenState();
final Party customerModel;
}
class _DueCollectionScreenState extends State<DueCollectionScreen> {
// Key for MultiPaymentWidget
final GlobalKey<MultiPaymentWidgetState> paymentWidgetKey = GlobalKey();
num paidAmount = 0;
num remainDueAmount = 0;
num dueAmount = 0;
num calculateDueAmount({required num total}) {
if (total < 0) {
remainDueAmount = 0;
} else {
remainDueAmount = dueAmount - total;
}
return dueAmount - total;
}
TextEditingController paidText = TextEditingController();
TextEditingController dateController = TextEditingController(text: DateTime.now().toString().substring(0, 10));
DateTime selectedDate = DateTime.now();
SalesDuesInvoice? selectedInvoice;
// int? paymentType; // Removed old single payment type
// List of items in our dropdown menu
int count = 0;
@override
void initState() {
super.initState();
// Listener to update state when paidText changes (either manually or via MultiPaymentWidget)
paidText.addListener(() {
if (paidText.text.isEmpty) {
if (mounted) {
setState(() {
paidAmount = 0;
});
}
} else {
final val = double.tryParse(paidText.text) ?? 0;
// Validation: Cannot pay more than due
if (val <= dueAmount) {
if (mounted) {
setState(() {
paidAmount = val;
});
}
} else {
// If widget pushes value > due, or user types > due
// You might want to handle this gracefully.
// For now, keeping your old logic:
paidText.clear();
if (mounted) {
setState(() {
paidAmount = 0;
});
}
EasyLoading.showError(lang.S.of(context).youCanNotPayMoreThenDue);
}
}
});
}
@override
Widget build(BuildContext context) {
count++;
return Consumer(builder: (context, consumerRef, __) {
final personalData = consumerRef.watch(businessInfoProvider);
final dueInvoiceData = consumerRef.watch(dueInvoiceListProvider(widget.customerModel.id?.round() ?? 0));
final _theme = Theme.of(context);
return personalData.when(data: (data) {
List<SalesDuesInvoice> items = [];
num openingDueAmount = 0;
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
lang.S.of(context).collectDue,
),
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
elevation: 0.0,
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16),
child: Column(
children: [
Row(
children: [
dueInvoiceData.when(data: (data) {
num totalDueInInvoice = 0;
if (data.salesDues?.isNotEmpty ?? false) {
for (var element in data.salesDues!) {
totalDueInInvoice += element.dueAmount ?? 0;
items.add(element);
}
}
openingDueAmount = (data.due ?? 0) - totalDueInInvoice;
if (selectedInvoice == null) {
dueAmount = openingDueAmount;
}
return Expanded(
child: DropdownButtonFormField<SalesDuesInvoice>(
isExpanded: true,
value: selectedInvoice,
hint: Text(
lang.S.of(context).selectAInvoice,
),
icon: selectedInvoice != null
? GestureDetector(
onTap: () {
setState(() {
selectedInvoice = null;
// Reset payment widget when invoice is cleared
// paymentWidgetKey.currentState?.clear();
});
},
child: const Icon(
Icons.close,
color: Colors.red,
size: 16,
),
)
: const Icon(Icons.keyboard_arrow_down, color: kGreyTextColor),
items: items.map((SalesDuesInvoice invoice) {
return DropdownMenuItem(
value: invoice,
child: Text(
invoice.invoiceNumber.toString(),
style: _theme.textTheme.bodyMedium,
),
);
}).toList(),
onChanged: (newValue) {
setState(() {
dueAmount = newValue?.dueAmount ?? 0;
paidAmount = 0;
paidText.clear();
selectedInvoice = newValue;
// Reset payment widget when invoice changes
// paymentWidgetKey.currentState?.clear();
});
},
decoration: const InputDecoration(),
),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
}),
const SizedBox(width: 14),
Expanded(
child: TextFormField(
keyboardType: TextInputType.name,
readOnly: true,
controller: dateController,
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).date,
border: const OutlineInputBorder(),
suffixIcon: IconButton(
onPressed: () async {
final DateTime? picked = await showDatePicker(
initialDate: DateTime.now(),
firstDate: DateTime(2015, 8),
lastDate: DateTime(2101),
context: context,
);
if (picked != null) {
setState(() {
selectedDate = selectedDate.copyWith(
year: picked.year,
month: picked.month,
day: picked.day,
);
dateController.text = picked.toString().substring(0, 10);
});
}
},
icon: const Icon(FeatherIcons.calendar),
),
),
),
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
RichText(
text: TextSpan(
text: "${lang.S.of(context).totalDueAmount}: ",
style: _theme.textTheme.bodyMedium?.copyWith(
fontSize: 14,
color: DAppColors.kSecondary,
),
children: [
TextSpan(
text: widget.customerModel.due == null
? '$currency${0}'
: '$currency${widget.customerModel.due!}',
style: const TextStyle(color: Color(0xFFFF8C34)),
),
]),
)
],
),
const SizedBox(height: 10),
TextFormField(
keyboardType: TextInputType.name,
readOnly: true,
initialValue: widget.customerModel.name,
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).customerName,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
///_____Total______________________________
Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(5),
),
color: _theme.colorScheme.primaryContainer,
boxShadow: [
BoxShadow(
color: const Color(0xff000000).withValues(alpha: 0.08),
spreadRadius: 0,
offset: const Offset(0, 4),
blurRadius: 24,
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration(
color: Color(0xffFEF0F1),
borderRadius: BorderRadius.only(
topRight: Radius.circular(5),
topLeft: Radius.circular(5),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
lang.S.of(context).totalAmount,
style: const TextStyle(fontSize: 16),
),
Text(
dueAmount.toStringAsFixed(2),
style: const TextStyle(fontSize: 16),
),
],
),
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
lang.S.of(context).paidAmount,
style: const TextStyle(fontSize: 16),
),
SizedBox(
width: context.width() / 4,
height: 30,
child: TextFormField(
controller: paidText,
// Make ReadOnly if multiple payments are selected to avoid conflict
readOnly: (paymentWidgetKey.currentState?.getPaymentEntries().length ?? 1) > 1,
textAlign: TextAlign.right,
decoration: const InputDecoration(
hintText: '0',
hintStyle: TextStyle(color: kNeutralColor),
border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
focusedBorder: UnderlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
),
keyboardType: TextInputType.number,
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(10.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
lang.S.of(context).dueAmount,
style: const TextStyle(fontSize: 16),
),
Text(
calculateDueAmount(total: paidAmount).toStringAsFixed(2),
style: const TextStyle(fontSize: 16),
),
],
),
),
],
),
),
const SizedBox(height: 10),
],
),
),
///__________Payment_Type_Widget_______________________________________
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const Divider(height: 20),
MultiPaymentWidget(
key: paymentWidgetKey,
showWalletOption: true, // Configure as needed
showChequeOption: (widget.customerModel.type != 'Supplier'), // Configure as needed
totalAmountController: paidText,
onPaymentListChanged: () {},
),
const Divider(height: 20),
],
),
),
],
),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
maximumSize: const Size(double.infinity, 48),
minimumSize: const Size(double.infinity, 48),
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
),
onPressed: () async {
Navigator.pop(context);
},
child: Text(
lang.S.of(context).cancel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _theme.colorScheme.primary,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
const SizedBox(width: 20),
Expanded(
child: ElevatedButton(
style: OutlinedButton.styleFrom(
maximumSize: const Size(double.infinity, 48),
minimumSize: const Size(double.infinity, 48),
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
),
onPressed: () async {
if (paidAmount > 0 && dueAmount > 0) {
// Get payments from widget
List<PaymentEntry> payments = paymentWidgetKey.currentState?.getPaymentEntries() ?? [];
if (payments.isEmpty) {
EasyLoading.showError(lang.S.of(context).noDueSelected); // Or "Please select payment"
} else {
EasyLoading.show();
// Serialize Payment List
List<Map<String, dynamic>> paymentData = payments.map((e) => e.toJson()).toList();
DueRepo repo = DueRepo();
DueCollection? dueData;
dueData = await repo.dueCollect(
ref: consumerRef,
context: context,
partyId: widget.customerModel.id ?? 0,
invoiceNumber: selectedInvoice?.invoiceNumber,
paymentDate: selectedDate.toIso8601String(),
payments: paymentData,
payDueAmount: paidAmount,
);
if (dueData != null) {
DueInvoiceDetails(
dueCollection: dueData,
personalInformationModel: data,
isFromDue: true,
).launch(context);
}
}
} else {
EasyLoading.showError(
lang.S.of(context).noDueSelected,
);
}
},
child: Text(
lang.S.of(context).save,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: _theme.colorScheme.primaryContainer,
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
),
),
],
),
),
),
);
}, error: (e, stack) {
return Center(
child: Text(e.toString()),
);
}, loading: () {
return const Center(child: CircularProgressIndicator());
});
});
}
}

View File

@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconly/iconly.dart';
import 'package:mobile_pos/Provider/profile_provider.dart';
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
import 'package:mobile_pos/Screens/Due%20Calculation/due_collection_screen.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../Const/api_config.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../constant.dart' as DAppColors;
import '../../constant.dart';
import '../../currency.dart';
import '../../http_client/custome_http_client.dart';
import '../../widgets/empty_widget/_empty_widget.dart';
import '../Customers/Provider/customer_provider.dart';
import '../../service/check_user_role_permission_provider.dart';
class DueCalculationContactScreen extends StatefulWidget {
const DueCalculationContactScreen({super.key});
@override
State<DueCalculationContactScreen> createState() => _DueCalculationContactScreenState();
}
class _DueCalculationContactScreenState extends State<DueCalculationContactScreen> {
late Color color;
@override
Widget build(BuildContext context) {
final _theme = Theme.of(context);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
resizeToAvoidBottomInset: true,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
lang.S.of(context).dueList,
),
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
elevation: 0.0,
),
body: SingleChildScrollView(
child: Consumer(builder: (context, ref, __) {
final providerData = ref.watch(partiesProvider);
final businessInfo = ref.watch(businessInfoProvider);
final permissionService = PermissionService(ref);
return providerData.when(data: (parties) {
List<Party> dueCustomerList = [];
for (var party in parties) {
if ((party.due ?? 0) > 0) {
dueCustomerList.add(party);
}
}
return dueCustomerList.isNotEmpty
? businessInfo.when(data: (details) {
if (!permissionService.hasPermission(Permit.duesRead.value)) {
return Center(child: PermitDenyWidget());
}
return ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const NeverScrollableScrollPhysics(),
itemCount: dueCustomerList.length,
itemBuilder: (_, index) {
dueCustomerList[index].type == 'Retailer' ? color = const Color(0xFF56da87) : Colors.white;
dueCustomerList[index].type == 'Wholesaler'
? color = const Color(0xFF25a9e0)
: Colors.white;
dueCustomerList[index].type == 'Dealer' ? color = const Color(0xFFff5f00) : Colors.white;
dueCustomerList[index].type == 'Supplier' ? color = const Color(0xFFA569BD) : Colors.white;
final item = dueCustomerList[index];
final normalizedType = (item.type ?? '').toLowerCase();
String effectiveDisplayType;
if (normalizedType == 'retailer') {
effectiveDisplayType = lang.S.of(context).customer;
} else if (normalizedType == 'wholesaler') {
effectiveDisplayType = lang.S.of(context).wholesaler;
} else if (normalizedType == 'dealer') {
effectiveDisplayType = lang.S.of(context).dealer;
} else if (normalizedType == 'supplier') {
effectiveDisplayType = lang.S.of(context).supplier;
} else {
effectiveDisplayType = item.type ?? '';
}
return ListTile(
visualDensity: const VisualDensity(vertical: -2),
contentPadding: EdgeInsets.zero,
onTap: () async {
DueCollectionScreen(customerModel: dueCustomerList[index]).launch(context);
},
leading: dueCustomerList[index].image != null
? Container(
height: 40,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: DAppColors.kBorder, width: 0.3),
image: DecorationImage(
image: NetworkImage(
'${APIConfig.domain}${dueCustomerList[index].image ?? ''}',
),
fit: BoxFit.cover,
),
),
)
: CircleAvatarWidget(name: dueCustomerList[index].name ?? 'n/a'),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
dueCustomerList[index].name ?? '',
maxLines: 1,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: Colors.black,
fontSize: 16.0,
),
),
),
const SizedBox(width: 4),
Text(
'$currency ${dueCustomerList[index].due}',
style: _theme.textTheme.bodyMedium?.copyWith(
fontSize: 16.0,
),
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
// dueCustomerList[index].type ?? '',
effectiveDisplayType,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _theme.textTheme.bodyMedium?.copyWith(
color: color,
fontSize: 14.0,
),
),
),
const SizedBox(width: 4),
Text(
dueCustomerList[index].due != null && dueCustomerList[index].due != 0
? lang.S.of(context).due
: 'No Due',
style: _theme.textTheme.bodyMedium?.copyWith(
color: dueCustomerList[index].due != null && dueCustomerList[index].due != 0
? const Color(0xFFff5f00)
: const Color(0xff808191),
fontSize: 14.0,
),
),
],
),
trailing: const Icon(
IconlyLight.arrow_right_2,
size: 18,
),
);
});
}, error: (e, stack) {
return const CircularProgressIndicator();
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
})
: Center(
child: Text(
lang.S.of(context).noDataAvailabe,
maxLines: 2,
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 20.0),
),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return const Center(child: CircularProgressIndicator());
});
}),
),
),
);
}
}

View File

@@ -0,0 +1,41 @@
class ExpenseCategory {
ExpenseCategory({
this.id,
this.categoryName,
this.businessId,
this.categoryDescription,
this.status,
this.createdAt,
this.updatedAt,
});
ExpenseCategory.fromJson(dynamic json) {
id = json['id'];
categoryName = json['categoryName'];
businessId = json['business_id'];
categoryDescription = json['categoryDescription'];
status = json['status'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
num? id;
String? categoryName;
num? businessId;
String? categoryDescription;
bool? status;
String? createdAt;
String? updatedAt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['categoryName'] = categoryName;
map['business_id'] = businessId;
map['categoryDescription'] = categoryDescription;
map['status'] = status;
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
return map;
}
}

View File

@@ -0,0 +1,96 @@
class Expense {
Expense({
this.id,
this.account,
this.amount,
this.expenseCategoryId,
this.userId,
this.businessId,
this.expanseFor,
this.paymentType,
this.paymentTypeId,
this.referenceNo,
this.note,
this.expenseDate,
this.createdAt,
this.updatedAt,
this.category,
});
Expense.fromJson(dynamic json) {
id = json['id'];
account = json['account'];
amount = json['amount'];
expenseCategoryId = json['expense_category_id'];
userId = json['user_id'];
businessId = json['business_id'];
expanseFor = json['expanseFor'];
paymentTypeId = json["payment_type_id"];
paymentType = json['paymentType'];
referenceNo = json['referenceNo'];
note = json['note'];
expenseDate = json['expenseDate'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
category = json['category'] != null ? Category.fromJson(json['category']) : null;
}
num? id;
dynamic account;
num? amount;
num? expenseCategoryId;
num? userId;
num? businessId;
String? expanseFor;
int? paymentTypeId;
String? paymentType;
String? referenceNo;
String? note;
String? expenseDate;
String? createdAt;
String? updatedAt;
Category? category;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['account'] = account;
map['amount'] = amount;
map['expense_category_id'] = expenseCategoryId;
map['user_id'] = userId;
map['business_id'] = businessId;
map['expanseFor'] = expanseFor;
map['paymentType'] = paymentType;
map['referenceNo'] = referenceNo;
map['note'] = note;
map['expenseDate'] = expenseDate;
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
if (category != null) {
map['category'] = category?.toJson();
}
return map;
}
}
class Category {
Category({
this.id,
this.categoryName,
});
Category.fromJson(dynamic json) {
id = json['id'];
categoryName = json['categoryName'];
}
num? id;
String? categoryName;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['categoryName'] = categoryName;
return map;
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Expense/Model/expense_modle.dart';
import '../../../Provider/transactions_provider.dart';
import '../Repo/expanse_repo.dart';
//---------income for duration--------------------------------
final expenseRepoProvider = Provider<ExpenseRepo>(
(ref) => ExpenseRepo(),
);
final filteredExpenseProvider = FutureProvider.family.autoDispose<List<Expense>, FilterModel>(
(ref, filter) {
final repo = ref.read(expenseRepoProvider);
return repo.fetchAllIExpense(
type: filter.duration,
fromDate: filter.fromDate,
toDate: filter.toDate,
);
},
);

View File

@@ -0,0 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Expense/Model/expanse_category.dart';
import '../Repo/expanse_category_repo.dart';
ExpanseCategoryRepo expenseCategoryRepo = ExpanseCategoryRepo();
final expanseCategoryProvider = FutureProvider.autoDispose<List<ExpenseCategory>>((ref) => expenseCategoryRepo.fetchAllExpanseCategory());

View File

@@ -0,0 +1,68 @@
//ignore_for_file: file_names, unused_element, unused_local_variable
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/Screens/Expense/Model/expanse_category.dart';
import 'package:mobile_pos/Screens/Expense/Providers/expense_category_proivder.dart';
import '../../../Const/api_config.dart';
import '../../../Repository/constant_functions.dart';
import '../../../http_client/custome_http_client.dart';
import '../../../http_client/customer_http_client_get.dart';
class ExpanseCategoryRepo {
Future<List<ExpenseCategory>> fetchAllExpanseCategory() async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/expense-categories');
try {
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
final categoryList = parsedData['data'] as List<dynamic>;
return categoryList.map((category) => ExpenseCategory.fromJson(category)).toList();
} else {
// Handle specific error cases based on response codes
throw Exception('Failed to fetch categories: ${response.statusCode}');
}
} catch (error) {
// Handle unexpected errors gracefully
rethrow; // Re-throw to allow further handling upstream
}
}
Future<void> addExpanseCategory({
required WidgetRef ref,
required BuildContext context,
required String categoryName,
}) async {
final uri = Uri.parse('${APIConfig.url}/expense-categories');
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
var responseData = await customHttpClient.post(url: uri, body: {
'categoryName': categoryName,
});
EasyLoading.dismiss();
try {
final parsedData = jsonDecode(responseData.body);
if (responseData.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successful!')));
var data1 = ref.refresh(expanseCategoryProvider);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Category creation failed: ${parsedData['message']}')));
}
} catch (error) {
// Handle unexpected errors gracefully
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
}
}
}

View File

@@ -0,0 +1,147 @@
//ignore_for_file: file_names, unused_element, unused_local_variable
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:mobile_pos/Provider/profile_provider.dart';
import 'package:mobile_pos/Screens/Expense/Providers/all_expanse_provider.dart';
import '../../../Const/api_config.dart';
import '../../../Repository/constant_functions.dart';
import '../../../http_client/custome_http_client.dart';
import '../../../http_client/customer_http_client_get.dart';
import '../../../widgets/multipal payment mathods/multi_payment_widget.dart';
import '../Model/expense_modle.dart';
import '../add_erxpense.dart';
class ExpenseRepo {
Future<List<Expense>> fetchAllIExpense({
String? type,
String? fromDate,
String? toDate,
}) async {
final client = CustomHttpClientGet(client: http.Client());
final Map<String, String> queryParams = {};
if (type != null && type.isNotEmpty) {
queryParams['duration'] = type;
}
if (type == 'custom_date') {
if (fromDate != null && fromDate.isNotEmpty) {
queryParams['from_date'] = fromDate;
}
if (toDate != null && toDate.isNotEmpty) {
queryParams['to_date'] = toDate;
}
}
final Uri uri = Uri.parse('${APIConfig.url}/expenses').replace(
queryParameters: queryParams.isNotEmpty ? queryParams : null,
);
print('Request URI: $uri');
final response = await client.get(url: uri);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body) as Map<String, dynamic>;
final list = parsed['data'] as List<dynamic>;
return list.map((json) => Expense.fromJson(json)).toList();
} else {
throw Exception('Failed to fetch Due List. Status code: ${response.statusCode}');
}
}
// Future<List<Expense>> fetchExpense() async {
// CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
// final uri = Uri.parse('${APIConfig.url}/expenses');
//
// final response = await clientGet.get(url: uri);
//
// if (response.statusCode == 200) {
// final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
//
// final partyList = parsedData['data'] as List<dynamic>;
// return partyList.map((category) => Expense.fromJson(category)).toList();
// // Parse into Party objects
// } else {
// throw Exception('Failed to fetch expense list');
// }
// }
Future<void> createExpense({
required WidgetRef ref,
required BuildContext context,
required num amount,
required num expenseCategoryId,
required String expanseFor,
required String referenceNo,
required String expenseDate,
required String note,
required List<PaymentEntry> payments, // <<< Updated parameter
}) async {
final uri = Uri.parse('${APIConfig.url}/expenses');
// Build the request body as a Map<String, String> for form-data
// This will be sent as 'application/x-www-form-urlencoded'
Map<String, String> requestBody = {
'amount': amount.toString(),
'expense_category_id': expenseCategoryId.toString(),
'expanseFor': expanseFor,
'referenceNo': referenceNo,
'expenseDate': expenseDate,
'note': note,
};
// Add payments in the format: payments[index][key]
for (int i = 0; i < payments.length; i++) {
final payment = payments[i];
final paymentAmount = num.tryParse(payment.amountController.text) ?? 0;
// Only add valid payments
if (payment.type != null && paymentAmount > 0) {
requestBody['payments[$i][type]'] = payment.type!;
requestBody['payments[$i][amount]'] = paymentAmount.toString();
if (payment.type == 'cheque' && payment.chequeNumberController.text.isNotEmpty) {
requestBody['payments[$i][cheque_number]'] = payment.chequeNumberController.text;
}
}
}
try {
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
print('POST DATA OF EXPENSE: $requestBody');
var responseData = await customHttpClient.post(
url: uri,
body: requestBody,
addContentTypeInHeader: false,
);
final parsedData = jsonDecode(responseData.body);
EasyLoading.dismiss();
if (responseData.statusCode == 200 || responseData.statusCode == 201) {
Navigator.pop(context, true);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(parsedData['message'] ?? 'Expense created successfully'),
));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Expense creation failed: ${parsedData['message']}')));
return;
}
} catch (error) {
EasyLoading.dismiss();
// Handle unexpected errors gracefully
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
// return null;
}
}
}

View File

@@ -0,0 +1,332 @@
// ignore_for_file: 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:iconly/iconly.dart';
import 'package:intl/intl.dart';
import 'package:mobile_pos/Screens/Expense/Model/expanse_category.dart';
import 'package:mobile_pos/Screens/Expense/expense_category_list.dart';
import 'package:nb_utils/nb_utils.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../constant.dart';
import '../../generated/l10n.dart' as lang;
import '../../service/check_user_role_permission_provider.dart';
import '../../widgets/multipal payment mathods/multi_payment_widget.dart';
import 'Repo/expanse_repo.dart';
// ignore: must_be_immutable
class AddExpense extends ConsumerStatefulWidget {
const AddExpense({
super.key,
});
@override
// ignore: library_private_types_in_public_api
_AddExpenseState createState() => _AddExpenseState();
}
class _AddExpenseState extends ConsumerState<AddExpense> {
ExpenseCategory? selectedCategory;
final dateController = TextEditingController();
TextEditingController expanseForNameController = TextEditingController();
TextEditingController expanseAmountController = TextEditingController();
TextEditingController expanseNoteController = TextEditingController();
TextEditingController expanseRefController = TextEditingController();
// (CHANGE 1) GlobalKey-ke public state class (`MultiPaymentWidgetState`) diye update kora holo
final GlobalKey<MultiPaymentWidgetState> _paymentKey = GlobalKey<MultiPaymentWidgetState>();
@override
void initState() {
super.initState();
// All payment listeners are now in MultiPaymentWidget
}
@override
void dispose() {
// Dispose all parent controllers
dateController.dispose();
expanseForNameController.dispose();
expanseAmountController.dispose();
expanseNoteController.dispose();
expanseRefController.dispose();
// All payment controllers are disposed by MultiPaymentWidget
super.dispose();
}
DateTime selectedDate = DateTime.now();
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2015, 8),
lastDate: DateTime(2021)); // Error fixed: 20121 -> 2021
if (picked != null && picked != selectedDate) {
setState(() {
selectedDate = picked;
});
}
}
GlobalKey<FormState> formKey = GlobalKey<FormState>();
bool validateAndSave() {
final form = formKey.currentState;
if (form!.validate()) {
form.save();
return true;
}
return false;
}
@override
Widget build(BuildContext context) {
final permissionService = PermissionService(ref);
// bankListAsync is no longer needed here, MultiPaymentWidget will handle it
return GlobalPopup(
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
title: Text(
lang.S.of(context).addExpense,
),
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.black),
elevation: 0.0,
),
body: SingleChildScrollView(
child: SizedBox(
width: context.width(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
///_______date________________________________
SizedBox(
height: 48,
child: FormField(
builder: (FormFieldState<dynamic> field) {
return InputDecorator(
decoration: kInputDecoration.copyWith(
suffixIcon: const Icon(IconlyLight.calendar, color: kGreyTextColor),
contentPadding: const EdgeInsets.all(8),
labelText: lang.S.of(context).expenseDate,
hintText: lang.S.of(context).enterExpenseDate,
),
child: Text(
'${DateFormat.d().format(selectedDate)} ${DateFormat.MMM().format(selectedDate)} ${DateFormat.y().format(selectedDate)}',
),
);
},
).onTap(() => _selectDate(context)),
),
const SizedBox(height: 20),
///_________category_______________________________________________
Container(
height: 48.0,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(color: kBorderColor),
),
child: GestureDetector(
onTap: () async {
selectedCategory = await const ExpenseCategoryList().launch(context);
setState(() {});
},
child: Row(
children: [
const SizedBox(width: 10.0),
Text(selectedCategory?.categoryName ?? lang.S.of(context).selectCategory),
const Spacer(),
const Icon(Icons.keyboard_arrow_down),
const SizedBox(
width: 10.0,
),
],
),
),
),
const SizedBox(height: 20),
///________Expense_for_______________________________________________
TextFormField(
showCursor: true,
controller: expanseForNameController,
validator: (value) {
if (value.isEmptyOrNull) {
return lang.S.of(context).pleaseEnterName;
}
return null;
},
onSaved: (value) {
expanseForNameController.text = value!;
},
decoration: kInputDecoration.copyWith(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).expenseFor,
hintText: lang.S.of(context).enterName,
),
),
const SizedBox(height: 20),
///_________________Total Amount_____________________________
TextFormField(
controller: expanseAmountController,
// (CHANGE 2) readOnly logic-ti notun key diye update kora holo
readOnly: (_paymentKey.currentState?.getPaymentEntries().length ?? 1) > 1,
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
validator: (value) {
if (value.isEmptyOrNull) {
return lang.S.of(context).pleaseEnterAmount;
}
// Get total from the controller itself
final total = double.tryParse(value ?? '') ?? 0.0;
if (total <= 0) {
return lang.S.of(context).amountMustBeGreaterThanZero;
}
return null;
},
decoration: kInputDecoration.copyWith(
// (CHANGE 3) fillColor logic-ti notun key diye update kora holo
fillColor: (_paymentKey.currentState?.getPaymentEntries().length ?? 1) > 1
? Colors.grey.shade100
: Colors.white,
filled: true,
border: const OutlineInputBorder(),
errorBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.red),
),
labelText: lang.S.of(context).amount,
floatingLabelBehavior: FloatingLabelBehavior.always,
hintText: '0.00',
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 20),
///_______reference_________________________________
TextFormField(
showCursor: true,
controller: expanseRefController,
validator: (value) {
return null;
},
onSaved: (value) {
expanseRefController.text = value!;
},
decoration: kInputDecoration.copyWith(
border: const OutlineInputBorder(),
labelText: lang.S.of(context).referenceNo,
floatingLabelBehavior: FloatingLabelBehavior.always,
hintText: lang.S.of(context).enterRefNumber,
),
),
const SizedBox(height: 20),
///_________note____________________________________________________
TextFormField(
showCursor: true,
controller: expanseNoteController,
validator: (value) {
return null;
},
onSaved: (value) {
expanseNoteController.text = value!;
},
decoration: kInputDecoration.copyWith(
border: const OutlineInputBorder(),
labelText: lang.S.of(context).note,
hintText: lang.S.of(context).enterNote,
),
),
const SizedBox(height: 20),
MultiPaymentWidget(
key: _paymentKey,
totalAmountController: expanseAmountController,
showChequeOption: false,
onPaymentListChanged: () {
setState(() {});
},
),
const SizedBox(height: 20),
///_______button_________________________________
SizedBox(
width: double.infinity,
height: 45,
child: ElevatedButton.icon(
iconAlignment: IconAlignment.end,
label: Text(lang.S.of(context).continueButton),
onPressed: () async {
if (!permissionService.hasPermission(Permit.expensesCreate.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).youDonNotHavePermissionToCreateExpense),
),
);
return;
}
if (validateAndSave()) {
// (CHANGE 5) Notun key diye data neya hocche
final totalExpense = double.tryParse(expanseAmountController.text) ?? 0.0;
final payments = _paymentKey.currentState?.getPaymentEntries();
if (selectedCategory == null) {
EasyLoading.showError(lang.S.of(context).pleaseSelectAExpenseCategory);
return;
}
if (totalExpense <= 0) {
EasyLoading.showError(lang.S.of(context).amountMustBeGreaterThanZero);
return;
}
if (payments == null || payments.isEmpty) {
EasyLoading.showError(lang.S.of(context).canNotRetrievePaymentDetails);
return;
}
EasyLoading.show();
ExpenseRepo repo = ExpenseRepo();
await repo.createExpense(
ref: ref,
context: context,
amount: totalExpense, // Use state variable
expenseCategoryId: selectedCategory?.id ?? 0,
expanseFor: expanseForNameController.text,
referenceNo: expanseRefController.text,
expenseDate: selectedDate.toString(),
note: expanseNoteController.text,
payments: payments, // Pass the payment list
);
}
},
icon: const Icon(
Icons.arrow_forward,
color: Colors.white,
),
),
),
],
)),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,110 @@
// ignore_for_file: unused_result
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Expense/Repo/expanse_category_repo.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../http_client/custome_http_client.dart';
import '../../service/check_user_role_permission_provider.dart';
class AddExpenseCategory extends StatefulWidget {
const AddExpenseCategory({Key? key}) : super(key: key);
@override
// ignore: library_private_types_in_public_api
_AddExpenseCategoryState createState() => _AddExpenseCategoryState();
}
class _AddExpenseCategoryState extends State<AddExpenseCategory> {
bool showProgress = false;
TextEditingController nameController = TextEditingController();
GlobalKey<FormState> key = GlobalKey();
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, __) {
//final allCategory = ref.watch(expanseCategoryProvider);
final permissionService = PermissionService(ref);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
lang.S.of(context).addExpenseCat,
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Visibility(
visible: showProgress,
child: const CircularProgressIndicator(
color: kMainColor,
strokeWidth: 5.0,
),
),
Form(
key: key,
child: TextFormField(
validator: (value) {
if (value?.trim().isEmptyOrNull ?? true) {
//return 'Enter expanse category name';
return lang.S.of(context).enterExpanseCategoryName;
}
return null;
},
controller: nameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: lang.S.of(context).fashions,
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: lang.S.of(context).categoryName,
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
if (!permissionService.hasPermission(Permit.expenseCategoriesCreate.value)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(lang.S.of(context).youDoNotHavePermissionToCreateExpenseCategory),
),
);
return;
}
if (key.currentState?.validate() ?? false) {
EasyLoading.show();
final categoryRepo = ExpanseCategoryRepo();
await categoryRepo.addExpanseCategory(
ref: ref,
context: context,
categoryName: nameController.text.trim(),
);
}
},
child: Text(lang.S.of(context).save),
),
],
),
),
),
),
);
});
}
}

View File

@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_pos/Screens/Expense/add_expense_category.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import '../../GlobalComponents/glonal_popup.dart';
import '../../constant.dart';
import '../../http_client/custome_http_client.dart';
import '../../widgets/empty_widget/_empty_widget.dart';
import '../../service/check_user_role_permission_provider.dart';
import 'Providers/expense_category_proivder.dart';
class ExpenseCategoryList extends StatefulWidget {
const ExpenseCategoryList({Key? key, this.mainContext}) : super(key: key);
final BuildContext? mainContext;
@override
// ignore: library_private_types_in_public_api
_ExpenseCategoryListState createState() => _ExpenseCategoryListState();
}
class _ExpenseCategoryListState extends State<ExpenseCategoryList> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer(builder: (context, ref, _) {
final data = ref.watch(expanseCategoryProvider);
final permissionService = PermissionService(ref);
return GlobalPopup(
child: Scaffold(
backgroundColor: kWhite,
appBar: AppBar(
title: Text(
lang.S.of(context).expenseCat,
),
iconTheme: const IconThemeData(color: Colors.black),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0.0,
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
flex: 3,
child: AppTextField(
textFieldType: TextFieldType.NAME,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: lang.S.of(context).search,
prefixIcon: Icon(
Icons.search,
color: kGreyTextColor.withOpacity(0.5),
),
),
),
),
const SizedBox(
width: 10.0,
),
Expanded(
flex: 1,
child: GestureDetector(
onTap: () {
const AddExpenseCategory().launch(context);
},
child: Container(
padding: const EdgeInsets.only(left: 20.0, right: 20.0),
height: 48.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(color: kBorderColor),
),
child: const Icon(
Icons.add,
color: kGreyTextColor,
),
),
),
),
],
),
const SizedBox(
height: 10,
),
data.when(data: (data) {
if (!permissionService.hasPermission(Permit.incomeCategoriesRead.value)) {
return Center(child: PermitDenyWidget());
}
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
data[index].categoryName ?? '',
style: theme.textTheme.titleLarge?.copyWith(
fontSize: 18.0,
),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: kBackgroundColor,
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 12),
minimumSize: Size(
50,
25,
),
),
// buttonDecoration: kButtonDecoration.copyWith(color: kDarkWhite),
onPressed: () {
// const AddExpense().launch(context);
Navigator.pop(
context,
data[index],
);
},
child: Text(
lang.S.of(context).select,
style: theme.textTheme.titleSmall?.copyWith(color: Colors.black, fontWeight: FontWeight.w600),
),
),
],
),
);
},
);
}, error: (error, stackTrace) {
return Text(error.toString());
}, loading: () {
return const CircularProgressIndicator();
})
],
),
),
),
);
});
}
}

View File

@@ -0,0 +1,332 @@
// import 'package:flutter/material.dart';
// import 'package:flutter_feather_icons/flutter_feather_icons.dart';
// import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:intl/intl.dart';
// import 'package:mobile_pos/Provider/profile_provider.dart';
// import 'package:mobile_pos/Screens/Expense/Providers/all_expanse_provider.dart';
// import 'package:mobile_pos/Screens/Expense/add_erxpense.dart';
// import 'package:mobile_pos/generated/l10n.dart' as lang;
// import 'package:nb_utils/nb_utils.dart';
//
// import '../../GlobalComponents/glonal_popup.dart';
// import '../../constant.dart';
// import '../../currency.dart';
// import '../../http_client/custome_http_client.dart';
// import '../../service/check_actions_when_no_branch.dart';
// import '../../widgets/empty_widget/_empty_widget.dart';
// import '../../service/check_user_role_permission_provider.dart';
// import 'Providers/expense_category_proivder.dart';
//
// class ExpenseList extends StatefulWidget {
// const ExpenseList({super.key});
//
// @override
// // ignore: library_private_types_in_public_api
// _ExpenseListState createState() => _ExpenseListState();
// }
//
// class _ExpenseListState extends State<ExpenseList> {
// final dateController = TextEditingController();
// TextEditingController fromDateTextEditingController = TextEditingController(text: DateFormat.yMMMd().format(DateTime(2021)));
// TextEditingController toDateTextEditingController = TextEditingController(text: DateFormat.yMMMd().format(DateTime.now()));
// DateTime fromDate = DateTime(2021);
// DateTime toDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
// num totalExpense = 0;
//
// @override
// void dispose() {
// dateController.dispose();
// super.dispose();
// }
//
// bool _isRefreshing = false; // Prevents multiple refresh calls
//
// Future<void> refreshData(WidgetRef ref) async {
// if (_isRefreshing) return; // Prevent duplicate refresh calls
// _isRefreshing = true;
//
// ref.refresh(expenseProvider);
// ref.refresh(expanseCategoryProvider);
//
// await Future.delayed(const Duration(seconds: 1)); // Optional delay
// _isRefreshing = false;
// }
//
// @override
// Widget build(BuildContext context) {
// totalExpense = 0;
// return Consumer(builder: (context, ref, __) {
// final expenseData = ref.watch(expenseProvider);
// final businessInfoData = ref.watch(businessInfoProvider);
// final permissionService = PermissionService(ref);
// return GlobalPopup(
// child: Scaffold(
// backgroundColor: kWhite,
// appBar: AppBar(
// title: Text(
// lang.S.of(context).expense,
// ),
// iconTheme: const IconThemeData(color: Colors.black),
// centerTitle: true,
// backgroundColor: Colors.white,
// elevation: 0.0,
// ),
// body: RefreshIndicator(
// onRefresh: () => refreshData(ref),
// child: SingleChildScrollView(
// physics: const AlwaysScrollableScrollPhysics(),
// child: Padding(
// padding: const EdgeInsets.all(10.0),
// child: Column(
// children: [
// if (permissionService.hasPermission(Permit.expensesRead.value)) ...{
// Padding(
// padding: const EdgeInsets.only(right: 10.0, left: 10.0, top: 10, bottom: 10),
// child: Row(
// children: [
// Expanded(
// child: AppTextField(
// textFieldType: TextFieldType.NAME,
// readOnly: true,
// controller: fromDateTextEditingController,
// decoration: InputDecoration(
// floatingLabelBehavior: FloatingLabelBehavior.always,
// labelText: lang.S.of(context).fromDate,
// border: const OutlineInputBorder(),
// suffixIcon: IconButton(
// onPressed: () async {
// final DateTime? picked = await showDatePicker(
// initialDate: DateTime.now(),
// firstDate: DateTime(2015, 8),
// lastDate: DateTime(2101),
// context: context,
// );
// setState(() {
// fromDateTextEditingController.text = DateFormat.yMMMd().format(picked ?? DateTime.now());
// fromDate = picked!;
// totalExpense = 0;
// });
// },
// icon: const Icon(FeatherIcons.calendar),
// ),
// ),
// ),
// ),
// const SizedBox(width: 10),
// Expanded(
// child: AppTextField(
// textFieldType: TextFieldType.NAME,
// readOnly: true,
// controller: toDateTextEditingController,
// decoration: InputDecoration(
// floatingLabelBehavior: FloatingLabelBehavior.always,
// labelText: lang.S.of(context).toDate,
// border: const OutlineInputBorder(),
// suffixIcon: IconButton(
// onPressed: () async {
// final DateTime? picked = await showDatePicker(
// initialDate: toDate,
// firstDate: DateTime(2015, 8),
// lastDate: DateTime(2101),
// context: context,
// );
//
// setState(() {
// toDateTextEditingController.text = DateFormat.yMMMd().format(picked ?? DateTime.now());
// picked!.isToday ? toDate = DateTime.now() : toDate = picked;
// totalExpense = 0;
// });
// },
// icon: const Icon(FeatherIcons.calendar),
// ),
// ),
// ),
// ),
// ],
// ),
// ),
//
// ///__________expense_data_table____________________________________________
// Container(
// width: context.width(),
// height: 50,
// padding: const EdgeInsets.all(10),
// decoration: const BoxDecoration(color: kDarkWhite),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// SizedBox(
// width: 130,
// child: Text(
// lang.S.of(context).expenseFor,
// ),
// ),
// SizedBox(
// width: 100,
// child: Text(lang.S.of(context).date),
// ),
// Container(
// alignment: Alignment.centerRight,
// width: 70,
// child: Text(lang.S.of(context).amount),
// )
// ],
// ),
// ),
//
// expenseData.when(data: (mainData) {
// if (mainData.isNotEmpty) {
// totalExpense = 0;
// for (var element in mainData) {
// final dateStr = element.expenseDate;
// if (dateStr != null && dateStr.isNotEmpty) {
// final parsedDate = DateTime.tryParse(dateStr.substring(0, 10));
// if (parsedDate != null &&
// (fromDate.isBefore(parsedDate) || fromDate.isAtSameMomentAs(parsedDate)) &&
// (toDate.isAfter(parsedDate) || toDate.isAtSameMomentAs(parsedDate))) {
// totalExpense += element.amount ?? 0;
// }
// }
// }
// return SizedBox(
// width: context.width(),
// child: ListView.builder(
// shrinkWrap: true,
// itemCount: mainData.length,
// physics: const NeverScrollableScrollPhysics(),
// itemBuilder: (BuildContext context, int index) {
// return Visibility(
// visible: mainData[index].expenseDate != null &&
// mainData[index].expenseDate!.isNotEmpty &&
// DateTime.tryParse(mainData[index].expenseDate!.substring(0, 10)) != null &&
// (fromDate.isBefore(DateTime.parse(mainData[index].expenseDate!.substring(0, 10))) ||
// fromDate.isAtSameMomentAs(DateTime.parse(mainData[index].expenseDate!.substring(0, 10)))) &&
// (toDate.isAfter(DateTime.parse(mainData[index].expenseDate!.substring(0, 10))) ||
// toDate.isAtSameMomentAs(DateTime.parse(mainData[index].expenseDate!.substring(0, 10)))),
// child: Column(
// children: [
// Padding(
// padding: const EdgeInsets.all(10.0),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// SizedBox(
// width: 130,
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(
// mainData[index].expanseFor ?? '',
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// ),
// const SizedBox(height: 5),
// Text(
// mainData[index].category?.categoryName ?? '',
// maxLines: 2,
// overflow: TextOverflow.ellipsis,
// style: const TextStyle(color: Colors.grey, fontSize: 11),
// ),
// ],
// ),
// ),
// SizedBox(
// width: 100,
// child: Text(
// mainData[index].expenseDate != null && mainData[index].expenseDate!.isNotEmpty
// ? DateFormat.yMMMd().format(DateTime.parse(mainData[index].expenseDate!))
// : 'N/A',
// ),
// ),
// Container(
// alignment: Alignment.centerRight,
// width: 70,
// child: Text('$currency${mainData[index].amount.toString()}'),
// )
// ],
// ),
// ),
// Container(
// height: 1,
// color: Colors.black12,
// )
// ],
// ),
// );
// },
// ),
// );
// } else {
// return Padding(
// padding: const EdgeInsets.all(20),
// child: Center(
// child: Text(lang.S.of(context).noData),
// ),
// );
// }
// }, error: (Object error, StackTrace? stackTrace) {
// return Text(error.toString());
// }, loading: () {
// return const Center(child: CircularProgressIndicator());
// }),
// } else
// Center(child: PermitDenyWidget()),
// ],
// ),
// ),
// ),
// ),
// bottomNavigationBar: Padding(
// padding: const EdgeInsets.all(10.0),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// ///_________total______________________________________________
// if (permissionService.hasPermission(Permit.expensesRead.value))
// Container(
// height: 50,
// padding: const EdgeInsets.all(10),
// decoration: const BoxDecoration(color: kDarkWhite),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// lang.S.of(context).totalExpense,
// ),
// Text('$currency$totalExpense')
// ],
// ),
// ),
// const SizedBox(height: 10),
//
// ///________button________________________________________________
// businessInfoData.when(data: (details) {
// return ElevatedButton(
// onPressed: () async {
// bool result = await checkActionWhenNoBranch(ref: ref, context: context);
// if (!result) {
// return;
// }
// const AddExpense().launch(context);
// },
// child: Text(lang.S.of(context).addExpense),
// );
// }, error: (e, stack) {
// return Text(e.toString());
// }, loading: () {
// return const Center(
// child: CircularProgressIndicator(),
// );
// })
// ],
// ),
// ),
// ),
// );
// });
// }
// }

View File

@@ -0,0 +1,33 @@
class Banner {
Banner({
this.id,
this.imageUrl,
this.status,
this.createdAt,
this.updatedAt,
});
Banner.fromJson(dynamic json) {
id = json['id'];
imageUrl = json['imageUrl'];
status = json['status'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
num? id;
String? imageUrl;
num? status;
String? createdAt;
String? updatedAt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['imageUrl'] = imageUrl;
map['status'] = status;
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
return map;
}
}

View File

@@ -0,0 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../Model/banner_model.dart';
import '../Repo/banner_repo.dart';
BannerRepo imageRepo = BannerRepo();
final bannerProvider = FutureProvider<List<Banner>>((ref) => imageRepo.fetchAllIBanners());

View File

@@ -0,0 +1,26 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../Const/api_config.dart';
import '../../../Repository/constant_functions.dart';
import '../../../http_client/customer_http_client_get.dart';
import '../Model/banner_model.dart';
class BannerRepo {
Future<List<Banner>> fetchAllIBanners() async {
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
final uri = Uri.parse('${APIConfig.url}/banners');
final response = await clientGet.get(url: uri);
if (response.statusCode == 200) {
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
final partyList = parsedData['data'] as List<dynamic>;
return partyList.map((user) => Banner.fromJson(user)).toList();
// Parse into Party objects
} else {
throw Exception('Failed to fetch Users');
}
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/Screens/Settings/settings_screen.dart';
import 'package:mobile_pos/constant.dart';
import 'package:nb_utils/nb_utils.dart';
class BottomNav extends StatefulWidget {
const BottomNav({
Key? key,
}) : super(key: key);
@override
State<BottomNav> createState() => _BottomNavState();
}
class _BottomNavState extends State<BottomNav> {
int _selectedIndex = 0;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
switch (_selectedIndex) {
case 0:
Navigator.pushNamed(context, '/home');
break;
case 1:
Navigator.pushNamed(context, '/order');
break;
case 2:
Navigator.pushNamed(context, '/featuredProduct');
break;
case 3:
const SettingScreen().launch(context);
break;
}
});
}
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
elevation: 6.0,
selectedItemColor: kMainColor,
// ignore: prefer_const_literals_to_create_immutables
items: [
const BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
const BottomNavigationBarItem(
icon: Icon(Icons.flare_sharp),
label: 'Maan',
),
const BottomNavigationBarItem(
icon: Icon(Icons.backpack),
label: 'Package',
),
const BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
);
}
}

View File

@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
class GridItems {
final String title, icon, route;
GridItems({required this.title, required this.icon, required this.route});
}
List<GridItems> getFreeIcons({required BuildContext context, bool? brunchPermission, bool? hrmPermission}) {
List<GridItems> freeIcons = [
GridItems(
title: lang.S.of(context).sale,
icon: 'assets/sales.svg',
route: 'Sales',
),
GridItems(
title: lang.S.of(context).posSale,
icon: 'images/dash_pos.svg',
route: 'Pos Sale',
),
GridItems(
title: lang.S.of(context).parties,
icon: 'assets/parties.svg',
route: 'Parties',
),
GridItems(
title: lang.S.of(context).purchase,
icon: 'assets/purchase.svg',
route: 'Purchase',
),
GridItems(
title: lang.S.of(context).product,
icon: 'assets/products.svg',
route: 'Products',
),
GridItems(
title: lang.S.of(context).dueList,
icon: 'assets/duelist.svg',
route: 'Due List',
),
GridItems(
title: lang.S.of(context).stockList,
icon: 'assets/h_stock.svg',
route: 'Stock',
),
GridItems(
title: lang.S.of(context).reports,
icon: 'assets/reports.svg',
route: 'Reports',
),
GridItems(
title: lang.S.of(context).saleList,
icon: 'assets/salelist.svg',
route: 'Sales List',
),
GridItems(
title: lang.S.of(context).purchaseList,
icon: 'assets/purchaseLisst.svg',
route: 'Purchase List',
),
GridItems(
// TODO: Shakil change this to `Profit & Loss`
title: lang.S.of(context).profitAndLoss,
icon: 'assets/h_lossProfit.svg',
route: 'Loss/Profit',
),
GridItems(
title: lang.S.of(context).ledger,
icon: 'assets/ledger.svg',
route: 'ledger',
),
GridItems(
title: lang.S.of(context).income,
icon: 'assets/h_income.svg',
route: 'Income',
),
GridItems(
title: lang.S.of(context).expense,
icon: 'assets/expense.svg',
route: 'Expense',
),
GridItems(
title: lang.S.of(context).vatAndTax,
icon: 'assets/tax.svg',
route: 'tax',
),
// GridItems(
// title: 'Warehouse',
// icon: 'assets/tax.svg',
// route: 'warehouse',
// ),
GridItems(
title: lang.S.of(context).customPrint,
icon: 'assets/printer.svg',
route: 'customPrint',
),
if (brunchPermission == true)
GridItems(
title: lang.S.of(context).branch,
icon: 'assets/branch.svg',
route: 'branch',
),
if (hrmPermission ?? false)
GridItems(
title: lang.S.of(context).hrm,
icon: 'assets/hrm/hrm.svg',
route: 'hrm',
),
];
return freeIcons;
}

132
lib/Screens/Home/home.dart Normal file
View File

@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:mobile_pos/Screens/DashBoard/dashboard.dart';
import 'package:mobile_pos/Screens/Home/home_screen.dart';
import 'package:mobile_pos/Screens/Report/reports.dart';
import 'package:mobile_pos/Screens/Settings/settings_screen.dart';
import 'package:mobile_pos/Screens/pos_sale/pos_sale.dart';
import 'package:mobile_pos/constant.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:mobile_pos/model/business_info_model.dart' as visible;
import '../../GlobalComponents/glonal_popup.dart';
import '../../Provider/profile_provider.dart';
import '../../service/check_actions_when_no_branch.dart';
class Home extends StatefulWidget {
const Home({super.key});
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int _tabIndex = 0;
late final PageController pageController = PageController(initialPage: _tabIndex);
@override
void dispose() {
pageController.dispose();
super.dispose();
}
void _handleNavigation(
int index,
BuildContext context,
) {
setState(() => _tabIndex = index);
pageController.jumpToPage(index);
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async =>
await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(lang.S.of(context).areYouSure),
content: Text(lang.S.of(context).doYouWantToExitTheApp),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(lang.S.of(context).no)),
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(lang.S.of(context).yes)),
],
),
) ??
false,
child: Consumer(builder: (context, ref, __) {
ref.watch(getExpireDateProvider(ref));
return GlobalPopup(
child: Scaffold(
body: PageView(
controller: pageController,
physics: const NeverScrollableScrollPhysics(),
onPageChanged: (v) => setState(() => _tabIndex = v),
children: [
HomeScreen(),
PosSaleScreen(),
DashboardScreen(),
Reports(),
SettingScreen(),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _tabIndex,
backgroundColor: Colors.white,
// onTap: (i) => _handleNavigation(i, context, visibility),
onTap: (i) => _handleNavigation(
i,
context,
),
items: [
_buildNavItem(index: 0, activeIcon: 'cHome', icon: 'home', label: lang.S.of(context).home),
_buildNavItem(
index: 1,
activeIcon: 'cPos',
icon: 'pos',
label: lang.S.of(context).pos,
),
_buildNavItem(
index: 2,
activeIcon: 'dashbord1',
icon: 'dashbord',
label: lang.S.of(context).dashboard,
),
_buildNavItem(
index: 3,
activeIcon: 'cFile',
icon: 'file',
label: lang.S.of(context).reports,
),
_buildNavItem(
index: 4,
activeIcon: 'cSetting',
icon: 'setting',
label: lang.S.of(context).setting,
),
],
type: BottomNavigationBarType.fixed,
selectedItemColor: kMainColor,
unselectedItemColor: kGreyTextColor,
selectedLabelStyle: const TextStyle(fontSize: 14),
unselectedLabelStyle: const TextStyle(fontSize: 14),
),
),
);
}),
);
}
BottomNavigationBarItem _buildNavItem(
{required int index, required String activeIcon, required String icon, required String label}) {
return BottomNavigationBarItem(
icon: _tabIndex == index
? SvgPicture.asset('assets/$activeIcon.svg', height: 28, width: 28, fit: BoxFit.scaleDown)
: SvgPicture.asset('assets/$icon.svg',
colorFilter: const ColorFilter.mode(kGreyTextColor, BlendMode.srcIn), height: 24, width: 24),
label: label,
);
}
}

View File

@@ -0,0 +1,500 @@
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:mobile_pos/Const/api_config.dart';
import 'package:mobile_pos/Repository/check_addon_providers.dart';
import 'package:mobile_pos/Screens/DashBoard/dashboard.dart';
import 'package:mobile_pos/Screens/Home/components/grid_items.dart';
import 'package:mobile_pos/Screens/Profile%20Screen/profile_details.dart';
import 'package:mobile_pos/core/theme/_app_colors.dart';
import 'package:mobile_pos/generated/l10n.dart' as lang;
import 'package:nb_utils/nb_utils.dart';
import 'package:restart_app/restart_app.dart';
import '../../Provider/profile_provider.dart';
import '../../constant.dart';
import '../../currency.dart';
import '../../service/check_actions_when_no_branch.dart';
import '../Customers/Provider/customer_provider.dart';
import '../DashBoard/global_container.dart';
import '../Home/Model/banner_model.dart' as b;
import '../../service/check_user_role_permission_provider.dart';
import '../branch/branch_list.dart';
import '../branch/repo/branch_repo.dart';
import '../subscription/package_screen.dart';
import 'Provider/banner_provider.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
PageController pageController = PageController(initialPage: 0, viewportFraction: 0.8);
bool _isRefreshing = false;
Future<void> refreshAllProviders({required WidgetRef ref}) async {
if (_isRefreshing) return; // Prevent multiple refresh calls
_isRefreshing = true;
try {
ref.refresh(summaryInfoProvider);
ref.refresh(bannerProvider);
ref.refresh(businessInfoProvider);
ref.refresh(partiesProvider);
ref.refresh(getExpireDateProvider(ref));
await Future.delayed(const Duration(seconds: 3));
} finally {
_isRefreshing = false;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Consumer(builder: (_, ref, __) {
final businessInfo = ref.watch(businessInfoProvider);
final summaryInfo = ref.watch(summaryInfoProvider);
final banner = ref.watch(bannerProvider);
final permissionService = PermissionService(ref);
return businessInfo.when(data: (details) {
final icons = getFreeIcons(
context: context,
hrmPermission: (details.data?.addons?.hrmAddon == true),
brunchPermission: (((details.data?.addons?.multiBranchAddon == true) &&
(details.data?.enrolledPlan?.allowMultibranch == 1) &&
(details.data?.user?.branchId == null)))
? true
: false);
return Scaffold(
backgroundColor: kBackgroundColor,
appBar: AppBar(
backgroundColor: kWhite,
titleSpacing: 5,
surfaceTintColor: kWhite,
actions: [
if ((details.data?.addons?.multiBranchAddon ?? false) && (details.data?.user?.activeBranch != null))
TextButton.icon(
label: Text(
'${details.data?.user?.activeBranch?.name}',
style: theme.textTheme.bodyMedium?.copyWith(color: kTitleColor),
),
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadiusGeometry.circular(2),
),
),
textStyle: WidgetStatePropertyAll(
theme.textTheme.bodyMedium?.copyWith(color: kTitleColor),
),
),
onPressed: () async {
if (details.data?.user?.branchId != null) {
return;
}
bool switchBranch = await BranchListScreen.switchDialog(context: context, isLogin: false);
if (switchBranch) {
EasyLoading.show();
final switched =
await BranchRepo().exitBranch(id: details.data?.user?.activeBranchId.toString() ?? '');
if (switched) {
Restart.restartApp();
}
EasyLoading.dismiss();
}
},
icon: SvgPicture.asset(
'assets/branch_icon.svg',
height: 16,
width: 16,
),
),
IconButton(onPressed: () async => refreshAllProviders(ref: ref), icon: const Icon(Icons.refresh))
],
leading: Padding(
padding: const EdgeInsets.all(10),
child: GestureDetector(
onTap: () {
const ProfileDetails().launch(context);
},
child: Container(
height: 50,
width: 50,
decoration: details.data?.pictureUrl == null
? BoxDecoration(
image:
const DecorationImage(image: AssetImage('images/no_shop_image.png'), fit: BoxFit.cover),
borderRadius: BorderRadius.circular(50),
)
: BoxDecoration(
image: DecorationImage(
image: NetworkImage('${APIConfig.domain}${details.data?.pictureUrl}'),
fit: BoxFit.cover),
borderRadius: BorderRadius.circular(50),
),
),
),
),
title: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
details.data?.user?.role == 'staff'
? '${details.data?.companyName ?? ''} [${details.data?.user?.name ?? ''}]'
: details.data?.companyName ?? '',
style: theme.textTheme.titleLarge?.copyWith(
fontSize: 18.0,
fontWeight: FontWeight.w500,
),
),
GestureDetector(
// onTap: () {
// showDialog(
// context: context,
// builder: (BuildContext context) {
// return goToPackagePagePopup(
// context: context,
// enrolledPlan: details.enrolledPlan);
// });
// },
child: Text.rich(
TextSpan(
text: '${details.data?.enrolledPlan?.plan?.subscriptionName ?? 'No Active'} Plan',
children: [
// if (details.enrolledPlan?.duration != null &&
// details.enrolledPlan!.duration! <= 7)
// TextSpan(
// text: ' (${getDayLeftInExpiring(
// expireDate: details.willExpire,
// shortMSG: false,
// )})',
// style: theme.textTheme.bodySmall?.copyWith(
// fontSize: 13,
// color: kPeraColor,
// ),
// ),
],
),
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 13,
color: kPeraColor,
fontWeight: FontWeight.w500,
)),
)
],
),
),
resizeToAvoidBottomInset: true,
body: RefreshIndicator.adaptive(
onRefresh: () async => refreshAllProviders(ref: ref),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (permissionService.hasPermission(Permit.dashboardRead.value)) ...{
summaryInfo.when(data: (summary) {
return Container(
padding: EdgeInsets.fromLTRB(16, 16, 16, 12),
decoration: BoxDecoration(
color: kMainColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
lang.S.of(context).quickOver,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 18,
color: kWhite,
),
),
),
GestureDetector(
onTap: () => Navigator.push(
context, MaterialPageRoute(builder: (context) => DashboardScreen())),
child: Text(
lang.S.of(context).viewAll,
style: theme.textTheme.bodySmall?.copyWith(color: kWhite, fontSize: 16),
),
)
],
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Flexible(
child: GlobalContainer(
minVerticalPadding: 0,
minTileHeight: 0,
titlePadding: EdgeInsets.zero,
// isShadow: true,
textColor: true,
title: lang.S.of(context).sales,
subtitle: '$currency${formatAmount(summary.data!.sales.toString())}',
),
),
Flexible(
child: GlobalContainer(
minVerticalPadding: 0,
minTileHeight: 0,
// isShadow: true,
textColor: true,
alainRight: true,
titlePadding: EdgeInsets.zero,
title: lang.S.of(context).purchased,
subtitle: '$currency${formatAmount(summary.data!.purchase.toString())}',
),
),
],
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Flexible(
child: GlobalContainer(
minVerticalPadding: 0,
textColor: true,
minTileHeight: 0,
titlePadding: EdgeInsets.zero,
title: lang.S.of(context).income,
subtitle: '$currency${formatAmount(summary.data!.income.toString())}',
),
),
Flexible(
child: GlobalContainer(
minVerticalPadding: 0,
minTileHeight: 0,
textColor: true,
alainRight: true,
titlePadding: EdgeInsets.zero,
title: lang.S.of(context).expense,
subtitle: '$currency${formatAmount(summary.data!.expense.toString())}',
),
),
],
),
],
),
);
}, error: (e, stack) {
return Text(e.toString());
}, loading: () {
return Center(
child: CircularProgressIndicator(),
);
}),
SizedBox(height: 16),
},
GridView.count(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
childAspectRatio: 3.0,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: List.generate(
icons.length,
(index) => HomeGridCards(
gridItems: icons[index],
),
),
),
const SizedBox(height: 20),
///________________Banner_______________________________________
banner.when(data: (imageData) {
List<b.Banner> images = [];
if (imageData.isNotEmpty) {
images.addAll(imageData.where(
(element) => element.status == 1,
));
}
if (images.isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lang.S.of(context).whatNew,
textAlign: TextAlign.start,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 12),
Container(
height: 150,
width: MediaQuery.of(context).size.width,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
),
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
itemCount: images.length,
itemBuilder: (_, index) {
return GestureDetector(
onTap: () {
const PackageScreen().launch(context);
},
child: Padding(
padding: EdgeInsetsDirectional.only(end: 10), // Spacing between items
child: ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Image.network(
"${APIConfig.domain}${images[index].imageUrl}",
width: MediaQuery.of(context).size.width * 0.7, // 80% width
fit: BoxFit.cover,
),
),
),
);
},
),
),
const SizedBox(height: 12),
],
);
} else {
return Center(
child: Container(
height: 150,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage('images/banner1.png'),
),
),
),
);
}
}, error: (e, stack) {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Center(
child: Text(
lang.S.of(context).noDataFound,
style: theme.textTheme.titleMedium,
//'No Data Found'
),
),
);
}, loading: () {
return const CircularProgressIndicator();
}),
],
),
),
),
));
}, error: (e, stack) {
return Center(child: Text(e.toString()));
}, loading: () {
return const Center(child: CircularProgressIndicator());
});
});
}
}
class HomeGridCards extends StatefulWidget {
const HomeGridCards({
super.key,
required this.gridItems,
// this.visibility,
});
final GridItems gridItems;
// final business.Visibility? visibility;
@override
State<HomeGridCards> createState() => _HomeGridCardsState();
}
class _HomeGridCardsState extends State<HomeGridCards> {
@override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, __) {
return GestureDetector(
onTap: () async {
bool result = await checkActionWhenNoBranch(context: context, actionName: widget.gridItems.title, ref: ref);
if (!result) {
return;
}
Navigator.of(context).pushNamed('/${widget.gridItems.route}');
},
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: kWhite, boxShadow: [
BoxShadow(
color: const Color(0xff171717).withOpacity(0.07),
offset: const Offset(0, 3),
blurRadius: 50,
spreadRadius: -4)
]),
child: Row(
children: [
SvgPicture.asset(
widget.gridItems.icon.toString(),
height: 40,
width: 40,
),
const SizedBox(
width: 8,
),
Flexible(
child: Text(
widget.gridItems.title.toString(),
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: DAppColors.kNeutral700),
overflow: TextOverflow.ellipsis,
maxLines: 1,
))
],
),
),
);
});
}
}
String getSubscriptionExpiring({required String? expireDate, required bool shortMSG}) {
if (expireDate == null) {
return shortMSG ? 'N/A' : lang.S.current.subscribeNow;
}
DateTime expiringDay = DateTime.parse(expireDate).add(const Duration(days: 1));
if (expiringDay.isBefore(DateTime.now())) {
return lang.S.current.expired;
}
if (expiringDay.difference(DateTime.now()).inDays < 1) {
return shortMSG
? '${expiringDay.difference(DateTime.now()).inHours}\n${lang.S.current.hoursLeft}'
: '${expiringDay.difference(DateTime.now()).inHours} ${lang.S.current.hoursLeft}';
} else {
return shortMSG
? '${expiringDay.difference(DateTime.now()).inDays}\n${lang.S.current.daysLeft}'
: '${expiringDay.difference(DateTime.now()).inDays} ${lang.S.current.daysLeft}';
}
}

View File

@@ -0,0 +1,41 @@
class IncomeCategory {
IncomeCategory({
this.id,
this.categoryName,
this.businessId,
this.categoryDescription,
this.status,
this.createdAt,
this.updatedAt,
});
IncomeCategory.fromJson(dynamic json) {
id = json['id'];
categoryName = json['categoryName'];
businessId = json['business_id'];
categoryDescription = json['categoryDescription'];
status = json['status'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
num? id;
String? categoryName;
num? businessId;
String? categoryDescription;
bool? status;
String? createdAt;
String? updatedAt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['id'] = id;
map['categoryName'] = categoryName;
map['business_id'] = businessId;
map['categoryDescription'] = categoryDescription;
map['status'] = status;
map['created_at'] = createdAt;
map['updated_at'] = updatedAt;
return map;
}
}

Some files were not shown because too many files have changed in this diff Show More