first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user