first commit

This commit is contained in:
Aditya Siregar
2025-07-30 22:38:44 +07:00
commit 73320561b0
444 changed files with 64633 additions and 0 deletions
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/components/custom_text_field.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/presentation/setting/bloc/add_discount/add_discount_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/discount/discount_bloc.dart';
import '../../../core/components/buttons.dart';
import '../../../core/components/spaces.dart';
import '../models/discount_model.dart';
class FormDiscountDialog extends StatefulWidget {
final DiscountModel? data;
const FormDiscountDialog({super.key, this.data});
@override
State<FormDiscountDialog> createState() => _FormDiscountDialogState();
}
class _FormDiscountDialogState extends State<FormDiscountDialog> {
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final discountController = TextEditingController();
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
),
const Text('Tambah Diskon'),
const Spacer(),
],
),
content: SingleChildScrollView(
child: SizedBox(
width: context.deviceWidth / 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextField(
controller: nameController,
label: 'Nama Diskon',
onChanged: (value) {},
),
const SpaceHeight(24.0),
CustomTextField(
controller: descriptionController,
label: 'Deskripsi (Opsional)',
onChanged: (value) {},
),
const SpaceHeight(24.0),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: CustomTextField(
controller: TextEditingController(text: 'Presentase'),
label: 'Nilai',
suffixIcon: const Icon(Icons.chevron_right),
onChanged: (value) {},
readOnly: true,
),
),
const SpaceWidth(14.0),
Flexible(
child: CustomTextField(
showLabel: false,
controller: discountController,
label: 'Percent',
prefixIcon: const Icon(Icons.percent),
onChanged: (value) {},
keyboardType: TextInputType.number,
),
),
],
),
const SpaceHeight(24.0),
BlocConsumer<AddDiscountBloc, AddDiscountState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
success: () {
context
.read<DiscountBloc>()
.add(const DiscountEvent.getDiscounts());
context.pop();
},
);
},
builder: (context, state) {
return state.maybeWhen(orElse: () {
return Button.filled(
onPressed: () {
context.read<AddDiscountBloc>().add(
AddDiscountEvent.addDiscount(
name: nameController.text,
description: descriptionController.text,
value: int.parse(discountController.text),
),
);
},
label: 'Simpan Diskon',
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
});
},
)
],
),
),
),
);
}
}
@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import '../../../core/components/buttons.dart';
import '../../../core/components/custom_dropdown.dart';
import '../../../core/components/custom_text_field.dart';
import '../../../core/components/spaces.dart';
import '../../../core/constants/colors.dart';
import '../models/printer_model.dart';
class FormPrinterDialog extends StatelessWidget {
final PrinterModel? data;
const FormPrinterDialog({super.key, this.data});
@override
Widget build(BuildContext context) {
final nameController = TextEditingController(text: data?.name ?? '');
final ipAddressController =
TextEditingController(text: data?.ipAddress ?? '');
final sizeController = ValueNotifier<String>(data?.size ?? '58mm');
final typeController =
ValueNotifier<PrinterType>(data?.type ?? PrinterType.wifi);
final isCutReceipt = ValueNotifier<bool>(false);
final isOpenLychee = ValueNotifier<bool>(false);
return AlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
),
Text(data == null ? 'Tambah Printer' : 'Edit Printer'),
const Spacer(),
],
),
content: SingleChildScrollView(
child: SizedBox(
width: context.deviceWidth / 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextField(
controller: nameController,
label: 'Nama Printer',
onChanged: (value) {},
),
const SpaceHeight(24.0),
CustomTextField(
controller: ipAddressController,
label: 'Alamat IP',
onChanged: (value) {},
),
const SpaceHeight(24.0),
ValueListenableBuilder(
valueListenable: sizeController,
builder: (context, value, child) => CustomDropdown(
value: value,
items: const ['58mm', '80mm'],
label: 'Ukuruan Struk',
onChanged: (value) => sizeController.value = value ?? '',
),
),
const SpaceHeight(24.0),
ValueListenableBuilder(
valueListenable: typeController,
builder: (context, value, child) => CustomDropdown(
value: value.value,
items: [
PrinterType.wifi.value,
PrinterType.bluetooth.value,
],
label: 'Type',
onChanged: (value) =>
typeController.value = PrinterType.fromValue(value ?? ''),
),
),
const SpaceHeight(24.0),
ValueListenableBuilder(
valueListenable: isCutReceipt,
builder: (context, value, child) => GestureDetector(
onTap: () => isCutReceipt.value = !value,
child: ListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Potong struk setelah selesai cetak',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
),
subtitle: const Text(
'hanya aktifkan jika printer kamu support',
style: TextStyle(fontSize: 12.0),
),
trailing: Checkbox(
value: value,
onChanged: (_) => isCutReceipt.value = !value,
),
textColor: AppColors.primary,
),
),
),
const SpaceHeight(24.0),
ValueListenableBuilder(
valueListenable: isOpenLychee,
builder: (context, value, child) => GestureDetector(
onTap: () => isOpenLychee.value = !value,
child: ListTile(
contentPadding: EdgeInsets.zero,
title: const Text(
'Buka laci setelah selesai cetak',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
),
subtitle: const Text(
'hanya aktifkan jika printer kamu support',
style: TextStyle(fontSize: 12.0),
),
trailing: Checkbox(
value: value,
onChanged: (_) => isOpenLychee.value = !value,
),
textColor: AppColors.primary,
),
),
),
const SpaceHeight(24.0),
Button.filled(
onPressed: () {
if (data == null) {
// TODO: do add printer
} else {
// TODO: do edit printer
}
context.pop();
},
label: data == null ? 'Simpan' : 'Perbarui',
)
],
),
),
),
);
}
}
@@ -0,0 +1,409 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/components/custom_text_field.dart';
import 'package:enaklo_pos/core/components/image_picker_widget.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
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/category_response_model.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
import 'package:enaklo_pos/presentation/setting/bloc/add_product/add_product_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/get_categories/get_categories_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/get_products/get_products_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/sync_product/sync_product_bloc.dart';
import 'package:enaklo_pos/presentation/setting/bloc/update_product/update_product_bloc.dart';
import 'package:image_picker/image_picker.dart';
import '../../../core/components/buttons.dart';
import '../../../core/components/spaces.dart';
class FormProductDialog extends StatefulWidget {
final Product? product;
const FormProductDialog({
super.key,
this.product,
});
@override
State<FormProductDialog> createState() => _FormProductDialogState();
}
class _FormProductDialogState extends State<FormProductDialog> {
TextEditingController? nameController;
TextEditingController? priceController;
TextEditingController? stockController;
XFile? imageFile;
bool isBestSeller = false;
int priceValue = 0;
CategoryModel? selectCategory;
String? imageUrl;
String? printType = 'kitchen';
bool isEditMode = false;
@override
void initState() {
context.read<GetCategoriesBloc>().add(const GetCategoriesEvent.fetch());
nameController = TextEditingController();
priceController = TextEditingController();
stockController = TextEditingController();
// Check if we're in edit mode
isEditMode = widget.product != null;
if (isEditMode) {
// Pre-fill the form with existing product data
final product = widget.product!;
nameController!.text = product.name ?? '';
priceValue = int.tryParse(product.price ?? '0') ?? 0;
priceController!.text = priceValue.currencyFormatRp;
stockController!.text = (product.stock ?? 0).toString();
isBestSeller = product.isFavorite == 1;
printType = product.printerType ?? 'kitchen';
imageUrl = product.image;
}
super.initState();
}
@override
void dispose() {
super.dispose();
nameController!.dispose();
priceController!.dispose();
stockController!.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
),
Text(
isEditMode ? 'Edit Product' : 'Add Product',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Spacer(),
],
),
content: SingleChildScrollView(
child: SizedBox(
width: context.deviceWidth / 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextField(
controller: nameController!,
label: 'Product Name',
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.words,
),
const SpaceHeight(20.0),
CustomTextField(
controller: priceController!,
label: 'Price',
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onChanged: (value) {
priceValue = value.toIntegerFromText;
final int newValue = value.toIntegerFromText;
priceController!.text = newValue.currencyFormatRp;
priceController!.selection = TextSelection.fromPosition(
TextPosition(offset: priceController!.text.length));
},
),
const SpaceHeight(20.0),
ImagePickerWidget(
label: 'Photo Product',
onChanged: (file) {
if (file == null) {
return;
}
imageFile = file;
},
initialImageUrl: imageUrl,
),
const SpaceHeight(20.0),
CustomTextField(
controller: stockController!,
label: 'Stock',
keyboardType: TextInputType.number,
),
const SpaceHeight(20.0),
const Text(
"Category",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SpaceHeight(12.0),
BlocBuilder<GetCategoriesBloc, GetCategoriesState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
},
success: (categories) {
// Set the selected category if in edit mode and not already set
if (isEditMode && selectCategory == null && widget.product?.category != null) {
try {
selectCategory = categories.firstWhere(
(cat) => cat.id == widget.product!.category!.id,
);
} catch (e) {
// If no exact match found, leave selectCategory as null
// This will show the hint text instead
log("No matching category found for product category ID: ${widget.product!.category!.id}");
}
}
return DropdownButtonHideUnderline(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
child: DropdownButton<CategoryModel>(
value: selectCategory,
hint: const Text("Select Category"),
isExpanded: true, // Untuk mengisi lebar container
onChanged: (newValue) {
if (newValue != null) {
selectCategory = newValue;
setState(() {});
log("selectCategory: ${selectCategory!.name}");
}
},
items: categories
.map<DropdownMenuItem<CategoryModel>>(
(CategoryModel category) {
return DropdownMenuItem<CategoryModel>(
value: category,
child: Text(category.name!),
);
}).toList(),
),
),
);
},
);
},
),
const SpaceHeight(12.0),
const Text(
"Print Type",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
//radio printer type
const SpaceHeight(12.0),
Row(
children: [
Radio(
value: 'kitchen',
groupValue: printType,
onChanged: (value) {
setState(() {
printType = value;
});
},
),
const Text('Kitchen'),
],
),
Row(
children: [
Radio(
value: 'bar',
groupValue: printType,
onChanged: (value) {
setState(() {
printType = value;
});
},
),
const Text('Bar'),
],
),
const SpaceHeight(20.0),
Row(
children: [
Checkbox(
value: isBestSeller,
onChanged: (value) {
setState(() {
isBestSeller = value!;
});
},
),
const Text('Favorite Product'),
],
),
const SpaceHeight(20.0),
const SpaceHeight(24.0),
if (isEditMode)
BlocConsumer<UpdateProductBloc, UpdateProductState>(
listener: (context, state) {
state.maybeMap(
orElse: () {},
success: (_) {
context
.read<SyncProductBloc>()
.add(const SyncProductEvent.syncProduct());
context
.read<GetProductsBloc>()
.add(const GetProductsEvent.fetch());
context.pop(true);
const snackBar = SnackBar(
content: Text('Success Update Product'),
backgroundColor: AppColors.primary,
);
ScaffoldMessenger.of(context).showSnackBar(
snackBar,
);
},
error: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $message'),
backgroundColor: Colors.red,
),
);
},
);
},
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return Button.filled(
onPressed: () {
if (selectCategory == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select a category'),
backgroundColor: Colors.red,
),
);
return;
}
log("isBestSeller: $isBestSeller");
final String name = nameController!.text;
final int stock = stockController!.text.toIntegerFromText;
final Product product = widget.product!.copyWith(
name: name,
price: priceValue.toString(),
stock: stock,
categoryId: selectCategory!.id!,
isFavorite: isBestSeller ? 1 : 0,
printerType: printType,
);
context.read<UpdateProductBloc>().add(
UpdateProductEvent.updateProduct(product, imageFile));
},
label: 'Update Product',
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
)
else
BlocConsumer<AddProductBloc, AddProductState>(
listener: (context, state) {
state.maybeMap(
orElse: () {},
success: (_) {
context
.read<SyncProductBloc>()
.add(const SyncProductEvent.syncProduct());
context
.read<GetProductsBloc>()
.add(const GetProductsEvent.fetch());
context.pop(true);
const snackBar = SnackBar(
content: Text('Success Add Product'),
backgroundColor: AppColors.primary,
);
ScaffoldMessenger.of(context).showSnackBar(
snackBar,
);
},
);
},
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return Button.filled(
onPressed: () {
if (selectCategory == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select a category'),
backgroundColor: Colors.red,
),
);
return;
}
log("isBestSeller: $isBestSeller");
final String name = nameController!.text;
final int stock =
stockController!.text.toIntegerFromText;
final Product product = Product(
name: name,
price: priceValue.toString(),
stock: stock,
categoryId: selectCategory!.id!,
isFavorite: isBestSeller ? 1 : 0,
image: imageFile!.path,
printerType: printType,
);
context.read<AddProductBloc>().add(
AddProductEvent.addProduct(product, imageFile!));
},
label: 'Save Product',
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
),
const SpaceHeight(16.0),
],
),
),
),
);
}
}
@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import '../../../core/components/buttons.dart';
import '../../../core/components/custom_text_field.dart';
import '../../../core/components/spaces.dart';
class FormTaxDialog extends StatefulWidget {
final int taxValue;
final int serviceChargeValue;
final Function(int taxValue, int serviceChargeValue)? onSave;
const FormTaxDialog({
super.key,
required this.taxValue,
required this.serviceChargeValue,
this.onSave,
});
@override
State<FormTaxDialog> createState() => _FormTaxDialogState();
}
class _FormTaxDialogState extends State<FormTaxDialog> {
late final TextEditingController serviceFeeController;
late final TextEditingController taxFeeController;
@override
void initState() {
super.initState();
serviceFeeController = TextEditingController(text: widget.serviceChargeValue.toString());
taxFeeController = TextEditingController(text: widget.taxValue.toString());
}
@override
void dispose() {
serviceFeeController.dispose();
taxFeeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.close),
),
const Text('Edit Perhitungan Biaya'),
const Spacer(),
],
),
content: SingleChildScrollView(
child: SizedBox(
width: context.deviceWidth / 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextField(
controller: serviceFeeController,
label: 'Biaya Layanan (%)',
onChanged: (value) {},
keyboardType: TextInputType.number,
suffixIcon: const Icon(Icons.percent),
),
const SpaceHeight(24.0),
CustomTextField(
controller: taxFeeController,
label: 'Pajak PB1 (%)',
onChanged: (value) {},
keyboardType: TextInputType.number,
suffixIcon: const Icon(Icons.percent),
),
const SpaceHeight(24.0),
Button.filled(
onPressed: () {
final taxValue = int.tryParse(taxFeeController.text) ?? 0;
final serviceChargeValue = int.tryParse(serviceFeeController.text) ?? 0;
if (widget.onSave != null) {
widget.onSave!(taxValue, serviceChargeValue);
}
context.pop();
},
label: 'Simpan',
)
],
),
),
),
);
}
}