결재 템플릿 단계 적용 구현

- ApprovalTemplate 엔티티·DTO·원격 리포지토리 추가
- ApprovalController에 템플릿 로딩/적용 상태와 assignSteps 호출 연동
- ApprovalPage 단계 탭에 템플릿 선택 UI 및 적용 확인 다이얼로그 구현
- 템플릿 적용 단위 테스트와 IMPLEMENTATION_TASKS 현황 갱신
This commit is contained in:
JiWoong Sul
2025-09-25 00:21:12 +09:00
parent b6e50464d2
commit c3010965ad
63 changed files with 10179 additions and 1436 deletions

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:superport_v2/core/constants/app_sections.dart';
import 'package:superport_v2/widgets/app_layout.dart';
import 'package:superport_v2/widgets/components/filter_bar.dart';
class InboundPage extends StatefulWidget {
const InboundPage({super.key});
@@ -43,95 +47,67 @@ class _InboundPageState extends State<InboundPage> {
final theme = ShadTheme.of(context);
final filtered = _filteredRecords;
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return AppLayout(
title: '입고 관리',
subtitle: '입고 처리, 라인 품목, 상태를 한 화면에서 확인하고 관리합니다.',
breadcrumbs: const [
AppBreadcrumbItem(label: '대시보드', path: dashboardRoutePath),
AppBreadcrumbItem(label: '입·출고', path: '/inventory/inbound'),
AppBreadcrumbItem(label: '입고'),
],
actions: [
ShadButton(
leading: const Icon(LucideIcons.plus, size: 16),
onPressed: _handleCreate,
child: const Text('입고 등록'),
),
ShadButton.outline(
leading: const Icon(LucideIcons.pencil, size: 16),
onPressed:
_selectedRecord == null ? null : () => _handleEdit(_selectedRecord!),
child: const Text('선택 항목 수정'),
),
],
toolbar: FilterBar(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('입고 관리', style: theme.textTheme.h2),
const SizedBox(height: 6),
Text(
'입고 처리, 라인 품목, 상태를 한 화면에서 확인하고 관리합니다.',
style: theme.textTheme.muted,
),
],
),
),
Row(
SizedBox(
width: 260,
child: ShadInput(
controller: _searchController,
placeholder: const Text('트랜잭션번호, 작성자, 제품 검색'),
leading: const Icon(LucideIcons.search, size: 16),
onChanged: (_) => setState(() {}),
),
),
SizedBox(
width: 220,
child: ShadButton.outline(
onPressed: _pickDateRange,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
ShadButton(
leading: const Icon(LucideIcons.plus, size: 16),
onPressed: _handleCreate,
child: const Text('입고 등록'),
),
const SizedBox(width: 12),
ShadButton.outline(
leading: const Icon(LucideIcons.pencil, size: 16),
onPressed: _selectedRecord == null
? null
: () => _handleEdit(_selectedRecord!),
child: const Text('선택 항목 수정'),
const Icon(LucideIcons.calendar, size: 16),
const SizedBox(width: 8),
Text(
_dateRange == null
? '기간 선택'
: '${_dateFormatter.format(_dateRange!.start)} ~ ${_dateFormatter.format(_dateRange!.end)}',
),
],
),
],
),
const SizedBox(height: 24),
ShadCard(
title: Text('검색 필터', style: theme.textTheme.h3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 16,
runSpacing: 16,
children: [
SizedBox(
width: 260,
child: ShadInput(
controller: _searchController,
placeholder: const Text('트랜잭션번호, 작성자, 제품 검색'),
leading: const Icon(LucideIcons.search, size: 16),
onChanged: (_) => setState(() {}),
),
),
SizedBox(
width: 220,
child: ShadButton.outline(
onPressed: _pickDateRange,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(LucideIcons.calendar, size: 16),
const SizedBox(width: 8),
Text(
_dateRange == null
? '기간 선택'
: '${_dateFormatter.format(_dateRange!.start)} ~ ${_dateFormatter.format(_dateRange!.end)}',
),
],
),
),
),
if (_dateRange != null)
ShadButton.ghost(
onPressed: () => setState(() => _dateRange = null),
child: const Text('기간 초기화'),
),
],
),
],
),
),
const SizedBox(height: 24),
if (_dateRange != null)
ShadButton.ghost(
onPressed: () => setState(() => _dateRange = null),
child: const Text('기간 초기화'),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShadCard(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -158,18 +134,21 @@ class _InboundPageState extends State<InboundPage> {
.toList(),
children: [
for (final record in filtered)
_buildRecordRow(record).map(
(value) => ShadTableCell(
child: Text(
value,
overflow: TextOverflow.ellipsis,
),
),
),
_buildRecordRow(record)
.map(
(value) => ShadTableCell(
child: Text(
value,
overflow: TextOverflow.ellipsis,
),
),
)
.toList(),
],
columnSpanExtent: (index) =>
const FixedTableSpanExtent(140),
rowSpanExtent: (index) => const FixedTableSpanExtent(56),
rowSpanExtent: (index) =>
const FixedTableSpanExtent(56),
onRowTap: (rowIndex) {
setState(() {
_selectedRecord = filtered[rowIndex];