재고 상세 다이얼로그화 및 마스터 레이아웃 개선
This commit is contained in:
@@ -28,10 +28,12 @@ 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 'package:superport_v2/features/masters/customer/domain/entities/customer.dart';
|
||||
import 'package:superport_v2/features/masters/customer/domain/repositories/customer_repository.dart';
|
||||
import '../../../lookups/domain/entities/lookup_item.dart';
|
||||
import '../../../lookups/domain/repositories/inventory_lookup_repository.dart';
|
||||
import '../widgets/outbound_detail_view.dart';
|
||||
|
||||
const String _outboundTransactionTypeId = '출고';
|
||||
|
||||
@@ -577,9 +579,11 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
OutboundTableSpec.rowSpanHeight,
|
||||
),
|
||||
onRowTap: (rowIndex) {
|
||||
final record = visibleRecords[rowIndex];
|
||||
setState(() {
|
||||
_selectedRecord = visibleRecords[rowIndex];
|
||||
_selectedRecord = record;
|
||||
});
|
||||
_showDetailDialog(record);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -672,36 +676,6 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_selectedRecord != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
_OutboundDetailCard(
|
||||
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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -795,6 +769,82 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _showDetailDialog(OutboundRecord record) async {
|
||||
await showInventoryTransactionDetailDialog<void>(
|
||||
context: context,
|
||||
title: '출고 상세',
|
||||
transactionNumber: record.transactionNumber,
|
||||
body: OutboundDetailView(
|
||||
record: record,
|
||||
dateFormatter: _dateFormatter,
|
||||
currencyFormatter: _currencyFormatter,
|
||||
transitionsEnabled: _transitionsEnabled,
|
||||
),
|
||||
actions: _buildDetailActions(record),
|
||||
constraints: const BoxConstraints(maxWidth: 920),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildDetailActions(OutboundRecord 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 _showOutboundFormDialog();
|
||||
if (record != null) {
|
||||
@@ -2122,202 +2172,6 @@ class _OutboundPageState extends State<OutboundPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _OutboundDetailCard extends StatelessWidget {
|
||||
const _OutboundDetailCard({
|
||||
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 OutboundRecord 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.customerCount}'),
|
||||
_DetailChip(label: '품목 수', value: '${record.itemCount}'),
|
||||
_DetailChip(label: '총 수량', value: '${record.totalQuantity}'),
|
||||
_DetailChip(
|
||||
label: '총 금액',
|
||||
value: currencyFormatter.format(record.totalAmount),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('출고 고객사', style: theme.textTheme.h4),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final customer in record.customers)
|
||||
ShadBadge(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
child: Text('${customer.name} · ${customer.code}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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 _OutboundLineItemRow extends StatelessWidget {
|
||||
const _OutboundLineItemRow({
|
||||
required this.draft,
|
||||
@@ -2829,29 +2683,3 @@ enum _OutboundSortField {
|
||||
writer,
|
||||
customerCount,
|
||||
}
|
||||
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../models/outbound_record.dart';
|
||||
|
||||
/// 출고 트랜잭션 상세 정보를 다이얼로그에 표시하는 뷰이다.
|
||||
class OutboundDetailView extends StatelessWidget {
|
||||
const OutboundDetailView({
|
||||
super.key,
|
||||
required this.record,
|
||||
required this.dateFormatter,
|
||||
required this.currencyFormatter,
|
||||
this.transitionsEnabled = true,
|
||||
});
|
||||
|
||||
final OutboundRecord record;
|
||||
final intl.DateFormat dateFormatter;
|
||||
final intl.NumberFormat currencyFormatter;
|
||||
final bool transitionsEnabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ShadTheme.of(context);
|
||||
return 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.customerCount}'),
|
||||
_DetailChip(label: '품목 수', value: '${record.itemCount}'),
|
||||
_DetailChip(label: '총 수량', value: '${record.totalQuantity}'),
|
||||
_DetailChip(
|
||||
label: '총 금액',
|
||||
value: currencyFormatter.format(record.totalAmount),
|
||||
),
|
||||
if (record.remark.isNotEmpty && record.remark != '-')
|
||||
_DetailChip(label: '비고', value: record.remark),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('출고 고객사', style: theme.textTheme.h4),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final customer in record.customers)
|
||||
ShadBadge(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: Text('${customer.name} · ${customer.code}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text('라인 품목', style: theme.textTheme.h4),
|
||||
const SizedBox(height: 8),
|
||||
_buildLineTable(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLineTable() {
|
||||
final height = (record.items.length * 52).clamp(160, 260).toDouble();
|
||||
return SizedBox(
|
||||
height: height,
|
||||
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 Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.card,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: theme.colorScheme.border),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(label, style: theme.textTheme.small, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.p,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user