Compare commits

...

7 Commits

Author SHA1 Message Date
efrilm
e825e5daed feat: report page 2025-08-01 18:27:40 +07:00
efrilm
8e4a289625 feat: diskon page 2025-08-01 15:41:02 +07:00
efrilm
8c946ce3d9 feat: tax page 2025-08-01 15:30:33 +07:00
efrilm
1384253e8a feat: sync data page 2025-08-01 15:12:09 +07:00
efrilm
15b27d8f67 feat: server key page 2025-08-01 15:01:44 +07:00
efrilm
06bf0d7987 feat: form product 2025-08-01 14:47:24 +07:00
efrilm
a1f3fbe854 fix: detail product 2025-08-01 14:17:11 +07:00
34 changed files with 1718 additions and 1158 deletions

View File

@ -1,4 +1,4 @@
# EnakloPOS
# ApskelPOS
A new Flutter project.

View File

@ -11,7 +11,7 @@
<!-- Izin khusus untuk akses foto (media images) di Android 33 ke atas -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application
android:label="EnakloPOS"
android:label="ApskelPOS"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<activity

View File

@ -4,31 +4,31 @@ import 'lib/core/utils/app_icon_generator.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
print('Generating EnakloPOS app icon...');
print('Generating ApskelPOS app icon...');
try {
final iconData = await AppIconGenerator.generateAppIcon();
// Ensure the assets/logo directory exists
final logoDir = Directory('assets/logo');
if (!await logoDir.exists()) {
await logoDir.create(recursive: true);
}
// Write the generated icon to file
final iconFile = File('assets/logo/logo_app_icon.png');
await iconFile.writeAsBytes(iconData);
print('✅ App icon generated successfully at: assets/logo/logo_app_icon.png');
print(
'✅ App icon generated successfully at: assets/logo/logo_app_icon.png');
print('📱 The icon features:');
print(' - White background for visibility');
print(' - Blue circular background');
print(' - Gift box with "e" inside');
print(' - "ENAKLO" and "POS" text');
print(' - 1024x1024 resolution for high quality');
} catch (e) {
print('❌ Error generating app icon: $e');
}
}
}

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>EnakloPOS</string>
<string>ApskelPOS</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

View File

@ -12,6 +12,7 @@ class CustomModalDialog extends StatelessWidget {
final double? maxWidth;
final double? minHeight;
final double? maxHeight;
final EdgeInsets? contentPadding;
const CustomModalDialog({
super.key,
@ -23,6 +24,7 @@ class CustomModalDialog extends StatelessWidget {
this.maxWidth,
this.minHeight,
this.maxHeight,
this.contentPadding,
});
@override
@ -100,7 +102,10 @@ class CustomModalDialog extends StatelessWidget {
],
),
),
child,
Padding(
padding: contentPadding ?? EdgeInsets.zero,
child: child,
),
],
),
),

View File

