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), ), ), ),