import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'dart:async'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:superport/core/constants/app_constants.dart'; import 'package:superport/models/company_item_model.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/common/widgets/pagination.dart'; import 'package:superport/screens/common/widgets/unified_search_bar.dart'; import 'package:superport/screens/common/widgets/standard_action_bar.dart'; import 'package:superport/screens/common/widgets/standard_states.dart'; import 'package:superport/screens/common/layouts/base_list_screen.dart'; import 'package:superport/screens/company/controllers/company_list_controller.dart'; import 'package:superport/screens/company/components/company_tree_view.dart'; /// shadcn/ui 스타일로 재설계된 회사 관리 화면 (통일된 UI 컴포넌트 사용) class CompanyList extends StatefulWidget { const CompanyList({super.key}); @override State createState() => _CompanyListState(); } class _CompanyListState extends State { late CompanyListController _controller; final TextEditingController _searchController = TextEditingController(); Timer? _debounceTimer; @override void initState() { super.initState(); _controller = CompanyListController(); _controller.initialize(pageSize: 10); // 통일된 초기화 방식 } @override void dispose() { _controller.dispose(); _searchController.dispose(); _debounceTimer?.cancel(); super.dispose(); } /// 검색어 입력 처리 (디바운싱) void _onSearchChanged(String value) { _debounceTimer?.cancel(); _debounceTimer = Timer(AppConstants.searchDebounce, () { _controller.search(value); // Controller가 페이지 리셋 처리 }); } /// 회사 추가 화면으로 이동 void _navigateToAddScreen() async { final result = await Navigator.pushNamed(context, '/company/add'); if (result == true) { _controller.refresh(); } } /// 지점 추가 화면으로 이동 void _navigateToBranchAddScreen() async { final result = await Navigator.pushNamed(context, '/company/branch/add'); if (result == true) { _controller.refresh(); } } /// 회사 삭제 처리 void _deleteCompany(int id) { showDialog( context: context, builder: (context) => ShadDialog( title: const Text('삭제 확인'), description: const Text('이 회사 정보를 삭제하시겠습니까?'), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ ShadButton.outline( onPressed: () => Navigator.pop(context), child: const Text('취소'), ), const SizedBox(width: 8), ShadButton( onPressed: () async { Navigator.pop(context); try { await _controller.deleteCompany(id); if (mounted) { ShadToaster.of(context).show( const ShadToast( title: Text('성공'), description: Text('회사가 삭제되었습니다.'), ), ); } } catch (e) { if (mounted) { ShadToaster.of(context).show( ShadToast.destructive( title: const Text('오류'), description: Text(e.toString()), ), ); } } }, child: const Text('삭제'), ), ], ), ), ); } /// 지점 삭제 처리 void _deleteBranch(int companyId, int branchId) { showDialog( context: context, builder: (context) => ShadDialog( title: const Text('지점 삭제 확인'), description: const Text('이 지점 정보를 삭제하시겠습니까?'), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ ShadButton.outline( onPressed: () => Navigator.pop(context), child: const Text('취소'), ), const SizedBox(width: 8), ShadButton( onPressed: () async { Navigator.pop(context); try { await _controller.deleteBranch(companyId, branchId); if (mounted) { ShadToaster.of(context).show( const ShadToast( title: Text('성공'), description: Text('지점이 삭제되었습니다.'), ), ); } } catch (e) { if (mounted) { ShadToaster.of(context).show( ShadToast.destructive( title: const Text('오류'), description: Text(e.toString()), ), ); } } }, child: const Text('삭제'), ), ], ), ), ); } /// 본사/지점 구분 배지 생성 Widget _buildCompanyTypeLabel(bool isBranch) { return ShadcnBadge( text: isBranch ? '지점' : '본사', variant: isBranch ? ShadcnBadgeVariant.companyBranch // Purple (#7C3AED) - 차별화 : ShadcnBadgeVariant.companyHeadquarters, // Blue (#2563EB) size: ShadcnBadgeSize.small, ); } /// CompanyItem의 계층적 이름 표시 Widget _buildDisplayNameText(CompanyItem item) { if (item.isBranch) { return Text.rich( TextSpan( children: [ TextSpan(text: '${item.parentCompanyName} > ', style: ShadcnTheme.bodyMuted), TextSpan(text: item.name, style: ShadcnTheme.bodyMedium), ], ), ); } else { return Text(item.name, style: ShadcnTheme.bodyMedium); } } /// 활성 상태 배지 생성 Widget _buildStatusBadge(bool isActive) { return ShadcnBadge( text: isActive ? '활성' : '비활성', variant: isActive ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.secondary, size: ShadcnBadgeSize.small, ); } /// 날짜 포맷팅 String _formatDate(DateTime? date) { if (date == null) return '-'; return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } /// 담당자 정보 통합 표시 (이름 + 직책) Widget _buildContactInfo(CompanyItem item) { final name = item.contactName ?? '-'; final position = item.contactPosition; if (position != null && position.isNotEmpty) { return Text( '$name ($position)', style: ShadcnTheme.bodySmall.copyWith(fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, maxLines: 1, ); } else { return Text( name, style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 1, ); } } /// 연락처 정보 통합 표시 (전화 + 이메일) Widget _buildContactDetails(CompanyItem item) { final phone = item.contactPhone ?? '-'; final email = item.contactEmail; if (email != null && email.isNotEmpty) { return Text( '$phone\n$email', style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 2, ); } else { return Text( phone, style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 1, ); } } /// 파트너/고객 플래그 표시 Widget _buildPartnerCustomerFlags(CompanyItem item) { if (item.isBranch) { return Text('-', style: ShadcnTheme.bodySmall); } final flags = []; if (item.isPartner) { flags.add(ShadcnBadge( text: '파트너', variant: ShadcnBadgeVariant.companyPartner, size: ShadcnBadgeSize.small, )); } if (item.isCustomer) { flags.add(ShadcnBadge( text: '고객', variant: ShadcnBadgeVariant.companyCustomer, size: ShadcnBadgeSize.small, )); } if (flags.isEmpty) { return Text('-', style: ShadcnTheme.bodySmall); } return Wrap( spacing: 4, runSpacing: 2, children: flags, ); } /// 등록일/수정일 표시 Widget _buildDateInfo(CompanyItem item) { final createdAt = item.createdAt; final updatedAt = item.updatedAt; if (createdAt == null) { return Text('-', style: ShadcnTheme.bodySmall); } final created = _formatDate(createdAt); if (updatedAt != null && updatedAt != createdAt) { return Text( '등록:$created\n수정:${_formatDate(updatedAt)}', style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 2, ); } else { return Text( created, style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, maxLines: 1, ); } } /// ShadTable을 사용한 회사 데이터 테이블 빌드 Widget _buildCompanyShadTable(List items, CompanyListController controller) { final theme = ShadTheme.of(context); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: ConstrainedBox( constraints: const BoxConstraints( minWidth: 1200, // 최소 너비 설정 maxWidth: 2000, // 최대 너비 설정 ), child: ShadTable( columnCount: 11, rowCount: items.length + 1, // +1 for header header: (context, column) { final headers = [ '번호', '회사명', '구분', '주소', '담당자', '연락처', '파트너/고객', '상태', '등록/수정일', '비고', '관리' ]; return ShadTableCell( child: Container( constraints: const BoxConstraints( minHeight: 50, // 헤더 높이 maxHeight: 50, ), alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Text( headers[column], style: theme.textTheme.muted.copyWith(fontWeight: FontWeight.bold), ), ), ); }, builder: (context, vicinity) { final column = vicinity.column; final row = vicinity.row - 1; // -1 because header is row 0 if (row < 0 || row >= items.length) { return const ShadTableCell(child: SizedBox.shrink()); } final item = items[row]; final index = ((controller.currentPage - 1) * controller.pageSize) + row; // 모든 셀에 최소 높이 설정 Widget wrapWithHeight(Widget child) { return Container( constraints: const BoxConstraints( minHeight: 60, // 최소 높이 60px maxHeight: 80, // 최대 높이 80px ), alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: child, ); } switch (column) { case 0: // 번호 return ShadTableCell( child: wrapWithHeight( Text('${index + 1}', style: theme.textTheme.small) ) ); case 1: // 회사명 return ShadTableCell( child: wrapWithHeight(_buildDisplayNameText(item)) ); case 2: // 구분 return ShadTableCell( child: wrapWithHeight(_buildCompanyTypeLabel(item.isBranch)) ); case 3: // 주소 return ShadTableCell( child: wrapWithHeight( Text( item.address.isNotEmpty ? item.address : '-', style: theme.textTheme.small, overflow: TextOverflow.ellipsis, maxLines: 2, ), ), ); case 4: // 담당자 return ShadTableCell( child: wrapWithHeight(_buildContactInfo(item)) ); case 5: // 연락처 return ShadTableCell( child: wrapWithHeight(_buildContactDetails(item)) ); case 6: // 파트너/고객 return ShadTableCell( child: wrapWithHeight(_buildPartnerCustomerFlags(item)) ); case 7: // 상태 return ShadTableCell( child: wrapWithHeight(_buildStatusBadge(item.isActive)) ); case 8: // 등록/수정일 return ShadTableCell( child: wrapWithHeight(_buildDateInfo(item)) ); case 9: // 비고 return ShadTableCell( child: wrapWithHeight( Text( item.remark ?? '-', style: theme.textTheme.small, overflow: TextOverflow.ellipsis, maxLines: 2, ), ), ); case 10: // 관리 return ShadTableCell( child: wrapWithHeight( SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.min, children: [ if (item.id != null) ...[ InkWell( onTap: () { 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: Container( width: 24, height: 24, alignment: Alignment.center, child: const Icon(Icons.edit, size: 16), ), ), const SizedBox(width: 4), InkWell( onTap: () { if (item.isBranch) { _deleteBranch(item.parentCompanyId!, item.id!); } else { _deleteCompany(item.id!); } }, child: Container( width: 24, height: 24, alignment: Alignment.center, child: const Icon(Icons.delete, size: 16), ), ), ], ], ), ), ), ); default: return const ShadTableCell(child: SizedBox.shrink()); } }, ), ), ); } /// Tree View 빌드 Widget _buildTreeView(BuildContext context, CompanyListController controller) { if (controller.companyHierarchy == null) { return const StandardLoadingState(message: '계층 구조를 불러오는 중...'); } return CompanyTreeView( hierarchy: controller.companyHierarchy!, expandedNodes: controller.expandedNodes, onToggleExpand: controller.toggleNodeExpansion, onNodeTap: (nodeId) { // 회사 상세 화면으로 이동 Navigator.pushNamed( context, '/company/edit', arguments: {'companyId': int.parse(nodeId)}, ); }, onEdit: (nodeId) { Navigator.pushNamed( context, '/company/edit', arguments: {'companyId': int.parse(nodeId)}, ); }, onDelete: (nodeId) async { final companyId = int.parse(nodeId); // 삭제 가능 여부 먼저 확인 final canDelete = await controller.canDeleteCompany(companyId); if (canDelete) { _deleteCompany(companyId); } else { if (mounted) { ShadToaster.of(context).show( const ShadToast( title: Text('삭제 불가'), description: Text('자식 회사가 있어 삭제할 수 없습니다.'), ), ); } } }, ); } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _controller, child: Consumer( builder: (context, controller, child) { // CompanyItem 데이터 직접 사용 (복잡한 변환 로직 제거) final companyItems = controller.companyItems; final int totalCount = controller.total; final int actualHeadquartersCount = controller.actualHeadquartersCount; final int actualBranchesCount = totalCount - actualHeadquartersCount; // 지점 개수 = 전체 - 본사 final int displayedHeadquartersCount = controller.displayedHeadquartersCount; final int displayedBranchesCount = companyItems.where((item) => item.isBranch).length; print('🔍 [VIEW DEBUG] CompanyItem 페이지네이션 상태'); print(' • CompanyItem items: ${controller.companyItems.length}개'); print(' • 전체 개수: ${controller.total}개'); print(' • 실제 본사 개수(API): $actualHeadquartersCount개'); print(' • 실제 지점 개수(계산): $actualBranchesCount개'); print(' • 표시된 본사 개수: $displayedHeadquartersCount개'); print(' • 표시된 지점 개수: $displayedBranchesCount개'); print(' • 현재 페이지: ${controller.currentPage}'); print(' • 페이지 크기: ${controller.pageSize}'); // 로딩 상태 if (controller.isLoading && controller.companyItems.isEmpty) { return const StandardLoadingState(message: '회사 데이터를 불러오는 중...'); } return BaseListScreen( isLoading: false, error: controller.error, onRefresh: controller.refresh, emptyMessage: controller.searchQuery.isNotEmpty ? '검색 결과가 없습니다' : '등록된 회사가 없습니다', emptyIcon: Icons.business_outlined, // 검색바 searchBar: UnifiedSearchBar( controller: _searchController, placeholder: '회사명, 담당자명, 연락처로 검색', onChanged: _onSearchChanged, // 실시간 검색 (디바운싱) onSearch: () => _controller.search( _searchController.text, ), // 즉시 검색 onClear: () { _searchController.clear(); _onSearchChanged(''); }, ), // 액션바 actionBar: StandardActionBar( leftActions: [ // 회사 추가 버튼 StandardActionButtons.addButton( text: '회사 추가', onPressed: _navigateToAddScreen, ), // 지점 추가 버튼 StandardActionButtons.addButton( text: '지점 추가', onPressed: _navigateToBranchAddScreen, icon: Icons.domain_add, ), ], rightActions: [ // Tree View 토글 버튼 IconButton( icon: Icon( controller.isTreeView ? Icons.list : Icons.account_tree, color: controller.isTreeView ? ShadcnTheme.primary : null, ), tooltip: controller.isTreeView ? '리스트 보기' : '계층 보기', onPressed: () => controller.toggleTreeView(), ), const SizedBox(width: 8), // 관리자용 비활성 포함 체크박스 // TODO: 실제 권한 체크 로직 추가 필요 Row( children: [ Checkbox( value: controller.includeInactive, onChanged: (_) => controller.toggleIncludeInactive(), ), const Text('비활성 포함'), ], ), ], totalCount: totalCount, // 전체 회사 수 (본사 + 지점) onRefresh: controller.refresh, statusMessage: controller.searchQuery.isNotEmpty ? '"${controller.searchQuery}" 검색 결과' : actualHeadquartersCount > 0 ? '본사: $actualHeadquartersCount개, 지점: $actualBranchesCount개 총 $totalCount개' : null, ), // 에러 메시지 filterSection: controller.error != null ? StandardInfoMessage( message: controller.error!, icon: Icons.error_outline, color: ShadcnTheme.destructive, onClose: controller.clearError, ) : null, // 데이터 테이블 또는 Tree View dataTable: controller.isTreeView ? _buildTreeView(context, controller) : companyItems.isEmpty ? StandardEmptyState( title: controller.searchQuery.isNotEmpty ? '검색 결과가 없습니다' : '등록된 회사가 없습니다', icon: Icons.business_outlined, action: controller.searchQuery.isEmpty ? StandardActionButtons.addButton( text: '첫 회사 추가하기', onPressed: _navigateToAddScreen, ) : null, ) : _buildCompanyShadTable(companyItems, controller), // 페이지네이션 (BaseListController의 goToPage 사용) pagination: Pagination( totalCount: controller.total, currentPage: controller.currentPage, pageSize: controller.pageSize, onPageChanged: (page) { controller.goToPage(page); }, ), ); }, ), ); } }