@ -38,7 +38,7 @@ class ItemSalesInvoice {
return HelperPdfService.saveDocument(
name:
'Enaklo POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
'Apskel POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf);
}
@ -48,7 +48,7 @@ class ItemSalesInvoice {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Item Sales Report',
Text('Apskel POS | Item Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,

View File

@ -22,13 +22,14 @@ class RevenueInvoice {
log("Starting PDF generation for summary report");
log("Summary model: ${summaryModel.toMap()}");
log("Search date formatted: $searchDateFormatted");
final pdf = Document();
log("PDF document created");
// Load logo image
log("Loading logo image...");
final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
final ByteData dataImage =
await rootBundle.load('assets/images/logo.png');
final Uint8List bytes = dataImage.buffer.asUint8List();
final image = pw.MemoryImage(bytes);
log("Logo image loaded successfully, size: ${bytes.length} bytes");
@ -49,7 +50,7 @@ class RevenueInvoice {
log("Saving PDF document...");
return HelperPdfService.saveDocument(
name:
'Enaklo POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
'Apskel POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf,
);
} catch (e) {
@ -69,7 +70,7 @@ class RevenueInvoice {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Summary Sales Report',
Text('Apskel POS | Summary Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@ -93,7 +94,7 @@ class RevenueInvoice {
static Widget buildTotal(SummaryModel summaryModel) {
log("Building total section with summary model: ${summaryModel.toMap()}");
// Helper function to safely parse string to int
int safeParseInt(String? value) {
if (value == null || value.isEmpty) return 0;
@ -104,7 +105,7 @@ class RevenueInvoice {
return 0;
}
}
return Container(
width: double.infinity,
child: Column(
@ -125,7 +126,8 @@ class RevenueInvoice {
buildText(
title: 'Discount',
titleStyle: TextStyle(fontWeight: FontWeight.normal),
value: "- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
value:
"- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
unite: true,
textStyle: TextStyle(
color: PdfColor.fromHex('#FF0000'),
@ -147,7 +149,8 @@ class RevenueInvoice {
titleStyle: TextStyle(
fontWeight: FontWeight.normal,
),
value: safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
value:
safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
unite: true,
),
Divider(),

View File

@ -38,7 +38,7 @@ class TransactionSalesInvoice {
return HelperPdfService.saveDocument(
name:
'Enaklo POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
'Apskel POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf);
}
@ -48,7 +48,7 @@ class TransactionSalesInvoice {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Transaction Sales Report',
Text('Apskel POS | Transaction Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,

View File

@ -35,7 +35,7 @@ class PrintDataoutputs {
final total = totalPrice + pajak;
bytes += generator.reset();
bytes += generator.text('Enaklo POS',
bytes += generator.text('Apskel POS',
styles: const PosStyles(
bold: true,
align: PosAlign.center,
@ -202,7 +202,7 @@ class PrintDataoutputs {
// bytes += generator.feed(3);
// }
bytes += generator.text('Enaklo POS',
bytes += generator.text('Apskel POS',
styles: const PosStyles(
bold: true,
align: PosAlign.center,
@ -474,21 +474,21 @@ class PrintDataoutputs {
}
Future<List<int>> printOrderV3(
List<ProductQuantity> products,
int totalQuantity,
int totalPrice,
String paymentMethod,
int nominalBayar,
int kembalian,
int subTotal,
int discount,
int pajak,
int serviceCharge,
String namaKasir,
String customerName,
int paper,
{int taxPercentage = 11, int serviceChargePercentage = 5}
) async {
List<ProductQuantity> products,
int totalQuantity,
int totalPrice,
String paymentMethod,
int nominalBayar,
int kembalian,
int subTotal,
int discount,
int pajak,
int serviceCharge,
String namaKasir,
String customerName,
int paper,
{int taxPercentage = 11,
int serviceChargePercentage = 5}) async {
List<int> bytes = [];
final profile = await CapabilityProfile.load();
@ -778,8 +778,13 @@ class PrintDataoutputs {
return bytes;
}
Future<List<int>> printChecker(List<ProductQuantity> products,
String tableName, String draftName, String cashierName, int paper, String orderType) async {
Future<List<int>> printChecker(
List<ProductQuantity> products,
String tableName,
String draftName,
String cashierName,
int paper,
String orderType) async {
List<int> bytes = [];
final profile = await CapabilityProfile.load();
@ -908,8 +913,13 @@ class PrintDataoutputs {
return bytes;
}
Future<List<int>> printKitchen(List<ProductQuantity> products,
String tableNumber, String draftName, String cashierName, int paper, String orderType) async {
Future<List<int>> printKitchen(
List<ProductQuantity> products,
String tableNumber,
String draftName,
String cashierName,
int paper,
String orderType) async {
List<int> bytes = [];
final profile = await CapabilityProfile.load();

View File

@ -49,7 +49,7 @@ class _LoginPageState extends State<LoginPage> {
const SpaceHeight(24.0),
const Center(
child: Text(
'Enaklo POS',
'Apskel POS',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,

View File

@ -34,264 +34,314 @@ class _ReportPageState extends State<ReportPage> {
DateTime fromDate = DateTime.now().subtract(const Duration(days: 30));
DateTime toDate = DateTime.now();
@override
void initState() {
super.initState();
context.read<TransactionReportBloc>().add(
TransactionReportEvent.getReport(
startDate: DateFormatter.formatDateTime(fromDate),
endDate: DateFormatter.formatDateTime(toDate)),
);
}
@override
Widget build(BuildContext context) {
String searchDateFormatted =
'${fromDate.toFormattedDate2()} to ${toDate.toFormattedDate2()}';
return Scaffold(
body: Row(
backgroundColor: AppColors.background,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// LEFT CONTENT
Expanded(
flex: 2,
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ReportTitle(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: CustomDatePicker(
prefix: const Text('From: '),
initialDate: fromDate,
onDateSelected: (selectedDate) {
fromDate = selectedDate;
ReportTitle(
actionWidget: [
SizedBox(
width: 300,
child: CustomDatePicker(
prefix: const Text('From: '),
initialDate: fromDate,
onDateSelected: (selectedDate) {
fromDate = selectedDate;
setState(() {});
},
),
),
const SpaceWidth(24.0),
Flexible(
child: CustomDatePicker(
prefix: const Text('To: '),
initialDate: toDate,
onDateSelected: (selectedDate) {
toDate = selectedDate;
setState(() {});
// context.read<TransactionReportBloc>().add(
// TransactionReportEvent.getReport(
// startDate:
// DateFormatter.formatDateTime(
// fromDate),
// endDate: DateFormatter.formatDateTime(
// toDate)),
// );
// context.read<ItemSalesReportBloc>().add(
// ItemSalesReportEvent.getItemSales(
// startDate:
// DateFormatter.formatDateTime(
// fromDate),
// endDate: DateFormatter.formatDateTime(
// toDate)),
// );
},
),
),
],
),
Padding(
padding: const EdgeInsets.all(15.0),
child: Wrap(
children: [
ReportMenu(
label: 'Transaction Report',
onPressed: () {
selectedMenu = 0;
title = 'Transaction Report';
setState(() {});
//enddate is 1 month before the current date
context.read<TransactionReportBloc>().add(
TransactionReportEvent.getReport(
startDate: DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
);
},
isActive: selectedMenu == 0,
),
ReportMenu(
label: 'Item Sales Report',
onPressed: () {
selectedMenu = 1;
title = 'Item Sales Report';
setState(() {});
context.read<ItemSalesReportBloc>().add(
ItemSalesReportEvent.getItemSales(
startDate: DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
);
},
isActive: selectedMenu == 1,
),
ReportMenu(
label: 'Product Sales Chart',
onPressed: () {
selectedMenu = 2;
title = 'Product Sales Chart';
setState(() {});
context.read<ProductSalesBloc>().add(
ProductSalesEvent.getProductSales(
DateFormatter.formatDateTime(fromDate),
DateFormatter.formatDateTime(toDate)),
);
},
isActive: selectedMenu == 2,
),
ReportMenu(
label: 'Summary Sales Report',
onPressed: () {
selectedMenu = 3;
title = 'Summary Sales Report';
setState(() {});
context.read<SummaryBloc>().add(
SummaryEvent.getSummary(
DateFormatter.formatDateTime(fromDate),
DateFormatter.formatDateTime(toDate)),
);
log("Date ${DateFormatter.formatDateTime(fromDate)}");
},
isActive: selectedMenu == 3,
),
ReportMenu(
label: 'Payment Method Report',
onPressed: () {
selectedMenu = 4;
title = 'Payment Method Report';
setState(() {});
context.read<PaymentMethodReportBloc>().add(
PaymentMethodReportEvent.getPaymentMethodReport(
startDate: DateFormatter.formatDateTime(fromDate),
endDate: DateFormatter.formatDateTime(toDate)),
);
},
isActive: selectedMenu == 4,
),
],
),
),
],
setState(() {});
},
),
),
const SpaceWidth(24.0),
SizedBox(
width: 300,
child: CustomDatePicker(
prefix: const Text('To: '),
initialDate: toDate,
onDateSelected: (selectedDate) {
toDate = selectedDate;
setState(() {});
// context.read<TransactionReportBloc>().add(
// TransactionReportEvent.getReport(
// startDate:
// DateFormatter.formatDateTime(
// fromDate),
// endDate: DateFormatter.formatDateTime(
// toDate)),
// );
// context.read<ItemSalesReportBloc>().add(
// ItemSalesReportEvent.getItemSales(
// startDate:
// DateFormatter.formatDateTime(
// fromDate),
// endDate: DateFormatter.formatDateTime(
// toDate)),
// );
},
),
),
],
),
Expanded(
child: Row(
children: [
// LEFT CONTENT
Expanded(
flex: 2,
child: Material(
color: AppColors.white,
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReportMenu(
label: 'Laporan Transaksi',
subtitle:
'Menampilkan riwayat lengkap semua transaksi yang telah dilakukan.',
icon: Icons.receipt_long_outlined,
onPressed: () {
selectedMenu = 0;
title = 'Laporan Transaksi';
setState(() {});
//enddate is 1 month before the current date
context.read<TransactionReportBloc>().add(
TransactionReportEvent.getReport(
startDate:
DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
);
},
isActive: selectedMenu == 0,
),
ReportMenu(
label: 'Laporan Penjualan Item',
subtitle:
'Laporan penjualan berdasarkan masing-masing item atau produk.',
icon: Icons.inventory_2_outlined,
onPressed: () {
selectedMenu = 1;
title = 'Laporan Penjualan Item';
setState(() {});
context.read<ItemSalesReportBloc>().add(
ItemSalesReportEvent.getItemSales(
startDate:
DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
);
},
isActive: selectedMenu == 1,
),
ReportMenu(
label: 'Chart Penjualan Produk',
subtitle:
'Grafik visual penjualan produk untuk analisa performa penjualan.',
icon: Icons.bar_chart_outlined,
onPressed: () {
selectedMenu = 2;
title = 'Chart Penjualan Produk';
setState(() {});
context.read<ProductSalesBloc>().add(
ProductSalesEvent.getProductSales(
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
);
},
isActive: selectedMenu == 2,
),
ReportMenu(
label: 'Ringkasan Laporan Penjualan',
subtitle:
'Ringkasan total penjualan dalam periode tertentu.',
icon: Icons.insert_drive_file_outlined,
onPressed: () {
selectedMenu = 3;
title = 'Ringkasan Laporan Penjualan';
setState(() {});
context.read<SummaryBloc>().add(
SummaryEvent.getSummary(
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
);
log("Date ${DateFormatter.formatDateTime(fromDate)}");
},
isActive: selectedMenu == 3,
),
ReportMenu(
label: 'Laporan Metode Pembayaran',
subtitle:
'Laporan metode pembayaran yang digunakan.',
icon: Icons.payment_outlined,
onPressed: () {
selectedMenu = 4;
title = 'Laporan Metode Pembayaran';
setState(() {});
context.read<PaymentMethodReportBloc>().add(
PaymentMethodReportEvent
.getPaymentMethodReport(
startDate:
DateFormatter.formatDateTime(
fromDate),
endDate:
DateFormatter.formatDateTime(
toDate)),
);
},
isActive: selectedMenu == 4,
),
],
),
),
),
),
),
// RIGHT CONTENT
Expanded(
flex: 4,
child: selectedMenu == 0
? BlocBuilder<TransactionReportBloc,
TransactionReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
loaded: (transactionReport) {
return TransactionReportWidget(
transactionReport: transactionReport,
title: title,
searchDateFormatted: searchDateFormatted,
headerWidgets: _getTitleReportPageWidget(),
);
},
);
},
)
: selectedMenu == 1
? BlocBuilder<ItemSalesReportBloc,
ItemSalesReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
loaded: (itemSales) {
return ItemSalesReportWidget(
itemSales: itemSales,
title: title,
searchDateFormatted:
searchDateFormatted,
headerWidgets:
_getItemSalesPageWidget(),
);
},
);
},
)
: selectedMenu == 2
? BlocBuilder<ProductSalesBloc,
ProductSalesState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
success: (productSales) {
return ProductSalesChartWidgets(
title: title,
searchDateFormatted:
searchDateFormatted,
productSales: productSales,
);
},
);
},
)
: selectedMenu == 3
? BlocBuilder<SummaryBloc, SummaryState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child:
CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
success: (summary) {
return SummaryReportWidget(
summary: summary,
title: title,
searchDateFormatted:
searchDateFormatted,
);
},
);
},
)
: selectedMenu == 4
? BlocBuilder<PaymentMethodReportBloc,
PaymentMethodReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child:
CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
loaded: (paymentMethodData) {
return PaymentMethodReportWidget(
paymentMethodData:
paymentMethodData,
title: title,
searchDateFormatted:
searchDateFormatted,
headerWidgets:
_getPaymentMethodPageWidget(),
);
},
);
},
)
: const SizedBox.shrink()),
],
),
),
// RIGHT CONTENT
Expanded(
flex: 2,
child: selectedMenu == 0
? BlocBuilder<TransactionReportBloc, TransactionReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
loaded: (transactionReport) {
return TransactionReportWidget(
transactionReport: transactionReport,
title: title,
searchDateFormatted: searchDateFormatted,
headerWidgets: _getTitleReportPageWidget(),
);
},
);
},
)
: selectedMenu == 1
? BlocBuilder<ItemSalesReportBloc, ItemSalesReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
loaded: (itemSales) {
return ItemSalesReportWidget(
itemSales: itemSales,
title: title,
searchDateFormatted: searchDateFormatted,
headerWidgets: _getItemSalesPageWidget(),
);
},
);
},
)
: selectedMenu == 2
? BlocBuilder<ProductSalesBloc, ProductSalesState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
success: (productSales) {
return ProductSalesChartWidgets(
title: title,
searchDateFormatted: searchDateFormatted,
productSales: productSales,
);
},
);
},
)
: selectedMenu == 3
? BlocBuilder<SummaryBloc, SummaryState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
success: (summary) {
return SummaryReportWidget(
summary: summary,
title: title,
searchDateFormatted: searchDateFormatted,
);
},
);
},
)
: selectedMenu == 4
? BlocBuilder<PaymentMethodReportBloc, PaymentMethodReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
loaded: (paymentMethodData) {
return PaymentMethodReportWidget(
paymentMethodData: paymentMethodData,
title: title,
searchDateFormatted: searchDateFormatted,
headerWidgets: _getPaymentMethodPageWidget(),
);
},
);
},
)
: const SizedBox.shrink()),
],
),
);
@ -314,11 +364,11 @@ class _ReportPageState extends State<ReportPage> {
List<Widget> _getItemSalesPageWidget() {
return [
_getTitleItemWidget('ID', 80),
_getTitleItemWidget('Order', 60),
_getTitleItemWidget('Product', 160),
_getTitleItemWidget('Order', 100),
_getTitleItemWidget('Product', 200),
_getTitleItemWidget('Qty', 60),
_getTitleItemWidget('Price', 140),
_getTitleItemWidget('Total Price', 140),
_getTitleItemWidget('Price', 150),
_getTitleItemWidget('Total Price', 160),
];
}

View File

@ -4,12 +4,12 @@ import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:enaklo_pos/presentation/report/widgets/report_page_title.dart';
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/utils/item_sales_invoice.dart';
import 'package:enaklo_pos/core/utils/permession_handler.dart';
import 'package:enaklo_pos/data/models/response/item_sales_response_model.dart';
import 'package:horizontal_data_table/horizontal_data_table.dart';
import 'package:permission_handler/permission_handler.dart';
class ItemSalesReportWidget extends StatelessWidget {
final String title;
@ -26,166 +26,123 @@ class ItemSalesReportWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: const Color.fromARGB(255, 255, 255, 255),
child: Column(
children: [
const SpaceHeight(24.0),
Center(
child: Text(
title,
style:
const TextStyle(fontWeight: FontWeight.w800, fontSize: 16.0),
),
),
const SizedBox(
height: 8.0,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
searchDateFormatted,
style: const TextStyle(fontSize: 16.0),
return Column(
children: [
ReportPageTitle(
title: title,
searchDateFormatted: searchDateFormatted,
onExport: () async {
try {
final status = await PermessionHelper().checkPermission();
if (status) {
final pdfFile = await ItemSalesInvoice.generate(
itemSales, searchDateFormatted);
log("pdfFile: $pdfFile");
await HelperPdfService.openFile(pdfFile);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to save PDF'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
log("Error generating PDF: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to generate PDF: $e'),
backgroundColor: Colors.red,
),
GestureDetector(
onTap: () async {
try {
final status = await PermessionHelper().checkPermission();
if (status) {
final pdfFile = await ItemSalesInvoice.generate(
itemSales, searchDateFormatted);
log("pdfFile: $pdfFile");
await HelperPdfService.openFile(pdfFile);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to save PDF'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
log("Error generating PDF: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to generate PDF: $e'),
backgroundColor: Colors.red,
),
);
}
},
child: const Row(
children: [
Text(
"PDF",
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
);
}
},
),
const SpaceHeight(16.0),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: HorizontalDataTable(
leftHandSideColumnWidth: 80,
rightHandSideColumnWidth: 670,
isFixedHeader: true,
headerWidgets: headerWidgets,
// isFixedFooter: true,
// footerWidgets: _getTitleWidget(),
leftSideItemBuilder: (context, index) {
return Container(
width: 80,
height: 52,
alignment: Alignment.centerLeft,
child: Center(child: Text(itemSales[index].id.toString())),
);
},
rightSideItemBuilder: (context, index) {
return Row(
children: <Widget>[
Container(
width: 100,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(itemSales[index].orderId.toString())),
),
Container(
width: 200,
height: 52,
alignment: Alignment.centerLeft,
child:
Center(child: Text(itemSales[index].productName!)),
),
Container(
width: 60,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(itemSales[index].quantity.toString())),
),
Container(
width: 150,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
itemSales[index].price!.currencyFormatRp,
)),
),
Container(
width: 160,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
(itemSales[index].price! * itemSales[index].quantity!)
.currencyFormatRp,
)),
),
Icon(
Icons.download_outlined,
color: AppColors.primary,
)
],
),
);
},
itemCount: itemSales.length,
rowSeparatorWidget: const Divider(
color: Colors.black38,
height: 1.0,
thickness: 0.0,
),
],
),
),
const SpaceHeight(16.0),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: HorizontalDataTable(
leftHandSideColumnWidth: 80,
rightHandSideColumnWidth: 560,
isFixedHeader: true,
headerWidgets: headerWidgets,
leftHandSideColBackgroundColor: AppColors.white,
rightHandSideColBackgroundColor: AppColors.white,
// isFixedFooter: true,
// footerWidgets: _getTitleWidget(),
leftSideItemBuilder: (context, index) {
return Container(
width: 80,
height: 52,
alignment: Alignment.centerLeft,
child:
Center(child: Text(itemSales[index].id.toString())),
);
},
rightSideItemBuilder: (context, index) {
return Row(
children: <Widget>[
Container(
width: 60,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(itemSales[index].orderId.toString())),
),
Container(
width: 160,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(itemSales[index].productName!)),
),
Container(
width: 60,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child:
Text(itemSales[index].quantity.toString())),
),
Container(
width: 140,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
itemSales[index].price!.currencyFormatRp,
)),
),
Container(
width: 140,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
(itemSales[index].price! *
itemSales[index].quantity!)
.currencyFormatRp,
)),
),
],
);
},
itemCount: itemSales.length,
rowSeparatorWidget: const Divider(
color: Colors.black38,
height: 1.0,
thickness: 0.0,
),
leftHandSideColBackgroundColor: AppColors.white,
rightHandSideColBackgroundColor: AppColors.white,
itemExtent: 55,
),
itemExtent: 55,
),
),
),
],
),
),
],
);
}
}

