frontendPlayer/lib/presentation/pages/course_page.dart
2026-04-10 09:55:19 +03:30

470 lines
14 KiB
Dart

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_details_page.dart';
import 'dashboard_page.dart';
import 'license_page.dart';
import 'new_course_page.dart';
import 'ticket_page.dart';
import 'transaction_page.dart';
class CoursePage extends StatefulWidget {
const CoursePage({super.key});
@override
State<CoursePage> createState() => _CoursePageState();
}
class _CoursePageState extends State<CoursePage> {
bool _isLoading = true;
String? _errorMessage;
List<Map<String, dynamic>> _courses = <Map<String, dynamic>>[];
int _currentPage = 1;
int _lastPage = 1;
@override
void initState() {
super.initState();
_loadCourses();
}
Future<void> _loadCourses({int page = 1}) async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final apiService = Provider.of<ApiService>(context, listen: false);
final response = await apiService.getCourses(page: page);
final data = response['data'];
final meta = response['meta'];
setState(() {
_courses = data is List
? data
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList()
: <Map<String, dynamic>>[];
if (meta is Map<String, dynamic>) {
_currentPage = (meta['current_page'] as num?)?.toInt() ?? page;
_lastPage = (meta['last_page'] as num?)?.toInt() ?? 1;
} else {
_currentPage = page;
_lastPage = 1;
}
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString();
});
}
}
@override
Widget build(BuildContext context) {
final isDesktop = MediaQuery.of(context).size.width > 900;
return TeacherPanelScaffold(
selectedMenu: PanelMenu.course,
onDashboardTap: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const DashboardPage()),
);
},
onCourseTap: () {},
onLicenseTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const PanelLicensePage()),
);
},
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: _CourseToolbar(isDesktop: isDesktop),
),
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: 5),
_HeaderCell(title: 'کاور', flex: 2),
],
),
),
Expanded(child: _buildContent(isDesktop)),
_PaginationBar(
currentPage: _currentPage,
lastPage: _lastPage,
onPrevious: _currentPage > 1
? () => _loadCourses(page: _currentPage - 1)
: null,
onNext: _currentPage < _lastPage
? () => _loadCourses(page: _currentPage + 1)
: null,
),
],
),
),
),
],
),
),
),
);
}
Widget _buildContent(bool isDesktop) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_errorMessage != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_errorMessage!,
style: AppTextStyles.bodyMedium.copyWith(color: Colors.red),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () => _loadCourses(page: _currentPage),
child: const Text('تلاش مجدد'),
),
],
),
),
);
}
if (_courses.isEmpty) {
return const Center(child: _EmptyCourseState());
}
return ListView.separated(
itemCount: _courses.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final course = _courses[index];
final title = (course['title'] ?? '-').toString();
final teacherName = _extractTeacherName(course['teacher_name']);
final description = (course['description'] ?? '-').toString();
final coverUrl = course['cover_url']?.toString();
final courseId = (course['id'] as num?)?.toInt();
return InkWell(
onTap: courseId == null
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CourseDetailsPage(courseId: courseId),
),
);
},
child: SizedBox(
height: isDesktop ? 68 : 88,
child: Row(
children: [
_BodyCell(text: title, flex: 4),
_BodyCell(text: teacherName, flex: 3),
_BodyCell(text: description, flex: 5),
Expanded(
flex: 2,
child: Center(
child: coverUrl == null || coverUrl.isEmpty
? const Icon(Icons.image_not_supported_outlined)
: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
coverUrl,
width: 42,
height: 42,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => const Icon(
Icons.broken_image_outlined,
),
),
),
),
),
],
),
),
);
},
);
}
String _extractTeacherName(dynamic teacherObj) {
if (teacherObj is Map<String, dynamic>) {
final value = teacherObj['user_name'];
if (value is String && value.trim().isNotEmpty) {
return value;
}
}
return '-';
}
}
class _CourseToolbar extends StatelessWidget {
final bool isDesktop;
const _CourseToolbar({required this.isDesktop});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SizedBox(
height: 36,
width: isDesktop ? 120 : double.infinity,
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const NewCoursePage()),
);
},
icon: const Icon(Icons.add, size: 18),
label: const Text('دوره جدید'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF56B33E),
foregroundColor: Colors.white,
),
),
),
SizedBox(
width: isDesktop ? 180 : double.infinity,
height: 36,
child: DropdownButtonFormField<String>(
initialValue: 'فیلتر نوع محتوا',
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(color: Colors.grey.shade400),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
),
items: const [
DropdownMenuItem(
value: 'فیلتر نوع محتوا',
child: Text('فیلتر نوع محتوا'),
),
DropdownMenuItem(value: 'ویدئو', child: Text('ویدئو')),
DropdownMenuItem(value: 'صوت', child: Text('صوت')),
DropdownMenuItem(value: 'فایل', child: Text('فایل')),
],
onChanged: (_) {},
),
),
SizedBox(
width: isDesktop ? 380 : double.infinity,
height: 36,
child: TextField(
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 _EmptyCourseState extends StatelessWidget {
const _EmptyCourseState();
@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(
'هنوز اطلاعاتی وارد نشده است.',
style: AppTextStyles.bodyMedium.copyWith(color: AppColors.sidebarBg),
),
],
);
}
}