801 lines
25 KiB
Dart
801 lines
25 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_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<PanelLicensePage> createState() => _PanelLicensePageState();
|
|
}
|
|
|
|
class _PanelLicensePageState extends State<PanelLicensePage> {
|
|
bool _isLoadingCourses = true;
|
|
bool _isLoadingLicenses = false;
|
|
String? _errorMessage;
|
|
|
|
List<Map<String, dynamic>> _courses = <Map<String, dynamic>>[];
|
|
List<Map<String, dynamic>> _licenses = <Map<String, dynamic>>[];
|
|
List<Map<String, dynamic>> _filteredLicenses = <Map<String, dynamic>>[];
|
|
|
|
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<void> _loadCoursesAndInitialLicenses() async {
|
|
setState(() {
|
|
_isLoadingCourses = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
try {
|
|
final apiService = Provider.of<ApiService>(context, listen: false);
|
|
final response = await apiService.getCourses();
|
|
final data = response['data'];
|
|
|
|
final courses = data is List
|
|
? data
|
|
.whereType<Map>()
|
|
.map((e) => Map<String, dynamic>.from(e))
|
|
.toList()
|
|
: <Map<String, dynamic>>[];
|
|
|
|
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<void> _loadLicensesForSelectedCourse({int page = 1}) async {
|
|
if (_selectedCourseId == null) return;
|
|
|
|
setState(() {
|
|
_isLoadingLicenses = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
try {
|
|
final apiService = Provider.of<ApiService>(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>()
|
|
.map((e) => Map<String, dynamic>.from(e))
|
|
.toList()
|
|
: <Map<String, dynamic>>[];
|
|
|
|
setState(() {
|
|
_licenses = licenses;
|
|
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;
|
|
}
|
|
_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<Map<String, dynamic>>.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<String, dynamic>? course) {
|
|
if (course == null) return null;
|
|
return (course['id'] as num?)?.toInt();
|
|
}
|
|
|
|
String _extractCourseTitle(Map<String, dynamic>? 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<String, dynamic>) {
|
|
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<String, dynamic>) {
|
|
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: () => <String, dynamic>{},
|
|
);
|
|
|
|
final created = await Navigator.of(context).push<bool>(
|
|
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<String, dynamic>
|
|
? _extractCourseTitle(item['course'] as Map<String, dynamic>)
|
|
: _extractCourseTitle(
|
|
_courses.firstWhere(
|
|
(e) => _extractCourseId(e) == _selectedCourseId,
|
|
orElse: () => <String, dynamic>{},
|
|
),
|
|
);
|
|
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<Map<String, dynamic>> courses;
|
|
final int? selectedCourseId;
|
|
final VoidCallback onCreateNewLicense;
|
|
final ValueChanged<int?> 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<int>(
|
|
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<int>(
|
|
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<String>(
|
|
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),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|