View File

@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/constants/colors.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/payment_method_response_model.dart';
import 'package:enaklo_pos/presentation/report/widgets/report_title.dart';
import '../../../core/components/spaces.dart';
@ -109,20 +107,20 @@ class PaymentMethodReportWidget extends StatelessWidget {
),
),
child: Row(
children: [
_getBodyItemWidget(
item.paymentMethod ?? '-',
180,
),
_getBodyItemWidget(
item.totalAmount?.currencyFormatRpV2 ?? 'Rp 0',
180,
),
_getBodyItemWidget(
item.transactionCount?.toString() ?? '0',
180,
),
],
children: [
_getBodyItemWidget(
item.paymentMethod ?? '-',
180,
),
_getBodyItemWidget(
item.totalAmount?.currencyFormatRpV2 ?? 'Rp 0',
180,
),
_getBodyItemWidget(
item.transactionCount?.toString() ?? '0',
180,
),
],
),
);
}).toList() ??
@ -150,4 +148,4 @@ class PaymentMethodReportWidget extends StatelessWidget {
),
);
}
}
}

View File

@ -1,6 +1,7 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/presentation/report/widgets/report_page_title.dart';
import 'package:flutter/material.dart';
import 'package:pie_chart/pie_chart.dart';
@ -67,59 +68,61 @@ class _ProductSalesChartWidgetsState extends State<ProductSalesChartWidgets> {
@override
Widget build(BuildContext context) {
return Card(
color: const Color.fromARGB(255, 255, 255, 255),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: Column(
children: [
const SpaceHeight(24.0),
Center(
child: Text(
widget.title,
style: const TextStyle(
fontWeight: FontWeight.w800, fontSize: 16.0),
),
),
Center(
child: Text(
widget.searchDateFormatted,
style: const TextStyle(fontSize: 16.0),
),
),
const SpaceHeight(16.0),
PieChart(
dataMap: dataMap2,
animationDuration: Duration(milliseconds: 800),
chartLegendSpacing: 32,
chartRadius: MediaQuery.of(context).size.width / 3.2,
colorList: colorList,
initialAngleInDegree: 0,
chartType: ChartType.disc,
ringStrokeWidth: 32,
// centerText: "HYBRID",
legendOptions: LegendOptions(
showLegendsInRow: false,
legendPosition: LegendPosition.right,
showLegends: true,
legendShape: BoxShape.circle,
legendTextStyle: TextStyle(
fontWeight: FontWeight.bold,
),
),
chartValuesOptions: ChartValuesOptions(
showChartValueBackground: true,
showChartValues: true,
showChartValuesInPercentage: false,
showChartValuesOutside: false,
decimalPlaces: 0,
),
// gradientList: ---To add gradient colors---
// emptyColorGradient: ---Empty Color gradient---
)
],
return Column(
children: [
ReportPageTitle(
title: widget.title,
searchDateFormatted: widget.searchDateFormatted,
onExport: () async {},
isExport: false, // Set to false if export is not needed
),
),
const SpaceHeight(16.0),
Expanded(
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(16.0),
margin: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
PieChart(
dataMap: dataMap2,
animationDuration: Duration(milliseconds: 800),
chartLegendSpacing: 32,
chartRadius: MediaQuery.of(context).size.width / 3.2,
colorList: colorList,
initialAngleInDegree: 0,
chartType: ChartType.disc,
ringStrokeWidth: 32,
// centerText: "HYBRID",
legendOptions: LegendOptions(
showLegendsInRow: false,
legendPosition: LegendPosition.right,
showLegends: true,
legendShape: BoxShape.circle,
legendTextStyle: TextStyle(
fontWeight: FontWeight.bold,
),
),
chartValuesOptions: ChartValuesOptions(
showChartValueBackground: true,
showChartValues: true,
showChartValuesInPercentage: false,
showChartValuesOutside: false,
decimalPlaces: 0,
),
// gradientList: ---To add gradient colors---
// emptyColorGradient: ---Empty Color gradient---
),
],
),
),
),
)
],
);
}
}

