feat: 完成7项功能任务

后端:
- fix(backend): InventoryLog 增加 Product/Warehouse 关联,Logs 接口加 Preload 修复流水记录显示"-"
- feat(backend): 新增 /inventory/batches 批次追踪接口
- feat(backend): 新增财务记录、编号规则接口(finance、number_rule handler)
- fix(backend): service/stock_test.go 修复 model.Date 类型错误

前端:
- feat(client): 入库/出库管理 Tab 调换顺序(审核在前),审核 Tab 加新建按钮,列表 Tab 无按钮
- feat(client): 入库单/出库单增加详情弹窗,显示完整单据信息和商品明细
- feat(client): 完善出库单新建表单(StockOutFormScreen),支持选客户/仓库/商品
- fix(client): StockInItem/StockOutItem.fromJson 修复读取嵌套 product 对象而非平铺字段
- feat(client): 批次追踪页面(BatchTrackingScreen)及侧边栏入口
- feat(client): 财务管理、盘点、编号规则接入真实后端 API
- fix(client): 入库/出库审核通过后 invalidate inventoryListProvider 刷新库存

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-10 22:03:19 +08:00
parent 5f8e7cfa7e
commit ce1cbf404c
28 changed files with 2507 additions and 1072 deletions
@@ -0,0 +1,581 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../models/product.dart';
import '../../providers/partner_provider.dart';
import '../../providers/product_provider.dart';
import '../../providers/stock_out_provider.dart';
import '../../providers/warehouse_provider.dart';
class StockOutFormScreen extends ConsumerStatefulWidget {
const StockOutFormScreen({super.key});
@override
ConsumerState<StockOutFormScreen> createState() => _StockOutFormScreenState();
}
class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
final _formKey = GlobalKey<FormState>();
final _remarkCtrl = TextEditingController();
int? _warehouseId;
int? _partnerId;
DateTime _orderDate = DateTime.now();
bool _submitting = false;
final List<_ItemRow> _items = [];
@override
void initState() {
super.initState();
_items.add(_ItemRow());
}
@override
void dispose() {
_remarkCtrl.dispose();
for (final item in _items) {
item.dispose();
}
super.dispose();
}
double get _totalAmount {
double total = 0;
for (final item in _items) {
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
total += qty * price;
}
return total;
}
void _addItem() {
setState(() => _items.add(_ItemRow()));
}
void _removeItem(int index) {
setState(() {
_items[index].dispose();
_items.removeAt(index);
});
}
Future<void> _submit(bool asDraft) async {
if (!asDraft && !_formKey.currentState!.validate()) return;
if (_warehouseId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请选择出库仓库'), backgroundColor: AppTheme.danger),
);
return;
}
if (_items.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请添加商品明细'), backgroundColor: AppTheme.danger),
);
return;
}
setState(() => _submitting = true);
final itemsData = _items.map((item) {
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
return {
'product_id': item.productId ?? 0,
'quantity': qty,
'unit_price': price,
'total_price': qty * price,
};
}).toList();
final data = {
'warehouse_id': _warehouseId,
if (_partnerId != null) 'partner_id': _partnerId,
'order_date':
'${_orderDate.year}-${_orderDate.month.toString().padLeft(2, '0')}-${_orderDate.day.toString().padLeft(2, '0')}',
if (_remarkCtrl.text.trim().isNotEmpty) 'remark': _remarkCtrl.text.trim(),
'items': itemsData,
'status': asDraft ? 'draft' : 'pending',
};
try {
await ref.read(stockOutListProvider.notifier).createOrder(data);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(asDraft ? '已保存为草稿' : '出库单已提交审核'),
backgroundColor: AppTheme.success,
),
);
context.go('/stock-out');
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('操作失败:$e'), backgroundColor: AppTheme.danger),
);
}
} finally {
if (mounted) setState(() => _submitting = false);
}
}
@override
Widget build(BuildContext context) {
final asyncWarehouses = ref.watch(warehouseListProvider);
final asyncCustomers = ref.watch(customerListProvider);
return Scaffold(
backgroundColor: AppTheme.background,
body: Column(
children: [
Container(
height: 52,
color: AppTheme.surface,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, size: 20),
onPressed: () => context.go('/stock-out'),
tooltip: '返回',
),
const SizedBox(width: 8),
const Text('新建出库单',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600)),
const Spacer(),
OutlinedButton(
onPressed: _submitting ? null : () => _submit(true),
child: const Text('保存草稿'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: _submitting ? null : () => _submit(false),
icon: _submitting
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Icon(Icons.send, size: 16),
label: const Text('提交审核'),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () => context.go('/stock-out'),
icon: const Icon(Icons.cancel_outlined, size: 16),
label: const Text('取消'),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('基本信息',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
const SizedBox(height: 16),
Wrap(
spacing: 16,
runSpacing: 16,
children: [
// Warehouse dropdown
_FormField(
label: '出库仓库',
required: true,
child: asyncWarehouses.when(
loading: () =>
const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
data: (warehouses) =>
DropdownButtonFormField<int>(
value: _warehouseId,
hint: const Text('请选择仓库',
style:
TextStyle(fontSize: 13)),
items: warehouses
.map((w) => DropdownMenuItem(
value: w.id,
child: Text(w.name,
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) => setState(
() => _warehouseId = v),
validator: (v) =>
v == null ? '不能为空' : null,
decoration: const InputDecoration(),
),
),
),
// Customer dropdown
_FormField(
label: '客户',
child: asyncCustomers.when(
loading: () =>
const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
data: (result) =>
DropdownButtonFormField<int>(
value: _partnerId,
hint: const Text('请选择客户',
style:
TextStyle(fontSize: 13)),
items: result.data
.map((p) => DropdownMenuItem(
value: p.id,
child: Text(p.name,
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) => setState(
() => _partnerId = v),
decoration: const InputDecoration(),
),
),
),
// Date picker
_FormField(
label: '出库日期',
required: true,
child: InkWell(
onTap: _pickDate,
child: InputDecorator(
decoration:
const InputDecoration(),
child: Row(
children: [
Expanded(
child: Text(
'${_orderDate.year}-${_orderDate.month.toString().padLeft(2, '0')}-${_orderDate.day.toString().padLeft(2, '0')}',
style: const TextStyle(
fontSize: 13),
),
),
const Icon(
Icons.calendar_today,
size: 16,
color:
AppTheme.textSecondary),
],
),
),
),
),
],
),
const SizedBox(height: 16),
_FormField(
label: '备注',
width: double.infinity,
child: TextFormField(
controller: _remarkCtrl,
maxLines: 2,
decoration: const InputDecoration(
hintText: '选填,如有特殊说明请在此注明',
),
),
),
],
),
),
),
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text('商品明细',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
const Spacer(),
ElevatedButton.icon(
onPressed: _addItem,
icon: const Icon(Icons.add, size: 16),
label: const Text('添加商品'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 32)),
),
],
),
const SizedBox(height: 12),
Table(
columnWidths: const {
0: FixedColumnWidth(36),
1: FlexColumnWidth(3),
2: FlexColumnWidth(1.5),
3: FlexColumnWidth(1.5),
4: FlexColumnWidth(1.5),
5: FixedColumnWidth(60),
},
children: [
TableRow(
decoration: const BoxDecoration(
color: Color(0xFFF0F4FF)),
children: [
'序号',
'商品',
'数量',
'单价',
'金额',
'操作',
]
.map((h) => Padding(
padding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 10),
child: Text(h,
style: const TextStyle(
fontSize: 13,
fontWeight:
FontWeight.w600,
color: AppTheme
.primaryDark)),
))
.toList(),
),
...List.generate(
_items.length,
(i) => _buildItemRow(i)),
],
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text('合计金额:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500)),
Text(
'¥${_totalAmount.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppTheme.danger),
),
],
),
),
],
),
),
),
],
),
),
),
),
],
),
);
}
TableRow _buildItemRow(int index) {
final item = _items[index];
final asyncProducts = ref.watch(productListProvider);
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
final amount = qty * price;
return TableRow(
decoration: BoxDecoration(
color: index.isEven ? Colors.white : const Color(0xFFFAFAFA),
),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text('${index + 1}',
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
),
Padding(
padding: const EdgeInsets.all(4),
child: asyncProducts.when(
loading: () => const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
data: (result) => DropdownButtonFormField<int>(
value: item.productId,
hint: const Text('选择商品',
style: TextStyle(fontSize: 13)),
items: result.data
.map((p) => DropdownMenuItem(
value: p.id,
child: Text('${p.name}${p.spec ?? p.unit}',
style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: (v) {
setState(() {
item.productId = v;
// Auto-fill sale price if available
if (v != null) {
final found = result.data.firstWhere(
(p) => p.id == v,
orElse: () => Product(
id: 0, code: '', name: '', unit: ''));
if (found.salePrice != null) {
item.priceCtrl.text =
found.salePrice!.toStringAsFixed(2);
}
}
});
},
validator: (v) => v == null ? '不能为空' : null,
decoration: const InputDecoration(),
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: item.qtyCtrl,
decoration: const InputDecoration(hintText: '0'),
style: const TextStyle(fontSize: 13),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
onChanged: (_) => setState(() {}),
validator: (v) {
if (v == null || v.isEmpty) return '不能为空';
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
return null;
},
),
),
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: item.priceCtrl,
decoration: const InputDecoration(
hintText: '0.00', prefixText: '¥'),
style: const TextStyle(fontSize: 13),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
onChanged: (_) => setState(() {}),
validator: (v) {
if (v == null || v.isEmpty) return '不能为空';
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text(
'¥${amount.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 13, fontWeight: FontWeight.w500),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
child: IconButton(
icon: const Icon(Icons.delete_outline,
size: 18, color: AppTheme.danger),
onPressed:
_items.length > 1 ? () => _removeItem(index) : null,
tooltip: '删除',
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 28, minHeight: 28),
),
),
],
);
}
Future<void> _pickDate() async {
final date = await showDatePicker(
context: context,
initialDate: _orderDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (date != null) setState(() => _orderDate = date);
}
}
class _ItemRow {
int? productId;
final TextEditingController qtyCtrl = TextEditingController();
final TextEditingController priceCtrl = TextEditingController();
void dispose() {
qtyCtrl.dispose();
priceCtrl.dispose();
}
}
class _FormField extends StatelessWidget {
final String label;
final Widget child;
final bool required;
final double width;
const _FormField({
required this.label,
required this.child,
this.required = false,
this.width = 240,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (required)
const Text('*',
style:
TextStyle(color: AppTheme.danger, fontSize: 13)),
Text(label,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
],
),
const SizedBox(height: 6),
child,
],
),
);
}
}
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../models/stock_out.dart';
import '../../providers/stock_out_provider.dart';
import '../../repositories/stock_out_repository.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
@@ -45,17 +47,17 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
return PageScaffold(
title: '出库管理',
tabs: const [
Tab(text: '出库单'),
Tab(text: '出库审核'),
Tab(text: '出库单'),
],
tabViews: [
_buildListTab(filterStatus: null),
_buildListTab(filterStatus: 'pending'),
_buildListTab(filterStatus: 'pending', showNewButton: true),
_buildListTab(filterStatus: null, showNewButton: false),
],
);
}
Widget _buildListTab({String? filterStatus}) {
Widget _buildListTab({String? filterStatus, required bool showNewButton}) {
final asyncOrders = ref.watch(stockOutListProvider);
return asyncOrders.when(
loading: () => const Center(child: CircularProgressIndicator()),
@@ -85,6 +87,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
totalCount: filterStatus != null ? orders.length : result.total,
page: result.page,
showStatusFilter: filterStatus == null,
showNewButton: showNewButton,
);
},
);
@@ -95,6 +98,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
required int totalCount,
required int page,
required bool showStatusFilter,
required bool showNewButton,
}) {
return DataTableCard(
totalCount: totalCount,
@@ -103,11 +107,12 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
ref.read(stockOutListProvider.notifier).setPage(p),
toolbar: Row(
children: [
ElevatedButton.icon(
onPressed: () => _showCreateDialog(context),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建出库单'),
),
if (showNewButton)
ElevatedButton.icon(
onPressed: () => context.go('/stock-out/new'),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建出库审核单'),
),
const Spacer(),
if (showStatusFilter) ...[
_StatusFilterDropdown(
@@ -186,6 +191,13 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _showDetail(context, o.id),
child: const Text('详情',
style: TextStyle(
fontSize: 12,
color: AppTheme.primary)),
),
if (o.status == 'draft')
TextButton(
onPressed: () =>
@@ -223,6 +235,16 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
);
}
Future<void> _showDetail(BuildContext context, int orderId) async {
showDialog(
context: context,
builder: (ctx) => _StockOutDetailDialog(
orderId: orderId,
repository: ref.read(stockOutRepositoryProvider),
),
);
}
Future<void> _pickDateRange() async {
final range = await showDateRangePicker(
context: context,
@@ -238,24 +260,6 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
}
}
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新建出库单'),
content: const SizedBox(
width: 400,
child: Text('出库单完整创建功能请参考入库单流程。'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消')),
],
),
);
}
Future<void> _confirmSubmit(
BuildContext context, StockOutOrder o) async {
final confirmed = await showDialog<bool>(
@@ -324,7 +328,6 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
content: Text('审核通过'), backgroundColor: AppTheme.success));
}
} catch (e) {
// Show inventory-shortage error clearly
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('审核失败:$e'),
@@ -376,6 +379,217 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
}
}
// Detail dialog — fetches full order with items
class _StockOutDetailDialog extends StatefulWidget {
final int orderId;
final StockOutRepository repository;
const _StockOutDetailDialog({
required this.orderId,
required this.repository,
});
@override
State<_StockOutDetailDialog> createState() => _StockOutDetailDialogState();
}
class _StockOutDetailDialogState extends State<_StockOutDetailDialog> {
late Future<StockOutOrder> _future;
@override
void initState() {
super.initState();
_future = widget.repository.get(widget.orderId);
}
@override
Widget build(BuildContext context) {
return Dialog(
child: Container(
width: 720,
constraints: const BoxConstraints(maxHeight: 600),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: const BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
const Text('出库单详情',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
Expanded(
child: FutureBuilder<StockOutOrder>(
future: _future,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return Center(
child: Text('加载失败:${snap.error}',
style: const TextStyle(color: AppTheme.danger)));
}
return _buildContent(snap.data!);
},
),
),
],
),
),
);
}
Widget _buildContent(StockOutOrder o) {
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 32,
runSpacing: 12,
children: [
_InfoField('出库单号', o.orderNo),
_InfoField('状态', _statusLabel(o.status)),
_InfoField('客户/往来单位', o.partnerName ?? '-'),
_InfoField('仓库', o.warehouseName ?? '-'),
_InfoField('出库日期', o.orderDate?.substring(0, 10) ?? '-'),
_InfoField('合计金额',
o.totalAmount != null ? '¥${o.totalAmount!.toStringAsFixed(2)}' : '-'),
if (o.remark != null && o.remark!.isNotEmpty)
_InfoField('备注', o.remark!),
],
),
const SizedBox(height: 20),
const Text('商品明细',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
const SizedBox(height: 8),
if (o.items.isEmpty)
const Text('无商品明细',
style: TextStyle(color: AppTheme.textSecondary))
else
Table(
border: TableBorder.all(color: AppTheme.border, width: 0.5),
columnWidths: const {
0: FixedColumnWidth(36),
1: FlexColumnWidth(3),
2: FlexColumnWidth(1.5),
3: FlexColumnWidth(1.5),
4: FlexColumnWidth(1.5),
5: FlexColumnWidth(1.5),
},
children: [
TableRow(
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
children: ['序号', '商品', '规格', '数量', '单价', '金额']
.map((h) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 8),
child: Text(h,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppTheme.primaryDark)),
))
.toList(),
),
...o.items.asMap().entries.map((e) {
final i = e.key;
final item = e.value;
return TableRow(
decoration: BoxDecoration(
color: i.isEven ? Colors.white : const Color(0xFFFAFAFA)),
children: [
_TableCell('${i + 1}'),
_TableCell(item.productName ?? '-'),
_TableCell(item.productSpec ?? '-'),
_TableCell(item.quantity.toStringAsFixed(3)),
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'),
],
);
}),
],
),
],
),
);
}
String _statusLabel(String status) {
switch (status) {
case 'draft':
return '草稿';
case 'pending':
return '待审核';
case 'approved':
return '已审核';
case 'rejected':
return '已拒绝';
default:
return status;
}
}
}
class _InfoField extends StatelessWidget {
final String label;
final String value;
const _InfoField(this.label, this.value);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
const SizedBox(height: 2),
Text(value,
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.w500)),
],
);
}
}
class _TableCell extends StatelessWidget {
final String text;
const _TableCell(this.text);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text(text, style: const TextStyle(fontSize: 13)),
);
}
}
class _StatusFilterDropdown extends StatelessWidget {
final String value;
final ValueChanged<String?> onChanged;