470 lines
14 KiB
Dart
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),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|