157 lines
5.4 KiB
Dart
157 lines
5.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../models/product_model.dart';
|
|
|
|
/// 可変な明細行データを保持するフォームモデル。
|
|
class LineItemFormData {
|
|
LineItemFormData({
|
|
this.id,
|
|
this.productId,
|
|
String? productName,
|
|
int? quantity,
|
|
int? unitPrice,
|
|
this.taxRate,
|
|
int? costAmount,
|
|
bool? costIsProvisional,
|
|
}) : descriptionController = TextEditingController(text: productName ?? ''),
|
|
quantityController = TextEditingController(text: quantity?.toString() ?? ''),
|
|
unitPriceController = TextEditingController(text: unitPrice?.toString() ?? ''),
|
|
costAmount = costAmount ?? 0,
|
|
costIsProvisional = costIsProvisional ?? true;
|
|
|
|
final String? id;
|
|
String? productId;
|
|
final TextEditingController descriptionController;
|
|
final TextEditingController quantityController;
|
|
final TextEditingController unitPriceController;
|
|
double? taxRate;
|
|
int costAmount;
|
|
bool costIsProvisional;
|
|
|
|
bool get hasProduct => productId != null && productId!.isNotEmpty;
|
|
String get description => descriptionController.text;
|
|
int get quantityValue => int.tryParse(quantityController.text) ?? 0;
|
|
int get unitPriceValue => int.tryParse(unitPriceController.text) ?? 0;
|
|
|
|
void applyProduct(Product product) {
|
|
productId = product.id;
|
|
descriptionController.text = product.name;
|
|
if (quantityController.text.trim().isEmpty || quantityController.text.trim() == '0') {
|
|
quantityController.text = '1';
|
|
}
|
|
unitPriceController.text = product.defaultUnitPrice.toString();
|
|
costAmount = product.wholesalePrice;
|
|
costIsProvisional = product.wholesalePrice <= 0;
|
|
}
|
|
|
|
void registerChangeListener(VoidCallback listener) {
|
|
descriptionController.addListener(listener);
|
|
quantityController.addListener(listener);
|
|
unitPriceController.addListener(listener);
|
|
}
|
|
|
|
void removeChangeListener(VoidCallback listener) {
|
|
descriptionController.removeListener(listener);
|
|
quantityController.removeListener(listener);
|
|
unitPriceController.removeListener(listener);
|
|
}
|
|
|
|
void dispose() {
|
|
descriptionController.dispose();
|
|
quantityController.dispose();
|
|
unitPriceController.dispose();
|
|
}
|
|
}
|
|
|
|
/// 明細1行分を編集するカード。仕入/売上どちらの画面でも流用できるよう
|
|
/// 追加のメタ情報やフッターを挿入できるようにしている。
|
|
class LineItemCard extends StatelessWidget {
|
|
const LineItemCard({
|
|
super.key,
|
|
required this.data,
|
|
required this.onPickProduct,
|
|
required this.onRemove,
|
|
this.meta,
|
|
this.footer,
|
|
});
|
|
|
|
final LineItemFormData data;
|
|
final VoidCallback onPickProduct;
|
|
final VoidCallback onRemove;
|
|
final Widget? meta;
|
|
final Widget? footer;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
color: Colors.white,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ListTile(
|
|
dense: true,
|
|
contentPadding: EdgeInsets.zero,
|
|
visualDensity: VisualDensity.compact,
|
|
title: Text(
|
|
data.descriptionController.text.isEmpty ? '商品を選択' : data.descriptionController.text,
|
|
style: theme.textTheme.titleMedium,
|
|
),
|
|
subtitle: data.hasProduct
|
|
? null
|
|
: const Text(
|
|
'商品マスタから選択してください',
|
|
style: TextStyle(color: Colors.redAccent),
|
|
),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [meta, const Icon(Icons.chevron_right)]
|
|
.whereType<Widget>()
|
|
.toList(growable: false),
|
|
),
|
|
onTap: onPickProduct,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: data.quantityController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(
|
|
labelText: '数量',
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
|
),
|
|
scrollPadding: const EdgeInsets.only(bottom: 160),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: data.unitPriceController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(
|
|
labelText: '単価(税抜)',
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
|
),
|
|
scrollPadding: const EdgeInsets.only(bottom: 160),
|
|
),
|
|
),
|
|
IconButton(onPressed: onRemove, icon: const Icon(Icons.close)),
|
|
],
|
|
),
|
|
...[
|
|
footer == null ? null : const SizedBox(height: 8),
|
|
footer,
|
|
].whereType<Widget>(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|