View File

@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/assets/assets.gen.dart';
import '../../../core/components/spaces.dart';
import '../../../core/constants/colors.dart';
class ReportMenu extends StatelessWidget {
final String label;
final String subtitle;
final IconData icon;
final VoidCallback onPressed;
final bool isActive;
@ -16,6 +15,8 @@ class ReportMenu extends StatelessWidget {
required this.label,
required this.onPressed,
required this.isActive,
required this.subtitle,
required this.icon,
});
@override
@ -23,38 +24,43 @@ class ReportMenu extends StatelessWidget {
return GestureDetector(
onTap: onPressed,
child: Container(
margin: const EdgeInsets.all(5.0),
width: 180.0,
height: 160.0,
alignment: Alignment.center,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color:
isActive ? AppColors.primary.withOpacity(0.13) : AppColors.white,
borderRadius: BorderRadius.circular(18.0),
border: Border.all(
color: isActive ? AppColors.primary : AppColors.stroke,
border: Border(
right: BorderSide(
color: isActive ? AppColors.primary : Colors.transparent,
width: 4.0,
),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
child: Row(
children: [
Assets.icons.report.svg(
colorFilter: isActive
? const ColorFilter.mode(
AppColors.primary,
BlendMode.srcIn,
)
: null,
Icon(
icon,
size: 24.0,
color: isActive ? AppColors.primary : AppColors.grey,
),
const SpaceHeight(28.0),
Text(
label,
style: TextStyle(
color: isActive ? AppColors.primary : AppColors.black,
fontWeight: FontWeight.w600,
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
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),
),
],
),
),
const SpaceHeight(24.0),
],
),
),

View File

@ -0,0 +1,65 @@
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:flutter/material.dart';
class ReportPageTitle extends StatelessWidget {
final String title;
final String searchDateFormatted;
final Function() onExport;
final bool isExport;
const ReportPageTitle(
{super.key,
required this.title,
required this.searchDateFormatted,
required this.onExport,
this.isExport = true});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(color: AppColors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w800, fontSize: 16.0),
),
const SizedBox(
height: 8.0,
),
Text(
searchDateFormatted,
style: const TextStyle(fontSize: 16.0),
),
],
),
if (isExport)
GestureDetector(
onTap: onExport,
child: const Row(
children: [
Text(
"PDF",
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
Icon(
Icons.download_outlined,
color: AppColors.primary,
)
],
),
),
],
),
);
}
}

View File

@ -1,38 +1,60 @@
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/components/components.dart';
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
import '../../../core/constants/colors.dart';
class ReportTitle extends StatelessWidget {
const ReportTitle({super.key});
final List<Widget>? actionWidget;
const ReportTitle({super.key, this.actionWidget});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Report',
style: TextStyle(
color: AppColors.primary,
fontSize: 28,
fontWeight: FontWeight.w600,
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 10.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,
),
),
const SpaceHeight(4.0),
Text(
DateTime.now().toFormattedDate(),
style: const TextStyle(
color: AppColors.subtitle,
fontSize: 16,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Laporan',
style: TextStyle(
color: AppColors.black,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
Text(
DateTime.now().toFormattedDate2(),
style: TextStyle(
color: AppColors.grey,
fontSize: 14,
),
),
],
),
),
const SpaceHeight(20.0),
const Divider(),
],
if (actionWidget != null)
Row(
children: actionWidget!,
),
],
),
);
}
}

View File

@ -9,7 +9,6 @@ import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:enaklo_pos/core/utils/permession_handler.dart';
import 'package:enaklo_pos/data/models/response/summary_response_model.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/revenue_invoice.dart';
@ -55,27 +54,29 @@ class SummaryReportWidget extends StatelessWidget {
log("PDF button clicked for summary report");
try {
log("Checking permissions...");
final status = await PermessionHelper().checkPermission();
final status =
await PermessionHelper().checkPermission();
log("Permission status: $status");
if (status) {
log("Permission granted, starting PDF generation...");
log("Summary data: ${summary.toMap()}");
log("Search date: $searchDateFormatted");
final pdfFile = await RevenueInvoice.generate(
summary, searchDateFormatted);
log("PDF file generated: ${pdfFile.path}");
log("PDF file exists: ${await pdfFile.exists()}");
log("PDF file size: ${await pdfFile.length()} bytes");
log("Attempting to open PDF file...");
await HelperPdfService.openFile(pdfFile);
log("PDF file opened successfully");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('PDF saved successfully: ${pdfFile.path}'),
content: Text(
'PDF saved successfully: ${pdfFile.path}'),
backgroundColor: Colors.green,
),
);
@ -83,7 +84,8 @@ class SummaryReportWidget extends StatelessWidget {
log("Permission denied");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to save PDF'),
content: Text(
'Storage permission is required to save PDF'),
backgroundColor: Colors.red,
),
);

View File

@ -5,12 +5,12 @@ import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:enaklo_pos/presentation/report/widgets/report_page_title.dart';
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/utils/permession_handler.dart';
import 'package:enaklo_pos/core/utils/transaction_sales_invoice.dart';
import 'package:enaklo_pos/data/models/response/order_remote_datasource.dart';
import 'package:horizontal_data_table/horizontal_data_table.dart';
import 'package:permission_handler/permission_handler.dart';
class TransactionReportWidget extends StatelessWidget {
final String title;
@ -27,208 +27,168 @@ class TransactionReportWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: const Color.fromARGB(255, 255, 255, 255),
child: Column(
children: [
const SpaceHeight(24.0),
Center(
child: Text(
title,
style:
const TextStyle(fontWeight: FontWeight.w800, fontSize: 16.0),
),
),
const SizedBox(
height: 8.0,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
searchDateFormatted,
style: const TextStyle(fontSize: 16.0),
return Column(
children: [
ReportPageTitle(
title: title,
searchDateFormatted: searchDateFormatted,
onExport: () async {
try {
final status = await PermessionHelper().checkPermission();
if (status) {
final pdfFile = await TransactionSalesInvoice.generate(
transactionReport, searchDateFormatted);
log("pdfFile: $pdfFile");
await HelperPdfService.openFile(pdfFile);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to save PDF'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
log("Error generating PDF: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to generate PDF: $e'),
backgroundColor: Colors.red,
),
GestureDetector(
onTap: () async {
try {
final status = await PermessionHelper().checkPermission();
if (status) {
final pdfFile = await TransactionSalesInvoice.generate(
transactionReport, searchDateFormatted);
log("pdfFile: $pdfFile");
await HelperPdfService.openFile(pdfFile);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to save PDF'),
backgroundColor: Colors.red,
);
}
},
),
const SpaceHeight(16.0),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: HorizontalDataTable(
leftHandSideColumnWidth: 50,
rightHandSideColumnWidth: 1020,
isFixedHeader: true,
headerWidgets: headerWidgets,
// isFixedFooter: true,
// footerWidgets: _getTitleWidget(),
leftSideItemBuilder: (context, index) {
return Container(
width: 40,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(transactionReport[index].id.toString())),
);
},
rightSideItemBuilder: (context, index) {
return Row(
children: <Widget>[
Container(
width: 120,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].total!.currencyFormatRp,
)),
),
Container(
width: 120,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].subTotal!.currencyFormatRp,
)),
),
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].tax!.currencyFormatRp,
)),
),
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
int.parse(transactionReport[index]
.discountAmount!
.replaceAll('.00', ''))
.currencyFormatRp,
),
);
}
} catch (e) {
log("Error generating PDF: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to generate PDF: $e'),
backgroundColor: Colors.red,
),
);
}
},
child: const Row(
children: [
Text(
"PDF",
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
Icon(
Icons.download_outlined,
color: AppColors.primary,
)
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index]
.serviceCharge!
.currencyFormatRp,
),
),
),
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].totalItem.toString()),
),
),
Container(
width: 150,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(transactionReport[index].namaKasir!),
),
),
Container(
width: 230,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(transactionReport[index]
.transactionTime!
.toFormattedDate()),
),
),
],
),
);
},
itemCount: transactionReport.length,
rowSeparatorWidget: const Divider(
color: Colors.black38,
height: 1.0,
thickness: 0.0,
),
],
),
),
const SpaceHeight(16.0),
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: HorizontalDataTable(
leftHandSideColumnWidth: 50,
rightHandSideColumnWidth: 1020,
isFixedHeader: true,
headerWidgets: headerWidgets,
// isFixedFooter: true,
// footerWidgets: _getTitleWidget(),
leftSideItemBuilder: (context, index) {
return Container(
width: 40,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(transactionReport[index].id.toString())),
);
},
rightSideItemBuilder: (context, index) {
return Row(
children: <Widget>[
Container(
width: 120,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].total!.currencyFormatRp,
)),
),
Container(
width: 120,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].subTotal!.currencyFormatRp,
)),
),
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].tax!.currencyFormatRp,
)),
),
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
int.parse(transactionReport[index]
.discountAmount!
.replaceAll('.00', ''))
.currencyFormatRp,
),
),
),
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index]
.serviceCharge!
.currencyFormatRp,
),
),
),
Container(
width: 100,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
transactionReport[index].totalItem.toString()),
),
),
Container(
width: 150,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(transactionReport[index].namaKasir!),
),
),
Container(
width: 230,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(transactionReport[index]
.transactionTime!
.toFormattedDate()),
),
),
],
);
},
itemCount: transactionReport.length,
rowSeparatorWidget: const Divider(
color: Colors.black38,
height: 1.0,
thickness: 0.0,
),
leftHandSideColBackgroundColor: AppColors.white,
rightHandSideColBackgroundColor: AppColors.white,
leftHandSideColBackgroundColor: AppColors.white,
rightHandSideColBackgroundColor: AppColors.white,
itemExtent: 55,
),
itemExtent: 55,
),
),
),
],
),
),
],
);
}
}

