Update: lib/screens/add_edit_product_screen.dart
This commit is contained in:
parent
697ed887f1
commit
c5ca9acdac
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import '../models/product.dart';
|
import '../models/product.dart';
|
||||||
import '../services/database_helper.dart';
|
import '../services/database_helper.dart';
|
||||||
|
|
||||||
|
@ -18,6 +19,30 @@ class _AddEditProductScreenState extends State<AddEditProductScreen> {
|
||||||
late TextEditingController _quantityController;
|
late TextEditingController _quantityController;
|
||||||
late TextEditingController _priceController;
|
late TextEditingController _priceController;
|
||||||
late TextEditingController _descriptionController;
|
late TextEditingController _descriptionController;
|
||||||
|
|
||||||
|
late TextEditingController _imageUrlController;
|
||||||
|
late TextEditingController _serialNumberController;
|
||||||
|
late TextEditingController _modelController;
|
||||||
|
late TextEditingController _tagsController;
|
||||||
|
|
||||||
|
late TextEditingController _costPriceController;
|
||||||
|
late TextEditingController _taxRateController;
|
||||||
|
late TextEditingController _discountController;
|
||||||
|
late TextEditingController _profitMarginController;
|
||||||
|
|
||||||
|
late TextEditingController _unitOfMeasureController;
|
||||||
|
late TextEditingController _locationController;
|
||||||
|
late TextEditingController _minimumStockController;
|
||||||
|
late TextEditingController _supplierController;
|
||||||
|
|
||||||
|
late TextEditingController _entryDateController;
|
||||||
|
late TextEditingController _manufactureDateController;
|
||||||
|
late TextEditingController _expirationDateController;
|
||||||
|
late TextEditingController _statusController;
|
||||||
|
|
||||||
|
late TextEditingController _categoryController;
|
||||||
|
late TextEditingController _brandController;
|
||||||
|
|
||||||
late DatabaseHelper _databaseHelper;
|
late DatabaseHelper _databaseHelper;
|
||||||
|
|
||||||
bool get isEditing => widget.product != null;
|
bool get isEditing => widget.product != null;
|
||||||
|
@ -26,17 +51,37 @@ class _AddEditProductScreenState extends State<AddEditProductScreen> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_databaseHelper = DatabaseHelper();
|
_databaseHelper = DatabaseHelper();
|
||||||
|
|
||||||
_nameController = TextEditingController(text: widget.product?.name);
|
_nameController = TextEditingController(text: widget.product?.name);
|
||||||
_codeController = TextEditingController(text: widget.product?.code);
|
_codeController = TextEditingController(text: widget.product?.code);
|
||||||
_quantityController = TextEditingController(
|
_descriptionController = TextEditingController(text: widget.product?.description);
|
||||||
text: widget.product?.quantity.toString(),
|
_imageUrlController = TextEditingController(text: widget.product?.imageUrl);
|
||||||
);
|
_serialNumberController = TextEditingController(text: widget.product?.serialNumber);
|
||||||
_priceController = TextEditingController(
|
_modelController = TextEditingController(text: widget.product?.model);
|
||||||
text: widget.product?.price.toString(),
|
_tagsController = TextEditingController(text: widget.product?.tags?.join(', '));
|
||||||
);
|
|
||||||
_descriptionController = TextEditingController(
|
_priceController = TextEditingController(text: widget.product?.price.toString());
|
||||||
text: widget.product?.description,
|
_costPriceController = TextEditingController(text: widget.product?.costPrice?.toString());
|
||||||
);
|
_taxRateController = TextEditingController(text: widget.product?.taxRate?.toString());
|
||||||
|
_discountController = TextEditingController(text: widget.product?.discount?.toString());
|
||||||
|
_profitMarginController = TextEditingController(text: widget.product?.profitMargin?.toString());
|
||||||
|
|
||||||
|
_quantityController = TextEditingController(text: widget.product?.quantity.toString());
|
||||||
|
_unitOfMeasureController = TextEditingController(text: widget.product?.unitOfMeasure);
|
||||||
|
_locationController = TextEditingController(text: widget.product?.location);
|
||||||
|
_minimumStockController = TextEditingController(text: widget.product?.minimumStock?.toString());
|
||||||
|
_supplierController = TextEditingController(text: widget.product?.supplier);
|
||||||
|
|
||||||
|
_entryDateController = TextEditingController(
|
||||||
|
text: widget.product?.entryDate != null ? DateFormat('yyyy-MM-dd').format(widget.product!.entryDate!) : '');
|
||||||
|
_manufactureDateController = TextEditingController(
|
||||||
|
text: widget.product?.manufactureDate != null ? DateFormat('yyyy-MM-dd').format(widget.product!.manufactureDate!) : '');
|
||||||
|
_expirationDateController = TextEditingController(
|
||||||
|
text: widget.product?.expirationDate != null ? DateFormat('yyyy-MM-dd').format(widget.product!.expirationDate!) : '');
|
||||||
|
_statusController = TextEditingController(text: widget.product?.status);
|
||||||
|
|
||||||
|
_categoryController = TextEditingController(text: widget.product?.category);
|
||||||
|
_brandController = TextEditingController(text: widget.product?.brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -46,9 +91,47 @@ class _AddEditProductScreenState extends State<AddEditProductScreen> {
|
||||||
_quantityController.dispose();
|
_quantityController.dispose();
|
||||||
_priceController.dispose();
|
_priceController.dispose();
|
||||||
_descriptionController.dispose();
|
_descriptionController.dispose();
|
||||||
|
|
||||||
|
_imageUrlController.dispose();
|
||||||
|
_serialNumberController.dispose();
|
||||||
|
_modelController.dispose();
|
||||||
|
_tagsController.dispose();
|
||||||
|
|
||||||
|
_costPriceController.dispose();
|
||||||
|
_taxRateController.dispose();
|
||||||
|
_discountController.dispose();
|
||||||
|
_profitMarginController.dispose();
|
||||||
|
|
||||||
|
_unitOfMeasureController.dispose();
|
||||||
|
_locationController.dispose();
|
||||||
|
_minimumStockController.dispose();
|
||||||
|
_supplierController.dispose();
|
||||||
|
|
||||||
|
_entryDateController.dispose();
|
||||||
|
_manufactureDateController.dispose();
|
||||||
|
_expirationDateController.dispose();
|
||||||
|
_statusController.dispose();
|
||||||
|
|
||||||
|
_categoryController.dispose();
|
||||||
|
_brandController.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDate(TextEditingController controller) async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2101),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
controller.text = DateFormat('yyyy-MM-dd').format(picked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveProduct() async {
|
Future<void> _saveProduct() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
|
@ -57,12 +140,36 @@ class _AddEditProductScreenState extends State<AddEditProductScreen> {
|
||||||
id: isEditing ? widget.product!.id : null,
|
id: isEditing ? widget.product!.id : null,
|
||||||
name: _nameController.text,
|
name: _nameController.text,
|
||||||
code: _codeController.text,
|
code: _codeController.text,
|
||||||
quantity: int.parse(_quantityController.text),
|
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
|
||||||
|
imageUrl: _imageUrlController.text.isEmpty ? null : _imageUrlController.text,
|
||||||
|
serialNumber: _serialNumberController.text.isEmpty ? null : _serialNumberController.text,
|
||||||
|
model: _modelController.text.isEmpty ? null : _modelController.text,
|
||||||
|
tags: _tagsController.text.isEmpty ? null : _tagsController.text.split(',').map((e) => e.trim()).toList(),
|
||||||
|
|
||||||
price: double.parse(_priceController.text),
|
price: double.parse(_priceController.text),
|
||||||
description:
|
costPrice: double.tryParse(_costPriceController.text),
|
||||||
_descriptionController.text.isEmpty
|
taxRate: double.tryParse(_taxRateController.text),
|
||||||
? null
|
discount: double.tryParse(_discountController.text),
|
||||||
: _descriptionController.text,
|
profitMargin: double.tryParse(_profitMarginController.text),
|
||||||
|
|
||||||
|
quantity: int.parse(_quantityController.text),
|
||||||
|
unitOfMeasure: _unitOfMeasureController.text.isEmpty ? null : _unitOfMeasureController.text,
|
||||||
|
location: _locationController.text.isEmpty ? null : _locationController.text,
|
||||||
|
minimumStock: int.tryParse(_minimumStockController.text),
|
||||||
|
supplier: _supplierController.text.isEmpty ? null : _supplierController.text,
|
||||||
|
|
||||||
|
entryDate: _entryDateController.text.isNotEmpty ? DateTime.parse(_entryDateController.text) : null,
|
||||||
|
manufactureDate: _manufactureDateController.text.isNotEmpty ? DateTime.parse(_manufactureDateController.text) : null,
|
||||||
|
expirationDate: _expirationDateController.text.isNotEmpty ? DateTime.parse(_expirationDateController.text) : null,
|
||||||
|
status: _statusController.text.isEmpty ? null : _statusController.text,
|
||||||
|
|
||||||
|
category: _categoryController.text.isEmpty ? null : _categoryController.text,
|
||||||
|
brand: _brandController.text.isEmpty ? null : _brandController.text,
|
||||||
|
|
||||||
|
createdBy: isEditing ? widget.product?.createdBy : null,
|
||||||
|
createdAt: isEditing ? widget.product?.createdAt : null,
|
||||||
|
lastModifiedBy: isEditing ? widget.product?.lastModifiedBy : null,
|
||||||
|
lastModifiedAt: isEditing ? widget.product?.lastModifiedAt : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
@ -89,74 +196,182 @@ class _AddEditProductScreenState extends State<AddEditProductScreen> {
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
_buildSectionTitle('مشخصات شناسایی و ظاهری'),
|
||||||
controller: _nameController,
|
_buildTextFormField(
|
||||||
decoration: const InputDecoration(labelText: 'نام کالا'),
|
controller: _nameController,
|
||||||
validator: (value) {
|
labelText: 'نام کالا',
|
||||||
if (value == null || value.isEmpty) {
|
validator: (value) => value!.isEmpty ? 'لطفا نام کالا را وارد کنید' : null),
|
||||||
return 'لطفا نام کالا را وارد کنید';
|
_buildTextFormField(
|
||||||
}
|
controller: _codeController,
|
||||||
return null;
|
labelText: 'کد کالا',
|
||||||
},
|
validator: (value) => value!.isEmpty ? 'لطفا کد کالا را وارد کنید' : null),
|
||||||
),
|
_buildTextFormField(
|
||||||
const SizedBox(height: 10),
|
controller: _descriptionController,
|
||||||
TextFormField(
|
|
||||||
controller: _codeController,
|
|
||||||
decoration: const InputDecoration(labelText: 'کد کالا'),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'لطفا کد کالا را وارد کنید';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextFormField(
|
|
||||||
controller: _quantityController,
|
|
||||||
decoration: const InputDecoration(labelText: 'تعداد'),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'لطفا تعداد را وارد کنید';
|
|
||||||
}
|
|
||||||
if (int.tryParse(value) == null) {
|
|
||||||
return 'تعداد باید عدد باشد';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextFormField(
|
|
||||||
controller: _priceController,
|
|
||||||
decoration: const InputDecoration(labelText: 'قیمت'),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'لطفا قیمت را وارد کنید';
|
|
||||||
}
|
|
||||||
if (double.tryParse(value) == null) {
|
|
||||||
return 'قیمت باید عدد باشد';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
TextFormField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'توضیحات (اختیاری)',
|
labelText: 'توضیحات (اختیاری)',
|
||||||
),
|
maxLines: 3),
|
||||||
maxLines: 3,
|
_buildTextFormField(
|
||||||
),
|
controller: _imageUrlController,
|
||||||
|
labelText: 'آدرس عکس کالا (URL/مسیر)',
|
||||||
|
keyboardType: TextInputType.url),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _serialNumberController,
|
||||||
|
labelText: 'شماره سریال (اختیاری)'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _modelController,
|
||||||
|
labelText: 'شماره مدل (اختیاری)'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _tagsController,
|
||||||
|
labelText: 'هشتگها (با کاما جدا کنید)',
|
||||||
|
hintText: 'مثال: لوازم، برقی، آشپزخانه'),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_buildSectionTitle('اطلاعات مالی و تجاری'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _priceController,
|
||||||
|
labelText: 'قیمت فروش (تومان)',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) => value!.isEmpty || double.tryParse(value) == null ? 'لطفا قیمت معتبر وارد کنید' : null),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _costPriceController,
|
||||||
|
labelText: 'قیمت خرید / بهای تمام شده (اختیاری)',
|
||||||
|
keyboardType: TextInputType.number),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _taxRateController,
|
||||||
|
labelText: 'نرخ مالیات (مثلاً 0.09 برای 9%) (اختیاری)',
|
||||||
|
keyboardType: TextInputType.number),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _discountController,
|
||||||
|
labelText: 'میزان تخفیف (درصد یا مبلغ) (اختیاری)',
|
||||||
|
keyboardType: TextInputType.number),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _profitMarginController,
|
||||||
|
labelText: 'درصد سود مورد انتظار (اختیاری)',
|
||||||
|
keyboardType: TextInputType.number),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_buildSectionTitle('موجودی و لجستیک'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _quantityController,
|
||||||
|
labelText: 'تعداد',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) => value!.isEmpty || int.tryParse(value) == null ? 'لطفا تعداد معتبر وارد کنید' : null),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _unitOfMeasureController,
|
||||||
|
labelText: 'واحد اندازهگیری (مثلاً عدد، کیلوگرم) (اختیاری)'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _locationController,
|
||||||
|
labelText: 'مکان فیزیکی در انبار (اختیاری)'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _minimumStockController,
|
||||||
|
labelText: 'حداقل موجودی برای سفارش مجدد (اختیاری)',
|
||||||
|
keyboardType: TextInputType.number),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _supplierController,
|
||||||
|
labelText: 'تامینکننده (اختیاری)'),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_buildSectionTitle('تاریخگذاری و وضعیت'),
|
||||||
|
_buildDateField(
|
||||||
|
controller: _entryDateController,
|
||||||
|
labelText: 'تاریخ ورود/ثبت (اختیاری)'),
|
||||||
|
_buildDateField(
|
||||||
|
controller: _manufactureDateController,
|
||||||
|
labelText: 'تاریخ تولید (اختیاری)'),
|
||||||
|
_buildDateField(
|
||||||
|
controller: _expirationDateController,
|
||||||
|
labelText: 'تاریخ انقضا (اختیاری)'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _statusController,
|
||||||
|
labelText: 'وضعیت کالا (مثلاً موجود، ناموجود) (اختیاری)'),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_buildSectionTitle('دستهبندی و طبقهبندی'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _categoryController,
|
||||||
|
labelText: 'دسته بندی (مثلاً الکترونیک) (اختیاری)'),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _brandController,
|
||||||
|
labelText: 'برند/شرکت سازنده (اختیاری)'),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _saveProduct,
|
onPressed: _saveProduct,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
textStyle: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
child: Text(isEditing ? 'ذخیره تغییرات' : 'افزودن کالا'),
|
child: Text(isEditing ? 'ذخیره تغییرات' : 'افزودن کالا'),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Widget _buildTextFormField({
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String labelText,
|
||||||
|
String? Function(String?)? validator,
|
||||||
|
TextInputType keyboardType = TextInputType.text,
|
||||||
|
int maxLines = 1,
|
||||||
|
String? hintText,
|
||||||
|
bool readOnly = false,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: labelText,
|
||||||
|
hintText: hintText,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
|
),
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
maxLines: maxLines,
|
||||||
|
validator: validator,
|
||||||
|
readOnly: readOnly,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateField({
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String labelText,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: labelText,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
|
),
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(controller),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionTitle(String title) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10, top: 10),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue