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
@@ -1,15 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../config/app_config.dart';
|
||||
import '../../models/document_model.dart';
|
||||
import '../../main.dart'; // To access localStorageProvider
|
||||
import '../documents/pending_documents_provider.dart';
|
||||
import '../../data/master_sku.dart';
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/network/api_exception.dart';
|
||||
import 'widgets/header_details_card.dart';
|
||||
import 'widgets/shipment_details_card.dart';
|
||||
import 'widgets/location_details_card.dart';
|
||||
import 'widgets/items_list_card.dart';
|
||||
import 'widgets/confirmation_dialog.dart';
|
||||
|
||||
class EditorScreen extends ConsumerStatefulWidget {
|
||||
final String? pendingId;
|
||||
@@ -23,13 +25,12 @@ class EditorScreen extends ConsumerStatefulWidget {
|
||||
class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
DocumentModel? _document;
|
||||
|
||||
// Controllers for Header
|
||||
// Controllers
|
||||
final _tanggalCtrl = TextEditingController();
|
||||
final _noPoCtrl = TextEditingController();
|
||||
final _noSoCtrl = TextEditingController();
|
||||
final _noDoCtrl = TextEditingController();
|
||||
|
||||
// Controllers for Shipment
|
||||
final _kepadaYthCtrl = TextEditingController();
|
||||
final _orderUntukCtrl = TextEditingController();
|
||||
final _alamatCtrl = TextEditingController();
|
||||
@@ -37,13 +38,10 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
final _namaDriverCtrl = TextEditingController();
|
||||
final _namaPenerimaCtrl = TextEditingController();
|
||||
|
||||
// Controllers for Geotagging
|
||||
final _latitudeCtrl = TextEditingController();
|
||||
final _longitudeCtrl = TextEditingController();
|
||||
|
||||
// Controllers for Items
|
||||
List<Map<String, TextEditingController>> _itemControllers = [];
|
||||
|
||||
bool _isAgreed = false;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@@ -53,10 +51,31 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
_loadDocumentData();
|
||||
}
|
||||
|
||||
void _loadDocumentData() {
|
||||
if (widget.pendingId == null) {
|
||||
return;
|
||||
@override
|
||||
void dispose() {
|
||||
_tanggalCtrl.dispose();
|
||||
_noPoCtrl.dispose();
|
||||
_noSoCtrl.dispose();
|
||||
_noDoCtrl.dispose();
|
||||
_kepadaYthCtrl.dispose();
|
||||
_orderUntukCtrl.dispose();
|
||||
_alamatCtrl.dispose();
|
||||
_platTrukCtrl.dispose();
|
||||
_namaDriverCtrl.dispose();
|
||||
_namaPenerimaCtrl.dispose();
|
||||
_latitudeCtrl.dispose();
|
||||
_longitudeCtrl.dispose();
|
||||
for (var ctrls in _itemControllers) {
|
||||
ctrls['sku']?.dispose();
|
||||
ctrls['nama']?.dispose();
|
||||
ctrls['banyak']?.dispose();
|
||||
ctrls['jumlah']?.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _loadDocumentData() {
|
||||
if (widget.pendingId == null) return;
|
||||
|
||||
final pendingDocs = ref.read(pendingDocumentsProvider);
|
||||
final pendingDoc = pendingDocs.firstWhere(
|
||||
@@ -96,24 +115,12 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _addItem({String sku = '', String nama = '', String banyak = '', String jumlah = ''}) {
|
||||
final skuCtrl = TextEditingController(text: sku);
|
||||
final namaCtrl = TextEditingController(text: nama);
|
||||
final banyakCtrl = TextEditingController(text: banyak);
|
||||
final jumlahCtrl = TextEditingController(text: jumlah);
|
||||
|
||||
// Auto rename based on master SKU
|
||||
skuCtrl.addListener(() {
|
||||
final currentSku = skuCtrl.text;
|
||||
if (MasterSku.data.containsKey(currentSku)) {
|
||||
namaCtrl.text = MasterSku.data[currentSku]!;
|
||||
} else {
|
||||
namaCtrl.text = 'SKU Tidak Terdaftar';
|
||||
}
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_itemControllers.add({
|
||||
'sku': skuCtrl,
|
||||
@@ -145,85 +152,22 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setModalState) {
|
||||
final isButtonEnabled = _isAgreed && _namaPenerimaCtrl.text.trim().isNotEmpty;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 24,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Konfirmasi Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Masukkan nama Anda (sebagai penerima/penjaga toko) dan centang kotak persetujuan untuk mengonfirmasi pengiriman.',
|
||||
style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _namaPenerimaCtrl,
|
||||
onChanged: (val) {
|
||||
setModalState(() {});
|
||||
},
|
||||
style: AppConfig.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nama Penerima (Penjaga Toko)',
|
||||
labelStyle: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _isAgreed,
|
||||
activeColor: AppConfig.primaryColor,
|
||||
onChanged: (val) {
|
||||
setModalState(() {
|
||||
_isAgreed = val ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text('Saya setuju dan mengonfirmasi bahwa data ini sudah benar.', style: AppConfig.bodyMedium),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: isButtonEnabled ? () {
|
||||
Navigator.pop(context); // close modal
|
||||
_submitDocument();
|
||||
} : null,
|
||||
child: const Text('Konfirmasi'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ConfirmationDialog(
|
||||
nameController: _namaPenerimaCtrl,
|
||||
initialAgreed: _isAgreed,
|
||||
onConfirm: (agreed, name) {
|
||||
setState(() {
|
||||
_isAgreed = agreed;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
_submitDocument();
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submitDocument() async {
|
||||
// Construct final document
|
||||
final pendingDocs = ref.read(pendingDocumentsProvider);
|
||||
final pendingDoc = pendingDocs.firstWhere(
|
||||
(doc) => doc.id == widget.pendingId,
|
||||
@@ -255,11 +199,9 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Save locally via Riverpod injected LocalStorage
|
||||
final localStorage = ref.read(localStorageProvider);
|
||||
await localStorage.saveDocument(finalDoc);
|
||||
|
||||
// Try to sync with server
|
||||
bool syncedToServer = false;
|
||||
String? syncError;
|
||||
try {
|
||||
@@ -269,19 +211,14 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
data: finalDoc.toPutPayload(),
|
||||
);
|
||||
syncedToServer = true;
|
||||
debugPrint('Document synced with server successfully');
|
||||
} catch (e) {
|
||||
syncError = ApiException.from(e).displayMessage;
|
||||
debugPrint('Failed to sync document with server: $syncError. Saved locally (cached).');
|
||||
}
|
||||
|
||||
if (widget.pendingId != null) {
|
||||
if (syncedToServer) {
|
||||
ref.read(pendingDocumentsProvider.notifier).removeDocument(widget.pendingId!);
|
||||
} else {
|
||||
// Keep it visible under "Pending & Processing" with a distinct status so the
|
||||
// correction isn't silently lost - the user can retry the sync from there
|
||||
// without re-uploading the image or re-running OCR.
|
||||
ref.read(pendingDocumentsProvider.notifier).markSyncFailed(
|
||||
widget.pendingId!,
|
||||
finalDoc,
|
||||
@@ -291,7 +228,6 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Show SnackBar BEFORE pop so context is still valid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
syncedToServer
|
||||
? const SnackBar(content: Text('Dokumen Berhasil Disimpan!'), backgroundColor: AppConfig.successColor)
|
||||
@@ -301,57 +237,10 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
duration: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
// Pass true as result so DocumentsScreen's .then() knows to reload
|
||||
context.pop(true);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTextField(
|
||||
String label,
|
||||
TextEditingController controller, {
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
bool readOnly = false,
|
||||
VoidCallback? onTap,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
readOnly: readOnly,
|
||||
onTap: onTap,
|
||||
inputFormatters: inputFormatters,
|
||||
style: AppConfig.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppConfig.primaryColor),
|
||||
),
|
||||
),
|
||||
validator: validator ?? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$label harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_document == null) {
|
||||
@@ -370,23 +259,23 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: AppConfig.screenPadding,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: AppConfig.paddingLarge,
|
||||
decoration: BoxDecoration(
|
||||
color: AppConfig.primaryColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
border: Border.all(color: AppConfig.primaryColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: AppConfig.primaryColor),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: AppConfig.spaceMedium),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Tinjau dan edit data hasil ekstraksi di bawah ini untuk memastikan keakuratannya sebelum konfirmasi.',
|
||||
@@ -396,287 +285,49 @@ class _EditorScreenState extends ConsumerState<EditorScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Header Details
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Header', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Tanggal',
|
||||
_tanggalCtrl,
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
DateTime? pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: AppConfig.primaryColor,
|
||||
onPrimary: Colors.white,
|
||||
onSurface: AppConfig.textPrimary,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (pickedDate != null) {
|
||||
String formattedDate = DateFormat('dd MMMM yyyy').format(pickedDate);
|
||||
_tanggalCtrl.text = formattedDate;
|
||||
}
|
||||
}
|
||||
),
|
||||
_buildTextField(
|
||||
'No PO',
|
||||
_noPoCtrl,
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) return 'No PO harus diisi';
|
||||
if (!RegExp(r'^PO/\d{2}/\d{10}$').hasMatch(val)) {
|
||||
return 'Format harus PO/[XX]/[XXXXXXXXXX]';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
),
|
||||
_buildTextField(
|
||||
'No SO',
|
||||
_noSoCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) return 'No SO harus diisi';
|
||||
if (val.length != 10) {
|
||||
return 'SO harus persis 10 angka';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
),
|
||||
_buildTextField('No DO', _noDoCtrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: AppConfig.spaceXL),
|
||||
|
||||
// Shipment Details
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField('Kepada Yth', _kepadaYthCtrl),
|
||||
_buildTextField('Order Untuk', _orderUntukCtrl),
|
||||
_buildTextField('Alamat', _alamatCtrl),
|
||||
_buildTextField('Plat Truk', _platTrukCtrl),
|
||||
_buildTextField('Nama Driver', _namaDriverCtrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Geotagging Details
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Lokasi', style: AppConfig.h2),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Lintang',
|
||||
_latitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
_buildTextField(
|
||||
'Bujur',
|
||||
_longitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Items Section (Editable + CRUD)
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Barang (${_itemControllers.length})', style: AppConfig.h2),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._itemControllers.asMap().entries.map((entry) {
|
||||
int index = entry.key;
|
||||
var ctrls = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: ctrls['nama']!,
|
||||
builder: (context, value, child) {
|
||||
return Text(
|
||||
value.text.isNotEmpty ? value.text : 'Item Baru',
|
||||
style: AppConfig.h3,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () => _removeItem(index),
|
||||
tooltip: 'Hapus Barang',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildTextField(
|
||||
'SKU',
|
||||
ctrls['sku']!,
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'SKU harus diisi';
|
||||
}
|
||||
if (!MasterSku.data.containsKey(val)) {
|
||||
return 'SKU tidak terdaftar';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
_buildTextField('Nama Barang', ctrls['nama']!),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTextField('Banyak', ctrls['banyak']!)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildTextField('Jumlah', ctrls['jumlah']!)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _addItem(),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Tambah Item Baru'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppConfig.primaryColor,
|
||||
side: const BorderSide(color: AppConfig.primaryColor),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
HeaderDetailsCard(
|
||||
tanggalCtrl: _tanggalCtrl,
|
||||
noPoCtrl: _noPoCtrl,
|
||||
noSoCtrl: _noSoCtrl,
|
||||
noDoCtrl: _noDoCtrl,
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
ShipmentDetailsCard(
|
||||
kepadaYthCtrl: _kepadaYthCtrl,
|
||||
orderUntukCtrl: _orderUntukCtrl,
|
||||
alamatCtrl: _alamatCtrl,
|
||||
platTrukCtrl: _platTrukCtrl,
|
||||
namaDriverCtrl: _namaDriverCtrl,
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
|
||||
LocationDetailsCard(
|
||||
latitudeCtrl: _latitudeCtrl,
|
||||
longitudeCtrl: _longitudeCtrl,
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
|
||||
ItemsListCard(
|
||||
itemControllers: _itemControllers,
|
||||
onRemoveItem: _removeItem,
|
||||
onAddItem: () => _addItem(),
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceXL),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: _showConfirmationDialog,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
),
|
||||
child: const Text('Simpan & Konfirmasi'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: _showConfirmationDialog,
|
||||
child: const Text('Selesai Ditinjau', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tanggalCtrl.dispose();
|
||||
_noPoCtrl.dispose();
|
||||
_noSoCtrl.dispose();
|
||||
_noDoCtrl.dispose();
|
||||
_kepadaYthCtrl.dispose();
|
||||
_orderUntukCtrl.dispose();
|
||||
_alamatCtrl.dispose();
|
||||
_platTrukCtrl.dispose();
|
||||
_namaDriverCtrl.dispose();
|
||||
_namaPenerimaCtrl.dispose();
|
||||
_latitudeCtrl.dispose();
|
||||
_longitudeCtrl.dispose();
|
||||
for (var ctrls in _itemControllers) {
|
||||
ctrls['sku']?.dispose();
|
||||
ctrls['nama']?.dispose();
|
||||
ctrls['banyak']?.dispose();
|
||||
ctrls['jumlah']?.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user