chore: normalize line endings (CRLF -> LF)
No content changes: git diff --ignore-all-space over these files is empty. The churn came from editing on Windows against a repo checked out with LF.
This commit is contained in:
1 parent
15566a6951
commit
caf8e98378
315 files changed
+86950
-86950
No files matched your search
@@ -1,101 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ConfirmationDialog extends StatefulWidget {
|
||||
final TextEditingController nameController;
|
||||
final bool initialAgreed;
|
||||
final void Function(bool, String) onConfirm;
|
||||
|
||||
const ConfirmationDialog({
|
||||
super.key,
|
||||
required this.nameController,
|
||||
required this.initialAgreed,
|
||||
required this.onConfirm,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ConfirmationDialog> createState() => _ConfirmationDialogState();
|
||||
}
|
||||
|
||||
class _ConfirmationDialogState extends State<ConfirmationDialog> {
|
||||
late bool _isAgreed;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isAgreed = widget.initialAgreed;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isButtonEnabled = _isAgreed && widget.nameController.text.trim().isNotEmpty;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
left: AppConfig.spaceXL,
|
||||
right: AppConfig.spaceXL,
|
||||
top: AppConfig.spaceXL,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Konfirmasi Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
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: AppConfig.spaceLarge),
|
||||
TextField(
|
||||
controller: widget.nameController,
|
||||
onChanged: (val) {
|
||||
setState(() {});
|
||||
},
|
||||
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(AppConfig.borderRadiusMedium),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _isAgreed,
|
||||
activeColor: AppConfig.primaryColor,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_isAgreed = val ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text('Saya setuju dan mengonfirmasi bahwa data ini sudah benar.', style: AppConfig.bodyMedium),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceXL),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: isButtonEnabled ? () {
|
||||
widget.onConfirm(_isAgreed, widget.nameController.text.trim());
|
||||
} : null,
|
||||
child: const Text('Konfirmasi'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceXL),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ConfirmationDialog extends StatefulWidget {
|
||||
final TextEditingController nameController;
|
||||
final bool initialAgreed;
|
||||
final void Function(bool, String) onConfirm;
|
||||
|
||||
const ConfirmationDialog({
|
||||
super.key,
|
||||
required this.nameController,
|
||||
required this.initialAgreed,
|
||||
required this.onConfirm,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ConfirmationDialog> createState() => _ConfirmationDialogState();
|
||||
}
|
||||
|
||||
class _ConfirmationDialogState extends State<ConfirmationDialog> {
|
||||
late bool _isAgreed;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isAgreed = widget.initialAgreed;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isButtonEnabled = _isAgreed && widget.nameController.text.trim().isNotEmpty;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
left: AppConfig.spaceXL,
|
||||
right: AppConfig.spaceXL,
|
||||
top: AppConfig.spaceXL,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Konfirmasi Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
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: AppConfig.spaceLarge),
|
||||
TextField(
|
||||
controller: widget.nameController,
|
||||
onChanged: (val) {
|
||||
setState(() {});
|
||||
},
|
||||
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(AppConfig.borderRadiusMedium),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _isAgreed,
|
||||
activeColor: AppConfig.primaryColor,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_isAgreed = val ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text('Saya setuju dan mengonfirmasi bahwa data ini sudah benar.', style: AppConfig.bodyMedium),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceXL),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: isButtonEnabled ? () {
|
||||
widget.onConfirm(_isAgreed, widget.nameController.text.trim());
|
||||
} : null,
|
||||
child: const Text('Konfirmasi'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceXL),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class EditorTextField extends StatelessWidget {
|
||||
final String label;
|
||||
final TextEditingController controller;
|
||||
final TextInputType keyboardType;
|
||||
final bool readOnly;
|
||||
final VoidCallback? onTap;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
const EditorTextField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.controller,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.readOnly = false,
|
||||
this.onTap,
|
||||
this.inputFormatters,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppConfig.spaceLarge),
|
||||
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(AppConfig.borderRadiusMedium),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
borderSide: const BorderSide(color: AppConfig.primaryColor),
|
||||
),
|
||||
),
|
||||
validator: validator ?? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$label harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class EditorTextField extends StatelessWidget {
|
||||
final String label;
|
||||
final TextEditingController controller;
|
||||
final TextInputType keyboardType;
|
||||
final bool readOnly;
|
||||
final VoidCallback? onTap;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
const EditorTextField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.controller,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.readOnly = false,
|
||||
this.onTap,
|
||||
this.inputFormatters,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppConfig.spaceLarge),
|
||||
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(AppConfig.borderRadiusMedium),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
borderSide: const BorderSide(color: AppConfig.primaryColor),
|
||||
),
|
||||
),
|
||||
validator: validator ?? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '$label harus diisi';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class HeaderDetailsCard extends StatelessWidget {
|
||||
final TextEditingController tanggalCtrl;
|
||||
final TextEditingController noPoCtrl;
|
||||
final TextEditingController noSoCtrl;
|
||||
final TextEditingController noDoCtrl;
|
||||
|
||||
const HeaderDetailsCard({
|
||||
super.key,
|
||||
required this.tanggalCtrl,
|
||||
required this.noPoCtrl,
|
||||
required this.noSoCtrl,
|
||||
required this.noDoCtrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Header', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
EditorTextField(
|
||||
label: 'Tanggal',
|
||||
controller: 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;
|
||||
}
|
||||
},
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'No PO',
|
||||
controller: 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;
|
||||
},
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'No SO',
|
||||
controller: 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;
|
||||
},
|
||||
),
|
||||
EditorTextField(label: 'No DO', controller: noDoCtrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class HeaderDetailsCard extends StatelessWidget {
|
||||
final TextEditingController tanggalCtrl;
|
||||
final TextEditingController noPoCtrl;
|
||||
final TextEditingController noSoCtrl;
|
||||
final TextEditingController noDoCtrl;
|
||||
|
||||
const HeaderDetailsCard({
|
||||
super.key,
|
||||
required this.tanggalCtrl,
|
||||
required this.noPoCtrl,
|
||||
required this.noSoCtrl,
|
||||
required this.noDoCtrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Header', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
EditorTextField(
|
||||
label: 'Tanggal',
|
||||
controller: 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;
|
||||
}
|
||||
},
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'No PO',
|
||||
controller: 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;
|
||||
},
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'No SO',
|
||||
controller: 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;
|
||||
},
|
||||
),
|
||||
EditorTextField(label: 'No DO', controller: noDoCtrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../../data/master_sku.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class ItemsListCard extends StatelessWidget {
|
||||
final List<Map<String, TextEditingController>> itemControllers;
|
||||
final void Function(int) onRemoveItem;
|
||||
final VoidCallback onAddItem;
|
||||
|
||||
const ItemsListCard({
|
||||
super.key,
|
||||
required this.itemControllers,
|
||||
required this.onRemoveItem,
|
||||
required this.onAddItem,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Barang (${itemControllers.length})', style: AppConfig.h2),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
...itemControllers.asMap().entries.map((entry) {
|
||||
int index = entry.key;
|
||||
var ctrls = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppConfig.spaceLarge),
|
||||
child: Container(
|
||||
padding: AppConfig.paddingMedium,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
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: () => onRemoveItem(index),
|
||||
tooltip: 'Hapus Barang',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceSmall),
|
||||
EditorTextField(
|
||||
label: 'SKU',
|
||||
controller: ctrls['sku']!,
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'SKU harus diisi';
|
||||
}
|
||||
if (!MasterSku.data.containsKey(val)) {
|
||||
return 'SKU tidak terdaftar';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'Nama Barang',
|
||||
controller: ctrls['nama']!,
|
||||
readOnly: true,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: EditorTextField(
|
||||
label: 'Banyak',
|
||||
controller: ctrls['banyak']!,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppConfig.spaceMedium),
|
||||
Expanded(
|
||||
child: EditorTextField(
|
||||
label: 'Jumlah',
|
||||
controller: ctrls['jumlah']!,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onAddItem,
|
||||
icon: const Icon(Icons.add, color: AppConfig.primaryColor),
|
||||
label: const Text(
|
||||
'Tambah Barang',
|
||||
style: TextStyle(
|
||||
color: AppConfig.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: AppConfig.primaryColor),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: AppConfig.spaceMedium),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../../data/master_sku.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class ItemsListCard extends StatelessWidget {
|
||||
final List<Map<String, TextEditingController>> itemControllers;
|
||||
final void Function(int) onRemoveItem;
|
||||
final VoidCallback onAddItem;
|
||||
|
||||
const ItemsListCard({
|
||||
super.key,
|
||||
required this.itemControllers,
|
||||
required this.onRemoveItem,
|
||||
required this.onAddItem,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Barang (${itemControllers.length})', style: AppConfig.h2),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
...itemControllers.asMap().entries.map((entry) {
|
||||
int index = entry.key;
|
||||
var ctrls = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppConfig.spaceLarge),
|
||||
child: Container(
|
||||
padding: AppConfig.paddingMedium,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
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: () => onRemoveItem(index),
|
||||
tooltip: 'Hapus Barang',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceSmall),
|
||||
EditorTextField(
|
||||
label: 'SKU',
|
||||
controller: ctrls['sku']!,
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'SKU harus diisi';
|
||||
}
|
||||
if (!MasterSku.data.containsKey(val)) {
|
||||
return 'SKU tidak terdaftar';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'Nama Barang',
|
||||
controller: ctrls['nama']!,
|
||||
readOnly: true,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: EditorTextField(
|
||||
label: 'Banyak',
|
||||
controller: ctrls['banyak']!,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppConfig.spaceMedium),
|
||||
Expanded(
|
||||
child: EditorTextField(
|
||||
label: 'Jumlah',
|
||||
controller: ctrls['jumlah']!,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onAddItem,
|
||||
icon: const Icon(Icons.add, color: AppConfig.primaryColor),
|
||||
label: const Text(
|
||||
'Tambah Barang',
|
||||
style: TextStyle(
|
||||
color: AppConfig.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: AppConfig.primaryColor),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: AppConfig.spaceMedium),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class LocationDetailsCard extends StatelessWidget {
|
||||
final TextEditingController latitudeCtrl;
|
||||
final TextEditingController longitudeCtrl;
|
||||
|
||||
const LocationDetailsCard({
|
||||
super.key,
|
||||
required this.latitudeCtrl,
|
||||
required this.longitudeCtrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Lokasi', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
EditorTextField(
|
||||
label: 'Lintang',
|
||||
controller: latitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'Bujur',
|
||||
controller: longitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class LocationDetailsCard extends StatelessWidget {
|
||||
final TextEditingController latitudeCtrl;
|
||||
final TextEditingController longitudeCtrl;
|
||||
|
||||
const LocationDetailsCard({
|
||||
super.key,
|
||||
required this.latitudeCtrl,
|
||||
required this.longitudeCtrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Lokasi', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
EditorTextField(
|
||||
label: 'Lintang',
|
||||
controller: latitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
EditorTextField(
|
||||
label: 'Bujur',
|
||||
controller: longitudeCtrl,
|
||||
readOnly: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,120 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ProductDropdownCard extends StatelessWidget {
|
||||
final String selectedSku;
|
||||
final List<Map<String, dynamic>> matches;
|
||||
final bool hasAutoMatch;
|
||||
final ValueChanged<String?> onSkuChanged;
|
||||
|
||||
const ProductDropdownCard({
|
||||
super.key,
|
||||
required this.selectedSku,
|
||||
required this.matches,
|
||||
required this.hasAutoMatch,
|
||||
required this.onSkuChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final typedMatches = matches.cast<Map<String, dynamic>>().toList();
|
||||
final activeMatch = typedMatches.firstWhere(
|
||||
(m) => m['no_sku'] == selectedSku,
|
||||
orElse: () => typedMatches.isNotEmpty ? typedMatches.first : <String, dynamic>{'no_sku': '', 'nama_item': 'Unknown', 'confidence': 0.0},
|
||||
);
|
||||
final rawConfidence = activeMatch['confidence'] ?? activeMatch['score'] ?? activeMatch['yoloSimilarity'];
|
||||
final double confidence = rawConfidence is num
|
||||
? rawConfidence.toDouble()
|
||||
: (rawConfidence != null
|
||||
? double.tryParse(rawConfidence.toString()) ?? 0.0
|
||||
: 0.0);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('AI Product Classification', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedSku.isNotEmpty && typedMatches.any((m) => m['no_sku'] == selectedSku) ? selectedSku : null,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Produk / SKU',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
isExpanded: true,
|
||||
style: AppConfig.bodyMedium,
|
||||
items: typedMatches.map((match) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: match['no_sku'],
|
||||
child: Text(
|
||||
'${match['no_sku']} - ${match['nama_item']}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onSkuChanged,
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceSmall),
|
||||
// Product name for the selected SKU - informational only, not a
|
||||
// form field (mirrors ProductExpiryCard's "Tanggal terdeteksi
|
||||
// otomatis dari OCR" info row below the batch dropdown).
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.inventory_2_outlined, size: 16, color: AppConfig.textSecondary),
|
||||
const SizedBox(width: AppConfig.spaceXS),
|
||||
Expanded(
|
||||
child: Text(
|
||||
(activeMatch['nama_item'] as String?)?.isNotEmpty == true
|
||||
? activeMatch['nama_item'] as String
|
||||
: '-',
|
||||
style: AppConfig.bodySmall.copyWith(color: AppConfig.textSecondary, fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
if (hasAutoMatch) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Confidence Score', style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary)),
|
||||
Text(
|
||||
'${(confidence * 100).toStringAsFixed(1)}%',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: AppConfig.successColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceSmall),
|
||||
LinearProgressIndicator(
|
||||
value: confidence,
|
||||
color: AppConfig.successColor,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
] else
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 16, color: AppConfig.textSecondary),
|
||||
const SizedBox(width: AppConfig.spaceXS),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Tidak ada rekomendasi otomatis — pilih SKU secara manual.',
|
||||
style: AppConfig.bodySmall.copyWith(color: AppConfig.textSecondary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ProductDropdownCard extends StatelessWidget {
|
||||
final String selectedSku;
|
||||
final List<Map<String, dynamic>> matches;
|
||||
final bool hasAutoMatch;
|
||||
final ValueChanged<String?> onSkuChanged;
|
||||
|
||||
const ProductDropdownCard({
|
||||
super.key,
|
||||
required this.selectedSku,
|
||||
required this.matches,
|
||||
required this.hasAutoMatch,
|
||||
required this.onSkuChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final typedMatches = matches.cast<Map<String, dynamic>>().toList();
|
||||
final activeMatch = typedMatches.firstWhere(
|
||||
(m) => m['no_sku'] == selectedSku,
|
||||
orElse: () => typedMatches.isNotEmpty ? typedMatches.first : <String, dynamic>{'no_sku': '', 'nama_item': 'Unknown', 'confidence': 0.0},
|
||||
);
|
||||
final rawConfidence = activeMatch['confidence'] ?? activeMatch['score'] ?? activeMatch['yoloSimilarity'];
|
||||
final double confidence = rawConfidence is num
|
||||
? rawConfidence.toDouble()
|
||||
: (rawConfidence != null
|
||||
? double.tryParse(rawConfidence.toString()) ?? 0.0
|
||||
: 0.0);
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('AI Product Classification', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedSku.isNotEmpty && typedMatches.any((m) => m['no_sku'] == selectedSku) ? selectedSku : null,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Produk / SKU',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
isExpanded: true,
|
||||
style: AppConfig.bodyMedium,
|
||||
items: typedMatches.map((match) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: match['no_sku'],
|
||||
child: Text(
|
||||
'${match['no_sku']} - ${match['nama_item']}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: onSkuChanged,
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceSmall),
|
||||
// Product name for the selected SKU - informational only, not a
|
||||
// form field (mirrors ProductExpiryCard's "Tanggal terdeteksi
|
||||
// otomatis dari OCR" info row below the batch dropdown).
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.inventory_2_outlined, size: 16, color: AppConfig.textSecondary),
|
||||
const SizedBox(width: AppConfig.spaceXS),
|
||||
Expanded(
|
||||
child: Text(
|
||||
(activeMatch['nama_item'] as String?)?.isNotEmpty == true
|
||||
? activeMatch['nama_item'] as String
|
||||
: '-',
|
||||
style: AppConfig.bodySmall.copyWith(color: AppConfig.textSecondary, fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
if (hasAutoMatch) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Confidence Score', style: AppConfig.bodyMedium.copyWith(color: AppConfig.textSecondary)),
|
||||
Text(
|
||||
'${(confidence * 100).toStringAsFixed(1)}%',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: AppConfig.successColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceSmall),
|
||||
LinearProgressIndicator(
|
||||
value: confidence,
|
||||
color: AppConfig.successColor,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
] else
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 16, color: AppConfig.textSecondary),
|
||||
const SizedBox(width: AppConfig.spaceXS),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Tidak ada rekomendasi otomatis — pilih SKU secara manual.',
|
||||
style: AppConfig.bodySmall.copyWith(color: AppConfig.textSecondary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ProductExpiryCard extends StatelessWidget {
|
||||
final String selectedSku;
|
||||
final String? selectedBatchOption;
|
||||
final bool isManualDate;
|
||||
final Map<String, List<String>> skuBatches;
|
||||
final TextEditingController expDateCtrl;
|
||||
final TextEditingController notesCtrl;
|
||||
final ValueChanged<String?> onBatchChanged;
|
||||
final VoidCallback onSelectDate;
|
||||
|
||||
const ProductExpiryCard({
|
||||
super.key,
|
||||
required this.selectedSku,
|
||||
required this.selectedBatchOption,
|
||||
required this.isManualDate,
|
||||
required this.skuBatches,
|
||||
required this.expDateCtrl,
|
||||
required this.notesCtrl,
|
||||
required this.onBatchChanged,
|
||||
required this.onSelectDate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final batches = skuBatches[selectedSku] ?? [];
|
||||
final dropdownItems = <DropdownMenuItem<String>>[];
|
||||
for (int i = 0; i < batches.length; i++) {
|
||||
dropdownItems.add(DropdownMenuItem(
|
||||
value: 'Batch ${i + 1}: ${batches[i]}',
|
||||
child: Text('Batch ${i + 1}: ${batches[i]}'),
|
||||
));
|
||||
}
|
||||
dropdownItems.add(const DropdownMenuItem(value: 'manual', child: Text('Set Tanggal Manual...')));
|
||||
|
||||
// Ensure value matches an item or falls back
|
||||
String? dropdownValue = isManualDate ? 'manual' : selectedBatchOption;
|
||||
if (dropdownValue != null && dropdownValue != 'manual' && !dropdownItems.any((item) => item.value == dropdownValue)) {
|
||||
dropdownValue = dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('AI OCR Expiry Extraction', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
DropdownButtonFormField<String>(
|
||||
value: dropdownValue,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Batch Expiry',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
isExpanded: true,
|
||||
style: AppConfig.bodyMedium,
|
||||
items: dropdownItems,
|
||||
onChanged: onBatchChanged,
|
||||
),
|
||||
if (isManualDate) ...[
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
TextFormField(
|
||||
controller: expDateCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Expired Date Manual (DD/MM/YYYY)',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.calendar_today, color: AppConfig.primaryColor),
|
||||
onPressed: onSelectDate,
|
||||
),
|
||||
),
|
||||
validator: (val) => val == null || val.isEmpty ? 'Tanggal kadaluwarsa harus diisi' : null,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isManualDate ? Icons.edit_calendar : Icons.auto_awesome,
|
||||
size: 16,
|
||||
color: isManualDate ? Colors.deepOrange : AppConfig.successColor,
|
||||
),
|
||||
const SizedBox(width: AppConfig.spaceXS),
|
||||
Text(
|
||||
isManualDate ? 'Tanggal diinput manual' : 'Tanggal terdeteksi otomatis dari OCR',
|
||||
style: AppConfig.bodySmall.copyWith(
|
||||
color: isManualDate ? Colors.deepOrange : AppConfig.successColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
TextFormField(
|
||||
controller: notesCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Catatan Tambahan'),
|
||||
maxLines: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ProductExpiryCard extends StatelessWidget {
|
||||
final String selectedSku;
|
||||
final String? selectedBatchOption;
|
||||
final bool isManualDate;
|
||||
final Map<String, List<String>> skuBatches;
|
||||
final TextEditingController expDateCtrl;
|
||||
final TextEditingController notesCtrl;
|
||||
final ValueChanged<String?> onBatchChanged;
|
||||
final VoidCallback onSelectDate;
|
||||
|
||||
const ProductExpiryCard({
|
||||
super.key,
|
||||
required this.selectedSku,
|
||||
required this.selectedBatchOption,
|
||||
required this.isManualDate,
|
||||
required this.skuBatches,
|
||||
required this.expDateCtrl,
|
||||
required this.notesCtrl,
|
||||
required this.onBatchChanged,
|
||||
required this.onSelectDate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final batches = skuBatches[selectedSku] ?? [];
|
||||
final dropdownItems = <DropdownMenuItem<String>>[];
|
||||
for (int i = 0; i < batches.length; i++) {
|
||||
dropdownItems.add(DropdownMenuItem(
|
||||
value: 'Batch ${i + 1}: ${batches[i]}',
|
||||
child: Text('Batch ${i + 1}: ${batches[i]}'),
|
||||
));
|
||||
}
|
||||
dropdownItems.add(const DropdownMenuItem(value: 'manual', child: Text('Set Tanggal Manual...')));
|
||||
|
||||
// Ensure value matches an item or falls back
|
||||
String? dropdownValue = isManualDate ? 'manual' : selectedBatchOption;
|
||||
if (dropdownValue != null && dropdownValue != 'manual' && !dropdownItems.any((item) => item.value == dropdownValue)) {
|
||||
dropdownValue = dropdownItems.isNotEmpty ? dropdownItems.first.value : null;
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('AI OCR Expiry Extraction', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
DropdownButtonFormField<String>(
|
||||
value: dropdownValue,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pilih Batch Expiry',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
isExpanded: true,
|
||||
style: AppConfig.bodyMedium,
|
||||
items: dropdownItems,
|
||||
onChanged: onBatchChanged,
|
||||
),
|
||||
if (isManualDate) ...[
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
TextFormField(
|
||||
controller: expDateCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Expired Date Manual (DD/MM/YYYY)',
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.calendar_today, color: AppConfig.primaryColor),
|
||||
onPressed: onSelectDate,
|
||||
),
|
||||
),
|
||||
validator: (val) => val == null || val.isEmpty ? 'Tanggal kadaluwarsa harus diisi' : null,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isManualDate ? Icons.edit_calendar : Icons.auto_awesome,
|
||||
size: 16,
|
||||
color: isManualDate ? Colors.deepOrange : AppConfig.successColor,
|
||||
),
|
||||
const SizedBox(width: AppConfig.spaceXS),
|
||||
Text(
|
||||
isManualDate ? 'Tanggal diinput manual' : 'Tanggal terdeteksi otomatis dari OCR',
|
||||
style: AppConfig.bodySmall.copyWith(
|
||||
color: isManualDate ? Colors.deepOrange : AppConfig.successColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
TextFormField(
|
||||
controller: notesCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Catatan Tambahan'),
|
||||
maxLines: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,47 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ProductPreviewCard extends StatelessWidget {
|
||||
final String imagePath;
|
||||
|
||||
const ProductPreviewCard({super.key, required this.imagePath});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imagePath.isEmpty) return const SizedBox.shrink();
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('AI Crop Region / Preview', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceMedium),
|
||||
Container(
|
||||
height: 120,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
child: Image.file(
|
||||
File(imagePath),
|
||||
fit: BoxFit.cover,
|
||||
alignment: const Alignment(0, 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ProductPreviewCard extends StatelessWidget {
|
||||
final String imagePath;
|
||||
|
||||
const ProductPreviewCard({super.key, required this.imagePath});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (imagePath.isEmpty) return const SizedBox.shrink();
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('AI Crop Region / Preview', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceMedium),
|
||||
Container(
|
||||
height: 120,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
|
||||
child: Image.file(
|
||||
File(imagePath),
|
||||
fit: BoxFit.cover,
|
||||
alignment: const Alignment(0, 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class ShipmentDetailsCard extends StatelessWidget {
|
||||
final TextEditingController kepadaYthCtrl;
|
||||
final TextEditingController orderUntukCtrl;
|
||||
final TextEditingController alamatCtrl;
|
||||
final TextEditingController platTrukCtrl;
|
||||
final TextEditingController namaDriverCtrl;
|
||||
|
||||
const ShipmentDetailsCard({
|
||||
super.key,
|
||||
required this.kepadaYthCtrl,
|
||||
required this.orderUntukCtrl,
|
||||
required this.alamatCtrl,
|
||||
required this.platTrukCtrl,
|
||||
required this.namaDriverCtrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
EditorTextField(label: 'Kepada Yth', controller: kepadaYthCtrl),
|
||||
EditorTextField(label: 'Order Untuk', controller: orderUntukCtrl),
|
||||
EditorTextField(label: 'Alamat', controller: alamatCtrl),
|
||||
EditorTextField(label: 'Plat Truk', controller: platTrukCtrl),
|
||||
EditorTextField(label: 'Nama Driver', controller: namaDriverCtrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
import 'editor_text_field.dart';
|
||||
|
||||
class ShipmentDetailsCard extends StatelessWidget {
|
||||
final TextEditingController kepadaYthCtrl;
|
||||
final TextEditingController orderUntukCtrl;
|
||||
final TextEditingController alamatCtrl;
|
||||
final TextEditingController platTrukCtrl;
|
||||
final TextEditingController namaDriverCtrl;
|
||||
|
||||
const ShipmentDetailsCard({
|
||||
super.key,
|
||||
required this.kepadaYthCtrl,
|
||||
required this.orderUntukCtrl,
|
||||
required this.alamatCtrl,
|
||||
required this.platTrukCtrl,
|
||||
required this.namaDriverCtrl,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: AppConfig.paddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Detail Pengiriman', style: AppConfig.h2),
|
||||
const SizedBox(height: AppConfig.spaceLarge),
|
||||
EditorTextField(label: 'Kepada Yth', controller: kepadaYthCtrl),
|
||||
EditorTextField(label: 'Order Untuk', controller: orderUntukCtrl),
|
||||
EditorTextField(label: 'Alamat', controller: alamatCtrl),
|
||||
EditorTextField(label: 'Plat Truk', controller: platTrukCtrl),
|
||||
EditorTextField(label: 'Nama Driver', controller: namaDriverCtrl),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user