View File

@ -32,7 +32,7 @@ class _SalesPageState extends State<SalesPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Enaklo POS ',
'Apskel POS ',
style: TextStyle(
color: AppColors.primary,
fontSize: 22,

View File

@ -32,6 +32,8 @@ class DetailProductDialog extends StatelessWidget {
? product.image!
: '${Variables.baseUrl}/${product.image}',
fit: BoxFit.cover,
width: 120,
height: 120,
errorWidget: (context, url, error) => Container(
width: 120,
height: 120,

View File

@ -1,3 +1,4 @@
import 'package:enaklo_pos/core/components/custom_modal_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/components/custom_text_field.dart';
@ -23,19 +24,10 @@ class _FormDiscountDialogState extends State<FormDiscountDialog> {
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(
return CustomModalDialog(
title: widget.data == null ? 'Tambah Diskon' : 'Edit Diskon',
contentPadding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: SizedBox(
width: context.deviceWidth / 3,
child: Column(

View File

@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:enaklo_pos/core/components/custom_modal_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/components/custom_text_field.dart';
@ -21,10 +22,7 @@ import '../../../core/components/spaces.dart';
class FormProductDialog extends StatefulWidget {
final Product? product;
const FormProductDialog({
super.key,
this.product,
});
const FormProductDialog({super.key, this.product});
@override
State<FormProductDialog> createState() => _FormProductDialogState();
@ -54,7 +52,393 @@ class _FormProductDialogState extends State<FormProductDialog> {
// 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 CustomModalDialog(
title: widget.product == null ? "Tambah Produk" : "Ubah Produk",
subtitle: widget.product == null
? "Silakan isi formulir untuk menambahkan produk baru"
: "Silakan edit formulir untuk memperbarui produk",
child: Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextField(
controller: nameController!,
label: 'Nama Produk',
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.words,
),
const SpaceHeight(20.0),
CustomTextField(
controller: priceController!,
label: 'Harga',
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: 'Foto Produk',
onChanged: (file) {
if (file == null) {
return;
}
imageFile = file;
},
initialImageUrl: imageUrl,
),
const SpaceHeight(20.0),
CustomTextField(
controller: stockController!,
label: 'Stok',
keyboardType: TextInputType.number,
),
const SpaceHeight(20.0),
const Text(
"Kategori",
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("Pilih Kategori"),
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(
"Tipe Print",
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('Produk Favorit'),
],
),
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: 'Ubah Produk',
);
},
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: 'Simpan Produk',
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
),
const SpaceHeight(16.0),
],
),
),
),
),
);
}
}
class FormProductDialogOld extends StatefulWidget {
final Product? product;
const FormProductDialogOld({
super.key,
this.product,
});
@override
State<FormProductDialogOld> createState() => _FormProductDialogOldState();
}
class _FormProductDialogOldState extends State<FormProductDialogOld> {
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!;
@ -158,7 +542,9 @@ class _FormProductDialogState extends State<FormProductDialog> {
},
success: (categories) {
// Set the selected category if in edit mode and not already set
if (isEditMode && selectCategory == null && widget.product?.category != null) {
if (isEditMode &&
selectCategory == null &&
widget.product?.category != null) {
try {
selectCategory = categories.firstWhere(
(cat) => cat.id == widget.product!.category!.id,
@ -169,7 +555,7 @@ class _FormProductDialogState extends State<FormProductDialog> {
log("No matching category found for product category ID: ${widget.product!.category!.id}");
}
}
return DropdownButtonHideUnderline(
child: Container(
decoration: BoxDecoration(
@ -304,11 +690,12 @@ class _FormProductDialogState extends State<FormProductDialog> {
);
return;
}
log("isBestSeller: $isBestSeller");
final String name = nameController!.text;
final int stock = stockController!.text.toIntegerFromText;
final int stock =
stockController!.text.toIntegerFromText;
final Product product = widget.product!.copyWith(
name: name,
price: priceValue.toString(),
@ -317,9 +704,10 @@ class _FormProductDialogState extends State<FormProductDialog> {
isFavorite: isBestSeller ? 1 : 0,
printerType: printType,
);
context.read<UpdateProductBloc>().add(
UpdateProductEvent.updateProduct(product, imageFile));
UpdateProductEvent.updateProduct(
product, imageFile));
},
label: 'Update Product',
);
@ -370,7 +758,7 @@ class _FormProductDialogState extends State<FormProductDialog> {
);
return;
}
log("isBestSeller: $isBestSeller");
final String name = nameController!.text;
@ -386,7 +774,8 @@ class _FormProductDialogState extends State<FormProductDialog> {
printerType: printType,
);
context.read<AddProductBloc>().add(
AddProductEvent.addProduct(product, imageFile!));
AddProductEvent.addProduct(
product, imageFile!));
},
label: 'Save Product',
);

View File

@ -1,3 +1,4 @@
import 'package:enaklo_pos/core/components/custom_modal_dialog.dart';
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
@ -28,7 +29,8 @@ class _FormTaxDialogState extends State<FormTaxDialog> {
@override
void initState() {
super.initState();
serviceFeeController = TextEditingController(text: widget.serviceChargeValue.toString());
serviceFeeController =
TextEditingController(text: widget.serviceChargeValue.toString());
taxFeeController = TextEditingController(text: widget.taxValue.toString());
}
@ -41,19 +43,10 @@ class _FormTaxDialogState extends State<FormTaxDialog> {
@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(
return CustomModalDialog(
title: 'Edit Perhitungan Biaya',
contentPadding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: SizedBox(
width: context.deviceWidth / 3,
child: Column(
@ -78,12 +71,13 @@ class _FormTaxDialogState extends State<FormTaxDialog> {
Button.filled(
onPressed: () {
final taxValue = int.tryParse(taxFeeController.text) ?? 0;
final serviceChargeValue = int.tryParse(serviceFeeController.text) ?? 0;
final serviceChargeValue =
int.tryParse(serviceFeeController.text) ?? 0;
if (widget.onSave != null) {
widget.onSave!(taxValue, serviceChargeValue);
}
context.pop();
},
label: 'Simpan',

View File

@ -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/presentation/setting/bloc/discount/discount_bloc.dart';
@ -5,7 +7,6 @@ import 'package:enaklo_pos/presentation/setting/bloc/discount/discount_bloc.dart
import '../../home/widgets/custom_tab_bar.dart';
import '../dialogs/form_discount_dialog.dart';
import '../models/discount_model.dart';
import '../widgets/add_data.dart';
import '../widgets/manage_discount_card.dart';
import '../widgets/settings_title.dart';
@ -49,83 +50,94 @@ class _DiscountPageState extends State<DiscountPage> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SettingsTitle('Kelola Diskon'),
const SizedBox(height: 24),
CustomTabBar(
tabTitles: const ['Semua'],
initialTabIndex: 0,
tabViews: [
// SEMUA TAB
SizedBox(
child: BlocBuilder<DiscountBloc, DiscountState>(
builder: (context, state) {
return state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (discounts) {
return GridView.builder(
shrinkWrap: true,
itemCount: discounts.length + 1,
physics: const NeverScrollableScrollPhysics(),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
mainAxisSpacing: 30.0,
),
itemBuilder: (context, index) {
if (index == 0) {
return AddData(
title: 'Tambah Diskon Baru',
onPressed: onAddDataTap,
return Column(
children: [
SettingsTitle(
'Kelola Diskon',
subtitle: 'Kelola diskon untuk produk Anda',
actionWidget: [
Button.outlined(
onPressed: onAddDataTap,
label: "Tambah Diskon",
icon: Icon(Icons.add, color: AppColors.primary),
)
],
),
Expanded(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
CustomTabBar(
tabTitles: const ['Semua'],
initialTabIndex: 0,
tabViews: [
// SEMUA TAB
SizedBox(
child: BlocBuilder<DiscountBloc, DiscountState>(
builder: (context, state) {
return state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}
final item = discounts[index - 1];
return ManageDiscountCard(
data: item,
onEditTap: (){},
);
}, loaded: (discounts) {
return GridView.builder(
shrinkWrap: true,
itemCount: discounts.length,
physics: const NeverScrollableScrollPhysics(),
padding:
const EdgeInsets.symmetric(horizontal: 16.0),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
mainAxisSpacing: 30.0,
),
itemBuilder: (context, index) {
final item = discounts[index];
return ManageDiscountCard(
data: item,
onEditTap: () {},
);
},
);
});
// return GridView.builder(
// shrinkWrap: true,
// itemCount: discounts.length + 1,
// physics: const NeverScrollableScrollPhysics(),
// gridDelegate:
// const SliverGridDelegateWithFixedCrossAxisCount(
// childAspectRatio: 0.85,
// crossAxisCount: 3,
// crossAxisSpacing: 30.0,
// mainAxisSpacing: 30.0,
// ),
// itemBuilder: (context, index) {
// if (index == 0) {
// return AddData(
// title: 'Tambah Diskon Baru',
// onPressed: onAddDataTap,
// );
// }
// final item = discounts[index - 1];
// return ManageDiscountCard(
// data: item,
// onEditTap: () => onEditTap(item),
// );
// },
// );
},
);
});
// return GridView.builder(
// shrinkWrap: true,
// itemCount: discounts.length + 1,
// physics: const NeverScrollableScrollPhysics(),
// gridDelegate:
// const SliverGridDelegateWithFixedCrossAxisCount(
// childAspectRatio: 0.85,
// crossAxisCount: 3,
// crossAxisSpacing: 30.0,
// mainAxisSpacing: 30.0,
// ),
// itemBuilder: (context, index) {
// if (index == 0) {
// return AddData(
// title: 'Tambah Diskon Baru',
// onPressed: onAddDataTap,
// );
// }
// final item = discounts[index - 1];
// return ManageDiscountCard(
// data: item,
// onEditTap: () => onEditTap(item),
// );
// },
// );
},
),
),
],
),
),
],
],
),
),
],
),
),
],
);
}
}

View File

@ -171,8 +171,7 @@ class _ManagePrinterPageState extends State<ManagePrinterPage> {
//bytes += generator.setGlobalFont(PosFontType.fontA);
bytes += generator.reset();
bytes +=
generator.text('Enaklo POS', styles: const PosStyles(bold: true));
bytes += generator.text('Apskel POS', styles: const PosStyles(bold: true));
bytes +=
generator.text('Reverse text', styles: const PosStyles(reverse: true));
bytes += generator.text('Underlined text',

View File

@ -1,3 +1,6 @@
import 'package:enaklo_pos/core/components/components.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/presentation/setting/widgets/settings_title.dart';
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
@ -36,37 +39,64 @@ class _ServerKeyPageState extends State<ServerKeyPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Save Server Key'),
centerTitle: true,
),
//textfield untuk input server key
backgroundColor: AppColors.background,
body: Column(
children: [
SettingsTitle('Server Key'),
Padding(
padding: const EdgeInsets.all(20.0),
child: TextField(
controller: serverKeyController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Server Key',
),
),
),
//button untuk save server key
ElevatedButton(
onPressed: () {
AuthLocalDataSource()
.saveMidtransServerKey(serverKeyController!.text);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Server Key saved'),
backgroundColor: AppColors.primary,
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Server Key',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.black,
),
),
SpaceHeight(4),
TextField(
controller: serverKeyController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Server Key',
),
),
],
),
),
);
},
child: const Text('Save'),
const SpaceHeight(20),
Align(
alignment: Alignment.centerRight,
child: Button.filled(
width: context.deviceWidth * 0.2,
height: 44,
onPressed: () {
AuthLocalDataSource()
.saveMidtransServerKey(serverKeyController!.text);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Server Key saved'),
backgroundColor: AppColors.primary,
),
);
},
label: 'Simpan',
),
),
],
),
),
],
),

