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

@ -5,7 +5,7 @@ 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();
@ -20,14 +20,14 @@ void main() async {
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

@ -28,7 +28,8 @@ class RevenueInvoice {
// 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,
@ -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,
@ -487,8 +487,8 @@ class PrintDataoutputs {
String namaKasir,
String customerName,
int paper,
{int taxPercentage = 11, int serviceChargePercentage = 5}
) async {
{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,28 +34,29 @@ 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(
children: [
// LEFT CONTENT
Expanded(
flex: 2,
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
backgroundColor: AppColors.background,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ReportTitle(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
ReportTitle(
actionWidget: [
SizedBox(
width: 300,
child: CustomDatePicker(
prefix: const Text('From: '),
initialDate: fromDate,
@ -67,7 +68,8 @@ class _ReportPageState extends State<ReportPage> {
),
),
const SpaceWidth(24.0),
Flexible(
SizedBox(
width: 300,
child: CustomDatePicker(
prefix: const Text('To: '),
initialDate: toDate,
@ -95,20 +97,34 @@ class _ReportPageState extends State<ReportPage> {
),
],
),
Padding(
padding: const EdgeInsets.all(15.0),
child: Wrap(
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: 'Transaction Report',
label: 'Laporan Transaksi',
subtitle:
'Menampilkan riwayat lengkap semua transaksi yang telah dilakukan.',
icon: Icons.receipt_long_outlined,
onPressed: () {
selectedMenu = 0;
title = 'Transaction Report';
title = 'Laporan Transaksi';
setState(() {});
//enddate is 1 month before the current date
context.read<TransactionReportBloc>().add(
TransactionReportEvent.getReport(
startDate: DateFormatter.formatDateTime(
startDate:
DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
@ -117,14 +133,18 @@ class _ReportPageState extends State<ReportPage> {
isActive: selectedMenu == 0,
),
ReportMenu(
label: 'Item Sales Report',
label: 'Laporan Penjualan Item',
subtitle:
'Laporan penjualan berdasarkan masing-masing item atau produk.',
icon: Icons.inventory_2_outlined,
onPressed: () {
selectedMenu = 1;
title = 'Item Sales Report';
title = 'Laporan Penjualan Item';
setState(() {});
context.read<ItemSalesReportBloc>().add(
ItemSalesReportEvent.getItemSales(
startDate: DateFormatter.formatDateTime(
startDate:
DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
@ -133,28 +153,36 @@ class _ReportPageState extends State<ReportPage> {
isActive: selectedMenu == 1,
),
ReportMenu(
label: 'Product Sales Chart',
label: 'Chart Penjualan Produk',
subtitle:
'Grafik visual penjualan produk untuk analisa performa penjualan.',
icon: Icons.bar_chart_outlined,
onPressed: () {
selectedMenu = 2;
title = 'Product Sales Chart';
title = 'Chart Penjualan Produk';
setState(() {});
context.read<ProductSalesBloc>().add(
ProductSalesEvent.getProductSales(
DateFormatter.formatDateTime(fromDate),
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
);
},
isActive: selectedMenu == 2,
),
ReportMenu(
label: 'Summary Sales Report',
label: 'Ringkasan Laporan Penjualan',
subtitle:
'Ringkasan total penjualan dalam periode tertentu.',
icon: Icons.insert_drive_file_outlined,
onPressed: () {
selectedMenu = 3;
title = 'Summary Sales Report';
title = 'Ringkasan Laporan Penjualan';
setState(() {});
context.read<SummaryBloc>().add(
SummaryEvent.getSummary(
DateFormatter.formatDateTime(fromDate),
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
);
@ -163,15 +191,23 @@ class _ReportPageState extends State<ReportPage> {
isActive: selectedMenu == 3,
),
ReportMenu(
label: 'Payment Method Report',
label: 'Laporan Metode Pembayaran',
subtitle:
'Laporan metode pembayaran yang digunakan.',
icon: Icons.payment_outlined,
onPressed: () {
selectedMenu = 4;
title = 'Payment Method Report';
title = 'Laporan Metode Pembayaran';
setState(() {});
context.read<PaymentMethodReportBloc>().add(
PaymentMethodReportEvent.getPaymentMethodReport(
startDate: DateFormatter.formatDateTime(fromDate),
endDate: DateFormatter.formatDateTime(toDate)),
PaymentMethodReportEvent
.getPaymentMethodReport(
startDate:
DateFormatter.formatDateTime(
fromDate),
endDate:
DateFormatter.formatDateTime(
toDate)),
);
},
isActive: selectedMenu == 4,
@ -179,17 +215,16 @@ class _ReportPageState extends State<ReportPage> {
],
),
),
],
),
),
),
),
// RIGHT CONTENT
Expanded(
flex: 2,
flex: 4,
child: selectedMenu == 0
? BlocBuilder<TransactionReportBloc, TransactionReportState>(
? BlocBuilder<TransactionReportBloc,
TransactionReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
@ -210,7 +245,8 @@ class _ReportPageState extends State<ReportPage> {
},
)
: selectedMenu == 1
? BlocBuilder<ItemSalesReportBloc, ItemSalesReportState>(
? BlocBuilder<ItemSalesReportBloc,
ItemSalesReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
@ -223,15 +259,18 @@ class _ReportPageState extends State<ReportPage> {
return ItemSalesReportWidget(
itemSales: itemSales,
title: title,
searchDateFormatted: searchDateFormatted,
headerWidgets: _getItemSalesPageWidget(),
searchDateFormatted:
searchDateFormatted,
headerWidgets:
_getItemSalesPageWidget(),
);
},
);
},
)
: selectedMenu == 2
? BlocBuilder<ProductSalesBloc, ProductSalesState>(
? BlocBuilder<ProductSalesBloc,
ProductSalesState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
@ -243,7 +282,8 @@ class _ReportPageState extends State<ReportPage> {
success: (productSales) {
return ProductSalesChartWidgets(
title: title,
searchDateFormatted: searchDateFormatted,
searchDateFormatted:
searchDateFormatted,
productSales: productSales,
);
},
@ -255,7 +295,8 @@ class _ReportPageState extends State<ReportPage> {
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
child:
CircularProgressIndicator(),
),
error: (message) {
return Text(message);
@ -264,28 +305,34 @@ class _ReportPageState extends State<ReportPage> {
return SummaryReportWidget(
summary: summary,
title: title,
searchDateFormatted: searchDateFormatted,
searchDateFormatted:
searchDateFormatted,
);
},
);
},
)
: selectedMenu == 4
? BlocBuilder<PaymentMethodReportBloc, PaymentMethodReportState>(
? BlocBuilder<PaymentMethodReportBloc,
PaymentMethodReportState>(
builder: (context, state) {
return state.maybeWhen(
orElse: () => const Center(
child: CircularProgressIndicator(),
child:
CircularProgressIndicator(),
),
error: (message) {
return Text(message);
},
loaded: (paymentMethodData) {
return PaymentMethodReportWidget(
paymentMethodData: paymentMethodData,
paymentMethodData:
paymentMethodData,
title: title,
searchDateFormatted: searchDateFormatted,
headerWidgets: _getPaymentMethodPageWidget(),
searchDateFormatted:
searchDateFormatted,
headerWidgets:
_getPaymentMethodPageWidget(),
);
},
);
@ -294,6 +341,9 @@ class _ReportPageState extends State<ReportPage> {
: 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,32 +26,12 @@ class ItemSalesReportWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: const Color.fromARGB(255, 255, 255, 255),
child: Column(
return 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),
),
GestureDetector(
onTap: () async {
ReportPageTitle(
title: title,
searchDateFormatted: searchDateFormatted,
onExport: () async {
try {
final status = await PermessionHelper().checkPermission();
if (status) {
@ -77,25 +57,6 @@ class ItemSalesReportWidget extends StatelessWidget {
);
}
},
child: const Row(
children: [
Text(
"PDF",
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
Icon(
Icons.download_outlined,
color: AppColors.primary,
)
],
),
),
],
),
),
const SpaceHeight(16.0),
Expanded(
@ -105,7 +66,7 @@ class ItemSalesReportWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(12),
child: HorizontalDataTable(
leftHandSideColumnWidth: 80,
rightHandSideColumnWidth: 560,
rightHandSideColumnWidth: 670,
isFixedHeader: true,
headerWidgets: headerWidgets,
@ -116,37 +77,35 @@ class ItemSalesReportWidget extends StatelessWidget {
width: 80,
height: 52,
alignment: Alignment.centerLeft,
child:
Center(child: Text(itemSales[index].id.toString())),
child: Center(child: Text(itemSales[index].id.toString())),
);
},
rightSideItemBuilder: (context, index) {
return Row(
children: <Widget>[
Container(
width: 60,
width: 100,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(itemSales[index].orderId.toString())),
),
Container(
width: 160,
width: 200,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child: Text(itemSales[index].productName!)),
child:
Center(child: Text(itemSales[index].productName!)),
),
Container(
width: 60,
height: 52,
alignment: Alignment.centerLeft,
child: Center(
child:
Text(itemSales[index].quantity.toString())),
child: Text(itemSales[index].quantity.toString())),
),
Container(
width: 140,
width: 150,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
@ -156,14 +115,13 @@ class ItemSalesReportWidget extends StatelessWidget {
)),
),
Container(
width: 140,
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!)
(itemSales[index].price! * itemSales[index].quantity!)
.currencyFormatRp,
)),
),
@ -185,7 +143,6 @@ class ItemSalesReportWidget extends StatelessWidget {
),
),
],
),
);
}
}

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';

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,27 +68,26 @@ 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(
return 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),
),
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),
@ -116,10 +116,13 @@ class _ProductSalesChartWidgetsState extends State<ProductSalesChartWidgets> {
),
// 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),
const SpaceWidth(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: isActive ? AppColors.primary : AppColors.black,
fontWeight: FontWeight.w600,
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(
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,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Report',
'Laporan',
style: TextStyle(
color: AppColors.primary,
fontSize: 28,
color: AppColors.black,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const SpaceHeight(4.0),
Text(
DateTime.now().toFormattedDate(),
style: const TextStyle(
color: AppColors.subtitle,
fontSize: 16,
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,7 +54,8 @@ 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) {
@ -75,7 +75,8 @@ class SummaryReportWidget extends StatelessWidget {
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,32 +27,12 @@ class TransactionReportWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
color: const Color.fromARGB(255, 255, 255, 255),
child: Column(
return 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),
),
GestureDetector(
onTap: () async {
ReportPageTitle(
title: title,
searchDateFormatted: searchDateFormatted,
onExport: () async {
try {
final status = await PermessionHelper().checkPermission();
if (status) {
@ -78,25 +58,6 @@ class TransactionReportWidget extends StatelessWidget {
);
}
},
child: const Row(
children: [
Text(
"PDF",
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
Icon(
Icons.download_outlined,
color: AppColors.primary,
)
],
),
),
],
),
),
const SpaceHeight(16.0),
Expanded(
@ -228,7 +189,6 @@ class TransactionReportWidget extends StatelessWidget {
),
),
],
),
);
}
}

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();
@ -78,6 +76,392 @@ class _FormProductDialogState extends State<FormProductDialog> {
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!;
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(
@ -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,
@ -307,7 +693,8 @@ class _FormProductDialogState extends State<FormProductDialog> {
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,
@ -319,7 +706,8 @@ class _FormProductDialogState extends State<FormProductDialog> {
);
context.read<UpdateProductBloc>().add(
UpdateProductEvent.updateProduct(product, imageFile));
UpdateProductEvent.updateProduct(
product, imageFile));
},
label: 'Update Product',
);
@ -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,7 +71,8 @@ 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);

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,12 +50,24 @@ class _DiscountPageState extends State<DiscountPage> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
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: [
const SettingsTitle('Kelola Diskon'),
const SizedBox(height: 24),
CustomTabBar(
tabTitles: const ['Semua'],
initialTabIndex: 0,
@ -70,8 +83,10 @@ class _DiscountPageState extends State<DiscountPage> {
}, loaded: (discounts) {
return GridView.builder(
shrinkWrap: true,
itemCount: discounts.length + 1,
itemCount: discounts.length,
physics: const NeverScrollableScrollPhysics(),
padding:
const EdgeInsets.symmetric(horizontal: 16.0),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
@ -80,16 +95,10 @@ class _DiscountPageState extends State<DiscountPage> {
mainAxisSpacing: 30.0,
),
itemBuilder: (context, index) {
if (index == 0) {
return AddData(
title: 'Tambah Diskon Baru',
onPressed: onAddDataTap,
);
}
final item = discounts[index - 1];
final item = discounts[index];
return ManageDiscountCard(
data: item,
onEditTap: (){},
onEditTap: () {},
);
},
);
@ -126,6 +135,9 @@ class _DiscountPageState extends State<DiscountPage> {
),
],
),
),
),
],
);
}
}

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,25 +39,48 @@ 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(16.0),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(20.0),
child: TextField(
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(),
labelText: 'Server Key',
hintText: 'Server Key',
),
),
],
),
//button untuk save server key
ElevatedButton(
),
const SpaceHeight(20),
Align(
alignment: Alignment.centerRight,
child: Button.filled(
width: context.deviceWidth * 0.2,
height: 44,
onPressed: () {
AuthLocalDataSource()
.saveMidtransServerKey(serverKeyController!.text);
@ -66,7 +92,11 @@ class _ServerKeyPageState extends State<ServerKeyPage> {
),
);
},
child: const Text('Save'),
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,11 +19,32 @@ 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: [
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(
@ -34,8 +58,10 @@ class _SyncDataPageState extends State<SyncDataPage> {
);
},
loaded: (productResponseModel) async {
await ProductLocalDatasource.instance.deleteAllProducts();
await ProductLocalDatasource.instance.insertProducts(
await ProductLocalDatasource.instance
.deleteAllProducts();
await ProductLocalDatasource.instance
.insertProducts(
productResponseModel.data!,
);
ScaffoldMessenger.of(context).showSnackBar(
@ -50,13 +76,15 @@ class _SyncDataPageState extends State<SyncDataPage> {
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return ElevatedButton(
return Button.filled(
width: 100,
height: 40,
onPressed: () {
context
.read<SyncProductBloc>()
.add(const SyncProductEvent.syncProduct());
context.read<SyncProductBloc>().add(
const SyncProductEvent.syncProduct());
},
child: const Text('Sync Product'));
label: 'Sinkronasikan',
);
},
loading: () {
return const Center(
@ -66,6 +94,26 @@ class _SyncDataPageState extends State<SyncDataPage> {
);
},
),
],
),
),
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(
@ -91,7 +139,9 @@ class _SyncDataPageState extends State<SyncDataPage> {
builder: (context, state) {
return state.maybeWhen(
orElse: () {
return ElevatedButton(
return Button.filled(
width: 100,
height: 40,
onPressed: () {
log("🔘 Sync Order button pressed");
log("🔘 SyncOrderBloc instance: ${context.read<SyncOrderBloc>()}");
@ -100,7 +150,7 @@ class _SyncDataPageState extends State<SyncDataPage> {
.add(const SyncOrderEvent.syncOrder());
log("🔘 SyncOrderEvent.syncOrder dispatched");
},
child: const Text('Sync Order'),
label: 'Sinkronasikan',
);
},
loading: () {
@ -113,6 +163,12 @@ class _SyncDataPageState extends State<SyncDataPage> {
)
],
),
),
],
),
),
],
),
);
}
}

View File

@ -61,21 +61,29 @@ class _TaxPageState extends State<TaxPage> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SettingsTitle('Perhitungan Biaya'),
const SizedBox(height: 24),
BlocBuilder<TaxSettingsBloc, TaxSettingsState>(
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()),
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(
name: 'Biaya Layanan',
type: TaxType.layanan,
value: serviceChargeValue),
taxModel,
];
@ -89,7 +97,9 @@ class _TaxPageState extends State<TaxPage> {
shrinkWrap: true,
itemCount: 2, // Add button + 1 service charge item
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
padding: const EdgeInsets.symmetric(horizontal: 16),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
@ -99,13 +109,16 @@ class _TaxPageState extends State<TaxPage> {
if (index == 0) {
return AddData(
title: 'Edit Perhitungan',
onPressed: () => onAddDataTap(serviceChargeValue, taxModel.value),
onPressed: () => onAddDataTap(
serviceChargeValue, taxModel.value),
);
}
final item = items.firstWhere((element) => element.type.isLayanan);
final item = items.firstWhere(
(element) => element.type.isLayanan);
return ManageTaxCard(
data: item,
onEditTap: () => onEditTap(item, serviceChargeValue, taxModel.value),
onEditTap: () => onEditTap(
item, serviceChargeValue, taxModel.value),
);
},
),
@ -117,7 +130,9 @@ class _TaxPageState extends State<TaxPage> {
shrinkWrap: true,
itemCount: 2, // Add button + 1 tax item
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
padding: const EdgeInsets.symmetric(horizontal: 16),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.85,
crossAxisCount: 3,
crossAxisSpacing: 30.0,
@ -127,13 +142,16 @@ class _TaxPageState extends State<TaxPage> {
if (index == 0) {
return AddData(
title: 'Edit Perhitungan',
onPressed: () => onAddDataTap(serviceChargeValue, taxModel.value),
onPressed: () => onAddDataTap(
serviceChargeValue, taxModel.value),
);
}
final item = items.firstWhere((element) => element.type.isPajak);
final item = items.firstWhere(
(element) => element.type.isPajak);
return ManageTaxCard(
data: item,
onEditTap: () => onEditTap(item, serviceChargeValue, taxModel.value),
onEditTap: () => onEditTap(
item, serviceChargeValue, taxModel.value),
);
},
),
@ -144,8 +162,9 @@ class _TaxPageState extends State<TaxPage> {
);
},
),
],
),
),
],
);
}
}

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,19 +19,26 @@ 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(
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(
title,

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,26 +46,15 @@ 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,
),
),
],
child: Text(
data.name ?? "-",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
fontWeight: FontWeight.w600,
color: AppColors.black,
),
),
),
),
const Spacer(),
],
),

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,26 +46,15 @@ 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,
),
),
],
child: Text(
data.type.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
fontWeight: FontWeight.w600,
color: AppColors.black,
),
),
),
),
const Spacer(),
],
),

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