import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../common/ui/painter/dashed_line_painter.dart'; import '../../../domain/wallet/wallet.dart'; import '../../components/toast/flushbar.dart'; import '../wallet/wallet_ui.dart'; /// Label jenis voucher (§9.1). String voucherTypeLabel(VoucherType type, int faceValue) => switch (type) { VoucherType.fixedValue => 'Potongan ${faceValue.currencyFormatRp}', VoucherType.percentage => 'Potongan persen', VoucherType.freeItem => 'Gratis item', VoucherType.merchantBenefit => 'Benefit merchant', VoucherType.unknown => 'Voucher', }; /// "senilai Rp 20.000". `null` untuk potongan nominal, karena nilainya sudah /// ada di [voucherTypeLabel], dan bila server tidak mengirim nilai. String? voucherFaceValueLabel(VoucherType type, int faceValue) => type == VoucherType.fixedValue || faceValue <= 0 ? null : 'senilai ${faceValue.currencyFormatRp}'; extension VoucherRedemptionStatusUi on VoucherRedemptionStatus { String get label => switch (this) { VoucherRedemptionStatus.completed => 'Siap dipakai', VoucherRedemptionStatus.pending => 'Sedang diproses', VoucherRedemptionStatus.failed => 'Gagal', VoucherRedemptionStatus.unknown => '', }; Color get color => switch (this) { VoucherRedemptionStatus.completed => AppColor.success, VoucherRedemptionStatus.pending => AppColor.warning, VoucherRedemptionStatus.failed => AppColor.error, VoucherRedemptionStatus.unknown => AppColor.textSecondary, }; } class VoucherRedemptionStatusBadge extends StatelessWidget { const VoucherRedemptionStatusBadge({super.key, required this.status}); final VoucherRedemptionStatus status; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: status.color.withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: Text( status.label, style: AppStyle.xs.copyWith( color: status.color, fontWeight: FontWeight.w700, ), ), ); } } /// Pesan untuk penolakan tukar voucher (§9.2). `cause` dari server berbahasa /// Inggris dan tidak ditampilkan mentah. [reloadCatalog] menandai voucher yang /// sudah tidak tersedia, sehingga katalog perlu dimuat ulang. ({String message, bool reloadCatalog}) describeVoucherRedeemFailure( WalletFailure failure, ) { final cause = failure.maybeMap( rejected: (value) => value.cause.toLowerCase(), orElse: () => null, ); if (cause == null) { return (message: walletFailureMessage(failure), reloadCatalog: false); } if (cause.contains('not enough enakpoint')) { return (message: 'EnakPoint kamu kurang.', reloadCatalog: false); } if (cause.contains('out of stock')) { return (message: 'Voucher sudah habis.', reloadCatalog: true); } if (cause.contains('per customer')) { return ( message: 'Kamu sudah mencapai batas penukaran voucher ini.', reloadCatalog: false, ); } if (cause.contains('customer is not active')) { return (message: 'Akun tidak aktif.', reloadCatalog: false); } if (cause.contains('not available') || cause.contains('cannot be redeemed yet') || cause.contains('has ended')) { return (message: 'Voucher tidak tersedia.', reloadCatalog: true); } // Termasuk Idempotency-Key yang terpakai untuk voucher lain (bug di app). return ( message: 'Penukaran voucher tidak bisa diproses.', reloadCatalog: false, ); } /// Gambar voucher dari `image_url`; ikon tiket bila kosong. class VoucherImage extends StatelessWidget { const VoucherImage({ super.key, required this.url, required this.size, this.dimmed = false, }); final String? url; final double size; /// Abu-abu untuk voucher yang tidak bisa ditukar / dipakai. final bool dimmed; static const ColorFilter _greyscale = ColorFilter.matrix([ 0.2126, 0.7152, 0.0722, 0, 0, // 0.2126, 0.7152, 0.0722, 0, 0, // 0.2126, 0.7152, 0.0722, 0, 0, // 0, 0, 0, 0.5, 0, // ]); @override Widget build(BuildContext context) { final fallback = SizedBox( width: size, height: size, child: Icon( Icons.confirmation_number_outlined, size: size * 0.7, color: dimmed ? AppColor.textLight : AppColor.primary, ), ); final image = url == null ? fallback : ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( imageUrl: url!, width: size, height: size, fit: BoxFit.cover, placeholder: (_, _) => SizedBox(width: size, height: size), errorWidget: (_, _, _) => fallback, ), ); if (!dimmed || url == null) return image; return ColorFiltered(colorFilter: _greyscale, child: image); } } /// Kode voucher yang bisa disalin. Customer menunjukkan layar ini ke kasir. class VoucherCodeBox extends StatelessWidget { const VoucherCodeBox({super.key, required this.code}); final String code; Future _copy(BuildContext context) async { await Clipboard.setData(ClipboardData(text: code)); if (!context.mounted) return; AppFlushbar.showSuccess(context, 'Kode voucher disalin'); } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.fromLTRB(16, 14, 8, 8), decoration: BoxDecoration( border: Border.all(color: AppColor.border), borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Text( 'Tunjukkan kode ini ke kasir', style: AppStyle.sm.copyWith(color: AppColor.textSecondary), ), const SizedBox(height: 10), CustomPaint( size: const Size(double.infinity, 1), painter: DashedLinePainter(), ), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: SelectableText( code, textAlign: TextAlign.center, style: AppStyle.xl.copyWith( fontWeight: FontWeight.w700, letterSpacing: 2, ), ), ), IconButton( onPressed: () => _copy(context), tooltip: 'Salin kode', icon: const Icon( Icons.copy_rounded, size: 20, color: AppColor.primary, ), ), ], ), ], ), ); } } /// Catatan untuk voucher tanpa kode (`code` null): customer menunjukkan layar /// voucher ke kasir. class VoucherNoCodeNote extends StatelessWidget { const VoucherNoCodeNote({super.key}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColor.successWithOpacity(0.08), borderRadius: BorderRadius.circular(AppValue.borderRadius), ), child: Row( children: [ const Icon( Icons.storefront_outlined, size: 20, color: AppColor.success, ), const SizedBox(width: 10), Expanded( child: Text( 'Voucher ini tidak memakai kode. Tunjukkan layar ini ke kasir.', style: AppStyle.sm, ), ), ], ), ); } }