From 445a22a5a4fd595edf2d33da86bfe5bad66a6eec Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 31 Jul 2025 19:25:45 +0700 Subject: [PATCH 001/128] feat: slicing home page --- lib/core/components/buttons.dart | 29 +- lib/core/constants/colors.dart | 2 + .../datasources/product_local_datasource.dart | 2 +- lib/main.dart | 8 +- .../home/dialog/discount_dialog.dart | 130 +- .../home/dialog/service_dialog.dart | 110 +- lib/presentation/home/pages/home_page.dart | 1069 +++++++++-------- .../home/widgets/custom_tab_bar.dart | 119 +- .../home/widgets/home_right_title.dart | 60 + lib/presentation/home/widgets/home_title.dart | 76 +- .../home/widgets/item_notes_dialog.dart | 65 +- lib/presentation/home/widgets/order_menu.dart | 165 ++- .../home/widgets/product_card.dart | 92 +- 13 files changed, 1231 insertions(+), 696 deletions(-) create mode 100644 lib/presentation/home/widgets/home_right_title.dart diff --git a/lib/core/components/buttons.dart b/lib/core/components/buttons.dart index 2301468..77bec38 100644 --- a/lib/core/components/buttons.dart +++ b/lib/core/components/buttons.dart @@ -18,6 +18,8 @@ class Button extends StatelessWidget { this.icon, this.disabled = false, this.fontSize = 16.0, + this.elevation, + this.labelStyle, }); const Button.outlined({ @@ -33,6 +35,8 @@ class Button extends StatelessWidget { this.icon, this.disabled = false, this.fontSize = 16.0, + this.elevation, + this.labelStyle, }); final Function() onPressed; @@ -43,9 +47,11 @@ class Button extends StatelessWidget { final double? width; final double height; final double borderRadius; + final double? elevation; final Widget? icon; final bool disabled; final double fontSize; + final TextStyle? labelStyle; @override Widget build(BuildContext context) { @@ -60,6 +66,7 @@ class Button extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadius), ), + elevation: elevation, padding: const EdgeInsets.symmetric(horizontal: 16.0), ), child: Row( @@ -73,11 +80,12 @@ class Button extends StatelessWidget { fit: BoxFit.scaleDown, child: Text( label, - style: TextStyle( - color: disabled ? Colors.grey : textColor, - fontSize: fontSize, - fontWeight: FontWeight.w600, - ), + style: labelStyle ?? + TextStyle( + color: disabled ? Colors.grey : textColor, + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), ), @@ -106,11 +114,12 @@ class Button extends StatelessWidget { fit: BoxFit.scaleDown, child: Text( label, - style: TextStyle( - color: disabled ? Colors.grey : textColor, - fontSize: fontSize, - fontWeight: FontWeight.w600, - ), + style: labelStyle ?? + TextStyle( + color: disabled ? Colors.grey : textColor, + fontSize: fontSize, + fontWeight: FontWeight.w600, + ), textAlign: TextAlign.center, ), ), diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart index 3bfc2c3..41c5465 100644 --- a/lib/core/constants/colors.dart +++ b/lib/core/constants/colors.dart @@ -36,4 +36,6 @@ class AppColors { /// stroke = #EFF0F6 static const Color stroke = Color(0xffEFF0F6); + + static const Color background = Color.fromARGB(255, 241, 241, 241); } diff --git a/lib/data/datasources/product_local_datasource.dart b/lib/data/datasources/product_local_datasource.dart index a7b63e9..711d574 100644 --- a/lib/data/datasources/product_local_datasource.dart +++ b/lib/data/datasources/product_local_datasource.dart @@ -151,7 +151,7 @@ class ProductLocalDatasource { final dbExists = await databaseExists(path); if (dbExists) { log("Deleting existing database to ensure new schema with order_type column"); - await deleteDatabase(path); + // await deleteDatabase(path); } } catch (e) { log("Error deleting database: $e"); diff --git a/lib/main.dart b/lib/main.dart index e3017be..fd244fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,5 @@ import 'dart:developer'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:enaklo_pos/data/dataoutputs/laman_print.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart'; import 'package:enaklo_pos/data/datasources/category_remote_datasource.dart'; @@ -39,7 +37,6 @@ import 'package:enaklo_pos/presentation/setting/bloc/update_product/update_produ import 'package:enaklo_pos/presentation/setting/bloc/update_printer/update_printer_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/change_position_table/change_position_table_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/create_table/create_table_bloc.dart'; -import 'package:enaklo_pos/presentation/table/blocs/generate_table/generate_table_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/get_table/get_table_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/local_product/local_product_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/order/order_bloc.dart'; @@ -51,9 +48,7 @@ import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_b import 'package:enaklo_pos/presentation/setting/bloc/tax_settings/tax_settings_bloc.dart'; import 'package:enaklo_pos/presentation/table/blocs/update_table/update_table_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/add_order_items/add_order_items_bloc.dart'; -import 'package:enaklo_pos/presentation/table/pages/new_table_management_page.dart'; import 'package:google_fonts/google_fonts.dart'; -// import 'package:imin_printer/imin_printer.dart'; import 'core/constants/colors.dart'; import 'presentation/auth/bloc/login/login_bloc.dart'; @@ -61,8 +56,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'presentation/home/pages/dashboard_page.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/presentation/home/dialog/discount_dialog.dart b/lib/presentation/home/dialog/discount_dialog.dart index 30ecb8b..f951684 100644 --- a/lib/presentation/home/dialog/discount_dialog.dart +++ b/lib/presentation/home/dialog/discount_dialog.dart @@ -1,3 +1,5 @@ +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/data/models/response/discount_response_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; @@ -25,29 +27,38 @@ class _DiscountDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Stack( - alignment: Alignment.center, + backgroundColor: AppColors.white, + title: Row( children: [ - const Text( - 'DISKON', - style: TextStyle( - color: AppColors.primary, - fontSize: 28, - fontWeight: FontWeight.w600, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pilih Diskon', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.black, + ), + ), + const SizedBox(height: 4.0), + Text( + 'Pilih diskon yang ingin diterapkan pada pesanan', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], ), ), - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () { - context.pop(); - }, - icon: const Icon( - Icons.cancel, - color: AppColors.primary, - size: 30.0, - ), - ), + SpaceWidth(12), + IconButton( + icon: const Icon(Icons.close, color: AppColors.black), + onPressed: () { + context.pop(); + }, ), ], ), @@ -63,30 +74,7 @@ class _DiscountDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: discounts - .map( - (discount) => ListTile( - title: Text('Nama Diskon: ${discount.name}'), - subtitle: Text('Potongan harga (${discount.value}%)'), - contentPadding: EdgeInsets.zero, - textColor: AppColors.primary, - trailing: Checkbox( - value: discount.id == discountIdSelected, - onChanged: (value) { - setState(() { - discountIdSelected = discount.id!; - context.read().add( - CheckoutEvent.addDiscount( - discount, - ), - ); - }); - }, - ), - onTap: () { - // context.pop(); - }, - ), - ) + .map((discount) => _buildDiscountItem(discount)) .toList(), ); }, @@ -95,4 +83,58 @@ class _DiscountDialogState extends State { ), ); } + + Widget _buildDiscountItem(Discount discount) { + return GestureDetector( + onTap: () { + setState(() { + discountIdSelected = discount.id!; + context.read().add( + CheckoutEvent.addDiscount( + discount, + ), + ); + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: discountIdSelected == discount.id + ? AppColors.primary + : AppColors.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: discountIdSelected == discount.id + ? AppColors.primary + : AppColors.grey, + width: 1.0, + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + "${discount.name} (${discount.value})", + style: TextStyle( + fontSize: 16, + color: discountIdSelected == discount.id + ? AppColors.white + : AppColors.black, + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + SpaceWidth(12.0), + Icon(Icons.check_circle, + color: discountIdSelected == discount.id + ? AppColors.green + : Colors.transparent), + ], + ), + ), + ); + } } diff --git a/lib/presentation/home/dialog/service_dialog.dart b/lib/presentation/home/dialog/service_dialog.dart index 8d76cd6..a2caa69 100644 --- a/lib/presentation/home/dialog/service_dialog.dart +++ b/lib/presentation/home/dialog/service_dialog.dart @@ -1,3 +1,4 @@ +import 'package:enaklo_pos/core/components/spaces.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; @@ -11,27 +12,38 @@ class ServiceDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Stack( - alignment: Alignment.center, + backgroundColor: AppColors.white, + title: Row( children: [ - const Text( - 'LAYANAN', - style: TextStyle( - color: AppColors.primary, - fontSize: 28, - fontWeight: FontWeight.w600, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pilih Layanan', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.black, + ), + ), + const SizedBox(height: 4.0), + Text( + 'Pilih layanan yang ingin diterapkan pada pesanan', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], ), ), - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () => context.pop(), - icon: const Icon( - Icons.cancel, - color: AppColors.primary, - size: 30.0, - ), - ), + SpaceWidth(12), + IconButton( + icon: const Icon(Icons.close, color: AppColors.black), + onPressed: () { + context.pop(); + }, ), ], ), @@ -44,23 +56,8 @@ class ServiceDialog extends StatelessWidget { return state.maybeWhen( initial: () => const SizedBox(), loading: () => const Center(child: CircularProgressIndicator()), - loaded: (data, a, b, c, d, service, e, f, g, orderType) => ListTile( - title: Text('Presentase ($service%)'), - subtitle: const Text('Biaya layanan'), - contentPadding: EdgeInsets.zero, - textColor: AppColors.primary, - trailing: Checkbox( - value: service > 0, - onChanged: (value) { - context.read().add( - CheckoutEvent.addServiceCharge(service > 0 ? 0 : service), - ); - }, - ), - onTap: () { - context.pop(); - }, - ), + loaded: (data, a, b, c, d, service, e, f, g, orderType) => + _buildServiceItem(context, service), orElse: () => const SizedBox(), ); }, @@ -69,4 +66,47 @@ class ServiceDialog extends StatelessWidget { ), ); } + + Widget _buildServiceItem(BuildContext context, int service) { + return GestureDetector( + onTap: () { + context.read().add( + CheckoutEvent.addServiceCharge(service > 0 ? 0 : service), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: AppColors.primary, + width: 1.0, + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + "Biaya layanan ($service%)", + style: TextStyle( + fontSize: 16, + color: AppColors.white, + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + SpaceWidth(12.0), + Icon( + Icons.check_circle, + color: AppColors.green, + ), + ], + ), + ), + ); + } } diff --git a/lib/presentation/home/pages/home_page.dart b/lib/presentation/home/pages/home_page.dart index a255743..4512fc0 100644 --- a/lib/presentation/home/pages/home_page.dart +++ b/lib/presentation/home/pages/home_page.dart @@ -1,4 +1,5 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:enaklo_pos/presentation/home/widgets/home_right_title.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -8,9 +9,7 @@ import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; import 'package:enaklo_pos/presentation/home/bloc/local_product/local_product_bloc.dart'; import 'package:enaklo_pos/presentation/home/dialog/discount_dialog.dart'; -import 'package:enaklo_pos/presentation/home/dialog/tax_dialog.dart'; import 'package:enaklo_pos/presentation/home/pages/confirm_payment_page.dart'; -import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_bloc.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; @@ -21,7 +20,6 @@ import '../../../core/components/spaces.dart'; import '../../../core/constants/colors.dart'; import '../bloc/checkout/checkout_bloc.dart'; import '../dialog/service_dialog.dart'; -import '../widgets/column_button.dart'; import '../widgets/custom_tab_bar.dart'; import '../widgets/home_title.dart'; import '../widgets/order_menu.dart'; @@ -54,12 +52,12 @@ class _HomePageState extends State { void _syncAndLoadProducts() { // Trigger sync from API first context.read().add(const SyncProductEvent.syncProduct()); - + // Also load local products initially in case sync fails or takes time context .read() .add(const LocalProductEvent.getLocalProduct()); - + // Initialize checkout with tax and service charge settings context.read().add(const CheckoutEvent.started()); } @@ -75,7 +73,7 @@ class _HomePageState extends State { if (searchQuery.isEmpty) { return products; } - + return products.where((product) { final productName = product.name?.toLowerCase() ?? ''; final queryLower = searchQuery.toLowerCase(); @@ -83,9 +81,12 @@ class _HomePageState extends State { }).toList(); } - List _filterProductsByCategory(List products, int categoryId) { + List _filterProductsByCategory( + List products, int categoryId) { final filteredBySearch = _filterProducts(products); - return filteredBySearch.where((element) => element.category?.id == categoryId).toList(); + return filteredBySearch + .where((element) => element.category?.id == categoryId) + .toList(); } @override @@ -93,6 +94,7 @@ class _HomePageState extends State { return Hero( tag: 'confirmation_screen', child: Scaffold( + backgroundColor: AppColors.background, body: BlocListener( listener: (context, state) { state.maybeWhen( @@ -106,7 +108,7 @@ class _HomePageState extends State { loaded: (productResponseModel) async { // Store context reference before async operations final localProductBloc = context.read(); - + // Save synced products to local database await ProductLocalDatasource.instance.deleteAllProducts(); await ProductLocalDatasource.instance.insertProducts( @@ -123,195 +125,190 @@ class _HomePageState extends State { flex: 3, child: Align( alignment: AlignmentDirectional.topStart, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - HomeTitle( - controller: searchController, - onChanged: (value) { - setState(() { - searchQuery = value; - }); - }, - ), - const SizedBox(height: 24), - BlocBuilder( - builder: (context, state) { - return CustomTabBar( - tabTitles: const [ - 'Semua', - 'Makanan', - 'Minuman', - 'Paket' - ], - initialTabIndex: 0, - tabViews: [ - // All Products Tab - SizedBox( - child: state.maybeWhen(orElse: () { + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + HomeTitle( + controller: searchController, + onChanged: (value) { + setState(() { + searchQuery = value; + }); + }, + ), + BlocBuilder( + builder: (context, state) { + return Expanded( + child: CustomTabBarV2( + tabTitles: const [ + 'Semua', + 'Makanan', + 'Minuman', + 'Paket' + ], + tabViews: [ + // All Products Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + final filteredProducts = + _filterProducts(products); + if (filteredProducts.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items Found'), ); - }, loading: () { + } + return GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) => + ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ), + ); + }), + ), + // Makanan Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + if (products.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items'), ); - }, loaded: (products) { - final filteredProducts = _filterProducts(products); - if (filteredProducts.isEmpty) { - return const Center( - child: Text('No Items Found'), - ); - } - return GridView.builder( - shrinkWrap: true, - itemCount: filteredProducts.length, - physics: - const NeverScrollableScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 0.85, - crossAxisCount: 3, - crossAxisSpacing: 30.0, - mainAxisSpacing: 30.0, - ), - itemBuilder: (context, index) => - ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ), - ); - }), - ), - // Makanan Tab - SizedBox( - child: state.maybeWhen(orElse: () { + } + final filteredProducts = + _filterProductsByCategory(products, 1); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) => + ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ), + ); + }), + ), + // Minuman Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + if (products.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items'), ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - final filteredProducts = _filterProductsByCategory(products, 1); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - shrinkWrap: true, - itemCount: filteredProducts.length, - physics: - const NeverScrollableScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 0.85, - crossAxisCount: 3, - crossAxisSpacing: 30.0, - mainAxisSpacing: 30.0, - ), - itemBuilder: (context, index) => - ProductCard( + } + final filteredProducts = + _filterProductsByCategory(products, 2); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) { + return ProductCard( data: filteredProducts[index], onCartButton: () {}, - ), - ); - }), - ), - // Minuman Tab - SizedBox( - child: state.maybeWhen(orElse: () { + ); + }, + ); + }), + ), + // Snack Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products) { + if (products.isEmpty) { return const Center( - child: CircularProgressIndicator(), + child: Text('No Items'), ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - final filteredProducts = _filterProductsByCategory(products, 2); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - shrinkWrap: true, - itemCount: filteredProducts.length, - physics: - const NeverScrollableScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 0.85, - crossAxisCount: 3, - crossAxisSpacing: 30.0, - mainAxisSpacing: 30.0, - ), - itemBuilder: (context, index) { - return ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ); - }, - ); - }), - ), - // Snack Tab - SizedBox( - child: state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - final filteredProducts = _filterProductsByCategory(products, 3); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - shrinkWrap: true, - itemCount: filteredProducts.length, - physics: - const NeverScrollableScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 0.85, - crossAxisCount: 3, - crossAxisSpacing: 30.0, - mainAxisSpacing: 30.0, - ), - itemBuilder: (context, index) { - return ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ); - }, - ); - }), - ), - ], - ); - }, - ), - ], + } + final filteredProducts = + _filterProductsByCategory(products, 3); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: filteredProducts.length, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) { + return ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ); + }, + ); + }), + ), + ], + ), + ); + }, ), - ), + ], ), ), ), @@ -319,344 +316,363 @@ class _HomePageState extends State { flex: 2, child: Align( alignment: Alignment.topCenter, - child: Stack( - children: [ - SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Material( + color: Colors.white, + child: Column( + children: [ + Column( children: [ - GestureDetector( - onTap: () { - if (widget.table == null) { - context.push(DashboardPage( - index: 1, - )); - } - }, - child: Text( - 'Meja: ${widget.table == null ? 'Belum Pilih Meja' : '${widget.table!.id}'}', - style: TextStyle( - color: AppColors.primary, - fontSize: 20, - fontWeight: FontWeight.w600, + HomeRightTitle( + table: widget.table, + ), + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: AppColors.grey, + width: 1.0, + ), ), ), - ), - const SpaceHeight(8.0), - Button.filled( - width: 180.0, - height: 40, - onPressed: () {}, - label: 'Pesanan#', - ), - - // Row( - // children: [ - // Button.filled( - // width: 120.0, - // height: 40, - // onPressed: () {}, - // label: 'Dine In', - // ), - // const SpaceWidth(8.0), - // Button.outlined( - // width: 100.0, - // height: 40, - // onPressed: () {}, - // label: 'To Go', - // ), - // ], - // ), - const SpaceHeight(16.0), - const Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Item', - style: TextStyle( - color: AppColors.primary, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - SizedBox( - width: 130, - ), - SizedBox( - width: 50.0, - child: Text( - 'Qty', + child: const Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Order #", style: TextStyle( - color: AppColors.primary, fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), - SizedBox( - child: Text( - 'Price', - style: TextStyle( - color: AppColors.primary, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - const SpaceHeight(8), - const Divider(), - const SpaceHeight(8), - BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () => const Center( - child: Text('No Items'), - ), - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, orderType) { - if (products.isEmpty) { - return const Center( - child: Text('No Items'), - ); - } - return ListView.separated( - shrinkWrap: true, - physics: - const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => - OrderMenu(data: products[index]), - separatorBuilder: (context, index) => - const SpaceHeight(1.0), - itemCount: products.length, - ); - }, - ); - }, - ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ColumnButton( - label: 'Diskon', - svgGenImage: Assets.icons.diskon, - onPressed: () => showDialog( - context: context, - barrierDismissible: false, - builder: (context) => const DiscountDialog(), - ), - ), - ColumnButton( - label: 'Pajak PB1', - svgGenImage: Assets.icons.pajak, - onPressed: () => showDialog( - context: context, - builder: (context) => const TaxDialog(), - ), - ), - ColumnButton( - label: 'Layanan', - svgGenImage: Assets.icons.layanan, - onPressed: () => showDialog( - context: context, - builder: (context) => const ServiceDialog(), - ), - ), - ], - ), - const SpaceHeight(8.0), - const Divider(), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Pajak PB1', - style: TextStyle(color: AppColors.grey), - ), - BlocBuilder( - builder: (context, state) { - final tax = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, orderType) { - if (products.isEmpty) { - return 0; - } - return tax; - }); - return Text( - '$tax %', - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], - ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Layanan', - style: TextStyle(color: AppColors.grey), - ), - BlocBuilder( - builder: (context, state) { - final serviceCharge = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, orderType) { - return serviceCharge; - }); - return Text( - '$serviceCharge %', - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], - ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Diskon', - style: TextStyle(color: AppColors.grey), - ), - BlocBuilder( - builder: (context, state) { - final discount = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, orderType) { - if (discountModel == null) { - return 0; - } - return discountModel.value! - .replaceAll('.00', '') - .toIntegerFromText; - }); - return Text( - '$discount %', - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], - ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Sub total', - style: TextStyle( - color: AppColors.grey, fontWeight: FontWeight.bold, - fontSize: 16), - ), - BlocBuilder( - builder: (context, state) { - final price = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, orderType) { - if (products.isEmpty) { - return 0; - } - return products - .map((e) => - e.product.price! - .toIntegerFromText * - e.quantity) - .reduce((value, element) => - value + element); - }); - - return Text( - price.currencyFormatRp, - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.bold, - fontSize: 16), - ); - }, - ), - ], + ), + ), + Text( + "Total: 0", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), ), - const SpaceHeight(100.0), ], ), - ), - Align( - alignment: Alignment.bottomCenter, - child: ColoredBox( - color: AppColors.white, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 24.0, vertical: 16.0), - child: Button.filled( - onPressed: () { - context.push(ConfirmPaymentPage( - isTable: widget.isTable, - table: widget.table, - )); - }, - label: 'Lanjutkan Pembayaran', + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => const Center( + child: Text('No Items'), + ), + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return const Center( + child: Text('No Items'), + ); + } + return ListView.separated( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => + OrderMenu(data: products[index]), + separatorBuilder: (context, index) => + const SpaceHeight(1.0), + itemCount: products.length, + ); + }, + ); + }, + ), + const SpaceHeight(8.0), + ], ), ), ), - ), - ], + Column( + children: [ + Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + // Row( + // mainAxisAlignment: + // MainAxisAlignment.spaceEvenly, + // children: [ + // ColumnButton( + // label: 'Diskon', + // svgGenImage: Assets.icons.diskon, + // onPressed: () => showDialog( + // context: context, + // barrierDismissible: false, + // builder: (context) => + // const DiscountDialog(), + // ), + // ), + // ColumnButton( + // label: 'Pajak PB1', + // svgGenImage: Assets.icons.pajak, + // onPressed: () => showDialog( + // context: context, + // builder: (context) => + // const TaxDialog(), + // ), + // ), + // ColumnButton( + // label: 'Layanan', + // svgGenImage: Assets.icons.layanan, + // onPressed: () => showDialog( + // context: context, + // builder: (context) => + // const ServiceDialog(), + // ), + // ), + // ], + // ), + // const SpaceHeight(8.0), + // const Divider(), + // const SpaceHeight(8.0), + + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Sub total', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, + ), + ), + BlocBuilder( + builder: (context, state) { + final price = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return 0; + } + return products + .map((e) => + e.product.price! + .toIntegerFromText * + e.quantity) + .reduce((value, element) => + value + element); + }); + + return Text( + price.currencyFormatRp, + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w900, + ), + ); + }, + ), + ], + ), + const SpaceHeight(8.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Pajak PB1', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, + ), + ), + BlocBuilder( + builder: (context, state) { + final tax = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return 0; + } + return tax; + }); + return Text( + '$tax %', + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ); + }, + ), + ], + ), + const SpaceHeight(8.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Layanan', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, + ), + ), + BlocBuilder( + builder: (context, state) { + final serviceCharge = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + return serviceCharge; + }); + return Text( + '$serviceCharge %', + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ); + }, + ), + ], + ), + const SpaceHeight(8.0), + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildSmallButton( + onTap: () => showDialog( + context: context, + builder: (context) => + const ServiceDialog(), + ), + label: 'Layanan', + ), + SpaceWidth(8), + _buildSmallButton( + onTap: () => showDialog( + context: context, + builder: (context) => + const DiscountDialog(), + ), + label: 'Diskon', + ), + ], + ), + const SpaceHeight(8.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Diskon', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, + ), + ), + BlocBuilder( + builder: (context, state) { + final discount = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (discountModel == null) { + return 0; + } + return discountModel.value! + .replaceAll('.00', '') + .toIntegerFromText; + }); + return Text( + '$discount %', + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, + ), + ); + }, + ), + ], + ), + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: context.deviceHeight * 0.09, + width: double.infinity, + color: AppColors.white, + child: Expanded( + child: Button.filled( + borderRadius: 0, + elevation: 0, + onPressed: () { + context.push(ConfirmPaymentPage( + isTable: widget.isTable, + table: widget.table, + )); + }, + label: 'Lanjutkan Pembayaran', + ), + ), + ), + ), + ], + ), + ], + ), ), ), ), @@ -666,6 +682,33 @@ class _HomePageState extends State { ), ); } + + GestureDetector _buildSmallButton({ + required Function() onTap, + required String label, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10.0, + vertical: 6.0, + ), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + label, + style: TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.bold, + fontSize: 12.0, + ), + ), + ), + ); + } } class _IsEmpty extends StatelessWidget { diff --git a/lib/presentation/home/widgets/custom_tab_bar.dart b/lib/presentation/home/widgets/custom_tab_bar.dart index ca7c652..ba1bdcc 100644 --- a/lib/presentation/home/widgets/custom_tab_bar.dart +++ b/lib/presentation/home/widgets/custom_tab_bar.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import '../../../core/constants/colors.dart'; - - class CustomTabBar extends StatefulWidget { final List tabTitles; final int initialTabIndex; @@ -34,33 +32,38 @@ class _CustomTabBarState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - children: List.generate( - widget.tabTitles.length, - (index) => GestureDetector( - onTap: () { - setState(() { - _selectedIndex = index; - }); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 14), - margin: const EdgeInsets.only(right: 32), - decoration: BoxDecoration( - border: _selectedIndex == index - ? const Border( - bottom: BorderSide( - width: 3.0, - color: AppColors.primary, - ), - ) - : null, - ), - child: Text( - widget.tabTitles[index], - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.bold, + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + ), + child: Row( + children: List.generate( + widget.tabTitles.length, + (index) => GestureDetector( + onTap: () { + setState(() { + _selectedIndex = index; + }); + }, + child: Container( + padding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + margin: const EdgeInsets.only(right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: _selectedIndex == index + ? AppColors.primary + : Colors.transparent, + ), + child: Text( + widget.tabTitles[index], + style: TextStyle( + color: _selectedIndex == index + ? Colors.white + : AppColors.primary, + fontWeight: FontWeight.bold, + ), ), ), ), @@ -75,3 +78,61 @@ class _CustomTabBarState extends State { ); } } + +class CustomTabBarV2 extends StatelessWidget { + final List tabTitles; + final List tabViews; + + const CustomTabBarV2({ + super.key, + required this.tabTitles, + required this.tabViews, + }); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: tabTitles.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Material( + elevation: 0, + color: Colors.white, + borderOnForeground: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + labelColor: Colors.white, + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + ), + dividerColor: Colors.transparent, + unselectedLabelColor: AppColors.primary, + indicator: BoxDecoration( + color: AppColors.primary, // Warna button saat aktif + borderRadius: BorderRadius.circular(8), + ), + indicatorColor: Colors.transparent, + tabs: tabTitles + .map((title) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Tab(text: title), + )) + .toList(), + ), + ), + ), + Expanded( + // ✅ ini bagian penting + child: TabBarView( + children: tabViews, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/home/widgets/home_right_title.dart b/lib/presentation/home/widgets/home_right_title.dart new file mode 100644 index 0000000..ed4fdbd --- /dev/null +++ b/lib/presentation/home/widgets/home_right_title.dart @@ -0,0 +1,60 @@ +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/data/models/response/table_model.dart'; +import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; +import 'package:flutter/material.dart'; + +class HomeRightTitle extends StatelessWidget { + final TableModel? table; + const HomeRightTitle({super.key, this.table}); + + @override + Widget build(BuildContext context) { + return Container( + height: context.deviceHeight * 0.1, + decoration: BoxDecoration( + color: AppColors.primary, + border: Border( + left: BorderSide( + color: Colors.white, + width: 1.0, + ), + ), + ), + child: Row( + children: [ + Expanded( + child: Button.filled( + width: 180.0, + height: context.deviceHeight, + elevation: 0, + onPressed: () {}, + label: 'List Order', + ), + ), + Container( + width: 1, + height: context.deviceHeight, + color: Colors.white, + ), + Expanded( + child: Button.filled( + width: 180.0, + height: context.deviceHeight, + elevation: 0, + onPressed: () { + if (table == null) { + context.push(DashboardPage( + index: 1, + )); + } + }, + label: table == null ? 'Pilih Meja' : '${table!.id}', + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/home/widgets/home_title.dart b/lib/presentation/home/widgets/home_title.dart index 17636cc..356c232 100644 --- a/lib/presentation/home/widgets/home_title.dart +++ b/lib/presentation/home/widgets/home_title.dart @@ -1,3 +1,4 @@ +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; @@ -16,39 +17,52 @@ class HomeTitle extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Enaklo POS', - style: TextStyle( - color: AppColors.primary, - fontSize: 22, - fontWeight: FontWeight.w600, + return Container( + height: context.deviceHeight * 0.1, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: AppColors.primary, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.store, color: AppColors.white, size: 32.0), + SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Enaklo POS', + style: TextStyle( + color: AppColors.white, + fontSize: 22, + fontWeight: FontWeight.w600, + ), + ), + Text( + DateTime.now().toFormattedDate(), + style: TextStyle( + color: Colors.grey.shade300, + fontSize: 16, + ), + ), + ], ), - ), - const SizedBox(height: 4.0), - Text( - DateTime.now().toFormattedDate(), - style: const TextStyle( - color: AppColors.subtitle, - fontSize: 16, - ), - ), - ], - ), - SizedBox( - width: 300.0, - child: SearchInput( - controller: controller, - onChanged: onChanged, - hintText: 'Search..', + ], ), - ), - ], + SizedBox( + width: 300.0, + child: SearchInput( + controller: controller, + onChanged: onChanged, + hintText: 'Search..', + ), + ), + ], + ), ); } } diff --git a/lib/presentation/home/widgets/item_notes_dialog.dart b/lib/presentation/home/widgets/item_notes_dialog.dart index 2384997..319b971 100644 --- a/lib/presentation/home/widgets/item_notes_dialog.dart +++ b/lib/presentation/home/widgets/item_notes_dialog.dart @@ -1,16 +1,15 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; -import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import '../../../core/components/buttons.dart'; import '../../../core/components/spaces.dart'; -import '../../../core/constants/colors.dart'; class ItemNotesDialog extends StatefulWidget { final ProductQuantity item; - + const ItemNotesDialog({ super.key, required this.item, @@ -38,24 +37,52 @@ class _ItemNotesDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Add Notes'), + backgroundColor: Colors.white, + title: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.item.product.name ?? 'Catatan Item', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppColors.black, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4.0), + Text( + 'Masukkan catatan untuk item ini', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ), + SpaceWidth(12), + IconButton( + icon: const Icon(Icons.close, color: AppColors.black), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.item.product.name ?? 'Product', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - const SpaceHeight(16.0), TextField( controller: notesController, maxLines: 3, decoration: const InputDecoration( - hintText: 'Enter notes for this item...', + hintText: 'Masukkan catatan untuk item ini', border: OutlineInputBorder(), ), ), @@ -69,11 +96,11 @@ class _ItemNotesDialogState extends State { Button.filled( onPressed: () { context.read().add( - CheckoutEvent.updateItemNotes( - widget.item.product, - notesController.text, - ), - ); + CheckoutEvent.updateItemNotes( + widget.item.product, + notesController.text, + ), + ); Navigator.of(context).pop(); }, label: 'Save', @@ -81,4 +108,4 @@ class _ItemNotesDialogState extends State { ], ); } -} \ No newline at end of file +} diff --git a/lib/presentation/home/widgets/order_menu.dart b/lib/presentation/home/widgets/order_menu.dart index 667675f..62629a0 100644 --- a/lib/presentation/home/widgets/order_menu.dart +++ b/lib/presentation/home/widgets/order_menu.dart @@ -15,6 +15,166 @@ class OrderMenu extends StatelessWidget { final ProductQuantity data; const OrderMenu({super.key, required this.data}); + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(16.0), + margin: EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: AppColors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.0), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + data.product.name ?? "_", + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(width: 12.0), + _buildIconButton( + onTap: () { + showDialog( + context: context, + builder: (context) => ItemNotesDialog(item: data), + ); + }, + icon: Icons.edit_note, + ), + SizedBox(width: 8.0), + _buildIconButton( + onTap: () {}, + icon: Icons.delete_outline, + iconColor: AppColors.red, + ), + ], + ), + const SpaceHeight(12.0), + Row( + children: [ + Expanded( + child: Text( + (data.product.price!.toIntegerFromText * data.quantity) + .currencyFormatRp, + overflow: TextOverflow.ellipsis, + maxLines: 2, + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + SpaceWidth(16), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(50.0), + ), + child: Row( + children: [ + _buildIconButton( + onTap: () { + context + .read() + .add(CheckoutEvent.removeItem(data.product)); + }, + icon: Icons.remove, + iconColor: AppColors.primary, + bgColor: Colors.grey.shade300, + ), + SizedBox( + width: 30.0, + child: Center( + child: Text( + data.quantity.toString(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ), + _buildIconButton( + onTap: () { + context + .read() + .add(CheckoutEvent.addItem(data.product)); + }, + icon: Icons.add, + iconColor: AppColors.white, + bgColor: AppColors.primary, + ), + ], + ), + ), + ], + ), + if (data.notes.isNotEmpty) ...[ + SpaceHeight(8.0), + Divider(), + SpaceHeight(8.0), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + 'Notes: ${data.notes}', + style: const TextStyle( + fontSize: 14, + color: AppColors.black, + fontWeight: FontWeight.w500, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ], + ), + ); + } + + GestureDetector _buildIconButton({ + required Function()? onTap, + Color iconColor = AppColors.black, + Color bgColor = AppColors.white, + required IconData icon, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: bgColor, + shape: BoxShape.circle, + ), + child: Icon( + icon, + size: 20, + color: iconColor, + ), + ), + ); + } +} + +class OrderMenuOld extends StatelessWidget { + final ProductQuantity data; + const OrderMenuOld({super.key, required this.data}); + @override Widget build(BuildContext context) { return Column( @@ -39,6 +199,8 @@ class OrderMenu extends StatelessWidget { width: 50.0, height: 50.0, fit: BoxFit.cover, + errorWidget: (context, url, error) => + const Icon(Icons.error), ), ), title: Row( @@ -70,7 +232,8 @@ class OrderMenu extends StatelessWidget { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(data.product.price!.toIntegerFromText.currencyFormatRp), + Text( + data.product.price!.toIntegerFromText.currencyFormatRp), if (data.notes.isNotEmpty) ...[ const SpaceHeight(4.0), Container( diff --git a/lib/presentation/home/widgets/product_card.dart b/lib/presentation/home/widgets/product_card.dart index 0477d13..79d59c4 100644 --- a/lib/presentation/home/widgets/product_card.dart +++ b/lib/presentation/home/widgets/product_card.dart @@ -1,4 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/constants/variables.dart'; @@ -14,8 +15,87 @@ import '../../../core/constants/colors.dart'; class ProductCard extends StatelessWidget { final Product data; final VoidCallback onCartButton; + const ProductCard( + {super.key, required this.data, required this.onCartButton}); - const ProductCard({ + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + context.read().add(CheckoutEvent.addItem(data)); + }, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(12.0), + ), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: CachedNetworkImage( + imageUrl: data.image!.contains('http') + ? data.image! + : '${Variables.baseUrl}/${data.image}', + width: double.infinity, + height: context.deviceHeight * 0.18, + fit: BoxFit.fill, + errorWidget: (context, url, error) => Container( + width: double.infinity, + height: context.deviceHeight * 0.18, + decoration: BoxDecoration( + color: AppColors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.image_outlined, + color: AppColors.grey, + size: 40, + ), + ), + ), + ), + const SpaceHeight(8.0), + Row( + children: [ + Expanded( + child: Text( + "${data.name}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Spacer(), + const SpaceHeight(4.0), + Align( + alignment: Alignment.centerRight, + child: Text( + data.price!.toIntegerFromText.currencyFormatRp, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: AppColors.primary, + ), + ), + ), + ], + ), + )); + } +} + +class ProductCardOld extends StatelessWidget { + final Product data; + final VoidCallback onCartButton; + + const ProductCardOld({ super.key, required this.data, required this.onCartButton, @@ -29,11 +109,9 @@ class ProductCard extends StatelessWidget { }, child: Container( padding: const EdgeInsets.all(16.0), - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide(width: 1, color: AppColors.card), - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(12.0), ), child: Stack( children: [ @@ -63,6 +141,8 @@ class ProductCard extends StatelessWidget { width: 60, height: 60, fit: BoxFit.cover, + errorWidget: (context, url, error) => + const Icon(Icons.error), ), ), ), From 805673755ba9bbc2aa299e0e22776da8fa14c1de Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 31 Jul 2025 20:58:10 +0700 Subject: [PATCH 002/128] feat: setting page and setting product page --- lib/core/components/buttons.dart | 2 +- .../home/widgets/product_card.dart | 39 +-- .../setting/pages/product_page.dart | 121 +++++----- .../setting/pages/setting_tile.dart | 72 ++++++ .../setting/pages/settings_page.dart | 224 ++++++++++-------- .../setting/widgets/menu_product_item.dart | 217 ++++++++++++++++- .../setting/widgets/settings_title.dart | 66 ++++-- 7 files changed, 538 insertions(+), 203 deletions(-) create mode 100644 lib/presentation/setting/pages/setting_tile.dart diff --git a/lib/core/components/buttons.dart b/lib/core/components/buttons.dart index 77bec38..e763f81 100644 --- a/lib/core/components/buttons.dart +++ b/lib/core/components/buttons.dart @@ -97,7 +97,7 @@ class Button extends StatelessWidget { onPressed: disabled ? null : onPressed, style: OutlinedButton.styleFrom( backgroundColor: color, - side: const BorderSide(color: Colors.grey), + side: const BorderSide(color: AppColors.primary), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadius), ), diff --git a/lib/presentation/home/widgets/product_card.dart b/lib/presentation/home/widgets/product_card.dart index 79d59c4..c8d4bdd 100644 --- a/lib/presentation/home/widgets/product_card.dart +++ b/lib/presentation/home/widgets/product_card.dart @@ -32,26 +32,29 @@ class ProductCard extends StatelessWidget { ), child: Column( children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: CachedNetworkImage( - imageUrl: data.image!.contains('http') - ? data.image! - : '${Variables.baseUrl}/${data.image}', - width: double.infinity, - height: context.deviceHeight * 0.18, - fit: BoxFit.fill, - errorWidget: (context, url, error) => Container( + AspectRatio( + aspectRatio: 1.2, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: CachedNetworkImage( + imageUrl: data.image!.contains('http') + ? data.image! + : '${Variables.baseUrl}/${data.image}', width: double.infinity, height: context.deviceHeight * 0.18, - decoration: BoxDecoration( - color: AppColors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.image_outlined, - color: AppColors.grey, - size: 40, + fit: BoxFit.cover, + errorWidget: (context, url, error) => Container( + width: double.infinity, + height: context.deviceHeight * 0.18, + decoration: BoxDecoration( + color: AppColors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.image_outlined, + color: AppColors.grey, + size: 40, + ), ), ), ), diff --git a/lib/presentation/setting/pages/product_page.dart b/lib/presentation/setting/pages/product_page.dart index 50bcc50..76a110b 100644 --- a/lib/presentation/setting/pages/product_page.dart +++ b/lib/presentation/setting/pages/product_page.dart @@ -1,3 +1,5 @@ +import 'package:enaklo_pos/core/components/buttons.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; @@ -27,40 +29,68 @@ class _ProductPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: ListView( - padding: const EdgeInsets.all(24.0), + backgroundColor: AppColors.background, + body: Column( children: [ - const SettingsTitle('Manage Products'), - const SizedBox(height: 24), - BlocBuilder( - builder: (context, state) { - return state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, success: (products) { - return GridView.builder( - padding: EdgeInsets.zero, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1, - crossAxisCount: 3, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - ), - itemCount: products.length + 1, - shrinkWrap: true, - physics: const ScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - if (index == 0) { - return AddData( - title: 'Add New Product', - onPressed: () { + SettingsTitle( + 'Kelola Produk', + subtitle: 'Kelola produk anda', + actionWidget: [ + Button.outlined( + onPressed: () { + showDialog( + context: context, + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + AddProductBloc(ProductRemoteDatasource()), + ), + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: context.read(), + ), + ], + child: const FormProductDialog(), + ), + ); + }, + label: "Tambah Produk", + icon: Icon(Icons.add, color: AppColors.primary), + ) + ], + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, success: (products) { + return GridView.builder( + padding: EdgeInsets.all(16), + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemCount: products.length, + itemBuilder: (BuildContext context, int index) { + final item = products[index]; + return MenuProductItem( + data: item, + onTapEdit: () { showDialog( context: context, builder: (context) => MultiBlocProvider( providers: [ BlocProvider( - create: (context) => AddProductBloc(ProductRemoteDatasource()), + create: (context) => UpdateProductBloc( + ProductRemoteDatasource()), ), BlocProvider.value( value: context.read(), @@ -69,39 +99,16 @@ class _ProductPageState extends State { value: context.read(), ), ], - child: const FormProductDialog(), + child: FormProductDialog(product: item), ), ); }, ); - } - final item = products[index - 1]; - return MenuProductItem( - data: item, - onTapEdit: () { - showDialog( - context: context, - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => UpdateProductBloc(ProductRemoteDatasource()), - ), - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: context.read(), - ), - ], - child: FormProductDialog(product: item), - ), - ); - }, - ); - }, - ); - }); - }, + }, + ); + }); + }, + ), ), ], ), diff --git a/lib/presentation/setting/pages/setting_tile.dart b/lib/presentation/setting/pages/setting_tile.dart new file mode 100644 index 0000000..217c09f --- /dev/null +++ b/lib/presentation/setting/pages/setting_tile.dart @@ -0,0 +1,72 @@ +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:flutter/material.dart'; + +class SettingTile extends StatelessWidget { + final int index; + final int currentIndex; + final String title; + final String subtitle; + final IconData icon; + final Function() onTap; + + const SettingTile({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + required this.onTap, + required this.index, + required this.currentIndex, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + border: Border( + right: BorderSide( + color: currentIndex == index + ? AppColors.primary + : Colors.transparent, + width: 4.0, + ), + ), + ), + child: Row( + children: [ + Icon( + icon, + size: 24.0, + color: currentIndex == index ? AppColors.primary : AppColors.grey, + ), + const SpaceWidth(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4.0), + Text( + subtitle, + style: + const TextStyle(fontSize: 14.0, color: AppColors.grey), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/setting/pages/settings_page.dart b/lib/presentation/setting/pages/settings_page.dart index 4f7958b..589e721 100644 --- a/lib/presentation/setting/pages/settings_page.dart +++ b/lib/presentation/setting/pages/settings_page.dart @@ -1,15 +1,14 @@ +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/presentation/setting/pages/setting_tile.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/presentation/sales/pages/sales_page.dart'; import 'package:enaklo_pos/presentation/setting/pages/discount_page.dart'; -import 'package:enaklo_pos/presentation/setting/pages/manage_printer_page.dart'; import 'package:enaklo_pos/presentation/setting/pages/product_page.dart'; import 'package:enaklo_pos/presentation/setting/pages/server_key_page.dart'; import 'package:enaklo_pos/presentation/setting/pages/sync_data_page.dart'; import 'package:enaklo_pos/presentation/setting/pages/tax_page.dart'; -import '../../../core/assets/assets.gen.dart'; -import '../../../core/components/spaces.dart'; import '../../../core/constants/colors.dart'; class SettingsPage extends StatefulWidget { @@ -45,6 +44,7 @@ class _SettingsPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: AppColors.background, body: Row( children: [ // LEFT CONTENT @@ -52,89 +52,108 @@ class _SettingsPageState extends State { flex: 2, child: Align( alignment: Alignment.topCenter, - child: ListView( - padding: const EdgeInsets.all(16.0), - children: [ - const Text( - 'Settings', - style: TextStyle( - color: AppColors.primary, - fontSize: 28, - fontWeight: FontWeight.w600, - ), - ), - const SpaceHeight(16.0), - role != null && role! != 'admin' - ? const SizedBox() - : ListTile( - contentPadding: const EdgeInsets.all(12.0), - leading: Assets.icons.kelolaProduk.svg(), - title: const Text('Manage Products'), - subtitle: const Text('Manage products in your store'), - textColor: AppColors.primary, - tileColor: currentIndex == 0 - ? AppColors.blueLight - : Colors.transparent, - onTap: () => indexValue(0), + child: Material( + color: AppColors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + width: double.infinity, + height: context.deviceHeight * 0.1, + decoration: const BoxDecoration( + color: AppColors.white, + border: Border( + bottom: BorderSide( + color: AppColors.background, + width: 1.0, + ), ), - ListTile( - contentPadding: const EdgeInsets.all(12.0), - leading: Assets.icons.kelolaDiskon.svg(), - title: const Text('Kelola Diskon'), - subtitle: const Text('Kelola Diskon Pelanggan'), - textColor: AppColors.primary, - tileColor: currentIndex == 1 - ? AppColors.blueLight - : Colors.transparent, - onTap: () => indexValue(1), - ), - ListTile( - contentPadding: const EdgeInsets.all(12.0), - leading: Assets.icons.dashboard.svg(), - title: const Text('History Transaksi'), - subtitle: const Text('Lihat history transaksi'), - textColor: AppColors.primary, - tileColor: currentIndex == 2 - ? AppColors.blueLight - : Colors.transparent, - onTap: () => indexValue(2), - ), - ListTile( - contentPadding: const EdgeInsets.all(12.0), - leading: Assets.icons.kelolaPajak.svg(), - title: const Text('Perhitungan Biaya'), - subtitle: const Text('Kelola biaya diluar biaya modal'), - textColor: AppColors.primary, - tileColor: currentIndex == 3 - ? AppColors.blueLight - : Colors.transparent, - onTap: () => indexValue(3), - ), - ListTile( - contentPadding: const EdgeInsets.all(12.0), - leading: Assets.icons.kelolaPajak.svg(), - title: const Text('Sync Data'), - subtitle: - const Text('Sinkronisasi data dari dan ke server'), - textColor: AppColors.primary, - tileColor: currentIndex == 4 - ? AppColors.blueLight - : Colors.transparent, - onTap: () => indexValue(4), - ), - ListTile( - contentPadding: const EdgeInsets.all(12.0), - leading: Image.asset(Assets.images.manageQr.path, - fit: BoxFit.contain), - title: const Text('QR Key Setting'), - subtitle: const Text('QR Key Configuration'), - textColor: AppColors.primary, - tileColor: currentIndex == 6 - ? AppColors.blueLight - : Colors.transparent, - onTap: () => indexValue(6), - ), - ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Pengaturan', + style: TextStyle( + color: AppColors.black, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + Text( + 'Kelola pengaturan aplikasi', + style: TextStyle( + color: AppColors.grey, + fontSize: 14, + ), + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + role != null && role! != 'admin' + ? const SizedBox() + : SettingTile( + index: 0, + currentIndex: currentIndex, + title: 'Kelola Produk', + subtitle: 'Kelola produk anda', + icon: Icons.inventory_outlined, + onTap: () => indexValue(0), + ), + SettingTile( + index: 1, + currentIndex: currentIndex, + title: 'Kelola Diskon', + subtitle: 'Kelola diskon pelanggan', + icon: Icons.discount_outlined, + onTap: () => indexValue(1), + ), + SettingTile( + index: 2, + currentIndex: currentIndex, + title: 'Riwayat Transaksi', + subtitle: 'Lihat riwayat transaksi', + icon: Icons.receipt_long_outlined, + onTap: () => indexValue(2), + ), + SettingTile( + index: 3, + currentIndex: currentIndex, + title: 'Perhitungan Biaya', + subtitle: 'Kelola biaya diluar biaya modal', + icon: Icons.attach_money_outlined, + onTap: () => indexValue(3), + ), + SettingTile( + index: 4, + currentIndex: currentIndex, + title: 'Sinkronisasi Data', + subtitle: 'Sinkronisasi data dari dan ke server', + icon: Icons.sync_outlined, + onTap: () => indexValue(4), + ), + SettingTile( + index: 6, + currentIndex: currentIndex, + title: 'Qr Key Setting', + subtitle: 'Kelola QR Key', + icon: Icons.qr_code_2_outlined, + onTap: () => indexValue(6), + ), + ], + ), + ), + ), + ], + ), ), ), ), @@ -144,26 +163,21 @@ class _SettingsPageState extends State { flex: 4, child: Align( alignment: AlignmentDirectional.topStart, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: IndexedStack( - index: currentIndex, - children: [ - role != null && role! != 'admin' - ? SizedBox() - : ProductPage(), - DiscountPage(), - SalesPage(), - TaxPage(), - SyncDataPage(), - ProductPage(), - ServerKeyPage() - // Text('tax'), - // ManageDiscount(), - // ManagePrinterPage(), - // ManageTax(), - ], - ), + child: IndexedStack( + index: currentIndex, + children: [ + role != null && role! != 'admin' ? SizedBox() : ProductPage(), + DiscountPage(), + SalesPage(), + TaxPage(), + SyncDataPage(), + ProductPage(), + ServerKeyPage() + // Text('tax'), + // ManageDiscount(), + // ManagePrinterPage(), + // ManageTax(), + ], ), ), ), diff --git a/lib/presentation/setting/widgets/menu_product_item.dart b/lib/presentation/setting/widgets/menu_product_item.dart index 083393d..734e02d 100644 --- a/lib/presentation/setting/widgets/menu_product_item.dart +++ b/lib/presentation/setting/widgets/menu_product_item.dart @@ -1,5 +1,6 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:cached_network_image/cached_network_image.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; @@ -11,7 +12,221 @@ import '../../../core/constants/variables.dart'; class MenuProductItem extends StatelessWidget { final Product data; final Function() onTapEdit; - const MenuProductItem({ + const MenuProductItem( + {super.key, required this.data, required this.onTapEdit}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.zero, + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(12.0), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8.0).copyWith(bottom: 0), + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1.2, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: CachedNetworkImage( + imageUrl: data.image!.contains('http') + ? data.image! + : '${Variables.baseUrl}/${data.image}', + fit: BoxFit.cover, + errorWidget: (context, url, error) => Container( + width: double.infinity, + height: context.deviceHeight * 0.18, + decoration: BoxDecoration( + color: AppColors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.image_outlined, + color: AppColors.grey, + size: 40, + ), + ), + ), + ), + ), + Positioned( + top: 8, + right: 8, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + data.category?.name ?? "", + style: const TextStyle( + color: AppColors.white, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + children: [ + Expanded( + child: Text( + "${data.name}", + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + showDialog( + context: context, + // backgroundColor: AppColors.white, + builder: (context) { + //container for product detail + return AlertDialog( + contentPadding: const EdgeInsets.all(16.0), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + data.name!, + style: const TextStyle( + fontSize: 20, + ), + ), + IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close), + ), + ], + ), + const SpaceHeight(10.0), + ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(10.0)), + child: CachedNetworkImage( + imageUrl: + '${Variables.baseUrl}${data.image}', + placeholder: (context, url) => const Center( + child: CircularProgressIndicator()), + errorWidget: (context, url, error) => + const Icon( + Icons.food_bank_outlined, + size: 80, + ), + width: 80, + ), + ), + const SpaceHeight(10.0), + Text( + data.category?.name ?? '-', + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SpaceHeight(10.0), + Text( + data.price.toString(), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SpaceHeight(10.0), + Text( + data.stock.toString(), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SpaceHeight(10.0), + ], + ), + ); + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(12), + ), + ), + child: Icon( + Icons.visibility_outlined, + color: AppColors.white, + size: 18, + ), + ), + ), + ), + Container( + width: 1, + color: AppColors.grey.withOpacity(0.2), + ), + Expanded( + child: GestureDetector( + onTap: onTapEdit, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(12), + ), + ), + child: Icon( + Icons.edit_outlined, + color: AppColors.white, + size: 18, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} + +class MenuProductItemOld extends StatelessWidget { + final Product data; + final Function() onTapEdit; + const MenuProductItemOld({ super.key, required this.data, required this.onTapEdit, diff --git a/lib/presentation/setting/widgets/settings_title.dart b/lib/presentation/setting/widgets/settings_title.dart index 00e30b1..813a6e7 100644 --- a/lib/presentation/setting/widgets/settings_title.dart +++ b/lib/presentation/setting/widgets/settings_title.dart @@ -1,45 +1,69 @@ +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; import '../../../core/components/search_input.dart'; import '../../../core/constants/colors.dart'; - - class SettingsTitle extends StatelessWidget { final String title; + final String? subtitle; final TextEditingController? controller; final Function(String value)? onChanged; + final List? actionWidget; const SettingsTitle( this.title, { super.key, this.controller, this.onChanged, + this.actionWidget, + this.subtitle, }); @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: const TextStyle( - color: AppColors.primary, - fontSize: 20, - fontWeight: FontWeight.w600, + return Container( + height: context.deviceHeight * 0.1, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + decoration: BoxDecoration( + color: AppColors.white, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: AppColors.black, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + if (subtitle != null) + Text( + subtitle ?? '', + style: TextStyle( + color: AppColors.grey, + fontSize: 14, + ), + ), + ], ), - ), - if (controller != null) - SizedBox( - width: 300.0, - child: SearchInput( - controller: controller!, - onChanged: onChanged, - hintText: 'Search for food, coffe, etc..', + if (controller != null) + SizedBox( + width: 300.0, + child: SearchInput( + controller: controller!, + onChanged: onChanged, + hintText: 'Search for food, coffe, etc..', + ), ), - ), - ], + if (actionWidget != null) ...actionWidget!, + ], + ), ); } } From 8767f021097a6991aa877c03a456d90009c5e5ce Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 31 Jul 2025 23:22:34 +0700 Subject: [PATCH 003/128] feat: update home page --- lib/core/components/buttons.dart | 13 +- lib/core/components/custom_modal_dialog.dart | 96 ++++ lib/core/constants/theme.dart | 38 ++ lib/main.dart | 28 +- lib/presentation/home/pages/home_page.dart | 446 ++++++------------ .../home/widgets/custom_tab_bar.dart | 12 +- .../home/widgets/home_right_title.dart | 112 +++-- lib/presentation/home/widgets/home_title.dart | 36 +- lib/presentation/home/widgets/order_menu.dart | 274 +++-------- .../home/widgets/product_card.dart | 160 ++----- .../setting/widgets/menu_product_item.dart | 134 +++--- 11 files changed, 552 insertions(+), 797 deletions(-) create mode 100644 lib/core/components/custom_modal_dialog.dart create mode 100644 lib/core/constants/theme.dart diff --git a/lib/core/components/buttons.dart b/lib/core/components/buttons.dart index e763f81..e0614b1 100644 --- a/lib/core/components/buttons.dart +++ b/lib/core/components/buttons.dart @@ -20,6 +20,8 @@ class Button extends StatelessWidget { this.fontSize = 16.0, this.elevation, this.labelStyle, + this.mainAxisAlignment = MainAxisAlignment.center, + this.crossAxisAlignment = CrossAxisAlignment.center, }); const Button.outlined({ @@ -37,6 +39,8 @@ class Button extends StatelessWidget { this.fontSize = 16.0, this.elevation, this.labelStyle, + this.mainAxisAlignment = MainAxisAlignment.center, + this.crossAxisAlignment = CrossAxisAlignment.center, }); final Function() onPressed; @@ -52,6 +56,8 @@ class Button extends StatelessWidget { final bool disabled; final double fontSize; final TextStyle? labelStyle; + final MainAxisAlignment mainAxisAlignment; + final CrossAxisAlignment crossAxisAlignment; @override Widget build(BuildContext context) { @@ -70,8 +76,8 @@ class Button extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16.0), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, children: [ icon ?? const SizedBox.shrink(), if (icon != null) const SizedBox(width: 10.0), @@ -104,7 +110,8 @@ class Button extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16.0), ), child: Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, mainAxisSize: MainAxisSize.min, children: [ icon ?? const SizedBox.shrink(), diff --git a/lib/core/components/custom_modal_dialog.dart b/lib/core/components/custom_modal_dialog.dart new file mode 100644 index 0000000..f6cac75 --- /dev/null +++ b/lib/core/components/custom_modal_dialog.dart @@ -0,0 +1,96 @@ +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:flutter/material.dart'; + +class CustomModalDialog extends StatelessWidget { + final String title; + final String? subtitle; + final Widget child; + final VoidCallback? onClose; + + const CustomModalDialog( + {super.key, + required this.title, + this.subtitle, + required this.child, + this.onClose}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: AppColors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: 200, + maxWidth: 600, + minHeight: 200, + maxHeight: 600, + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColors.primary, + const Color.fromARGB(255, 67, 69, 195) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.vertical( + top: Radius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: AppColors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + if (subtitle != null) + Text( + subtitle ?? '', + style: TextStyle( + color: AppColors.grey, + fontSize: 16, + ), + ), + ], + ), + ), + SpaceWidth(12), + IconButton( + icon: Icon(Icons.close, color: AppColors.white), + onPressed: () { + if (onClose != null) { + onClose!(); + } else { + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ), + child, + ], + ), + ), + ); + } +} diff --git a/lib/core/constants/theme.dart b/lib/core/constants/theme.dart new file mode 100644 index 0000000..ca1c990 --- /dev/null +++ b/lib/core/constants/theme.dart @@ -0,0 +1,38 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +ThemeData getApplicationTheme = ThemeData( + primaryColor: AppColors.primary, + scaffoldBackgroundColor: AppColors.white, + appBarTheme: AppBarTheme( + color: AppColors.white, + elevation: 0, + titleTextStyle: GoogleFonts.quicksand( + color: AppColors.primary, + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), + iconTheme: const IconThemeData( + color: AppColors.primary, + ), + ), + fontFamily: GoogleFonts.quicksand().fontFamily, + colorScheme: ColorScheme.fromSeed(seedColor: AppColors.primary), + useMaterial3: true, + inputDecorationTheme: InputDecorationTheme( + contentPadding: const EdgeInsets.symmetric(horizontal: 16.0), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide(color: AppColors.primary), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide(color: AppColors.primary), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide(color: AppColors.primary), + ), + ), +); diff --git a/lib/main.dart b/lib/main.dart index fd244fe..d7f5452 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'package:enaklo_pos/core/constants/theme.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/auth_remote_datasource.dart'; @@ -57,7 +58,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'presentation/home/pages/dashboard_page.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; - void main() async { WidgetsFlutterBinding.ensureInitialized(); // await LamanPrint.init(); @@ -110,7 +110,8 @@ class _MyAppState extends State { LocalProductBloc(ProductLocalDatasource.instance), ), BlocProvider( - create: (context) => CheckoutBloc(settingsLocalDatasource: SettingsLocalDatasource()), + create: (context) => + CheckoutBloc(settingsLocalDatasource: SettingsLocalDatasource()), ), BlocProvider( create: (context) => TaxSettingsBloc(SettingsLocalDatasource()), @@ -186,7 +187,8 @@ class _MyAppState extends State { create: (context) => QrisBloc(MidtransRemoteDatasource()), ), BlocProvider( - create: (context) => PaymentMethodsBloc(PaymentMethodsRemoteDatasource()), + create: (context) => + PaymentMethodsBloc(PaymentMethodsRemoteDatasource()), ), BlocProvider( create: (context) => OnlineCheckerBloc(), @@ -216,25 +218,7 @@ class _MyAppState extends State { child: MaterialApp( debugShowCheckedModeBanner: false, title: 'POS Resto App', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: AppColors.primary), - useMaterial3: true, - textTheme: GoogleFonts.quicksandTextTheme( - Theme.of(context).textTheme, - ), - appBarTheme: AppBarTheme( - color: AppColors.white, - elevation: 0, - titleTextStyle: GoogleFonts.quicksand( - color: AppColors.primary, - fontSize: 16.0, - fontWeight: FontWeight.w500, - ), - iconTheme: const IconThemeData( - color: AppColors.primary, - ), - ), - ), + theme: getApplicationTheme, home: FutureBuilder( future: AuthLocalDataSource().isAuthDataExists(), builder: (context, snapshot) { diff --git a/lib/presentation/home/pages/home_page.dart b/lib/presentation/home/pages/home_page.dart index 4512fc0..daed74e 100644 --- a/lib/presentation/home/pages/home_page.dart +++ b/lib/presentation/home/pages/home_page.dart @@ -8,7 +8,6 @@ import 'package:enaklo_pos/core/extensions/int_ext.dart'; import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; import 'package:enaklo_pos/presentation/home/bloc/local_product/local_product_bloc.dart'; -import 'package:enaklo_pos/presentation/home/dialog/discount_dialog.dart'; import 'package:enaklo_pos/presentation/home/pages/confirm_payment_page.dart'; import 'package:enaklo_pos/data/datasources/product_local_datasource.dart'; import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_bloc.dart'; @@ -19,7 +18,6 @@ import '../../../core/components/buttons.dart'; import '../../../core/components/spaces.dart'; import '../../../core/constants/colors.dart'; import '../bloc/checkout/checkout_bloc.dart'; -import '../dialog/service_dialog.dart'; import '../widgets/custom_tab_bar.dart'; import '../widgets/home_title.dart'; import '../widgets/order_menu.dart'; @@ -29,10 +27,10 @@ class HomePage extends StatefulWidget { final bool isTable; final TableModel? table; const HomePage({ - Key? key, + super.key, required this.isTable, this.table, - }) : super(key: key); + }); @override State createState() => _HomePageState(); @@ -94,7 +92,7 @@ class _HomePageState extends State { return Hero( tag: 'confirmation_screen', child: Scaffold( - backgroundColor: AppColors.background, + backgroundColor: AppColors.white, body: BlocListener( listener: (context, state) { state.maybeWhen( @@ -320,47 +318,61 @@ class _HomePageState extends State { color: Colors.white, child: Column( children: [ - Column( - children: [ - HomeRightTitle( - table: widget.table, - ), - Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: AppColors.grey, - width: 1.0, - ), - ), - ), - child: const Row( + HomeRightTitle( + table: widget.table, + ), + Padding( + padding: const EdgeInsets.all(16.0) + .copyWith(bottom: 0, top: 27), + child: Column( + children: [ + const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Order #", + 'Item', style: TextStyle( + color: AppColors.primary, fontSize: 16, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, ), ), - Text( - "Total: 0", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + SizedBox( + width: 130, + ), + SizedBox( + width: 50.0, + child: Text( + 'Qty', + style: TextStyle( + color: AppColors.primary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox( + child: Text( + 'Price', + style: TextStyle( + color: AppColors.primary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ], ), - ), - ], + const SpaceHeight(8), + const Divider(), + ], + ), ), Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), + padding: + const EdgeInsets.all(16.0).copyWith(top: 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -404,260 +416,109 @@ class _HomePageState extends State { ), ), ), - Column( - children: [ - Padding( - padding: const EdgeInsets.all(24.0), - child: Column( + Padding( + padding: const EdgeInsets.all(16.0).copyWith(top: 0), + child: Column( + children: [ + const Divider(), + const SpaceHeight(16.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - // Row( - // mainAxisAlignment: - // MainAxisAlignment.spaceEvenly, - // children: [ - // ColumnButton( - // label: 'Diskon', - // svgGenImage: Assets.icons.diskon, - // onPressed: () => showDialog( - // context: context, - // barrierDismissible: false, - // builder: (context) => - // const DiscountDialog(), - // ), - // ), - // ColumnButton( - // label: 'Pajak PB1', - // svgGenImage: Assets.icons.pajak, - // onPressed: () => showDialog( - // context: context, - // builder: (context) => - // const TaxDialog(), - // ), - // ), - // ColumnButton( - // label: 'Layanan', - // svgGenImage: Assets.icons.layanan, - // onPressed: () => showDialog( - // context: context, - // builder: (context) => - // const ServiceDialog(), - // ), - // ), - // ], - // ), - // const SpaceHeight(8.0), - // const Divider(), - // const SpaceHeight(8.0), - - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Sub total', - style: TextStyle( - color: AppColors.black, - fontWeight: FontWeight.bold, - ), - ), - BlocBuilder( - builder: (context, state) { - final price = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, - orderType) { - if (products.isEmpty) { - return 0; - } - return products - .map((e) => - e.product.price! - .toIntegerFromText * - e.quantity) - .reduce((value, element) => - value + element); - }); - - return Text( - price.currencyFormatRp, - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w900, - ), - ); - }, - ), - ], + const Text( + 'Pajak', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, + ), ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Pajak PB1', - style: TextStyle( - color: AppColors.black, - fontWeight: FontWeight.bold, + BlocBuilder( + builder: (context, state) { + final tax = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return 0; + } + return tax; + }); + return Text( + '$tax %', + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w600, ), - ), - BlocBuilder( - builder: (context, state) { - final tax = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, - orderType) { - if (products.isEmpty) { - return 0; - } - return tax; - }); - return Text( - '$tax %', - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], - ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Layanan', - style: TextStyle( - color: AppColors.black, - fontWeight: FontWeight.bold, - ), - ), - BlocBuilder( - builder: (context, state) { - final serviceCharge = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, - orderType) { - return serviceCharge; - }); - return Text( - '$serviceCharge %', - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], - ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - _buildSmallButton( - onTap: () => showDialog( - context: context, - builder: (context) => - const ServiceDialog(), - ), - label: 'Layanan', - ), - SpaceWidth(8), - _buildSmallButton( - onTap: () => showDialog( - context: context, - builder: (context) => - const DiscountDialog(), - ), - label: 'Diskon', - ), - ], - ), - const SpaceHeight(8.0), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Diskon', - style: TextStyle( - color: AppColors.black, - fontWeight: FontWeight.bold, - ), - ), - BlocBuilder( - builder: (context, state) { - final discount = state.maybeWhen( - orElse: () => 0, - loaded: (products, - discountModel, - discount, - discountAmount, - tax, - serviceCharge, - totalQuantity, - totalPrice, - draftName, - orderType) { - if (discountModel == null) { - return 0; - } - return discountModel.value! - .replaceAll('.00', '') - .toIntegerFromText; - }); - return Text( - '$discount %', - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.w600, - ), - ); - }, - ), - ], + ); + }, ), ], ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: context.deviceHeight * 0.09, - width: double.infinity, - color: AppColors.white, + const SpaceHeight(16.0), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Sub total', + style: TextStyle( + color: AppColors.black, + fontWeight: FontWeight.bold, + ), + ), + BlocBuilder( + builder: (context, state) { + final price = state.maybeWhen( + orElse: () => 0, + loaded: (products, + discountModel, + discount, + discountAmount, + tax, + serviceCharge, + totalQuantity, + totalPrice, + draftName, + orderType) { + if (products.isEmpty) { + return 0; + } + return products + .map((e) => + e.product.price! + .toIntegerFromText * + e.quantity) + .reduce((value, element) => + value + element); + }); + + return Text( + price.currencyFormatRp, + style: const TextStyle( + color: AppColors.primary, + fontWeight: FontWeight.w900, + ), + ); + }, + ), + ], + ), + SpaceHeight(16.0), + Align( + alignment: Alignment.bottomCenter, child: Expanded( child: Button.filled( - borderRadius: 0, - elevation: 0, + borderRadius: 12, + elevation: 1, onPressed: () { context.push(ConfirmPaymentPage( isTable: widget.isTable, @@ -668,8 +529,8 @@ class _HomePageState extends State { ), ), ), - ), - ], + ], + ), ), ], ), @@ -682,33 +543,6 @@ class _HomePageState extends State { ), ); } - - GestureDetector _buildSmallButton({ - required Function() onTap, - required String label, - }) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - vertical: 6.0, - ), - decoration: BoxDecoration( - color: AppColors.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(8.0), - ), - child: Text( - label, - style: TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.bold, - fontSize: 12.0, - ), - ), - ), - ); - } } class _IsEmpty extends StatelessWidget { diff --git a/lib/presentation/home/widgets/custom_tab_bar.dart b/lib/presentation/home/widgets/custom_tab_bar.dart index ba1bdcc..dd3d51b 100644 --- a/lib/presentation/home/widgets/custom_tab_bar.dart +++ b/lib/presentation/home/widgets/custom_tab_bar.dart @@ -105,17 +105,15 @@ class CustomTabBarV2 extends StatelessWidget { child: TabBar( isScrollable: true, tabAlignment: TabAlignment.start, - labelColor: Colors.white, + labelColor: AppColors.primary, labelStyle: TextStyle( fontWeight: FontWeight.bold, ), - dividerColor: Colors.transparent, + dividerColor: AppColors.primary, unselectedLabelColor: AppColors.primary, - indicator: BoxDecoration( - color: AppColors.primary, // Warna button saat aktif - borderRadius: BorderRadius.circular(8), - ), - indicatorColor: Colors.transparent, + indicatorSize: TabBarIndicatorSize.label, + indicatorWeight: 4, + indicatorColor: AppColors.primary, tabs: tabTitles .map((title) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/lib/presentation/home/widgets/home_right_title.dart b/lib/presentation/home/widgets/home_right_title.dart index ed4fdbd..b539f02 100644 --- a/lib/presentation/home/widgets/home_right_title.dart +++ b/lib/presentation/home/widgets/home_right_title.dart @@ -22,36 +22,92 @@ class HomeRightTitle extends StatelessWidget { ), ), ), - child: Row( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: Button.filled( - width: 180.0, - height: context.deviceHeight, - elevation: 0, - onPressed: () {}, - label: 'List Order', - ), + Row( + children: [ + Expanded( + child: Button.filled( + width: 180.0, + height: 40, + elevation: 0, + onPressed: () {}, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + icon: Icon( + Icons.list, + color: Colors.white, + size: 24, + ), + label: 'Daftar Pesanan', + ), + ), + Expanded( + child: Button.filled( + width: 180.0, + height: 40, + elevation: 0, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + icon: Icon( + Icons.person_outline, + color: Colors.white, + size: 24, + ), + onPressed: () { + if (table == null) { + context.push(DashboardPage( + index: 1, + )); + } + }, + label: 'Pelanggan', + ), + ), + ], ), - Container( - width: 1, - height: context.deviceHeight, - color: Colors.white, - ), - Expanded( - child: Button.filled( - width: 180.0, - height: context.deviceHeight, - elevation: 0, - onPressed: () { - if (table == null) { - context.push(DashboardPage( - index: 1, - )); - } - }, - label: table == null ? 'Pilih Meja' : '${table!.id}', - ), + Row( + children: [ + Expanded( + child: Button.filled( + width: 180.0, + height: 40, + elevation: 0, + onPressed: () {}, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + icon: Icon( + Icons.dinner_dining_outlined, + color: Colors.white, + size: 24, + ), + label: 'Dine In', + ), + ), + Expanded( + child: Button.filled( + width: 180.0, + height: 40, + elevation: 0, + icon: Icon( + Icons.table_restaurant_outlined, + color: Colors.white, + size: 24, + ), + onPressed: () { + if (table == null) { + context.push(DashboardPage( + index: 1, + )); + } + }, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + label: table == null ? 'Pilih Meja' : '${table!.id}', + ), + ), + ], ), ], ), diff --git a/lib/presentation/home/widgets/home_title.dart b/lib/presentation/home/widgets/home_title.dart index 356c232..135051d 100644 --- a/lib/presentation/home/widgets/home_title.dart +++ b/lib/presentation/home/widgets/home_title.dart @@ -1,6 +1,5 @@ import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; -import 'package:enaklo_pos/core/extensions/date_time_ext.dart'; import '../../../core/components/search_input.dart'; import '../../../core/constants/colors.dart'; @@ -21,37 +20,18 @@ class HomeTitle extends StatelessWidget { height: context.deviceHeight * 0.1, padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), decoration: BoxDecoration( - color: AppColors.primary, + color: AppColors.white, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.store, color: AppColors.white, size: 32.0), - SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Enaklo POS', - style: TextStyle( - color: AppColors.white, - fontSize: 22, - fontWeight: FontWeight.w600, - ), - ), - Text( - DateTime.now().toFormattedDate(), - style: TextStyle( - color: Colors.grey.shade300, - fontSize: 16, - ), - ), - ], - ), - ], + const Text( + 'DEFAULT OUTLET', + style: TextStyle( + color: AppColors.primary, + fontSize: 28, + fontWeight: FontWeight.w600, + ), ), SizedBox( width: 300.0, diff --git a/lib/presentation/home/widgets/order_menu.dart b/lib/presentation/home/widgets/order_menu.dart index 62629a0..a46bfa7 100644 --- a/lib/presentation/home/widgets/order_menu.dart +++ b/lib/presentation/home/widgets/order_menu.dart @@ -6,174 +6,41 @@ import 'package:enaklo_pos/core/extensions/int_ext.dart'; import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; import 'package:enaklo_pos/presentation/home/models/product_quantity.dart'; -import 'package:enaklo_pos/presentation/home/widgets/item_notes_dialog.dart'; import '../../../core/components/spaces.dart'; import '../../../core/constants/colors.dart'; -class OrderMenu extends StatelessWidget { +class OrderMenu extends StatefulWidget { final ProductQuantity data; const OrderMenu({super.key, required this.data}); @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(16.0), - margin: EdgeInsets.only(bottom: 8.0), - decoration: BoxDecoration( - color: AppColors.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(8.0), - ), - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Text( - data.product.name ?? "_", - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - SizedBox(width: 12.0), - _buildIconButton( - onTap: () { - showDialog( - context: context, - builder: (context) => ItemNotesDialog(item: data), - ); - }, - icon: Icons.edit_note, - ), - SizedBox(width: 8.0), - _buildIconButton( - onTap: () {}, - icon: Icons.delete_outline, - iconColor: AppColors.red, - ), - ], - ), - const SpaceHeight(12.0), - Row( - children: [ - Expanded( - child: Text( - (data.product.price!.toIntegerFromText * data.quantity) - .currencyFormatRp, - overflow: TextOverflow.ellipsis, - maxLines: 2, - style: const TextStyle( - color: AppColors.primary, - fontWeight: FontWeight.bold, - ), - ), - ), - SpaceWidth(16), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.circular(50.0), - ), - child: Row( - children: [ - _buildIconButton( - onTap: () { - context - .read() - .add(CheckoutEvent.removeItem(data.product)); - }, - icon: Icons.remove, - iconColor: AppColors.primary, - bgColor: Colors.grey.shade300, - ), - SizedBox( - width: 30.0, - child: Center( - child: Text( - data.quantity.toString(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ), - _buildIconButton( - onTap: () { - context - .read() - .add(CheckoutEvent.addItem(data.product)); - }, - icon: Icons.add, - iconColor: AppColors.white, - bgColor: AppColors.primary, - ), - ], - ), - ), - ], - ), - if (data.notes.isNotEmpty) ...[ - SpaceHeight(8.0), - Divider(), - SpaceHeight(8.0), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - decoration: BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.circular(8.0), - ), - child: Text( - 'Notes: ${data.notes}', - style: const TextStyle( - fontSize: 14, - color: AppColors.black, - fontWeight: FontWeight.w500, - fontStyle: FontStyle.italic, - ), - ), - ), - ], - ], - ), - ); - } - - GestureDetector _buildIconButton({ - required Function()? onTap, - Color iconColor = AppColors.black, - Color bgColor = AppColors.white, - required IconData icon, - }) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: bgColor, - shape: BoxShape.circle, - ), - child: Icon( - icon, - size: 20, - color: iconColor, - ), - ), - ); - } + State createState() => _OrderMenuState(); } -class OrderMenuOld extends StatelessWidget { - final ProductQuantity data; - const OrderMenuOld({super.key, required this.data}); +class _OrderMenuState extends State { + final _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = widget.data.notes; + + _controller.addListener(() { + context.read().add( + CheckoutEvent.updateItemNotes( + widget.data.product, + _controller.text, + ), + ); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -193,9 +60,9 @@ class OrderMenuOld extends StatelessWidget { // color: AppColors.primary, // ), CachedNetworkImage( - imageUrl: data.product.image!.contains('http') - ? data.product.image! - : '${Variables.baseUrl}/${data.product.image}', + imageUrl: widget.data.product.image!.contains('http') + ? widget.data.product.image! + : '${Variables.baseUrl}/${widget.data.product.image}', width: 50.0, height: 50.0, fit: BoxFit.cover, @@ -206,55 +73,21 @@ class OrderMenuOld extends StatelessWidget { title: Row( children: [ Expanded( - child: Text(data.product.name ?? "-", - maxLines: 2, + child: Text(widget.data.product.name ?? "-", + maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, )), ), - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) => ItemNotesDialog(item: data), - ); - }, - child: const Icon( - Icons.edit_note, - size: 20, - color: AppColors.primary, - ), - ), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - data.product.price!.toIntegerFromText.currencyFormatRp), - if (data.notes.isNotEmpty) ...[ - const SpaceHeight(4.0), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - decoration: BoxDecoration( - color: AppColors.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(4.0), - ), - child: Text( - 'Notes: ${data.notes}', - style: const TextStyle( - fontSize: 12, - color: AppColors.primary, - fontStyle: FontStyle.italic, - ), - ), - ), - ], + Text(widget.data.product.price!.toIntegerFromText + .currencyFormatRp), ], ), ), @@ -265,7 +98,7 @@ class OrderMenuOld extends StatelessWidget { onTap: () { context .read() - .add(CheckoutEvent.removeItem(data.product)); + .add(CheckoutEvent.removeItem(widget.data.product)); }, child: Container( width: 30, @@ -281,14 +114,14 @@ class OrderMenuOld extends StatelessWidget { width: 30.0, child: Center( child: Text( - data.quantity.toString(), + widget.data.quantity.toString(), )), ), GestureDetector( onTap: () { context .read() - .add(CheckoutEvent.addItem(data.product)); + .add(CheckoutEvent.addItem(widget.data.product)); }, child: Container( width: 30, @@ -306,7 +139,8 @@ class OrderMenuOld extends StatelessWidget { SizedBox( width: 80.0, child: Text( - (data.product.price!.toIntegerFromText * data.quantity) + (widget.data.product.price!.toIntegerFromText * + widget.data.quantity) .currencyFormatRp, textAlign: TextAlign.right, style: const TextStyle( @@ -317,6 +151,40 @@ class OrderMenuOld extends StatelessWidget { ), ], ), + SpaceHeight(8.0), + SizedBox( + height: 40, + child: Row( + children: [ + Flexible( + child: TextFormField( + cursorColor: AppColors.primary, + controller: _controller, + style: const TextStyle( + fontSize: 12, + color: AppColors.black, + ), + decoration: InputDecoration( + hintText: 'Catatan Pesanan', + ), + ), + ), + const SpaceWidth(16.0), + Container( + height: 40, + width: 40, + decoration: BoxDecoration( + color: AppColors.primary, + borderRadius: BorderRadius.circular(8.0), + ), + child: Icon( + Icons.delete_outline, + color: AppColors.white, + ), + ), + ], + ), + ) ], ); } diff --git a/lib/presentation/home/widgets/product_card.dart b/lib/presentation/home/widgets/product_card.dart index c8d4bdd..78655e9 100644 --- a/lib/presentation/home/widgets/product_card.dart +++ b/lib/presentation/home/widgets/product_card.dart @@ -1,5 +1,4 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:enaklo_pos/core/constants/variables.dart'; @@ -8,97 +7,14 @@ import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; import 'package:enaklo_pos/presentation/home/bloc/checkout/checkout_bloc.dart'; -import '../../../core/assets/assets.gen.dart'; import '../../../core/components/spaces.dart'; import '../../../core/constants/colors.dart'; class ProductCard extends StatelessWidget { final Product data; final VoidCallback onCartButton; - const ProductCard( - {super.key, required this.data, required this.onCartButton}); - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - context.read().add(CheckoutEvent.addItem(data)); - }, - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.circular(12.0), - ), - child: Column( - children: [ - AspectRatio( - aspectRatio: 1.2, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: CachedNetworkImage( - imageUrl: data.image!.contains('http') - ? data.image! - : '${Variables.baseUrl}/${data.image}', - width: double.infinity, - height: context.deviceHeight * 0.18, - fit: BoxFit.cover, - errorWidget: (context, url, error) => Container( - width: double.infinity, - height: context.deviceHeight * 0.18, - decoration: BoxDecoration( - color: AppColors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.image_outlined, - color: AppColors.grey, - size: 40, - ), - ), - ), - ), - ), - const SpaceHeight(8.0), - Row( - children: [ - Expanded( - child: Text( - "${data.name}", - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - Spacer(), - const SpaceHeight(4.0), - Align( - alignment: Alignment.centerRight, - child: Text( - data.price!.toIntegerFromText.currencyFormatRp, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13, - color: AppColors.primary, - ), - ), - ), - ], - ), - )); - } -} - -class ProductCardOld extends StatelessWidget { - final Product data; - final VoidCallback onCartButton; - - const ProductCardOld({ + const ProductCard({ super.key, required this.data, required this.onCartButton, @@ -115,6 +31,9 @@ class ProductCardOld extends StatelessWidget { decoration: BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.circular(12.0), + border: Border.all( + color: AppColors.disabled, + ), ), child: Stack( children: [ @@ -160,28 +79,29 @@ class ProductCardOld extends StatelessWidget { overflow: TextOverflow.ellipsis, ), const Spacer(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - data.category?.name ?? '-', - style: const TextStyle( - color: AppColors.grey, - fontSize: 12, - ), - ), + Align( + alignment: Alignment.center, + child: Text( + data.category?.name ?? '-', + style: const TextStyle( + fontSize: 14, + color: AppColors.grey, + fontWeight: FontWeight.w500, ), - Flexible( - child: Text( - data.price!.toIntegerFromText.currencyFormatRp, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13, - ), - ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const Spacer(), + Align( + alignment: Alignment.center, + child: Text( + data.price!.toIntegerFromText.currencyFormatRp, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, ), - ], + ), ), const Spacer(), ], @@ -232,34 +152,8 @@ class ProductCardOld extends StatelessWidget { ), ), ) - : Align( - alignment: Alignment.topRight, - child: Container( - width: 36, - height: 36, - padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(9.0)), - color: AppColors.primary, - ), - child: Assets.icons.shoppingBasket.svg(), - ), - ) - : Align( - alignment: Alignment.topRight, - child: Container( - width: 36, - height: 36, - padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( - borderRadius: - BorderRadius.all(Radius.circular(9.0)), - color: AppColors.primary, - ), - child: Assets.icons.shoppingBasket.svg(), - ), - ); + : SizedBox.shrink() + : SizedBox.shrink(); }, ); }, diff --git a/lib/presentation/setting/widgets/menu_product_item.dart b/lib/presentation/setting/widgets/menu_product_item.dart index 734e02d..1e1d67b 100644 --- a/lib/presentation/setting/widgets/menu_product_item.dart +++ b/lib/presentation/setting/widgets/menu_product_item.dart @@ -102,79 +102,79 @@ class MenuProductItem extends StatelessWidget { child: GestureDetector( onTap: () { showDialog( - context: context, - // backgroundColor: AppColors.white, - builder: (context) { - //container for product detail - return AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - data.name!, - style: const TextStyle( - fontSize: 20, - ), + context: context, + builder: (context) { + //container for product detail + return AlertDialog( + backgroundColor: AppColors.white, + contentPadding: const EdgeInsets.all(16.0), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + data.name!, + style: const TextStyle( + fontSize: 20, ), - IconButton( - onPressed: () { - Navigator.pop(context); - }, - icon: const Icon(Icons.close), - ), - ], - ), - const SpaceHeight(10.0), - ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(10.0)), - child: CachedNetworkImage( - imageUrl: - '${Variables.baseUrl}${data.image}', - placeholder: (context, url) => const Center( - child: CircularProgressIndicator()), - errorWidget: (context, url, error) => - const Icon( - Icons.food_bank_outlined, - size: 80, - ), - width: 80, ), - ), - const SpaceHeight(10.0), - Text( - data.category?.name ?? '-', - style: const TextStyle( - fontSize: 12, - color: Colors.grey, + IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.close), ), - ), - const SpaceHeight(10.0), - Text( - data.price.toString(), - style: const TextStyle( - fontSize: 12, - color: Colors.grey, + ], + ), + const SpaceHeight(10.0), + ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(10.0)), + child: CachedNetworkImage( + imageUrl: '${Variables.baseUrl}${data.image}', + placeholder: (context, url) => const Center( + child: CircularProgressIndicator()), + errorWidget: (context, url, error) => + const Icon( + Icons.food_bank_outlined, + size: 80, ), + width: 80, ), - const SpaceHeight(10.0), - Text( - data.stock.toString(), - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), + ), + const SpaceHeight(10.0), + Text( + data.category?.name ?? '-', + style: const TextStyle( + fontSize: 12, + color: Colors.grey, ), - const SpaceHeight(10.0), - ], - ), - ); - }); + ), + const SpaceHeight(10.0), + Text( + data.price.toString(), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SpaceHeight(10.0), + Text( + data.stock.toString(), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SpaceHeight(10.0), + ], + ), + ); + }, + ); }, child: Container( padding: const EdgeInsets.symmetric(vertical: 4), From 74460c921b6cfcd6052332bb1e9d2af28d4c0002 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 1 Aug 2025 00:46:22 +0700 Subject: [PATCH 004/128] feat: color and image empty --- lib/core/constants/colors.dart | 3 ++- .../home/widgets/product_card.dart | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart index 41c5465..9577a9b 100644 --- a/lib/core/constants/colors.dart +++ b/lib/core/constants/colors.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; class AppColors { /// primary = #3949AB - static const Color primary = Color(0xff6466f1); + static const Color primary = Color(0xff36175e); + static const Color secondary = Color(0xfff1eaf9); /// grey = #B7B7B7 static const Color grey = Color(0xffB7B7B7); diff --git a/lib/presentation/home/widgets/product_card.dart b/lib/presentation/home/widgets/product_card.dart index 78655e9..3e80058 100644 --- a/lib/presentation/home/widgets/product_card.dart +++ b/lib/presentation/home/widgets/product_card.dart @@ -50,21 +50,25 @@ class ProductCard extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(40.0)), - child: - // Icon( - // Icons.fastfood, - // size: 40, - // color: AppColors.primary, - // ), - CachedNetworkImage( + child: CachedNetworkImage( imageUrl: data.image!.contains('http') ? data.image! : '${Variables.baseUrl}/${data.image}', + fit: BoxFit.cover, width: 60, height: 60, - fit: BoxFit.cover, - errorWidget: (context, url, error) => - const Icon(Icons.error), + errorWidget: (context, url, error) => Container( + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.disabled.withOpacity(0.4), + ), + child: const Icon( + Icons.image_not_supported, + color: AppColors.grey, + ), + ), ), ), ), From f3e6d49a271a5d0e8a57af0068cce74f0826276e Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 1 Aug 2025 01:17:00 +0700 Subject: [PATCH 005/128] feat: type dialog --- lib/core/components/custom_modal_dialog.dart | 119 +++++++++--------- lib/core/constants/colors.dart | 2 +- lib/presentation/home/dialog/type_dialog.dart | 101 +++++++++++++++ .../home/widgets/home_right_title.dart | 9 +- 4 files changed, 172 insertions(+), 59 deletions(-) create mode 100644 lib/presentation/home/dialog/type_dialog.dart diff --git a/lib/core/components/custom_modal_dialog.dart b/lib/core/components/custom_modal_dialog.dart index f6cac75..99da382 100644 --- a/lib/core/components/custom_modal_dialog.dart +++ b/lib/core/components/custom_modal_dialog.dart @@ -1,5 +1,6 @@ import 'package:enaklo_pos/core/components/spaces.dart'; import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:flutter/material.dart'; class CustomModalDialog extends StatelessWidget { @@ -24,71 +25,75 @@ class CustomModalDialog extends StatelessWidget { ), child: ConstrainedBox( constraints: BoxConstraints( - minWidth: 200, - maxWidth: 600, - minHeight: 200, - maxHeight: 600, + minWidth: context.deviceWidth * 0.3, + maxWidth: context.deviceWidth * 0.8, + minHeight: context.deviceHeight * 0.3, + maxHeight: context.deviceHeight * 0.8, ), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppColors.primary, - const Color.fromARGB(255, 67, 69, 195) - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color.fromARGB(255, 81, 40, 134), + AppColors.primary, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.vertical( + top: Radius.circular(16), + ), ), - borderRadius: BorderRadius.vertical( - top: Radius.circular(16), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - color: AppColors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - if (subtitle != null) + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - subtitle ?? '', + title, style: TextStyle( - color: AppColors.grey, - fontSize: 16, + color: AppColors.white, + fontSize: 20, + fontWeight: FontWeight.bold, ), ), - ], + if (subtitle != null) + Text( + subtitle ?? '', + style: TextStyle( + color: AppColors.grey, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), - ), - SpaceWidth(12), - IconButton( - icon: Icon(Icons.close, color: AppColors.white), - onPressed: () { - if (onClose != null) { - onClose!(); - } else { - Navigator.of(context).pop(); - } - }, - ), - ], + SpaceWidth(12), + IconButton( + icon: Icon(Icons.close, color: AppColors.white), + onPressed: () { + if (onClose != null) { + onClose!(); + } else { + Navigator.of(context).pop(); + } + }, + ), + ], + ), ), - ), - child, - ], + child, + ], + ), ), ), ); diff --git a/lib/core/constants/colors.dart b/lib/core/constants/colors.dart index 9577a9b..1c83980 100644 --- a/lib/core/constants/colors.dart +++ b/lib/core/constants/colors.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; class AppColors { /// primary = #3949AB static const Color primary = Color(0xff36175e); - static const Color secondary = Color(0xfff1eaf9); /// grey = #B7B7B7 static const Color grey = Color(0xffB7B7B7); @@ -19,6 +18,7 @@ class AppColors { /// white = #FFFFFF static const Color white = Color(0xffFFFFFF); + static const Color whiteText = Color(0xfff1eaf9); /// green = #50C474 static const Color green = Color(0xff50C474); diff --git a/lib/presentation/home/dialog/type_dialog.dart b/lib/presentation/home/dialog/type_dialog.dart new file mode 100644 index 0000000..5fca75e --- /dev/null +++ b/lib/presentation/home/dialog/type_dialog.dart @@ -0,0 +1,101 @@ +import 'package:enaklo_pos/core/components/custom_modal_dialog.dart'; +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:flutter/material.dart'; + +class TypeDialog extends StatefulWidget { + const TypeDialog({super.key}); + + @override + State createState() => _TypeDialogState(); +} + +class _TypeDialogState extends State { + String selectedType = 'dine_in'; + + List> types = [ + {'value': 'dine_in', 'label': 'Dine In', 'icon': Icons.restaurant_outlined}, + { + 'value': 'take_away', + 'label': 'Take Away', + 'icon': Icons.takeout_dining_outlined + }, + { + 'value': 'delivery', + 'label': 'Delivery', + 'icon': Icons.delivery_dining_outlined + }, + ]; + + @override + Widget build(BuildContext context) { + return CustomModalDialog( + title: 'Pilih Tipe', + subtitle: 'Silahkan pilih tipe yang sesuai', + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: List.generate(types.length, (index) { + return _buildItem(context, types[index]); + }), + ), + ), + ); + } + + Widget _buildItem(BuildContext context, Map type) { + return GestureDetector( + onTap: () { + setState(() { + selectedType = type['value']!; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: selectedType == type['value'] + ? AppColors.primary + : AppColors.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: selectedType == type['value'] + ? AppColors.primary + : AppColors.grey, + width: 1.0, + ), + ), + child: Row( + children: [ + Icon(type['icon'], + color: selectedType == type['value'] + ? AppColors.white + : AppColors.black), + SpaceWidth(12.0), + Expanded( + child: Text( + type['label']!, + style: TextStyle( + fontSize: 16, + color: selectedType == type['value'] + ? AppColors.white + : AppColors.black, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + SpaceWidth(12.0), + Icon( + Icons.check_circle, + color: selectedType == type['value'] + ? AppColors.green + : Colors.transparent, + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/home/widgets/home_right_title.dart b/lib/presentation/home/widgets/home_right_title.dart index b539f02..29ee4ef 100644 --- a/lib/presentation/home/widgets/home_right_title.dart +++ b/lib/presentation/home/widgets/home_right_title.dart @@ -2,6 +2,7 @@ import 'package:enaklo_pos/core/components/buttons.dart'; import 'package:enaklo_pos/core/constants/colors.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; +import 'package:enaklo_pos/presentation/home/dialog/type_dialog.dart'; import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart'; import 'package:flutter/material.dart'; @@ -74,7 +75,13 @@ class HomeRightTitle extends StatelessWidget { width: 180.0, height: 40, elevation: 0, - onPressed: () {}, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return TypeDialog(); + }); + }, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, icon: Icon( From 7a6da0397b38f3e66ef6770fa0cb2902a6b413e2 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 1 Aug 2025 01:40:33 +0700 Subject: [PATCH 006/128] feat: detail product dialog --- lib/core/components/custom_modal_dialog.dart | 29 ++-- .../dialogs/detail_product_dialog.dart | 153 ++++++++++++++++++ .../setting/pages/product_page.dart | 1 - .../setting/widgets/menu_product_item.dart | 81 +--------- 4 files changed, 177 insertions(+), 87 deletions(-) create mode 100644 lib/presentation/setting/dialogs/detail_product_dialog.dart diff --git a/lib/core/components/custom_modal_dialog.dart b/lib/core/components/custom_modal_dialog.dart index 99da382..f4b9966 100644 --- a/lib/core/components/custom_modal_dialog.dart +++ b/lib/core/components/custom_modal_dialog.dart @@ -8,13 +8,22 @@ class CustomModalDialog extends StatelessWidget { final String? subtitle; final Widget child; final VoidCallback? onClose; + final double? minWidth; + final double? maxWidth; + final double? minHeight; + final double? maxHeight; - const CustomModalDialog( - {super.key, - required this.title, - this.subtitle, - required this.child, - this.onClose}); + const CustomModalDialog({ + super.key, + required this.title, + this.subtitle, + required this.child, + this.onClose, + this.minWidth, + this.maxWidth, + this.minHeight, + this.maxHeight, + }); @override Widget build(BuildContext context) { @@ -25,10 +34,10 @@ class CustomModalDialog extends StatelessWidget { ), child: ConstrainedBox( constraints: BoxConstraints( - minWidth: context.deviceWidth * 0.3, - maxWidth: context.deviceWidth * 0.8, - minHeight: context.deviceHeight * 0.3, - maxHeight: context.deviceHeight * 0.8, + minWidth: minWidth ?? context.deviceWidth * 0.3, + maxWidth: maxWidth ?? context.deviceWidth * 0.8, + minHeight: minHeight ?? context.deviceHeight * 0.3, + maxHeight: maxHeight ?? context.deviceHeight * 0.8, ), child: IntrinsicWidth( child: Column( diff --git a/lib/presentation/setting/dialogs/detail_product_dialog.dart b/lib/presentation/setting/dialogs/detail_product_dialog.dart new file mode 100644 index 0000000..f3e3e12 --- /dev/null +++ b/lib/presentation/setting/dialogs/detail_product_dialog.dart @@ -0,0 +1,153 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:enaklo_pos/core/components/custom_modal_dialog.dart'; +import 'package:enaklo_pos/core/components/spaces.dart'; +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/core/constants/variables.dart'; +import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/core/extensions/string_ext.dart'; +import 'package:enaklo_pos/data/models/response/product_response_model.dart'; +import 'package:flutter/material.dart'; + +class DetailProductDialog extends StatelessWidget { + final Product product; + const DetailProductDialog({super.key, required this.product}); + + @override + Widget build(BuildContext context) { + return CustomModalDialog( + title: "Detail Produk", + maxWidth: context.deviceWidth * 0.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: CachedNetworkImage( + imageUrl: product.image!.contains('http') + ? product.image! + : '${Variables.baseUrl}/${product.image}', + fit: BoxFit.cover, + errorWidget: (context, url, error) => Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: AppColors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.image_outlined, + color: AppColors.grey, + size: 40, + ), + ), + ), + ), + const SpaceWidth(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerRight, + child: _buildStatus(), + ), + const SpaceHeight(8), + Text( + product.name ?? "-", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + if (product.description != null && + product.description!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + product.description!, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey, + ), + ), + ), + ], + ), + ), + ], + ), + const SpaceHeight(16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildItem( + product.category?.name ?? "-", + "Kategori", + ), + _buildItem( + "${product.stock}", + "Stok", + valueColor: product.stock! < 50 + ? AppColors.red + : product.stock! < 100 + ? Colors.yellow + : AppColors.green, + ), + _buildItem( + (product.price ?? "0").currencyFormatRpV2, + "Harga", + valueColor: AppColors.primary, + ), + ], + ), + ], + ), + ), + ); + } + + Column _buildItem(String value, String label, + {Color valueColor = AppColors.black}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const SpaceHeight(4), + Text( + value, + style: TextStyle( + fontSize: 14, + color: valueColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } + + Container _buildStatus() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: product.status == 1 ? AppColors.green : AppColors.red, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + product.status == 1 ? 'Aktif' : 'Tidak Aktif', + style: const TextStyle( + color: Colors.white, fontSize: 12, fontWeight: FontWeight.w700), + ), + ); + } +} diff --git a/lib/presentation/setting/pages/product_page.dart b/lib/presentation/setting/pages/product_page.dart index 76a110b..eb6d8c3 100644 --- a/lib/presentation/setting/pages/product_page.dart +++ b/lib/presentation/setting/pages/product_page.dart @@ -8,7 +8,6 @@ import 'package:enaklo_pos/presentation/setting/bloc/update_product/update_produ import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/add_product/add_product_bloc.dart'; import 'package:enaklo_pos/presentation/setting/dialogs/form_product_dialog.dart'; -import 'package:enaklo_pos/presentation/setting/widgets/add_data.dart'; import 'package:enaklo_pos/presentation/setting/widgets/menu_product_item.dart'; import 'package:enaklo_pos/presentation/setting/widgets/settings_title.dart'; diff --git a/lib/presentation/setting/widgets/menu_product_item.dart b/lib/presentation/setting/widgets/menu_product_item.dart index 1e1d67b..b9cc346 100644 --- a/lib/presentation/setting/widgets/menu_product_item.dart +++ b/lib/presentation/setting/widgets/menu_product_item.dart @@ -1,6 +1,7 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:cached_network_image/cached_network_image.dart'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/presentation/setting/dialogs/detail_product_dialog.dart'; import 'package:flutter/material.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; @@ -100,82 +101,10 @@ class MenuProductItem extends StatelessWidget { children: [ Expanded( child: GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (context) { - //container for product detail - return AlertDialog( - backgroundColor: AppColors.white, - contentPadding: const EdgeInsets.all(16.0), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - data.name!, - style: const TextStyle( - fontSize: 20, - ), - ), - IconButton( - onPressed: () { - Navigator.pop(context); - }, - icon: const Icon(Icons.close), - ), - ], - ), - const SpaceHeight(10.0), - ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(10.0)), - child: CachedNetworkImage( - imageUrl: '${Variables.baseUrl}${data.image}', - placeholder: (context, url) => const Center( - child: CircularProgressIndicator()), - errorWidget: (context, url, error) => - const Icon( - Icons.food_bank_outlined, - size: 80, - ), - width: 80, - ), - ), - const SpaceHeight(10.0), - Text( - data.category?.name ?? '-', - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - const SpaceHeight(10.0), - Text( - data.price.toString(), - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - const SpaceHeight(10.0), - Text( - data.stock.toString(), - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - const SpaceHeight(10.0), - ], - ), - ); - }, - ); - }, + onTap: () => showDialog( + context: context, + builder: (context) => DetailProductDialog(product: data), + ), child: Container( padding: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( From ba2e0101508127c1b97f374f73fda5d3a245388d Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 1 Aug 2025 13:53:29 +0700 Subject: [PATCH 007/128] feat: remove google font and add quicksand --- assets/fonts/quicksand/Quicksand-Bold.ttf | Bin 0 -> 78592 bytes assets/fonts/quicksand/Quicksand-Light.ttf | Bin 0 -> 78656 bytes assets/fonts/quicksand/Quicksand-Medium.ttf | Bin 0 -> 78944 bytes assets/fonts/quicksand/Quicksand-Regular.ttf | Bin 0 -> 78932 bytes assets/fonts/quicksand/Quicksand-SemiBold.ttf | Bin 0 -> 78816 bytes lib/core/assets/fonts.gen.dart | 15 ++++++++ lib/core/constants/theme.dart | 6 ++-- lib/main.dart | 2 -- pubspec.lock | 8 ----- pubspec.yaml | 33 ++++++++---------- 10 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 assets/fonts/quicksand/Quicksand-Bold.ttf create mode 100644 assets/fonts/quicksand/Quicksand-Light.ttf create mode 100644 assets/fonts/quicksand/Quicksand-Medium.ttf create mode 100644 assets/fonts/quicksand/Quicksand-Regular.ttf create mode 100644 assets/fonts/quicksand/Quicksand-SemiBold.ttf create mode 100644 lib/core/assets/fonts.gen.dart diff --git a/assets/fonts/quicksand/Quicksand-Bold.ttf b/assets/fonts/quicksand/Quicksand-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0106805ff5eddc4da561019fe8124c3831c69164 GIT binary patch literal 78592 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�hnUnyIc- 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