import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_theme.dart'; import '../../models/student.dart'; import '../../providers/dashboard_provider.dart'; import '../../providers/student_provider.dart'; class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @override ConsumerState createState() => _ProfileScreenState(); } class _ProfileScreenState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Student Profile'), bottom: TabBar( controller: _tabController, labelColor: Colors.white, unselectedLabelColor: Colors.white70, indicatorColor: Colors.white, tabs: const [ Tab(icon: Icon(Icons.person_outline, size: 20), text: 'Profile'), Tab(icon: Icon(Icons.folder_outlined, size: 20), text: 'Documents'), Tab(icon: Icon(Icons.card_membership_outlined, size: 20), text: 'Certificates'), ], ), ), body: TabBarView( controller: _tabController, children: const [ _ProfileTab(), _DocumentsTab(), _CertificatesTab(), ], ), ); } } // ============================================================================ // 1. PROFILE TAB // ============================================================================ class _ProfileTab extends ConsumerWidget { const _ProfileTab(); @override Widget build(BuildContext context, WidgetRef ref) { final profileAsync = ref.watch(studentProfileProvider); return RefreshIndicator( onRefresh: () async { ref.invalidate(studentProfileProvider); await ref.read(studentProfileProvider.future); }, child: profileAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline_rounded, size: 48, color: AppTheme.errorColor), const SizedBox(height: 12), const Text( 'Failed to load profile details', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( err.toString(), textAlign: TextAlign.center, style: const TextStyle(color: AppTheme.textSecondary, fontSize: 13), ), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(studentProfileProvider), child: const Text('Retry'), ), ], ), ), ), data: (profile) => _buildProfileBody(context, ref, profile), ), ); } Widget _buildProfileBody(BuildContext context, WidgetRef ref, StudentProfile profile) { return ListView( padding: const EdgeInsets.all(16), children: [ // Profile Header Card Card( child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ CircleAvatar( radius: 36, backgroundColor: AppTheme.primaryColor, child: Text( profile.fullName.isNotEmpty ? profile.fullName[0].toUpperCase() : 'S', style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 12), Text( profile.fullName, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppTheme.primaryLight.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Text( 'ID: ${profile.studentId}', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.primaryDark, ), ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: Colors.green.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Text( profile.status.toUpperCase(), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.green, ), ), ), ], ), const SizedBox(height: 14), // Profile Completion Progress Bar Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Profile Completion', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textSecondary), ), Text( '${profile.profileCompletion}%', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: AppTheme.primaryColor), ), ], ), const SizedBox(height: 6), LinearProgressIndicator( value: profile.profileCompletion / 100.0, backgroundColor: Colors.grey.shade200, valueColor: const AlwaysStoppedAnimation(AppTheme.primaryColor), minHeight: 6, borderRadius: BorderRadius.circular(3), ), ], ), const SizedBox(height: 16), OutlinedButton.icon( onPressed: () => _openEditProfileDialog(context, ref, profile), icon: const Icon(Icons.edit_outlined, size: 18), label: const Text('Edit Contact Info'), ), ], ), ), ), const SizedBox(height: 16), // Academic Information Card (Read-only) Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.school_outlined, size: 20, color: AppTheme.primaryColor), SizedBox(width: 8), Text( 'Academic Details (Institutional)', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ], ), const Divider(), _buildInfoTile('Admission Number', profile.admissionNumber), if (profile.registrationNumber != null) _buildInfoTile('Registration Number', profile.registrationNumber!), if (profile.rollNumber != null) _buildInfoTile('Roll Number', profile.rollNumber!), if (profile.program?.name != null) _buildInfoTile('Program', profile.program!.name), if (profile.department?.name != null) _buildInfoTile('Department', profile.department!.name), if (profile.level?.name != null) _buildInfoTile('Level', profile.level!.name), if (profile.academicYear?.name != null) _buildInfoTile('Academic Year', profile.academicYear!.name), if (profile.academicTerm?.name != null) _buildInfoTile('Academic Term', profile.academicTerm!.name), if (profile.batch?.name != null) _buildInfoTile('Batch', profile.batch!.name), if (profile.semester?.name != null) _buildInfoTile('Semester', profile.semester!.name), if (profile.section?.name != null) _buildInfoTile('Section', profile.section!.name), if (profile.campus?.name != null) _buildInfoTile('Campus', profile.campus!.name), _buildInfoTile('Status', profile.status.toUpperCase()), ], ), ), ), const SizedBox(height: 16), // Contact & Emergency Information Card (Self-service editable) Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Row( children: [ Icon(Icons.contact_phone_outlined, size: 20, color: AppTheme.primaryColor), SizedBox(width: 8), Text( 'Contact & Emergency', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ], ), IconButton( icon: const Icon(Icons.edit, size: 18, color: AppTheme.primaryColor), onPressed: () => _openEditProfileDialog(context, ref, profile), tooltip: 'Edit Details', ), ], ), const Divider(), _buildInfoTile('Email', profile.email ?? 'Not set'), _buildInfoTile('Mobile', profile.mobile ?? 'Not set'), if (profile.phone != null) _buildInfoTile('Landline', profile.phone!), _buildInfoTile('Blood Group', profile.bloodGroup?.toUpperCase() ?? 'Not set'), _buildInfoTile('Emergency Contact', profile.emergencyContact ?? 'Not set'), _buildInfoTile('Emergency Phone', profile.emergencyPhone ?? 'Not set'), _buildInfoTile('City', profile.city ?? 'Not set'), if (profile.street != null) _buildInfoTile('Address', profile.street!), if (profile.zip != null) _buildInfoTile('ZIP', profile.zip!), ], ), ), ), const SizedBox(height: 16), // Guardian Information Card if (profile.guardianName != null || profile.guardianPhone != null) Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.family_restroom_outlined, size: 20, color: AppTheme.primaryColor), SizedBox(width: 8), Text( 'Guardian Information', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ], ), const Divider(), if (profile.guardianName != null) _buildInfoTile('Guardian Name', profile.guardianName!), if (profile.guardianRelation != null) _buildInfoTile('Relationship', profile.guardianRelation!), if (profile.guardianPhone != null) _buildInfoTile('Phone', profile.guardianPhone!), if (profile.guardianEmail != null) _buildInfoTile('Email', profile.guardianEmail!), ], ), ), ), ], ); } void _openEditProfileDialog(BuildContext context, WidgetRef ref, StudentProfile profile) { showDialog( context: context, builder: (ctx) => _EditProfileDialog(profile: profile), ); } Widget _buildInfoTile(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 140, child: Text( label, style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary, ), ), ), Expanded( child: Text( value, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ), ], ), ); } } // ============================================================================ // EDIT PROFILE DIALOG // ============================================================================ class _EditProfileDialog extends ConsumerStatefulWidget { final StudentProfile profile; const _EditProfileDialog({required this.profile}); @override ConsumerState<_EditProfileDialog> createState() => _EditProfileDialogState(); } class _EditProfileDialogState extends ConsumerState<_EditProfileDialog> { final _formKey = GlobalKey(); late TextEditingController _mobileCtrl; late TextEditingController _emailCtrl; late TextEditingController _bloodGroupCtrl; late TextEditingController _emergencyContactCtrl; late TextEditingController _emergencyPhoneCtrl; late TextEditingController _streetCtrl; late TextEditingController _cityCtrl; late TextEditingController _zipCtrl; bool _isLoading = false; @override void initState() { super.initState(); _mobileCtrl = TextEditingController(text: widget.profile.mobile ?? ''); _emailCtrl = TextEditingController(text: widget.profile.email ?? ''); _bloodGroupCtrl = TextEditingController(text: widget.profile.bloodGroup ?? ''); _emergencyContactCtrl = TextEditingController(text: widget.profile.emergencyContact ?? ''); _emergencyPhoneCtrl = TextEditingController(text: widget.profile.emergencyPhone ?? ''); _streetCtrl = TextEditingController(text: widget.profile.street ?? ''); _cityCtrl = TextEditingController(text: widget.profile.city ?? ''); _zipCtrl = TextEditingController(text: widget.profile.zip ?? ''); } @override void dispose() { _mobileCtrl.dispose(); _emailCtrl.dispose(); _bloodGroupCtrl.dispose(); _emergencyContactCtrl.dispose(); _emergencyPhoneCtrl.dispose(); _streetCtrl.dispose(); _cityCtrl.dispose(); _zipCtrl.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); try { final repo = ref.read(studentRepositoryProvider); await repo.updateProfile({ 'email': _emailCtrl.text.trim(), 'mobile': _mobileCtrl.text.trim(), if (_bloodGroupCtrl.text.trim().isNotEmpty) 'blood_group': _bloodGroupCtrl.text.trim().toLowerCase(), 'emergency_contact': _emergencyContactCtrl.text.trim(), 'emergency_phone': _emergencyPhoneCtrl.text.trim(), 'street': _streetCtrl.text.trim(), 'city': _cityCtrl.text.trim(), 'zip': _zipCtrl.text.trim(), }); ref.invalidate(studentProfileProvider); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Profile updated successfully!'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Update failed: $e'), backgroundColor: AppTheme.errorColor, ), ); } } finally { if (mounted) setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Edit Contact Info'), content: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: _emailCtrl, decoration: const InputDecoration(labelText: 'Email Address'), keyboardType: TextInputType.emailAddress, validator: (val) { if (val == null || val.isEmpty) return 'Email cannot be blank'; if (!val.contains('@') || !val.contains('.')) return 'Enter a valid email'; return null; }, ), const SizedBox(height: 12), TextFormField( controller: _mobileCtrl, decoration: const InputDecoration(labelText: 'Mobile Number'), keyboardType: TextInputType.phone, validator: (val) { if (val != null && val.isNotEmpty && val.length < 7) { return 'Enter a valid mobile number'; } return null; }, ), const SizedBox(height: 12), TextFormField( controller: _emergencyContactCtrl, decoration: const InputDecoration(labelText: 'Emergency Contact Name'), ), const SizedBox(height: 12), TextFormField( controller: _emergencyPhoneCtrl, decoration: const InputDecoration(labelText: 'Emergency Phone'), keyboardType: TextInputType.phone, ), const SizedBox(height: 12), TextFormField( controller: _bloodGroupCtrl, decoration: const InputDecoration(labelText: 'Blood Group (e.g. O+, A+, B-)'), ), const SizedBox(height: 12), TextFormField( controller: _cityCtrl, decoration: const InputDecoration(labelText: 'City'), ), const SizedBox(height: 12), TextFormField( controller: _streetCtrl, decoration: const InputDecoration(labelText: 'Address Line'), ), const SizedBox(height: 12), TextFormField( controller: _zipCtrl, decoration: const InputDecoration(labelText: 'ZIP / Postal Code'), ), ], ), ), ), actions: [ TextButton( onPressed: _isLoading ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), ElevatedButton( onPressed: _isLoading ? null : _submit, child: _isLoading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Save Changes'), ), ], ); } } // ============================================================================ // 2. DOCUMENTS TAB // ============================================================================ class _DocumentsTab extends ConsumerWidget { const _DocumentsTab(); @override Widget build(BuildContext context, WidgetRef ref) { final docsAsync = ref.watch(studentDocumentsProvider); return Scaffold( floatingActionButton: FloatingActionButton.extended( onPressed: () => _openUploadDialog(context, ref), icon: const Icon(Icons.upload_file), label: const Text('Upload Document'), ), body: RefreshIndicator( onRefresh: () async { ref.invalidate(studentDocumentsProvider); await ref.read(studentDocumentsProvider.future); }, child: docsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline_rounded, size: 48, color: AppTheme.errorColor), const SizedBox(height: 12), const Text('Failed to load documents', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text(err.toString(), style: const TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(studentDocumentsProvider), child: const Text('Retry'), ), ], ), ), ), data: (docs) { if (docs.isEmpty) { return ListView( children: [ SizedBox(height: MediaQuery.of(context).size.height * 0.25), const Center( child: Column( children: [ Icon(Icons.folder_open_outlined, size: 64, color: AppTheme.textSecondary), SizedBox(height: 12), Text( 'No documents uploaded yet', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), SizedBox(height: 6), Text( 'Upload identification or previous academic marksheet documents.', style: TextStyle(color: AppTheme.textSecondary, fontSize: 13), ), ], ), ), ], ); } return ListView.separated( padding: const EdgeInsets.all(16).copyWith(bottom: 80), itemCount: docs.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (ctx, index) { final doc = docs[index]; return _buildDocumentCard(context, doc); }, ); }, ), ), ); } Widget _buildDocumentCard(BuildContext context, StudentDocument doc) { Color badgeColor; IconData badgeIcon; if (doc.isVerified) { badgeColor = Colors.green; badgeIcon = Icons.check_circle_outline; } else if (doc.isRejected) { badgeColor = AppTheme.errorColor; badgeIcon = Icons.cancel_outlined; } else { badgeColor = Colors.orange; badgeIcon = Icons.hourglass_top_outlined; } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( doc.name, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: badgeColor.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(badgeIcon, size: 14, color: badgeColor), const SizedBox(width: 4), Text( doc.verificationStatus.toUpperCase(), style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: badgeColor), ), ], ), ), ], ), const SizedBox(height: 8), Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.08), borderRadius: BorderRadius.circular(4), ), child: Text( doc.categoryLabel, style: const TextStyle(fontSize: 11, color: Colors.blue, fontWeight: FontWeight.w500), ), ), const SizedBox(width: 8), if (doc.fileName != null) Expanded( child: Text( doc.fileName!, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary), ), ), ], ), if (doc.isRejected && doc.rejectionReason != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.errorColor.withOpacity(0.08), borderRadius: BorderRadius.circular(6), ), child: Text( 'Reason: ${doc.rejectionReason}', style: const TextStyle(fontSize: 12, color: AppTheme.errorColor), ), ), ], const SizedBox(height: 8), Text( 'Uploaded: ${doc.uploadDate}', style: const TextStyle(fontSize: 11, color: AppTheme.textSecondary), ), ], ), ), ); } void _openUploadDialog(BuildContext context, WidgetRef ref) { showDialog( context: context, builder: (ctx) => const _UploadDocumentDialog(), ); } } // ============================================================================ // UPLOAD DOCUMENT DIALOG // ============================================================================ class _UploadDocumentDialog extends ConsumerStatefulWidget { const _UploadDocumentDialog(); @override ConsumerState<_UploadDocumentDialog> createState() => _UploadDocumentDialogState(); } class _UploadDocumentDialogState extends ConsumerState<_UploadDocumentDialog> { final _formKey = GlobalKey(); final _titleCtrl = TextEditingController(); final _fileNameCtrl = TextEditingController(text: 'document.pdf'); String _category = 'academic'; bool _isLoading = false; final Map _categories = { 'identity': 'Identity Proof', 'academic': 'Academic Certificate / Marksheet', 'transfer': 'Transfer / Migration Certificate', 'address': 'Address Proof', 'other': 'Other Document', }; @override void dispose() { _titleCtrl.dispose(); _fileNameCtrl.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); try { final repo = ref.read(studentRepositoryProvider); // Valid dummy PDF in base64 format for mobile upload final dummyPdfBase64 = base64Encode(utf8.encode('%PDF-1.4\n1 0 obj<>endobj\n')); await repo.uploadDocument( name: _titleCtrl.text.trim(), category: _category, fileContentBase64: dummyPdfBase64, fileName: _fileNameCtrl.text.trim(), ); ref.invalidate(studentDocumentsProvider); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Document uploaded successfully for verification!'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Upload failed: $e'), backgroundColor: AppTheme.errorColor, ), ); } } finally { if (mounted) setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Upload Document'), content: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: _titleCtrl, decoration: const InputDecoration(labelText: 'Document Title (e.g. 12th Marksheet)'), validator: (val) => val == null || val.isEmpty ? 'Title is required' : null, ), const SizedBox(height: 12), DropdownButtonFormField( value: _category, decoration: const InputDecoration(labelText: 'Category'), items: _categories.entries .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) .toList(), onChanged: (val) => setState(() => _category = val ?? 'academic'), ), const SizedBox(height: 12), TextFormField( controller: _fileNameCtrl, decoration: const InputDecoration(labelText: 'Filename (.pdf, .jpg, .png)'), validator: (val) { if (val == null || val.isEmpty) return 'Filename is required'; final ext = val.split('.').last.toLowerCase(); if (!['pdf', 'jpg', 'jpeg', 'png'].contains(ext)) { return 'Only PDF, JPG, PNG allowed'; } return null; }, ), const SizedBox(height: 12), const Text( 'Note: Maximum file size is 5 MB. Documents will be reviewed by academic administration.', style: TextStyle(fontSize: 11, color: AppTheme.textSecondary), ), ], ), ), ), actions: [ TextButton( onPressed: _isLoading ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), ElevatedButton( onPressed: _isLoading ? null : _submit, child: _isLoading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Upload'), ), ], ); } } // ============================================================================ // 3. CERTIFICATES TAB // ============================================================================ class _CertificatesTab extends ConsumerWidget { const _CertificatesTab(); @override Widget build(BuildContext context, WidgetRef ref) { final certsAsync = ref.watch(studentCertificatesProvider); return Scaffold( floatingActionButton: FloatingActionButton.extended( onPressed: () => _openRequestDialog(context, ref), icon: const Icon(Icons.add_task), label: const Text('Request Certificate'), ), body: RefreshIndicator( onRefresh: () async { ref.invalidate(studentCertificatesProvider); await ref.read(studentCertificatesProvider.future); }, child: certsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline_rounded, size: 48, color: AppTheme.errorColor), const SizedBox(height: 12), const Text('Failed to load certificates', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text(err.toString(), style: const TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(studentCertificatesProvider), child: const Text('Retry'), ), ], ), ), ), data: (certs) { if (certs.isEmpty) { return ListView( children: [ SizedBox(height: MediaQuery.of(context).size.height * 0.25), const Center( child: Column( children: [ Icon(Icons.card_membership_outlined, size: 64, color: AppTheme.textSecondary), SizedBox(height: 12), Text( 'No certificates requested yet', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), SizedBox(height: 6), Text( 'Request bonafide certificate, student ID card, or course completion.', style: TextStyle(color: AppTheme.textSecondary, fontSize: 13), ), ], ), ), ], ); } return ListView.separated( padding: const EdgeInsets.all(16).copyWith(bottom: 80), itemCount: certs.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (ctx, index) { final cert = certs[index]; return _buildCertificateCard(context, cert); }, ); }, ), ), ); } Widget _buildCertificateCard(BuildContext context, StudentCertificate cert) { Color badgeColor; IconData badgeIcon; if (cert.isIssued) { badgeColor = Colors.green; badgeIcon = Icons.verified_outlined; } else if (cert.isApproved) { badgeColor = Colors.blue; badgeIcon = Icons.thumb_up_alt_outlined; } else if (cert.isRejected) { badgeColor = AppTheme.errorColor; badgeIcon = Icons.cancel_outlined; } else { badgeColor = Colors.orange; badgeIcon = Icons.pending_actions_outlined; } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( cert.certificateTypeLabel, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: badgeColor.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(badgeIcon, size: 14, color: badgeColor), const SizedBox(width: 4), Text( cert.state.toUpperCase(), style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: badgeColor), ), ], ), ), ], ), const SizedBox(height: 6), Text( 'Cert No: ${cert.name}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.primaryDark), ), if (cert.remarks != null && cert.remarks!.isNotEmpty) ...[ const SizedBox(height: 6), Text( 'Purpose: ${cert.remarks}', style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary), ), ], if (cert.isRejected && cert.rejectionReason != null) ...[ const SizedBox(height: 6), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.errorColor.withOpacity(0.08), borderRadius: BorderRadius.circular(6), ), child: Text( 'Reason: ${cert.rejectionReason}', style: const TextStyle(fontSize: 12, color: AppTheme.errorColor), ), ), ], const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Requested: ${cert.requestDate}', style: const TextStyle(fontSize: 11, color: AppTheme.textSecondary), ), if (cert.issueDate != null) Text( 'Issued: ${cert.issueDate}', style: const TextStyle(fontSize: 11, color: Colors.green, fontWeight: FontWeight.w500), ), ], ), ], ), ), ); } void _openRequestDialog(BuildContext context, WidgetRef ref) { showDialog( context: context, builder: (ctx) => const _RequestCertificateDialog(), ); } } // ============================================================================ // REQUEST CERTIFICATE DIALOG // ============================================================================ class _RequestCertificateDialog extends ConsumerStatefulWidget { const _RequestCertificateDialog(); @override ConsumerState<_RequestCertificateDialog> createState() => _RequestCertificateDialogState(); } class _RequestCertificateDialogState extends ConsumerState<_RequestCertificateDialog> { final _formKey = GlobalKey(); final _remarksCtrl = TextEditingController(); String _selectedType = 'bonafide'; bool _isLoading = false; final Map _certTypes = { 'bonafide': 'Bonafide Certificate', 'id_card': 'Student ID Card', 'course_completion': 'Course Completion Certificate', 'conduct': 'Conduct / Character Certificate', }; @override void dispose() { _remarksCtrl.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); try { final repo = ref.read(studentRepositoryProvider); await repo.requestCertificate( certificateType: _selectedType, remarks: _remarksCtrl.text.trim(), ); ref.invalidate(studentCertificatesProvider); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Certificate request submitted to academic office!'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Request failed: $e'), backgroundColor: AppTheme.errorColor, ), ); } } finally { if (mounted) setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Request Certificate / ID Card'), content: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( value: _selectedType, decoration: const InputDecoration(labelText: 'Certificate Type'), items: _certTypes.entries .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) .toList(), onChanged: (val) => setState(() => _selectedType = val ?? 'bonafide'), ), const SizedBox(height: 12), TextFormField( controller: _remarksCtrl, decoration: const InputDecoration(labelText: 'Purpose / Remarks (e.g. For Passport)'), maxLines: 2, ), const SizedBox(height: 12), const Text( 'Note: Official certificates require institutional review and approval by authorized staff before issuance.', style: TextStyle(fontSize: 11, color: AppTheme.textSecondary), ), ], ), ), ), actions: [ TextButton( onPressed: _isLoading ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), ElevatedButton( onPressed: _isLoading ? null : _submit, child: _isLoading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Submit Request'), ), ], ); } }