first commit

This commit is contained in:
Aditya Siregar
2025-07-30 22:38:44 +07:00
commit 73320561b0
444 changed files with 64633 additions and 0 deletions
+489
View File
@@ -0,0 +1,489 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart' as _svg;
import 'package:vector_graphics/vector_graphics.dart' as _vg;
class $AssetsIconsGen {
const $AssetsIconsGen();
/// File path: assets/icons/all_categories.svg
SvgGenImage get allCategories =>
const SvgGenImage('assets/icons/all_categories.svg');
/// File path: assets/icons/calendar.svg
SvgGenImage get calendar => const SvgGenImage('assets/icons/calendar.svg');
/// File path: assets/icons/cash.svg
SvgGenImage get cash => const SvgGenImage('assets/icons/cash.svg');
/// File path: assets/icons/dashboard.svg
SvgGenImage get dashboard => const SvgGenImage('assets/icons/dashboard.svg');
/// File path: assets/icons/debit.svg
SvgGenImage get debit => const SvgGenImage('assets/icons/debit.svg');
/// File path: assets/icons/delete.svg
SvgGenImage get delete => const SvgGenImage('assets/icons/delete.svg');
/// File path: assets/icons/discount.svg
SvgGenImage get discount => const SvgGenImage('assets/icons/discount.svg');
/// File path: assets/icons/diskon.svg
SvgGenImage get diskon => const SvgGenImage('assets/icons/diskon.svg');
/// File path: assets/icons/done.svg
SvgGenImage get done => const SvgGenImage('assets/icons/done.svg');
/// File path: assets/icons/drink.svg
SvgGenImage get drink => const SvgGenImage('assets/icons/drink.svg');
/// File path: assets/icons/edit.svg
SvgGenImage get edit => const SvgGenImage('assets/icons/edit.svg');
/// File path: assets/icons/food.svg
SvgGenImage get food => const SvgGenImage('assets/icons/food.svg');
/// File path: assets/icons/history.svg
SvgGenImage get history => const SvgGenImage('assets/icons/history.svg');
/// File path: assets/icons/home-resto.svg
SvgGenImage get homeResto => const SvgGenImage('assets/icons/home-resto.svg');
/// File path: assets/icons/home.svg
SvgGenImage get home => const SvgGenImage('assets/icons/home.svg');
/// File path: assets/icons/image.svg
SvgGenImage get image => const SvgGenImage('assets/icons/image.svg');
/// File path: assets/icons/kelola-diskon.svg
SvgGenImage get kelolaDiskon =>
const SvgGenImage('assets/icons/kelola-diskon.svg');
/// File path: assets/icons/kelola-pajak.svg
SvgGenImage get kelolaPajak =>
const SvgGenImage('assets/icons/kelola-pajak.svg');
/// File path: assets/icons/kelola-printer.svg
SvgGenImage get kelolaPrinter =>
const SvgGenImage('assets/icons/kelola-printer.svg');
/// File path: assets/icons/kelola-produk.svg
SvgGenImage get kelolaProduk =>
const SvgGenImage('assets/icons/kelola-produk.svg');
/// File path: assets/icons/layanan.svg
SvgGenImage get layanan => const SvgGenImage('assets/icons/layanan.svg');
/// File path: assets/icons/logout.svg
SvgGenImage get logout => const SvgGenImage('assets/icons/logout.svg');
/// File path: assets/icons/no-product.svg
SvgGenImage get noProduct => const SvgGenImage('assets/icons/no-product.svg');
/// File path: assets/icons/ongkir.svg
SvgGenImage get ongkir => const SvgGenImage('assets/icons/ongkir.svg');
/// File path: assets/icons/orders.svg
SvgGenImage get orders => const SvgGenImage('assets/icons/orders.svg');
/// File path: assets/icons/pajak.svg
SvgGenImage get pajak => const SvgGenImage('assets/icons/pajak.svg');
/// File path: assets/icons/payments.svg
SvgGenImage get payments => const SvgGenImage('assets/icons/payments.svg');
/// File path: assets/icons/print.svg
SvgGenImage get print => const SvgGenImage('assets/icons/print.svg');
/// File path: assets/icons/qr_code.svg
SvgGenImage get qrCode => const SvgGenImage('assets/icons/qr_code.svg');
/// File path: assets/icons/report.svg
SvgGenImage get report => const SvgGenImage('assets/icons/report.svg');
/// File path: assets/icons/setting.svg
SvgGenImage get setting => const SvgGenImage('assets/icons/setting.svg');
/// File path: assets/icons/shopping-basket.svg
SvgGenImage get shoppingBasket =>
const SvgGenImage('assets/icons/shopping-basket.svg');
/// File path: assets/icons/snack.svg
SvgGenImage get snack => const SvgGenImage('assets/icons/snack.svg');
/// File path: assets/icons/success.svg
SvgGenImage get success => const SvgGenImage('assets/icons/success.svg');
/// List of all assets
List<SvgGenImage> get values => [
allCategories,
calendar,
cash,
dashboard,
debit,
delete,
discount,
diskon,
done,
drink,
edit,
food,
history,
homeResto,
home,
image,
kelolaDiskon,
kelolaPajak,
kelolaPrinter,
kelolaProduk,
layanan,
logout,
noProduct,
ongkir,
orders,
pajak,
payments,
print,
qrCode,
report,
setting,
shoppingBasket,
snack,
success
];
}
class $AssetsImagesGen {
const $AssetsImagesGen();
/// File path: assets/images/drink1.png
AssetGenImage get drink1 => const AssetGenImage('assets/images/drink1.png');
/// File path: assets/images/drink2.png
AssetGenImage get drink2 => const AssetGenImage('assets/images/drink2.png');
/// File path: assets/images/drink3.png
AssetGenImage get drink3 => const AssetGenImage('assets/images/drink3.png');
/// File path: assets/images/drink4.png
AssetGenImage get drink4 => const AssetGenImage('assets/images/drink4.png');
/// File path: assets/images/drink5.png
AssetGenImage get drink5 => const AssetGenImage('assets/images/drink5.png');
/// File path: assets/images/drink6.png
AssetGenImage get drink6 => const AssetGenImage('assets/images/drink6.png');
/// File path: assets/images/drink7.png
AssetGenImage get drink7 => const AssetGenImage('assets/images/drink7.png');
/// File path: assets/images/logo.png
AssetGenImage get logo => const AssetGenImage('assets/images/logo.png');
/// File path: assets/images/manage_printer.png
AssetGenImage get managePrinter =>
const AssetGenImage('assets/images/manage_printer.png');
/// File path: assets/images/manage_product.png
AssetGenImage get manageProduct =>
const AssetGenImage('assets/images/manage_product.png');
/// File path: assets/images/manage_qr.png
AssetGenImage get manageQr =>
const AssetGenImage('assets/images/manage_qr.png');
/// File path: assets/images/menu1.png
AssetGenImage get menu1 => const AssetGenImage('assets/images/menu1.png');
/// File path: assets/images/menu10.png
AssetGenImage get menu10 => const AssetGenImage('assets/images/menu10.png');
/// File path: assets/images/menu11.png
AssetGenImage get menu11 => const AssetGenImage('assets/images/menu11.png');
/// File path: assets/images/menu12.png
AssetGenImage get menu12 => const AssetGenImage('assets/images/menu12.png');
/// File path: assets/images/menu13.png
AssetGenImage get menu13 => const AssetGenImage('assets/images/menu13.png');
/// File path: assets/images/menu14.png
AssetGenImage get menu14 => const AssetGenImage('assets/images/menu14.png');
/// File path: assets/images/menu2.png
AssetGenImage get menu2 => const AssetGenImage('assets/images/menu2.png');
/// File path: assets/images/menu3.png
AssetGenImage get menu3 => const AssetGenImage('assets/images/menu3.png');
/// File path: assets/images/menu4.png
AssetGenImage get menu4 => const AssetGenImage('assets/images/menu4.png');
/// File path: assets/images/menu5.png
AssetGenImage get menu5 => const AssetGenImage('assets/images/menu5.png');
/// File path: assets/images/menu6.png
AssetGenImage get menu6 => const AssetGenImage('assets/images/menu6.png');
/// File path: assets/images/menu7.png
AssetGenImage get menu7 => const AssetGenImage('assets/images/menu7.png');
/// File path: assets/images/menu8.png
AssetGenImage get menu8 => const AssetGenImage('assets/images/menu8.png');
/// File path: assets/images/product1.jpg
AssetGenImage get product1 =>
const AssetGenImage('assets/images/product1.jpg');
/// File path: assets/images/product2.jpg
AssetGenImage get product2 =>
const AssetGenImage('assets/images/product2.jpg');
/// File path: assets/images/product3.jpg
AssetGenImage get product3 =>
const AssetGenImage('assets/images/product3.jpg');
/// File path: assets/images/product4.jpg
AssetGenImage get product4 =>
const AssetGenImage('assets/images/product4.jpg');
/// List of all assets
List<AssetGenImage> get values => [
drink1,
drink2,
drink3,
drink4,
drink5,
drink6,
drink7,
logo,
managePrinter,
manageProduct,
manageQr,
menu1,
menu10,
menu11,
menu12,
menu13,
menu14,
menu2,
menu3,
menu4,
menu5,
menu6,
menu7,
menu8,
product1,
product2,
product3,
product4
];
}
class $AssetsLogoGen {
const $AssetsLogoGen();
/// File path: assets/logo/logo.png
AssetGenImage get logo => const AssetGenImage('assets/logo/logo.png');
/// File path: assets/logo/logo2.png
AssetGenImage get logo2 => const AssetGenImage('assets/logo/logo2.png');
/// File path: assets/logo/logo3.png
AssetGenImage get logo3 => const AssetGenImage('assets/logo/logo3.png');
/// File path: assets/logo/logo4.png
AssetGenImage get logo4 => const AssetGenImage('assets/logo/logo4.png');
/// File path: assets/logo/logo5.png
AssetGenImage get logo5 => const AssetGenImage('assets/logo/logo5.png');
/// File path: assets/logo/logo6.png
AssetGenImage get logo6 => const AssetGenImage('assets/logo/logo6.png');
/// File path: assets/logo/logo_app_icon.png
AssetGenImage get logoAppIcon =>
const AssetGenImage('assets/logo/logo_app_icon.png');
/// File path: assets/logo/mylogo.png
AssetGenImage get mylogo => const AssetGenImage('assets/logo/mylogo.png');
/// List of all assets
List<AssetGenImage> get values =>
[logo, logo2, logo3, logo4, logo5, logo6, logoAppIcon, mylogo];
}
class Assets {
Assets._();
static const $AssetsIconsGen icons = $AssetsIconsGen();
static const $AssetsImagesGen images = $AssetsImagesGen();
static const $AssetsLogoGen logo = $AssetsLogoGen();
}
class AssetGenImage {
const AssetGenImage(
this._assetName, {
this.size,
this.flavors = const {},
});
final String _assetName;
final Size? size;
final Set<String> flavors;
Image image({
Key? key,
AssetBundle? bundle,
ImageFrameBuilder? frameBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? scale,
double? width,
double? height,
Color? color,
Animation<double>? opacity,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = true,
bool isAntiAlias = false,
String? package,
FilterQuality filterQuality = FilterQuality.low,
int? cacheWidth,
int? cacheHeight,
}) {
return Image.asset(
_assetName,
key: key,
bundle: bundle,
frameBuilder: frameBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
scale: scale,
width: width,
height: height,
color: color,
opacity: opacity,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
package: package,
filterQuality: filterQuality,
cacheWidth: cacheWidth,
cacheHeight: cacheHeight,
);
}
ImageProvider provider({
AssetBundle? bundle,
String? package,
}) {
return AssetImage(
_assetName,
bundle: bundle,
package: package,
);
}
String get path => _assetName;
String get keyName => _assetName;
}
class SvgGenImage {
const SvgGenImage(
this._assetName, {
this.size,
this.flavors = const {},
}) : _isVecFormat = false;
const SvgGenImage.vec(
this._assetName, {
this.size,
this.flavors = const {},
}) : _isVecFormat = true;
final String _assetName;
final Size? size;
final Set<String> flavors;
final bool _isVecFormat;
_svg.SvgPicture svg({
Key? key,
bool matchTextDirection = false,
AssetBundle? bundle,
String? package,
double? width,
double? height,
BoxFit fit = BoxFit.contain,
AlignmentGeometry alignment = Alignment.center,
bool allowDrawingOutsideViewBox = false,
WidgetBuilder? placeholderBuilder,
String? semanticsLabel,
bool excludeFromSemantics = false,
_svg.SvgTheme? theme,
ColorFilter? colorFilter,
Clip clipBehavior = Clip.hardEdge,
@deprecated Color? color,
@deprecated BlendMode colorBlendMode = BlendMode.srcIn,
@deprecated bool cacheColorFilter = false,
}) {
final _svg.BytesLoader loader;
if (_isVecFormat) {
loader = _vg.AssetBytesLoader(
_assetName,
assetBundle: bundle,
packageName: package,
);
} else {
loader = _svg.SvgAssetLoader(
_assetName,
assetBundle: bundle,
packageName: package,
theme: theme,
);
}
return _svg.SvgPicture(
loader,
key: key,
matchTextDirection: matchTextDirection,
width: width,
height: height,
fit: fit,
alignment: alignment,
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox,
placeholderBuilder: placeholderBuilder,
semanticsLabel: semanticsLabel,
excludeFromSemantics: excludeFromSemantics,
colorFilter: colorFilter ??
(color == null ? null : ColorFilter.mode(color, colorBlendMode)),
clipBehavior: clipBehavior,
cacheColorFilter: cacheColorFilter,
);
}
String get path => _assetName;
String get keyName => _assetName;
}
+123
View File
@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import '../constants/colors.dart';
enum ButtonStyle { filled, outlined }
class Button extends StatelessWidget {
const Button.filled({
super.key,
required this.onPressed,
required this.label,
this.style = ButtonStyle.filled,
this.color = AppColors.primary,
this.textColor = Colors.white,
this.width,
this.height = 50.0,
this.borderRadius = 16.0,
this.icon,
this.disabled = false,
this.fontSize = 16.0,
});
const Button.outlined({
super.key,
required this.onPressed,
required this.label,
this.style = ButtonStyle.outlined,
this.color = Colors.transparent,
this.textColor = AppColors.primary,
this.width,
this.height = 50.0,
this.borderRadius = 16.0,
this.icon,
this.disabled = false,
this.fontSize = 16.0,
});
final Function() onPressed;
final String label;
final ButtonStyle style;
final Color color;
final Color textColor;
final double? width;
final double height;
final double borderRadius;
final Widget? icon;
final bool disabled;
final double fontSize;
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
width: width,
child: style == ButtonStyle.filled
? ElevatedButton(
onPressed: disabled ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: const EdgeInsets.symmetric(horizontal: 16.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
icon ?? const SizedBox.shrink(),
if (icon != null) const SizedBox(width: 10.0),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
label,
style: TextStyle(
color: disabled ? Colors.grey : textColor,
fontSize: fontSize,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
),
],
),
)
: OutlinedButton(
onPressed: disabled ? null : onPressed,
style: OutlinedButton.styleFrom(
backgroundColor: color,
side: const BorderSide(color: Colors.grey),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
padding: const EdgeInsets.symmetric(horizontal: 16.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
icon ?? const SizedBox.shrink(),
if (icon != null) const SizedBox(width: 10.0),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
label,
style: TextStyle(
color: disabled ? Colors.grey : textColor,
fontSize: fontSize,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
),
),
],
),
),
);
}
}
+9
View File
@@ -0,0 +1,9 @@
export 'buttons.dart';
export 'custom_dropdown.dart';
export 'custom_screen_manager.dart';
export 'custom_text_field.dart';
export 'image_picker_widget.dart';
export 'menu_button.dart';
export 'search_input.dart';
export 'spaces.dart';
export 'tab_custom.dart';
@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/assets/assets.gen.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
class CustomDatePicker extends StatefulWidget {
final void Function(DateTime selectedDate)? onDateSelected;
final DateTime initialDate;
final Widget? prefix;
const CustomDatePicker({
super.key,
required this.initialDate,
this.onDateSelected,
this.prefix,
});
@override
State<CustomDatePicker> createState() => _CustomDatePickerState();
}
class _CustomDatePickerState extends State<CustomDatePicker> {
late DateTime selectedDate;
@override
void initState() {
selectedDate = widget.initialDate;
super.initState();
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null && picked != selectedDate) {
setState(() {
selectedDate = picked;
});
if (widget.onDateSelected != null) {
widget.onDateSelected!(picked);
}
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 500,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: GestureDetector(
onTap: () => _selectDate(context),
child: AbsorbPointer(
child: TextFormField(
style: const TextStyle(
color: AppColors.black,
fontWeight: FontWeight.bold,
),
readOnly: true,
controller: TextEditingController(
text: selectedDate.toFormattedDate2(),
),
decoration: InputDecoration(
suffixIcon: Padding(
padding: const EdgeInsets.all(16.0),
child: Assets.icons.calendar.svg(),
),
prefix: widget.prefix,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
borderSide: const BorderSide(color: AppColors.stroke),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
borderSide: const BorderSide(color: AppColors.stroke),
),
),
),
),
),
),
],
),
),
],
);
}
}
+55
View File
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'spaces.dart';
class CustomDropdown extends StatelessWidget {
final String? value;
final List<String> items;
final String label;
final Function(String? value)? onChanged;
const CustomDropdown({
super.key,
required this.value,
required this.items,
required this.label,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SpaceHeight(12.0),
DropdownButtonFormField<String>(
value: value,
onChanged: onChanged,
items: items.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0),
borderSide: const BorderSide(color: Colors.grey),
),
),
),
],
);
}
}
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class CustomScreenManager extends StatelessWidget {
final Widget mobile;
final Widget desktop;
const CustomScreenManager({
super.key,
required this.mobile,
required this.desktop,
});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
if (screenWidth < 1200) {
return mobile;
} else {
return desktop;
}
}
}
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'spaces.dart';
class CustomTextField extends StatelessWidget {
final TextEditingController controller;
final String label;
final Function(String value)? onChanged;
final bool obscureText;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final TextCapitalization? textCapitalization;
final bool showLabel;
final Widget? prefixIcon;
final Widget? suffixIcon;
final bool readOnly;
const CustomTextField({
super.key,
required this.controller,
required this.label,
this.onChanged,
this.obscureText = false,
this.keyboardType,
this.textInputAction,
this.textCapitalization,
this.showLabel = true,
this.prefixIcon,
this.suffixIcon,
this.readOnly = false,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLabel) ...[
Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SpaceHeight(12.0),
],
TextFormField(
controller: controller,
onChanged: onChanged,
obscureText: obscureText,
keyboardType: keyboardType,
textInputAction: textInputAction,
textCapitalization: textCapitalization ?? TextCapitalization.none,
readOnly: readOnly,
decoration: InputDecoration(
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16.0),
borderSide: const BorderSide(color: Colors.grey),
),
hintText: label,
),
),
],
);
}
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class DashedLine extends StatelessWidget {
const DashedLine({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: List.generate(
400 ~/ 10,
(index) => Expanded(
child: Container(
color: index % 2 == 1 ? Colors.transparent : Colors.grey,
height: 1,
),
),
),
),
);
}
}
@@ -0,0 +1,130 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../assets/assets.gen.dart';
import '../constants/colors.dart';
import '../constants/variables.dart';
import 'buttons.dart';
import 'spaces.dart';
class ImagePickerWidget extends StatefulWidget {
final String label;
final void Function(XFile? file) onChanged;
final bool showLabel;
final String? initialImageUrl;
const ImagePickerWidget({
super.key,
required this.label,
required this.onChanged,
this.showLabel = true,
this.initialImageUrl,
});
@override
State<ImagePickerWidget> createState() => _ImagePickerWidgetState();
}
class _ImagePickerWidgetState extends State<ImagePickerWidget> {
String? imagePath;
bool hasInitialImage = false;
@override
void initState() {
super.initState();
hasInitialImage = widget.initialImageUrl != null;
}
Future<void> _pickImage() async {
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.gallery,
);
setState(() {
if (pickedFile != null) {
imagePath = pickedFile.path;
hasInitialImage = false; // Clear initial image when new image is picked
widget.onChanged(pickedFile);
} else {
debugPrint('No image selected.');
widget.onChanged(null);
}
});
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showLabel) ...[
Text(
widget.label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
const SpaceHeight(12.0),
],
Container(
padding: const EdgeInsets.all(6.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(color: AppColors.primary),
),
child: Row(
children: [
SizedBox(
width: 80.0,
height: 80.0,
child: ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: imagePath != null
? Image.file(
File(imagePath!),
fit: BoxFit.cover,
)
: hasInitialImage && widget.initialImageUrl != null
? CachedNetworkImage(
imageUrl: widget.initialImageUrl!.contains('http')
? widget.initialImageUrl!
: '${Variables.baseUrl}/${widget.initialImageUrl}',
placeholder: (context, url) =>
const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) => Container(
padding: const EdgeInsets.all(16.0),
color: AppColors.black.withOpacity(0.05),
child: Assets.icons.image.svg(),
),
fit: BoxFit.cover,
)
: Container(
padding: const EdgeInsets.all(16.0),
color: AppColors.black.withOpacity(0.05),
child: Assets.icons.image.svg(),
),
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Button.filled(
height: 30.0,
width: 140.0,
onPressed: _pickImage,
label: 'Choose Photo',
fontSize: 12.0,
borderRadius: 5.0,
),
),
],
),
),
],
);
}
}
+76
View File
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../constants/colors.dart';
import 'components.dart';
class MenuButton extends StatelessWidget {
final String iconPath;
final String label;
final bool isActive;
final VoidCallback onPressed;
final bool isImage;
final double size;
const MenuButton({
super.key,
required this.iconPath,
required this.label,
this.isActive = false,
required this.onPressed,
this.isImage = false,
this.size = 90,
});
@override
Widget build(BuildContext context) {
return Flexible(
child: InkWell(
onTap: onPressed,
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: Container(
width: context.deviceWidth,
padding: const EdgeInsets.all(15.0),
decoration: BoxDecoration(
color: isActive ? AppColors.primary : AppColors.white,
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
boxShadow: [
BoxShadow(
offset: const Offset(0, 4),
blurRadius: 20.0,
blurStyle: BlurStyle.outer,
spreadRadius: 0,
color: AppColors.black.withOpacity(0.1),
),
],
),
child: Column(
children: [
isImage
? Image.asset(iconPath,
width: size, height: size, fit: BoxFit.contain)
: SvgPicture.asset(
iconPath,
colorFilter: ColorFilter.mode(
isActive ? AppColors.white : AppColors.primary,
BlendMode.srcIn,
),
),
const SpaceHeight(10.0),
Text(
label,
style: TextStyle(
color: isActive ? AppColors.white : AppColors.primary,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
);
}
}
+52
View File
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import '../constants/colors.dart';
class SearchInput extends StatelessWidget {
final TextEditingController controller;
final Function(String value)? onChanged;
final VoidCallback? onTap;
final String hintText;
const SearchInput({
super.key,
required this.controller,
this.onChanged,
this.onTap,
this.hintText = 'Cari di sini',
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(8.0),
),
child: TextFormField(
onTap: onTap,
readOnly: onTap != null,
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
hintText: hintText,
prefixIcon: const Icon(
Icons.search,
color: AppColors.primary,
),
contentPadding: const EdgeInsets.all(16.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: const BorderSide(color: Colors.grey),
),
),
),
);
}
}
+17
View File
@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class SpaceHeight extends StatelessWidget {
final double height;
const SpaceHeight(this.height, {super.key});
@override
Widget build(BuildContext context) => SizedBox(height: height);
}
class SpaceWidth extends StatelessWidget {
final double width;
const SpaceWidth(this.width, {super.key});
@override
Widget build(BuildContext context) => SizedBox(width: width);
}
+71
View File
@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import '../constants/colors.dart';
class TabCustom extends StatelessWidget {
final List<TabMenu> children;
final EdgeInsetsGeometry? padding;
const TabCustom({
super.key,
required this.children,
this.padding,
});
@override
Widget build(BuildContext context) {
return Container(
margin: padding,
decoration: BoxDecoration(
border: Border.all(color: AppColors.primary, width: 1.5),
borderRadius: const BorderRadius.all(Radius.circular(9.0)),
),
child: Row(
children: children,
),
);
}
}
class TabMenu extends StatelessWidget {
final String label;
final VoidCallback onTap;
final bool isActive;
const TabMenu({
super.key,
required this.label,
required this.onTap,
this.isActive = false,
});
@override
Widget build(BuildContext context) {
return Flexible(
child: InkWell(
onTap: onTap,
child: Container(
width: context.deviceWidth,
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 15.0),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
color: isActive ? AppColors.primary : AppColors.white,
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isActive ? AppColors.white : AppColors.primary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
),
),
);
}
}
+39
View File
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class AppColors {
/// primary = #3949AB
static const Color primary = Color(0xff6466f1);
/// grey = #B7B7B7
static const Color grey = Color(0xffB7B7B7);
/// light = #F8F5FF
static const Color light = Color(0xffF8F5FF);
/// blueLight = #C7D0EB
static const Color blueLight = Color(0xffC7D0EB);
/// black = #000000
static const Color black = Color(0xff000000);
/// white = #FFFFFF
static const Color white = Color(0xffFFFFFF);
/// green = #50C474
static const Color green = Color(0xff50C474);
/// red = #F4261A
static const Color red = Color(0xffF4261A);
/// card = #E5E5E5
static const Color card = Color(0xffE5E5E5);
/// disabled = #C8D1E1
static const Color disabled = Color(0xffC8D1E1);
/// subtitle = #7890CD
static const Color subtitle = Color(0xff7890CD);
/// stroke = #EFF0F6
static const Color stroke = Color(0xffEFF0F6);
}
+6
View File
@@ -0,0 +1,6 @@
class Variables {
static const String appName = 'POS Kasir Resto App';
static const String apiVersion = 'v1';
// static const String baseUrl = 'http://192.168.1.202:8000';
static const String baseUrl = 'https://pos-app-tablet.enaklo.co.id';
}
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
extension BuildContextExt on BuildContext {
double get deviceHeight => MediaQuery.of(this).size.height;
double get deviceWidth => MediaQuery.of(this).size.width;
}
extension NavigatorExt on BuildContext {
void pop<T extends Object>([T? result]) {
Navigator.pop(this, result);
}
void popToRoot<T extends Object>() {
Navigator.popUntil(this, (route) => route.isFirst);
}
Future<T?> push<T extends Object>(Widget widget, [String? name]) async {
return Navigator.push<T>(
this,
MaterialPageRoute(
builder: (context) => widget,
settings: RouteSettings(name: name),
),
);
}
Future<T?> pushReplacement<T extends Object, TO extends Object>(
Widget widget) async {
return Navigator.pushReplacement<T, TO>(
this,
MaterialPageRoute(builder: (context) => widget),
);
}
Future<T?> pushAndRemoveUntil<T extends Object>(
Widget widget, bool Function(Route<dynamic> route) predicate) async {
return Navigator.pushAndRemoveUntil<T>(
this,
MaterialPageRoute(builder: (context) => widget),
predicate,
);
}
}
+66
View File
@@ -0,0 +1,66 @@
const List<String> _dayNames = [
'Senin',
'Selasa',
'Rabu',
'Kamis',
'Jumat',
'Sabtu',
'Minggu',
];
const List<String> _monthNames = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember'
];
extension DateTimeExt on DateTime {
String toFormattedTime() {
final int hour12 = hour % 12;
final String monthName = _monthNames[month - 1];
return '$day $monthName, ${hour12.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
}
String toFormattedDate() {
String dayName = _dayNames[weekday - 1];
String day = this.day.toString();
String month = _monthNames[this.month - 1];
String year = this.year.toString();
return '$dayName, $day $month $year';
}
String toFormattedDate2() {
String day = this.day.toString();
String month = _monthNames[this.month - 1];
String year = this.year.toString();
return '$day $month $year';
}
String toFormattedDate3() {
String day = this.day.toString();
String month = _monthNames[this.month - 1];
String year = this.year.toString();
String hour = this
.hour
.toString()
.padLeft(2, '0'); // Menambahkan nol di depan jika jam hanya satu digit
String minute = this.minute.toString().padLeft(
2, '0'); // Menambahkan nol di depan jika menit hanya satu digit
String second = this.second.toString().padLeft(
2, '0'); // Menambahkan nol di depan jika detik hanya satu digit
return '$day $month $year, $hour:$minute:$second';
}
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:intl/intl.dart';
extension IntegerExt on int {
String get currencyFormatRp => NumberFormat.currency(
locale: 'id',
symbol: 'Rp. ',
decimalDigits: 0,
).format(this);
String get currencyFormatRpV2 => NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
).format(this);
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:intl/intl.dart';
extension StringExt on String {
int get toIntegerFromText {
final cleanedText = replaceAll(RegExp(r'[^0-9]'), '');
final parsedValue = int.tryParse(cleanedText) ?? 0;
return parsedValue;
}
String get currencyFormatRpV2 {
final parsedValue = int.tryParse(this) ?? 0;
return NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
).format(parsedValue);
}
}
+132
View File
@@ -0,0 +1,132 @@
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AppIconGenerator {
static Future<Uint8List> generateAppIcon() async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
const size = Size(1024, 1024);
// Create a white background
final paint = Paint()..color = Colors.white;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
// Create a blue circle background
paint.color = const Color(0xFF2196F3); // Material Blue
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
size.width * 0.4,
paint,
);
// Draw the gift box icon
_drawGiftBox(canvas, size);
// Draw the text
_drawText(canvas, size);
final picture = recorder.endRecording();
final image = await picture.toImage(1024, 1024);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
return byteData!.buffer.asUint8List();
}
static void _drawGiftBox(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = size.width * 0.02;
final centerX = size.width / 2;
final centerY = size.height / 2 - size.height * 0.1;
final boxSize = size.width * 0.15;
// Draw gift box
final boxRect = Rect.fromCenter(
center: Offset(centerX, centerY),
width: boxSize,
height: boxSize,
);
canvas.drawRect(boxRect, paint);
// Draw bow
final bowWidth = boxSize * 0.8;
final bowHeight = boxSize * 0.3;
final bowRect = Rect.fromCenter(
center: Offset(centerX, centerY - boxSize / 2),
width: bowWidth,
height: bowHeight,
);
canvas.drawRRect(
RRect.fromRectAndRadius(bowRect, Radius.circular(bowHeight / 2)),
paint,
);
// Draw 'e' inside the box
final textPainter = TextPainter(
text: TextSpan(
text: 'e',
style: TextStyle(
color: Colors.white,
fontSize: boxSize * 0.4,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
centerX - textPainter.width / 2,
centerY - textPainter.height / 2,
),
);
}
static void _drawText(Canvas canvas, Size size) {
// Draw "ENAKLO"
final enakloPainter = TextPainter(
text: TextSpan(
text: 'ENAKLO',
style: TextStyle(
color: Colors.white,
fontSize: size.width * 0.08,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
enakloPainter.layout();
enakloPainter.paint(
canvas,
Offset(
size.width / 2 - enakloPainter.width / 2,
size.height / 2 + size.height * 0.05,
),
);
// Draw "POS"
final posPainter = TextPainter(
text: TextSpan(
text: 'POS',
style: TextStyle(
color: Colors.white,
fontSize: size.width * 0.06,
fontWeight: FontWeight.w500,
),
),
textDirection: TextDirection.ltr,
);
posPainter.layout();
posPainter.paint(
canvas,
Offset(
size.width / 2 - posPainter.width / 2,
size.height / 2 + size.height * 0.15,
),
);
}
}
+19
View File
@@ -0,0 +1,19 @@
import 'package:intl/intl.dart';
class DateFormatter {
static String formatDateTime(DateTime dateTime) {
return '${dateTime.year}-${_addZeroPrefix(dateTime.month)}-${_addZeroPrefix(dateTime.day)}';
}
static String _addZeroPrefix(int value) {
return value.toString().padLeft(2, '0');
}
static String formatDateTime2(String dateTimeString) {
final dateTime = DateTime.parse(dateTimeString);
final formatter = DateFormat(
'dd MMMM yyyy, HH:mm',
);
return formatter.format(dateTime);
}
}
+79
View File
@@ -0,0 +1,79 @@
import 'dart:developer';
import 'dart:io';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/widgets.dart';
class HelperPdfService {
static Future<File> saveDocument({
required String name,
required Document pdf,
}) async {
try {
log("Starting PDF save process for: $name");
log("PDF document object: $pdf");
final bytes = await pdf.save();
log("PDF bytes generated successfully, size: ${bytes.length} bytes");
if (bytes.isEmpty) {
log("WARNING: PDF bytes are empty!");
return Future.error("PDF bytes are empty");
}
final dir = await getApplicationDocumentsDirectory();
log("Documents directory: ${dir.path}");
final file = File('${dir.path}/$name');
log("Saving PDF to: ${file.path}");
await file.writeAsBytes(bytes);
log("PDF saved successfully to: ${file.path}");
// Verify file was created
if (await file.exists()) {
final fileSize = await file.length();
log("File exists and size is: $fileSize bytes");
} else {
log("ERROR: File was not created!");
return Future.error("File was not created");
}
return file;
} catch (e) {
log("Failed to save document: $e");
log("Error stack trace: ${StackTrace.current}");
return Future.error("Failed to save document: $e");
}
}
static Future openFile(File file) async {
try {
final url = file.path;
log("Attempting to open file: $url");
if (!await file.exists()) {
log("ERROR: File does not exist: $url");
return;
}
final fileSize = await file.length();
log("File exists and size is: $fileSize bytes");
log("Calling OpenFile.open...");
final result = await OpenFile.open(url, type: "application/pdf");
log("OpenFile result: $result");
if (result.type == ResultType.done) {
log("File opened successfully");
} else {
log("File opening failed with result: ${result.type}");
log("Error message: ${result.message}");
}
} catch (e) {
log("Failed to open file: $e");
log("Error stack trace: ${StackTrace.current}");
}
}
}
+154
View File
@@ -0,0 +1,154 @@
import 'dart:io';
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:flutter/services.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:enaklo_pos/data/models/response/item_sales_response_model.dart';
import 'package:pdf/widgets.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class ItemSalesInvoice {
static late Font ttf;
static Future<File> generate(
List<ItemSales> itemSales, String searchDateFormatted) async {
final pdf = Document();
// var data = await rootBundle.load("assets/fonts/noto-sans.ttf");
// ttf = Font.ttf(data);
final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
final Uint8List bytes = dataImage.buffer.asUint8List();
// Membuat objek Image dari gambar
final image = pw.MemoryImage(bytes);
pdf.addPage(
MultiPage(
build: (context) => [
buildHeader(image, searchDateFormatted),
SizedBox(height: 1 * PdfPageFormat.cm),
buildInvoice(itemSales),
Divider(),
SizedBox(height: 0.25 * PdfPageFormat.cm),
],
footer: (context) => buildFooter(),
),
);
return HelperPdfService.saveDocument(
name:
'Enaklo POS | Item Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf);
}
static Widget buildHeader(MemoryImage image, String searchDateFormatted) =>
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Item Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
)),
SizedBox(height: 0.2 * PdfPageFormat.cm),
Text(
"Data: $searchDateFormatted",
),
Text(
'Created At: ${DateTime.now().toFormattedDate3()}',
),
],
),
Image(
image,
width: 80.0,
height: 80.0,
fit: BoxFit.fill,
),
]);
static Widget buildInvoice(List<ItemSales> itemSales) {
final headers = ['Id', 'Order', 'Product', 'Qty', 'Price', 'Total'];
final data = itemSales.map((item) {
return [
item.id!,
item.orderId,
item.productName,
item.price!.currencyFormatRp,
item.quantity,
(item.price! * item.quantity!).currencyFormatRp
];
}).toList();
return Table.fromTextArray(
headers: headers,
data: data,
border: null,
headerStyle: TextStyle(
fontWeight: FontWeight.bold, color: PdfColor.fromHex('FFFFFF')),
headerDecoration: BoxDecoration(color: PdfColors.blue),
cellHeight: 30,
cellAlignments: {
0: Alignment.centerLeft,
1: Alignment.center,
2: Alignment.center,
3: Alignment.centerLeft,
4: Alignment.centerLeft,
5: Alignment.centerLeft,
},
);
}
static Widget buildFooter() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Divider(),
SizedBox(height: 2 * PdfPageFormat.mm),
buildSimpleText(
title: 'Address',
value:
'Jalan Melati No. 12, Mranggen, Demak, Central Java, 89568'),
SizedBox(height: 1 * PdfPageFormat.mm),
],
);
static buildSimpleText({
required String title,
required String value,
}) {
final style = TextStyle(fontWeight: FontWeight.bold);
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
Text(title, style: style),
SizedBox(width: 2 * PdfPageFormat.mm),
Text(value),
],
);
}
static buildText({
required String title,
required String value,
double width = double.infinity,
TextStyle? titleStyle,
bool unite = false,
}) {
final style = titleStyle ?? TextStyle(fontWeight: FontWeight.bold);
return Container(
width: width,
child: Row(
children: [
Expanded(child: Text(title, style: style)),
Text(value, style: unite ? style : null),
],
),
);
}
}
+43
View File
@@ -0,0 +1,43 @@
import 'dart:developer';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
class PermessionHelper {
Future<bool> checkPermission() async {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
bool permissionStatus;
if (deviceInfo.version.sdkInt > 32) {
permissionStatus = await Permission.photos.request().isGranted;
} else {
permissionStatus = await Permission.storage.request().isGranted;
}
if (permissionStatus) {
log('Izin penyimpanan sudah diberikan.');
} else {
if (deviceInfo.version.sdkInt > 32) {
log('deviceInfo.version.sdkInt > 32.');
permissionStatus = await Permission.photos.request().isGranted;
} else {
permissionStatus = await Permission.storage.request().isGranted;
}
// } else {
// openAppSettings();
// }
}
log('permissionStatus: $permissionStatus');
return permissionStatus;
}
void permessionPrinter() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetooth,
Permission.bluetoothScan,
Permission.bluetoothAdvertise,
Permission.bluetoothConnect,
].request();
log("statuses: $statuses");
}
}
+157
View File
@@ -0,0 +1,157 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_esc_pos_network/flutter_esc_pos_network.dart';
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
import 'package:enaklo_pos/data/models/response/print_model.dart';
class PrinterService {
static final PrinterService _instance = PrinterService._internal();
factory PrinterService() => _instance;
PrinterService._internal();
/// Connect to Bluetooth printer
Future<bool> connectBluetoothPrinter(String macAddress) async {
try {
// Check if already connected
bool isConnected = await PrintBluetoothThermal.connectionStatus;
if (isConnected) {
log("Already connected to Bluetooth printer");
return true;
}
// Connect to the printer
bool connected = await PrintBluetoothThermal.connect(
macPrinterAddress: macAddress);
if (connected) {
log("Successfully connected to Bluetooth printer: $macAddress");
} else {
log("Failed to connect to Bluetooth printer: $macAddress");
}
return connected;
} catch (e) {
log("Error connecting to Bluetooth printer: $e");
return false;
}
}
/// Print using Bluetooth printer
Future<bool> printBluetooth(List<int> printData) async {
try {
bool isConnected = await PrintBluetoothThermal.connectionStatus;
if (!isConnected) {
log("Not connected to Bluetooth printer");
return false;
}
bool printResult = await PrintBluetoothThermal.writeBytes(printData);
if (printResult) {
log("Successfully printed via Bluetooth");
} else {
log("Failed to print via Bluetooth");
}
return printResult;
} catch (e) {
log("Error printing via Bluetooth: $e");
return false;
}
}
/// Print using Network printer
Future<bool> printNetwork(String ipAddress, List<int> printData) async {
try {
final printer = PrinterNetworkManager(ipAddress);
PosPrintResult connect = await printer.connect();
if (connect == PosPrintResult.success) {
PosPrintResult printing = await printer.printTicket(printData);
printer.disconnect();
if (printing == PosPrintResult.success) {
log("Successfully printed via Network printer: $ipAddress");
return true;
} else {
log("Failed to print via Network printer: ${printing.msg}");
return false;
}
} else {
log("Failed to connect to Network printer: ${connect.msg}");
return false;
}
} catch (e) {
log("Error printing via Network: $e");
return false;
}
}
/// Print with automatic printer type detection
Future<bool> printWithPrinter(PrintModel printer, List<int> printData, BuildContext context) async {
try {
if (printer.type == 'Bluetooth') {
bool connected = await connectBluetoothPrinter(printer.address);
if (!connected) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to connect to ${printer.name}')),
);
return false;
}
bool printResult = await printBluetooth(printData);
if (!printResult) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to print to ${printer.name}')),
);
}
return printResult;
} else {
bool printResult = await printNetwork(printer.address, printData);
if (!printResult) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to print to ${printer.name}')),
);
}
return printResult;
}
} catch (e) {
log("Error printing with printer ${printer.name}: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error printing to ${printer.name}: $e')),
);
return false;
}
}
/// Disconnect from Bluetooth printer
Future<bool> disconnectBluetooth() async {
try {
bool result = await PrintBluetoothThermal.disconnect;
log("Bluetooth printer disconnected: $result");
return result;
} catch (e) {
log("Error disconnecting Bluetooth printer: $e");
return false;
}
}
/// Check if Bluetooth is enabled
Future<bool> isBluetoothEnabled() async {
try {
return await PrintBluetoothThermal.bluetoothEnabled;
} catch (e) {
log("Error checking Bluetooth status: $e");
return false;
}
}
/// Get paired Bluetooth devices
Future<List<BluetoothInfo>> getPairedBluetoothDevices() async {
try {
return await PrintBluetoothThermal.pairedBluetooths;
} catch (e) {
log("Error getting paired Bluetooth devices: $e");
return [];
}
}
}
+223
View File
@@ -0,0 +1,223 @@
import 'dart:io';
import 'dart:developer';
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/data/models/response/summary_response_model.dart';
import 'package:flutter/services.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:pdf/widgets.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:flutter/foundation.dart';
class RevenueInvoice {
static late Font ttf;
static Future<File> generate(
SummaryModel summaryModel,
String searchDateFormatted,
) async {
try {
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 Uint8List bytes = dataImage.buffer.asUint8List();
final image = pw.MemoryImage(bytes);
log("Logo image loaded successfully, size: ${bytes.length} bytes");
log("Adding page to PDF...");
pdf.addPage(
MultiPage(
build: (context) => [
buildHeader(summaryModel, image, searchDateFormatted),
SizedBox(height: 1 * PdfPageFormat.cm),
buildTotal(summaryModel),
],
footer: (context) => buildFooter(summaryModel),
),
);
log("PDF page added successfully");
log("Saving PDF document...");
return HelperPdfService.saveDocument(
name:
'Enaklo POS | Summary Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf,
);
} catch (e) {
log("Error generating PDF: $e");
log("Error stack trace: ${StackTrace.current}");
return Future.error("Failed to generate PDF: $e");
}
}
static Widget buildHeader(
SummaryModel invoice,
MemoryImage image,
String searchDateFormatted,
) =>
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Summary Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
)),
SizedBox(height: 0.2 * PdfPageFormat.cm),
Text(
"Data: $searchDateFormatted",
),
Text(
'Created At: ${DateTime.now().toFormattedDate3()}',
),
],
),
Image(
image,
width: 80.0,
height: 80.0,
fit: BoxFit.fill,
),
]);
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;
try {
return int.parse(value.replaceAll('.00', ''));
} catch (e) {
log("Error parsing value '$value' to int: $e");
return 0;
}
}
return Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildText(
title: 'Revenue',
value: safeParseInt(summaryModel.totalRevenue).currencyFormatRp,
unite: true,
),
Divider(),
buildText(
title: 'Sub Total',
titleStyle: TextStyle(fontWeight: FontWeight.normal),
value: safeParseInt(summaryModel.totalSubtotal).currencyFormatRp,
unite: true,
),
buildText(
title: 'Discount',
titleStyle: TextStyle(fontWeight: FontWeight.normal),
value: "- ${safeParseInt(summaryModel.totalDiscount).currencyFormatRp}",
unite: true,
textStyle: TextStyle(
color: PdfColor.fromHex('#FF0000'),
fontWeight: FontWeight.bold,
),
),
buildText(
title: 'Tax',
titleStyle: TextStyle(fontWeight: FontWeight.normal),
value: "- ${safeParseInt(summaryModel.totalTax).currencyFormatRp}",
textStyle: TextStyle(
color: PdfColor.fromHex('#FF0000'),
fontWeight: FontWeight.bold,
),
unite: true,
),
buildText(
title: 'Service Charge',
titleStyle: TextStyle(
fontWeight: FontWeight.normal,
),
value: safeParseInt(summaryModel.totalServiceCharge).currencyFormatRp,
unite: true,
),
Divider(),
buildText(
title: 'Total ',
titleStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
value: (summaryModel.total ?? 0).currencyFormatRp,
unite: true,
),
SizedBox(height: 2 * PdfPageFormat.mm),
Container(height: 1, color: PdfColors.grey400),
SizedBox(height: 0.5 * PdfPageFormat.mm),
Container(height: 1, color: PdfColors.grey400),
],
),
);
}
static Widget buildFooter(SummaryModel summaryModel) => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Divider(),
SizedBox(height: 2 * PdfPageFormat.mm),
buildSimpleText(
title: 'Address',
value:
'Jalan Melati No. 12, Mranggen, Demak, Central Java, 89568'),
SizedBox(height: 1 * PdfPageFormat.mm),
],
);
static buildSimpleText({
required String title,
required String value,
}) {
final style = TextStyle(fontWeight: FontWeight.bold);
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
Text(title, style: style),
SizedBox(width: 2 * PdfPageFormat.mm),
Text(value),
],
);
}
static buildText({
required String title,
required String value,
double width = double.infinity,
TextStyle? titleStyle,
TextStyle? textStyle,
bool unite = false,
}) {
final style = titleStyle ?? TextStyle(fontWeight: FontWeight.bold);
final style2 = textStyle ?? TextStyle(fontWeight: FontWeight.bold);
return Container(
width: width,
child: Row(
children: [
Expanded(child: Text(title, style: style)),
Text(value, style: style2),
],
),
);
}
}
@@ -0,0 +1,161 @@
import 'dart:io';
import 'package:enaklo_pos/core/extensions/date_time_ext.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:flutter/services.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart';
import 'package:enaklo_pos/data/models/response/order_remote_datasource.dart';
import 'package:pdf/widgets.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class TransactionSalesInvoice {
static late Font ttf;
static Future<File> generate(
List<ItemOrder> itemOrders, String searchDateFormatted) async {
final pdf = Document();
// var data = await rootBundle.load("assets/fonts/noto-sans.ttf");
// ttf = Font.ttf(data);
final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
final Uint8List bytes = dataImage.buffer.asUint8List();
// Membuat objek Image dari gambar
final image = pw.MemoryImage(bytes);
pdf.addPage(
MultiPage(
build: (context) => [
buildHeader(image, searchDateFormatted),
SizedBox(height: 1 * PdfPageFormat.cm),
buildInvoice(itemOrders),
Divider(),
SizedBox(height: 0.25 * PdfPageFormat.cm),
],
footer: (context) => buildFooter(),
),
);
return HelperPdfService.saveDocument(
name:
'Enaklo POS | Transaction Sales Report | ${DateTime.now().millisecondsSinceEpoch}.pdf',
pdf: pdf);
}
static Widget buildHeader(MemoryImage image, String searchDateFormatted) =>
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 1 * PdfPageFormat.cm),
Text('Enaklo POS | Transaction Sales Report',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
)),
SizedBox(height: 0.2 * PdfPageFormat.cm),
Text(
"Data: $searchDateFormatted",
),
Text(
'Created At: ${DateTime.now().toFormattedDate3()}',
),
],
),
Image(
image,
width: 80.0,
height: 80.0,
fit: BoxFit.fill,
),
]);
static Widget buildInvoice(List<ItemOrder> itemOrders) {
final headers = [
'Total',
'Sub Total',
'Tax',
'Discount',
'Service',
'Time'
];
final data = itemOrders.map((item) {
return [
item.total!.currencyFormatRp,
item.subTotal!.currencyFormatRp,
item.tax!.currencyFormatRp,
int.parse(item.discountAmount!.replaceAll('.00', '')).currencyFormatRp,
item.serviceCharge!.currencyFormatRp,
item.transactionTime!.toFormattedDate2(),
];
}).toList();
return Table.fromTextArray(
headers: headers,
data: data,
border: null,
headerStyle: TextStyle(
fontWeight: FontWeight.bold, color: PdfColor.fromHex('FFFFFF')),
headerDecoration: BoxDecoration(color: PdfColors.blue),
cellHeight: 30,
cellAlignments: {
0: Alignment.center,
1: Alignment.center,
2: Alignment.center,
3: Alignment.center,
4: Alignment.center,
5: Alignment.center,
},
);
}
static Widget buildFooter() => Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Divider(),
SizedBox(height: 2 * PdfPageFormat.mm),
buildSimpleText(
title: 'Address',
value:
'Jalan Melati No. 12, Mranggen, Demak, Central Java, 89568'),
SizedBox(height: 1 * PdfPageFormat.mm),
],
);
static buildSimpleText({
required String title,
required String value,
}) {
final style = TextStyle(fontWeight: FontWeight.bold);
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
Text(title, style: style),
SizedBox(width: 2 * PdfPageFormat.mm),
Text(value),
],
);
}
static buildText({
required String title,
required String value,
double width = double.infinity,
TextStyle? titleStyle,
bool unite = false,
}) {
final style = titleStyle ?? TextStyle(fontWeight: FontWeight.bold);
return Container(
width: width,
child: Row(
children: [
Expanded(child: Text(title, style: style)),
Text(value, style: unite ? style : null),
],
),
);
}
}