import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.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/widgets/pagination.dart'; import 'package:superport/screens/user/controllers/user_list_controller.dart'; import 'package:superport/utils/constants.dart'; import 'package:superport/services/mock_data_service.dart'; import 'package:superport/utils/user_utils.dart'; /// shadcn/ui 스타일로 재설계된 사용자 관리 화면 class UserListRedesign extends StatefulWidget { const UserListRedesign({super.key}); @override State createState() => _UserListRedesignState(); } class _UserListRedesignState extends State { final MockDataService _dataService = MockDataService(); final TextEditingController _searchController = TextEditingController(); int _currentPage = 1; final int _pageSize = 10; @override void initState() { super.initState(); // 초기 데이터 로드 WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadUsers(); }); // 검색 디바운싱 _searchController.addListener(() { _onSearchChanged(_searchController.text); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } /// 검색어 변경 처리 (디바운싱) Timer? _debounce; void _onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 300), () { setState(() { _currentPage = 1; }); context.read().setSearchQuery(query); }); } /// 회사명 반환 함수 String _getCompanyName(int companyId) { final company = _dataService.getCompanyById(companyId); return company?.name ?? '-'; } /// 상태별 색상 반환 Color _getStatusColor(bool isActive) { return isActive ? Colors.green : Colors.red; } /// 사용자 권한 표시 배지 Widget _buildUserRoleBadge(String role) { final roleName = getRoleName(role); ShadcnBadgeVariant variant; switch (role) { case 'S': variant = ShadcnBadgeVariant.destructive; break; case 'M': variant = ShadcnBadgeVariant.primary; break; default: variant = ShadcnBadgeVariant.outline; } return ShadcnBadge( text: roleName, variant: variant, size: ShadcnBadgeSize.small, ); } /// 사용자 추가 폼으로 이동 void _navigateToAdd() async { final result = await Navigator.pushNamed(context, Routes.userAdd); if (result == true && mounted) { context.read().loadUsers(refresh: true); } } /// 사용자 수정 폼으로 이동 void _navigateToEdit(int userId) async { final result = await Navigator.pushNamed( context, Routes.userEdit, arguments: userId, ); if (result == true && mounted) { context.read().loadUsers(refresh: true); } } /// 사용자 삭제 다이얼로그 void _showDeleteDialog(int userId, String userName) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('사용자 삭제'), content: Text('"$userName" 사용자를 정말로 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), TextButton( onPressed: () async { Navigator.of(context).pop(); await context.read().deleteUser( userId, () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('사용자가 삭제되었습니다')), ); }, (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(error), backgroundColor: Colors.red), ); }, ); }, child: const Text('삭제', style: TextStyle(color: Colors.red)), ), ], ), ); } /// 상태 변경 확인 다이얼로그 void _showStatusChangeDialog(User user) { final newStatus = !user.isActive; final statusText = newStatus ? '활성화' : '비활성화'; showDialog( context: context, builder: (context) => AlertDialog( title: Text('사용자 상태 변경'), content: Text('"${user.name}" 사용자를 $statusText 하시겠습니까?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('취소'), ), TextButton( onPressed: () async { Navigator.of(context).pop(); await context.read().changeUserStatus( user.id!, newStatus, (error) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(error), backgroundColor: Colors.red), ); }, ); }, child: Text(statusText), ), ], ), ); } @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => UserListController(dataService: _dataService), child: Consumer( builder: (context, controller, child) { if (controller.isLoading && controller.users.isEmpty) { return const Center( child: CircularProgressIndicator(), ); } if (controller.error != null && controller.users.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.red[300]), const SizedBox(height: 16), Text( '데이터를 불러올 수 없습니다', style: ShadcnTheme.headingH4, ), const SizedBox(height: 8), Text( controller.error!, style: ShadcnTheme.bodyMuted, textAlign: TextAlign.center, ), const SizedBox(height: 16), ShadcnButton( text: '다시 시도', onPressed: () => controller.loadUsers(refresh: true), variant: ShadcnButtonVariant.primary, ), ], ), ); } // 페이지네이션을 위한 데이터 처리 final int totalUsers = controller.users.length; final int startIndex = (_currentPage - 1) * _pageSize; final int endIndex = startIndex + _pageSize; final List pagedUsers = controller.users.sublist( startIndex, endIndex > totalUsers ? totalUsers : endIndex, ); return SingleChildScrollView( padding: const EdgeInsets.all(ShadcnTheme.spacing6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 검색 및 필터 섹션 Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), side: BorderSide(color: Colors.black), ), child: Padding( padding: const EdgeInsets.all(ShadcnTheme.spacing4), child: Column( children: [ // 검색 바 TextField( controller: _searchController, decoration: InputDecoration( hintText: '이름, 이메일, 사용자명으로 검색...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); controller.setSearchQuery(''); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), contentPadding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3, ), ), ), const SizedBox(height: ShadcnTheme.spacing3), // 필터 버튼들 Row( children: [ // 상태 필터 ShadcnButton( text: controller.filterIsActive == null ? '모든 상태' : controller.filterIsActive! ? '활성 사용자' : '비활성 사용자', onPressed: () { controller.setFilters( isActive: controller.filterIsActive == null ? true : controller.filterIsActive! ? false : null, ); }, variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.filter_list), ), const SizedBox(width: ShadcnTheme.spacing2), // 권한 필터 PopupMenuButton( child: ShadcnButton( text: controller.filterRole == null ? '모든 권한' : getRoleName(controller.filterRole!), onPressed: null, variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.person), ), onSelected: (role) { controller.setFilters(role: role); }, itemBuilder: (context) => [ const PopupMenuItem( value: null, child: Text('모든 권한'), ), const PopupMenuItem( value: 'S', child: Text('관리자'), ), const PopupMenuItem( value: 'M', child: Text('멤버'), ), ], ), const Spacer(), // 필터 초기화 if (controller.searchQuery.isNotEmpty || controller.filterIsActive != null || controller.filterRole != null) ShadcnButton( text: '필터 초기화', onPressed: () { _searchController.clear(); controller.clearFilters(); }, variant: ShadcnButtonVariant.ghost, icon: const Icon(Icons.clear_all), ), ], ), ], ), ), ), const SizedBox(height: ShadcnTheme.spacing4), // 헤더 액션 바 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '총 ${controller.users.length}명 사용자', style: ShadcnTheme.bodyMuted, ), Row( children: [ ShadcnButton( text: '새로고침', onPressed: () => controller.loadUsers(refresh: true), variant: ShadcnButtonVariant.secondary, icon: const Icon(Icons.refresh), ), const SizedBox(width: ShadcnTheme.spacing2), ShadcnButton( text: '사용자 추가', onPressed: _navigateToAdd, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.add), ), ], ), ], ), const SizedBox(height: ShadcnTheme.spacing4), // 테이블 컨테이너 Container( width: double.infinity, decoration: BoxDecoration( border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, 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: Colors.black), ), ), child: Row( children: [ const SizedBox(width: 50, child: Text('번호', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('사용자명', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('이메일', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('회사명', style: TextStyle(fontWeight: FontWeight.bold))), const Expanded(flex: 2, child: Text('지점명', style: TextStyle(fontWeight: FontWeight.bold))), const SizedBox(width: 100, child: Text('권한', style: TextStyle(fontWeight: FontWeight.bold))), const SizedBox(width: 80, child: Text('상태', style: TextStyle(fontWeight: FontWeight.bold))), const SizedBox(width: 120, child: Text('관리', style: TextStyle(fontWeight: FontWeight.bold))), ], ), ), // 테이블 데이터 if (controller.users.isEmpty) Container( padding: const EdgeInsets.all(ShadcnTheme.spacing8), child: Center( child: Text( controller.searchQuery.isNotEmpty || controller.filterIsActive != null || controller.filterRole != null ? '검색 결과가 없습니다.' : '등록된 사용자가 없습니다.', style: ShadcnTheme.bodyMuted, ), ), ) else ...pagedUsers.asMap().entries.map((entry) { final int index = startIndex + entry.key; final User user = entry.value; return Container( padding: const EdgeInsets.all(ShadcnTheme.spacing4), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: Colors.black), ), color: index % 2 == 0 ? null : ShadcnTheme.muted.withValues(alpha: 0.1), ), child: Row( children: [ // 번호 SizedBox( width: 50, child: Text( '${index + 1}', style: ShadcnTheme.bodySmall, ), ), // 사용자명 Expanded( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.name, style: ShadcnTheme.bodyMedium, ), if (user.username != null) Text( '@${user.username}', style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.muted, ), ), ], ), ), // 이메일 Expanded( flex: 2, child: Text( user.email ?? '미등록', style: ShadcnTheme.bodySmall, ), ), // 회사명 Expanded( flex: 2, child: Text( _getCompanyName(user.companyId), style: ShadcnTheme.bodySmall, ), ), // 지점명 Expanded( flex: 2, child: Text( controller.getBranchName( user.companyId, user.branchId, ), style: ShadcnTheme.bodySmall, ), ), // 권한 SizedBox( width: 100, child: _buildUserRoleBadge(user.role), ), // 상태 SizedBox( width: 80, child: Row( children: [ Icon( Icons.circle, size: 8, color: _getStatusColor(user.isActive), ), const SizedBox(width: 4), Text( user.isActive ? '활성' : '비활성', style: ShadcnTheme.bodySmall.copyWith( color: _getStatusColor(user.isActive), ), ), ], ), ), // 관리 SizedBox( width: 120, child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: Icon( Icons.power_settings_new, size: 16, color: user.isActive ? Colors.orange : Colors.green, ), onPressed: user.id != null ? () => _showStatusChangeDialog(user) : null, tooltip: user.isActive ? '비활성화' : '활성화', ), ), Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: Icon( Icons.edit, size: 16, color: ShadcnTheme.primary, ), onPressed: user.id != null ? () => _navigateToEdit(user.id!) : null, tooltip: '수정', ), ), Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: Icon( Icons.delete, size: 16, color: ShadcnTheme.destructive, ), onPressed: user.id != null ? () => _showDeleteDialog(user.id!, user.name) : null, tooltip: '삭제', ), ), ], ), ), ], ), ); }), ], ), ), // 페이지네이션 컴포넌트 if (totalUsers > _pageSize) Pagination( totalCount: totalUsers, currentPage: _currentPage, pageSize: _pageSize, onPageChanged: (page) { setState(() { _currentPage = page; }); }, ), ], ), ); }, ), ); } }