feat(app): implement Product Scan review flow, dynamic batch expiry picker, custom PDFs, PO relationships, and aligned card layouts
This commit is contained in:
1 parent
9ff4a4a922
commit
577be04308
69 files changed
+5645
-1657
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user