Files
apskel-pos-flutter-v2/lib/presentation/components/dialog/print_progress_dialog.dart
T

274 lines
8.0 KiB
Dart
Raw Normal View History

part of 'dialog.dart';
/// Dijaga supaya satu job cetak hanya membuka satu dialog, walau listener
/// terpicu lebih dari sekali.
bool _isPrintProgressDialogOpen = false;
Future<void> showPrintProgressDialog(BuildContext context) async {
if (_isPrintProgressDialogOpen) return;
_isPrintProgressDialogOpen = true;
try {
await showDialog<void>(
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<PrintProgressDialog> createState() => _PrintProgressDialogState();
}
class _PrintProgressDialogState extends State<PrintProgressDialog> {
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<PrintStruckBloc, PrintStruckState>(
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<PrintStruckBloc>().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',
};
}