View File

@ -1,4 +1,7 @@
import 'dart:developer';
import 'package:enaklo_pos/core/components/buttons.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/presentation/setting/widgets/settings_title.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
@ -16,101 +19,154 @@ class _SyncDataPageState extends State<SyncDataPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sync Data'),
),
backgroundColor: AppColors.background,
body: Column(
children: [
BlocConsumer<SyncProductBloc, SyncProductState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
error: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
},
loaded: (productResponseModel) async {
await ProductLocalDatasource.instance.deleteAllProducts();
await ProductLocalDatasource.instance.insertProducts(
productResponseModel.data!,
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sync Product Success2'),
backgroundColor: Colors.green,
),
);
},
);
},
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return ElevatedButton(
onPressed: () {
context
.read<SyncProductBloc>()
.add(const SyncProductEvent.syncProduct());
},
child: const Text('Sync Product'));
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
SettingsTitle('Sync Data'),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16.0),
margin: const EdgeInsets.only(bottom: 16.0),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sinkronasikan Produk',
style: TextStyle(
color: AppColors.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
BlocConsumer<SyncProductBloc, SyncProductState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
error: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
},
loaded: (productResponseModel) async {
await ProductLocalDatasource.instance
.deleteAllProducts();
await ProductLocalDatasource.instance
.insertProducts(
productResponseModel.data!,
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sync Product Success2'),
backgroundColor: Colors.green,
),
);
},
);
},
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return Button.filled(
width: 100,
height: 40,
onPressed: () {
context.read<SyncProductBloc>().add(
const SyncProductEvent.syncProduct());
},
label: 'Sinkronasikan',
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
),
],
),
),
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sinkronasikan Pesanan',
style: TextStyle(
color: AppColors.black,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
BlocConsumer<SyncOrderBloc, SyncOrderState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
error: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
},
loaded: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sync Order Success'),
backgroundColor: Colors.green,
),
);
},
);
},
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return Button.filled(
width: 100,
height: 40,
onPressed: () {
log("🔘 Sync Order button pressed");
log("🔘 SyncOrderBloc instance: ${context.read<SyncOrderBloc>()}");
context
.read<SyncOrderBloc>()
.add(const SyncOrderEvent.syncOrder());
log("🔘 SyncOrderEvent.syncOrder dispatched");
},
label: 'Sinkronasikan',
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
)
],
),
),
],
),
),
BlocConsumer<SyncOrderBloc, SyncOrderState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
error: (message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
},
loaded: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sync Order Success'),
backgroundColor: Colors.green,
),
);
},
);
},
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return ElevatedButton(
onPressed: () {
log("🔘 Sync Order button pressed");
log("🔘 SyncOrderBloc instance: ${context.read<SyncOrderBloc>()}");
context
.read<SyncOrderBloc>()
.add(const SyncOrderEvent.syncOrder());
log("🔘 SyncOrderEvent.syncOrder dispatched");
},
child: const Text('Sync Order'),
);
},
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
)
],
),
);

View File

