diff --git a/.vscode/launch.json b/.vscode/launch.json index 7bb90b4..f08c024 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,9 +2,14 @@ "version": "0.2.0", "configurations": [ { - "name": "Flutter", + "name": "Flutter Dev", "request": "launch", - "type": "dart" - } + "type": "dart", + "program": "lib/main-dev.dart", + "args": [ + "-t", + "lib/main-dev.dart", + ], + }, ] } \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index e1d4657..d406937 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -34,7 +34,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.mokkon.fcs_dev.fcs" - minSdkVersion 16 + minSdkVersion 21 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/assets/MyMMUnicodeUniversal.ttf b/assets/MyMMUnicodeUniversal.ttf new file mode 100644 index 0000000..128085b Binary files /dev/null and b/assets/MyMMUnicodeUniversal.ttf differ diff --git a/assets/address.png b/assets/address.png new file mode 100644 index 0000000..ae42728 Binary files /dev/null and b/assets/address.png differ diff --git a/assets/admin.png b/assets/admin.png new file mode 100644 index 0000000..294efd3 Binary files /dev/null and b/assets/admin.png differ diff --git a/assets/amount.png b/assets/amount.png new file mode 100644 index 0000000..5e3f1fc Binary files /dev/null and b/assets/amount.png differ diff --git a/assets/approve.png b/assets/approve.png new file mode 100644 index 0000000..a58d32f Binary files /dev/null and b/assets/approve.png differ diff --git a/assets/block.png b/assets/block.png new file mode 100644 index 0000000..9070fd0 Binary files /dev/null and b/assets/block.png differ diff --git a/assets/buyer.png b/assets/buyer.png new file mode 100644 index 0000000..881b91d Binary files /dev/null and b/assets/buyer.png differ diff --git a/assets/date_filter.png b/assets/date_filter.png new file mode 100644 index 0000000..537c2e3 Binary files /dev/null and b/assets/date_filter.png differ diff --git a/assets/device.png b/assets/device.png new file mode 100644 index 0000000..0d51338 Binary files /dev/null and b/assets/device.png differ diff --git a/assets/do.png b/assets/do.png new file mode 100644 index 0000000..7eda7e0 Binary files /dev/null and b/assets/do.png differ diff --git a/assets/email.png b/assets/email.png new file mode 100644 index 0000000..6015d56 Binary files /dev/null and b/assets/email.png differ diff --git a/assets/employee.png b/assets/employee.png new file mode 100644 index 0000000..d77f4c7 Binary files /dev/null and b/assets/employee.png differ diff --git a/assets/eng_flag.png b/assets/eng_flag.png new file mode 100755 index 0000000..e8049d9 Binary files /dev/null and b/assets/eng_flag.png differ diff --git a/assets/fonts/MyMMUnicodeUniversal.ttf b/assets/fonts/MyMMUnicodeUniversal.ttf new file mode 100644 index 0000000..128085b Binary files /dev/null and b/assets/fonts/MyMMUnicodeUniversal.ttf differ diff --git a/assets/fonts/OpenSans-Regular.ttf b/assets/fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..29bfd35 Binary files /dev/null and b/assets/fonts/OpenSans-Regular.ttf differ diff --git a/assets/fonts/Roboto-Regular.ttf b/assets/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2b6392f Binary files /dev/null and b/assets/fonts/Roboto-Regular.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-Black.ttf b/assets/fonts/Work_Sans/WorkSans-Black.ttf new file mode 100755 index 0000000..3f972d1 Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-Black.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-Bold.ttf b/assets/fonts/Work_Sans/WorkSans-Bold.ttf new file mode 100755 index 0000000..7718ada Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-Bold.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-ExtraBold.ttf b/assets/fonts/Work_Sans/WorkSans-ExtraBold.ttf new file mode 100755 index 0000000..e571099 Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-ExtraBold.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-ExtraLight.ttf b/assets/fonts/Work_Sans/WorkSans-ExtraLight.ttf new file mode 100755 index 0000000..3798fbe Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-ExtraLight.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-Light.ttf b/assets/fonts/Work_Sans/WorkSans-Light.ttf new file mode 100755 index 0000000..8b03227 Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-Light.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-Medium.ttf b/assets/fonts/Work_Sans/WorkSans-Medium.ttf new file mode 100755 index 0000000..137cf56 Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-Medium.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-Regular.ttf b/assets/fonts/Work_Sans/WorkSans-Regular.ttf new file mode 100755 index 0000000..ba11a2d Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-Regular.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-SemiBold.ttf b/assets/fonts/Work_Sans/WorkSans-SemiBold.ttf new file mode 100755 index 0000000..5cefd7c Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-SemiBold.ttf differ diff --git a/assets/fonts/Work_Sans/WorkSans-Thin.ttf b/assets/fonts/Work_Sans/WorkSans-Thin.ttf new file mode 100755 index 0000000..47a06ef Binary files /dev/null and b/assets/fonts/Work_Sans/WorkSans-Thin.ttf differ diff --git a/assets/gender.png b/assets/gender.png new file mode 100644 index 0000000..44e9de1 Binary files /dev/null and b/assets/gender.png differ diff --git a/assets/img/bg.jpg b/assets/img/bg.jpg new file mode 100644 index 0000000..993d5e4 Binary files /dev/null and b/assets/img/bg.jpg differ diff --git a/assets/img/login_logo.png b/assets/img/login_logo.png new file mode 100644 index 0000000..0531be9 Binary files /dev/null and b/assets/img/login_logo.png differ diff --git a/assets/img/logo.png b/assets/img/logo.png new file mode 100644 index 0000000..6254dd0 Binary files /dev/null and b/assets/img/logo.png differ diff --git a/assets/inventory.png b/assets/inventory.png new file mode 100644 index 0000000..5f1a3d1 Binary files /dev/null and b/assets/inventory.png differ diff --git a/assets/licence.png b/assets/licence.png new file mode 100644 index 0000000..a742f69 Binary files /dev/null and b/assets/licence.png differ diff --git a/assets/local/localization_en.json b/assets/local/localization_en.json new file mode 100644 index 0000000..6460c9b --- /dev/null +++ b/assets/local/localization_en.json @@ -0,0 +1,467 @@ +{ + "btn.save": "Save", + "btn.approve":"Approve", + + "product": "Product", + "price": "Price", + "volume": "Quantity", + "amount": "Amount", + "storage" :"Storage", + + "welcome.price.updateinfo":"Last updated on :{0}", + "welcome.price.trend":"Price trend", + "welcome.price.detail":"Details", + + "products.title":"Products", + "prodcuts":"Products", + "prices":"Prices", + "product.confirm":"Confirm to update product price?", + "products.prices":"Prices", + "products.gas":"Products", + "product.item":"Product", + "product.name":"Product Name", + "product.name_empty":"Please enter product name", + "product.new_price":"New Price", + "product.price_empty":"Please enter price", + "product.order":"Display Order", + "product.order_empty":"Please enter display order", + "product.balance_qty":"PO Balance Quantity by Product", + "product.qtys":"Balance Quantities", + "product.update.date":"Date", + "product.close": "Order is closed now.", + "product.open": "Order is opened at {0} to {1}\nOn {2}.", + "product.purchase.order": "Order", + + "login": "Login", + "login.title":"Login", + "login.btn":"Login", + "login.name":"Name", + "login.phone":"Phone Number", + "login.password":"Password", + "login.confirm_password":"Confirm Password", + "login.name_empty": "Please enter your name", + "login.phone_empty": "Please enter your phone number", + "login.phone_code": "Only starting '+959'", + "login.password_empty": "Please enter password", + "login.password_size": "At least 6 characters", + "login.password_mismatch": "Confirm password mismatch", + "login.confirm_password_empty": "Please enter confirm password", + "login.sms_empty":"Please enter sms code", + "login.sms_size": "Must be 6 characters", + "login.forgot_password":"Forgot Password?", + "login.email.title":"Recovery Email", + "login.email":"Email", + "login.email.empty":"Please Enter Email", + "login.email.add":"Add", + "login.email.skip":"Skip", + + "sing.up":"SIGN UP", + "sing.title":"Signup", + + "change.password.title":"Change Password", + "change":"CHANGE", + "change.phone":"Change Phone Number", + "change.new.phone":"New Phone Number", + "change.phone_empty": "Please enter new phone number", + "change.email":"Recovery Email", + "change.pin.title":"Add PIN Code", + "change.pin":"PIN Code", + "change.pin_empty":"Please enter PIN Code", + "change.pin_size":"Must be 6 characters", + "pin.add_btn":"UPDATE PIN", + "pin.clear_btn":"CLEAR PIN ", + "pin.switch":"Clear PIN", + + "forget.password":"Forgot Password", + "forget.email":"Email (or) Phone Number", + "forget.email.empty":"Please Enter Email (or) Phone Number", + "forget.enter":"Enter", + + "reset.password.title":"Reset Password", + "reset.sms":"SMS Code", + "reset.new_password":"New Password", + "reset":"RESET", + + "reg.title": "My Registration", + "reg.biz_name": "Business Name", + "reg.biz_address": "Business Address", + "reg.biz_shops": "Number of Shops", + "reg.biz_shop_addresses":"Shop Addresses", + "reg.type_shop": "Shop", + "reg.type_agent": "Agent", + "reg.status": "Status", + "reg.table_product": "Product", + "reg.table_storage_vol": "Storage Quantity", + "reg.table_sale_vol": "Daily Sale Quantity", + "reg.quota": "Daily Quota Quantity", + "reg.max_quota": "Max Quota Quantity", + "reg.quota.used": "Daily Quota Used Quantity", + "reg.max_quota.used": "Max Quota Used Quantity", + "reg.empty_biz_name": "Empty businuee name", + "reg.empty_biz_address":"Empty business address", + "reg.empty_shops":"Empty shops", + "reg_info.title": "My Registration Info", + "reg_info.nric_front": "NRIC Front", + "reg_info.nric_back": "NRIC Back", + "reg.confirm":"Submit Registration?", + "reg.date":"Registeration Date", + + "profile.title": "My Profile", + "profile.edit_title": "Edit My Profile", + "profile.name": "Name", + "profile.phone": "Phone", + "profile.language": "Languages", + "profile.logout": "logout", + "profile.logout.confirm":"Are you sure want to logout?", + "profile.devices":"Devices", + "profile.email":"Email", + "profile.privilege":"Privilege", + + "device.confirm":"Confirm this device?", + "device.logout":"Logout this device?", + "device.set_primary":"Set this primary device?", + + "po.title":"POs", + "po":"PO", + "po.approval":"PO Approval", + "po.date": "PO Date", + "po.number": "PO No.", + "po.name":"Buyer Name", + "po.po_num": "PO Number", + "po.biz":"Business name", + "po.status": "PO Status", + "po.receipt":"Receipts", + "po.po_payment":"PO Payment", + "po.po_payment_receipt":"Payment Receipt", + "po.storage_receipt":"Storage Receipt", + "po.no.photo": "No selected photo", + "po.storage_charge" :"Storage Charge", + "po.product": "Product", + "po.price": "Price", + "po.volume": "Volume", + "po.balance.volume":"Balance Volume", + "po.amount": "Amount", + "po.form.volume":"Please enter volume", + "po.comment":"Comment", + "po.confirm":"Submit Purchase Order?", + "po.approve.confirm":"Approve this PO?", + "po.cancel.confirm":"Cancel this PO?", + "po.counts":"PO Counts", + "po.details":"Details", + "po.count":"PO Count", + "po.balances":"PO Balance Quantity", + "po.info":"PO Info", + "po.retrieved.amount":"DO Balance\nQty", + "po.count.status":"PO Status", + "po.avail.qty":"PO Balance\nQty", + "po.total_count":"Total Count", + + "do.title":"DOs", + "do":"DO", + "do.do_date":"DO Date", + "do.date": "Delivery Date", + "do.licence": "Driver Licence", + "do.driver":"Driver Name", + "do.car":"Car No.", + "do.status": "DO Status", + "do.delivery.status": "Delivery Status", + "do.delivery.init.time":"Delivery Initiated At", + "do.do_num": "DO Number", + "do.type": "DO Type", + "do.name":"Buyer Name", + "do.biz":"Business name", + "do.form.date":"Please enter DO date", + "do.form.licence":"Please enter car licence", + "do.form.driver":"Please enter driver name", + "do.form.car":"Please enter car number", + "do.product": "Product", + "do.price": "Price", + "do.volume": "Volume", + "do.amount": "Amount", + "do.storage":"Storage", + "do.quantity":"Quantity", + "do.form.volume":"Please enter volume", + "do.form.amount":"Please enter amount", + "do.po_qty":"PO Qty", + "do.po_balance_qty":"PO Balance\nQty", + "do.do_qty":"DO Qty", + "do.po_num":"PO Number", + "do.driver.image":"Driver Image", + "do.driver.sign":"Driver Signature", + "do.receipt":"Delivery Receipt", + "do.no.photo":"No selected photo", + "do.storage_charge" :"Storage Charge", + "do.storage_receipt":"Storage Receipt", + "do.comment":"Comment", + "do.confirm":"Submit Delivery Order?", + "do.approve.confirm":"Approve this DO?", + "do.cancel.confirm":"Cancel this DO?", + "do.end.confirm":"End delivery?", + "do.receipt.title":"Delivery Receipt", + "do.single":"Single", + "do.multiple": "Multiple", + "do_qty":"DO Qty", + "do.product.title":"DO Product", + "do.cancel":"Cancel", + "do.enter":"Enter", + "do.qtys":"DO Qtys", + "do.details":"Details", + "do.counts":"DO Counts", + "do.count":"DO Count", + "do.products":"DO Products", + "do.count.status":"DO Status", + "do.total_count":"Total Count", + + "storage.name":"Storage Name", + "storage.form.name":"Please enter storage name", + "storage.date":"Date", + "storage.form.date":"Please enter date", + "storge": "Storage", + "storage.product.qty":"Quantity", + "storage.delete_confirm":"Delete storage?", + + "name": "Name", + "form.name":"Please enter name", + + + "input_sms": "Input SMS Code", + "sms.sms": "Enter SMS Code", + "sms.empty":"Please Enter SMS Code", + "sms.enter":"Enter", + "singin": "Sign In", + + "email.input": "Input Email Code", + "email.code":"Enter Email Code", + "email.code_empty":"Please Enter SMS Code", + + + "inventory.takings":"Inventory Takings", + "inventory.take":"Take Inventory", + "inventory.item":"Inventory", + "inventory.product": "Product", + "inventory.storage": "Storage", + "inventory.quantity": "Quantity", + "inventory.old.qty": "Old Quantity", + "inventory.new.qty": "New Quantity", + "inventory.form.qty":"Please enter quantity", + "inventory.confirm":"Submit Inventory Taking?", + + + "pd":"PD", + "pd.product.title":"PD Product", + "pd.date" :"Date", + "pd.product": "Product", + "pd.storage": "Storage", + "pd.quantity": "Quantity", + "pd.form.quan":"Please enter quantity", + "pd.confirm":"Submit Purchase Delivery?", + + "buyer.title":"Buyers", + "buyer.type_biz":"Business Type", + "buyer.account_name":"Account Name", + "buyer.phone_number":"Phone Number", + "buyer.quota":"Daily Quota", + "buyer.max.quota":"Maximum Quota", + "buyer.name":"Buyer", + "buyer.product":"Product", + "buyer.balQty":"Balance Volume", + "buyer.approve.confirm":"Confirm to approve buyer?", + "buyer.delete.confirm":"Delete buyer request?", + "buyer.allocate.quota.confirm":"Allocate Quota?", + + "term.agree_btn":"Agree", + "term.iagree":"I agree on terms and condition.", + + "noti.title":"Notifications", + "log.title":"Logs", + + "document.log.title":"Document Logs", + "document.date":"Date", + "document.by":"By", + "document.desc":"Description", + + "contact.title":"Contacts", + "contact.phone.title":"Input Phone Number", + "contact.phone":"Phone Number", + "contact.phone.confim":"Call {0}?", + "contact.phone.empty":"Please enter phone number", + "contact.email":"Email", + "contact.email.configm":"Email to '{0}'?", + "contact.email.empty":"Please enter email", + "contact.facebook":"Facebook", + "contact.facebook.empty":"Please enter facebook url", + "contact.google":"Website", + "contact.google.empty":"Please enter website url", + "contact.confrim":"Confirm update contact?", + "contact.open.confrim":"Open '{0}'?", + "contact.bank.accounts":"Bank Accounts Info", + "contact.address":"Address", + "contact.delivery.phone":"Delivery Phone", + + "term.title":"Terms", + "manual.title":"Manual", + "myreg.title":"My\nRegistration", + "storage.title":"Storages", + "storage.item.title":"Storage", + "pd.title":"PDs", + + "employee.title":"Employees", + "employee.item.title":"Employee", + "employee.phone":"Phone Number", + "employee.add":"Add", + "employee.save":"Save", + + "setting.title":"Settings", + "setting.confirm":"Confirm update setting?", + + "users.title":"Users", + "user.title":"User", + "user.save":"Save", + "user.block.confirm":"Confirm to block this user?", + "user.block_list":"Block lists", + "user.unblock.confirm":"Confirm to unblock this user?", + + "banks.title":"Bank Accounts", + "banks.edit.title":"Bank Account", + "banks.name":"Bank Name", + "banks.account.name":"Account Name", + "banks.account.number":"Account Number", + "banks.account.delete.confirmation":"Delete Bank Account?", + + + "delivery.title":"Deliveries", + "delivery":"Delivery", + "delivery.confirm":"End delivery?", + "delivery.detail":"Details", + "delivery.date":"Date", + "delivery.qty":"Quantity", + "delivery.do.title":"Delivery DO", + "delivery.do.details":"Details", + "delivery.do.counts":"Delivery DO Counts", + "delivery.do.count":"Delivery DO Count", + "delivery.do.summary":"Delivery DO Summary ", + "delivery.do.sum.counts":"Delivery DO Summary Counts", + "delivery.days":"Days", + "delivery.summary":"Delivery Summary", + "delivery.sum.amounts":"Delivery Summary Amounts", + + "delivery.data.title":"Deliveries", + + "delivery.detail.title":"{0} Deliveries", + + "chart.daily.title":"Daily Quota ({0} Liters)", + "chart.max.title": "Maximum Quota ({0} Liters)", + "chart.remaining": "remaining", + "chart.used": "used", + "chart.revenue":"Revenue", + "chart.spending":"Spending", + "chart.date":"Date", + "chart.30_days":"(Last 30 days)", + + "revenue.amounts":"Revenue Amounts", + "revenue.date":"Date", + "revenue.amount":"Amount", + "revenue.detail":"Details", + "revenue.detail.title":"{0} Revenue Amounts", + + "spending.amounts":"Spending Amounts", + "spending.detail.title":"{0} Spending Amounts", + + "load": "Loading...", + + "Cancel": "Cancel", + "Ok": "Ok", + + "singup": "Sing Up", + "buyer.reg": "Buyer Registeration", + "po.sub": "PO Submission", + "do.sub": "DO Submission", + "inventory.taking": "Inventory Taking", + "do.approved": "DO approved", + "po.approved": "PO approved", + "buyer.approved": "Buyer approved", + "purchase.delivery": "Purchase Delivery", + "do.delivery": "DO Delivery", + "product.price": "Product Price", + + "Click 'Initiate Delivery' button": "Click 'Initiate Delivery' button", + "Click 'Start Delivery'": "Click 'Start Delivery'", + + "1":"1", + "2":"2", + "3":"3", + + "signup.slide.initial":"1.Click Arrow button", + "signup.slide.s1": "1.Select 'Signup' button \n2.Enter all data info\n3.Click 'SIGNUP' button", + "signup.slide.s2" : "1.Enter SMS code\n2.Click 'Sign In' button", + "signup.slide.s3": "1.Click 'Agree' button", + "signup.slide.s4": "1.Click 'Ok' button", + "signup.slide.s5": "Finish SignUp", + + "login.slide.s1":"1.Select 'Login' button\n2.Enter Phone Number and Password\n3.Click 'LOGIN' button", + + "buyerreg.slide.s1":"1.Click 'My Registration' button", + "buyerreg.slide.s2":"1.Enter all data info\n2.Click Sent button", + "buyerreg.slide.s4":"Finished Registration", + + "posub.slide.s1": "1.Click 'POs'", + "posub.slide.s2": "1.Click '+' button", + "posub.slide.s3":"1.Click '+' button\n2.Click Sent button", + "posub.slide.s4":"1.Enter Volume and select Product\n2.Click 'Save' button", + "posub.slide.s6": "Finished PO Submission", + + "dosub.slide.s1": "1.Click 'More' button", + "dosub.slide.s2": "Click 'Create DO'", + "dosub.slide.s4" : "1.Enter all data info\n2.Click Sent button", + "dosub.slide.s6": "Finished DO Submission", + + "storage.slide.s1": "Click Storages", + "storage.slide.s3" : "1.Enter storage name\n2.Click 'Save' button", + + "inventory.taking.s1": "Click Inventory Takings", + "inventory.taking.s4":"1.Select storage name , product and enter quantity\n2.Click 'Save' button,", + "inventory.taking.s5": "1.Click Sent button", + "inventory.taking.s7": "Finished Inventory Taking", + + "poapproved.slide.s1": "Select you want to approve PO", + "poapproved.slide.s3": "1.Click Approve PO", + "poapproved.slide.s5": "Finished PO Approved", + + "doapproved.slide.s1": "Select you want to approve DO", + "doapproved.slide.s3": "1.Select storage name", + "doapproved.slide.s6": "Finished DO Approved", + "doapproved.slide.s5": "1.Click 'More' button\n2.Click Approve DO", + + "buyerapproved.slide.s1": "1.Select New Buyer Registration", + "buyerapproved.slide.s3": "1.Click 'Approve'", + + "product.slide.s1": "1.Click 'Products'", + "product.slide.s2": "1.Click pencil button", + "product.slide.s3": "1.Enter product info\n2.Click 'Save' button", + + + "pd.slide.s1": "1.Click 'PDs'", + "pd.slide.s5": "1.Click Sent button\n2.Click 'Ok' button", + + "dodelivery.slide.s1": "Select DO, want to delivery", + + "Login": "Login", + "Signup": "Signup", + + "forget.pass" : "Forgot Password?", + "manual.confirm": "Are you sure want to delete?", + + "offline.status":"Offline, unable to connect to server!", + + "report.title":"Reports", + "report.user_delete_confirm":"Delete this report user?", + "report.user.search":"Search", + "report.users.title":"{0} Users", + + "announcement.title":"Announcements", + "announcement.form.title":"Announcement", + "announcement.name":"Subject", + "announcement.name_empty":"Please enter subject", + "announcement.delete_confirm":"Delete announcement?", + "announcement.desc":"Description" +} \ No newline at end of file diff --git a/assets/local/localization_mu.json b/assets/local/localization_mu.json new file mode 100644 index 0000000..2842df0 --- /dev/null +++ b/assets/local/localization_mu.json @@ -0,0 +1,500 @@ +{ + "btn.save":"သိမ်းဆည်းရန်", + "btn.approve":"အတည်ပြုရန်", + + "product": "ကုန်ပစ္စည်း", + "price": "ဈေးနှုန်း", + "volume": "လီတာ", + "amount": "ပမာဏ", + "storage" :"သိုလှောင်", + + "welcome.price.updateinfo":"နောက်ဆုံး စျေးပြောင်းချိန် : {0}", + "welcome.price.trend":"စျေးလှုပ်ရှားမှု", + "welcome.price.detail":"အသေးစိတ်", + + "products.title":"ဆီအမျိုးအစားများ", + "prodcuts":"ကုန်ပစ္စည်းများ", + "prices":"ဈေးနှုန်းများ", + "product.confirm":"ဆီဈေးနှုန်းပြင်ရန်သေချာပြီလား?", + "products.prices":"ဈေးနှုန်းများ", + "products.gas":"ဆီအမျိုးအစား", + "product.item":"ဆီအမျိုးအစား", + "product.name":"ကုန်ပစ္စည်း အမည်", + "product.name_empty":"ကျေးဇူးပြု၍ ကုန်ပစ္စည်းနာမည်ပေးပါ", + "product.new_price":"စျေးနှုန်းအသစ်", + "product.price_empty":"ကျေးဇူးပြု၍ စျေးနှုန်းကိုထည့်ပါ", + "product.order":"အမှာစာ", + "product.order_empty":"ကျေးဇူးပြု၍ အမှာစာကိုထည့်ပါ", + "product.balance_qty":"ဆီအဝယ်ထုတ်ရန်\nလက်ကျန်အရေအတွက်", + "product.qtys":"လက်ကျန်အရေအတွက်များ", + "product.update.date":"ရက်စွဲ", + "product.close": "ယခု ဆီအဝယ် ပိတ်ပါသည်။", + "product.open": "ဆီအဝယ် ကို {0} မှ {1}\n{2} နေ့များတွင် ဖွင့်ပါသည်။", + "product.purchase.order": "ဝယ်မည်", + + "login.title":"အကောင့်ဒ်၀င်ရန်", + "login.btn":"ဝင်မည်", + "login.name":"နာမည်", + "login.phone":"ဖုန်းနံပါတ်", + "login.password":"စကားဝှက်", + "login.confirm_password":"အတည်ပြုစကားဝှက်", + "login.name_empty": "ကျေးဇူးပြု၍ နာမည်ထည့်ပေးပါ", + "login.phone_empty": "ကျေးဇူးပြု၍ သင့်ဖုန်းနံပါတ်ထည့်ပါ", + "login.phone_code": "'+ 959'သာစတင်သည်", + "login.password_empty": "ကျေးဇူးပြု၍ စကားဝှက်ကိုရိုက်ထည့်ပါ", + "login.password_size": "အနည်းဆုံးစာလုံး ၆ လုံး", + "login.confirm_password_empty": "အတည်ပြုစကားဝှက်ကိုရိုက်ထည့်ပါ", + "login.sms_empty":"ကျေးဇူးပြုပြီး sms ကုဒ်ကိုထည့်ပါ", + "login.forgot_password":"စကားဝှက်ကိုမေ့နေပါသလား?", + "login.email.title":"Account ပြန်ယူသော အီးမေးလ်", + "login.email":"အီးမေးလ်", + "login.email.empty":"ကျေးဇူးပြု၍အီးမေးလ်ထည့်ပါ", + "login.email.add":"ထည့်ရန်", + "login.email.skip":"ကျော်ရန်", + + "sing.up":"အသစ်ပြုလုပ်မည်", + "sing.title":"အသစ်ပြုလုပ်ရန်", + + "change.password.title":"စကားဝှက်ကိုပြောင်းရန်", + "change":"ပြောင်းမည်", + "change.phone":"ဖုန်းနံပါတ်ပြောင်းရန်", + "change.new.phone":"ဖုန်းနံပါတ်အသစ်", + "change.phone_empty": "ကျေးဇူးပြု၍ ဖုန်းနံပါတ်အသစ်ထည့်ပါ", + "change.email":"Account ပြန်ယူသော အီးမေးလ်", + "change.pin.title":"ပင်နံပါတ်ထည့်ရန်", + "change.pin":"ပင်နံပါတ်", + "change.pin_empty":"ကျေးဇူးပြု၍ ပင်နံပါတ်ကိုရိုက်ထည့်ပါ", + "change.pin_size":"စာလုံး ၆ လုံးဖြစ်ရမည်", + "pin.add_btn":"ပင်ထည့်မည်", + "pin.clear_btn":"ပင်ရှင်းမည် ", + "pin.switch":"ပင်နံပါတ်ရှင်းရန်", + + "forget.password":"စကားဝှက်ကိုပြင်ရန်", + "forget.email":"အီးမေးလ်(သို့) ဖုန်းနံပါတ်", + "forget.email.empty":"ကျေးဇူးပြု၍အီးမေးလ်(သို့)ဖုန်းနံပါတ်ထည့်ပါ", + "forget.enter":"ဝင်မည်", + + "reset.password.title":"စကားဝှက်ကိုပြန်လုပ်ရန်", + "reset.sms":"SMS ကုဒ်", + "reset.new_password":"စကားဝှကိအသစ်", + "reset":"ပြင်မည်", + + "reg.title":"ကိုယ်ရေးအချက်အလက်", + "reg.biz_name":"လုပ်ငန်း အမည်", + "reg.biz_address":"လုပ်ငန်း လိပ်စာ", + "reg.biz_shops":"ဆိုင်အရေအတွက်", + "reg.biz_shop_addresses":"ဆိုင်လိပ်စာများ", + "reg.type_shop":"ကိုယ်ပိုင်", + "reg.type_agent":"ကိုယ်စားလှယ်", + "reg.status": "အခြေအနေ", + "reg.table_product":"ကုန်ပစ္စည်း", + "reg.table_storage_vol":"သိုလှောင်", + "reg.table_sale_vol":"ရောင်းအား", + "reg.quota": "နေ့စဉ်ခွဲတမ်း", + "reg.max_quota": "အများဆုံးခွဲတမ်း", + "reg.quota.used": "နေ့စဉ်\nအသုံးပြုပြီးခွဲတမ်း", + "reg.max_quota.used": "အများဆုံး\nအသုံးပြုပြီးခွဲတမ်း", + "reg.empty_biz_name": "Empty biz name", + "reg.empty_biz_address":"Empty biz address", + "reg.empty_shops":"Empty shops", + "reg_info.nric_front": "မှတ်ပုံတင် ရှေ့ခြမ်း", + "reg_info.nric_back": "မှတ်ပုံတင် နောက်ခြမ်း", + "reg_info.title": "ကိုယ်ရေးအချက်အလက်", + "reg.confirm":"မှတ်ပုံတင်သွင်းမည်လား?", + "reg.date":"မှတ်ပုံတင်သည့် နေ့စွဲ", + + "profile.title":"ပရိုဖိုင်", + "profile.edit_title":"ပရိုဖိုင်ကိုပြုပြင်ရန်", + "profile.name":"နာမည်", + "profile.phone": "ဖုန်းနံပါတ်", + "profile.language": "ဘာသာစကားများ", + "profile.logout": "အကောင့်ထွက်ရန်", + "profile.logout.confirm":"အကောင့်ထွက်ရန်သေချာပြီလား?", + "profile.devices":"ဖုန်းမော်ဒယ်အမျိုးအစားများ", + "profile.email":"အီးမေးလ်", + "profile.privilege":"လုပ်ပိုင်ခွင့်", + + "device.confirm":"ဒီဖုန်းမော်ဒယ်ကိုအတည်ပြုမည်လား?", + "device.logout":"ဒီဖုန်းမော်ဒယ်ကိုထွက်ရန်သေချာပြီလား?", + "device.set_primary":"ဒီဖုန်းမော်ဒယ်ကိုမူလမော်ဒယ်ထည့်ရန်သေချာပြီလား?", + + "po.title":"ဆီအဝယ်များ", + "po":"ဆီအဝယ်", + "po.approval":"ကုန်ပစ္စည်းအမှာစာအချက်အလက်", + "po.date" :"ဆီအဝယ် ရက်စွဲ", + "po.number":"အဝယ် နံပါတ်", + "po.name":"နာမည်", + "po.po_num": "ဆီအဝယ် နံပါတ်", + "po.biz":"လုပ်ငန်း အမည်", + "po.status":"ဆီအဝယ် အခြေအနေ", + "po.receipt":"လက်ခံဖြတ်ပိုင်းများ", + "po.po_payment":"ဆီအဝယ် ပေးဆောင်ခြင်း", + "po.po_payment_receipt":"ပေးဆောင်ခြင်းလက်ခံဖြတ်ပိုင်း", + "po.storage_receipt":"သိုလှောင် လက်ခံဖြတ်ပိုင်း", + "po.no.photo": "ပုံများရွေးချယ်ထားခြင်းမရှိပါ", + "po.storage_charge" :"သိုလှောင်ခ", + "po.product": "ကုန်ပစ္စည်း", + "po.price": "ဈေးနှုန်း", + "po.volume": "လီတာ", + "po.balance.volume":"လက်ကျန်လီတာ", + "po.amount": "ပမာဏ", + "po.form.volume":"ကျေးဇူးပြု၍ လီတာပမာဏကိုထည့်ပါ", + "po.comment":"မှတ်ချက်", + "po.confirm":"ဆီအဝယ်တင်သွင်းမည်လား?", + "po.approve.confirm":"ဆီဝယ်ခြင်းအတည်ပြုမည်လား?", + "po.cancel.confirm":"ဆီဝယ်ခြင်းပယ်ဖျက်မည်လား?", + "po.counts":"ဆီအဝယ် အရေအတွက်များ", + "po.details":"အသေးစိတ်", + "po.count":"အရေအတွက်", + "po.balances":"ဆီအဝယ်လက်ကျန်အရေအတွက်", + "po.info":"ဆီအဝယ်အချက်အလက်များ", + "po.retrieved.amount":"ဆီထုတ်လက်ကျန်\nအရေအတွက်", + "po.count.status":"ဆီအဝယ်\nအခြေအနေ", + "po.avail.qty":"အဝယ်လက်ကျန်\nအရေအတွက်", + "po.total_count":"စုစုပေါင်းအရေအတွက်", + + "do.title":"ဆီအထုတ်များ", + "do":"ဆီအထုတ်", + "do.do_date":"ဆီအထုတ်\nတင်သွင်းသည့်ရက်စွဲ", + "do.date": "ဆီအထုတ် ရက်စွဲ", + "do.licence": "ယာဉ်မောင်းလိုင်စင်", + "do.driver":"ယာဉ်မောင်း ", + "do.car":"ကားနံပါတ်", + "do.status": "ဆီအထုတ်များ အခြေအနေ", + "do.delivery.status": "ဆီပေးပို့မှု အခြေအနေ", + "do.delivery.init.time":"Delivery Initiated At", + "do.do_num": "ဆီအထုတ် နံပါတ်", + "do.type": "ဆီအထုတ်\nအမျိုးအစား", + "do.name":"နာမည်", + "do.biz":"လုပ်ငန်း အမည်", + "do.form.date":"ကျေးဇူးပြု၍ ဆီအထုတ် နေ့စွဲရိုက်ထည့်ပါ", + "do.form.licence":"ကျေးဇူးပြု၍ ကားလိုင်စင်ထည့်ပါ", + "do.form.driver":"ကျေးဇူးပြု၍ ယာဉ်မောင်းအမည်ထည့်ပါ", + "do.form.car":"ကျေးဇူးပြုပြီးကားနံပါတ်ထည့်ပါ", + "do.product": "ကုန်ပစ္စည်း", + "do.price": "ဈေးနှုန်း", + "do.volume": "လီတာ", + "do.amount": "ပမာဏ", + "do.storage":"သိုလှောင်", + "do.quantity":"အရေအတွက်", + "do.form.volume":"ကျေးဇူးပြု၍ လီတာပမာဏကိုထည့်ပါ", + "do.form.amount":"ကျေးဇူးပြု၍ ငွေပမာဏ ကိုထည့်ပါ", + "do.po_qty":"အဝယ်\nအရေအတွက်", + "do.po_balance_qty":"အဝယ်လက်ကျန်\nအရေအတွက်", + "do.do_qty":"အထုတ်\nအရေအတွက်", + "do.po_num":"အဝယ် နံပါတ်", + "do.driver.image":"Driver Image", + "do.driver.sign":"Driver Signature", + "do.receipt":"ဆီအထုတ် လက်ခံဖြတ်ပိုင်း", + "do.no.photo":"ပုံများရွေးချယ်ထားခြင်းမရှိပါ", + "do.storage_charge" :"သိုလှောင်ခ", + "do.storage_receipt":"သိုလှောင်လက်ခံဖြတ်ပိုင်း", + "do.comment":"မှတ်ချက်", + "do.confirm":"ဆီအထွက်တင်သွင်းမည်လား?", + "do.approve.confirm":"ဆီထုတ်ခြင်းအတည်ပြုမည်လား?", + "do.cancel.confirm":"ဆီဝယ်ခြင်းပယ်ဖျက်မည်လား?", + "do.end.confirm":"ပေးပို့ခြင်းအဆုံးသတ်မည်လား?", + "do.receipt.title":"ဆီအထုတ် လက်ခံဖြတ်ပိုင်း", + "do.single":"အကုန်ထုတ်", + "do.multiple": "ခွဲထုတ်", + "do_qty":"ဆီအထုတ်အရေအတွက်", + "do.product.title":"ဆီအထုတ်ကုန်ပစ္စည်း", + "do.cancel":"ပယ်ဖျက်မည်", + "do.enter":"ဝင်မည်", + "do.qtys":"DO အရေအတွက်များ", + "do.details":"အသေးစိတ်", + "do.counts":"ဆီအထုတ် အရေအတွက်များ", + "do.count":"အရေအတွက်", + "do.products":"ဆီအထုတ် ကုန်ပစ္စည်းများ", + "do.count.status":"ဆီအထုတ်\nအခြေအနေ", + "do.total_count":"စုစုပေါင်းအရေအတွက်", + + "storage.name":"သိုလှောင် အမည်", + "storage.form.name":"ကျေးဇူးပြု၍ သိုလှောင် အမည်ကိုထည့်ပါ", + "storage.date":"ရက်စွဲ", + "storage.form.date":"ကျေးဇူးပြု၍ ရက်စွဲ ကိုထည့်ပါ", + "storge": "သိုလှောင်", + "storage.product.qty":"အရေအတွက်", + "storage.delete_confirm":"သိုလှောင် ကိုဖျက်မည်လား?", + + "name": "နာမည်", + "form.name":"နာမည်ကိုရိုက်ထည့်ပေးပါ", + + "input_sms": "SMS ကုဒ်ကိုရိုက်ထည့်ရန်", + "sms.sms": "SMS ကုဒ်ကိုရိုက်ထည့်ပါ", + "sms.empty":"ကျေးဇူးပြု၍ SMS Code ထည့်ပါ", + "sms.enter":"ဝင်မည်", + "singin": "ဝင်မည်", + + "email.input": "အီးမေးလ် ကုဒ်ကိုရိုက်ထည့်ရန်", + "email.code":"အီးမေးလ် ကုဒ်ကိုရိုက်ထည့်ပါ", + "email.code_empty":"ကျေးဇူးပြု၍ အီးမေးလ်ကုဒ် ထည့်ပါ", + + "inventory.takings":"စာရင်းများ", + "inventory.take":"စာရင်းယူခြင်း", + "inventory.item":"စာရင်း", + "inventory.product": "ကုန်ပစ္စည်း", + "inventory.storage": "သိုလှောင်", + "inventory.quantity": "အရေအတွက်", + "inventory.old.qty": "အဟောင်းအရေအတွက်", + "inventory.new.qty": "အသစ်အရေအတွက်", + "inventory.form.qty":"ကျေးဇူးပြု၍ အရေအတွက် ကိုထည့်ပါ", + "inventory.confirm":"စာရင်းရယူခြင်းတင်သွင်းမည်လား?", + + "pd":"ဆီအဝင်", + "pd.product.title":"ဆီအဝင်ကုန်ပစ္စည်း", + "pd.date" :"ရက်စွဲ", + "pd.product": "ကုန်ပစ္စည်း", + "pd.storage": "သိုလှောင်", + "pd.quantity": "အရေအတွက်", + "pd.form.quan":"ကျေးဇူးပြု၍ အရေအတွက် ကိုထည့်ပါ", + "pd.confirm":"ဆီအဝင်အတည်ပြုမည်လား?", + + "buyer.title":"ဝယ်ယူသူ", + "buyer.type_biz":"လုပ်ငန်း အမျိုးအစား", + "buyer.account_name":"အကောင့်ဒ် နာမည်", + "buyer.phone_number":"ဖုန်းနံပါတ်", + "buyer.quota":"ခွဲတမ်း", + "buyer.max.quota":"အများဆုံးခွဲတမ်း", + "buyer.name":"ဝယ်သူ", + "buyer.product":"ကုန်ပစ္စည်း", + "buyer.balQty":"လက်ကျန်လီတာ", + "buyer.approve.confirm":"ဝယ်ယူသူအတည်ပြုရန်သေချာပြီလား?", + "buyer.delete.confirm":"ဝယ်ယူသူပယ်ဖျက်မည်လား?", + "buyer.allocate.quota.confirm":"ခွဲတမ်းသတ်မှတ်မည်လား?", + + "term.agree_btn":"သဘောတူပါသည်", + "term.iagree":"ကျွန်တော် သဘောတူပါသည်", + + "noti.title":"အသိပေးချက်များ", + "log.title":"မှတ်တမ်းများ", + + "document.log.title":"မှတ်တမ်းများ", + "document.date":"ရက်စွဲ", + "document.by":"အားဖြင့်", + "document.desc":"ဖော်ပြချက်", + + "contact.title":"ဆက်သွယ်ရန်", + "contact.phone.title":"ဖုန်းနံပါတ်ထည့်သွင်းရန်", + "contact.phone":"ဖုန်းနံပါတ်", + "contact.phone.confim":"ဖုန်းနံပါတ် '{0}' ကို ခေါ်မလား?", + "contact.phone.empty":"ကျေးဇူးပြု၍ ဖုန်းနံပါတ်ထည့်ပါ", + "contact.email":"အီးမေးလ်", + "contact.email.configm":"အီးမေးလ် '{0}' ကိုပို့ မလား?", + "contact.email.empty":"ကျေးဇူးပြု၍ အီးမေးလ်ထည့်ပါ", + "contact.facebook":"ဖေ့စ်ဘွတ်ခ်", + "contact.facebook.empty":"ကျေးဇူးပြုပြီး ဖေ့စ်ဘွတ်ခ် လင့်ခ် ကိုရိုက်ထည့်ပါ", + "contact.google":"ဝဘ်ဆိုက်", + "contact.google.empty":"ကျေးဇူးပြုပြီး ဝဘ်ဆိုက် လင့်ခ် ကိုရိုက်ထည့်ပါ", + "contact.confrim":"ဆက်သွယ်ခြင်း ကိုပြင်မည်လား?", + "contact.open.confrim":"'{0}' ကိုဖွင့် မလား?", + "contact.bank.accounts":"ဘဏ်အကောင့်အချက်အလက်", + "contact.address":"လိပ်စာ", + "contact.delivery.phone":"သီလဝါ ဆီထုတ် ဖုန်းနံပါတ်", + + + "term.title":"စည်းကမ်းချက်များ", + "manual.title":"လက်စွဲစာအုပ်", + "myreg.title":"ကိုယ်ရေး\nအချက်အလက်", + "storage.title":"သိုလှောင်ကန်များ", + "storage.item.title":"သိုလှောင်ကန်", + "pd.title":"ဆီအဝင်များ", + + "employee.title":"ဝန်ထမ်းများ", + "employee.item.title":"ဝန်ထမ်း", + "employee.phone":"ဖုန်းနံပါတ်", + "employee.add":"ထည့်မည်", + "employee.save":"သိမ်းဆည်းမည်", + + "setting.title":"ချိန်ညှိချက်များ", + "setting.confirm":"ချိန်ညှိချက်များ ကိုပြင်မည်လား?", + + "users.title":"အသုံးပြုသူများ", + "user.title":"အသုံးပြုသူ", + "user.save":"သိမ်းဆည်းမည်", + "user.block.confirm":"ဤအသုံးပြုသူအားတားဆီးရန်အတည်ပြုမည်လား?", + "user.block_list":"တားမြစ်ထားသူများ", + "user.unblock.confirm":"တားမြစ်ထားသူအားပယ်ဖျက်မည်လား?", + + "banks.title":"ဘဏ်အကောင့်များ", + "banks.edit.title":"ဘဏ်အကောင့်", + "banks.name":"ဘဏ် အမည်", + "banks.account.name":"ဘဏ်အကောင့် အမည်", + "banks.account.number":"အကောင့် နံပါတ်", + "banks.account.delete.confirmation":"Delete Bank Account?", + + "delivery.title":"ပေးပို့ရန်များ", + "delivery":"ပေးပို့ရန်", + "delivery.confirm":"ပေးပို့ခြင်း အဆုံးသတ်မည်လား?", + "delivery.detail":"အသေးစိတ်", + "delivery.date":"ရက်စွဲ", + "delivery.qty":"ပမာဏ", + "delivery.do.title":"ဆီအထုတ်ပေးပို့ခြင်း", + "delivery.do.details":"အသေးစိတ်", + "delivery.do.counts":"ဆီအထုတ်အရေအတွက်များ", + "delivery.do.count":"အရေအတွက်များ", + "delivery.do.summary":"ဆီအထုတ်ပေးပို့ခြင်း\nအကျဉ်းချုပ်", + "delivery.do.sum.counts":"ဆီအထုတ်ပေးပို့ခြင်း အကျဉ်းချုပ်", + "delivery.days":"ရက်ပေါင်း", + "delivery.summary":"ဆီအထုတ် အကျဉ်းချုပ်", + "delivery.sum.amounts":"ဆီအထုတ် အကျဉ်းချုပ် ပမာဏများ", + + "delivery.data.title":"ပေးပို့ရန်များ", + + "delivery.detail.title":"{0} ပေးပို့ရန်များ", + + "chart.daily.title":"နေ့စဉ် ခွဲတမ်း ({0} လီတာ)", + "chart.max.title": "အများဆုံး ခွဲတမ်း ({0} လီတာ)", + "chart.remaining": "လက်ကျန်", + "chart.used": "အသုံးပြုပြီး", + "chart.revenue":"ဝင်ငွေ", + "chart.spending":"အသုံးစရိတ်", + "chart.date":"ရက်စွဲ", + "chart.30_days":"(နောက်ဆုံး ၃၀ရက်)", + + "revenue.amounts":"ဝင်ငွေပမာဏများ", + "revenue.date":"ရက်စွဲ", + "revenue.amount":"ပမာဏ", + "revenue.detail":"အသေးစိတ်", + "revenue.detail.title":"{0} ဝင်ငွေပမာဏများ", + + "spending.amounts":"အသုံးစရိတ် ပမာဏများ", + "spending.detail.title":"{0} အသုံးစရိတ် ပမာဏများ", + + "load": "လုပ်ဆောင်နေပါသည်...", + + "Cancel": "မလုပ်နဲ့", + "Ok": "အိုကေ", + + "singup": "အကောင့်ဒ်အသစ်ပြုလုပ်ခြင်း", + "login": "အကောင့်ဒ်၀င်ရန်", + "buyer.reg": "ဝယ်ယူသူမှတ်ပုံတင်ခြင်း", + "po.sub": "ဆီအဝယ် တင်သွင်းခြင်း", + "do.sub": "ဆီအထုတ် တင်သွင်းခြင်း", + "inventory.taking": "စာရင်းယူခြင်း", + "do.approved": "ဆီအထုတ်အတည်ပြုခြင်း", + "po.approved": "ဆီအဝယ်အတည်ပြုခြင်း", + "buyer.approved": "ဝယ်ယူသူအတည်ပြုခြင်း", + "purchase.delivery": "ကုန်ပစ္စည်း​​​​ဝယ်ယူခြင်း", + "do.delivery": "ကုန်ပစ္စည်း​​​​ပို့ခြင်း", + "product.price": "ဆီဈေးနှုန်း", + + + "1.Select 'Signup' button": "၁. 'Signup'ကိုရွေးပါ", + "2.Enter all data info": "၂.အချက်အလက်များကိုထည့်ပါ", + "3.Click 'SIGNUP' button": "၃.'SIGNUP'ခလုတ်ကိုနှိပ်ပါ", + "1.Enter SMS code": "၁.ကုဒ်နံပါတ်ရိုက်ထည့်ပါ", + "2.Click 'Sign In' button": "၁.'ဝင်မည်'ခလုတ်ကိုနှိပ်ပါ", + "Click 'Ok' button": "'အိုကေ'ခလုတ်ကိုနှိပ်ပါ", + "2.Click 'Ok' button": "၂.'အိုကေ'ခလုတ်ကိုနှိပ်ပါ", + "1.Select 'Login' button": "၁.'ဝင်မည်'ခလုတ်ကိုရွေးပါ", + "2.Enter Phone Number and Password": "၂.ဖုန်းနံပါတ်နှင့်လျှို့ဝှက်နံပါတ်ကိုရိုက်ထည့်ပါ", + "3.Click 'LOGIN' button": "၃.'ဝင်မည်' ခလုတ်ကိုနှိပ်ပါ", + "1.Enter all data info": "၁.အချက်အလက်များကိုထည့်ပါ", + "2.Click Sent button": "၂.Sentခလုတ်ကိုနှိပ်ပါ", + "1.Click Sent button": "၁.Sentခလုတ်ကိုနှိပ်ပါ", + "1.Click 'POs'": "၁. 'ဆီအဝယ်များ'ကိုနှိပ်ပါ", + "1.Enter Volume and select Product": "၁.ပမာဏ နှင့် ကုန်ပစ္စည်းကိုရွေးပါ", + "2.Click 'Save' button": "၂. 'သိမ်းဆည်းမည်'ခလုတ်ကိုနှိပ်ပါ", + "Finished DO Submission": "ဆီအထုတ်တင်ပြချက်ပြီးဆုံးသည်", + "Click Storages": "သိုလှောင်ကန်များကို ရွေးပါ", + "1.Enter storage name": "၁.သိုလှောင်ကန်အမည်ကိုရွေးပါ", + "Click Inventory Takings": "စာရင်းယူခြင်းခလုတ်ကိုနှိပ်ပါ", + "1.Select storage name , product and enter quantity": "၁.သိုလှောင်ကန်အမည်, ကုန်ပစ္စည်းနှင့် ပမာဏကိုထည့်ပါ", + "Finished Inventory Taking": "စာရင်းယူခြင်းပြီးဆုံးသည်", + "Select you want to approve PO": "အတည်ပြုမည့်ဆီအဝယ်ကိုရွေးပါ", + "Click Approve PO": "1.Approve POကိုနှိပ်ပါ", + "Finished PO Approved": " ဆီအဝယ်အတည်ပြုခြင်းပြီးဆုံးသည်", + "Select you want to approve DO": "အတည်ပြုမည့်ဆီအထုတ်ကိုရွေးပါ", + "Select storage name": "၁. သိုလှောင်ကန်အမည်ကိုရွေးပါ", + "2.Click Approve DO": "၂.Approve DO ကိုနှိပ်ပါ", + "Finished DO Approved": "ဆီအထုတ် အတည်ပြုခြင်းပြီးဆုံးသည်", + "Select New Buyer Registration": "၁.New Buyer Registrationကိုရွေးပါ", + "Click 'Approve'": "၁.'Approve'ကိုနှိပ်ပါ", + "Click 'Products'": "'ဆီဈေးနှုန်း'ကိုနှိပ်ပါ", + "Click pencil button": "ခဲတံခလုတ်ကိုနှိပ်ပါ", + "1.Enter product info": "၁.ကုန်ပစ္စည်း အချက်အလက်များကိုထည့်ပါ", + "Select DO, want to delivery": "ပို့မည့်ဆီအထုတ်ကိုရွေးပါ", + "Click 'Initiate Delivery' button": "'Initiate Delivery' ခလုတ်ကိုနှိပ်ပါ", + "Click 'Start Delivery'": "'Start Delivery'ကိုနှိပ်ပါ", + "1.Click '+' button": "၁.'+'ခလုတ်ကိုနှိပ်ပါ", + "Click 'PDs'": "'ဆီအဝင်များ'ကိုနှိပ်ပါ" , + "Click 'Sent' button": "Sentခလုတ်ကိုနှိပ်ပါ", + + "1":"၁", + "2":"၂", + "3":"၃", + + "signup.slide.initial":"၁. မြှားခလုတ်ကိုနှိပ်ပါ", + "signup.slide.s1": "၁. 'အသစ်ပြုလုပ်ရန်'ကိုရွေးပါ\n၂. အချက်အလက်များကိုထည့်ပါ\n၃. 'အသစ်ပြုလုပ်မည်'ခလုတ်ကိုနှိပ်ပါ", + "signup.slide.s2" : "၁. ကုဒ်နံပါတ်ရိုက်ထည့်ပါ\n၂. 'ဝင်မည်'ခလုတ်ကိုနှိပ်ပါ", + "signup.slide.s3": "၁.'သဘောတူပါသည်'ခလုတ်ကိုနှိပ်ပါ", + "signup.slide.s4": "၁. 'အိုကေ'ခလုတ်ကိုနှိပ်ပါ", + "signup.slide.s5": "အကောင့်ဒ်အသစ်ပြုလုပ်ခြင်းပြီးဆုံးသည်", + + "login.slide.s1":"၁. 'အကောင့်ဒ်၀င်ရန်'ခလုတ်ကိုရွေးပါ\n၂. ဖုန်းနံပါတ်နှင့်စကားဝှက်ကိုရိုက်ထည့်ပါ\n၃. 'ဝင်မည်' ခလုတ်ကိုနှိပ်ပါ", + + "buyerreg.slide.s1":"၁. 'ကိုယ်ရေးအချက်အလက်'ခလုတ်ကိုနှိပ်ပါ", + "buyerreg.slide.s2":"၁. အချက်အလက်များကိုထည့်ပါ\n၂. Sentခလုတ်ကိုနှိပ်ပါ", + "buyerreg.slide.s4":"ဝယ်သူမှတ်ပုံတင်ခြင်းပြီးဆုံးသည်", + + "posub.slide.s1": "၁. 'ဆီအဝယ်များ'ကိုနှိပ်ပါ", + "posub.slide.s2": "၁. '+'ခလုတ်ကိုနှိပ်ပါ", + "posub.slide.s3":"၁. '+'ခလုတ်ကိုနှိပ်ပါ\n၂. Sentခလုတ်ကိုနှိပ်ပါ", + "posub.slide.s4":"၁. ပမာဏ နှင့် ကုန်ပစ္စည်းကိုရွေးပါ\n၂. 'သိမ်းဆည်းမည်'ခလုတ်ကိုနှိပ်ပါ", + "posub.slide.s6": "ဆီအဝယ်တင်ပြချက်ပြီးဆုံးသည်", + + "dosub.slide.s1": "၁.'More' ခလုတ်ကိုနှိပ်ပါ", + "dosub.slide.s2": "၁.'Create DO'ကိုနှိပ်ပါ'", + "dosub.slide.s4" : "၁. အချက်အလက်များကိုထည့်ပါ\n၂.Sentခလုတ်ကိုနှိပ်ပါ", + "dosub.slide.s6": "ဆီအထုတ်တင်ပြချက်ပြီးဆုံးသည်", + + "storage.slide.s1": "Click Storages", + "storage.slide.s3" : "၁. သိုလှောင်ကန်အမည်ကိုရွေးပါ\n၂. 'သိမ်းဆည်းမည်'ခလုတ်ကိုနှိပ်ပါ", + + "inventory.taking.s1": "Click Inventory Takings", + "inventory.taking.s4":"၁.သိုလှောင်ကန်အမည်, ကုန်ပစ္စည်းနှင့် ပမာဏကိုထည့်ပါ\n၂. 'သိမ်းဆည်းမည်'ခလုတ်ကိုနှိပ်ပါ", + "inventory.taking.s5": "1.Click Sent button", + "inventory.taking.s7": "Finished Inventory Taking", + + "poapproved.slide.s1": "Select you want to approve PO", + "poapproved.slide.s3": "1.Click Approve PO", + "poapproved.slide.s5": "Finished PO Approved", + + "doapproved.slide.s1": "Select you want to approve DO", + "doapproved.slide.s3": "1.Select storage name", + "doapproved.slide.s6": "Finished DO Approved", + "doapproved.slide.s5": "၁.'More' ခလုတ်ကိုနှိပ်ပါ\n၂.Approve DO ကိုနှိပ်ပါ", + + "buyerapproved.slide.s1": "1.Select New Buyer Registration", + "buyerapproved.slide.s3": "1.Click 'Approve'", + + "product.slide.s1": "1.Click 'Products'", + "product.slide.s2": "1.Click pencil button", + "product.slide.s3": "1.Enter product info\n2.Click 'Save' button", + + + "pd.slide.s1": "1.Click 'PDs'", + "pd.slide.s5": "1.Click Sent button\n2.Click 'Ok' button", + + "dodelivery.slide.s1": "Select DO, want to delivery", + + "Signup": "အသစ်ပြုလုပ်ရန်", + "Login": "အကောင့်ဒ်၀င်ရန်", + "forget.pass" : "စကားဝှက်မေ့နေပါသလား?", + "manual.confirm": "Are you sure want to delete?", + + "offline.status":"အင်တာနက် ပိတ်ထားသည်, ဆာဗာကို ဆက်သွယ်၍ မရပါ!", + + "report.title":"Reports", + "report.user_delete_confirm":"ဤအစီရင်ခံစာအသုံးပြုသူအားဖျက်မည်လား?", + "report.user.search":"Search", + "report.users.title":"{0} အသုံးပြုသူများ", + + "announcement.title":"ကြေငြာချက်များ", + "announcement.form.title":"ကြေငြာချက်", + "announcement.name":"အကြောင်းအရာ", + "announcement.name_empty":"ကျေးဇူးပြု၍ အကြောင်းအရာ အမည်ပေးပါ", + "announcement.delete_confirm":"ကြေငြာချက်ကိုဖျက်မည်လား?", + "announcement.desc":"ဖော်ပြချက်" +} \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..6254dd0 Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/logo1 (copy).png b/assets/logo1 (copy).png new file mode 100644 index 0000000..6254dd0 Binary files /dev/null and b/assets/logo1 (copy).png differ diff --git a/assets/logo_title.png b/assets/logo_title.png new file mode 100644 index 0000000..3ad5663 Binary files /dev/null and b/assets/logo_title.png differ diff --git a/assets/manual.png b/assets/manual.png new file mode 100644 index 0000000..6b9b54d Binary files /dev/null and b/assets/manual.png differ diff --git a/assets/message.png b/assets/message.png new file mode 100644 index 0000000..e8c3a17 Binary files /dev/null and b/assets/message.png differ diff --git a/assets/my_file.json b/assets/my_file.json new file mode 100644 index 0000000..e69de29 diff --git a/assets/myan_flag.png b/assets/myan_flag.png new file mode 100755 index 0000000..a3ea115 Binary files /dev/null and b/assets/myan_flag.png differ diff --git a/assets/page.png b/assets/page.png new file mode 100755 index 0000000..e608240 Binary files /dev/null and b/assets/page.png differ diff --git a/assets/password.png b/assets/password.png new file mode 100644 index 0000000..d0a7e97 Binary files /dev/null and b/assets/password.png differ diff --git a/assets/pay.png b/assets/pay.png new file mode 100644 index 0000000..384d33c Binary files /dev/null and b/assets/pay.png differ diff --git a/assets/pdo.png b/assets/pdo.png new file mode 100644 index 0000000..9203800 Binary files /dev/null and b/assets/pdo.png differ diff --git a/assets/phone.png b/assets/phone.png new file mode 100644 index 0000000..5e8b0f5 Binary files /dev/null and b/assets/phone.png differ diff --git a/assets/pin.png b/assets/pin.png new file mode 100644 index 0000000..33de204 Binary files /dev/null and b/assets/pin.png differ diff --git a/assets/product.png b/assets/product.png new file mode 100644 index 0000000..45e3497 Binary files /dev/null and b/assets/product.png differ diff --git a/assets/product_icon.png b/assets/product_icon.png new file mode 100644 index 0000000..356510c Binary files /dev/null and b/assets/product_icon.png differ diff --git a/assets/quota.png b/assets/quota.png new file mode 100644 index 0000000..b5d057d Binary files /dev/null and b/assets/quota.png differ diff --git a/assets/r.jpg b/assets/r.jpg new file mode 100644 index 0000000..1ca666a Binary files /dev/null and b/assets/r.jpg differ diff --git a/assets/reg.png b/assets/reg.png new file mode 100644 index 0000000..01a2763 Binary files /dev/null and b/assets/reg.png differ diff --git a/assets/report.png b/assets/report.png new file mode 100644 index 0000000..f5ea278 Binary files /dev/null and b/assets/report.png differ diff --git a/assets/sales.png b/assets/sales.png new file mode 100644 index 0000000..e4ab266 Binary files /dev/null and b/assets/sales.png differ diff --git a/assets/status.png b/assets/status.png new file mode 100644 index 0000000..d038e3f Binary files /dev/null and b/assets/status.png differ diff --git a/assets/storage.png b/assets/storage.png new file mode 100644 index 0000000..686572c Binary files /dev/null and b/assets/storage.png differ diff --git a/assets/storage12.png b/assets/storage12.png new file mode 100644 index 0000000..167f698 Binary files /dev/null and b/assets/storage12.png differ diff --git a/assets/submission.png b/assets/submission.png new file mode 100644 index 0000000..8c3a7ea Binary files /dev/null and b/assets/submission.png differ diff --git a/assets/term.png b/assets/term.png new file mode 100644 index 0000000..f460533 Binary files /dev/null and b/assets/term.png differ diff --git a/assets/truck.png b/assets/truck.png new file mode 100644 index 0000000..e750a8e Binary files /dev/null and b/assets/truck.png differ diff --git a/assets/volume.png b/assets/volume.png new file mode 100644 index 0000000..8549179 Binary files /dev/null and b/assets/volume.png differ diff --git a/assets/whole.png b/assets/whole.png new file mode 100644 index 0000000..6254dd0 Binary files /dev/null and b/assets/whole.png differ diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..320de7a --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/buyer_model.dart'; +import 'package:fcs/model/delivery_model.dart'; +import 'package:fcs/model/manual_model.dart'; +import 'package:fcs/model/notification_model.dart'; +import 'package:fcs/model/pd_model.dart'; +import 'package:fcs/model/reg_model.dart'; +import 'package:fcs/model/report_model.dart'; +import 'package:fcs/model/storage_model.dart'; +import 'package:fcs/model/test_model.dart'; +import 'package:fcs/pages/email_page.dart'; +import 'package:fcs/pages/login_page.dart'; + +import 'model/announcement_model.dart'; +import 'model/chart_model.dart'; +import 'model/device_model.dart'; +import 'model/do_model.dart'; +import 'model/employee_model.dart'; +import 'model/language_model.dart'; +import 'model/log_model.dart'; +import 'model/main_model.dart'; +import 'model/po_model.dart'; +import 'model/product_model.dart'; +import 'model/report_user_model.dart'; +import 'model/user_model.dart'; +import 'pages/home_page.dart'; +import 'pages/splash.dart'; +import 'pages/term.dart'; +import 'pages/welcome_page.dart'; +import 'widget/localization/app_translations_delegate.dart'; +import 'widget/localization/transalation.dart'; + +class App extends StatefulWidget { + @override + _AppState createState() => _AppState(); +} + +class _AppState extends State { + final MainModel mainModel = new MainModel(); + final UserModel userModel = new UserModel(); + final ProductModel productModel = new ProductModel(); + final EmployeeModel employeeModel = new EmployeeModel(); + final POSubmissionModel poSubmissionModel = new POSubmissionModel(); + final DOModel doModel = new DOModel(); + final LanguageModel lanuguageModel = new LanguageModel(); + final StorageModel storageModel = new StorageModel(); + final PDModel pdModel = new PDModel(); + final RegModel regModel = new RegModel(); + final BuyerModel buyerModel = new BuyerModel(); + final NotificationModel notificationModel = new NotificationModel(); + final ChartModel chartModel = new ChartModel(); + final DeliveryModel deliveryModel = new DeliveryModel(); + final ManualModel manualModel = new ManualModel(); + final TestModel testModel = new TestModel(); + final LogModel logModel = new LogModel(); + final PhoneDeviceModel phoneDeviceModel = new PhoneDeviceModel(); + final ReportModel reportModel = new ReportModel(); + final AnnouncementModel announcementModel = new AnnouncementModel(); + final ReportUserModel reportUserModel = new ReportUserModel(); + + AppTranslationsDelegate _newLocaleDelegate; + + @override + void initState() { + super.initState(); + _newLocaleDelegate = AppTranslationsDelegate(newLocale: null); + Translation().onLocaleChanged = onLocaleChange; + mainModel + ..addModel(userModel) + ..addModel(employeeModel) + ..addModel(lanuguageModel) + ..addModel(storageModel) + ..addModel(regModel) + ..addModel(poSubmissionModel) + ..addModel(doModel) + ..addModel(productModel) + ..addModel(pdModel) + ..addModel(buyerModel) + ..addModel(notificationModel) + ..addModel(chartModel) + ..addModel(deliveryModel) + ..addModel(logModel) + ..addModel(manualModel) + ..addModel(phoneDeviceModel) + ..addModel(regModel) + ..addModel(announcementModel) + ..addModel(reportModel) + ..addModel(testModel) + ..addModel(reportUserModel); + this.mainModel.init(); + } + + void onLocaleChange(Locale locale) { + setState(() { + _newLocaleDelegate = AppTranslationsDelegate(newLocale: locale); + }); + } + + Map route(BuildContext context) { + final routes = { + '/': (_) => SplashScreen(), + '/home': (_) => HomePage(), + '/welcome': (context) => WelcomePage(), + '/term': (context) => Term( + agreePage: true, + ), + '/login': (context) => LoginPage(), + '/email': (context) => EmailPage() + }; + return routes; + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(builder: (context) => mainModel), + ChangeNotifierProvider(builder: (context) => userModel), + ChangeNotifierProvider(builder: (context) => productModel), + ChangeNotifierProvider(builder: (context) => employeeModel), + ChangeNotifierProvider(builder: (context) => poSubmissionModel), + ChangeNotifierProvider(builder: (context) => doModel), + ChangeNotifierProvider(builder: (context) => storageModel), + ChangeNotifierProvider(builder: (context) => pdModel), + ChangeNotifierProvider(builder: (context) => lanuguageModel), + ChangeNotifierProvider(builder: (context) => regModel), + ChangeNotifierProvider(builder: (context) => buyerModel), + ChangeNotifierProvider(builder: (context) => notificationModel), + ChangeNotifierProvider(builder: (context) => chartModel), + ChangeNotifierProvider(builder: (context) => deliveryModel), + ChangeNotifierProvider(builder: (context) => manualModel), + ChangeNotifierProvider(builder: (context) => logModel), + ChangeNotifierProvider(builder: (context) => deliveryModel), + ChangeNotifierProvider(builder: (context) => phoneDeviceModel), + ChangeNotifierProvider(builder: (context) => reportModel), + ChangeNotifierProvider(builder: (context) => announcementModel), + ChangeNotifierProvider(builder: (context) => reportUserModel), + ChangeNotifierProvider( + builder: (context) => testModel, + ), + ], + child: Consumer( + builder: (BuildContext context, LanguageModel value, Widget child) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Ok Energy', + routes: route(context), + theme: ThemeData(accentColor: Colors.black), + localizationsDelegates: [ + _newLocaleDelegate, + //provides localised strings + GlobalMaterialLocalizations.delegate, + //provides RTL support + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: Translation().supportedLocales()); + }, + ), + ); + } +} diff --git a/lib/charts/bar_chart.dart b/lib/charts/bar_chart.dart new file mode 100644 index 0000000..4421669 --- /dev/null +++ b/lib/charts/bar_chart.dart @@ -0,0 +1,119 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:charts_flutter/flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/charts/qtyby_customer_table.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/local_text.dart'; + +class BarChart extends StatefulWidget { + @override + _BarChartState createState() => _BarChartState(); +} + +class _BarChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + List chartSummary = new List(); + List> series; + + @override + void initState() { + super.initState(); + var chartModel = Provider.of(context, listen: false); + this.chartSummary = chartModel.chartSummary; + } + + @override + Widget build(BuildContext context) { + var productModel = Provider.of(context); + if (this.chartSummary.isNotEmpty) { + this.chartSummary.forEach((s) { + productModel.products.forEach((p) { + if (p.id == s.productID) { + s.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + this.chartSummary + .sort((s1, s2) => s1.displayOrder.compareTo(s2.displayOrder)); + } + List> series = [ + charts.Series( + id: "Subscribers", + data: this.chartSummary, + domainFn: (POChartData series, _) => series.productName, + measureFn: (POChartData series, _) => series.balanceQty, + colorFn: (POChartData series, _) => + charts.ColorUtil.fromDartColor(series.getColor), + labelAccessorFn: (POChartData series, _) => + '${numberFormatter.format(series.balanceQty)}'), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, 'product.balance_qty', + color: primaryColor, fontSize: 16), + IconButton( + icon: Icon( + Icons.refresh, + color: primaryColor, + ), + onPressed: () { + _load(); + }, + ) + ], + ), + Expanded( + child: charts.BarChart( + series, + animate: true, + vertical: false, + defaultRenderer: new charts.BarRendererConfig( + barRendererDecorator: new charts.BarLabelDecorator( + labelPosition: charts.BarLabelPosition.auto, + ), + ), + selectionModels: [ + SelectionModelConfig(changedListener: (SelectionModel model) { + final selectedDatum = model.selectedDatum; + if (selectedDatum.isNotEmpty) { + selectedDatum.forEach((charts.SeriesDatum datumPair) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => QtyByCustomerTable( + poChartData: datumPair.datum, + )), + ); + }); + } + }) + ], + ), + ), + ], + ), + ); + } + + Future _load() async { + var chartModel = Provider.of(context); + var _s = await chartModel.loadSummary(); + setState(() { + this.chartSummary = _s ?? []; + }); + } +} diff --git a/lib/charts/delivery_do_line.dart b/lib/charts/delivery_do_line.dart new file mode 100644 index 0000000..f9f53d9 --- /dev/null +++ b/lib/charts/delivery_do_line.dart @@ -0,0 +1,83 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; + +import 'delivery_do_line_detail.dart'; + +class DODeliveryLineChart extends StatefulWidget { + @override + _DODeliveryLineChartState createState() => _DODeliveryLineChartState(); +} + +class _DODeliveryLineChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + List> series = [ + charts.Series( + id: "Subscribers", + data: chartModel.revenue.getDeliveryDo(), + domainFn: (Data series, _) => series.date, + measureFn: (Data series, _) => series.count, + colorFn: (_, __) => charts.ColorUtil.fromDartColor(primaryColor), + labelAccessorFn: (Data series, _) => + '${numberFormatter.format(series.count)}', + ), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, "delivery.do.title", color: primaryColor, fontSize: 16), + InkWell( + child: LocalText( + context, + "delivery.do.details", + color: secondaryColor, + fontSize: 14, + ), + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => DODeliveryLineDetail())); + }, + ), + ], + ), + Expanded( + child: charts.TimeSeriesChart( + series, + animate: true, + defaultRenderer: new charts.LineRendererConfig( + includePoints: true, + ), + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + zeroBound: false, desiredTickCount: 10), + renderSpec: new charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + ))), + ), + ), + ], + ), + ); + } +} diff --git a/lib/charts/delivery_do_line_detail.dart b/lib/charts/delivery_do_line_detail.dart new file mode 100644 index 0000000..b8ba3cb --- /dev/null +++ b/lib/charts/delivery_do_line_detail.dart @@ -0,0 +1,85 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; + +class DODeliveryLineDetail extends StatefulWidget { + const DODeliveryLineDetail(); + @override + _DODeliveryLineDetailState createState() => _DODeliveryLineDetailState(); +} + +class _DODeliveryLineDetailState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'delivery.do.counts', + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 20), + child: MyDataTable( + columnSpacing: 100, + columns: [ + MyDataColumn(label: LocalText(context, "delivery.date")), + MyDataColumn(label: LocalText(context, "delivery.do.count")), + ], + rows: getProductRow(chartModel.revenue.getDeliveryDo()), + ), + ), + ), + ), + ), + ); + } + + List getProductRow(List doList) { + return doList.map((d) { + var r = MyDataRow( + cells: [ + MyDataCell( + new Text(dateFormatter.format(d.date), style: textStyle), + ), + MyDataCell( + new Text( + numberFormatter.format(d.count), + style: textStyle, + ), + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/delivery_do_summary.dart b/lib/charts/delivery_do_summary.dart new file mode 100644 index 0000000..889299c --- /dev/null +++ b/lib/charts/delivery_do_summary.dart @@ -0,0 +1,79 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; + +import 'delivery_do_summary_details.dart'; + +class DeliveryDoSummaryChart extends StatefulWidget { + @override + _DeliveryDoSummaryChartState createState() => _DeliveryDoSummaryChartState(); +} + +class _DeliveryDoSummaryChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + List> series = [ + charts.Series( + id: "Subscribers", + data: chartModel.revenue.getDeliveryDoSummary(), + domainFn: (Data series, _) => "${series.totalDay}days", + measureFn: (Data series, _) => series.totalCount, + labelAccessorFn: (Data series, _) => + '${numberFormatter.format(series.totalCount)}'), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, "delivery.do.summary", + color: primaryColor, fontSize: 16), + InkWell( + child: LocalText( + context, + "delivery.do.details", + color: secondaryColor, + fontSize: 14, + ), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => DeliveryDoSummaryDetail())); + }, + ), + ], + ), + Expanded( + child: charts.BarChart( + series, + animate: true, + vertical: false, + defaultRenderer: new charts.BarRendererConfig( + barRendererDecorator: new charts.BarLabelDecorator( + labelPosition: charts.BarLabelPosition.auto, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/charts/delivery_do_summary_details.dart b/lib/charts/delivery_do_summary_details.dart new file mode 100644 index 0000000..e9c77e0 --- /dev/null +++ b/lib/charts/delivery_do_summary_details.dart @@ -0,0 +1,86 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; + +class DeliveryDoSummaryDetail extends StatefulWidget { + const DeliveryDoSummaryDetail(); + @override + _DeliveryDoSummaryDetailState createState() => + _DeliveryDoSummaryDetailState(); +} + +class _DeliveryDoSummaryDetailState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'delivery.do.sum.counts', + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 20), + child: MyDataTable( + columnSpacing: 100, + columns: [ + MyDataColumn(label: LocalText(context, "delivery.days")), + MyDataColumn(label: LocalText(context, "delivery.do.count")), + ], + rows: getProductRow(chartModel.revenue.getDeliveryDoSummary()), + ), + ), + ), + ), + ), + ); + } + + List getProductRow(List doList) { + return doList.map((d) { + var r = MyDataRow( + cells: [ + MyDataCell( + new Text(d.totalDay.toString(), style: textStyle), + ), + MyDataCell( + new Text( + numberFormatter.format(d.totalCount), + style: textStyle, + ), + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/delivery_line.dart b/lib/charts/delivery_line.dart new file mode 100644 index 0000000..5e74c90 --- /dev/null +++ b/lib/charts/delivery_line.dart @@ -0,0 +1,107 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; + +import 'delivery_line_data.dart'; + +class DeliveryBarChart extends StatefulWidget { + @override + _DeliveryBarChartState createState() => _DeliveryBarChartState(); +} + +class _DeliveryBarChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + List> series = [ + charts.Series( + id: "Subscribers", + data: chartModel.revenue.getDelivery(), + domainFn: (Data series, _) => + "${series.date.day}-${series.date.month}-${series.date.year}", + measureFn: (Data series, _) => series.amount, + labelAccessorFn: (Data series, _) => + '${numberFormatter.format(series.amount)}'), + ]; + + List> seriesLine = [ + charts.Series( + id: "Subscribers", + data: chartModel.revenue.getDelivery(), + domainFn: (Data series, _) => series.date, + measureFn: (Data series, _) => series.amount, + colorFn: (_, __) => charts.ColorUtil.fromDartColor(primaryColor), + labelAccessorFn: (Data series, _) => + '${numberFormatter.format(series.amount)}', + ), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, "delivery", color: primaryColor, fontSize: 16), + InkWell( + child: LocalText( + context, + "delivery.detail", + color: secondaryColor, + fontSize: 14, + ), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => DeliveryBarData())); + }, + ), + ], + ), + Expanded( + child: charts.TimeSeriesChart( + seriesLine, + animate: true, + defaultRenderer: new charts.LineRendererConfig( + includePoints: true, + ), + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + zeroBound: false, desiredTickCount: 10), + renderSpec: new charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + ))), + ), + ), + // Expanded( + // child: charts.BarChart( + // series, + // animate: true, + // vertical: true, + // defaultRenderer: new charts.BarRendererConfig( + // barRendererDecorator: new charts.BarLabelDecorator( + // labelPosition: charts.BarLabelPosition.auto, + // ), + // ), + // ), + // ), + ], + ), + ); + } +} diff --git a/lib/charts/delivery_line_data.dart b/lib/charts/delivery_line_data.dart new file mode 100644 index 0000000..8428f61 --- /dev/null +++ b/lib/charts/delivery_line_data.dart @@ -0,0 +1,92 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'delivery_line_detail.dart'; + +class DeliveryBarData extends StatefulWidget { + const DeliveryBarData(); + @override + _DeliveryBarDataState createState() => _DeliveryBarDataState(); +} + +class _DeliveryBarDataState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'delivery.data.title', + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 20), + child: MyDataTable( + columnSpacing: 100, + columns: [ + MyDataColumn(label: LocalText(context, "delivery.date")), + MyDataColumn(label: LocalText(context, "delivery.qty"),numeric: true), + ], + rows: getProductRow(chartModel.revenue.getDelivery()), + ), + ), + ), + ), + ), + ); + } + + List getProductRow(List doList) { + return doList.map((d) { + var r = MyDataRow( + onSelectChanged: (bool selected) async { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => DeliveryBarDetail(d.date)), + ); + }, + cells: [ + MyDataCell( + new Text(dateFormatter.format(d.date), style: textStyle), + ), + MyDataCell( + NumberCell(d.amount) + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/delivery_line_detail.dart b/lib/charts/delivery_line_detail.dart new file mode 100644 index 0000000..8319317 --- /dev/null +++ b/lib/charts/delivery_line_detail.dart @@ -0,0 +1,94 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/do_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +class DeliveryBarDetail extends StatefulWidget { + final DateTime date; + const DeliveryBarDetail(this.date); + @override + _DeliveryBarDetailState createState() => _DeliveryBarDetailState(); +} + +class _DeliveryBarDetailState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd-MMM-yyyy'); + bool _isLoading = false; + List dos = []; + + @override + void initState() { + super.initState(); + DOModel dOModel = Provider.of(context, listen: false); + dOModel.getDOForDelivery(widget.date).then((dos) { + setState(() { + this.dos = dos; + }); + }); + } + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'delivery.detail.title', + translationVariables: [dateFormatter.format(widget.date)], + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + padding: EdgeInsets.only(top: 5), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 3), + child: MyDataTable( + columnSpacing: 20, + columns: [ + MyDataColumn(label: LocalText(context, "do.name")), + MyDataColumn(label: LocalText(context, "do.do_num")), + MyDataColumn(label: LocalText(context, "do.quantity"),numeric: true), + ], + rows: getProductRow(), + ), + ), + ), + ), + ), + ); + } + + List getProductRow() { + return dos.map((d) { + var r = MyDataRow( + cells: [ + MyDataCell( + new Text(d.userName, style: textStyle), + ), + MyDataCell( + new Text(d.doNumber, style: textStyle), + ), + MyDataCell( + NumberCell(d.totalQty) + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/delivery_summary.dart b/lib/charts/delivery_summary.dart new file mode 100644 index 0000000..49a3e28 --- /dev/null +++ b/lib/charts/delivery_summary.dart @@ -0,0 +1,78 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'delivery_summary_detail.dart'; + +class DeliverySummary extends StatefulWidget { + @override + _DeliverySummaryState createState() => _DeliverySummaryState(); +} + +class _DeliverySummaryState extends State { + static final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + List> series = [ + charts.Series( + id: "Subscribers", + data: chartModel.revenue.getDeliverySummary(), + domainFn: (Data series, _) => "${series.totalDay}days", + measureFn: (Data series, _) => series.totalAmount, + labelAccessorFn: (Data series, _) => + '${numberFormatter.format(series.totalAmount)}'), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, "delivery.summary", + color: primaryColor, fontSize: 16), + InkWell( + child: LocalText( + context, + "delivery.detail", + color: secondaryColor, + fontSize: 14, + ), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => DeliverySummaryDetail())); + }, + ), + ], + ), + Expanded( + child: charts.BarChart( + series, + animate: true, + vertical: false, + defaultRenderer: new charts.BarRendererConfig( + barRendererDecorator: new charts.BarLabelDecorator( + labelPosition: charts.BarLabelPosition.auto, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/charts/delivery_summary_detail.dart b/lib/charts/delivery_summary_detail.dart new file mode 100644 index 0000000..0a6fcae --- /dev/null +++ b/lib/charts/delivery_summary_detail.dart @@ -0,0 +1,85 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; + +class DeliverySummaryDetail extends StatefulWidget { + const DeliverySummaryDetail(); + @override + _DeliverySummaryDetailState createState() => _DeliverySummaryDetailState(); +} + +class _DeliverySummaryDetailState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'delivery.sum.amounts', + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 20), + child: MyDataTable( + columnSpacing: 100, + columns: [ + MyDataColumn(label: LocalText(context, "delivery.days")), + MyDataColumn(label: LocalText(context, "delivery.amount")), + ], + rows: getProductRow(chartModel.revenue.getDeliverySummary()), + ), + ), + ), + ), + ), + ); + } + + List getProductRow(List doList) { + return doList.map((d) { + var r = MyDataRow( + cells: [ + MyDataCell( + new Text(d.totalDay.toString(), style: textStyle), + ), + MyDataCell( + new Text( + numberFormatter.format(d.totalAmount), + style: textStyle, + ), + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/do_line.dart b/lib/charts/do_line.dart new file mode 100644 index 0000000..6300b05 --- /dev/null +++ b/lib/charts/do_line.dart @@ -0,0 +1,156 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po_do_count.dart'; +import 'package:fcs/widget/local_text.dart'; + +import 'do_line_detail.dart'; + +class DOLineChart extends StatefulWidget { + @override + _DOLineChartState createState() => _DOLineChartState(); +} + +class _DOLineChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + List> series = [ + charts.Series( + id: "pending", + data: chartModel.podoCount.getDODataCounts("pending"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "approved", + data: chartModel.podoCount.getDODataCounts("approved"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "canceled", + data: chartModel.podoCount.getDODataCounts("canceled"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.gray.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "rejected", + data: chartModel.podoCount.getDODataCounts("rejected"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "expired", + data: chartModel.podoCount.getDODataCounts("expired"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + dashPatternFn: (_, __) => [8, 5, 2, 5], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "closed", + data: chartModel.podoCount.getDODataCounts("closed"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.indigo.shadeDefault, + dashPatternFn: (_, __) => [8, 5, 2, 5], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + LocalText(context, "do", color: primaryColor, fontSize: 16), + LocalText(context, 'chart.30_days', + color: primaryColor, fontSize: 14) + ], + ), + InkWell( + child: LocalText( + context, + "do.details", + color: secondaryColor, + fontSize: 14, + ), + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => DOLineDetail())); + }, + ), + ], + ), + Expanded( + child: charts.TimeSeriesChart( + series, + animate: true, + defaultRenderer: new charts.LineRendererConfig( + includePoints: true, + ), + behaviors: [ + new charts.SeriesLegend( + position: charts.BehaviorPosition.end, + outsideJustification: + charts.OutsideJustification.middleDrawArea, + entryTextStyle: charts.TextStyleSpec( + color: charts.Color(r: 127, g: 63, b: 191), + fontFamily: 'Georgia', + fontSize: 11), + ) + ], + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + zeroBound: false, desiredTickCount: 10), + renderSpec: new charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + ))), + ), + ), + ], + ), + ); + } +} diff --git a/lib/charts/do_line_detail.dart b/lib/charts/do_line_detail.dart new file mode 100644 index 0000000..5faa01e --- /dev/null +++ b/lib/charts/do_line_detail.dart @@ -0,0 +1,121 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po_do_count.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; + +class DOLineDetail extends StatefulWidget { + const DOLineDetail(); + @override + _DOLineDetailState createState() => _DOLineDetailState(); +} + +class _DOLineDetailState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'do.counts', + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + child: ListView( + children: [ + Column( + children: [ + Container( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, "chart.date"), + LocalText(context, "do.total_count") + ], + ), + ), + Column( + children: getRowTotalCountWidget( + chartModel.podoCount.getDOTotalCounts()), + ) + ], + ), + ], + ), + ), + ), + ); + } + + List getRowTotalCountWidget(List data) { + return data.map((d) { + return Container( + child: ExpansionTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(dateFormatter.format(d.date), style: textStyle), + Text(numberFormatter.format(d.totalCount), style: textStyle), + ], + ), + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + columnSpacing: 100, + columns: [ + MyDataColumn(label: LocalText(context, "do.count.status")), + MyDataColumn(label: LocalText(context, "do.count")), + ], + rows: getStatusRow(d.detailCountsList), + ), + ), + ], + ), + ); + }).toList(); + } + + List getStatusRow(List doList) { + doList.sort((a, b) => a.status.compareTo(b.status)); + return doList.map((d) { + var r = MyDataRow( + cells: [ + MyDataCell( + new Text(d.status, style: textStyle), + ), + MyDataCell( + new Text( + numberFormatter.format(d.count), + style: textStyle, + ), + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/lines.dart b/lib/charts/lines.dart new file mode 100644 index 0000000..bcd4167 --- /dev/null +++ b/lib/charts/lines.dart @@ -0,0 +1,107 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../model/product_model.dart'; + +typedef void ProductClick(DateTime date, Map measures); + +class ProductsChart extends StatelessWidget { + static final numberFormatter = new NumberFormat("#,###"); + final ChartData chartData; + final ProductClick productClick; + const ProductsChart(this.chartData, {Key key, this.productClick}) + : super(key: key); + factory ProductsChart.fromModel(ProductModel productModel, + {ProductClick productClick}) { + return new ProductsChart(_createData(productModel), + productClick: productClick); + } + + static ChartData _createData(ProductModel productModel) { + List> list = []; + var min = 9999, max = 0; + productModel.products.forEach((p) { + List data = []; + if (p.priceHistory != null) { + var dateKeys = {}; + p.priceHistory.entries.forEach((e) { + dateKeys[DateTime.parse(e.key)] = e.value; + }); + + var sortedKeys = dateKeys.keys.toList()..sort((a, b) => b.compareTo(a)); + sortedKeys.forEach((k) { + var v = dateKeys[k]; + data.add(new TimeSeriesSales(k, v)); + if (v < min) min = v; + if (v > max) max = v; + }); + } + + list.add(new charts.Series( + id: p.name, + colorFn: (_, __) => charts.ColorUtil.fromDartColor(Color(p.color)), + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + labelAccessorFn: (TimeSeriesSales series, _) => + '${numberFormatter.format(series.sales)}', + measureFormatterFn: (TimeSeriesSales series, _) => (n) => "s", + )); + }); + var chartData = ChartData(list, min, max); + return chartData; + } + + @override + Widget build(BuildContext context) { + return charts.TimeSeriesChart( + chartData.seriesList, + animate: true, + defaultRenderer: new charts.LineRendererConfig( + includePoints: true, + ), + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + zeroBound: false, desiredTickCount: 10), + renderSpec: new charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + ))), + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + updatedListener: _onSelectionChanged, + ) + ], + ); + } + + _onSelectionChanged(charts.SelectionModel model) { + final selectedDatum = model.selectedDatum; + if (selectedDatum.isNotEmpty) { + var _time = selectedDatum.first.datum.time; + Map _measures = {}; + + selectedDatum.forEach((charts.SeriesDatum datumPair) { + _measures[datumPair.series.displayName] = datumPair.datum.sales; + }); + if (productClick != null) { + productClick(_time, _measures); + } + } + } +} + +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} + +class ChartData { + final List seriesList; + final num min, max; + ChartData(this.seriesList, this.min, this.max); +} diff --git a/lib/charts/po_balance_chart.dart b/lib/charts/po_balance_chart.dart new file mode 100644 index 0000000..be48f37 --- /dev/null +++ b/lib/charts/po_balance_chart.dart @@ -0,0 +1,134 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:charts_flutter/flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/charts/po_balance_table.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/local_text.dart'; + +class POBalanceChart extends StatefulWidget { + @override + _POBalanceChartState createState() => _POBalanceChartState(); +} + +class _POBalanceChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + List chartSummary = new List(); + List> series; + + @override + void initState() { + super.initState(); + var chartModel = Provider.of(context, listen: false); + if (mounted) { + load(chartModel); + } + } + + Future load(ChartModel chartModel) async { + var _u = await chartModel.loadPOBalancesForBuyer(); + if (_u == null) return; + if (mounted) { + setState(() { + this.chartSummary = _u; + }); + } + } + + @override + Widget build(BuildContext context) { + var productModel = Provider.of(context); + if (this.chartSummary.isNotEmpty) { + this.chartSummary.forEach((s) { + productModel.products.forEach((p) { + if (p.id == s.productID) { + s.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + this + .chartSummary + .sort((s1, s2) => s1.displayOrder.compareTo(s2.displayOrder)); + } + List> series = [ + charts.Series( + id: "Subscribers", + data: this.chartSummary, + domainFn: (POChartData series, _) => series.productName, + measureFn: (POChartData series, _) => series.balanceQty, + colorFn: (POChartData series, _) => + charts.ColorUtil.fromDartColor(series.getColor), + labelAccessorFn: (POChartData series, _) => + '${numberFormatter.format(series.balanceQty)}'), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, 'po.balances', + color: primaryColor, fontSize: 16), + IconButton( + icon: Icon( + Icons.refresh, + color: primaryColor, + ), + onPressed: () { + _load(); + }, + ) + ], + ), + Expanded( + child: charts.BarChart( + series, + animate: true, + vertical: false, + defaultRenderer: new charts.BarRendererConfig( + barRendererDecorator: new charts.BarLabelDecorator( + labelPosition: charts.BarLabelPosition.auto, + ), + ), + selectionModels: [ + SelectionModelConfig(changedListener: (SelectionModel model) { + final selectedDatum = model.selectedDatum; + if (selectedDatum.isNotEmpty) { + selectedDatum.forEach((charts.SeriesDatum datumPair) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => POBalanceTable( + poChartData: datumPair.datum, + )), + ); + }); + } + }) + ], + ), + ), + ], + ), + ); + } + + Future _load() async { + var chartModel = Provider.of(context); + var _s = await chartModel.loadPOBalancesForBuyer(); + if (mounted) { + setState(() { + this.chartSummary = _s ?? []; + }); + } + } +} diff --git a/lib/charts/po_balance_table.dart b/lib/charts/po_balance_table.dart new file mode 100644 index 0000000..b28ec94 --- /dev/null +++ b/lib/charts/po_balance_table.dart @@ -0,0 +1,106 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +class POBalanceTable extends StatefulWidget { + final POChartData poChartData; + const POBalanceTable({Key key, this.poChartData}) : super(key: key); + @override + _POBalanceTableState createState() => _POBalanceTableState(); +} + +class _POBalanceTableState extends State { + final numberFormatter = new NumberFormat("#,###"); + List chartUser = new List(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + var chartModel = Provider.of(context, listen: false); + if (mounted) { + load(chartModel); + } + } + + Future load(ChartModel chartModel) async { + var _u = await chartModel.loadPOBalProductsForBuyer(); + setState(() { + this.chartUser = _u; + }); + } + + @override + Widget build(BuildContext context) { + List data = this + .chartUser + .where((u) => u.productName == widget.poChartData.productName) + .toList(); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text("product.qtys"), + style: Provider.of(context).isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode'), + ), + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 40, + columns: [ + MyDataColumn(label: LocalText(context, "buyer.name")), + MyDataColumn(label: LocalText(context, "buyer.product")), + MyDataColumn( + label: LocalText(context, "buyer.balQty"), numeric: true), + ], + rows: getProductRow(data), + ), + ), + ), + ), + ), + ); + } + + List getProductRow(List poLines) { + return poLines.map((p) { + return MyDataRow( + cells: [ + MyDataCell( + new Text(p.userName, style: textStyle), + ), + MyDataCell( + new Text( + p.productName, + style: textStyle, + ), + ), + MyDataCell( + NumberCell(p.balanceQty), + //new Text(numberFormatter.format(p.balanceQty), style: textStyle), + ), + ], + ); + }).toList(); + } +} diff --git a/lib/charts/po_balanceby_buyer.dart b/lib/charts/po_balanceby_buyer.dart new file mode 100644 index 0000000..7a11fdc --- /dev/null +++ b/lib/charts/po_balanceby_buyer.dart @@ -0,0 +1,98 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/local_text.dart'; + +class POBalanceChart_ extends StatefulWidget { + @override + _POBalanceChartState createState() => _POBalanceChartState(); + + +} + +class _POBalanceChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + List chartSummary = new List(); + List> series; + + @override + void initState() { + super.initState(); + var chartModel = Provider.of(context, listen: false); + if (mounted) { + load(chartModel); + } + } + + Future load(ChartModel chartModel) async { + var _u = await chartModel.loadPOBalancesForBuyer_(); + if (_u == null) return; + setState(() { + this.chartSummary = _u; + }); + } + + @override + Widget build(BuildContext context) { + List> series = [ + charts.Series( + id: "Subscribers", + data: this.chartSummary, + domainFn: (POBuyerData series, _) => series.status, + measureFn: (POBuyerData series, _) => series.amount, + colorFn: (POBuyerData series, _) => + charts.ColorUtil.fromDartColor(series.getColor), + labelAccessorFn: (POBuyerData series, _) => + '${numberFormatter.format(series.amount)}'), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, 'po.balances', + color: primaryColor, fontSize: 16), + IconButton( + icon: Icon( + Icons.refresh, + color: primaryColor, + ), + onPressed: () { + _load(); + }, + ) + ], + ), + Expanded( + child: charts.BarChart( + series, + animate: true, + vertical: false, + defaultRenderer: new charts.BarRendererConfig( + barRendererDecorator: new charts.BarLabelDecorator( + labelPosition: charts.BarLabelPosition.auto, + ), + ), + ), + ), + ], + ), + ); + } + + Future _load() async { + var chartModel = Provider.of(context); + var _s = await chartModel.loadPOBalancesForBuyer_(); + if (_s == null) return; + setState(() { + this.chartSummary = _s; + }); + } +} diff --git a/lib/charts/po_line.dart b/lib/charts/po_line.dart new file mode 100644 index 0000000..a89cf0a --- /dev/null +++ b/lib/charts/po_line.dart @@ -0,0 +1,155 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/charts/po_line_detail.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po_do_count.dart'; +import 'package:fcs/widget/local_text.dart'; + +class POLineChart extends StatefulWidget { + @override + _POLineChartState createState() => _POLineChartState(); +} + +class _POLineChartState extends State { + static final numberFormatter = new NumberFormat("#,###"); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + List> series = [ + charts.Series( + id: "pending", + data: chartModel.podoCount.getPODataCounts('pending'), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "approved", + data: chartModel.podoCount.getPODataCounts("approved"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "canceled", + data: chartModel.podoCount.getPODataCounts("canceled"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.gray.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "rejected", + data: chartModel.podoCount.getPODataCounts("rejected"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "expired", + data: chartModel.podoCount.getPODataCounts("expired"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + dashPatternFn: (_, __) => [8, 5, 2, 5], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + charts.Series( + id: "closed", + data: chartModel.podoCount.getPODataCounts("closed"), + domainFn: (CountData series, _) => series.date, + measureFn: (CountData series, _) => + series.count == null ? 0 : series.count, + colorFn: (_, __) => charts.MaterialPalette.indigo.shadeDefault, + dashPatternFn: (_, __) => [8, 5, 2, 5], + labelAccessorFn: (CountData series, _) => + '${numberFormatter.format(series.count)}', + ), + ]; + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + LocalText(context, "po", color: primaryColor, fontSize: 16), + LocalText(context, 'chart.30_days', + color: primaryColor, fontSize: 14) + ], + ), + InkWell( + child: LocalText( + context, + "po.details", + color: secondaryColor, + fontSize: 14, + ), + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => POLineDetail())); + }, + ), + ], + ), + Expanded( + child: charts.TimeSeriesChart( + series, + animate: true, + defaultRenderer: new charts.LineRendererConfig( + includePoints: true, + ), + behaviors: [ + new charts.SeriesLegend( + position: charts.BehaviorPosition.end, + outsideJustification: + charts.OutsideJustification.middleDrawArea, + entryTextStyle: charts.TextStyleSpec( + color: charts.Color(r: 127, g: 63, b: 191), + fontFamily: 'Georgia', + fontSize: 11), + ) + ], + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + zeroBound: false, desiredTickCount: 10), + renderSpec: new charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + ))), + ), + ), + ], + ), + ); + } +} diff --git a/lib/charts/po_line_detail.dart b/lib/charts/po_line_detail.dart new file mode 100644 index 0000000..002a30f --- /dev/null +++ b/lib/charts/po_line_detail.dart @@ -0,0 +1,121 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po_do_count.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; + +class POLineDetail extends StatefulWidget { + const POLineDetail(); + @override + _POLineDetailState createState() => _POLineDetailState(); +} + +class _POLineDetailState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'po.counts', + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + child: ListView( + children: [ + Column( + children: [ + Container( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocalText(context, "chart.date"), + LocalText(context, "po.total_count") + ], + ), + ), + Column( + children: getRowTotalCountWidget( + chartModel.podoCount.getPOTotalCounts()), + ) + ], + ), + ], + ), + ), + )); + } + + List getRowTotalCountWidget(List data) { + return data.map((d) { + return Container( + child: ExpansionTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(dateFormatter.format(d.date), style: textStyle), + Text(numberFormatter.format(d.totalCount), style: textStyle), + ], + ), + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + columnSpacing: 100, + columns: [ + MyDataColumn(label: LocalText(context, "po.count.status")), + MyDataColumn(label: LocalText(context, "po.count")), + ], + rows: getStatusRow(d.detailCountsList), + ), + ), + ], + ), + ); + }).toList(); + } + + List getStatusRow(List po) { + po.sort((a, b) => a.status.compareTo(b.status)); + return po.map((p) { + var r = MyDataRow( + cells: [ + MyDataCell( + new Text(p.status, style: textStyle), + ), + MyDataCell( + new Text( + p.count != null ? numberFormatter.format(p.count) : '0', + style: textStyle, + ), + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/qtyby_customer_table.dart b/lib/charts/qtyby_customer_table.dart new file mode 100644 index 0000000..4892da7 --- /dev/null +++ b/lib/charts/qtyby_customer_table.dart @@ -0,0 +1,103 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +class QtyByCustomerTable extends StatefulWidget { + final POChartData poChartData; + const QtyByCustomerTable({Key key, this.poChartData}) : super(key: key); + @override + _QtyByCustomerTableState createState() => _QtyByCustomerTableState(); +} + +class _QtyByCustomerTableState extends State { + final numberFormatter = new NumberFormat("#,###"); + List chartUser = new List(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + var chartModel = Provider.of(context, listen: false); + if (mounted) { + load(chartModel); + } + } + + Future load(ChartModel chartModel) async { + var _u = await chartModel.loadUsers(); + setState(() { + this.chartUser = _u; + }); + } + + @override + Widget build(BuildContext context) { + List data = this + .chartUser + .where((u) => u.productName == widget.poChartData.productName) + .toList(); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text("product.qtys"), + style: Provider.of(context).isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode'), + ), + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 40, + columns: [ + MyDataColumn(label: LocalText(context, "buyer.name")), + MyDataColumn(label: LocalText(context, "buyer.product")), + MyDataColumn( + label: LocalText(context, "buyer.balQty"), numeric: true), + ], + rows: getProductRow(data), + ), + ), + ), + ), + ), + ); + } + + List getProductRow(List poLines) { + return poLines.map((p) { + return MyDataRow( + cells: [ + MyDataCell( + new Text(p.userName, style: textStyle), + ), + MyDataCell( + new Text( + p.productName, + style: textStyle, + ), + ), + MyDataCell(NumberCell(p.balanceQty)), + ], + ); + }).toList(); + } +} diff --git a/lib/charts/quota.dart b/lib/charts/quota.dart new file mode 100644 index 0000000..285ebb2 --- /dev/null +++ b/lib/charts/quota.dart @@ -0,0 +1,118 @@ +/// Simple pie chart example. +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; + +class QuotaChart extends StatelessWidget { + static final numberFormatter = new NumberFormat("#,###"); + + final List quotaSeries; + final String title; + + QuotaChart(this.quotaSeries, this.title); + + factory QuotaChart.dailyQuota( + BuildContext context, Buyer buyer, List products) { + List data = []; + products.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + products.forEach((p) { + if (buyer.dailyQuotaUsedProducts.containsKey(p.id)) { + int value = buyer.dailyQuotaUsedProducts[p.id]; + data.add(Quota(p.name, value, p.color)); + } + }); + data.add( + new Quota(AppTranslations.of(context).text("chart.remaining"), + buyer.dailyQuota - buyer.dailyQuotaUsed, Colors.purple.value), + ); + + return new QuotaChart( + _createData(data), + AppTranslations.of(context).text("chart.daily.title", + translationVariables: [numberFormatter.format(buyer.dailyQuota)])); + } + + factory QuotaChart.maxQuota( + BuildContext context, Buyer buyer, List products) { + List data = []; + products.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + products.forEach((p) { + if (buyer.maxQuotaUsedProducts.containsKey(p.id)) { + int value = buyer.maxQuotaUsedProducts[p.id]; + data.add(Quota(p.name, value, p.color)); + } + }); + data.add( + new Quota(AppTranslations.of(context).text("chart.remaining"), + buyer.maxQuota - buyer.maxQuotaUsed, Colors.purple.value), + ); + return new QuotaChart( + _createData(data), + AppTranslations.of(context).text("chart.max.title", + translationVariables: [numberFormatter.format(buyer.maxQuota)])); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text(title), + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + height: 200, + child: charts.PieChart( + quotaSeries, + animate: false, + behaviors: [ + new charts.DatumLegend( + position: charts.BehaviorPosition.end, + horizontalFirst: false, + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + entryTextStyle: charts.TextStyleSpec( + color: charts.MaterialPalette.purple.shadeDefault, + fontFamily: 'Georgia', + fontSize: 11), + ) + ], + defaultRenderer: new charts.ArcRendererConfig( + arcWidth: 60, + arcRendererDecorators: [ + new charts.ArcLabelDecorator( + labelPosition: charts.ArcLabelPosition.auto, + labelPadding: 0) + ], + ), + ), + ), + ), + ], + ); + } + + static List> _createData(List data) { + return [ + new charts.Series( + id: 'Daily Quota', + domainFn: (Quota quota, _) => "${quota.label}\n", + measureFn: (Quota quota, _) => quota.quota, + data: data, + colorFn: (Quota quota, i) => + charts.ColorUtil.fromDartColor(Color(quota.color)), + labelAccessorFn: (Quota row, _) => + '${numberFormatter.format(row.quota)}', + ) + ]; + } +} + +class Quota { + final String label; + final int quota; + final int color; + + Quota(this.label, this.quota, this.color); +} diff --git a/lib/charts/revenue_line.dart b/lib/charts/revenue_line.dart new file mode 100644 index 0000000..ad3f9e8 --- /dev/null +++ b/lib/charts/revenue_line.dart @@ -0,0 +1,118 @@ +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; + +import 'revenue_line_data.dart'; + +class RevenueLineChart extends StatefulWidget { + @override + _RevenueLineChartState createState() => _RevenueLineChartState(); +} + +class _RevenueLineChartState extends State { + static final numberFormatter = NumberFormat.compact(); + int actualChart = 0; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + var mainModel = Provider.of(context); + + List> series = [ + charts.Series( + id: "Subscribers", + data: chartModel.revenue.getData(), + domainFn: (Data series, _) => series.date, + measureFn: (Data series, _) => series.amount, + colorFn: (_, __) => charts.ColorUtil.fromDartColor(primaryColor), + labelAccessorFn: (Data series, _) => + '${numberFormatter.format(series.amount)}', + ), + ]; + + final moneyFormatter = + new charts.BasicNumericTickFormatterSpec.fromNumberFormat( + new NumberFormat.compact()); + + return Container( + height: 200, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + mainModel.user.isOwnerAndAbove() + ? Row( + children: [ + LocalText(context, 'chart.revenue', + fontSize: 16, color: primaryColor), + LocalText(context, 'chart.30_days', + color: primaryColor, fontSize: 14), + ], + ) + : Row( + children: [ + LocalText(context, 'chart.spending', + fontSize: 16, color: primaryColor), + LocalText(context, 'chart.30_days', + color: primaryColor, fontSize: 14) + ], + ), + Text( + "${chartModel.revenue.mapData == null ? "" : numberFormatter.format(chartModel.revenue.getTotal(actualChart))}", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w700, + fontSize: 17.0)) + ], + ), + InkWell( + child: LocalText( + context, + "revenue.detail", + color: secondaryColor, + fontSize: 14, + ), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => RevenueLineData())); + }, + ), + ], + ), + Expanded( + child: charts.TimeSeriesChart( + series, + animate: true, + defaultRenderer: new charts.LineRendererConfig( + includePoints: true, + ), + primaryMeasureAxis: new charts.NumericAxisSpec( + tickFormatterSpec: moneyFormatter, + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + zeroBound: false, desiredTickCount: 10), + renderSpec: new charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + ))), + ), + ), + ], + ), + ); + } +} diff --git a/lib/charts/revenue_line_data.dart b/lib/charts/revenue_line_data.dart new file mode 100644 index 0000000..d5301d7 --- /dev/null +++ b/lib/charts/revenue_line_data.dart @@ -0,0 +1,92 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/chart_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/revenue.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'revenue_line_detail.dart'; + +class RevenueLineData extends StatefulWidget { + const RevenueLineData(); + @override + _RevenueLineDataState createState() => _RevenueLineDataState(); +} + +class _RevenueLineDataState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var chartModel = Provider.of(context); + var mainModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: mainModel.user.isOwnerAndAbove() + ? LocalText(context, 'revenue.amounts', + color: Colors.white, fontSize: 18) + : LocalText(context, 'spending.amounts', + color: Colors.white, fontSize: 18), + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 20), + child: MyDataTable( + columnSpacing: 100, + columns: [ + MyDataColumn(label: LocalText(context, "revenue.date")), + MyDataColumn(label: LocalText(context, "revenue.amount"),numeric: true), + ], + rows: getProductRow(chartModel.revenue.getData()), + ), + ), + ), + ), + ), + ); + } + + List getProductRow(List revs) { + return revs.map((p) { + var r = MyDataRow( + onSelectChanged: (bool selected) async { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => RevenueLineDetail(p.date)), + ); + }, + cells: [ + MyDataCell( + new Text(dateFormatter.format(p.date), style: textStyle), + ), + MyDataCell( + NumberCell(p.amount) + ), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/revenue_line_detail.dart b/lib/charts/revenue_line_detail.dart new file mode 100644 index 0000000..03187ad --- /dev/null +++ b/lib/charts/revenue_line_detail.dart @@ -0,0 +1,106 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/po_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +class RevenueLineDetail extends StatefulWidget { + final DateTime date; + const RevenueLineDetail(this.date); + @override + _RevenueLineDetailState createState() => _RevenueLineDetailState(); +} + +class _RevenueLineDetailState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd-MMM-yyyy'); + bool _isLoading = false; + List pos = []; + + @override + void initState() { + super.initState(); + POSubmissionModel pOModel = + Provider.of(context, listen: false); + pOModel.getPOForRevenue(widget.date).then((pos) { + if (mounted) { + setState(() { + this.pos = pos; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + var mainModel = Provider.of(context); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: mainModel.user.isOwnerAndAbove() + ? LocalText( + context, + 'revenue.detail.title', + translationVariables: [dateFormatter.format(widget.date)], + color: Colors.white, + fontSize: 18, + ) + : LocalText( + context, + 'spending.detail.title', + translationVariables: [dateFormatter.format(widget.date)], + color: Colors.white, + fontSize: 18, + ), + ), + body: Container( + padding: EdgeInsets.only(top: 5), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(left: 3), + child: MyDataTable( + columnSpacing: 20, + columns: [ + MyDataColumn(label: LocalText(context, "po.name")), + MyDataColumn(label: LocalText(context, "po.po_num")), + MyDataColumn( + label: LocalText(context, "po.amount"), numeric: true), + ], + rows: getProductRow(), + ), + ), + ), + ), + ), + ); + } + + List getProductRow() { + return pos.map((p) { + var r = MyDataRow( + cells: [ + MyDataCell( + new Text(p.userName, style: textStyle), + ), + MyDataCell( + new Text(p.poNumber, style: textStyle), + ), + MyDataCell(NumberCell(p.amount)), + ], + ); + + return r; + }).toList(); + } +} diff --git a/lib/charts/time.dart b/lib/charts/time.dart new file mode 100644 index 0000000..a7f7ef3 --- /dev/null +++ b/lib/charts/time.dart @@ -0,0 +1,60 @@ +/// Timeseries chart example +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimpleTimeSeriesChart extends StatelessWidget { + final List seriesList; + final bool animate; + + SimpleTimeSeriesChart(this.seriesList, {this.animate}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory SimpleTimeSeriesChart.withSampleData() { + return new SimpleTimeSeriesChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..ca00137 --- /dev/null +++ b/lib/config.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; + +enum Flavor { DEV, STAGING, PRODUCTION, LOCAL } +const FlavorNames = ["Development", "Staging", "Production", "Local"]; + +class Config { + static Config _instance; + + final Flavor flavor; + final String name; + final Color color; + final String apiURL; + final String reportURL; + final Level level; + final String reportProjectID; + + factory Config( + {@required Flavor flavor, + @required String apiURL, + @required String reportURL, + @required String reportProjectID, + Color color: Colors.blue, + Level level: Level.SEVERE}) { + _instance ??= Config._internal(flavor, FlavorNames[flavor.index], color, + apiURL, reportURL, level, reportProjectID); + + Logger.root.level = level; + Logger.root.onRecord.listen((record) { + print( + '${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}'); + }); + + return _instance; + } + + Config._internal(this.flavor, this.name, this.color, this.apiURL, + this.reportURL, this.level, this.reportProjectID); + + static Config get instance { + return _instance; + } + + static bool isProduction() => _instance.flavor == Flavor.PRODUCTION; + static bool isDevelopment() => _instance.flavor == Flavor.DEV; + static bool isStaging() => _instance.flavor == Flavor.STAGING; + static bool isLocal() => _instance.flavor == Flavor.LOCAL; +} diff --git a/lib/face/face_detection_camera.dart b/lib/face/face_detection_camera.dart new file mode 100644 index 0000000..2bd1dac --- /dev/null +++ b/lib/face/face_detection_camera.dart @@ -0,0 +1,177 @@ +import 'smile_painter.dart'; +import 'package:flutter/material.dart'; +import 'package:camera/camera.dart'; +import 'package:firebase_ml_vision/firebase_ml_vision.dart'; +import 'package:flutter/foundation.dart'; +import 'dart:ui' as ui show Image; +import 'utils.dart'; + +class FaceDetectionFromLiveCamera extends StatefulWidget { + FaceDetectionFromLiveCamera({Key key}) : super(key: key); + + @override + _FaceDetectionFromLiveCameraState createState() => + _FaceDetectionFromLiveCameraState(); +} + +class _FaceDetectionFromLiveCameraState + extends State { + final FaceDetector faceDetector = FirebaseVision.instance.faceDetector(); + List faces; + CameraController _camera; + + bool _isDetecting = false; + CameraLensDirection _direction = CameraLensDirection.back; + + @override + void initState() { + super.initState(); + _initializeCamera(); + } + + void _initializeCamera() async { + CameraDescription description = await getCamera(_direction); + ImageRotation rotation = rotationIntToImageRotation( + description.sensorOrientation, + ); + + _camera = CameraController( + description, + defaultTargetPlatform == TargetPlatform.iOS + ? ResolutionPreset.low + : ResolutionPreset.medium, + ); + await _camera.initialize(); + + _camera.startImageStream((CameraImage image) { + if (_isDetecting) return; + + _isDetecting = true; + + detect( + image, + FirebaseVision.instance + .faceDetector(FaceDetectorOptions( + mode: FaceDetectorMode.accurate, + enableClassification: true)) + .processImage, + rotation) + .then( + (dynamic result) { + setState(() { + faces = result; + }); + + _isDetecting = false; + }, + ).catchError( + (_) { + _isDetecting = false; + }, + ); + }); + } + + Widget _buildResults() { + const Text noResultsText = const Text('No results!'); + const Text multipleFaceText = const Text('Multiple faces!'); + const Text pleaseSmileText = const Text('Please smile!'); + + if (faces == null || _camera == null || !_camera.value.isInitialized) { + return noResultsText; + } + + CustomPainter painter; + + final Size imageSize = Size( + _camera.value.previewSize.height, + _camera.value.previewSize.width, + ); + + if (faces is! List || + faces.isEmpty || + faces == null || + faces.length == 0) return noResultsText; + if (faces.length > 1) return multipleFaceText; + var face = faces[0]; + if (face.smilingProbability == null || face.smilingProbability < 0.8) { + return pleaseSmileText; + } + + painter = SmilePainterLiveCamera(imageSize, faces); + return CustomPaint( + painter: painter, + ); + } + + Widget _buildImage() { + return Container( + constraints: const BoxConstraints.expand(), + child: _camera == null + ? const Center( + child: Text( + 'Initializing Camera...', + style: TextStyle( + color: Colors.green, + fontSize: 30.0, + ), + ), + ) + : Stack( + fit: StackFit.expand, + children: [ + CameraPreview(_camera), + _buildResults(), + Positioned( + bottom: 0.0, + left: 0.0, + right: 0.0, + child: Container( + color: Colors.white, + height: 50.0, + child: ListView( + children: faces + .map((face) => Text( + "${face.boundingBox.center.toString()}, Smile:${face.smilingProbability}")) + .toList(), + ), + ), + ), + ], + ), + ); + } + + void _toggleCameraDirection() async { + if (_direction == CameraLensDirection.back) { + _direction = CameraLensDirection.front; + } else { + _direction = CameraLensDirection.back; + } + + await _camera.stopImageStream(); + await _camera.dispose(); + + setState(() { + _camera = null; + }); + + _initializeCamera(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Face Detection with Smile"), + ), + body: _buildImage(), + floatingActionButton: FloatingActionButton( + onPressed: _toggleCameraDirection, + child: _direction == CameraLensDirection.back + ? const Icon(Icons.camera_front) + : const Icon(Icons.camera_rear), + ), + ); + } +} diff --git a/lib/face/face_detection_image.dart b/lib/face/face_detection_image.dart new file mode 100644 index 0000000..eabe94d --- /dev/null +++ b/lib/face/face_detection_image.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'dart:io'; +import 'smile_painter.dart'; +import 'package:firebase_ml_vision/firebase_ml_vision.dart'; +import 'dart:ui' as ui show Image; +import 'package:image_picker/image_picker.dart'; + +class FaceDetectionFromImage extends StatefulWidget { + @override + _FaceDetectionFromImageState createState() => _FaceDetectionFromImageState(); +} + +class _FaceDetectionFromImageState extends State { + bool loading = true; + ui.Image image; + List faces; + final FaceDetector faceDetector = FirebaseVision.instance.faceDetector(); + + Future _loadImage(File file) async { + final data = await file.readAsBytes(); + return await decodeImageFromList(data); + } + + void pickAndProcessImage() async { + final File file = await ImagePicker.pickImage(source: ImageSource.gallery); + final FirebaseVisionImage visionImage = FirebaseVisionImage.fromFile(file); + faces = await faceDetector.processImage(visionImage); + image = await _loadImage(file); + setState(() { + loading = false; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Face detection with Smile'), + ), + body: Center( + child: loading + ? Text('Press The floating Action Button for load image!') + : FittedBox( + child: SizedBox( + width: image.width.toDouble(), + height: image.height.toDouble(), + child: FacePaint( + painter: SmilePainter(image, faces), + ), + ), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: pickAndProcessImage, + child: Icon(Icons.image), + ), + ); + } +} diff --git a/lib/face/home.dart b/lib/face/home.dart new file mode 100644 index 0000000..feba004 --- /dev/null +++ b/lib/face/home.dart @@ -0,0 +1,40 @@ +import 'face_detection_camera.dart'; +import 'face_detection_image.dart'; +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Smile To Face App'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RaisedButton( + child: Text('Add Smile to Face from Image'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => FaceDetectionFromImage(), + ), + ); + }), + RaisedButton( + child: Text('Add Smile to Face from Live Camera'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => FaceDetectionFromLiveCamera(), + ), + ); + }), + ], + ), + ), + ); + } +} + diff --git a/lib/face/main.dart b/lib/face/main.dart new file mode 100644 index 0000000..c7d1ff7 --- /dev/null +++ b/lib/face/main.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'home.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: HomeScreen(), + ); + } +} \ No newline at end of file diff --git a/lib/face/smile_painter.dart b/lib/face/smile_painter.dart new file mode 100644 index 0000000..73057b8 --- /dev/null +++ b/lib/face/smile_painter.dart @@ -0,0 +1,138 @@ +import 'dart:ui' as ui show Image; +import 'dart:math' as Math; +import 'package:firebase_ml_vision/firebase_ml_vision.dart'; +import 'package:flutter/material.dart'; + +class FacePaint extends CustomPaint { + final CustomPainter painter; + + FacePaint({this.painter}) : super(painter: painter); +} + +class SmilePainter extends CustomPainter { + final ui.Image image; + final List faces; + + SmilePainter(this.image, this.faces); + + @override + void paint(Canvas canvas, Size size) { + if (image != null) { + canvas.drawImage(image, Offset.zero, Paint()); + } + + final paintRectStyle = Paint() + ..color = Colors.red + ..strokeWidth = 30.0 + ..style = PaintingStyle.stroke; + + //Draw Body + final paint = Paint()..color = Colors.yellow; + + for (var i = 0; i < faces.length; i++) { + final radius = + Math.min(faces[i].boundingBox.width, faces[i].boundingBox.height) / 2; + final center = faces[i].boundingBox.center; + final smilePaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = radius / 8; + canvas.drawRect(faces[i].boundingBox, paintRectStyle); + canvas.drawCircle(center, radius, paint); + canvas.drawArc( + Rect.fromCircle( + center: center.translate(0, radius / 8), radius: radius / 2), + 0, + Math.pi, + false, + smilePaint); + //Draw the eyes + canvas.drawCircle(Offset(center.dx - radius / 2, center.dy - radius / 2), + radius / 8, Paint()); + canvas.drawCircle(Offset(center.dx + radius / 2, center.dy - radius / 2), + radius / 8, Paint()); + } + } + + @override + bool shouldRepaint(SmilePainter oldDelegate) { + return image != oldDelegate.image || faces != oldDelegate.faces; + } +} + +class SmilePainterLiveCamera extends CustomPainter { + final Size imageSize; + final List faces; + + SmilePainterLiveCamera(this.imageSize, this.faces); + + @override + void paint(Canvas canvas, Size size) { +// final paintRectStyle = Paint() +// ..color = Colors.red +// ..strokeWidth = 10.0 +// ..style = PaintingStyle.stroke; + + final paint = Paint()..color = Colors.yellow; + + for (var i = 0; i < faces.length; i++) { + //Scale rect to image size + final rect = _scaleRect( + rect: faces[i].boundingBox, + imageSize: imageSize, + widgetSize: size, + ); + + //Radius for smile circle + final radius = Math.min(rect.width, rect.height) / 2; + + //Center of face rect + final Offset center = rect.center; + + final smilePaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = radius / 8; + + //Draw rect border + //canvas.drawRect(rect, paintRectStyle); + + //Draw body + canvas.drawCircle(center, radius, paint); + + //Draw mouth + canvas.drawArc( + Rect.fromCircle( + center: center.translate(0, radius / 8), radius: radius / 2), + 0, + Math.pi, + false, + smilePaint); + + //Draw the eyes + canvas.drawCircle(Offset(center.dx - radius / 2, center.dy - radius / 2), + radius / 8, Paint()); + canvas.drawCircle(Offset(center.dx + radius / 2, center.dy - radius / 2), + radius / 8, Paint()); + } + } + + @override + bool shouldRepaint(SmilePainterLiveCamera oldDelegate) { + return imageSize != oldDelegate.imageSize || faces != oldDelegate.faces; + } +} + +Rect _scaleRect({ + @required Rect rect, + @required Size imageSize, + @required Size widgetSize, +}) { + final double scaleX = widgetSize.width / imageSize.width; + final double scaleY = widgetSize.height / imageSize.height; + + return Rect.fromLTRB( + rect.left.toDouble() * scaleX, + rect.top.toDouble() * scaleY, + rect.right.toDouble() * scaleX, + rect.bottom.toDouble() * scaleY, + ); +} diff --git a/lib/face/utils.dart b/lib/face/utils.dart new file mode 100644 index 0000000..b861937 --- /dev/null +++ b/lib/face/utils.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; +import 'package:camera/camera.dart'; +import 'package:firebase_ml_vision/firebase_ml_vision.dart'; +import 'package:flutter/foundation.dart'; + +typedef HandleDetection = Future> Function(FirebaseVisionImage image); + +Future getCamera(CameraLensDirection dir) async { + return await availableCameras().then( + (List cameras) => cameras.firstWhere( + (CameraDescription camera) => camera.lensDirection == dir, + ), + ); +} + +Uint8List concatenatePlanes(List planes) { + final WriteBuffer allBytes = WriteBuffer(); + planes.forEach((Plane plane) => allBytes.putUint8List(plane.bytes)); + return allBytes.done().buffer.asUint8List(); +} + +FirebaseVisionImageMetadata buildMetaData( + CameraImage image, + ImageRotation rotation, + ) { + return FirebaseVisionImageMetadata( + rawFormat: image.format.raw, + size: Size(image.width.toDouble(), image.height.toDouble()), + rotation: rotation, + planeData: image.planes.map( + (Plane plane) { + return FirebaseVisionImagePlaneMetadata( + bytesPerRow: plane.bytesPerRow, + height: plane.height, + width: plane.width, + ); + }, + ).toList(), + ); +} + +Future> detect( + CameraImage image, + HandleDetection handleDetection, + ImageRotation rotation, + ) async { + return handleDetection( + FirebaseVisionImage.fromBytes( + concatenatePlanes(image.planes), + buildMetaData(image, rotation), + ), + ); +} + +ImageRotation rotationIntToImageRotation(int rotation) { + switch (rotation) { + case 0: + return ImageRotation.rotation0; + case 90: + return ImageRotation.rotation90; + case 180: + return ImageRotation.rotation180; + default: + assert(rotation == 270); + return ImageRotation.rotation270; + } +} diff --git a/lib/main-dev.dart b/lib/main-dev.dart new file mode 100644 index 0000000..446fb23 --- /dev/null +++ b/lib/main-dev.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/config.dart'; + +import 'app.dart'; + +void main() { + Config( + flavor: Flavor.DEV, + color: Colors.blue, + apiURL: + "https://asia-northeast1-mokkon-wholesale-dev.cloudfunctions.net/APIOK", + reportURL: "http://petrok-dev.mokkon.com:8080", + reportProjectID: "dev", + level: Level.ALL); + runApp(App()); +} diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100644 index 11655b6..0000000 --- a/lib/main.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.blue, - // This makes the visual density adapt to the platform that you run - // the app on. For desktop platforms, the controls will be smaller and - // closer together (more dense) than on mobile platforms. - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); - } -} diff --git a/lib/model/announcement_model.dart b/lib/model/announcement_model.dart new file mode 100644 index 0000000..e5f8fd0 --- /dev/null +++ b/lib/model/announcement_model.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/vo/announcement.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class AnnouncementModel extends BaseModel { + List announcements = []; + + void initUser(user) { + super.initUser(user); + _loadAnnouncements(); + } + + @override + logout() async { + announcements = []; + } + + Future _loadAnnouncements() async { + Stream snapshots = Firestore.instance + .collection( + "/$biz_collection/${setting.okEnergyId}/$announcement_collection") + .snapshots(); + + snapshots.listen((snaps) async { + announcements = snaps.documents.map((documentSnapshot) { + var data = Announcement.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + return data; + }).toList(); + + notifyListeners(); + }); + } + + + Future getAnnouncement(String id) async { + String path = "/$biz_collection/${setting.okEnergyId}/$announcement_collection"; + var snap = await getDocSnap(path, id); + return Announcement.fromMap(snap.data, snap.documentID); + } + + Future createAnnouncement(Announcement announcement) async { + await request("/announcement", "POST", + payload: announcement.toMap(), token: await getToken()); + } + + Future updateAnnouncement(Announcement announcement) async { + await request("/announcement", "PUT", + payload: announcement.toMap(), token: await getToken()); + } + + Future deleteAnnouncement(Announcement announcement) async { + await request("/announcement", "DELETE", + payload: announcement.toMap(), token: await getToken()); + } +} diff --git a/lib/model/api_helper.dart b/lib/model/api_helper.dart new file mode 100644 index 0000000..cb6e5ce --- /dev/null +++ b/lib/model/api_helper.dart @@ -0,0 +1,147 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:device_info/device_info.dart'; +import 'package:dio/dio.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/vo/status.dart'; + +import '../config.dart'; + +final log = Logger('requestAPI'); + +// request makes http request +// if token is null +Future requestAPI( + String path, + method, { + dynamic payload, + String token, + String url, +}) async { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + String deviceName = "${androidInfo.model}(${androidInfo.id})"; + log.info("device:${androidInfo.androidId},deviceName:$deviceName"); + + Map headers = {}; + if (token != null) { + headers["Token"] = token; + } + if (androidInfo.androidId != null) { + headers["Device"] = androidInfo.androidId + ":" + deviceName; + } + headers["Project-ID"] = Config.instance.reportProjectID; + + BaseOptions options = new BaseOptions( + method: method, + baseUrl: url == null ? Config.instance.apiURL : url, + connectTimeout: 10000, + receiveTimeout: 10000, + headers: headers, + ); + log.info("baseUrl:${options.baseUrl}, path:$path"); + try { + Dio dio = new Dio(options); + Response response = await dio.request( + path, + data: payload, + ); + var data = Status.fromJson(response.data); + if (data.status == 'Ok') { + return response.data["data"]; + } else { + throw Exception(data.message); + } + } catch (e) { + log.warning("path:$path, api:$e"); + throw e; + } +} + +// request makes http request +// if token is null +Future requestDownloadAPI(String path, method, + {dynamic payload, String token, String url, String filePath}) async { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + String deviceName = "${androidInfo.model}(${androidInfo.id})"; + log.info("device:${androidInfo.androidId},deviceName:$deviceName"); + + var bytes = utf8.encode(payload); + var base64Str = base64.encode(bytes); + String escapePayload = HtmlEscape().convert(base64Str); + + try { + String baseUrl = url == null ? Config.instance.apiURL : url; + log.info("Path:$baseUrl$path"); + HttpClient client = new HttpClient(); + var _downloadData = StringBuffer(); + var fileSave = new File(filePath); + var request = await client.getUrl(Uri.parse("$baseUrl$path")); + request.headers.set("Project-ID", Config.instance.reportProjectID); + request.headers + .set(HttpHeaders.contentTypeHeader, "application/json; charset=UTF-8"); + if (token != null) { + request.headers.set("Token", token); + } + if (androidInfo.androidId != null) { + request.headers.set("Device", androidInfo.androidId + ":" + deviceName); + } + request.headers.set("payload", escapePayload); + var response = await request.close(); + print("headers:${response.headers}"); + response.transform(utf8.decoder).listen((d) => _downloadData.write(d), + onDone: () { + fileSave.writeAsString(_downloadData.toString()); + }); + } catch (e) { + log.warning("path:$path, api:$e"); + throw e; + } +} + +// request makes http request +// if token is null +Future requestDownloadPDFAPI(String path, method, + {dynamic payload, String token, String url, String filePath}) async { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + String deviceName = "${androidInfo.model}(${androidInfo.id})"; + log.info("device:${androidInfo.androidId},deviceName:$deviceName"); + + var bytes = utf8.encode(payload); + var base64Str = base64.encode(bytes); + String escapePayload = HtmlEscape().convert(base64Str); + + try { + String baseUrl = url == null ? Config.instance.apiURL : url; + log.info("Path:$baseUrl$path"); + HttpClient client = new HttpClient(); + // var _downloadData = StringBuffer(); + var fileSave = new File(filePath); + var request = await client.getUrl(Uri.parse("$baseUrl$path")); + request.headers.set("Project-ID", Config.instance.reportProjectID); + if (token != null) { + request.headers.set("Token", token); + } + if (androidInfo.androidId != null) { + request.headers.set("Device", androidInfo.androidId + ":" + deviceName); + } + request.headers.set("payload", escapePayload); + var response = await request.close(); + print("headers:${response.headers}"); + var _downloadData = List(); + + response.listen((d) => _downloadData.addAll(d), onDone: () { + fileSave.writeAsBytes(_downloadData); + }); + // response.transform(utf8.decoder).listen((d) => _downloadData.write(d), + // onDone: () { + // fileSave.writeAsString(_downloadData.toString()); + // }); + } catch (e) { + log.warning("path:$path, api:$e"); + throw e; + } +} diff --git a/lib/model/base_model.dart b/lib/model/base_model.dart new file mode 100644 index 0000000..a85bcea --- /dev/null +++ b/lib/model/base_model.dart @@ -0,0 +1,36 @@ +import 'package:flutter/foundation.dart'; +import 'package:fcs/model/api_helper.dart'; +import 'package:fcs/model/main_model.dart'; + +import '../vo/setting.dart'; +import '../vo/user.dart'; + +abstract class BaseModel extends ChangeNotifier { + User user; + Setting setting; + MainModel mainModel; + + void initUser(User user) async { + this.user = user; + } + + void initSetting(Setting setting) async { + this.setting = setting; + } + + void logout(); + +// request makes http request +// if token is null + dynamic request( + String path, + method, { + dynamic payload, + String token, + String url, + }) async { + mainModel.resetPinTimer(); + return await requestAPI(path, method, + payload: payload, token: token, url: url); + } +} diff --git a/lib/model/buyer_model.dart b/lib/model/buyer_model.dart new file mode 100644 index 0000000..88a277d --- /dev/null +++ b/lib/model/buyer_model.dart @@ -0,0 +1,177 @@ +import 'dart:async'; +import 'dart:convert' show HtmlEscape, base64, utf8; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/config.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/vo/popup_menu.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class BuyerModel extends BaseModel { + final log = Logger('BuyerModel'); + + StreamSubscription listener; + + List buyers = []; + PopupMenu popupMenu = new PopupMenu(index: 0); + PopupMenu sortMenu = new PopupMenu(); + Buyer searchBuyer; + void initUser(user) { + super.initUser(user); + _loadBuyers(); + } + + Future _loadBuyers() async { + if (!user.isOwnerAndAbove() && !user.hasBuyer()) { + return; + } + + listener = getQuerySnapshotByOrder( + "/$biz_collection/${setting.okEnergyId}/$buyer_collection", + 'user_name') + .listen((snaps) async { + buyers.clear(); + snaps.documents.forEach((d) { + buyers.add(Buyer.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } + + @override + logout() async { + if (listener != null) await listener.cancel(); + buyers = []; + } + + Future getBuyer(String buyerID) async { + var snap = await getDocSnap( + "/$biz_collection/${setting.okEnergyId}/$buyer_collection", buyerID); + return Buyer.fromMap(snap.data, snap.documentID); + } + + Future loadBuyerProducts(Buyer buyer, {bool force = false}) async { + if (!force && buyer.buyerProducts != null && buyer.buyerProducts.length > 0) + return buyer; + var snaps = await getSnapshot( + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${buyer.id}/$product_collection"); + buyer.buyerProducts = snaps.documents + .map((s) => BuyerProduct.fromMap(s.data, s.documentID)) + .toList(); + return buyer; + } + + Future delete(Buyer buyer) async { + await request("/buyer/${buyer.id}", "DELETE", token: await getToken()); + } + + Future approve(Buyer buyer) async { + await request("/buyer/approve", "PUT", + payload: buyer.toMap(), token: await getToken()); + } + + Future reject(Buyer buyer) async { + await request("/buyer/reject", "POST", + payload: buyer.toMap(), token: await getToken()); + } + + Future allocate(Buyer buyer) async { + await request("/buyer/allocate", "POST", + payload: buyer.toMap(), token: await getToken()); + } + + void filterStatus(String status, int _selectedIndex, int _sleectedSortIndex) { + this.sortMenu.index = _sleectedSortIndex; + buyers.clear(); + if (listener != null) { + listener.cancel(); + } + + this.popupMenu.index = _selectedIndex; + String path = "/$biz_collection/${setting.okEnergyId}/$buyer_collection"; + listener = getFilterStatusSnapshot(path, status, 'user_name') + .listen((snaps) async { + buyers.clear(); + snaps.documents.forEach((d) { + buyers.add(Buyer.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + notifyListeners(); + } + + Future> search(String searchBuyer) async { + if (searchBuyer == null || searchBuyer == '') return List(); + + var bytes = utf8.encode(searchBuyer); + var base64Str = base64.encode(bytes); + HtmlEscape htmlEscape = const HtmlEscape(); + String escapeBuyer = htmlEscape.convert(base64Str); + + int limit = 20; + List _buyers = []; + + try { + var data = await request( + "/api/fts/$buyer_collection/$escapeBuyer/$limit", "GET", + token: await getToken(), url: Config.instance.reportURL); + if (data == null) return List(); + + data.forEach((buyer) { + var _buyer = Buyer.fromJson(buyer); + _buyers.add(_buyer); + }); + } catch (e) { + // permission error + log.warning("buyer error:" + e.toString()); + return null; + } + return _buyers; + } + + void filterSorting(int _sleectedSortIndex, int _selectedIndex) { + this.popupMenu.index = _selectedIndex; + buyers.clear(); + if (listener != null) { + listener.cancel(); + } + String _fieldName; + bool descending = false; + + if (_sleectedSortIndex == 0) { + _fieldName = 'user_name'; + descending = false; + } + if (_sleectedSortIndex == 1) { + _fieldName = 'user_name'; + descending = true; + } + if (_sleectedSortIndex == 2) { + _fieldName = 'phone_number'; + descending = false; + } + if (_sleectedSortIndex == 3) { + _fieldName = 'phone_number'; + descending = true; + } + + this.sortMenu.index = _sleectedSortIndex; + String path = "/$biz_collection/${setting.okEnergyId}/$buyer_collection"; + listener = + getFilterSnapshot(path, descending, _fieldName).listen((snaps) async { + buyers.clear(); + snaps.documents.forEach((d) { + buyers.add(Buyer.fromMap(d.data, d.documentID)); + notifyListeners(); + }); + + notifyListeners(); + }); + notifyListeners(); + } +} diff --git a/lib/model/chart_model.dart b/lib/model/chart_model.dart new file mode 100644 index 0000000..62e7209 --- /dev/null +++ b/lib/model/chart_model.dart @@ -0,0 +1,244 @@ +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/config.dart'; +import 'package:fcs/model/firebase_helper.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/vo/po_do_count.dart'; +import 'package:fcs/vo/revenue.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class ChartModel extends BaseModel { + final log = Logger('ChartModel'); + + List chartSummary = []; + + Revenue revenue = new Revenue(); + PODOCount podoCount = new PODOCount(); + void initUser(user) async { + super.initUser(user); + revenue = new Revenue(); + podoCount = new PODOCount(); + + _loadRev(); + _loadPODOCount(); + if (user.hasPO() || user.isOwnerAndAbove()) { + _getSummary(); + } + } + + Future _loadRev() async { + try { + String path = "/$biz_collection/${setting.okEnergyId}/$report_collection"; + String reportID = "revenue"; + + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$report_collection"; + reportID = "spending"; + } + + getDocSnapshot(path, reportID).listen((DocumentSnapshot snapshot) async { + if (snapshot.data == null) { + return; + } + revenue = Revenue.fromMap(snapshot.data, snapshot.documentID); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future _loadPODOCount() async { + try { + String path = "/$biz_collection/${setting.okEnergyId}/$report_collection"; + String reportID = "po_do_count"; + + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$report_collection"; + } + + getDocSnapshot(path, reportID).listen((DocumentSnapshot snapshot) async { + if (snapshot.data == null) { + return; + } + podoCount = PODOCount.fromMap(snapshot.data, snapshot.documentID); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + @override + logout() async { + chartSummary = []; + revenue = new Revenue(); + podoCount = new PODOCount(); + } + + _getSummary() async { + chartSummary.clear(); + try { + var data = { + "fields": 'quantity,product_id,product_name,color', + "aggfuns": "sum,,,", + "groupbys": 'product_id', + }; + var result = await request("/api/data/po_product_view", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + if (result == null) return; + chartSummary.clear(); + result.forEach((chart) { + var _list = POChartData.fromJson(chart, "quantity_sum"); + chartSummary.add(_list); + }); + } catch (e) { + log.warning("Error get Summary>>>>${e.toString()}"); + } + } + + Future> loadSummary() async { + List _list = List(); + try { + var data = { + "fields": 'quantity,product_id,product_name,color', + "aggfuns": "sum,,,", + "groupbys": 'product_id', + }; + var result = await request("/api/data/po_product_view", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + if (result != null) { + result.forEach((chart) { + var _data = POChartData.fromJson(chart, "quantity_sum"); + _list.add(_data); + }); + } + return _list; + } catch (e) { + log.warning("Error>>>>${e.toString()}"); + return null; + } + } + + Future> loadUsers() async { + List _list = List(); + var data = { + "fields": 'quantity,user_name,product_name', + "aggfuns": "sum,,", + "groupbys": 'product_id,user_id', + }; + try { + var result = await request("/api/data/po_product_view", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + + result.forEach((chart) { + var _buyer = POChartData.fromJson(chart, "quantity_sum"); + _list.add(_buyer); + }); + return _list; + } catch (e) { + log.warning("Error load>>>>${e.toString()}"); + return null; + } + } + + Future> loadPOBalancesForBuyer_() async { + List _list = List(); + try { + var data = { + "fields": 'amount,status', + "aggfuns": "sum,", + "groupbys": 'status', + "filters": [ + {"field": "user_id", "compare": "==", "value": user.docID} + ] + }; + var result = await request("/api/data/po_buyer_view", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + + if (result != null) { + result.forEach((chart) { + var _data = POBuyerData.fromJson(chart, "amount_sum"); + _list.add(_data); + }); + } + return _list; + } catch (e) { + log.warning("Error>>>>${e.toString()}"); + return null; + } + } + + Future> loadPOBalancesForBuyer() async { + List _list = List(); + try { + var data = { + "fields": 'quantity,product_id,product_name,user_id,color', + "aggfuns": "sum,,,,", + "groupbys": 'product_id,user_id', + "filters": [ + {"field": "user_id", "compare": "==", "value": user.docID} + ] + }; + var result = await request("/api/data/po_product_view", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + if (result != null) { + result.forEach((chart) { + var _data = POChartData.fromJson(chart, "quantity_sum"); + _list.add(_data); + }); + } + return _list; + } catch (e) { + log.warning("Error>>>>${e.toString()}"); + return null; + } + } + + Future> loadPOBalProductsForBuyer() async { + List _list = List(); + var data = { + "fields": 'quantity,user_name,user_id,product_name', + "aggfuns": "sum,,,", + "groupbys": 'product_id,user_id', + "filters": [ + {"field": "user_id", "compare": "==", "value": user.docID} + ] + }; + try { + var result = await request("/api/data/po_product_view", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + result.forEach((chart) { + var _buyer = POChartData.fromJson(chart, "quantity_sum"); + _list.add(_buyer); + }); + return _list; + } catch (e) { + log.warning("Error load>>>>${e.toString()}"); + return null; + } + } +} diff --git a/lib/model/constants.dart b/lib/model/constants.dart new file mode 100644 index 0000000..2d0c0fc --- /dev/null +++ b/lib/model/constants.dart @@ -0,0 +1,37 @@ +const ok_doc_id = "ok"; +const setting_doc_id = "ok_setting"; + +const config_collection = "configs"; +const biz_collection = "bizs"; +const product_collection = "products"; +const user_collection = "users"; +const privilege_collection = "privileges"; +const user_level_collection = "user_levels"; +const storage_collection = "storages"; +const buyer_collection = "buyers"; +const buying_pos = "buying_pos"; +const selling_pos = "selling_pos"; +const inventory_takings = "inventory_takings"; +const inventory_lines = "inventory_lines"; +const pds_collection = "pds"; +const pos_collection = "pos"; +const dos_collection = "dos"; +const notification_collection = "notifications"; +const log_collection = "logs"; +const report_collection = "reports"; +const po_product_collection = "po_products"; +const device_collection = "devices"; +const do_po_lines_collection = "do_po_lines"; +const reports_collection = "reports"; +const announcement_collection = "announcements"; +const report_user_collection = "report_users"; + +const po_files_path = "/ok/po"; +const reg_files_path = "/ok/reg"; +const do_files_path = "/ok/do"; +const sign_files_path = "/ok/sign"; +const bank_images_path = "/ok/banks"; + +const po_approved_status = "approved"; +const po_closed_status = "closed"; +const do_approved_status = "approved"; diff --git a/lib/model/delivery_model.dart b/lib/model/delivery_model.dart new file mode 100644 index 0000000..506be95 --- /dev/null +++ b/lib/model/delivery_model.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:path/path.dart' as Path; +import 'package:fcs/model/constants.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/vo/popup_menu.dart'; + +import 'base_model.dart'; +import 'firebase_helper.dart'; + +class DeliveryModel extends BaseModel { + StreamSubscription listener; + + List dos = []; + PopupMenu popupMenu = new PopupMenu(index: 0); + int dateIndex = 0; + DateTime selectedDate = DateTime.now(); + int timber = 0; + + void initUser(user) async { + super.initUser(user); + _loadDOs(); + } + + Future _loadDOs() async { + if (!user.isOwnerAndAbove() && !user.hasDelivery()) { + return; + } + + String path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + + var startDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0); + var endDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 23, 59, 59); + + listener = getDeliverySnapshot( + path, 'delivery_date', startDate, endDate, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } + + @override + logout() async { + if (listener != null) await listener.cancel(); + dos = []; + } + + Future getDO(String id) async { + String path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection"; + } + var doSnap = await getDocSnap(path, id); + return DOSubmission.fromMap(doSnap.data, doSnap.documentID); + } + + Future endDelivery(DOSubmission doObj, Uint8List img) async { + String path = Path.join(do_files_path, user.docID); + String imgUrl = await uploadStorageData(path, img, fileName: doObj.id); + doObj.doReceiptUrl = imgUrl; + await request("/do/ended", "POST", + payload: doObj.toMap(), token: await getToken()); + } + + void filterData( + String status, DateTime dateTime, int _selectedIndex, int _dateIndex) { + dos.clear(); + + var startDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 0, 0, 0); + var endDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59); + + if (listener != null) { + listener.cancel(); + } + + this.popupMenu.index = _selectedIndex; + this.dateIndex = _dateIndex; + this.selectedDate = dateTime == null + ? new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0) + : dateTime; + + String path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection"; + } + + if (status != null && dateTime == null) { + listener = getDeliveryStatusSnapshot(path, status, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else if (dateTime != null && status == null) { + listener = getDeliveryDateSnapshot( + path, 'delivery_date', startDate, endDate, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else if (status != null && dateTime != null) { + listener = getDeliveryDataSnapshot( + path, status, 'delivery_date', startDate, endDate, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else { + listener = + getQuerySnapshotByOrder(path, 'do_number').listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } + } +} diff --git a/lib/model/device_model.dart b/lib/model/device_model.dart new file mode 100644 index 0000000..e5e0f0c --- /dev/null +++ b/lib/model/device_model.dart @@ -0,0 +1,76 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:device_info/device_info.dart'; +import 'package:fcs/vo/device.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class PhoneDeviceModel extends BaseModel { + List devices = new List(); + + bool isLogout = false; + void initUser(user) { + super.initUser(user); + _loadDevices(); + } + + Future _loadDevices() async { + Stream snapshots = Firestore.instance + .collection( + "/$biz_collection/${setting.okEnergyId}/$user_collection/${user.docID}/$device_collection") + .where("primary_device", isEqualTo: false) + .snapshots(); + + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + + snapshots.listen((snaps) async { + devices = snaps.documents.map((documentSnapshot) { + var data = PhoneDevice.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + if (!data.deviceOn && + !data.primaryDevice && + data.id == androidInfo.androidId && + !documentSnapshot.metadata.isFromCache) { + this.isLogout = true; + this.mainModel.logout(); + notifyListeners(); + } + return data; + }).toList(); + + notifyListeners(); + }); + } + + + @override + logout() async { + devices = []; + } + + bool isLogoutDevice() { + return this.isLogout; + } + + setDevice(bool status) { + this.isLogout = status; + notifyListeners(); + } + + Future confirmDevice(String id, String deviceID) async { + await request("/dev/on", "POST", + payload: {"id": id, "device_id": deviceID}, token: await getToken()); + } + + Future logoutDevice(String id, String deviceID) async { + await request("/dev/off", "POST", + payload: {"id": id, "device_id": deviceID}, token: await getToken()); + } + + Future setPrimaryDevice(String id, String deviceID) async { + await request("/dev", "PUT", + payload: {"id": id, "device_id": deviceID}, token: await getToken()); + } +} diff --git a/lib/model/do_model.dart b/lib/model/do_model.dart new file mode 100644 index 0000000..d311062 --- /dev/null +++ b/lib/model/do_model.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:path/path.dart' as Path; +import 'package:fcs/model/api_helper.dart'; +import 'package:fcs/model/constants.dart'; +import 'package:fcs/pages/do/do_files.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/vo/popup_menu.dart'; + +import 'base_model.dart'; +import 'firebase_helper.dart'; + +class DOModel extends BaseModel { + StreamSubscription listener; + + List dos = []; + PopupMenu popupMenu = new PopupMenu(index: 0); + int dateIndex = 0; + DateTime selectedDate = DateTime.now(); + int timber = 0; + + void initUser(user) async { + super.initUser(user); + _loadDOs(); + } + + + @override + logout() async { + if (listener != null) await listener.cancel(); + dos = []; + } + + Future _loadDOs() async { + String path; + if (user.hasDO() || user.isOwnerAndAbove()) { + path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + } + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection"; + } + var startDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0); + var endDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 23, 59, 59); + + listener = + getFilterDateSnapshot(path, 'do_date', startDate, endDate, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } + + Future loadDOPOLines(DOSubmission doSub) async { + String path = + "/$biz_collection/${setting.okEnergyId}/$dos_collection/${doSub.id}/$do_po_lines_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection/${doSub.id}/$do_po_lines_collection"; + } + var snaps = await getSnapshot(path); + doSub.dopoLies = + snaps.documents.map((s) => DOPOLine.fromMap(s.data)).toList(); + return doSub; + } + + Future getDO(String id) async { + String path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection"; + } + var doSnap = await getDocSnap(path, id); + return DOSubmission.fromMap(doSnap.data, doSnap.documentID); + } + + Future createDO(DOSubmission doSubmission, DOFiles files) async { + doSubmission.userID = user.docID; + String path = Path.join(do_files_path, user.docID); + + if (files.storageChargeFile != null) { + String url = await uploadStorage(path, files.storageChargeFile); + doSubmission.storageReceiptUrl = url; + } + if (files.licenseFile != null) { + String url = await uploadStorage(path, files.licenseFile); + doSubmission.driverLicenceUrl = url; + } + + await request("/do", "POST", + payload: doSubmission.toMap(), token: await getToken()); + } + + Future updateDO(DOSubmission doSubmission, DOFiles files) async { + String path = Path.join(do_files_path, user.docID); + if (files.storageFileChanged) { + if (doSubmission.storageReceiptUrl != null && + doSubmission.storageReceiptUrl != '') { + await deleteStorageFromUrl(doSubmission.storageReceiptUrl); + } + doSubmission.storageReceiptUrl = null; + String url = await uploadStorage(path, files.storageChargeFile); + doSubmission.storageReceiptUrl = url; + } + + if (files.licenseFileChanged) { + if (doSubmission.driverLicenceUrl != null && + doSubmission.driverLicenceUrl != '') { + await deleteStorageFromUrl(doSubmission.driverLicenceUrl); + } + doSubmission.driverLicenceUrl = null; + String url = await uploadStorage(path, files.licenseFile); + doSubmission.driverLicenceUrl = url; + } + await request("/do", "PUT", + payload: doSubmission.toMap(), token: await getToken()); + } + + Future approveDO(DOSubmission doObj) async { + await request("/do/approved", "POST", + payload: doObj.toMap(), token: await getToken()); + } + + Future rejectDO(DOSubmission doObj) async { + await request("/do/rejected", "POST", + payload: doObj.toMap(), token: await getToken()); + } + + Future cancelDO(DOSubmission doObj) async { + await request("/do/canceled", "POST", + payload: doObj.toMap(), token: await getToken()); + } + + Future initDelivery(DOSubmission doObj) async { + String path = Path.join(do_files_path, user.docID); + String imgUrl = + await uploadStorage(path, doObj.driverImg, fileName: doObj.id); + doObj.driverImgUrl = imgUrl; + await request("/do/initiated", "POST", + payload: doObj.toMap(), token: await getToken()); + } + + Future startDelivery(DOSubmission doObj) async { + await request("/do/started", "POST", + payload: doObj.toMap(), token: await getToken()); + } + + Future endDelivery(DOSubmission doObj, Uint8List img) async { + String path = Path.join(do_files_path, user.docID); + String imgUrl = await uploadStorageData(path, img, fileName: doObj.id); + doObj.doReceiptUrl = imgUrl; + await request("/do/ended", "POST", + payload: doObj.toMap(), token: await getToken()); + } + + void filterData( + String status, DateTime dateTime, int _selectedIndex, int _dateIndex) { + dos.clear(); + if (listener != null) { + listener.cancel(); + } + + this.popupMenu.index = _selectedIndex; + this.dateIndex = _dateIndex; + this.selectedDate = dateTime == null + ? new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0) + : dateTime; + + String path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection"; + } + + if (status != null && dateTime == null) { + listener = getFilterStatusSnapshot(path, status, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else if (dateTime != null && status == null) { + var endDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59); + listener = + getFilterDateSnapshot(path, 'do_date', dateTime, endDate, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else if (status != null && dateTime != null) { + var endDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59); + listener = getFilterDataSnapshot( + path, status, 'do_date', dateTime, endDate, 'do_number') + .listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else { + listener = + getQuerySnapshotByOrder(path, 'do_number').listen((snaps) async { + dos.clear(); + snaps.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } + } + + addTimber(int count) { + timber = count; + notifyListeners(); + } + + Future> getDOForDelivery(DateTime dateTime) async { + List dos = []; + + String path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection"; + } + DateTime date = DateTime(dateTime.year, dateTime.month, dateTime.day); + DateTime dateAddOne = date.add(Duration(days: 1)); + + QuerySnapshot snapshots = await Firestore.instance + .collection(path) + .where("status", isEqualTo: do_approved_status) + .where("delivery_date", isGreaterThanOrEqualTo: date) + .where("delivery_date", isLessThan: dateAddOne) + .orderBy("delivery_date").orderBy("user_name") + .limit(100) + .getDocuments(); + snapshots.documents.forEach((d) { + dos.add(DOSubmission.fromMap(d.data, d.documentID)); + }); + return dos; + } +} diff --git a/lib/model/employee_model.dart b/lib/model/employee_model.dart new file mode 100644 index 0000000..9e9ffa8 --- /dev/null +++ b/lib/model/employee_model.dart @@ -0,0 +1,59 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/vo/user.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class EmployeeModel extends BaseModel { + final log = Logger('EmployeeModel'); + + List employees = []; + + void initUser(user) async { + super.initUser(user); + _loadEmployees(); + } + + @override + logout() async { + employees = []; + } + + Future _loadEmployees() async { + if (!user.isOwnerAndAbove() && !user.hasAccount()) { + return; + } + + try { + Firestore.instance + .collection("/$biz_collection/${setting.okEnergyId}/$user_collection") + .where("is_employee", isEqualTo: true) + .snapshots() + .listen((QuerySnapshot snapshot) { + employees.clear(); + employees = snapshot.documents.map((documentSnapshot) { + var user = + User.fromMap(documentSnapshot.data, documentSnapshot.documentID); + return user; + }).toList(); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future updatePrivileges(String userID, List privileges) async { + try { + await request("/employee/privileges", "PUT", + payload: {"id": userID, "privileges": privileges}, + token: await getToken()); + } catch (e) { + throw Exception(e); + } + } +} diff --git a/lib/model/firebase_helper.dart b/lib/model/firebase_helper.dart new file mode 100644 index 0000000..a1fc131 --- /dev/null +++ b/lib/model/firebase_helper.dart @@ -0,0 +1,269 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:logging/logging.dart'; +import 'package:uuid/uuid.dart'; + +final log = Logger('firebaseHelper'); + +final FirebaseAuth auth = FirebaseAuth.instance; + +Future getToken() async { + FirebaseUser firebaseUser = await auth.currentUser(); + IdTokenResult token = await firebaseUser.getIdToken(); + return token.token; +} + +Stream getQuerySnapshot(String path) { + log.info("getQuerySnapshot Path: $path"); + Stream snapshots = + Firestore.instance.collection(path).snapshots(); + return snapshots; +} + +Stream getQuerySnapshotByOrder(String path, String orderName) { + log.info("getQuerySnapshotByOrder Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getFilterStatusSnapshot( + String path, String searchStatus, String orderName) { + log.info("getFilterStatusSnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .where('status', isEqualTo: searchStatus) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getFilterSnapshot( + String path, bool descending, String orderName) { + log.info("getFilterSnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .orderBy(orderName, descending: descending) + .snapshots(); + return snapshots; +} + +Stream getDeliveryStatusSnapshot( + String path, String searchStatus, String orderName) { + log.info("getDeliveryStatusSnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .where('status', isEqualTo: searchStatus) + .where("in_delivery", isEqualTo: true) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getFilterDateSnapshot(String path, String type, + DateTime startDate, DateTime endDate, String orderName) { + log.info("getFilterDateSnapshot Path: $path"); + Firestore.instance + .collection(path) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .limit(1) + .getDocuments() + .then((s) {}); + + Stream snapshots = Firestore.instance + .collection(path) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getDeliveryDateSnapshot(String path, String type, + DateTime startDate, DateTime endDate, String orderName) { + log.info("getDeliveryDateSnapshot Path: $path"); + Firestore.instance + .collection(path) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .where("in_delivery", isEqualTo: true) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .limit(1) + .getDocuments() + .then((s) {}); + + Stream snapshots = Firestore.instance + .collection(path) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .where("in_delivery", isEqualTo: true) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getDeliverySnapshot(String path, String type, + DateTime startDate, DateTime endDate, String orderName) { + log.info("getDeliverySnapshot Path: $path"); + Firestore.instance + .collection(path) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .where("in_delivery", isEqualTo: true) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .limit(1) + .getDocuments() + .then((s) {}); + + Stream snapshots = Firestore.instance + .collection(path) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .where("in_delivery", isEqualTo: true) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getFilterDataSnapshot(String path, String status, + String type, DateTime startDate, DateTime endDate, String orderName) { + log.info("getFilterDateSnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .where('status', isEqualTo: status) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getDeliveryDataSnapshot(String path, String status, + String type, DateTime startDate, DateTime endDate, String orderName) { + log.info("getDeliveryDataSnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .where('status', isEqualTo: status) + .where("in_delivery", isEqualTo: true) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .orderBy(type, descending: true) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Stream getsearchBuyerSnapshot(String path, String buyer) { + log.info("getFilterDateSnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .where("user_name", isEqualTo: buyer) + .snapshots(); + return snapshots; +} + +Stream getDocSnapshot(String path, String id) { + log.info("getDocSnapshot Path: $path, ID: $id"); + Stream snapshot = + Firestore.instance.collection(path).document(id).snapshots(); + return snapshot; +} + +Stream getQuerySnapshotF(String path, accountID) { + log.info("getQuerySnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .where("account_id", isEqualTo: accountID) + .snapshots(); + return snapshots; +} + +Stream getFilterDateSnapshotF(String path, String accountID, + String type, DateTime startDate, DateTime endDate, String orderName) { + log.info("getFilterDateSnapshot Path: $path"); + Stream snapshots = Firestore.instance + .collection(path) + .where("account_id", isEqualTo: accountID) + .where(type, isGreaterThanOrEqualTo: startDate) + .where(type, isLessThanOrEqualTo: endDate) + .orderBy(orderName, descending: true) + .snapshots(); + return snapshots; +} + +Future getSnapshot(String path) { + log.info("getSnapshot Path: $path"); + Future snapshots = + Firestore.instance.collection(path).getDocuments(); + + return snapshots; +} + +Future getDocSnap(String path, String id) { + log.info("getDocSnap Path: $path"); + return Firestore.instance.collection(path).document(id).get(); +} + +Future uploadStorage(String path, File file, {String fileName}) async { + if (fileName == null) { + fileName = Uuid().v4(); + } + StorageReference storageReference = + FirebaseStorage.instance.ref().child('$path/$fileName'); + StorageUploadTask uploadTask = storageReference.putFile(file); + await uploadTask.onComplete; + String downloadUrl = await storageReference.getDownloadURL(); + print("name:${await storageReference.getName()}"); + print("bucket:${await storageReference.getBucket()}"); + print("path:${await storageReference.getPath()}"); + print("meta:${await storageReference.getMetadata()}"); + return downloadUrl; +} + +Future uploadStorageData(String path, Uint8List data, + {String fileName}) async { + if (fileName == null) { + fileName = Uuid().v4(); + } + StorageReference storageReference = + FirebaseStorage.instance.ref().child('$path/$fileName'); + StorageUploadTask uploadTask = storageReference.putData(data); + await uploadTask.onComplete; + String downloadUrl = await storageReference.getDownloadURL(); + return downloadUrl; +} + +Future deleteStorage(String path, name) async { + try { + StorageReference storageReference = + FirebaseStorage.instance.ref().child('$path/$name'); + await storageReference.delete(); + } catch (e) { + log.warning("deleteStorage:$e"); + } +} + +Future deleteStorageFromUrl(String url) async { + try { + StorageReference storageReference = + await FirebaseStorage.instance.getReferenceFromUrl(url); + await storageReference.delete(); + } catch (e) { + log.warning("deleteStorage:$e"); + } +} diff --git a/lib/model/language_model.dart b/lib/model/language_model.dart new file mode 100644 index 0000000..07b5562 --- /dev/null +++ b/lib/model/language_model.dart @@ -0,0 +1,45 @@ +import 'package:flutter/painting.dart'; +import 'package:fcs/model/base_model.dart'; +import 'package:fcs/model/shared_pref.dart'; +import 'package:fcs/vo/setting.dart'; +import 'package:fcs/widget/localization/transalation.dart'; + +class LanguageModel extends BaseModel { + String language; + bool isEng = true; + static final List languageCodesList = + Translation().supportedLanguagesCodes; + static final List languagesList = Translation().supportedLanguages; + + final Map languagesMap = { + languagesList[0]: languageCodesList[0], + languagesList[1]: languageCodesList[1], + }; + + void initSetting(Setting setting) async { + this.language = await load(); + this.isEng = this.language == "English"; + Translation().onLocaleChanged(Locale(languagesMap[language])); + notifyListeners(); + } + + + @override + logout() async { + } + + Future load() async { + var data =await SharedPref.getLang(); + if (data == null) return languagesList[1]; + return data; + } + + void saveLanguage(String language) async { + Translation().onLocaleChanged(Locale(languagesMap[language])); + + SharedPref.saveLang(language); + this.language = language; + this.isEng = this.language == "English"; + notifyListeners(); + } +} diff --git a/lib/model/log_model.dart b/lib/model/log_model.dart new file mode 100644 index 0000000..9d5b44a --- /dev/null +++ b/lib/model/log_model.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/vo/document_log.dart'; +import 'package:fcs/vo/log.dart'; + +import 'base_model.dart'; +import 'constants.dart'; + +class LogModel extends BaseModel { + List logs = []; + List docLogs = []; + + void initUser(user) { + super.initUser(user); + _loadLogs(); + } + + + @override + logout() async { + logs = []; + docLogs=[]; + } + Future _loadLogs() async { + Stream snapshots = Firestore.instance + .collection( + "/$biz_collection/${setting.okEnergyId}/$user_collection/${user.docID}/$log_collection") + .orderBy("active_time", descending: true) + .limit(30) + .snapshots(); + + snapshots.listen((snaps) async { + logs = snaps.documents + .map((documentSnapshot) => + Log.fromMap(documentSnapshot.data, documentSnapshot.documentID)) + .toList(); + notifyListeners(); + }); + } + + Future loadDocLogs(String docID) async { + Stream snapshots = Firestore.instance + .collection("/$biz_collection/${setting.okEnergyId}/$log_collection") + .where('doc_id', isEqualTo: docID) + .orderBy('date', descending: true) + .limit(30) + .snapshots(); + + snapshots.listen((snaps) async { + docLogs.clear(); + docLogs = snaps.documents + .map((documentSnapshot) => DocLog.fromMap( + documentSnapshot.data, documentSnapshot.documentID)) + .toList(); + notifyListeners(); + }); + } +} diff --git a/lib/model/main_model.dart b/lib/model/main_model.dart new file mode 100644 index 0000000..1ac35fb --- /dev/null +++ b/lib/model/main_model.dart @@ -0,0 +1,450 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:device_info/device_info.dart'; +import 'package:dio/dio.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:package_info/package_info.dart'; +import 'package:path/path.dart' as Path; +import 'package:fcs/model/shared_pref.dart'; +import 'package:fcs/util.dart'; +import 'package:fcs/vo/bank_account.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/vo/setting.dart'; +import 'package:fcs/widget/NetworkConnectivity.dart'; + +import '../config.dart'; +import '../vo/status.dart'; +import '../vo/user.dart'; +import 'api_helper.dart'; +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class ImplementInterfaceModel { + void initUser(User user) => {}; + void initSetting(Setting setting) => {}; +} + +class MainModel extends ChangeNotifier { + final log = Logger('MainModel'); + + final FirebaseAuth auth = FirebaseAuth.instance; + FirebaseMessaging firebaseMessaging; + + List models = []; + User user; + Buyer buyer; + FirebaseUser firebaseUser; + StreamSubscription userListener; + StreamSubscription buyerListener; + bool pinRequired; + Timer pinTimer; + + Setting setting; + PackageInfo packageInfo; + bool isLoaded = true; + bool isOnline = true; + + static const PIN_TIME_MIN = 10; + + MainModel() { + // NetworkConnectivity.instance.statusStream.listen((data) { + // bool _isOnline = data["isOnline"]; + // if (_isOnline && !this.isOnline) { + // init(); + // } + // this.isOnline = _isOnline; + // notifyListeners(); + // }); + } + + resetPinTimer() { + if (pinTimer != null && pinTimer.isActive) { + pinTimer.cancel(); + } + pinRequired = false; + pinTimer = Timer(Duration(minutes: PIN_TIME_MIN), () { + pinRequired = true; + }); + } + + bool isLogin() { + return true; + } + + bool hasEmail() { + return this.user != null && this.user.isEmail(); + } + + bool agreedTerm() { + return this.user != null && this.user.agreeTerms; + } + + bool isBuyer() { + return this.user == null || this.user.isBuyer(); + } + + bool isRegBuyer() { + return isBuyer() && buyer != null; + } + + bool isApprovedBuyer() { + return isBuyer() && buyer != null && buyer.isApproved(); + } + + bool isSysAdmin() { + return this.user != null && this.user.isSysAdmin(); + } + + bool isSysSupport() { + return this.user != null && this.user.isSysSupport(); + } + + bool isBizAdmin() { + return this.user != null && this.user.isBizAdmin(); + } + + bool isOwnerAndAbove() { + return this.user != null && this.user.isOwnerAndAbove(); + } + + bool isAdmin() { + return this.user != null && this.user.hasAdmin(); + } + + bool showHistoryBtn() { + return isSysAdmin() || isSysSupport() || isBizAdmin(); + } + + init() async { + // await _loadSetting(); + // _loadUser(); + // resetPinTimer(); + } + + void addModel(BaseModel model) { + models.add(model); + model.mainModel = this; + } + + void _initUser(User user) { + models.forEach((m) => m.initUser(user)); + + if (firebaseMessaging != null) { + firebaseMessaging.subscribeToTopic(user.docID); + } + } + + void _initSetting(Setting setting) { + models.forEach((m) => m.initSetting(setting)); + } + + Future _loadSetting() async { + this.setting = await _getSetting(); + this.packageInfo = await PackageInfo.fromPlatform(); + _initSetting(setting); + } + + void _loadUser() async { + this.firebaseUser = await auth.currentUser(); + if (this.firebaseUser == null) { + this.isLoaded = true; + notifyListeners(); + return; + } + + _logUser(this.firebaseUser); + + // load from local, if successful,notify listeners + User _user = await SharedPref.getUser(); + if (_user != null) { + await _user.setFirebaseUser(this.firebaseUser); + _initUser(_user); + this.user = _user; + + if (this.user.isRegisteredBuyer()) { + _loadBuyer(); + } + this.isLoaded = true; + notifyListeners(); + log.info("user loaded from shared pref!"); + } + + _listenUser(); + } + + void _listenUser() { + if (this.userListener != null) { + this.userListener.cancel(); + } + + this.userListener = getDocSnapshot( + "/$biz_collection/${setting.okEnergyId}/$user_collection", + firebaseUser.uid) + .listen((userSnap) async { + if (userSnap.exists) { + User _user = User.fromMap(userSnap.data, userSnap.documentID); + + // load claims + try { + FirebaseUser _firebaseUser = await getProfile(this.firebaseUser); + + await _user.setFirebaseUser(_firebaseUser); + _initUser(_user); + this.user = _user; + this.firebaseUser = _firebaseUser; + await SharedPref.saveUser(this.user); + } catch (e) { + log.warning(e.toString()); + } + + log.info( + "_loadUser => ID: ${this.user.docID}, AccountID: ${this.user.accountID}," + "BizID: ${this.user.accountID}," + ", Privileges: ${this.user.claimPrivileges}, isSysAdmin: ${this.user.isSysAdmin()}"); + + if (this.user.isRegisteredBuyer()) { + _loadBuyer(); + } + this.isLoaded = true; + notifyListeners(); + } + }); + } + + void _loadBuyer() async { + if (this.user == null) return; + if (buyerListener != null) buyerListener.cancel(); + buyerListener = getDocSnapshot( + "/$biz_collection/${setting.okEnergyId}/$buyer_collection", + this.user.docID) + .listen((buyerSnap) async { + if (buyerSnap.exists) { + this.buyer = Buyer.fromMap(buyerSnap.data, buyerSnap.documentID); + } else { + this.buyer = null; + } + notifyListeners(); + }); + } + + @override + void dispose() { + // super.dispose(); + // if (this.userListener != null) { + // this.userListener.cancel(); + // } + // SharedPref.removeUser(); + // this.user = User(); + } + + Future login(String phoneNumber, String pass) async { + var id = phoneNumber.replaceFirst("+", ""); + id = updatePhoneNumber(id); + + var data = {"id": id, "password": pass}; + var result = await requestAPI("/login", "POST", payload: data); + + var token = result["Token"]; + + // login with custom token + AuthResult r = await this.auth.signInWithCustomToken(token: token); + this.firebaseUser = r.user; + isLoaded = false; + _loadUser(); + _logUser(this.firebaseUser); + } + + Future getProfile(FirebaseUser firebaseUser) async { + IdTokenResult idtoken = await firebaseUser.getIdToken(); + var data = await requestAPI( + "/profile", + "GET", + token: idtoken.token, + ); + var _token = data["Token"]; + AuthResult a = await this.auth.signInWithCustomToken(token: _token); + return a.user; + } + + Future _logUser(FirebaseUser firebaseUser) async { + IdTokenResult idtoken = await firebaseUser.getIdToken(); + + await requestAPI( + "/log", + "GET", + token: idtoken.token, + ); + } + + Future logout() async { + if (this.userListener != null) { + await this.userListener.cancel(); + } + + await auth.signOut(); + this.user = null; + this.buyer = null; + this.firebaseUser = null; + await SharedPref.removeUser(); + if (firebaseMessaging != null) { + firebaseMessaging.unsubscribeFromTopic(user.docID); + } + + // logout models + models.forEach((m) => m.logout()); + + notifyListeners(); + } + + Future signup( + String name, password, confirmPassword, phoneNumber) async { + if (password == "" || password.length < 6) { + throw Exception("Password must be at least 6 characters"); + } + if (password != confirmPassword) { + throw Exception("Password mismatch"); + } + var id = phoneNumber.replaceFirst("+", ""); + id = updatePhoneNumber(id); + + var inputData = {"id": id, "password": password, "user_name": name}; + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + String deviceName = "${androidInfo.model}(${androidInfo.id})"; + + var url = "${Config.instance.apiURL}/signup"; + Response response = await Dio().post(url, + data: inputData, + options: Options( + headers: {"Device": androidInfo.androidId + ":" + deviceName})); + + var data = Status.fromJson(response.data); + if (data.status != 'Ok') { + throw Exception("${data.errorCode} : ${data.message}"); + } + } + + Future confirmSignup( + String phoneNumber, password, confirmSMSCode) async { + var id = phoneNumber.replaceFirst("+", ""); + id = updatePhoneNumber(id); + + if (confirmSMSCode == "" || confirmSMSCode.length != 6) { + throw Exception("Password must be 6 characters"); + } + + var inputData = { + "id": id, + "password": password, + "confirmation_code": confirmSMSCode + }; + var url = "${Config.instance.apiURL}/confirm"; + Response response = await Dio().post( + url, + data: inputData, + ); + var data = Status.fromJson(response.data); + if (data.status != 'Ok') { + throw Exception(data.message); + } + } + + bool isSupport() { + if (packageInfo == null || setting == null) return false; + return int.parse(packageInfo.buildNumber) >= setting.supportBuildNum; + } + + Future _getSetting() async { + var snap = await Firestore.instance + .collection(config_collection) + .document(setting_doc_id) + .get(); + if (!snap.exists) { + return null; + } + _listSetting(); + return Setting.fromMap(snap.data); + } + + void _listSetting() { + getDocSnapshot("/configs", setting_doc_id).listen((snap) { + this.setting = Setting.fromMap(snap.data); + notifyListeners(); + }); + } + + Future updateProfile(String name) async { + await requestAPI("/user", "PUT", + payload: {"user_name": name}, token: await getToken()); + } + + Future updateTerms(String terms) async { + await requestAPI("/terms", "PUT", + payload: {"terms": terms}, token: await getToken()); + } + + Future agreeTerms() async { + await requestAPI("/user/agree", "PUT", token: await getToken()); + } + + Future updateContact(Setting setting) async { + await requestAPI("/contact", "PUT", + payload: { + 'email': setting.email, + 'facebook_url': setting.facebook, + 'web_url': setting.website, + 'phones': setting.phones, + 'bank_account_info': setting.bankAccountInfo, + 'delivery_phone': setting.deliveryPhone, + 'address': setting.address, + }, + token: await getToken()); + } + + Future updateSetting(Setting setting) async { + await requestAPI("/setting", "PUT", + payload: { + 'do_expire_hours': setting.doExpireInHours, + 'po_expire_hours': setting.poExpireInHours, + 'po_open_at': setting.poOpenAt, + 'po_close_at': setting.poCloseAt, + 'po_close_on': setting.poCloseOn, + 'first_storage_charge_in': setting.firstStorageChargeIn, + 'first_storage_charge': setting.firstStorageCharge, + 'second_storage_charge_in': setting.secondStorageChargeIn, + 'second_storage_charge': setting.secondStorageCharge, + 'latest_delivery_days': setting.latestDeliveryDay, + }, + token: await getToken()); + } + + Future addBankAccount(BankAccount bankAccount, File image) async { + String url = await uploadStorage(bank_images_path, image); + bankAccount.bankLogo = url; + + await requestAPI("/bank_accounts", "POST", + payload: bankAccount.toMap(), token: await getToken()); + } + + Future updateBankAccount(BankAccount bankAccount, File image) async { + if (image != null) { + String url = await uploadStorage(bank_images_path, image); + bankAccount.bankLogo = url; + } + + await requestAPI("/bank_accounts", "PUT", + payload: bankAccount.toMap(), token: await getToken()); + } + + Future deleteBankAccount(BankAccount bankAccount) async { + await requestAPI("/bank_accounts", "DELETE", + payload: bankAccount.toMap(), token: await getToken()); + } +} diff --git a/lib/model/manual_model.dart b/lib/model/manual_model.dart new file mode 100644 index 0000000..8a778da --- /dev/null +++ b/lib/model/manual_model.dart @@ -0,0 +1,234 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:archive/archive_io.dart'; +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:fcs/model/firebase_helper.dart'; +import 'package:fcs/vo/manual.dart'; +import 'package:fcs/vo/setting.dart'; +import 'package:path/path.dart' as Path; + +import 'base_model.dart'; + +typedef void SlideDataCallback(); + +class ManualModel extends BaseModel { + final log = Logger('ManualModel'); + + List helps = []; + List manuals = []; + String version; + SlideDataCallback slideDataCallback; + List deleteImage = []; + + String dataDir; + + void initSetting(Setting setting) async { + _download(setting); + super.initSetting(setting); + } + + @override + logout() async {} + + Future _download(Setting setting) async { + this.dataDir = (await getApplicationDocumentsDirectory()).path; + version = setting.helpVersion; + var file = File('$dataDir/${setting.helpFileName()}'); + if (await file.exists()) { + _loadJsonData(); + return; + } + + String url = setting.helpURL; + var req = await http.Client().get(Uri.parse(url)); + + File zippedFile = await file.writeAsBytes(req.bodyBytes); + File prev = File('$dataDir/manual'); + if (await prev.exists()) { + await prev.delete(recursive: true); + } + + var bytes = zippedFile.readAsBytesSync(); + var archive = ZipDecoder().decodeBytes(bytes); + for (var file in archive) { + var fileName = '$dataDir/manual/${file.name}'; + if (file.isFile) { + var outFile = File(fileName); + outFile = await outFile.create(recursive: true); + await outFile.writeAsBytes(file.content); + } + } + _loadJsonData(); + } + + List getHelpList(bool isBuyer) { + return helps.where((h) => isBuyer ? h.isBuyer : true).toList(); + } + + getSlideList(int manIndex) { + var slides = helps[manIndex].slides; + return slides; + } + + getSlideData(int manIndex, int slideIndex) { + var slide; + slide = helps[manIndex].slides[slideIndex]; + return slide; + } + + Future _loadJsonData() async { + try { + final directory = await getApplicationDocumentsDirectory(); + File file = File('${directory.path}/manual/manual.json'); + String contents = await file.readAsString(); + + var convertArray = jsonDecode(contents); + manuals.clear(); + convertArray.forEach((item) { + var _list = ManualItem.fromMap(item); + manuals.add(_list); + }); + } catch (e) { + log.warning("Error:${e.toString()}"); + } + helps.clear(); + helps = manuals.map((e) => ManualItem.clone(e)).toList(); + } + + addManualTitle(ManualItem manualItem) { + helps.add(manualItem); + notifyListeners(); + } + + uploadStorageManualData(String version, String dir) async { + String fileName = 'help-v$version.zip'; + + var converthelps = []; + for (final converthelp in helps) { + converthelps.add(converthelp.toJson()); + } + var json = jsonEncode(converthelps); + + Directory myDir = new Directory('$dir/manual/img'); + List _images; + _images = myDir.listSync(recursive: true, followLinks: false); + + var newImgData = await convertArchiveImgFile(_images, dir, json); + File updateImgFile = File('$dir/manual/update'); + updateImgFile.writeAsBytesSync(newImgData); + + var bytes = updateImgFile.readAsBytesSync(); + uploadDataZip(bytes, fileName); + } + + convertArchiveImgFile(List imgList, String dataDir, json) async { + File file = new File('$dataDir/update'); + if (await file.exists()) { + await file.delete(recursive: true); + } + + Archive zipArchive = new Archive(); + List utf8encoded = utf8.encode(json); + ArchiveFile jsonFile = + new ArchiveFile("manual.json", utf8encoded.length, utf8encoded); + zipArchive.addFile(jsonFile); + + for (var img in imgList) { + String basename = Path.basename(img.path); + if (deleteImage.length != 0) { + for (var dImgName in deleteImage) { + if (dImgName != basename) { + Uint8List bytesPhoto = img.readAsBytesSync() as Uint8List; + ArchiveFile jsonFile = + new ArchiveFile("img/$basename", bytesPhoto.length, bytesPhoto); + zipArchive.addFile(jsonFile); + } + } + } else { + Uint8List bytesPhoto = img.readAsBytesSync() as Uint8List; + ArchiveFile jsonFile = + new ArchiveFile("img/$basename", bytesPhoto.length, bytesPhoto); + zipArchive.addFile(jsonFile); + } + } + List zipInBytes = new ZipEncoder().encode(zipArchive); + file.writeAsBytesSync(zipInBytes); + var bytes = file.readAsBytesSync(); + return bytes; + } + + uploadDataZip(Uint8List data, String fileName) async { + String path = '/ok/img'; + String url = await uploadStorageData(path, data, fileName: fileName); + } + + resetManualItems() { + helps.clear(); + //clone manauals + helps = manuals.map((p) => ManualItem.clone(p)).toList(); + // return helps.where((h) => isBuyer ? h.isBuyer : true).toList(); + } + + saveInstruction(int manIndex, int slideIndex, int instIndex, + Instruction instruction, Instruction oldInst, bool isEng) { + if (isEng) { + var inst = helps[manIndex].slides[slideIndex].instructions.toList(); + instruction.id = oldInst.id; + if (inst.length != 0) { + helps[manIndex].slides[slideIndex].instructions.remove(oldInst); + helps[manIndex] + .slides[slideIndex] + .instructions + .insert(instIndex, instruction); + } else { + helps[manIndex].slides[slideIndex].instructions.add(instruction); + } + } else { + var inst = helps[manIndex].slides[slideIndex].instructionsmm.toList(); + instruction.id = oldInst.id; + if (inst.length != 0) { + helps[manIndex].slides[slideIndex].instructionsmm.remove(oldInst); + helps[manIndex] + .slides[slideIndex] + .instructionsmm + .insert(instIndex, instruction); + } else { + helps[manIndex].slides[slideIndex].instructionsmm.add(instruction); + } + } + notifyListeners(); + } + + saveSlideData(int manIndex, int slideIndex, SlideData slideData) { + helps[manIndex].slides.add(slideData); + notifyListeners(); + } + + changeSlideImage(int manIndex, int slideIndex, SlideData slideData) { + var oldSlide = helps[manIndex].slides[slideIndex]; + helps[manIndex].slides.remove(oldSlide); + helps[manIndex].slides.insert(slideIndex, slideData); + notifyListeners(); + } + + deleteSlideData(int manIndex, SlideData slideData) { + String engImage = slideData.image; + String mmImage = slideData.imagemm; + deleteImage.add(mmImage); + deleteImage.add(engImage); + helps[manIndex].slides.remove(slideData); + notifyListeners(); + } + + deleteManualItem(ManualItem item) { + // if(helps.isEmpty){ + // helps = new List(); + // } + helps.remove(item); + notifyListeners(); + } +} diff --git a/lib/model/messaging.dart b/lib/model/messaging.dart new file mode 100644 index 0000000..c3c8fbf --- /dev/null +++ b/lib/model/messaging.dart @@ -0,0 +1,35 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/vo/user.dart'; + +class MessagingFCM { + final log = Logger('MessagingFCM'); + + final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); + + MessagingFCM(User user) { + _firebaseMessaging.configure( + onMessage: (Map message) async { + log.info("onMessage: $message"); + }, + // onBackgroundMessage: backgroundMessageHandler, + onLaunch: (Map message) async { + log.info("onLaunch: $message"); + }, + onResume: (Map message) async { + log.info("onResume: $message"); + }, + ); + _firebaseMessaging.requestNotificationPermissions( + const IosNotificationSettings( + sound: true, badge: true, alert: true, provisional: true)); + _firebaseMessaging.onIosSettingsRegistered + .listen((IosNotificationSettings settings) { + log.info("Settings registered: $settings"); + }); + _firebaseMessaging.getToken().then((String token) { + log.info("Token:$token"); + }); + _firebaseMessaging.subscribeToTopic(user.docID); + } +} diff --git a/lib/model/notification_model.dart b/lib/model/notification_model.dart new file mode 100644 index 0000000..f7c1b70 --- /dev/null +++ b/lib/model/notification_model.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/vo/notification.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class NotificationModel extends BaseModel { + int filer=0; + List notifications = []; + + var filterValues = {1: "po", 2: "do", 3: "buyer"}; + List get notis { + return notifications + .where((n) => filer == 0 || n.itemType == filterValues[filer]) + .toList(); + } + + int unseen = 0; + + void initUser(user) { + super.initUser(user); + _loadNotifications(); + } + + + @override + logout() async { + notifications = []; + } + + Future _loadNotifications() async { + Stream snapshots = Firestore.instance + .collection( + "/$biz_collection/${setting.okEnergyId}/$user_collection/${user.docID}/$notification_collection") + .orderBy("time", descending: true) + .limit(50) + .snapshots(); + + snapshots.listen((snaps) async { + notifications.clear(); + unseen = 0; + // snaps.documentChanges.forEach((c) { + // if (c.type == DocumentChangeType.added) { + // FlutterRingtonePlayer.play( + // android: AndroidSounds.notification, + // ios: IosSounds.glass, + // ); + // } + // }); + snaps.documents.forEach((d) { + var n = Notification.fromMap(d.data, d.documentID); + if (!n.seen) unseen++; + notifications.add(n); + }); + notifyListeners(); + }); + } + + void filter(int filter) { + this.filer = filter; + } + + Future seen() async { + await request("/notification/seen", "POST", token: await getToken()); + } + + Future seenID(String id) async { + await request("/notification/seen/${user.docID}/$id", "POST", + token: await getToken()); + } +} diff --git a/lib/model/pagination/paginator.dart b/lib/model/pagination/paginator.dart new file mode 100644 index 0000000..60275d2 --- /dev/null +++ b/lib/model/pagination/paginator.dart @@ -0,0 +1,4 @@ + +class Listener { + +} \ No newline at end of file diff --git a/lib/model/pd_model.dart b/lib/model/pd_model.dart new file mode 100644 index 0000000..3e50965 --- /dev/null +++ b/lib/model/pd_model.dart @@ -0,0 +1,88 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/model/constants.dart'; +import 'package:fcs/vo/pd.dart'; +import 'package:fcs/vo/user.dart'; + +import 'base_model.dart'; +import 'firebase_helper.dart'; + +class PDModel extends BaseModel { + final log = Logger('PDModel'); + + List pds = []; + int dateIndex = 0; + DateTime selectedDate = DateTime.now(); + + void initUser(User user) async { + super.initUser(user); + loadPDs(); + } + + + @override + logout() async { + pds = []; + } + + loadPDs() { + if (!user.isOwnerAndAbove() && !user.hasInventory()) { + return; + } + + try { + String path = "/$biz_collection/${setting.okEnergyId}/$pds_collection"; + var startDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0); + var endDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 23, 59, 59); + pds.clear(); + getFilterDateSnapshot(path, 'date', startDate, endDate, 'pd_number') + .listen((QuerySnapshot snapshot) { + pds = snapshot.documents.map((documentSnapshot) { + var data = + PD.fromMap(documentSnapshot.data, documentSnapshot.documentID); + return data; + }).toList(); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future loadPDLines(PD pd) async { + var snaps = await getSnapshot( + "/$biz_collection/${setting.okEnergyId}/$pds_collection/${pd.id}/$product_collection"); + pd.pdLines = snaps.documents.map((s) => PDLine.fromMap(s.data)).toList(); + return pd; + } + + Future createPD(PD pd) async { + await request("/pd", "POST", + payload: pd.toMap(), token: await getToken()); + } + + void filterDate(DateTime dateTime, int _dateIndex) { + this.selectedDate = dateTime; + this.dateIndex = _dateIndex; + String path = "/$biz_collection/${setting.okEnergyId}/$pds_collection"; + var endDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59); + pds.clear(); + getFilterDateSnapshot(path, 'date', dateTime, endDate, 'pd_number').listen( + (snaps) { + pds = snaps.documents.map((documentSnapshot) { + var data = + PD.fromMap(documentSnapshot.data, documentSnapshot.documentID); + return data; + }).toList(); + notifyListeners(); + }, onError: (error) { + log.warning("FIRESTORE ERROR>>$error"); + }); + } +} + diff --git a/lib/model/po_model.dart b/lib/model/po_model.dart new file mode 100644 index 0000000..5bd6a98 --- /dev/null +++ b/lib/model/po_model.dart @@ -0,0 +1,294 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as Path; +import 'package:fcs/model/constants.dart'; +import 'package:fcs/pages/po/po_files.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/vo/popup_menu.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class POSubmissionModel extends BaseModel { + final log = Logger('POSubmissionModel'); + + StreamSubscription listener; + + List pos = []; + List approvedPOs = []; + PopupMenu popupMenu = new PopupMenu(index: 0); + int dateIndex = 0; + DateTime selectedDate = DateTime.now(); + void initUser(user) async { + super.initUser(user); + _loadPOs(); + _loadApprovedPOs(); + } + + @override + logout() async { + if (listener != null) await listener.cancel(); + pos = []; + approvedPOs = []; + } + + Future _loadPOs() async { + String path; + if (user.hasPO() || user.isOwnerAndAbove()) { + path = "/$biz_collection/${setting.okEnergyId}/$pos_collection"; + } + + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$pos_collection"; + } + var startDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0); + var endDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 23, 59, 59); + listener = + getFilterDateSnapshot(path, 'po_date', startDate, endDate, 'po_number') + .listen((snaps) async { + pos.clear(); + snaps.documents.forEach((d) { + pos.add(POSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } + + Future _loadApprovedPOs() async { + if (!user.isBuyer()) { + return; + } + approvedPOs.clear(); + + String path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$pos_collection"; + var docs = await Firestore.instance + .collection(path) + .where("status", isEqualTo: po_approved_status) + .orderBy("po_approved_date", descending: false) + .limit(1) + .getDocuments(); + + Firestore.instance + .collection(path) + .where("status", isEqualTo: po_approved_status) + .orderBy("po_approved_date", descending: false) + .limit(10) + .snapshots(includeMetadataChanges: true) + .listen((snaps) async { + List _approved = []; + for (var d in snaps.documents) { + if (d.metadata.isFromCache) continue; + var po = POSubmission.fromMap(d.data, d.documentID); + po.poLines = await loadPOLines(po.id); + _approved.add(po); + } + approvedPOs.clear(); + approvedPOs.addAll(_approved); + notifyListeners(); + }); + } + + Future getPO(String id) async { + String path = "/$biz_collection/${setting.okEnergyId}/$pos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$pos_collection"; + } + var poSnap = await getDocSnap(path, id); + return POSubmission.fromMap(poSnap.data, poSnap.documentID); + } + + Future> loadPOLines(String poID) async { + String path = + "/$biz_collection/${setting.okEnergyId}/$pos_collection/$poID/$po_product_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$pos_collection/$poID/$product_collection"; + } + var snaps = await Firestore.instance.collection(path).getDocuments(); + List poLines = + snaps.documents.map((s) => POLine.fromMap(s.data)).toList(); + return poLines; + } + + Future loadDOs(POSubmission po) async { + String path = "/$biz_collection/${setting.okEnergyId}/$dos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection"; + } + var snaps = await Firestore.instance + .collection(path) + .where("po_number", isEqualTo: po.poNumber) + .orderBy('do_number', descending: true) + .getDocuments(); + + po.dos = snaps.documents + .map((s) => DOSubmission.fromMap(s.data, s.documentID)) + .toList(); + return po; + } + + Future loadDOLines(DOSubmission doSub) async { + String path = + "/$biz_collection/${setting.okEnergyId}/$dos_collection/${doSub.id}/$product_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$dos_collection/${doSub.id}/$product_collection"; + } + var snaps = await getSnapshot(path); + doSub.doLines = snaps.documents.map((s) => DOLine.fromMap(s.data)).toList(); + return doSub; + } + + Future createPO(POSubmission po, List files) async { + if (files != null) { + if (files.length > 5) throw Exception("Exceed number of file upload"); + po.poReceiptUrls = []; + for (File f in files) { + String path = Path.join(po_files_path, user.docID); + String url = await uploadStorage(path, f); + po.poReceiptUrls.add(url); + } + } + + await request("/po", "POST", payload: po.toMap(), token: await getToken()); + } + + Future updatePO( + POSubmission po, List files, List deletedUrls) async { + if (deletedUrls != null) + for (String url in deletedUrls) { + po.poReceiptUrls.remove(url); + await deleteStorageFromUrl(url); + } + + if (files != null) { + if (files.length + po.poReceiptUrls.length > 5) + throw Exception("Exceed number of file upload"); + po.poReceiptUrls = po.poReceiptUrls == null ? [] : po.poReceiptUrls; + for (File f in files) { + String path = Path.join(po_files_path, user.docID); + String url = await uploadStorage(path, f); + po.poReceiptUrls.add(url); + } + } + await request("/po", "PUT", payload: po.toMap(), token: await getToken()); + } + + Future approvePO(POSubmission po) async { + await request("/po/approved", "POST", + payload: po.toMap(), token: await getToken()); + } + + Future rejectPO(POSubmission po) async { + await request("/po/rejected", "POST", + payload: po.toMap(), token: await getToken()); + } + + Future cancelPO(POSubmission po) async { + await request("/po/canceled", "POST", + payload: po.toMap(), token: await getToken()); + } + + void filterData( + String status, DateTime dateTime, int _selectedStatus, int _dateIndex) { + pos.clear(); + var startDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 0, 0, 0); + var endDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59); + + if (this.listener != null) { + this.listener.cancel(); + } + + this.popupMenu.index = _selectedStatus; + this.dateIndex = _dateIndex; + this.selectedDate = dateTime == null + ? new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0) + : dateTime; + + String path = "/$biz_collection/${setting.okEnergyId}/$pos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$pos_collection"; + } + + if (status != null && dateTime == null) { + this.listener = getFilterStatusSnapshot(path, status, 'po_number') + .listen((snaps) async { + pos.clear(); + snaps.documents.forEach((d) { + pos.add(POSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else if (dateTime != null && status == null) { + this.listener = getFilterDateSnapshot( + path, 'po_date', startDate, endDate, 'po_number') + .listen((snaps) async { + pos.clear(); + snaps.documents.forEach((d) { + pos.add(POSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else if (status != null && dateTime != null) { + this.listener = getFilterDataSnapshot( + path, status, 'po_date', startDate, endDate, 'po_number') + .listen((snaps) { + pos.clear(); + snaps.documents.forEach((d) { + pos.add(POSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } else { + this.listener = + getQuerySnapshotByOrder(path, 'po_number').listen((snaps) async { + pos.clear(); + snaps.documents.forEach((d) { + pos.add(POSubmission.fromMap(d.data, d.documentID)); + }); + notifyListeners(); + }); + } + } + + Future> getPOForRevenue(DateTime dateTime) async { + List pos = []; + + String path = "/$biz_collection/${setting.okEnergyId}/$pos_collection"; + if (user.isBuyer()) { + path = + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$pos_collection"; + } + DateTime date = DateTime(dateTime.year, dateTime.month, dateTime.day); + DateTime dateAddOne = date.add(Duration(days: 1)); + + QuerySnapshot snapshots = await Firestore.instance + .collection(path) + .where('status', whereIn: [po_approved_status, po_closed_status]) + .where("po_approved_date", isGreaterThanOrEqualTo: date) + .where("po_approved_date", isLessThan: dateAddOne) + .orderBy("po_approved_date") + .orderBy("user_name") + .limit(100) + .getDocuments(); + snapshots.documents.forEach((d) { + pos.add(POSubmission.fromMap(d.data, d.documentID)); + }); + return pos; + } +} diff --git a/lib/model/product_model.dart b/lib/model/product_model.dart new file mode 100644 index 0000000..14e21d4 --- /dev/null +++ b/lib/model/product_model.dart @@ -0,0 +1,119 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/vo/setting.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class ProductModel extends BaseModel { +final log = Logger('ProductModel'); + + List products = []; + List tempProducts = []; + + List get getPrices { + List productPrices = []; + this.products.forEach((p) => productPrices.addAll(p.getPrices)); + productPrices.sort((p1, p2) => p1.compareTo(p2)); + return productPrices; + } + + String getProductName(String productID) { + return products.firstWhere((p) => p.id == productID).name; + } + + Product getProduct(String productID) { + return products.firstWhere((p) => p.id == productID); + } + + List get productsToEdit { + // clone products + tempProducts = products.map((p) => Product.clone(p)).toList(); + tempProducts.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + // set old price with price + tempProducts.forEach((p) { + p.oldPirce = p.price; + p.action = "update"; + }); + + return tempProducts; + } + + void saveProduct(Product product, String name, price, displayOrder, int color, + bool isDisable) { + if (product == null) { + tempProducts.add(Product( + action: "create", + name: name, + price: int.parse(price), + color: color, + displayOrder: int.parse(displayOrder), + isDisable: isDisable)); + } else { + Product _product = product; + _product.name = name; + _product.price = int.parse(price); + _product.color = color; + _product.displayOrder = int.parse(displayOrder); + _product.isDisable = isDisable; + if (_product.id == null) { + _product.action = "create"; + } else { + _product.action = "update"; + } + } + notifyListeners(); + } + + void deleteProduct(Product product) { + if (product == null) { + return; + } + if (product.id == null) { + tempProducts.remove(product); + } else { + Product _product = tempProducts.firstWhere((p) => p.id == product.id); + _product.action = "delete"; + } + notifyListeners(); + } + + @override + void initSetting(Setting setting) { + super.initSetting(setting); + + try { + getQuerySnapshot( + "/$biz_collection/${setting.okEnergyId}/$product_collection") + .listen((QuerySnapshot snapshot) { + products.clear(); + products = snapshot.documents.map((documentSnapshot) { + var data = Product.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + return data; + }).toList(); + products.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + @override + logout() async { + tempProducts = []; + } + + updateProducts(List products) async { + var items = []; + products.forEach((p) { + if (p.action != null) items.add(p.toMap()); + }); + await request("/products", "PUT", payload: items, token: await getToken()); + } +} diff --git a/lib/model/reg_model.dart b/lib/model/reg_model.dart new file mode 100644 index 0000000..9b08d4f --- /dev/null +++ b/lib/model/reg_model.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:path/path.dart' as Path; +import 'package:fcs/vo/buyer.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class Attachments { + File nricFront, nricBack; +} + +class RegModel extends BaseModel { + Buyer reg = Buyer(); + bool isLoaded = false; + + StreamSubscription regListener; + + void initUser(user) { + super.initUser(user); + if (user.isRegisteredBuyer()) { + _loadReg(); + } else { + reg = Buyer(); + } + } + + @override + logout() async { + if (regListener != null) await regListener.cancel(); + reg = Buyer(); + } + + Future _loadReg() async { + if (regListener != null) { + regListener.cancel(); + } + regListener = getDocSnapshot( + "/$biz_collection/${setting.okEnergyId}/$buyer_collection", + "${user.docID}") + .listen((snap) async { + if (snap.exists) { + reg = Buyer.fromMap(snap.data, snap.documentID); + QuerySnapshot q = await getSnapshot( + "/$biz_collection/${setting.okEnergyId}/$buyer_collection/${user.docID}/$product_collection"); + reg.buyerProducts.clear(); + q.documents.forEach((d) { + reg.buyerProducts.add(BuyerProduct.fromMap(d.data, d.documentID)); + }); + } else { + reg = Buyer(); + } + isLoaded = true; + notifyListeners(); + }); + } + + Future register(Buyer buyer, Attachments attachments) async { + String path = Path.join(reg_files_path, user.docID); + String urlFront = await uploadStorage(path, attachments.nricFront); + buyer.nricFrontUrl = urlFront; + String urlBack = await uploadStorage(path, attachments.nricBack); + buyer.nricBackUrl = urlBack; + + await request("/reg", "POST", + payload: buyer.toMap(), token: await getToken()); + } + + Future update(Buyer buyer, Attachments attachments) async { + String path = Path.join(reg_files_path, user.docID); + if (attachments.nricFront != null) { + String urlFront = await uploadStorage(path, attachments.nricFront); + buyer.nricFrontUrl = urlFront; + } + if (attachments.nricBack != null) { + String urlBack = await uploadStorage(path, attachments.nricBack); + buyer.nricBackUrl = urlBack; + } + + await request("/buyer/update", "PUT", + payload: buyer.toMap(), token: await getToken()); + } +} diff --git a/lib/model/report_model.dart b/lib/model/report_model.dart new file mode 100644 index 0000000..849ba98 --- /dev/null +++ b/lib/model/report_model.dart @@ -0,0 +1,700 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/vo/report.dart'; +import 'package:fcs/vo/report_user.dart'; +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:fcs/model/api_helper.dart' as api; +import 'package:fcs/vo/report.dart'; + +import '../config.dart'; +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; +import 'shared_pref.dart'; + +class ReportModel extends BaseModel { + StreamSubscription listener; + StreamSubscription listenerUser; + + List reports = []; + List userReports = []; + List filterValue = []; + + List get reportList { + List _reports = []; + if (user.isOwnerAndAbove() || user.hasAdmin()) { + _reports = reports; + } else { + _reports.addAll(reports); + userReports.forEach((r) { + if (!_reports.contains(r)) { + _reports.add(r); + } + }); + } + _reports.sort((a, b) => a.display.compareTo(b.display)); + return _reports; + } + + void initUser(user) { + super.initUser(user); + reports = []; + _loadReports(); + } + + Future _loadReports() async { + if (listener != null) listener.cancel(); + if (listenerUser != null) listenerUser.cancel(); + + if (user.isOwnerAndAbove() || user.hasAdmin()) { + listener = Firestore.instance + .collection("/$reports_collection") + .snapshots() + .listen((snaps) async { + reports.clear(); + reports = snaps.documents.map((documentSnapshot) { + var report = Report.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + return report; + }).toList(); + notifyListeners(); + }); + } else { + listener = Firestore.instance + .collection("/$reports_collection") + .where("for_all_users", isEqualTo: true) + .snapshots() + .listen((snaps) async { + reports.clear(); + reports = snaps.documents.map((documentSnapshot) { + var report = Report.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + return report; + }).toList(); + notifyListeners(); + }); + + listenerUser = Firestore.instance + .collection("/$report_user_collection") + .where("user_id", isEqualTo: user.docID) + .snapshots() + .listen((snaps) async { + userReports.clear(); + userReports = snaps.documents.map((documentSnapshot) { + var user = ReportUser.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + var report = Report(id: user.reportID, display: user.reportName); + return report; + }).toList(); + notifyListeners(); + }); + } + notifyListeners(); + } + + @override + void logout() { + reports = []; + if (listener != null) listener.cancel(); + if (listenerUser != null) listenerUser.cancel(); + } + + Future getReport(String repID) async { + String path = "/$reports_collection"; + + var snap = await Firestore.instance.collection(path).document(repID).get(); + Report report = Report.fromMap(snap.data, snap.documentID); + return report; + } + + Future getReportData(Report report, int limit, int offset) async { + List reportDataList = []; + var aggFun = []; + var fields = []; + var groupbys = []; + // print('report => $report'); + try { + report.fields.asMap().forEach((key, value) { + if (value.aggFun == '') { + aggFun.add(''); + } else { + aggFun.add(value.aggFun); + } + }); + report.fields.asMap().forEach((key, value) { + fields.add(value.name); + }); + + String strFields; + fields.forEach((element) { + if (strFields == null) { + strFields = element; + } else { + strFields = strFields + ',' + element; + } + }); + + String strAgg; + aggFun.forEach((element) { + if (strAgg == null) { + strAgg = element; + } else { + strAgg = strAgg + ',' + element; + } + }); + + String strGroup; + groupbys.forEach((element) { + if (strGroup == null) { + strGroup = element; + } else { + strGroup = strGroup + ',' + element; + } + }); + + var data = { + "fields": strFields == null ? '' : strFields, + "aggfuns": strAgg == null ? '' : strAgg, + // "groupbys": strGroup == null ? '' : strGroup, + "limit": limit, + "offset": offset + }; + print("payload:$data"); + + var rdata = { + "fields": 'quantity,product_id,product_name', + "aggfuns": ",,", + "groupbys": 'product_id', + "limit": limit, + "offset": offset + }; + var result = await request("/api/data/${report.object}", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + + if (result == null) return []; + result.forEach((rdata) { + reportDataList.add(rdata); + }); + // print('reportDataList => $reportDataList'); + notifyListeners(); + return reportDataList; + } catch (e) { + log.warning("Error get Summary>>>>${e.toString()}"); + } + } + + Future downloadReportData(Report report) async { + var aggFun = []; + var fields = []; + report.fields.asMap().forEach((key, value) { + if (value.aggFun == '') { + aggFun.add(''); + } else { + aggFun.add(value.aggFun); + } + }); + report.fields.asMap().forEach((key, value) { + fields.add(value.name); + }); + + String strFields; + fields.forEach((element) { + if (strFields == null) { + strFields = element; + } else { + strFields = strFields + ',' + element; + } + }); + + String strAgg; + aggFun.forEach((element) { + if (strAgg == null) { + strAgg = element; + } else { + strAgg = strAgg + ',' + element; + } + }); + + // final directory = await getApplicationDocumentsDirectory(); + final directory = await getExternalStorageDirectory(); + String path = ('${directory.path}/${report.id}.pdf'); + log.info("download file path:$path"); + + var data = { + "fields": strFields == null ? '' : strFields, + "aggfuns": strAgg == null ? '' : strAgg, + "greoupbys": '', + "limit": 0, + "offset": 0 + }; + + await api.requestDownloadPDFAPI("/api/report-pdf/${report.object}", "GET", + filePath: path, + url: Config.instance.reportURL, + token: await getToken(), + payload: jsonEncode(data)); + + final message = await OpenFile.open(path); + log.info("Open file result:$message"); + } + + Future getJson(Report report) async { + var aggFun = []; + var fields = []; + report.fields.asMap().forEach((key, value) { + if (value.aggFun == '') { + aggFun.add(''); + } else { + aggFun.add(value.aggFun); + } + }); + report.fields.asMap().forEach((key, value) { + fields.add(value.name); + }); + + String strFields; + fields.forEach((element) { + if (strFields == null) { + strFields = element; + } else { + strFields = strFields + ',' + element; + } + }); + + String strAgg; + aggFun.forEach((element) { + if (strAgg == null) { + strAgg = element; + } else { + strAgg = strAgg + ',' + element; + } + }); + + var data = { + "fields": strFields == null ? '' : strFields, + "aggfuns": strAgg == null ? '' : strAgg, + "greoupbys": '', + "limit": 0, + "offset": 0 + }; + return jsonEncode(data); + } + + Future getEscapeJson(Report report) async { + var bytes = utf8.encode(await getJson(report)); + var base64Str = base64.encode(bytes); + return HtmlEscape().convert(base64Str); + } + + Future getReportDataWithFilters(Report report, List filters) async { + List reportDataList = []; + var aggFun = []; + var fields = []; + var groupbys = []; + + try { + var data = report.convertArrayToString(report, filters); + print('data=> $data'); + + var result = await request("/api/data/${report.object}", "POST", + token: await getToken(), + url: Config.instance.reportURL, + payload: jsonEncode(data)); + + if (result == null) return []; + result.forEach((rdata) { + reportDataList.add(rdata); + }); + + notifyListeners(); + return reportDataList; + } catch (e) { + log.warning("Error get Summary>>>>${e.toString()}"); + } + } + + Future addreport() async { + var data = [ + { + "display": "Buyer Rpt", + "object": "buyer_rpt", + "display_filters": [ + { + "name": "user_name", + "display_name": "User Name", + "compare": "==", + "data_type": "string", + }, + ], + "fields": [ + { + "name": "user_name", + "display_name": "User Name", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "reg_date", + "display_name": "Registered Date", + "to_fixed": 0, + "type": "intdate", + "agg_fun": "", + }, + { + "name": "biz_name", + "display_name": "Business Name", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "biz_address", + "display_name": "Business Address", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + ], + "display_fields": ['biz_name', 'biz_name', 'reg_date', 'user_name'], + "sorts": [], + "groupbys": [] + }, + { + "display": "Delivery Rpt", + "object": "delivery_rpt", + "display_filters": [ + { + "name": "user_name", + "display_name": "User Name", + "compare": "==", + "data_type": "string", + }, + ], + "fields": [ + { + "name": "user_name", + "display_name": "User Name", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "delivery_ended_date", + "display_name": "Delivery Ended Date", + "to_fixed": 0, + "type": "intdate", + "agg_fun": "", + }, + { + "name": "qty_92", + "display_name": "Qty 92", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_95", + "display_name": "Qty 95", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_d", + "display_name": "Qty D", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_p", + "display_name": "Qty P", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + ], + "display_fields": [ + 'qty_p', + 'qty_d', + 'qty_95', + 'qty_92', + 'delivery_ended_date', + 'user_name', + ], + "sorts": [], + "groupbys": [] + }, + { + "display": "DOs Rpt", + "object": "dos_rpt", + "display_filters": [ + { + "name": "user_name", + "display_name": "User Name", + "compare": "==", + "data_type": "string", + }, + ], + "fields": [ + { + "name": "user_name", + "display_name": "User Name", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "doc_date", + "display_name": "DO Date", + "to_fixed": 0, + "type": "intdate", + "agg_fun": "", + }, + { + "name": "doc_number", + "display_name": "PO/DO Number", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "qty_92", + "display_name": "Qty 92", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_95", + "display_name": "Qty 95", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_d", + "display_name": "Qty D", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_p", + "display_name": "Qty P", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + ], + "display_fields": [ + 'qty_p', + 'qty_d', + 'qty_95', + 'qty_92', + 'doc_date', + 'user_name', + 'doc_number' + ], + "sorts": [], + "groupbys": [] + }, + { + "display": "POs Rpt", + "object": "pos_rpt", + "display_filters": [ + { + "name": "user_name", + "display_name": "User Name", + "compare": "==", + "data_type": "string", + }, + { + "name": "po_number", + "display_name": "PO Number", + "compare": "==", + "data_type": "string", + } + ], + "fields": [ + { + "name": "user_name", + "display_name": "User Name", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "po_date", + "display_name": "PO Date", + "to_fixed": 0, + "type": "intdate", + "agg_fun": "", + }, + { + "name": "po_number", + "display_name": "PO Number", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "qty_92", + "display_name": "Qty 92", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_95", + "display_name": "Qty 95", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_d", + "display_name": "Qty D", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "qty_p", + "display_name": "Qty P", + "to_fixed": 0, + "type": "float", + "agg_fun": "", + }, + { + "name": "price_92", + "display_name": "Price 92", + "to_fixed": 0, + "type": "integer", + "agg_fun": "", + }, + { + "name": "price_95", + "display_name": "Price 95", + "to_fixed": 0, + "type": "integer", + "agg_fun": "", + }, + { + "name": "price_d", + "display_name": "Price D", + "to_fixed": 0, + "type": "integer", + "agg_fun": "", + }, + { + "name": "price_p", + "display_name": "Price P", + "to_fixed": 0, + "type": "integer", + "agg_fun": "", + } + ], + "display_fields": [ + 'qty_p', + 'qty_d', + 'qty_95', + 'qty_92', + 'price_p', + 'price_d', + 'price_95', + 'price_92', + 'po_date', + 'user_name', + 'po_number' + ], + "sorts": [], + "groupbys": [] + }, + { + "display": "Storage Charge Rpt", + "object": "storage_charge_rpt", + "display_filters": [ + { + "name": "user_name", + "display_name": "User Name", + "compare": "==", + "data_type": "string", + }, + ], + "fields": [ + { + "name": "user_name", + "display_name": "User Name", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "total_qty", + "display_name": "Quantity", + "to_fixed": 3, + "type": "float", + "agg_fun": "", + }, + { + "name": "rate", + "display_name": "Rate", + "to_fixed": 0, + "type": "string", + "agg_fun": "", + }, + { + "name": "storage_charge", + "display_name": "Amount", + "to_fixed": 3, + "type": "integer", + "agg_fun": "", + }, + ], + "display_fields": [ + 'qty_p', + 'qty_d', + 'qty_95', + 'qty_92', + 'price_p', + 'price_d', + 'price_95', + 'price_92', + 'po_date', + 'user_name', + 'po_number' + ], + "sorts": [], + "groupbys": [] + } + ]; + + data.asMap().forEach((key, value) { + Firestore.instance + .collection("/$reports_collection") + .document() + .setData(value); + }); + } + + Future saveSelectedFieldsAndPosition( + String id, List positonFields, List selectedFields) async { + // positonFields.toJson + ReportFieldPositionSelection report = ReportFieldPositionSelection( + fieldPosition: positonFields, fieldSelection: selectedFields); + await SharedPref.saveReport(report); + } + + Future loadSelectedFieldsAndPosition( + String id) async { + var data = await SharedPref.getReport(id); + return data; + } +} diff --git a/lib/model/report_user_model.dart b/lib/model/report_user_model.dart new file mode 100644 index 0000000..e97d150 --- /dev/null +++ b/lib/model/report_user_model.dart @@ -0,0 +1,83 @@ +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/model/constants.dart'; +import 'package:fcs/vo/report.dart'; +import 'package:fcs/vo/report_user.dart'; +import 'package:fcs/vo/user.dart'; + +import '../config.dart'; +import 'base_model.dart'; +import 'firebase_helper.dart'; + +class ReportUserModel extends BaseModel { + List reportUsers = []; + void initUser(user) { + super.initUser(user); + } + + @override + logout() async {} + + Future> getUsersForReport(String reportID) async { + List users = []; + String path = "/$report_user_collection"; + + QuerySnapshot snapshots = await Firestore.instance + .collection(path) + .where('report_id', isEqualTo: reportID) + .getDocuments(); + snapshots.documents.forEach((d) { + users.add(ReportUser.fromMap(d.data, d.documentID)); + }); + return users; + } + + Future> findUser(String searchUser) async { + if (searchUser == null || searchUser == '') return List(); + + var bytes = utf8.encode(searchUser); + var base64Str = base64.encode(bytes); + HtmlEscape htmlEscape = const HtmlEscape(); + String escapeUser = htmlEscape.convert(base64Str); + + int limit = 20; + List _users = []; + + try { + var data = await request( + "/api/fts/$user_collection/$escapeUser/$limit", "GET", + token: await getToken(), url: Config.instance.reportURL); + if (data == null) return List(); + + data.forEach((user) { + var _user = User.fromUserJson(user); + _users.add(_user); + }); + } catch (e) { + // permission error + log.warning("user error:" + e.toString()); + return null; + } + return _users; + } + + Future updateReportForAllUsers(Report report) async { + await request("/report", "PUT", + payload: {'id': report.id, 'for_all_users': report.forAllUser}, + token: await getToken()); + notifyListeners(); + } + + Future assignUser(ReportUser reportUser) async { + await request("/report_user", "POST", + payload: reportUser.toMap(), token: await getToken()); + notifyListeners(); + } + + Future deleteReportUser(ReportUser reportUser) async { + await request("/report_user", "DELETE", + payload: reportUser.toMap(), token: await getToken()); + notifyListeners(); + } +} diff --git a/lib/model/shared_pref.dart b/lib/model/shared_pref.dart new file mode 100644 index 0000000..f35f1aa --- /dev/null +++ b/lib/model/shared_pref.dart @@ -0,0 +1,76 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:fcs/vo/report.dart'; +import 'dart:convert'; + +import 'package:fcs/vo/user.dart'; + +class SharedPref { + static final SharedPref instance = SharedPref._(); + SharedPref._(); + + static Future getLang() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString('language'); + } + + static Future saveLang(String lang) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString('language', lang); + } + + static Future saveUser(User user) async { + await _save("user", user.toJson()); + } + + static Future saveReport(ReportFieldPositionSelection report) async { + await _save('report-${report.id}', report.toJson()); + } + + static Future getReport(String id) async { + try { + return ReportFieldPositionSelection.fromJson(await _read("report-$id")); + } catch (e) { + return null; + } + } + + static Future getUser() async { + try { + return User.fromJson(await _read("user")); + } catch (e) { + return null; + } + } + + static Future removeUser() async { + return await _remove("user"); + } + + static Future saveSkippedRecoverEmail(bool skipped) async { + await _save("skipped_recovery_email", skipped); + } + + static Future getSkippedRecoverEmail() async { + try { + bool _skipped = await _read("skipped_recovery_email"); + return _skipped; + } catch (e) { + return null; + } + } + + static _read(String key) async { + final prefs = await SharedPreferences.getInstance(); + return json.decode(prefs.getString(key)); + } + + static _save(String key, value) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString(key, json.encode(value)); + } + + static _remove(String key) async { + final prefs = await SharedPreferences.getInstance(); + prefs.remove(key); + } +} diff --git a/lib/model/storage_model.dart b/lib/model/storage_model.dart new file mode 100644 index 0000000..4356b8f --- /dev/null +++ b/lib/model/storage_model.dart @@ -0,0 +1,155 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/vo/inventory_line.dart'; +import 'package:fcs/vo/inventory_taking.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/vo/storage.dart'; + +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class StorageModel extends BaseModel { + final log = Logger('StorageModel'); + + List storages = []; + List inventoryTakings = []; + int selectedIndex = 0; + DateTime selectedDate = DateTime.now(); + + List getStorage(String productID) { + return storages + .where((s) => s.products.any((p) => p.id == productID)) + .toList(); + } + + void initUser(user) async { + super.initUser(user); + if (!user.isOwnerAndAbove() && !user.hasInventory()) { + return; + } + _loadStorages(); + _loadInventoryTakings(); + } + + @override + logout() async { + storages = []; + inventoryTakings = []; + } + + String getStorageName(String storageID) { + return storages.firstWhere((s) => s.id == storageID).name; + } + + void _loadStorages() async { + try { + getQuerySnapshotF( + "/$biz_collection/${setting.okEnergyId}/$storage_collection", + user.accountID) + .listen((QuerySnapshot snapshot) { + storages.clear(); + storages = snapshot.documents.map((documentSnapshot) { + var storage = Storage.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + loadProducts(storage); + return storage; + }).toList(); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + void _loadInventoryTakings() async { + try { + String path = "/$biz_collection/${setting.okEnergyId}/$inventory_takings"; + var startDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 0, 0, 0); + var endDate = new DateTime( + selectedDate.year, selectedDate.month, selectedDate.day, 23, 59, 59); + + inventoryTakings.clear(); + getFilterDateSnapshotF(path, user.accountID, 'date_time', startDate, + endDate, 'date_time') + .listen((QuerySnapshot snapshot) { + inventoryTakings = snapshot.documents.map((documentSnapshot) { + var data = InventoryTaking.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + + return data; + }).toList(); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + loadInventoryLines(InventoryTaking inventoryTaking) async { + if (inventoryTaking.linesLoaded) return; + var snaps = await getSnapshot( + "/$biz_collection/${setting.okEnergyId}/$inventory_takings/${inventoryTaking.id}/$inventory_lines"); + inventoryTaking.inventoryLines = + snaps.documents.map((s) => InventoryLine.fromMap(s.data)).toList(); + inventoryTaking.linesLoaded = true; + notifyListeners(); + } + + loadProducts(Storage storage) async { + if (storage.productsLoaded) return; + var snaps = await getSnapshot( + "/$biz_collection/${setting.okEnergyId}/$storage_collection/${storage.id}/$product_collection"); + storage.products = snaps.documents + .map((s) => Product.fromMap(s.data, s.documentID)) + .toList(); + storage.productsLoaded = true; + notifyListeners(); + } + + Future createStorage(Storage storage) async { + await request("/storage", "POST", + payload: storage.toMap(), token: await getToken()); + } + + Future updateStorage(Storage storage) async { + await request("/storage", "PUT", + payload: storage.toMap(), token: await getToken()); + } + + Future deleteStorage(String storageID) async { + await request("/storage/" + storageID, "DELETE", token: await getToken()); + } + + Future createInventoryTaking(InventoryTaking inventoryTaking) async { + await request("/inventory", "POST", + payload: inventoryTaking.toMap(), token: await getToken()); + } + + void filterDate(DateTime dateTime, int _selectedIndex) async { + this.selectedIndex = _selectedIndex; + this.selectedDate = dateTime; + + String path = "/$biz_collection/${setting.okEnergyId}/$inventory_takings"; + var endDate = + new DateTime(dateTime.year, dateTime.month, dateTime.day, 23, 59, 59); + inventoryTakings.clear(); + getFilterDateSnapshotF( + path, user.accountID, 'date_time', dateTime, endDate, 'date_time') + .listen((snapshot) { + inventoryTakings = snapshot.documents.map((documentSnapshot) { + var data = InventoryTaking.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + + return data; + }).toList(); + notifyListeners(); + }); + notifyListeners(); + } +} diff --git a/lib/model/test_model.dart b/lib/model/test_model.dart new file mode 100644 index 0000000..123d0b6 --- /dev/null +++ b/lib/model/test_model.dart @@ -0,0 +1,177 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:faker/faker.dart'; +import 'package:logging/logging.dart'; + +import 'base_model.dart'; + +class TestModel extends BaseModel { + final log = Logger('TestModel'); + + List tests = []; + DocumentSnapshot prev; + static const int row_count = 10; + bool ended = false; + + StreamSubscription listener; + final Query queryBase = Firestore.instance.collection("/tests"); + final Query query = Firestore.instance + .collection("/tests") + // .orderBy("deleted") + .orderBy("age", descending: false); + + void initData() async { + _clearState(); + _initListener(); + load(); + } + + void _clearState() { + prev = null; + tests = []; + ended = false; + if (listener != null) listener.cancel(); + listener = null; + } + + void _initListener() { + Query _query = queryBase.orderBy("update_time", descending: true).limit(1); + _query.getDocuments(source: Source.server).then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + if (count == 1) { + var test = Test.fromMap( + snapshot.documents[0].data, snapshot.documents[0].documentID); + + Query _queryListener = queryBase + .where("update_time", isGreaterThan: test.updateTime) + .orderBy("update_time", descending: true); + + listener = + _queryListener.snapshots(includeMetadataChanges: true).listen((qs) { + qs.documentChanges.forEach((c) { + switch (c.type) { + case DocumentChangeType.added: + var test = Test.fromMap(c.document.data, c.document.documentID); + if (tests.contains(test)) { + tests[tests.indexOf(test)].name = test.name; + notifyListeners(); + } + if (!tests.contains(test)) { + tests.add(test); + notifyListeners(); + } + break; + case DocumentChangeType.modified: + var test = Test.fromMap(c.document.data, c.document.documentID); + if (tests.contains(test)) { + bool deleted = c.document.data["deleted"]; + if (deleted != null && deleted) { + tests.remove(test); + } else { + tests[tests.indexOf(test)].name = test.name; + } + notifyListeners(); + } + break; + default: + } + }); + }); + notifyListeners(); + } + }); + } + + Future load() async { + Query _query = prev != null ? query.startAfterDocument(prev) : query; + try { + _query + // .where("deleted", isNull: null) + .limit(row_count) + .getDocuments(source: Source.server) + .then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + ended = count < row_count; + prev = count > 0 ? snapshot.documents[count - 1] : prev; + snapshot.documents.forEach((e) { + var test = Test.fromMap(e.data, e.documentID); + if (!tests.contains(test)) tests.add(test); + }); + notifyListeners(); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + void populate() { + for (var i = 0; i < 30; i++) { + Firestore.instance + .collection('tests') + .document(faker.person.name()) + .setData({ + 'name': faker.person.name(), + 'age': random.decimal(), + 'update_time': DateTime.now().microsecondsSinceEpoch + }); + } + } + + void add() { + Firestore.instance + .collection('tests') + .document(faker.person.name()) + .setData({ + 'name': faker.person.name(), + 'age': random.decimal(), + 'update_time': DateTime.now().microsecondsSinceEpoch + }); + } + + void update() { + Firestore.instance.collection('tests').document(tests[0].id).setData({ + 'name': faker.person.name(), + 'update_time': DateTime.now().microsecondsSinceEpoch + }, merge: true); + } + + void remove() { + Firestore.instance.collection('tests').document(tests[0].id).setData( + {'deleted': 1, 'update_time': DateTime.now().microsecondsSinceEpoch}, + merge: true); + } + + @override + void logout() { + _clearState(); + } +} + +class Test { + String id; + String name; + double age; + int updateTime; + Test(this.id, {this.name, this.age, this.updateTime}); + + factory Test.fromMap(Map map, String id) { + return Test(id, + name: map['name'], age: map['age'], updateTime: map['update_time']); + } + + @override + bool operator ==(other) { + if (identical(this, other)) { + return true; + } + return other.id == this.id; + } + + @override + int get hashCode { + int result = 17; + result = 37 * result + id.hashCode; + return result; + } +} diff --git a/lib/model/user_model.dart b/lib/model/user_model.dart new file mode 100644 index 0000000..7953da8 --- /dev/null +++ b/lib/model/user_model.dart @@ -0,0 +1,368 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/util.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/vo/role.dart'; + +import '../config.dart'; +import '../vo/user.dart'; +import 'base_model.dart'; +import 'constants.dart'; +import 'firebase_helper.dart'; + +class UserModel extends BaseModel { + final log = Logger('UserModel'); + StreamSubscription listener; + PopupMenu popupMenu = new PopupMenu(); + + List users = []; + List privileges = []; + List userLevels = []; + User user = new User(); + + List get getPrivileges { + return privileges + .where((p) => !p.sysAdminOnly || user.isSysAdmin()) + .toList(); + } + + List cont = []; + + void initUser(user) async { + super.initUser(user); + this.user = user; + if (user.isBuyer()) return; + _loadUsers(user); + _loadPrivileges(); + _loadUserLevels(user); + } + + @override + logout() async { + users = []; + userLevels = []; + } + + List getUserPrivileges() { + List result = new List(); + if (user.privilegeIds.isNotEmpty) { + user.privilegeIds.forEach((pID) { + privileges.forEach((p) { + if (p.id == pID) { + var _priv = Privilege(id: p.id, name: p.name, desc: p.desc); + result.add(_priv); + } + }); + }); + } + + return result; + } + + List getBlockListUsers() { + return users.where((u) => u.isBlock == true).toList(); + } + + List getUserList() { + return users.where((u) => u.docID != this.user.docID).toList(); + } + + Future _loadUsers(User user) async { + try { + String path = "/$biz_collection/${setting.okEnergyId}/$user_collection"; + var snaps = await Firestore.instance + .collection(path) + .where('user_level', isLessThanOrEqualTo: user.userLevel) + .limit(1) + .getDocuments(); + + Stream snapshots = Firestore.instance + .collection(path) + .where('user_level', isLessThanOrEqualTo: user.userLevel) + .snapshots(); + + snapshots.listen((snaps) async { + users = snaps.documents.map((documentSnapshot) { + var data = + User.fromMap(documentSnapshot.data, documentSnapshot.documentID); + if (data.docID == user.docID && data.isBlock) { + this.mainModel.logout(); + notifyListeners(); + } + return data; + }).toList(); + notifyListeners(); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future findUser(String phoneNumber) async { + var _phoneNumber = updatePhoneNumber(phoneNumber); + try { + var data = await request("/user/find/$_phoneNumber", "GET", + token: await getToken()); + return User.fromJson(data); + } catch (e) { + throw Exception(e); + } + } + + Future _loadPrivileges() async { + // if (!user.isOwner() && !user.hasAccount()) { + // return; + // } + + try { + getQuerySnapshot("/$privilege_collection") + .listen((QuerySnapshot snapshot) { + privileges.clear(); + privileges = snapshot.documents.map((documentSnapshot) { + var privilege = Privilege.fromMap( + documentSnapshot.data, documentSnapshot.documentID); + return privilege; + }).toList(); + notifyListeners(); + }).onError((e) { + log.warning("Error! $e"); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future _loadUserLevels(User user) async { + try { + Stream snapshots = Firestore.instance + .collection("/$user_level_collection") + .where('level', isLessThan: user.userLevel) + .snapshots(); + + snapshots.listen((snaps) async { + userLevels = snaps.documents + .map((documentSnapshot) => UserLevel.fromMap( + documentSnapshot.data, documentSnapshot.documentID)) + .toList(); + notifyListeners(); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + Future getUser(String id) async { + String path = "/$biz_collection/${setting.okEnergyId}/$user_collection"; + print("id=> $id"); + var snap = await getDocSnap(path, id); + print("snap=> $snap"); + return User.fromMap(snap.data, snap.documentID); + } + + Future addUserToOk(String userID, List privileges) async { + await addUserToBiz(userID, setting.okEnergyId, privileges); + } + + Future addUserToBiz( + String userID, bizID, List privileges) async { + try { + await request("/user/add-biz", "POST", + payload: { + "user_id": userID, + "biz_id": bizID, + "privileges": privileges + }, + token: await getToken()); + } catch (e) { + throw Exception(e); + } + } + + Future forgetPassword(String id) async { + var _id = updatePhoneNumber(id); + await request("/forget", "POST", payload: {"id": _id}); + } + + Future resetPassword( + String id, String newPassword, String confirmationCode) async { + var _id = updatePhoneNumber(id); + await request( + "/reset", + "POST", + payload: { + "id": _id, + "password": newPassword, + "confirmation_code": confirmationCode + }, + ); + } + + Future changePassword(String id, String newPassword) async { + await request( + "/change/password", + "POST", + payload: { + "id": id, + "password": newPassword, + }, + ); + } + + Future changePhone(String id, String newPhone) async { + var _newPhone = updatePhoneNumber(newPhone); + await request( + "/change/phone", + "POST", + payload: { + "id": id, + "phone_number": _newPhone, + }, + ); + } + + Future addEmail(String id, String email) async { + await request( + "/email", + "PUT", + payload: { + "id": id, + "email": email, + }, + ); + } + + Future confirmEmail( + String id, String email, String phone, String confirmCode) async { + var _id = updatePhoneNumber(id); + var _phone = updatePhoneNumber(phone); + await request( + "/econfirm", + "POST", + payload: { + "id": _id, + "email": email == null ? '' : email, + "phone_number": _phone == null ? '' : _phone, + "confirmation_code": confirmCode + }, + ); + } + + Future deleteStorage(String storageID) async { + await request("/storage/" + storageID, "DELETE", token: await getToken()); + } + + Future blockPhone(String phone) async { + var _phone = updatePhoneNumber(phone); + await request("/blist", "PUT", + payload: {"phone_number": _phone}, token: await getToken()); + } + + Future unblockPhone(String phone) async { + var _phone = updatePhoneNumber(phone); + await request("/wlist", "PUT", + payload: {"phone_number": _phone}, token: await getToken()); + } + + Future addLevel( + String phone, String levelId, List privs) async { + var _phone = updatePhoneNumber(phone); + await request("/lvl", "PUT", + payload: { + "phone_number": _phone, + "user_level_id": levelId, + "privileges": privs + }, + token: await getToken()); + } + + Future updatePin(String pin, String password) async { + await request("/pin", "POST", + payload: { + "id": user.docID, + "pin": pin, + "password": password, + }, + token: await getToken()); + } + + Future clearPin(String password) async { + await request("/pin/clear", "POST", + payload: { + "id": user.docID, + "password": password, + }, + token: await getToken()); + } + + Future> searchUser(String searchUser) async { + if (searchUser == null || searchUser == '') return List(); + + var bytes = utf8.encode(searchUser); + var base64Str = base64.encode(bytes); + HtmlEscape htmlEscape = const HtmlEscape(); + String escapeUser = htmlEscape.convert(base64Str); + + int limit = 20; + List _users = []; + + try { + var data = await request( + "/api/fts/$user_collection/$escapeUser/$limit", "GET", + token: await getToken(), url: Config.instance.reportURL); + if (data == null) return List(); + + data.forEach((user) { + var _user = User.fromUserJson(user); + _users.add(_user); + }); + } catch (e) { + // permission error + log.warning("user error:" + e.toString()); + return null; + } + return _users; + } + + void filterSorting(int _selectedIndex) { + users.clear(); + if (listener != null) { + listener.cancel(); + } + String _fieldName; + bool descending = false; + + if (_selectedIndex == 0) { + _fieldName = 'user_name'; + descending = false; + } + if (_selectedIndex == 1) { + _fieldName = 'user_name'; + descending = true; + } + if (_selectedIndex == 2) { + _fieldName = 'phone_number'; + descending = false; + } + if (_selectedIndex == 3) { + _fieldName = 'phone_number'; + descending = true; + } + + this.popupMenu.index = _selectedIndex; + String path = "/$biz_collection/${setting.okEnergyId}/$user_collection"; + listener = + getFilterSnapshot(path, descending, _fieldName).listen((snaps) async { + users.clear(); + snaps.documents.forEach((d) { + users.add(User.fromMap(d.data, d.documentID)); + notifyListeners(); + }); + users.where((user) => user.userLevel <= this.user.userLevel); + + notifyListeners(); + }); + } +} diff --git a/lib/model_page/user_list_model.dart b/lib/model_page/user_list_model.dart new file mode 100644 index 0000000..1924b9c --- /dev/null +++ b/lib/model_page/user_list_model.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; +import 'package:fcs/model/base_model.dart'; +import 'package:fcs/model/constants.dart'; +import 'package:fcs/model/firebase_helper.dart'; +import 'package:fcs/util.dart'; + +import '../vo/user.dart'; + +class UserModel extends BaseModel { + final log = Logger('UserListModel'); + StreamSubscription listener; + List users = []; + + void initUser(user) async { + super.initUser(user); + _loadUsers(user); + } + + Future _loadUsers(User user) async { + try { + String path = "/$biz_collection/${setting.okEnergyId}/$user_collection"; + var snaps = await Firestore.instance + .collection(path) + .where('user_level', isLessThanOrEqualTo: user.userLevel) + .limit(1) + .getDocuments(); + + Stream snapshots = Firestore.instance + .collection(path) + .where('user_level', isLessThanOrEqualTo: user.userLevel) + .snapshots(); + + snapshots.listen((snaps) async { + users = snaps.documents.map((documentSnapshot) { + var data = + User.fromMap(documentSnapshot.data, documentSnapshot.documentID); + if (data.docID == user.docID && data.isBlock) { + this.mainModel.logout(); + notifyListeners(); + } + return data; + }).toList(); + notifyListeners(); + }); + } catch (e) { + log.warning("Error!! $e"); + } + } + + @override + logout() async { + users = []; + if (listener != null) listener.cancel(); + } + + List getUserList() { + return users.where((u) => u.docID != this.user.docID).toList(); + } + + Future findUser(String phoneNumber) async { + var _phoneNumber = updatePhoneNumber(phoneNumber); + try { + var data = await request("/user/find/$_phoneNumber", "GET", + token: await getToken()); + return User.fromJson(data); + } catch (e) { + throw Exception(e); + } + } + + Future getUser(String id) async { + String path = "/$biz_collection/${setting.okEnergyId}/$user_collection"; + print("id=> $id"); + var snap = await getDocSnap(path, id); + print("snap=> $snap"); + return User.fromMap(snap.data, snap.documentID); + } +} diff --git a/lib/pages/add_pin_editor.dart b/lib/pages/add_pin_editor.dart new file mode 100644 index 0000000..e70aed4 --- /dev/null +++ b/lib/pages/add_pin_editor.dart @@ -0,0 +1,363 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart' as Theme; +import 'util.dart'; + +class AddPINEditor extends StatefulWidget { + final User user; + AddPINEditor( + this.user, { + Key key, + }) : super(key: key); + + @override + _AddPINEditorState createState() => new _AddPINEditorState(); +} + +class _AddPINEditorState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _scaffoldKey = new GlobalKey(); + + final FocusNode myFocusNodePassword = FocusNode(); + final FocusNode myFocusNodeEmail = FocusNode(); + + bool _obscureTextLogin = true; + bool _obscureTextSignup = true; + bool _obscureTextSignupConfirm = true; + + TextEditingController _passwordController = new TextEditingController(); + TextEditingController _pinController = new TextEditingController(); + final formKey = GlobalKey(); + bool _isLoading = false; + bool isSwitched = false; + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + key: _scaffoldKey, + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height >= 775.0 + ? MediaQuery.of(context).size.height + : 580.0, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(top: 35.0, bottom: 10), + child: ListTile( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: LocalText( + context, + 'change.pin.title', + color: Colors.black87, + fontSize: 17, + ), + ), + ), + Expanded( + flex: 2, + child: PageView( + children: [ + new ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildReset(context), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + void dispose() { + myFocusNodePassword.dispose(); + myFocusNodeEmail.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + // SystemChrome.setPreferredOrientations([ + // DeviceOrientation.portraitUp, + // DeviceOrientation.portraitDown, + // ]); + } + + Widget _buildReset(BuildContext context) { + final switchBtnBox = Row( + children: [ + Container( + padding: EdgeInsets.only(left: 20), + child: LocalText( + context, + 'pin.switch', + fontSize: 15, + )), + Switch( + value: isSwitched, + onChanged: (value) { + setState(() { + isSwitched = value; + if (!isSwitched) { + _pinController.clear(); + } + }); + }, + activeColor: Theme.secondaryColor, + ), + ], + ); + + final pinInputBox = Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + controller: _pinController, + keyboardType: TextInputType.number, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Image.asset( + 'assets/pin.png', + width: 30, + height: 30, + ), + labelText: AppTranslations.of(context).text("change.pin"), + labelStyle: Provider.of(context).isEng + ? TextStyle(fontFamily: "WorkSansSemiBold", color: Colors.grey) + : TextStyle(fontFamily: "MyanmarUnicode", color: Colors.grey), + ), + validator: _validatePinCode, + ), + ); + + final passwordInputBox = Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodePassword, + controller: _passwordController, + obscureText: _obscureTextSignup, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + color: Colors.black, + ), + labelText: AppTranslations.of(context).text("login.password"), + labelStyle: Provider.of(context).isEng + ? TextStyle(fontFamily: "WorkSansSemiBold", color: Colors.grey) + : TextStyle(fontFamily: "MyanmarUnicode", color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleSignup, + child: Icon( + _obscureTextSignup + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validatePassword, + ), + ); + + final updatePinBtn = Container( + decoration: new BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.primaryColor, + ), + child: MaterialButton( + highlightColor: Colors.transparent, + splashColor: Theme.LoginColors.loginGradientEnd, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 42.0), + child: LocalText( + context, + 'pin.add_btn', + color: Colors.white, + fontSize: 18.0, + ), + ), + onPressed: () => _change(context)), + ); + + final clearPinBtn = Container( + // margin: EdgeInsets.only(top: 320.0), + decoration: new BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.primaryColor, + ), + child: MaterialButton( + highlightColor: Colors.transparent, + splashColor: Theme.LoginColors.loginGradientEnd, + //shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))), + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 10.0, horizontal: 42.0), + child: LocalText( + context, + 'pin.clear_btn', + color: Colors.white, + fontSize: 18.0, + ), + ), + onPressed: () => _clear(context)), + ); + return Container( + child: ListView( + children: [ + Column( + children: [ + Form( + key: formKey, + child: Card( + elevation: 2.0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Container( + width: 300.0, + child: Column( + children: [ + switchBtnBox, + isSwitched ? Container() : pinInputBox, + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + passwordInputBox + ], + ), + ), + ), + ), + SizedBox( + height: 15, + ), + isSwitched ? Container() : updatePinBtn, + SizedBox( + height: 15, + ), + !isSwitched ? Container() : clearPinBtn + ], + ), + ], + ), + ); + } + + void _toggleSignup() { + setState(() { + _obscureTextSignup = !_obscureTextSignup; + }); + } + + void _change(BuildContext context) async { + if (!formKey.currentState.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + UserModel userModel = Provider.of(context); + try { + await userModel.updatePin(_pinController.text, _passwordController.text); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + } + + void _clear(BuildContext context) async { + if (!formKey.currentState.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + UserModel userModel = Provider.of(context); + try { + await userModel.clearPin(_passwordController.text); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + } + + String _validatePassword(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.password_empty"); + } + if (value.length < 6) { + return AppTranslations.of(context).text("login.password_size"); + } + return null; + } + + String _validatePinCode(value) { + if (!isSwitched) { + if (value.isEmpty) { + return AppTranslations.of(context).text("change.pin_empty"); + } + if (value.length < 6 || value.length > 6) { + return AppTranslations.of(context).text("change.pin_size"); + } + } + + return null; + } +} diff --git a/lib/pages/announcement.dart b/lib/pages/announcement.dart new file mode 100644 index 0000000..8eb7ef4 --- /dev/null +++ b/lib/pages/announcement.dart @@ -0,0 +1,184 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/announcement_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/announcement.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/popupmenu.dart'; +import 'package:fcs/widget/progress.dart'; +import 'package:zefyr/zefyr.dart'; + +import '../theme/theme.dart'; +import 'announcement_editor.dart'; + +class AnnouncementPage extends StatefulWidget { + final Announcement announcement; + + const AnnouncementPage({Key key, this.announcement}) : super(key: key); + @override + _AnnouncementState createState() => _AnnouncementState(); +} + +class _AnnouncementState extends State { + ZefyrController _textController; + + TextEditingController nameController = new TextEditingController(); + FocusNode _focusNode; + NotusDocument document = new NotusDocument(); + bool isLoading = false; + + Announcement _announcement = new Announcement(); + + @override + void initState() { + super.initState(); + if (widget.announcement != null) { + _announcement = widget.announcement; + nameController.text = _announcement.name; + _textController = ZefyrController(_loadDocument(_announcement)); + _focusNode = FocusNode(); + } + } + + NotusDocument _loadDocument(Announcement announcement) { + NotusDocument doc; + try { + doc = NotusDocument.fromJson(jsonDecode(announcement.text)); + } catch (e) {} + if (doc == null) { + doc = NotusDocument(); + } + return doc; + } + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + + bool isOwnerAndAbove = + mainModel.user != null && mainModel.user.isOwnerAndAbove(); + bool hasAdmin = mainModel.user != null && mainModel.user.hasAdmin(); + + final nameBox = Container( + padding: EdgeInsets.only(top: 20, left: 20, right: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocalText(context, 'announcement.name'), + SizedBox( + height: 15, + ), + Text( + nameController.text, + style: textStyle, + ) + ], + ), + ); + + final textBox = Expanded( + child: Container( + padding: EdgeInsets.only(left: 5, right: 20), + child: ZefyrTheme( + data: ZefyrThemeData().copyWith(), + child: ZefyrScaffold( + child: ZefyrEditor( + mode: ZefyrMode.view, + padding: EdgeInsets.all(16), + controller: _textController, + focusNode: _focusNode, + ), + )), + )); + + return LocalProgress( + inAsyncCall: isLoading, + child: Scaffold( + appBar: AppBar( + title: LocalText(context, 'announcement.form.title', + color: Colors.white, fontSize: 20), + backgroundColor: primaryColor, + actions: [ + isOwnerAndAbove || hasAdmin + ? PopupMenuButton( + elevation: 3.2, + tooltip: 'This is tooltip', + onSelected: _select, + itemBuilder: (BuildContext context) { + return announcementMenu.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Text(choice.status), + ); + }).toList(); + }) + : Container() + ], + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + nameBox, + Container( + padding: EdgeInsets.only(left: 10, right: 10), + child: Divider( + color: Colors.grey, + )), + Container( + padding: EdgeInsets.only(left: 20, right: 20), + child: LocalText(context, 'announcement.desc')), + textBox, + SizedBox(height: 10) + ], + ), + ), + ); + } + + void _select(PopupMenu choice) async { + if (choice.index == 1) { + Announcement _anno = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AnnouncementEditor(announcement: _announcement)), + ); + if (_anno == null) return; + + setState(() { + _announcement = _anno; + nameController.text = _announcement.name; + _textController = ZefyrController(_loadDocument(_announcement)); + _focusNode = FocusNode(); + }); + } else if (choice.index == 2) { + showConfirmDialog(context, "announcement.delete_confirm", () { + _delete(context); + }); + } + } + + void _delete(BuildContext context) async { + setState(() { + isLoading = true; + }); + try { + if (widget.announcement != null) { + await Provider.of(context) + .deleteAnnouncement(widget.announcement); + } + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + isLoading = false; + }); + } + } +} diff --git a/lib/pages/announcement_editor.dart b/lib/pages/announcement_editor.dart new file mode 100644 index 0000000..4cdcd04 --- /dev/null +++ b/lib/pages/announcement_editor.dart @@ -0,0 +1,152 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/announcement_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/announcement.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; +import 'package:zefyr/zefyr.dart'; + +class AnnouncementEditor extends StatefulWidget { + final Announcement announcement; + + const AnnouncementEditor({Key key, this.announcement}) : super(key: key); + @override + _AnnouncementEditorState createState() => _AnnouncementEditorState(); +} + +class _AnnouncementEditorState extends State { + final _formKey = GlobalKey(); + static final _scafoldKey = new GlobalKey(); + TextEditingController nameController = new TextEditingController(); + bool _isLoading = false; + FocusNode _focusNode; + ZefyrController _textController; + Announcement _announcement = new Announcement(); + + @override + void initState() { + super.initState(); + if (widget.announcement != null) { + _announcement = widget.announcement; + nameController.text = _announcement.name; + NotusDocument doc = + NotusDocument.fromJson(jsonDecode(widget.announcement.text)); + _textController = ZefyrController(doc); + _focusNode = FocusNode(); + } else { + _textController = ZefyrController(NotusDocument()); + _focusNode = FocusNode(); + } + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + + final nameBox = Container( + padding: EdgeInsets.only(top: 10), + child: TextFormField( + controller: nameController, + autofocus: false, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("announcement.name"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("announcement.name_empty"); + } + return null; + }, + ), + ); + + final textEditor = ZefyrField( + height: 200.0, + decoration: InputDecoration( + labelText: AppTranslations.of(context).text('announcement.desc'), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + ), + controller: _textController, + focusNode: _focusNode, + autofocus: false, + physics: ClampingScrollPhysics(), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + "announcement.form.title", + color: Colors.white, + fontSize: 20, + ), + actions: [ + IconButton( + icon: Icon(Icons.save), + onPressed: () { + _save(context); + }, + ) + ], + ), + body: ZefyrScaffold( + key: _scafoldKey, + child: Padding( + padding: EdgeInsets.only(left: 20.0, right: 20.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + nameBox, + SizedBox(height: 10), + textEditor, + ], + )), + ), + ), + ), + ); + } + + void _save(BuildContext context) { + if (!_formKey.currentState.validate()) return; + + setState(() { + _isLoading = true; + }); + try { + _announcement.name = nameController.text; + final contents = jsonEncode(_textController.document); + _announcement.text = contents; + + if (widget.announcement != null) { + Provider.of(context, listen: false) + .updateAnnouncement(_announcement); + } else { + Provider.of(context, listen: false) + .createAnnouncement(_announcement); + } + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + Future.delayed(const Duration(milliseconds: 3000), () { + _isLoading = false; + Navigator.pop(context, this._announcement); + }); + } + } +} diff --git a/lib/pages/announcement_list.dart b/lib/pages/announcement_list.dart new file mode 100644 index 0000000..1866ac9 --- /dev/null +++ b/lib/pages/announcement_list.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/announcement_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/vo/announcement.dart' as Announce; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'announcement.dart'; +import 'announcement_editor.dart'; + +class AnnouncementList extends StatefulWidget { + @override + _AnnouncementListState createState() => _AnnouncementListState(); +} + +class _AnnouncementListState extends State { + var timeFormatter = new DateFormat('KK:mm a'); + var dateFormatter = new DateFormat('dd MMM'); + + final double dotSize = 15.0; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + AnnouncementModel announcementModel = + Provider.of(context); + + MainModel mainModel = Provider.of(context); + bool isOwnerAndAbove = + mainModel.user != null && mainModel.user.isOwnerAndAbove(); + bool hasAdmin = mainModel.user != null && mainModel.user.hasAdmin(); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: + LocalText( + context, + "announcement.title", + color: Colors.white, + fontSize: 20, + ) + ), + floatingActionButton: isOwnerAndAbove || hasAdmin + ? FloatingActionButton( + backgroundColor: primaryColor, + child: Icon(Icons.add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AnnouncementEditor()), + ); + }, + ) + : Container(), + body: new ListView.separated( + separatorBuilder: (context, index) => Divider( + color: Colors.black, + ), + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: announcementModel.announcements.length, + itemBuilder: (BuildContext context, int index) { + Announce.Announcement announce = + announcementModel.announcements[index]; + return InkWell( + onTap: () { + setState(() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AnnouncementPage(announcement: announce)), + ); + }); + }, + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Icon( + Icons.announcement, + color: primaryColor, + size: 30, + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + announce.name == null + ? Container() + : new Text( + announce.name, + style: new TextStyle( + fontSize: 15.0, + color: Colors.black), + ), + ], + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(announce.time==null?"":timeFormatter.format(announce.time)), + ), + announce.fromToday() + ? Container() + : Text(announce.time==null?"":dateFormatter.format(announce.time)), + ], + ) + + ], + ), + ), + ), + ], + ), + ); + }), + ), + ); + } +} diff --git a/lib/pages/banks/bank_edit.dart b/lib/pages/banks/bank_edit.dart new file mode 100644 index 0000000..f5e4b1f --- /dev/null +++ b/lib/pages/banks/bank_edit.dart @@ -0,0 +1,195 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/bank_account.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/local_text_field.dart'; +import 'package:fcs/widget/progress.dart'; + +class BankEdit extends StatefulWidget { + final BankAccount bankAccount; + + const BankEdit({Key key, this.bankAccount}) : super(key: key); + + @override + _BankEditState createState() => _BankEditState(); +} + +class _BankEditState extends State { + TextEditingController bankNameController = new TextEditingController(); + TextEditingController accountNameController = new TextEditingController(); + TextEditingController accountNumberController = new TextEditingController(); + + bool _isLoading; + bool _isEdit; + File image; + BankAccount bankAccount; + + @override + void initState() { + super.initState(); + _isLoading = false; + _isEdit = widget.bankAccount != null; + bankAccount = BankAccount(); + if (_isEdit) { + bankAccount = widget.bankAccount; + bankNameController.text = bankAccount.bankName; + accountNameController.text = bankAccount.accountName; + accountNumberController.text = bankAccount.accountNumber; + } + } + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: LocalText(context, 'banks.edit.title', + color: Colors.white, fontSize: 20), + backgroundColor: primaryColor, + actions: [ + IconButton( + icon: Icon(Icons.save), + onPressed: () { + _save(); + }, + ), + IconButton( + icon: Icon(Icons.delete), + onPressed: () { + _delete(); + }, + ) + ], + ), + body: Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + LocalTextField( + labelKey: "banks.name", + textEditingController: bankNameController), + LocalTextField( + labelKey: "banks.account.name", + textEditingController: accountNameController), + LocalTextField( + labelKey: "banks.account.number", + textEditingController: accountNumberController, + textInputType: TextInputType.number, + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: Center( + child: Stack( + children: [ + Container( + width: 80, + height: 80, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + border: Border.all( + color: primaryColor, + width: 1.0, + ), + ), + child: image == null + ? Image.network( + _isEdit ? widget.bankAccount.bankLogo : "", + height: 80, + width: 80, + ) + : Image.file(image), + ), + Positioned( + bottom: -10, + right: -10, + child: IconButton( + color: primaryColor, + icon: const Icon( + Icons.edit, + color: Colors.grey, + ), + onPressed: () async { + File _image = await ImagePicker.pickImage( + source: ImageSource.gallery, + maxWidth: 300, + maxHeight: 300, + imageQuality: 80); + if (_image != null) { + setState(() { + this.image = _image; + }); + } + })) + ], + ), + ), + ) + ], + ), + ), + ], + ), + ), + ); + } + + _save() async { + if (!_isEdit && image == null) { + showMsgDialog(context, "Error", "Need bank logo!"); + return; + } + setState(() { + _isLoading = true; + }); + try { + bankAccount.bankName = bankNameController.text; + bankAccount.accountName = accountNameController.text; + bankAccount.accountNumber = accountNumberController.text; + MainModel mainModel = Provider.of(context, listen: false); + if (_isEdit) { + await mainModel.updateBankAccount(bankAccount, image); + } else { + await mainModel.addBankAccount(bankAccount, image); + } + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + return; + } finally { + setState(() { + _isLoading = false; + }); + } + Navigator.pop(context); + } + + _delete() async { + showConfirmDialog(context, "banks.account.delete.confirmation", () async { + setState(() { + _isLoading = true; + }); + try { + MainModel mainModel = Provider.of(context, listen: false); + if (_isEdit) { + await mainModel.deleteBankAccount(bankAccount); + } + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + Navigator.pop(context); + } + }); + } +} diff --git a/lib/pages/banks/banks.dart b/lib/pages/banks/banks.dart new file mode 100644 index 0000000..c2471a5 --- /dev/null +++ b/lib/pages/banks/banks.dart @@ -0,0 +1,259 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/banks/bank_edit.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/bank_account.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +class BankAccounts extends StatefulWidget { + const BankAccounts({Key key}) : super(key: key); + @override + _BankAccountsState createState() => _BankAccountsState(); +} + +class _BankAccountsState extends State { + bool isLoading = false; + bool isEdit = false; + + final TextEditingController bankNameCtl = TextEditingController(); + final TextEditingController accountNameCtl = TextEditingController(); + final TextEditingController accountNumberCtl = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + List bankAccounts = mainModel.setting.bankAccounts; + + bool isOwnerAndAbove = + mainModel.user != null && mainModel.user.isOwnerAndAbove(); + bool hasAdmin = mainModel.user != null && mainModel.user.hasAdmin(); + + return WillPopScope( + onWillPop: () { + if (isEdit) { + setState(() { + isEdit = false; + }); + return Future.value(false); + } + return Future.value(true); + }, + child: LocalProgress( + inAsyncCall: isLoading, + child: Scaffold( + appBar: AppBar( + centerTitle: true, + automaticallyImplyLeading: !isEdit, + title: LocalText(context, 'banks.title', + color: Colors.white, fontSize: 20), + backgroundColor: primaryColor, + actions: [ + (isOwnerAndAbove || hasAdmin) + ? isEdit + ? IconButton( + icon: Icon(Icons.done), + onPressed: () { + setState(() { + isEdit = false; + }); + }, + ) + : IconButton( + icon: Icon(Icons.edit), + onPressed: () { + _edit(); + }, + ) + : Container() + ], + ), + floatingActionButton: isEdit + ? FloatingActionButton( + backgroundColor: primaryColor, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => BankEdit()), + ); + }, + child: const Icon( + Icons.add, + color: Colors.white, + ), + ) + : null, + body: new ListView.builder( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(top: 15), + shrinkWrap: true, + itemCount: bankAccounts.length, + itemBuilder: (BuildContext context, int index) { + return _item(context, bankAccounts[index]); + }), + ), + ), + ); + } + + _item(BuildContext context, BankAccount bankAccount) { + return InkWell( + onTap: isEdit + ? () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BankEdit( + bankAccount: bankAccount, + )), + ) + : null, + child: Row( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + border: Border.all( + color: primaryColor, + width: 1.0, + ), + ), + child: Image.network( + bankAccount.bankLogo, + height: 80, + width: 80, + ), + ), + ), + ], + ), + Expanded( + child: new Row( + children: [ + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(3.0), + child: new Text( + bankAccount.bankName, + style: new TextStyle( + fontSize: 20.0, color: Colors.black), + ), + ), + Padding( + padding: const EdgeInsets.all(3.0), + child: new Text( + bankAccount.accountName, + style: + new TextStyle(fontSize: 16.0, color: Colors.grey), + ), + ), + Padding( + padding: const EdgeInsets.all(3.0), + child: Row( + children: [ + Text( + bankAccount.accountNumber, + style: new TextStyle( + fontSize: 16.0, color: Colors.grey), + ), + InkWell( + onTap: () { + Clipboard.setData(ClipboardData( + text: bankAccount.accountNumber)); + _showToast(context, bankAccount.bankName); + }, + child: Padding( + padding: const EdgeInsets.only(left: 7.0), + child: Icon( + Icons.content_copy, + color: Colors.grey, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Future _displayEditDialog(BuildContext context) async { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Edit'), + content: Column( + children: [ + TextField( + controller: bankNameCtl, + keyboardType: TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration(hintText: "Enter Bank Name"), + ), + TextField( + controller: accountNameCtl, + keyboardType: TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration(hintText: "Enter Account Name"), + ), + TextField( + controller: accountNumberCtl, + keyboardType: TextInputType.number, + decoration: InputDecoration(hintText: "Enter Account Number"), + ), + IconButton(icon: Icon(Icons.photo_library), onPressed: null) + ], + ), + actions: [ + FlatButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: const Text('Ok'), + onPressed: () { + Navigator.of(context).pop(); + }), + ], + ); + }); + } + + void _edit() { + setState(() { + isEdit = true; + }); + } + + void _showToast(BuildContext context, String bankName) { + final scaffold = Scaffold.of(context); + scaffold.showSnackBar( + SnackBar( + content: Text('copied "$bankName" account number to clipboard'), + backgroundColor: primaryColor, + duration: Duration(seconds: 1), + ), + ); + } +} diff --git a/lib/pages/block_list.dart b/lib/pages/block_list.dart new file mode 100644 index 0000000..01a5989 --- /dev/null +++ b/lib/pages/block_list.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/popupmenu.dart'; +import 'package:fcs/widget/progress.dart'; + +class BlockList extends StatefulWidget { + @override + _BlockListState createState() => _BlockListState(); +} + +class _BlockListState extends State { + final double dotSize = 15.0; + bool _isLoading = false; + PopupMenu selectedChoices = blocklistpopup[0]; + User user = new User(); + + @override + Widget build(BuildContext context) { + var userModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText(context, 'user.block_list', + color: Colors.white, fontSize: 20), + ), + body: new ListView.builder( + padding: EdgeInsets.only(left: 15, right: 15, top: 15, bottom: 10), + shrinkWrap: true, + itemCount: userModel.getBlockListUsers().length, + itemBuilder: (BuildContext context, int index) { + return Card( + elevation: 10, + color: Colors.white, + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () {}, + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 7.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Image.asset( + 'assets/block.png', + width: 50, + height: 50, + color: primaryColor, + )), + new Text( + userModel.getBlockListUsers()[index].name == + null + ? "" + : userModel.getBlockListUsers()[index].name, + style: new TextStyle( + fontSize: 17.0, color: Colors.black), + ), + ], + ), + ), + ), + ), + PopupMenuButton( + elevation: 3.2, + tooltip: 'This is tooltip', + onSelected: _select, + itemBuilder: (BuildContext context) { + this.user = userModel.getBlockListUsers()[index]; + return blocklistpopup.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Text(choice.status), + ); + }).toList(); + }) + ], + ), + ); + }), + ), + ); + } + + void _select(PopupMenu choice) async { + selectedChoices = choice; + if (choice.index == 1) { + showConfirmDialog(context, "user.unblock.confirm", () { + _unblock(); + }); + } + } + + _unblock() async { + setState(() { + _isLoading = true; + }); + + try { + var userModel = Provider.of(context); + await userModel.unblockPhone(this.user.phoneNumber); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/buyer_info.dart b/lib/pages/buyer_info.dart new file mode 100644 index 0000000..f39e419 --- /dev/null +++ b/lib/pages/buyer_info.dart @@ -0,0 +1,281 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/buyer_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/quota_page.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/util.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/widget/label_widgets.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'document_log_page.dart'; + +class BuyerInfo extends StatefulWidget { + final Buyer buyer; + const BuyerInfo({this.buyer}); + @override + _BuyerInfoState createState() => _BuyerInfoState(); +} + +class _BuyerInfoState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy - hh:mm a'); + TextEditingController _companyName = new TextEditingController(); + TextEditingController _comAddress = new TextEditingController(); + TextEditingController _numOfShops = new TextEditingController(); + TextEditingController _bizType = new TextEditingController(); + TextEditingController _accountName = new TextEditingController(); + TextEditingController _accountNumber = new TextEditingController(); + + bool _isLoading = false; + Buyer buyer; + + @override + void initState() { + super.initState(); + if (widget.buyer != null) { + buyer = widget.buyer; + Provider.of(context, listen: false) + .loadBuyerProducts(buyer) + .then((b) { + if (mounted) { + setState(() { + buyer = b; + }); + } + }); + } + } + + @override + Widget build(BuildContext context) { + var mainModel = Provider.of(context); + + _companyName.text = buyer.bizName; + _comAddress.text = buyer.bizAddress; + _numOfShops.text = buyer.numOfShops.toString(); + _bizType.text = buyer.bizType; + _accountName.text = buyer.userName; + _accountNumber.text = buyer.userID; + + final dateBox = + labeledText(context, dateFormatter.format(buyer.regDate), "reg.date"); + final accountBox = + labeledText(context, buyer.userName, "buyer.account_name"); + final phoneBox = labeledText(context, buyer.phone, "buyer.phone_number"); + final statusBox = labeledText(context, buyer.status, "reg.status"); + final bizNameBox = labeledText(context, _companyName.text, "reg.biz_name"); + final bizAddressBox = + labeledText(context, _comAddress.text, "reg.biz_address"); + final shopNumberBox = + labeledText(context, _numOfShops.text, "reg.biz_shops"); + final typeBox = labeledText(context, _bizType.text, "buyer.type_biz"); + final dailyQuotaBox = labeledText( + context, formatNumber(buyer.dailyQuota), "reg.quota", + number: true); + final dailyQuotaUsedBox = labeledText( + context, formatNumber(buyer.dailyQuotaUsed), "reg.quota.used", + number: true); + final maxQuotaBox = labeledText( + context, formatNumber(buyer.maxQuota), "reg.max_quota", + number: true); + final maxQuotaUsedBox = labeledText( + context, formatNumber(buyer.maxQuotaUsed), "reg.max_quota.used", + number: true); + final nricFrontBox = + labeledImg(context, buyer.nricFrontUrl, "reg_info.nric_front"); + final nricBackBox = + labeledImg(context, buyer.nricBackUrl, "reg_info.nric_back"); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("buyer.title")), + actions: [ + mainModel.showHistoryBtn() + ? IconButton( + icon: Icon(Icons.history), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DocumentLogPage(docID: buyer.id)), + ); + }, + ) + : Container(), + PopupMenuButton( + onSelected: (s) { + if (s == 1) { + showConfirmDialog(context, "buyer.delete.confirm", () { + _delete(); + }); + } else if (s == 2) { + showConfirmDialog(context, "buyer.approve.confirm", () { + _approve(); + }); + } else if (s == 3) { + showCommentDialog(context, (comment) { + _reject(comment); + }); + } else if (s == 4) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => QuotaPage( + buyer: this.buyer, + isApproved: true, + )), + ); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 1, + child: Text("Delete"), + ), + PopupMenuItem( + enabled: buyer.isPending(), + value: 2, + child: Text("Approve"), + ), + PopupMenuItem( + enabled: buyer.isPending(), + value: 3, + child: Text("Reject"), + ), + PopupMenuItem( + enabled: buyer.isApproved(), + value: 4, + child: Text("Allocate Quota"), + ), + ], + ), + ], + ), + body: Container( + padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + child: ListView( + children: [ + dateBox, + Divider(), + accountBox, + Divider(), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: phoneBox, + ), + InkWell( + onTap: () => call(context, buyer.phone), + child: Icon( + Icons.open_in_new, + color: Colors.grey, + size: 15, + ), + ), + ], + ), + Divider(), + statusBox, + Divider(), + bizNameBox, + Divider(), + bizAddressBox, + Divider(), + typeBox, + Divider(), + dailyQuotaBox, + Divider(), + dailyQuotaUsedBox, + Divider(), + maxQuotaBox, + Divider(), + maxQuotaUsedBox, + Divider(), + nricFrontBox, + Divider(), + nricBackBox + ], + ), + ), + ), + ); + } + + _delete() async { + setState(() { + _isLoading = true; + }); + + try { + await Provider.of(context).delete(buyer); + Navigator.pop(context, true); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _approve() async { + var _buyer = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => QuotaPage( + buyer: this.buyer, + isApproved: false, + )), + ); + if (_buyer == null) return; + + setState(() { + _isLoading = true; + }); + + try { + this.buyer.dailyQuota = _buyer.dailyQuota; + this.buyer.maxQuota = _buyer.maxQuota; + await Provider.of(context).approve(this.buyer); + Navigator.pop(context, true); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _reject(comment) async { + if (comment == null || comment == "") { + showMsgDialog(context, "Error", "Please enter comment!"); + return; + } + buyer.comment = comment; + setState(() { + _isLoading = true; + }); + + try { + await Provider.of(context).reject(buyer); + Navigator.pop(context, true); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/buyer_list.dart b/lib/pages/buyer_list.dart new file mode 100644 index 0000000..9d3501a --- /dev/null +++ b/lib/pages/buyer_list.dart @@ -0,0 +1,208 @@ +import 'package:provider/provider.dart'; +import 'package:fcs/model/buyer_model.dart'; +import 'package:fcs/pages/search_page.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/popupmenu.dart'; + +import 'package:flutter/material.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'buyer_list_row.dart'; + +class BuyerList extends StatefulWidget { + @override + _BuyerListState createState() => _BuyerListState(); +} + +class _BuyerListState extends State { + Buyer buyer; + int _selectedIndex = 0; + bool _isLoading = false; + int _selectedSortIndex; + + @override + void initState() { + super.initState(); + var buyerModel = Provider.of(context, listen: false); + var index = buyerModel.popupMenu.index; + _selectedIndex = index; + var sortIndexndex = buyerModel.sortMenu.index; + _selectedSortIndex = sortIndexndex; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + BuyerModel buyerModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("buyer.title")), + actions: [ + IconButton( + icon: Icon( + Icons.search, + color: Colors.white, + ), + iconSize: 30, + onPressed: () => showPlacesSearch(context), + ), + PopupMenuButton( + elevation: 3.2, + onSelected: (selected) { + setState(() { + this._selectedSortIndex = selected.index; + this._selectedIndex = 0; + buyerModel.filterSorting( + _selectedSortIndex, this._selectedIndex); + }); + }, + icon: Container( + width: 30, + height: 30, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Icon( + Icons.sort, + color: primaryColor, + ), + _selectedSortIndex != null + ? Positioned( + bottom: 0, + right: 0, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + : Container() + ], + )), + itemBuilder: (BuildContext context) { + return userMenu.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Container( + padding: EdgeInsets.only(left: 8), + child: Row( + children: [ + Text(choice.status), + SizedBox( + width: 10, + ), + _selectedSortIndex != null && + _selectedSortIndex == choice.index + ? Icon( + Icons.check, + color: Colors.grey, + ) + : Container(), + ], + ), + ), + ); + }).toList(); + }), + PopupMenuButton( + elevation: 3.2, + onSelected: (selected) { + String status; + setState(() { + this._selectedIndex = selected.index; + this._selectedSortIndex = null; + if (selected.status == 'All') { + status = null; + } else { + status = selected.status; + } + buyerModel.filterStatus( + status, _selectedIndex, this._selectedSortIndex); + }); + }, + icon: Container( + width: 30, + height: 30, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Icon( + Icons.filter_list, + color: primaryColor, + ), + _selectedIndex != 0 + ? Positioned( + bottom: 0, + right: 0, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + : Container() + ], + )), + itemBuilder: (BuildContext context) { + return buyerStatusMenu.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Row( + children: [ + Text(choice.status), + SizedBox( + width: 10, + ), + _selectedIndex != null && + _selectedIndex == choice.index + ? Icon( + Icons.check, + color: Colors.grey, + ) + : Container(), + ], + ), + ); + }).toList(); + }), + ], + ), + body: new ListView.builder( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(top: 15), + shrinkWrap: true, + itemCount: buyerModel.buyers.length, + itemBuilder: (BuildContext context, int index) { + return BuyerListRow( + buyer: buyerModel.buyers[index], + ); + }), + ), + ); + } +} diff --git a/lib/pages/buyer_list_row.dart b/lib/pages/buyer_list_row.dart new file mode 100644 index 0000000..e0d4fa1 --- /dev/null +++ b/lib/pages/buyer_list_row.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/buyer_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/buyer.dart'; + +import 'buyer_info.dart'; + +class BuyerListRow extends StatefulWidget { + final Buyer buyer; + const BuyerListRow({this.buyer}); + + @override + _BuyerListRowState createState() => _BuyerListRowState(); +} + +class _BuyerListRowState extends State { + final double dotSize = 15.0; + Buyer _buyer = new Buyer(); + + @override + void initState() { + super.initState(); + BuyerModel buyerModel = Provider.of(context, listen: false); + if (widget.buyer != null) { + buyerModel.buyers.forEach((b) { + if (widget.buyer.id == b.id) { + _buyer = b; + } + }); + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: 15, right: 15), + child: Card( + elevation: 10, + color: Colors.white, + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => BuyerInfo(buyer: _buyer)), + ); + }, + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Image.asset( + "assets/buyer.png", + width: 40, + height: 40, + color: primaryColor, + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Text( + _buyer.userName == null ? '' : _buyer.userName, + style: new TextStyle( + fontSize: 15.0, color: Colors.black), + ), + new Text( + _buyer.bizName == null ? "" : _buyer.bizName, + style: new TextStyle( + fontSize: 13.0, color: Colors.grey), + ), + new Text( + _buyer.phone == null ? "" : _buyer.phone, + style: new TextStyle( + fontSize: 13.0, color: Colors.grey), + ), + ], + ), + ), + ], + ), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: getStatus(_buyer.status), + ), + ], + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/chage_phone_number.dart b/lib/pages/chage_phone_number.dart new file mode 100644 index 0000000..c65004a --- /dev/null +++ b/lib/pages/chage_phone_number.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart' as Theme; +import 'confirm_email.dart'; +import 'util.dart'; + +class ChangePhoneNumber extends StatefulWidget { + final User user; + ChangePhoneNumber( + this.user, { + Key key, + }) : super(key: key); + + @override + _ChangePhoneNumberState createState() => new _ChangePhoneNumberState(); +} + +class _ChangePhoneNumberState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _scaffoldKey = new GlobalKey(); + + final FocusNode myFocusNodePhone = FocusNode(); + final FocusNode myFocusNodenewPhone = FocusNode(); + + TextEditingController _phoneController = new TextEditingController(); + TextEditingController _newPhoneController = new TextEditingController(); + final formKey = GlobalKey(); + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + key: _scaffoldKey, + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height >= 775.0 + ? MediaQuery.of(context).size.height + : 580.0, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(top: 35.0, bottom: 10), + child: ListTile( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: LocalText( + context, + 'change.phone', + color: Colors.black87, + fontSize: 17, + ), + ), + ), + Expanded( + flex: 2, + child: PageView( + children: [ + new ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildReset(context), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + void dispose() { + myFocusNodenewPhone.dispose(); + myFocusNodePhone.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _phoneController.text = widget.user.phone; + _newPhoneController.text = "09"; + + // SystemChrome.setPreferredOrientations([ + // DeviceOrientation.portraitUp, + // DeviceOrientation.portraitDown, + // ]); + } + + Widget _buildReset(BuildContext context) { + return Container( + child: ListView( + children: [ + Column( + children: [ + Form( + key: formKey, + child: Card( + elevation: 2.0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Container( + width: 300.0, + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodePhone, + controller: _phoneController, + readOnly: true, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.phone, + color: Colors.black, + size: 22.0, + ), + labelText: AppTranslations.of(context) + .text("login.phone"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + ), + ), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodenewPhone, + controller: _newPhoneController, + keyboardType: TextInputType.phone, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.phone, + color: Colors.black, + size: 22.0, + ), + labelText: AppTranslations.of(context) + .text("change.new.phone"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + ), + validator: _validatePhone, + ), + ), + ], + ), + ), + ), + ), + SizedBox( + height: 15, + ), + Container( + // margin: EdgeInsets.only(top: 320.0), + decoration: new BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.primaryColor, + ), + child: MaterialButton( + highlightColor: Colors.transparent, + splashColor: Theme.LoginColors.loginGradientEnd, + //shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 42.0), + child: LocalText( + context, + 'change', + color: Colors.white, + fontSize: 18.0, + ), + ), + onPressed: () => _change(context)), + ), + ], + ), + ], + ), + ); + } + + void _change(BuildContext context) async { + if (!formKey.currentState.validate()) { + return; + } + var _phone = _newPhoneController.text; + + setState(() { + _isLoading = true; + }); + + UserModel userModel = Provider.of(context); + try { + await userModel.changePhone(widget.user.phoneNumber, _phone); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ConfirmEmail( + id: widget.user.phoneNumber, + phoneNumber: _phone, + ))); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + } + + String _validatePhone(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("change.phone_empty"); + } + if (!value.startsWith("09")) { + return 'Only "09".'; + } + return null; + } +} diff --git a/lib/pages/change_password.dart b/lib/pages/change_password.dart new file mode 100644 index 0000000..e64e363 --- /dev/null +++ b/lib/pages/change_password.dart @@ -0,0 +1,320 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart' as Theme; +import 'util.dart'; + +class ChangePassword extends StatefulWidget { + final User user; + ChangePassword( + this.user, { + Key key, + }) : super(key: key); + + @override + _ChangePasswordState createState() => new _ChangePasswordState(); +} + +class _ChangePasswordState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _scaffoldKey = new GlobalKey(); + + final FocusNode myFocusNodePassword = FocusNode(); + final FocusNode myFocusNodeEmail = FocusNode(); + + bool _obscureTextLogin = true; + bool _obscureTextSignup = true; + bool _obscureTextSignupConfirm = true; + + TextEditingController _smsController = new TextEditingController(); + TextEditingController _passwordController = new TextEditingController(); + TextEditingController _confirmPasswordController = + new TextEditingController(); + final formKey = GlobalKey(); + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + key: _scaffoldKey, + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height >= 775.0 + ? MediaQuery.of(context).size.height + : 580.0, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(top: 35.0, bottom: 10), + child: ListTile( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: LocalText( + context, + 'change.password.title', + color: Colors.black87, + fontSize: 17, + ), + ), + ), + Expanded( + flex: 2, + child: PageView( + children: [ + new ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildReset(context), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + void dispose() { + myFocusNodePassword.dispose(); + myFocusNodeEmail.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + // SystemChrome.setPreferredOrientations([ + // DeviceOrientation.portraitUp, + // DeviceOrientation.portraitDown, + // ]); + + _smsController.text = ""; + } + + Widget _buildReset(BuildContext context) { + return Container( + child: ListView( + children: [ + Column( + children: [ + Form( + key: formKey, + child: Card( + elevation: 2.0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Container( + width: 300.0, + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodePassword, + controller: _passwordController, + obscureText: _obscureTextSignup, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.password"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleSignup, + child: Icon( + _obscureTextSignup + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validatePassword, + ), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + controller: _confirmPasswordController, + obscureText: _obscureTextSignupConfirm, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.confirm_password"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleSignupConfirm, + child: Icon( + _obscureTextSignupConfirm + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validateConfirmPassword, + ), + ), + ], + ), + ), + ), + ), + SizedBox( + height: 15, + ), + Container( + // margin: EdgeInsets.only(top: 320.0), + decoration: new BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.primaryColor, + ), + child: MaterialButton( + highlightColor: Colors.transparent, + splashColor: Theme.LoginColors.loginGradientEnd, + //shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 42.0), + child: LocalText( + context, + 'change', + color: Colors.white, + fontSize: 18.0, + ), + ), + onPressed: () => _change(context)), + ), + ], + ), + ], + ), + ); + } + + void _toggleLogin() { + setState(() { + _obscureTextLogin = !_obscureTextLogin; + }); + } + + void _toggleSignup() { + setState(() { + _obscureTextSignup = !_obscureTextSignup; + }); + } + + void _toggleSignupConfirm() { + setState(() { + _obscureTextSignupConfirm = !_obscureTextSignupConfirm; + }); + } + + void _change(BuildContext context) async { + if (!formKey.currentState.validate()) { + return; + } + var password = _passwordController.text; + + setState(() { + _isLoading = true; + }); + + UserModel userModel = Provider.of(context); + try { + await userModel.changePassword(widget.user.phoneNumber, password); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + } + + String _validatePassword(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.password_empty"); + } + if (value.length < 6) { + return AppTranslations.of(context).text("login.password_size"); + } + return null; + } + + String _validateConfirmPassword(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.password_empty"); + } + if (value.length < 6) { + return AppTranslations.of(context).text("login.password_size"); + } + if (value != _passwordController.text) { + return AppTranslations.of(context).text("login.password_mismatch"); + } + return null; + } +} diff --git a/lib/pages/confirm_email.dart b/lib/pages/confirm_email.dart new file mode 100644 index 0000000..efaa60f --- /dev/null +++ b/lib/pages/confirm_email.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'util.dart'; + +class ConfirmEmail extends StatefulWidget { + final String id, email, phoneNumber; + + const ConfirmEmail({Key key, this.id, this.email, this.phoneNumber}) + : super(key: key); + @override + _ConfirmEmailState createState() => _ConfirmEmailState(); +} + +class _ConfirmEmailState extends State { + final TextEditingController _sms = new TextEditingController(); + bool _isLoading = false; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + _confimEmail() async { + UserModel userModel = Provider.of(context); + if (!_formKey.currentState.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + try { + await userModel.confirmEmail( + widget.id, widget.email, widget.phoneNumber, _sms.text); + Navigator.pushNamedAndRemoveUntil(context, "/home", (r) => false); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } + + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + final smsInput = TextFormField( + controller: _sms, + keyboardType: TextInputType.number, + autofocus: false, + decoration: new InputDecoration( + labelText: widget.email == null + ? AppTranslations.of(context).text("sms.sms") + : AppTranslations.of(context).text("email.code"), + labelStyle: labelStyle, + hintText: 'eg. 123456', + icon: Icon( + Icons.lock, + color: primaryColor, + )), + validator: (value) { + if (value.isEmpty) { + return widget.email == null + ? AppTranslations.of(context).text("sms.empty") + : AppTranslations.of(context).text("email.code_empty"); + } + return null; + }, + ); + + final enterButton = Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: RaisedButton( + onPressed: () => _confimEmail(), + padding: EdgeInsets.all(12), + color: primaryColor, + child: Text(AppTranslations.of(context).text("sms.enter"), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: Text(widget.email == null + ? AppTranslations.of(context).text("input_sms") + : AppTranslations.of(context).text("email.input")), + backgroundColor: primaryColor, + ), + body: Center( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + Form(key: _formKey, child: smsInput), + SizedBox(height: 8.0), + enterButton, + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/contact.dart b/lib/pages/contact.dart new file mode 100644 index 0000000..8726d5c --- /dev/null +++ b/lib/pages/contact.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:package_info/package_info.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'contact_editor.dart'; + +class Contact extends StatefulWidget { + @override + _ContactState createState() => _ContactState(); +} + +class _ContactState extends State { + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + bool isOwner = mainModel.user != null && mainModel.user.isOwner(); + bool hasAdmin = mainModel.user != null && mainModel.user.hasAdmin(); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + iconTheme: IconThemeData( + color: Colors.grey, + ), + elevation: 0, + centerTitle: true, + title: Image( + height: 30, + fit: BoxFit.scaleDown, + image: new AssetImage('assets/img/logo.png')), + actions: [ + isOwner || hasAdmin + ? IconButton( + icon: Icon(Icons.edit), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ContactEditor(setting: mainModel.setting)), + ); + }, + ) + : Container() + ], + ), + body: ListView( + children: [ + Center( + child: Padding( + padding: EdgeInsets.only(top: 5.0, bottom: 5), + child: LocalText( + context, + "contact.title", + fontSize: 25, + )), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + List.generate(mainModel.setting.phones.length, (index) { + return link(mainModel.setting.phones[index], Icons.phone, + onTap: () => _call(mainModel.setting.phones[index])); + }), + ), + ), + link(mainModel.setting.deliveryPhone, Icons.phone_forwarded, + onTap: () => _call(mainModel.setting.deliveryPhone), + label: LocalText(context, "contact.delivery.phone")), + link(mainModel.setting.email, Icons.email, + onTap: () => _email(mainModel.setting.email)), + link(mainModel.setting.facebook, FontAwesomeIcons.facebook, + onTap: () => _openLink(mainModel.setting.facebook)), + link(mainModel.setting.website, FontAwesomeIcons.chrome, + onTap: () => _openLink(mainModel.setting.website)), + link(mainModel.setting.address, Icons.location_on), + ], + ), + ), + ); + } + + Widget link(String text, IconData iconData, + {Function() onTap, Widget label}) { + return Padding( + padding: const EdgeInsets.only(left: 18.0, bottom: 5), + child: InkWell( + onTap: () => onTap != null ? onTap() : null, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + iconData, + color: primaryColor, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label == null ? Container() : label, + Text( + text == null ? "" : text, + overflow: TextOverflow.ellipsis, + maxLines: 5, + ), + ], + ), + SizedBox( + width: 5, + ), + onTap == null + ? Container() + : Icon( + Icons.open_in_new, + color: Colors.grey, + size: 15, + ) + ], + ), + )), + ); + } + + Future getVersionNumber() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String version = packageInfo.version + "+" + packageInfo.buildNumber; + return version; + } + + _call(String phone) { + showConfirmDialog( + context, "contact.phone.confim", () => launch("tel:$phone"), + translationVariables: ["$phone"]); + } + + _email(String email) { + showConfirmDialog( + context, "contact.email.configm", () => launch("mailto:$email"), + translationVariables: ["$email"]); + } + + _openLink(String link) { + showConfirmDialog(context, "contact.open.confrim", () => launch("$link"), + translationVariables: ["$link"]); + } +} diff --git a/lib/pages/contact_editor.dart b/lib/pages/contact_editor.dart new file mode 100644 index 0000000..16f1c7f --- /dev/null +++ b/lib/pages/contact_editor.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/phone_input.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/setting.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/local_text_field.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +class ContactEditor extends StatefulWidget { + final Setting setting; + const ContactEditor({this.setting}); + @override + _ContactEditorState createState() => _ContactEditorState(); +} + +class _ContactEditorState extends State { + TextEditingController _email = new TextEditingController(); + TextEditingController _facebook = new TextEditingController(); + TextEditingController _website = new TextEditingController(); + TextEditingController _address = new TextEditingController(); + TextEditingController _deliveryPhone = new TextEditingController(); + + final _formKey = GlobalKey(); + bool _isLoading = false; + + List phones = new List(); + List _initPhones = new List(); + + @override + void initState() { + super.initState(); + if (widget.setting != null) { + this._initPhones = widget.setting.phones; + _email.text = widget.setting.email; + _facebook.text = widget.setting.facebook; + _website.text = widget.setting.website; + _deliveryPhone.text = widget.setting.deliveryPhone; + _address.text = widget.setting.address; + + phones.clear(); + _initPhones.forEach((p) { + phones.add(p); + }); + } + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + + final emailBox = TextFormField( + controller: _email, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text('contact.email'), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + Icons.email, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text('contact.email.empty'); + } + return null; + }, + ); + + final faceBookBox = TextFormField( + controller: _facebook, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text('contact.facebook'), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.facebook, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text('contact.facebook.empty'); + } + return null; + }, + ); + + final googleBox = TextFormField( + controller: _website, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text('contact.google'), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.chrome, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text('contact.google.empty'); + } + return null; + }, + ); + + final addPhoneNumber = ListTile( + contentPadding: EdgeInsets.only(top: 15), + title: ButtonTheme( + height: 45, + child: RaisedButton( + color: Colors.white, + onPressed: () async { + var phone = await showDialog( + context: context, builder: (_) => PhoneEditor()); + _save(phone); + }, + child: Text("Add Phone", + style: TextStyle( + fontSize: 18, + )), + ), + )); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + "contact.title", + fontSize: 20, + color: Colors.white, + ), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + if (!_formKey.currentState.validate()) return; + showConfirmDialog(context, "contact.confrim", () { + _submit(); + }); + }) + ], + ), + body: Form( + key: _formKey, + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + this.phones.isNotEmpty + ? ConstrainedBox( + constraints: BoxConstraints(maxHeight: 1000), + child: ListView.builder( + shrinkWrap: true, + itemBuilder: (context, index) { + return Stack( + alignment: const Alignment(1.0, 1.0), + children: [ + new TextField( + controller: new TextEditingController( + text: this.phones[index]), + cursorColor: primaryColor, + readOnly: true, + decoration: new InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Icon( + Icons.phone, + color: primaryColor, + ), + ), + ), + new FlatButton( + onPressed: () { + setState(() { + this.phones.remove(this.phones[index]); + }); + }, + child: new Icon( + Icons.cancel, + size: 25, + )) + ], + ); + }, + itemCount: this.phones.length, + ), + ) + : Container(), + addPhoneNumber, + LocalTextField( + textEditingController: _deliveryPhone, + icon: Icon( + Icons.phone_forwarded, + color: primaryColor, + ), + labelKey: "contact.delivery.phone", + ), + emailBox, + faceBookBox, + googleBox, + LocalTextField( + textEditingController: _address, + icon: Icon( + Icons.location_on, + color: primaryColor, + ), + labelKey: "contact.address", + maxLines: 3, + ), + ], + ), + ), + )); + } + + _save(String phone) { + if (phone == null) return; + setState(() { + this.phones.add(phone); + }); + } + + _submit() async { + setState(() { + _isLoading = true; + }); + try { + widget.setting.email = _email.text; + widget.setting.facebook = _facebook.text; + widget.setting.website = _website.text; + widget.setting.phones = this.phones; + widget.setting.address = _address.text; + widget.setting.deliveryPhone = _deliveryPhone.text; + var mainModel = Provider.of(context); + await mainModel.updateContact(widget.setting); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/delivery/delivery_item.dart b/lib/pages/delivery/delivery_item.dart new file mode 100644 index 0000000..1edf5fc --- /dev/null +++ b/lib/pages/delivery/delivery_item.dart @@ -0,0 +1,521 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:quiver/async.dart'; +import 'package:fcs/model/do_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/po_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/pages/do/photo_page.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/widget/img_file.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../util.dart'; + +class DeliveryItem extends StatefulWidget { + final DOSubmission doSubmission; + const DeliveryItem({this.doSubmission}); + @override + _DeliveryItemState createState() => _DeliveryItemState(); +} + +class _DeliveryItemState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy'); + final numberFormatter = new NumberFormat("#,###"); + + bool _isLoading = false; + TextEditingController _date = new TextEditingController(); + TextEditingController _number = new TextEditingController(); + TextEditingController _licence = new TextEditingController(); + TextEditingController _driver = new TextEditingController(); + TextEditingController _carNo = new TextEditingController(); + TextEditingController _type = new TextEditingController(); + TextEditingController _name = new TextEditingController(); + TextEditingController _bizName = new TextEditingController(); + TextEditingController _storage = new TextEditingController(); + TextEditingController _comment = new TextEditingController(); + DOSubmission doObj = DOSubmission(); + int _count; + DateTime _result; + File storageChargeFile; + File receiptImageFile; + + @override + void initState() { + super.initState(); + + var mainModel = Provider.of(context, listen: false); + var doModel = Provider.of(context, listen: false); + + doObj = widget.doSubmission; + _date.text = doObj.deliveryDate != null + ? dateFormatter.format(doObj.deliveryDate) + : ""; + _number.text = doObj.doNumber.toString(); + _licence.text = doObj.driverLicenseNumber; + _driver.text = doObj.driverName; + _carNo.text = doObj.carNo; + _type.text = doObj.type; + _name.text = doObj.userName; + _bizName.text = doObj.bizName; + _storage.text = doObj.storageCharge == null + ? "" + : numberFormatter.format(doObj.storageCharge); + _comment.text = doObj.comment; + + if (doObj.deliveryStatus == 'initiated') { + _count = doModel.timber; + Duration diff = DateTime.now().difference(doObj.deliveryInitiatedTime); + + if (diff.inMinutes < mainModel.setting.deliveryStartWaitMin) { + var time = mainModel.setting.deliveryStartWaitMin - diff.inMinutes; + new CountdownTimer( + new Duration(minutes: time), new Duration(seconds: 1)) + .listen((data) { + if (mounted) { + setState(() { + _count = data.remaining.inSeconds; + doModel.addTimber(_count); + }); + } + }); + } + } else { + _count = 0; + } + _load(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + String formattedTime; + if (doObj.deliveryStatus == 'initiated') { + _result = DateTime( + doObj.deliveryInitiatedTime.year, + doObj.deliveryInitiatedTime.month, + doObj.deliveryInitiatedTime.day, + doObj.deliveryInitiatedTime.hour, + doObj.deliveryInitiatedTime.minute, + _count); + formattedTime = DateFormat.ms().format(_result); + } + + final dateBox = Container( + padding: EdgeInsets.only(left: 20, top: 15), + child: Row( + children: [ + LocalText(context, "do.date"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _date.text, + style: textStyle, + ), + ) + ], + ), + ); + + final numberBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.do_num"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _number.text, + style: textStyle, + ), + ) + ], + ), + ); + final driverBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.driver"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _driver.text, + style: textStyle, + ), + ) + ], + ), + ); + final carNoBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.car"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _carNo.text, + style: textStyle, + ), + ) + ], + ), + ); + final licenceBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.licence"), + ImageFile( + enabled: false, + title: "Image", + initialImgUrl: doObj.driverLicenceUrl, + onFile: (file) {}), + ], + ), + ); + final statusBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.status"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doObj.status, + style: doObj.isPending + ? textHighlightBlueStyle + : doObj.isApproved + ? textHighlightGreenStyle + : textHighlightRedStyle, + ), + ), + ], + ), + ); + final deliveryStatusBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.delivery.status"), + Container( + padding: EdgeInsets.only(left: 10, right: 15), + child: Text( + doObj.getDeliveryStatus, + style: textStyle, + ), + ), + doObj.deliveryStatus == 'initiated' + ? Text( + "(can start in $formattedTime)", + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ) + : Container() + ], + ), + ); + + final typeBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.type"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _type.text, + style: textStyle, + ), + ) + ], + ), + ); + + final userNameBox = Container( + padding: EdgeInsets.only(top: 5, left: 20), + child: Row( + children: [ + LocalText(context, "do.name"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _name.text, + style: textStyle, + ), + ) + ], + ), + ); + + final bizNameBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.biz"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _bizName.text, + style: textStyle, + ), + ) + ], + ), + ); + + final receiptImagebox = Container( + padding: EdgeInsets.only(left: 20, top: 0), + child: Row(children: [ + LocalText(context, "do.receipt"), + Container( + padding: EdgeInsets.only(left: 10), + child: ImageFile( + enabled: true, + initialImgUrl: doObj.doReceiptUrl, + title: "Receipt File", + onFile: (file) { + this.receiptImageFile = file; + }, + ), + ), + ])); + + final storageBox = Container( + padding: EdgeInsets.only(left: 20), + child: Row( + children: [ + LocalText(context, "do.storage_charge"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _storage.text, + style: textStyle, + ), + ) + ], + ), + ); + final storagePaymentBox = Container( + padding: EdgeInsets.only(left: 20), + child: Row(children: [ + LocalText(context, "do.storage_receipt"), + ImageFile( + enabled: false, + title: "Receipt File", + initialImgUrl: this.doObj.storageReceiptUrl, + onFile: (file) { + this.storageChargeFile = file; + }), + ])); + + final commentBox = Container( + padding: EdgeInsets.only(top: 5, left: 20), + child: Row( + children: [ + LocalText(context, "do.comment"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _comment.text, + style: textStyle, + ), + ) + ], + ), + ); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("delivery"), + style: Provider.of(context).isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode')), + actions: [ + isBuyer + ? Container() + : PopupMenuButton( + onSelected: _select, + itemBuilder: (context) => List.from([ + PopupMenuItem( + enabled: this.doObj.isApproved, + value: 5, + child: Text("End Delivery"), + ), + ]), + ), + ], + ), + body: Container( + padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + child: Card( + elevation: 23, + child: ListView( + children: [ + Column( + children: [ + dateBox, + Divider(), + numberBox, + Divider(), + userNameBox, + Divider(), + bizNameBox, + Divider(), + typeBox, + Divider(), + statusBox, + Divider(), + doObj.comment == null || doObj.comment == '' + ? Container() + : commentBox, + doObj.comment == null || doObj.comment == '' + ? Container() + : Divider(), + driverBox, + Divider(), + carNoBox, + Divider(), + licenceBox, + Divider(), + receiptImagebox, + Divider(), + doObj.hasStorageCharge() ? storageBox : Container(), + doObj.hasStorageCharge() ? Divider() : Container(), + doObj.hasStorageCharge() + ? storagePaymentBox + : Container(), + doObj.isApproved || doObj.isClosed + ? deliveryStatusBox + : Container(), + doObj.isApproved || doObj.isClosed + ? Divider() + : Container(), + Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 40, + columns: [ + MyDataColumn( + label: LocalText(context, "do.product"), + ), + MyDataColumn( + label: LocalText(context, "do.storage"), + ), + MyDataColumn( + label: LocalText(context, "do.quantity"),numeric: true + ), + ], + rows: getProductRow(doObj.doLines), + ), + ), + ), + SizedBox( + height: 15, + ) + ], + ), + ], + ), + ), + )), + ); + } + + List getProductRow(List doLines) { + ProductModel productModel = Provider.of(context); + if (doLines.isNotEmpty) { + doLines.forEach((d) { + productModel.products.forEach((p) { + if (p.id == d.productID) { + d.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + doLines.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + } + return doLines.map((d) { + return MyDataRow( + cells: [ + MyDataCell( + new Text( + d.productName, + style: textStyle, + ), + ), + MyDataCell( + new Text(d.storageName, style: textStyle), + ), + MyDataCell( + NumberCell(d.qty) + ), + ], + ); + }).toList(); + } + + _select(s) { + if (s == 5) { + if (receiptImageFile == null) { + showMsgDialog(context, "Error", "Please insert delivery receipt file"); + return; + } + showConfirmDialog(context, "delivery.confirm", () { + _endDelivery(receiptImageFile); + }); + } + } + + Future _load() async { + POSubmissionModel poModel = + Provider.of(context, listen: false); + var _doSub = await poModel.loadDOLines(doObj); + setState(() { + doObj.doLines = _doSub.doLines; + }); + } + + _endDelivery(dynamic receiptFile) async { + Uint8List bytesPhoto = receiptFile.readAsBytesSync() as Uint8List; + + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.endDelivery(doObj, bytesPhoto); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/delivery/delivery_list.dart b/lib/pages/delivery/delivery_list.dart new file mode 100644 index 0000000..aa9968d --- /dev/null +++ b/lib/pages/delivery/delivery_list.dart @@ -0,0 +1,274 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/delivery_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/delivery/delivery_item.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/popupmenu.dart'; +import 'package:fcs/widget/progress.dart'; + +class DeliveryList extends StatefulWidget { + @override + _DeliveryListState createState() => _DeliveryListState(); +} + +class _DeliveryListState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy'); + final double dotSize = 10.0; + DateTime _selectedDate = DateTime.now(); + String status; + int _selectedIndex = 0; + int _dateIndex = 0; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + + var doModel = Provider.of(context, listen: false); + _selectedIndex = doModel.popupMenu.index; + _dateIndex = doModel.dateIndex; + _selectedDate = doModel.selectedDate; + } + + @override + void dispose() { + super.dispose(); + } + + Future _selectDate(BuildContext context) async { + var deliveryModel = Provider.of(context); + + final DateTime picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101), + builder: (BuildContext context, Widget child) { + return Theme( + data: ThemeData.light().copyWith( + primaryColor: primaryColor, //Head background + accentColor: secondaryColor, //selection color + dialogBackgroundColor: Colors.white, //Background color + ), + child: child, + ); + }, + ); + + if (picked != null) { + var pickedDate = new DateTime(picked.year, picked.month, picked.day); + var currentDate = new DateTime( + DateTime.now().year, DateTime.now().month, DateTime.now().day); + this._dateIndex = pickedDate == currentDate ? 0 : 1; + setState(() { + _selectedDate = picked; + deliveryModel.filterData( + status, _selectedDate, _selectedIndex, _dateIndex); + }); + } + } + + @override + Widget build(BuildContext context) { + var deliveryModel = Provider.of(context); + MainModel mainModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + var languageModle = Provider.of(context); + + return Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text("delivery.title"), + style: languageModle.isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode'), + ), + actions: [ + InkWell( + child: Container( + padding: EdgeInsets.only(top: 15), + child: Stack( + children: [ + Image.asset( + "assets/date_filter.png", + color: Colors.white, + width: 25, + ), + _dateIndex == 0 + ? Container() + : Positioned( + bottom: 15, + right: 10, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + ], + ), + ), + onTap: () => _selectDate(context), + ), + PopupMenuButton( + elevation: 3.2, + onSelected: (selected) { + setState(() { + _selectedIndex = selected.index; + }); + if (selected.status == 'All') { + status = null; + } else { + status = selected.status; + } + deliveryModel.filterData( + status, _selectedDate, _selectedIndex, _dateIndex); + }, + icon: Container( + width: 30, + height: 30, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Icon( + Icons.filter_list, + color: primaryColor, + ), + _selectedIndex != 0 + ? Positioned( + bottom: 0, + right: 0, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + : Container() + ], + )), + itemBuilder: (BuildContext context) { + return deliveryStatusMenu.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Row( + children: [ + Text(choice.status), + SizedBox( + width: 10, + ), + _selectedIndex != null && _selectedIndex == choice.index + ? Icon( + Icons.check, + color: Colors.grey, + ) + : Container(), + ], + ), + ); + }).toList(); + }), + ], + ), + body: LocalProgress( + inAsyncCall: _isLoading, + child: new ListView.builder( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: deliveryModel.dos.length, + itemBuilder: (BuildContext context, int index) { + return Card( + elevation: 10, + color: Colors.white, + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DeliveryItem( + doSubmission: deliveryModel.dos[index])), + ); + }, + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Image.asset( + "assets/truck.png", + width: 50, + height: 50, + color: primaryColor, + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Text( + deliveryModel.dos[index].doNumber, + style: new TextStyle( + fontSize: 12.0, color: Colors.black), + ), + new Text( + deliveryModel.dos[index].deliveryDate == + null + ? "" + : dateFormatter.format(deliveryModel + .dos[index].deliveryDate), + style: new TextStyle( + fontSize: 14.0, color: Colors.grey), + ), + !isBuyer + ? new Text( + deliveryModel.dos[index].userName, + style: new TextStyle( + fontSize: 12.0, + color: Colors.grey), + ) + : Container() + ], + ), + ), + Container( + padding: EdgeInsets.only(right: 15), + child: + getStatus(deliveryModel.dos[index].status), + ), + ], + ), + ), + ), + ), + ], + ), + ); + }), + ), + ); + } +} diff --git a/lib/pages/device_list.dart b/lib/pages/device_list.dart new file mode 100644 index 0000000..a0a3db4 --- /dev/null +++ b/lib/pages/device_list.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/device_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/vo/device.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/popupmenu.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'util.dart'; + +class PhoneDeviceList extends StatefulWidget { + @override + _PhoneDeviceListState createState() => _PhoneDeviceListState(); +} + +class _PhoneDeviceListState extends State { + final double dotSize = 15.0; + PopupMenu selectedChoices = deviceMenu[0]; + bool _isLoading = false; + PhoneDevice phoneDevice = new PhoneDevice(); + + @override + Widget build(BuildContext context) { + var deviceModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'profile.devices', + color: Colors.white, + fontSize: 20, + ), + ), + body: new ListView.builder( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: deviceModel.devices.length, + itemBuilder: (BuildContext context, int index) { + return Card( + elevation: 10, + color: Colors.white, + child: InkWell( + onTap: () {}, + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 7.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 15.0 - dotSize / 2), + child: Padding( + padding: EdgeInsets.all(5.0), + child: Image.asset( + "assets/device.png", + width: 40, + height: 40, + color: primaryColor, + ), + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Text( + deviceModel.devices[index].name, + style: new TextStyle( + fontSize: 13.0, color: Colors.black), + ), + ], + ), + ), + ], + ), + ), + ), + PopupMenuButton( + elevation: 3.2, + onSelected: _select, + itemBuilder: (BuildContext context) { + this.phoneDevice = deviceModel.devices[index]; + return deviceMenu.map((PopupMenu choice) { + return PopupMenuItem( + enabled: choice.index == 0 + ? deviceModel.devices[index].isDeviceOn() + ? false + : true + : choice.index == 1 + ? deviceModel.devices[index] + .isDeviceOn() + ? true + : false + : true, + value: choice, + child: Text(choice.status), + ); + }).toList(); + }), + ], + ), + ), + ); + }), + ), + ); + } + + void _select(PopupMenu choice) async { + selectedChoices = choice; + if (choice.index == 0) { + showConfirmDialog(context, "device.confirm", () { + _confirm(); + }); + } else if (choice.index == 1) { + showConfirmDialog(context, "device.logout", () { + _logout(); + }); + } else if (choice.index == 2) { + showConfirmDialog(context, "device.set_primary", () { + _setPrimaryDevice(); + }); + } + } + + _confirm() async { + setState(() { + _isLoading = true; + }); + + try { + var deviceModel = Provider.of(context); + var mainModel = Provider.of(context); + + await deviceModel.confirmDevice( + mainModel.user.phoneNumber, this.phoneDevice.id); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _logout() async { + setState(() { + _isLoading = true; + }); + + try { + var deviceModel = Provider.of(context); + var mainModel = Provider.of(context); + + await deviceModel.logoutDevice( + mainModel.user.phoneNumber, this.phoneDevice.id); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _setPrimaryDevice() async { + setState(() { + _isLoading = true; + }); + + try { + var deviceModel = Provider.of(context); + var mainModel = Provider.of(context); + await deviceModel.setPrimaryDevice( + mainModel.user.phoneNumber, this.phoneDevice.id); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/do/do_approve.dart b/lib/pages/do/do_approve.dart new file mode 100644 index 0000000..d4042b0 --- /dev/null +++ b/lib/pages/do/do_approve.dart @@ -0,0 +1,904 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:quiver/async.dart'; +import 'package:fcs/model/do_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/log_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/po_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/pages/do/photo_page.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/img_file.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../document_log_page.dart'; +import '../util.dart'; +import 'do_files.dart'; +import 'do_storage_item.dart'; + +class DOApproval extends StatefulWidget { + final DOSubmission doSubmission; + const DOApproval({this.doSubmission}); + @override + _DOApprovalState createState() => _DOApprovalState(); +} + +class _DOApprovalState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy'); + final numberFormatter = new NumberFormat("#,###"); + var doDateFormatter = new DateFormat('dd MMM yyyy - hh:mm a'); + + bool _isLoading = false; + TextEditingController _date = new TextEditingController(); + TextEditingController _doDate = new TextEditingController(); + TextEditingController _number = new TextEditingController(); + TextEditingController _licence = new TextEditingController(); + TextEditingController _driver = new TextEditingController(); + TextEditingController _carNo = new TextEditingController(); + TextEditingController _type = new TextEditingController(); + TextEditingController _name = new TextEditingController(); + TextEditingController _bizName = new TextEditingController(); + TextEditingController _storage = new TextEditingController(); + TextEditingController _comment = new TextEditingController(); + DOSubmission doObj = DOSubmission(); + int _count; + DateTime _result; + DOFiles files = DOFiles(); + List doLines = new List(); + + @override + void initState() { + super.initState(); + + var mainModel = Provider.of(context, listen: false); + var doModel = Provider.of(context, listen: false); + + doObj = widget.doSubmission; + _date.text = doObj.deliveryDate != null + ? dateFormatter.format(doObj.deliveryDate) + : ""; + _doDate.text = + doObj.doDate != null ? doDateFormatter.format(doObj.doDate) : ""; + _number.text = doObj.doNumber.toString(); + _licence.text = doObj.driverLicenseNumber; + _driver.text = doObj.driverName; + _carNo.text = doObj.carNo; + _type.text = doObj.type; + _name.text = doObj.userName; + _bizName.text = doObj.bizName; + _storage.text = doObj.storageCharge == null + ? "" + : numberFormatter.format(doObj.storageCharge); + _comment.text = doObj.comment; + + if (doObj.deliveryStatus == 'initiated') { + _count = doModel.timber; + + Duration diff = DateTime.now().difference(doObj.deliveryInitiatedTime); + + if (diff.inMinutes < mainModel.setting.deliveryStartWaitMin) { + var time = mainModel.setting.deliveryStartWaitMin - diff.inMinutes; + new CountdownTimer( + new Duration(minutes: time), new Duration(seconds: 1)) + .listen((data) { + if (mounted) { + setState(() { + _count = data.remaining.inSeconds; + doModel.addTimber(_count); + }); + } + }); + } + } else { + _count = 0; + } + + _load(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + var logModel = Provider.of(context); + String formattedTime; + if (doObj.deliveryStatus == 'initiated') { + _result = DateTime( + doObj.deliveryInitiatedTime.year, + doObj.deliveryInitiatedTime.month, + doObj.deliveryInitiatedTime.day, + doObj.deliveryInitiatedTime.hour, + doObj.deliveryInitiatedTime.minute, + _count); + formattedTime = DateFormat.ms().format(_result); + } + + final doDateBox = Container( + padding: EdgeInsets.only(left: 20, top: 15), + child: Row( + children: [ + LocalText(context, "do.do_date"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _doDate.text, + style: textStyle, + ), + ) + ], + ), + ); + + final dateBox = Container( + padding: EdgeInsets.only(left: 20, top: 8), + child: Row( + children: [ + LocalText(context, "do.date"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _date.text, + style: textStyle, + ), + ) + ], + ), + ); + + final numberBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.do_num"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _number.text, + style: textStyle, + ), + ) + ], + ), + ); + final driverBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.driver"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _driver.text, + style: textStyle, + ), + ) + ], + ), + ); + final carNoBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.car"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _carNo.text, + style: textStyle, + ), + ) + ], + ), + ); + final licenceBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.licence"), + ImageFile( + enabled: false, + title: "Image", + initialImgUrl: doObj.driverLicenceUrl, + onFile: (file) {}), + ], + ), + ); + final statusBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.status"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doObj.status, + style: doObj.isPending + ? textHighlightBlueStyle + : doObj.isApproved + ? textHighlightGreenStyle + : textHighlightRedStyle, + ), + ), + ], + ), + ); + final deliveryStatusBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.delivery.status"), + Container( + padding: EdgeInsets.only(left: 10, right: 15), + child: Text( + doObj.getDeliveryStatus, + style: textStyle, + ), + ), + doObj.deliveryStatus == 'initiated' + ? Text( + "(can start in $formattedTime)", + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ) + : Container() + ], + ), + ); + + final typeBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.type"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _type.text, + style: textStyle, + ), + ) + ], + ), + ); + + final userNameBox = Container( + padding: EdgeInsets.only(top: 5, left: 20), + child: Row( + children: [ + LocalText(context, "do.name"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _name.text, + style: textStyle, + ), + ) + ], + ), + ); + + final bizNameBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.biz"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _bizName.text, + style: textStyle, + ), + ) + ], + ), + ); + + final driverImgUrlBox = Container( + padding: EdgeInsets.only(left: 20), + child: Row(children: [ + LocalText(context, "do.driver.image"), + Container( + padding: EdgeInsets.only(left: 10), + child: ImageFile( + enabled: !isBuyer && doObj.deliveryStatus == null, + initialImgUrl: doObj.driverImgUrl, + title: "Image", + imageSource: ImageSource.camera, + onFile: (file) { + doObj.driverImg = file; + }), + ), + ])); + + final receiptImagebox = Container( + padding: EdgeInsets.only(left: 20, top: 0), + child: Row(children: [ + LocalText(context, "do.receipt"), + Container( + padding: EdgeInsets.only(left: 10), + child: ImageFile( + enabled: false, + initialImgUrl: doObj.doReceiptUrl, + title: "Receipt", + ), + ), + ])); + + final deliveryInitTimeBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.delivery.init.time"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doObj.deliveryInitTime, + style: textStyle, + ), + ) + ], + ), + ); + + final storageBox = Container( + padding: EdgeInsets.only(left: 20), + child: Row( + children: [ + LocalText(context, "do.storage_charge"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _storage.text, + style: textStyle, + ), + ) + ], + ), + ); + final storagePaymentBox = Container( + padding: EdgeInsets.only(left: 20), + child: Row(children: [ + LocalText(context, "do.storage_receipt"), + ImageFile( + enabled: mainModel.user.isBuyer() ? true : false, + title: "Receipt File", + initialImgUrl: this.doObj.storageReceiptUrl, + onFile: (file) { + this.files.setStorageChargeFile = file; + }), + ])); + + final commentBox = Container( + padding: EdgeInsets.only(top: 5, left: 20), + child: Row( + children: [ + LocalText(context, "do.comment"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _comment.text, + style: textStyle, + ), + ) + ], + ), + ); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("do"), + style: Provider.of(context).isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode')), + actions: [ + mainModel.showHistoryBtn() + ? IconButton( + icon: Icon(Icons.history), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DocumentLogPage(docID: doObj.id)), + ); + }, + ) + : Container(), + isBuyer + ? doObj.isPending + ? PopupMenuButton( + onSelected: _selectBuyer, + itemBuilder: (context) => List.from([ + PopupMenuItem( + enabled: this.doObj.isPending && + this.doObj.storageCharge > 0, + value: 1, + child: Text("Update DO"), + ), + PopupMenuItem( + enabled: this.doObj.isPending, + value: 2, + child: Text("Cancel DO"), + ), + ])) + : Container() + : PopupMenuButton( + onSelected: _select, + itemBuilder: (context) => List.from([ + PopupMenuItem( + enabled: this.doObj.isPending, + value: 1, + child: Text("Approve DO"), + ), + PopupMenuItem( + enabled: this.doObj.isPending, + value: 2, + child: Text("Reject DO"), + ), + PopupMenuItem( + enabled: + this.doObj.isApproved && mainModel.user.isOwner(), + value: 6, + child: Text("Cancel DO"), + ) + ]), + ), + ], + ), + body: Container( + padding: + EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + child: Card( + elevation: 23, + child: ListView( + children: [ + Column( + children: [ + doDateBox, + Divider(), + dateBox, + Divider(), + numberBox, + Divider(), + userNameBox, + Divider(), + bizNameBox, + Divider(), + typeBox, + Divider(), + statusBox, + Divider(), + doObj.comment == null || doObj.comment == '' + ? Container() + : commentBox, + doObj.comment == null || doObj.comment == '' + ? Container() + : Divider(), + driverBox, + Divider(), + carNoBox, + Divider(), + licenceBox, + Divider(), + receiptImagebox, + Divider(), + doObj.hasStorageCharge() ? storageBox : Container(), + doObj.hasStorageCharge() ? Divider() : Container(), + doObj.hasStorageCharge() + ? storagePaymentBox + : Container(), + doObj.isApproved || doObj.isClosed + ? deliveryStatusBox + : Container(), + doObj.isApproved || doObj.isClosed + ? Divider() + : Container(), + Container( + padding: EdgeInsets.only(top: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: LocalText( + context, + 'do.products', + fontSize: 14, + fontWeight: FontWeight.bold, + underline: true, + color: secondaryColor, + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 40, + columns: [ + MyDataColumn( + label: LocalText(context, "do.product"), + ), + MyDataColumn( + label: LocalText(context, "do.storage"), + ), + MyDataColumn( + label: LocalText(context, "do.quantity"),numeric: true + ), + ], + rows: getProductRow(doObj.doLines), + ), + ), + ], + ), + ), + SizedBox( + height: 15, + ), + getPOProductTable() + ], + ), + ], + ), + )), + )); + } + + List getProductRow(List doLines) { + MainModel mainModel = Provider.of(context); + ProductModel productModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + + if (doLines.isNotEmpty) { + doLines.forEach((d) { + productModel.products.forEach((p) { + if (p.id == d.productID) { + d.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + doLines.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + } + return doLines.map((d) { + return MyDataRow( + onSelectChanged: (bool selected) async { + if (isBuyer) return; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DOStorageItem( + doLine: d, + onSave: (storageID, storageName) { + setState(() { + d.storageID = storageID; + d.storageName = storageName; + }); + }, + )), + ); + }, + cells: [ + MyDataCell( + new Text( + d.productName, + style: textStyle, + ), + ), + MyDataCell( + new Text(d.storageName, style: textStyle), + ), + MyDataCell(NumberCell(d.qty)), + ], + ); + }).toList(); + } + + Widget getPOProductTable() { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + Center( + child: LocalText(context, 'po.info', + fontSize: 14, + fontWeight: FontWeight.bold, + underline: true, + color: secondaryColor), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 20, + columns: [ + MyDataColumn(label: LocalText(context, "po.number")), + MyDataColumn(label: LocalText(context, "po.product")), + MyDataColumn( + label: LocalText(context, "do.po_qty"),numeric: true, + ), + MyDataColumn( + label: LocalText(context, "do.po_balance_qty"),numeric: true, + ), + MyDataColumn( + label: LocalText(context, "po.retrieved.amount"),numeric: true, + ), + MyDataColumn( + label: LocalText(context, "do.do_qty"),numeric: true, + ), + ], + rows: getPOProductRow(), + ), + ), + ], + ), + ); + } + + List getPOProductRow() { + ProductModel productModel = Provider.of(context); + if (doObj.dopoLies.isNotEmpty) { + doObj.dopoLies.sort((p1, p2) => p1.poNumber.compareTo(p2.poNumber)); + + doObj.dopoLies.forEach((d) { + productModel.products.forEach((p) { + if (p.id == d.productID) { + d.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + doObj.dopoLies.sort((p1, p2) { + if (p1.displayOrder != p2.displayOrder) + return p1.displayOrder.compareTo(p2.displayOrder); + return p1.poNumber.compareTo(p2.poNumber); + }); + } + return doObj.dopoLies.map((d) { + return MyDataRow( + cells: [ + MyDataCell( + new Text( + d.poNumber, + style: textStyle, + ), + ), + MyDataCell( + new Text( + d.productName, + style: textStyle, + ), + ), + MyDataCell(NumberCell(d.poQty)), + MyDataCell(NumberCell(d.poBalAtCreate)), + MyDataCell(NumberCell(d.getPoBalanceQtyAtCreate)), + MyDataCell(NumberCell(d.doQty)), + ], + ); + }).toList(); + } + + _select(s) { + if (s == 1) { + showConfirmDialog(context, "do.approve.confirm", () { + _approve(); + }); + } else if (s == 2) { + showCommentDialog(context, (comment) { + doObj.comment = comment; + _reject(); + }); + } else if (s == 5) { + showConfirmDialog(context, "do.end.confirm", () { + _endDelivery(); + }); + } else if (s == 6) { + showConfirmDialog(context, "do.cancel.confirm", () { + _cancelDelivery(); + }); + } + } + + _selectBuyer(s) { + if (s == 1) { + showConfirmDialog(context, "do.confirm", () { + _submit(); + }); + } else if (s == 2) { + showConfirmDialog(context, "do.cancel.confirm", () { + _cancelDelivery(); + }); + } + } + + Future _load() async { + POSubmissionModel poModel = + Provider.of(context, listen: false); + DOModel doModel = Provider.of(context, listen: false); + + var _doSub = await poModel.loadDOLines(doObj); + _doSub = await doModel.loadDOPOLines(_doSub); + // set po balance + List pos = _doSub.getPOs(); + for (var po in pos) { + List poLines = await poModel.loadPOLines(po); + _doSub.setDOPOLineBalance(po, poLines); + } + if (mounted) { + setState(() { + doObj.doLines = _doSub.doLines; + doObj.dopoLies = _doSub.dopoLies; + }); + } + } + + _approve() async { + if (doObj.doLines.any((l) => l.storageID.isEmpty)) { + showMsgDialog(context, "Error", "Storage required for every product"); + return; + } + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.approveDO(doObj); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _reject() async { + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.rejectDO(doObj); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _initDelivery() async { + if (doObj.driverImg == null) { + showMsgDialog(context, "Error", "Please attach driver image"); + return; + } + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.initDelivery(doObj); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _startDelivery() async { + MainModel mainModel = Provider.of(context); + Duration diff = DateTime.now().difference(doObj.deliveryInitiatedTime); + if (diff.inMinutes < mainModel.setting.deliveryStartWaitMin) { + showMsgDialog(context, "Waiting...", + "Can not start delivery, wait for ${mainModel.setting.deliveryStartWaitMin} minutes"); + return; + } + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.startDelivery(doObj); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _endDelivery() async { + var photo = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => PhotoPage()), + ); + if (photo == null) { + return; + } + Uint8List bytesPhoto = photo.readAsBytesSync() as Uint8List; + + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.endDelivery(doObj, bytesPhoto); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _cancelDelivery() async { + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.cancelDO(doObj); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _submit() async { + if (doObj.hasStorageCharge()) { + if (files.storageChargeFile == null && doObj.storageReceiptUrl == '') { + showMsgDialog(context, "Error", "Please insert storage receipt"); + return; + } + } + + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + await doModel.updateDO(doObj, files); + Navigator.pop(context, true); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/do/do_creation_form.dart b/lib/pages/do/do_creation_form.dart new file mode 100644 index 0000000..ce34abe --- /dev/null +++ b/lib/pages/do/do_creation_form.dart @@ -0,0 +1,650 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/do_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/po_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/pages/do/do_product_item.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/img_file.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'do_files.dart'; +import 'po_selection.dart'; + +class DOForm extends StatefulWidget { + final DOSubmission doSubmission; + const DOForm({this.doSubmission}); + @override + _DOFormState createState() => _DOFormState(); +} + +class _DOFormState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy'); + final numberFormatter = new NumberFormat("#,###"); + + TextEditingController _deliveryDate = new TextEditingController(); + TextEditingController _licence = new TextEditingController(); + TextEditingController _driver = new TextEditingController(); + TextEditingController _carNo = new TextEditingController(); + TextEditingController _doStatus = new TextEditingController(); + TextEditingController _storage = new TextEditingController(); + TextEditingController _doNumber = new TextEditingController(); + DOSubmission doSubmission = DOSubmission(); + + bool _isLoading = false; + bool _isNew = true; + final _formKey = GlobalKey(); + int doTypeValue = 0; + DOLine doLine = new DOLine(); + DOFiles files = DOFiles(); + + @override + void initState() { + super.initState(); + + if (widget.doSubmission != null) { + this.doSubmission = widget.doSubmission; + this._isNew = false; + _deliveryDate.text = + dateFormatter.format(widget.doSubmission.deliveryDate); + _licence.text = widget.doSubmission.driverLicenseNumber; + _driver.text = widget.doSubmission.driverName; + _carNo.text = widget.doSubmission.carNo; + _doStatus.text = widget.doSubmission.status; + _storage.text = widget.doSubmission.storageCharge == null + ? "" + : numberFormatter.format(widget.doSubmission.storageCharge); + _doNumber.text = widget.doSubmission.doNumber; + + if (widget.doSubmission.type == 'multiple') { + doTypeValue = 1; + } else { + doTypeValue = 0; + } + this.doLine.action = 'update'; + } else { + this.doLine.action = 'create'; + doSubmission.type = 'single'; + _storage.text = "0"; + } + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + var mainModel = Provider.of(context); + var poModel = Provider.of(context); + + bool isBuyer = mainModel.user.isBuyer(); + + final doNumberBox = Container( + padding: EdgeInsets.only(left: 20, top: 10), + child: Row( + children: [ + LocalText(context, "do.do_num"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _doNumber.text, + style: textStyle, + ), + ) + ], + ), + ); + final _doTypeValueBox = Container( + padding: EdgeInsets.only(left: 20, top: 0), + child: Row( + children: [ + LocalText(context, "do.type"), + Container( + width: 130, + child: RadioListTile( + dense: true, + title: LocalText(context, "do.single"), + value: 0, + groupValue: doTypeValue, + onChanged: handleRadioValueChanged, + activeColor: primaryColor, + ), + ), + Container( + width: 136, + child: RadioListTile( + dense: true, + title: LocalText(context, 'do.multiple'), + value: 1, + groupValue: doTypeValue, + onChanged: handleRadioValueChanged, + activeColor: primaryColor, + )) + ], + ), + ); + + final deliveryDateBox = Container( + padding: EdgeInsets.only(left: 20, right: 15), + child: InkWell( + onTap: () { + DatePicker.showDatePicker(context, + showTitleActions: true, + currentTime: _deliveryDate.text == "" + ? null + : dateFormatter.parse(_deliveryDate.text), + minTime: DateTime.now(), + maxTime: DateTime(2030, 12, 31), onConfirm: (date) { + setState(() { + _deliveryDate.text = dateFormatter.format(date); + doSubmission.deliveryDate = + dateFormatter.parse(_deliveryDate.text); + doSubmission.updateStorageCharge(mainModel.setting); + }); + }, locale: LocaleType.en); + }, + child: TextFormField( + controller: _deliveryDate, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + enabled: false, + keyboardType: TextInputType.datetime, + decoration: new InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + labelText: AppTranslations.of(context).text("do.date"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + contentPadding: + EdgeInsets.symmetric(vertical: 10.0, horizontal: 0.0), + icon: Icon( + Icons.date_range, + color: primaryColor, + )), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("do.form.date"); + } + return null; + }, + ), + )); + + final driverBox = Container( + padding: EdgeInsets.only(left: 20, right: 15), + child: TextFormField( + controller: _driver, + autofocus: false, + style: textStyle, + cursorColor: primaryColor, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("do.driver"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + icon: Icon( + Icons.account_box, + color: primaryColor, + )), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("do.form.driver"); + } + return null; + }, + )); + + final carNoBox = Container( + padding: EdgeInsets.only(left: 20, right: 15), + child: TextFormField( + controller: _carNo, + autofocus: false, + style: textStyle, + cursorColor: primaryColor, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("do.car"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + icon: Icon( + Icons.directions_car, + color: primaryColor, + )), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("do.form.car"); + } + return null; + }, + )); + + final doStatusBox = Container( + padding: EdgeInsets.only(left: 20, right: 15), + child: TextFormField( + controller: _doStatus, + autofocus: false, + style: textStyle, + readOnly: true, + decoration: new InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Image.asset("assets/status.png", + width: 25, color: primaryColor)), + validator: (value) { + if (value.isEmpty) { + return "Please enter DO Status"; + } + return null; + }, + )); + + final storageBox = Container( + padding: EdgeInsets.only(left: 20, top: 10), + child: Row( + children: [ + LocalText(context, "do.storage_charge"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doSubmission.storageCharge == null + ? "" + : numberFormatter.format(doSubmission.storageCharge), + style: textStyle, + ), + ) + ], + ), + ); + final storagePaymentBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row(children: [ + LocalText(context, "do.storage_receipt"), + ImageFile( + enabled: isBuyer, + title: "Receipt File", + initialImgUrl: this.doSubmission.storageReceiptUrl, + onFile: (file) { + this.files.setStorageChargeFile = file; + }), + ])); + final licesebox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row(children: [ + LocalText(context, "do.licence"), + ImageFile( + enabled: isBuyer, + title: "Image", + initialImgUrl: this.doSubmission.driverLicenceUrl, + onFile: (file) { + this.files.setlicenseFile = file; + }), + ])); + + final poButtun = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row(children: [ + LocalText(context, "po.title"), + IconButton( + onPressed: () async { + POSelection.showPOSelection( + context, poModel.approvedPOs, doSubmission.pos, + ok: (List pos) async { + for (var po in pos) { + po.poLines = await poModel.loadPOLines(po.id); + } + setState(() { + doSubmission.pos = pos; + doSubmission.loadPOs(); + doSubmission.updateStorageCharge(mainModel.setting); + }); + }); + }, + icon: Icon(Icons.edit)), + ])); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("do"), + style: Provider.of(context).isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode')), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + if (!_formKey.currentState.validate()) return; + showConfirmDialog(context, "do.confirm", () { + _submit(mainModel); + }); + }, + ) + ], + ), + body: Form( + key: _formKey, + child: Container( + child: ListView( + children: [ + Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + this.doSubmission.doNumber != null + ? doNumberBox + : Container(), + this.doSubmission.hasStorageCharge() + ? storageBox + : Container(), + this.doSubmission.hasStorageCharge() + ? storagePaymentBox + : Container(), + _doTypeValueBox, + deliveryDateBox, + driverBox, + licesebox, + carNoBox, + poButtun, + widget.doSubmission == null ? Container() : doStatusBox, + getProductTable(), + getPOProductTable(), + ], + ), + ), + ], + ), + ), + )), + ); + } + + void handleRadioValueChanged(int value) { + var mainModel = Provider.of(context, listen: false); + setState(() { + doSubmission.type = value == 0 ? 'single' : 'multiple'; + doTypeValue = value; + doSubmission.loadPOs(); + doSubmission.updateStorageCharge(mainModel.setting); + switch (doTypeValue) { + case 0: + break; + case 1: + break; + } + }); + } + + Widget getProductTable() { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: LocalText( + context, + 'do.products', + fontSize: 14, + fontWeight: FontWeight.bold, + underline: true, + color: secondaryColor, + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(top: 10), + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 20, + columns: [ + MyDataColumn(label: LocalText(context, "do.product")), + MyDataColumn( + label: LocalText(context, "po.avail.qty"), + numeric: true + ), + MyDataColumn( + label: LocalText(context, "do.do_qty"), + numeric: true, + ), + ], + rows: getProductRow(doSubmission.doLines), + ), + ), + ], + ), + ); + } + + List getProductRow(List doLines) { + ProductModel productModel = Provider.of(context); + if (doLines.isNotEmpty) { + doLines.forEach((d) { + productModel.products.forEach((p) { + if (p.id == d.productID) { + d.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + doLines.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + } + return doLines.map((d) { + return MyDataRow( + onSelectChanged: (bool selected) async { + if (doTypeValue == 0) return; + var doLine = await showDialog( + context: context, + builder: (_) => DOProductItem( + doLine: DOLine(productID: d.productID, qty: d.qty), + )); + _updateQty(doLine); + }, + cells: [ + MyDataCell( + new Text( + d.productName, + style: textStyle, + ), + ), + MyDataCell( + NumberCell(d.poBalQty) + ), + MyDataCell( + Container( + color: Colors.cyan, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + new Text(d.qty == null ? "0" : d.qty.toString(), + style: textStyle), + ], + )), + ), + ], + ); + }).toList(); + } + + Widget getPOProductTable() { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + Center( + child: LocalText(context, 'po.info', + fontSize: 14, + fontWeight: FontWeight.bold, + underline: true, + color: secondaryColor), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 20, + columns: [ + MyDataColumn(label: LocalText(context, "po.number")), + MyDataColumn(label: LocalText(context, "po.product")), + MyDataColumn( + label: LocalText(context, "do.po_qty"),numeric: true, + ), + MyDataColumn( + label: LocalText(context, "po.avail.qty"),numeric: true, + ), + MyDataColumn( + label: LocalText(context, "po.retrieved.amount"),numeric: true, + ), + MyDataColumn( + label: LocalText(context, "do.do_qty"),numeric: true, + ), + ], + rows: getPOProductRow(), + ), + ), + ], + ), + ); + } + + List getPOProductRow() { + ProductModel productModel = Provider.of(context); + if (doSubmission.dopoLies.isNotEmpty) { + doSubmission.dopoLies + .sort((p1, p2) => p1.poNumber.compareTo(p2.poNumber)); + doSubmission.dopoLies.forEach((d) { + productModel.products.forEach((p) { + if (p.id == d.productID) { + d.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + doSubmission.dopoLies.sort((p1, p2) { + if (p1.displayOrder!=p2.displayOrder) + return p1.displayOrder.compareTo(p2.displayOrder); + return p1.poNumber.compareTo(p2.poNumber); + }); + } + return doSubmission.dopoLies.map((d) { + return MyDataRow( + cells: [ + MyDataCell( + new Text( + d.poNumber, + style: textStyle, + ), + ), + MyDataCell( + new Text( + d.productName, + style: textStyle, + ), + ), + MyDataCell( + NumberCell(d.poQty) + ), + MyDataCell( + NumberCell(d.poBalQty) + ), + MyDataCell( + NumberCell(d.getPoBalanceQty) + ), + MyDataCell( + Container( + color: Colors.grey, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + new Text(d.doQty == null ? "0" : d.doQty.toString(), + style: textStyle), + ], + )), + ), + ], + ); + }).toList(); + } + + _updateQty(DOLine doLine) { + if (doLine == null) return; + + try { + var mainModel = Provider.of(context); + setState(() { + doSubmission.updateDoline(doLine.productID, doLine.qty); + doSubmission.updateStorageCharge(mainModel.setting); + }); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } + } + + _submit(MainModel mainModel) async { + if (doSubmission.doLines.length == 0) { + showMsgDialog(context, "Error", "No product line"); + return; + } + if (files.licenseFile == null) { + showMsgDialog(context, "Error", "Please insert driver licence"); + return; + } + + int total = 0; + doSubmission.doLines.forEach((doLine) { + total += doLine.qty; + }); + + if (total <= 0) { + showMsgDialog(context, "Error", "must be greater than zero"); + return; + } + + doSubmission.carNo = _carNo.text; + doSubmission.driverName = _driver.text; + doSubmission.driverLicenseNumber = _licence.text; + doSubmission.deliveryDate = dateFormatter.parse(_deliveryDate.text); + doSubmission.type = doTypeValue == 0 ? 'single' : 'multiple'; + doSubmission.doLines.removeWhere((d) => d.qty == 0); + doSubmission.dopoLies.removeWhere((d) => d.doQty == 0); + + setState(() { + _isLoading = true; + }); + try { + DOModel doModel = Provider.of(context); + + if (_isNew) { + await doModel.createDO(doSubmission, files); + } else { + await doModel.updateDO(doSubmission, files); + } + Navigator.pop(context, true); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/do/do_creation_todelete.dart b/lib/pages/do/do_creation_todelete.dart new file mode 100644 index 0000000..9b6bb20 --- /dev/null +++ b/lib/pages/do/do_creation_todelete.dart @@ -0,0 +1,423 @@ +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/po_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/img_file.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:progress/progress.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'do_creation_form.dart'; + +class DoCreation extends StatefulWidget { + final POSubmission poSubmission; + const DoCreation({this.poSubmission}); + @override + _DoCreationState createState() => _DoCreationState(); +} + +class _DoCreationState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy'); + bool _isLoading = false; + TextEditingController _podate = new TextEditingController(); + TextEditingController _ponumber = new TextEditingController(); + TextEditingController _postatus = new TextEditingController(); + POSubmission poSubmission = POSubmission(); + + @override + void initState() { + super.initState(); + this.poSubmission = widget.poSubmission; + _podate.text = dateFormatter.format(this.poSubmission.poDate); + _ponumber.text = this.poSubmission.poNumber.toString(); + _postatus.text = this.poSubmission.status; + _isLoading = true; + _load(); + } + + Future _load() async { + List poLines = + await Provider.of(context, listen: false) + .loadPOLines(poSubmission.id); + var _poDos = await Provider.of(context, listen: false) + .loadDOs(poSubmission); + setState(() { + this.poSubmission.poLines = poLines; + this.poSubmission.dos = _poDos.dos; + _isLoading = false; + }); + } + + Widget doDateBox(BuildContext context, DOSubmission doSubmission) { + return Container( + padding: EdgeInsets.only(left: 20, top: 0), + child: Row( + children: [ + LocalText(context, "do.date"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + dateFormatter.format(doSubmission.deliveryDate), + style: textStyle, + ), + ) + ], + ), + ); + } + + Widget doNumberBox(BuildContext context, DOSubmission doSubmission) { + return Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.do_num"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doSubmission.doNumber, + style: textStyle, + ), + ) + ], + ), + ); + } + + Widget driverBox(BuildContext context, DOSubmission doSubmission) { + return Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.driver"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doSubmission.driverName, + style: textStyle, + ), + ) + ], + ), + ); + } + + Widget carNoBox(BuildContext context, DOSubmission doSubmission) { + return Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.car"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doSubmission.carNo, + style: textStyle, + ), + ) + ], + ), + ); + } + + Widget doStatusBox(BuildContext context, DOSubmission doSubmission) { + return Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.status"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + doSubmission.status, + style: textStyle, + ), + ) + ], + ), + ); + } + + Widget driverLicence(BuildContext context, DOSubmission doSubmission) { + return Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.licence"), + ImageFile( + enabled: false, + title: "Receipt File", + initialImgUrl: doSubmission.driverLicenceUrl, + onFile: (file) {}), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + var languageModle = Provider.of(context); + + final poDateBox = Container( + padding: EdgeInsets.only(left: 20, top: 15), + child: Row( + children: [ + LocalText(context, "po.date"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _podate.text, + style: textStyle, + ), + ) + ], + ), + ); + + final poNumberBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "do.po_num"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _ponumber.text, + style: textStyle, + ), + ) + ], + ), + ); + + final poStatusBox = Container( + padding: EdgeInsets.only(left: 20, top: 5), + child: Row( + children: [ + LocalText(context, "po.status"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _postatus.text, + style: textStyle, + ), + ) + ], + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("do.title"), + style: languageModle.isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode')), + ), + floatingActionButton: FloatingActionButton( + backgroundColor: primaryColor, + heroTag: "btn2", + onPressed: () async { + if (poSubmission.poLines + .fold(0, (p, e) => p + e.balanceQty) == + 0) { + showMsgDialog(context, "Error", "Zero PO balance qty"); + return; + } + + final bool successful = await Navigator.push( + context, MaterialPageRoute(builder: (context) => DOForm())); + if (successful != null && successful) _load(); + }, + child: Icon(Icons.add), + ), + body: Container( + padding: EdgeInsets.only(top: 5), + child: ListView( + children: [ + Container( + padding: EdgeInsets.only(left: 10, right: 10), + child: Card( + elevation: 23, + child: Column( + children: [ + poDateBox, + poNumberBox, + poStatusBox, + Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 15, + columns: [ + MyDataColumn( + label: LocalText(context, "po.product"), + ), + MyDataColumn( + label: LocalText(context, "do.po_qty"), + ), + MyDataColumn( + label: + LocalText(context, "do.po_balance_qty"), + ), + ], + rows: + getPoProductRow(widget.poSubmission.poLines), + ), + ), + ), + SizedBox( + height: 15, + ) + ], + ), + ), + ), + Column( + children: getDos(poSubmission.dos), + ), + SizedBox( + height: 10, + ) + ], + ), + ), + )); + } + + List getPoProductRow(List poLines) { + return poLines.map((p) { + return MyDataRow( + cells: [ + MyDataCell( + new Text( + p.productName, + style: textStyle, + ), + ), + MyDataCell( + new Text(p.qty.toString(), style: textStyle), + ), + MyDataCell( + new Text(p.balanceQty.toString(), style: textStyle), + ), + ], + ); + }).toList(); + } + + List getDos(List dos) { + return dos.map((d) { + return Container( + padding: EdgeInsets.only(left: 10, right: 10), + child: Card( + child: Theme( + data: ThemeData(accentColor: Colors.grey), + child: ExpansionTile( + onExpansionChanged: (e) => _onExpend(context, e, d), + title: ListTile( + title: Text(dateFormatter.format(d.deliveryDate)), + subtitle: Text(d.doNumber), + ), + children: [ + Container( + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: Container( + padding: EdgeInsets.only(right: 20), + child: InkWell( + child: Icon(Icons.edit), + onTap: () async { + final bool successful = + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DOForm( + doSubmission: d, + ))); + if (successful != null && successful) _load(); + }, + )), + ), + doDateBox(context, d), + doNumberBox(context, d), + doStatusBox(context, d), + driverBox(context, d), + carNoBox(context, d), + driverLicence(context, d), + Container( + alignment: AlignmentDirectional.centerStart, + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 15, + columns: [ + MyDataColumn( + label: LocalText(context, "do.product"), + ), + MyDataColumn( + label: LocalText(context, "do.do_qty"), + ), + ], + rows: getdoProductRow(d.doLines), + ), + ), + ), + SizedBox( + height: 15, + ) + ], + ), + ), + ], + ), + ), + ), + ); + }).toList(); + } + + List getdoProductRow(List doLines) { + return doLines.map((p) { + return MyDataRow( + cells: [ + MyDataCell( + new Text( + p.productName, + style: textStyle, + ), + ), + MyDataCell( + new Text(p.qty.toString(), style: textStyle), + ), + ], + ); + }).toList(); + } + + _onExpend(BuildContext context, expended, DOSubmission doSub) async { + if (!expended) return; + POSubmissionModel poModel = Provider.of(context); + var _doSub = await poModel.loadDOLines(doSub); + setState(() { + doSub = _doSub; + }); + } +} diff --git a/lib/pages/do/do_files.dart b/lib/pages/do/do_files.dart new file mode 100644 index 0000000..70bbdaa --- /dev/null +++ b/lib/pages/do/do_files.dart @@ -0,0 +1,26 @@ +import 'dart:io'; + +class DOFiles { + File doPaymentFile, storageChargeFile, licenseFile; + bool doFileChanged = false, + storageFileChanged = false, + licenseFileChanged = false; + + set setDoPaymentFile(File file) { + doPaymentFile = file; + doFileChanged = true; + } + + set setStorageChargeFile(File file) { + storageChargeFile = file; + storageFileChanged = true; + } + + set setlicenseFile(File file) { + licenseFile = file; + licenseFileChanged = true; + } + + bool get anyChanged => + doFileChanged || storageFileChanged || licenseFileChanged; +} diff --git a/lib/pages/do/do_list.dart b/lib/pages/do/do_list.dart new file mode 100644 index 0000000..b18f6d0 --- /dev/null +++ b/lib/pages/do/do_list.dart @@ -0,0 +1,286 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/do_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/do/do_creation_form.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/popupmenu.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'do_approve.dart'; + +class DOList extends StatefulWidget { + @override + _DOListState createState() => _DOListState(); +} + +class _DOListState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy'); + final double dotSize = 10.0; + DateTime _selectedDate = DateTime.now(); + String status; + int _selectedIndex = 0; + int _dateIndex = 0; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + + var doModel = Provider.of(context, listen: false); + _selectedIndex = doModel.popupMenu.index; + _dateIndex = doModel.dateIndex; + _selectedDate = doModel.selectedDate; + } + + @override + void dispose() { + super.dispose(); + } + + Future _selectDate(BuildContext context) async { + var doModel = Provider.of(context); + + final DateTime picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101), + builder: (BuildContext context, Widget child) { + return Theme( + data: ThemeData.light().copyWith( + primaryColor: primaryColor, //Head background + accentColor: secondaryColor, //selection color + dialogBackgroundColor: Colors.white, //Background color + ), + child: child, + ); + }, + ); + + if (picked != null) { + var pickedDate = new DateTime(picked.year, picked.month, picked.day); + var currentDate = new DateTime( + DateTime.now().year, DateTime.now().month, DateTime.now().day); + this._dateIndex = pickedDate == currentDate ? 0 : 1; + setState(() { + _selectedDate = picked; + doModel.filterData(status, _selectedDate, _selectedIndex, _dateIndex); + }); + } + } + + @override + Widget build(BuildContext context) { + var doModel = Provider.of(context); + MainModel mainModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + var languageModle = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text("do.title"), + style: languageModle.isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode'), + ), + actions: [ + InkWell( + child: Container( + padding: EdgeInsets.only(top: 15), + child: Stack( + children: [ + Image.asset( + "assets/date_filter.png", + color: Colors.white, + width: 25, + ), + _dateIndex == 0 + ? Container() + : Positioned( + bottom: 15, + right: 10, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + ], + ), + ), + onTap: () => _selectDate(context), + ), + PopupMenuButton( + elevation: 3.2, + onSelected: (selected) { + setState(() { + _selectedIndex = selected.index; + }); + if (selected.status == 'All') { + status = null; + } else { + status = selected.status; + } + doModel.filterData( + status, _selectedDate, _selectedIndex, _dateIndex); + }, + icon: Container( + width: 30, + height: 30, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Icon( + Icons.filter_list, + color: primaryColor, + ), + _selectedIndex != 0 + ? Positioned( + bottom: 0, + right: 0, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + : Container() + ], + )), + itemBuilder: (BuildContext context) { + return statusMenu.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Row( + children: [ + Text(choice.status), + SizedBox( + width: 10, + ), + _selectedIndex != null && + _selectedIndex == choice.index + ? Icon( + Icons.check, + color: Colors.grey, + ) + : Container(), + ], + ), + ); + }).toList(); + }), + ], + ), + floatingActionButton: mainModel.isBuyer() + ? FloatingActionButton( + backgroundColor: primaryColor, + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => DOForm()), + ), + child: Icon(Icons.add), + ) + : null, + body: new ListView.builder( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: doModel.dos.length, + itemBuilder: (BuildContext context, int index) { + return Card( + elevation: 10, + color: Colors.white, + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DOApproval( + doSubmission: doModel.dos[index])), + ); + }, + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Padding( + padding: EdgeInsets.all(5.0), + child: Image.asset( + "assets/do.png", + width: 40, + height: 40, + color: primaryColor, + ), + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Text( + doModel.dos[index].doNumber, + style: new TextStyle( + fontSize: 12.0, color: Colors.black), + ), + new Text( + doModel.dos[index].deliveryDate == null + ? "" + : dateFormatter.format( + doModel.dos[index].deliveryDate), + style: new TextStyle( + fontSize: 14.0, color: Colors.grey), + ), + !isBuyer + ? new Text( + doModel.dos[index].userName, + style: new TextStyle( + fontSize: 12.0, + color: Colors.grey), + ) + : Container() + ], + ), + ), + Container( + padding: EdgeInsets.only(right: 15), + child: getStatus(doModel.dos[index].status), + ), + ], + ), + ), + ), + ), + ], + ), + ); + }), + ), + ); + } +} diff --git a/lib/pages/do/do_product_item.dart b/lib/pages/do/do_product_item.dart new file mode 100644 index 0000000..ebb7e92 --- /dev/null +++ b/lib/pages/do/do_product_item.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +class DOProductItem extends StatefulWidget { + final DOLine doLine; + const DOProductItem({Key key, this.doLine}) : super(key: key); + @override + _DOProductItemState createState() => _DOProductItemState(); +} + +class _DOProductItemState extends State { + final _formKey = GlobalKey(); + TextEditingController _doQty = new TextEditingController(); + DOLine doLine; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + if (widget.doLine != null) { + doLine = widget.doLine; + _doQty.text = widget.doLine.qty.toString(); + } + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: AlertDialog( + title: Center( + child: Text( + AppTranslations.of(context).text("do_qty"), + style: TextStyle( + color: primaryColor, fontWeight: FontWeight.bold, fontSize: 20), + )), + content: Form( + key: _formKey, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new Expanded( + child: new TextFormField( + keyboardType: TextInputType.number, + autofocus: true, + controller: _doQty, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + cursorColor: primaryColor, + decoration: new InputDecoration( + fillColor: primaryColor, + icon: Icon( + FontAwesomeIcons.sortNumericUpAlt, + color: primaryColor, + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("do.form.volume"); + } + return null; + }, + )) + ], + ), + ), + actions: [ + FlatButton( + child: LocalText( + context, + 'do.cancel', + color: secondaryColor, + ), + onPressed: () { + _doQty.clear(); + Navigator.of(context).pop(); + }), + FlatButton( + color: primaryColor, + child: LocalText( + context, + 'do.enter', + color: Colors.white, + fontWeight: FontWeight.bold, + ), + onPressed: () async { + if (!_formKey.currentState.validate()) return; + _save(); + }) + ], + ), + ); + } + + _save() { + setState(() { + _isLoading = true; + }); + try { + var qty = int.parse(_doQty.text); + if (qty < 0) + throw Exception("invalid number, must be zero or greater than zero"); + // if (qty > doLine.poLine.balanceQty) + // throw Exception( + // "invalid number, must be less than or equal to PO balance qty"); + this.doLine.qty = qty; + Navigator.pop(context, this.doLine); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _delete() { + this.doLine.action = "delete"; + this.doLine.qty = 0; + Navigator.pop(context, this.doLine); + } +} diff --git a/lib/pages/do/do_storage_item.dart b/lib/pages/do/do_storage_item.dart new file mode 100644 index 0000000..5fc50d4 --- /dev/null +++ b/lib/pages/do/do_storage_item.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/storage_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/util.dart'; +import 'package:fcs/vo/do.dart'; +import 'package:fcs/vo/storage.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +typedef OnSave = void Function(String storageID, String storageName); + +class DOStorageItem extends StatefulWidget { + final DOLine doLine; + final OnSave onSave; + const DOStorageItem({Key key, this.doLine, this.onSave}) : super(key: key); + @override + _DOStorageItemState createState() => _DOStorageItemState(); +} + +class _DOStorageItemState extends State { + final _formKey = GlobalKey(); + bool _isLoading = false; + String currentStorageID; + TextEditingController _product = new TextEditingController(); + TextEditingController _quantity = new TextEditingController(); + DOLine doLine = DOLine(); + + @override + void initState() { + super.initState(); + this.doLine = widget.doLine; + + if (doLine.storageID != null && doLine.storageID.isNotEmpty) { + this.currentStorageID = doLine.storageID; + } + _product.text = this.doLine.productName; + _quantity.text = this.doLine.qty.toString(); + } + + Widget showStorages(BuildContext context, StorageModel storageModel) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + "assets/inventory.png", + width: 30, + height: 30, + color: primaryColor, + ), + SizedBox( + width: 20, + ), + new Flexible( + child: Container( + width: 170.0, + child: DropdownButton( + value: currentStorageID, + isExpanded: true, + hint: Text( + 'Select Storage', + style: labelStyle, + ), + onChanged: changedStorage, + items: storageModel + .getStorage(doLine.productID) + .map>((Storage storage) { + return new DropdownMenuItem( + value: storage.id, + child: new Text(storage.name, style: textStyle), + ); + }).toList(), + ), + ), + ), + ], + ), + ); + } + + void changedStorage(selected) { + setState(() { + currentStorageID = selected; + }); + } + + Widget showStorgeTable(BuildContext context, StorageModel storageModel) { + return Container( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 100, + columns: [ + MyDataColumn( + label: LocalText(context, "storge"), + ), + MyDataColumn( + label: LocalText(context, "storage.product.qty"), + ), + ], + rows: getProductRow(storageModel.getStorage(doLine.productID)), + ), + ), + ); + } + + List getProductRow(List storages) { + return storages.map((s) { + return MyDataRow( + onSelectChanged: (bool selected) { + setState(() { + currentStorageID = s.id; + }); + }, + cells: [ + MyDataCell( + new Text(s.name, style: textStyle), + ), + MyDataCell( + NumberCell(s.products + .firstWhere((p) => p.id == doLine.productID) + .quantity), + ), + ], + ); + }).toList(); + } + + @override + Widget build(BuildContext context) { + var storgeModel = Provider.of(context); + + final productbox = Container( + padding: EdgeInsets.only(top: 10), + child: Row( + children: [ + LocalText(context, "do.product"), + SizedBox( + width: 20, + ), + Text(_product.text, style: textStyle) + ], + )); + + final quantitybox = Container( + padding: EdgeInsets.only(top: 15), + child: Row( + children: [ + LocalText(context, "do.quantity"), + SizedBox( + width: 20, + ), + Text(formatNumber(this.doLine.qty), style: textStyle) + ], + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text("DO"), + actions: [ + IconButton( + icon: Icon(Icons.save), + onPressed: () { + _save(); + }, + ) + ], + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0, top: 10), + children: [ + productbox, + quantitybox, + showStorages(context, storgeModel), + showStorgeTable(context, storgeModel) + ], + ), + ), + ], + ), + )), + ); + } + + _save() { + this.doLine.storageID = currentStorageID; + var storageName = + Provider.of(context).getStorageName(currentStorageID); + this.doLine.storageName = storageName; + if (widget.onSave != null) + widget.onSave(this.doLine.storageID, storageName); + Navigator.pop(context, this.doLine); + } +} diff --git a/lib/pages/do/photo_page.dart b/lib/pages/do/photo_page.dart new file mode 100644 index 0000000..24b4807 --- /dev/null +++ b/lib/pages/do/photo_page.dart @@ -0,0 +1,165 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; + +import '../util.dart'; + +class PhotoPage extends StatefulWidget { + PhotoPage({Key key}) : super(key: key); + + @override + _PhotoPageState createState() => _PhotoPageState(); +} + +class _PhotoPageState extends State { + File receiptImageFile; + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + + final receiptImageBox = Padding( + padding: EdgeInsets.symmetric(vertical: 5.0), + child: Container( + color: Colors.grey[400], + child: Column( + children: [ + Container( + padding: EdgeInsets.only(top: 15), + child: Text( + AppTranslations.of(context).text('do.receipt'), + style: + languageModel.isEng ? photoLabelStyle : photoLabelStyleMM, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ButtonTheme.bar( + child: new ButtonBar( + alignment: MainAxisAlignment.start, + children: [ + new FlatButton( + child: Icon( + Icons.add_photo_alternate, + size: 60.0, + color: Colors.black, + ), + onPressed: () { + pickImageFromGallery(ImageSource.gallery); + }, + ), + ], + )), + ButtonTheme.bar( + child: new ButtonBar( + alignment: MainAxisAlignment.start, + children: [ + new FlatButton( + child: Icon( + Icons.add_a_photo, + size: 50.0, + color: Colors.black, + ), + onPressed: () { + pickImageFromCamera(ImageSource.camera); + }, + ), + ], + )), + ], + ), + receiptShowImage(), + SizedBox( + height: 10, + ) + ], + ), + ), + ); + + return Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("do.receipt.title"), + style: Provider.of(context).isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode')), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + if (receiptImageFile == null) { + showMsgDialog( + context, "Error", "Please insert delivery receipt file"); + return; + } + Navigator.pop(context, receiptImageFile); + }, + ) + ], + ), + body: Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0, top: 20), + children: [ + receiptImageBox, + ], + ), + ), + SizedBox( + height: 20, + ) + ], + ), + ); + } + + Widget receiptShowImage() { + return Container( + child: receiptImageFile == null ? initialImage() : receiptEnableImage(), + ); + } + + Widget initialImage() { + var languageModel = Provider.of(context); + + return Center( + child: Text( + AppTranslations.of(context).text('do.no.photo'), + style: languageModel.isEng ? photoLabelStyle : photoLabelStyleMM, + ), + ); + } + + Widget receiptEnableImage() { + return Center( + child: Image.file( + receiptImageFile, + ), + ); + } + + pickImageFromGallery(ImageSource source) async { + var tempImage = await ImagePicker.pickImage( + source: source, imageQuality: 80, maxWidth: 300); + setState(() { + receiptImageFile = tempImage; + }); + } + + pickImageFromCamera(ImageSource source) async { + var tempImage = await ImagePicker.pickImage( + source: source, imageQuality: 80, maxWidth: 300); + setState(() { + receiptImageFile = tempImage; + }); + } +} diff --git a/lib/pages/do/po_selection.dart b/lib/pages/do/po_selection.dart new file mode 100644 index 0000000..12d7e70 --- /dev/null +++ b/lib/pages/do/po_selection.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/local_text.dart'; + +class POSelection extends StatefulWidget { + final List pos; + final List selectedPOs; + + POSelection({Key key, this.pos, this.selectedPOs}) : super(key: key); + + @override + _POSelectionState createState() => new _POSelectionState(); + + static Future showPOSelection(BuildContext context, + List pos, List selectedPOs, + {ok(List pos)}) async { + List _selectedPOs = []; + selectedPOs.forEach((i) => _selectedPOs.add(i)); + + final poselection = POSelection( + pos: pos, + selectedPOs: _selectedPOs, + ); + + await showDialog( + context: context, + builder: (_) { + return AlertDialog( + title: Center( + child: LocalText(context, "po.title"), + ), + content: Container( + width: double.maxFinite, + child: ListView( + shrinkWrap: true, + children: [ + poselection, + Container( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlatButton( + child: LocalText(context, "Cancel"), + onPressed: () { + Navigator.of(context).pop(); + }), + FlatButton( + color: primaryColor, + child: LocalText(context, "Ok"), + onPressed: () async { + if (ok != null) ok(_selectedPOs); + Navigator.of(context).pop(); + }) + ], + ), + ), + ], + ), + ), + ); + }); + } +} + +class _POSelectionState extends State { + List pos; + + @override + void initState() { + super.initState(); + pos = widget.pos; + pos.sort((p1, p2) => p1.poNumber.compareTo(p2.poNumber)); + } + + @override + Widget build(BuildContext context) { + var width = MediaQuery.of(context).size.width * 0.8; + var height = MediaQuery.of(context).size.height * 0.5; + + return Column( + children: [ + FlatButton( + child: Text("Select All"), + onPressed: () { + setState(() { + widget.selectedPOs.clear(); + widget.selectedPOs.addAll(pos); + }); + }), + Container( + width: width, + height: height, + child: Column( + children: pos.asMap().entries.map((p) { + return InkWell( + onTap: () { + setState(() { + if (widget.selectedPOs.contains(p.value)) { + widget.selectedPOs.remove(p.value); + } else { + widget.selectedPOs.add(p.value); + } + }); + }, + child: Row( + children: [ + Checkbox( + onChanged: (v) => {_update(p.key)}, + value: widget.selectedPOs.contains(p.value), + ), + Text(p.value.poNumber), + ], + ), + ); + }).toList(), + ), + // child: ListView.builder( + // itemCount: pos.length, + // scrollDirection: Axis.vertical, + // itemBuilder: (BuildContext ctxt, int index) { + // return InkWell( + // onTap: () { + // setState(() { + // if (widget.selectedPOs.contains(pos[index])) { + // widget.selectedPOs.remove(pos[index]); + // } else { + // widget.selectedPOs.add(pos[index]); + // } + // }); + // }, + // child: Row( + // children: [ + // Checkbox( + // onChanged: (v) => {_update(index)}, + // value: widget.selectedPOs.contains(pos[index]), + // ), + // Text(pos[index].poNumber), + // ], + // ), + // ); + // }), + ), + ], + ); + } + + _update(int index) { + setState(() { + if (widget.selectedPOs.contains(pos[index])) { + widget.selectedPOs.remove(pos[index]); + } else { + widget.selectedPOs.add(pos[index]); + } + }); + } +} diff --git a/lib/pages/document_log_page.dart b/lib/pages/document_log_page.dart new file mode 100644 index 0000000..1d4faea --- /dev/null +++ b/lib/pages/document_log_page.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/log_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/document_log.dart'; +import 'package:fcs/vo/role.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; + +class DocumentLogPage extends StatefulWidget { + final String docID; + + const DocumentLogPage({Key key, this.docID}) : super(key: key); + @override + _DocumentLogPageState createState() => _DocumentLogPageState(); +} + +class _DocumentLogPageState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy\nhh:mm:ss a'); + + @override + void initState() { + super.initState(); + if (widget.docID != null) { + Provider.of(context, listen: false).loadDocLogs(widget.docID); + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var logModel = Provider.of(context); + var userModel = Provider.of(context); + return Scaffold( + appBar: AppBar( + title: LocalText(context, 'document.log.title', + fontSize: 20, color: Colors.white), + backgroundColor: primaryColor, + ), + body: Container( + padding: EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 40, + columns: [ + MyDataColumn(label: LocalText(context, "document.date")), + MyDataColumn(label: LocalText(context, "document.by")), + MyDataColumn(label: LocalText(context, "document.desc")), + ], + rows: getProductRow(logModel.docLogs, userModel.privileges), + ), + ), + ), + ); + } + + List getProductRow( + List docLogs, List privileges) { + return docLogs.map((d) { + return MyDataRow( + cells: [ + MyDataCell( + new Text(dateFormatter.format(d.date), style: textStyle), + ), + MyDataCell( + new Text( + d.actionerName == null ? '' : d.actionerName, + style: textStyle, + ), + ), + MyDataCell( + new Text(d.getDesc(privileges) == null ? '' : d.getDesc(privileges), + style: textStyle), + ), + ], + ); + }).toList(); + } +} diff --git a/lib/pages/email_page.dart b/lib/pages/email_page.dart new file mode 100644 index 0000000..2a687ae --- /dev/null +++ b/lib/pages/email_page.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/shared_pref.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'confirm_email.dart'; +import 'util.dart'; + +class EmailPage extends StatefulWidget { + final User user; + EmailPage({this.user}); + @override + _EmailPageState createState() => _EmailPageState(); +} + +class _EmailPageState extends State { + final TextEditingController _email = new TextEditingController(); + bool _isLoading = false; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + if (widget.user != null) { + _email.text = widget.user.email; + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + var languageModel = Provider.of(context); + final emailInput = TextFormField( + controller: _email, + autofocus: false, + decoration: new InputDecoration( + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + labelText: AppTranslations.of(context).text("login.email"), + labelStyle: labelStyle, + icon: Icon( + Icons.email, + color: primaryColor, + )), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.email.empty"); + } + return null; + }, + ); + + final enterButton = Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: RaisedButton( + onPressed: () => _add(mainModel), + padding: EdgeInsets.all(10), + color: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(7), + ), + child: Text(AppTranslations.of(context).text("login.email.add"), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ); + + final skipButton = Container( + child: InkWell( + onTap: () { + SharedPref.saveSkippedRecoverEmail(true); + Navigator.pushNamedAndRemoveUntil(context, "/home", (r) => false); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + AppTranslations.of(context).text("login.email.skip"), + style: languageModel.isEng + ? TextStyle(fontSize: 18, fontFamily: 'WorkSansMedium') + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode'), + ), + Container( + padding: EdgeInsets.only(top: 4), + child: Icon( + Icons.skip_next, + size: 30, + )), + ], + )), + ); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: LocalText( + context, + 'login.email.title', + fontSize: 20, + color: Colors.white, + ), + backgroundColor: primaryColor, + ), + body: Center( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + Form(key: _formKey, child: emailInput), + SizedBox(height: 8.0), + enterButton, + widget.user != null ? Container() : skipButton + ], + ), + ), + ), + ); + } + + Future _add(MainModel mainModel) async { + if (!_formKey.currentState.validate()) { + return; + } + setState(() { + _isLoading = true; + }); + try { + UserModel userModel = Provider.of(context); + await userModel.addEmail(mainModel.user.phoneNumber, _email.text); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ConfirmEmail( + id: mainModel.user.phoneNumber, + email: _email.text, + ))); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/employee_editor.dart b/lib/pages/employee_editor.dart new file mode 100644 index 0000000..1c78b89 --- /dev/null +++ b/lib/pages/employee_editor.dart @@ -0,0 +1,370 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/employee_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/log_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/role.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; +import 'document_log_page.dart'; +import 'util.dart'; + +typedef void FindCallBack(); + +class EmployeeEditor extends StatefulWidget { + final User employee; + const EmployeeEditor({this.employee}); + @override + _EmployeeEditorState createState() => _EmployeeEditorState(); +} + +class _EmployeeEditorState extends State { + TextEditingController _name = new TextEditingController(); + TextEditingController _phone = new TextEditingController(); + TextEditingController _phoneInput = new TextEditingController(); + + final _formKey = GlobalKey(); + bool _isLoading = false; + String currentBizId; + bool isSend = false; + User user; + User selectedUser; + List privileges = []; + + @override + void initState() { + super.initState(); + privileges = Provider.of(context, listen: false).getPrivileges; + if (widget.employee != null) { + _name.text = widget.employee.name; + _phone.text = widget.employee.phone; + privileges.forEach((p) => widget.employee.privilegeIds.contains(p.id) + ? p.isChecked = true + : p.isChecked = false); + } else { + privileges.forEach((p) => p.isChecked = false); + } + } + + Widget showprivilegeList(BuildContext context, UserModel userModel) { + return Container( + width: 300, + height: 300, + child: ListView.builder( + itemCount: privileges.length, + itemBuilder: (BuildContext context, int index) { + return new ListTile( + title: new Row( + children: [ + new Checkbox( + value: privileges[index].isChecked == null + ? false + : privileges[index].isChecked, + activeColor: primaryColor, + onChanged: (bool value) { + setState(() { + privileges[index].isChecked = value; + }); + }), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Text( + userModel.getPrivileges[index].name, + style: TextStyle( + fontSize: 15.0, + ), + ), + Container( + width: MediaQuery.of(context).size.width * 0.5, + child: new Text( + userModel.getPrivileges[index].desc, + style: + TextStyle(fontSize: 12.0, color: Colors.grey[600]), + ), + ), + ], + ), + ], + )); + }), + ); + } + + Widget phoneInputbox(BuildContext context, FindCallBack findCallBack) { + var languageModel = Provider.of(context); + return Container( + padding: EdgeInsets.only(top: 10), + child: Stack( + alignment: const Alignment(1.2, 1.0), + children: [ + TextFormField( + controller: _phoneInput, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text('employee.phone'), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + Icons.phone, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return "Please enter phone number"; + } + return null; + }, + ), + new FlatButton( + onPressed: () { + if (!_formKey.currentState.validate()) return; + this.isSend = true; + findCallBack(); + }, + child: new Icon( + Icons.search, + size: 25, + )) + ], + )); + } + + @override + Widget build(BuildContext context) { + var userModel = Provider.of(context); + MainModel mainModel = Provider.of(context); + + final namebox = TextFormField( + controller: _name, + autofocus: false, + readOnly: true, + cursorColor: primaryColor, + decoration: new InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Icon( + Icons.person, + color: primaryColor, + ), + ), + ); + + final displayPhoneNo = TextFormField( + controller: _phone, + autofocus: false, + readOnly: true, + cursorColor: primaryColor, + decoration: new InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Icon( + Icons.phone, + color: primaryColor, + ), + ), + ); + + var phoneNumberBox = Row( + children: [ + Expanded(child: displayPhoneNo), + Expanded( + child: InkWell( + onTap: () => call(context, _phone.text), + child: Icon( + Icons.open_in_new, + color: Colors.grey, + size: 15, + ), + ), + ), + ], + ); + + final saveButton = Container( + padding: EdgeInsets.only( + left: 24.0, + right: 24.0, + ), + child: Container( + height: 45.0, + color: primaryColor, + child: ButtonTheme( + minWidth: 900.0, + height: 100.0, + child: FlatButton( + onPressed: _save, + child: LocalText( + context, + 'employee.save', + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + final addButton = Container( + padding: EdgeInsets.only( + left: 24.0, + right: 24.0, + ), + child: Container( + height: 45.0, + color: primaryColor, + child: ButtonTheme( + minWidth: 900.0, + height: 100.0, + child: FlatButton( + onPressed: () { + if (!_formKey.currentState.validate()) return; + _add(context); + }, + child: LocalText( + context, + 'employee.add', + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + "employee.item.title", + fontSize: 20, + color: Colors.white, + ), + actions: [ + widget.employee == null || !mainModel.showHistoryBtn() + ? Container() + : IconButton( + icon: Icon(Icons.history), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DocumentLogPage( + docID: widget.employee.docID)), + ); + }, + ), + ], + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + widget.employee == null + ? phoneInputbox(context, () => _findUser(context)) + : phoneNumberBox, + widget.employee == null + ? this.isSend ? namebox : Container() + : namebox, + showprivilegeList(context, userModel) + ], + ), + ), + widget.employee == null ? addButton : saveButton, + SizedBox( + height: 20, + ) + ], + ), + )), + ); + } + + _add(BuildContext context) async { + if (selectedUser == null) return; + setState(() { + _isLoading = true; + }); + var employeeModel = Provider.of(context); + try { + await employeeModel.updatePrivileges( + this.selectedUser.docID, privilegesIDs()); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + List privilegesIDs() { + return this.privileges.where((p) => p.isChecked).map((p) => p.id).toList(); + } + + _save() async { + setState(() { + _isLoading = true; + }); + if (widget.employee == null) return; + var employeeModel = Provider.of(context); + try { + await employeeModel.updatePrivileges( + widget.employee.docID, privilegesIDs()); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _findUser(BuildContext context) async { + var userModel = Provider.of(context); + setState(() { + _isLoading = true; + }); + try { + selectedUser = await userModel.findUser(_phoneInput.text); + setState(() { + isSend = true; + _name.text = selectedUser.name; + if (selectedUser.privilegeIds != null) { + privileges.forEach((p) => selectedUser.privilegeIds.contains(p.id) + ? p.isChecked = true + : p.isChecked = false); + } + }); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/employee_list.dart b/lib/pages/employee_list.dart new file mode 100644 index 0000000..4806b3f --- /dev/null +++ b/lib/pages/employee_list.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import 'package:progress/progress.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/employee_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'employee_editor.dart'; + +class EmployeeList extends StatefulWidget { + @override + _EmployeeListState createState() => _EmployeeListState(); +} + +class _EmployeeListState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy - hh:mm:ss a'); + final double dotSize = 15.0; + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + var employeeModel = Provider.of(context); + var languageModle = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text("employee.title"), + style: languageModle.isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode'), + ), + ), + floatingActionButton: FloatingActionButton( + backgroundColor: primaryColor, + child: Icon(Icons.add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => EmployeeEditor()), + ); + }, + ), + body: new ListView.builder( + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: employeeModel.employees.length, + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmployeeEditor( + employee: employeeModel.employees[index], + )), + ); + }, + child: Card( + elevation: 10, + color: Colors.white, + child: Row( + children: [ + new Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Image.asset( + "assets/employee.png", + width: 40, + height: 40, + color: primaryColor, + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + employeeModel.employees[index].name == null + ? "" + : employeeModel.employees[index].name, + style: new TextStyle( + fontSize: 17.0, color: Colors.black), + ), + Text( + employeeModel.employees[index].phone == + null + ? "" + : employeeModel + .employees[index].phone, + style: new TextStyle( + fontSize: 14.0, color: Colors.grey), + ), + employeeModel.employees[index].device == null + ? Text("No login", + style: TextStyle(color: Colors.red)) + : Text("last active", + style: TextStyle(color: Colors.green)), + Text( + "${employeeModel.employees[index].device == null ? "" : employeeModel.employees[index].device}", + style: TextStyle(fontSize: 11)), + Text( + "${employeeModel.employees[index].lastActiveTime == null ? "" : dateFormatter.format(employeeModel.employees[index].lastActiveTime)}") + ], + ), + ], + ), + ), + ], + ), + ), + ); + }), + ), + ); + } +} diff --git a/lib/pages/forget_password.dart b/lib/pages/forget_password.dart new file mode 100644 index 0000000..784aae2 --- /dev/null +++ b/lib/pages/forget_password.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:progress/progress.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/pages/reset_password.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'util.dart'; + +class ForgetPassword extends StatefulWidget { + final phoneNumber; + + const ForgetPassword({Key key, this.phoneNumber}) : super(key: key); + @override + _ForgetPasswordState createState() => _ForgetPasswordState(); +} + +class _ForgetPasswordState extends State { + final TextEditingController _email = new TextEditingController(); + bool _isLoading = false; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _email.text = widget.phoneNumber; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final emailInput = TextFormField( + controller: _email, + autofocus: false, + decoration: new InputDecoration( + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + labelText: AppTranslations.of(context).text("forget.email"), + labelStyle: labelStyle, + icon: Icon( + Icons.email, + color: primaryColor, + )), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("forget.email.empty"); + } + return null; + }, + ); + + final enterButton = Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: RaisedButton( + onPressed: () => _forgetPassword(), + padding: EdgeInsets.all(10), + color: primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(7), + ), + child: Text(AppTranslations.of(context).text("forget.enter"), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: LocalText( + context, + 'forget.password', + fontSize: 20, + color: Colors.white, + ), + backgroundColor: primaryColor, + ), + body: Center( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + Form(key: _formKey, child: emailInput), + SizedBox(height: 8.0), + enterButton, + ], + ), + ), + ), + ); + } + + Future _forgetPassword() async { + var phoneNumber = _email.text; + if (phoneNumber.isEmpty) { + showMsgDialog(context, "Error", "Please input email(or)phone number"); + return; + } + setState(() { + _isLoading = true; + }); + try { + UserModel userModel = Provider.of(context); + await userModel.forgetPassword(phoneNumber); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ResetPasswordPage(phoneNumber))); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/help.dart b/lib/pages/help.dart new file mode 100644 index 0000000..1b361c0 --- /dev/null +++ b/lib/pages/help.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:archive/archive.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:http_server/http_server.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/vo/setting.dart'; +import 'package:fcs/widget/progress.dart'; + +typedef void ProfileCallback(); + +class Help extends StatefulWidget { + + const Help({Key key}) : super(key: key); + @override + _HelpState createState() => _HelpState(); +} + +class _HelpState extends State { + final log = Logger('Help'); + + bool isLoading = false; + HttpServer httpServer; + // WebViewController _controller; + String url = ""; + + @override + void initState() { + super.initState(); + MainModel mainModel = Provider.of(context, listen: false); + var isBuyer = mainModel.isBuyer(); + this.url = "http://localhost:7777/web/index.html" + + (isBuyer ? "?is_buyer=true" : "?is_buyer=false"); + _run(mainModel.setting); + } + + void _run(Setting setting) async { + await _download(setting); + final directory = await getApplicationDocumentsDirectory(); + var staticFiles = new VirtualDirectory('${directory.path}') + ..allowDirectoryListing = true; + + HttpServer.bind('0.0.0.0', 7777).then((server) async { + httpServer = server; + log.info('Server running'); + server.listen(staticFiles.serveRequest); + // _controller.loadUrl(url); + log.info("locad url:$url"); + }, onError: (e) { + log.warning("Error===>:$e"); + }); + } + + Future dispose() async { + super.dispose(); + if (httpServer != null) await httpServer.close(force: true); + } + + Future _download(Setting setting) async { + final directory = (await getApplicationDocumentsDirectory()).path; + + var file = File('$directory/${setting.helpFileName()}'); + if (await file.exists()) { + return; + } + + String url = setting.helpURL; + var req = await http.Client().get(Uri.parse(url)); + File zippedFile = await file.writeAsBytes(req.bodyBytes); + + File prev = File('$directory/web'); + if (await prev.exists()) { + await prev.delete(recursive: true); + } + + var bytes = zippedFile.readAsBytesSync(); + var archive = ZipDecoder().decodeBytes(bytes); + for (var file in archive) { + var fileName = '$directory/web/${file.name}'; + if (file.isFile) { + var outFile = File(fileName); + outFile = await outFile.create(recursive: true); + await outFile.writeAsBytes(file.content); + } + } + } + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: isLoading, + child: Scaffold( + appBar: AppBar( + iconTheme: IconThemeData( + color: Colors.black, //change your color here + ), + centerTitle: true, + backgroundColor: Colors.white, + title: ClipRRect( + child: Image.asset("assets/logo.png", height: 40), + borderRadius: new BorderRadius.circular(15.0), + )), + body:Text("abc"), + // body: WebView( + // initialUrl: url, + // javascriptMode: JavascriptMode.unrestricted, + // onWebViewCreated: (WebViewController webViewController) { + // _controller = webViewController; + // }, + // ), + ), + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart new file mode 100644 index 0000000..05c0c06 --- /dev/null +++ b/lib/pages/home_page.dart @@ -0,0 +1,469 @@ +import 'dart:async'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/charts/bar_chart.dart'; +import 'package:fcs/charts/delivery_do_line.dart'; +import 'package:fcs/charts/delivery_do_summary.dart'; +import 'package:fcs/charts/delivery_line.dart'; +import 'package:fcs/charts/delivery_summary.dart'; +import 'package:fcs/charts/do_line.dart'; +import 'package:fcs/charts/po_balance_chart.dart'; +import 'package:fcs/charts/po_line.dart'; +import 'package:fcs/charts/quota.dart'; +import 'package:fcs/charts/revenue_line.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/notification_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/pages/banks/banks.dart'; +import 'package:fcs/pages/buyer_list.dart'; +import 'package:fcs/pages/contact.dart'; +import 'package:fcs/pages/delivery/delivery_list.dart'; +import 'package:fcs/pages/manual/manual_page.dart'; +import 'package:fcs/pages/my_registeration_info.dart'; +import 'package:fcs/pages/notification_list.dart'; +import 'package:fcs/pages/pin_login_dialog.dart'; +import 'package:fcs/pages/settings.dart'; +import 'package:fcs/pages/term.dart'; +import 'package:fcs/pages/test_list.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/reports/report_list.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/badge.dart'; +import 'package:fcs/widget/banner.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/offline_redirect.dart'; +import 'package:fcs/vo/notification.dart' as Noti; + +import '../theme/theme.dart'; +import 'announcement_list.dart'; +import 'do/do_list.dart'; +import 'employee_list.dart'; +import 'my_registeration.dart'; +import 'pd/pd_list.dart'; +import 'po/po_submission_list.dart'; +import 'products_list.dart'; +import 'profile_page.dart'; +import 'storage/storage_list.dart'; +import 'user_list.dart'; + +final msgLog = Logger('backgroundMessageHandler'); + +class HomePage extends StatefulWidget { + @override + _HomePageState createState() => _HomePageState(); +} + +typedef BtnCallback(); + +class _HomePageState extends State { + final log = Logger('_HomePageState'); + + @override + void initState() { + super.initState(); + } + + void dispose() { + super.dispose(); + } + + static final List chartDropdownItems = [ + 'Last 7 days', + 'Last month', + 'Last three months' + ]; + String actualDropdown = chartDropdownItems[0]; + int actualChart = 0; + final numberFormatter = new NumberFormat("#,###"); + + String pin; + + @override + Widget build(BuildContext context) { + final helpBtn = _buildBtn("manual.title", + imgIcon: Image.asset( + "assets/manual.png", + width: 40, + height: 40, + color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => ManualPage())) + // btnCallback: () => Navigator.of(context) + // .push(MaterialPageRoute(builder: (_) => TestList())) + ); + final announcementBtn = _buildBtn("announcement.title", + icon: Icons.announcement, + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => AnnouncementList()))); + + final buyerBtn = _buildBtn("buyer.title", + imgIcon: Image.asset( + "assets/buyer.png", + width: 40, + height: 40, + color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => BuyerList()))); + + final reportBtn = _buildBtn("report.title", + imgIcon: Image.asset( + "assets/report.png", + width: 50, + height: 50, + // color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => ReportList()))); + + final myRegBtn = _buildBtn("myreg.title", + imgIcon: Image.asset( + "assets/reg.png", + width: 40, + height: 30, + color: primaryColor, + ), + btnCallback: () async {}); + + final posBtn = _buildBtn("po.title", + imgIcon: Image.asset( + "assets/pay.png", + width: 40, + height: 40, + color: primaryColor, + ), + btnCallback: () {}); + + final dosBtn = _buildBtn("do.title", + imgIcon: Image.asset( + "assets/do.png", + width: 40, + height: 40, + color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => DOList()))); + + final deliveryBtn = _buildBtn("delivery.title", + imgIcon: Image.asset( + "assets/truck.png", + width: 50, + height: 50, + color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => DeliveryList()))); + + final storageBtn = _buildBtn("storage.title", + imgIcon: Image.asset( + "assets/inventory.png", + width: 40, + height: 40, + color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => StorageList()))); + + final pdosBtn = _buildBtn("pd.title", + imgIcon: Image.asset( + "assets/pdo.png", + width: 40, + height: 40, + color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => PDList()))); + + final employeeBtn = _buildBtn("employee.title", + imgIcon: Image.asset( + "assets/employee.png", + width: 40, + height: 40, + color: primaryColor, + ), + btnCallback: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => EmployeeList()))); + + final termBtn = _buildBtn("term.title", + imgIcon: Image.asset( + "assets/term.png", + width: 40, + height: 30, + color: primaryColor, + ), btnCallback: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => Term())); + }); + + final userBtn = + _buildBtn("users.title", icon: Icons.group, btnCallback: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => UserList()), + ); + }); + + final settingsBtn = + _buildBtn("setting.title", icon: Icons.settings, btnCallback: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Settings()), + ); + }); + + final _bankAccountsBtn = _buildBtn("banks.title", + icon: FontAwesomeIcons.moneyCheck, btnCallback: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => BankAccounts()), + ); + }); + + + List widgets = [helpBtn]; + widgets.add(announcementBtn); + widgets.add(reportBtn); + widgets.add(termBtn); + widgets.add(_bankAccountsBtn); + + var revenueChart = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [RevenueLineChart()], + )); + + var productListBox = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocalText(context, 'products.prices', color: primaryColor), + Row( + children: [ + LocalText(context, 'products.gas', + color: Colors.black, + fontSize: 19, + fontWeight: FontWeight.w700), + Consumer(builder: (context, model, child) { + return Text(' ${model.products.length}', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w700, + fontSize: 19.0)); + }), + ], + ), + ], + ), + Material( + color: thirdColor, + borderRadius: BorderRadius.circular(10.0), + child: Center( + child: Padding( + padding: const EdgeInsets.all(5.0), + child: //Icon(Icons.timeline, color: Colors.white, size: 30.0), + Image.asset( + "assets/product.png", + width: 60, + height: 70, + color: Colors.white, + )))) + ]); + + var poqtyByProductChart = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [BarChart()], + )); + + var poChart = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [POLineChart()], + )); + + var doChart = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [DOLineChart()], + )); + + var deliveryChart = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [DeliveryBarChart()], + )); + + var deliveryDOChart = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [DODeliveryLineChart()], + )); + + var deliveryDoSummary = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [DeliveryDoSummaryChart()], + )); + var deliverySummary = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [DeliverySummary()], + )); + var poBalancebyBuyerChart = Padding( + padding: const EdgeInsets.all(10.0), + child: ListView( + shrinkWrap: true, + children: [POBalanceChart()], + )); + List chartWidgets = []; + + return OfflineRedirect( + child: FlavorBanner( + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: ClipRRect( + child: Image.asset("assets/logo.png", height: 40), + borderRadius: new BorderRadius.circular(15.0), + ), + actions: [ + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Contact()), + ); + }, + iconSize: 30, + icon: Icon(Icons.phone), + ), + IconButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Profile()), + ); + }, + iconSize: 30, + icon: Icon(Icons.account_circle), + ), + ]), + body: StaggeredGridView.count( + crossAxisCount: 3, + crossAxisSpacing: 12.0, + mainAxisSpacing: 12.0, + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + children: [ + _buildTile( + Padding( + padding: const EdgeInsets.all(20.0), + child: productListBox, + ), + onTap: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => ProductsList())), + ), + new GridView.count( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + crossAxisCount: 1, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + padding: EdgeInsets.only(bottom: 5, left: 10, right: 5), + children: widgets, + ), + _buildTile( + PageView( + children: chartWidgets, + ), + ), + ], + staggeredTiles: [ + StaggeredTile.extent(5, 110.0), + StaggeredTile.extent(3, 110.0), + StaggeredTile.extent(3, 250.0), + ], + )), + ), + ); + } + + Widget _buildTile(Widget child, {Function() onTap}) { + return Material( + elevation: 30.0, + borderRadius: BorderRadius.circular(12.0), + shadowColor: Color(0x802196F3), + child: InkWell( + onTap: onTap != null + ? () => onTap() + : () { + log.info('Not set yet'); + }, + child: child)); + } + + Widget _buildBtn(String title, + {Image imgIcon, IconData icon, BtnCallback btnCallback}) { + var languageModel = Provider.of(context); + return _buildTile( + Padding( + padding: const EdgeInsets.all(5.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + icon != null + ? Material( + child: Padding( + padding: EdgeInsets.only(top: 10), + child: Icon(icon, color: primaryColor, size: 35.0))) + : Container( + padding: EdgeInsets.only(top: 3), + child: imgIcon, + ), + Padding(padding: EdgeInsets.only(bottom: 10.0)), + Text(AppTranslations.of(context).text(title), + style: languageModel.isEng + ? TextStyle( + color: Colors.black, + fontWeight: FontWeight.w700, + fontSize: 12.0) + : TextStyle( + color: Colors.black, + fontWeight: FontWeight.w700, + fontSize: 12.0, + fontFamily: "MyanmarUnicode")), + ]), + ), + onTap: btnCallback, + ); + } + + _showNotifications() async { + if (!super.mounted) return; + await Navigator.push( + context, + MaterialPageRoute(builder: (context) => NotificationList()), + ); + // Provider.of(context, listen: false).seen(); + } +} diff --git a/lib/pages/log_list.dart b/lib/pages/log_list.dart new file mode 100644 index 0000000..1e7926e --- /dev/null +++ b/lib/pages/log_list.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/log_model.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; + +class LogList extends StatefulWidget { + @override + _LogListState createState() => _LogListState(); +} + +class _LogListState extends State { + final double dotSize = 15.0; + var dateFormatter = new DateFormat('dd MMM yyyy - hh:mm:ss a'); + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + LogModel logModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'log.title', + color: Colors.white, + fontSize: 20, + ), + ), + body: new ListView.separated( + separatorBuilder: (context, index) => Divider( + color: Colors.black, + ), + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: logModel.logs.length, + itemBuilder: (BuildContext context, int index) { + return Stack( + children: [ + InkWell( + onTap: () {}, + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 0.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Icon( + Icons.message, + color: primaryColor, + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + new Text( + logModel.logs[index].deviceName, + style: new TextStyle( + fontSize: 15.0, + color: Colors.black), + ), + new Text( + "Last login at : ${logModel.logs[index].activeTime == null ? "" : dateFormatter.format(logModel.logs[index].activeTime)}", + style: new TextStyle( + fontSize: 13.0, color: Colors.grey), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + }), + ), + ); + } +} diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart new file mode 100644 index 0000000..b75eac6 --- /dev/null +++ b/lib/pages/login_page.dart @@ -0,0 +1,757 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/pages/reset_password.dart'; +import 'package:fcs/widget/bubble_indication_painter.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart' as Theme; +import 'forget_password.dart'; +import 'sms_page.dart'; +import 'util.dart'; + +class LoginPage extends StatefulWidget { + LoginPage({Key key}) : super(key: key); + + @override + _LoginPageState createState() => new _LoginPageState(); +} + +class _LoginPageState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _scaffoldKey = new GlobalKey(); + + final FocusNode myFocusNodeEmailLogin = FocusNode(); + final FocusNode myFocusNodePasswordLogin = FocusNode(); + + final FocusNode myFocusNodePassword = FocusNode(); + final FocusNode myFocusNodeEmail = FocusNode(); + final FocusNode myFocusNodeName = FocusNode(); + + TextEditingController loginPhoneController = new TextEditingController(); + TextEditingController loginPasswordController = new TextEditingController(); + + bool _obscureTextLogin = true; + bool _obscureTextSignup = true; + bool _obscureTextSignupConfirm = true; + + TextEditingController signupPhoneNumberController = + new TextEditingController(); + TextEditingController signupNameController = new TextEditingController(); + TextEditingController signupPasswordController = new TextEditingController(); + TextEditingController signupConfirmPasswordController = + new TextEditingController(); + + PageController _pageController; + + Color left = Colors.black; + Color right = Colors.white; + final loginFormKey = GlobalKey(); + final signupFormKey = GlobalKey(); + + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + key: _scaffoldKey, + appBar: AppBar( + backgroundColor: Colors.white, + iconTheme: IconThemeData( + color: Colors.grey, + ), + elevation: 0, + centerTitle: true, + title: new Image( + height: 30, + fit: BoxFit.scaleDown, + image: new AssetImage('assets/img/logo.png')), + ), + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height >= 775.0 + ? MediaQuery.of(context).size.height + : 580.0, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(top: 50.0), + child: _buildMenuBar(context), + ), + Expanded( + flex: 2, + child: PageView( + controller: _pageController, + onPageChanged: (i) { + if (i == 0) { + setState(() { + right = Colors.white; + left = Colors.black; + }); + } else if (i == 1) { + setState(() { + right = Colors.black; + left = Colors.white; + }); + } + }, + children: [ + new ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildLogin(context), + ), + new ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildSignUp(context), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + void dispose() { + myFocusNodePassword.dispose(); + myFocusNodeEmail.dispose(); + myFocusNodeName.dispose(); + _pageController?.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + // SystemChrome.setPreferredOrientations([ + // DeviceOrientation.portraitUp, + // DeviceOrientation.portraitDown, + // ]); + + _pageController = PageController(); + loginPhoneController.text = "09"; + signupPhoneNumberController.text = "09"; + } + + Widget _buildMenuBar(BuildContext context) { + return Container( + width: 300.0, + height: 40.0, + decoration: BoxDecoration( + color: Color(0x552B2B2B), + borderRadius: BorderRadius.all(Radius.circular(25.0)), + ), + child: CustomPaint( + painter: TabIndicationPainter(pageController: _pageController), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: FlatButton( + onPressed: _onSignInButtonPress, + child: Text( + AppTranslations.of(context).text("login.title"), + style: Provider.of(context).isEng + ? TextStyle( + color: left, + fontSize: 14.0, + fontFamily: "WorkSansSemiBold") + : TextStyle( + color: left, + fontSize: 15.0, + fontFamily: "MyanmarUnicode"), + ), + ), + ), + //Container(height: 33.0, width: 1.0, color: Colors.white), + Expanded( + child: FlatButton( + onPressed: _onSignUpButtonPress, + child: Text( + AppTranslations.of(context).text("sing.title"), + style: Provider.of(context).isEng + ? TextStyle( + color: right, + fontSize: 14.0, + fontFamily: "WorkSansSemiBold") + : TextStyle( + color: right, + fontSize: 15.0, + fontFamily: "MyanmarUnicode"), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildLogin(BuildContext context) { + return Container( + child: ListView( + children: [ + Column( + children: [ + Form( + key: loginFormKey, + child: Card( + elevation: 2.0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Container( + width: 300.0, + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodeEmailLogin, + controller: loginPhoneController, + keyboardType: TextInputType.phone, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.phone, + color: Colors.black, + size: 22.0, + ), + labelText: AppTranslations.of(context) + .text("login.phone"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + ), + validator: _validatePhone, + ), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodePasswordLogin, + controller: loginPasswordController, + obscureText: _obscureTextLogin, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + size: 22.0, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.password"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleLogin, + child: Icon( + _obscureTextLogin + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validatePassword, + ), + ), + ], + ), + ), + ), + ), + Container( + decoration: new BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.primaryColor, + ), + child: MaterialButton( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 42.0), + child: Text( + AppTranslations.of(context).text("login.btn"), + style: Provider.of(context).isEng + ? TextStyle( + color: Colors.white, + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: "WorkSansBold") + : TextStyle( + color: Colors.white, + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontFamily: "MyanmarUnicode"), + ), + ), + onPressed: () => _login(context)), + ), + ], + ), + Padding( + padding: EdgeInsets.only(top: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + gradient: new LinearGradient( + colors: [ + Colors.white10, + Colors.white, + ], + begin: const FractionalOffset(0.0, 0.0), + end: const FractionalOffset(1.0, 1.0), + stops: [0.0, 1.0], + tileMode: TileMode.clamp), + ), + width: 100.0, + height: 1.0, + ), + Container( + decoration: BoxDecoration( + gradient: new LinearGradient( + colors: [ + Colors.white, + Colors.white10, + ], + begin: const FractionalOffset(0.0, 0.0), + end: const FractionalOffset(1.0, 1.0), + stops: [0.0, 1.0], + tileMode: TileMode.clamp), + ), + width: 100.0, + height: 1.0, + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 10.0), + child: FlatButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ForgetPassword( + phoneNumber: loginPhoneController.text, + ))); + }, + child: Text( + AppTranslations.of(context).text("login.forgot_password"), + style: Provider.of(context).isEng + ? TextStyle( + decoration: TextDecoration.underline, + color: Colors.black, + fontSize: 16.0, + fontFamily: "WorkSansMedium") + : TextStyle( + decoration: TextDecoration.underline, + color: Colors.black, + fontSize: 16.0, + fontFamily: "MyanmarUnicode"), + )), + ), + ], + ), + ); + } + + Widget _buildSignUp(BuildContext context) { + return Container( + child: ListView( + children: [ + Column( + children: [ + Form( + key: signupFormKey, + child: Card( + elevation: 2.0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Container( + width: 300.0, + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodeName, + controller: signupNameController, + keyboardType: TextInputType.text, + textCapitalization: TextCapitalization.words, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.user, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.name"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + hintStyle: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context) + .text("login.name_empty"); + } + return null; + }), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodeEmail, + controller: signupPhoneNumberController, + keyboardType: TextInputType.phone, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.phone, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.phone"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + ), + validator: _validatePhone), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodePassword, + controller: signupPasswordController, + obscureText: _obscureTextSignup, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.password"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleSignup, + child: Icon( + _obscureTextSignup + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validatePassword, + ), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + controller: signupConfirmPasswordController, + obscureText: _obscureTextSignupConfirm, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.confirm_password"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleSignupConfirm, + child: Icon( + _obscureTextSignupConfirm + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validatePassword, + ), + ), + ], + ), + ), + ), + ), + Container( + // margin: EdgeInsets.only(top: 320.0), + decoration: new BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.primaryColor, + ), + child: MaterialButton( + highlightColor: Colors.transparent, + splashColor: Theme.LoginColors.loginGradientEnd, + //shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 42.0), + child: Text( + AppTranslations.of(context).text("sing.up"), + style: Provider.of(context).isEng + ? TextStyle( + color: Colors.white, + fontSize: 18.0, + fontFamily: "WorkSansBold") + : TextStyle( + color: Colors.white, + fontSize: 16.0, + fontFamily: "MyanmarUnicode"), + ), + ), + onPressed: () => _signup(context)), + ), + ], + ), + ], + ), + ); + } + + void _onSignInButtonPress() { + _pageController.animateToPage(0, + duration: Duration(milliseconds: 500), curve: Curves.decelerate); + } + + void _onSignUpButtonPress() { + _pageController?.animateToPage(1, + duration: Duration(milliseconds: 500), curve: Curves.decelerate); + } + + void _toggleLogin() { + setState(() { + _obscureTextLogin = !_obscureTextLogin; + }); + } + + void _toggleSignup() { + setState(() { + _obscureTextSignup = !_obscureTextSignup; + }); + } + + void _toggleSignupConfirm() { + setState(() { + _obscureTextSignupConfirm = !_obscureTextSignupConfirm; + }); + } + + void _signup(BuildContext context) async { + if (!signupFormKey.currentState.validate()) { + return; + } + setState(() { + _isLoading = true; + }); + + MainModel authModel = Provider.of(context); + + var name = signupNameController.text; + var password = signupPasswordController.text; + var confirmPassword = signupConfirmPasswordController.text; + var phoneNumber = signupPhoneNumberController.text; + try { + await authModel.signup(name, password, confirmPassword, phoneNumber); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SmsCodePage(id: phoneNumber, password: password), + ), + ); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + } + + void _login(BuildContext context) async { + if (!loginFormKey.currentState.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + MainModel mainModel = Provider.of(context); + var phoneNumber = loginPhoneController.text; + var password = loginPasswordController.text; + + try { + await mainModel.login(phoneNumber, password); + Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } + + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + + String _validatePassword(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.password_empty"); + } + if (value.length < 6) { + return AppTranslations.of(context).text("login.password_size"); + } + return null; + } + + String _validatePhone(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.phone_empty"); + } + if (!value.startsWith("09") && !value.startsWith("959")) { + return 'Only "09 or 959".'; + } + return null; + } + + Future _forgetPassword() async { + var phoneNumber = loginPhoneController.text; + if (phoneNumber.isEmpty) { + showMsgDialog(context, "Error", "Please input phone number"); + return; + } + setState(() { + _isLoading = true; + }); + try { + UserModel userModel = Provider.of(context); + await userModel.forgetPassword(phoneNumber); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ResetPasswordPage(phoneNumber))); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/manual/instruction_data_page.dart b/lib/pages/manual/instruction_data_page.dart new file mode 100644 index 0000000..4d32f60 --- /dev/null +++ b/lib/pages/manual/instruction_data_page.dart @@ -0,0 +1,227 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/manual_model.dart'; +import 'package:fcs/pages/manual/moveable_stack_item.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/manual.dart'; +import 'package:path/path.dart' as Path; +import 'package:fcs/widget/progress.dart'; + +class InstructionDataPage extends StatefulWidget { + final SlideData slideData; + final String path; + final int slideIndex; + final int manIndex; + + InstructionDataPage( + {this.slideData, this.path, this.slideIndex, this.manIndex}); + @override + _InstructionDataPageState createState() => _InstructionDataPageState(); +} + +class _InstructionDataPageState extends State { + String selectedLanguage; + File slideImageFile; + bool isEng; + List _selection = List.generate(2, (_) => false); + String imgName; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + isEng = true; + } + + @override + Widget build(BuildContext context) { + var manualModel = Provider.of(context); + if (isEng) { + imgName = widget.slideData.image; + } else { + imgName = widget.slideData.imagemm; + } + + var screenSize = MediaQuery.of(context).size; + var width = screenSize.width; + var imgWidth = width - (width * 0.15); + var height = screenSize.height; + var imgHeight = height - (height * 0.29); + + var toggleButtons = Container( + child: ToggleButtons( + children: [ + Row( + children: [ + Image.asset( + "assets/eng_flag.png", + width: 25, + ), + Text( + "English", + style: + TextStyle(color: this.isEng ? secondaryColor : Colors.black), + ) + ], + ), + Container( + padding: EdgeInsets.only(left: 10, right: 10), + child: Row( + children: [ + Image.asset( + "assets/myan_flag.png", + width: 25, + ), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + "မြန်မာ", + style: TextStyle( + color: !this.isEng ? secondaryColor : Colors.black, + fontFamily: "MyanmarUnicode"), + ), + ) + ], + ), + ) + ], + renderBorder: false, + isSelected: _selection, + selectedColor: secondaryColor, + onPressed: (int index) { + setState(() { + _selection[index] = !_selection[index]; + if (index == 0) { + this.isEng = true; + } else { + this.isEng = false; + } + }); + }, + )); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: Text('Instruction Data'), + actions: [toggleButtons], + ), + body: Container( + alignment: Alignment.topCenter, + child: Stack( + children: [ + Container( + child: Stack( + children: instructionData( + context, imgWidth, imgHeight, manualModel), + ), + ), + Positioned( + top: 0, + right: 0, + child: IconButton( + icon: Icon(Icons.image), + onPressed: () { + pickImageFromGallery(ImageSource.gallery, manualModel); + }), + ) + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + addInstructionData(); + }, + tooltip: 'Pick Image', + child: Icon(Icons.add), + ), + ), + ); + } + + addInstructionData() { + var instructionList = + isEng ? widget.slideData.instructions : widget.slideData.instructionsmm; + Instruction inst; + if (instructionList.toList().length == 0) { + inst = Instruction( + id: 1, + text: '', + left: 0.0, + top: 0.0, + ); + } else { + dynamic max = instructionList.first; + instructionList.forEach((e) { + if (e.id > max.id) max = e; + }); + inst = Instruction( + id: max.id + 1, + text: '', + left: 0.0, + top: 0.0, + ); + } + + setState(() { + if (isEng) { + widget.slideData.instructions.add(inst); + } else { + widget.slideData.instructionsmm.add(inst); + } + }); + } + + instructionData( + BuildContext context, double imgW, double imgH, ManualModel manualModel) { + List textFields = []; + textFields.add( + Image.file( + File('${widget.path}/manual/img/' + imgName), + alignment: AlignmentDirectional.topCenter, + height: imgH, + width: imgW, + ), + ); + var instructionList = + isEng ? widget.slideData.instructions : widget.slideData.instructionsmm; + if (instructionList.length != 0) { + instructionList.asMap().forEach((k, instruction) { + MoveableStackItem mitem = MoveableStackItem( + instruction: instruction, + manIndex: widget.manIndex, + slideIndex: widget.slideIndex, + instIndex: k, + isEng: this.isEng, + key: Key(this.isEng.toString() + k.toString()), + ); + + textFields.add(mitem); + }); + } + return textFields; + } + + pickImageFromGallery(ImageSource source, ManualModel manualModel) async { + File tempImage = await ImagePicker.pickImage( + source: source, imageQuality: 80, maxWidth: 300); + + var fileName = Path.basename(tempImage.path); + var path = '${manualModel.dataDir}'; + File newImage = await tempImage.copy('$path/manual/img/$fileName'); + var slideData = widget.slideData; + if (this.isEng) { + slideData.image = fileName; + } else { + slideData.imagemm = fileName; + } + + manualModel.changeSlideImage(widget.manIndex, widget.slideIndex, slideData); + } +} diff --git a/lib/pages/manual/manual_item_title_dialog.dart b/lib/pages/manual/manual_item_title_dialog.dart new file mode 100644 index 0000000..93121a4 --- /dev/null +++ b/lib/pages/manual/manual_item_title_dialog.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/manual_model.dart'; +import 'package:fcs/vo/manual.dart'; + +class ManualItemTitleDialog extends StatefulWidget { + @override + State createState() { + return _ManualItemTitleDialogState(); + } +} + +class _ManualItemTitleDialogState extends State { + TextEditingController _engTextFieldController = TextEditingController(); + TextEditingController _mmTextFieldController = TextEditingController(); + ManualItem item; + bool buyer = false; + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: new Text('Enter Title'), + content: Container( + height: 200, + child: Column( + children: [ + TextField( + controller: _engTextFieldController, + decoration: InputDecoration(hintText: "Enter English"), + maxLines: null, + style: TextStyle(fontSize: 13.0), + ), + TextField( + controller: _mmTextFieldController, + decoration: InputDecoration(hintText: "Enter Myanmar"), + maxLines: null, + style: TextStyle(fontSize: 13.0), + ), + CheckboxListTile( + title: Text("For Buyer"), + value: buyer, + onChanged: (val) { + setState(() { + buyer = val; + }); + }, + ), + ], + ), + ), + actions: [ + new FlatButton( + onPressed: () { + _save(); + }, + child: new Text('Save')) + ], + ); + } + + _save() { + try { + String eng = _engTextFieldController.text; + String mm = _mmTextFieldController.text; + + ManualItem item = + ManualItem(title: eng, titlemm: mm, isBuyer: buyer, slides: []); + + Navigator.pop(context, item); + } catch (e) { + // showMsgDialog(context, "Error", e.toString()); + } + } +} diff --git a/lib/pages/manual/manual_page.dart b/lib/pages/manual/manual_page.dart new file mode 100644 index 0000000..ce3171d --- /dev/null +++ b/lib/pages/manual/manual_page.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/manual_model.dart'; +import 'package:fcs/pages/manual/manual_item_title_dialog.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/manual.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +import 'slide_page.dart'; + +class ManualPage extends StatefulWidget { + @override + _ManualPageState createState() => _ManualPageState(); +} + +class _ManualPageState extends State { + TextEditingController _manualVersionController = TextEditingController(); + final double dotSize = 10.0; + List helpList = new List(); + bool isEng; + String versionName; + bool _isLoading = false; + + @override + void initState() { + helpList.clear(); + var manualModel = Provider.of(context, listen: false); + var mainModel = Provider.of(context, listen: false); + versionName = manualModel.version; + helpList = manualModel.getHelpList(mainModel.isBuyer()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var manualModel = Provider.of(context); + var mainModel = Provider.of(context); + var languageModel = Provider.of(context); + isEng = languageModel.isEng; + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + actions: [ + mainModel.isSysAdmin() + ? Row( + children: [ + Text( + versionName, + style: TextStyle(fontSize: 15, color: Colors.black), + textAlign: TextAlign.center, + ), + IconButton( + icon: Icon(Icons.refresh), + color: Colors.blue, + onPressed: () { + setState(() { + manualModel.resetManualItems(); + helpList = + manualModel.getHelpList(mainModel.isBuyer()); + }); + }), + IconButton( + icon: Icon(Icons.cloud_upload), + color: Colors.blue, + onPressed: () { + _inputManualVersion(context, manualModel); + }) + ], + ) + : Container(), + ], + iconTheme: IconThemeData( + color: Colors.grey, + ), + centerTitle: true, + title: Stack( + children: [ + LocalText( + context, + "manual.title", + fontSize: 25, + ), + ], + ), + ), + body: Column( + children: [ + new Expanded( + child: new ListView.builder( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 5), + shrinkWrap: true, + itemCount: helpList.length, + itemBuilder: (BuildContext context, int index) { + return Card( + elevation: 10, + color: Colors.white, + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SlidePage( + helpDetail: helpList[index], + index: index, + )), + ); + }, + child: Row(children: [ + Expanded( + child: new Padding( + padding: + const EdgeInsets.symmetric(vertical: 5.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 15.0 - dotSize / 2), + child: Container( + child: Image.asset( + "assets/page.png", + width: 50, + height: 50, + )), + ), + new Expanded( + child: new Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + isEng + ? helpList[index].title + : helpList[index].titlemm, + style: TextStyle( + fontSize: 15, + color: Colors.grey), + ) + ], + ), + ), + ], + ), + ), + ), + mainModel.isSysAdmin() + ? Container( + padding: EdgeInsets.only(right: 20), + child: InkWell( + onTap: () { + showConfirmDialog( + context, "manual.confirm", () { + setState(() { + manualModel.deleteManualItem( + helpList[index]); + helpList = manualModel.getHelpList( + mainModel.isBuyer()); + }); + }); + }, + child: Icon(Icons.delete), + )) + : Container() + ])), + ); + }), + ), + ], + ), + floatingActionButton: mainModel.isSysAdmin() + ? FloatingActionButton( + onPressed: () async { + ManualItem newItem = await showDialog( + context: context, + builder: (_) => ManualItemTitleDialog()); + if (helpList.toList().length == 0) { + newItem.id = 1; + } else { + dynamic max = helpList.first; + helpList.forEach((e) { + if (e.id > max.id) max = e; + }); + newItem.id = max.id + 1; + } + setState(() { + helpList.add(newItem); + }); + manualModel.addManualTitle(newItem); + }, + child: Icon(Icons.add), + ) + : null, + ), + ); + } + + _deleteManualItem(ManualItem item, ManualModel manualModel) { + return showConfirmDialog(context, "manual.confirm", () { + setState(() { + manualModel.deleteManualItem(item); + }); + }); + } + + _inputManualVersion(BuildContext context, ManualModel manModel) async { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: new Text('Version'), + content: TextField( + controller: _manualVersionController, + decoration: InputDecoration(hintText: "Enter manual version"), + ), + actions: [ + new FlatButton( + onPressed: () { + String version = _manualVersionController.text; + String dir = manModel.dataDir; + manModel.uploadStorageManualData(version, dir); + Navigator.of(context).pop(); + }, + child: new Text('Save')) + ], + ); + }); + } +} diff --git a/lib/pages/manual/moveable_stack_item.dart b/lib/pages/manual/moveable_stack_item.dart new file mode 100644 index 0000000..fedb0a0 --- /dev/null +++ b/lib/pages/manual/moveable_stack_item.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/manual_model.dart'; +import 'package:fcs/vo/manual.dart'; + +class MoveableStackItem extends StatefulWidget { + Instruction instruction; + final int instIndex; + final int slideIndex; + final int manIndex; + final bool isEng; + + MoveableStackItem( + {this.instIndex, + this.slideIndex, + this.manIndex, + this.instruction, + this.isEng, + Key key}) + : super(key: key); + + @override + State createState() { + return _MoveableStackItemState(); + } +} + +class _MoveableStackItemState extends State { + TextEditingController _textFieldController = TextEditingController(); + double xPosition = 0; + double yPosition = 0; + + @override + void initState() { + super.initState(); + _textFieldController.text = widget.instruction.text; + yPosition = + widget.instruction.top != null ? widget.instruction.top : yPosition; + xPosition = + widget.instruction.left != null ? widget.instruction.left : xPosition; + } + + @override + Widget build(BuildContext context) { + var manualModel = Provider.of(context); + return Positioned( + top: yPosition, + left: xPosition, + child: GestureDetector( + onPanUpdate: (tapInfo) { + setState(() { + xPosition += tapInfo.delta.dx; + yPosition += tapInfo.delta.dy; + }); + var data = Instruction( + id: widget.instruction.id, + top: yPosition, + left: xPosition, + text: _textFieldController.text, + ); + manualModel.saveInstruction(widget.manIndex, widget.slideIndex, + widget.instIndex, data, widget.instruction, widget.isEng); + }, + child: Container( + width: 250, + color: Colors.grey, + child: InkWell( + onTap: () { + _displayDialog(context, manualModel); + }, + child: Container( + child: Text( + _textFieldController.text, + maxLines: null, + style: + TextStyle(fontSize: 15.0, fontWeight: FontWeight.w700), + ))))), + ); + } + + _displayDialog(BuildContext context, ManualModel manualModel) async { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: new Text('Instruction'), + content: Container( + height: 100, + child: Column( + children: [ + TextField( + controller: _textFieldController, + decoration: InputDecoration(hintText: "Enter Instruction"), + maxLines: null, + style: TextStyle(fontSize: 13.0), + ), + ], + ), + ), + actions: [ + new FlatButton( + onPressed: () { + var data = Instruction( + id: widget.instruction.id, + top: yPosition, + left: xPosition, + text: _textFieldController.text, + ); + manualModel.saveInstruction( + widget.manIndex, + widget.slideIndex, + widget.instIndex, + data, + widget.instruction, + widget.isEng); + Navigator.pop(context); + }, + child: new Text('Save')) + ], + ); + }); + } +} diff --git a/lib/pages/manual/slide_data_page.dart b/lib/pages/manual/slide_data_page.dart new file mode 100644 index 0000000..4325f25 --- /dev/null +++ b/lib/pages/manual/slide_data_page.dart @@ -0,0 +1,210 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path/path.dart' as Path; +import 'package:provider/provider.dart'; +import 'package:fcs/model/manual_model.dart'; +import 'package:fcs/pages/manual/instruction_data_page.dart'; +import 'package:fcs/vo/manual.dart'; +import 'package:fcs/widget/img_file.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +class SlideDataPage extends StatefulWidget { + final ManualItem manItem; + final int manIndex; + + SlideDataPage({this.manItem, this.manIndex}); + @override + _SlideDataPageState createState() => _SlideDataPageState(); +} + +class _SlideDataPageState extends State { + File slideImageFile; + List slideList = []; + List _initSlideList = []; + File imageFile; + File engImgFile; + File mmImgFile; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + } + + pickImageFromGallery(ImageSource source, ManualModel manualModel) async { + File tempImage = await ImagePicker.pickImage( + source: source, imageQuality: 80, maxWidth: 300); + + var fileName = Path.basename(tempImage.path); + var path = '${manualModel.dataDir}'; + File newImage = await tempImage.copy('$path/manual/img/$fileName'); + var slideData = + SlideData(id: slideList.length, image: fileName, instructions: []); + setState(() { + slideList.add(slideData); + }); + // manualModel.saveSlideData(widget.manIndex, slideList.length, slideData); + } + + @override + Widget build(BuildContext context) { + var manualModel = Provider.of(context); + slideList = manualModel.getSlideList(widget.manIndex); + + final engImgBox = Container( + padding: EdgeInsets.only(left: 20), + child: Column(children: [ + Text('English image'), + ImageFile( + imageSource: ImageSource.gallery, + enabled: true, + title: "Image", + onFile: (file) { + engImgFile = file; + }) + ])); + + final mmImgBox = Container( + padding: EdgeInsets.only(left: 20, top: 20), + child: Column(children: [ + Text('Myanmar image'), + ImageFile( + imageSource: ImageSource.gallery, + enabled: true, + title: "Image", + onFile: (file) { + mmImgFile = file; + }), + ])); + + final saveImages = Container( + padding: EdgeInsets.only(left: 20, top: 20), + child: FlatButton( + onPressed: () { + saveSlideImages(context, manualModel, widget.manIndex); + }, + child: Text('Save'))); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: Text('Slide Data'), + ), + body: Container( + child: GridView.builder( + itemCount: slideList.length, + itemBuilder: (BuildContext context, int index) { + return Card( + child: InkWell( + onTap: () => { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => InstructionDataPage( + slideData: slideList[index], + path: manualModel.dataDir, + slideIndex: index, + manIndex: widget.manIndex, + ))) + }, + child: Container( + child: Stack( + fit: StackFit.expand, + children: [ + Image.file(File('${manualModel.dataDir}/manual/img/' + + slideList[index].image)), + Positioned( + top: 0, + right: 0, + child: IconButton( + icon: Icon(Icons.delete), + onPressed: () { + setState(() { + manualModel.deleteSlideData( + widget.manIndex, slideList[index]); + }); + }), + ) + ], + ), + ), + ), + ); + }, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async => await _dialog( + context, manualModel, engImgBox, mmImgBox, saveImages), + tooltip: 'Pick Image', + child: Icon(Icons.add), + ), + ), + ); + } + + Future _dialog(BuildContext context, ManualModel manualModel, + Widget engImg, Widget mmImg, Widget saveImgs) { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: Container( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [engImg, mmImg, saveImgs], + ), + ], + ), + ), + ), + ); + }, + ); + } + + saveSlideImages( + BuildContext context, ManualModel manualModel, int manIndex) async { + var engFileName = Path.basename(engImgFile.path); + var mmFileName = Path.basename(mmImgFile.path); + var path = '${manualModel.dataDir}'; + File engImage = await engImgFile.copy('$path/manual/img/$engFileName'); + File mmImage = await mmImgFile.copy('$path/manual/img/$mmFileName'); + var length = slideList.toList().length; + + var slideData; + if (length == 0) { + slideData = SlideData( + id: 1, + image: engFileName, + imagemm: mmFileName, + instructions: [], + instructionsmm: []); + } else { + dynamic max = slideList.first; + slideList.forEach((e) { + if (e.id > max.id) max = e; + }); + slideData = SlideData( + id: max.id + 1, + image: engFileName, + imagemm: mmFileName, + instructions: [], + instructionsmm: []); + } + manualModel.saveSlideData(manIndex, length, slideData); + Navigator.pop(context); + } +} diff --git a/lib/pages/manual/slide_page.dart b/lib/pages/manual/slide_page.dart new file mode 100644 index 0000000..484337f --- /dev/null +++ b/lib/pages/manual/slide_page.dart @@ -0,0 +1,249 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:introduction_screen/introduction_screen.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/manual_model.dart'; +import 'package:fcs/pages/manual/slide_data_page.dart'; +import 'package:fcs/vo/manual.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +class SlidePage extends StatefulWidget { + final ManualItem helpDetail; + final int index; + + SlidePage({this.helpDetail, this.index}); + @override + _SlidePageState createState() => _SlidePageState(); +} + +class _SlidePageState extends State { + bool isEng; + bool _isLoading = false; + + void _onIntroEnd(context) { + Navigator.pop(context); + } + + List instructionText(BuildContext context, isEng, image, imgHeight, + width, List instructions) { + List list = new List(); + var imgWidth = width - (width * 0.25); + File imgFile = File(image); + + list.add( + Card( + color: const Color(0x7f7c94b6), + child: Opacity( + opacity: 0.6, + child: Image.file( + imgFile, + alignment: AlignmentDirectional.topCenter, + height: imgHeight, + width: imgWidth, + )), + ), + ); + + for (var i = 0; i < instructions.length; i++) { + var instruction = instructions[i]; + var textPositionTop = (imgHeight / 480) * instruction.top; + var textPositionLeft = (imgWidth / 360) * instruction.left; + + list.add(Positioned( + top: double.parse(textPositionTop.toString()), + left: double.parse(textPositionLeft.toString()), + child: instruction.text.length > 1 + ? Container( + constraints: BoxConstraints(maxWidth: 300), + child: Card( + color: Colors.blue, + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Text( + instruction.text, + style: TextStyle(color: Colors.white), + ), + ), + )) + : Container( + constraints: BoxConstraints(maxWidth: 200), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + alignment: Alignment.topCenter, + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, color: Colors.blue), + child: isEng + ? Container( + padding: EdgeInsets.only(top: 5), + child: Text( + instruction.text, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, fontSize: 14.0), + ), + ) + : Text( + instruction.text, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, fontSize: 14.0), + ), + ), + ], + ), + ), + )); + } + return list; + } + + @override + Widget build(BuildContext context) { + var mainModel = Provider.of(context); + var languageModel = Provider.of(context); + var manualModel = Provider.of(context); + isEng = languageModel.isEng; + + List pageViews = new List(); + var screenSize = MediaQuery.of(context).size; + var width = screenSize.width; + var height = screenSize.height; + var imgHeight = height - (height * 0.25); + const bodyStyle = TextStyle(fontSize: 19.0); + const pageDecoration = const PageDecoration( + titleTextStyle: TextStyle(fontSize: 28.0, fontWeight: FontWeight.w700), + bodyTextStyle: bodyStyle, + descriptionPadding: EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0), + pageColor: Colors.white, + imagePadding: EdgeInsets.zero, + ); + + var pageSlides = widget.helpDetail.slides; + + if (pageSlides.length == 0) { + pageViews.add( + PageViewModel( + titleWidget: Row( + children: [ + Text( + isEng ? widget.helpDetail.title : widget.helpDetail.titlemm, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.black, + ), + ), + mainModel.isSysAdmin() + ? FlatButton( + textColor: Colors.blue, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SlideDataPage( + manItem: widget.helpDetail, + manIndex: widget.index)), + ); + }, + child: Icon(Icons.edit), + shape: CircleBorder( + side: BorderSide(color: Colors.transparent)), + ) + : Container(), + ], + ), + bodyWidget: Container( + width: width, + alignment: Alignment.center, + ), + decoration: pageDecoration), + ); + } + + for (var i = 0; i < pageSlides.length; i++) { + var instructions; + if (isEng) { + instructions = pageSlides[i].instructions; + } else { + instructions = pageSlides[i].instructionsmm; + } + + var imageName; + if (isEng) { + imageName = '${manualModel.dataDir}/manual/img/' + pageSlides[i].image; + } else { + imageName = + '${manualModel.dataDir}/manual/img/' + pageSlides[i].imagemm; + } + + pageViews.add( + PageViewModel( + titleWidget: Row( + children: [ + Text( + isEng ? widget.helpDetail.title : widget.helpDetail.titlemm, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + color: Colors.black, + ), + ), + mainModel.isSysAdmin() + ? FlatButton( + textColor: Colors.blue, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SlideDataPage( + manItem: widget.helpDetail, + manIndex: widget.index)), + ); + }, + child: Icon(Icons.edit), + shape: CircleBorder( + side: BorderSide(color: Colors.transparent)), + ) + : Container(), + ], + ), + bodyWidget: Container( + width: width, + alignment: Alignment.center, + child: Stack( + alignment: AlignmentDirectional.topStart, + children: instructionText( + context, isEng, imageName, imgHeight, width, instructions), + ), + ), + decoration: pageDecoration), + ); + } + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + body: IntroductionScreen( + pages: pageViews, + onDone: () => _onIntroEnd(context), + showSkipButton: true, + skipFlex: 0, + nextFlex: 0, + skip: const Text('Skip'), + next: const Icon(Icons.arrow_forward), + done: const Text('Done', style: TextStyle(fontWeight: FontWeight.w600)), + dotsDecorator: DotsDecorator(spacing: EdgeInsets.all(1.0)), + )), + ); + } +} diff --git a/lib/pages/my_registeration.dart b/lib/pages/my_registeration.dart new file mode 100644 index 0000000..f0c2c2a --- /dev/null +++ b/lib/pages/my_registeration.dart @@ -0,0 +1,316 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/reg_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/widget/img_file.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import '../widget/localization/app_translations.dart'; +import 'my_registeration_item.dart'; + +enum BuyerType { shop, agent } + +class MyRegisteration extends StatefulWidget { + final Buyer buyer; + const MyRegisteration({this.buyer}); + @override + _MyRegisterationState createState() => _MyRegisterationState(); +} + +class _MyRegisterationState extends State { + TextEditingController _bizName = new TextEditingController(); + TextEditingController _bizAddress = new TextEditingController(); + BuyerType buyerType; + Buyer buyer = Buyer(); + final formKey = GlobalKey(); + bool _isLoading = false; + Attachments attachments = Attachments(); + bool _isNew = true; + + @override + void initState() { + if (widget.buyer != null) { + _isNew = false; + buyerType = + widget.buyer.bizType == "shop" ? BuyerType.shop : BuyerType.agent; + buyer = widget.buyer; + _bizName.text = buyer.bizName; + _bizAddress.text = buyer.bizAddress; + } else { + buyerType = BuyerType.shop; + } + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + + final companyName = Container( + padding: EdgeInsets.only(top: 0, left: 20, right: 15), + child: TextFormField( + controller: _bizName, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("reg.biz_name"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + Icons.business, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: _validateBizName, + )); + + final companyAddress = Container( + padding: EdgeInsets.only(top: 0, left: 20, right: 15), + child: TextFormField( + minLines: 2, + maxLines: 3, + controller: _bizAddress, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("reg.biz_address"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Image.asset( + "assets/address.png", + height: 25, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: _validateBizAddress, + )); + + final nricFrontBox = Container( + padding: EdgeInsets.only(left: 20), + child: Row(children: [ + LocalText(context, 'reg_info.nric_front'), + ImageFile( + enabled: true, + initialImgUrl: + widget.buyer == null ? "" : widget.buyer.nricFrontUrl, + title: "Image", + onFile: (file) { + attachments.nricFront = file; + }) + ])); + + final nricBackBox = Container( + padding: EdgeInsets.only(left: 20, top: 20), + child: Row(children: [ + LocalText(context, 'reg_info.nric_back'), + ImageFile( + enabled: true, + initialImgUrl: + widget.buyer == null ? '' : widget.buyer.nricBackUrl, + title: "Image", + onFile: (file) { + attachments.nricBack = file; + }), + ])); + final nric = Container( + padding: EdgeInsets.only(top: 20, left: 20, right: 15), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [nricFrontBox, nricBackBox], + )); + + final buyerTypeWidget = Row(children: [ + Flexible( + child: ListTile( + onTap: () => setState(() { + buyerType = BuyerType.shop; + }), + title: Text(AppTranslations.of(context).text("reg.type_shop")), + leading: Radio( + activeColor: primaryColor, + value: BuyerType.shop, + groupValue: buyerType, + onChanged: (BuyerType value) { + setState(() { + buyerType = value; + }); + }, + ), + ), + ), + Flexible( + child: ListTile( + onTap: () => setState(() { + buyerType = BuyerType.agent; + }), + title: Text(AppTranslations.of(context).text("reg.type_agent")), + leading: Radio( + activeColor: primaryColor, + value: BuyerType.agent, + groupValue: buyerType, + onChanged: (BuyerType value) { + setState(() { + buyerType = value; + }); + }, + ), + ), + ), + ]); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("reg.title")), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + showConfirmDialog(context, "reg.confirm", () { + _submit(); + }); + }, + ) + ], + ), + body: Container( + child: Form( + key: formKey, + child: ListView( + children: [ + companyName, + companyAddress, + buyerTypeWidget, + nric, + ], + ), + ), + ), + ), + ); + } + + List getProductRow(Buyer buyer) { + return buyer.buyerProducts.map((b) { + return MyDataRow( + onSelectChanged: (bool selected) async { + final BuyerProduct buyerProduct = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MyRegisterationItem( + buyerProduct: b, + )), + ); + _save(buyerProduct); + }, + cells: [ + MyDataCell( + new Text( + b.productName, + style: textStyle, + ), + ), + MyDataCell( + new Text(b.storageCapacityQty.toString(), style: textStyle), + ), + MyDataCell( + new Text(b.dailySaleQty.toString(), style: textStyle), + ), + ], + ); + }).toList(); + } + + String _validateBizName(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("reg.empty_biz_name"); + } + return null; + } + + String _validateBizAddress(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("reg.empty_biz_address"); + } + return null; + } + + String _validateShops(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("reg.empty_shops"); + } + return null; + } + + _save(BuyerProduct buyerProduct) { + if (buyerProduct == null) return; + if (buyerProduct.action == "create") { + if (buyer.buyerProducts.contains(buyerProduct)) { + showMsgDialog(context, "Error", "Duplicate line"); + return; + } + buyer.buyerProducts.add(buyerProduct); + } else if (buyerProduct.action == "delete") { + buyer.buyerProducts.remove(buyerProduct); + } + } + + _submit() async { + if (!formKey.currentState.validate()) { + return; + } + + if (_isNew) { + if (attachments.nricFront == null || attachments.nricBack == null) { + showMsgDialog( + context, "Error", "Required NRIC front and back attachments"); + return; + } + } + + setState(() { + _isLoading = true; + }); + try { + String type = buyerType == BuyerType.agent ? "agent" : "shop"; + + buyer.bizName = _bizName.text; + buyer.bizAddress = _bizAddress.text; + buyer.bizType = type; + + if (_isNew) { + await Provider.of(context).register(buyer, attachments); + } else { + await Provider.of(context).update(buyer, attachments); + } + Navigator.pop(context, true); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/my_registeration_info.dart b/lib/pages/my_registeration_info.dart new file mode 100644 index 0000000..9f25235 --- /dev/null +++ b/lib/pages/my_registeration_info.dart @@ -0,0 +1,117 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/reg_model.dart'; +import 'package:fcs/widget/label_widgets.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import '../util.dart'; +import '../widget/localization/app_translations.dart'; +import 'my_registeration.dart'; + +enum BuyerType { shop, agent } + +class MyRegisterationInfo extends StatefulWidget { + @override + _MyRegisterationInfoState createState() => _MyRegisterationInfoState(); +} + +class _MyRegisterationInfoState extends State { + bool inProgress = true; + + BuyerType buyerType = BuyerType.shop; + TextEditingController _bizName = new TextEditingController(); + TextEditingController _bizAddress = new TextEditingController(); + TextEditingController _status = new TextEditingController(); + + @override + Widget build(BuildContext context) { + RegModel regModel = Provider.of(context); + MainModel mainModel = Provider.of(context); + buyerType = + regModel.reg.bizType == "shop" ? BuyerType.shop : BuyerType.agent; + _bizName.text = regModel.reg.bizName; + _bizAddress.text = regModel.reg.bizAddress; + _status.text = regModel.reg.status; + bool isRegBuyer = mainModel.isRegBuyer(); + if (regModel.isLoaded) { + setState(() { + this.inProgress = false; + }); + } + + final bizNameBox = + labeledText(context, regModel.reg.bizName, "reg.biz_name"); + final bizAddressBox = + labeledText(context, regModel.reg.bizAddress, "reg.biz_address"); + final typeBox = + labeledText(context, regModel.reg.bizType, "buyer.type_biz"); + final statusBox = labeledText(context, regModel.reg.status, "reg.status"); + final dailyQuotaBox = labeledText( + context, formatNumber(regModel.reg.dailyQuota), "reg.quota",number: true); + final dailyQuotaUsedBox = labeledText( + context, formatNumber(regModel.reg.dailyQuotaUsed), "reg.quota.used",number: true); + final maxQuotaBox = labeledText( + context, formatNumber(regModel.reg.maxQuota), "reg.max_quota",number: true); + final maxQuotaUsedBox = labeledText( + context, formatNumber(regModel.reg.maxQuotaUsed), "reg.max_quota.used",number: true); + final nricFrontBox = + labeledImg(context, regModel.reg.nricFrontUrl, "reg_info.nric_front"); + final nricBackBox = + labeledImg(context, regModel.reg.nricBackUrl, "reg_info.nric_back"); + + return LocalProgress( + inAsyncCall: inProgress, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("reg_info.title")), + actions: [ + !isRegBuyer + ? Container() + : regModel.reg.isApproved() + ? Container() + : IconButton( + icon: Icon(Icons.edit), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => MyRegisteration( + buyer: regModel.reg, + ))); + }, + ), + ], + ), + body: Container( + padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + child: ListView( + children: [ + bizNameBox, + Divider(), + bizAddressBox, + Divider(), + typeBox, + Divider(), + statusBox, + Divider(), + dailyQuotaBox, + Divider(), + dailyQuotaUsedBox, + Divider(), + maxQuotaBox, + Divider(), + maxQuotaUsedBox, + Divider(), + nricFrontBox, + Divider(), + nricBackBox, + Divider() + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/my_registeration_item.dart b/lib/pages/my_registeration_item.dart new file mode 100644 index 0000000..eea09a5 --- /dev/null +++ b/lib/pages/my_registeration_item.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/widget/progress.dart'; + +class MyRegisterationItem extends StatefulWidget { + final BuyerProduct buyerProduct; + const MyRegisterationItem({Key key, this.buyerProduct}) : super(key: key); + @override + _MyRegisterationItemState createState() => _MyRegisterationItemState(); +} + +class _MyRegisterationItemState extends State { + final _formKey = GlobalKey(); + bool _isLoading = false; + TextEditingController _storage = new TextEditingController(); + TextEditingController _sales = new TextEditingController(); + String currentProductID; + BuyerProduct buyerProduct = BuyerProduct(action: "create"); + + @override + void initState() { + super.initState(); + if (widget.buyerProduct != null) { + buyerProduct = widget.buyerProduct; + buyerProduct.action = "update"; + currentProductID = buyerProduct.productID; + _sales.text = buyerProduct.dailySaleQty.toString(); + _storage.text = buyerProduct.storageCapacityQty.toString(); + } + } + + Widget showProducts(BuildContext context, ProductModel productModel) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon( + FontAwesomeIcons.tag, + color: primaryColor, + size: 20, + ), + SizedBox( + width: 20, + ), + new Flexible( + child: Container( + width: 170.0, + child: DropdownButton( + value: currentProductID, + isExpanded: true, + hint: Text( + 'Select Product', + style: labelStyle, + ), + onChanged: changedProduct, + items: productModel.products + .map>((Product product) { + return new DropdownMenuItem( + value: product.id, + child: new Text(product.name, style: textStyle), + ); + }).toList(), + ), + ), + ), + ], + ); + } + + void changedProduct(selected) { + setState(() { + currentProductID = selected; + }); + } + + @override + Widget build(BuildContext context) { + ProductModel productModel = Provider.of(context); + + final volumeBox = Container( + padding: EdgeInsets.only(top: 10), + child: TextFormField( + controller: _storage, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.number, + decoration: new InputDecoration( + labelText: "Enter total storage capacity", + labelStyle: labelStyle, + icon: Image.asset( + "assets/volume.png", + width: 23, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return "Please total storage capacity"; + } + return null; + }, + ), + ); + final amountBox = Container( + child: TextFormField( + controller: _sales, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.number, + decoration: new InputDecoration( + labelText: "Enter daily sale quantity", + labelStyle: labelStyle, + icon: Image.asset( + "assets/sales.png", + width: 23, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return "Please enter daily sale quantity"; + } + return null; + }, + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text("Product registration"), + actions: [ + IconButton( + icon: Icon(Icons.delete), + onPressed: () { + buyerProduct.action = "delete"; + Navigator.pop(context, buyerProduct); + }, + ), + IconButton( + icon: Icon(Icons.save), + onPressed: () { + if (!_formKey.currentState.validate()) return; + _save(); + }, + ) + ], + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + showProducts(context, productModel), + volumeBox, + amountBox, + ], + ), + ), + ], + ), + )), + ); + } + + _save() { + if (currentProductID == null) return; + this.buyerProduct.productID = currentProductID; + var productName = + Provider.of(context).getProductName(currentProductID); + this.buyerProduct.productName = productName; + this.buyerProduct.storageCapacityQty = int.parse(_storage.text); + this.buyerProduct.dailySaleQty = int.parse(_sales.text); + Navigator.pop(context, this.buyerProduct); + } +} diff --git a/lib/pages/notification_list.dart b/lib/pages/notification_list.dart new file mode 100644 index 0000000..cbeea2b --- /dev/null +++ b/lib/pages/notification_list.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/notification_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/notification.dart' as Noti; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/popupmenu.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; + +class NotificationList extends StatefulWidget { + @override + _NotificationListState createState() => _NotificationListState(); +} + +class _NotificationListState extends State { + var timeFormatter = new DateFormat('KK:mm a'); + var dateFormatter = new DateFormat('dd MMM'); + final double dotSize = 15.0; + int _selectedIndex = 0; + bool _isLoading = false; + bool _isClicked = false; + + @override + Widget build(BuildContext context) { + NotificationModel notificationModel = + Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("noti.title")), + actions: [ + PopupMenuButton( + elevation: 3.2, + onSelected: (selected) { + setState(() { + this._selectedIndex = selected.index; + }); + notificationModel.filter(selected.index); + }, + icon: Container( + width: 30, + height: 30, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Icon( + Icons.filter_list, + color: primaryColor, + ), + _selectedIndex != 0 + ? Positioned( + bottom: 0, + right: 0, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + : Container() + ], + )), + itemBuilder: (BuildContext context) { + return notificationMenu.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Row( + children: [ + Text(choice.status), + SizedBox( + width: 10, + ), + _selectedIndex != null && + _selectedIndex == choice.index + ? Icon( + Icons.check, + color: Colors.grey, + ) + : Container(), + ], + ), + ); + }).toList(); + }), + ], + ), + body: new ListView.separated( + separatorBuilder: (context, index) => Divider( + color: Colors.black, + ), + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: notificationModel.notis.length, + itemBuilder: (BuildContext context, int index) { + Noti.Notification noti = notificationModel.notis[index]; + return Stack( + children: [ + InkWell( + onTap: () => _display(noti), + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Icon(Icons.message), + ), + new Expanded( + child: new Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + new Text( + noti.getDesc, + style: new TextStyle( + fontSize: 15.0, + color: Colors.black), + ), + new Text( + noti.itemType == "reg" + ? "" + : noti.itemNumber, + style: new TextStyle( + fontSize: 13.0, color: Colors.grey), + ), + ], + ), + ), + ], + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(timeFormatter.format(noti.time)), + ), + noti.fromToday() + ? Container() + : Text(dateFormatter.format(noti.time)), + ], + ) + ], + ), + ), + noti.seen + ? Container() + : new Positioned( + left: 11, + top: 11, + child: new Container( + padding: EdgeInsets.all(2), + decoration: new BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(6), + ), + constraints: BoxConstraints( + minWidth: 18, + minHeight: 18, + ), + child: Text( + 'new', + style: TextStyle( + color: Colors.white, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ), + ) + ], + ); + }), + ), + ); + } + + _display(Noti.Notification noti) async { + if (_isClicked) return; + _isClicked = true; + await displayNotiContent(context, noti); + _isClicked = false; + } +} diff --git a/lib/pages/offline.dart b/lib/pages/offline.dart new file mode 100644 index 0000000..69d0ae6 --- /dev/null +++ b/lib/pages/offline.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:fcs/widget/local_text.dart'; + + +class Offline extends StatefulWidget { + @override + _OfflineState createState() => _OfflineState(); +} + +class _OfflineState extends State { + + @override + Widget build(BuildContext context) { + final retryButton = Card( + elevation: 23, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.offline_bolt), + LocalText(context, "offline.status"), + ], + ), + ), + ); + + return Scaffold( + body: Center(child: retryButton), + ); + } +} diff --git a/lib/pages/pd/pd_form.dart b/lib/pages/pd/pd_form.dart new file mode 100644 index 0000000..a35b28c --- /dev/null +++ b/lib/pages/pd/pd_form.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/log_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/pd_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/pd.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/progress.dart'; +import '../util.dart'; +import 'pd_item.dart'; + +class PDForm extends StatefulWidget { + final PD pd; + const PDForm({Key key, this.pd}) : super(key: key); + @override + _PDFormState createState() => _PDFormState(); +} + +class _PDFormState extends State { + TextEditingController _date = new TextEditingController(); + TextEditingController _userName = new TextEditingController(); + + PD pd = PD(); + bool _isLoading = false; + bool isNew = true; + + @override + void initState() { + super.initState(); + if (widget.pd != null) { + _userName.text = widget.pd.userName; + } else { + var mainModel = Provider.of(context, listen: false); + _userName.text = mainModel.user.name; + } + + _load(); + } + + _load() async { + if (widget.pd != null) { + this.pd = widget.pd; + _date.text = DateFormat('dd MMM yyyy – hh:mm a').format(widget.pd.date); + isNew = false; + Provider.of(context, listen: false).loadPDLines(pd).then((_pd) { + setState(() { + this.pd = _pd; + }); + }); + } + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + final _formKey = GlobalKey(); + + final dateBox = Container( + padding: EdgeInsets.only(left: 20, right: 15), + child: TextFormField( + controller: _date, + enabled: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("pd.date"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + Icons.date_range, + color: primaryColor, + size: 23, + )), + )); + + final nameBox = Container( + padding: EdgeInsets.only(left: 20, right: 15), + child: TextFormField( + controller: _userName, + autofocus: false, + readOnly: true, + style: textStyle, + decoration: new InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Icon( + Icons.person, + color: primaryColor, + size: 25, + )), + )); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text('pd'), + style: languageModel.isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode'), + ), + actions: [ + isNew + ? IconButton( + icon: Icon(Icons.send), + onPressed: () { + if (!_formKey.currentState.validate()) return; + showConfirmDialog(context, "pd.confirm", () { + _submit(); + }); + }, + ) + : Container() + ], + ), + floatingActionButton: isNew + ? FloatingActionButton( + backgroundColor: primaryColor, + child: Icon(Icons.add), + onPressed: () async { + final PDLine pdLine = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => PDItem()), + ); + _save(pdLine); + }, + ) + : null, + body: Form( + key: _formKey, + child: ListView( + children: [ + isNew ? Container() : dateBox, + nameBox, + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + columns: [ + MyDataColumn( + label: LocalText(context, "pd.product")), + MyDataColumn( + label: LocalText(context, "pd.storage"), + ), + MyDataColumn( + label: LocalText(context, "pd.quantity")), + ], + rows: getProductRow(pd), + ), + ), + ], + ), + )), + ); + } + + List getProductRow(PD pd) { + return pd.pdLines.map((p) { + return MyDataRow( + onSelectChanged: (bool selected) async { + if (!isNew) return; + var pdLine = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PDItem( + pdLine: p, + )), + ); + _save(pdLine); + }, + cells: [ + MyDataCell( + new Text( + p.productName, + style: textStyle, + ), + ), + MyDataCell( + new Text(p.storageName, style: textStyle), + ), + MyDataCell( + new Text(p.quantity.toString(), style: textStyle), + ), + ], + ); + }).toList(); + } + + _save(PDLine pdLine) { + if (pdLine == null) return; + if (pdLine.action == "create") { + if (pd.pdLines.contains(pdLine)) { + showMsgDialog(context, "Error", "Duplicate line"); + return; + } + pd.pdLines.add(pdLine); + } else if (pdLine.action == "delete") { + pd.pdLines.remove(pdLine); + } + } + + _submit() async { + if (pd.pdLines.length == 0) { + showMsgDialog(context, "Error", "No product line"); + return; + } + setState(() { + _isLoading = true; + }); + try { + PDModel pdModel = Provider.of(context); + await pdModel.createPD(pd); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/pd/pd_item.dart b/lib/pages/pd/pd_item.dart new file mode 100644 index 0000000..8b3f7e5 --- /dev/null +++ b/lib/pages/pd/pd_item.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/model/storage_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/pd.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/vo/storage.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +class PDItem extends StatefulWidget { + final PDLine pdLine; + const PDItem({Key key, this.pdLine}) : super(key: key); + @override + _PDItemState createState() => _PDItemState(); +} + +class _PDItemState extends State { + final _formKey = GlobalKey(); + bool _isLoading = false; + String currentStorageID; + String currentProductID; + TextEditingController _quantity = new TextEditingController(); + PDLine pdLine = PDLine(); + + @override + void initState() { + super.initState(); + if (widget.pdLine != null) { + this._quantity.text = widget.pdLine.quantity.toString(); + this.currentProductID = widget.pdLine.productID; + this.currentStorageID = widget.pdLine.storageID; + this.pdLine = widget.pdLine; + this.pdLine.action = "update"; + } else { + this.pdLine.action = "create"; + } + } + + Widget showInventoryList(BuildContext context, StorageModel storageModel) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + "assets/inventory.png", + color: primaryColor, + width: 25, + ), + SizedBox( + width: 20, + ), + new Flexible( + child: Container( + width: 170.0, + child: DropdownButton( + value: currentStorageID, + isExpanded: true, + hint: Text( + 'Select Storage', + style: labelStyle, + ), + onChanged: changedDropDownItem, + items: storageModel.storages + .map>((Storage storage) { + return new DropdownMenuItem( + value: storage.id, + child: new Text(storage.name, style: textStyle), + ); + }).toList(), + ), + ), + ), + ], + ); + } + + void changedDropDownItem(selected) { + setState(() { + currentStorageID = selected; + }); + } + + Widget showProducts(BuildContext context, ProductModel productModel) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon( + FontAwesomeIcons.tag, + color: primaryColor, + size: 20, + ), + SizedBox( + width: 20, + ), + new Flexible( + child: Container( + width: 170.0, + child: DropdownButton( + value: currentProductID, + isExpanded: true, + hint: Text( + 'Select Product', + style: labelStyle, + ), + onChanged: changedProduct, + items: productModel.products + .map>((Product product) { + return new DropdownMenuItem( + value: product.id, + child: new Text(product.name, style: textStyle), + ); + }).toList(), + ), + ), + ), + ], + ); + } + + void changedProduct(selected) { + setState(() { + currentProductID = selected; + }); + } + + @override + Widget build(BuildContext context) { + var storageModel = Provider.of(context); + var productModel = Provider.of(context); + var languageModel = Provider.of(context); + + final quantityBox = Container( + padding: EdgeInsets.only(top: 10), + child: TextFormField( + controller: _quantity, + keyboardType: TextInputType.number, + autofocus: false, + cursorColor: primaryColor, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("pd.quantity"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.sortNumericUpAlt, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("pd.form.quan"); + } + return null; + }, + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text('pd.product.title'), + style: languageModel.isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode'), + ), + actions: [ + IconButton( + icon: Icon(Icons.delete), + onPressed: () { + _delete(); + }, + ), + IconButton( + icon: Icon(Icons.save), + onPressed: () { + if (!_formKey.currentState.validate()) return; + _save(); + }, + ) + ], + ), + body: Form( + key: _formKey, + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 20.0), + children: [ + quantityBox, + showInventoryList(context, storageModel), + showProducts(context, productModel) + ], + ), + )), + ); + } + + _save() { + if (currentProductID == null || currentStorageID == null) return; + this.pdLine.storageID = currentStorageID; + var storageName = + Provider.of(context).getStorageName(currentStorageID); + this.pdLine.storageName = storageName; + this.pdLine.productID = currentProductID; + var productName = + Provider.of(context).getProductName(currentProductID); + this.pdLine.productName = productName; + this.pdLine.quantity = int.parse(_quantity.text); + Navigator.pop(context, this.pdLine); + } + + _delete() { + this.pdLine.action = "delete"; + Navigator.pop(context, this.pdLine); + } +} diff --git a/lib/pages/pd/pd_list.dart b/lib/pages/pd/pd_list.dart new file mode 100644 index 0000000..3344980 --- /dev/null +++ b/lib/pages/pd/pd_list.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/pd_model.dart'; +import 'package:fcs/pages/pd/pd_form.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +class PDList extends StatefulWidget { + @override + _PDListState createState() => _PDListState(); +} + +class _PDListState extends State { + final double dotSize = 15.0; + DateTime _selectedDate = DateTime.now(); + int _dateIndex = 0; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + + var pdModel = Provider.of(context, listen: false); + // pdModel.loadPDs(); + _selectedDate = pdModel.selectedDate; + _dateIndex = pdModel.dateIndex; + } + + Future _selectDate(BuildContext context) async { + var pdModel = Provider.of(context); + + final DateTime picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101), + builder: (BuildContext context, Widget child) { + return Theme( + data: ThemeData.light().copyWith( + primaryColor: primaryColor, //Head background + accentColor: secondaryColor, //selection color + dialogBackgroundColor: Colors.white, //Background color + ), + child: child, + ); + }, + ); + + if (picked != null) { + var pickedDate = new DateTime(picked.year, picked.month, picked.day); + var currentDate = new DateTime( + DateTime.now().year, DateTime.now().month, DateTime.now().day); + + this._dateIndex = pickedDate == currentDate ? 0 : 1; + setState(() { + _selectedDate = picked; + pdModel.filterDate(_selectedDate, _dateIndex); + }); + } + } + + @override + Widget build(BuildContext context) { + var pdModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text('pd.title'), + style: Provider.of(context).isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode'), + ), + actions: [ + InkWell( + child: Container( + padding: EdgeInsets.only(right: 15, top: 15), + child: Stack( + children: [ + Image.asset( + "assets/date_filter.png", + color: Colors.white, + width: 25, + ), + _dateIndex == 0 + ? Container() + : Positioned( + bottom: 15, + right: 10, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + ], + ), + ), + onTap: () => _selectDate(context), + ), + ], + ), + floatingActionButton: FloatingActionButton( + backgroundColor: primaryColor, + child: Icon(Icons.add), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => PDForm()), + ); + }, + ), + body: new ListView.builder( + padding: EdgeInsets.only(left: 10, right: 10, top: 15), + shrinkWrap: true, + itemCount: pdModel.pds.length, + itemBuilder: (BuildContext context, int index) { + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PDForm( + pd: pdModel.pds[index], + )), + ); + }, + child: Card( + elevation: 10, + color: Colors.white, + child: Row( + children: [ + new Padding( + padding: EdgeInsets.all(10), + child: Padding( + padding: EdgeInsets.all(10.0), + child: Image.asset( + "assets/pdo.png", + width: 30, + color: primaryColor, + )), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: new Text( + pdModel.pds[index].date == null + ? "" + : DateFormat('dd MMM yyyy') + .format(pdModel.pds[index].date), + style: textStyle), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: new Text( + pdModel.pds[index].pdNumber == null + ? '' + : pdModel.pds[index].pdNumber, + style: new TextStyle( + fontSize: 12.0, color: Colors.grey), + ), + ), + ], + ), + SizedBox( + height: 15, + ) + ], + ), + ), + ); + }), + ), + ); + } +} diff --git a/lib/pages/phone_input.dart b/lib/pages/phone_input.dart new file mode 100644 index 0000000..253e910 --- /dev/null +++ b/lib/pages/phone_input.dart @@ -0,0 +1,103 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; + +class PhoneEditor extends StatefulWidget { + @override + _PhoneEditorState createState() => _PhoneEditorState(); +} + +class _PhoneEditorState extends State { + final _formKey = GlobalKey(); + TextEditingController _phone = new TextEditingController(); + @override + void initState() { + super.initState(); + _phone.text ='09'; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Center( + child: Text( + AppTranslations.of(context).text("contact.phone.title"), + style: TextStyle( + color: primaryColor, fontWeight: FontWeight.bold, fontSize: 20), + )), + content: Form( + key: _formKey, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new Expanded( + child: new TextFormField( + keyboardType: TextInputType.number, + autofocus: true, + controller: _phone, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + cursorColor: primaryColor, + decoration: new InputDecoration( + fillColor: primaryColor, + icon: Icon( + Icons.phone, + color: primaryColor, + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context) + .text("contact.phone.empty"); + } + return null; + }, + )) + ], + ), + ), + actions: [ + FlatButton( + child: LocalText( + context, + 'do.cancel', + color: secondaryColor, + ), + onPressed: () { + _phone.clear(); + Navigator.of(context).pop(); + }), + FlatButton( + color: primaryColor, + child: LocalText( + context, + 'do.enter', + color: Colors.white, + fontWeight: FontWeight.bold, + ), + onPressed: () async { + if (!_formKey.currentState.validate()) return; + _save(); + }) + ], + ); + } + + _save() { + try { + Navigator.pop(context, _phone.text); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } + } +} diff --git a/lib/pages/pin_login_dialog.dart b/lib/pages/pin_login_dialog.dart new file mode 100644 index 0000000..349d67d --- /dev/null +++ b/lib/pages/pin_login_dialog.dart @@ -0,0 +1,102 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_verification_code_input/flutter_verification_code_input.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:quiver/async.dart'; + +class PinLoginDialog extends StatefulWidget { + @override + _PinLoginDialogState createState() => _PinLoginDialogState(); +} + +class _PinLoginDialogState extends State { + String pin; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var mainModel = Provider.of(context); + + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(32.0))), + title: Column( + children: [ + Image.asset( + "assets/pin.png", + height: 90, + color: primaryColor, + ), + Text( + "Enter PIN Code", + style: TextStyle( + color: primaryColor, fontWeight: FontWeight.bold, fontSize: 20), + ), + ], + ), + content: Container( + width: double.maxFinite, + height: 120.0, + child: new ListView( + shrinkWrap: true, + children: [ + Center( + child: VerificationCodeInput( + keyboardType: TextInputType.number, + length: 6, + autofocus: false, + itemSize: 40, + itemDecoration: BoxDecoration( + border: Border.all( + color: Colors.grey, + ), + ), + textStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 23), + onCompleted: (String value) { + this.pin = value; + }, + ), + ), + SizedBox( + height: 15, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FlatButton( + child: Text("Cancel"), + onPressed: () { + Navigator.of(context).pop(); + }), + FlatButton( + color: primaryColor, + child: Text("OK", + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)), + onPressed: () async { + if (this.pin == null) return; + + if (mainModel.user.pin == this.pin) { + mainModel.resetPinTimer(); + Navigator.of(context).pop(); + } else { + showMsgDialog(context, "Error", "Invalid PIN Code !"); + } + }), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/po/po_files.dart b/lib/pages/po/po_files.dart new file mode 100644 index 0000000..0d1268d --- /dev/null +++ b/lib/pages/po/po_files.dart @@ -0,0 +1,27 @@ +import 'dart:io'; + +class POFiles { + File poPaymentFile, storageChargeFile; + List poPaymentFilesAdded=[]; + List poPaymentFilesRemoved=[]; // only url + bool poFileChanged = false, storageFileChanged = false; + + set addPoPaymentFile(File file) { + poPaymentFilesAdded.add(file); + poFileChanged = true; + } + + set removePoPaymentFile(String url) { + poPaymentFilesRemoved.add(url); + poFileChanged = true; + } + + set setStorageChargeFile(File file) { + storageChargeFile = file; + storageFileChanged = true; + } + + bool get anyChanged => poFileChanged || storageFileChanged; + + +} diff --git a/lib/pages/po/po_item.dart b/lib/pages/po/po_item.dart new file mode 100644 index 0000000..9db9e39 --- /dev/null +++ b/lib/pages/po/po_item.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../util.dart'; + +class POItem extends StatefulWidget { + final POLine poLine; + const POItem({Key key, this.poLine}) : super(key: key); + @override + _POItemState createState() => _POItemState(); +} + +class _POItemState extends State { + final _formKey = GlobalKey(); + bool _isLoading = false; + String currentProductID; + TextEditingController _qty = new TextEditingController(); + POLine poLine = POLine(); + + @override + void initState() { + super.initState(); + if (widget.poLine != null) { + this.poLine = widget.poLine; + + this._qty.text = this.poLine.qty.toString(); + this.currentProductID = this.poLine.productID; + this.poLine.action = "update"; + } else { + this.poLine.action = "create"; + } + } + + @override + void dispose() { + super.dispose(); + } + + Widget showProducts(BuildContext context, ProductModel productModel) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Icon( + FontAwesomeIcons.tag, + color: primaryColor, + size: 20, + ), + SizedBox( + width: 20, + ), + new Flexible( + child: Container( + width: 170.0, + child: DropdownButton( + value: currentProductID, + isExpanded: true, + hint: Text( + 'Select Product', + style: labelStyle, + ), + onChanged: changedProduct, + items: productModel.products + .map>((Product product) { + return new DropdownMenuItem( + value: product.id, + child: new Text(product.name, style: textStyle), + ); + }).toList(), + ), + ), + ), + ], + ); + } + + void changedProduct(selected) { + setState(() { + // currentProductID = selected; + }); + } + + @override + Widget build(BuildContext context) { + var productModel = Provider.of(context); + var languageModel = Provider.of(context); + + final volumeBox = Container( + padding: EdgeInsets.only(top: 10), + child: TextFormField( + controller: _qty, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.number, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("po.volume"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.sortNumericUpAlt, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("po.form.volume"); + } + return null; + }, + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + "po", + color: Colors.white, + fontSize: 20, + ), + actions: [ + // IconButton( + // icon: Icon(Icons.delete), + // onPressed: () { + // _delete(); + // }, + // ), + IconButton( + icon: Icon(Icons.save), + onPressed: () { + if (!_formKey.currentState.validate()) return; + _save(); + }, + ) + ], + ), + body: Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + volumeBox, + showProducts(context, productModel) + ], + ), + ), + ], + ), + )), + ); + } + + _save() { + setState(() { + _isLoading = true; + }); + try { + this.poLine.productID = currentProductID; + var product = + Provider.of(context).getProduct(currentProductID); + this.poLine.productName = product.name; + this.poLine.price = product.price; + this.poLine.qty = int.parse(_qty.text); + this.poLine.amount = this.poLine.price * this.poLine.qty; + Navigator.pop(context, this.poLine); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _delete() { + setState(() { + _isLoading = true; + }); + try { + this.poLine.action = "delete"; + Navigator.pop(context, this.poLine); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/po/po_submission_form.dart b/lib/pages/po/po_submission_form.dart new file mode 100644 index 0000000..ab51231 --- /dev/null +++ b/lib/pages/po/po_submission_form.dart @@ -0,0 +1,601 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/log_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/po_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/pages/po/po_item.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/attach.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/widget/img_file.dart'; +import 'package:fcs/widget/label_widgets.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/multi_img_controller.dart'; +import 'package:fcs/widget/multi_img_file.dart'; +import 'package:fcs/widget/my_data_table.dart'; +import 'package:fcs/widget/number_cell.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../../util.dart'; +import '../document_log_page.dart'; +import '../util.dart'; +import 'po_files.dart'; + +class POSubmissionForm extends StatefulWidget { + final POSubmission poSubmission; + + POSubmissionForm({this.poSubmission}); + + @override + _POSubmissionFormState createState() => _POSubmissionFormState(); +} + +class _POSubmissionFormState extends State { + final numberFormatter = new NumberFormat("#,###"); + var dateFormatter = new DateFormat('dd MMM yyyy - hh:mm a'); + MultiImgController multiImgController = MultiImgController(); + + TextEditingController _numberController = new TextEditingController(); + TextEditingController _storage = new TextEditingController(); + TextEditingController _comment = new TextEditingController(); + TextEditingController _name = new TextEditingController(); + TextEditingController _bizName = new TextEditingController(); + + List poLines = new List(); + bool _isLoading = false; + bool _isNew = true; + int _amount = 0; + String _date = "", _status = ""; + POSubmission poSubmission = POSubmission(); + AttachFile attachFile; + POFiles files = POFiles(); + + @override + void initState() { + super.initState(); + if (widget.poSubmission != null) { + _isNew = false; + poSubmission = widget.poSubmission; + + _date = dateFormatter.format(poSubmission.poDate); + _numberController.text = poSubmission.poNumber.toString(); + _comment.text = poSubmission.comment; + _name.text = poSubmission.userName; + _bizName.text = poSubmission.bizName; + poLines = poSubmission.poLines; + _status = poSubmission.status; + _storage.text = poSubmission.storageCharge.toString(); + multiImgController.setImageUrls = poSubmission.poReceiptUrls; + + Provider.of(context, listen: false) + .loadPOLines(poSubmission.id) + .then((poLines) { + setState(() { + this.poSubmission.poLines = poLines; + _amount = poSubmission.getAmount; + }); + }); + } else { + _amount = 0; + _date = dateFormatter.format(DateTime.now()); + var productModel = Provider.of(context, listen: false); + productModel.products.forEach((p) { + var _poLine = POLine( + productID: p.id, + productName: p.name, + price: p.price, + balanceQty: 0, + qty: 0, + amount: 0); + poSubmission.poLines.add(_poLine); + }); + } + } + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + var logModel = Provider.of(context); + + final dateBox = Container( + padding: EdgeInsets.only(top: 15, left: 20, bottom: 5), + child: Row( + children: [ + LocalText(context, "po.date"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text(_date, style: textStyle), + ), + ], + ), + ); + + final numberBox = Container( + padding: EdgeInsets.only(top: 5, left: 20, bottom: 5), + child: Row( + children: [ + LocalText(context, "po.number"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _numberController.text, + style: textStyle, + ), + ) + ], + ), + ); + + final userNameBox = Container( + padding: EdgeInsets.only(top: 5, left: 20, bottom: 5), + child: Row( + children: [ + LocalText(context, "po.name"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _name.text, + style: textStyle, + ), + ) + ], + ), + ); + final bizNameBox = Container( + padding: EdgeInsets.only(top: 5, left: 20, bottom: 5), + child: Row( + children: [ + LocalText(context, "po.biz"), + Container( + padding: EdgeInsets.only(left: 20), + child: Text( + _bizName.text, + style: textStyle, + ), + ) + ], + ), + ); + + final statusBox = Container( + padding: EdgeInsets.only(top: 5, left: 20), + child: Row( + children: [ + LocalText(context, "po.status"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _status, + style: textStyle, + ), + ) + ], + ), + ); + final commentBox = Container( + padding: EdgeInsets.only(top: 5, left: 20), + child: Row( + children: [ + LocalText(context, "po.comment"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _comment.text, + style: textStyle, + ), + ) + ], + ), + ); + + final amountBox = Container( + padding: EdgeInsets.only(top: 5, left: 10), + child: labeledText(context, formatNumber(_amount), "po.amount", + number: true)); + + final storageBox = Container( + padding: EdgeInsets.only(top: 5, left: 20), + child: Row( + children: [ + LocalText(context, "po.storage_charge"), + Container( + padding: EdgeInsets.only(left: 10), + child: Text( + _storage.text, + style: textStyle, + ), + ) + ], + ), + ); + final poPaymentBox = Container( + padding: EdgeInsets.only(left: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocalText(context, "po.po_payment_receipt"), + MultiImageFile( + enabled: isBuyer + ? _isNew || this.poSubmission.isPending() ? true : false + : false, + controller: multiImgController, + title: "Receipt File", + ) + ])); + + final storagePaymentBox = Container( + padding: EdgeInsets.only(left: 20), + child: Row(children: [ + LocalText(context, "po.storage_receipt"), + ImageFile( + enabled: isBuyer + ? _isNew || this.poSubmission.isPending() ? true : false + : false, + initialImgUrl: this.poSubmission.storageReceiptUrl, + title: "Receipt File", + onFile: (file) { + this.files.setStorageChargeFile = file; + }), + ])); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("po"), + style: Provider.of(context).isEng + ? TextStyle(fontSize: 18) + : TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode')), + actions: [ + _isNew || !mainModel.showHistoryBtn() + ? Container() + : IconButton( + icon: Icon(Icons.history), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DocumentLogPage(docID: poSubmission.id)), + ); + }, + ), + isBuyer && (_isNew || poSubmission.isPending()) + ? IconButton( + icon: Icon(Icons.send), + onPressed: () { + showConfirmDialog(context, "po.confirm", () { + _submit(); + }); + }, + ) + : Container(), + isBuyer + ? Container() + : PopupMenuButton( + onSelected: _select, + itemBuilder: (context) => [ + PopupMenuItem( + enabled: poSubmission.isPending(), + value: 1, + child: Text("Approve PO"), + ), + PopupMenuItem( + enabled: poSubmission.isPending(), + value: 2, + child: Text("Reject PO"), + ), + PopupMenuItem( + enabled: mainModel.user.isOwner() && + poSubmission.isApproved(), + value: 3, + child: Text("Cancel PO"), + ), + ], + ), + ], + ), + // floatingActionButton: isBuyer && (_isNew || poSubmission.isPending()) + // ? FloatingActionButton( + // backgroundColor: primaryColor, + // child: Icon(Icons.add), + // onPressed: () async { + // final POLine poLine = await Navigator.push( + // context, + // MaterialPageRoute(builder: (context) => POItem()), + // ); + // _save(poLine); + // }, + // ) + // : null, + body: Container( + child: ListView( + children: [ + dateBox, + Divider(), + _isNew ? Container() : numberBox, + _isNew ? Container() : Divider(), + _isNew ? Container() : userNameBox, + _isNew ? Container() : Divider(), + _isNew ? Container() : bizNameBox, + _isNew ? Container() : Divider(), + _isNew ? Container() : statusBox, + _isNew || + widget.poSubmission.comment == null || + widget.poSubmission.comment == '' + ? Container() + : Divider(), + _isNew || + widget.poSubmission.comment == null || + widget.poSubmission.comment == '' + ? Container() + : commentBox, + _isNew ? Container() : Divider(), + amountBox, + Divider(), + poPaymentBox, + Divider(), + _isNew || !poSubmission.hasStorageCharge() + ? Container() + : storageBox, + _isNew || !poSubmission.hasStorageCharge() + ? Container() + : Divider(), + _isNew || !poSubmission.hasStorageCharge() + ? Container() + : storagePaymentBox, + _isNew || !poSubmission.hasStorageCharge() + ? Container() + : Divider(), + Container( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MyDataTable( + headingRowHeight: 40, + columnSpacing: 7, + columns: _isNew + ? [ + MyDataColumn( + label: LocalText(context, "po.product")), + MyDataColumn( + label: LocalText(context, "po.price"), + numeric: true), + MyDataColumn( + label: LocalText(context, "po.volume"), + numeric: true), + MyDataColumn( + label: LocalText(context, "po.amount"), + numeric: true), + ] + : [ + MyDataColumn( + label: LocalText(context, "po.product")), + MyDataColumn( + label: LocalText(context, "po.price"), + numeric: true), + MyDataColumn( + label: LocalText(context, "po.balance.volume"), + numeric: true), + MyDataColumn( + label: LocalText(context, "po.volume"), + numeric: true), + MyDataColumn( + label: LocalText(context, "po.amount"), + numeric: true), + ], + rows: getProductRow(poSubmission.poLines), + ), + ), + ), + ], + ), + ), + ), + ); + } + + _select(s) { + if (s == 1) { + showConfirmDialog(context, "po.approve.confirm", () { + _approve(); + }); + } else if (s == 2) { + showCommentDialog(context, (comment) { + this.poSubmission.comment = comment; + _reject(); + }); + } else if (s == 3) { + showConfirmDialog(context, "po.cancel.confirm", () { + _cancel(); + }); + } + } + + List getProductRow(List poLines) { + MainModel mainModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + + ProductModel productModel = Provider.of(context); + if (poLines.isNotEmpty) { + poLines.forEach((d) { + productModel.products.forEach((p) { + if (p.id == d.productID) { + d.displayOrder = p.displayOrder; + } else { + return; + } + }); + }); + + poLines.sort((p1, p2) => p1.displayOrder.compareTo(p2.displayOrder)); + } + + return poLines.map((p) { + return MyDataRow( + onSelectChanged: (bool selected) async { + if (!isBuyer) return; + + if (_isNew || this.poSubmission.isPending()) { + var poLine = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => POItem( + poLine: p, + )), + ); + _save(poLine); + } + }, + cells: _isNew + ? [ + MyDataCell( + new Text( + p.productName, + style: textStyle, + ), + ), + MyDataCell(NumberCell(p.price)), + MyDataCell( + Container( + alignment: Alignment.centerRight, + width: 100, + child: NumberCell( + p.qty, + textStyle: textStyleOdd, + ), + ), + ), + MyDataCell(NumberCell(p.amount)), + ] + : [ + MyDataCell( + new Text( + p.productName, + style: textStyle, + ), + ), + MyDataCell(NumberCell(p.price)), + MyDataCell(NumberCell(p.balanceQty)), + MyDataCell(NumberCell(p.qty)), + MyDataCell(NumberCell(p.amount)), + ], + ); + }).toList(); + } + + _save(POLine poLine) { + if (poLine == null) return; + if (poLine.action == "create") { + if (poSubmission.poLines.contains(poLine)) { + showMsgDialog(context, "Error", "Duplicate line"); + return; + } + poSubmission.poLines.add(poLine); + } else if (poLine.action == "delete") { + poSubmission.poLines.remove(poLine); + } + setState(() { + _amount = poSubmission.getAmount; + }); + } + + _submit() async { + if (poSubmission.poLines.length == 0) { + showMsgDialog(context, "Error", "No product line"); + return; + } + List _poLines = []; + poSubmission.poLines.forEach((p) => p.qty <= 0 ? _poLines.add(p) : p); + poSubmission.poLines.removeWhere((p) => p.qty <= 0); + + setState(() { + _isLoading = true; + }); + try { + POSubmissionModel poModel = Provider.of(context); + if (_isNew) { + await poModel.createPO(poSubmission, multiImgController.getAddedFile); + } else { + if (poSubmission.hasStorageCharge()) { + if (files.storageChargeFile == null) { + showMsgDialog( + context, "Error", "Please insert storage charge file"); + return; + } + } + await poModel.updatePO(poSubmission, multiImgController.getAddedFile, + multiImgController.getDeletedUrl); + } + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + _poLines.forEach((e) { + if (!poSubmission.poLines.contains(e)) poSubmission.poLines.add(e); + }); + }); + } + } + + _approve() async { + setState(() { + _isLoading = true; + }); + var oldStatus = poSubmission.status; + try { + POSubmissionModel poModel = Provider.of(context); + + poSubmission.status = "approved"; + await poModel.approvePO(poSubmission); + Navigator.pop(context); + } catch (e) { + poSubmission.status = oldStatus; + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _reject() async { + setState(() { + _isLoading = true; + }); + try { + POSubmissionModel poModel = Provider.of(context); + await poModel.rejectPO(poSubmission); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + _cancel() async { + setState(() { + _isLoading = true; + }); + try { + POSubmissionModel poModel = Provider.of(context); + await poModel.cancelPO(poSubmission); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/po/po_submission_list.dart b/lib/pages/po/po_submission_list.dart new file mode 100644 index 0000000..5e24e93 --- /dev/null +++ b/lib/pages/po/po_submission_list.dart @@ -0,0 +1,291 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/po_model.dart'; +import 'package:fcs/pages/po/po_submission_form.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/po.dart'; +import 'package:fcs/vo/popup_menu.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/popupmenu.dart'; +import 'package:fcs/widget/progress.dart'; + +class SubmissionList extends StatefulWidget { + @override + _SubmissionListState createState() => _SubmissionListState(); +} + +class _SubmissionListState extends State { + var dateFormatter = new DateFormat('dd MMM yyyy'); + final double dotSize = 10.0; + PopupMenu selectedChoices = poMenus[0]; + POSubmission poSubmission; + DateTime _selectedDate = DateTime.now(); + String status; + int _selectedIndex = 0; + int _dateIndex = 0; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + + var poModel = Provider.of(context, listen: false); + _selectedIndex = poModel.popupMenu.index; + _dateIndex = poModel.dateIndex; + _selectedDate = poModel.selectedDate; + } + + @override + void dispose() { + super.dispose(); + } + + Future _selectDate(BuildContext context) async { + var poSubmissionModel = Provider.of(context); + + final DateTime picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101), + builder: (BuildContext context, Widget child) { + return Theme( + data: ThemeData.light().copyWith( + primaryColor: primaryColor, //Head background + accentColor: secondaryColor, //selection color + dialogBackgroundColor: Colors.white, //Background color + ), + child: child, + ); + }, + ); + if (picked != null) { + var pickedDate = new DateTime(picked.year, picked.month, picked.day); + var currentDate = new DateTime( + DateTime.now().year, DateTime.now().month, DateTime.now().day); + + this._dateIndex = pickedDate == currentDate ? 0 : 1; + setState(() { + _selectedDate = picked; + poSubmissionModel.filterData( + this.status, _selectedDate, this._selectedIndex, this._dateIndex); + }); + } + } + + @override + Widget build(BuildContext context) { + var poSubmissionModel = Provider.of(context); + MainModel mainModel = Provider.of(context); + bool isBuyer = mainModel.user.isBuyer(); + var languageModle = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text( + AppTranslations.of(context).text("po.title"), + style: languageModle.isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode'), + ), + actions: [ + InkWell( + child: Container( + padding: EdgeInsets.only(top: 15), + child: Stack( + children: [ + Image.asset( + "assets/date_filter.png", + color: Colors.white, + width: 25, + ), + _dateIndex == 0 + ? Container() + : Positioned( + bottom: 15, + right: 10, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + ], + ), + ), + onTap: () => _selectDate(context), + ), + PopupMenuButton( + elevation: 3.2, + onSelected: (selected) { + setState(() { + _selectedIndex = selected.index; + }); + if (selected.status == 'All') { + status = null; + } else { + status = selected.status; + } + poSubmissionModel.filterData( + status, _selectedDate, _selectedIndex, _dateIndex); + }, + icon: Container( + width: 30, + height: 30, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Icon( + Icons.filter_list, + color: primaryColor, + ), + _selectedIndex != 0 + ? Positioned( + bottom: 0, + right: 0, + child: Container( + width: 10, + height: 10, + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + ), + ) + : Container() + ], + )), + itemBuilder: (BuildContext context) { + return statusMenu.map((PopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Row( + children: [ + Text(choice.status), + SizedBox( + width: 10, + ), + _selectedIndex != null && + _selectedIndex == choice.index + ? Icon( + Icons.check, + color: Colors.grey, + ) + : Container(), + ], + ), + ); + }).toList(); + }), + ], + ), + floatingActionButton: isBuyer + ? FloatingActionButton( + backgroundColor: primaryColor, + heroTag: "btn2", + onPressed: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => POSubmissionForm()), + ), + child: Icon(Icons.add), + ) + : null, + body: new ListView.builder( + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: poSubmissionModel.pos.length, + itemBuilder: (BuildContext context, int index) { + return Card( + elevation: 10, + color: Colors.white, + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => POSubmissionForm( + poSubmission: poSubmissionModel.pos[index], + )), + ); + }, + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 15.0 - dotSize / 2), + child: Padding( + padding: EdgeInsets.all(5.0), + child: Image.asset( + "assets/pay.png", + width: 40, + height: 40, + color: primaryColor, + ), + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Text( + poSubmissionModel.pos[index].poNumber + .toString(), + style: new TextStyle( + fontSize: 12.0, color: Colors.black), + ), + new Text( + dateFormatter.format( + poSubmissionModel.pos[index].poDate), + style: new TextStyle( + fontSize: 12.0, color: Colors.grey), + ), + !isBuyer + ? new Text( + poSubmissionModel + .pos[index].userName + .toString(), + style: new TextStyle( + fontSize: 12.0, + color: Colors.grey), + ) + : Container() + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 18.0), + child: getStatus(poSubmissionModel.pos[index].status), + ), + ], + ), + ), + ); + }), + ), + ); + } +} diff --git a/lib/pages/product_edit_item.dart b/lib/pages/product_edit_item.dart new file mode 100644 index 0000000..d2b3ad5 --- /dev/null +++ b/lib/pages/product_edit_item.dart @@ -0,0 +1,223 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; + +class ProductEditItem extends StatefulWidget { + final Product product; + + const ProductEditItem({Key key, this.product}) : super(key: key); + @override + _ProductEditItemState createState() => _ProductEditItemState(); +} + +class _ProductEditItemState extends State { + TextEditingController nameController = new TextEditingController(); + TextEditingController priceController = new TextEditingController(); + TextEditingController orderController = new TextEditingController(); + int color = primaryColor.value; + bool isDisable = false; + + final _formKey = GlobalKey(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + + if (widget.product != null) { + this.color = widget.product.color; + nameController.text = widget.product.name; + priceController.text = widget.product.price.toString(); + orderController.text = widget.product.displayOrder.toString(); + if (widget.product.isDisable != null) { + isDisable = widget.product.isDisable; + } else { + isDisable = false; + } + } + } + + @override + Widget build(BuildContext context) { + var maingModel = Provider.of(context); + final nameWidget = Container( + padding: EdgeInsets.only(top: 10), + child: TextFormField( + controller: nameController, + autofocus: true, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + icon: InkWell( + child: Icon( + FontAwesomeIcons.tag, + color: Color(this.color), + size: 25, + ), + onTap: () => showColorPicker(context, Color(this.color), (color) { + setState(() { + this.color = color.value; + }); + }), + ), + labelText: AppTranslations.of(context).text("product.name"), + labelStyle: maingModel.isEng ? labelStyle : labelStyleMM, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("product.name_empty"); + } + return null; + }, + ), + ); + + final priceInput = Container( + child: TextFormField( + controller: priceController, + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal), + keyboardType: TextInputType.number, + cursorColor: primaryColor, + decoration: new InputDecoration( + icon: Icon( + FontAwesomeIcons.moneyBill, + color: primaryColor, + size: 25, + ), + border: InputBorder.none, + labelText: AppTranslations.of(context).text("product.new_price"), + labelStyle: maingModel.isEng ? labelStyle : labelStyleMM, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("product.price_empty"); + } + return null; + }, + ), + ); + + final orderInput = Container( + child: TextFormField( + controller: orderController, + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal), + keyboardType: TextInputType.number, + cursorColor: primaryColor, + decoration: new InputDecoration( + icon: Icon( + Icons.sort, + color: primaryColor, + size: 25, + ), + border: InputBorder.none, + labelText: AppTranslations.of(context).text("product.order"), + labelStyle: maingModel.isEng ? labelStyle : labelStyleMM, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("product.order_empty"); + } + return null; + }, + ), + ); + + final disableBox = Container( + padding: EdgeInsets.only(top: 10), + child: CheckboxListTile( + title: Text("Disable"), + value: isDisable, + activeColor: primaryColor, + onChanged: (value) { + setState(() { + isDisable = value; + }); + }, + controlAffinity: ListTileControlAffinity.leading, + )); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: Text(AppTranslations.of(context).text("product.item"), + style: maingModel.isEng + ? TextStyle(color: Colors.white, fontSize: 20.0) + : TextStyle( + color: Colors.white, + fontSize: 20.0, + fontFamily: 'MyanmarUnicode')), + actions: [ + IconButton( + icon: Icon(Icons.save), + onPressed: () { + if (!_formKey.currentState.validate()) return; + Provider.of(context, listen: false).saveProduct( + widget.product, + nameController.text, + priceController.text, + orderController.text, + color, + isDisable); + Product _product = new Product(); + if (widget.product != null) { + _product = widget.product; + _product.name = nameController.text; + _product.price = int.parse(priceController.text); + _product.color = color; + _product.displayOrder = int.parse(orderController.text); + _product.isDisable = isDisable; + if (_product.id == null) { + _product.action = "create"; + } else { + _product.action = "update"; + } + } + Navigator.pop(context, _product); + }, + ) + ], + backgroundColor: primaryColor, + ), + body: Form( + key: _formKey, + child: SingleChildScrollView( + padding: EdgeInsets.only( + left: 25.0, + right: 25.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + nameWidget, + priceInput, + orderInput, + this.widget.product != null ? disableBox : Container(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/products_list.dart b/lib/pages/products_list.dart new file mode 100644 index 0000000..f3edc40 --- /dev/null +++ b/lib/pages/products_list.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/products.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../model/product_model.dart'; +import 'products_list_edit.dart'; + +class ProductsList extends StatefulWidget { + @override + _ProductsListState createState() => _ProductsListState(); +} + +class _ProductsListState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var productModel = Provider.of(context); + var mainModel = Provider.of(context); + bool _isLoading = false; + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("products.title"), + style: Provider.of(context).isEng + ? TextStyle() + : TextStyle(fontFamily: 'MyanmarUnicode')), + actions: [ + mainModel.user != null && mainModel.user.isOwner() || + mainModel.user.hasAdmin() + ? IconButton( + icon: Icon(Icons.edit), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProductsListEdit( + products: productModel.productsToEdit, + )), + ); + }, + ) + : Container() + ], + ), + backgroundColor: Colors.white, + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.only(top: 5), + child: ProductsWidget(isWelcomePage: false), + ), + ], + )), + ); + } +} diff --git a/lib/pages/products_list_edit.dart b/lib/pages/products_list_edit.dart new file mode 100644 index 0000000..66f6031 --- /dev/null +++ b/lib/pages/products_list_edit.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/product_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/product.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'product_edit_item.dart'; + +class ProductsListEdit extends StatefulWidget { + final List products; + + const ProductsListEdit({Key key, this.products}) : super(key: key); + + @override + _ProductsListEditState createState() => _ProductsListEditState(); +} + +class _ProductsListEditState extends State { + List products; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + products = widget.products; + } + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text(AppTranslations.of(context).text("products.title"), + style: Provider.of(context).isEng + ? TextStyle(color: Colors.white, fontSize: 20.0) + : TextStyle( + color: Colors.white, + fontSize: 20.0, + fontFamily: 'MyanmarUnicode')), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + _update(context); + }, + ) + ], + ), + floatingActionButton: FloatingActionButton( + backgroundColor: primaryColor, + child: Icon(Icons.add), + onPressed: _add, + ), + body: ListView( + shrinkWrap: true, + padding: EdgeInsets.all(5.0), + children: [ + new ListView.builder( + padding: EdgeInsets.only(left: 15, right: 15, top: 15), + shrinkWrap: true, + itemCount: products.length, + itemBuilder: (BuildContext context, int index) { + return _row(products[index]); + }), + ], + ), + ), + ); + } + + _add() { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => ProductEditItem()), + ); + } + + void _update(BuildContext context) async { + await showConfirmDialog(context, "product.confirm", () async { + setState(() { + _isLoading = true; + }); + try { + var productModel = Provider.of(context); + await productModel.updateProducts(this.products); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + }); + } + + Widget _row(Product product) { + return Container( + child: InkWell( + child: Card( + elevation: 10, + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0), + child: new Row( + children: [ + InkWell( + child: new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 20.0 - 10 / 2), + child: Icon( + FontAwesomeIcons.tag, + color: Color(product.color), + size: 30, + ), + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: product.isDisable == true + ? TextStyle( + decoration: + TextDecoration.lineThrough, + color: Colors.red) + : null, + ) + ], + ), + ), + ], + ), + ), + ), + Column(children: [ + product.id == null + ? Text("New Product") + : Text("Old Price: ${product.oldPirce}"), + Text("New Price: ${product.price}") + ]), + ], + ), + ), + ), + onTap: () async { + Product _p = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProductEditItem( + product: product, + )), + ); + if (_p == null) return; + setState(() { + product = _p; + }); + }), + ); + } +} diff --git a/lib/pages/profile_edit.dart b/lib/pages/profile_edit.dart new file mode 100644 index 0000000..ef76977 --- /dev/null +++ b/lib/pages/profile_edit.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; + +typedef void ProfileCallback(); + +class ProfileEdit extends StatefulWidget { + @override + _ProfileEditState createState() => _ProfileEditState(); +} + +class _ProfileEditState extends State { + final TextEditingController nameController = new TextEditingController(); + bool _loading = false; + + @override + void initState() { + super.initState(); + MainModel mainModel = Provider.of(context, listen: false); + nameController.text = mainModel.user.name; + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + + final name = Container( + padding: EdgeInsets.only(top: 0, left: 20, right: 15, bottom: 30), + child: TextFormField( + controller: nameController, + autofocus: true, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("profile.name"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + Icons.account_box, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + )); + + final saveBtn = Card( + elevation: 23, + child: ButtonTheme( + minWidth: 200.0, + height: 50.0, + child: FlatButton.icon( + onPressed: () => _save(), + label: Text(AppTranslations.of(context).text("btn.save"), + style: languageModel.isEng + ? TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal) + : TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontFamily: "MyanmarUnicode")), + icon: Icon( + Icons.save, + ), + ), + ), + ); + + return LocalProgress( + inAsyncCall: _loading, + child: Scaffold( + appBar: AppBar( + title: Text( + AppTranslations.of(context).text("profile.edit_title"), + ), + backgroundColor: primaryColor, + ), + body: Column( + children: [ + name, + saveBtn, + ], + ), + ), + ); + } + + _save() async { + setState(() { + _loading = true; + }); + try { + await Provider.of(context, listen: false) + .updateProfile(nameController.text); + Navigator.pop(context); + setState(() { + _loading = false; + }); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } + } +} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart new file mode 100644 index 0000000..a0e0c83 --- /dev/null +++ b/lib/pages/profile_page.dart @@ -0,0 +1,392 @@ +import 'package:flutter/material.dart'; +import 'package:package_info/package_info.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/pages/profile_edit.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/role.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/localization/transalation.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'profile_setting.dart'; + +typedef void ProfileCallback(); + +class Profile extends StatefulWidget { + @override + _ProfileState createState() => _ProfileState(); +} + +class _ProfileState extends State { + bool _isLoading = false; + String selectedLanguage; + TextEditingController bizNameController = new TextEditingController(); + + static final List languagesList = Translation().supportedLanguages; + static final List languageCodesList = + Translation().supportedLanguagesCodes; + + final Map languagesMap = { + languagesList[0]: languageCodesList[0], + languagesList[1]: languageCodesList[1], + }; + + buildLanguage(LanguageModel languageModel) async { + var lan = await languageModel.load(); + if (this.selectedLanguage != lan) { + setState(() { + this.selectedLanguage = lan; + }); + } + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + MainModel mainModel = Provider.of(context); + + buildLanguage(languageModel); + _selectedDropdown(String selected) { + setState(() { + selectedLanguage = selected; + languageModel.saveLanguage(selectedLanguage); + }); + } + + final namebox = Container( + padding: EdgeInsets.only(top: 10), + child: Container( + height: 45.0, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 0.0), + child: Text( + AppTranslations.of(context).text("profile.name"), + style: languageModel.isEng + ? TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal) + : TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontFamily: "MyanmarUnicode"), + ), + ), + SizedBox( + width: 30, + ), + Container( + child: Center( + child: Text( + mainModel.user == null ? "" : mainModel.user.name, + style: + TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal), + ), + ), + ) + ], + ), + )); + final phonenumberbox = Container( + height: 45.0, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 0.0), + child: Text( + AppTranslations.of(context).text("profile.phone"), + style: languageModel.isEng + ? TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal) + : TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontFamily: "MyanmarUnicode"), + ), + ), + SizedBox( + width: 27, + ), + Container( + child: Center( + child: Text( + mainModel.user == null + ? "" + : mainModel.user.phone == null ? '' : mainModel.user.phone, + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal), + ), + ), + ) + ], + ), + ); + final emailBox = Container( + padding: EdgeInsets.only(top: 10, left: 0), + child: Row( + children: [ + Text( + AppTranslations.of(context).text("profile.email"), + style: languageModel.isEng + ? TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal) + : TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontFamily: "MyanmarUnicode"), + ), + SizedBox( + width: 35, + ), + Text( + mainModel.user == null + ? "" + : mainModel.user.email == null || mainModel.user.email == '' + ? '' + : mainModel.user.email, + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal), + ), + ], + ), + ); + final languageBox = Container( + padding: EdgeInsets.only(bottom: 15, top: 7), + child: Container( + height: 45.0, + child: Row( + children: [ + Text( + AppTranslations.of(context).text("profile.language"), + style: languageModel.isEng + ? TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal) + : TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontFamily: "MyanmarUnicode"), + ), + Container( + width: 140, + padding: EdgeInsets.only(left: 30), + child: Theme( + data: new ThemeData( + canvasColor: Colors.white, + ), + child: DropdownButton( + hint: Text("English"), + value: selectedLanguage, + isExpanded: true, + iconSize: 40, + items: languagesList + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: _selectedDropdown), + )), + ], + ), + )); + + final logoutbutton = Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: Card( + elevation: 23, + child: Container( + height: 45.0, + child: ButtonTheme( + minWidth: 900.0, + height: 100.0, + child: FlatButton.icon( + onPressed: () { + showConfirmDialog(context, "profile.logout.confirm", + () async { + setState(() { + _isLoading = true; + }); + await mainModel.logout(); + // Navigator.of(context) + // .pushNamedAndRemoveUntil("/", ModalRoute.withName('/')); + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + }); + }, + label: Text(AppTranslations.of(context).text("profile.logout"), + style: languageModel.isEng + ? TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal) + : TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontFamily: "MyanmarUnicode")), + icon: Icon( + Icons.exit_to_app, + ), + ), + ), + ), + )); + Future getVersionNumber() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String version = packageInfo.version + "+" + packageInfo.buildNumber; + + return version; + } + + final versionbox = Container( + padding: EdgeInsets.only(top: 15), + child: Container( + child: Center( + child: FutureBuilder( + future: getVersionNumber(), + builder: (BuildContext context, AsyncSnapshot snapshot) => + Text( + snapshot.hasData ? "v${snapshot.data}" : "Loading ...", + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal), + ), + )), + )); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: Text( + AppTranslations.of(context).text("profile.title"), + ), + backgroundColor: primaryColor, + actions: [ + IconButton( + icon: Icon(Icons.edit), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => ProfileEdit()), + ); + }, + ), + IconButton( + icon: Icon(Icons.settings), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => ProfileSetting()), + ); + }, + ) + ], + ), + body: ListView( + padding: EdgeInsets.only( + left: 25.0, + right: 25.0, + ), + shrinkWrap: true, + children: [ + namebox, + mainModel.isBuyer() ? Container() : getPrivilegeBox(context), + phonenumberbox, + mainModel.user == null + ? Container() + : mainModel.user.email == null || mainModel.user.email == '' + ? Container() + : emailBox, + languageBox, + logoutbutton, + Divider(color: secondaryColor), + versionbox, + SizedBox( + height: 20, + ) + ], + ), + ), + ); + } + + Widget getPrivilegeBox(BuildContext context) { + var languageModel = Provider.of(context); + var userModel = Provider.of(context); + + return ListTileTheme( + contentPadding: EdgeInsets.all(0), + child: ExpansionTile( + title: Text( + AppTranslations.of(context).text("profile.privilege"), + style: languageModel.isEng + ? TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + ) + : TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.normal, + fontFamily: "MyanmarUnicode"), + ), + children: [ + Align( + alignment: Alignment.topLeft, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + getRowPrivilegeWidget(userModel.getUserPrivileges())), + ), + ) + ], + ), + ); + } + + List getRowPrivilegeWidget(List privileges) { + return privileges.map((p) { + return Container( + padding: EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(p.name, + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal)), + SizedBox( + width: 30, + ), + Container( + child: Text( + "- ${p.desc}", + style: TextStyle(fontSize: 16.0, fontStyle: FontStyle.normal), + ), + ) + ], + ), + ); + }).toList(); + } +} diff --git a/lib/pages/profile_setting.dart b/lib/pages/profile_setting.dart new file mode 100644 index 0000000..5a4b459 --- /dev/null +++ b/lib/pages/profile_setting.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'add_pin_editor.dart'; +import 'block_list.dart'; +import 'chage_phone_number.dart'; +import 'change_password.dart'; +import 'device_list.dart'; +import 'email_page.dart'; +import 'log_list.dart'; + +class ProfileSetting extends StatefulWidget { + @override + _ProfileSettingtate createState() => _ProfileSettingtate(); +} + +class _ProfileSettingtate extends State { + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + MainModel mainModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + title: LocalText( + context, + "setting.title", + fontSize: 20, + color: Colors.white, + ), + backgroundColor: primaryColor, + ), + body: SingleChildScrollView( + padding: EdgeInsets.only( + left: 25.0, + right: 25.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: 8, + ), + buildSettingTile( + context: context, + text: 'log.title', + image: "assets/message.png", + width: 25, + height: 25, + tap: () { + Navigator.push(context, + MaterialPageRoute(builder: (context) => LogList())); + }, + ), + buildSettingTile( + context: context, + text: 'profile.devices', + image: "assets/device.png", + width: 29, + height: 29, + tap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PhoneDeviceList())); + }, + ), + buildSettingTile( + context: context, + text: 'change.password.title', + image: "assets/password.png", + width: 27, + height: 27, + tap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ChangePassword(mainModel.user))); + }, + ), + buildSettingTile( + context: context, + text: 'change.phone', + image: "assets/phone.png", + width: 30, + height: 25, + tap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ChangePhoneNumber(mainModel.user))); + }, + ), + buildSettingTile( + context: context, + text: 'change.email', + image: "assets/email.png", + width: 25, + height: 25, + tap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmailPage( + user: mainModel.user, + ))); + }, + ), + // buildSettingTile( + // context: context, + // text: 'user.block_list', + // image: "assets/block.png", + // width: 27, + // height: 27, + // tap: () { + // Navigator.push(context, + // MaterialPageRoute(builder: (context) => BlockList())); + // }, + // ), + buildSettingTile( + context: context, + text: 'change.pin.title', + image: "assets/pin.png", + width: 30, + height: 30, + tap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddPINEditor( + mainModel.user, + ))); + }, + ), + ], + ), + ), + ), + ); + } +} + +Widget buildSettingTile( + {@required String text, + @required BuildContext context, + @required String image, + @required double width, + @required double height, + @required GestureTapCallback tap}) { + return InkWell( + onTap: () { + tap(); + }, + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 5), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 12.0, right: 20.0), + child: Image.asset( + image, + width: width, + height: height, + color: primaryColor, + ), + ), + LocalText( + context, + text, + fontSize: 15.0, + color: Colors.black87, + ) + ], + ), + ), + Icon(Icons.keyboard_arrow_right) + ], + ), + ), + Divider( + color: Colors.grey, + ) + ], + ), + ); +} diff --git a/lib/pages/quota_form.dart b/lib/pages/quota_form.dart new file mode 100644 index 0000000..d91bf39 --- /dev/null +++ b/lib/pages/quota_form.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +class QuotaForm extends StatefulWidget { + final BuyerProduct buyerProduct; + const QuotaForm({Key key, this.buyerProduct}) : super(key: key); + @override + _QuotaFormState createState() => _QuotaFormState(); +} + +class _QuotaFormState extends State { + final _formKey = GlobalKey(); + TextEditingController _product = new TextEditingController(); + TextEditingController _storageQty = new TextEditingController(); + TextEditingController _saleQty = new TextEditingController(); + TextEditingController _dailyQuota = new TextEditingController(); + TextEditingController _maxQuota = new TextEditingController(); + + BuyerProduct buyerProduct; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + if (widget.buyerProduct != null) { + this.buyerProduct = widget.buyerProduct; + _product.text = widget.buyerProduct.productName; + _storageQty.text = widget.buyerProduct.storageCapacityQty == null + ? "" + : widget.buyerProduct.storageCapacityQty.toString(); + _saleQty.text = widget.buyerProduct.dailySaleQty == null + ? "" + : widget.buyerProduct.dailySaleQty.toString(); + _dailyQuota.text = widget.buyerProduct.dailyQuota == null + ? "" + : widget.buyerProduct.dailyQuota.toString(); + _maxQuota.text = widget.buyerProduct.maxQuota == null + ? "" + : widget.buyerProduct.maxQuota.toString(); + } + } + + @override + Widget build(BuildContext context) { + var language = Provider.of(context); + final productbox = Container( + padding: EdgeInsets.only(top: 10), + child: Row( + children: [ + LocalText(context, "reg.table_product"), + SizedBox( + width: 30, + ), + Text(_product.text, style: textStyle) + ], + )); + + final storageQtybox = Container( + padding: EdgeInsets.only(top: 10), + child: Row( + children: [ + LocalText(context, "reg.table_storage_vol"), + SizedBox( + width: 30, + ), + Text(_storageQty.text, style: textStyle) + ], + )); + + final saleQtybox = Container( + padding: EdgeInsets.only(top: 10), + child: Row( + children: [ + LocalText(context, "reg.table_sale_vol"), + SizedBox( + width: 25, + ), + Text(_saleQty.text, style: textStyle) + ], + )); + + final dailyQuotaBox = Container( + child: TextFormField( + controller: _dailyQuota, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text('buyer.quota'), + labelStyle: language.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.sortNumericUpAlt, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return "Please enter quota"; + } + return null; + }, + ), + ); + + final maxQuotaBox = Container( + child: TextFormField( + controller: _maxQuota, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text('buyer.max.quota'), + labelStyle: language.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.sortNumericUpAlt, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return "Please enter quota"; + } + return null; + }, + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text("Quota"), + actions: [ + IconButton( + icon: Icon(Icons.save), + onPressed: () { + if (!_formKey.currentState.validate()) return; + buyerProduct.dailyQuota = int.parse(_dailyQuota.text); + buyerProduct.maxQuota = int.parse(_maxQuota.text); + Navigator.pop(context, buyerProduct); + }, + ) + ], + ), + body: Form( + key: _formKey, + child: Container( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + productbox, + storageQtybox, + saleQtybox, + dailyQuotaBox, + maxQuotaBox + ], + ), + ), + )), + ); + } +} diff --git a/lib/pages/quota_page.dart b/lib/pages/quota_page.dart new file mode 100644 index 0000000..1590579 --- /dev/null +++ b/lib/pages/quota_page.dart @@ -0,0 +1,216 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/buyer_model.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/vo/buyer.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; + +class QuotaPage extends StatefulWidget { + final Buyer buyer; + final bool isApproved; + const QuotaPage({this.buyer, this.isApproved}); + @override + _QuotaPageState createState() => _QuotaPageState(); +} + +class _QuotaPageState extends State { + final formKey = GlobalKey(); + + TextEditingController _dailyQuota = new TextEditingController(); + TextEditingController _maxQuota = new TextEditingController(); + + TextEditingController _companyName = new TextEditingController(); + TextEditingController _accountName = new TextEditingController(); + Buyer buyer = Buyer(); + bool _isLoading = false; + + @override + void initState() { + super.initState(); + if (widget.buyer != null) { + this.buyer = widget.buyer; + _companyName.text = buyer.bizName; + _accountName.text = buyer.userName; + _dailyQuota.text = buyer.dailyQuota.toString(); + _maxQuota.text = buyer.maxQuota.toString(); + Provider.of(context, listen: false) + .loadBuyerProducts(buyer, force: true) + .then((b) { + setState(() { + buyer = b; + }); + }); + } + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + + final companyNameBox = Container( + padding: EdgeInsets.only(top: 0, left: 20, right: 20), + child: TextFormField( + controller: _companyName, + autofocus: false, + cursorColor: primaryColor, + readOnly: true, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("reg.biz_name"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Icon( + Icons.business, + color: primaryColor, + ), + ), + )); + final accountNameBox = Container( + padding: EdgeInsets.only(top: 0, left: 20, right: 20), + child: TextFormField( + controller: _accountName, + autofocus: false, + cursorColor: primaryColor, + readOnly: true, + style: textStyle, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("buyer.account_name"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Icon( + Icons.business, + color: primaryColor, + ), + ), + )); + + final dailyQuotaBox = Container( + padding: EdgeInsets.only(top: 0, left: 20, right: 15), + child: TextFormField( + controller: _dailyQuota, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + keyboardType: TextInputType.number, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("reg.quota"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + Icons.business, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: _validateQuota, + )); + + final maxQuotaBox = Container( + padding: EdgeInsets.only(top: 0, left: 20, right: 15), + child: TextFormField( + controller: _maxQuota, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + keyboardType: TextInputType.number, + decoration: new InputDecoration( + labelText: AppTranslations.of(context).text("reg.max_quota"), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + Icons.business, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: _validateQuota, + )); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: Text("Quota"), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + showConfirmDialog(context, "buyer.allocate.quota.confirm", () { + _allocate(); + }); + }, + ) + ], + ), + body: Form( + key: formKey, + child: Container( + padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + child: ListView( + children: [ + Container( + height: 500, + child: Card( + elevation: 23, + child: Column( + children: [ + accountNameBox, + companyNameBox, + dailyQuotaBox, + maxQuotaBox, + ], + ), + ), + ) + ], + ), + ), + ), + ), + ); + } + + String _validateQuota(value) { + if (value.isEmpty) { + return "Invalid number"; + } + return null; + } + + _allocate() async { + if (!formKey.currentState.validate()) { + return; + } + setState(() { + _isLoading = true; + }); + try { + buyer.dailyQuota = int.parse(_dailyQuota.text); + buyer.maxQuota = int.parse(_maxQuota.text); + if (widget.isApproved) { + await Provider.of(context).allocate(buyer); + Navigator.pop(context, true); + } else { + Navigator.pop(context, this.buyer); + } + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/report_user_editor.dart b/lib/pages/report_user_editor.dart new file mode 100644 index 0000000..74b8972 --- /dev/null +++ b/lib/pages/report_user_editor.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/report_user_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/report.dart'; +import 'package:fcs/vo/report_user.dart'; +import 'package:fcs/vo/user.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; +import 'util.dart'; + +typedef void FindCallBack(); + +class ReportUserEditor extends StatefulWidget { + final Report report; + const ReportUserEditor({this.report}); + @override + _ReportUserEditorState createState() => _ReportUserEditorState(); +} + +class _ReportUserEditorState extends State { + TextEditingController _name = new TextEditingController(); + TextEditingController _searchInput = new TextEditingController(); + final GlobalKey _scaffoldKey = new GlobalKey(); + + bool _isLoading = false; + bool isSend = false; + User selectedUser; + List _users = []; + int selectedIndex; + + @override + void initState() { + super.initState(); + } + + Widget searchInputBox(BuildContext context, FindCallBack findCallBack) { + var languageModel = Provider.of(context); + return Container( + padding: EdgeInsets.only(top: 10, left: 15, right: 15), + child: Stack( + alignment: const Alignment(1.2, 1.0), + children: [ + TextFormField( + controller: _searchInput, + autofocus: false, + cursorColor: primaryColor, + style: textStyle, + decoration: new InputDecoration( + labelText: + AppTranslations.of(context).text('report.user.search'), + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + ), + new FlatButton( + onPressed: () { + findCallBack(); + }, + child: new Icon( + Icons.search, + size: 25, + )) + ], + )); + } + + @override + Widget build(BuildContext context) { + final namebox = Container( + padding: EdgeInsets.only(left: 10, top: 10), + child: TextFormField( + controller: _name, + autofocus: false, + readOnly: true, + cursorColor: primaryColor, + decoration: new InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + icon: Icon( + Icons.person, + color: primaryColor, + ), + ), + ), + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + key: _scaffoldKey, + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText(context, "user.title", + fontSize: 20, color: Colors.white), + actions: [ + IconButton( + icon: Icon( + Icons.save, + color: Colors.white, + ), + onPressed: () { + _save(context); + }) + ], + ), + body: Column( + children: [ + Expanded( + child: ListView( + shrinkWrap: true, + children: [ + searchInputBox(context, () => _findUser(context)), + this.isSend ? namebox : Container(), + Container( + padding: EdgeInsets.only( + top: this.isSend ? 10 : 20, left: 20, right: 20), + child: Column( + children: _getUsers(context), + ), + ), + ], + ), + ), + SizedBox( + height: 20, + ) + ], + )), + ); + } + + _findUser(BuildContext context) async { + var reportUserModel = Provider.of(context); + if (_searchInput.text == '') { + showMsgDialog(context, "Error", 'Please fill the search field'); + } + setState(() { + _isLoading = true; + }); + + try { + List users = await reportUserModel.findUser(_searchInput.text); + if (users.isEmpty) return; + setState(() { + this._users = users; + }); + } catch (e) { + setState(() { + this.isSend = false; + }); + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + List _getUsers(BuildContext context) { + return _users.asMap().entries.map((u) { + return Container( + child: Card( + elevation: 10, + color: Colors.white, + child: InkWell( + onTap: () { + setState(() { + this.selectedUser = u.value; + isSend = true; + _name.text = selectedUser.name; + selectedIndex = u.key; + }); + }, + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - 15.0 / 2), + child: Icon( + Icons.account_circle, + color: Colors.grey, + size: 50, + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + u.value.name == null ? "" : u.value.name, + style: new TextStyle( + fontSize: 15.0, color: Colors.black), + ), + ], + ), + ], + ), + ), + ), + selectedIndex != null && selectedIndex == u.key + ? Container( + padding: EdgeInsets.only(right: 25), + child: Icon(Icons.check)) + : Container() + ], + ), + ), + )); + }).toList(); + } + + _save(BuildContext context) async { + var reportUserModel = Provider.of(context); + + if (selectedUser == null) return; + + setState(() { + _isLoading = true; + }); + + reportUserModel.getUsersForReport(widget.report.id).then((users) async { + if (users.any((u) => u.userID == this.selectedUser.docID)) { + setState(() { + _isLoading = false; + }); + showMsgDialog(context, "Error", 'Duplicate User'); + } else { + try { + ReportUser _reportUser = ReportUser( + userID: this.selectedUser.docID, + userName: this.selectedUser.name, + reportID: widget.report.id, + reportName: widget.report.display); + + await reportUserModel.assignUser(_reportUser); + reportUserModel.getUsersForReport(widget.report.id).then((users) { + Navigator.pop>(context, users); + }); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } + }); + } +} diff --git a/lib/pages/report_user_list.dart b/lib/pages/report_user_list.dart new file mode 100644 index 0000000..bcb8c72 --- /dev/null +++ b/lib/pages/report_user_list.dart @@ -0,0 +1,220 @@ +import 'package:provider/provider.dart'; +import 'package:fcs/model/report_user_model.dart'; +import 'package:fcs/vo/report.dart'; +import 'package:fcs/vo/report_user.dart'; +import 'package:fcs/widget/local_text.dart'; + +import 'package:flutter/material.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart'; +import 'report_user_editor.dart'; +import 'util.dart'; + +class ReportUserList extends StatefulWidget { + final Report report; + + const ReportUserList({Key key, this.report}) : super(key: key); + @override + _ReportUserListState createState() => _ReportUserListState(); +} + +class _ReportUserListState extends State { + Report _report = new Report(); + final double dotSize = 15.0; + bool _isLoading = false; + List _users = []; + bool isForAllUsers = false; + + @override + void initState() { + super.initState(); + + if (widget.report != null) { + this._report = widget.report; + var reportUserModel = + Provider.of(context, listen: false); + reportUserModel.getUsersForReport(this._report.id).then((users) { + if (mounted) { + setState(() { + this._users = users; + }); + } + }); + this.isForAllUsers = + widget.report.forAllUser == null ? false : widget.report.forAllUser; + } + } + + @override + Widget build(BuildContext context) { + final allUserBox = Container( + child: new ListTile( + title: new Row( + children: [ + new Checkbox( + value: isForAllUsers, + activeColor: primaryColor, + onChanged: (bool value) async { + setState(() { + _isLoading = true; + }); + try { + setState(() { + this.isForAllUsers = value; + }); + this._report.forAllUser = this.isForAllUsers; + var reportUserModel = Provider.of(context); + await reportUserModel.updateReportForAllUsers(this._report); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + }), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Text( + 'All Users', + style: TextStyle( + fontSize: 15.0, + ), + ), + ], + ), + ], + ))); + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + 'report.users.title', + translationVariables: [this._report.display], + color: Colors.white, + fontSize: 18, + ), + ), + floatingActionButton: FloatingActionButton( + backgroundColor: primaryColor, + child: Icon(Icons.add), + onPressed: () async { + List _us = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ReportUserEditor(report: widget.report)), + ); + + if (_us == null) return; + setState(() { + _users.clear(); + _users.addAll(_us); + }); + }, + ), + body: ListView( + shrinkWrap: true, + children: [ + allUserBox, + Container( + padding: EdgeInsets.only(left: 10, right: 10), + child: Column( + children: _getUserRow(context), + ), + ), + SizedBox(height: 15) + ], + ), + ), + ); + } + + List _getUserRow(BuildContext context) { + _users.sort((a, b) => a.userName.compareTo(b.userName)); + return _users.map((u) { + return Container( + child: Card( + elevation: 10, + color: Colors.white, + child: InkWell( + onTap: () {}, + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 32.0 - dotSize / 2), + child: Icon( + Icons.account_circle, + color: primaryColor, + size: 50, + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + u.userName == null ? "" : u.userName, + style: new TextStyle( + fontSize: 17.0, color: Colors.black), + ), + ], + ), + ], + ), + ), + ), + IconButton( + padding: EdgeInsets.only(right: 10), + icon: Icon( + Icons.delete, + color: Colors.grey[700], + ), + onPressed: () { + showConfirmDialog(context, "report.user_delete_confirm", + () { + _delete(context, u); + }); + }) + ], + ), + ), + )); + }).toList(); + } + + void _delete(BuildContext context, ReportUser reportUser) async { + setState(() { + _isLoading = true; + }); + try { + var reportUserModel = Provider.of(context); + await reportUserModel.deleteReportUser(reportUser); + + reportUserModel.getUsersForReport(widget.report.id).then((users) { + if (mounted) { + setState(() { + this._users = users; + }); + } + }); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/reset_password.dart b/lib/pages/reset_password.dart new file mode 100644 index 0000000..121b10d --- /dev/null +++ b/lib/pages/reset_password.dart @@ -0,0 +1,368 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/user_model.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/localization/app_translations.dart'; +import 'package:fcs/widget/progress.dart'; + +import '../theme/theme.dart' as Theme; +import 'util.dart'; + +class ResetPasswordPage extends StatefulWidget { + final String phoneNumber; + ResetPasswordPage( + this.phoneNumber, { + Key key, + }) : super(key: key); + + @override + _ResetPasswordPageState createState() => new _ResetPasswordPageState(); +} + +class _ResetPasswordPageState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _scaffoldKey = new GlobalKey(); + + final FocusNode myFocusNodePassword = FocusNode(); + final FocusNode myFocusNodeEmail = FocusNode(); + + bool _obscureTextLogin = true; + bool _obscureTextSignup = true; + bool _obscureTextSignupConfirm = true; + + TextEditingController _smsController = new TextEditingController(); + TextEditingController _passwordController = new TextEditingController(); + TextEditingController _confirmPasswordController = + new TextEditingController(); + final formKey = GlobalKey(); + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + key: _scaffoldKey, + body: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height >= 775.0 + ? MediaQuery.of(context).size.height + : 580.0, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.only(top: 35.0, bottom: 10), + child: ListTile( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: LocalText( + context, + 'reset.password.title', + color: Colors.black87, + fontSize: 17, + ), + ), + ), + Expanded( + flex: 2, + child: PageView( + children: [ + new ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: _buildReset(context), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + void dispose() { + myFocusNodePassword.dispose(); + myFocusNodeEmail.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + // SystemChrome.setPreferredOrientations([ + // DeviceOrientation.portraitUp, + // DeviceOrientation.portraitDown, + // ]); + + _smsController.text = ""; + } + + Widget _buildReset(BuildContext context) { + return Container( + child: ListView( + children: [ + Column( + children: [ + Form( + key: formKey, + child: Card( + elevation: 2.0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + child: Container( + width: 300.0, + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodeEmail, + controller: _smsController, + keyboardType: TextInputType.phone, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.sms, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("reset.sms"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + ), + validator: _validateSMSCode), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + focusNode: myFocusNodePassword, + controller: _passwordController, + obscureText: _obscureTextSignup, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("reset.new_password"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleSignup, + child: Icon( + _obscureTextSignup + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validatePassword, + ), + ), + Container( + width: 250.0, + height: 1.0, + color: Colors.grey[400], + ), + Padding( + padding: EdgeInsets.only(left: 25.0, right: 25.0), + child: TextFormField( + controller: _confirmPasswordController, + obscureText: _obscureTextSignupConfirm, + style: TextStyle( + fontFamily: "WorkSansSemiBold", + fontSize: 16.0, + color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + icon: Icon( + FontAwesomeIcons.lock, + color: Colors.black, + ), + labelText: AppTranslations.of(context) + .text("login.confirm_password"), + labelStyle: + Provider.of(context).isEng + ? TextStyle( + fontFamily: "WorkSansSemiBold", + color: Colors.grey) + : TextStyle( + fontFamily: "MyanmarUnicode", + color: Colors.grey), + suffixIcon: GestureDetector( + onTap: _toggleSignupConfirm, + child: Icon( + _obscureTextSignupConfirm + ? FontAwesomeIcons.eye + : FontAwesomeIcons.eyeSlash, + size: 15.0, + color: Colors.black, + ), + ), + ), + validator: _validateConfirmPassword, + ), + ), + ], + ), + ), + ), + ), + Container( + // margin: EdgeInsets.only(top: 320.0), + decoration: new BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.primaryColor, + ), + child: MaterialButton( + highlightColor: Colors.transparent, + splashColor: Theme.LoginColors.loginGradientEnd, + //shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 42.0), + child: Text( + AppTranslations.of(context).text("reset"), + style: Provider.of(context).isEng + ? TextStyle( + color: Colors.white, + fontSize: 18.0, + fontFamily: "WorkSansBold") + : TextStyle( + color: Colors.white, + fontSize: 18.0, + fontFamily: "MyanmarUnicode"), + ), + ), + onPressed: () => _reset(context)), + ), + ], + ), + ], + ), + ); + } + + void _toggleLogin() { + setState(() { + _obscureTextLogin = !_obscureTextLogin; + }); + } + + void _toggleSignup() { + setState(() { + _obscureTextSignup = !_obscureTextSignup; + }); + } + + void _toggleSignupConfirm() { + setState(() { + _obscureTextSignupConfirm = !_obscureTextSignupConfirm; + }); + } + + void _reset(BuildContext context) async { + if (!formKey.currentState.validate()) { + return; + } + + var smsCode = _smsController.text; + var password = _passwordController.text; + + setState(() { + _isLoading = true; + }); + + UserModel userModel = Provider.of(context); + try { + await userModel.resetPassword(widget.phoneNumber, password, smsCode); + Navigator.pushNamedAndRemoveUntil(context, "/login", (r) => false); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + }); + } + } + + String _validatePassword(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.password_empty"); + } + if (value.length < 6) { + return AppTranslations.of(context).text("login.password_size"); + } + return null; + } + + String _validateConfirmPassword(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.confirm_password_empty"); + } + if (value.length < 6) { + return AppTranslations.of(context).text("login.password_size"); + } + if (value != _passwordController.text) { + return AppTranslations.of(context).text("login.password_mismatch"); + } + return null; + } + + String _validateSMSCode(value) { + if (value.isEmpty) { + return AppTranslations.of(context).text("login.sms_empty"); + } + if (value.length != 6) { + return AppTranslations.of(context).text("login.sms_size"); + } + return null; + } +} diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart new file mode 100644 index 0000000..e53f10b --- /dev/null +++ b/lib/pages/search_page.dart @@ -0,0 +1,113 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/buyer_model.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/buyer.dart'; + +import 'buyer_list_row.dart'; + +Future showPlacesSearch(BuildContext context) async => + await showSearch( + context: context, + delegate: UserSearchDelegate(), + ); + +class UserSearchDelegate extends SearchDelegate { + @override + ThemeData appBarTheme(BuildContext context) { + final ThemeData theme = Theme.of(context); + return theme.copyWith( + inputDecorationTheme: InputDecorationTheme( + hintStyle: TextStyle( + color: theme.primaryTextTheme.title.color, fontSize: 16)), + primaryColor: primaryColor, + primaryIconTheme: theme.primaryIconTheme.copyWith(color: Colors.white), + primaryColorBrightness: Brightness.light, + primaryTextTheme: theme.textTheme, + textTheme: theme.textTheme.copyWith( + title: theme.textTheme.title.copyWith( + color: theme.primaryTextTheme.title.color, fontSize: 16)), + ); + } + + @override + List buildActions(BuildContext context) { + return [ + IconButton( + icon: Icon(Icons.clear), + onPressed: () => query = '', + ), + ]; + } + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () => close(context, null), + ); + } + + @override + Widget buildResults(BuildContext context) { + final buyerModel = Provider.of(context); + return FutureBuilder( + future: buyerModel.search(query), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + if (snapshot.data.length == 0) { + return Container( + child: Center( + child: Text( + "Error :No Search Buyer", + textAlign: TextAlign.center, + ), + ), + ); + } + return Container( + padding: EdgeInsets.only(top: 15), + child: ListView( + children: + snapshot.data.map((u) => BuyerListRow(buyer: u)).toList(), + ), + ); + } else if (snapshot.hasError) { + return Container( + child: Center( + child: Text( + '${snapshot.error}', + textAlign: TextAlign.center, + ), + ), + ); + } else { + return Container( + child: Center( + child: CircularProgressIndicator( + valueColor: + new AlwaysStoppedAnimation(primaryColor)), + ), + ); + } + }); + } + + @override + Widget buildSuggestions(BuildContext context) { + return Container( + child: Center( + child: Opacity( + opacity: 0.2, + child: Icon( + Icons.supervised_user_circle, + size: 200, + )), + ), + ); + } +} diff --git a/lib/pages/setting_editor.dart b/lib/pages/setting_editor.dart new file mode 100644 index 0000000..70c2b7c --- /dev/null +++ b/lib/pages/setting_editor.dart @@ -0,0 +1,416 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/language_model.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/setting.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +class SettingEidtor extends StatefulWidget { + final Setting setting; + const SettingEidtor({this.setting}); + @override + _SettingEidtorState createState() => _SettingEidtorState(); +} + +class _SettingEidtorState extends State { + TextEditingController _doExpire = new TextEditingController(); + TextEditingController _poExpire = new TextEditingController(); + TextEditingController _poOpend = new TextEditingController(); + TextEditingController _poClosed = new TextEditingController(); + TextEditingController _latestDeliveryDay = new TextEditingController(); + TextEditingController _firstStorageDay = new TextEditingController(); + TextEditingController _firstStorageCharge = new TextEditingController(); + TextEditingController _secondStorageDay = new TextEditingController(); + TextEditingController _secondStorgeCharge = new TextEditingController(); + + final _formKey = GlobalKey(); + bool _isLoading = false; + List days = []; + + @override + void initState() { + super.initState(); + days = dayLists; + if (widget.setting != null) { + _doExpire.text = widget.setting.doExpireInHours.toString(); + _poExpire.text = widget.setting.poExpireInHours.toString(); + _poOpend.text = widget.setting.poOpenAt.toString(); + _poClosed.text = widget.setting.poCloseAt.toString(); + _latestDeliveryDay.text =widget.setting.latestDeliveryDay.toString(); + _firstStorageDay.text = widget.setting.firstStorageChargeIn.toString(); + _firstStorageCharge.text = widget.setting.firstStorageCharge.toString(); + _secondStorageDay.text = widget.setting.secondStorageChargeIn.toString(); + _secondStorgeCharge.text = widget.setting.secondStorageCharge.toString(); + + days.forEach((d) => widget.setting.poCloseOn.contains(d.id) + ? d.isChecked = true + : d.isChecked = false); + } + } + + Widget showDayList(BuildContext context, MainModel mainModel) { + return Container( + margin: EdgeInsets.symmetric(vertical: 5.0), + height: 75.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 10, top: 10), + child: Text( + "PO submission closed Day", + style: TextStyle(color: Colors.black54), + ), + ), + Expanded( + child: ListView.builder( + itemCount: days.length, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only(top: 10), + itemBuilder: (BuildContext context, int index) { + return new Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Checkbox( + value: days[index].isChecked == null + ? false + : days[index].isChecked, + activeColor: primaryColor, + onChanged: (bool value) { + setState(() { + days[index].isChecked = value; + }); + }), + Container( + padding: EdgeInsets.only(top: 13), + child: new Text( + dayLists[index].name, + style: TextStyle( + fontSize: 15.0, + ), + ), + ), + ], + ); + }), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + var languageModel = Provider.of(context); + var mainModel = Provider.of(context); + + final doExpireBox = TextFormField( + controller: _doExpire, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'DO expired Time', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.clock, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter DO expired Time'; + } + return null; + }, + ); + + final poExpireBox = TextFormField( + controller: _poExpire, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'PO expired Time', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.clock, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter PO expired Time'; + } + return null; + }, + ); + + final poOpenedBox = TextFormField( + controller: _poOpend, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'PO submission opened Time', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.clock, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter PO submission opened Time'; + } + return null; + }, + ); + + final poClosedBox = TextFormField( + controller: _poClosed, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'PO submission closed Time', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.clock, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter PO submission closed Time'; + } + return null; + }, + ); + + final latestDeliveryDayBox = TextFormField( + controller: _latestDeliveryDay, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'Latest Delivery Day', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.clock, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter Latest Delivery Day'; + } + return null; + }, + ); + + final firstStorageDayBox = TextFormField( + controller: _firstStorageDay, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'First storage charge starts Day', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.calendarDay, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter First storage charge starts Day'; + } + return null; + }, + ); + + final firstStorgeChargeBox = TextFormField( + controller: _firstStorageCharge, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'First storage charge Fees', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.sortNumericUp, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter First storage charge Fees'; + } + return null; + }, + ); + + final secondStorgeDayBox = TextFormField( + controller: _secondStorageDay, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'Second storage charge starts Day', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.calendarDay, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter Second storage charge starts Day'; + } + return null; + }, + ); + + final secondStorgeChargeBox = TextFormField( + controller: _secondStorgeCharge, + autofocus: false, + cursorColor: primaryColor, + keyboardType: TextInputType.phone, + style: textStyle, + decoration: new InputDecoration( + labelText: 'Second storage charge Fees', + labelStyle: languageModel.isEng ? labelStyle : labelStyleMM, + icon: Icon( + FontAwesomeIcons.sortNumericUp, + color: primaryColor, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryColor, width: 1.0)), + ), + validator: (value) { + if (value.isEmpty) { + return 'Please enter Second storage charge Fees'; + } + return null; + }, + ); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + "setting.title", + fontSize: 20, + color: Colors.white, + ), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + if (!_formKey.currentState.validate()) return; + showConfirmDialog(context, "setting.confirm", () { + _submit(); + }); + }) + ], + ), + body: Form( + key: _formKey, + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + // doExpireBox, + poExpireBox, + poOpenedBox, + poClosedBox, + showDayList(context, mainModel), + latestDeliveryDayBox, + firstStorageDayBox, + firstStorgeChargeBox, + secondStorgeDayBox, + secondStorgeChargeBox + ], + ), + ), + )); + } + + _submit() async { + setState(() { + _isLoading = true; + }); + try { + widget.setting.poExpireInHours = int.parse(_poExpire.text); + widget.setting.poOpenAt = int.parse(_poOpend.text); + widget.setting.poCloseAt = int.parse(_poClosed.text); + widget.setting.latestDeliveryDay=int.parse(_latestDeliveryDay.text); + widget.setting.firstStorageChargeIn = int.parse(_firstStorageDay.text); + widget.setting.firstStorageCharge = int.parse(_firstStorageCharge.text); + widget.setting.secondStorageChargeIn = int.parse(_secondStorageDay.text); + widget.setting.secondStorageCharge = int.parse(_secondStorgeCharge.text); + widget.setting.poCloseOn = + this.days.where((d) => d.isChecked == true).map((p) => p.id).toList(); + var mainModel = Provider.of(context); + await mainModel.updateSetting(widget.setting); + Navigator.pop(context); + } catch (e) { + showMsgDialog(context, "Error", e.toString()); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/pages/setting_editor_byOwner.dart b/lib/pages/setting_editor_byOwner.dart new file mode 100644 index 0000000..59be90a --- /dev/null +++ b/lib/pages/setting_editor_byOwner.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:fcs/model/main_model.dart'; +import 'package:fcs/pages/util.dart'; +import 'package:fcs/theme/theme.dart'; +import 'package:fcs/vo/setting.dart'; +import 'package:fcs/widget/local_text.dart'; +import 'package:fcs/widget/progress.dart'; + +class Time { + final int value; + Time(this.value); + String get getValue => + value > 12 ? (value - 12).toString() + "PM" : value.toString() + "AM"; + + @override + bool operator ==(other) { + if (identical(this, other)) { + return true; + } + return other.value == this.value; + } + + @override + int get hashCode { + int result = 17; + result = 37 * result + value.hashCode; + return result; + } +} + +class SettingOwner extends StatefulWidget { + final Setting setting; + const SettingOwner({this.setting}); + @override + _SettingOwnerState createState() => _SettingOwnerState(); +} + +class _SettingOwnerState extends State { + final _formKey = GlobalKey(); + bool _isLoading = false; + List days = []; + int poOpenAt = 0; + int poCloseAt = 0; + + @override + void initState() { + super.initState(); + days = dayLists; + if (widget.setting != null) { + poOpenAt = widget.setting.poOpenAt; + poCloseAt = widget.setting.poCloseAt; + days.forEach((d) => widget.setting.poCloseOn.contains(d.id) + ? d.isChecked = true + : d.isChecked = false); + } + } + + Widget showDayList(BuildContext context, MainModel mainModel) { + return Container( + margin: EdgeInsets.symmetric(vertical: 5.0), + height: 500.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 10, top: 10), + child: Text( + "PO submission closed Day", + style: TextStyle(color: Colors.black54), + ), + ), + Expanded( + child: ListView.builder( + itemCount: days.length, + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(top: 10), + itemBuilder: (BuildContext context, int index) { + return new Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Checkbox( + value: days[index].isChecked == null + ? false + : days[index].isChecked, + activeColor: primaryColor, + onChanged: (bool value) { + setState(() { + days[index].isChecked = value; + }); + }), + Container( + padding: EdgeInsets.only(top: 13), + child: new Text( + dayLists[index].name, + style: TextStyle( + fontSize: 15.0, + ), + ), + ), + ], + ); + }), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + var mainModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + title: LocalText( + context, + "setting.title", + fontSize: 20, + color: Colors.white, + ), + actions: [ + IconButton( + icon: Icon(Icons.send), + onPressed: () { + if (!_formKey.currentState.validate()) return; + showConfirmDialog(context, "setting.confirm", () { + _submit(); + }); + }) + ], + ), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.only(left: 24), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text("PO submission opened at:"), + ), + getDropDown(poOpenAt, (value) { + setState(() { + setState(() { + this.poOpenAt = value; + }); + }); + }), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(left: 24), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text("PO submission closed at:"), + ), + getDropDown(poCloseAt, (value) { + setState(() { + setState(() { + this.poCloseAt = value; + }); + }); + }), + ], + ), + ), + Form( + key: _formKey, + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + showDayList(context, mainModel), + ], + ), + ), + ], + ), + )); + } + + Widget getDropDown(int initial, Function(int) onChanged) { + Time value = Time(initial); + return DropdownButton