import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/constants/app_colors.dart'; import '../../core/constants/app_text_styles.dart'; import '../../data/services/api_service.dart'; import '../widgets/teacher_panel_scaffold.dart'; import 'course_page.dart'; import 'dashboard_page.dart'; import 'license_builder_page.dart'; import 'new_license_page.dart'; import 'ticket_page.dart'; import 'transaction_page.dart'; class PanelLicensePage extends StatefulWidget { const PanelLicensePage({super.key}); @override State createState() => _PanelLicensePageState(); } class _PanelLicensePageState extends State { bool _isLoadingCourses = true; bool _isLoadingLicenses = false; String? _errorMessage; List> _courses = >[]; List> _licenses = >[]; List> _filteredLicenses = >[]; int? _selectedCourseId; int _currentPage = 1; int _lastPage = 1; final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); _loadCoursesAndInitialLicenses(); _searchController.addListener(_applySearchFilter); } @override void dispose() { _searchController.dispose(); super.dispose(); } Future _loadCoursesAndInitialLicenses() async { setState(() { _isLoadingCourses = true; _errorMessage = null; }); try { final apiService = Provider.of(context, listen: false); final response = await apiService.getCourses(); final data = response['data']; final courses = data is List ? data .whereType() .map((e) => Map.from(e)) .toList() : >[]; setState(() { _courses = courses; _selectedCourseId = _extractCourseId(courses.isNotEmpty ? courses.first : null); _isLoadingCourses = false; }); if (_selectedCourseId != null) { await _loadLicensesForSelectedCourse(page: 1); } } catch (e) { setState(() { _isLoadingCourses = false; _errorMessage = e.toString(); }); } } Future _loadLicensesForSelectedCourse({int page = 1}) async { if (_selectedCourseId == null) return; setState(() { _isLoadingLicenses = true; _errorMessage = null; }); try { final apiService = Provider.of(context, listen: false); final response = await apiService.getCourseLicenses( courseId: _selectedCourseId!, page: page, ); final data = response['data']; final meta = response['meta']; final licenses = data is List ? data .whereType() .map((e) => Map.from(e)) .toList() : >[]; setState(() { _licenses = licenses; if (meta is Map) { _currentPage = (meta['current_page'] as num?)?.toInt() ?? page; _lastPage = (meta['last_page'] as num?)?.toInt() ?? 1; } else { _currentPage = page; _lastPage = 1; } _isLoadingLicenses = false; }); _applySearchFilter(); } catch (e) { setState(() { _isLoadingLicenses = false; _errorMessage = e.toString(); }); } } void _applySearchFilter() { final query = _searchController.text.trim().toLowerCase(); if (query.isEmpty) { setState(() { _filteredLicenses = List>.from(_licenses); }); return; } setState(() { _filteredLicenses = _licenses.where((item) { final code = (item['activation_code'] ?? '').toString().toLowerCase(); final status = (item['status'] ?? '').toString().toLowerCase(); final userName = _extractUserName(item['user']).toLowerCase(); return code.contains(query) || status.contains(query) || userName.contains(query); }).toList(); }); } int? _extractCourseId(Map? course) { if (course == null) return null; return (course['id'] as num?)?.toInt(); } String _extractCourseTitle(Map? course) { if (course == null) return '-'; final title = course['title']; if (title is String && title.trim().isNotEmpty) { return title; } return '-'; } String _extractUserName(dynamic userObj) { if (userObj is Map) { final value = userObj['user_name']; if (value is String && value.trim().isNotEmpty) { return value; } } return '-'; } int _extractDeviceCount(dynamic limitsObj) { if (limitsObj is List) { int sum = 0; for (final item in limitsObj) { if (item is Map) { final count = item['count'] ?? item['device_count'] ?? item['limit']; if (count is num) { sum += count.toInt(); } } } if (sum > 0) return sum; return limitsObj.length; } return 0; } @override Widget build(BuildContext context) { final isDesktop = MediaQuery.of(context).size.width > 900; return TeacherPanelScaffold( selectedMenu: PanelMenu.license, onDashboardTap: () { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => const DashboardPage()), ); }, onCourseTap: () { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => const CoursePage()), ); }, onLicenseTap: () {}, onTransactionTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const TransactionPage()), ); }, onTicketTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const TicketPage()), ); }, child: Padding( padding: EdgeInsets.all(isDesktop ? 24 : 12), child: Container( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(10), ), child: Column( children: [ Padding( padding: EdgeInsets.all(isDesktop ? 14 : 10), child: _LicenseToolbar( isDesktop: isDesktop, searchController: _searchController, courses: _courses, selectedCourseId: _selectedCourseId, onCreateNewLicense: () async { if (_selectedCourseId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('ابتدا یک دوره انتخاب کنید.'), ), ); return; } final selectedCourse = _courses.firstWhere( (e) => _extractCourseId(e) == _selectedCourseId, orElse: () => {}, ); final created = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => NewLicensePage( courseId: _selectedCourseId!, courseTitle: _extractCourseTitle(selectedCourse), ), ), ); if (created == true) { await _loadLicensesForSelectedCourse(page: _currentPage); } }, onCourseChanged: (courseId) async { setState(() { _selectedCourseId = courseId; _currentPage = 1; }); await _loadLicensesForSelectedCourse(page: 1); }, ), ), Expanded( child: Container( margin: EdgeInsets.fromLTRB( isDesktop ? 14 : 8, 0, isDesktop ? 14 : 8, isDesktop ? 14 : 8, ), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Container( height: 46, decoration: const BoxDecoration( color: Color(0xFFD5D5D5), borderRadius: BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), ), child: const Row( children: [ _HeaderCell(title: 'کد فعال‌سازی', flex: 4), _HeaderCell(title: 'کاربر', flex: 3), _HeaderCell(title: 'دوره', flex: 4), _HeaderCell(title: 'دستگاه', flex: 2), _HeaderCell(title: 'وضعیت', flex: 2), ], ), ), Expanded(child: _buildContent()), _PaginationBar( currentPage: _currentPage, lastPage: _lastPage, onPrevious: _currentPage > 1 ? () => _loadLicensesForSelectedCourse(page: _currentPage - 1) : null, onNext: _currentPage < _lastPage ? () => _loadLicensesForSelectedCourse(page: _currentPage + 1) : null, ), ], ), ), ), ], ), ), ), ); } Widget _buildContent() { if (_isLoadingCourses || _isLoadingLicenses) { return const Center(child: CircularProgressIndicator()); } if (_errorMessage != null) { return Center( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( _errorMessage!, style: AppTextStyles.bodyMedium.copyWith(color: Colors.red), textAlign: TextAlign.center, ), const SizedBox(height: 10), OutlinedButton( onPressed: _selectedCourseId == null ? _loadCoursesAndInitialLicenses : () => _loadLicensesForSelectedCourse(page: _currentPage), child: const Text('تلاش مجدد'), ), ], ), ), ); } if (_courses.isEmpty) { return const Center(child: _EmptyLicenseState(message: 'هیچ دوره‌ای یافت نشد.')); } if (_selectedCourseId == null) { return const Center(child: _EmptyLicenseState(message: 'دوره‌ای برای نمایش انتخاب نشده است.')); } if (_filteredLicenses.isEmpty) { return const Center(child: _EmptyLicenseState(message: 'برای این دوره لایسنسی ثبت نشده است.')); } return ListView.separated( itemCount: _filteredLicenses.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final item = _filteredLicenses[index]; final activationCode = (item['activation_code'] ?? '-').toString(); final userName = _extractUserName(item['user']); final courseTitle = item['course'] is Map ? _extractCourseTitle(item['course'] as Map) : _extractCourseTitle( _courses.firstWhere( (e) => _extractCourseId(e) == _selectedCourseId, orElse: () => {}, ), ); final deviceCount = _extractDeviceCount(item['device_limits']); final status = (item['status'] ?? '-').toString(); return SizedBox( height: 62, child: Row( children: [ _BodyCell(text: activationCode, flex: 4), _BodyCell(text: userName, flex: 3), _BodyCell(text: courseTitle, flex: 4), _BodyCell(text: deviceCount.toString(), flex: 2), _BodyCell(text: status, flex: 2), ], ), ); }, ); } } class _LicenseToolbar extends StatelessWidget { final bool isDesktop; final TextEditingController searchController; final List> courses; final int? selectedCourseId; final VoidCallback onCreateNewLicense; final ValueChanged onCourseChanged; const _LicenseToolbar({ required this.isDesktop, required this.searchController, required this.courses, required this.selectedCourseId, required this.onCreateNewLicense, required this.onCourseChanged, }); @override Widget build(BuildContext context) { if (!isDesktop) { return Column( children: [ _searchBox(double.infinity), const SizedBox(height: 8), _courseDropdown(width: double.infinity), const SizedBox(height: 8), _staticDropdown('تاریخ ایجاد', width: double.infinity), const SizedBox(height: 8), _staticDropdown('تنظیمات', width: double.infinity), const SizedBox(height: 8), SizedBox( width: double.infinity, height: 36, child: _buildActionButton(context, 'تنظیمات پیش فرض'), ), const SizedBox(height: 8), SizedBox( width: double.infinity, height: 36, child: ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: AppColors.sidebarBg, foregroundColor: Colors.white, ), child: const Text('دانلود اکسل'), ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, height: 36, child: ElevatedButton.icon( onPressed: onCreateNewLicense, icon: const Icon(Icons.add, size: 18), label: const Text('لایسنس جدید'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF56B33E), foregroundColor: Colors.white, ), ), ), ], ); } return LayoutBuilder( builder: (context, constraints) { const fixedWidth = 160 + 120 + 100 + 146 + 146 + 146; const gaps = 10.0 * 6; const minSearch = 170.0; final canExpandSearch = constraints.maxWidth >= (fixedWidth + gaps + minSearch); if (canExpandSearch) { return Row( textDirection: TextDirection.rtl, children: [ Expanded(child: _searchBox(double.infinity)), const SizedBox(width: 10), _courseDropdown(width: 160), const SizedBox(width: 10), _staticDropdown('تاریخ ایجاد', width: 120), const SizedBox(width: 10), _staticDropdown('تنظیمات', width: 100), const SizedBox(width: 10), SizedBox( width: 146, height: 36, child: _buildActionButton(context, 'تنظیمات پیش فرض'), ), const SizedBox(width: 10), SizedBox( height: 36, width: 146, child: ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: AppColors.sidebarBg, foregroundColor: Colors.white, textStyle: AppTextStyles.bodyMedium.copyWith(color: Colors.white), padding: const EdgeInsets.symmetric(horizontal: 10), ), child: const Text('دانلود اکسل'), ), ), const SizedBox(width: 10), SizedBox( height: 36, width: 146, child: ElevatedButton.icon( onPressed: onCreateNewLicense, icon: const Icon(Icons.add, size: 18), label: const Text('لایسنس جدید'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF56B33E), foregroundColor: Colors.white, ), ), ), ], ); } return Align( alignment: Alignment.centerRight, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( textDirection: TextDirection.rtl, children: [ _searchBox(170), const SizedBox(width: 10), _courseDropdown(width: 160), const SizedBox(width: 10), _staticDropdown('تاریخ ایجاد', width: 120), const SizedBox(width: 10), _staticDropdown('تنظیمات', width: 100), const SizedBox(width: 10), SizedBox( width: 146, height: 36, child: _buildActionButton(context, 'تنظیمات پیش فرض'), ), const SizedBox(width: 10), SizedBox( height: 36, width: 146, child: ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: AppColors.sidebarBg, foregroundColor: Colors.white, ), child: const Text('دانلود اکسل'), ), ), const SizedBox(width: 10), SizedBox( height: 36, width: 146, child: ElevatedButton.icon( onPressed: onCreateNewLicense, icon: const Icon(Icons.add, size: 18), label: const Text('لایسنس جدید'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF56B33E), foregroundColor: Colors.white, ), ), ), ], ), ), ); }, ); } Widget _courseDropdown({required double width}) { final items = courses .where((c) => (c['id'] as num?) != null) .map( (c) => DropdownMenuItem( value: (c['id'] as num).toInt(), child: Text((c['title'] ?? 'بدون عنوان').toString()), ), ) .toList(); final hasSelected = selectedCourseId != null && items.any((e) => e.value == selectedCourseId); return SizedBox( width: width, height: 36, child: DropdownButtonFormField( initialValue: hasSelected ? selectedCourseId : null, isExpanded: true, hint: const Text('فیلتر دوره'), decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: Colors.grey.shade400), ), contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), ), items: items, onChanged: onCourseChanged, ), ); } Widget _staticDropdown(String hint, {required double width}) { return SizedBox( width: width, height: 36, child: DropdownButtonFormField( initialValue: hint, isExpanded: true, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: Colors.grey.shade400), ), contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), ), items: [ DropdownMenuItem(value: hint, child: Text(hint)), const DropdownMenuItem(value: 'همه', child: Text('همه')), ], onChanged: (_) {}, ), ); } Widget _buildActionButton(BuildContext context, String text) { return OutlinedButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const LicenseBuilderPage()), ); }, style: OutlinedButton.styleFrom( side: const BorderSide(color: AppColors.sidebarBg), foregroundColor: AppColors.sidebarBg, ), child: Text(text, maxLines: 1, overflow: TextOverflow.ellipsis), ); } Widget _searchBox(double width) { return SizedBox( width: width, height: 36, child: TextField( controller: searchController, decoration: InputDecoration( hintText: 'جست و جو', prefixIcon: const Icon(Icons.search, size: 18), border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide(color: Colors.grey.shade400), ), contentPadding: const EdgeInsets.symmetric(vertical: 8), ), ), ); } } class _HeaderCell extends StatelessWidget { final String title; final int flex; const _HeaderCell({required this.title, required this.flex}); @override Widget build(BuildContext context) { return Expanded( flex: flex, child: Center( child: Text( title, style: AppTextStyles.bodyLarge.copyWith(fontWeight: FontWeight.w700), ), ), ); } } class _BodyCell extends StatelessWidget { final String text; final int flex; const _BodyCell({required this.text, required this.flex}); @override Widget build(BuildContext context) { return Expanded( flex: flex, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( text, maxLines: 2, overflow: TextOverflow.ellipsis, style: AppTextStyles.bodyMedium, textAlign: TextAlign.center, ), ), ); } } class _PaginationBar extends StatelessWidget { final int currentPage; final int lastPage; final VoidCallback? onPrevious; final VoidCallback? onNext; const _PaginationBar({ required this.currentPage, required this.lastPage, this.onPrevious, this.onNext, }); @override Widget build(BuildContext context) { return Container( height: 52, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: const BoxDecoration( border: Border(top: BorderSide(color: Colors.black12)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('صفحه $currentPage از $lastPage', style: AppTextStyles.bodyMedium), Row( children: [ TextButton(onPressed: onPrevious, child: const Text('قبلی')), const SizedBox(width: 8), TextButton(onPressed: onNext, child: const Text('بعدی')), ], ), ], ), ); } } class _EmptyLicenseState extends StatelessWidget { final String message; const _EmptyLicenseState({required this.message}); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 340, constraints: const BoxConstraints(maxWidth: 340), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: Column( children: [ Icon( Icons.desktop_windows_outlined, size: 120, color: AppColors.sidebarBg.withValues(alpha: 0.7), ), const SizedBox(height: 6), Text( 'NO DATA', style: AppTextStyles.headlineMedium.copyWith( color: AppColors.sidebarBg, fontSize: 18, ), ), ], ), ), const SizedBox(height: 22), Text( message, style: AppTextStyles.bodyMedium.copyWith(color: AppColors.sidebarBg), ), ], ); } }