463 lines
15 KiB
Dart
463 lines
15 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_page.dart';
|
|
import 'ticket_page.dart';
|
|
import 'transaction_page.dart';
|
|
|
|
class NewLicensePage extends StatefulWidget {
|
|
final int courseId;
|
|
final String courseTitle;
|
|
|
|
const NewLicensePage({
|
|
super.key,
|
|
required this.courseId,
|
|
required this.courseTitle,
|
|
});
|
|
|
|
@override
|
|
State<NewLicensePage> createState() => _NewLicensePageState();
|
|
}
|
|
|
|
class _NewLicensePageState extends State<NewLicensePage> {
|
|
final TextEditingController _studentPhoneController = TextEditingController();
|
|
final TextEditingController _studentNameController = TextEditingController();
|
|
final TextEditingController _studentEmailController = TextEditingController();
|
|
|
|
final List<_DeviceLimitForm> _deviceLimits = <_DeviceLimitForm>[
|
|
_DeviceLimitForm(),
|
|
];
|
|
|
|
bool _isSubmitting = false;
|
|
String? _errorMessage;
|
|
|
|
@override
|
|
void dispose() {
|
|
_studentPhoneController.dispose();
|
|
_studentNameController.dispose();
|
|
_studentEmailController.dispose();
|
|
for (final item in _deviceLimits) {
|
|
item.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
void _addDeviceLimit() {
|
|
setState(() {
|
|
_deviceLimits.add(_DeviceLimitForm());
|
|
});
|
|
}
|
|
|
|
void _removeDeviceLimit(int index) {
|
|
if (_deviceLimits.length <= 1) return;
|
|
final removed = _deviceLimits.removeAt(index);
|
|
removed.dispose();
|
|
setState(() {});
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
final studentPhone = _studentPhoneController.text.trim();
|
|
final studentName = _studentNameController.text.trim();
|
|
final studentEmail = _studentEmailController.text.trim();
|
|
|
|
if (!RegExp(r'^09\d{9}$').hasMatch(studentPhone)) {
|
|
setState(() {
|
|
_errorMessage = 'شماره موبایل دانشجو باید با 09 شروع شود و 11 رقم باشد.';
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (studentName.length > 255) {
|
|
setState(() {
|
|
_errorMessage = 'حداکثر طول نام دانشجو 255 کاراکتر است.';
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (studentEmail.isNotEmpty) {
|
|
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
|
|
if (!emailRegex.hasMatch(studentEmail)) {
|
|
setState(() {
|
|
_errorMessage = 'ایمیل دانشجو معتبر نیست.';
|
|
});
|
|
return;
|
|
}
|
|
if (studentEmail.length > 255) {
|
|
setState(() {
|
|
_errorMessage = 'حداکثر طول ایمیل دانشجو 255 کاراکتر است.';
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
final payloadDeviceLimits = <Map<String, dynamic>>[];
|
|
for (final item in _deviceLimits) {
|
|
final limit = int.tryParse(item.limitController.text.trim());
|
|
if (limit == null || limit < 1 || limit > 10) {
|
|
setState(() {
|
|
_errorMessage = 'مقدار limit هر آیتم باید عدد بین 1 تا 10 باشد.';
|
|
});
|
|
return;
|
|
}
|
|
payloadDeviceLimits.add({
|
|
'os': item.os,
|
|
'limit': limit,
|
|
});
|
|
}
|
|
|
|
if (payloadDeviceLimits.isEmpty) {
|
|
setState(() {
|
|
_errorMessage = 'حداقل یک device limit باید وارد شود.';
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isSubmitting = true;
|
|
_errorMessage = null;
|
|
});
|
|
|
|
try {
|
|
final apiService = Provider.of<ApiService>(context, listen: false);
|
|
final response = await apiService.createCourseLicense(
|
|
courseId: widget.courseId,
|
|
studentPhone: studentPhone,
|
|
studentName: studentName.isEmpty ? null : studentName,
|
|
studentEmail: studentEmail.isEmpty ? null : studentEmail,
|
|
deviceLimits: payloadDeviceLimits,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
final message = (response['message'] ?? 'لایسنس با موفقیت ساخته شد.').toString();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message)),
|
|
);
|
|
Navigator.of(context).pop(true);
|
|
} catch (e) {
|
|
setState(() {
|
|
_errorMessage = e.toString();
|
|
});
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isSubmitting = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
@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: () {
|
|
Navigator.of(context).pushReplacement(
|
|
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: SingleChildScrollView(
|
|
padding: EdgeInsets.all(isDesktop ? 24 : 12),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.35),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
padding: EdgeInsets.all(isDesktop ? 18 : 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
InkWell(
|
|
onTap: () => Navigator.pop(context),
|
|
child: const Icon(Icons.arrow_back, color: AppColors.sidebarBg),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
const Icon(Icons.add_card, size: 18, color: AppColors.sidebarBg),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'ساخت لایسنس جدید',
|
|
style: AppTextStyles.headlineMedium.copyWith(
|
|
color: AppColors.sidebarBg,
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFEFF5FF),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: AppColors.sidebarBg.withValues(alpha: 0.3)),
|
|
),
|
|
child: Text(
|
|
'این لایسنس برای دوره: ${widget.courseTitle} (ID: ${widget.courseId})',
|
|
style: AppTextStyles.bodyLarge.copyWith(color: AppColors.sidebarBg),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(height: 1, color: Colors.black12),
|
|
const SizedBox(height: 16),
|
|
|
|
_buildLabel('شماره موبایل دانشجو *'),
|
|
const SizedBox(height: 6),
|
|
_buildInput(
|
|
_studentPhoneController,
|
|
keyboardType: TextInputType.phone,
|
|
hint: '09xxxxxxxxx',
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
_buildLabel('نام دانشجو (اختیاری)'),
|
|
const SizedBox(height: 6),
|
|
_buildInput(_studentNameController),
|
|
const SizedBox(height: 12),
|
|
|
|
_buildLabel('ایمیل دانشجو (اختیاری)'),
|
|
const SizedBox(height: 6),
|
|
_buildInput(
|
|
_studentEmailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
),
|
|
|
|
const SizedBox(height: 22),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('Device Limits *', style: AppTextStyles.headlineMedium),
|
|
OutlinedButton.icon(
|
|
onPressed: _addDeviceLimit,
|
|
icon: const Icon(Icons.add, size: 16),
|
|
label: const Text('افزودن آیتم'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
..._deviceLimits.asMap().entries.map(
|
|
(entry) {
|
|
final index = entry.key;
|
|
final item = entry.value;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: _buildDeviceLimitRow(index, item, isDesktop),
|
|
);
|
|
},
|
|
),
|
|
|
|
if (_errorMessage != null) ...[
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
_errorMessage!,
|
|
style: AppTextStyles.bodyMedium.copyWith(color: Colors.red),
|
|
),
|
|
],
|
|
|
|
const SizedBox(height: 22),
|
|
Wrap(
|
|
spacing: 10,
|
|
runSpacing: 10,
|
|
children: [
|
|
SizedBox(
|
|
width: isDesktop ? 140 : double.infinity,
|
|
height: 44,
|
|
child: ElevatedButton(
|
|
onPressed: _isSubmitting ? null : _submit,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF43C61C),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: _isSubmitting
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Text('تایید'),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: isDesktop ? 140 : double.infinity,
|
|
height: 44,
|
|
child: ElevatedButton(
|
|
onPressed: _isSubmitting ? null : () => Navigator.pop(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFC95757),
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('انصراف'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDeviceLimitRow(int index, _DeviceLimitForm item, bool isDesktop) {
|
|
final osItems = const [
|
|
DropdownMenuItem(value: 'android', child: Text('android')),
|
|
DropdownMenuItem(value: 'windows', child: Text('windows')),
|
|
DropdownMenuItem(value: 'linux', child: Text('linux')),
|
|
DropdownMenuItem(value: 'ios', child: Text('ios')),
|
|
];
|
|
|
|
if (!isDesktop) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.black12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
DropdownButtonFormField<String>(
|
|
initialValue: item.os,
|
|
decoration: _inputDecoration('os'),
|
|
items: osItems,
|
|
onChanged: (v) {
|
|
if (v == null) return;
|
|
setState(() {
|
|
item.os = v;
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
TextField(
|
|
controller: item.limitController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: _inputDecoration('limit (1..10)'),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: IconButton(
|
|
onPressed: _deviceLimits.length > 1 ? () => _removeDeviceLimit(index) : null,
|
|
icon: const Icon(Icons.delete_outline),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 3,
|
|
child: DropdownButtonFormField<String>(
|
|
initialValue: item.os,
|
|
decoration: _inputDecoration('os'),
|
|
items: osItems,
|
|
onChanged: (v) {
|
|
if (v == null) return;
|
|
setState(() {
|
|
item.os = v;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
flex: 2,
|
|
child: TextField(
|
|
controller: item.limitController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: _inputDecoration('limit (1..10)'),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
onPressed: _deviceLimits.length > 1 ? () => _removeDeviceLimit(index) : null,
|
|
icon: const Icon(Icons.delete_outline),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildLabel(String text) {
|
|
return Text(text, style: AppTextStyles.bodyLarge);
|
|
}
|
|
|
|
Widget _buildInput(
|
|
TextEditingController controller, {
|
|
TextInputType? keyboardType,
|
|
String? hint,
|
|
}) {
|
|
return TextField(
|
|
controller: controller,
|
|
keyboardType: keyboardType,
|
|
decoration: _inputDecoration(hint),
|
|
);
|
|
}
|
|
|
|
InputDecoration _inputDecoration(String? hint) {
|
|
return InputDecoration(
|
|
hintText: hint,
|
|
isDense: true,
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
borderSide: BorderSide(color: AppColors.sidebarBg.withValues(alpha: 0.7)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
borderSide: BorderSide(color: AppColors.sidebarBg.withValues(alpha: 0.7)),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DeviceLimitForm {
|
|
String os = 'android';
|
|
final TextEditingController limitController;
|
|
|
|
_DeviceLimitForm({
|
|
String limit = '1',
|
|
}) : limitController = TextEditingController(text: limit);
|
|
|
|
void dispose() {
|
|
limitController.dispose();
|
|
}
|
|
}
|