arc/docs/SCENARIO_DELETE_TICKETS_SUPERADMIN.md
2026-04-14 19:34:55 +03:30

887 lines
33 KiB
Markdown
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# سناریو: حذف تیکت‌های پشتیبانی توسط مدیر سیستم
## 📋 خلاصه اجرایی
این سناریو قابلیت حذف تیکت‌های پشتیبانی توسط مدیر سیستم (superadmin) را پیاده‌سازی می‌کند. در این سناریو:
- فقط مدیر سیستم می‌تواند تیکت‌ها را حذف کند
- اپراتورها و کاربران عادی امکان حذف تیکت ندارند
- دکمه حذف فقط در صفحه لیست تیکت‌های اپراتور برای مدیر سیستم نمایش داده می‌شود
- حذف تیکت به صورت Hard Delete انجام می‌شود (تیکت و پیام‌های مرتبط کاملاً حذف می‌شوند)
---
## 🎯 اهداف
1. **امنیت**: فقط مدیر سیستم (superadmin) بتواند تیکت‌ها را حذف کند
2. **UI/UX**: دکمه حذف فقط برای superadmin در لیست تیکت‌های اپراتور نمایش داده شود
3. **تأیید دو مرحله‌ای**: قبل از حذف، از کاربر تأیید گرفته شود
4. **حذف کامل**: تیکت و تمام پیام‌های مرتبط با آن حذف شوند
---
## 🏗️ معماری
```
┌─────────────────────────────────────────────────────────┐
│ Frontend (Flutter) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ OperatorTicketsPage │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ if (isSuperAdmin) │ │ │
│ │ │ - نمایش دکمه حذف در Bulk Actions │ │ │
│ │ │ - نمایش آیکون حذف در هر ردیف │ │ │
│ │ │ else │ │ │
│ │ │ - عدم نمایش دکمه حذف │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ SupportService.deleteTicket(ticketId) │ │
│ │ ↓ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ DELETE /api/v1/support/operator/tickets/{id} │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ @require_superadmin │ │ │
│ │ │ 1. بررسی وجود تیکت │ │ │
│ │ │ 2. حذف تیکت از دیتابیس │ │ │
│ │ │ 3. بازگشت پاسخ موفقیت │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ TicketRepository.delete_ticket(ticket_id) │ │
│ │ ↓ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Database │
│ ┌────────────────────────────────────────────────────┐ │
│ │ support_tickets │ │
│ │ - حذف رکورد تیکت (CASCADE) │ │
│ │ support_messages │ │
│ │ - حذف تمام پیام‌های مرتبط (CASCADE) │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 📝 پیاده‌سازی
### 1. Backend - Repository Layer
**فایل:** `hesabixAPI/adapters/db/repositories/support/ticket_repository.py`
در این فایل، متد جدید `delete_ticket` را اضافه می‌کنیم:
```python
def delete_ticket(self, ticket_id: int) -> bool:
"""
حذف تیکت و تمام پیام‌های مرتبط
Args:
ticket_id: شناسه تیکت
Returns:
True اگر تیکت حذف شد، False اگر تیکت یافت نشد
"""
ticket = self.get_by_id(ticket_id)
if not ticket:
return False
# حذف تیکت (پیام‌ها به صورت CASCADE حذف می‌شوند)
self.db.delete(ticket)
self.db.commit()
return True
```
**توضیحات:**
- متد `get_by_id` از `BaseRepository` ارث‌بری شده و تیکت را از دیتابیس می‌گیرد
- با حذف تیکت، به دلیل `ondelete="CASCADE"` در model، تمام پیام‌های مرتبط نیز حذف می‌شوند
- اگر تیکت یافت نشد، `False` برمی‌گرداند
---
### 2. Backend - API Layer
**فایل:** `hesabixAPI/adapters/api/v1/support/operator.py`
در این فایل، endpoint جدید برای حذف تیکت اضافه می‌کنیم:
```python
@router.delete("/tickets/{ticket_id}", response_model=SuccessResponse)
@require_app_permission("superadmin")
async def delete_ticket(
request: Request,
ticket_id: int,
current_user: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""حذف تیکت (فقط برای مدیر سیستم)"""
ticket_repo = TicketRepository(db)
# حذف تیکت
deleted = ticket_repo.delete_ticket(ticket_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="تیکت یافت نشد یا قبلاً حذف شده است"
)
return success_response(
{"message": "تیکت با موفقیت حذف شد", "ticket_id": ticket_id},
request
)
```
**توضیحات:**
- دکوراتور `@require_app_permission("superadmin")` تضمین می‌کند فقط superadmin بتواند این endpoint را صدا بزند
- متد HTTP: `DELETE`
- اگر تیکت یافت نشد، خطای `404` برمی‌گرداند
- در صورت موفقیت، پیام موفقیت و ID تیکت حذف شده را برمی‌گرداند
---
### 3. Backend - Permission Decorator (اختیاری)
اگر دکوراتور `@require_app_permission("superadmin")` وجود ندارد، می‌توانیم آن را به فایل permissions اضافه کنیم:
**فایل:** `hesabixAPI/app/core/permissions.py`
```python
from functools import wraps
from fastapi import HTTPException, status
from app.core.auth_dependency import AuthContext
def require_superadmin(func):
"""
دکوراتور برای محدود کردن دسترسی به superadmin
"""
@wraps(func)
async def wrapper(*args, current_user: AuthContext, **kwargs):
if not current_user.is_superadmin():
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="فقط مدیر سیستم مجاز به انجام این عملیات است"
)
return await func(*args, current_user=current_user, **kwargs)
return wrapper
```
**توضیحات:**
- این دکوراتور می‌تواند به جای `@require_app_permission("superadmin")` استفاده شود
- با استفاده از `current_user.is_superadmin()` چک می‌کند که آیا کاربر superadmin است یا نه
---
### 4. Frontend - Service Layer
**فایل:** `hesabixUI/hesabix_ui/lib/services/support_service.dart`
متد جدید برای حذف تیکت اضافه می‌کنیم:
```dart
/// حذف تیکت (فقط برای مدیر سیستم)
Future<void> deleteTicket(int ticketId) async {
try {
await _apiClient.delete(
'/api/v1/support/operator/tickets/$ticketId',
);
} on DioException catch (e) {
throw _handleError(e);
}
}
/// حذف چندین تیکت به صورت گروهی (فقط برای مدیر سیستم)
Future<Map<String, dynamic>> deleteTickets(List<int> ticketIds) async {
try {
final results = <int, dynamic>{};
for (final ticketId in ticketIds) {
try {
await deleteTicket(ticketId);
results[ticketId] = {'success': true};
} catch (e) {
results[ticketId] = {'success': false, 'error': e.toString()};
}
}
final successCount = results.values.where((r) => r['success'] == true).length;
final failCount = results.values.where((r) => r['success'] == false).length;
return {
'total': ticketIds.length,
'success': successCount,
'failed': failCount,
'results': results,
};
} on DioException catch (e) {
throw _handleError(e);
}
}
```
**توضیحات:**
- متد `deleteTicket`: حذف یک تیکت
- متد `deleteTickets`: حذف چندین تیکت به صورت گروهی (برای Bulk Delete)
- در صورت خطا، پیام خطا را به صورت Exception برمی‌گرداند
---
### 5. Frontend - UI Layer
**فایل:** `hesabixUI/hesabix_ui/lib/pages/profile/operator/operator_tickets_page.dart`
تغییرات مورد نیاز:
#### 5.1. اضافه کردن State برای چک کردن superadmin
```dart
class _OperatorTicketsPageState extends State<OperatorTicketsPage> {
Set<int> _selectedRows = <int>{};
// Support data for filters
final SupportService _supportService = SupportService(ApiClient());
List<SupportStatus> _statuses = [];
List<SupportPriority> _priorities = [];
// Refresh counter to force data table refresh
int _refreshCounter = 0;
// Check if current user is superadmin
bool _isSuperAdmin = false;
@override
void initState() {
super.initState();
_loadMetadata();
_checkUserPermissions();
}
Future<void> _checkUserPermissions() async {
try {
final apiClient = ApiClient();
final response = await apiClient.get<Map<String, dynamic>>('/api/v1/auth/me');
final permissions = response.data?['data']?['permissions'] as Map<String, dynamic>?;
final isSuperAdmin = permissions?['is_superadmin'] as bool? ?? false;
setState(() {
_isSuperAdmin = isSuperAdmin;
});
} catch (e) {
// Handle error silently
}
}
// ... بقیه کد
}
```
#### 5.2. اضافه کردن متدهای حذف
```dart
Future<void> _deleteTicket(int ticketId) async {
// نمایش دیالوگ تأیید
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(t.confirmDelete),
content: Text(t.deleteTicketConfirmMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(t.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: Text(t.delete),
),
],
),
);
if (confirmed != true) return;
try {
await _supportService.deleteTicket(ticketId);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.ticketDeletedSuccessfully)),
);
// Refresh the data table
setState(() {
_refreshCounter++;
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(t.errorDeletingTicket(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _deleteSelectedTickets() async {
if (_selectedRows.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.noTicketsSelected)),
);
return;
}
// نمایش دیالوگ تأیید
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(t.confirmBulkDelete),
content: Text(t.deleteBulkTicketsConfirmMessage(_selectedRows.length)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(t.cancel),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: Text(t.delete),
),
],
),
);
if (confirmed != true) return;
try {
// نمایش loading
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
final result = await _supportService.deleteTickets(_selectedRows.toList());
// بستن loading
if (mounted) Navigator.of(context).pop();
if (mounted) {
final successCount = result['success'] as int;
final failCount = result['failed'] as int;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
t.bulkDeleteResult(successCount, failCount),
),
backgroundColor: failCount > 0 ? Colors.orange : Colors.green,
),
);
// پاک کردن انتخاب‌ها و refresh
setState(() {
_selectedRows.clear();
_refreshCounter++;
});
}
} catch (e) {
// بستن loading
if (mounted) Navigator.of(context).pop();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(t.errorDeletingTickets(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
```
#### 5.3. اضافه کردن دکمه حذف گروهی و آیکون حذف به DataTable
```dart
@override
Widget build(BuildContext context) {
final t = AppLocalizations.of(context);
final theme = Theme.of(context);
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
t.operatorPanel,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
// دکمه حذف گروهی (فقط برای superadmin)
if (_isSuperAdmin && _selectedRows.isNotEmpty) ...[
ElevatedButton.icon(
onPressed: _deleteSelectedTickets,
icon: const Icon(Icons.delete_outline),
label: Text(t.deleteSelected(_selectedRows.length)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade50,
foregroundColor: Colors.red.shade700,
),
),
const SizedBox(width: 8),
],
],
),
const SizedBox(height: 16),
Expanded(
child: DataTableWidget<Map<String, dynamic>>(
key: ValueKey('data_table_$_refreshCounter'),
config: DataTableConfig<Map<String, dynamic>>(
title: 'لیست تیکت‌های پشتیبانی - پنل اپراتور',
endpoint: '/api/v1/support/operator/tickets/search',
columns: [
// اضافه کردن ستون Actions در ابتدا (فقط برای superadmin)
if (_isSuperAdmin)
ActionsColumn<Map<String, dynamic>>(
'actions',
t.actions,
width: ColumnWidth.small,
actions: [
DataTableAction(
icon: Icons.delete_outline,
label: t.delete,
color: Colors.red,
onPressed: (ticketData) {
final ticketId = ticketData['id'] as int;
_deleteTicket(ticketId);
},
),
],
),
TextColumn(
'title',
'عنوان',
sortable: true,
searchable: true,
width: ColumnWidth.large,
),
// ... بقیه ستون‌ها
],
searchFields: ['title', 'description', 'user.first_name', 'user.last_name', 'user.email'],
// ... بقیه تنظیمات
),
fromJson: (json) => json,
calendarController: widget.calendarController,
),
),
],
),
),
);
}
```
---
### 6. Frontend - Localization (ترجمه‌ها)
**فایل:** `hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart`
کلیدهای ترجمه جدید که باید اضافه شوند:
```dart
// در کلاس AppLocalizations
String get confirmDelete => 'تأیید حذف';
String get deleteTicketConfirmMessage => 'آیا از حذف این تیکت اطمینان دارید؟ این عملیات قابل برگشت نیست.';
String get ticketDeletedSuccessfully => 'تیکت با موفقیت حذف شد';
String errorDeletingTicket(String error) => 'خطا در حذف تیکت: $error';
String get confirmBulkDelete => 'تأیید حذف گروهی';
String deleteBulkTicketsConfirmMessage(int count) => 'آیا از حذف $count تیکت انتخاب شده اطمینان دارید؟ این عملیات قابل برگشت نیست.';
String get noTicketsSelected => 'هیچ تیکتی انتخاب نشده است';
String deleteSelected(int count) => 'حذف $count مورد انتخابی';
String bulkDeleteResult(int success, int failed) =>
failed > 0
? 'حذف انجام شد: $success موفق، $failed ناموفق'
: '$success تیکت با موفقیت حذف شد';
String errorDeletingTickets(String error) => 'خطا در حذف تیکت‌ها: $error';
String get actions => 'عملیات';
String get delete => 'حذف';
String get cancel => 'لغو';
```
---
## 🔒 نکات امنیتی
### 1. بررسی دسترسی در Backend
- استفاده از دکوراتور `@require_app_permission("superadmin")` تضمین می‌کند که فقط superadmin بتواند endpoint را صدا بزند
- حتی اگر کاربر از Postman یا ابزار دیگری استفاده کند، بدون permission مناسب نمی‌تواند تیکت را حذف کند
### 2. عدم نمایش UI برای کاربران غیرمجاز
- با چک کردن `_isSuperAdmin` در frontend، دکمه حذف فقط برای superadmin نمایش داده می‌شود
- این امر از سردرگمی کاربران جلوگیری می‌کند
### 3. تأیید قبل از حذف
- قبل از حذف، از کاربر تأیید گرفته می‌شود تا از حذف تصادفی جلوگیری شود
- پیام تأیید شامل هشدار "عملیات قابل برگشت نیست" است
### 4. Hard Delete vs Soft Delete
- در این پیاده‌سازی، حذف به صورت Hard Delete است (تیکت کاملاً از دیتابیس حذف می‌شود)
- اگر نیاز به Soft Delete دارید (نگهداری تاریخچه)، می‌توانید فیلد `deleted_at` به model اضافه کنید
---
## 🧪 تست سناریو
### تست Backend (Postman/cURL)
#### 1. تست حذف تیکت با SuperAdmin
```bash
# درخواست با API Key مدیر سیستم
curl -X DELETE \
'http://localhost:8000/api/v1/support/operator/tickets/123' \
-H 'Authorization: ApiKey YOUR_SUPERADMIN_API_KEY' \
-H 'Content-Type: application/json'
# پاسخ موفق:
{
"data": {
"message": "تیکت با موفقیت حذف شد",
"ticket_id": 123
},
"success": true
}
```
#### 2. تست حذف تیکت با اپراتور عادی (باید خطا بدهد)
```bash
# درخواست با API Key اپراتور عادی
curl -X DELETE \
'http://localhost:8000/api/v1/support/operator/tickets/123' \
-H 'Authorization: ApiKey OPERATOR_API_KEY' \
-H 'Content-Type: application/json'
# پاسخ خطا:
{
"detail": "فقط مدیر سیستم مجاز به انجام این عملیات است",
"success": false
}
```
#### 3. تست حذف تیکت غیرموجود
```bash
curl -X DELETE \
'http://localhost:8000/api/v1/support/operator/tickets/99999' \
-H 'Authorization: ApiKey YOUR_SUPERADMIN_API_KEY' \
-H 'Content-Type: application/json'
# پاسخ خطا:
{
"detail": "تیکت یافت نشد یا قبلاً حذف شده است",
"success": false
}
```
---
### تست Frontend
#### سناریوی تست 1: مدیر سیستم
1. با حساب superadmin وارد شوید
2. به صفحه "پنل اپراتور" بروید
3. **انتظار:** دکمه حذف و آیکون حذف در هر ردیف نمایش داده شود
4. یک تیکت را انتخاب کنید
5. روی آیکون حذف کلیک کنید
6. **انتظار:** دیالوگ تأیید نمایش داده شود
7. روی "حذف" کلیک کنید
8. **انتظار:** تیکت حذف شود و پیام موفقیت نمایش داده شود
9. **انتظار:** لیست تیکت‌ها refresh شود
#### سناریوی تست 2: اپراتور عادی
1. با حساب اپراتور عادی وارد شوید
2. به صفحه "پنل اپراتور" بروید
3. **انتظار:** دکمه حذف و آیکون حذف نمایش داده نشود
4. **انتظار:** فقط عملیات مجاز (مشاهده، ویرایش وضعیت، پاسخ) نمایش داده شود
#### سناریوی تست 3: حذف گروهی
1. با حساب superadmin وارد شوید
2. چند تیکت را انتخاب کنید
3. **انتظار:** دکمه "حذف X مورد انتخابی" نمایش داده شود
4. روی دکمه کلیک کنید
5. **انتظار:** دیالوگ تأیید با تعداد تیکت‌های انتخابی نمایش داده شود
6. روی "حذف" کلیک کنید
7. **انتظار:** loading نمایش داده شود
8. **انتظار:** تمام تیکت‌های انتخابی حذف شوند
9. **انتظار:** پیام موفقیت با آمار (تعداد موفق/ناموفق) نمایش داده شود
---
## 📊 Database Schema
تغییری در schema نیاز نیست، اما CASCADE relationship را بررسی کنید:
**فایل:** `hesabixAPI/adapters/db/models/support/message.py`
```python
class Message(Base):
"""پیام‌های تیکت"""
__tablename__ = "support_messages"
# ...
# Foreign Key با CASCADE
ticket_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("support_tickets.id", ondelete="CASCADE"), # ✅ این باید CASCADE باشد
nullable=False,
index=True
)
```
اگر `ondelete="CASCADE"` تنظیم نشده، باید آن را اضافه کنید و migration بزنید.
---
## 🚀 Migration (در صورت نیاز)
اگر `ondelete="CASCADE"` در model موجود نیست:
```bash
# ایجاد migration جدید
cd hesabixAPI
alembic revision --autogenerate -m "Add CASCADE delete for ticket messages"
# اعمال migration
alembic upgrade head
```
---
## 📝 Checklist پیاده‌سازی
### Backend
- [ ] اضافه کردن متد `delete_ticket` به `TicketRepository`
- [ ] اضافه کردن endpoint `DELETE /tickets/{ticket_id}` در `operator.py`
- [ ] تست endpoint با superadmin API key
- [ ] تست endpoint با operator API key (باید خطا بدهد)
- [ ] تست حذف تیکت غیرموجود
- [ ] بررسی CASCADE delete در database schema
- [ ] اجرای migration (در صورت نیاز)
### Frontend
- [ ] اضافه کردن متد `deleteTicket` به `SupportService`
- [ ] اضافه کردن متد `deleteTickets` (bulk delete) به `SupportService`
- [ ] اضافه کردن چک `_isSuperAdmin` در `OperatorTicketsPage`
- [ ] اضافه کردن دکمه حذف گروهی (conditional rendering)
- [ ] اضافه کردن ستون Actions با آیکون حذف
- [ ] پیاده‌سازی متد `_deleteTicket` با دیالوگ تأیید
- [ ] پیاده‌سازی متد `_deleteSelectedTickets` با دیالوگ تأیید
- [ ] اضافه کردن کلیدهای ترجمه به `app_localizations_fa.dart`
- [ ] تست UI با حساب superadmin
- [ ] تست UI با حساب operator
- [ ] تست حذف تک تیکت
- [ ] تست حذف گروهی
### Documentation
- [ ] به‌روزرسانی API documentation
- [ ] به‌روزرسانی User Guide (در صورت نیاز)
---
## 🎨 UI/UX Mockup
### نمایش برای SuperAdmin
```
┌─────────────────────────────────────────────────────────────┐
│ پنل اپراتور [حذف 3 مورد انتخابی] │
├─────────────────────────────────────────────────────────────┤
│ [✓] [🗑️] | عنوان تیکت 1 | کاربر 1 | باز | عادی | ... │
│ [✓] [🗑️] | عنوان تیکت 2 | کاربر 2 | بسته | فوری | ... │
│ [✓] [🗑️] | عنوان تیکت 3 | کاربر 3 | باز | عادی | ... │
│ [ ] [🗑️] | عنوان تیکت 4 | کاربر 4 | باز | کم | ... │
└─────────────────────────────────────────────────────────────┘
```
### نمایش برای Operator (بدون دکمه حذف)
```
┌─────────────────────────────────────────────────────────────┐
│ پنل اپراتور │
├─────────────────────────────────────────────────────────────┤
│ [✓] | عنوان تیکت 1 | کاربر 1 | باز | عادی | ... │
│ [✓] | عنوان تیکت 2 | کاربر 2 | بسته | فوری | ... │
│ [✓] | عنوان تیکت 3 | کاربر 3 | باز | عادی | ... │
│ [ ] | عنوان تیکت 4 | کاربر 4 | باز | کم | ... │
└─────────────────────────────────────────────────────────────┘
```
### دیالوگ تأیید حذف
```
┌──────────────────────────────────┐
│ تأیید حذف │
├──────────────────────────────────┤
│ آیا از حذف این تیکت اطمینان │
│ دارید؟ این عملیات قابل برگشت │
│ نیست. │
│ │
│ [لغو] [حذف 🗑️] │
└──────────────────────────────────┘
```
---
## 🔄 جریان کامل (Flow Diagram)
```
کاربر SuperAdmin
↓
ورود به صفحه لیست تیکت‌های اپراتور
↓
چک permission → is_superadmin == true
↓
نمایش دکمه حذف در UI
↓
کلیک روی آیکون حذف یک تیکت
↓
نمایش دیالوگ تأیید
↓
کاربر تأیید می‌کند
↓
Frontend → API Call: DELETE /api/v1/support/operator/tickets/{id}
↓
Backend → بررسی permission با @require_app_permission("superadmin")
↓
Backend → حذف تیکت از database (CASCADE delete پیام‌ها)
↓
Backend → پاسخ موفقیت
↓
Frontend → نمایش پیام موفقیت
↓
Frontend → Refresh لیست تیکت‌ها
↓
پایان
```
---
## 💡 پیشنهادات بهبود (اختیاری)
### 1. Soft Delete به جای Hard Delete
اگر می‌خواهید تاریخچه را نگه دارید:
```python
# در model Ticket
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
deleted_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
# در repository
def soft_delete_ticket(self, ticket_id: int, user_id: int) -> bool:
ticket = self.get_by_id(ticket_id)
if not ticket:
return False
ticket.deleted_at = datetime.utcnow()
ticket.deleted_by = user_id
self.db.commit()
return True
```
### 2. Activity Log
ثبت لاگ حذف تیکت برای audit:
```python
# بعد از حذف تیکت
from app.services.activity_log_service import log_activity
log_activity(
db=db,
user_id=current_user.get_user_id(),
entity_type="ticket",
entity_id=ticket_id,
action="delete",
details={"ticket_title": ticket.title}
)
```
### 3. Undo قابلیت بازگردانی
با استفاده از Soft Delete + Undo API:
```python
@router.post("/tickets/{ticket_id}/restore", response_model=SuccessResponse)
@require_app_permission("superadmin")
async def restore_ticket(ticket_id: int, ...):
"""بازگردانی تیکت حذف شده"""
# ...
```
### 4. Bulk Delete API Endpoint
بهینه‌سازی حذف گروهی با یک API call:
```python
@router.delete("/tickets/bulk", response_model=SuccessResponse)
@require_app_permission("superadmin")
async def bulk_delete_tickets(
ticket_ids: List[int],
...
):
"""حذف گروهی تیکت‌ها"""
# ...
```
---
## 📞 پشتیبانی و سوالات
اگر در پیاده‌سازی با مشکل مواجه شدید:
1. لاگ‌های backend را بررسی کنید
2. Network tab مرورگر را برای خطاهای API چک کنید
3. مطمئن شوید permission ها صحیح تنظیم شده‌اند
4. مطمئن شوید CASCADE delete در database تنظیم شده است
---
## 📚 منابع مرتبط
- [سند سیستم دسترسی دو سطحی](PERMISSIONS_SYSTEM.md)
- [سند مدیریت اپراتورها](SUPPORT_OPERATORS_MANAGEMENT_SCENARIO.md)
- [FastAPI Security Best Practices](https://fastapi.tiangolo.com/tutorial/security/)
---
**تاریخ ایجاد:** 2025-12-05
**نسخه:** 1.0
**نویسنده:** AI Assistant (Cursor)