feat(app): implement Product Scan review flow, dynamic batch expiry picker, custom PDFs, PO relationships, and aligned card layouts

This commit is contained in:
Rafhan Mazaya Fathurrahman committed 2026-07-09 12:36:06 +07:00
1 parent 9ff4a4a922
commit 577be04308
69 files changed
+5645 -1657

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../config/app_config.dart';
import '../../models/document_model.dart';
class DocumentCard extends StatelessWidget {
final DocumentModel doc;
final Future<void> Function(DocumentModel) onPrint;
const DocumentCard({
super.key,
required this.doc,
required this.onPrint,
});
@override
Widget build(BuildContext context) {
final isProductScan = doc.orderUntuk == 'PRODUCT SCAN';
// Header category text
final categoryText = isProductScan
? (doc.items.isNotEmpty ? doc.items.first.namaBarang : 'Product Scan')
: (doc.namaPenerima.isNotEmpty ? doc.namaPenerima : 'Staff Toko');
// Category color (greenish-blue for product, orange-yellow for DO)
final categoryColor = isProductScan ? AppConfig.primaryColor : Colors.orange.shade700;
// Bold title
final boldTitle = isProductScan
? doc.noDo
: doc.noPo;
// Subtitle text
final subtitleText = isProductScan
? '${_formatDateString(doc.alamat.replaceAll("Product Scan: Expired ", ""))} • ${doc.noPo}'
: '${doc.tanggal} • ${doc.items.length} Item';
return Card(
elevation: 0,
margin: const EdgeInsets.only(bottom: AppConfig.spaceMedium),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
side: BorderSide(color: Colors.grey.shade200),
),
child: InkWell(
onTap: () => onPrint(doc),
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
child: Padding(
padding: AppConfig.paddingLarge,
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top category label
Text(
categoryText,
style: AppConfig.bodySmall.copyWith(
color: categoryColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: AppConfig.spaceXS),
// Large bold title
Text(
boldTitle,
style: AppConfig.bodyLarge.copyWith(
fontWeight: FontWeight.bold,
color: AppConfig.textPrimary,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: AppConfig.spaceXS),
// Subtitle details
Text(
subtitleText,
style: AppConfig.bodyMedium.copyWith(
color: AppConfig.textSecondary,
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: AppConfig.spaceMedium),
// Right column with status badge and print button
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Status badge (e.g. Approved / Terkonfirmasi)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppConfig.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
),
child: Text(
'Terkonfirmasi',
style: AppConfig.bodySmall.copyWith(
color: AppConfig.successColor,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
const SizedBox(height: AppConfig.spaceMedium),
// Print button styled as light-gray square container with print icon
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
),
child: const Icon(
Icons.print,
size: 18,
color: AppConfig.textSecondary,
),
),
],
),
],
),
),
),
);
}
String _formatDateString(String input) {
try {
if (input.contains('/')) {
final parts = input.split('/');
if (parts.length == 3) {
final day = int.parse(parts[0]);
final month = int.parse(parts[1]);
final year = int.parse(parts[2]);
final date = DateTime(year, month, day);
return DateFormat('dd MMMM yyyy').format(date);
}
}
} catch (e) {
// Fallback
}
return input;
}
}