web: migrate health notifications to js_interop; add browser hook
- Replace dart:js with package:js in health_check_service_web.dart\n- Implement showHealthCheckNotification in web/index.html\n- Pin js dependency to ^0.6.7 for flutter_secure_storage_web compatibility auth: harden AuthInterceptor + tests - Allow overrideAuthRepository injection for testing\n- Normalize imports to package: paths\n- Add unit test covering token attach, 401→refresh→retry, and failure path\n- Add integration test skeleton gated by env vars ui/data: map User.companyName to list column - Add companyName to domain User\n- Map UserDto.company?.name\n- Render companyName in user_list cleanup: remove legacy equipment table + unused code; minor warnings - Remove _buildFlexibleTable and unused helpers\n- Remove unused zipcode details and cache retry constant\n- Fix null-aware and non-null assertions\n- Address child-last warnings in administrator dialog docs: update AGENTS.md session context
This commit is contained in:
@@ -471,6 +471,22 @@ class _AdministratorFormDialogState extends State<_AdministratorFormDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
return ShadDialog(
|
||||
title: Text(widget.title),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: _isSubmitting ? null : _handleSubmit,
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(_isEditMode ? '수정' : '생성'),
|
||||
),
|
||||
],
|
||||
child: SizedBox(
|
||||
width: 500,
|
||||
child: Form(
|
||||
@@ -575,22 +591,7 @@ class _AdministratorFormDialogState extends State<_AdministratorFormDialog> {
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ShadButton.outline(
|
||||
child: const Text('취소'),
|
||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(),
|
||||
),
|
||||
ShadButton(
|
||||
onPressed: _isSubmitting ? null : _handleSubmit,
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(_isEditMode ? '수정' : '생성'),
|
||||
),
|
||||
],
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1137,6 +1137,14 @@ class SidebarMenu extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
_buildMenuItem(
|
||||
icon: Icons.calendar_month_outlined,
|
||||
title: '임대 관리',
|
||||
route: Routes.rent,
|
||||
isActive: currentRoute == Routes.rent,
|
||||
badge: null,
|
||||
),
|
||||
|
||||
|
||||
if (!collapsed) ...[
|
||||
@@ -1376,6 +1384,8 @@ class SidebarMenu extends StatelessWidget {
|
||||
return Icons.factory;
|
||||
case Icons.category_outlined:
|
||||
return Icons.category;
|
||||
case Icons.calendar_month_outlined:
|
||||
return Icons.calendar_month;
|
||||
default:
|
||||
return outlinedIcon;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
|
||||
/// 폼 화면의 일관된 레이아웃을 제공하는 템플릿 위젯
|
||||
class FormLayoutTemplate extends StatelessWidget {
|
||||
@@ -27,27 +28,30 @@ class FormLayoutTemplate extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFF5F7FA),
|
||||
backgroundColor: ShadcnTheme.background, // Phase 10: 통일된 배경색
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
style: ShadcnTheme.headingH3.copyWith( // Phase 10: 표준 헤딩 스타일
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1F36),
|
||||
color: ShadcnTheme.foreground,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: ShadcnTheme.card, // Phase 10: 카드 배경색
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios, color: Color(0xFF6B7280), size: 20),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: ShadcnTheme.mutedForeground, // Phase 10: 뮤트된 전경색
|
||||
size: 20,
|
||||
),
|
||||
onPressed: onCancel ?? () => Navigator.of(context).pop(),
|
||||
),
|
||||
actions: customActions != null ? [customActions!] : null,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Container(
|
||||
color: Color(0xFFE5E7EB),
|
||||
color: ShadcnTheme.border, // Phase 10: 통일된 테두리 색상
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
@@ -60,13 +64,13 @@ class FormLayoutTemplate extends StatelessWidget {
|
||||
Widget _buildBottomBar(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: ShadcnTheme.card, // Phase 10: 카드 배경색
|
||||
border: Border(
|
||||
top: BorderSide(color: Color(0xFFE5E7EB), width: 1),
|
||||
top: BorderSide(color: ShadcnTheme.border, width: 1), // Phase 10: 통일된 테두리
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: ShadcnTheme.foreground.withValues(alpha: 0.05), // Phase 10: 그림자 색상
|
||||
offset: Offset(0, -2),
|
||||
blurRadius: 4,
|
||||
),
|
||||
@@ -125,24 +129,22 @@ class FormSection extends StatelessWidget {
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
style: ShadcnTheme.bodyLarge.copyWith( // Phase 10: 표준 바디 라지
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1A1F36),
|
||||
color: ShadcnTheme.foreground, // Phase 10: 전경색
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF6B7280),
|
||||
style: ShadcnTheme.bodyMedium.copyWith( // Phase 10: 표준 바디 미디엄
|
||||
color: ShadcnTheme.mutedForeground, // Phase 10: 뮤트된 전경색
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 20),
|
||||
Divider(color: Color(0xFFE5E7EB), height: 1),
|
||||
Divider(color: ShadcnTheme.border, height: 1), // Phase 10: 테두리 색상
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
if (children.isNotEmpty)
|
||||
|
||||
@@ -45,19 +45,19 @@ class ShadcnTheme {
|
||||
static const Color infoLight = Color(0xFFCFFAFE); // cyan-100
|
||||
static const Color infoForeground = Color(0xFFFFFFFF);
|
||||
|
||||
// ============= 비즈니스 상태 색상 =============
|
||||
// 회사 구분 색상
|
||||
static const Color companyHeadquarters = Color(0xFF2563EB); // 본사 - Primary Blue (권위)
|
||||
static const Color companyBranch = Color(0xFF7C3AED); // 지점 - Purple (연결성)
|
||||
static const Color companyPartner = Color(0xFF059669); // 파트너사 - Green (협력)
|
||||
static const Color companyCustomer = Color(0xFFEA580C); // 고객사 - Orange (활력)
|
||||
// ============= 비즈니스 상태 색상 (색체심리학 기반) =============
|
||||
// 회사 구분 색상 - Phase 10 업데이트
|
||||
static const Color companyHeadquarters = Color(0xFF2563EB); // 본사 - 진한 파랑 (권위, 안정성)
|
||||
static const Color companyBranch = Color(0xFF3B82F6); // 지점 - 밝은 파랑 (연결성, 확장)
|
||||
static const Color companyPartner = Color(0xFF10B981); // 파트너사 - 에메랄드 (협력, 신뢰)
|
||||
static const Color companyCustomer = Color(0xFF059669); // 고객사 - 진한 그린 (성장, 번영)
|
||||
|
||||
// 장비 상태 색상
|
||||
static const Color equipmentIn = Color(0xFF059669); // 입고 - Green (진입/추가)
|
||||
static const Color equipmentOut = Color(0xFF0891B2); // 출고 - Cyan (이동/프로세스)
|
||||
static const Color equipmentRent = Color(0xFF7C3AED); // 대여 - Purple (임시 상태)
|
||||
static const Color equipmentDisposal = Color(0xFF6B7280); // 폐기 - Gray (비활성)
|
||||
static const Color equipmentRepair = Color(0xFFD97706); // 수리중 - Amber (주의 필요)
|
||||
// 트랜잭션 상태 색상 - Phase 10 업데이트
|
||||
static const Color equipmentIn = Color(0xFF10B981); // 입고 - 에메랄드 (추가/성장)
|
||||
static const Color equipmentOut = Color(0xFF3B82F6); // 출고 - 블루 (이동/처리)
|
||||
static const Color equipmentRent = Color(0xFF8B5CF6); // 대여 - Purple (임시 상태)
|
||||
static const Color equipmentDisposal = Color(0xFF6B7280); // 폐기 - Gray (종료/비활성)
|
||||
static const Color equipmentRepair = Color(0xFFF59E0B); // 수리중 - Amber (주의/진행)
|
||||
static const Color equipmentUnknown = Color(0xFF9CA3AF); // 알수없음 - Light Gray
|
||||
|
||||
// ============= UI 요소 색상 =============
|
||||
@@ -93,8 +93,15 @@ class ShadcnTheme {
|
||||
|
||||
// 추가 색상 (기존 호환)
|
||||
static const Color blue = primary;
|
||||
static const Color purple = companyBranch;
|
||||
static const Color green = companyPartner;
|
||||
static const Color purple = equipmentRent;
|
||||
static const Color green = equipmentIn;
|
||||
|
||||
// Phase 10 - 알림/경고 색상 체계 (긴급도 기반)
|
||||
static const Color alertNormal = Color(0xFF10B981); // 60일 이상 - 안전 (그린)
|
||||
static const Color alertWarning60 = Color(0xFFF59E0B); // 60일 이내 - 주의 (앰버)
|
||||
static const Color alertWarning30 = Color(0xFFF97316); // 30일 이내 - 경고 (오렌지)
|
||||
static const Color alertCritical7 = Color(0xFFEF4444); // 7일 이내 - 위험 (레드)
|
||||
static const Color alertExpired = Color(0xFFDC2626); // 만료됨 - 심각 (진한 레드)
|
||||
|
||||
static const Color radius = Color(0xFF000000); // 사용하지 않음
|
||||
|
||||
@@ -526,51 +533,60 @@ class ShadcnTheme {
|
||||
}
|
||||
|
||||
// ============= 유틸리티 메서드 =============
|
||||
/// 회사 타입에 따른 색상 반환
|
||||
/// 회사 타입에 따른 색상 반환 (Phase 10 업데이트)
|
||||
static Color getCompanyColor(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case '본사':
|
||||
case 'headquarters':
|
||||
return companyHeadquarters;
|
||||
return companyHeadquarters; // #2563eb - 진한 파랑
|
||||
case '지점':
|
||||
case 'branch':
|
||||
return companyBranch;
|
||||
return companyBranch; // #3b82f6 - 밝은 파랑
|
||||
case '파트너사':
|
||||
case 'partner':
|
||||
return companyPartner;
|
||||
return companyPartner; // #10b981 - 에메랄드
|
||||
case '고객사':
|
||||
case 'customer':
|
||||
return companyCustomer;
|
||||
return companyCustomer; // #059669 - 진한 그린
|
||||
default:
|
||||
return secondary;
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 상태에 따른 색상 반환
|
||||
/// 트랜잭션 상태에 따른 색상 반환 (Phase 10 업데이트)
|
||||
static Color getEquipmentStatusColor(String status) {
|
||||
switch (status.toLowerCase()) {
|
||||
case '입고':
|
||||
case 'in':
|
||||
return equipmentIn;
|
||||
return equipmentIn; // #10b981 - 에메랄드
|
||||
case '출고':
|
||||
case 'out':
|
||||
return equipmentOut;
|
||||
return equipmentOut; // #3b82f6 - 블루
|
||||
case '대여':
|
||||
case 'rent':
|
||||
return equipmentRent;
|
||||
return equipmentRent; // #8b5cf6 - 퍼플
|
||||
case '폐기':
|
||||
case 'disposal':
|
||||
return equipmentDisposal;
|
||||
return equipmentDisposal; // #6b7280 - 그레이
|
||||
case '수리중':
|
||||
case 'repair':
|
||||
return equipmentRepair;
|
||||
return equipmentRepair; // #f59e0b - 앰버
|
||||
case '알수없음':
|
||||
case 'unknown':
|
||||
return equipmentUnknown;
|
||||
return equipmentUnknown; // #9ca3af - 라이트 그레이
|
||||
default:
|
||||
return secondary;
|
||||
}
|
||||
}
|
||||
|
||||
/// 알림/경고 긴급도에 따른 색상 반환 (Phase 10 신규 추가)
|
||||
static Color getAlertColor(int daysUntilExpiry) {
|
||||
if (daysUntilExpiry < 0) return alertExpired; // 만료됨
|
||||
if (daysUntilExpiry <= 7) return alertCritical7; // 7일 이내
|
||||
if (daysUntilExpiry <= 30) return alertWarning30; // 30일 이내
|
||||
if (daysUntilExpiry <= 60) return alertWarning60; // 60일 이내
|
||||
return alertNormal; // 60일 이상
|
||||
}
|
||||
|
||||
/// 상태별 배경색 반환 (연한 버전)
|
||||
static Color getStatusBackgroundColor(String status) {
|
||||
|
||||
@@ -162,13 +162,13 @@ class _CompanyListState extends State<CompanyList> {
|
||||
}
|
||||
|
||||
|
||||
/// 본사/지점 구분 배지 생성
|
||||
/// 본사/지점 구분 배지 생성 - Phase 10: 색체심리학 기반 색상 적용
|
||||
Widget _buildCompanyTypeLabel(bool isBranch) {
|
||||
return ShadcnBadge(
|
||||
text: isBranch ? '지점' : '본사',
|
||||
variant: isBranch
|
||||
? ShadcnBadgeVariant.companyBranch // Purple (#7C3AED) - 차별화
|
||||
: ShadcnBadgeVariant.companyHeadquarters, // Blue (#2563EB)
|
||||
? ShadcnBadgeVariant.companyBranch // Phase 10: 지점 - 밝은 파랑
|
||||
: ShadcnBadgeVariant.companyHeadquarters, // Phase 10: 본사 - 진한 파랑
|
||||
size: ShadcnBadgeSize.small,
|
||||
);
|
||||
}
|
||||
@@ -261,7 +261,7 @@ class _CompanyListState extends State<CompanyList> {
|
||||
if (item.isPartner) {
|
||||
flags.add(ShadcnBadge(
|
||||
text: '파트너',
|
||||
variant: ShadcnBadgeVariant.companyPartner,
|
||||
variant: ShadcnBadgeVariant.companyPartner, // Phase 10: 협력 - 에메랄드
|
||||
size: ShadcnBadgeSize.small,
|
||||
));
|
||||
}
|
||||
@@ -269,7 +269,7 @@ class _CompanyListState extends State<CompanyList> {
|
||||
if (item.isCustomer) {
|
||||
flags.add(ShadcnBadge(
|
||||
text: '고객',
|
||||
variant: ShadcnBadgeVariant.companyCustomer,
|
||||
variant: ShadcnBadgeVariant.companyCustomer, // Phase 10: 고객 - 진한 그린
|
||||
size: ShadcnBadgeSize.small,
|
||||
));
|
||||
}
|
||||
@@ -313,259 +313,105 @@ class _CompanyListState extends State<CompanyList> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌더
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
// (Deprecated) 기존 커스텀 테이블 빌더 유틸들은 ShadTable 전환으로 제거되었습니다.
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 셀 빌더
|
||||
Widget _buildDataCell(
|
||||
Widget child, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: container);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: container);
|
||||
}
|
||||
}
|
||||
|
||||
/// 헤더 셀 리스트
|
||||
List<Widget> _buildHeaderCells() {
|
||||
return [
|
||||
_buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50),
|
||||
_buildHeaderCell('회사명', flex: 3, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('구분', flex: 0, useExpanded: false, minWidth: 60),
|
||||
_buildHeaderCell('주소', flex: 2, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('담당자', flex: 2, useExpanded: true, minWidth: 80),
|
||||
_buildHeaderCell('연락처', flex: 2, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('파트너/고객', flex: 1, useExpanded: true, minWidth: 80),
|
||||
_buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 60),
|
||||
_buildHeaderCell('등록/수정일', flex: 2, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('비고', flex: 1, useExpanded: true, minWidth: 80),
|
||||
_buildHeaderCell('관리', flex: 0, useExpanded: false, minWidth: 100),
|
||||
];
|
||||
}
|
||||
|
||||
/// 테이블 행 빌더
|
||||
Widget _buildTableRow(CompanyItem item, int index, CompanyListController controller) {
|
||||
final rowNumber = ((controller.currentPage - 1) * controller.pageSize) + index + 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven
|
||||
? ShadcnTheme.muted.withValues(alpha: 0.1)
|
||||
: null,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDataCell(
|
||||
Text(
|
||||
rowNumber.toString(),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 50,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildDisplayNameText(item),
|
||||
flex: 3,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildCompanyTypeLabel(item.isBranch),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 60,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
item.address.isNotEmpty ? item.address : '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildContactInfo(item),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildContactDetails(item),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildPartnerCustomerFlags(item),
|
||||
flex: 1,
|
||||
useExpanded: true,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildStatusBadge(item.isActive),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 60,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildDateInfo(item),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
item.remark ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: true,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (item.id != null) ...[
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () {
|
||||
if (item.isBranch) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/branch/edit',
|
||||
arguments: {
|
||||
'companyId': item.parentCompanyId,
|
||||
'branchId': item.id,
|
||||
'parentCompanyName': item.parentCompanyName,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true) controller.refresh();
|
||||
});
|
||||
} else {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/company/edit',
|
||||
arguments: {
|
||||
'companyId': item.id,
|
||||
'isBranch': false,
|
||||
},
|
||||
).then((result) {
|
||||
if (result == true) controller.refresh();
|
||||
});
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () {
|
||||
if (item.isBranch) {
|
||||
_deleteBranch(item.parentCompanyId!, item.id!);
|
||||
} else {
|
||||
_deleteCompany(item.id!);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 100,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 헤더 고정 패턴 회사 테이블 빌더
|
||||
/// ShadTable 기반 회사 테이블 빌더 (기존 컬럼 구성 유지)
|
||||
Widget _buildCompanyShadTable(List<CompanyItem> items, CompanyListController controller) {
|
||||
if (items.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.business_outlined, size: 64, color: ShadcnTheme.mutedForeground),
|
||||
const SizedBox(height: 16),
|
||||
Text('등록된 회사가 없습니다', style: ShadcnTheme.bodyMedium.copyWith(color: ShadcnTheme.mutedForeground)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(bottom: BorderSide(color: Colors.black)),
|
||||
),
|
||||
child: Row(children: _buildHeaderCells()),
|
||||
),
|
||||
// 스크롤 바디
|
||||
Expanded(
|
||||
child: items.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
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('연락처')),
|
||||
ShadTableCell.header(child: Text('파트너/고객')),
|
||||
ShadTableCell.header(child: Text('상태')),
|
||||
ShadTableCell.header(child: Text('등록/수정일')),
|
||||
ShadTableCell.header(child: Text('비고')),
|
||||
ShadTableCell.header(child: Text('관리')),
|
||||
],
|
||||
children: [
|
||||
for (int index = 0; index < items.length; index++)
|
||||
[
|
||||
// 번호
|
||||
ShadTableCell(child: Text(((((controller.currentPage - 1) * controller.pageSize) + index + 1)).toString(), style: ShadcnTheme.bodySmall)),
|
||||
// 회사명 (본사>지점 표기 유지)
|
||||
ShadTableCell(child: _buildDisplayNameText(items[index])),
|
||||
// 구분 (본사/지점)
|
||||
ShadTableCell(child: _buildCompanyTypeLabel(items[index].isBranch)),
|
||||
// 주소
|
||||
ShadTableCell(child: Text(items[index].address.isNotEmpty ? items[index].address : '-', overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodySmall)),
|
||||
// 담당자 요약
|
||||
ShadTableCell(child: _buildContactInfo(items[index])),
|
||||
// 연락처 상세
|
||||
ShadTableCell(child: _buildContactDetails(items[index])),
|
||||
// 파트너/고객 플래그
|
||||
ShadTableCell(child: _buildPartnerCustomerFlags(items[index])),
|
||||
// 상태
|
||||
ShadTableCell(child: _buildStatusBadge(items[index].isActive)),
|
||||
// 등록/수정일
|
||||
ShadTableCell(child: _buildDateInfo(items[index])),
|
||||
// 비고
|
||||
ShadTableCell(child: Text(items[index].remark ?? '-', overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodySmall)),
|
||||
// 관리(편집/삭제)
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (items[index].id != null) ...[
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () async {
|
||||
// 기존 편집 흐름 유지
|
||||
final args = {'companyId': items[index].id};
|
||||
final result = await Navigator.pushNamed(context, '/company/edit', arguments: args);
|
||||
if (result == true) {
|
||||
controller.refresh();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'등록된 회사가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () {
|
||||
if (items[index].isBranch) {
|
||||
_deleteBranch(items[index].parentCompanyId!, items[index].id!);
|
||||
} else {
|
||||
_deleteCompany(items[index].id!);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(items[index], index, controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -761,4 +607,4 @@ class _CompanyListState extends State<CompanyList> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,13 +233,13 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
label: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '장비 번호 * 🔒' : '장비 번호 *'),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
if ((value ?? '').trim().isEmpty) {
|
||||
return '장비 번호는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: _controller.isFieldReadOnly('serialNumber') ? null : (value) {
|
||||
_controller.serialNumber = value?.trim() ?? '';
|
||||
_controller.serialNumber = value.trim();
|
||||
setState(() {});
|
||||
print('DEBUG [장비번호 입력] value: "$value", controller.serialNumber: "${_controller.serialNumber}"');
|
||||
},
|
||||
@@ -252,7 +252,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
placeholder: const Text('바코드를 입력하세요'),
|
||||
label: const Text('바코드'),
|
||||
onChanged: (value) {
|
||||
_controller.barcode = value?.trim() ?? '';
|
||||
_controller.barcode = value.trim();
|
||||
print('DEBUG [바코드 입력] value: "$value", controller.barcode: "${_controller.barcode}"');
|
||||
},
|
||||
),
|
||||
@@ -504,7 +504,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
label: const Text('워런티 번호 *'),
|
||||
placeholder: const Text('워런티 번호를 입력하세요'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty ?? true) {
|
||||
if (value.trim().isEmpty) {
|
||||
return '워런티 번호는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
@@ -683,4 +683,4 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 드롭다운 데이터를 미리 로드하는 메서드
|
||||
Future<void> _preloadDropdownData() async {
|
||||
try {
|
||||
@@ -94,6 +96,157 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
});
|
||||
}
|
||||
|
||||
/// ShadTable 기반 장비 목록 테이블
|
||||
///
|
||||
/// - 표준 컴포넌트 사용으로 일관성 확보
|
||||
/// - 핵심 컬럼만 우선 도입 (상태/장비번호/시리얼/제조사/모델/회사/창고/일자/관리)
|
||||
/// - 반응형: 가용 너비에 따라 일부 컬럼은 숨김 처리 가능
|
||||
Widget _buildShadTable(List<UnifiedEquipment> items, {required double availableWidth}) {
|
||||
final allSelected = items.isNotEmpty &&
|
||||
items.every((e) => _selectedItems.contains(e.equipment.id));
|
||||
|
||||
return ShadTable.list(
|
||||
header: [
|
||||
// 선택
|
||||
ShadTableCell.header(
|
||||
child: ShadCheckbox(
|
||||
value: allSelected,
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
if (checked == true) {
|
||||
_selectedItems
|
||||
..clear()
|
||||
..addAll(items.map((e) => e.equipment.id).whereType<int>());
|
||||
} else {
|
||||
_selectedItems.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
ShadTableCell.header(child: const Text('상태')),
|
||||
ShadTableCell.header(child: const Text('장비번호')),
|
||||
ShadTableCell.header(child: const Text('시리얼')),
|
||||
ShadTableCell.header(child: const Text('제조사')),
|
||||
ShadTableCell.header(child: const Text('모델')),
|
||||
if (availableWidth > 900) ShadTableCell.header(child: const Text('회사')),
|
||||
if (availableWidth > 1100) ShadTableCell.header(child: const Text('창고')),
|
||||
if (availableWidth > 800) ShadTableCell.header(child: const Text('일자')),
|
||||
ShadTableCell.header(child: const Text('관리')),
|
||||
],
|
||||
children: items.map((item) {
|
||||
final id = item.equipment.id;
|
||||
final selected = id != null && _selectedItems.contains(id);
|
||||
return [
|
||||
// 선택 체크박스
|
||||
ShadTableCell(
|
||||
child: ShadCheckbox(
|
||||
value: selected,
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
if (id == null) return;
|
||||
if (checked == true) {
|
||||
_selectedItems.add(id);
|
||||
} else {
|
||||
_selectedItems.remove(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
// 상태
|
||||
ShadTableCell(child: _buildStatusBadge(item.status)),
|
||||
// 장비번호
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.equipment.equipmentNumber,
|
||||
item.equipment.equipmentNumber,
|
||||
),
|
||||
),
|
||||
// 시리얼
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.equipment.serialNumber ?? '-',
|
||||
item.equipment.serialNumber ?? '-',
|
||||
),
|
||||
),
|
||||
// 제조사
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.vendorName ?? item.equipment.manufacturer,
|
||||
item.vendorName ?? item.equipment.manufacturer,
|
||||
),
|
||||
),
|
||||
// 모델
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.modelName ?? item.equipment.modelName,
|
||||
item.modelName ?? item.equipment.modelName,
|
||||
),
|
||||
),
|
||||
// 회사 (반응형)
|
||||
if (availableWidth > 900)
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.companyName ?? item.currentCompany ?? '-',
|
||||
item.companyName ?? item.currentCompany ?? '-',
|
||||
),
|
||||
),
|
||||
// 창고 (반응형)
|
||||
if (availableWidth > 1100)
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
item.warehouseLocation ?? '-',
|
||||
item.warehouseLocation ?? '-',
|
||||
),
|
||||
),
|
||||
// 일자 (반응형)
|
||||
if (availableWidth > 800)
|
||||
ShadTableCell(
|
||||
child: _buildTextWithTooltip(
|
||||
_formatDate(item.date),
|
||||
_formatDate(item.date),
|
||||
),
|
||||
),
|
||||
// 관리 액션
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: '이력 보기',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(item.equipment.id ?? 0),
|
||||
child: const Icon(Icons.history, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Tooltip(
|
||||
message: '수정',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEdit(item),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Tooltip(
|
||||
message: '삭제',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDelete(item),
|
||||
child: const Icon(Icons.delete_outline, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 라우트에 따른 초기 필터 설정
|
||||
void _setInitialFilter() {
|
||||
switch (widget.currentRoute) {
|
||||
@@ -173,32 +326,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
|
||||
/// 전체 선택/해제
|
||||
void _onSelectAll(bool? value) {
|
||||
setState(() {
|
||||
final equipments = _getFilteredEquipments();
|
||||
_selectedItems.clear(); // UI 체크박스 상태 초기화
|
||||
|
||||
if (value == true) {
|
||||
for (final equipment in equipments) {
|
||||
if (equipment.equipment.id != null) {
|
||||
_selectedItems.add(equipment.equipment.id!);
|
||||
_controller.selectEquipment(equipment);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_controller.clearSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 전체 선택 상태 확인
|
||||
bool _isAllSelected() {
|
||||
final equipments = _getFilteredEquipments();
|
||||
if (equipments.isEmpty) return false;
|
||||
return equipments.every((e) =>
|
||||
_controller.selectedEquipmentIds.contains('${e.equipment.id}:${e.status}'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 필터링된 장비 목록 반환
|
||||
@@ -986,339 +1114,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
return totalWidth;
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌더
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 데이터 셀 빌더
|
||||
Widget _buildDataCell(
|
||||
Widget child, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: container);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: container);
|
||||
}
|
||||
}
|
||||
|
||||
/// 유연한 테이블 빌더 - Virtual Scrolling 적용
|
||||
Widget _buildFlexibleTable(List<UnifiedEquipment> pagedEquipments, {required bool useExpanded, required double availableWidth}) {
|
||||
final hasOutOrRent = pagedEquipments.any((e) =>
|
||||
e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent
|
||||
);
|
||||
|
||||
// 헤더를 별도로 빌드 - 반응형 컬럼 적용
|
||||
Widget header = Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing1, // spacing2 -> spacing1로 더 축소
|
||||
vertical: 6, // 8 -> 6으로 더 축소
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 필수 컬럼들 (항상 표시) - 축소된 너비 적용
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
value: _isAllSelected(),
|
||||
onChanged: (bool? value) => _onSelectAll(value),
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 30,
|
||||
),
|
||||
// 번호
|
||||
_buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 35),
|
||||
// 회사명 (소유회사)
|
||||
_buildHeaderCell('소유회사', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
// 제조사
|
||||
_buildHeaderCell('제조사', flex: 2, useExpanded: useExpanded, minWidth: 60),
|
||||
// 모델명
|
||||
_buildHeaderCell('모델명', flex: 3, useExpanded: useExpanded, minWidth: 80),
|
||||
// 장비번호
|
||||
_buildHeaderCell('장비번호', flex: 3, useExpanded: useExpanded, minWidth: 70),
|
||||
// 상태
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 50),
|
||||
// 관리
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 100),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) ...[
|
||||
// 수량
|
||||
_buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 35),
|
||||
// 입출고일
|
||||
_buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
],
|
||||
|
||||
// 상세 컬럼들 (1200px 이상에서만 표시)
|
||||
if (_showDetailedColumns && availableWidth > 1200) ...[
|
||||
_buildHeaderCell('바코드', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('구매가격', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('구매일', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('보증기간', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 빈 상태 처리
|
||||
if (pagedEquipments.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
header,
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'데이터가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Virtual Scrolling을 위한 CustomScrollView 사용
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
header, // 헤더는 고정
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: pagedEquipments.length,
|
||||
itemBuilder: (context, index) {
|
||||
final UnifiedEquipment equipment = pagedEquipments[index];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing1, // spacing2 -> spacing1로 더 축소
|
||||
vertical: 2, // 3 -> 2로 더 축소
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 필수 컬럼들 (항상 표시) - 축소된 너비 적용
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
value: _selectedItems.contains(equipment.equipment.id ?? 0),
|
||||
onChanged: (bool? value) {
|
||||
if (equipment.equipment.id != null) {
|
||||
_onItemSelected(equipment.equipment.id!, value ?? false);
|
||||
}
|
||||
},
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 30,
|
||||
),
|
||||
// 번호
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 35,
|
||||
),
|
||||
// 소유회사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.companyName ?? 'N/A',
|
||||
equipment.companyName ?? 'N/A',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 제조사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.vendorName ?? 'N/A',
|
||||
equipment.vendorName ?? 'N/A',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 60,
|
||||
),
|
||||
// 모델명
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.modelName ?? '-',
|
||||
equipment.modelName ?? '-',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 장비번호
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.serialNumber ?? '',
|
||||
equipment.equipment.serialNumber ?? '',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 상태
|
||||
_buildDataCell(
|
||||
_buildStatusBadge(equipment.status),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 50,
|
||||
),
|
||||
// 관리 (아이콘 전용 버튼으로 최적화)
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: '이력 보기',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(equipment.equipment.id ?? 0),
|
||||
child: const Icon(Icons.history, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 1),
|
||||
Tooltip(
|
||||
message: '수정',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEdit(equipment),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 1),
|
||||
Tooltip(
|
||||
message: '삭제',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDelete(equipment),
|
||||
child: const Icon(Icons.delete_outline, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 100,
|
||||
),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) ...[
|
||||
// 수량 (백엔드에서 관리하지 않으므로 고정값)
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'1',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 35,
|
||||
),
|
||||
// 입출고일
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatDate(equipment.date),
|
||||
_formatDate(equipment.date),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
],
|
||||
|
||||
// 상세 컬럼들 (1200px 이상에서만 표시)
|
||||
if (_showDetailedColumns && availableWidth > 1200) ...[
|
||||
// 바코드
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.barcode ?? '-',
|
||||
equipment.equipment.barcode ?? '-',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 구매가격
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatPrice(equipment.equipment.purchasePrice),
|
||||
_formatPrice(equipment.equipment.purchasePrice),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 구매일
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatDate(equipment.equipment.purchaseDate),
|
||||
_formatDate(equipment.equipment.purchaseDate),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 보증기간
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatWarrantyPeriod(equipment.equipment.warrantyStartDate, equipment.equipment.warrantyEndDate),
|
||||
_formatWarrantyPeriod(equipment.equipment.warrantyStartDate, equipment.equipment.warrantyEndDate),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(List<UnifiedEquipment> filteredEquipments) {
|
||||
@@ -1367,19 +1167,18 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
final minimumWidth = _getMinimumTableWidth(pagedEquipments, availableWidth);
|
||||
final needsHorizontalScroll = minimumWidth > availableWidth;
|
||||
|
||||
// ShadTable 경로로 일괄 전환 (가로 스크롤은 ShadTable 외부에서 처리)
|
||||
if (needsHorizontalScroll) {
|
||||
// 최소 너비보다 작을 때만 스크롤 활성화
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalScrollController,
|
||||
child: SizedBox(
|
||||
width: minimumWidth,
|
||||
child: _buildFlexibleTable(pagedEquipments, useExpanded: false, availableWidth: availableWidth),
|
||||
child: _buildShadTable(pagedEquipments, availableWidth: availableWidth),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 충분한 공간이 있을 때는 Expanded 사용
|
||||
return _buildFlexibleTable(pagedEquipments, useExpanded: true, availableWidth: availableWidth);
|
||||
return _buildShadTable(pagedEquipments, availableWidth: availableWidth);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1399,10 +1198,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
/// 가격 포맷팅
|
||||
String _formatPrice(double? price) {
|
||||
if (price == null) return '-';
|
||||
return '${(price / 10000).toStringAsFixed(0)}만원';
|
||||
}
|
||||
|
||||
|
||||
/// 날짜 포맷팅
|
||||
String _formatDate(DateTime? date) {
|
||||
@@ -1411,75 +1207,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
/// 보증기간 포맷팅
|
||||
String _formatWarrantyPeriod(DateTime? startDate, DateTime? endDate) {
|
||||
if (startDate == null || endDate == null) return '-';
|
||||
|
||||
final now = DateTime.now();
|
||||
final isExpired = now.isAfter(endDate);
|
||||
final remainingDays = isExpired ? 0 : endDate.difference(now).inDays;
|
||||
|
||||
if (isExpired) {
|
||||
return '만료됨';
|
||||
} else if (remainingDays <= 30) {
|
||||
return '$remainingDays일 남음';
|
||||
} else {
|
||||
return _formatDate(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 재고 상태 위젯 빌더 (백엔드 기반 단순화)
|
||||
Widget _buildInventoryStatus(UnifiedEquipment equipment) {
|
||||
// 백엔드 Equipment_History 기반으로 단순 상태만 표시
|
||||
Widget stockInfo;
|
||||
if (equipment.status == 'I') {
|
||||
// 입고 상태: 재고 있음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'보유중',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.green[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == 'O') {
|
||||
// 출고 상태: 재고 없음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.orange, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'출고됨',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.orange[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == 'T') {
|
||||
// 대여 상태
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.schedule, color: Colors.blue, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'대여중',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.blue[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// 기타 상태
|
||||
stockInfo = Text(
|
||||
'-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
);
|
||||
}
|
||||
|
||||
return stockInfo;
|
||||
}
|
||||
|
||||
/// 상태 배지 빌더
|
||||
Widget _buildStatusBadge(String status) {
|
||||
@@ -1528,51 +1257,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 입출고일 위젯 빌더
|
||||
Widget _buildCreatedDateWidget(UnifiedEquipment equipment) {
|
||||
String dateStr = equipment.date.toString().substring(0, 10);
|
||||
return Text(
|
||||
dateStr,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// 액션 버튼 빌더
|
||||
Widget _buildActionButtons(int equipmentId) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 이력 버튼 - 텍스트 + 아이콘으로 강화
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(equipmentId),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.history, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text('이력', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 편집 버튼
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEditById(equipmentId),
|
||||
child: const Icon(Icons.edit_outlined, size: 14),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 삭제 버튼
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDeleteById(equipmentId),
|
||||
child: const Icon(Icons.delete_outline, size: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 장비 이력 다이얼로그 표시
|
||||
void _showEquipmentHistoryDialog(int equipmentId) async {
|
||||
@@ -1596,43 +1282,10 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
|
||||
// 편집 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리
|
||||
void _handleEditById(int equipmentId) {
|
||||
// 해당 장비 찾기
|
||||
final equipment = _controller.equipments.firstWhere(
|
||||
(e) => e.equipment.id == equipmentId,
|
||||
orElse: () => throw Exception('Equipment not found'),
|
||||
);
|
||||
_handleEdit(equipment);
|
||||
}
|
||||
|
||||
// 삭제 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리
|
||||
void _handleDeleteById(int equipmentId) {
|
||||
// 해당 장비 찾기
|
||||
final equipment = _controller.equipments.firstWhere(
|
||||
(e) => e.equipment.id == equipmentId,
|
||||
orElse: () => throw Exception('Equipment not found'),
|
||||
);
|
||||
_handleDelete(equipment);
|
||||
}
|
||||
|
||||
/// 체크박스 선택 관련 함수들
|
||||
void _onItemSelected(int id, bool selected) {
|
||||
// 해당 장비 찾기
|
||||
final equipment = _controller.equipments.firstWhere(
|
||||
(e) => e.equipment.id == id,
|
||||
orElse: () => throw Exception('Equipment not found'),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedItems.add(id);
|
||||
_controller.selectEquipment(equipment); // Controller에도 전달
|
||||
} else {
|
||||
_selectedItems.remove(id);
|
||||
_controller.toggleSelection(equipment); // 선택 해제
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 사용하지 않는 카테고리 관련 함수들 제거됨 (리스트 API에서 제공하지 않음)
|
||||
|
||||
@@ -75,180 +75,8 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌더
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
);
|
||||
// (Deprecated) 기존 커스텀 테이블 빌더 유틸들은 ShadTable 전환으로 제거되었습니다.
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 셀 빌더
|
||||
Widget _buildDataCell(
|
||||
Widget child, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: container);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: container);
|
||||
}
|
||||
}
|
||||
|
||||
/// 헤더 셀 리스트 (요구사항에 맞게 재정의)
|
||||
List<Widget> _buildHeaderCells() {
|
||||
return [
|
||||
_buildHeaderCell('장비명', flex: 3, useExpanded: true, minWidth: 150),
|
||||
_buildHeaderCell('시리얼번호', flex: 2, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('위치', flex: 2, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('변동일', flex: 1, useExpanded: false, minWidth: 100),
|
||||
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('비고', flex: 2, useExpanded: true, minWidth: 120),
|
||||
];
|
||||
}
|
||||
|
||||
/// 테이블 행 빌더 (요구사항에 맞게 재정의)
|
||||
Widget _buildTableRow(InventoryHistoryViewModel history, int index) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven ? ShadcnTheme.muted.withValues(alpha: 0.1) : null,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black12, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 장비명
|
||||
_buildDataCell(
|
||||
Tooltip(
|
||||
message: history.equipmentName,
|
||||
child: Text(
|
||||
history.equipmentName,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: true,
|
||||
minWidth: 150,
|
||||
),
|
||||
// 시리얼번호
|
||||
_buildDataCell(
|
||||
Tooltip(
|
||||
message: history.serialNumber,
|
||||
child: Text(
|
||||
history.serialNumber,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 위치 (출고/대여: 고객사, 입고/폐기: 창고)
|
||||
_buildDataCell(
|
||||
Tooltip(
|
||||
message: history.location,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
history.isCustomerLocation ? Icons.business : Icons.warehouse,
|
||||
size: 14,
|
||||
color: history.isCustomerLocation ? Colors.blue : Colors.green,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
history.location,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 변동일
|
||||
_buildDataCell(
|
||||
Text(
|
||||
history.formattedDate,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: false,
|
||||
minWidth: 100,
|
||||
),
|
||||
// 작업 (상세보기만)
|
||||
_buildDataCell(
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDetail(history),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.history, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text('상세보기', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 비고
|
||||
_buildDataCell(
|
||||
Tooltip(
|
||||
message: history.remark ?? '비고 없음',
|
||||
child: Text(
|
||||
history.remark ?? '-',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 장비 이력 상세보기 다이얼로그 표시
|
||||
void _showEquipmentHistoryDetail(InventoryHistoryViewModel history) async {
|
||||
@@ -304,17 +132,104 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
height: 40,
|
||||
width: 120,
|
||||
child: ShadSelect<String>(
|
||||
selectedOptionBuilder: (context, value) => Text(
|
||||
_getTransactionTypeDisplayText(value),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
selectedOptionBuilder: (context, value) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (value != 'all') ...[
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _getTransactionTypeColor(value),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Text(
|
||||
_getTransactionTypeDisplayText(value),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
placeholder: const Text('거래 유형'),
|
||||
options: [
|
||||
const ShadOption(value: 'all', child: Text('전체')),
|
||||
const ShadOption(value: 'I', child: Text('입고')),
|
||||
const ShadOption(value: 'O', child: Text('출고')),
|
||||
const ShadOption(value: 'R', child: Text('대여')),
|
||||
const ShadOption(value: 'D', child: Text('폐기')),
|
||||
const ShadOption(
|
||||
value: 'all',
|
||||
child: Text('전체'),
|
||||
),
|
||||
ShadOption(
|
||||
value: 'I',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.equipmentIn,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text('입고'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ShadOption(
|
||||
value: 'O',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.equipmentOut,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text('출고'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ShadOption(
|
||||
value: 'R',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.equipmentRent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text('대여'),
|
||||
],
|
||||
),
|
||||
),
|
||||
ShadOption(
|
||||
value: 'D',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.equipmentDisposal,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text('폐기'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
@@ -407,7 +322,7 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
],
|
||||
totalCount: stats['total'],
|
||||
statusMessage: controller.hasActiveFilters
|
||||
? '${controller.filterStatusText}'
|
||||
? controller.filterStatusText
|
||||
: '장비 입출고 이력을 조회합니다',
|
||||
);
|
||||
},
|
||||
@@ -432,7 +347,23 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 테이블 빌더
|
||||
/// 거래 유형별 Phase 10 색상 반환
|
||||
Color _getTransactionTypeColor(String type) {
|
||||
switch (type) {
|
||||
case 'I':
|
||||
return ShadcnTheme.equipmentIn; // 입고 - 그린
|
||||
case 'O':
|
||||
return ShadcnTheme.equipmentOut; // 출고 - 블루
|
||||
case 'R':
|
||||
return ShadcnTheme.equipmentRent; // 대여 - 퍼플
|
||||
case 'D':
|
||||
return ShadcnTheme.equipmentDisposal; // 폐기 - 그레이
|
||||
default:
|
||||
return ShadcnTheme.foregroundMuted; // 기본/전체
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 테이블 빌더 (ShadTable)
|
||||
Widget _buildDataTable(List<InventoryHistoryViewModel> historyList) {
|
||||
if (historyList.isEmpty) {
|
||||
return Center(
|
||||
@@ -471,31 +402,66 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
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: historyList.map((history) {
|
||||
return [
|
||||
// 장비명
|
||||
ShadTableCell(
|
||||
child: Tooltip(
|
||||
message: history.equipmentName,
|
||||
child: Text(history.equipmentName, overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
// 시리얼번호
|
||||
ShadTableCell(
|
||||
child: Tooltip(
|
||||
message: history.serialNumber,
|
||||
child: Text(history.serialNumber, overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodySmall),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(children: _buildHeaderCells()),
|
||||
),
|
||||
// 스크롤 바디
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: historyList.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(historyList[index], index),
|
||||
),
|
||||
),
|
||||
],
|
||||
// 위치
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(history.isCustomerLocation ? Icons.business : Icons.warehouse, size: 14, color: history.isCustomerLocation ? ShadcnTheme.companyCustomer : ShadcnTheme.equipmentIn),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(child: Text(history.location, overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodySmall)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 변동일
|
||||
ShadTableCell(child: Text(history.formattedDate, style: ShadcnTheme.bodySmall)),
|
||||
// 작업
|
||||
ShadTableCell(
|
||||
child: ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDetail(history),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [Icon(Icons.history, size: 14), SizedBox(width: 4), Text('상세보기', style: TextStyle(fontSize: 12))],
|
||||
),
|
||||
),
|
||||
),
|
||||
// 비고
|
||||
ShadTableCell(
|
||||
child: Tooltip(
|
||||
message: history.remark ?? '비고 없음',
|
||||
child: Text(history.remark ?? '-', overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodySmall.copyWith(color: ShadcnTheme.mutedForeground)),
|
||||
),
|
||||
),
|
||||
];
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -539,4 +505,4 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,25 +192,26 @@ class _MaintenanceAlertDashboardState extends State<MaintenanceAlertDashboard> {
|
||||
Color color;
|
||||
IconData icon;
|
||||
|
||||
// Phase 10: 색체심리학 기반 알림 색상 체계 적용
|
||||
switch (type) {
|
||||
case 'expiring_7':
|
||||
color = Colors.red.shade600;
|
||||
color = ShadcnTheme.alertCritical7; // 7일 이내 - 위험 (레드)
|
||||
icon = Icons.priority_high_outlined;
|
||||
break;
|
||||
case 'expiring_30':
|
||||
color = Colors.orange.shade600;
|
||||
color = ShadcnTheme.alertWarning30; // 30일 이내 - 경고 (오렌지)
|
||||
icon = Icons.warning_amber_outlined;
|
||||
break;
|
||||
case 'expiring_60':
|
||||
color = Colors.amber.shade600;
|
||||
color = ShadcnTheme.alertWarning60; // 60일 이내 - 주의 (앰버)
|
||||
icon = Icons.schedule_outlined;
|
||||
break;
|
||||
case 'expired':
|
||||
color = Colors.red.shade800;
|
||||
color = ShadcnTheme.alertExpired; // 만료됨 - 심각 (진한 레드)
|
||||
icon = Icons.error_outline;
|
||||
break;
|
||||
default:
|
||||
color = Colors.grey.shade600;
|
||||
color = ShadcnTheme.alertNormal; // 정상 - 안전 (그린)
|
||||
icon = Icons.info_outline;
|
||||
}
|
||||
|
||||
@@ -449,13 +450,14 @@ class _MaintenanceAlertDashboardState extends State<MaintenanceAlertDashboard> {
|
||||
),
|
||||
),
|
||||
|
||||
// 고객사
|
||||
// 고객사 - Phase 10: 회사 타입별 색상 적용
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
child: Text(
|
||||
controller.getCompanyName(maintenance),
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.foreground,
|
||||
color: ShadcnTheme.companyCustomer, // 고객사 - 진한 그린
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -468,12 +470,13 @@ class _MaintenanceAlertDashboardState extends State<MaintenanceAlertDashboard> {
|
||||
child: Text(
|
||||
'${maintenance.endedAt.year}-${maintenance.endedAt.month.toString().padLeft(2, '0')}-${maintenance.endedAt.day.toString().padLeft(2, '0')}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
// Phase 10: 만료 상태별 색상 체계 적용
|
||||
color: isExpired
|
||||
? Colors.red.shade600
|
||||
? ShadcnTheme.alertExpired // 만료됨 - 심각 (진한 레드)
|
||||
: isExpiringSoon
|
||||
? Colors.orange.shade600
|
||||
: ShadcnTheme.foreground,
|
||||
fontWeight: isExpired || isExpiringSoon ? FontWeight.w600 : FontWeight.normal,
|
||||
? ShadcnTheme.alertWarning30 // 만료 임박 - 경고 (오렌지)
|
||||
: ShadcnTheme.alertNormal, // 정상 - 안전 (그린)
|
||||
fontWeight: isExpired || isExpiringSoon ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -506,11 +509,12 @@ class _MaintenanceAlertDashboardState extends State<MaintenanceAlertDashboard> {
|
||||
? '${daysRemaining.abs()}일 지연'
|
||||
: '$daysRemaining일 남음',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
// Phase 10: 남은 일수 상태별 색상 체계 적용
|
||||
color: isExpired
|
||||
? Colors.red.shade600
|
||||
? ShadcnTheme.alertExpired // 지연 - 심각 (진한 레드)
|
||||
: isExpiringSoon
|
||||
? Colors.orange.shade600
|
||||
: Colors.green.shade600,
|
||||
? ShadcnTheme.alertWarning30 // 임박 - 경고 (오렌지)
|
||||
: ShadcnTheme.alertNormal, // 충분 - 안전 (그린)
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -543,14 +547,15 @@ class _MaintenanceAlertDashboardState extends State<MaintenanceAlertDashboard> {
|
||||
}
|
||||
|
||||
/// 유지보수 타입별 색상
|
||||
// Phase 10: 유지보수 타입별 색상 체계
|
||||
Color _getMaintenanceTypeColor(String maintenanceType) {
|
||||
switch (maintenanceType) {
|
||||
case 'V': // 방문
|
||||
return Colors.blue.shade600;
|
||||
case 'R': // 원격
|
||||
return Colors.green.shade600;
|
||||
case 'V': // 방문 - 본사/지점 계열 (블루)
|
||||
return ShadcnTheme.companyHeadquarters;
|
||||
case 'R': // 원격 - 협력/성장 계열 (그린)
|
||||
return ShadcnTheme.companyPartner;
|
||||
default:
|
||||
return Colors.grey.shade600;
|
||||
return ShadcnTheme.muted;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,13 +70,26 @@ class _MaintenanceListState extends State<MaintenanceList> {
|
||||
value: _controller,
|
||||
child: Scaffold(
|
||||
backgroundColor: ShadcnTheme.background,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildActionBar(),
|
||||
_buildFilterBar(),
|
||||
Expanded(child: _buildMainContent()),
|
||||
_buildBottomBar(),
|
||||
],
|
||||
body: Consumer<MaintenanceController>(
|
||||
builder: (context, controller, child) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildActionBar(),
|
||||
_buildFilterBar(),
|
||||
Expanded(child: _buildMainContent()),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing4),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
border: Border(
|
||||
top: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
),
|
||||
child: _buildPagination(controller),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -260,209 +273,299 @@ class _MaintenanceListState extends State<MaintenanceList> {
|
||||
|
||||
/// 데이터 테이블
|
||||
Widget _buildDataTable(MaintenanceController controller) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalScrollController,
|
||||
child: DataTable(
|
||||
columns: _buildHeaders(),
|
||||
rows: _buildRows(controller.maintenances),
|
||||
final maintenances = controller.maintenances;
|
||||
|
||||
if (maintenances.isEmpty) {
|
||||
return const StandardEmptyState(
|
||||
icon: Icons.build_circle_outlined,
|
||||
title: '유지보수가 없습니다',
|
||||
message: '새로운 유지보수를 등록해보세요.',
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(bottom: BorderSide(color: ShadcnTheme.border)),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(ShadcnTheme.radiusMd),
|
||||
topRight: Radius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalScrollController,
|
||||
child: _buildFixedHeader(),
|
||||
),
|
||||
),
|
||||
|
||||
// 스크롤 가능한 바디
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: _horizontalScrollController,
|
||||
child: SizedBox(
|
||||
width: _calculateTableWidth(),
|
||||
child: ListView.builder(
|
||||
itemCount: maintenances.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(maintenances[index], index),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 테이블 헤더
|
||||
List<DataColumn> _buildHeaders() {
|
||||
return [
|
||||
const DataColumn(label: Text('선택')),
|
||||
const DataColumn(label: Text('ID')),
|
||||
const DataColumn(label: Text('장비 정보')),
|
||||
const DataColumn(label: Text('유지보수 타입')),
|
||||
const DataColumn(label: Text('시작일')),
|
||||
const DataColumn(label: Text('종료일')),
|
||||
if (_showDetailedColumns) ...[
|
||||
const DataColumn(label: Text('주기')),
|
||||
const DataColumn(label: Text('상태')),
|
||||
const DataColumn(label: Text('남은 일수')),
|
||||
],
|
||||
const DataColumn(label: Text('작업')),
|
||||
];
|
||||
}
|
||||
|
||||
/// 테이블 로우
|
||||
List<DataRow> _buildRows(List<MaintenanceDto> maintenances) {
|
||||
return maintenances.map((maintenance) {
|
||||
final isSelected = _selectedItems.contains(maintenance.id);
|
||||
|
||||
return DataRow(
|
||||
selected: isSelected,
|
||||
onSelectChanged: (_) => _showMaintenanceDetail(maintenance),
|
||||
cells: [
|
||||
// 선택 체크박스
|
||||
DataCell(
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedItems.add(maintenance.id!);
|
||||
} else {
|
||||
_selectedItems.remove(maintenance.id!);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// ID
|
||||
DataCell(Text(maintenance.id?.toString() ?? '-')),
|
||||
|
||||
// 장비 정보
|
||||
DataCell(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
maintenance.equipmentSerial ?? '시리얼 번호 없음',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
if (maintenance.equipmentModel != null)
|
||||
Text(
|
||||
maintenance.equipmentModel!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 유지보수 타입
|
||||
DataCell(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getMaintenanceTypeColor(maintenance.maintenanceType),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
MaintenanceType.getDisplayName(maintenance.maintenanceType),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 시작일
|
||||
DataCell(Text(DateFormat('yyyy-MM-dd').format(maintenance.startedAt))),
|
||||
|
||||
// 종료일
|
||||
DataCell(Text(DateFormat('yyyy-MM-dd').format(maintenance.endedAt))),
|
||||
|
||||
// 상세 컬럼들
|
||||
/// 고정 헤더 빌드
|
||||
Widget _buildFixedHeader() {
|
||||
return SizedBox(
|
||||
width: _calculateTableWidth(),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildHeaderCell('선택', 60),
|
||||
_buildHeaderCell('ID', 80),
|
||||
_buildHeaderCell('장비 정보', 200),
|
||||
_buildHeaderCell('유지보수 타입', 120),
|
||||
_buildHeaderCell('시작일', 100),
|
||||
_buildHeaderCell('종료일', 100),
|
||||
if (_showDetailedColumns) ...[
|
||||
// 주기
|
||||
DataCell(Text('${maintenance.periodMonth}개월')),
|
||||
|
||||
// 상태
|
||||
DataCell(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _controller.getMaintenanceStatusColor(maintenance),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
_controller.getMaintenanceStatusText(maintenance),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 남은 일수
|
||||
DataCell(
|
||||
Text(
|
||||
maintenance.daysRemaining != null
|
||||
? '${maintenance.daysRemaining}일'
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
color: maintenance.daysRemaining != null &&
|
||||
maintenance.daysRemaining! <= 30
|
||||
? Colors.red
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildHeaderCell('주기', 80),
|
||||
_buildHeaderCell('상태', 100),
|
||||
_buildHeaderCell('남은 일수', 100),
|
||||
],
|
||||
|
||||
// 작업 버튼들
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
onPressed: () => _showMaintenanceForm(maintenance: maintenance),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ShadButton.ghost(
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
size: 16,
|
||||
color: Colors.red[400],
|
||||
),
|
||||
onPressed: () => _deleteMaintenance(maintenance),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildHeaderCell('작업', 120),
|
||||
],
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 하단바 (페이지네이션)
|
||||
Widget _buildBottomBar() {
|
||||
return Consumer<MaintenanceController>(
|
||||
builder: (context, controller, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
border: Border(
|
||||
top: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 선택된 항목 정보
|
||||
if (_selectedItems.isNotEmpty)
|
||||
Text('${_selectedItems.length}개 선택됨'),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// 페이지네이션
|
||||
Pagination(
|
||||
totalCount: controller.totalCount,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: 20, // MaintenanceController._perPage 상수값
|
||||
onPageChanged: (page) => controller.goToPage(page),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 테이블 총 너비 계산
|
||||
double _calculateTableWidth() {
|
||||
double width = 60 + 80 + 200 + 120 + 100 + 100 + 120; // 기본 컬럼들
|
||||
if (_showDetailedColumns) {
|
||||
width += 80 + 100 + 100; // 상세 컬럼들
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌드
|
||||
Widget _buildHeaderCell(String text, double width) {
|
||||
return Container(
|
||||
width: width,
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing2),
|
||||
child: Text(
|
||||
text,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 테이블 행 빌드
|
||||
Widget _buildTableRow(MaintenanceDto maintenance, int index) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven ? ShadcnTheme.muted.withValues(alpha: 0.1) : null,
|
||||
border: Border(bottom: BorderSide(color: ShadcnTheme.border.withValues(alpha: 0.3))),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showMaintenanceDetail(maintenance),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3),
|
||||
child: Row(
|
||||
children: [
|
||||
// 선택 체크박스
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: ShadCheckbox(
|
||||
value: _selectedItems.contains(maintenance.id),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedItems.add(maintenance.id!);
|
||||
} else {
|
||||
_selectedItems.remove(maintenance.id!);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// ID
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
maintenance.id?.toString() ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 장비 정보
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
maintenance.equipmentSerial ?? '시리얼 번호 없음',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (maintenance.equipmentModel != null)
|
||||
Text(
|
||||
maintenance.equipmentModel!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 유지보수 타입
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing2, vertical: ShadcnTheme.spacing1),
|
||||
decoration: BoxDecoration(
|
||||
color: _getMaintenanceTypeColor(maintenance.maintenanceType),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusSm),
|
||||
),
|
||||
child: Text(
|
||||
MaintenanceType.getDisplayName(maintenance.maintenanceType),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 시작일
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
DateFormat('yyyy-MM-dd').format(maintenance.startedAt),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 종료일
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
DateFormat('yyyy-MM-dd').format(maintenance.endedAt),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 상세 컬럼들
|
||||
if (_showDetailedColumns) ...[
|
||||
// 주기
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'${maintenance.periodMonth}개월',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 상태
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing2, vertical: ShadcnTheme.spacing1),
|
||||
decoration: BoxDecoration(
|
||||
color: _controller.getMaintenanceStatusColor(maintenance),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusSm),
|
||||
),
|
||||
child: Text(
|
||||
_controller.getMaintenanceStatusText(maintenance),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 남은 일수
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
maintenance.daysRemaining != null
|
||||
? '${maintenance.daysRemaining}일'
|
||||
: '-',
|
||||
style: TextStyle(
|
||||
color: maintenance.daysRemaining != null &&
|
||||
maintenance.daysRemaining! <= 30
|
||||
? ShadcnTheme.destructive
|
||||
: ShadcnTheme.foreground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 작업 버튼들
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
onPressed: () => _showMaintenanceForm(maintenance: maintenance),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
size: 16,
|
||||
color: ShadcnTheme.destructive,
|
||||
),
|
||||
onPressed: () => _deleteMaintenance(maintenance),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 하단 페이지네이션
|
||||
Widget _buildPagination(MaintenanceController controller) {
|
||||
return Pagination(
|
||||
totalCount: controller.totalCount,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: 20, // MaintenanceController._perPage 상수값
|
||||
onPageChanged: (page) => controller.goToPage(page),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 유틸리티 메서드들
|
||||
Color _getMaintenanceTypeColor(String type) {
|
||||
switch (type) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:superport/screens/model/controllers/model_controller.dart';
|
||||
import 'package:superport/screens/model/model_form_dialog.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/injection_container.dart' as di;
|
||||
|
||||
@@ -18,6 +19,10 @@ class ModelListScreen extends StatefulWidget {
|
||||
|
||||
class _ModelListScreenState extends State<ModelListScreen> {
|
||||
late final ModelController _controller;
|
||||
|
||||
// 클라이언트 사이드 페이지네이션
|
||||
int _currentPage = 1;
|
||||
static const int _pageSize = 10;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -28,6 +33,33 @@ class _ModelListScreenState extends State<ModelListScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
// 현재 페이지의 모델 목록 반환
|
||||
List<ModelDto> _getCurrentPageModels() {
|
||||
final allModels = _controller.models;
|
||||
final startIndex = (_currentPage - 1) * _pageSize;
|
||||
final endIndex = startIndex + _pageSize;
|
||||
|
||||
if (startIndex >= allModels.length) return [];
|
||||
if (endIndex >= allModels.length) return allModels.sublist(startIndex);
|
||||
|
||||
return allModels.sublist(startIndex, endIndex);
|
||||
}
|
||||
|
||||
// 총 페이지 수 계산
|
||||
int _getTotalPages() {
|
||||
return (_controller.models.length / _pageSize).ceil();
|
||||
}
|
||||
|
||||
// 페이지 이동
|
||||
void _goToPage(int page) {
|
||||
final totalPages = _getTotalPages();
|
||||
if (page < 1 || page > totalPages) return;
|
||||
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
@@ -78,21 +110,21 @@ class _ModelListScreenState extends State<ModelListScreen> {
|
||||
'전체 모델',
|
||||
controller.models.length.toString(),
|
||||
Icons.category,
|
||||
ShadcnTheme.primary,
|
||||
ShadcnTheme.companyCustomer,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
_buildStatCard(
|
||||
'제조사',
|
||||
controller.vendors.length.toString(),
|
||||
Icons.business,
|
||||
ShadcnTheme.success,
|
||||
ShadcnTheme.companyPartner,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
_buildStatCard(
|
||||
'활성 모델',
|
||||
controller.models.where((m) => !m.isDeleted).length.toString(),
|
||||
Icons.check_circle,
|
||||
ShadcnTheme.info,
|
||||
ShadcnTheme.equipmentIn,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -105,7 +137,12 @@ class _ModelListScreenState extends State<ModelListScreen> {
|
||||
flex: 2,
|
||||
child: ShadInput(
|
||||
placeholder: const Text('모델명 검색...'),
|
||||
onChanged: controller.setSearchQuery,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentPage = 1; // 검색 시 첫 페이지로 리셋
|
||||
});
|
||||
controller.setSearchQuery(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
@@ -135,7 +172,12 @@ class _ModelListScreenState extends State<ModelListScreen> {
|
||||
return const Text('전체');
|
||||
}
|
||||
},
|
||||
onChanged: controller.setVendorFilter,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentPage = 1; // 필터 변경 시 첫 페이지로 리셋
|
||||
});
|
||||
controller.setVendorFilter(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -169,200 +211,121 @@ class _ModelListScreenState extends State<ModelListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌더
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 셀 빌더
|
||||
Widget _buildDataCell(
|
||||
Widget child, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: container);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: container);
|
||||
}
|
||||
}
|
||||
|
||||
/// 헤더 셀 리스트
|
||||
List<Widget> _buildHeaderCells() {
|
||||
return [
|
||||
_buildHeaderCell('ID', flex: 0, useExpanded: false, minWidth: 60),
|
||||
_buildHeaderCell('제조사', flex: 2, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('모델명', flex: 3, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('등록일', flex: 2, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 100),
|
||||
];
|
||||
}
|
||||
|
||||
/// 테이블 행 빌더
|
||||
Widget _buildTableRow(ModelDto model, int index) {
|
||||
final vendor = _controller.getVendorById(model.vendorsId);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven
|
||||
? ShadcnTheme.muted.withValues(alpha: 0.1)
|
||||
: null,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDataCell(
|
||||
Text(
|
||||
model.id.toString(),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 60,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
vendor?.name ?? '알 수 없음',
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
model.name,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
model.registeredAt != null
|
||||
? DateFormat('yyyy-MM-dd').format(model.registeredAt)
|
||||
: '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildStatusChip(model.isDeleted),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showEditDialog(model),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showDeleteConfirmDialog(model),
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 100,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataTable(ModelController controller) {
|
||||
final models = controller.models;
|
||||
final allModels = controller.models;
|
||||
final currentPageModels = _getCurrentPageModels();
|
||||
|
||||
if (allModels.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
Text(
|
||||
'등록된 모델이 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(bottom: BorderSide(color: Colors.black)),
|
||||
),
|
||||
child: Row(children: _buildHeaderCells()),
|
||||
),
|
||||
// 스크롤 바디
|
||||
Expanded(
|
||||
child: models.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'등록된 모델이 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ShadTable.list(
|
||||
header: const [
|
||||
ShadTableCell.header(child: Text('ID')),
|
||||
ShadTableCell.header(child: Text('제조사')),
|
||||
ShadTableCell.header(child: Text('모델명')),
|
||||
ShadTableCell.header(child: Text('등록일')),
|
||||
ShadTableCell.header(child: Text('상태')),
|
||||
ShadTableCell.header(child: Text('작업')),
|
||||
],
|
||||
children: currentPageModels.map((model) {
|
||||
final vendor = _controller.getVendorById(model.vendorsId);
|
||||
return [
|
||||
ShadTableCell(child: Text(model.id.toString(), style: ShadcnTheme.bodySmall)),
|
||||
ShadTableCell(child: Text(vendor?.name ?? '알 수 없음', overflow: TextOverflow.ellipsis)),
|
||||
ShadTableCell(child: Text(model.name, overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500))),
|
||||
ShadTableCell(child: Text(model.registeredAt != null ? DateFormat('yyyy-MM-dd').format(model.registeredAt) : '-', style: ShadcnTheme.bodySmall)),
|
||||
ShadTableCell(child: _buildStatusChip(model.isDeleted)),
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showEditDialog(model),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: models.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(models[index], index),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showDeleteConfirmDialog(model),
|
||||
child: Icon(Icons.delete, size: 16, color: ShadcnTheme.destructive),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildPagination(ModelController controller) {
|
||||
// 모델 목록은 현재 페이지네이션이 없는 것 같으니 빈 위젯 반환
|
||||
return const SizedBox();
|
||||
final totalCount = controller.models.length;
|
||||
final totalPages = _getTotalPages();
|
||||
|
||||
if (totalCount <= _pageSize) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
border: Border(
|
||||
top: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'총 $totalCount개 모델',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
border: Border(
|
||||
top: BorderSide(color: ShadcnTheme.border),
|
||||
),
|
||||
),
|
||||
child: Pagination(
|
||||
totalCount: totalCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
onPageChanged: _goToPage,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard(
|
||||
@@ -416,11 +379,15 @@ class _ModelListScreenState extends State<ModelListScreen> {
|
||||
|
||||
Widget _buildStatusChip(bool isDeleted) {
|
||||
if (isDeleted) {
|
||||
return ShadBadge.destructive(
|
||||
return ShadBadge(
|
||||
backgroundColor: ShadcnTheme.equipmentDisposal.withValues(alpha: 0.1),
|
||||
foregroundColor: ShadcnTheme.equipmentDisposal,
|
||||
child: const Text('비활성'),
|
||||
);
|
||||
} else {
|
||||
return ShadBadge.secondary(
|
||||
return ShadBadge(
|
||||
backgroundColor: ShadcnTheme.equipmentIn.withValues(alpha: 0.1),
|
||||
foregroundColor: ShadcnTheme.equipmentIn,
|
||||
child: const Text('활성'),
|
||||
);
|
||||
}
|
||||
@@ -467,4 +434,4 @@ class _ModelListScreenState extends State<ModelListScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,163 +153,6 @@ class _RentListScreenState extends State<RentListScreen> {
|
||||
_controller.loadRents();
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌더
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 셀 빌더
|
||||
Widget _buildDataCell(
|
||||
Widget child, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: container);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: container);
|
||||
}
|
||||
}
|
||||
|
||||
/// 헤더 셀 리스트
|
||||
List<Widget> _buildHeaderCells() {
|
||||
return [
|
||||
_buildHeaderCell('ID', flex: 0, useExpanded: false, minWidth: 60),
|
||||
_buildHeaderCell('장비 이력 ID', flex: 1, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('시작일', flex: 1, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('종료일', flex: 1, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('기간 (일)', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 120),
|
||||
];
|
||||
}
|
||||
|
||||
/// 테이블 행 빌더
|
||||
Widget _buildTableRow(RentDto rent, int index) {
|
||||
final days = _controller.calculateRentDays(rent.startedAt, rent.endedAt);
|
||||
final status = _controller.getRentStatus(rent);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven
|
||||
? ShadcnTheme.muted.withValues(alpha: 0.1)
|
||||
: null,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDataCell(
|
||||
Text(
|
||||
rent.id?.toString() ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 60,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
rent.equipmentHistoryId.toString(),
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
DateFormat('yyyy-MM-dd').format(rent.startedAt),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
DateFormat('yyyy-MM-dd').format(rent.endedAt),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'$days일',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildStatusChip(status),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEditDialog(rent),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (status == '진행중')
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _returnRent(rent),
|
||||
child: const Icon(Icons.assignment_return, size: 16),
|
||||
),
|
||||
if (status == '진행중')
|
||||
const SizedBox(width: 4),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _deleteRent(rent),
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 120,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 상태 배지 빌더
|
||||
Widget _buildStatusChip(String? status) {
|
||||
switch (status) {
|
||||
@@ -332,59 +175,196 @@ class _RentListScreenState extends State<RentListScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 데이터 테이블 빌더
|
||||
Widget _buildDataTable(RentController controller) {
|
||||
final rents = controller.rents;
|
||||
|
||||
|
||||
if (rents.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business_center_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
Text(
|
||||
'등록된 임대 계약이 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(bottom: BorderSide(color: Colors.black)),
|
||||
border: Border(bottom: BorderSide(color: ShadcnTheme.border)),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(ShadcnTheme.radiusMd),
|
||||
topRight: Radius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildHeaderCell('ID', 60),
|
||||
_buildHeaderCell('장비 이력 ID', 120),
|
||||
_buildHeaderCell('시작일', 100),
|
||||
_buildHeaderCell('종료일', 100),
|
||||
_buildHeaderCell('기간 (일)', 80),
|
||||
_buildHeaderCell('상태', 80),
|
||||
_buildHeaderCell('작업', 140),
|
||||
],
|
||||
),
|
||||
child: Row(children: _buildHeaderCells()),
|
||||
),
|
||||
// 스크롤 바디
|
||||
|
||||
// 스크롤 가능한 바디
|
||||
Expanded(
|
||||
child: rents.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business_center_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'등록된 임대 계약이 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: rents.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(rents[index], index),
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemCount: rents.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(rents[index], index),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌드
|
||||
Widget _buildHeaderCell(String text, double width) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Text(
|
||||
text,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 테이블 행 빌드
|
||||
Widget _buildTableRow(RentDto rent, int index) {
|
||||
final days = _controller.calculateRentDays(rent.startedAt, rent.endedAt);
|
||||
final status = _controller.getRentStatus(rent);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven ? ShadcnTheme.muted.withValues(alpha: 0.1) : null,
|
||||
border: Border(bottom: BorderSide(color: ShadcnTheme.border.withValues(alpha: 0.3))),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showEditDialog(rent),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3),
|
||||
child: Row(
|
||||
children: [
|
||||
// ID
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
rent.id?.toString() ?? '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 장비 이력 ID
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
rent.equipmentHistoryId.toString(),
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
// 시작일
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
DateFormat('yyyy-MM-dd').format(rent.startedAt),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 종료일
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
DateFormat('yyyy-MM-dd').format(rent.endedAt),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 기간 (일)
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'$days일',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
|
||||
// 상태
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: _buildStatusChip(status),
|
||||
),
|
||||
|
||||
// 작업 버튼들
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEditDialog(rent),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
if (status == '진행중')
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _returnRent(rent),
|
||||
child: const Icon(Icons.assignment_return, size: 16),
|
||||
),
|
||||
if (status == '진행중')
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _deleteRent(rent),
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
size: 16,
|
||||
color: ShadcnTheme.destructive,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 검색바 빌더
|
||||
Widget _buildSearchBar() {
|
||||
return Row(
|
||||
@@ -465,13 +445,21 @@ class _RentListScreenState extends State<RentListScreen> {
|
||||
Widget? _buildPagination() {
|
||||
return Consumer<RentController>(
|
||||
builder: (context, controller, child) {
|
||||
if (controller.totalPages <= 1) return const SizedBox.shrink();
|
||||
|
||||
return Pagination(
|
||||
totalCount: controller.totalRents,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: AppConstants.rentPageSize,
|
||||
onPageChanged: (page) => controller.loadRents(page: page),
|
||||
// 항상 페이지네이션 정보 표시 (총 개수라도)
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: ShadcnTheme.spacing2),
|
||||
child: controller.totalPages > 1
|
||||
? Pagination(
|
||||
totalCount: controller.totalRents,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: AppConstants.rentPageSize,
|
||||
onPageChanged: (page) => controller.loadRents(page: page),
|
||||
)
|
||||
: Text(
|
||||
'총 ${controller.totalRents}개 임대 계약',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:superport/screens/user/controllers/user_form_controller.dart';
|
||||
import 'package:superport/utils/formatters/korean_phone_formatter.dart';
|
||||
import 'package:superport/screens/common/widgets/standard_dropdown.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
|
||||
// 사용자 등록/수정 화면 (UI만 담당, 상태/로직 분리)
|
||||
class UserFormScreen extends StatefulWidget {
|
||||
@@ -39,17 +40,37 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
},
|
||||
child: Consumer<UserFormController>(
|
||||
builder: (context, controller, child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(controller.isEditMode ? '사용자 수정' : '사용자 등록'),
|
||||
),
|
||||
body: controller.isLoading
|
||||
// Phase 10: FormLayoutTemplate 적용
|
||||
return FormLayoutTemplate(
|
||||
title: controller.isEditMode ? '사용자 수정' : '사용자 등록',
|
||||
isLoading: controller.isLoading,
|
||||
onSave: () async {
|
||||
// 폼 검증
|
||||
if (!controller.formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
controller.formKey.currentState!.save();
|
||||
|
||||
// 사용자 저장
|
||||
bool success = false;
|
||||
await controller.saveUser((error) {
|
||||
if (error == null) {
|
||||
success = true;
|
||||
} else {
|
||||
// 에러 처리는 controller에서 관리
|
||||
}
|
||||
});
|
||||
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
child: controller.isLoading
|
||||
? const Center(child: ShadProgress())
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
: Form(
|
||||
key: controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -85,32 +106,35 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
// 회사 선택 (*필수)
|
||||
_buildCompanyDropdown(controller),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 중복 검사 상태 메시지 영역 (고정 높이)
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Center(
|
||||
child: controller.isCheckingEmailDuplicate
|
||||
? const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ShadProgress(),
|
||||
SizedBox(width: 8),
|
||||
Text('중복 검사 중...'),
|
||||
],
|
||||
)
|
||||
: controller.emailDuplicateMessage != null
|
||||
? Text(
|
||||
controller.emailDuplicateMessage!,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
// 중복 검사 상태 메시지 영역
|
||||
if (controller.isCheckingEmailDuplicate)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ShadProgress(),
|
||||
SizedBox(width: 8),
|
||||
Text('중복 검사 중...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (controller.emailDuplicateMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
controller.emailDuplicateMessage!,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 오류 메시지 표시
|
||||
if (controller.error != null)
|
||||
@@ -121,22 +145,10 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
description: Text(controller.error!),
|
||||
),
|
||||
),
|
||||
// 저장 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ShadButton(
|
||||
onPressed: controller.isLoading || controller.isCheckingEmailDuplicate
|
||||
? null
|
||||
: () => _onSaveUser(controller),
|
||||
size: ShadButtonSize.lg,
|
||||
child: Text(controller.isEditMode ? '수정하기' : '등록하기'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -153,7 +165,6 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
String? Function(String?)? validator,
|
||||
void Function(String?)? onSaved,
|
||||
void Function(String)? onChanged,
|
||||
Widget? suffixIcon,
|
||||
}) {
|
||||
final controller = TextEditingController(text: initialValue.isNotEmpty ? initialValue : '');
|
||||
return Padding(
|
||||
@@ -247,45 +258,4 @@ class _UserFormScreenState extends State<UserFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 저장 버튼 클릭 시 사용자 저장
|
||||
void _onSaveUser(UserFormController controller) async {
|
||||
// 먼저 폼 유효성 검사
|
||||
if (controller.formKey.currentState?.validate() != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 폼 데이터 저장
|
||||
controller.formKey.currentState?.save();
|
||||
|
||||
// 이메일 중복 검사 (저장 시점)
|
||||
final emailIsUnique = await controller.checkDuplicateEmail(controller.email);
|
||||
|
||||
if (!emailIsUnique) {
|
||||
// 중복이 발견되면 저장하지 않음
|
||||
return;
|
||||
}
|
||||
|
||||
// 이메일 중복이 없으면 저장 진행
|
||||
await controller.saveUser((error) {
|
||||
if (error != null) {
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('오류'),
|
||||
description: Text(error),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('성공'),
|
||||
description: Text(
|
||||
controller.isEditMode ? '사용자 정보가 수정되었습니다' : '사용자가 등록되었습니다',
|
||||
),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/models/user_model.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
@@ -55,24 +54,28 @@ class _UserListState extends State<UserList> {
|
||||
/// 사용자 권한 표시 배지
|
||||
Widget _buildUserRoleBadge(UserRole role) {
|
||||
final roleName = role.displayName;
|
||||
ShadcnBadgeVariant variant;
|
||||
Color backgroundColor;
|
||||
Color foregroundColor;
|
||||
|
||||
switch (role) {
|
||||
case UserRole.admin:
|
||||
variant = ShadcnBadgeVariant.destructive;
|
||||
backgroundColor = ShadcnTheme.alertCritical7.withValues(alpha: 0.1);
|
||||
foregroundColor = ShadcnTheme.alertCritical7;
|
||||
break;
|
||||
case UserRole.manager:
|
||||
variant = ShadcnBadgeVariant.primary;
|
||||
backgroundColor = ShadcnTheme.companyHeadquarters.withValues(alpha: 0.1);
|
||||
foregroundColor = ShadcnTheme.companyHeadquarters;
|
||||
break;
|
||||
case UserRole.staff:
|
||||
variant = ShadcnBadgeVariant.secondary;
|
||||
backgroundColor = ShadcnTheme.equipmentDisposal.withValues(alpha: 0.1);
|
||||
foregroundColor = ShadcnTheme.equipmentDisposal;
|
||||
break;
|
||||
}
|
||||
|
||||
return ShadcnBadge(
|
||||
text: roleName,
|
||||
variant: variant,
|
||||
size: ShadcnBadgeSize.small,
|
||||
return ShadBadge(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
child: Text(roleName),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,21 +205,21 @@ class _UserListState extends State<UserList> {
|
||||
'전체 사용자',
|
||||
_controller.total.toString(),
|
||||
Icons.people,
|
||||
ShadcnTheme.primary,
|
||||
ShadcnTheme.companyHeadquarters,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
_buildStatCard(
|
||||
'활성 사용자',
|
||||
_controller.users.where((u) => u.isActive).length.toString(),
|
||||
Icons.check_circle,
|
||||
ShadcnTheme.success,
|
||||
ShadcnTheme.equipmentIn,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
_buildStatCard(
|
||||
'비활성 사용자',
|
||||
_controller.users.where((u) => !u.isActive).length.toString(),
|
||||
Icons.person_off,
|
||||
ShadcnTheme.mutedForeground,
|
||||
ShadcnTheme.equipmentDisposal,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -265,203 +268,92 @@ class _UserListState extends State<UserList> {
|
||||
Widget _buildDataTable() {
|
||||
final users = _controller.users;
|
||||
|
||||
if (users.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'등록된 사용자가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(bottom: BorderSide(color: Colors.black)),
|
||||
),
|
||||
child: Row(children: _buildHeaderCells()),
|
||||
),
|
||||
// 스크롤 바디
|
||||
Expanded(
|
||||
child: users.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'등록된 사용자가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: users.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(users[index], index),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
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('상태')),
|
||||
ShadTableCell.header(child: Text('작업')),
|
||||
],
|
||||
children: [
|
||||
for (int index = 0; index < users.length; index++)
|
||||
[
|
||||
// 번호
|
||||
ShadTableCell(child: Text(((_controller.currentPage - 1) * _controller.pageSize + index + 1).toString(), style: ShadcnTheme.bodySmall)),
|
||||
// 이름
|
||||
ShadTableCell(child: Text(users[index].name, overflow: TextOverflow.ellipsis)),
|
||||
// 이메일
|
||||
ShadTableCell(child: Text(users[index].email ?? '-', overflow: TextOverflow.ellipsis, style: ShadcnTheme.bodySmall)),
|
||||
// 회사
|
||||
ShadTableCell(child: Text(users[index].companyName ?? '-', overflow: TextOverflow.ellipsis)),
|
||||
// 권한
|
||||
ShadTableCell(child: _buildUserRoleBadge(users[index].role)),
|
||||
// 상태
|
||||
ShadTableCell(child: _buildStatusChip(users[index].isActive)),
|
||||
// 작업
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: users[index].id != null ? () => _navigateToEdit(users[index].id!) : null,
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showStatusChangeDialog(users[index]),
|
||||
child: Icon(users[index].isActive ? Icons.person_off : Icons.person, size: 16),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: users[index].id != null ? () => _showDeleteDialog(users[index].id!, users[index].name) : null,
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildHeaderCells() {
|
||||
return [
|
||||
_buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50),
|
||||
_buildHeaderCell('이름', flex: 2, useExpanded: true, minWidth: 80),
|
||||
_buildHeaderCell('이메일', flex: 3, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('회사', flex: 2, useExpanded: true, minWidth: 80),
|
||||
_buildHeaderCell('권한', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 120),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDataCell(
|
||||
Widget child, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: container);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: container);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTableRow(User user, int index) {
|
||||
final rowNumber = (_controller.currentPage - 1) * _controller.pageSize + index + 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven
|
||||
? ShadcnTheme.muted.withValues(alpha: 0.1)
|
||||
: null,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDataCell(
|
||||
Text(
|
||||
rowNumber.toString(),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 50,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
user.name,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
user.email ?? '',
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'-', // Company name not available in current model
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildUserRoleBadge(user.role),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildStatusChip(user.isActive),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: user.id != null ? () => _navigateToEdit(user.id!) : null,
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showStatusChangeDialog(user),
|
||||
child: Icon(
|
||||
user.isActive ? Icons.person_off : Icons.person,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: user.id != null ? () => _showDeleteDialog(user.id!, user.name) : null,
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 120,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// (Deprecated) 기존 커스텀 테이블 빌더 유틸들은 ShadTable 전환으로 제거되었습니다.
|
||||
|
||||
Widget _buildPagination() {
|
||||
if (_controller.totalPages <= 1) return const SizedBox();
|
||||
|
||||
@@ -524,11 +416,15 @@ class _UserListState extends State<UserList> {
|
||||
|
||||
Widget _buildStatusChip(bool isActive) {
|
||||
if (isActive) {
|
||||
return ShadBadge.secondary(
|
||||
return ShadBadge(
|
||||
backgroundColor: ShadcnTheme.equipmentIn.withValues(alpha: 0.1),
|
||||
foregroundColor: ShadcnTheme.equipmentIn,
|
||||
child: const Text('활성'),
|
||||
);
|
||||
} else {
|
||||
return ShadBadge.destructive(
|
||||
return ShadBadge(
|
||||
backgroundColor: ShadcnTheme.equipmentDisposal.withValues(alpha: 0.1),
|
||||
foregroundColor: ShadcnTheme.equipmentDisposal,
|
||||
child: const Text('비활성'),
|
||||
);
|
||||
}
|
||||
@@ -536,4 +432,3 @@ class _UserListState extends State<UserList> {
|
||||
|
||||
// StandardDataRow 임시 정의
|
||||
}
|
||||
|
||||
|
||||
253
lib/screens/vendor/vendor_list_screen.dart
vendored
253
lib/screens/vendor/vendor_list_screen.dart
vendored
@@ -164,21 +164,21 @@ class _VendorListScreenState extends State<VendorListScreen> {
|
||||
'전체 벤더',
|
||||
controller.totalCount.toString(),
|
||||
Icons.business,
|
||||
ShadcnTheme.primary,
|
||||
ShadcnTheme.companyPartner,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
_buildStatCard(
|
||||
'활성 벤더',
|
||||
controller.vendors.where((v) => !v.isDeleted).length.toString(),
|
||||
Icons.check_circle,
|
||||
ShadcnTheme.success,
|
||||
ShadcnTheme.equipmentIn,
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing4),
|
||||
_buildStatCard(
|
||||
'비활성 벤더',
|
||||
controller.vendors.where((v) => v.isDeleted).length.toString(),
|
||||
Icons.cancel,
|
||||
ShadcnTheme.mutedForeground,
|
||||
ShadcnTheme.equipmentDisposal,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -222,187 +222,88 @@ class _VendorListScreenState extends State<VendorListScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 헤더 셀 빌더
|
||||
Widget _buildHeaderCell(
|
||||
String text, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
text,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: child);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 셀 빌더
|
||||
Widget _buildDataCell(
|
||||
Widget child, {
|
||||
required int flex,
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (useExpanded) {
|
||||
return Expanded(flex: flex, child: container);
|
||||
} else {
|
||||
return SizedBox(width: minWidth, child: container);
|
||||
}
|
||||
}
|
||||
|
||||
/// 헤더 셀 리스트
|
||||
List<Widget> _buildHeaderCells() {
|
||||
return [
|
||||
_buildHeaderCell('번호', flex: 0, useExpanded: false, minWidth: 50),
|
||||
_buildHeaderCell('벤더명', flex: 3, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('등록일', flex: 2, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('상태', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 100),
|
||||
];
|
||||
}
|
||||
|
||||
/// 테이블 행 빌더
|
||||
Widget _buildTableRow(dynamic vendor, int index) {
|
||||
final rowNumber = (_controller.currentPage - 1) * _controller.pageSize + index + 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven
|
||||
? ShadcnTheme.muted.withValues(alpha: 0.1)
|
||||
: null,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDataCell(
|
||||
Text(
|
||||
rowNumber.toString(),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 50,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
vendor.name,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
vendor.createdAt != null
|
||||
? DateFormat('yyyy-MM-dd').format(vendor.createdAt!)
|
||||
: '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
_buildStatusChip(vendor.isDeleted),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showEditDialog(vendor.id!),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showDeleteConfirmDialog(vendor.id!, vendor.name),
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 100,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataTable(VendorController controller) {
|
||||
final vendors = controller.vendors;
|
||||
|
||||
if (vendors.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: ShadcnTheme.spacing4),
|
||||
Text(
|
||||
'등록된 벤더가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(bottom: BorderSide(color: Colors.black)),
|
||||
),
|
||||
child: Row(children: _buildHeaderCells()),
|
||||
),
|
||||
// 스크롤 바디
|
||||
Expanded(
|
||||
child: vendors.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.business_outlined,
|
||||
size: 64,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'등록된 벤더가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: vendors.length,
|
||||
itemBuilder: (context, index) => _buildTableRow(vendors[index], index),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
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('작업')),
|
||||
],
|
||||
children: [
|
||||
for (int index = 0; index < vendors.length; index++)
|
||||
[
|
||||
// 번호
|
||||
ShadTableCell(child: Text(((_controller.currentPage - 1) * _controller.pageSize + index + 1).toString(), style: ShadcnTheme.bodySmall)),
|
||||
// 벤더명
|
||||
ShadTableCell(child: Text(vendors[index].name, overflow: TextOverflow.ellipsis)),
|
||||
// 등록일
|
||||
ShadTableCell(child: Text(
|
||||
vendors[index].createdAt != null
|
||||
? DateFormat('yyyy-MM-dd').format(vendors[index].createdAt!)
|
||||
: '-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
)),
|
||||
// 상태
|
||||
ShadTableCell(child: _buildStatusChip(vendors[index].isDeleted)),
|
||||
// 작업
|
||||
ShadTableCell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showEditDialog(vendors[index].id!),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: ShadcnTheme.spacing1),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => _showDeleteConfirmDialog(vendors[index].id!, vendors[index].name),
|
||||
child: Icon(Icons.delete, size: 16, color: ShadcnTheme.destructive),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildPagination(VendorController controller) {
|
||||
if (controller.totalPages <= 1) return const SizedBox();
|
||||
|
||||
@@ -465,16 +366,18 @@ class _VendorListScreenState extends State<VendorListScreen> {
|
||||
|
||||
Widget _buildStatusChip(bool isDeleted) {
|
||||
if (isDeleted) {
|
||||
return ShadBadge.destructive(
|
||||
return ShadBadge(
|
||||
backgroundColor: ShadcnTheme.equipmentDisposal.withValues(alpha: 0.1),
|
||||
foregroundColor: ShadcnTheme.equipmentDisposal,
|
||||
child: const Text('비활성'),
|
||||
);
|
||||
} else {
|
||||
return ShadBadge.secondary(
|
||||
return ShadBadge(
|
||||
backgroundColor: ShadcnTheme.equipmentIn.withValues(alpha: 0.1),
|
||||
foregroundColor: ShadcnTheme.equipmentIn,
|
||||
child: const Text('활성'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// StandardDataRow 클래스 정의 (임시)
|
||||
}
|
||||
|
||||
|
||||
@@ -290,142 +290,5 @@ class ZipcodeTable extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddressDetails(BuildContext context, ZipcodeDto zipcode) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog(
|
||||
title: const Text('우편번호 상세정보'),
|
||||
description: const Text('선택한 우편번호의 상세 정보입니다'),
|
||||
child: Container(
|
||||
width: 400,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 우편번호
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'우편번호',
|
||||
zipcode.zipcode.toString().padLeft(5, '0'),
|
||||
Icons.local_post_office,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 시도
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'시도',
|
||||
zipcode.sido,
|
||||
Icons.location_city,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 구/군
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'구/군',
|
||||
zipcode.gu,
|
||||
Icons.location_on,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 상세주소
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'상세주소',
|
||||
zipcode.etc,
|
||||
Icons.home,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 전체주소
|
||||
_buildInfoRow(
|
||||
context,
|
||||
'전체주소',
|
||||
zipcode.fullAddress,
|
||||
Icons.place,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 액션 버튼
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ShadButton.outline(
|
||||
onPressed: () => _copyToClipboard(
|
||||
context,
|
||||
zipcode.fullAddress,
|
||||
'전체주소'
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.copy, size: 16),
|
||||
SizedBox(width: 6),
|
||||
Text('주소 복사'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ShadButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onSelect(zipcode);
|
||||
},
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.check, size: 16),
|
||||
SizedBox(width: 6),
|
||||
Text('선택'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(BuildContext context, String label, String value, IconData icon) {
|
||||
final theme = ShadTheme.of(context);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: theme.textTheme.small.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user