재고 상세 다이얼로그화 및 마스터 레이아웃 개선

This commit is contained in:
JiWoong Sul
2025-10-22 18:52:21 +09:00
parent a14133df52
commit 09c31b2503
20 changed files with 1187 additions and 923 deletions

View File

@@ -28,8 +28,10 @@ import 'package:superport_v2/features/inventory/transactions/domain/entities/sto
import 'package:superport_v2/features/inventory/transactions/domain/entities/stock_transaction_input.dart';
import 'package:superport_v2/features/inventory/transactions/domain/repositories/stock_transaction_repository.dart';
import 'package:superport_v2/features/inventory/transactions/presentation/services/transaction_detail_sync_service.dart';
import 'package:superport_v2/features/inventory/transactions/presentation/widgets/transaction_detail_dialog.dart';
import '../../../lookups/domain/entities/lookup_item.dart';
import '../../../lookups/domain/repositories/inventory_lookup_repository.dart';
import '../widgets/inbound_detail_view.dart';
const String _inboundTransactionTypeId = '입고';
@@ -456,6 +458,7 @@ class _InboundPageState extends State<InboundPage> {
selected: _selectedRecord,
onSelect: (record) {
setState(() => _selectedRecord = record);
_showDetailDialog(record);
},
dateFormatter: _dateFormatter,
currencyFormatter: _currencyFormatter,
@@ -572,51 +575,6 @@ class _InboundPageState extends State<InboundPage> {
],
),
),
if (_selectedRecord != null)
ResponsiveVisibility(
visibleOn: const {
DeviceBreakpoint.tablet,
DeviceBreakpoint.desktop,
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
_DetailCard(
record: _selectedRecord!,
dateFormatter: _dateFormatter,
currencyFormatter: _currencyFormatter,
onEdit: () => _handleEdit(_selectedRecord!),
transitionsEnabled: _transitionsEnabled,
onSubmit: _canSubmit(_selectedRecord!)
? () async => _submitRecord(_selectedRecord!)
: null,
onComplete: _canComplete(_selectedRecord!)
? () async => _completeRecord(_selectedRecord!)
: null,
onApprove: _canApprove(_selectedRecord!)
? () async => _approveRecord(_selectedRecord!)
: null,
onReject: _canReject(_selectedRecord!)
? () async => _rejectRecord(_selectedRecord!)
: null,
onCancel: _canCancel(_selectedRecord!)
? () async => _cancelRecord(_selectedRecord!)
: null,
canSubmit:
!_isProcessing(_selectedRecord!.id) && !_isLoading,
canComplete:
!_isProcessing(_selectedRecord!.id) && !_isLoading,
canApprove:
!_isProcessing(_selectedRecord!.id) && !_isLoading,
canReject:
!_isProcessing(_selectedRecord!.id) && !_isLoading,
canCancel:
!_isProcessing(_selectedRecord!.id) && !_isLoading,
),
],
),
),
],
),
);
@@ -704,9 +662,11 @@ class _InboundPageState extends State<InboundPage> {
rowSpanExtent: (index) =>
const FixedTableSpanExtent(InboundTableSpec.rowSpanHeight),
onRowTap: (rowIndex) {
final record = records[rowIndex];
setState(() {
_selectedRecord = records[rowIndex];
_selectedRecord = record;
});
_showDetailDialog(record);
},
);
}
@@ -728,6 +688,82 @@ class _InboundPageState extends State<InboundPage> {
];
}
Future<void> _showDetailDialog(InboundRecord record) async {
await showInventoryTransactionDetailDialog<void>(
context: context,
title: '입고 상세',
transactionNumber: record.transactionNumber,
body: InboundDetailView(
record: record,
dateFormatter: _dateFormatter,
currencyFormatter: _currencyFormatter,
transitionsEnabled: _transitionsEnabled,
),
actions: _buildDetailActions(record),
constraints: const BoxConstraints(maxWidth: 920),
);
}
List<Widget> _buildDetailActions(InboundRecord record) {
final isProcessing = _isProcessing(record.id) || _isLoading;
final actions = <Widget>[];
if (_canSubmit(record)) {
actions.add(
ShadButton.outline(
onPressed: isProcessing ? null : () => _submitRecord(record),
child: const Text('상신'),
),
);
}
if (_canApprove(record)) {
actions.add(
ShadButton.outline(
onPressed: isProcessing ? null : () => _approveRecord(record),
child: const Text('승인'),
),
);
}
if (_canReject(record)) {
actions.add(
ShadButton.outline(
onPressed: isProcessing ? null : () => _rejectRecord(record),
child: const Text('반려'),
),
);
}
if (_canCancel(record)) {
actions.add(
ShadButton.outline(
onPressed: isProcessing ? null : () => _cancelRecord(record),
child: const Text('취소'),
),
);
}
if (_canComplete(record)) {
actions.add(
ShadButton.outline(
onPressed: isProcessing ? null : () => _completeRecord(record),
child: const Text('완료 처리'),
),
);
}
actions.add(
ShadButton.outline(
leading: const Icon(lucide.LucideIcons.pencil, size: 16),
onPressed: isProcessing
? null
: () {
Navigator.of(context).maybePop();
_handleEdit(record);
},
child: const Text('수정'),
),
);
return actions;
}
Future<void> _handleCreate() async {
final record = await _showInboundFormDialog();
if (record != null) {
@@ -1900,208 +1936,6 @@ class _InboundPageState extends State<InboundPage> {
}
}
class _DetailCard extends StatelessWidget {
const _DetailCard({
required this.record,
required this.dateFormatter,
required this.currencyFormatter,
required this.onEdit,
this.transitionsEnabled = true,
this.onSubmit,
this.onComplete,
this.onApprove,
this.onReject,
this.onCancel,
this.canSubmit = true,
this.canComplete = true,
this.canApprove = true,
this.canReject = true,
this.canCancel = true,
});
final InboundRecord record;
final DateFormat dateFormatter;
final NumberFormat currencyFormatter;
final VoidCallback onEdit;
final bool transitionsEnabled;
final Future<void> Function()? onSubmit;
final Future<void> Function()? onComplete;
final Future<void> Function()? onApprove;
final Future<void> Function()? onReject;
final Future<void> Function()? onCancel;
final bool canSubmit;
final bool canComplete;
final bool canApprove;
final bool canReject;
final bool canCancel;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ShadCard(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('선택된 입고 상세', style: theme.textTheme.h3),
Wrap(
spacing: 8,
children: [
if (onSubmit != null)
ShadButton.outline(
onPressed: canSubmit
? () {
onSubmit?.call();
}
: null,
child: const Text('상신'),
),
if (onApprove != null)
ShadButton.outline(
onPressed: canApprove
? () {
onApprove?.call();
}
: null,
child: const Text('승인'),
),
if (onReject != null)
ShadButton.outline(
onPressed: canReject
? () {
onReject?.call();
}
: null,
child: const Text('반려'),
),
if (onCancel != null)
ShadButton.outline(
onPressed: canCancel
? () {
onCancel?.call();
}
: null,
child: const Text('취소'),
),
if (onComplete != null)
ShadButton.outline(
onPressed: canComplete
? () {
onComplete?.call();
}
: null,
child: const Text('완료 처리'),
),
ShadButton.outline(
leading: const Icon(lucide.LucideIcons.pencil, size: 16),
onPressed: onEdit,
child: const Text('수정'),
),
],
),
],
),
description: Text(
'트랜잭션번호 ${record.transactionNumber}',
style: theme.textTheme.muted,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!transitionsEnabled) ...[
ShadBadge.outline(
child: Text(
'재고 상태 전이가 비활성화된 상태입니다.',
style: theme.textTheme.small,
),
),
const SizedBox(height: 16),
],
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_DetailChip(
label: '처리일자',
value: dateFormatter.format(record.processedAt),
),
_DetailChip(label: '창고', value: record.warehouse),
_DetailChip(label: '트랜잭션 유형', value: record.transactionType),
_DetailChip(label: '상태', value: record.status),
_DetailChip(label: '작성자', value: record.writer),
_DetailChip(label: '품목 수', value: '${record.itemCount}'),
_DetailChip(label: '총 수량', value: '${record.totalQuantity}'),
_DetailChip(
label: '총 금액',
value: currencyFormatter.format(record.totalAmount),
),
],
),
const SizedBox(height: 24),
Text('라인 품목', style: theme.textTheme.h4),
const SizedBox(height: 8),
SizedBox(
height: (record.items.length * 52).clamp(160, 260).toDouble(),
child: ShadTable.list(
header: const [
ShadTableCell.header(child: Text('제품')),
ShadTableCell.header(child: Text('제조사')),
ShadTableCell.header(child: Text('단위')),
ShadTableCell.header(child: Text('수량')),
ShadTableCell.header(child: Text('단가')),
ShadTableCell.header(child: Text('비고')),
],
children: [
for (final item in record.items)
[
ShadTableCell(child: Text(item.product)),
ShadTableCell(child: Text(item.manufacturer)),
ShadTableCell(child: Text(item.unit)),
ShadTableCell(child: Text('${item.quantity}')),
ShadTableCell(
child: Text(currencyFormatter.format(item.price)),
),
ShadTableCell(
child: Text(item.remark.isEmpty ? '-' : item.remark),
),
],
],
columnSpanExtent: (index) => const FixedTableSpanExtent(136),
rowSpanExtent: (index) => const FixedTableSpanExtent(52),
),
),
],
),
);
}
}
class _DetailChip extends StatelessWidget {
const _DetailChip({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = ShadTheme.of(context);
return ShadBadge.outline(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: theme.textTheme.small),
const SizedBox(height: 2),
Text(value, style: theme.textTheme.p),
],
),
),
);
}
}
class _SummaryBadge extends StatelessWidget {
const _SummaryBadge({
required this.icon,