part of 'dialog.dart'; /// Dijaga supaya satu job cetak hanya membuka satu dialog, walau listener /// terpicu lebih dari sekali. bool _isPrintProgressDialogOpen = false; Future showPrintProgressDialog(BuildContext context) async { if (_isPrintProgressDialogOpen) return; _isPrintProgressDialogOpen = true; try { await showDialog( context: context, // Cetak jalan di latar belakang, kasir tidak perlu menunggui dialognya. barrierDismissible: true, builder: (_) => const PrintProgressDialog(), ); } finally { _isPrintProgressDialogOpen = false; } } class PrintProgressDialog extends StatefulWidget { const PrintProgressDialog({super.key}); @override State createState() => _PrintProgressDialogState(); } class _PrintProgressDialogState extends State { Timer? _autoClose; @override void dispose() { _autoClose?.cancel(); super.dispose(); } /// Semua station keluar — tutup sendiri supaya antrean kasir tidak tertahan, /// tapi beri jeda sebentar agar centang hijaunya sempat terbaca. void _scheduleAutoClose() { _autoClose?.cancel(); _autoClose = Timer(const Duration(milliseconds: 1500), () { if (mounted) Navigator.of(context).pop(); }); } @override Widget build(BuildContext context) { return BlocConsumer( listenWhen: (previous, current) => previous.isPrinting && !current.isPrinting, listener: (context, state) { if (state.hasFailure) return; _scheduleAutoClose(); }, builder: (context, state) { final isDone = !state.isPrinting; return CustomModalDialog( title: isDone ? 'Hasil Cetak' : 'Mencetak Struk', maxWidth: 480, minHeight: 0, contentPadding: const EdgeInsets.all(20), bottom: _buildFooter(context, state), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildSummary(state), const SpaceHeight(16), for (final result in state.stations) ...[ _StationRow(result: result), const SpaceHeight(8), ], ], ), ); }, ); } Widget _buildSummary(PrintStruckState state) { final isDone = !state.isPrinting; final total = state.plannedCount; final success = state.successCount; final Color color; final String message; if (!isDone) { color = AppColor.primary; message = 'Mengirim ke printer…'; } else if (state.hasFailure) { color = AppColor.error; message = '${state.failedStations.length} station gagal dicetak'; } else { color = AppColor.success; message = 'Semua struk berhasil dicetak'; } return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.25)), ), child: Row( children: [ Expanded( child: Text( message, style: AppStyle.md.copyWith( color: color, fontWeight: FontWeight.w600, ), ), ), Text( '$success dari $total', style: AppStyle.md.copyWith( color: color, fontWeight: FontWeight.bold, ), ), ], ), ); } Widget _buildFooter(BuildContext context, PrintStruckState state) { final isDone = !state.isPrinting; return Container( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (!isDone) ...[ Text( 'Cetak berjalan di latar belakang. Dialog ini boleh ditutup.', style: AppStyle.sm.copyWith(color: AppColor.textSecondary), textAlign: TextAlign.center, ), const SpaceHeight(12), ], Row( children: [ Expanded( child: AppElevatedButton.outlined( onPressed: () => Navigator.of(context).pop(), label: 'Tutup', ), ), if (isDone && state.hasFailure) ...[ const SpaceWidth(12), Expanded( flex: 2, child: AppElevatedButton.filled( onPressed: () => context.read().add( const PrintStruckEvent.retryFailed(), ), label: 'Cetak Ulang yang Gagal', icon: const Icon( Icons.refresh_rounded, color: AppColor.white, size: 18, ), ), ), ], ], ), ], ), ); } } class _StationRow extends StatelessWidget { final PrintStationResult result; const _StationRow({required this.result}); @override Widget build(BuildContext context) { final color = _color; return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( color: result.isFailed ? color.withOpacity(0.4) : AppColor.border, ), color: result.isFailed ? color.withOpacity(0.05) : Colors.transparent, ), child: Row( children: [ SizedBox(width: 20, height: 20, child: Center(child: _icon)), const SpaceWidth(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( result.station.label, style: AppStyle.md.copyWith(fontWeight: FontWeight.w600), ), const SpaceHeight(2), Text( _statusText, style: AppStyle.sm.copyWith(color: color), ), ], ), ), if (result.elapsed > Duration.zero) ...[ const SpaceWidth(8), Text( '${(result.elapsed.inMilliseconds / 1000).toStringAsFixed(1)}s', style: AppStyle.sm.copyWith(color: AppColor.textSecondary), ), ], ], ), ); } Widget get _icon => switch (result.status) { PrintStationStatus.printing => const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), PrintStationStatus.pending => const Icon( Icons.schedule_rounded, size: 18, color: AppColor.textSecondary, ), PrintStationStatus.success => const Icon( Icons.check_circle_rounded, size: 20, color: AppColor.success, ), PrintStationStatus.failed => const Icon( Icons.error_rounded, size: 20, color: AppColor.error, ), PrintStationStatus.skipped => const Icon( Icons.remove_circle_outline_rounded, size: 18, color: AppColor.textSecondary, ), }; Color get _color => switch (result.status) { PrintStationStatus.printing => AppColor.primary, PrintStationStatus.success => AppColor.success, PrintStationStatus.failed => AppColor.error, PrintStationStatus.pending || PrintStationStatus.skipped => AppColor.textSecondary, }; String get _statusText => switch (result.status) { PrintStationStatus.pending => 'Menunggu giliran', PrintStationStatus.printing => 'Mengirim ke printer…', PrintStationStatus.success => 'Berhasil dicetak', PrintStationStatus.failed => result.message ?? 'Gagal dicetak', PrintStationStatus.skipped => result.message ?? 'Dilewati', }; }