diff --git a/assets/l10n/en/menu.yaml b/assets/l10n/en/menu.yaml index 976260eb..4e8042e2 100644 --- a/assets/l10n/en/menu.yaml +++ b/assets/l10n/en/menu.yaml @@ -4,18 +4,48 @@ subtitle: Categories, Products tutorial: title: Create Your Menu content: Let's start by creating a menu! + createExample: Help create an example menu to test. search: hint: Search for products, ingredients, quantities notFound: Couldn't find relevant information. Did you misspell something? +example: + catalog: + burger: Burgers + drink: Drinks + side: Side + other: Others + product: + cheeseBurger: Cheese Burger + veggieBurger: Veggie Burger + hamBurger: Ham Burger + cola: Cola + coffee: Coffee + fries: Fries + straw: Straw + plasticBag: Plastic Bag + ingredient: + cheese: Cheese + lettuce: Lettuce + tomato: Tomato + bun: Bun + chili: Chili Sauce + ham: Ham + cola: Can of Cola + coffee: Drip Coffee + fries: Bag of Fries + straw: Straw + plasticBag: Plastic Bag + quantity: + small: Small + large: Large + none: None catalog: headerInfo: - Categories - Displayed on the upper rectangle in homepage - tutorial: - title: Create First Catalog emptyBody: |- - Similar "products" will be grouped under "categories", - making it convenient for ordering, such as: + Similar products will be grouped under categories, + making ordering convenient, such as: • "Cheese Burger", "Veggie Burger" > "Burgers" • "Plastic Bag", "Eco Cup" > "Others" title: @@ -26,7 +56,8 @@ catalog: reorder: Reorder Categories dialogDeletionContent: - =0: No products inside - other: Will delete {count} products together + =1: Will also delete {count} related product + other: Will also delete {count} related products - Warning message when deleting product categories on the menu page - count: {type: int, mode: plural} name: diff --git a/assets/l10n/en/order.yaml b/assets/l10n/en/order.yaml index fadf5984..c5d497e4 100644 --- a/assets/l10n/en/order.yaml +++ b/assets/l10n/en/order.yaml @@ -1,6 +1,11 @@ $prefix: order title: Ordering btn: Order +tutorial: + title: Ordering! + content: | + Once you have set up your menu, you can start ordering! + Let's tap and go see what's available! snackbar: cashier: notEnough: Insufficient cash in the cashier! diff --git a/assets/l10n/en/order_attribute.yaml b/assets/l10n/en/order_attribute.yaml index e5120f47..1d99581c 100644 --- a/assets/l10n/en/order_attribute.yaml +++ b/assets/l10n/en/order_attribute.yaml @@ -17,6 +17,24 @@ tutorial: content: |- This is where you set customer information, such as dine-in, takeout, office worker, etc. This information helps us track who comes to consume and make better business strategies. + createExample: Help create an example to test. +example: + age: Age + _age: + $prefix: age + child: Child + adult: Adult + senior: Senior + place: Place + _place: + $prefix: place + takeout: Takeout + dineIn: Dine-in + ecoFriendly: Eco-Friendly + _ecoFriendly: + $prefix: ecoFriendly + reusableBottle: Reusable Bottle + reusableBag: Reusable Bag meta: mode: - 'Mode: {name}' diff --git a/assets/l10n/en/transit.yaml b/assets/l10n/en/transit.yaml index 4fc551ff..5feb5c9c 100644 --- a/assets/l10n/en/transit.yaml +++ b/assets/l10n/en/transit.yaml @@ -1,12 +1,6 @@ $prefix: transit title: Data Transfer description: Importing and Exporting Store Information and Orders -tutorial: - title: Sync Multiple Devices - content: |- - This is where you can import/export menu, inventory, order records, and other information. - - We provide two methods: Google Sheets and plain text, making it convenient to sync data across different devices. method: title: Please Select Transfer Method name: @@ -67,7 +61,7 @@ export: preview: btn: Preview title: Preview Output Result - btn: Import + btn: Export import: preview: btn: Preview @@ -86,7 +80,7 @@ import: header: After import, old ingredients won't be removed to avoid affecting the "Menu" status. quantity: header: After import, old quantities won't be removed to avoid affecting the "Menu" status. - btn: Export + btn: Import error: columnCount: - Insufficient data, {columns} columns required diff --git a/assets/l10n/zh/menu.yaml b/assets/l10n/zh/menu.yaml index 34332f4a..6de91419 100644 --- a/assets/l10n/zh/menu.yaml +++ b/assets/l10n/zh/menu.yaml @@ -4,13 +4,43 @@ subtitle: 產品種類、產品 tutorial: title: 建立屬於你的菜單 content: 首先我們來開始建立一份菜單吧! + createExample: 幫助建立一份範例菜單以供測試。 search: hint: 搜尋產品、成分、份量 notFound: 搜尋不到相關資訊,打錯字了嗎? +example: + catalog: + burger: 漢堡 + drink: 飲品 + side: 點心 + other: 其他 + product: + cheeseBurger: 起司漢堡 + veggieBurger: 蔬菜漢堡 + hamBurger: 火腿漢堡 + cola: 可樂 + coffee: 咖啡 + fries: 薯條 + straw: 吸管 + plasticBag: 塑膠袋 + ingredient: + cheese: 起司 + lettuce: 萵苣 + tomato: 番茄 + bun: 麵包 + chili: 醬料 + ham: 火腿 + cola: 可樂罐 + coffee: 濾掛咖啡包 + fries: 薯條(300g) + straw: 吸管(根) + plasticBag: 塑膠袋(個) + quantity: + small: 少量 + large: 增量 + none: 無 catalog: headerInfo: 種類 - tutorial: - title: Create First Catalog emptyBody: |- 我們會把相似「產品」放在「產品種類」中, 到時候點餐會比較方便,例如: diff --git a/assets/l10n/zh/order.yaml b/assets/l10n/zh/order.yaml index 5337292b..c3267677 100644 --- a/assets/l10n/zh/order.yaml +++ b/assets/l10n/zh/order.yaml @@ -1,6 +1,11 @@ $prefix: order title: 點餐 btn: 點餐 +tutorial: + title: 開始點餐! + content: | + 一旦設定好菜單,就可以開始點餐囉 + 讓我們趕緊進去看看有什麼吧! snackbar: cashier: notEnough: 收銀機錢不夠找囉! diff --git a/assets/l10n/zh/order_attribute.yaml b/assets/l10n/zh/order_attribute.yaml index 7f08d188..26b21b17 100644 --- a/assets/l10n/zh/order_attribute.yaml +++ b/assets/l10n/zh/order_attribute.yaml @@ -15,6 +15,24 @@ tutorial: content: |- 這裡是用來設定顧客的資訊,例如:內用、外帶、上班族等。 這些資訊可以幫助我們統計哪些人來消費,進而做出更好的經營策略。 + createExample: 幫助建立一份範例以供測試。 +example: + age: 年齡 + _age: + $prefix: age + child: 小孩 + adult: 成人 + senior: 長者 + place: 位置 + _place: + $prefix: place + takeout: 外帶 + dineIn: 內用 + ecoFriendly: 環保 + _ecoFriendly: + $prefix: ecoFriendly + reusableBottle: 環保杯 + reusableBag: 環保袋 meta: mode: 種類:{name} default: 預設:{name} diff --git a/assets/l10n/zh/transit.yaml b/assets/l10n/zh/transit.yaml index b861a1de..b6ab5443 100644 --- a/assets/l10n/zh/transit.yaml +++ b/assets/l10n/zh/transit.yaml @@ -1,12 +1,6 @@ $prefix: "transit" title: 資料轉移 description: 匯入、匯出店家資訊和訂單 -tutorial: - title: 同步多台裝置 - content: |- - 這裡是用來匯入匯出菜單、庫存、訂單記錄等資訊的地方。 - - 我們提供了 Google 試算表和純文字兩種方式,讓您可以方便地在不同裝置間同步資料。 method: title: 請選擇欲轉移的方式 name: @@ -51,7 +45,7 @@ export: preview: btn: 預覽 title: 預覽輸出結果 - btn: 匯入 + btn: 匯出 import: preview: btn: 預覽 @@ -66,7 +60,7 @@ import: header: 匯入後,為了避免影響「菜單」的狀況,並不會把舊的成分移除。 quantity: header: 匯入後,為了避免影響「菜單」的狀況,並不會把舊的份量移除。 - btn: 匯出 + btn: 匯入 error: columnCount: 資料量不足,需要 {columns} 個欄位 duplicate: 將忽略本行,相同的項目已於前面出現 diff --git a/lib/components/style/date_range_picker.dart b/lib/components/style/date_range_picker.dart index 07fc4e40..bb7a2f75 100644 --- a/lib/components/style/date_range_picker.dart +++ b/lib/components/style/date_range_picker.dart @@ -16,7 +16,7 @@ Future showMyDateRangePicker(BuildContext context, DateTimeRange initialEntryMode: DatePickerEntryMode.calendarOnly, firstDate: DateTime(2021, 1), lastDate: DateTime.now(), - locale: LanguageSetting.instance.value.locale, + locale: LanguageSetting.instance.language.locale, /// TODO: should fix this bug /// Wrapping the design, because the background will use a slightly diff --git a/lib/components/tutorial.dart b/lib/components/tutorial.dart index 74e3c792..8c6fce1d 100644 --- a/lib/components/tutorial.dart +++ b/lib/components/tutorial.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:possystem/services/cache.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; @@ -31,12 +30,19 @@ class TutorialWrapper extends StatelessWidget { class Tutorial extends StatelessWidget { final String id; - final String? title; - + /// index of the tutorial + /// + /// 0-based index, if not provided, the tutorial will be ordered by + /// the time of the widget built. final int? index; + final String? title; + final String message; + /// widget to be placed below the [message] + final Widget? below; + final SpotlightBuilder spotlightBuilder; final EdgeInsets padding; @@ -44,32 +50,29 @@ class Tutorial extends StatelessWidget { /// force disabling tutorial final bool disable; + /// if true, the tutorial will only be shown when the widget is 100% visible final bool monitorVisibility; final Widget child; - final SpotlightDurationConfig duration; + /// action to be executed after the tutorial is dismissed + final Future Function()? action; - final String? route; + static bool debug = false; const Tutorial({ super.key, required this.id, + this.index, this.title, required this.message, - this.index, + this.below, this.spotlightBuilder = const SpotlightCircularBuilder(), this.padding = const EdgeInsets.all(8), this.disable = false, this.monitorVisibility = false, - this.route, + this.action, required this.child, - this.duration = const SpotlightDurationConfig( - bump: Duration(milliseconds: 500), - zoomIn: Duration(milliseconds: 600), - zoomOut: Duration(milliseconds: 600), - contentFadeIn: Duration(milliseconds: 200), - ), }); @override @@ -78,31 +81,30 @@ class Tutorial extends StatelessWidget { return child; } + final theme = Theme.of(context); + SpotlightAnt.debug = true; return SpotlightAnt( enable: enabled, index: index, - duration: duration, + duration: debug ? SpotlightDurationConfig.zero : const SpotlightDurationConfig(), monitorId: monitorVisibility ? 'tutorial.$id' : null, onDismiss: _onDismiss, - onDismissed: route != null ? () => context.goNamed(route!) : null, + onDismissed: action, spotlight: SpotlightConfig( builder: spotlightBuilder, padding: padding, - onTap: route != null ? () async => SpotlightAntAction.skip : null, ), - backdrop: SpotlightBackdropConfig(silent: route != null), + backdrop: const SpotlightBackdropConfig(), action: const SpotlightActionConfig( enabled: [SpotlightAntAction.prev, SpotlightAntAction.next], ), content: SpotlightContent( + fontSize: theme.textTheme.titleMedium!.fontSize, child: Column(children: [ - if (title != null) - Text( - title!, - style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), - ), + if (title != null) Text(title!, style: theme.textTheme.headlineMedium!.copyWith(color: Colors.white)), const SizedBox(height: 16), Text(message), + if (below != null) below!, ]), ), child: child, diff --git a/lib/debug/setup_menu.dart b/lib/debug/setup_menu.dart deleted file mode 100644 index 210946df..00000000 --- a/lib/debug/setup_menu.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:developer'; - -import 'package:possystem/models/menu/catalog.dart'; -import 'package:possystem/models/objects/menu_object.dart'; -import 'package:possystem/models/repository/menu.dart'; -import 'package:possystem/models/repository/quantities.dart'; -import 'package:possystem/models/repository/stock.dart'; -import 'package:possystem/models/stock/ingredient.dart'; -import 'package:possystem/models/stock/quantity.dart'; - -Future debugSetupMenu() async { - if (Menu.instance.isNotEmpty) return; - - log('DEBUG setup stock'); - await Stock.instance.addItem(Ingredient( - id: 'cheese', - name: 'Cheese', - currentAmount: 30, - )); - await Stock.instance.addItem(Ingredient( - id: 'vegetable', - name: 'Vegetable', - currentAmount: 100, - )); - await Stock.instance.addItem(Ingredient( - id: 'bread', - name: 'Bread', - currentAmount: 50, - )); - - log('DEBUG setup quantities'); - await Quantities.instance.addItem(Quantity( - id: 'more', - name: 'More', - defaultProportion: 1.5, - )); - await Quantities.instance.addItem(Quantity( - id: 'less', - name: 'Less', - defaultProportion: 0.8, - )); - - log('DEBUG setup menu'); - await Menu.instance.addItem(Catalog.fromObject(CatalogObject.build({ - "id": "burger", - "index": 1, - "name": "Burger", - "createdAt": 1648885177, - "imagePath": null, - "products": { - "cheese-burger": { - "price": 60, - "cost": 55, - "index": 1, - "name": "Cheese Burger", - "imagePath": null, - "createdAt": 1648885807, - "ingredients": { - "cb-ingredient1": { - "ingredientId": "cheese", - "amount": 18, - "quantities": { - "cb-quantity-1": {"quantityId": "more", "amount": 18, "additionalCost": 5, "additionalPrice": 15}, - "cb-quantity-2": {"quantityId": "less", "amount": 9.0, "additionalCost": 0, "additionalPrice": 0} - } - }, - "cb-ingredient2": {"ingredientId": "bread", "amount": 15, "quantities": {}} - } - }, - "veg-burger": { - "price": 50, - "cost": 30, - "index": 2, - "name": "Veg Burger", - "imagePath": null, - "createdAt": 1648885992, - "ingredients": { - "vb-ingredient1": { - "ingredientId": "vegetable", - "amount": 10, - "quantities": { - "vb-quantity1": {"quantityId": "more", "amount": 20, "additionalCost": 2, "additionalPrice": 8}, - "vb-quantity2": {"quantityId": "less", "amount": 5.0, "additionalCost": 0, "additionalPrice": 0} - } - }, - "vb-ingredient2": {"ingredientId": "bread", "amount": 15, "quantities": {}} - } - }, - "rice-burger": { - "price": 88, - "cost": 60, - "index": 3, - "name": "Rice Burger", - "imagePath": null, - "createdAt": 1648886087, - "ingredients": {} - } - } - }))); -} diff --git a/lib/helpers/setup_example.dart b/lib/helpers/setup_example.dart new file mode 100644 index 00000000..11523ea5 --- /dev/null +++ b/lib/helpers/setup_example.dart @@ -0,0 +1,260 @@ +import 'dart:developer'; + +import 'package:possystem/models/menu/catalog.dart'; +import 'package:possystem/models/objects/menu_object.dart'; +import 'package:possystem/models/objects/order_attribute_object.dart'; +import 'package:possystem/models/order/order_attribute.dart'; +import 'package:possystem/models/order/order_attribute_option.dart'; +import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; +import 'package:possystem/models/repository/quantities.dart'; +import 'package:possystem/models/repository/stock.dart'; +import 'package:possystem/models/stock/ingredient.dart'; +import 'package:possystem/models/stock/quantity.dart'; +import 'package:possystem/translator.dart'; + +Future setupExampleMenu() async { + if (Menu.instance.isNotEmpty) return; + + log('setting stock', name: 'example menu'); + for (final e in [ + Ingredient(id: 'cheese', name: '🧀 ${S.menuExampleIngredientCheese}', currentAmount: 30, totalAmount: 30), + Ingredient(id: 'lettuce', name: '🥬 ${S.menuExampleIngredientLettuce}', currentAmount: 70, totalAmount: 70), + Ingredient(id: 'tomato', name: '🍅 ${S.menuExampleIngredientTomato}', currentAmount: 100, totalAmount: 100), + Ingredient(id: 'bun', name: '🍞 ${S.menuExampleIngredientBun}', currentAmount: 50, totalAmount: 50), + Ingredient(id: 'chili', name: '🌶 ${S.menuExampleIngredientChili}', currentAmount: 500, totalAmount: 500), + Ingredient(id: 'ham', name: '🍖 ${S.menuExampleIngredientHam}', currentAmount: 5, totalAmount: 5), + Ingredient(id: 'cola', name: '🥤 ${S.menuExampleIngredientCola}', currentAmount: 20, totalAmount: 20), + Ingredient(id: 'coffee', name: '☕️ ${S.menuExampleIngredientCoffee}', currentAmount: 50, totalAmount: 50), + Ingredient(id: 'fries', name: '🍟 ${S.menuExampleIngredientFries}', currentAmount: 3, totalAmount: 3), + Ingredient(id: 'straw', name: S.menuExampleIngredientStraw, currentAmount: 50, totalAmount: 50), + Ingredient(id: 'plasticBag', name: S.menuExampleIngredientPlasticBag, currentAmount: 50, totalAmount: 50), + ]) { + await Stock.instance.addItem(e); + } + + log('setting quantities', name: 'example menu'); + for (final e in [ + Quantity(id: 'none', name: S.menuExampleQuantityNone, defaultProportion: 0), + Quantity(id: 'small', name: S.menuExampleQuantitySmall, defaultProportion: 0.5), + Quantity(id: 'large', name: S.menuExampleQuantityLarge, defaultProportion: 1.5), + ]) { + await Quantities.instance.addItem(e); + } + + log('setting menu', name: 'example menu'); + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + for (final e in [ + Catalog.fromObject(CatalogObject.build({ + "id": "burger", + "index": 1, + "name": '🍔 ${S.menuExampleCatalogBurger}', + "createdAt": now, + "products": { + "cheese-burger": { + "price": 80, + "cost": 40, + "index": 1, + "name": S.menuExampleProductCheeseBurger, + "createdAt": now, + "ingredients": { + "cb-ingredient1": { + "ingredientId": "cheese", + "amount": 0.3, + "quantities": { + "cb1-quantity1": {"quantityId": "large", "amount": 0.5, "additionalCost": 5, "additionalPrice": 10}, + "cb1-quantity2": {"quantityId": "small", "amount": 0.1}, + } + }, + "cb-ingredient2": {"ingredientId": "bun", "amount": 1}, + "cb-ingredient3": { + "ingredientId": "lettuce", + "amount": 1, + "quantities": { + "cb3-quantity2": {"quantityId": "none", "amount": 0}, + } + }, + }, + }, + "veg-burger": { + "price": 60, + "cost": 30, + "index": 2, + "name": S.menuExampleProductVeggieBurger, + "createdAt": now, + "ingredients": { + "vb-ingredient1": { + "ingredientId": "tomato", + "amount": 0.2, + "quantities": { + "vb1-quantity1": {"quantityId": "more", "amount": 0.5, "additionalCost": 2, "additionalPrice": 5}, + "vb1-quantity2": {"quantityId": "less", "amount": 0.1}, + }, + }, + "vb-ingredient2": {"ingredientId": "bun", "amount": 1}, + "vb-ingredient3": { + "ingredientId": "lettuce", + "amount": 1, + "quantities": { + "cb3-quantity2": {"quantityId": "none", "amount": 0}, + } + }, + } + }, + "ham-burger": { + "price": 100, + "cost": 50, + "index": 3, + "name": S.menuExampleProductHamBurger, + "createdAt": now, + "ingredients": { + "hb-ingredient1": { + "ingredientId": "ham", + "amount": 0.3, + "quantities": { + "hb1-quantity1": {"quantityId": "more", "amount": 0.6, "additionalCost": 10, "additionalPrice": 30}, + }, + }, + "hb-ingredient2": {"ingredientId": "bun", "amount": 1}, + "hb-ingredient3": { + "ingredientId": "lettuce", + "amount": 1, + "quantities": { + "cb3-quantity2": {"quantityId": "none", "amount": 0}, + } + }, + } + }, + }, + })), + Catalog.fromObject(CatalogObject.build({ + "id": "drink", + "index": 2, + "name": '🍻 ${S.menuExampleCatalogDrink}', + "createdAt": now, + "products": { + "cola": { + "price": 30, + "cost": 20, + "index": 1, + "name": S.menuExampleProductCola, + "createdAt": now, + "ingredients": { + "cola-ingredient1": {"ingredientId": "cola", "amount": 1}, + "coffee-ingredient2": {"ingredientId": "straw", "amount": 1}, + }, + }, + "coffee": { + "price": 50, + "cost": 20, + "index": 2, + "name": S.menuExampleProductCoffee, + "createdAt": now, + "ingredients": { + "coffee-ingredient1": {"ingredientId": "coffee", "amount": 1}, + }, + }, + }, + })), + Catalog.fromObject(CatalogObject.build({ + "id": "side", + "index": 3, + "name": '🍰 ${S.menuExampleCatalogSide}', + "createdAt": now, + "products": { + "fries": { + "price": 50, + "cost": 25, + "index": 1, + "name": S.menuExampleProductFries, + "createdAt": now, + "ingredients": { + "fries-ingredient1": { + "ingredientId": "fries", + "amount": 0.1, + "quantities": { + "fries1-quantity1": {"quantityId": "more", "amount": 0.2, "additionalCost": 5, "additionalPrice": 10}, + }, + }, + }, + }, + }, + })), + Catalog.fromObject(CatalogObject.build({ + "id": "other", + "index": 4, + "name": '🛍 ${S.menuExampleCatalogOther}', + "createdAt": now, + "products": { + "plastic-bag": { + "price": 5, + "cost": 2, + "index": 1, + "name": S.menuExampleProductPlasticBag, + "createdAt": now, + "ingredients": { + "plastic-bag-ingredient1": {"ingredientId": "plasticBag", "amount": 1}, + }, + }, + "straw": { + "price": 5, + "cost": 0.1, + "index": 2, + "name": S.menuExampleProductStraw, + "createdAt": now, + "ingredients": { + "straw-ingredient1": {"ingredientId": "straw", "amount": 1}, + }, + }, + }, + })), + ]) { + await Menu.instance.addItem(e); + } +} + +Future setupExampleOrderAttrs() async { + if (OrderAttributes.instance.isNotEmpty) return; + + log('setting order attributes', name: 'example order attrs'); + for (final e in [ + OrderAttribute( + id: 'age', + name: S.orderAttributeExampleAge, + index: 1, + mode: OrderAttributeMode.statOnly, + options: { + 'child': OrderAttributeOption(id: 'child', name: '${S.orderAttributeExampleAgeChild} (0-12)', index: 1), + 'adult': OrderAttributeOption( + id: 'adult', name: '${S.orderAttributeExampleAgeAdult} (13-60)', index: 2, isDefault: true), + 'senior': OrderAttributeOption(id: 'senior', name: '${S.orderAttributeExampleAgeSenior} (60+)', index: 3), + }, + ), + OrderAttribute( + id: 'place', + name: S.orderAttributeExamplePlace, + index: 2, + mode: OrderAttributeMode.changeDiscount, + options: { + 'takeout': + OrderAttributeOption(id: 'takeout', name: S.orderAttributeExamplePlaceTakeout, index: 1, isDefault: true), + 'dine-in': + OrderAttributeOption(id: 'dine-in', name: S.orderAttributeExamplePlaceDineIn, index: 2, modeValue: 1.1), + }, + ), + OrderAttribute( + id: 'eco-friendly', + name: S.orderAttributeExampleEcoFriendly, + index: 3, + mode: OrderAttributeMode.changePrice, + options: { + 'reuseable-bag': OrderAttributeOption( + id: 'reuseable-bag', name: S.orderAttributeExampleEcoFriendlyReusableBag, index: 1, modeValue: -5), + 'reuseable-bottle': OrderAttributeOption( + id: 'reuseable-bottle', name: S.orderAttributeExampleEcoFriendlyReusableBottle, index: 1, modeValue: -30), + }, + ), + ]) { + await OrderAttributes.instance.addItem(e); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e8240938..cd5f6e13 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2024-06-02T09:08:47.831122Z", + "@@last_modified": "2024-07-06T10:00:50.953962Z", "@@author": "Lu Shueh Chou", "settingTab": "Settings", "settingVersion": "Version: {version}", @@ -213,8 +213,6 @@ "stockQuantityProportionHelper": "Applied when this quantity is used for an ingredient.\n\nFor example:\nif this quantity is \"Large\" and the default ratio is \"1.5\",\nand there's a product \"Cheeseburger\" with the ingredient \"Cheese,\"\nwhich uses \"2\" units of cheese per burger,\nwhen adding this quantity,\nthe quantity of \"Cheese\" will automatically be set to \"3\" (2 * 1.5).\n\nIf set to \"1,\" there's no effect.\n\nIf set to \"0,\" the ingredient won't be used.", "transitTitle": "Data Transfer", "transitDescription": "Importing and Exporting Store Information and Orders", - "transitTutorialTitle": "Sync Multiple Devices", - "transitTutorialContent": "This is where you can import/export menu, inventory, order records, and other information.\n\nWe provide two methods: Google Sheets and plain text, making it convenient to sync data across different devices.", "transitMethodTitle": "Please Select Transfer Method", "transitMethodName": "{name, select, googleSheet{Google Sheets} plainText{Plain Text} other{UNKNOWN}}", "@transitMethodName": { @@ -306,7 +304,7 @@ "transitOrderItemDialogTitle": "Order Details", "transitExportPreviewBtn": "Preview", "transitExportPreviewTitle": "Preview Output Result", - "transitExportBtn": "Import", + "transitExportBtn": "Export", "transitImportPreviewBtn": "Preview", "transitImportPreviewTitle": "Preview Import Result", "transitImportPreviewHeader": "Note: Importing will remove the data not listed below. Please confirm before executing!", @@ -333,7 +331,7 @@ }, "transitImportPreviewIngredientHeader": "After import, old ingredients won't be removed to avoid affecting the \"Menu\" status.", "transitImportPreviewQuantityHeader": "After import, old quantities won't be removed to avoid affecting the \"Menu\" status.", - "transitImportBtn": "Export", + "transitImportBtn": "Import", "transitImportErrorColumnCount": "Insufficient data, {columns} columns required", "@transitImportErrorColumnCount": { "placeholders": { @@ -1158,6 +1156,17 @@ }, "orderAttributeTutorialTitle": "Customer Settings", "orderAttributeTutorialContent": "This is where you set customer information, such as dine-in, takeout, office worker, etc.\nThis information helps us track who comes to consume and make better business strategies.", + "orderAttributeTutorialCreateExample": "Help create an example to test.", + "orderAttributeExampleAge": "Age", + "orderAttributeExampleAgeChild": "Child", + "orderAttributeExampleAgeAdult": "Adult", + "orderAttributeExampleAgeSenior": "Senior", + "orderAttributeExamplePlace": "Place", + "orderAttributeExamplePlaceTakeout": "Takeout", + "orderAttributeExamplePlaceDineIn": "Dine-in", + "orderAttributeExampleEcoFriendly": "Eco-Friendly", + "orderAttributeExampleEcoFriendlyReusableBottle": "Reusable Bottle", + "orderAttributeExampleEcoFriendlyReusableBag": "Reusable Bag", "orderAttributeMetaMode": "Mode: {name}", "@orderAttributeMetaMode": { "placeholders": { @@ -1277,21 +1286,47 @@ "menuSubtitle": "Categories, Products", "menuTutorialTitle": "Create Your Menu", "menuTutorialContent": "Let's start by creating a menu!", + "menuTutorialCreateExample": "Help create an example menu to test.", "menuSearchHint": "Search for products, ingredients, quantities", "menuSearchNotFound": "Couldn't find relevant information. Did you misspell something?", + "menuExampleCatalogBurger": "Burgers", + "menuExampleCatalogDrink": "Drinks", + "menuExampleCatalogSide": "Side", + "menuExampleCatalogOther": "Others", + "menuExampleProductCheeseBurger": "Cheese Burger", + "menuExampleProductVeggieBurger": "Veggie Burger", + "menuExampleProductHamBurger": "Ham Burger", + "menuExampleProductCola": "Cola", + "menuExampleProductCoffee": "Coffee", + "menuExampleProductFries": "Fries", + "menuExampleProductStraw": "Straw", + "menuExampleProductPlasticBag": "Plastic Bag", + "menuExampleIngredientCheese": "Cheese", + "menuExampleIngredientLettuce": "Lettuce", + "menuExampleIngredientTomato": "Tomato", + "menuExampleIngredientBun": "Bun", + "menuExampleIngredientChili": "Chili Sauce", + "menuExampleIngredientHam": "Ham", + "menuExampleIngredientCola": "Can of Cola", + "menuExampleIngredientCoffee": "Drip Coffee", + "menuExampleIngredientFries": "Bag of Fries", + "menuExampleIngredientStraw": "Straw", + "menuExampleIngredientPlasticBag": "Plastic Bag", + "menuExampleQuantitySmall": "Small", + "menuExampleQuantityLarge": "Large", + "menuExampleQuantityNone": "None", "menuCatalogHeaderInfo": "Categories", "@menuCatalogHeaderInfo": { "description": "Displayed on the upper rectangle in homepage" }, - "menuCatalogTutorialTitle": "Create First Catalog", - "menuCatalogEmptyBody": "Similar \"products\" will be grouped under \"categories\",\nmaking it convenient for ordering, such as:\n• \"Cheese Burger\", \"Veggie Burger\" > \"Burgers\"\n• \"Plastic Bag\", \"Eco Cup\" > \"Others\"", + "menuCatalogEmptyBody": "Similar products will be grouped under categories,\nmaking ordering convenient, such as:\n• \"Cheese Burger\", \"Veggie Burger\" > \"Burgers\"\n• \"Plastic Bag\", \"Eco Cup\" > \"Others\"", "menuCatalogTitleCreate": "Add Category", "@menuCatalogTitleCreate": { "description": "FloatingActionButton description on the menu page" }, "menuCatalogTitleUpdate": "Edit Category", "menuCatalogTitleReorder": "Reorder Categories", - "menuCatalogDialogDeletionContent": "{count, plural, =0{No products inside} other{Will delete {count} products together}}", + "menuCatalogDialogDeletionContent": "{count, plural, =0{No products inside} =1{Will also delete {count} related product} other{Will also delete {count} related products}}", "@menuCatalogDialogDeletionContent": { "description": "Warning message when deleting product categories on the menu page", "placeholders": { @@ -1536,6 +1571,8 @@ "cashierSurplusDiffTotalHelper": "The difference from the total amount of the cash register at the very beginning.\nThis can quickly help you understand how much money the cash register has gained today.", "orderTitle": "Ordering", "orderBtn": "Order", + "orderTutorialTitle": "Ordering!", + "orderTutorialContent": "Once you have set up your menu, you can start ordering!\nLet's tap and go see what's available!\n", "orderSnackbarCashierNotEnough": "Insufficient cash in the cashier!", "orderSnackbarCashierUsingSmallMoney": "Using smaller denominations to give change", "orderSnackbarCashierUsingSmallMoneyHelper": "When giving change to customers, if the cashier doesn't have the appropriate denominations, this message will appear.\n\nFor example, if the total is $65 and the customer pays $100, the change should be $35.\nIf the cashier only has two $10 bills and more than three $5 bills, this message will appear.\n\nTo avoid this prompt:\n• Go to the changer page and top up various denominations.\n• Go to the [settings page]({link}) to disable related prompts from the cashier.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index dc7e7ff6..b6ad7d04 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "@@last_modified": "2024-06-02T09:08:47.880851Z", + "@@last_modified": "2024-07-06T10:00:50.976445Z", "@@author": "Lu Shueh Chou", "settingTab": "設定", "settingVersion": "版本:{version}", @@ -213,8 +213,6 @@ "stockQuantityProportionHelper": "當產品成分使用此份量時,預設替該成分增加的比例。\n\n例如:此份量為「多量」預設份量為「1.5」,\n今有一產品「起司漢堡」的成分「起司」,每份漢堡會使用「2」單位的起司,\n當增加此份量時,則會自動替「起司」設定為「3」(2 * 1.5)的份量。\n\n若設為「1」則無任何影響。\n\n若設為「0」則代表將不會使用此成分", "transitTitle": "資料轉移", "transitDescription": "匯入、匯出店家資訊和訂單", - "transitTutorialTitle": "同步多台裝置", - "transitTutorialContent": "這裡是用來匯入匯出菜單、庫存、訂單記錄等資訊的地方。\n\n我們提供了 Google 試算表和純文字兩種方式,讓您可以方便地在不同裝置間同步資料。", "transitMethodTitle": "請選擇欲轉移的方式", "transitMethodName": "{name, select, googleSheet{Google 試算表} plainText{純文字} other{UNKNOWN}}", "@transitMethodName": { @@ -306,7 +304,7 @@ "transitOrderItemDialogTitle": "訂單細節", "transitExportPreviewBtn": "預覽", "transitExportPreviewTitle": "預覽輸出結果", - "transitExportBtn": "匯入", + "transitExportBtn": "匯出", "transitImportPreviewBtn": "預覽", "transitImportPreviewTitle": "預覽匯入結果", "transitImportPreviewHeader": "注意:匯入後將會把下面沒列到的資料移除,請確認是否執行!", @@ -333,7 +331,7 @@ }, "transitImportPreviewIngredientHeader": "匯入後,為了避免影響「菜單」的狀況,並不會把舊的成分移除。", "transitImportPreviewQuantityHeader": "匯入後,為了避免影響「菜單」的狀況,並不會把舊的份量移除。", - "transitImportBtn": "匯出", + "transitImportBtn": "匯入", "transitImportErrorColumnCount": "資料量不足,需要 {columns} 個欄位", "@transitImportErrorColumnCount": { "placeholders": { @@ -1158,6 +1156,17 @@ }, "orderAttributeTutorialTitle": "顧客設定", "orderAttributeTutorialContent": "這裡是用來設定顧客的資訊,例如:內用、外帶、上班族等。\n這些資訊可以幫助我們統計哪些人來消費,進而做出更好的經營策略。", + "orderAttributeTutorialCreateExample": "幫助建立一份範例以供測試。", + "orderAttributeExampleAge": "年齡", + "orderAttributeExampleAgeChild": "小孩", + "orderAttributeExampleAgeAdult": "成人", + "orderAttributeExampleAgeSenior": "長者", + "orderAttributeExamplePlace": "位置", + "orderAttributeExamplePlaceTakeout": "外帶", + "orderAttributeExamplePlaceDineIn": "內用", + "orderAttributeExampleEcoFriendly": "環保", + "orderAttributeExampleEcoFriendlyReusableBottle": "環保杯", + "orderAttributeExampleEcoFriendlyReusableBag": "環保袋", "orderAttributeMetaMode": "種類:{name}", "@orderAttributeMetaMode": { "placeholders": { @@ -1277,13 +1286,39 @@ "menuSubtitle": "產品種類、產品", "menuTutorialTitle": "建立屬於你的菜單", "menuTutorialContent": "首先我們來開始建立一份菜單吧!", + "menuTutorialCreateExample": "幫助建立一份範例菜單以供測試。", "menuSearchHint": "搜尋產品、成分、份量", "menuSearchNotFound": "搜尋不到相關資訊,打錯字了嗎?", + "menuExampleCatalogBurger": "漢堡", + "menuExampleCatalogDrink": "飲品", + "menuExampleCatalogSide": "點心", + "menuExampleCatalogOther": "其他", + "menuExampleProductCheeseBurger": "起司漢堡", + "menuExampleProductVeggieBurger": "蔬菜漢堡", + "menuExampleProductHamBurger": "火腿漢堡", + "menuExampleProductCola": "可樂", + "menuExampleProductCoffee": "咖啡", + "menuExampleProductFries": "薯條", + "menuExampleProductStraw": "吸管", + "menuExampleProductPlasticBag": "塑膠袋", + "menuExampleIngredientCheese": "起司", + "menuExampleIngredientLettuce": "萵苣", + "menuExampleIngredientTomato": "番茄", + "menuExampleIngredientBun": "麵包", + "menuExampleIngredientChili": "醬料", + "menuExampleIngredientHam": "火腿", + "menuExampleIngredientCola": "可樂罐", + "menuExampleIngredientCoffee": "濾掛咖啡包", + "menuExampleIngredientFries": "薯條(300g)", + "menuExampleIngredientStraw": "吸管(根)", + "menuExampleIngredientPlasticBag": "塑膠袋(個)", + "menuExampleQuantitySmall": "少量", + "menuExampleQuantityLarge": "增量", + "menuExampleQuantityNone": "無", "menuCatalogHeaderInfo": "種類", "@menuCatalogHeaderInfo": { "description": "Displayed on the upper rectangle in homepage" }, - "menuCatalogTutorialTitle": "Create First Catalog", "menuCatalogEmptyBody": "我們會把相似「產品」放在「產品種類」中,\n到時候點餐會比較方便,例如:\n• 「起司漢堡」、「蔬菜漢堡」整合進「漢堡」\n• 「塑膠袋」、「環保杯」整合進「其他」", "menuCatalogTitleCreate": "新增產品種類", "@menuCatalogTitleCreate": { @@ -1536,6 +1571,8 @@ "cashierSurplusDiffTotalHelper": "和收銀機最一開始的總額的差額。\n這可以快速幫你了解今天收銀機多了多少錢唷。", "orderTitle": "點餐", "orderBtn": "點餐", + "orderTutorialTitle": "開始點餐!", + "orderTutorialContent": "一旦設定好菜單,就可以開始點餐囉\n讓我們趕緊進去看看有什麼吧!\n", "orderSnackbarCashierNotEnough": "收銀機錢不夠找囉!", "orderSnackbarCashierUsingSmallMoney": "收銀機使用小錢去找零", "orderSnackbarCashierUsingSmallMoneyHelper": "找錢給顧客時,收銀機無法使用最適合的錢,就會顯示這個訊息。\n\n例如,售價「65」,消費者支付「100」,此時應找「35」\n如果收銀機只有兩個十元,且有三個以上的五元,就會顯示本訊息。\n\n怎麼避免本提示:\n• 到換錢頁面把各幣值補足。\n• 到[設定頁]({link})關閉收銀機的相關提示。", diff --git a/lib/main.dart b/lib/main.dart index 0699f4a5..22f1e2e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,6 @@ import 'package:possystem/models/analysis/analysis.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:provider/provider.dart'; -import 'debug/setup_menu.dart'; import 'firebase_compatible_options.dart'; import 'helpers/logger.dart'; import 'models/repository/cashier.dart'; @@ -66,10 +65,6 @@ void main() async { // Last for setup ingredient and quantity await Menu().initialize(); - if (isLocalTest) { - await debugSetupMenu(); - } - /// Why use provider? /// https://stackoverflow.com/questions/57157823/provider-vs-inheritedwidget runApp(MultiProvider( diff --git a/lib/models/objects/menu_object.dart b/lib/models/objects/menu_object.dart index 53a6f0f0..77d17b1f 100644 --- a/lib/models/objects/menu_object.dart +++ b/lib/models/objects/menu_object.dart @@ -271,9 +271,9 @@ class ProductQuantityObject extends ModelObject { return ProductQuantityObject( id: id as String, quantityId: quantityId as String, - amount: data['amount'] as num, - additionalCost: data['additionalCost'] as num, - additionalPrice: data['additionalPrice'] as num, + amount: data['amount'] as num? ?? 0, + additionalCost: data['additionalCost'] as num? ?? 0, + additionalPrice: data['additionalPrice'] as num? ?? 0, version: version, ); } diff --git a/lib/models/repository.dart b/lib/models/repository.dart index 83346f28..8176101d 100644 --- a/lib/models/repository.dart +++ b/lib/models/repository.dart @@ -224,7 +224,7 @@ mixin RepositoryStorage on Repository { @override Future saveItem(T item) { - Log.ger('add start', storageStore.name, _items.toString()); + Log.ger('add start', storageStore.name, item.toString()); final data = item.toObject().toMap(); return repoType == RepositoryStorageType.pureRepo diff --git a/lib/my_app.dart b/lib/my_app.dart index 974465a0..91f78702 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -57,6 +57,7 @@ class MyApp extends StatelessWidget { S = localizations; Intl.systemLocale = S.localeName; Intl.defaultLocale = S.localeName; + LanguageSetting.instance.systemLanguage = S.localeName; initializeDateFormatting(S.localeName); FlutterNativeSplash.remove(); @@ -68,7 +69,7 @@ class MyApp extends StatelessWidget { // Provide the generated AppLocalizations to the MaterialApp. This // allows descendant Widgets to display the correct translations // depending on the user's locale. - locale: LanguageSetting.instance.value.locale, + locale: LanguageSetting.instance.value?.locale, supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/lib/settings/currency_setting.dart b/lib/settings/currency_setting.dart index d19cffc6..6801850b 100644 --- a/lib/settings/currency_setting.dart +++ b/lib/settings/currency_setting.dart @@ -24,7 +24,7 @@ class CurrencySetting extends Setting { CurrencySetting._() { value = defaultValue; LanguageSetting.instance.addListener(() { - formatter = NumberFormat.compact(locale: LanguageSetting.instance.value.locale.toString()); + formatter = NumberFormat.compact(locale: LanguageSetting.instance.language.locale.toString()); }); } @@ -33,7 +33,7 @@ class CurrencySetting extends Setting { String get recordName => '新台幣'; - NumberFormat formatter = NumberFormat.compact(locale: LanguageSetting.instance.value.locale.toString()); + NumberFormat formatter = NumberFormat.compact(locale: LanguageSetting.instance.language.locale.toString()); /// Ceiling [value] to currency least value /// diff --git a/lib/settings/language_setting.dart b/lib/settings/language_setting.dart index b89682d2..9dd4b97b 100644 --- a/lib/settings/language_setting.dart +++ b/lib/settings/language_setting.dart @@ -1,15 +1,17 @@ import 'dart:ui'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:possystem/settings/setting.dart'; -class LanguageSetting extends Setting { - static final instance = LanguageSetting._(); +/// Language setting allow given null language which means system default. +class LanguageSetting extends Setting { + Language _systemLanguage = Language.en; - static const defaultValue = Language.en; + static final instance = LanguageSetting._(); LanguageSetting._() { - value = defaultValue; + value = Language.en; } @override @@ -18,15 +20,21 @@ class LanguageSetting extends Setting { @override bool get registryForApp => true; + set systemLanguage(String locale) { + _systemLanguage = parseLanguage(locale)!; + } + + Language get language => value ?? _systemLanguage; + @override void initialize() { - value = parseLanguage(service.get(key)) ?? defaultValue; + value = parseLanguage(service.get(key)); notifyListeners(); } @override - Future updateRemotely(Language data) { - return service.set(key, data.locale.toString()); + Future updateRemotely(Language? data) { + return service.set(key, data?.locale.toString() ?? ''); } Language? parseLanguage(String? value) { @@ -34,10 +42,7 @@ class LanguageSetting extends Setting { final codes = value.split('_'); - return Language.values.firstWhere( - (e) => e.locale.languageCode == codes[0], - orElse: () => defaultValue, - ); + return Language.values.firstWhereOrNull((e) => e.locale.languageCode == codes[0]); } } diff --git a/lib/ui/analysis/widgets/history_calendar_view.dart b/lib/ui/analysis/widgets/history_calendar_view.dart index 54915535..620faef9 100644 --- a/lib/ui/analysis/widgets/history_calendar_view.dart +++ b/lib/ui/analysis/widgets/history_calendar_view.dart @@ -52,7 +52,7 @@ class _HistoryCalendarViewState extends State { shouldFillViewport: widget.isPortrait ? false : true, startingDayOfWeek: StartingDayOfWeek.monday, rangeSelectionMode: RangeSelectionMode.disabled, - locale: LanguageSetting.instance.value.locale.toString(), + locale: LanguageSetting.instance.language.locale.toString(), // header // chinese will be hidden if using default value daysOfWeekHeight: 20.0, diff --git a/lib/ui/home/features_page.dart b/lib/ui/home/features_page.dart index da51a1d3..3e4c9791 100644 --- a/lib/ui/home/features_page.dart +++ b/lib/ui/home/features_page.dart @@ -81,7 +81,7 @@ class FeaturesPage extends StatelessWidget { key: const Key('feature.language'), leading: const Icon(Icons.language_outlined), title: Text(S.settingLanguageTitle), - subtitle: Text(LanguageSetting.instance.value.title), + subtitle: Text(LanguageSetting.instance.language.title), trailing: const Icon(Icons.arrow_forward_ios_sharp), onTap: () => navigateTo(Feature.language), ), @@ -229,7 +229,7 @@ enum Feature { case Feature.theme: return ThemeSetting.instance.value.index; case Feature.language: - return LanguageSetting.instance.value.index; + return LanguageSetting.instance.language.index; case Feature.orderOutlook: return OrderOutlookSetting.instance.value.index; case Feature.checkoutWarning: diff --git a/lib/ui/home/setting_view.dart b/lib/ui/home/setting_view.dart index 2753c43a..d9636a50 100644 --- a/lib/ui/home/setting_view.dart +++ b/lib/ui/home/setting_view.dart @@ -6,9 +6,11 @@ import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/app_themes.dart'; import 'package:possystem/constants/constant.dart'; import 'package:possystem/debug/debug_page.dart'; +import 'package:possystem/helpers/setup_example.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/routes.dart'; +import 'package:possystem/services/cache.dart'; import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; @@ -24,105 +26,130 @@ class SettingView extends StatefulWidget { class _SettingViewState extends State with AutomaticKeepAliveClientMixin { late final TutorialInTab? tab; + final GlobalKey<_TutorialCheckboxListTileState> _tutorialOrderAttrs = GlobalKey(); + final GlobalKey<_TutorialCheckboxListTileState> _tutorialMenu = GlobalKey(); @override Widget build(BuildContext context) { super.build(context); + var orderAttrIndex = OrderAttributes.instance.isEmpty ? (Menu.instance.isEmpty ? 1 : 0) : -1; return TutorialWrapper( tab: tab, - child: ListView(padding: const EdgeInsets.only(bottom: 76), children: [ - const _HeaderInfoList(), - if (!isProd) - ListTile( - key: const Key('setting.debug'), - leading: const Icon(Icons.bug_report_sharp), - title: const Text('Debug'), - subtitle: const Text('For developer only'), - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const DebugPage()), + child: Scaffold( + floatingActionButton: (Cache.instance.get('tutorial.home.order') ?? false) + ? null + : Tutorial( + id: 'home.order', + index: orderAttrIndex + 1, + spotlightBuilder: const SpotlightRectBuilder(borderRadius: 16.0), + title: S.orderTutorialTitle, + message: S.orderTutorialContent, + child: FloatingActionButton.extended( + heroTag: 'order.tutorial', + onPressed: null, + icon: const Icon(Icons.store_sharp), + label: Text(S.orderBtn), + ), + ), + body: ListView(padding: const EdgeInsets.only(bottom: 76), children: [ + const _HeaderInfoList(), + if (!isProd) + ListTile( + key: const Key('setting.debug'), + leading: const Icon(Icons.bug_report_sharp), + title: const Text('Debug'), + subtitle: const Text('For developer only'), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const DebugPage()), + ), + ), + Tutorial( + id: 'home.menu', + index: 0, + title: S.menuTutorialTitle, + message: S.menuTutorialContent, + below: _TutorialCheckboxListTile(key: _tutorialMenu, title: S.menuTutorialCreateExample), + spotlightBuilder: const SpotlightRectBuilder(), + disable: Menu.instance.isNotEmpty, + action: () async { + if (_tutorialMenu.currentState?.value == true) { + await setupExampleMenu(); + } + }, + child: _buildRouteTile( + id: 'menu', + icon: Icons.collections_sharp, + route: Routes.menu, + title: S.menuTitle, + subtitle: S.menuSubtitle, ), ), - Tutorial( - id: 'home.menu', - title: S.menuTutorialTitle, - message: S.menuTutorialContent, - spotlightBuilder: const SpotlightRectBuilder(), - disable: Menu.instance.isNotEmpty, - route: Routes.menu, - child: _buildRouteTile( - id: 'menu', - icon: Icons.collections_sharp, - route: Routes.menu, - title: S.menuTitle, - subtitle: S.menuSubtitle, - ), - ), - Tutorial( - id: 'home.exporter', - title: S.transitTutorialTitle, - message: S.transitTutorialContent, - spotlightBuilder: const SpotlightRectBuilder(), - child: _buildRouteTile( - id: 'exporter', + _buildRouteTile( + id: 'transit', icon: Icons.upload_file_sharp, route: Routes.transit, title: S.transitTitle, subtitle: S.transitDescription, ), - ), - Tutorial( - id: 'home.order_attr', - title: S.orderAttributeTutorialTitle, - message: S.orderAttributeTutorialContent, - spotlightBuilder: const SpotlightRectBuilder(), - child: _buildRouteTile( - id: 'order_attrs', - icon: Icons.assignment_ind_sharp, - route: Routes.orderAttr, - title: S.orderAttributeTitle, - subtitle: S.orderAttributeDescription, + Tutorial( + id: 'home.order_attr', + index: orderAttrIndex, + title: S.orderAttributeTutorialTitle, + message: S.orderAttributeTutorialContent, + below: _TutorialCheckboxListTile(key: _tutorialOrderAttrs, title: S.orderAttributeTutorialCreateExample), + spotlightBuilder: const SpotlightRectBuilder(), + disable: OrderAttributes.instance.isNotEmpty, + action: () async { + if (_tutorialOrderAttrs.currentState?.value == true) { + await setupExampleOrderAttrs(); + } + }, + child: _buildRouteTile( + id: 'order_attrs', + icon: Icons.assignment_ind_sharp, + route: Routes.orderAttr, + title: S.orderAttributeTitle, + subtitle: S.orderAttributeDescription, + ), ), - ), - _buildRouteTile( - id: 'quantity', - icon: Icons.exposure_sharp, - route: Routes.quantity, - title: S.stockQuantityTitle, - subtitle: S.stockQuantityDescription, - ), - _buildRouteTile( - id: 'feature_request', - icon: Icons.lightbulb_sharp, - route: Routes.featureRequest, - title: S.settingElfTitle, - subtitle: S.settingElfDescription, - ), - _buildRouteTile( - id: 'setting', - icon: Icons.settings_sharp, - route: Routes.features, - title: S.settingFeatureTitle, - subtitle: S.settingFeatureDescription, - ), - Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - TextButton( - onPressed: _bottomLinks[0].launch, - child: Text(_bottomLinks[0].text), + _buildRouteTile( + id: 'quantity', + icon: Icons.exposure_sharp, + route: Routes.quantity, + title: S.stockQuantityTitle, + subtitle: S.stockQuantityDescription, ), - const Text(MetaBlock.string), - TextButton( - onPressed: _bottomLinks[1].launch, - child: Text(_bottomLinks[1].text), + _buildRouteTile( + id: 'feature_request', + icon: Icons.lightbulb_sharp, + route: Routes.featureRequest, + title: S.settingElfTitle, + subtitle: S.settingElfDescription, ), - ]) - ]), + _buildRouteTile( + id: 'setting', + icon: Icons.settings_sharp, + route: Routes.features, + title: S.settingFeatureTitle, + subtitle: S.settingFeatureDescription, + ), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + TextButton( + onPressed: _bottomLinks[0].launch, + child: Text(_bottomLinks[0].text), + ), + const Text(MetaBlock.string), + TextButton( + onPressed: _bottomLinks[1].launch, + child: Text(_bottomLinks[1].text), + ), + ]) + ]), + ), ); } - @override - bool get wantKeepAlive => true; - @override void initState() { tab = widget.tabIndex == null ? null : TutorialInTab(index: widget.tabIndex!, context: context); @@ -130,6 +157,9 @@ class _SettingViewState extends State with AutomaticKeepAliveClient super.initState(); } + @override + bool get wantKeepAlive => true; + Widget _buildRouteTile({ required String id, required IconData icon, @@ -229,13 +259,39 @@ class _HeaderInfoList extends StatelessWidget { ), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title.toString(), style: theme.textTheme.headlineMedium), - Text(subtitle, style: theme.textTheme.bodyMedium), + Flexible(child: Text(subtitle, textAlign: TextAlign.center)), ]), ), ); } } +class _TutorialCheckboxListTile extends StatefulWidget { + final String title; + + const _TutorialCheckboxListTile({super.key, required this.title}); + + @override + State<_TutorialCheckboxListTile> createState() => _TutorialCheckboxListTileState(); +} + +class _TutorialCheckboxListTileState extends State<_TutorialCheckboxListTile> { + bool value = true; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: CheckboxListTile( + value: value, + onChanged: (v) => setState(() => value = v!), + tileColor: Theme.of(context).primaryColor, + title: Text(widget.title, style: const TextStyle(color: Colors.white)), + ), + ); + } +} + const _bottomLinks = [ LinkifyData('Privacy Policy', 'https://evan361425.github.io/flutter-pos-system/PRIVACY_POLICY/'), LinkifyData('License', 'https://evan361425.github.io/flutter-pos-system/LICENSE/'), diff --git a/lib/ui/menu/menu_page.dart b/lib/ui/menu/menu_page.dart index e31ee384..5a91743a 100644 --- a/lib/ui/menu/menu_page.dart +++ b/lib/ui/menu/menu_page.dart @@ -76,7 +76,14 @@ class _MenuPageState extends State { ), ], ), - floatingActionButton: widget.productOnly ? null : fab, + floatingActionButton: widget.productOnly + ? null + : FloatingActionButton( + key: const Key('menu.add'), + onPressed: _handleCreate, + tooltip: selected == null ? S.menuCatalogTitleCreate : S.menuProductTitleCreate, + child: const Icon(KIcons.add), + ), body: PageView( controller: controller, // disable scrolling, only control by program @@ -113,22 +120,6 @@ class _MenuPageState extends State { super.dispose(); } - Widget get fab { - return Tutorial( - id: 'add_menu', - disable: Menu.instance.isNotEmpty, - title: S.menuCatalogTutorialTitle, - message: S.menuCatalogEmptyBody, - route: Routes.menuNew, - child: FloatingActionButton( - key: const Key('menu.add'), - onPressed: _handleCreate, - tooltip: selected == null ? S.menuCatalogTitleCreate : S.menuProductTitleCreate, - child: const Icon(KIcons.add), - ), - ); - } - Widget get firstView { if (Menu.instance.isEmpty) { return Center( diff --git a/pubspec.lock b/pubspec.lock index 297abb4e..0dbdac85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1118,10 +1118,10 @@ packages: dependency: "direct main" description: name: spotlight_ant - sha256: a295d3dea0f54bc8bb82042c166e1ca0bedb9c97353ec3f1d67bc3ed5b6fd73e + sha256: "30c1fb6dbb2356dc92e61da332bd5cf7bf63e4b74c1414c618caab34127ed96a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" sprintf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 243745b0..f69fbf07 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: # components table_calendar: ^3.1.1 # 24, 02-09 syncfusion_flutter_charts: ^25.2.4 - spotlight_ant: ^1.1.0 + spotlight_ant: ^1.1.1 # image image: ^4.1.7 # 24, 01-10 diff --git a/release.config.json b/release.config.json index 66c94d80..43c26a64 100644 --- a/release.config.json +++ b/release.config.json @@ -17,8 +17,8 @@ "sort": "ASC", "pr_template": "- ${{TITLE}} #${{NUMBER}} : ${{AUTHOR}}", "empty_template": "Caught some bugs, time to clean up!", - "max_tags_to_fetch": 200, - "max_pull_requests": 200, + "max_tags_to_fetch": 10, + "max_pull_requests": 20, "max_back_track_time_days": 365, "exclude_merge_branches": [], "tag_resolver": { diff --git a/test/components/tutorial_test.dart b/test/components/tutorial_test.dart index 153fade8..44a503ec 100644 --- a/test/components/tutorial_test.dart +++ b/test/components/tutorial_test.dart @@ -18,7 +18,6 @@ void main() { id: '1', title: 'title1', message: 'message1', - duration: SpotlightDurationConfig.zero, child: Text('1'), ), ], @@ -78,6 +77,7 @@ void main() { }); setUpAll(() { + Tutorial.debug = true; initializeCache(); }); } @@ -111,7 +111,6 @@ class _ScaffoldState extends State<_Scaffold> with TickerProviderStateMixin { id: '1', title: 'title1', message: 'message1', - duration: SpotlightDurationConfig.zero, child: Text('1'), ), ), @@ -121,7 +120,6 @@ class _ScaffoldState extends State<_Scaffold> with TickerProviderStateMixin { id: '2', title: 'title2', message: 'message2', - duration: SpotlightDurationConfig.zero, child: Text('2'), ), ), diff --git a/test/debug/setup_menu_test.dart b/test/helpers/setup_example_test.dart similarity index 62% rename from test/debug/setup_menu_test.dart rename to test/helpers/setup_example_test.dart index fc8dc9b3..1e115190 100644 --- a/test/debug/setup_menu_test.dart +++ b/test/helpers/setup_example_test.dart @@ -1,29 +1,35 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:possystem/debug/setup_menu.dart'; +import 'package:possystem/helpers/setup_example.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stock.dart'; import '../mocks/mock_storage.dart'; +import '../test_helpers/translator.dart'; void main() { - group('Setup Menu in DEBUG mode', () { + group('Setup Menu', () { test('Should add once', () async { when(storage.add(any, any, any)).thenAnswer((_) => Future.value()); - await debugSetupMenu(); + await setupExampleMenu(); + await setupExampleOrderAttrs(); verify(storage.add(any, any, any)); - await debugSetupMenu(); + await setupExampleMenu(); + await setupExampleOrderAttrs(); verifyNever(storage.add(any, any, any)); }); setUpAll(() { initializeStorage(); + initializeTranslator(); Menu(); Stock(); Quantities(); + OrderAttributes(); }); }); } diff --git a/test/settings/language_setting_test.dart b/test/settings/language_setting_test.dart index 1019682d..9367096a 100644 --- a/test/settings/language_setting_test.dart +++ b/test/settings/language_setting_test.dart @@ -6,7 +6,7 @@ void main() { test('Parse language', () { final l = LanguageSetting.instance; expect(l.parseLanguage(''), isNull); - expect(l.parseLanguage('something'), equals(Language.en)); + expect(l.parseLanguage('something'), equals(null)); expect(l.parseLanguage('zh'), equals(Language.zhTW)); expect(l.parseLanguage('zh_TW'), equals(Language.zhTW)); expect(l.parseLanguage('zh_Hant'), equals(Language.zhTW)); diff --git a/test/ui/home/features_page_test.dart b/test/ui/home/features_page_test.dart index 4ccf0ab2..866f3dad 100644 --- a/test/ui/home/features_page_test.dart +++ b/test/ui/home/features_page_test.dart @@ -22,7 +22,7 @@ void main() { return ChangeNotifierProvider.value( value: SettingsProvider.instance..initialize(), builder: (_, __) => MaterialApp.router( - locale: LanguageSetting.defaultValue.locale, + locale: LanguageSetting.instance.language.locale, routerConfig: GoRouter(initialLocation: Routes.features, routes: [ GoRoute( path: '/', diff --git a/test/ui/home/home_page_test.dart b/test/ui/home/home_page_test.dart index 82039445..c387726f 100644 --- a/test/ui/home/home_page_test.dart +++ b/test/ui/home/home_page_test.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; +import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/app_themes.dart'; import 'package:possystem/models/analysis/analysis.dart'; import 'package:possystem/models/repository/cart.dart'; @@ -16,6 +17,7 @@ import 'package:possystem/my_app.dart'; import 'package:possystem/routes.dart'; import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/settings/settings_provider.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/home/home_page.dart'; import 'package:provider/provider.dart'; @@ -99,7 +101,7 @@ void main() { // rest await navAndPop('setting.debug', 'debug.list'); await navAndPop('setting.menu', 'menu.search'); - await navAndPop('setting.exporter', 'transit.google_sheet'); + await navAndPop('setting.transit', 'transit.google_sheet'); await navAndPop('setting.quantity', 'quantity.add'); await navAndPop('setting.order_attrs', 'order_attributes.reorder'); await dragDown(); @@ -112,6 +114,81 @@ void main() { await navAndCheck('home.analysis', 'anal.history'); }); + group('example menu', () { + setUp(() { + reset(cache); + when(cache.get(any)).thenReturn(null); + when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); + }); + + Widget buildApp() { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: SettingsProvider.instance), + ChangeNotifierProvider.value(value: Menu()), + ChangeNotifierProvider.value(value: Stock()), + ChangeNotifierProvider.value(value: Quantities()), + ChangeNotifierProvider.value(value: OrderAttributes()), + ], + child: MaterialApp.router( + routerConfig: GoRouter(observers: [ + MyApp.routeObserver + ], routes: [ + GoRoute( + path: '/', + routes: Routes.routes, + builder: (_, __) => const HomePage(tab: HomeTab.setting), + ) + ]), + theme: AppThemes.lightTheme, + darkTheme: AppThemes.darkTheme, + ), + ); + } + + Future startTutorial(WidgetTester tester) async { + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 5)); + } + + Future goNext(WidgetTester tester) async { + await tester.tapAt(Offset.zero); + await tester.pump(const Duration(milliseconds: 5)); + await tester.pump(const Duration(milliseconds: 5)); + } + + testWidgets('Setup', (tester) async { + await tester.pumpWidget(buildApp()); + expect(Menu.instance.isEmpty, isTrue); + expect(OrderAttributes.instance.isEmpty, isTrue); + + await startTutorial(tester); + await goNext(tester); + + expect(find.text(S.orderAttributeTutorialContent), findsOneWidget); + expect(Menu.instance.isNotEmpty, isTrue); + verify(cache.set('tutorial.home.menu', true)); + + await goNext(tester); + + expect(find.text(S.orderTutorialTitle), findsOneWidget); + expect(OrderAttributes.instance.isNotEmpty, isTrue); + verify(cache.set('tutorial.home.order_attr', true)); + }); + + testWidgets('Disable example menu', (tester) async { + await tester.pumpWidget(buildApp()); + + await startTutorial(tester); + await tester.tap(find.text(S.menuTutorialCreateExample)); + await goNext(tester); + + expect(find.text(S.orderAttributeTutorialContent), findsOneWidget); + expect(Menu.instance.isNotEmpty, isFalse); + verify(cache.set('tutorial.home.menu', true)); + }); + }); + setUp(() { // setup currency when(cache.get('currency')).thenReturn(null); @@ -129,6 +206,7 @@ void main() { }); setUpAll(() { + Tutorial.debug = true; initializeAuth(); initializeCache(); initializeStorage(); diff --git a/test/ui/transit/google_sheet/export_order_test.dart b/test/ui/transit/google_sheet/export_order_test.dart index 78581257..fd35cc61 100644 --- a/test/ui/transit/google_sheet/export_order_test.dart +++ b/test/ui/transit/google_sheet/export_order_test.dart @@ -63,13 +63,13 @@ void main() { ); await tester.pumpWidget(MaterialApp( - locale: LanguageSetting.defaultValue.locale, + locale: LanguageSetting.instance.language.locale, localizationsDelegates: const >[ DefaultWidgetsLocalizations.delegate, DefaultMaterialLocalizations.delegate, DefaultCupertinoLocalizations.delegate, ], - supportedLocales: [LanguageSetting.defaultValue.locale], + supportedLocales: [LanguageSetting.instance.language.locale], home: TransitStation( catalog: TransitCatalog.order, method: TransitMethod.googleSheet,