first commit
This commit is contained in:
6
lib/Const/api_config.dart
Normal file
6
lib/Const/api_config.dart
Normal 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';
|
||||
}
|
||||
190
lib/Const/lalnguage_data.dart
Normal file
190
lib/Const/lalnguage_data.dart
Normal 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';
|
||||
}
|
||||
150
lib/GlobalComponents/bar_code_scaner_widget.dart
Normal file
150
lib/GlobalComponents/bar_code_scaner_widget.dart
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
129
lib/GlobalComponents/button_global.dart
Normal file
129
lib/GlobalComponents/button_global.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/GlobalComponents/check_subscription.dart
Normal file
48
lib/GlobalComponents/check_subscription.dart
Normal 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,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
54
lib/GlobalComponents/glonal_popup.dart
Normal file
54
lib/GlobalComponents/glonal_popup.dart
Normal 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"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 don’t 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
55
lib/GlobalComponents/internet_connection_notifier.dart
Normal file
55
lib/GlobalComponents/internet_connection_notifier.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
44
lib/GlobalComponents/license_verifier.dart
Normal file
44
lib/GlobalComponents/license_verifier.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
31
lib/GlobalComponents/returned_tag_widget.dart
Normal file
31
lib/GlobalComponents/returned_tag_widget.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
387
lib/GlobalComponents/sales_transaction_widget.dart
Normal file
387
lib/GlobalComponents/sales_transaction_widget.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
122
lib/GlobalComponents/tab_buttons.dart
Normal file
122
lib/GlobalComponents/tab_buttons.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/GlobalComponents/url_lanuncer.dart
Normal file
32
lib/GlobalComponents/url_lanuncer.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
1008
lib/PDF Invoice/due_invoice_pdf.dart
Normal file
1008
lib/PDF Invoice/due_invoice_pdf.dart
Normal file
File diff suppressed because it is too large
Load Diff
221
lib/PDF Invoice/pdf_common_functions.dart
Normal file
221
lib/PDF Invoice/pdf_common_functions.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
1459
lib/PDF Invoice/purchase_invoice_pdf.dart
Normal file
1459
lib/PDF Invoice/purchase_invoice_pdf.dart
Normal file
File diff suppressed because it is too large
Load Diff
1868
lib/PDF Invoice/sales_invoice_pdf.dart
Normal file
1868
lib/PDF Invoice/sales_invoice_pdf.dart
Normal file
File diff suppressed because it is too large
Load Diff
592
lib/PDF Invoice/subscription_invoice_pdf.dart
Normal file
592
lib/PDF Invoice/subscription_invoice_pdf.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
lib/PDF Invoice/universal_image_widget.dart
Normal file
83
lib/PDF Invoice/universal_image_widget.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
lib/Provider/add_to_cart_purchase.dart
Normal file
161
lib/Provider/add_to_cart_purchase.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
10
lib/Provider/product_provider.dart
Normal file
10
lib/Provider/product_provider.dart
Normal 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);
|
||||
});
|
||||
20
lib/Provider/profile_provider.dart
Normal file
20
lib/Provider/profile_provider.dart
Normal 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);
|
||||
});
|
||||
7
lib/Provider/shop_category_provider.dart
Normal file
7
lib/Provider/shop_category_provider.dart
Normal 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());
|
||||
203
lib/Provider/transactions_provider.dart
Normal file
203
lib/Provider/transactions_provider.dart
Normal 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;
|
||||
}
|
||||
28
lib/Repository/API/business_category_repo.dart
Normal file
28
lib/Repository/API/business_category_repo.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
98
lib/Repository/API/business_info_repo.dart
Normal file
98
lib/Repository/API/business_info_repo.dart
Normal 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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
144
lib/Repository/API/business_info_update_repo.dart
Normal file
144
lib/Repository/API/business_info_update_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
146
lib/Repository/API/business_setup_repo.dart
Normal file
146
lib/Repository/API/business_setup_repo.dart
Normal 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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
24
lib/Repository/API/future_invoice.dart
Normal file
24
lib/Repository/API/future_invoice.dart
Normal 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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
39
lib/Repository/API/register_repo.dart
Normal file
39
lib/Repository/API/register_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
35
lib/Repository/check_addon_providers.dart
Normal file
35
lib/Repository/check_addon_providers.dart
Normal 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;
|
||||
}
|
||||
});
|
||||
17
lib/Repository/constant_functions.dart
Normal file
17
lib/Repository/constant_functions.dart
Normal 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);
|
||||
}
|
||||
94
lib/Repository/report_repository.dart
Normal file
94
lib/Repository/report_repository.dart
Normal 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));
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
211
lib/Screens/Authentication/Phone Auth/phone_OTP_screen.dart
Normal file
211
lib/Screens/Authentication/Phone Auth/phone_OTP_screen.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
214
lib/Screens/Authentication/Phone Auth/phone_auth_screen.dart
Normal file
214
lib/Screens/Authentication/Phone Auth/phone_auth_screen.dart
Normal 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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
lib/Screens/Authentication/Repo/licnese_repo.dart
Normal file
16
lib/Screens/Authentication/Repo/licnese_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
31
lib/Screens/Authentication/Repo/logout_repo.dart
Normal file
31
lib/Screens/Authentication/Repo/logout_repo.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
48
lib/Screens/Authentication/Repo/otp_settings_repo.dart
Normal file
48
lib/Screens/Authentication/Repo/otp_settings_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
82
lib/Screens/Authentication/Sign In/Repo/sign_in_repo.dart
Normal file
82
lib/Screens/Authentication/Sign In/Repo/sign_in_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
419
lib/Screens/Authentication/Sign In/sign_in_screen.dart
Normal file
419
lib/Screens/Authentication/Sign In/sign_in_screen.dart
Normal 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,
|
||||
//'Don’t 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(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/Screens/Authentication/Sign In/webview_login.dart
Normal file
109
lib/Screens/Authentication/Sign In/webview_login.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/Screens/Authentication/Sign Up/repo/sign_up_repo.dart
Normal file
118
lib/Screens/Authentication/Sign Up/repo/sign_up_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
260
lib/Screens/Authentication/Sign Up/sign_up_screen.dart
Normal file
260
lib/Screens/Authentication/Sign Up/sign_up_screen.dart
Normal 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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
251
lib/Screens/Authentication/Sign Up/verify_email.dart
Normal file
251
lib/Screens/Authentication/Sign Up/verify_email.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 !',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
89
lib/Screens/Authentication/check_email.dart
Normal file
89
lib/Screens/Authentication/check_email.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
140
lib/Screens/Authentication/forgot password/forgot_password.dart
Normal file
140
lib/Screens/Authentication/forgot password/forgot_password.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
165
lib/Screens/Authentication/forgot password/set_new_password.dart
Normal file
165
lib/Screens/Authentication/forgot password/set_new_password.dart
Normal 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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/Screens/Authentication/forgot_password.dart
Normal file
119
lib/Screens/Authentication/forgot_password.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
173
lib/Screens/Authentication/login_form.dart
Normal file
173
lib/Screens/Authentication/login_form.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
406
lib/Screens/Authentication/profile_setup_screen.dart
Normal file
406
lib/Screens/Authentication/profile_setup_screen.dart
Normal 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(),
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
221
lib/Screens/Authentication/register_screen.dart
Normal file
221
lib/Screens/Authentication/register_screen.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
lib/Screens/Authentication/success_screen.dart
Normal file
79
lib/Screens/Authentication/success_screen.dart
Normal 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());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
53
lib/Screens/Currency/Model/currency_model.dart
Normal file
53
lib/Screens/Currency/Model/currency_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
7
lib/Screens/Currency/Provider/currency_provider.dart
Normal file
7
lib/Screens/Currency/Provider/currency_provider.dart
Normal 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());
|
||||
63
lib/Screens/Currency/Repo/currency_repo.dart
Normal file
63
lib/Screens/Currency/Repo/currency_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
lib/Screens/Currency/currency_screen.dart
Normal file
129
lib/Screens/Currency/currency_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
263
lib/Screens/Customers/Model/parties_model.dart
Normal file
263
lib/Screens/Customers/Model/parties_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
7
lib/Screens/Customers/Provider/customer_provider.dart
Normal file
7
lib/Screens/Customers/Provider/customer_provider.dart
Normal 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());
|
||||
268
lib/Screens/Customers/Repo/parties_repo.dart
Normal file
268
lib/Screens/Customers/Repo/parties_repo.dart
Normal 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']}')));
|
||||
}
|
||||
}
|
||||
}
|
||||
981
lib/Screens/Customers/add_customer.dart
Normal file
981
lib/Screens/Customers/add_customer.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
721
lib/Screens/Customers/customer_details.dart
Normal file
721
lib/Screens/Customers/customer_details.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
775
lib/Screens/Customers/edit_customer.dart
Normal file
775
lib/Screens/Customers/edit_customer.dart
Normal 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)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
587
lib/Screens/Customers/party_list_screen.dart
Normal file
587
lib/Screens/Customers/party_list_screen.dart
Normal 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());
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/Screens/Customers/sms_sent_confirmation.dart
Normal file
119
lib/Screens/Customers/sms_sent_confirmation.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
175
lib/Screens/Customers/transaction_screen.dart
Normal file
175
lib/Screens/Customers/transaction_screen.dart
Normal 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,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/Screens/DashBoard/chart_data.dart
Normal file
7
lib/Screens/DashBoard/chart_data.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
class ChartData {
|
||||
ChartData(this.x, this.y, this.y1);
|
||||
|
||||
final String x;
|
||||
final double y;
|
||||
final double y1;
|
||||
}
|
||||
484
lib/Screens/DashBoard/dashboard.dart
Normal file
484
lib/Screens/DashBoard/dashboard.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
58
lib/Screens/DashBoard/global_container.dart
Normal file
58
lib/Screens/DashBoard/global_container.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
268
lib/Screens/DashBoard/numeric_axis.dart
Normal file
268
lib/Screens/DashBoard/numeric_axis.dart
Normal 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;
|
||||
}
|
||||
154
lib/Screens/DashBoard/test_numeric.dart
Normal file
154
lib/Screens/DashBoard/test_numeric.dart
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
144
lib/Screens/Due Calculation/Model/due_collection_model.dart
Normal file
144
lib/Screens/Due Calculation/Model/due_collection_model.dart
Normal 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;
|
||||
}
|
||||
29
lib/Screens/Due Calculation/Providers/due_provider.dart
Normal file
29
lib/Screens/Due Calculation/Providers/due_provider.dart
Normal 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));
|
||||
132
lib/Screens/Due Calculation/Repo/due_repo.dart
Normal file
132
lib/Screens/Due Calculation/Repo/due_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
475
lib/Screens/Due Calculation/due_collection_screen.dart
Normal file
475
lib/Screens/Due Calculation/due_collection_screen.dart
Normal 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());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
200
lib/Screens/Due Calculation/due_list_screen.dart
Normal file
200
lib/Screens/Due Calculation/due_list_screen.dart
Normal 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());
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/Screens/Expense/Model/expanse_category.dart
Normal file
41
lib/Screens/Expense/Model/expanse_category.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
96
lib/Screens/Expense/Model/expense_modle.dart
Normal file
96
lib/Screens/Expense/Model/expense_modle.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
23
lib/Screens/Expense/Providers/all_expanse_provider.dart
Normal file
23
lib/Screens/Expense/Providers/all_expanse_provider.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -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());
|
||||
68
lib/Screens/Expense/Repo/expanse_category_repo.dart
Normal file
68
lib/Screens/Expense/Repo/expanse_category_repo.dart
Normal 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')));
|
||||
}
|
||||
}
|
||||
}
|
||||
147
lib/Screens/Expense/Repo/expanse_repo.dart
Normal file
147
lib/Screens/Expense/Repo/expanse_repo.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
332
lib/Screens/Expense/add_erxpense.dart
Normal file
332
lib/Screens/Expense/add_erxpense.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
110
lib/Screens/Expense/add_expense_category.dart
Normal file
110
lib/Screens/Expense/add_expense_category.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
153
lib/Screens/Expense/expense_category_list.dart
Normal file
153
lib/Screens/Expense/expense_category_list.dart
Normal 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();
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
332
lib/Screens/Expense/expense_list.dart
Normal file
332
lib/Screens/Expense/expense_list.dart
Normal 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(),
|
||||
// );
|
||||
// })
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
33
lib/Screens/Home/Model/banner_model.dart
Normal file
33
lib/Screens/Home/Model/banner_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
7
lib/Screens/Home/Provider/banner_provider.dart
Normal file
7
lib/Screens/Home/Provider/banner_provider.dart
Normal 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());
|
||||
26
lib/Screens/Home/Repo/banner_repo.dart
Normal file
26
lib/Screens/Home/Repo/banner_repo.dart
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
64
lib/Screens/Home/components/bottom_nav.dart
Normal file
64
lib/Screens/Home/components/bottom_nav.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/Screens/Home/components/grid_items.dart
Normal file
113
lib/Screens/Home/components/grid_items.dart
Normal 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
132
lib/Screens/Home/home.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
500
lib/Screens/Home/home_screen.dart
Normal file
500
lib/Screens/Home/home_screen.dart
Normal 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}';
|
||||
}
|
||||
}
|
||||
41
lib/Screens/Income/Model/income_category.dart
Normal file
41
lib/Screens/Income/Model/income_category.dart
Normal 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
Reference in New Issue
Block a user