@ -31,11 +31,11 @@ class _TaxPageState extends State<TaxPage> {
serviceChargeValue: serviceChargeValue,
onSave: (newTaxValue, newServiceChargeValue) {
context.read<TaxSettingsBloc>().add(
TaxSettingsEvent.updateSettings(
taxValue: newTaxValue,
serviceChargeValue: newServiceChargeValue,
),
);
TaxSettingsEvent.updateSettings(
taxValue: newTaxValue,
serviceChargeValue: newServiceChargeValue,
),
);
},
),
);
@ -49,11 +49,11 @@ class _TaxPageState extends State<TaxPage> {
serviceChargeValue: serviceChargeValue,
onSave: (newTaxValue, newServiceChargeValue) {
context.read<TaxSettingsBloc>().add(
TaxSettingsEvent.updateSettings(
taxValue: newTaxValue,
serviceChargeValue: newServiceChargeValue,
),
);
TaxSettingsEvent.updateSettings(
taxValue: newTaxValue,
serviceChargeValue: newServiceChargeValue,
),
);
},
),
);
@ -61,91 +61,110 @@ class _TaxPageState extends State<TaxPage> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SettingsTitle('Perhitungan Biaya'),
const SizedBox(height: 24),
BlocBuilder<TaxSettingsBloc, TaxSettingsState>(
builder: (context, state) {
return state.when(
initial: () => const Center(child: CircularProgressIndicator()),
loading: () => const Center(child: CircularProgressIndicator()),
error: (message) => Center(child: Text('Error: $message')),
loaded: (taxModel, serviceChargeValue) {
final items = [
TaxModel(name: 'Biaya Layanan', type: TaxType.layanan, value: serviceChargeValue),
taxModel,
];
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SettingsTitle(
'Perhitungan Biaya',
subtitle: 'Biaya Layanan dan Pajak',
),
Expanded(
child: SingleChildScrollView(
child: BlocBuilder<TaxSettingsBloc, TaxSettingsState>(
builder: (context, state) {
return state.when(
initial: () =>
const Center(child: CircularProgressIndicator()),
loading: () =>
const Center(child: CircularProgressIndicator()),
error: (message) => Center(child: Text('Error: $message')),
loaded: (taxModel, serviceChargeValue) {
final items = [
TaxModel(
name: 'Biaya Layanan',
type: TaxType.layanan,
value: serviceChargeValue),
taxModel,
];
return CustomTabBar(
tabTitles: const ['Layanan', 'Pajak'],
initialTabIndex: 0,
tabViews: [
// LAYANAN TAB
SizedBox(
child: GridView.builder(
shrinkWrap: true,
itemCount: 2, // Add button + 1 service charge item
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
mainAxisSpacing: 30.0,
),
itemBuilder: (context, index) {
if (index == 0) {
return AddData(
title: 'Edit Perhitungan',
onPressed: () => onAddDataTap(serviceChargeValue, taxModel.value),
return CustomTabBar(
tabTitles: const ['Layanan', 'Pajak'],
initialTabIndex: 0,
tabViews: [
// LAYANAN TAB
SizedBox(
child: GridView.builder(
shrinkWrap: true,
itemCount: 2, // Add button + 1 service charge item
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
mainAxisSpacing: 30.0,
),
itemBuilder: (context, index) {
if (index == 0) {
return AddData(
title: 'Edit Perhitungan',
onPressed: () => onAddDataTap(
serviceChargeValue, taxModel.value),
);
}
final item = items.firstWhere(
(element) => element.type.isLayanan);
return ManageTaxCard(
data: item,
onEditTap: () => onEditTap(
item, serviceChargeValue, taxModel.value),
);
}
final item = items.firstWhere((element) => element.type.isLayanan);
return ManageTaxCard(
data: item,
onEditTap: () => onEditTap(item, serviceChargeValue, taxModel.value),
);
},
},
),
),
),
// PAJAK TAB
SizedBox(
child: GridView.builder(
shrinkWrap: true,
itemCount: 2, // Add button + 1 tax item
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
mainAxisSpacing: 30.0,
),
itemBuilder: (context, index) {
if (index == 0) {
return AddData(
title: 'Edit Perhitungan',
onPressed: () => onAddDataTap(serviceChargeValue, taxModel.value),
// PAJAK TAB
SizedBox(
child: GridView.builder(
shrinkWrap: true,
itemCount: 2, // Add button + 1 tax item
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
mainAxisSpacing: 30.0,
),
itemBuilder: (context, index) {
if (index == 0) {
return AddData(
title: 'Edit Perhitungan',
onPressed: () => onAddDataTap(
serviceChargeValue, taxModel.value),
);
}
final item = items.firstWhere(
(element) => element.type.isPajak);
return ManageTaxCard(
data: item,
onEditTap: () => onEditTap(
item, serviceChargeValue, taxModel.value),
);
}
final item = items.firstWhere((element) => element.type.isPajak);
return ManageTaxCard(
data: item,
onEditTap: () => onEditTap(item, serviceChargeValue, taxModel.value),
);
},
},
),
),
),
],
);
},
);
},
],
);
},
);
},
),
),
],
),
),
],
);
}
}

View File

@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
import '../../../core/components/spaces.dart';
import '../../../core/constants/colors.dart';
class AddData extends StatelessWidget {
final String title;
final VoidCallback onPressed;
@ -21,18 +19,25 @@ class AddData extends StatelessWidget {
onTap: onPressed,
child: Container(
padding: const EdgeInsets.all(16.0),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(width: 1, color: AppColors.card),
borderRadius: BorderRadius.circular(19),
),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.add,
color: AppColors.primary,
Container(
width: 56.0,
height: 56.0,
padding: const EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: AppColors.primary,
),
),
const SpaceHeight(8.0),
Text(

View File

@ -18,11 +18,9 @@ class ManageDiscountCard extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(width: 1, color: AppColors.card),
borderRadius: BorderRadius.circular(19),
),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: Stack(
children: [
@ -36,7 +34,7 @@ class ManageDiscountCard extends StatelessWidget {
margin: const EdgeInsets.only(top: 30.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.disabled.withOpacity(0.4),
color: AppColors.primary.withOpacity(0.1),
),
child: Text(
'${data.value!.replaceAll('.00', '')}%',
@ -48,23 +46,12 @@ class ManageDiscountCard extends StatelessWidget {
),
const Spacer(),
Center(
child: RichText(
text: TextSpan(
text: 'Nama Promo : ',
children: [
TextSpan(
text: data.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.black,
),
child: Text(
data.name ?? "-",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.black,
),
),
),

View File

@ -18,11 +18,9 @@ class ManageTaxCard extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(width: 1, color: AppColors.card),
borderRadius: BorderRadius.circular(19),
),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: Stack(
children: [
@ -36,7 +34,7 @@ class ManageTaxCard extends StatelessWidget {
margin: const EdgeInsets.only(top: 30.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.disabled.withOpacity(0.4),
color: AppColors.primary.withOpacity(0.1),
),
child: Text(
'${data.value}%',
@ -48,23 +46,12 @@ class ManageTaxCard extends StatelessWidget {
),
const Spacer(),
Center(
child: RichText(
text: TextSpan(
text: 'Nama Promo : ',
children: [
TextSpan(
text: data.type.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.black,
),
child: Text(
data.type.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.black,
),
),
),

View File

@ -27,12 +27,19 @@ class SettingsTitle extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: BoxDecoration(
color: AppColors.white,
border: Border(
bottom: BorderSide(
color: AppColors.background,
width: 1.0,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,

View File

@ -1,5 +1,5 @@
name: enaklo_pos
description: "EnakloPOS - Point of Sale Application"
description: "ApskelPOS - Point of Sale Application"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev