h-1.flutter.0/lib/widgets/line_item_editor.dart
2026-03-05 09:33:38 +09:00

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>(),
],
),
),
);
}
}