From 805673755ba9bbc2aa299e0e22776da8fa14c1de Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 31 Jul 2025 20:58:10 +0700 Subject: [PATCH] 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!, + ], + ), ); } }