Really-amin commited on
Commit
89ba751
·
verified ·
1 Parent(s): eea0f0a

Upload 25 files

Browse files
app/frontend/README.md ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Legal Dashboard Frontend Organization
2
+
3
+ ## Overview
4
+
5
+ This directory contains the frontend files for the Legal Dashboard OCR system. The structure follows hierarchical frontend organization principles for maintainability and clarity.
6
+
7
+ ## Directory Structure
8
+
9
+ ```
10
+ frontend/
11
+ ├── improved_legal_dashboard.html # Main application dashboard
12
+ ├── documents.html # Reference for advanced document features
13
+ ├── scraping_dashboard.html # Reference for advanced scraping features
14
+ ├── reports.html # Reports and analytics page
15
+ ├── index.html # Legacy dashboard (to be deprecated)
16
+ ├── scraping.html # Legacy scraping page (to be deprecated)
17
+ ├── upload.html # Legacy upload page (to be deprecated)
18
+ ├── dev/ # Development and testing tools
19
+ │ ├── api-test.html # API testing interface
20
+ │ └── test_integration.html # Integration testing page
21
+ └── js/ # JavaScript modules
22
+ ├── api-client.js # Core API communication
23
+ ├── file-upload-handler.js # File upload functionality
24
+ ├── document-crud.js # Document management operations
25
+ ├── scraping-control.js # Scraping functionality
26
+ ├── notifications.js # Toast and notification system
27
+ └── api-connection-test.js # API testing utilities
28
+ ```
29
+
30
+ ## File Status
31
+
32
+ ### ✅ **Primary Application**
33
+ - **`improved_legal_dashboard.html`** - Main dashboard with comprehensive functionality
34
+ - Complete feature set: statistics, charts, file upload, document management, scraping
35
+ - Real API integration with proper error handling
36
+ - Modern UI with Persian RTL support
37
+ - Chart.js integration for data visualization
38
+
39
+ ### 🔄 **Reference Files (To Be Merged)**
40
+ - **`documents.html`** - Advanced document management features
41
+ - Advanced filtering and search capabilities
42
+ - Document CRUD operations
43
+ - Status tracking and quality metrics
44
+ - Bulk operations support
45
+
46
+ - **`scraping_dashboard.html`** - Advanced scraping features
47
+ - Real-time scraping status monitoring
48
+ - Rating system for scraped content
49
+ - Performance metrics and statistics
50
+ - Bootstrap-based modern UI
51
+
52
+ ### 🧪 **Development Tools**
53
+ - **`dev/api-test.html`** - Comprehensive API testing tool
54
+ - **`dev/test_integration.html`** - Simple integration testing interface
55
+
56
+ ### ❌ **Legacy Files (To Be Deprecated)**
57
+ - **`index.html`** - Older version of main dashboard
58
+ - **`scraping.html`** - Basic scraping interface (superseded)
59
+ - **`upload.html`** - Standalone upload page (integrated in main)
60
+
61
+ ## JavaScript Architecture
62
+
63
+ ### Core Modules
64
+
65
+ #### `api-client.js`
66
+ - Centralized API communication layer
67
+ - Error handling and response transformation
68
+ - Request/response interceptors
69
+ - Health check and connection monitoring
70
+
71
+ #### `file-upload-handler.js`
72
+ - Drag-and-drop file upload
73
+ - File validation and processing
74
+ - Upload progress tracking
75
+ - Batch upload capabilities
76
+
77
+ #### `document-crud.js`
78
+ - Document creation, reading, updating, deletion
79
+ - Document search and filtering
80
+ - Status management
81
+ - Quality assessment
82
+
83
+ #### `scraping-control.js`
84
+ - Web scraping initiation and control
85
+ - Real-time status monitoring
86
+ - Result processing and rating
87
+ - Performance metrics
88
+
89
+ #### `notifications.js`
90
+ - Toast notification system
91
+ - Error reporting
92
+ - Success/error message handling
93
+ - User feedback mechanisms
94
+
95
+ #### `api-connection-test.js`
96
+ - API endpoint testing utilities
97
+ - Connection validation
98
+ - Response verification
99
+ - Development debugging tools
100
+
101
+ ## Integration Guidelines
102
+
103
+ ### API Integration
104
+ All frontend components use the centralized `api-client.js` for backend communication:
105
+
106
+ ```javascript
107
+ // Example usage
108
+ const api = new LegalDashboardAPI();
109
+ const documents = await api.getDocuments();
110
+ ```
111
+
112
+ ### Error Handling
113
+ Consistent error handling across all modules:
114
+
115
+ ```javascript
116
+ try {
117
+ const result = await api.request('/endpoint');
118
+ showToast('Success', 'success');
119
+ } catch (error) {
120
+ showToast(`Error: ${error.message}`, 'error');
121
+ }
122
+ ```
123
+
124
+ ### UI Components
125
+ Reusable components follow consistent patterns:
126
+ - Toast notifications for user feedback
127
+ - Loading states for async operations
128
+ - Error boundaries for graceful failure handling
129
+ - Responsive design for mobile compatibility
130
+
131
+ ## Development Workflow
132
+
133
+ ### Testing
134
+ 1. Use `dev/api-test.html` for comprehensive API testing
135
+ 2. Use `dev/test_integration.html` for quick integration checks
136
+ 3. All JavaScript modules include error handling and logging
137
+
138
+ ### Feature Development
139
+ 1. New features should be integrated into `improved_legal_dashboard.html`
140
+ 2. Reference files (`documents.html`, `scraping_dashboard.html`) provide advanced features to merge
141
+ 3. JavaScript modules should be modular and reusable
142
+
143
+ ### Code Organization
144
+ Following [hierarchical frontend structure principles](https://github.com/petejank/hierarchical-front-end-structure):
145
+
146
+ - **Separation of concerns**: Each file has a single responsibility
147
+ - **Hierarchical organization**: Related files are grouped together
148
+ - **Self-contained modules**: Files can be moved without breaking dependencies
149
+ - **Consistent naming**: Clear, descriptive file and directory names
150
+
151
+ ## Migration Plan
152
+
153
+ ### Phase 1: Consolidation
154
+ - [x] Move testing files to `dev/` directory
155
+ - [ ] Merge advanced document features from `documents.html` into main dashboard
156
+ - [ ] Merge advanced scraping features from `scraping_dashboard.html` into main dashboard
157
+
158
+ ### Phase 2: Cleanup
159
+ - [ ] Remove `index.html` (redirect to main dashboard)
160
+ - [ ] Remove `scraping.html` (functionality in main dashboard)
161
+ - [ ] Remove `upload.html` (functionality in main dashboard)
162
+
163
+ ### Phase 3: Enhancement
164
+ - [ ] Enhance main dashboard with merged features
165
+ - [ ] Improve real-time updates and monitoring
166
+ - [ ] Add advanced filtering and search capabilities
167
+ - [ ] Implement better error handling and user feedback
168
+
169
+ ## Best Practices
170
+
171
+ ### Code Quality
172
+ - Use consistent error handling patterns
173
+ - Implement proper loading states
174
+ - Provide clear user feedback
175
+ - Follow responsive design principles
176
+
177
+ ### Performance
178
+ - Minimize API calls through caching
179
+ - Use debouncing for search operations
180
+ - Implement lazy loading for large datasets
181
+ - Optimize bundle size through modular imports
182
+
183
+ ### Security
184
+ - Validate all user inputs
185
+ - Sanitize data before display
186
+ - Use HTTPS for all API communications
187
+ - Implement proper authentication checks
188
+
189
+ ### Accessibility
190
+ - Support RTL languages (Persian)
191
+ - Provide keyboard navigation
192
+ - Include proper ARIA labels
193
+ - Ensure color contrast compliance
194
+
195
+ ## API Endpoints
196
+
197
+ The frontend integrates with the following backend endpoints:
198
+
199
+ ### Dashboard
200
+ - `GET /api/dashboard/summary` - Dashboard statistics
201
+ - `GET /api/dashboard/charts-data` - Chart data
202
+ - `GET /api/dashboard/ai-suggestions` - AI recommendations
203
+
204
+ ### Documents
205
+ - `GET /api/documents` - List documents
206
+ - `POST /api/documents` - Create document
207
+ - `PUT /api/documents/{id}` - Update document
208
+ - `DELETE /api/documents/{id}` - Delete document
209
+
210
+ ### OCR Processing
211
+ - `POST /api/ocr/process` - Process document OCR
212
+ - `POST /api/ocr/batch-process` - Batch OCR processing
213
+ - `GET /api/ocr/status` - OCR processing status
214
+
215
+ ### Scraping
216
+ - `POST /api/scraping/scrape` - Start scraping
217
+ - `GET /api/scraping/status` - Scraping status
218
+ - `GET /api/scraping/items` - Scraped items
219
+
220
+ ### Analytics
221
+ - `GET /api/analytics/overview` - Analytics overview
222
+ - `GET /api/analytics/trends` - Trend analysis
223
+ - `GET /api/analytics/similarity` - Document similarity
224
+
225
+ ## Contributing
226
+
227
+ When adding new features:
228
+
229
+ 1. **Follow the hierarchical structure** - Group related files together
230
+ 2. **Use the API client** - Don't create direct fetch calls
231
+ 3. **Include error handling** - Always handle potential failures
232
+ 4. **Add user feedback** - Use toast notifications for important actions
233
+ 5. **Test thoroughly** - Use the development tools for testing
234
+ 6. **Document changes** - Update this README when adding new files
235
+
236
+ ## Support
237
+
238
+ For development questions or issues:
239
+ 1. Check the API testing tools in `dev/` directory
240
+ 2. Review the JavaScript modules for examples
241
+ 3. Test with the integration tools
242
+ 4. Follow the established patterns and conventions
app/frontend/analytics.html ADDED
@@ -0,0 +1,1912 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>آمار و تحلیل | سامانه حقوقی</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
12
+
13
+ <!-- Load API Client -->
14
+ <script src="/static/js/api-client.js"></script>
15
+ <script src="/static/js/core.js"></script>
16
+
17
+ <style>
18
+ :root {
19
+ --text-primary: #0f172a;
20
+ --text-secondary: #475569;
21
+ --text-muted: #64748b;
22
+ --text-light: #ffffff;
23
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
24
+ --card-bg: rgba(255, 255, 255, 0.95);
25
+ --glass-bg: rgba(255, 255, 255, 0.9);
26
+ --glass-border: rgba(148, 163, 184, 0.2);
27
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
28
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
29
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
30
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
31
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
32
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
33
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
34
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
35
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
36
+ --sidebar-width: 260px;
37
+ --border-radius: 12px;
38
+ --border-radius-sm: 8px;
39
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
40
+ --transition-fast: all 0.15s ease-in-out;
41
+ --font-size-xs: 0.7rem;
42
+ --font-size-sm: 0.8rem;
43
+ --font-size-base: 0.9rem;
44
+ --font-size-lg: 1.1rem;
45
+ --font-size-xl: 1.25rem;
46
+ --font-size-2xl: 1.5rem;
47
+ }
48
+
49
+ * {
50
+ margin: 0;
51
+ padding: 0;
52
+ box-sizing: border-box;
53
+ }
54
+
55
+ body {
56
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
57
+ background: var(--body-bg);
58
+ color: var(--text-primary);
59
+ line-height: 1.6;
60
+ overflow-x: hidden;
61
+ font-size: var(--font-size-base);
62
+ }
63
+
64
+ ::-webkit-scrollbar {
65
+ width: 6px;
66
+ height: 6px;
67
+ }
68
+
69
+ ::-webkit-scrollbar-track {
70
+ background: rgba(0, 0, 0, 0.02);
71
+ border-radius: 10px;
72
+ }
73
+
74
+ ::-webkit-scrollbar-thumb {
75
+ background: var(--primary-gradient);
76
+ border-radius: 10px;
77
+ }
78
+
79
+ .dashboard-container {
80
+ display: flex;
81
+ min-height: 100vh;
82
+ width: 100%;
83
+ }
84
+
85
+ /* سایدبار مشابه صفحات قبلی */
86
+ .sidebar {
87
+ width: var(--sidebar-width);
88
+ background: linear-gradient(135deg,
89
+ rgba(248, 250, 252, 0.98) 0%,
90
+ rgba(241, 245, 249, 0.95) 25%,
91
+ rgba(226, 232, 240, 0.98) 50%,
92
+ rgba(203, 213, 225, 0.95) 75%,
93
+ rgba(148, 163, 184, 0.1) 100%);
94
+ backdrop-filter: blur(25px);
95
+ -webkit-backdrop-filter: blur(25px);
96
+ padding: 1rem 0;
97
+ position: fixed;
98
+ height: 100vh;
99
+ right: 0;
100
+ top: 0;
101
+ z-index: 1000;
102
+ overflow-y: auto;
103
+ box-shadow:
104
+ 0 0 0 1px rgba(59, 130, 246, 0.08),
105
+ -8px 0 32px rgba(59, 130, 246, 0.12),
106
+ inset 0 1px 0 rgba(255, 255, 255, 0.6);
107
+ border-left: 1px solid rgba(59, 130, 246, 0.15);
108
+ }
109
+
110
+ .sidebar-header {
111
+ padding: 0 1rem 1rem;
112
+ border-bottom: 1px solid rgba(59, 130, 246, 0.12);
113
+ margin-bottom: 1rem;
114
+ display: flex;
115
+ justify-content: space-between;
116
+ align-items: center;
117
+ background: linear-gradient(135deg,
118
+ rgba(255, 255, 255, 0.4) 0%,
119
+ rgba(248, 250, 252, 0.2) 100%);
120
+ margin: 0 0.5rem 1rem;
121
+ border-radius: var(--border-radius);
122
+ backdrop-filter: blur(10px);
123
+ }
124
+
125
+ .logo {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 0.6rem;
129
+ color: var(--text-primary);
130
+ text-decoration: none;
131
+ }
132
+
133
+ .logo-icon {
134
+ width: 2rem;
135
+ height: 2rem;
136
+ background: var(--primary-gradient);
137
+ border-radius: var(--border-radius-sm);
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ font-size: 1rem;
142
+ color: white;
143
+ }
144
+
145
+ .logo-text {
146
+ font-size: var(--font-size-lg);
147
+ font-weight: 700;
148
+ background: var(--primary-gradient);
149
+ -webkit-background-clip: text;
150
+ -webkit-text-fill-color: transparent;
151
+ }
152
+
153
+ .nav-section {
154
+ margin-bottom: 1rem;
155
+ }
156
+
157
+ .nav-title {
158
+ padding: 0 1rem 0.4rem;
159
+ font-size: var(--font-size-xs);
160
+ font-weight: 600;
161
+ text-transform: uppercase;
162
+ letter-spacing: 0.5px;
163
+ color: var(--text-secondary);
164
+ }
165
+
166
+ .nav-menu {
167
+ list-style: none;
168
+ }
169
+
170
+ .nav-item {
171
+ margin: 0.15rem 0.5rem;
172
+ }
173
+
174
+ .nav-link {
175
+ display: flex;
176
+ align-items: center;
177
+ padding: 0.6rem 0.8rem;
178
+ color: var(--text-primary);
179
+ text-decoration: none;
180
+ border-radius: var(--border-radius-sm);
181
+ transition: var(--transition-smooth);
182
+ font-weight: 500;
183
+ font-size: var(--font-size-sm);
184
+ cursor: pointer;
185
+ border: 1px solid transparent;
186
+ }
187
+
188
+ .nav-link:hover {
189
+ color: var(--text-primary);
190
+ transform: translateX(-2px);
191
+ border-color: rgba(59, 130, 246, 0.15);
192
+ background: rgba(59, 130, 246, 0.05);
193
+ }
194
+
195
+ .nav-link.active {
196
+ background: var(--primary-gradient);
197
+ color: var(--text-light);
198
+ box-shadow: var(--shadow-md);
199
+ }
200
+
201
+ .nav-icon {
202
+ margin-left: 0.6rem;
203
+ width: 1rem;
204
+ text-align: center;
205
+ font-size: 0.9rem;
206
+ }
207
+
208
+ .nav-badge {
209
+ background: var(--danger-gradient);
210
+ color: white;
211
+ padding: 0.15rem 0.4rem;
212
+ border-radius: 10px;
213
+ font-size: var(--font-size-xs);
214
+ font-weight: 600;
215
+ margin-right: auto;
216
+ min-width: 1.2rem;
217
+ text-align: center;
218
+ }
219
+
220
+ /* محتوای اصلی */
221
+ .main-content {
222
+ flex: 1;
223
+ margin-right: var(--sidebar-width);
224
+ padding: 1rem;
225
+ min-height: 100vh;
226
+ width: calc(100% - var(--sidebar-width));
227
+ }
228
+
229
+ .page-header {
230
+ display: flex;
231
+ justify-content: space-between;
232
+ align-items: center;
233
+ margin-bottom: 2rem;
234
+ padding: 1rem 0;
235
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
236
+ }
237
+
238
+ .page-title {
239
+ font-size: var(--font-size-2xl);
240
+ font-weight: 800;
241
+ background: var(--primary-gradient);
242
+ -webkit-background-clip: text;
243
+ -webkit-text-fill-color: transparent;
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 0.6rem;
247
+ }
248
+
249
+ .page-actions {
250
+ display: flex;
251
+ gap: 0.8rem;
252
+ }
253
+
254
+ .btn {
255
+ padding: 0.6rem 1.2rem;
256
+ border: none;
257
+ border-radius: var(--border-radius-sm);
258
+ font-family: inherit;
259
+ font-weight: 600;
260
+ cursor: pointer;
261
+ transition: var(--transition-smooth);
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 0.5rem;
265
+ text-decoration: none;
266
+ font-size: var(--font-size-sm);
267
+ }
268
+
269
+ .btn-primary {
270
+ background: var(--primary-gradient);
271
+ color: white;
272
+ box-shadow: var(--shadow-sm);
273
+ }
274
+
275
+ .btn-primary:hover {
276
+ box-shadow: var(--shadow-md);
277
+ transform: translateY(-1px);
278
+ }
279
+
280
+ .btn-outline {
281
+ background: transparent;
282
+ color: var(--text-primary);
283
+ border: 1px solid rgba(59, 130, 246, 0.2);
284
+ }
285
+
286
+ .btn-outline:hover {
287
+ background: rgba(59, 130, 246, 0.05);
288
+ border-color: rgba(59, 130, 246, 0.4);
289
+ }
290
+
291
+ .btn-sm {
292
+ padding: 0.4rem 0.8rem;
293
+ font-size: var(--font-size-xs);
294
+ }
295
+
296
+ /* گرید تحلیلی */
297
+ .analytics-grid {
298
+ display: grid;
299
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
300
+ gap: 1.5rem;
301
+ margin-bottom: 2rem;
302
+ }
303
+
304
+ .analytics-card {
305
+ background: var(--card-bg);
306
+ backdrop-filter: blur(10px);
307
+ -webkit-backdrop-filter: blur(10px);
308
+ border-radius: var(--border-radius);
309
+ padding: 1.5rem;
310
+ box-shadow: var(--shadow-md);
311
+ border: 1px solid rgba(255, 255, 255, 0.3);
312
+ transition: var(--transition-smooth);
313
+ position: relative;
314
+ overflow: hidden;
315
+ }
316
+
317
+ .analytics-card::before {
318
+ content: '';
319
+ position: absolute;
320
+ top: 0;
321
+ left: 0;
322
+ right: 0;
323
+ height: 4px;
324
+ background: var(--primary-gradient);
325
+ }
326
+
327
+ .analytics-card:hover {
328
+ transform: translateY(-2px);
329
+ box-shadow: var(--shadow-lg);
330
+ }
331
+
332
+ .analytics-header {
333
+ display: flex;
334
+ justify-content: space-between;
335
+ align-items: center;
336
+ margin-bottom: 1rem;
337
+ }
338
+
339
+ .analytics-title {
340
+ font-size: var(--font-size-lg);
341
+ font-weight: 700;
342
+ color: var(--text-primary);
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 0.5rem;
346
+ }
347
+
348
+ .analytics-actions {
349
+ display: flex;
350
+ gap: 0.5rem;
351
+ }
352
+
353
+ .chart-container {
354
+ height: 300px;
355
+ position: relative;
356
+ }
357
+
358
+ .chart-filters {
359
+ display: flex;
360
+ gap: 0.3rem;
361
+ margin-bottom: 1rem;
362
+ }
363
+
364
+ .chart-filter {
365
+ padding: 0.3rem 0.8rem;
366
+ border: none;
367
+ border-radius: 12px;
368
+ background: rgba(59, 130, 246, 0.08);
369
+ color: var(--text-secondary);
370
+ font-family: inherit;
371
+ font-size: var(--font-size-xs);
372
+ font-weight: 500;
373
+ cursor: pointer;
374
+ transition: var(--transition-fast);
375
+ }
376
+
377
+ .chart-filter:hover {
378
+ background: rgba(59, 130, 246, 0.12);
379
+ }
380
+
381
+ .chart-filter.active {
382
+ background: var(--primary-gradient);
383
+ color: white;
384
+ box-shadow: var(--shadow-sm);
385
+ }
386
+
387
+ /* آمار کلی */
388
+ .overview-stats {
389
+ display: grid;
390
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
391
+ gap: 1rem;
392
+ margin-bottom: 2rem;
393
+ }
394
+
395
+ .stat-card {
396
+ background: var(--card-bg);
397
+ border-radius: var(--border-radius);
398
+ padding: 1.5rem;
399
+ text-align: center;
400
+ box-shadow: var(--shadow-sm);
401
+ border: 1px solid rgba(255, 255, 255, 0.3);
402
+ transition: var(--transition-smooth);
403
+ position: relative;
404
+ overflow: hidden;
405
+ }
406
+
407
+ .stat-card::before {
408
+ content: '';
409
+ position: absolute;
410
+ top: 0;
411
+ left: 0;
412
+ right: 0;
413
+ height: 3px;
414
+ }
415
+
416
+ .stat-card.primary::before { background: var(--primary-gradient); }
417
+ .stat-card.success::before { background: var(--success-gradient); }
418
+ .stat-card.warning::before { background: var(--warning-gradient); }
419
+ .stat-card.danger::before { background: var(--danger-gradient); }
420
+
421
+ .stat-card:hover {
422
+ transform: translateY(-3px);
423
+ box-shadow: var(--shadow-lg);
424
+ }
425
+
426
+ .stat-value {
427
+ font-size: var(--font-size-2xl);
428
+ font-weight: 800;
429
+ color: var(--text-primary);
430
+ margin-bottom: 0.5rem;
431
+ }
432
+
433
+ .stat-label {
434
+ font-size: var(--font-size-sm);
435
+ color: var(--text-secondary);
436
+ margin-bottom: 0.5rem;
437
+ }
438
+
439
+ .stat-change {
440
+ font-size: var(--font-size-xs);
441
+ font-weight: 600;
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: center;
445
+ gap: 0.3rem;
446
+ }
447
+
448
+ .stat-change.positive { color: #059669; }
449
+ .stat-change.negative { color: #dc2626; }
450
+
451
+ /* ویجت‌های پیشرفته */
452
+ .advanced-widgets {
453
+ display: grid;
454
+ grid-template-columns: 1fr 1fr;
455
+ gap: 1.5rem;
456
+ margin-bottom: 2rem;
457
+ }
458
+
459
+ .widget {
460
+ background: var(--card-bg);
461
+ border-radius: var(--border-radius);
462
+ padding: 1.5rem;
463
+ box-shadow: var(--shadow-md);
464
+ border: 1px solid rgba(255, 255, 255, 0.3);
465
+ }
466
+
467
+ .widget-header {
468
+ display: flex;
469
+ justify-content: space-between;
470
+ align-items: center;
471
+ margin-bottom: 1rem;
472
+ }
473
+
474
+ .widget-title {
475
+ font-size: var(--font-size-lg);
476
+ font-weight: 600;
477
+ color: var(--text-primary);
478
+ }
479
+
480
+ /* جدول آمار */
481
+ .stats-table {
482
+ width: 100%;
483
+ border-collapse: separate;
484
+ border-spacing: 0;
485
+ background: var(--card-bg);
486
+ border-radius: var(--border-radius);
487
+ overflow: hidden;
488
+ box-shadow: var(--shadow-sm);
489
+ }
490
+
491
+ .stats-table thead {
492
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.03), rgba(255, 255, 255, 0.1));
493
+ }
494
+
495
+ .stats-table th {
496
+ padding: 1rem;
497
+ text-align: right;
498
+ font-weight: 600;
499
+ color: var(--text-primary);
500
+ font-size: var(--font-size-sm);
501
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
502
+ }
503
+
504
+ .stats-table td {
505
+ padding: 0.8rem 1rem;
506
+ border-bottom: 1px solid rgba(0, 0, 0, 0.03);
507
+ font-size: var(--font-size-sm);
508
+ }
509
+
510
+ .stats-table tbody tr {
511
+ transition: all 0.2s ease;
512
+ }
513
+
514
+ .stats-table tbody tr:hover {
515
+ background: rgba(59, 130, 246, 0.02);
516
+ }
517
+
518
+ /* نمودار پیشرفت */
519
+ .progress-widget {
520
+ padding: 1rem;
521
+ }
522
+
523
+ .progress-item {
524
+ display: flex;
525
+ justify-content: space-between;
526
+ align-items: center;
527
+ margin-bottom: 1rem;
528
+ }
529
+
530
+ .progress-item:last-child {
531
+ margin-bottom: 0;
532
+ }
533
+
534
+ .progress-info {
535
+ flex: 1;
536
+ margin-left: 1rem;
537
+ }
538
+
539
+ .progress-label {
540
+ font-size: var(--font-size-sm);
541
+ font-weight: 500;
542
+ color: var(--text-primary);
543
+ margin-bottom: 0.3rem;
544
+ }
545
+
546
+ .progress-bar {
547
+ width: 100%;
548
+ height: 6px;
549
+ background: rgba(0, 0, 0, 0.08);
550
+ border-radius: 3px;
551
+ overflow: hidden;
552
+ }
553
+
554
+ .progress-fill {
555
+ height: 100%;
556
+ border-radius: 3px;
557
+ transition: width 0.6s ease;
558
+ }
559
+
560
+ .progress-fill.primary { background: var(--primary-gradient); }
561
+ .progress-fill.success { background: var(--success-gradient); }
562
+ .progress-fill.warning { background: var(--warning-gradient); }
563
+ .progress-fill.danger { background: var(--danger-gradient); }
564
+
565
+ .progress-value {
566
+ font-size: var(--font-size-sm);
567
+ font-weight: 600;
568
+ color: var(--text-primary);
569
+ min-width: 3rem;
570
+ text-align: left;
571
+ }
572
+
573
+ /* محصولات اصلی */
574
+ .main-analytics {
575
+ margin-bottom: 2rem;
576
+ }
577
+
578
+ .main-grid {
579
+ display: grid;
580
+ grid-template-columns: 2fr 1fr;
581
+ gap: 1.5rem;
582
+ margin-bottom: 1.5rem;
583
+ }
584
+
585
+ /* فیلترهای زمانی */
586
+ .time-filters {
587
+ background: var(--card-bg);
588
+ border-radius: var(--border-radius);
589
+ padding: 1rem;
590
+ margin-bottom: 1.5rem;
591
+ box-shadow: var(--shadow-sm);
592
+ border: 1px solid rgba(255, 255, 255, 0.3);
593
+ }
594
+
595
+ .filter-row {
596
+ display: flex;
597
+ gap: 1rem;
598
+ align-items: center;
599
+ flex-wrap: wrap;
600
+ }
601
+
602
+ .filter-group {
603
+ display: flex;
604
+ flex-direction: column;
605
+ gap: 0.3rem;
606
+ }
607
+
608
+ .filter-label {
609
+ font-size: var(--font-size-xs);
610
+ font-weight: 600;
611
+ color: var(--text-secondary);
612
+ }
613
+
614
+ .filter-input,
615
+ .filter-select {
616
+ padding: 0.5rem;
617
+ border: 1px solid var(--glass-border);
618
+ border-radius: var(--border-radius-sm);
619
+ background: var(--glass-bg);
620
+ color: var(--text-primary);
621
+ font-family: inherit;
622
+ font-size: var(--font-size-sm);
623
+ transition: var(--transition-smooth);
624
+ }
625
+
626
+ .filter-input:focus,
627
+ .filter-select:focus {
628
+ outline: none;
629
+ border-color: #3b82f6;
630
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
631
+ }
632
+
633
+ /* متریک‌های زنده */
634
+ .live-metrics {
635
+ display: grid;
636
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
637
+ gap: 1rem;
638
+ margin-bottom: 1.5rem;
639
+ }
640
+
641
+ .live-metric {
642
+ background: var(--card-bg);
643
+ border-radius: var(--border-radius);
644
+ padding: 1rem;
645
+ text-align: center;
646
+ box-shadow: var(--shadow-sm);
647
+ border: 1px solid rgba(255, 255, 255, 0.3);
648
+ position: relative;
649
+ }
650
+
651
+ .live-metric::before {
652
+ content: '';
653
+ position: absolute;
654
+ top: 0;
655
+ left: 0;
656
+ right: 0;
657
+ height: 3px;
658
+ background: var(--success-gradient);
659
+ }
660
+
661
+ .live-value {
662
+ font-size: var(--font-size-xl);
663
+ font-weight: 700;
664
+ color: var(--text-primary);
665
+ margin-bottom: 0.3rem;
666
+ }
667
+
668
+ .live-label {
669
+ font-size: var(--font-size-xs);
670
+ color: var(--text-secondary);
671
+ text-transform: uppercase;
672
+ letter-spacing: 0.5px;
673
+ }
674
+
675
+ .live-indicator {
676
+ position: absolute;
677
+ top: 0.5rem;
678
+ left: 0.5rem;
679
+ width: 8px;
680
+ height: 8px;
681
+ background: #10b981;
682
+ border-radius: 50%;
683
+ animation: pulse 2s infinite;
684
+ }
685
+
686
+ @keyframes pulse {
687
+ 0%, 100% { opacity: 1; }
688
+ 50% { opacity: 0.5; }
689
+ }
690
+
691
+ /* Toast Notifications */
692
+ .toast-container {
693
+ position: fixed;
694
+ top: 1rem;
695
+ left: 1rem;
696
+ z-index: 10001;
697
+ display: flex;
698
+ flex-direction: column;
699
+ gap: 0.5rem;
700
+ }
701
+
702
+ .toast {
703
+ background: var(--card-bg);
704
+ border-radius: var(--border-radius-sm);
705
+ padding: 1rem 1.5rem;
706
+ box-shadow: var(--shadow-lg);
707
+ border-left: 4px solid;
708
+ display: flex;
709
+ align-items: center;
710
+ gap: 0.8rem;
711
+ min-width: 300px;
712
+ transform: translateX(-100%);
713
+ transition: all 0.3s ease;
714
+ }
715
+
716
+ .toast.show {
717
+ transform: translateX(0);
718
+ }
719
+
720
+ .toast.success { border-left-color: #10b981; }
721
+ .toast.error { border-left-color: #ef4444; }
722
+ .toast.warning { border-left-color: #f59e0b; }
723
+ .toast.info { border-left-color: #3b82f6; }
724
+
725
+ .toast-icon {
726
+ font-size: 1.2rem;
727
+ }
728
+
729
+ .toast.success .toast-icon { color: #10b981; }
730
+ .toast.error .toast-icon { color: #ef4444; }
731
+ .toast.warning .toast-icon { color: #f59e0b; }
732
+ .toast.info .toast-icon { color: #3b82f6; }
733
+
734
+ .toast-content {
735
+ flex: 1;
736
+ }
737
+
738
+ .toast-title {
739
+ font-weight: 600;
740
+ font-size: var(--font-size-sm);
741
+ margin-bottom: 0.2rem;
742
+ }
743
+
744
+ .toast-message {
745
+ font-size: var(--font-size-xs);
746
+ color: var(--text-secondary);
747
+ }
748
+
749
+ .toast-close {
750
+ background: none;
751
+ border: none;
752
+ color: var(--text-secondary);
753
+ cursor: pointer;
754
+ font-size: 1rem;
755
+ transition: var(--transition-fast);
756
+ }
757
+
758
+ .toast-close:hover {
759
+ color: var(--text-primary);
760
+ }
761
+
762
+ /* واکنش‌گرایی */
763
+ @media (max-width: 992px) {
764
+ .sidebar {
765
+ transform: translateX(100%);
766
+ transition: transform 0.3s ease;
767
+ }
768
+
769
+ .sidebar.open {
770
+ transform: translateX(0);
771
+ }
772
+
773
+ .main-content {
774
+ margin-right: 0;
775
+ width: 100%;
776
+ padding: 1rem;
777
+ }
778
+
779
+ .analytics-grid {
780
+ grid-template-columns: 1fr;
781
+ }
782
+
783
+ .main-grid {
784
+ grid-template-columns: 1fr;
785
+ }
786
+
787
+ .advanced-widgets {
788
+ grid-template-columns: 1fr;
789
+ }
790
+
791
+ .filter-row {
792
+ flex-direction: column;
793
+ align-items: stretch;
794
+ }
795
+ }
796
+
797
+ @media (max-width: 768px) {
798
+ .main-content {
799
+ padding: 0.8rem;
800
+ }
801
+
802
+ .overview-stats {
803
+ grid-template-columns: repeat(2, 1fr);
804
+ }
805
+
806
+ .live-metrics {
807
+ grid-template-columns: 1fr;
808
+ }
809
+
810
+ .chart-container {
811
+ height: 250px;
812
+ }
813
+ }
814
+ </style>
815
+ </head>
816
+ <body>
817
+ <div class="dashboard-container">
818
+ <!-- سایدبار -->
819
+ <aside class="sidebar" id="sidebar">
820
+ <div class="sidebar-header">
821
+ <a href="/" class="logo">
822
+ <div class="logo-icon">
823
+ <i class="fas fa-scale-balanced"></i>
824
+ </div>
825
+ <div class="logo-text">سامانه حقوقی</div>
826
+ </a>
827
+ </div>
828
+
829
+ <nav>
830
+ <div class="nav-section">
831
+ <h6 class="nav-title">داشبورد</h6>
832
+ <ul class="nav-menu">
833
+ <li class="nav-item">
834
+ <a href="/" class="nav-link">
835
+ <i class="fas fa-chart-pie nav-icon"></i>
836
+ <span>نمای کلی</span>
837
+ </a>
838
+ </li>
839
+ </ul>
840
+ </div>
841
+
842
+ <div class="nav-section">
843
+ <h6 class="nav-title">مدیریت اسناد</h6>
844
+ <ul class="nav-menu">
845
+ <li class="nav-item">
846
+ <a href="/static/documents.html" class="nav-link">
847
+ <i class="fas fa-file-alt nav-icon"></i>
848
+ <span>مدیریت اسناد</span>
849
+ <span class="nav-badge" id="totalDocumentsBadge">6</span>
850
+ </a>
851
+ </li>
852
+
853
+ <li class="nav-item">
854
+ <a href="/static/upload.html" class="nav-link">
855
+ <i class="fas fa-cloud-upload-alt nav-icon"></i>
856
+ <span>آپلود فایل</span>
857
+ </a>
858
+ </li>
859
+
860
+ <li class="nav-item">
861
+ <a href="/static/search.html" class="nav-link">
862
+ <i class="fas fa-search nav-icon"></i>
863
+ <span>جستجو</span>
864
+ </a>
865
+ </li>
866
+ </ul>
867
+ </div>
868
+
869
+ <div class="nav-section">
870
+ <h6 class="nav-title">ابزارها</h6>
871
+ <ul class="nav-menu">
872
+ <li class="nav-item">
873
+ <a href="/static/scraping.html" class="nav-link">
874
+ <i class="fas fa-globe nav-icon"></i>
875
+ <span>استخراج محتوا</span>
876
+ </a>
877
+ </li>
878
+
879
+ <li class="nav-item">
880
+ <a href="/static/analytics.html" class="nav-link active">
881
+ <i class="fas fa-chart-line nav-icon"></i>
882
+ <span>آمار و تحلیل</span>
883
+ </a>
884
+ </li>
885
+
886
+ <li class="nav-item">
887
+ <a href="/static/reports.html" class="nav-link">
888
+ <i class="fas fa-file-export nav-icon"></i>
889
+ <span>گزارش‌ها</span>
890
+ </a>
891
+ </li>
892
+ </ul>
893
+ </div>
894
+
895
+ <div class="nav-section">
896
+ <h6 class="nav-title">تنظیمات</h6>
897
+ <ul class="nav-menu">
898
+ <li class="nav-item">
899
+ <a href="/static/settings.html" class="nav-link">
900
+ <i class="fas fa-cog nav-icon"></i>
901
+ <span>تنظیمات</span>
902
+ </a>
903
+ </li>
904
+ <li class="nav-item">
905
+ <a href="#" class="nav-link">
906
+ <i class="fas fa-sign-out-alt nav-icon"></i>
907
+ <span>خروج</span>
908
+ </a>
909
+ </li>
910
+ </ul>
911
+ </div>
912
+ </nav>
913
+ </aside>
914
+
915
+ <!-- محتوای اصلی -->
916
+ <main class="main-content">
917
+ <!-- هدر صفحه -->
918
+ <header class="page-header">
919
+ <h1 class="page-title">
920
+ <i class="fas fa-chart-line"></i>
921
+ آمار و تحلیل پیشرفته
922
+ </h1>
923
+ <div class="page-actions">
924
+ <button type="button" class="btn btn-outline" onclick="exportAnalytics()">
925
+ <i class="fas fa-download"></i>
926
+ خروجی گزارش
927
+ </button>
928
+ <button type="button" class="btn btn-primary" onclick="refreshAllAnalytics()">
929
+ <i class="fas fa-sync-alt"></i>
930
+ بروزرسانی همه
931
+ </button>
932
+ </div>
933
+ </header>
934
+
935
+ <!-- فیلترهای زمانی -->
936
+ <section class="time-filters">
937
+ <div class="filter-row">
938
+ <div class="filter-group">
939
+ <label class="filter-label">بازه زمانی</label>
940
+ <select class="filter-select" id="timeRange" onchange="updateTimeRange()">
941
+ <option value="today">امروز</option>
942
+ <option value="week" selected>هفته گذشته</option>
943
+ <option value="month">ماه گذشته</option>
944
+ <option value="quarter">سه ماه گذشته</option>
945
+ <option value="year">سال گذشته</option>
946
+ <option value="custom">بازه دلخواه</option>
947
+ </select>
948
+ </div>
949
+
950
+ <div class="filter-group">
951
+ <label class="filter-label">از تاریخ</label>
952
+ <input type="date" class="filter-input" id="startDate">
953
+ </div>
954
+
955
+ <div class="filter-group">
956
+ <label class="filter-label">تا تاریخ</label>
957
+ <input type="date" class="filter-input" id="endDate">
958
+ </div>
959
+
960
+ <div class="filter-group">
961
+ <label class="filter-label">نوع داده</label>
962
+ <select class="filter-select" id="dataType" onchange="updateDataType()">
963
+ <option value="all">همه داده‌ها</option>
964
+ <option value="documents">اسناد</option>
965
+ <option value="users">کاربران</option>
966
+ <option value="system">سیستم</option>
967
+ </select>
968
+ </div>
969
+
970
+ <button type="button" class="btn btn-primary btn-sm" onclick="applyFilters()">
971
+ <i class="fas fa-filter"></i>
972
+ اعمال فیلتر
973
+ </button>
974
+ </div>
975
+ </section>
976
+
977
+ <!-- متریک‌های زنده -->
978
+ <section class="live-metrics">
979
+ <div class="live-metric">
980
+ <div class="live-indicator"></div>
981
+ <div class="live-value" id="liveUsers">12</div>
982
+ <div class="live-label">کاربران آنلاین</div>
983
+ </div>
984
+
985
+ <div class="live-metric">
986
+ <div class="live-indicator"></div>
987
+ <div class="live-value" id="liveProcessing">3</div>
988
+ <div class="live-label">در حال پردازش</div>
989
+ </div>
990
+
991
+ <div class="live-metric">
992
+ <div class="live-indicator"></div>
993
+ <div class="live-value" id="liveRequests">847</div>
994
+ <div class="live-label">درخواست‌های امروز</div>
995
+ </div>
996
+
997
+ <div class="live-metric">
998
+ <div class="live-indicator"></div>
999
+ <div class="live-value" id="liveSuccess">98.7%</div>
1000
+ <div class="live-label">نرخ موفقیت</div>
1001
+ </div>
1002
+ </section>
1003
+
1004
+ <!-- آمار کلی -->
1005
+ <section class="overview-stats">
1006
+ <div class="stat-card primary">
1007
+ <div class="stat-value" id="totalDocumentsAnalytics">156</div>
1008
+ <div class="stat-label">کل اسناد پردازش شده</div>
1009
+ <div class="stat-change positive">
1010
+ <i class="fas fa-arrow-up"></i>
1011
+ <span>+15.2%</span>
1012
+ </div>
1013
+ </div>
1014
+
1015
+ <div class="stat-card success">
1016
+ <div class="stat-value" id="successfulProcessing">142</div>
1017
+ <div class="stat-label">پردازش موفق</div>
1018
+ <div class="stat-change positive">
1019
+ <i class="fas fa-arrow-up"></i>
1020
+ <span>+23.1%</span>
1021
+ </div>
1022
+ </div>
1023
+
1024
+ <div class="stat-card warning">
1025
+ <div class="stat-value" id="averageTime">2.4s</div>
1026
+ <div class="stat-label">میانگین زمان پردازش</div>
1027
+ <div class="stat-change negative">
1028
+ <i class="fas fa-arrow-down"></i>
1029
+ <span>-8.3%</span>
1030
+ </div>
1031
+ </div>
1032
+
1033
+ <div class="stat-card danger">
1034
+ <div class="stat-value" id="errorRate">1.3%</div>
1035
+ <div class="stat-label">نرخ خطا</div>
1036
+ <div class="stat-change positive">
1037
+ <i class="fas fa-arrow-down"></i>
1038
+ <span>-12.7%</span>
1039
+ </div>
1040
+ </div>
1041
+ </section>
1042
+
1043
+ <!-- تحلیل‌های اصلی -->
1044
+ <section class="main-analytics">
1045
+ <div class="main-grid">
1046
+ <div class="analytics-card">
1047
+ <div class="analytics-header">
1048
+ <h3 class="analytics-title">
1049
+ <i class="fas fa-chart-line"></i>
1050
+ روند پردازش اسناد
1051
+ </h3>
1052
+ <div class="chart-filters">
1053
+ <button type="button" class="chart-filter" onclick="updateTrendsChart('daily')">روزانه</button>
1054
+ <button type="button" class="chart-filter active" onclick="updateTrendsChart('weekly')">هفتگی</button>
1055
+ <button type="button" class="chart-filter" onclick="updateTrendsChart('monthly')">ماهانه</button>
1056
+ </div>
1057
+ </div>
1058
+ <div class="chart-container">
1059
+ <canvas id="trendsChart"></canvas>
1060
+ </div>
1061
+ </div>
1062
+
1063
+ <div class="analytics-card">
1064
+ <div class="analytics-header">
1065
+ <h3 class="analytics-title">
1066
+ <i class="fas fa-chart-pie"></i>
1067
+ توزیع دسته‌بندی
1068
+ </h3>
1069
+ <div class="analytics-actions">
1070
+ <button type="button" class="btn btn-outline btn-sm" onclick="refreshCategoryChart()">
1071
+ <i class="fas fa-sync-alt"></i>
1072
+ </button>
1073
+ </div>
1074
+ </div>
1075
+ <div class="chart-container">
1076
+ <canvas id="categoryChart"></canvas>
1077
+ </div>
1078
+ </div>
1079
+ </div>
1080
+ </section>
1081
+
1082
+ <!-- ویجت‌های پیشرفته -->
1083
+ <section class="advanced-widgets">
1084
+ <div class="widget">
1085
+ <div class="widget-header">
1086
+ <h3 class="widget-title">عملکرد کیفیت</h3>
1087
+ <button type="button" class="btn btn-outline btn-sm" onclick="refreshQualityWidget()">
1088
+ <i class="fas fa-sync-alt"></i>
1089
+ </button>
1090
+ </div>
1091
+ <div class="progress-widget">
1092
+ <div class="progress-item">
1093
+ <div class="progress-info">
1094
+ <div class="progress-label">دقت OCR</div>
1095
+ <div class="progress-bar">
1096
+ <div class="progress-fill success" style="width: 94%"></div>
1097
+ </div>
1098
+ </div>
1099
+ <div class="progress-value">94%</div>
1100
+ </div>
1101
+
1102
+ <div class="progress-item">
1103
+ <div class="progress-info">
1104
+ <div class="progress-label">کیفیت تصویر</div>
1105
+ <div class="progress-bar">
1106
+ <div class="progress-fill primary" style="width: 87%"></div>
1107
+ </div>
1108
+ </div>
1109
+ <div class="progress-value">87%</div>
1110
+ </div>
1111
+
1112
+ <div class="progress-item">
1113
+ <div class="progress-info">
1114
+ <div class="progress-label">تطبیق فرمت</div>
1115
+ <div class="progress-bar">
1116
+ <div class="progress-fill warning" style="width: 76%"></div>
1117
+ </div>
1118
+ </div>
1119
+ <div class="progress-value">76%</div>
1120
+ </div>
1121
+
1122
+ <div class="progress-item">
1123
+ <div class="progress-info">
1124
+ <div class="progress-label">کامل بودن محتوا</div>
1125
+ <div class="progress-bar">
1126
+ <div class="progress-fill danger" style="width: 68%"></div>
1127
+ </div>
1128
+ </div>
1129
+ <div class="progress-value">68%</div>
1130
+ </div>
1131
+ </div>
1132
+ </div>
1133
+
1134
+ <div class="widget">
1135
+ <div class="widget-header">
1136
+ <h3 class="widget-title">آمار سیستم</h3>
1137
+ <button type="button" class="btn btn-outline btn-sm" onclick="refreshSystemStats()">
1138
+ <i class="fas fa-sync-alt"></i>
1139
+ </button>
1140
+ </div>
1141
+ <div class="chart-container">
1142
+ <canvas id="systemChart"></canvas>
1143
+ </div>
1144
+ </div>
1145
+ </section>
1146
+
1147
+ <!-- گرید تحلیل‌های تفصیلی -->
1148
+ <section class="analytics-grid">
1149
+ <div class="analytics-card">
1150
+ <div class="analytics-header">
1151
+ <h3 class="analytics-title">
1152
+ <i class="fas fa-users"></i>
1153
+ فعالیت کاربران
1154
+ </h3>
1155
+ <div class="analytics-actions">
1156
+ <button type="button" class="btn btn-outline btn-sm" onclick="refreshUserActivity()">
1157
+ <i class="fas fa-sync-alt"></i>
1158
+ </button>
1159
+ </div>
1160
+ </div>
1161
+ <div class="chart-container">
1162
+ <canvas id="userActivityChart"></canvas>
1163
+ </div>
1164
+ </div>
1165
+
1166
+ <div class="analytics-card">
1167
+ <div class="analytics-header">
1168
+ <h3 class="analytics-title">
1169
+ <i class="fas fa-clock"></i>
1170
+ زمان پاسخ‌دهی
1171
+ </h3>
1172
+ <div class="analytics-actions">
1173
+ <button type="button" class="btn btn-outline btn-sm" onclick="refreshResponseTime()">
1174
+ <i class="fas fa-sync-alt"></i>
1175
+ </button>
1176
+ </div>
1177
+ </div>
1178
+ <div class="chart-container">
1179
+ <canvas id="responseTimeChart"></canvas>
1180
+ </div>
1181
+ </div>
1182
+
1183
+ <div class="analytics-card">
1184
+ <div class="analytics-header">
1185
+ <h3 class="analytics-title">
1186
+ <i class="fas fa-chart-bar"></i>
1187
+ مقایسه کارایی
1188
+ </h3>
1189
+ <div class="chart-filters">
1190
+ <button type="button" class="chart-filter active" onclick="updatePerformanceChart('hourly')">ساعتی</button>
1191
+ <button type="button" class="chart-filter" onclick="updatePerformanceChart('daily')">روزانه</button>
1192
+ </div>
1193
+ </div>
1194
+ <div class="chart-container">
1195
+ <canvas id="performanceChart"></canvas>
1196
+ </div>
1197
+ </div>
1198
+
1199
+ <div class="analytics-card">
1200
+ <div class="analytics-header">
1201
+ <h3 class="analytics-title">
1202
+ <i class="fas fa-exclamation-triangle"></i>
1203
+ تحلیل خطاها
1204
+ </h3>
1205
+ <div class="analytics-actions">
1206
+ <button type="button" class="btn btn-outline btn-sm" onclick="refreshErrorAnalysis()">
1207
+ <i class="fas fa-sync-alt"></i>
1208
+ </button>
1209
+ </div>
1210
+ </div>
1211
+ <table class="stats-table">
1212
+ <thead>
1213
+ <tr>
1214
+ <th>نوع خطا</th>
1215
+ <th>تعداد</th>
1216
+ <th>درصد</th>
1217
+ </tr>
1218
+ </thead>
1219
+ <tbody id="errorAnalysisTable">
1220
+ <tr>
1221
+ <td>خطای شبکه</td>
1222
+ <td>12</td>
1223
+ <td>45%</td>
1224
+ </tr>
1225
+ <tr>
1226
+ <td>خطای پردازش</td>
1227
+ <td>8</td>
1228
+ <td>30%</td>
1229
+ </tr>
1230
+ <tr>
1231
+ <td>خطای فرمت</td>
1232
+ <td>4</td>
1233
+ <td>15%</td>
1234
+ </tr>
1235
+ <tr>
1236
+ <td>خطای اعتبارسنجی</td>
1237
+ <td>3</td>
1238
+ <td>10%</td>
1239
+ </tr>
1240
+ </tbody>
1241
+ </table>
1242
+ </div>
1243
+ </section>
1244
+ </main>
1245
+ </div>
1246
+
1247
+ <!-- Toast Container -->
1248
+ <div class="toast-container" id="toastContainer"></div>
1249
+
1250
+ <script>
1251
+ // Global variables
1252
+ let chartsInstances = {};
1253
+ let liveMetricsInterval;
1254
+ let isOnline = false;
1255
+
1256
+ // Initialize page
1257
+ document.addEventListener('DOMContentLoaded', function() {
1258
+ console.log('📊 Analytics page loading...');
1259
+ initializeAnalyticsPage();
1260
+ });
1261
+
1262
+ async function initializeAnalyticsPage() {
1263
+ try {
1264
+ // Test backend connection
1265
+ isOnline = await testConnection();
1266
+
1267
+ // Initialize date filters
1268
+ initializeDateFilters();
1269
+
1270
+ // Initialize charts
1271
+ initializeCharts();
1272
+
1273
+ // Start live metrics updates
1274
+ startLiveMetrics();
1275
+
1276
+ showToast('داشبورد تحلیلی آماده است', 'success', 'آماده');
1277
+
1278
+ } catch (error) {
1279
+ console.error('Failed to initialize analytics page:', error);
1280
+
1281
+ // Fallback mode
1282
+ isOnline = false;
1283
+ initializeDateFilters();
1284
+ initializeCharts();
1285
+ startLiveMetrics();
1286
+
1287
+ showToast('حالت آفلاین فعال است', 'warning', 'اتصال ناموفق');
1288
+ }
1289
+ }
1290
+
1291
+ async function testConnection() {
1292
+ try {
1293
+ await window.legalAPI.healthCheck();
1294
+ return true;
1295
+ } catch (error) {
1296
+ return false;
1297
+ }
1298
+ }
1299
+
1300
+ function initializeDateFilters() {
1301
+ const endDate = new Date();
1302
+ const startDate = new Date();
1303
+ startDate.setDate(startDate.getDate() - 7); // Last week
1304
+
1305
+ document.getElementById('endDate').value = endDate.toISOString().split('T')[0];
1306
+ document.getElementById('startDate').value = startDate.toISOString().split('T')[0];
1307
+ }
1308
+
1309
+ function initializeCharts() {
1310
+ try {
1311
+ // Initialize all charts
1312
+ createTrendsChart();
1313
+ createCategoryChart();
1314
+ createSystemChart();
1315
+ createUserActivityChart();
1316
+ createResponseTimeChart();
1317
+ createPerformanceChart();
1318
+
1319
+ console.log('✅ All charts initialized');
1320
+
1321
+ } catch (error) {
1322
+ console.error('Failed to initialize charts:', error);
1323
+ showToast('خطا در بارگذاری نمودارها', 'error', 'خطا');
1324
+ }
1325
+ }
1326
+
1327
+ function createTrendsChart() {
1328
+ const ctx = document.getElementById('trendsChart');
1329
+ if (!ctx) return;
1330
+
1331
+ chartsInstances.trends = new Chart(ctx, {
1332
+ type: 'line',
1333
+ data: {
1334
+ labels: ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'],
1335
+ datasets: [
1336
+ {
1337
+ label: 'اسناد پردازش شده',
1338
+ data: [12, 19, 8, 15, 22, 18, 14],
1339
+ borderColor: '#3b82f6',
1340
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
1341
+ tension: 0.4,
1342
+ borderWidth: 3,
1343
+ pointRadius: 6,
1344
+ pointHoverRadius: 8
1345
+ },
1346
+ {
1347
+ label: 'اسناد آپلود شده',
1348
+ data: [15, 23, 12, 18, 25, 21, 16],
1349
+ borderColor: '#10b981',
1350
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
1351
+ tension: 0.4,
1352
+ borderWidth: 3,
1353
+ pointRadius: 6,
1354
+ pointHoverRadius: 8
1355
+ }
1356
+ ]
1357
+ },
1358
+ options: {
1359
+ responsive: true,
1360
+ maintainAspectRatio: false,
1361
+ plugins: {
1362
+ legend: {
1363
+ position: 'top',
1364
+ labels: {
1365
+ usePointStyle: true,
1366
+ font: { family: 'Vazirmatn', size: 12 }
1367
+ }
1368
+ }
1369
+ },
1370
+ scales: {
1371
+ y: {
1372
+ beginAtZero: true,
1373
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
1374
+ ticks: { font: { family: 'Vazirmatn' } }
1375
+ },
1376
+ x: {
1377
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
1378
+ ticks: { font: { family: 'Vazirmatn' } }
1379
+ }
1380
+ }
1381
+ }
1382
+ });
1383
+ }
1384
+
1385
+ function createCategoryChart() {
1386
+ const ctx = document.getElementById('categoryChart');
1387
+ if (!ctx) return;
1388
+
1389
+ chartsInstances.category = new Chart(ctx, {
1390
+ type: 'doughnut',
1391
+ data: {
1392
+ labels: ['قراردادها', 'دادخواست‌ها', 'احکام قضایی', 'آرای دیوان', 'سایر'],
1393
+ datasets: [{
1394
+ data: [35, 25, 20, 15, 5],
1395
+ backgroundColor: [
1396
+ '#3b82f6',
1397
+ '#10b981',
1398
+ '#f59e0b',
1399
+ '#ef4444',
1400
+ '#8b5cf6'
1401
+ ],
1402
+ borderColor: '#ffffff',
1403
+ borderWidth: 3,
1404
+ hoverBorderWidth: 5
1405
+ }]
1406
+ },
1407
+ options: {
1408
+ responsive: true,
1409
+ maintainAspectRatio: false,
1410
+ plugins: {
1411
+ legend: {
1412
+ position: 'bottom',
1413
+ labels: {
1414
+ usePointStyle: true,
1415
+ padding: 15,
1416
+ font: { family: 'Vazirmatn', size: 11 }
1417
+ }
1418
+ }
1419
+ },
1420
+ cutout: '60%'
1421
+ }
1422
+ });
1423
+ }
1424
+
1425
+ function createSystemChart() {
1426
+ const ctx = document.getElementById('systemChart');
1427
+ if (!ctx) return;
1428
+
1429
+ chartsInstances.system = new Chart(ctx, {
1430
+ type: 'bar',
1431
+ data: {
1432
+ labels: ['CPU', 'RAM', 'دیسک', 'شبکه'],
1433
+ datasets: [{
1434
+ label: 'درصد استفاده',
1435
+ data: [45, 62, 38, 28],
1436
+ backgroundColor: [
1437
+ 'rgba(59, 130, 246, 0.8)',
1438
+ 'rgba(16, 185, 129, 0.8)',
1439
+ 'rgba(245, 158, 11, 0.8)',
1440
+ 'rgba(239, 68, 68, 0.8)'
1441
+ ],
1442
+ borderColor: [
1443
+ '#3b82f6',
1444
+ '#10b981',
1445
+ '#f59e0b',
1446
+ '#ef4444'
1447
+ ],
1448
+ borderWidth: 2
1449
+ }]
1450
+ },
1451
+ options: {
1452
+ responsive: true,
1453
+ maintainAspectRatio: false,
1454
+ scales: {
1455
+ y: {
1456
+ beginAtZero: true,
1457
+ max: 100,
1458
+ ticks: {
1459
+ callback: function(value) { return value + '%'; },
1460
+ font: { family: 'Vazirmatn' }
1461
+ }
1462
+ },
1463
+ x: {
1464
+ ticks: { font: { family: 'Vazirmatn' } }
1465
+ }
1466
+ },
1467
+ plugins: {
1468
+ legend: { display: false }
1469
+ }
1470
+ }
1471
+ });
1472
+ }
1473
+
1474
+ function createUserActivityChart() {
1475
+ const ctx = document.getElementById('userActivityChart');
1476
+ if (!ctx) return;
1477
+
1478
+ chartsInstances.userActivity = new Chart(ctx, {
1479
+ type: 'radar',
1480
+ data: {
1481
+ labels: ['صبح', 'ظهر', 'بعدازظهر', 'عصر', 'شب'],
1482
+ datasets: [{
1483
+ label: 'فعالیت کاربران',
1484
+ data: [65, 89, 80, 81, 45],
1485
+ backgroundColor: 'rgba(59, 130, 246, 0.2)',
1486
+ borderColor: '#3b82f6',
1487
+ pointBackgroundColor: '#3b82f6',
1488
+ pointBorderColor: '#ffffff',
1489
+ pointHoverBackgroundColor: '#ffffff',
1490
+ pointHoverBorderColor: '#3b82f6'
1491
+ }]
1492
+ },
1493
+ options: {
1494
+ responsive: true,
1495
+ maintainAspectRatio: false,
1496
+ scales: {
1497
+ r: {
1498
+ beginAtZero: true,
1499
+ max: 100,
1500
+ ticks: { font: { family: 'Vazirmatn' } }
1501
+ }
1502
+ },
1503
+ plugins: {
1504
+ legend: { display: false }
1505
+ }
1506
+ }
1507
+ });
1508
+ }
1509
+
1510
+ function createResponseTimeChart() {
1511
+ const ctx = document.getElementById('responseTimeChart');
1512
+ if (!ctx) return;
1513
+
1514
+ chartsInstances.responseTime = new Chart(ctx, {
1515
+ type: 'line',
1516
+ data: {
1517
+ labels: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
1518
+ datasets: [{
1519
+ label: 'زمان پاسخ (ms)',
1520
+ data: [250, 180, 320, 280, 380, 220],
1521
+ borderColor: '#f59e0b',
1522
+ backgroundColor: 'rgba(245, 158, 11, 0.1)',
1523
+ tension: 0.4,
1524
+ borderWidth: 2,
1525
+ pointRadius: 4
1526
+ }]
1527
+ },
1528
+ options: {
1529
+ responsive: true,
1530
+ maintainAspectRatio: false,
1531
+ scales: {
1532
+ y: {
1533
+ beginAtZero: true,
1534
+ ticks: {
1535
+ callback: function(value) { return value + 'ms'; },
1536
+ font: { family: 'Vazirmatn' }
1537
+ }
1538
+ },
1539
+ x: {
1540
+ ticks: { font: { family: 'Vazirmatn' } }
1541
+ }
1542
+ },
1543
+ plugins: {
1544
+ legend: { display: false }
1545
+ }
1546
+ }
1547
+ });
1548
+ }
1549
+
1550
+ function createPerformanceChart() {
1551
+ const ctx = document.getElementById('performanceChart');
1552
+ if (!ctx) return;
1553
+
1554
+ chartsInstances.performance = new Chart(ctx, {
1555
+ type: 'bar',
1556
+ data: {
1557
+ labels: ['پردازش', 'آپلود', 'جستجو', 'تحلیل'],
1558
+ datasets: [
1559
+ {
1560
+ label: 'این هفته',
1561
+ data: [85, 92, 78, 88],
1562
+ backgroundColor: 'rgba(59, 130, 246, 0.8)',
1563
+ borderColor: '#3b82f6',
1564
+ borderWidth: 1
1565
+ },
1566
+ {
1567
+ label: 'هفته گذشته',
1568
+ data: [75, 85, 72, 82],
1569
+ backgroundColor: 'rgba(16, 185, 129, 0.8)',
1570
+ borderColor: '#10b981',
1571
+ borderWidth: 1
1572
+ }
1573
+ ]
1574
+ },
1575
+ options: {
1576
+ responsive: true,
1577
+ maintainAspectRatio: false,
1578
+ scales: {
1579
+ y: {
1580
+ beginAtZero: true,
1581
+ max: 100,
1582
+ ticks: {
1583
+ callback: function(value) { return value + '%'; },
1584
+ font: { family: 'Vazirmatn' }
1585
+ }
1586
+ },
1587
+ x: {
1588
+ ticks: { font: { family: 'Vazirmatn' } }
1589
+ }
1590
+ },
1591
+ plugins: {
1592
+ legend: {
1593
+ position: 'top',
1594
+ labels: { font: { family: 'Vazirmatn' } }
1595
+ }
1596
+ }
1597
+ }
1598
+ });
1599
+ }
1600
+
1601
+ // Live metrics updates
1602
+ function startLiveMetrics() {
1603
+ liveMetricsInterval = setInterval(updateLiveMetrics, 5000); // Update every 5 seconds
1604
+ updateLiveMetrics(); // Initial update
1605
+ }
1606
+
1607
+ function updateLiveMetrics() {
1608
+ // Simulate live data updates
1609
+ const liveUsers = Math.floor(Math.random() * 20) + 5;
1610
+ const liveProcessing = Math.floor(Math.random() * 8) + 1;
1611
+ const liveRequests = Math.floor(Math.random() * 100) + 800;
1612
+ const liveSuccess = (Math.random() * 5 + 95).toFixed(1);
1613
+
1614
+ document.getElementById('liveUsers').textContent = liveUsers;
1615
+ document.getElementById('liveProcessing').textContent = liveProcessing;
1616
+ document.getElementById('liveRequests').textContent = liveRequests;
1617
+ document.getElementById('liveSuccess').textContent = liveSuccess + '%';
1618
+ }
1619
+
1620
+ // Chart update functions
1621
+ function updateTrendsChart(period) {
1622
+ // Update active filter
1623
+ document.querySelectorAll('.chart-filter').forEach(btn => {
1624
+ btn.classList.remove('active');
1625
+ });
1626
+ event.target.classList.add('active');
1627
+
1628
+ // Update chart data based on period
1629
+ const data = {
1630
+ daily: {
1631
+ labels: ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'],
1632
+ processed: [12, 19, 8, 15, 22, 18, 14],
1633
+ uploaded: [15, 23, 12, 18, 25, 21, 16]
1634
+ },
1635
+ weekly: {
1636
+ labels: ['هفته 1', 'هفته 2', 'هفته 3', 'هفته 4'],
1637
+ processed: [85, 92, 78, 95],
1638
+ uploaded: [95, 105, 88, 110]
1639
+ },
1640
+ monthly: {
1641
+ labels: ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور'],
1642
+ processed: [340, 380, 290, 420, 380, 450],
1643
+ uploaded: [380, 420, 320, 460, 410, 490]
1644
+ }
1645
+ };
1646
+
1647
+ const selectedData = data[period] || data.weekly;
1648
+
1649
+ if (chartsInstances.trends) {
1650
+ chartsInstances.trends.data.labels = selectedData.labels;
1651
+ chartsInstances.trends.data.datasets[0].data = selectedData.processed;
1652
+ chartsInstances.trends.data.datasets[1].data = selectedData.uploaded;
1653
+ chartsInstances.trends.update('active');
1654
+ }
1655
+
1656
+ showToast(`نمودار به حالت ${period} تغییر کرد`, 'info', 'بروزرسانی');
1657
+ }
1658
+
1659
+ function updatePerformanceChart(type) {
1660
+ // Update active filter
1661
+ const parent = event.target.closest('.analytics-card');
1662
+ parent.querySelectorAll('.chart-filter').forEach(btn => {
1663
+ btn.classList.remove('active');
1664
+ });
1665
+ event.target.classList.add('active');
1666
+
1667
+ // Update chart based on type
1668
+ const data = {
1669
+ hourly: {
1670
+ labels: ['پردازش', 'آپلود', 'جستجو', 'تحلیل'],
1671
+ thisWeek: [85, 92, 78, 88],
1672
+ lastWeek: [75, 85, 72, 82]
1673
+ },
1674
+ daily: {
1675
+ labels: ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه'],
1676
+ thisWeek: [320, 450, 280, 380],
1677
+ lastWeek: [280, 420, 250, 350]
1678
+ }
1679
+ };
1680
+
1681
+ const selectedData = data[type] || data.hourly;
1682
+
1683
+ if (chartsInstances.performance) {
1684
+ chartsInstances.performance.data.labels = selectedData.labels;
1685
+ chartsInstances.performance.data.datasets[0].data = selectedData.thisWeek;
1686
+ chartsInstances.performance.data.datasets[1].data = selectedData.lastWeek;
1687
+ chartsInstances.performance.update('active');
1688
+ }
1689
+ }
1690
+
1691
+ // Filter functions
1692
+ function updateTimeRange() {
1693
+ const timeRange = document.getElementById('timeRange').value;
1694
+ const startDate = document.getElementById('startDate');
1695
+ const endDate = document.getElementById('endDate');
1696
+
1697
+ const end = new Date();
1698
+ let start = new Date();
1699
+
1700
+ switch (timeRange) {
1701
+ case 'today':
1702
+ start = new Date();
1703
+ break;
1704
+ case 'week':
1705
+ start.setDate(start.getDate() - 7);
1706
+ break;
1707
+ case 'month':
1708
+ start.setMonth(start.getMonth() - 1);
1709
+ break;
1710
+ case 'quarter':
1711
+ start.setMonth(start.getMonth() - 3);
1712
+ break;
1713
+ case 'year':
1714
+ start.setFullYear(start.getFullYear() - 1);
1715
+ break;
1716
+ case 'custom':
1717
+ return; // Don't update dates for custom range
1718
+ }
1719
+
1720
+ startDate.value = start.toISOString().split('T')[0];
1721
+ endDate.value = end.toISOString().split('T')[0];
1722
+
1723
+ applyFilters();
1724
+ }
1725
+
1726
+ function updateDataType() {
1727
+ applyFilters();
1728
+ }
1729
+
1730
+ function applyFilters() {
1731
+ const timeRange = document.getElementById('timeRange').value;
1732
+ const dataType = document.getElementById('dataType').value;
1733
+ const startDate = document.getElementById('startDate').value;
1734
+ const endDate = document.getElementById('endDate').value;
1735
+
1736
+ showToast('فیلترها اعمال شدند و داده‌ها بروزرسانی شدند', 'success', 'فیلترها');
1737
+
1738
+ // Here you would typically reload data based on filters
1739
+ refreshAllAnalytics();
1740
+ }
1741
+
1742
+ // Refresh functions
1743
+ function refreshAllAnalytics() {
1744
+ showToast('در حال بروزرسانی همه تحلیل‌ها...', 'info', 'بروزرسانی');
1745
+
1746
+ // Simulate data refresh
1747
+ setTimeout(() => {
1748
+ updateLiveMetrics();
1749
+
1750
+ // Update some chart data randomly
1751
+ if (chartsInstances.trends) {
1752
+ const newData = chartsInstances.trends.data.datasets[0].data.map(() =>
1753
+ Math.floor(Math.random() * 30) + 10
1754
+ );
1755
+ chartsInstances.trends.data.datasets[0].data = newData;
1756
+ chartsInstances.trends.update('active');
1757
+ }
1758
+
1759
+ showToast('همه تحلیل‌ها بروزرسانی شدند', 'success', 'بروزرسانی موفق');
1760
+ }, 2000);
1761
+ }
1762
+
1763
+ function refreshCategoryChart() {
1764
+ if (chartsInstances.category) {
1765
+ const newData = [
1766
+ Math.floor(Math.random() * 40) + 20,
1767
+ Math.floor(Math.random() * 30) + 15,
1768
+ Math.floor(Math.random() * 25) + 10,
1769
+ Math.floor(Math.random() * 20) + 5,
1770
+ Math.floor(Math.random() * 15) + 5
1771
+ ];
1772
+ chartsInstances.category.data.datasets[0].data = newData;
1773
+ chartsInstances.category.update('active');
1774
+ }
1775
+ showToast('نمودار دسته‌بندی بروزرسانی شد', 'success', 'بروزرسانی');
1776
+ }
1777
+
1778
+ function refreshQualityWidget() {
1779
+ // Update progress bars with random values
1780
+ const progressBars = document.querySelectorAll('.progress-fill');
1781
+ const progressValues = document.querySelectorAll('.progress-value');
1782
+
1783
+ progressBars.forEach((bar, index) => {
1784
+ const newValue = Math.floor(Math.random() * 30) + 60;
1785
+ bar.style.width = newValue + '%';
1786
+ progressValues[index].textContent = newValue + '%';
1787
+ });
1788
+
1789
+ showToast('ویجت کیفیت بروزرسانی شد', 'success', 'بروزرسانی');
1790
+ }
1791
+
1792
+ function refreshSystemStats() {
1793
+ if (chartsInstances.system) {
1794
+ const newData = [
1795
+ Math.floor(Math.random() * 40) + 30,
1796
+ Math.floor(Math.random() * 50) + 40,
1797
+ Math.floor(Math.random() * 30) + 20,
1798
+ Math.floor(Math.random() * 40) + 15
1799
+ ];
1800
+ chartsInstances.system.data.datasets[0].data = newData;
1801
+ chartsInstances.system.update('active');
1802
+ }
1803
+ showToast('آمار سیستم بروزرسانی شد', 'success', 'بروزرسانی');
1804
+ }
1805
+
1806
+ function refreshUserActivity() {
1807
+ if (chartsInstances.userActivity) {
1808
+ const newData = Array.from({length: 5}, () => Math.floor(Math.random() * 50) + 40);
1809
+ chartsInstances.userActivity.data.datasets[0].data = newData;
1810
+ chartsInstances.userActivity.update('active');
1811
+ }
1812
+ showToast('فعالیت کاربران بروزرسانی شد', 'success', 'بروزرسانی');
1813
+ }
1814
+
1815
+ function refreshResponseTime() {
1816
+ if (chartsInstances.responseTime) {
1817
+ const newData = Array.from({length: 6}, () => Math.floor(Math.random() * 200) + 150);
1818
+ chartsInstances.responseTime.data.datasets[0].data = newData;
1819
+ chartsInstances.responseTime.update('active');
1820
+ }
1821
+ showToast('زمان پاسخ‌دهی بروزرسانی شد', 'success', 'بروزرسانی');
1822
+ }
1823
+
1824
+ function refreshErrorAnalysis() {
1825
+ // Update error analysis table with random data
1826
+ const tbody = document.getElementById('errorAnalysisTable');
1827
+ const rows = tbody.querySelectorAll('tr');
1828
+
1829
+ rows.forEach(row => {
1830
+ const cells = row.querySelectorAll('td');
1831
+ if (cells.length >= 3) {
1832
+ const newCount = Math.floor(Math.random() * 15) + 3;
1833
+ const newPercent = Math.floor(Math.random() * 40) + 10;
1834
+ cells[1].textContent = newCount;
1835
+ cells[2].textContent = newPercent + '%';
1836
+ }
1837
+ });
1838
+
1839
+ showToast('تحلیل خطاها بروزرسانی شد', 'success', 'بروزرسانی');
1840
+ }
1841
+
1842
+ function exportAnalytics() {
1843
+ showToast('گزارش آمار در حال آماده‌سازی...', 'info', 'خروجی');
1844
+
1845
+ setTimeout(() => {
1846
+ showToast('گزارش آمار آماده شد و دانلود شروع شد', 'success', 'خروجی موفق');
1847
+ }, 3000);
1848
+ }
1849
+
1850
+ // Cleanup on page unload
1851
+ window.addEventListener('beforeunload', function() {
1852
+ if (liveMetricsInterval) {
1853
+ clearInterval(liveMetricsInterval);
1854
+ }
1855
+
1856
+ // Destroy all chart instances
1857
+ Object.values(chartsInstances).forEach(chart => {
1858
+ if (chart && chart.destroy) {
1859
+ chart.destroy();
1860
+ }
1861
+ });
1862
+ });
1863
+
1864
+ function showToast(message, type = 'info', title = 'اعلان') {
1865
+ const toastContainer = document.getElementById('toastContainer');
1866
+ if (!toastContainer) return;
1867
+
1868
+ const toast = document.createElement('div');
1869
+ toast.className = `toast ${type}`;
1870
+
1871
+ const icons = {
1872
+ success: 'check-circle',
1873
+ error: 'exclamation-triangle',
1874
+ warning: 'exclamation-circle',
1875
+ info: 'info-circle'
1876
+ };
1877
+
1878
+ toast.innerHTML = `
1879
+ <div class="toast-icon">
1880
+ <i class="fas fa-${icons[type]}"></i>
1881
+ </div>
1882
+ <div class="toast-content">
1883
+ <div class="toast-title">${title}</div>
1884
+ <div class="toast-message">${message}</div>
1885
+ </div>
1886
+ <button type="button" class="toast-close" onclick="this.parentElement.remove()">
1887
+ <i class="fas fa-times"></i>
1888
+ </button>
1889
+ `;
1890
+
1891
+ toastContainer.appendChild(toast);
1892
+
1893
+ // Show toast
1894
+ setTimeout(() => toast.classList.add('show'), 100);
1895
+
1896
+ // Auto remove after 5 seconds
1897
+ setTimeout(() => {
1898
+ if (toast.parentElement) {
1899
+ toast.classList.remove('show');
1900
+ setTimeout(() => {
1901
+ if (toast.parentElement) {
1902
+ toast.remove();
1903
+ }
1904
+ }, 300);
1905
+ }
1906
+ }, 5000);
1907
+ }
1908
+
1909
+ console.log('📊 Analytics Page Ready!');
1910
+ </script>
1911
+ </body>
1912
+ </html>
app/frontend/dev/api-test.html ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>API Connection Test - Legal Dashboard</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Arial', sans-serif;
10
+ max-width: 1200px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .test-section {
16
+ background: white;
17
+ padding: 20px;
18
+ margin: 20px 0;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+ }
22
+ .success { color: #10b981; }
23
+ .error { color: #ef4444; }
24
+ .info { color: #3b82f6; }
25
+ .warning { color: #f59e0b; }
26
+ button {
27
+ background: #007bff;
28
+ color: white;
29
+ border: none;
30
+ padding: 10px 20px;
31
+ border-radius: 4px;
32
+ cursor: pointer;
33
+ margin: 5px;
34
+ }
35
+ button:hover {
36
+ background: #0056b3;
37
+ }
38
+ pre {
39
+ background: #f8f9fa;
40
+ padding: 10px;
41
+ border-radius: 4px;
42
+ overflow-x: auto;
43
+ max-height: 300px;
44
+ overflow-y: auto;
45
+ }
46
+ .endpoint-grid {
47
+ display: grid;
48
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
49
+ gap: 15px;
50
+ margin-top: 20px;
51
+ }
52
+ .endpoint-card {
53
+ border: 1px solid #ddd;
54
+ border-radius: 8px;
55
+ padding: 15px;
56
+ background: white;
57
+ }
58
+ .endpoint-card.success {
59
+ border-color: #10b981;
60
+ background: #f0fdf4;
61
+ }
62
+ .endpoint-card.error {
63
+ border-color: #ef4444;
64
+ background: #fef2f2;
65
+ }
66
+ .endpoint-card.warning {
67
+ border-color: #f59e0b;
68
+ background: #fffbeb;
69
+ }
70
+ .status-indicator {
71
+ display: inline-block;
72
+ width: 12px;
73
+ height: 12px;
74
+ border-radius: 50%;
75
+ margin-right: 8px;
76
+ }
77
+ .status-indicator.success { background: #10b981; }
78
+ .status-indicator.error { background: #ef4444; }
79
+ .status-indicator.warning { background: #f59e0b; }
80
+ .summary-stats {
81
+ display: grid;
82
+ grid-template-columns: repeat(4, 1fr);
83
+ gap: 15px;
84
+ margin-bottom: 20px;
85
+ }
86
+ .stat-card {
87
+ background: white;
88
+ padding: 15px;
89
+ border-radius: 8px;
90
+ text-align: center;
91
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
92
+ }
93
+ .stat-number {
94
+ font-size: 2rem;
95
+ font-weight: bold;
96
+ margin-bottom: 5px;
97
+ }
98
+ .stat-label {
99
+ color: #666;
100
+ font-size: 0.9rem;
101
+ }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <h1>🔧 API Connection Test - Legal Dashboard</h1>
106
+
107
+ <div class="test-section">
108
+ <h2>📊 Test Summary</h2>
109
+ <div class="summary-stats" id="summaryStats">
110
+ <div class="stat-card">
111
+ <div class="stat-number" id="totalTests">0</div>
112
+ <div class="stat-label">Total Tests</div>
113
+ </div>
114
+ <div class="stat-card">
115
+ <div class="stat-number success" id="passedTests">0</div>
116
+ <div class="stat-label">Passed</div>
117
+ </div>
118
+ <div class="stat-card">
119
+ <div class="stat-number error" id="failedTests">0</div>
120
+ <div class="stat-label">Failed</div>
121
+ </div>
122
+ <div class="stat-card">
123
+ <div class="stat-number info" id="successRate">0%</div>
124
+ <div class="stat-label">Success Rate</div>
125
+ </div>
126
+ </div>
127
+
128
+ <button type="button" onclick="runAllTests()">Run All API Tests</button>
129
+ <button type="button" onclick="testEndpointPatterns()">Test Endpoint Patterns</button>
130
+ <button type="button" onclick="clearResults()">Clear Results</button>
131
+ </div>
132
+
133
+ <div class="test-section">
134
+ <h2>🔍 Endpoint Test Results</h2>
135
+ <div class="endpoint-grid" id="endpointResults">
136
+ <!-- Results will be populated here -->
137
+ </div>
138
+ </div>
139
+
140
+ <div class="test-section">
141
+ <h2>📋 Detailed Results</h2>
142
+ <div id="detailedResults">
143
+ <p class="info">Click "Run All API Tests" to start testing...</p>
144
+ </div>
145
+ </div>
146
+
147
+ <script src="js/api-connection-test.js"></script>
148
+ <script>
149
+ let testResults = [];
150
+
151
+ async function runAllTests() {
152
+ console.log('Starting comprehensive API tests...');
153
+
154
+ // Clear previous results
155
+ document.getElementById('endpointResults').innerHTML = '';
156
+ document.getElementById('detailedResults').innerHTML = '<p class="info">Running tests...</p>';
157
+
158
+ // Run the API tests
159
+ const results = await window.apiTester.runAllTests();
160
+ testResults = results;
161
+
162
+ // Update summary
163
+ updateSummary(results);
164
+
165
+ // Display detailed results
166
+ displayDetailedResults(results);
167
+
168
+ console.log('API tests completed');
169
+ }
170
+
171
+ async function testEndpointPatterns() {
172
+ console.log('Testing endpoint patterns...');
173
+ await window.apiTester.testEndpointPatterns();
174
+ }
175
+
176
+ function clearResults() {
177
+ document.getElementById('endpointResults').innerHTML = '';
178
+ document.getElementById('detailedResults').innerHTML = '<p class="info">Results cleared</p>';
179
+ updateSummary([]);
180
+ }
181
+
182
+ function updateSummary(results) {
183
+ const total = results.length;
184
+ const passed = results.filter(r => r.success).length;
185
+ const failed = total - passed;
186
+ const successRate = total > 0 ? ((passed / total) * 100).toFixed(1) : 0;
187
+
188
+ document.getElementById('totalTests').textContent = total;
189
+ document.getElementById('passedTests').textContent = passed;
190
+ document.getElementById('failedTests').textContent = failed;
191
+ document.getElementById('successRate').textContent = successRate + '%';
192
+ }
193
+
194
+ function displayDetailedResults(results) {
195
+ const container = document.getElementById('endpointResults');
196
+ const detailedContainer = document.getElementById('detailedResults');
197
+
198
+ // Clear containers
199
+ container.innerHTML = '';
200
+ detailedContainer.innerHTML = '';
201
+
202
+ // Group results by category
203
+ const categories = {};
204
+ results.forEach(result => {
205
+ if (!categories[result.category]) {
206
+ categories[result.category] = [];
207
+ }
208
+ categories[result.category].push(result);
209
+ });
210
+
211
+ // Create endpoint cards
212
+ results.forEach(result => {
213
+ const card = document.createElement('div');
214
+ card.className = `endpoint-card ${result.success ? 'success' : 'error'}`;
215
+
216
+ const statusClass = result.success ? 'success' : 'error';
217
+ const statusText = result.success ? 'PASS' : 'FAIL';
218
+
219
+ card.innerHTML = `
220
+ <div style="display: flex; align-items: center; margin-bottom: 10px;">
221
+ <span class="status-indicator ${statusClass}"></span>
222
+ <strong>${result.name}</strong>
223
+ <span style="margin-left: auto; font-size: 0.8rem; color: #666;">
224
+ ${result.responseTime}ms
225
+ </span>
226
+ </div>
227
+ <div style="font-size: 0.9rem; color: #666;">
228
+ <div>URL: ${result.url}</div>
229
+ <div>Method: ${result.method}</div>
230
+ <div>Status: ${result.status}</div>
231
+ ${result.error ? `<div style="color: #ef4444;">Error: ${result.error}</div>` : ''}
232
+ </div>
233
+ `;
234
+
235
+ container.appendChild(card);
236
+ });
237
+
238
+ // Create detailed results
239
+ let detailedHTML = '<h3>Test Results by Category</h3>';
240
+
241
+ Object.entries(categories).forEach(([category, categoryResults]) => {
242
+ const passed = categoryResults.filter(r => r.success).length;
243
+ const total = categoryResults.length;
244
+ const rate = ((passed / total) * 100).toFixed(1);
245
+
246
+ detailedHTML += `
247
+ <div style="margin-bottom: 20px;">
248
+ <h4>${category} (${passed}/${total} - ${rate}%)</h4>
249
+ <ul>
250
+ ${categoryResults.map(result => `
251
+ <li class="${result.success ? 'success' : 'error'}">
252
+ ${result.name}: ${result.success ? 'PASS' : 'FAIL'}
253
+ (${result.responseTime}ms)
254
+ ${result.error ? ` - ${result.error}` : ''}
255
+ </li>
256
+ `).join('')}
257
+ </ul>
258
+ </div>
259
+ `;
260
+ });
261
+
262
+ detailedContainer.innerHTML = detailedHTML;
263
+ }
264
+
265
+ // Auto-run tests when page loads
266
+ window.addEventListener('load', () => {
267
+ setTimeout(() => {
268
+ console.log('Auto-running API tests...');
269
+ runAllTests();
270
+ }, 1000);
271
+ });
272
+ </script>
273
+ </body>
274
+ </html>
app/frontend/dev/comprehensive-test.html ADDED
@@ -0,0 +1,767 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Comprehensive Frontend Test - Legal Dashboard</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Arial', sans-serif;
10
+ max-inline-size: 1400px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .test-section {
16
+ background: white;
17
+ padding: 20px;
18
+ margin: 20px 0;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+ }
22
+ .success { color: #10b981; }
23
+ .error { color: #ef4444; }
24
+ .info { color: #3b82f6; }
25
+ .warning { color: #f59e0b; }
26
+ button {
27
+ background: #007bff;
28
+ color: white;
29
+ border: none;
30
+ padding: 10px 20px;
31
+ border-radius: 4px;
32
+ cursor: pointer;
33
+ margin: 5px;
34
+ }
35
+ button:hover {
36
+ background: #0056b3;
37
+ }
38
+ button:disabled {
39
+ background: #ccc;
40
+ cursor: not-allowed;
41
+ }
42
+ .page-test {
43
+ border: 1px solid #ddd;
44
+ border-radius: 8px;
45
+ padding: 15px;
46
+ margin: 10px 0;
47
+ background: white;
48
+ }
49
+ .page-test.success {
50
+ border-color: #10b981;
51
+ background: #f0fdf4;
52
+ }
53
+ .page-test.error {
54
+ border-color: #ef4444;
55
+ background: #fef2f2;
56
+ }
57
+ .page-test.testing {
58
+ border-color: #3b82f6;
59
+ background: #eff6ff;
60
+ }
61
+ .status-indicator {
62
+ display: inline-block;
63
+ inline-size: 12px;
64
+ block-size: 12px;
65
+ border-radius: 50%;
66
+ margin-inline-end: 8px;
67
+ }
68
+ .status-indicator.success { background: #10b981; }
69
+ .status-indicator.error { background: #ef4444; }
70
+ .status-indicator.warning { background: #f59e0b; }
71
+ .status-indicator.info { background: #3b82f6; }
72
+ .status-indicator.testing {
73
+ background: #3b82f6;
74
+ animation: pulse 1s infinite;
75
+ }
76
+ @keyframes pulse {
77
+ 0% { opacity: 1; }
78
+ 50% { opacity: 0.5; }
79
+ 100% { opacity: 1; }
80
+ }
81
+ .test-results {
82
+ max-block-size: 400px;
83
+ overflow-y: auto;
84
+ border: 1px solid #ddd;
85
+ border-radius: 4px;
86
+ padding: 10px;
87
+ background: #f8f9fa;
88
+ font-family: 'Courier New', monospace;
89
+ font-size: 12px;
90
+ }
91
+ .summary-stats {
92
+ display: grid;
93
+ grid-template-columns: repeat(4, 1fr);
94
+ gap: 15px;
95
+ margin-block-end: 20px;
96
+ }
97
+ .stat-card {
98
+ background: white;
99
+ padding: 15px;
100
+ border-radius: 8px;
101
+ text-align: center;
102
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
103
+ }
104
+ .stat-number {
105
+ font-size: 2rem;
106
+ font-weight: bold;
107
+ margin-block-end: 5px;
108
+ }
109
+ .stat-label {
110
+ color: #666;
111
+ font-size: 0.9rem;
112
+ }
113
+ .progress-bar {
114
+ inline-size: 100%;
115
+ block-size: 4px;
116
+ background: #e5e7eb;
117
+ border-radius: 2px;
118
+ overflow: hidden;
119
+ margin: 10px 0;
120
+ }
121
+ .progress-fill {
122
+ block-size: 100%;
123
+ background: #3b82f6;
124
+ transition: width 0.3s ease;
125
+ }
126
+
127
+ .progress-fill.initial {
128
+ inline-size: 0%;
129
+ }
130
+ </style>
131
+ </head>
132
+ <body>
133
+ <h1>🔍 Comprehensive Frontend Test - Legal Dashboard</h1>
134
+ <div class="test-section">
135
+ <h2>📊 Test Summary</h2>
136
+ <div class="summary-stats">
137
+ <div class="stat-card">
138
+ <div class="stat-number" id="totalPages">0</div>
139
+ <div class="stat-label">Total Pages</div>
140
+ </div>
141
+ <div class="stat-card">
142
+ <div class="stat-number" id="passedPages">0</div>
143
+ <div class="stat-label">Passed</div>
144
+ </div>
145
+ <div class="stat-card">
146
+ <div class="stat-number" id="failedPages">0</div>
147
+ <div class="stat-label">Failed</div>
148
+ </div>
149
+ <div class="stat-card">
150
+ <div class="stat-number" id="successRate">0%</div>
151
+ <div class="stat-label">Success Rate</div>
152
+ </div>
153
+ </div>
154
+ <div class="progress-bar">
155
+ <div class="progress-fill initial" id="progressBar"></div>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="test-section">
160
+ <h2>🎛️ Test Controls</h2>
161
+ <button type="button" onclick="runAllTests()" id="runAllBtn">Run All Tests</button>
162
+ <button type="button" onclick="testCoreSystem()">Test Core System</button>
163
+ <button type="button" onclick="testAPIConnectivity()">Test API Connectivity</button>
164
+ <button type="button" onclick="testPageIntegration()">Test Page Integration</button>
165
+ <button type="button" onclick="clearResults()">Clear Results</button>
166
+ <button type="button" onclick="exportResults()">Export Results</button>
167
+ </div>
168
+
169
+ <div class="test-section">
170
+ <h2>📄 Page Tests</h2>
171
+ <div id="pageTests">
172
+ <!-- Page tests will be generated here -->
173
+ </div>
174
+ </div>
175
+
176
+ <div class="test-section">
177
+ <h2>📋 Test Results</h2>
178
+ <div class="test-results" id="testResults">
179
+ <!-- Test results will be displayed here -->
180
+ </div>
181
+ </div>
182
+
183
+ <script src="../js/api-client.js"></script>
184
+ <script src="../js/core.js"></script>
185
+ <script src="../js/notifications.js"></script>
186
+ <script>
187
+ class ComprehensiveTester {
188
+ constructor() {
189
+ this.baseURL = window.location.origin;
190
+ this.results = [];
191
+ this.testStats = {
192
+ total: 0,
193
+ passed: 0,
194
+ failed: 0,
195
+ successRate: 0
196
+ };
197
+ this.isRunning = false;
198
+
199
+ this.pages = [
200
+ {
201
+ name: 'Main Dashboard',
202
+ url: 'improved_legal_dashboard.html',
203
+ description: 'Main dashboard with analytics and charts',
204
+ tests: ['load', 'api', 'core', 'charts']
205
+ },
206
+ {
207
+ name: 'Documents Page',
208
+ url: 'documents.html',
209
+ description: 'Document management and CRUD operations',
210
+ tests: ['load', 'api', 'core', 'crud']
211
+ },
212
+ {
213
+ name: 'Upload Page',
214
+ url: 'upload.html',
215
+ description: 'File upload and OCR processing',
216
+ tests: ['load', 'api', 'core', 'upload']
217
+ },
218
+ {
219
+ name: 'Scraping Page',
220
+ url: 'scraping.html',
221
+ description: 'Web scraping and content extraction',
222
+ tests: ['load', 'api', 'core', 'scraping']
223
+ },
224
+ {
225
+ name: 'Scraping Dashboard',
226
+ url: 'scraping_dashboard.html',
227
+ description: 'Scraping statistics and monitoring',
228
+ tests: ['load', 'api', 'core', 'stats']
229
+ },
230
+ {
231
+ name: 'Reports Page',
232
+ url: 'reports.html',
233
+ description: 'Analytics reports and insights',
234
+ tests: ['load', 'api', 'core', 'reports']
235
+ },
236
+ {
237
+ name: 'Index Page',
238
+ url: 'index.html',
239
+ description: 'Landing page and navigation',
240
+ tests: ['load', 'api', 'core', 'navigation']
241
+ }
242
+ ];
243
+
244
+ this.initialize();
245
+ }
246
+
247
+ initialize() {
248
+ this.createPageTests();
249
+ this.updateStats();
250
+ }
251
+
252
+ createPageTests() {
253
+ const container = document.getElementById('pageTests');
254
+ container.innerHTML = '';
255
+
256
+ this.pages.forEach((page, index) => {
257
+ const testDiv = document.createElement('div');
258
+ testDiv.className = 'page-test';
259
+ testDiv.id = `page-${index}`;
260
+
261
+ testDiv.innerHTML = `
262
+ <div class="status-indicator"></div>
263
+ <h3>${page.name}</h3>
264
+ <p>${page.description}</p>
265
+ <div style="font-size: 0.8rem; color: #666; margin: 5px 0;">
266
+ File: ${page.url}
267
+ </div>
268
+ <div class="tests" id="tests-${index}">
269
+ ${page.tests.map((test, testIndex) => `
270
+ <div class="test" id="test-${index}-${testIndex}">
271
+ <span class="status-indicator"></span>
272
+ ${test.charAt(0).toUpperCase() + test.slice(1)} Test
273
+ </div>
274
+ `).join('')}
275
+ </div>
276
+ <button type="button" onclick="tester.testSinglePage(${index})" class="test-page-btn">
277
+ Test Page
278
+ </button>
279
+ `;
280
+
281
+ container.appendChild(testDiv);
282
+ });
283
+ }
284
+
285
+ async testSinglePage(pageIndex) {
286
+ const page = this.pages[pageIndex];
287
+ const testDiv = document.getElementById(`page-${pageIndex}`);
288
+
289
+ // Set testing state
290
+ testDiv.className = 'page-test testing';
291
+ testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
292
+ testDiv.querySelector('.test-page-btn').disabled = true;
293
+
294
+ this.logResult({
295
+ page: page.name,
296
+ status: 'started',
297
+ message: `Starting tests for ${page.name}`
298
+ });
299
+
300
+ let allTestsPassed = true;
301
+
302
+ for (let testIndex = 0; testIndex < page.tests.length; testIndex++) {
303
+ const test = page.tests[testIndex];
304
+ const testDiv = document.getElementById(`test-${pageIndex}-${testIndex}`);
305
+
306
+ // Set test testing state
307
+ testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
308
+
309
+ try {
310
+ const result = await this.executeTest(test, page);
311
+
312
+ if (result.success) {
313
+ testDiv.querySelector('.status-indicator').className = 'status-indicator success';
314
+ this.logResult({
315
+ page: page.name,
316
+ test: test,
317
+ status: 'success',
318
+ message: `${test} test passed for ${page.name}`
319
+ });
320
+ } else {
321
+ testDiv.querySelector('.status-indicator').className = 'status-indicator error';
322
+ allTestsPassed = false;
323
+ this.logResult({
324
+ page: page.name,
325
+ test: test,
326
+ status: 'error',
327
+ message: `${test} test failed for ${page.name}: ${result.error}`
328
+ });
329
+ }
330
+ } catch (error) {
331
+ testDiv.querySelector('.status-indicator').className = 'status-indicator error';
332
+ allTestsPassed = false;
333
+ this.logResult({
334
+ page: page.name,
335
+ test: test,
336
+ status: 'error',
337
+ message: `${test} test failed for ${page.name}: ${error.message}`
338
+ });
339
+ }
340
+
341
+ await this.delay(200); // Small delay between tests
342
+ }
343
+
344
+ // Update page status
345
+ testDiv.className = `page-test ${allTestsPassed ? 'success' : 'error'}`;
346
+ testDiv.querySelector('.status-indicator').className = `status-indicator ${allTestsPassed ? 'success' : 'error'}`;
347
+ testDiv.querySelector('.test-page-btn').disabled = false;
348
+
349
+ this.logResult({
350
+ page: page.name,
351
+ status: allTestsPassed ? 'completed' : 'failed',
352
+ message: `${page.name} ${allTestsPassed ? 'completed successfully' : 'failed'}`
353
+ });
354
+
355
+ this.updateStats();
356
+ }
357
+
358
+ async executeTest(test, page) {
359
+ switch (test) {
360
+ case 'load':
361
+ return await this.testPageLoad(page);
362
+ case 'api':
363
+ return await this.testAPIConnectivity(page);
364
+ case 'core':
365
+ return await this.testCoreIntegration(page);
366
+ case 'charts':
367
+ return await this.testChartsFunctionality(page);
368
+ case 'crud':
369
+ return await this.testCRUDOperations(page);
370
+ case 'upload':
371
+ return await this.testUploadFunctionality(page);
372
+ case 'scraping':
373
+ return await this.testScrapingFunctionality(page);
374
+ case 'stats':
375
+ return await this.testStatisticsFunctionality(page);
376
+ case 'reports':
377
+ return await this.testReportsFunctionality(page);
378
+ case 'navigation':
379
+ return await this.testNavigationFunctionality(page);
380
+ default:
381
+ return { success: false, error: 'Unknown test' };
382
+ }
383
+ }
384
+
385
+ async testPageLoad(page) {
386
+ try {
387
+ const response = await fetch(`${this.baseURL}/${page.url}`);
388
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
389
+ } catch (error) {
390
+ return { success: false, error: error.message };
391
+ }
392
+ }
393
+
394
+ async testAPIConnectivity(page) {
395
+ try {
396
+ const response = await fetch(`${this.baseURL}/api/health`);
397
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
398
+ } catch (error) {
399
+ return { success: false, error: error.message };
400
+ }
401
+ }
402
+
403
+ async testCoreIntegration(page) {
404
+ try {
405
+ // Check if core.js is loaded
406
+ if (typeof dashboardCore === 'undefined') {
407
+ return { success: false, error: 'Core module not loaded' };
408
+ }
409
+
410
+ // Check if core is initialized
411
+ if (!dashboardCore.isInitialized) {
412
+ return { success: false, error: 'Core module not initialized' };
413
+ }
414
+
415
+ return { success: true, error: null };
416
+ } catch (error) {
417
+ return { success: false, error: error.message };
418
+ }
419
+ }
420
+
421
+ async testChartsFunctionality(page) {
422
+ try {
423
+ // Check if Chart.js is available
424
+ if (typeof Chart === 'undefined') {
425
+ return { success: false, error: 'Chart.js not loaded' };
426
+ }
427
+
428
+ return { success: true, error: null };
429
+ } catch (error) {
430
+ return { success: false, error: error.message };
431
+ }
432
+ }
433
+
434
+ async testCRUDOperations(page) {
435
+ try {
436
+ const response = await fetch(`${this.baseURL}/api/documents`);
437
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
438
+ } catch (error) {
439
+ return { success: false, error: error.message };
440
+ }
441
+ }
442
+
443
+ async testUploadFunctionality(page) {
444
+ try {
445
+ const response = await fetch(`${this.baseURL}/api/ocr/status`);
446
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
447
+ } catch (error) {
448
+ return { success: false, error: error.message };
449
+ }
450
+ }
451
+
452
+ async testScrapingFunctionality(page) {
453
+ try {
454
+ const response = await fetch(`${this.baseURL}/api/scraping/health`);
455
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
456
+ } catch (error) {
457
+ return { success: false, error: error.message };
458
+ }
459
+ }
460
+
461
+ async testStatisticsFunctionality(page) {
462
+ try {
463
+ const response = await fetch(`${this.baseURL}/api/scraping/scrape/statistics`);
464
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
465
+ } catch (error) {
466
+ return { success: false, error: error.message };
467
+ }
468
+ }
469
+
470
+ async testReportsFunctionality(page) {
471
+ try {
472
+ const response = await fetch(`${this.baseURL}/api/analytics/overview`);
473
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
474
+ } catch (error) {
475
+ return { success: false, error: error.message };
476
+ }
477
+ }
478
+
479
+ async testNavigationFunctionality(page) {
480
+ try {
481
+ // Check if navigation elements exist
482
+ const response = await fetch(`${this.baseURL}/${page.url}`);
483
+ const html = await response.text();
484
+
485
+ // Check for navigation elements
486
+ const hasNavigation = html.includes('nav') || html.includes('sidebar') || html.includes('menu');
487
+
488
+ return { success: hasNavigation, error: hasNavigation ? null : 'No navigation found' };
489
+ } catch (error) {
490
+ return { success: false, error: error.message };
491
+ }
492
+ }
493
+
494
+ async runAllTests() {
495
+ if (this.isRunning) return;
496
+
497
+ this.isRunning = true;
498
+ document.getElementById('runAllBtn').disabled = true;
499
+ document.getElementById('runAllBtn').textContent = 'Running...';
500
+
501
+ this.clearResults();
502
+
503
+ for (let i = 0; i < this.pages.length; i++) {
504
+ await this.testSinglePage(i);
505
+ await this.delay(500); // Delay between pages
506
+ }
507
+
508
+ this.isRunning = false;
509
+ document.getElementById('runAllBtn').disabled = false;
510
+ document.getElementById('runAllBtn').textContent = 'Run All Tests';
511
+ }
512
+
513
+ async testCoreSystem() {
514
+ this.logResult({
515
+ test: 'Core System',
516
+ status: 'started',
517
+ message: 'Testing core system integration'
518
+ });
519
+
520
+ try {
521
+ // Test core module loading
522
+ if (typeof dashboardCore === 'undefined') {
523
+ throw new Error('Core module not loaded');
524
+ }
525
+
526
+ // Test core initialization
527
+ if (!dashboardCore.isInitialized) {
528
+ throw new Error('Core module not initialized');
529
+ }
530
+
531
+ // Test API client
532
+ if (!dashboardCore.apiClient) {
533
+ throw new Error('API client not available');
534
+ }
535
+
536
+ this.logResult({
537
+ test: 'Core System',
538
+ status: 'success',
539
+ message: 'Core system integration working correctly'
540
+ });
541
+
542
+ } catch (error) {
543
+ this.logResult({
544
+ test: 'Core System',
545
+ status: 'error',
546
+ message: `Core system test failed: ${error.message}`
547
+ });
548
+ }
549
+
550
+ this.updateStats();
551
+ }
552
+
553
+ async testAPIConnectivity() {
554
+ this.logResult({
555
+ test: 'API Connectivity',
556
+ status: 'started',
557
+ message: 'Testing API connectivity'
558
+ });
559
+
560
+ const endpoints = [
561
+ '/api/health',
562
+ '/api/dashboard/summary',
563
+ '/api/documents',
564
+ '/api/ocr/status',
565
+ '/api/scraping/health',
566
+ '/api/analytics/overview'
567
+ ];
568
+
569
+ let successCount = 0;
570
+ let totalCount = endpoints.length;
571
+
572
+ for (const endpoint of endpoints) {
573
+ try {
574
+ const response = await fetch(`${this.baseURL}${endpoint}`);
575
+ if (response.ok) {
576
+ successCount++;
577
+ this.logResult({
578
+ test: 'API Connectivity',
579
+ endpoint: endpoint,
580
+ status: 'success',
581
+ message: `${endpoint} - OK`
582
+ });
583
+ } else {
584
+ this.logResult({
585
+ test: 'API Connectivity',
586
+ endpoint: endpoint,
587
+ status: 'error',
588
+ message: `${endpoint} - HTTP ${response.status}`
589
+ });
590
+ }
591
+ } catch (error) {
592
+ this.logResult({
593
+ test: 'API Connectivity',
594
+ endpoint: endpoint,
595
+ status: 'error',
596
+ message: `${endpoint} - ${error.message}`
597
+ });
598
+ }
599
+ }
600
+
601
+ const successRate = Math.round((successCount / totalCount) * 100);
602
+ this.logResult({
603
+ test: 'API Connectivity',
604
+ status: 'completed',
605
+ message: `API connectivity test completed: ${successCount}/${totalCount} endpoints working (${successRate}%)`
606
+ });
607
+
608
+ this.updateStats();
609
+ }
610
+
611
+ async testPageIntegration() {
612
+ this.logResult({
613
+ test: 'Page Integration',
614
+ status: 'started',
615
+ message: 'Testing page integration with core system'
616
+ });
617
+
618
+ try {
619
+ // Test if pages can communicate with core
620
+ if (typeof dashboardCore !== 'undefined') {
621
+ // Test event broadcasting
622
+ dashboardCore.broadcast('testIntegration', { test: true });
623
+
624
+ // Test event listening
625
+ let eventReceived = false;
626
+ const unsubscribe = dashboardCore.listen('testIntegration', (data) => {
627
+ eventReceived = true;
628
+ });
629
+
630
+ // Broadcast again to trigger the listener
631
+ dashboardCore.broadcast('testIntegration', { test: true });
632
+
633
+ // Clean up
634
+ if (unsubscribe) unsubscribe();
635
+
636
+ this.logResult({
637
+ test: 'Page Integration',
638
+ status: 'success',
639
+ message: 'Page integration with core system working correctly'
640
+ });
641
+ } else {
642
+ throw new Error('Core system not available');
643
+ }
644
+
645
+ } catch (error) {
646
+ this.logResult({
647
+ test: 'Page Integration',
648
+ status: 'error',
649
+ message: `Page integration test failed: ${error.message}`
650
+ });
651
+ }
652
+
653
+ this.updateStats();
654
+ }
655
+
656
+ logResult(result) {
657
+ this.results.push({
658
+ ...result,
659
+ timestamp: new Date().toISOString()
660
+ });
661
+
662
+ const resultsDiv = document.getElementById('testResults');
663
+ const resultEntry = document.createElement('div');
664
+ resultEntry.className = `test-result ${result.status === 'success' || result.status === 'completed' ? 'success' : 'error'}`;
665
+ resultEntry.innerHTML = `
666
+ <strong>${result.page || result.test}</strong>${result.test && result.page ? ` - ${result.test}` : ''} -
667
+ ${result.status.toUpperCase()} -
668
+ ${result.message}
669
+ <br><small>${new Date().toLocaleTimeString()}</small>
670
+ `;
671
+
672
+ resultsDiv.appendChild(resultEntry);
673
+ resultsDiv.scrollTop = resultsDiv.scrollHeight;
674
+ }
675
+
676
+ updateStats() {
677
+ const total = this.results.length;
678
+ const passed = this.results.filter(r =>
679
+ r.status === 'success' || r.status === 'completed'
680
+ ).length;
681
+ const failed = total - passed;
682
+ const successRate = total > 0 ? Math.round((passed / total) * 100) : 0;
683
+
684
+ this.testStats = { total, passed, failed, successRate };
685
+
686
+ document.getElementById('totalPages').textContent = total;
687
+ document.getElementById('passedPages').textContent = passed;
688
+ document.getElementById('failedPages').textContent = failed;
689
+ document.getElementById('successRate').textContent = successRate + '%';
690
+
691
+ const progressBar = document.getElementById('progressBar');
692
+ progressBar.style.width = successRate + '%';
693
+ progressBar.style.background = successRate >= 80 ? '#10b981' : successRate >= 60 ? '#f59e0b' : '#ef4444';
694
+ }
695
+
696
+ clearResults() {
697
+ this.results = [];
698
+ document.getElementById('testResults').innerHTML = '';
699
+ this.updateStats();
700
+
701
+ // Reset all page tests
702
+ this.pages.forEach((page, index) => {
703
+ const testDiv = document.getElementById(`page-${index}`);
704
+ testDiv.className = 'page-test';
705
+ testDiv.querySelector('.status-indicator').className = 'status-indicator';
706
+ testDiv.querySelector('.test-page-btn').disabled = false;
707
+
708
+ page.tests.forEach((test, testIndex) => {
709
+ const testDiv = document.getElementById(`test-${index}-${testIndex}`);
710
+ testDiv.querySelector('.status-indicator').className = 'status-indicator';
711
+ });
712
+ });
713
+ }
714
+
715
+ exportResults() {
716
+ const data = {
717
+ timestamp: new Date().toISOString(),
718
+ stats: this.testStats,
719
+ results: this.results
720
+ };
721
+
722
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
723
+ const url = URL.createObjectURL(blob);
724
+ const a = document.createElement('a');
725
+ a.href = url;
726
+ a.download = `comprehensive-test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
727
+ a.click();
728
+ URL.revokeObjectURL(url);
729
+ }
730
+
731
+ delay(ms) {
732
+ return new Promise(resolve => setTimeout(resolve, ms));
733
+ }
734
+ }
735
+
736
+ // Global tester instance
737
+ const tester = new ComprehensiveTester();
738
+
739
+ // Global functions for button clicks
740
+ function runAllTests() {
741
+ tester.runAllTests();
742
+ }
743
+
744
+ function testCoreSystem() {
745
+ tester.testCoreSystem();
746
+ }
747
+
748
+ function testAPIConnectivity() {
749
+ tester.testAPIConnectivity();
750
+ }
751
+
752
+ function testPageIntegration() {
753
+ tester.testPageIntegration();
754
+ }
755
+
756
+ function clearResults() {
757
+ tester.clearResults();
758
+ }
759
+
760
+ function exportResults() {
761
+ tester.exportResults();
762
+ }
763
+
764
+ console.log('🔍 Comprehensive Tester initialized');
765
+ </script>
766
+ </body>
767
+ </html>
app/frontend/dev/functional-test.html ADDED
@@ -0,0 +1,885 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Functional Testing - Legal Dashboard</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Arial', sans-serif;
10
+ max-width: 1400px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .test-section {
16
+ background: white;
17
+ padding: 20px;
18
+ margin: 20px 0;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+ }
22
+ .success { color: #10b981; }
23
+ .error { color: #ef4444; }
24
+ .info { color: #3b82f6; }
25
+ .warning { color: #f59e0b; }
26
+ button {
27
+ background: #007bff;
28
+ color: white;
29
+ border: none;
30
+ padding: 10px 20px;
31
+ border-radius: 4px;
32
+ cursor: pointer;
33
+ margin: 5px;
34
+ font-size: 14px;
35
+ }
36
+ button:hover {
37
+ background: #0056b3;
38
+ }
39
+ button:disabled {
40
+ background: #ccc;
41
+ cursor: not-allowed;
42
+ }
43
+ .workflow-test {
44
+ border: 1px solid #ddd;
45
+ border-radius: 8px;
46
+ padding: 15px;
47
+ margin: 10px 0;
48
+ background: white;
49
+ }
50
+ .workflow-test.success {
51
+ border-color: #10b981;
52
+ background: #f0fdf4;
53
+ }
54
+ .workflow-test.error {
55
+ border-color: #ef4444;
56
+ background: #fef2f2;
57
+ }
58
+ .workflow-test.testing {
59
+ border-color: #3b82f6;
60
+ background: #eff6ff;
61
+ }
62
+ .test-results {
63
+ max-height: 400px;
64
+ overflow-y: auto;
65
+ border: 1px solid #ddd;
66
+ border-radius: 4px;
67
+ padding: 10px;
68
+ background: #f8f9fa;
69
+ font-family: 'Courier New', monospace;
70
+ font-size: 12px;
71
+ }
72
+ .progress-bar {
73
+ width: 100%;
74
+ height: 6px;
75
+ background: #e5e7eb;
76
+ border-radius: 3px;
77
+ overflow: hidden;
78
+ margin: 10px 0;
79
+ }
80
+ .progress-fill {
81
+ height: 100%;
82
+ background: #3b82f6;
83
+ transition: width 0.3s ease;
84
+ }
85
+ .file-upload-area {
86
+ border: 2px dashed #ddd;
87
+ padding: 30px;
88
+ text-align: center;
89
+ border-radius: 8px;
90
+ margin: 20px 0;
91
+ background: #fafafa;
92
+ }
93
+ .file-upload-area.dragover {
94
+ border-color: #3b82f6;
95
+ background: #eff6ff;
96
+ }
97
+ .status-indicator {
98
+ display: inline-block;
99
+ width: 12px;
100
+ height: 12px;
101
+ border-radius: 50%;
102
+ margin-right: 8px;
103
+ }
104
+ .status-indicator.success { background: #10b981; }
105
+ .status-indicator.error { background: #ef4444; }
106
+ .status-indicator.warning { background: #f59e0b; }
107
+ .status-indicator.info { background: #3b82f6; }
108
+ .status-indicator.testing {
109
+ background: #3b82f6;
110
+ animation: pulse 1s infinite;
111
+ }
112
+ @keyframes pulse {
113
+ 0% { opacity: 1; }
114
+ 50% { opacity: 0.5; }
115
+ 100% { opacity: 1; }
116
+ }
117
+ .summary-stats {
118
+ display: grid;
119
+ grid-template-columns: repeat(4, 1fr);
120
+ gap: 15px;
121
+ margin-bottom: 20px;
122
+ }
123
+ .stat-card {
124
+ background: white;
125
+ padding: 15px;
126
+ border-radius: 8px;
127
+ text-align: center;
128
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
129
+ }
130
+ .stat-number {
131
+ font-size: 2rem;
132
+ font-weight: bold;
133
+ margin-bottom: 5px;
134
+ }
135
+ .stat-label {
136
+ color: #666;
137
+ font-size: 0.9rem;
138
+ }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <h1>🔧 Functional Testing - Legal Dashboard</h1>
143
+
144
+ <div class="test-section">
145
+ <h2>📊 Test Summary</h2>
146
+ <div class="summary-stats">
147
+ <div class="stat-card">
148
+ <div class="stat-number" id="totalWorkflows">0</div>
149
+ <div class="stat-label">Total Workflows</div>
150
+ </div>
151
+ <div class="stat-card">
152
+ <div class="stat-number" id="passedWorkflows">0</div>
153
+ <div class="stat-label">Passed</div>
154
+ </div>
155
+ <div class="stat-card">
156
+ <div class="stat-number" id="failedWorkflows">0</div>
157
+ <div class="stat-label">Failed</div>
158
+ </div>
159
+ <div class="stat-card">
160
+ <div class="stat-number" id="successRate">0%</div>
161
+ <div class="stat-label">Success Rate</div>
162
+ </div>
163
+ </div>
164
+ <div class="progress-bar">
165
+ <div class="progress-fill" id="progressBar" style="width: 0%"></div>
166
+ </div>
167
+ </div>
168
+
169
+ <div class="test-section">
170
+ <h2>🎛️ Test Controls</h2>
171
+ <button type="button" onclick="runAllWorkflows()" id="runAllBtn">Run All Workflows</button>
172
+ <button type="button" onclick="testDocumentWorkflow()">Document Workflow</button>
173
+ <button type="button" onclick="testUploadWorkflow()">Upload Workflow</button>
174
+ <button type="button" onclick="testScrapingWorkflow()">Scraping Workflow</button>
175
+ <button type="button" onclick="testAnalyticsWorkflow()">Analytics Workflow</button>
176
+ <button type="button" onclick="clearResults()">Clear Results</button>
177
+ <button type="button" onclick="exportResults()">Export Results</button>
178
+ </div>
179
+
180
+ <div class="test-section">
181
+ <h2>📁 File Upload Test</h2>
182
+ <div class="file-upload-area" id="uploadZone">
183
+ <p><strong>Drag and drop a file here or click to select</strong></p>
184
+ <p>Supported formats: PDF, JPG, JPEG, PNG, TIFF</p>
185
+ <input type="file" id="testFileInput" accept=".pdf,.jpg,.jpeg,.png,.tiff" style="display: none;">
186
+ <button type="button" onclick="document.getElementById('testFileInput').click()">Select File</button>
187
+ </div>
188
+ <div id="uploadResults"></div>
189
+ </div>
190
+
191
+ <div class="test-section">
192
+ <h2>🔄 Workflow Tests</h2>
193
+ <div id="workflowTests">
194
+ <!-- Workflow tests will be generated here -->
195
+ </div>
196
+ </div>
197
+
198
+ <div class="test-section">
199
+ <h2>📋 Test Results</h2>
200
+ <div class="test-results" id="testResults">
201
+ <!-- Test results will be displayed here -->
202
+ </div>
203
+ </div>
204
+
205
+ <script src="../js/api-client.js"></script>
206
+ <script>
207
+ class FunctionalTester {
208
+ constructor() {
209
+ this.baseURL = window.location.origin;
210
+ this.results = [];
211
+ this.testStats = {
212
+ total: 0,
213
+ passed: 0,
214
+ failed: 0,
215
+ successRate: 0
216
+ };
217
+ this.isRunning = false;
218
+
219
+ this.workflows = [
220
+ {
221
+ name: 'Document Management Workflow',
222
+ description: 'Test complete document CRUD operations',
223
+ steps: [
224
+ { name: 'Get Documents List', action: 'getDocuments' },
225
+ { name: 'Create Test Document', action: 'createDocument' },
226
+ { name: 'Update Document', action: 'updateDocument' },
227
+ { name: 'Search Documents', action: 'searchDocuments' },
228
+ { name: 'Delete Test Document', action: 'deleteDocument' }
229
+ ]
230
+ },
231
+ {
232
+ name: 'File Upload & OCR Workflow',
233
+ description: 'Test file upload and OCR processing',
234
+ steps: [
235
+ { name: 'Upload Test File', action: 'uploadFile' },
236
+ { name: 'Process OCR', action: 'processOCR' },
237
+ { name: 'Get OCR Status', action: 'getOCRStatus' },
238
+ { name: 'Extract Text', action: 'extractText' }
239
+ ]
240
+ },
241
+ {
242
+ name: 'Dashboard Analytics Workflow',
243
+ description: 'Test dashboard and analytics functionality',
244
+ steps: [
245
+ { name: 'Get Dashboard Summary', action: 'getDashboardSummary' },
246
+ { name: 'Get Charts Data', action: 'getChartsData' },
247
+ { name: 'Get AI Suggestions', action: 'getAISuggestions' },
248
+ { name: 'Get Performance Metrics', action: 'getPerformanceMetrics' }
249
+ ]
250
+ },
251
+ {
252
+ name: 'Scraping & Rating Workflow',
253
+ description: 'Test web scraping and content rating',
254
+ steps: [
255
+ { name: 'Get Scraping Status', action: 'getScrapingStatus' },
256
+ { name: 'Get Scraping Statistics', action: 'getScrapingStatistics' },
257
+ { name: 'Get Rating Summary', action: 'getRatingSummary' },
258
+ { name: 'Check Scraping Health', action: 'getScrapingHealth' }
259
+ ]
260
+ },
261
+ {
262
+ name: 'Analytics & Reporting Workflow',
263
+ description: 'Test advanced analytics and reporting',
264
+ steps: [
265
+ { name: 'Get Analytics Overview', action: 'getAnalyticsOverview' },
266
+ { name: 'Get Performance Analytics', action: 'getPerformanceAnalytics' },
267
+ { name: 'Get Entity Analysis', action: 'getEntityAnalysis' },
268
+ { name: 'Get Quality Analysis', action: 'getQualityAnalysis' }
269
+ ]
270
+ }
271
+ ];
272
+
273
+ this.initialize();
274
+ }
275
+
276
+ initialize() {
277
+ this.createWorkflowTests();
278
+ this.setupFileUpload();
279
+ this.updateStats();
280
+ }
281
+
282
+ createWorkflowTests() {
283
+ const container = document.getElementById('workflowTests');
284
+ container.innerHTML = '';
285
+
286
+ this.workflows.forEach((workflow, index) => {
287
+ const testDiv = document.createElement('div');
288
+ testDiv.className = 'workflow-test';
289
+ testDiv.id = `workflow-${index}`;
290
+
291
+ testDiv.innerHTML = `
292
+ <div class="status-indicator"></div>
293
+ <h3>${workflow.name}</h3>
294
+ <p>${workflow.description}</p>
295
+ <div class="steps" id="steps-${index}">
296
+ ${workflow.steps.map((step, stepIndex) => `
297
+ <div class="step" id="step-${index}-${stepIndex}">
298
+ <span class="status-indicator"></span>
299
+ ${step.name}
300
+ </div>
301
+ `).join('')}
302
+ </div>
303
+ <button type="button" onclick="tester.runWorkflow(${index})" class="run-workflow-btn">
304
+ Run Workflow
305
+ </button>
306
+ `;
307
+
308
+ container.appendChild(testDiv);
309
+ });
310
+ }
311
+
312
+ setupFileUpload() {
313
+ const uploadZone = document.getElementById('uploadZone');
314
+ const fileInput = document.getElementById('testFileInput');
315
+
316
+ uploadZone.addEventListener('dragover', (e) => {
317
+ e.preventDefault();
318
+ uploadZone.classList.add('dragover');
319
+ });
320
+
321
+ uploadZone.addEventListener('dragleave', () => {
322
+ uploadZone.classList.remove('dragover');
323
+ });
324
+
325
+ uploadZone.addEventListener('drop', (e) => {
326
+ e.preventDefault();
327
+ uploadZone.classList.remove('dragover');
328
+ const files = e.dataTransfer.files;
329
+ if (files.length > 0) {
330
+ this.testFileUpload(files[0]);
331
+ }
332
+ });
333
+
334
+ fileInput.addEventListener('change', (e) => {
335
+ if (e.target.files.length > 0) {
336
+ this.testFileUpload(e.target.files[0]);
337
+ }
338
+ });
339
+ }
340
+
341
+ async runWorkflow(workflowIndex) {
342
+ const workflow = this.workflows[workflowIndex];
343
+ const testDiv = document.getElementById(`workflow-${workflowIndex}`);
344
+
345
+ // Set testing state
346
+ testDiv.className = 'workflow-test testing';
347
+ testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
348
+ testDiv.querySelector('.run-workflow-btn').disabled = true;
349
+
350
+ this.logResult({
351
+ workflow: workflow.name,
352
+ status: 'started',
353
+ message: `Starting ${workflow.name}`
354
+ });
355
+
356
+ let allStepsPassed = true;
357
+
358
+ for (let stepIndex = 0; stepIndex < workflow.steps.length; stepIndex++) {
359
+ const step = workflow.steps[stepIndex];
360
+ const stepDiv = document.getElementById(`step-${workflowIndex}-${stepIndex}`);
361
+
362
+ // Set step testing state
363
+ stepDiv.querySelector('.status-indicator').className = 'status-indicator testing';
364
+
365
+ try {
366
+ const result = await this.executeStep(step.action);
367
+
368
+ if (result.success) {
369
+ stepDiv.querySelector('.status-indicator').className = 'status-indicator success';
370
+ this.logResult({
371
+ workflow: workflow.name,
372
+ step: step.name,
373
+ status: 'success',
374
+ message: `${step.name} completed successfully`
375
+ });
376
+ } else {
377
+ stepDiv.querySelector('.status-indicator').className = 'status-indicator error';
378
+ allStepsPassed = false;
379
+ this.logResult({
380
+ workflow: workflow.name,
381
+ step: step.name,
382
+ status: 'error',
383
+ message: `${step.name} failed: ${result.error}`
384
+ });
385
+ }
386
+ } catch (error) {
387
+ stepDiv.querySelector('.status-indicator').className = 'status-indicator error';
388
+ allStepsPassed = false;
389
+ this.logResult({
390
+ workflow: workflow.name,
391
+ step: step.name,
392
+ status: 'error',
393
+ message: `${step.name} failed: ${error.message}`
394
+ });
395
+ }
396
+
397
+ await this.delay(200); // Small delay between steps
398
+ }
399
+
400
+ // Update workflow status
401
+ testDiv.className = `workflow-test ${allStepsPassed ? 'success' : 'error'}`;
402
+ testDiv.querySelector('.status-indicator').className = `status-indicator ${allStepsPassed ? 'success' : 'error'}`;
403
+ testDiv.querySelector('.run-workflow-btn').disabled = false;
404
+
405
+ this.logResult({
406
+ workflow: workflow.name,
407
+ status: allStepsPassed ? 'completed' : 'failed',
408
+ message: `${workflow.name} ${allStepsPassed ? 'completed successfully' : 'failed'}`
409
+ });
410
+
411
+ this.updateStats();
412
+ }
413
+
414
+ async executeStep(action) {
415
+ switch (action) {
416
+ case 'getDocuments':
417
+ return await this.testGetDocuments();
418
+ case 'createDocument':
419
+ return await this.testCreateDocument();
420
+ case 'updateDocument':
421
+ return await this.testUpdateDocument();
422
+ case 'searchDocuments':
423
+ return await this.testSearchDocuments();
424
+ case 'deleteDocument':
425
+ return await this.testDeleteDocument();
426
+ case 'uploadFile':
427
+ return await this.testUploadFile();
428
+ case 'processOCR':
429
+ return await this.testProcessOCR();
430
+ case 'getOCRStatus':
431
+ return await this.testGetOCRStatus();
432
+ case 'extractText':
433
+ return await this.testExtractText();
434
+ case 'getDashboardSummary':
435
+ return await this.testGetDashboardSummary();
436
+ case 'getChartsData':
437
+ return await this.testGetChartsData();
438
+ case 'getAISuggestions':
439
+ return await this.testGetAISuggestions();
440
+ case 'getPerformanceMetrics':
441
+ return await this.testGetPerformanceMetrics();
442
+ case 'getScrapingStatus':
443
+ return await this.testGetScrapingStatus();
444
+ case 'getScrapingStatistics':
445
+ return await this.testGetScrapingStatistics();
446
+ case 'getRatingSummary':
447
+ return await this.testGetRatingSummary();
448
+ case 'getScrapingHealth':
449
+ return await this.testGetScrapingHealth();
450
+ case 'getAnalyticsOverview':
451
+ return await this.testGetAnalyticsOverview();
452
+ case 'getPerformanceAnalytics':
453
+ return await this.testGetPerformanceAnalytics();
454
+ case 'getEntityAnalysis':
455
+ return await this.testGetEntityAnalysis();
456
+ case 'getQualityAnalysis':
457
+ return await this.testGetQualityAnalysis();
458
+ default:
459
+ return { success: false, error: 'Unknown action' };
460
+ }
461
+ }
462
+
463
+ // Individual step implementations
464
+ async testGetDocuments() {
465
+ try {
466
+ const response = await fetch(`${this.baseURL}/api/documents`);
467
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
468
+ } catch (error) {
469
+ return { success: false, error: error.message };
470
+ }
471
+ }
472
+
473
+ async testCreateDocument() {
474
+ try {
475
+ const testDoc = {
476
+ title: `Test Document ${Date.now()}`,
477
+ content: 'This is a test document for functional testing',
478
+ category: 'test',
479
+ source: 'functional_test'
480
+ };
481
+
482
+ const response = await fetch(`${this.baseURL}/api/documents`, {
483
+ method: 'POST',
484
+ headers: { 'Content-Type': 'application/json' },
485
+ body: JSON.stringify(testDoc)
486
+ });
487
+
488
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
489
+ } catch (error) {
490
+ return { success: false, error: error.message };
491
+ }
492
+ }
493
+
494
+ async testUpdateDocument() {
495
+ try {
496
+ const response = await fetch(`${this.baseURL}/api/documents/1`, {
497
+ method: 'PUT',
498
+ headers: { 'Content-Type': 'application/json' },
499
+ body: JSON.stringify({ title: 'Updated Test Document' })
500
+ });
501
+
502
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
503
+ } catch (error) {
504
+ return { success: false, error: error.message };
505
+ }
506
+ }
507
+
508
+ async testSearchDocuments() {
509
+ try {
510
+ const response = await fetch(`${this.baseURL}/api/documents/search?q=test`);
511
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
512
+ } catch (error) {
513
+ return { success: false, error: error.message };
514
+ }
515
+ }
516
+
517
+ async testDeleteDocument() {
518
+ try {
519
+ const response = await fetch(`${this.baseURL}/api/documents/1`, {
520
+ method: 'DELETE'
521
+ });
522
+
523
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
524
+ } catch (error) {
525
+ return { success: false, error: error.message };
526
+ }
527
+ }
528
+
529
+ async testUploadFile() {
530
+ try {
531
+ // Create a test file
532
+ const testContent = 'This is a test file for functional testing';
533
+ const blob = new Blob([testContent], { type: 'text/plain' });
534
+ const file = new File([blob], 'test.txt', { type: 'text/plain' });
535
+
536
+ const formData = new FormData();
537
+ formData.append('file', file);
538
+
539
+ const response = await fetch(`${this.baseURL}/api/ocr/upload`, {
540
+ method: 'POST',
541
+ body: formData
542
+ });
543
+
544
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
545
+ } catch (error) {
546
+ return { success: false, error: error.message };
547
+ }
548
+ }
549
+
550
+ async testProcessOCR() {
551
+ try {
552
+ const response = await fetch(`${this.baseURL}/api/ocr/process`, {
553
+ method: 'POST',
554
+ headers: { 'Content-Type': 'application/json' },
555
+ body: JSON.stringify({ file_id: 'test_file' })
556
+ });
557
+
558
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
559
+ } catch (error) {
560
+ return { success: false, error: error.message };
561
+ }
562
+ }
563
+
564
+ async testGetOCRStatus() {
565
+ try {
566
+ const response = await fetch(`${this.baseURL}/api/ocr/status`);
567
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
568
+ } catch (error) {
569
+ return { success: false, error: error.message };
570
+ }
571
+ }
572
+
573
+ async testExtractText() {
574
+ try {
575
+ const response = await fetch(`${this.baseURL}/api/ocr/extract`, {
576
+ method: 'POST',
577
+ headers: { 'Content-Type': 'application/json' },
578
+ body: JSON.stringify({ file_id: 'test_file' })
579
+ });
580
+
581
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
582
+ } catch (error) {
583
+ return { success: false, error: error.message };
584
+ }
585
+ }
586
+
587
+ async testGetDashboardSummary() {
588
+ try {
589
+ const response = await fetch(`${this.baseURL}/api/dashboard/summary`);
590
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
591
+ } catch (error) {
592
+ return { success: false, error: error.message };
593
+ }
594
+ }
595
+
596
+ async testGetChartsData() {
597
+ try {
598
+ const response = await fetch(`${this.baseURL}/api/dashboard/charts-data`);
599
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
600
+ } catch (error) {
601
+ return { success: false, error: error.message };
602
+ }
603
+ }
604
+
605
+ async testGetAISuggestions() {
606
+ try {
607
+ const response = await fetch(`${this.baseURL}/api/dashboard/ai-suggestions`);
608
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
609
+ } catch (error) {
610
+ return { success: false, error: error.message };
611
+ }
612
+ }
613
+
614
+ async testGetPerformanceMetrics() {
615
+ try {
616
+ const response = await fetch(`${this.baseURL}/api/dashboard/performance-metrics`);
617
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
618
+ } catch (error) {
619
+ return { success: false, error: error.message };
620
+ }
621
+ }
622
+
623
+ async testGetScrapingStatus() {
624
+ try {
625
+ const response = await fetch(`${this.baseURL}/api/scraping/scrape/status`);
626
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
627
+ } catch (error) {
628
+ return { success: false, error: error.message };
629
+ }
630
+ }
631
+
632
+ async testGetScrapingStatistics() {
633
+ try {
634
+ const response = await fetch(`${this.baseURL}/api/scraping/scrape/statistics`);
635
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
636
+ } catch (error) {
637
+ return { success: false, error: error.message };
638
+ }
639
+ }
640
+
641
+ async testGetRatingSummary() {
642
+ try {
643
+ const response = await fetch(`${this.baseURL}/api/scraping/rating/summary`);
644
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
645
+ } catch (error) {
646
+ return { success: false, error: error.message };
647
+ }
648
+ }
649
+
650
+ async testGetScrapingHealth() {
651
+ try {
652
+ const response = await fetch(`${this.baseURL}/api/scraping/health`);
653
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
654
+ } catch (error) {
655
+ return { success: false, error: error.message };
656
+ }
657
+ }
658
+
659
+ async testGetAnalyticsOverview() {
660
+ try {
661
+ const response = await fetch(`${this.baseURL}/api/analytics/overview`);
662
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
663
+ } catch (error) {
664
+ return { success: false, error: error.message };
665
+ }
666
+ }
667
+
668
+ async testGetPerformanceAnalytics() {
669
+ try {
670
+ const response = await fetch(`${this.baseURL}/api/analytics/performance`);
671
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
672
+ } catch (error) {
673
+ return { success: false, error: error.message };
674
+ }
675
+ }
676
+
677
+ async testGetEntityAnalysis() {
678
+ try {
679
+ const response = await fetch(`${this.baseURL}/api/analytics/entities`);
680
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
681
+ } catch (error) {
682
+ return { success: false, error: error.message };
683
+ }
684
+ }
685
+
686
+ async testGetQualityAnalysis() {
687
+ try {
688
+ const response = await fetch(`${this.baseURL}/api/analytics/quality-analysis`);
689
+ return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
690
+ } catch (error) {
691
+ return { success: false, error: error.message };
692
+ }
693
+ }
694
+
695
+ async testFileUpload(file) {
696
+ const resultsDiv = document.getElementById('uploadResults');
697
+ resultsDiv.innerHTML = `<p>Testing file upload: ${file.name} (${file.size} bytes)</p>`;
698
+
699
+ try {
700
+ const formData = new FormData();
701
+ formData.append('file', file);
702
+
703
+ const startTime = Date.now();
704
+ const response = await fetch(`${this.baseURL}/api/ocr/upload`, {
705
+ method: 'POST',
706
+ body: formData
707
+ });
708
+
709
+ const responseTime = Date.now() - startTime;
710
+ const responseData = await response.json();
711
+
712
+ const success = response.ok;
713
+
714
+ resultsDiv.innerHTML = `
715
+ <div class="${success ? 'success' : 'error'}">
716
+ <h4>File Upload Test Results</h4>
717
+ <p><strong>File:</strong> ${file.name}</p>
718
+ <p><strong>Size:</strong> ${file.size} bytes</p>
719
+ <p><strong>Status:</strong> ${response.status} ${response.statusText}</p>
720
+ <p><strong>Response Time:</strong> ${responseTime}ms</p>
721
+ <div class="response-data">
722
+ ${JSON.stringify(responseData, null, 2)}
723
+ </div>
724
+ </div>
725
+ `;
726
+
727
+ this.logResult({
728
+ workflow: 'File Upload',
729
+ status: success ? 'success' : 'error',
730
+ message: `File upload ${success ? 'succeeded' : 'failed'}: ${file.name}`
731
+ });
732
+
733
+ } catch (error) {
734
+ resultsDiv.innerHTML = `
735
+ <div class="error">
736
+ <h4>File Upload Test Failed</h4>
737
+ <p>Error: ${error.message}</p>
738
+ </div>
739
+ `;
740
+
741
+ this.logResult({
742
+ workflow: 'File Upload',
743
+ status: 'error',
744
+ message: `File upload failed: ${error.message}`
745
+ });
746
+ }
747
+
748
+ this.updateStats();
749
+ }
750
+
751
+ async runAllWorkflows() {
752
+ if (this.isRunning) return;
753
+
754
+ this.isRunning = true;
755
+ document.getElementById('runAllBtn').disabled = true;
756
+ document.getElementById('runAllBtn').textContent = 'Running...';
757
+
758
+ this.clearResults();
759
+
760
+ for (let i = 0; i < this.workflows.length; i++) {
761
+ await this.runWorkflow(i);
762
+ await this.delay(500); // Delay between workflows
763
+ }
764
+
765
+ this.isRunning = false;
766
+ document.getElementById('runAllBtn').disabled = false;
767
+ document.getElementById('runAllBtn').textContent = 'Run All Workflows';
768
+ }
769
+
770
+ logResult(result) {
771
+ this.results.push({
772
+ ...result,
773
+ timestamp: new Date().toISOString()
774
+ });
775
+
776
+ const resultsDiv = document.getElementById('testResults');
777
+ const resultEntry = document.createElement('div');
778
+ resultEntry.className = `test-result ${result.status === 'success' || result.status === 'completed' ? 'success' : 'error'}`;
779
+ resultEntry.innerHTML = `
780
+ <strong>${result.workflow}</strong>${result.step ? ` - ${result.step}` : ''} -
781
+ ${result.status.toUpperCase()} -
782
+ ${result.message}
783
+ <br><small>${new Date().toLocaleTimeString()}</small>
784
+ `;
785
+
786
+ resultsDiv.appendChild(resultEntry);
787
+ resultsDiv.scrollTop = resultsDiv.scrollHeight;
788
+ }
789
+
790
+ updateStats() {
791
+ const total = this.results.length;
792
+ const passed = this.results.filter(r =>
793
+ r.status === 'success' || r.status === 'completed'
794
+ ).length;
795
+ const failed = total - passed;
796
+ const successRate = total > 0 ? Math.round((passed / total) * 100) : 0;
797
+
798
+ this.testStats = { total, passed, failed, successRate };
799
+
800
+ document.getElementById('totalWorkflows').textContent = total;
801
+ document.getElementById('passedWorkflows').textContent = passed;
802
+ document.getElementById('failedWorkflows').textContent = failed;
803
+ document.getElementById('successRate').textContent = successRate + '%';
804
+
805
+ const progressBar = document.getElementById('progressBar');
806
+ progressBar.style.width = successRate + '%';
807
+ progressBar.style.background = successRate >= 80 ? '#10b981' : successRate >= 60 ? '#f59e0b' : '#ef4444';
808
+ }
809
+
810
+ clearResults() {
811
+ this.results = [];
812
+ document.getElementById('testResults').innerHTML = '';
813
+ this.updateStats();
814
+
815
+ // Reset all workflow tests
816
+ this.workflows.forEach((workflow, index) => {
817
+ const testDiv = document.getElementById(`workflow-${index}`);
818
+ testDiv.className = 'workflow-test';
819
+ testDiv.querySelector('.status-indicator').className = 'status-indicator';
820
+ testDiv.querySelector('.run-workflow-btn').disabled = false;
821
+
822
+ workflow.steps.forEach((step, stepIndex) => {
823
+ const stepDiv = document.getElementById(`step-${index}-${stepIndex}`);
824
+ stepDiv.querySelector('.status-indicator').className = 'status-indicator';
825
+ });
826
+ });
827
+ }
828
+
829
+ exportResults() {
830
+ const data = {
831
+ timestamp: new Date().toISOString(),
832
+ stats: this.testStats,
833
+ results: this.results
834
+ };
835
+
836
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
837
+ const url = URL.createObjectURL(blob);
838
+ const a = document.createElement('a');
839
+ a.href = url;
840
+ a.download = `functional-test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
841
+ a.click();
842
+ URL.revokeObjectURL(url);
843
+ }
844
+
845
+ delay(ms) {
846
+ return new Promise(resolve => setTimeout(resolve, ms));
847
+ }
848
+ }
849
+
850
+ // Global tester instance
851
+ const tester = new FunctionalTester();
852
+
853
+ // Global functions for button clicks
854
+ function runAllWorkflows() {
855
+ tester.runAllWorkflows();
856
+ }
857
+
858
+ function testDocumentWorkflow() {
859
+ tester.runWorkflow(0);
860
+ }
861
+
862
+ function testUploadWorkflow() {
863
+ tester.runWorkflow(1);
864
+ }
865
+
866
+ function testScrapingWorkflow() {
867
+ tester.runWorkflow(3);
868
+ }
869
+
870
+ function testAnalyticsWorkflow() {
871
+ tester.runWorkflow(4);
872
+ }
873
+
874
+ function clearResults() {
875
+ tester.clearResults();
876
+ }
877
+
878
+ function exportResults() {
879
+ tester.exportResults();
880
+ }
881
+
882
+ console.log('🔧 Functional Tester initialized');
883
+ </script>
884
+ </body>
885
+ </html>
app/frontend/dev/integration-test.html ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Integration Test - Legal Dashboard</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Arial', sans-serif;
10
+ max-inline-size: 1200px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .test-section {
16
+ background: white;
17
+ padding: 20px;
18
+ margin: 20px 0;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+ }
22
+ .success { color: #10b981; }
23
+ .error { color: #ef4444; }
24
+ .info { color: #3b82f6; }
25
+ .warning { color: #f59e0b; }
26
+ button {
27
+ background: #007bff;
28
+ color: white;
29
+ border: none;
30
+ padding: 10px 20px;
31
+ border-radius: 4px;
32
+ cursor: pointer;
33
+ margin: 5px;
34
+ }
35
+ button:hover {
36
+ background: #0056b3;
37
+ }
38
+ pre {
39
+ background: #f8f9fa;
40
+ padding: 10px;
41
+ border-radius: 4px;
42
+ overflow-x: auto;
43
+ max-block-size: 300px;
44
+ overflow-y: auto;
45
+ }
46
+ .event-log {
47
+ background: #1a1a1a;
48
+ color: #00ff00;
49
+ padding: 15px;
50
+ border-radius: 8px;
51
+ font-family: 'Courier New', monospace;
52
+ max-block-size: 400px;
53
+ overflow-y: auto;
54
+ }
55
+ .status-indicator {
56
+ display: inline-block;
57
+ inline-size: 12px;
58
+ block-size: 12px;
59
+ border-radius: 50%;
60
+ margin-inline-end: 8px;
61
+ }
62
+ .status-indicator.success { background: #10b981; }
63
+ .status-indicator.error { background: #ef4444; }
64
+ .status-indicator.warning { background: #f59e0b; }
65
+ .status-indicator.info { background: #3b82f6; }
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <h1>🔍 Integration Test - Legal Dashboard</h1>
70
+
71
+ <div class="test-section">
72
+ <h2>📦 Core Module Test</h2>
73
+ <button type="button" onclick="testCoreModule()">Test Core Module</button>
74
+ <div id="coreTestResult"></div>
75
+ </div>
76
+
77
+ <div class="test-section">
78
+ <h2>🔌 API Connectivity Test</h2>
79
+ <button type="button" onclick="testAPIConnectivity()">Test API Connectivity</button>
80
+ <div id="apiTestResult"></div>
81
+ </div>
82
+
83
+ <div class="test-section">
84
+ <h2>📡 Cross-Page Communication Test</h2>
85
+ <button type="button" onclick="testCrossPageCommunication()">Test Cross-Page Events</button>
86
+ <div id="communicationTestResult"></div>
87
+ </div>
88
+
89
+ <div class="test-section">
90
+ <h2>📊 Event Log</h2>
91
+ <button type="button" onclick="clearEventLog()">Clear Log</button>
92
+ <div id="eventLog" class="event-log"></div>
93
+ </div>
94
+
95
+ <div class="test-section">
96
+ <h2>🔄 Real-time Updates Test</h2>
97
+ <button type="button" onclick="simulateDocumentUpload()">Simulate Document Upload</button>
98
+ <button type="button" onclick="simulateDocumentUpdate()">Simulate Document Update</button>
99
+ <button type="button" onclick="simulateDocumentDelete()">Simulate Document Delete</button>
100
+ <div id="realtimeTestResult"></div>
101
+ </div>
102
+
103
+ <script src="../js/api-client.js"></script>
104
+ <script src="../js/core.js"></script>
105
+ <script>
106
+ let eventLog = [];
107
+
108
+ function logEvent(message, type = 'info') {
109
+ const timestamp = new Date().toLocaleTimeString();
110
+ const logEntry = `[${timestamp}] ${message}`;
111
+ eventLog.push({ message: logEntry, type });
112
+
113
+ const eventLogElement = document.getElementById('eventLog');
114
+ eventLogElement.innerHTML = eventLog.map(entry =>
115
+ `<div class="${entry.type}">${entry.message}</div>`
116
+ ).join('');
117
+
118
+ eventLogElement.scrollTop = eventLogElement.scrollHeight;
119
+ }
120
+
121
+ function clearEventLog() {
122
+ eventLog = [];
123
+ document.getElementById('eventLog').innerHTML = '';
124
+ }
125
+
126
+ async function testCoreModule() {
127
+ const resultDiv = document.getElementById('coreTestResult');
128
+ resultDiv.innerHTML = '<p>Testing core module...</p>';
129
+
130
+ try {
131
+ // Test if core module is loaded
132
+ if (typeof dashboardCore === 'undefined') {
133
+ throw new Error('Dashboard Core module not loaded');
134
+ }
135
+
136
+ // Test initialization
137
+ if (!dashboardCore.isInitialized) {
138
+ throw new Error('Dashboard Core not initialized');
139
+ }
140
+
141
+ // Test API client
142
+ if (!dashboardCore.apiClient) {
143
+ throw new Error('API client not initialized');
144
+ }
145
+
146
+ // Test event system
147
+ let eventReceived = false;
148
+ const unsubscribe = dashboardCore.listen('testEvent', (data) => {
149
+ eventReceived = true;
150
+ logEvent('✅ Test event received: ' + JSON.stringify(data), 'success');
151
+ });
152
+
153
+ dashboardCore.broadcast('testEvent', { test: true, timestamp: Date.now() });
154
+
155
+ setTimeout(() => {
156
+ unsubscribe();
157
+ if (eventReceived) {
158
+ resultDiv.innerHTML = `
159
+ <div class="success">
160
+ <span class="status-indicator success"></span>
161
+ ✅ Core module working correctly
162
+ <ul>
163
+ <li>Module loaded: ✅</li>
164
+ <li>Initialized: ✅</li>
165
+ <li>API client: ✅</li>
166
+ <li>Event system: ✅</li>
167
+ </ul>
168
+ </div>
169
+ `;
170
+ } else {
171
+ throw new Error('Event system not working');
172
+ }
173
+ }, 100);
174
+
175
+ } catch (error) {
176
+ resultDiv.innerHTML = `
177
+ <div class="error">
178
+ <span class="status-indicator error"></span>
179
+ ❌ Core module test failed: ${error.message}
180
+ </div>
181
+ `;
182
+ logEvent('❌ Core module test failed: ' + error.message, 'error');
183
+ }
184
+ }
185
+
186
+ async function testAPIConnectivity() {
187
+ const resultDiv = document.getElementById('apiTestResult');
188
+ resultDiv.innerHTML = '<p>Testing API connectivity...</p>';
189
+
190
+ const endpoints = [
191
+ '/api/health',
192
+ '/api/dashboard/summary',
193
+ '/api/documents',
194
+ '/api/ocr/status'
195
+ ];
196
+
197
+ const results = [];
198
+
199
+ for (const endpoint of endpoints) {
200
+ try {
201
+ const response = await fetch(endpoint);
202
+ const success = response.ok;
203
+ results.push({
204
+ endpoint,
205
+ success,
206
+ status: response.status,
207
+ statusText: response.statusText
208
+ });
209
+
210
+ logEvent(`${success ? '✅' : '❌'} ${endpoint}: ${response.status}`, success ? 'success' : 'error');
211
+ } catch (error) {
212
+ results.push({
213
+ endpoint,
214
+ success: false,
215
+ error: error.message
216
+ });
217
+ logEvent(`❌ ${endpoint}: ${error.message}`, 'error');
218
+ }
219
+ }
220
+
221
+ const successCount = results.filter(r => r.success).length;
222
+ const totalCount = results.length;
223
+ const successRate = Math.round((successCount / totalCount) * 100);
224
+
225
+ resultDiv.innerHTML = `
226
+ <div class="${successRate >= 75 ? 'success' : successRate >= 50 ? 'warning' : 'error'}">
227
+ <span class="status-indicator ${successRate >= 75 ? 'success' : successRate >= 50 ? 'warning' : 'error'}"></span>
228
+ API Connectivity: ${successCount}/${totalCount} (${successRate}%)
229
+ <ul>
230
+ ${results.map(r => `
231
+ <li class="${r.success ? 'success' : 'error'}">
232
+ ${r.success ? '✅' : '❌'} ${r.endpoint}: ${r.status || r.error}
233
+ </li>
234
+ `).join('')}
235
+ </ul>
236
+ </div>
237
+ `;
238
+ }
239
+
240
+ function testCrossPageCommunication() {
241
+ const resultDiv = document.getElementById('communicationTestResult');
242
+ resultDiv.innerHTML = '<p>Testing cross-page communication...</p>';
243
+
244
+ try {
245
+ // Test localStorage synchronization
246
+ const testData = { test: true, timestamp: Date.now() };
247
+ dashboardCore.storeEvent('testStorageEvent', testData);
248
+
249
+ // Verify event was stored
250
+ const events = JSON.parse(localStorage.getItem('dashboard_events') || '[]');
251
+ const lastEvent = events[events.length - 1];
252
+
253
+ if (lastEvent && lastEvent.name === 'testStorageEvent') {
254
+ logEvent('✅ localStorage synchronization working', 'success');
255
+ } else {
256
+ throw new Error('localStorage synchronization failed');
257
+ }
258
+
259
+ // Test event broadcasting
260
+ let eventReceived = false;
261
+ const unsubscribe = dashboardCore.listen('testCommunicationEvent', (data) => {
262
+ eventReceived = true;
263
+ logEvent('✅ Cross-page event received: ' + JSON.stringify(data), 'success');
264
+ });
265
+
266
+ dashboardCore.broadcast('testCommunicationEvent', {
267
+ message: 'Test cross-page communication',
268
+ timestamp: Date.now()
269
+ });
270
+
271
+ setTimeout(() => {
272
+ unsubscribe();
273
+ if (eventReceived) {
274
+ resultDiv.innerHTML = `
275
+ <div class="success">
276
+ <span class="status-indicator success"></span>
277
+ ✅ Cross-page communication working
278
+ <ul>
279
+ <li>Event broadcasting: ✅</li>
280
+ <li>Event listening: ✅</li>
281
+ <li>localStorage sync: ✅</li>
282
+ </ul>
283
+ </div>
284
+ `;
285
+ } else {
286
+ throw new Error('Event communication failed');
287
+ }
288
+ }, 100);
289
+
290
+ } catch (error) {
291
+ resultDiv.innerHTML = `
292
+ <div class="error">
293
+ <span class="status-indicator error"></span>
294
+ ❌ Cross-page communication test failed: ${error.message}
295
+ </div>
296
+ `;
297
+ logEvent('❌ Cross-page communication test failed: ' + error.message, 'error');
298
+ }
299
+ }
300
+
301
+ function simulateDocumentUpload() {
302
+ const testData = {
303
+ fileId: 'test_' + Date.now(),
304
+ fileName: 'test_document.pdf',
305
+ fileSize: 1024000,
306
+ status: 'uploaded'
307
+ };
308
+
309
+ dashboardCore.broadcast('documentUploaded', testData);
310
+ logEvent('📄 Simulated document upload: ' + testData.fileName, 'info');
311
+
312
+ document.getElementById('realtimeTestResult').innerHTML = `
313
+ <div class="success">
314
+ ✅ Document upload event broadcasted
315
+ <pre>${JSON.stringify(testData, null, 2)}</pre>
316
+ </div>
317
+ `;
318
+ }
319
+
320
+ function simulateDocumentUpdate() {
321
+ const testData = {
322
+ documentId: 'doc_' + Date.now(),
323
+ fileName: 'updated_document.pdf',
324
+ status: 'updated',
325
+ updatedAt: new Date().toISOString()
326
+ };
327
+
328
+ dashboardCore.broadcast('documentUpdated', testData);
329
+ logEvent('📝 Simulated document update: ' + testData.fileName, 'info');
330
+
331
+ document.getElementById('realtimeTestResult').innerHTML = `
332
+ <div class="success">
333
+ ✅ Document update event broadcasted
334
+ <pre>${JSON.stringify(testData, null, 2)}</pre>
335
+ </div>
336
+ `;
337
+ }
338
+
339
+ function simulateDocumentDelete() {
340
+ const testData = {
341
+ documentId: 'doc_' + Date.now(),
342
+ fileName: 'deleted_document.pdf',
343
+ status: 'deleted'
344
+ };
345
+
346
+ dashboardCore.broadcast('documentDeleted', testData);
347
+ logEvent('🗑️ Simulated document delete: ' + testData.fileName, 'info');
348
+
349
+ document.getElementById('realtimeTestResult').innerHTML = `
350
+ <div class="success">
351
+ ✅ Document delete event broadcasted
352
+ <pre>${JSON.stringify(testData, null, 2)}</pre>
353
+ </div>
354
+ `;
355
+ }
356
+
357
+ // Listen for all dashboard events
358
+ dashboardCore.listen('documentUploaded', (data) => {
359
+ logEvent('📄 Document upload event received: ' + data.fileName, 'success');
360
+ });
361
+
362
+ dashboardCore.listen('documentUpdated', (data) => {
363
+ logEvent('📝 Document update event received: ' + data.fileName, 'success');
364
+ });
365
+
366
+ dashboardCore.listen('documentDeleted', (data) => {
367
+ logEvent('🗑️ Document delete event received: ' + data.fileName, 'success');
368
+ });
369
+
370
+ dashboardCore.listen('healthUpdate', (data) => {
371
+ logEvent('💓 Health update: ' + data.status, 'info');
372
+ });
373
+
374
+ dashboardCore.listen('dashboardStatsUpdated', (data) => {
375
+ logEvent('📊 Dashboard stats updated', 'info');
376
+ });
377
+
378
+ // Initialize test page
379
+ document.addEventListener('DOMContentLoaded', () => {
380
+ logEvent('🚀 Integration test page loaded', 'info');
381
+ logEvent('📦 Dashboard Core module: ' + (typeof dashboardCore !== 'undefined' ? 'Loaded' : 'Not loaded'), 'info');
382
+ });
383
+ </script>
384
+ </body>
385
+ </html>
app/frontend/dev/real-api-test.html ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Real API Testing - Legal Dashboard</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Arial', sans-serif;
10
+ max-width: 1400px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .test-section {
16
+ background: white;
17
+ padding: 20px;
18
+ margin: 20px 0;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+ }
22
+ .success { color: #10b981; }
23
+ .error { color: #ef4444; }
24
+ .info { color: #3b82f6; }
25
+ .warning { color: #f59e0b; }
26
+ button {
27
+ background: #007bff;
28
+ color: white;
29
+ border: none;
30
+ padding: 10px 20px;
31
+ border-radius: 4px;
32
+ cursor: pointer;
33
+ margin: 5px;
34
+ }
35
+ button:hover {
36
+ background: #0056b3;
37
+ }
38
+ button:disabled {
39
+ background: #ccc;
40
+ cursor: not-allowed;
41
+ }
42
+ .endpoint-grid {
43
+ display: grid;
44
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
45
+ gap: 15px;
46
+ margin-top: 20px;
47
+ }
48
+ .endpoint-card {
49
+ border: 1px solid #ddd;
50
+ border-radius: 8px;
51
+ padding: 15px;
52
+ background: white;
53
+ position: relative;
54
+ }
55
+ .endpoint-card.success {
56
+ border-color: #10b981;
57
+ background: #f0fdf4;
58
+ }
59
+ .endpoint-card.error {
60
+ border-color: #ef4444;
61
+ background: #fef2f2;
62
+ }
63
+ .endpoint-card.warning {
64
+ border-color: #f59e0b;
65
+ background: #fffbeb;
66
+ }
67
+ .endpoint-card.testing {
68
+ border-color: #3b82f6;
69
+ background: #eff6ff;
70
+ }
71
+ .status-indicator {
72
+ display: inline-block;
73
+ width: 12px;
74
+ height: 12px;
75
+ border-radius: 50%;
76
+ margin-right: 8px;
77
+ }
78
+ .status-indicator.success { background: #10b981; }
79
+ .status-indicator.error { background: #ef4444; }
80
+ .status-indicator.warning { background: #f59e0b; }
81
+ .status-indicator.info { background: #3b82f6; }
82
+ .status-indicator.testing {
83
+ background: #3b82f6;
84
+ animation: pulse 1s infinite;
85
+ }
86
+ @keyframes pulse {
87
+ 0% { opacity: 1; }
88
+ 50% { opacity: 0.5; }
89
+ 100% { opacity: 1; }
90
+ }
91
+ .response-data {
92
+ background: #f8f9fa;
93
+ padding: 10px;
94
+ border-radius: 4px;
95
+ margin-top: 10px;
96
+ font-family: 'Courier New', monospace;
97
+ font-size: 12px;
98
+ max-height: 200px;
99
+ overflow-y: auto;
100
+ white-space: pre-wrap;
101
+ }
102
+ .test-controls {
103
+ display: flex;
104
+ gap: 10px;
105
+ margin-bottom: 20px;
106
+ flex-wrap: wrap;
107
+ }
108
+ .summary-stats {
109
+ display: grid;
110
+ grid-template-columns: repeat(4, 1fr);
111
+ gap: 15px;
112
+ margin-bottom: 20px;
113
+ }
114
+ .stat-card {
115
+ background: white;
116
+ padding: 15px;
117
+ border-radius: 8px;
118
+ text-align: center;
119
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
120
+ }
121
+ .stat-number {
122
+ font-size: 2rem;
123
+ font-weight: bold;
124
+ margin-bottom: 5px;
125
+ }
126
+ .stat-label {
127
+ color: #666;
128
+ font-size: 0.9rem;
129
+ }
130
+ .progress-bar {
131
+ width: 100%;
132
+ height: 4px;
133
+ background: #e5e7eb;
134
+ border-radius: 2px;
135
+ overflow: hidden;
136
+ margin: 10px 0;
137
+ }
138
+ .progress-fill {
139
+ height: 100%;
140
+ background: #3b82f6;
141
+ transition: width 0.3s ease;
142
+ }
143
+ .file-upload-test {
144
+ border: 2px dashed #ddd;
145
+ padding: 20px;
146
+ text-align: center;
147
+ border-radius: 8px;
148
+ margin: 20px 0;
149
+ }
150
+ .file-upload-test.dragover {
151
+ border-color: #3b82f6;
152
+ background: #eff6ff;
153
+ }
154
+ .test-results {
155
+ max-height: 400px;
156
+ overflow-y: auto;
157
+ border: 1px solid #ddd;
158
+ border-radius: 4px;
159
+ padding: 10px;
160
+ background: #f8f9fa;
161
+ }
162
+ </style>
163
+ </head>
164
+ <body>
165
+ <h1>🔍 Real API Testing - Legal Dashboard</h1>
166
+
167
+ <div class="test-section">
168
+ <h2>📊 Test Summary</h2>
169
+ <div class="summary-stats">
170
+ <div class="stat-card">
171
+ <div class="stat-number" id="totalTests">0</div>
172
+ <div class="stat-label">Total Tests</div>
173
+ </div>
174
+ <div class="stat-card">
175
+ <div class="stat-number" id="passedTests">0</div>
176
+ <div class="stat-label">Passed</div>
177
+ </div>
178
+ <div class="stat-card">
179
+ <div class="stat-number" id="failedTests">0</div>
180
+ <div class="stat-label">Failed</div>
181
+ </div>
182
+ <div class="stat-card">
183
+ <div class="stat-number" id="successRate">0%</div>
184
+ <div class="stat-label">Success Rate</div>
185
+ </div>
186
+ </div>
187
+ <div class="progress-bar">
188
+ <div class="progress-fill" id="progressBar"></div>
189
+ </div>
190
+ </div>
191
+
192
+ <div class="test-section">
193
+ <h2>🎛️ Test Controls</h2>
194
+ <div class="test-controls">
195
+ <button type="button" onclick="runAllTests()" id="runAllBtn">Run All Tests</button>
196
+ <button type="button" onclick="runHealthTests()">Health Tests Only</button>
197
+ <button type="button" onclick="runDashboardTests()">Dashboard Tests</button>
198
+ <button type="button" onclick="runDocumentTests()">Document Tests</button>
199
+ <button type="button" onclick="runOCRTests()">OCR Tests</button>
200
+ <button type="button" onclick="runScrapingTests()">Scraping Tests</button>
201
+ <button type="button" onclick="clearResults()">Clear Results</button>
202
+ <button type="button" onclick="exportResults()">Export Results</button>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="test-section">
207
+ <h2>📁 File Upload Test</h2>
208
+ <div class="file-upload-test" id="uploadZone">
209
+ <p>Drag and drop a file here or click to select</p>
210
+ <input type="file" id="testFileInput" accept=".pdf,.jpg,.jpeg,.png,.tiff" style="display: none;">
211
+ <button type="button" onclick="document.getElementById('testFileInput').click()">Select File</button>
212
+ </div>
213
+ <div id="uploadResults"></div>
214
+ </div>
215
+
216
+ <div class="test-section">
217
+ <h2>🔌 API Endpoint Tests</h2>
218
+ <div class="endpoint-grid" id="endpointGrid">
219
+ <!-- Endpoint cards will be generated here -->
220
+ </div>
221
+ </div>
222
+
223
+ <div class="test-section">
224
+ <h2>📋 Test Results</h2>
225
+ <div class="test-results" id="testResults">
226
+ <!-- Test results will be displayed here -->
227
+ </div>
228
+ </div>
229
+
230
+ <script src="../js/api-client.js"></script>
231
+ <script>
232
+ class RealAPITester {
233
+ constructor() {
234
+ this.baseURL = window.location.origin;
235
+ this.results = [];
236
+ this.testStats = {
237
+ total: 0,
238
+ passed: 0,
239
+ failed: 0,
240
+ successRate: 0
241
+ };
242
+ this.isRunning = false;
243
+
244
+ this.endpoints = [
245
+ // Health & System Tests
246
+ { name: 'Health Check', url: '/api/health', method: 'GET', category: 'Health', expectedStatus: 200 },
247
+ { name: 'API Docs', url: '/api/docs', method: 'GET', category: 'Health', expectedStatus: 200 },
248
+
249
+ // Dashboard Tests
250
+ { name: 'Dashboard Summary', url: '/api/dashboard/summary', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
251
+ { name: 'Charts Data', url: '/api/dashboard/charts-data', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
252
+ { name: 'AI Suggestions', url: '/api/dashboard/ai-suggestions', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
253
+ { name: 'Performance Metrics', url: '/api/dashboard/performance-metrics', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
254
+ { name: 'Trends', url: '/api/dashboard/trends', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
255
+
256
+ // Documents Tests
257
+ { name: 'Documents List', url: '/api/documents', method: 'GET', category: 'Documents', expectedStatus: 200 },
258
+ { name: 'Document Categories', url: '/api/documents/categories', method: 'GET', category: 'Documents', expectedStatus: 200 },
259
+ { name: 'Document Sources', url: '/api/documents/sources', method: 'GET', category: 'Documents', expectedStatus: 200 },
260
+ { name: 'Document Search', url: '/api/documents/search?q=test', method: 'GET', category: 'Documents', expectedStatus: 200 },
261
+
262
+ // OCR Tests
263
+ { name: 'OCR Status', url: '/api/ocr/status', method: 'GET', category: 'OCR', expectedStatus: 200 },
264
+ { name: 'OCR Models', url: '/api/ocr/models', method: 'GET', category: 'OCR', expectedStatus: 200 },
265
+
266
+ // Scraping Tests
267
+ { name: 'Scraping Statistics', url: '/api/scraping/scrape/statistics', method: 'GET', category: 'Scraping', expectedStatus: 200 },
268
+ { name: 'Scraping Status', url: '/api/scraping/scrape/status', method: 'GET', category: 'Scraping', expectedStatus: 200 },
269
+ { name: 'Rating Summary', url: '/api/scraping/rating/summary', method: 'GET', category: 'Scraping', expectedStatus: 200 },
270
+ { name: 'Scraping Health', url: '/api/scraping/health', method: 'GET', category: 'Scraping', expectedStatus: 200 },
271
+
272
+ // Analytics Tests (Expected to fail)
273
+ { name: 'Analytics Overview', url: '/api/analytics/overview', method: 'GET', category: 'Analytics', expectedStatus: 404 },
274
+ { name: 'Analytics Performance', url: '/api/analytics/performance', method: 'GET', category: 'Analytics', expectedStatus: 404 },
275
+ { name: 'Analytics Entities', url: '/api/analytics/entities', method: 'GET', category: 'Analytics', expectedStatus: 404 },
276
+ { name: 'Analytics Quality', url: '/api/analytics/quality-analysis', method: 'GET', category: 'Analytics', expectedStatus: 404 }
277
+ ];
278
+
279
+ this.initialize();
280
+ }
281
+
282
+ initialize() {
283
+ this.createEndpointCards();
284
+ this.setupFileUpload();
285
+ this.updateStats();
286
+ }
287
+
288
+ createEndpointCards() {
289
+ const grid = document.getElementById('endpointGrid');
290
+ grid.innerHTML = '';
291
+
292
+ this.endpoints.forEach((endpoint, index) => {
293
+ const card = document.createElement('div');
294
+ card.className = 'endpoint-card';
295
+ card.id = `endpoint-${index}`;
296
+
297
+ card.innerHTML = `
298
+ <div class="status-indicator"></div>
299
+ <strong>${endpoint.name}</strong>
300
+ <div style="font-size: 0.8rem; color: #666; margin: 5px 0;">
301
+ ${endpoint.method} ${endpoint.url}
302
+ </div>
303
+ <div class="response-data" id="response-${index}" style="display: none;"></div>
304
+ <button onclick="tester.testSingleEndpoint(${index})" class="test-btn">
305
+ Test
306
+ </button>
307
+ `;
308
+
309
+ grid.appendChild(card);
310
+ });
311
+ }
312
+
313
+ setupFileUpload() {
314
+ const uploadZone = document.getElementById('uploadZone');
315
+ const fileInput = document.getElementById('testFileInput');
316
+
317
+ uploadZone.addEventListener('dragover', (e) => {
318
+ e.preventDefault();
319
+ uploadZone.classList.add('dragover');
320
+ });
321
+
322
+ uploadZone.addEventListener('dragleave', () => {
323
+ uploadZone.classList.remove('dragover');
324
+ });
325
+
326
+ uploadZone.addEventListener('drop', (e) => {
327
+ e.preventDefault();
328
+ uploadZone.classList.remove('dragover');
329
+ const files = e.dataTransfer.files;
330
+ if (files.length > 0) {
331
+ this.testFileUpload(files[0]);
332
+ }
333
+ });
334
+
335
+ fileInput.addEventListener('change', (e) => {
336
+ if (e.target.files.length > 0) {
337
+ this.testFileUpload(e.target.files[0]);
338
+ }
339
+ });
340
+ }
341
+
342
+ async testSingleEndpoint(index) {
343
+ const endpoint = this.endpoints[index];
344
+ const card = document.getElementById(`endpoint-${index}`);
345
+ const responseDiv = document.getElementById(`response-${index}`);
346
+
347
+ // Set testing state
348
+ card.className = 'endpoint-card testing';
349
+ card.querySelector('.status-indicator').className = 'status-indicator testing';
350
+ card.querySelector('.test-btn').disabled = true;
351
+
352
+ const startTime = Date.now();
353
+
354
+ try {
355
+ const response = await fetch(`${this.baseURL}${endpoint.url}`, {
356
+ method: endpoint.method,
357
+ headers: {
358
+ 'Content-Type': 'application/json'
359
+ }
360
+ });
361
+
362
+ const responseTime = Date.now() - startTime;
363
+ const responseText = await response.text();
364
+ let responseData;
365
+
366
+ try {
367
+ responseData = JSON.parse(responseText);
368
+ } catch {
369
+ responseData = responseText;
370
+ }
371
+
372
+ const success = response.status === endpoint.expectedStatus;
373
+
374
+ // Update card
375
+ card.className = `endpoint-card ${success ? 'success' : 'error'}`;
376
+ card.querySelector('.status-indicator').className = `status-indicator ${success ? 'success' : 'error'}`;
377
+ card.querySelector('.test-btn').disabled = false;
378
+
379
+ // Show response data
380
+ responseDiv.style.display = 'block';
381
+ responseDiv.innerHTML = `
382
+ Status: ${response.status} ${response.statusText}
383
+ Time: ${responseTime}ms
384
+ Size: ${responseText.length} bytes
385
+
386
+ Response:
387
+ ${JSON.stringify(responseData, null, 2)}
388
+ `;
389
+
390
+ // Log result
391
+ this.logResult({
392
+ endpoint: endpoint.name,
393
+ url: endpoint.url,
394
+ method: endpoint.method,
395
+ status: response.status,
396
+ expectedStatus: endpoint.expectedStatus,
397
+ success: success,
398
+ responseTime: responseTime,
399
+ responseSize: responseText.length,
400
+ responseData: responseData
401
+ });
402
+
403
+ } catch (error) {
404
+ card.className = 'endpoint-card error';
405
+ card.querySelector('.status-indicator').className = 'status-indicator error';
406
+ card.querySelector('.test-btn').disabled = false;
407
+
408
+ responseDiv.style.display = 'block';
409
+ responseDiv.innerHTML = `Error: ${error.message}`;
410
+
411
+ this.logResult({
412
+ endpoint: endpoint.name,
413
+ url: endpoint.url,
414
+ method: endpoint.method,
415
+ status: 0,
416
+ expectedStatus: endpoint.expectedStatus,
417
+ success: false,
418
+ error: error.message
419
+ });
420
+ }
421
+
422
+ this.updateStats();
423
+ }
424
+
425
+ async testFileUpload(file) {
426
+ const resultsDiv = document.getElementById('uploadResults');
427
+ resultsDiv.innerHTML = `<p>Testing file upload: ${file.name} (${file.size} bytes)</p>`;
428
+
429
+ try {
430
+ const formData = new FormData();
431
+ formData.append('file', file);
432
+
433
+ const startTime = Date.now();
434
+ const response = await fetch(`${this.baseURL}/api/ocr/upload`, {
435
+ method: 'POST',
436
+ body: formData
437
+ });
438
+
439
+ const responseTime = Date.now() - startTime;
440
+ const responseData = await response.json();
441
+
442
+ const success = response.ok;
443
+
444
+ resultsDiv.innerHTML = `
445
+ <div class="${success ? 'success' : 'error'}">
446
+ <h4>File Upload Test Results</h4>
447
+ <p><strong>File:</strong> ${file.name}</p>
448
+ <p><strong>Size:</strong> ${file.size} bytes</p>
449
+ <p><strong>Status:</strong> ${response.status} ${response.statusText}</p>
450
+ <p><strong>Response Time:</strong> ${responseTime}ms</p>
451
+ <div class="response-data">
452
+ ${JSON.stringify(responseData, null, 2)}
453
+ </div>
454
+ </div>
455
+ `;
456
+
457
+ this.logResult({
458
+ endpoint: 'File Upload',
459
+ url: '/api/ocr/upload',
460
+ method: 'POST',
461
+ status: response.status,
462
+ success: success,
463
+ responseTime: responseTime,
464
+ fileSize: file.size,
465
+ fileName: file.name,
466
+ responseData: responseData
467
+ });
468
+
469
+ } catch (error) {
470
+ resultsDiv.innerHTML = `
471
+ <div class="error">
472
+ <h4>File Upload Test Failed</h4>
473
+ <p>Error: ${error.message}</p>
474
+ </div>
475
+ `;
476
+
477
+ this.logResult({
478
+ endpoint: 'File Upload',
479
+ url: '/api/ocr/upload',
480
+ method: 'POST',
481
+ status: 0,
482
+ success: false,
483
+ error: error.message,
484
+ fileName: file.name
485
+ });
486
+ }
487
+
488
+ this.updateStats();
489
+ }
490
+
491
+ async runAllTests() {
492
+ if (this.isRunning) return;
493
+
494
+ this.isRunning = true;
495
+ document.getElementById('runAllBtn').disabled = true;
496
+ document.getElementById('runAllBtn').textContent = 'Running...';
497
+
498
+ this.clearResults();
499
+
500
+ for (let i = 0; i < this.endpoints.length; i++) {
501
+ await this.testSingleEndpoint(i);
502
+ await this.delay(100); // Small delay between tests
503
+ }
504
+
505
+ this.isRunning = false;
506
+ document.getElementById('runAllBtn').disabled = false;
507
+ document.getElementById('runAllBtn').textContent = 'Run All Tests';
508
+ }
509
+
510
+ async runHealthTests() {
511
+ const healthEndpoints = this.endpoints.filter(e => e.category === 'Health');
512
+ for (let i = 0; i < this.endpoints.length; i++) {
513
+ if (this.endpoints[i].category === 'Health') {
514
+ await this.testSingleEndpoint(i);
515
+ await this.delay(100);
516
+ }
517
+ }
518
+ }
519
+
520
+ async runDashboardTests() {
521
+ const dashboardEndpoints = this.endpoints.filter(e => e.category === 'Dashboard');
522
+ for (let i = 0; i < this.endpoints.length; i++) {
523
+ if (this.endpoints[i].category === 'Dashboard') {
524
+ await this.testSingleEndpoint(i);
525
+ await this.delay(100);
526
+ }
527
+ }
528
+ }
529
+
530
+ async runDocumentTests() {
531
+ const documentEndpoints = this.endpoints.filter(e => e.category === 'Documents');
532
+ for (let i = 0; i < this.endpoints.length; i++) {
533
+ if (this.endpoints[i].category === 'Documents') {
534
+ await this.testSingleEndpoint(i);
535
+ await this.delay(100);
536
+ }
537
+ }
538
+ }
539
+
540
+ async runOCRTests() {
541
+ const ocrEndpoints = this.endpoints.filter(e => e.category === 'OCR');
542
+ for (let i = 0; i < this.endpoints.length; i++) {
543
+ if (this.endpoints[i].category === 'OCR') {
544
+ await this.testSingleEndpoint(i);
545
+ await this.delay(100);
546
+ }
547
+ }
548
+ }
549
+
550
+ async runScrapingTests() {
551
+ const scrapingEndpoints = this.endpoints.filter(e => e.category === 'Scraping');
552
+ for (let i = 0; i < this.endpoints.length; i++) {
553
+ if (this.endpoints[i].category === 'Scraping') {
554
+ await this.testSingleEndpoint(i);
555
+ await this.delay(100);
556
+ }
557
+ }
558
+ }
559
+
560
+ logResult(result) {
561
+ this.results.push({
562
+ ...result,
563
+ timestamp: new Date().toISOString()
564
+ });
565
+
566
+ const resultsDiv = document.getElementById('testResults');
567
+ const resultEntry = document.createElement('div');
568
+ resultEntry.className = `test-result ${result.success ? 'success' : 'error'}`;
569
+ resultEntry.innerHTML = `
570
+ <strong>${result.endpoint}</strong> -
571
+ ${result.success ? '✅ PASS' : '❌ FAIL'}
572
+ (${result.status || 'ERROR'}) -
573
+ ${result.responseTime ? result.responseTime + 'ms' : 'N/A'}
574
+ <br><small>${new Date().toLocaleTimeString()}</small>
575
+ `;
576
+
577
+ resultsDiv.appendChild(resultEntry);
578
+ resultsDiv.scrollTop = resultsDiv.scrollHeight;
579
+ }
580
+
581
+ updateStats() {
582
+ const total = this.results.length;
583
+ const passed = this.results.filter(r => r.success).length;
584
+ const failed = total - passed;
585
+ const successRate = total > 0 ? Math.round((passed / total) * 100) : 0;
586
+
587
+ this.testStats = { total, passed, failed, successRate };
588
+
589
+ document.getElementById('totalTests').textContent = total;
590
+ document.getElementById('passedTests').textContent = passed;
591
+ document.getElementById('failedTests').textContent = failed;
592
+ document.getElementById('successRate').textContent = successRate + '%';
593
+
594
+ const progressBar = document.getElementById('progressBar');
595
+ progressBar.style.width = successRate + '%';
596
+ progressBar.style.background = successRate >= 80 ? '#10b981' : successRate >= 60 ? '#f59e0b' : '#ef4444';
597
+ }
598
+
599
+ clearResults() {
600
+ this.results = [];
601
+ document.getElementById('testResults').innerHTML = '';
602
+ this.updateStats();
603
+
604
+ // Reset all endpoint cards
605
+ this.endpoints.forEach((endpoint, index) => {
606
+ const card = document.getElementById(`endpoint-${index}`);
607
+ card.className = 'endpoint-card';
608
+ card.querySelector('.status-indicator').className = 'status-indicator';
609
+ card.querySelector('.test-btn').disabled = false;
610
+ document.getElementById(`response-${index}`).style.display = 'none';
611
+ });
612
+ }
613
+
614
+ exportResults() {
615
+ const data = {
616
+ timestamp: new Date().toISOString(),
617
+ stats: this.testStats,
618
+ results: this.results
619
+ };
620
+
621
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
622
+ const url = URL.createObjectURL(blob);
623
+ const a = document.createElement('a');
624
+ a.href = url;
625
+ a.download = `api-test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
626
+ a.click();
627
+ URL.revokeObjectURL(url);
628
+ }
629
+
630
+ delay(ms) {
631
+ return new Promise(resolve => setTimeout(resolve, ms));
632
+ }
633
+ }
634
+
635
+ // Global tester instance
636
+ const tester = new RealAPITester();
637
+
638
+ // Global functions for button clicks
639
+ function runAllTests() {
640
+ tester.runAllTests();
641
+ }
642
+
643
+ function runHealthTests() {
644
+ tester.runHealthTests();
645
+ }
646
+
647
+ function runDashboardTests() {
648
+ tester.runDashboardTests();
649
+ }
650
+
651
+ function runDocumentTests() {
652
+ tester.runDocumentTests();
653
+ }
654
+
655
+ function runOCRTests() {
656
+ tester.runOCRTests();
657
+ }
658
+
659
+ function runScrapingTests() {
660
+ tester.runScrapingTests();
661
+ }
662
+
663
+ function clearResults() {
664
+ tester.clearResults();
665
+ }
666
+
667
+ function exportResults() {
668
+ tester.exportResults();
669
+ }
670
+
671
+ console.log('🔍 Real API Tester initialized');
672
+ </script>
673
+ </body>
674
+ </html>
app/frontend/dev/test_integration.html ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>تست اتصال فرانت‌اند و بک‌اند</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Arial', sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .test-section {
16
+ background: white;
17
+ padding: 20px;
18
+ margin: 20px 0;
19
+ border-radius: 8px;
20
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
21
+ }
22
+ .success { color: green; }
23
+ .error { color: red; }
24
+ .info { color: blue; }
25
+ button {
26
+ background: #007bff;
27
+ color: white;
28
+ border: none;
29
+ padding: 10px 20px;
30
+ border-radius: 4px;
31
+ cursor: pointer;
32
+ margin: 5px;
33
+ }
34
+ button:hover {
35
+ background: #0056b3;
36
+ }
37
+ pre {
38
+ background: #f8f9fa;
39
+ padding: 10px;
40
+ border-radius: 4px;
41
+ overflow-x: auto;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <h1>تست اتصال فرانت‌اند و بک‌اند</h1>
47
+
48
+ <div class="test-section">
49
+ <h2>تست اتصال API</h2>
50
+ <button onclick="testConnection()">تست اتصال</button>
51
+ <div id="connectionResult"></div>
52
+ </div>
53
+
54
+ <div class="test-section">
55
+ <h2>تست دریافت آمار داشبورد</h2>
56
+ <button onclick="testDashboardSummary()">دریافت آمار</button>
57
+ <div id="dashboardResult"></div>
58
+ </div>
59
+
60
+ <div class="test-section">
61
+ <h2>تست دریافت اسناد</h2>
62
+ <button onclick="testDocuments()">دریافت اسناد</button>
63
+ <div id="documentsResult"></div>
64
+ </div>
65
+
66
+ <div class="test-section">
67
+ <h2>تست شروع جمع‌آوری</h2>
68
+ <button onclick="testScraping()">شروع جمع‌آوری</button>
69
+ <div id="scrapingResult"></div>
70
+ </div>
71
+
72
+ <script>
73
+ const API_BASE = 'http://localhost:8000';
74
+
75
+ async function testConnection() {
76
+ const resultDiv = document.getElementById('connectionResult');
77
+ resultDiv.innerHTML = '<p class="info">در حال تست اتصال...</p>';
78
+
79
+ try {
80
+ const response = await fetch(`${API_BASE}/api/dashboard-summary`);
81
+ if (response.ok) {
82
+ resultDiv.innerHTML = '<p class="success">✅ اتصال موفق! سرور در دسترس است.</p>';
83
+ } else {
84
+ resultDiv.innerHTML = `<p class="error">❌ خطا در اتصال: ${response.status} ${response.statusText}</p>`;
85
+ }
86
+ } catch (error) {
87
+ resultDiv.innerHTML = `<p class="error">❌ خطا در اتصال: ${error.message}</p>`;
88
+ }
89
+ }
90
+
91
+ async function testDashboardSummary() {
92
+ const resultDiv = document.getElementById('dashboardResult');
93
+ resultDiv.innerHTML = '<p class="info">در حال دریافت آمار...</p>';
94
+
95
+ try {
96
+ const response = await fetch(`${API_BASE}/api/dashboard-summary`);
97
+ if (response.ok) {
98
+ const data = await response.json();
99
+ resultDiv.innerHTML = `
100
+ <p class="success">✅ آمار دریافت شد:</p>
101
+ <pre>${JSON.stringify(data, null, 2)}</pre>
102
+ `;
103
+ } else {
104
+ resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت آمار: ${response.status}</p>`;
105
+ }
106
+ } catch (error) {
107
+ resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت آمار: ${error.message}</p>`;
108
+ }
109
+ }
110
+
111
+ async function testDocuments() {
112
+ const resultDiv = document.getElementById('documentsResult');
113
+ resultDiv.innerHTML = '<p class="info">در حال دریافت اسناد...</p>';
114
+
115
+ try {
116
+ const response = await fetch(`${API_BASE}/api/documents?limit=5`);
117
+ if (response.ok) {
118
+ const data = await response.json();
119
+ resultDiv.innerHTML = `
120
+ <p class="success">✅ اسناد دریافت شد (${data.length} سند):</p>
121
+ <pre>${JSON.stringify(data, null, 2)}</pre>
122
+ `;
123
+ } else {
124
+ resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت اسناد: ${response.status}</p>`;
125
+ }
126
+ } catch (error) {
127
+ resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت اسناد: ${error.message}</p>`;
128
+ }
129
+ }
130
+
131
+ async function testScraping() {
132
+ const resultDiv = document.getElementById('scrapingResult');
133
+ resultDiv.innerHTML = '<p class="info">در حال شروع جمع‌آوری...</p>';
134
+
135
+ try {
136
+ const response = await fetch(`${API_BASE}/api/scrape-trigger`, {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/json',
140
+ },
141
+ body: JSON.stringify({ manual_trigger: true })
142
+ });
143
+
144
+ if (response.ok) {
145
+ const data = await response.json();
146
+ resultDiv.innerHTML = `
147
+ <p class="success">✅ جمع‌آوری شروع شد:</p>
148
+ <pre>${JSON.stringify(data, null, 2)}</pre>
149
+ `;
150
+ } else {
151
+ resultDiv.innerHTML = `<p class="error">❌ خطا در شروع جمع‌آوری: ${response.status}</p>`;
152
+ }
153
+ } catch (error) {
154
+ resultDiv.innerHTML = `<p class="error">❌ خطا در شروع جمع‌آوری: ${error.message}</p>`;
155
+ }
156
+ }
157
+
158
+ // Auto-test on page load
159
+ window.addEventListener('load', () => {
160
+ setTimeout(testConnection, 1000);
161
+ });
162
+ </script>
163
+ </body>
164
+ </html>
app/frontend/documents.html ADDED
@@ -0,0 +1,1597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>مدیریت اسناد | سامانه حقوقی</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+
12
+ <!-- Load API Client -->
13
+ <script src="/static/js/api-client.js"></script>
14
+ <script src="/static/js/core.js"></script>
15
+
16
+ <style>
17
+ :root {
18
+ --text-primary: #0f172a;
19
+ --text-secondary: #475569;
20
+ --text-muted: #64748b;
21
+ --text-light: #ffffff;
22
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
23
+ --card-bg: rgba(255, 255, 255, 0.95);
24
+ --glass-bg: rgba(255, 255, 255, 0.9);
25
+ --glass-border: rgba(148, 163, 184, 0.2);
26
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
27
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
28
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
29
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
30
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
31
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
32
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
33
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
35
+ --sidebar-width: 260px;
36
+ --border-radius: 12px;
37
+ --border-radius-sm: 8px;
38
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
39
+ --transition-fast: all 0.15s ease-in-out;
40
+ --font-size-xs: 0.7rem;
41
+ --font-size-sm: 0.8rem;
42
+ --font-size-base: 0.9rem;
43
+ --font-size-lg: 1.1rem;
44
+ --font-size-xl: 1.25rem;
45
+ --font-size-2xl: 1.5rem;
46
+ }
47
+
48
+ * {
49
+ margin: 0;
50
+ padding: 0;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ body {
55
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
56
+ background: var(--body-bg);
57
+ color: var(--text-primary);
58
+ line-height: 1.6;
59
+ overflow-x: hidden;
60
+ font-size: var(--font-size-base);
61
+ }
62
+
63
+ ::-webkit-scrollbar {
64
+ width: 6px;
65
+ height: 6px;
66
+ }
67
+
68
+ ::-webkit-scrollbar-track {
69
+ background: rgba(0, 0, 0, 0.02);
70
+ border-radius: 10px;
71
+ }
72
+
73
+ ::-webkit-scrollbar-thumb {
74
+ background: var(--primary-gradient);
75
+ border-radius: 10px;
76
+ }
77
+
78
+ .dashboard-container {
79
+ display: flex;
80
+ min-height: 100vh;
81
+ width: 100%;
82
+ }
83
+
84
+ /* سایدبار مشابه صفحه اصلی */
85
+ .sidebar {
86
+ width: var(--sidebar-width);
87
+ background: linear-gradient(135deg,
88
+ rgba(248, 250, 252, 0.98) 0%,
89
+ rgba(241, 245, 249, 0.95) 25%,
90
+ rgba(226, 232, 240, 0.98) 50%,
91
+ rgba(203, 213, 225, 0.95) 75%,
92
+ rgba(148, 163, 184, 0.1) 100%);
93
+ backdrop-filter: blur(25px);
94
+ -webkit-backdrop-filter: blur(25px);
95
+ padding: 1rem 0;
96
+ position: fixed;
97
+ height: 100vh;
98
+ right: 0;
99
+ top: 0;
100
+ z-index: 1000;
101
+ overflow-y: auto;
102
+ box-shadow:
103
+ 0 0 0 1px rgba(59, 130, 246, 0.08),
104
+ -8px 0 32px rgba(59, 130, 246, 0.12),
105
+ inset 0 1px 0 rgba(255, 255, 255, 0.6);
106
+ border-left: 1px solid rgba(59, 130, 246, 0.15);
107
+ }
108
+
109
+ .sidebar-header {
110
+ padding: 0 1rem 1rem;
111
+ border-bottom: 1px solid rgba(59, 130, 246, 0.12);
112
+ margin-bottom: 1rem;
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ background: linear-gradient(135deg,
117
+ rgba(255, 255, 255, 0.4) 0%,
118
+ rgba(248, 250, 252, 0.2) 100%);
119
+ margin: 0 0.5rem 1rem;
120
+ border-radius: var(--border-radius);
121
+ backdrop-filter: blur(10px);
122
+ }
123
+
124
+ .logo {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 0.6rem;
128
+ color: var(--text-primary);
129
+ text-decoration: none;
130
+ }
131
+
132
+ .logo-icon {
133
+ width: 2rem;
134
+ height: 2rem;
135
+ background: var(--primary-gradient);
136
+ border-radius: var(--border-radius-sm);
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ font-size: 1rem;
141
+ color: white;
142
+ }
143
+
144
+ .logo-text {
145
+ font-size: var(--font-size-lg);
146
+ font-weight: 700;
147
+ background: var(--primary-gradient);
148
+ -webkit-background-clip: text;
149
+ -webkit-text-fill-color: transparent;
150
+ }
151
+
152
+ .nav-section {
153
+ margin-bottom: 1rem;
154
+ }
155
+
156
+ .nav-title {
157
+ padding: 0 1rem 0.4rem;
158
+ font-size: var(--font-size-xs);
159
+ font-weight: 600;
160
+ text-transform: uppercase;
161
+ letter-spacing: 0.5px;
162
+ color: var(--text-secondary);
163
+ }
164
+
165
+ .nav-menu {
166
+ list-style: none;
167
+ }
168
+
169
+ .nav-item {
170
+ margin: 0.15rem 0.5rem;
171
+ }
172
+
173
+ .nav-link {
174
+ display: flex;
175
+ align-items: center;
176
+ padding: 0.6rem 0.8rem;
177
+ color: var(--text-primary);
178
+ text-decoration: none;
179
+ border-radius: var(--border-radius-sm);
180
+ transition: var(--transition-smooth);
181
+ font-weight: 500;
182
+ font-size: var(--font-size-sm);
183
+ cursor: pointer;
184
+ border: 1px solid transparent;
185
+ }
186
+
187
+ .nav-link:hover {
188
+ color: var(--text-primary);
189
+ transform: translateX(-2px);
190
+ border-color: rgba(59, 130, 246, 0.15);
191
+ background: rgba(59, 130, 246, 0.05);
192
+ }
193
+
194
+ .nav-link.active {
195
+ background: var(--primary-gradient);
196
+ color: var(--text-light);
197
+ box-shadow: var(--shadow-md);
198
+ }
199
+
200
+ .nav-icon {
201
+ margin-left: 0.6rem;
202
+ width: 1rem;
203
+ text-align: center;
204
+ font-size: 0.9rem;
205
+ }
206
+
207
+ .nav-badge {
208
+ background: var(--danger-gradient);
209
+ color: white;
210
+ padding: 0.15rem 0.4rem;
211
+ border-radius: 10px;
212
+ font-size: var(--font-size-xs);
213
+ font-weight: 600;
214
+ margin-right: auto;
215
+ min-width: 1.2rem;
216
+ text-align: center;
217
+ }
218
+
219
+ /* محتوای اصلی */
220
+ .main-content {
221
+ flex: 1;
222
+ margin-right: var(--sidebar-width);
223
+ padding: 1rem;
224
+ min-height: 100vh;
225
+ width: calc(100% - var(--sidebar-width));
226
+ }
227
+
228
+ .page-header {
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: center;
232
+ margin-bottom: 2rem;
233
+ padding: 1rem 0;
234
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
235
+ }
236
+
237
+ .page-title {
238
+ font-size: var(--font-size-2xl);
239
+ font-weight: 800;
240
+ background: var(--primary-gradient);
241
+ -webkit-background-clip: text;
242
+ -webkit-text-fill-color: transparent;
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 0.6rem;
246
+ }
247
+
248
+ .page-actions {
249
+ display: flex;
250
+ gap: 0.8rem;
251
+ }
252
+
253
+ .btn {
254
+ padding: 0.6rem 1.2rem;
255
+ border: none;
256
+ border-radius: var(--border-radius-sm);
257
+ font-family: inherit;
258
+ font-weight: 600;
259
+ cursor: pointer;
260
+ transition: var(--transition-smooth);
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 0.5rem;
264
+ text-decoration: none;
265
+ font-size: var(--font-size-sm);
266
+ }
267
+
268
+ .btn-primary {
269
+ background: var(--primary-gradient);
270
+ color: white;
271
+ box-shadow: var(--shadow-sm);
272
+ }
273
+
274
+ .btn-primary:hover {
275
+ box-shadow: var(--shadow-md);
276
+ transform: translateY(-1px);
277
+ }
278
+
279
+ .btn-outline {
280
+ background: transparent;
281
+ color: var(--text-primary);
282
+ border: 1px solid rgba(59, 130, 246, 0.2);
283
+ }
284
+
285
+ .btn-outline:hover {
286
+ background: rgba(59, 130, 246, 0.05);
287
+ border-color: rgba(59, 130, 246, 0.4);
288
+ }
289
+
290
+ /* فیلترها */
291
+ .filters-section {
292
+ background: var(--card-bg);
293
+ border-radius: var(--border-radius);
294
+ padding: 1.5rem;
295
+ margin-bottom: 1.5rem;
296
+ box-shadow: var(--shadow-sm);
297
+ border: 1px solid rgba(255, 255, 255, 0.3);
298
+ }
299
+
300
+ .filters-title {
301
+ font-size: var(--font-size-lg);
302
+ font-weight: 700;
303
+ margin-bottom: 1rem;
304
+ color: var(--text-primary);
305
+ }
306
+
307
+ .filters-grid {
308
+ display: grid;
309
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
310
+ gap: 1rem;
311
+ }
312
+
313
+ .filter-group {
314
+ display: flex;
315
+ flex-direction: column;
316
+ gap: 0.5rem;
317
+ }
318
+
319
+ .filter-label {
320
+ font-size: var(--font-size-sm);
321
+ font-weight: 600;
322
+ color: var(--text-secondary);
323
+ }
324
+
325
+ .filter-input,
326
+ .filter-select {
327
+ padding: 0.6rem 0.8rem;
328
+ border: 1px solid var(--glass-border);
329
+ border-radius: var(--border-radius-sm);
330
+ background: var(--glass-bg);
331
+ color: var(--text-primary);
332
+ font-family: inherit;
333
+ font-size: var(--font-size-sm);
334
+ transition: var(--transition-smooth);
335
+ }
336
+
337
+ .filter-input:focus,
338
+ .filter-select:focus {
339
+ outline: none;
340
+ border-color: #3b82f6;
341
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
342
+ }
343
+
344
+ .filters-actions {
345
+ margin-top: 1rem;
346
+ display: flex;
347
+ gap: 0.8rem;
348
+ }
349
+
350
+ .btn-sm {
351
+ padding: 0.4rem 0.8rem;
352
+ font-size: var(--font-size-xs);
353
+ }
354
+
355
+ /* جدول اسناد */
356
+ .documents-section {
357
+ background: var(--card-bg);
358
+ border-radius: var(--border-radius);
359
+ box-shadow: var(--shadow-md);
360
+ border: 1px solid rgba(255, 255, 255, 0.3);
361
+ overflow: hidden;
362
+ }
363
+
364
+ .documents-header {
365
+ padding: 1rem;
366
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
367
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.02), rgba(255, 255, 255, 0.1));
368
+ display: flex;
369
+ justify-content: space-between;
370
+ align-items: center;
371
+ }
372
+
373
+ .documents-title {
374
+ font-size: var(--font-size-lg);
375
+ font-weight: 700;
376
+ color: var(--text-primary);
377
+ }
378
+
379
+ .documents-stats {
380
+ display: flex;
381
+ gap: 1rem;
382
+ font-size: var(--font-size-sm);
383
+ color: var(--text-muted);
384
+ }
385
+
386
+ .documents-table {
387
+ width: 100%;
388
+ border-collapse: separate;
389
+ border-spacing: 0;
390
+ }
391
+
392
+ .documents-table thead {
393
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.03), rgba(255, 255, 255, 0.1));
394
+ }
395
+
396
+ .documents-table th {
397
+ padding: 1rem;
398
+ text-align: right;
399
+ font-weight: 700;
400
+ color: var(--text-primary);
401
+ font-size: var(--font-size-sm);
402
+ border-bottom: 2px solid rgba(59, 130, 246, 0.08);
403
+ }
404
+
405
+ .documents-table td {
406
+ padding: 1rem;
407
+ border-bottom: 1px solid rgba(0, 0, 0, 0.03);
408
+ font-size: var(--font-size-sm);
409
+ vertical-align: middle;
410
+ }
411
+
412
+ .documents-table tbody tr {
413
+ transition: all 0.2s ease;
414
+ cursor: pointer;
415
+ }
416
+
417
+ .documents-table tbody tr:hover {
418
+ background: rgba(59, 130, 246, 0.02);
419
+ transform: translateX(-3px);
420
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06);
421
+ }
422
+
423
+ .document-title {
424
+ font-weight: 600;
425
+ color: var(--text-primary);
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 0.5rem;
429
+ font-size: var(--font-size-sm);
430
+ }
431
+
432
+ .doc-icon {
433
+ width: 1.8rem;
434
+ height: 1.8rem;
435
+ border-radius: var(--border-radius-sm);
436
+ background: var(--primary-gradient);
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: center;
440
+ color: white;
441
+ font-size: var(--font-size-xs);
442
+ }
443
+
444
+ .quality-display {
445
+ display: flex;
446
+ flex-direction: column;
447
+ gap: 0.3rem;
448
+ }
449
+
450
+ .quality-score {
451
+ font-weight: 700;
452
+ font-size: var(--font-size-sm);
453
+ color: var(--text-primary);
454
+ }
455
+
456
+ .quality-bar {
457
+ width: 80px;
458
+ height: 4px;
459
+ background: rgba(0, 0, 0, 0.08);
460
+ border-radius: 2px;
461
+ overflow: hidden;
462
+ }
463
+
464
+ .quality-fill {
465
+ height: 100%;
466
+ border-radius: 2px;
467
+ }
468
+
469
+ .quality-excellent { background: var(--success-gradient); }
470
+ .quality-good { background: var(--primary-gradient); }
471
+ .quality-average { background: var(--warning-gradient); }
472
+ .quality-poor { background: var(--danger-gradient); }
473
+
474
+ .status-badge {
475
+ padding: 0.3rem 0.6rem;
476
+ border-radius: 15px;
477
+ font-size: var(--font-size-xs);
478
+ font-weight: 600;
479
+ display: inline-flex;
480
+ align-items: center;
481
+ gap: 0.3rem;
482
+ }
483
+
484
+ .status-processed {
485
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.08));
486
+ color: #047857;
487
+ border: 1px solid rgba(16, 185, 129, 0.2);
488
+ }
489
+
490
+ .status-pending {
491
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(217, 119, 6, 0.08));
492
+ color: #b45309;
493
+ border: 1px solid rgba(245, 158, 11, 0.2);
494
+ }
495
+
496
+ .status-error {
497
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.08));
498
+ color: #b91c1c;
499
+ border: 1px solid rgba(239, 68, 68, 0.2);
500
+ }
501
+
502
+ .status-processing {
503
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(29, 78, 216, 0.08));
504
+ color: #1d4ed8;
505
+ border: 1px solid rgba(59, 130, 246, 0.2);
506
+ }
507
+
508
+ .category-tag {
509
+ padding: 0.25rem 0.6rem;
510
+ border-radius: 10px;
511
+ background: rgba(59, 130, 246, 0.08);
512
+ color: var(--text-primary);
513
+ font-size: var(--font-size-xs);
514
+ font-weight: 500;
515
+ }
516
+
517
+ .document-actions {
518
+ display: flex;
519
+ gap: 0.3rem;
520
+ }
521
+
522
+ .action-btn {
523
+ padding: 0.4rem;
524
+ border: none;
525
+ border-radius: var(--border-radius-sm);
526
+ cursor: pointer;
527
+ transition: var(--transition-fast);
528
+ font-size: var(--font-size-xs);
529
+ display: flex;
530
+ align-items: center;
531
+ justify-content: center;
532
+ width: 2rem;
533
+ height: 2rem;
534
+ }
535
+
536
+ .action-btn.view {
537
+ background: rgba(59, 130, 246, 0.1);
538
+ color: #3b82f6;
539
+ }
540
+
541
+ .action-btn.download {
542
+ background: rgba(16, 185, 129, 0.1);
543
+ color: #10b981;
544
+ }
545
+
546
+ .action-btn.delete {
547
+ background: rgba(239, 68, 68, 0.1);
548
+ color: #ef4444;
549
+ }
550
+
551
+ .action-btn:hover {
552
+ transform: scale(1.1);
553
+ }
554
+
555
+ /* Pagination */
556
+ .pagination {
557
+ display: flex;
558
+ justify-content: center;
559
+ align-items: center;
560
+ gap: 0.4rem;
561
+ padding: 1rem;
562
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
563
+ }
564
+
565
+ .pagination-btn {
566
+ padding: 0.4rem 0.6rem;
567
+ border: 1px solid var(--glass-border);
568
+ background: var(--glass-bg);
569
+ color: var(--text-primary);
570
+ border-radius: var(--border-radius-sm);
571
+ cursor: pointer;
572
+ transition: var(--transition-fast);
573
+ font-size: var(--font-size-xs);
574
+ font-family: inherit;
575
+ }
576
+
577
+ .pagination-btn:hover:not(:disabled) {
578
+ background: var(--primary-gradient);
579
+ color: white;
580
+ border-color: transparent;
581
+ }
582
+
583
+ .pagination-btn.active {
584
+ background: var(--primary-gradient);
585
+ color: white;
586
+ border-color: transparent;
587
+ }
588
+
589
+ .pagination-btn:disabled {
590
+ opacity: 0.4;
591
+ cursor: not-allowed;
592
+ }
593
+
594
+ /* Loading States */
595
+ .loading-container {
596
+ padding: 3rem 1rem;
597
+ text-align: center;
598
+ color: var(--text-secondary);
599
+ }
600
+
601
+ .loading-spinner {
602
+ width: 2rem;
603
+ height: 2rem;
604
+ border: 2px solid rgba(59, 130, 246, 0.1);
605
+ border-radius: 50%;
606
+ border-top-color: #3b82f6;
607
+ animation: spin 1s linear infinite;
608
+ margin: 0 auto 1rem;
609
+ }
610
+
611
+ @keyframes spin {
612
+ to { transform: rotate(360deg); }
613
+ }
614
+
615
+ /* Empty State */
616
+ .empty-state {
617
+ padding: 3rem 1rem;
618
+ text-align: center;
619
+ color: var(--text-secondary);
620
+ }
621
+
622
+ .empty-icon {
623
+ font-size: 3rem;
624
+ margin-bottom: 1rem;
625
+ opacity: 0.4;
626
+ color: var(--text-muted);
627
+ }
628
+
629
+ .empty-title {
630
+ font-size: var(--font-size-lg);
631
+ font-weight: 600;
632
+ margin-bottom: 0.5rem;
633
+ color: var(--text-primary);
634
+ }
635
+
636
+ .empty-description {
637
+ font-size: var(--font-size-sm);
638
+ margin-bottom: 1.5rem;
639
+ max-width: 400px;
640
+ margin-left: auto;
641
+ margin-right: auto;
642
+ }
643
+
644
+ /* Modal */
645
+ .modal {
646
+ position: fixed;
647
+ top: 0;
648
+ left: 0;
649
+ width: 100%;
650
+ height: 100%;
651
+ background: rgba(0, 0, 0, 0.7);
652
+ backdrop-filter: blur(5px);
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: center;
656
+ z-index: 10000;
657
+ opacity: 0;
658
+ visibility: hidden;
659
+ transition: all 0.3s ease;
660
+ }
661
+
662
+ .modal.show {
663
+ opacity: 1;
664
+ visibility: visible;
665
+ }
666
+
667
+ .modal-content {
668
+ background: var(--card-bg);
669
+ border-radius: var(--border-radius);
670
+ box-shadow: var(--shadow-lg);
671
+ max-width: 80vw;
672
+ max-height: 80vh;
673
+ width: 100%;
674
+ overflow: hidden;
675
+ transform: scale(0.9);
676
+ transition: all 0.3s ease;
677
+ }
678
+
679
+ .modal.show .modal-content {
680
+ transform: scale(1);
681
+ }
682
+
683
+ .modal-header {
684
+ padding: 1.5rem;
685
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
686
+ display: flex;
687
+ justify-content: space-between;
688
+ align-items: center;
689
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.02), rgba(255, 255, 255, 0.1));
690
+ }
691
+
692
+ .modal-title {
693
+ font-size: var(--font-size-lg);
694
+ font-weight: 700;
695
+ color: var(--text-primary);
696
+ }
697
+
698
+ .modal-close {
699
+ background: none;
700
+ border: none;
701
+ font-size: 1.5rem;
702
+ color: var(--text-secondary);
703
+ cursor: pointer;
704
+ transition: var(--transition-fast);
705
+ }
706
+
707
+ .modal-close:hover {
708
+ color: var(--text-primary);
709
+ }
710
+
711
+ .modal-body {
712
+ padding: 1.5rem;
713
+ max-height: 60vh;
714
+ overflow-y: auto;
715
+ }
716
+
717
+ .document-metadata {
718
+ display: grid;
719
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
720
+ gap: 1rem;
721
+ margin-bottom: 1.5rem;
722
+ }
723
+
724
+ .metadata-item {
725
+ padding: 0.8rem;
726
+ background: rgba(0, 0, 0, 0.02);
727
+ border-radius: var(--border-radius-sm);
728
+ }
729
+
730
+ .metadata-label {
731
+ font-size: var(--font-size-xs);
732
+ color: var(--text-secondary);
733
+ font-weight: 600;
734
+ margin-bottom: 0.3rem;
735
+ }
736
+
737
+ .metadata-value {
738
+ font-size: var(--font-size-sm);
739
+ color: var(--text-primary);
740
+ font-weight: 500;
741
+ }
742
+
743
+ .ocr-text {
744
+ background: rgba(0, 0, 0, 0.02);
745
+ border-radius: var(--border-radius-sm);
746
+ padding: 1rem;
747
+ font-family: 'Vazirmatn', monospace;
748
+ font-size: var(--font-size-sm);
749
+ line-height: 1.8;
750
+ color: var(--text-primary);
751
+ border: 1px solid rgba(0, 0, 0, 0.05);
752
+ white-space: pre-wrap;
753
+ direction: rtl;
754
+ max-height: 300px;
755
+ overflow-y: auto;
756
+ }
757
+
758
+ /* Toast Notifications */
759
+ .toast-container {
760
+ position: fixed;
761
+ top: 1rem;
762
+ left: 1rem;
763
+ z-index: 10001;
764
+ display: flex;
765
+ flex-direction: column;
766
+ gap: 0.5rem;
767
+ }
768
+
769
+ .toast {
770
+ background: var(--card-bg);
771
+ border-radius: var(--border-radius-sm);
772
+ padding: 1rem 1.5rem;
773
+ box-shadow: var(--shadow-lg);
774
+ border-left: 4px solid;
775
+ display: flex;
776
+ align-items: center;
777
+ gap: 0.8rem;
778
+ min-width: 300px;
779
+ transform: translateX(-100%);
780
+ transition: all 0.3s ease;
781
+ }
782
+
783
+ .toast.show {
784
+ transform: translateX(0);
785
+ }
786
+
787
+ .toast.success { border-left-color: #10b981; }
788
+ .toast.error { border-left-color: #ef4444; }
789
+ .toast.warning { border-left-color: #f59e0b; }
790
+ .toast.info { border-left-color: #3b82f6; }
791
+
792
+ .toast-icon {
793
+ font-size: 1.2rem;
794
+ }
795
+
796
+ .toast.success .toast-icon { color: #10b981; }
797
+ .toast.error .toast-icon { color: #ef4444; }
798
+ .toast.warning .toast-icon { color: #f59e0b; }
799
+ .toast.info .toast-icon { color: #3b82f6; }
800
+
801
+ .toast-content {
802
+ flex: 1;
803
+ }
804
+
805
+ .toast-title {
806
+ font-weight: 600;
807
+ font-size: var(--font-size-sm);
808
+ margin-bottom: 0.2rem;
809
+ }
810
+
811
+ .toast-message {
812
+ font-size: var(--font-size-xs);
813
+ color: var(--text-secondary);
814
+ }
815
+
816
+ .toast-close {
817
+ background: none;
818
+ border: none;
819
+ color: var(--text-secondary);
820
+ cursor: pointer;
821
+ font-size: 1rem;
822
+ transition: var(--transition-fast);
823
+ }
824
+
825
+ .toast-close:hover {
826
+ color: var(--text-primary);
827
+ }
828
+
829
+ /* واکنش‌گرایی */
830
+ @media (max-width: 992px) {
831
+ .sidebar {
832
+ transform: translateX(100%);
833
+ transition: transform 0.3s ease;
834
+ }
835
+
836
+ .sidebar.open {
837
+ transform: translateX(0);
838
+ }
839
+
840
+ .main-content {
841
+ margin-right: 0;
842
+ width: 100%;
843
+ padding: 1rem;
844
+ }
845
+
846
+ .page-header {
847
+ flex-direction: column;
848
+ align-items: flex-start;
849
+ gap: 1rem;
850
+ }
851
+
852
+ .page-actions {
853
+ width: 100%;
854
+ justify-content: flex-start;
855
+ flex-wrap: wrap;
856
+ }
857
+
858
+ .filters-grid {
859
+ grid-template-columns: 1fr;
860
+ }
861
+
862
+ .documents-header {
863
+ flex-direction: column;
864
+ align-items: flex-start;
865
+ gap: 1rem;
866
+ }
867
+
868
+ .documents-stats {
869
+ width: 100%;
870
+ }
871
+ }
872
+
873
+ @media (max-width: 768px) {
874
+ .main-content {
875
+ padding: 0.8rem;
876
+ }
877
+
878
+ .documents-table {
879
+ font-size: var(--font-size-xs);
880
+ }
881
+
882
+ .documents-table th,
883
+ .documents-table td {
884
+ padding: 0.6rem;
885
+ }
886
+
887
+ .modal-content {
888
+ max-width: 95vw;
889
+ max-height: 90vh;
890
+ }
891
+
892
+ .document-metadata {
893
+ grid-template-columns: 1fr;
894
+ }
895
+ }
896
+ </style>
897
+ </head>
898
+ <body>
899
+ <div class="dashboard-container">
900
+ <!-- سایدبار -->
901
+ <aside class="sidebar" id="sidebar">
902
+ <div class="sidebar-header">
903
+ <a href="/" class="logo">
904
+ <div class="logo-icon">
905
+ <i class="fas fa-scale-balanced"></i>
906
+ </div>
907
+ <div class="logo-text">سامانه حقوقی</div>
908
+ </a>
909
+ </div>
910
+
911
+ <nav>
912
+ <div class="nav-section">
913
+ <h6 class="nav-title">داشبورد</h6>
914
+ <ul class="nav-menu">
915
+ <li class="nav-item">
916
+ <a href="/" class="nav-link">
917
+ <i class="fas fa-chart-pie nav-icon"></i>
918
+ <span>نمای کلی</span>
919
+ </a>
920
+ </li>
921
+ </ul>
922
+ </div>
923
+
924
+ <div class="nav-section">
925
+ <h6 class="nav-title">مدیریت اسناد</h6>
926
+ <ul class="nav-menu">
927
+ <li class="nav-item">
928
+ <a href="/static/documents.html" class="nav-link active">
929
+ <i class="fas fa-file-alt nav-icon"></i>
930
+ <span>مدیریت اسناد</span>
931
+ <span class="nav-badge" id="totalDocumentsBadge">...</span>
932
+ </a>
933
+ </li>
934
+
935
+ <li class="nav-item">
936
+ <a href="/static/upload.html" class="nav-link">
937
+ <i class="fas fa-cloud-upload-alt nav-icon"></i>
938
+ <span>آپلود فایل</span>
939
+ </a>
940
+ </li>
941
+
942
+ <li class="nav-item">
943
+ <a href="/static/search.html" class="nav-link">
944
+ <i class="fas fa-search nav-icon"></i>
945
+ <span>جستجو</span>
946
+ </a>
947
+ </li>
948
+ </ul>
949
+ </div>
950
+
951
+ <div class="nav-section">
952
+ <h6 class="nav-title">ابزارها</h6>
953
+ <ul class="nav-menu">
954
+ <li class="nav-item">
955
+ <a href="/static/scraping.html" class="nav-link">
956
+ <i class="fas fa-globe nav-icon"></i>
957
+ <span>استخراج محتوا</span>
958
+ </a>
959
+ </li>
960
+
961
+ <li class="nav-item">
962
+ <a href="/static/analytics.html" class="nav-link">
963
+ <i class="fas fa-chart-line nav-icon"></i>
964
+ <span>آمار و تحلیل</span>
965
+ </a>
966
+ </li>
967
+
968
+ <li class="nav-item">
969
+ <a href="/static/reports.html" class="nav-link">
970
+ <i class="fas fa-file-export nav-icon"></i>
971
+ <span>گزارش‌ها</span>
972
+ </a>
973
+ </li>
974
+ </ul>
975
+ </div>
976
+
977
+ <div class="nav-section">
978
+ <h6 class="nav-title">تنظیمات</h6>
979
+ <ul class="nav-menu">
980
+ <li class="nav-item">
981
+ <a href="/static/settings.html" class="nav-link">
982
+ <i class="fas fa-cog nav-icon"></i>
983
+ <span>تنظیمات</span>
984
+ </a>
985
+ </li>
986
+ <li class="nav-item">
987
+ <a href="#" class="nav-link">
988
+ <i class="fas fa-sign-out-alt nav-icon"></i>
989
+ <span>خروج</span>
990
+ </a>
991
+ </li>
992
+ </ul>
993
+ </div>
994
+ </nav>
995
+ </aside>
996
+
997
+ <!-- محتوای اصلی -->
998
+ <main class="main-content">
999
+ <!-- هدر صفحه -->
1000
+ <header class="page-header">
1001
+ <h1 class="page-title">
1002
+ <i class="fas fa-file-alt"></i>
1003
+ مدیریت اسناد حقوقی
1004
+ </h1>
1005
+ <div class="page-actions">
1006
+ <a href="/static/upload.html" class="btn btn-primary">
1007
+ <i class="fas fa-plus"></i>
1008
+ آپلود سند جدید
1009
+ </a>
1010
+ <button type="button" class="btn btn-outline" onclick="refreshDocuments()">
1011
+ <i class="fas fa-refresh"></i>
1012
+ به‌روزرسانی
1013
+ </button>
1014
+ <button type="button" class="btn btn-outline" onclick="exportDocuments()">
1015
+ <i class="fas fa-download"></i>
1016
+ خروجی Excel
1017
+ </button>
1018
+ </div>
1019
+ </header>
1020
+
1021
+ <!-- فیلترها -->
1022
+ <section class="filters-section">
1023
+ <h3 class="filters-title">فیلتر و جستجو</h3>
1024
+ <div class="filters-grid">
1025
+ <div class="filter-group">
1026
+ <label class="filter-label">جستجو در اسناد</label>
1027
+ <input type="text" class="filter-input" id="searchFilter" placeholder="نام فایل، محتوا، خلاصه...">
1028
+ </div>
1029
+ <div class="filter-group">
1030
+ <label class="filter-label">دسته‌بندی</label>
1031
+ <select class="filter-select" id="categoryFilter">
1032
+ <option value="">همه دسته‌ها</option>
1033
+ <option value="قراردادها">قراردادها</option>
1034
+ <option value="دادخواست‌ها">دادخواست‌ها</option>
1035
+ <option value="احکام قضایی">احکام قضایی</option>
1036
+ <option value="آرای دیوان">آرای دیوان</option>
1037
+ <option value="سایر">سایر</option>
1038
+ </select>
1039
+ </div>
1040
+ <div class="filter-group">
1041
+ <label class="filter-label">وضعیت</label>
1042
+ <select class="filter-select" id="statusFilter">
1043
+ <option value="">همه وضعیت‌ها</option>
1044
+ <option value="processed">پردازش شده</option>
1045
+ <option value="processing">در حال پردازش</option>
1046
+ <option value="pending">در انتظار</option>
1047
+ <option value="error">خطا</option>
1048
+ </select>
1049
+ </div>
1050
+ <div class="filter-group">
1051
+ <label class="filter-label">حداقل کیفیت</label>
1052
+ <select class="filter-select" id="qualityFilter">
1053
+ <option value="">همه کیفیت‌ها</option>
1054
+ <option value="8.5">عالی (8.5+)</option>
1055
+ <option value="6.5">خوب (6.5+)</option>
1056
+ <option value="4.5">متوسط (4.5+)</option>
1057
+ <option value="0">ضعیف (کمتر از 4.5)</option>
1058
+ </select>
1059
+ </div>
1060
+ </div>
1061
+ <div class="filters-actions">
1062
+ <button type="button" class="btn btn-primary btn-sm" onclick="applyFilters()">
1063
+ <i class="fas fa-filter"></i>
1064
+ اعمال فیلتر
1065
+ </button>
1066
+ <button type="button" class="btn btn-outline btn-sm" onclick="resetFilters()">
1067
+ <i class="fas fa-undo"></i>
1068
+ پاک کردن فیلترها
1069
+ </button>
1070
+ </div>
1071
+ </section>
1072
+
1073
+ <!-- جدول اسناد -->
1074
+ <section class="documents-section">
1075
+ <div class="documents-header">
1076
+ <h3 class="documents-title">لیست اسناد</h3>
1077
+ <div class="documents-stats">
1078
+ <span>کل: <strong id="totalCount">...</strong></span>
1079
+ <span>پردازش شده: <strong id="processedCount">...</strong></span>
1080
+ <span>در انتظار: <strong id="pendingCount">...</strong></span>
1081
+ <span>خطا: <strong id="errorCount">...</strong></span>
1082
+ </div>
1083
+ </div>
1084
+
1085
+ <div id="tableContainer">
1086
+ <!-- Loading State -->
1087
+ <div class="loading-container" id="loadingContainer">
1088
+ <div class="loading-spinner"></div>
1089
+ <p>در حال بارگذاری اسناد...</p>
1090
+ </div>
1091
+
1092
+ <!-- Documents Table -->
1093
+ <table class="documents-table" id="documentsTable" style="display: none;">
1094
+ <thead>
1095
+ <tr>
1096
+ <th>عنوان سند</th>
1097
+ <th>دسته‌بندی</th>
1098
+ <th>امتیاز کیفیت</th>
1099
+ <th>تاریخ ایجاد</th>
1100
+ <th>وضعیت</th>
1101
+ <th>اندازه فایل</th>
1102
+ <th>عملیات</th>
1103
+ </tr>
1104
+ </thead>
1105
+ <tbody id="documentsTableBody">
1106
+ <!-- Data will be loaded here -->
1107
+ </tbody>
1108
+ </table>
1109
+
1110
+ <!-- Empty State -->
1111
+ <div class="empty-state" id="emptyState" style="display: none;">
1112
+ <i class="fas fa-inbox empty-icon"></i>
1113
+ <div class="empty-title">هیچ سندی یافت نشد</div>
1114
+ <div class="empty-description">اسناد جدید آپلود کنید یا فیلترها را تغییر دهید</div>
1115
+ <a href="/static/upload.html" class="btn btn-primary">
1116
+ <i class="fas fa-plus"></i>
1117
+ آپلود سند جدید
1118
+ </a>
1119
+ </div>
1120
+
1121
+ <div class="pagination" id="pagination">
1122
+ <!-- Pagination will be populated by JavaScript -->
1123
+ </div>
1124
+ </div>
1125
+ </section>
1126
+ </main>
1127
+ </div>
1128
+
1129
+ <!-- Modal برای نمایش جزئیات سند -->
1130
+ <div class="modal" id="documentModal">
1131
+ <div class="modal-content">
1132
+ <div class="modal-header">
1133
+ <h3 class="modal-title" id="modalTitle">جزئیات سند</h3>
1134
+ <button type="button" class="modal-close" onclick="closeModal()">
1135
+ <i class="fas fa-times"></i>
1136
+ </button>
1137
+ </div>
1138
+ <div class="modal-body">
1139
+ <div class="document-metadata" id="documentMetadata">
1140
+ <!-- Document metadata will be loaded here -->
1141
+ </div>
1142
+ <div class="ocr-text" id="ocrText">
1143
+ <!-- OCR text will be loaded here -->
1144
+ </div>
1145
+ </div>
1146
+ </div>
1147
+ </div>
1148
+
1149
+ <!-- Toast Container -->
1150
+ <div class="toast-container" id="toastContainer"></div>
1151
+
1152
+ <script>
1153
+ // Global variables
1154
+ let currentPage = 1;
1155
+ let itemsPerPage = 10;
1156
+ let totalDocuments = 0;
1157
+ let currentFilters = {};
1158
+ let documents = [];
1159
+ let isOnline = false;
1160
+
1161
+ // Initialize page
1162
+ document.addEventListener('DOMContentLoaded', function() {
1163
+ console.log('📄 Documents page loading...');
1164
+ initializeDocumentsPage();
1165
+ });
1166
+
1167
+ async function initializeDocumentsPage() {
1168
+ try {
1169
+ // Test backend connection
1170
+ isOnline = await testConnection();
1171
+
1172
+ // Load documents from backend
1173
+ await loadDocuments();
1174
+
1175
+ // Setup event listeners
1176
+ setupEventListeners();
1177
+
1178
+ showToast('صفحه مدیریت اسناد بارگذاری شد', 'success', 'بارگذاری موفق');
1179
+
1180
+ } catch (error) {
1181
+ console.error('Failed to initialize documents page:', error);
1182
+
1183
+ // Fallback to mock data
1184
+ await loadFallbackDocuments();
1185
+ setupEventListeners();
1186
+
1187
+ showToast('حالت آفلاین فعال است', 'warning', 'اتصال ناموفق');
1188
+ }
1189
+ }
1190
+
1191
+ async function testConnection() {
1192
+ try {
1193
+ await window.legalAPI.healthCheck();
1194
+ return true;
1195
+ } catch (error) {
1196
+ return false;
1197
+ }
1198
+ }
1199
+
1200
+ async function loadDocuments() {
1201
+ try {
1202
+ showLoading(true);
1203
+
1204
+ // Get filter parameters from URL
1205
+ const urlParams = new URLSearchParams(window.location.search);
1206
+ const filters = {
1207
+ page: currentPage,
1208
+ limit: itemsPerPage,
1209
+ search: urlParams.get('search') || currentFilters.search || '',
1210
+ category: currentFilters.category || '',
1211
+ status: currentFilters.status || '',
1212
+ min_quality: currentFilters.min_quality || ''
1213
+ };
1214
+
1215
+ // Apply URL search to filter
1216
+ if (filters.search) {
1217
+ document.getElementById('searchFilter').value = filters.search;
1218
+ }
1219
+
1220
+ const response = await window.legalAPI.getDocuments(filters);
1221
+
1222
+ documents = response.documents.map(doc => new DocumentModel(doc));
1223
+ totalDocuments = response.pagination.total;
1224
+
1225
+ displayDocuments();
1226
+ updateStats();
1227
+ updatePagination(response.pagination);
1228
+
1229
+ console.log('✅ Real documents loaded:', documents.length);
1230
+
1231
+ } catch (error) {
1232
+ console.error('Failed to load documents:', error);
1233
+ throw error;
1234
+ } finally {
1235
+ showLoading(false);
1236
+ }
1237
+ }
1238
+
1239
+ async function loadFallbackDocuments() {
1240
+ // Fallback mock data
1241
+ const mockDocuments = [
1242
+ {
1243
+ id: 1,
1244
+ filename: "contract_001.pdf",
1245
+ original_filename: "قرارداد خرید املاک.pdf",
1246
+ file_size: 1024000,
1247
+ category: "قراردادها",
1248
+ quality_score: 8.5,
1249
+ status: "processed",
1250
+ created_at: "2024-01-15T10:30:00Z",
1251
+ ocr_text: "قرارداد خرید و فروش ملک واقع در تهران...",
1252
+ summary: "قرارداد خرید املاک مسکونی در منطقه 3 تهران"
1253
+ },
1254
+ {
1255
+ id: 2,
1256
+ filename: "lawsuit_002.pdf",
1257
+ original_filename: "دادخواست طلاق.pdf",
1258
+ file_size: 512000,
1259
+ category: "دادخواست‌ها",
1260
+ quality_score: 7.8,
1261
+ status: "processed",
1262
+ created_at: "2024-01-14T14:20:00Z",
1263
+ ocr_text: "دادخواست طلاق به درخواست زوجه...",
1264
+ summary: "درخواست طلاق توافقی با تقسیم اموال"
1265
+ }
1266
+ ];
1267
+
1268
+ documents = mockDocuments.map(doc => new DocumentModel(doc));
1269
+ totalDocuments = documents.length;
1270
+
1271
+ showLoading(false);
1272
+ displayDocuments();
1273
+ updateStats();
1274
+ updatePagination({page: 1, limit: itemsPerPage, total: totalDocuments, pages: 1});
1275
+
1276
+ console.log('⚠️ Using fallback documents data');
1277
+ }
1278
+
1279
+ function displayDocuments() {
1280
+ const tbody = document.getElementById('documentsTableBody');
1281
+ const table = document.getElementById('documentsTable');
1282
+ const emptyState = document.getElementById('emptyState');
1283
+
1284
+ if (documents.length === 0) {
1285
+ table.style.display = 'none';
1286
+ emptyState.style.display = 'block';
1287
+ return;
1288
+ }
1289
+
1290
+ table.style.display = 'table';
1291
+ emptyState.style.display = 'none';
1292
+
1293
+ tbody.innerHTML = documents.map(doc => `
1294
+ <tr onclick="viewDocument(${doc.id})" style="cursor: pointer;">
1295
+ <td>
1296
+ <div class="document-title">
1297
+ <div class="doc-icon">
1298
+ <i class="fas fa-file-pdf"></i>
1299
+ </div>
1300
+ <div>
1301
+ <div>${doc.original_filename}</div>
1302
+ <small style="color: var(--text-muted);">${doc.filename}</small>
1303
+ </div>
1304
+ </div>
1305
+ </td>
1306
+ <td>
1307
+ <span class="category-tag">${doc.category}</span>
1308
+ </td>
1309
+ <td>
1310
+ <div class="quality-display">
1311
+ <div class="quality-score">${doc.quality_score ? doc.quality_score.toFixed(1) : '0.0'}</div>
1312
+ <div class="quality-bar">
1313
+ <div class="quality-fill ${doc.getQualityClass()}"
1314
+ style="width: ${(doc.quality_score || 0) * 10}%"></div>
1315
+ </div>
1316
+ </div>
1317
+ </td>
1318
+ <td>${doc.getFormattedDate()}</td>
1319
+ <td>
1320
+ <span class="status-badge status-${doc.status}">
1321
+ <i class="fas fa-${doc.getStatusIcon()}"></i>
1322
+ ${doc.getStatusText()}
1323
+ </span>
1324
+ </td>
1325
+ <td>${doc.getFormattedFileSize()}</td>
1326
+ <td>
1327
+ <div class="document-actions">
1328
+ <button type="button" class="action-btn view" onclick="event.stopPropagation(); viewDocument(${doc.id})" title="مشاهده">
1329
+ <i class="fas fa-eye"></i>
1330
+ </button>
1331
+ <button type="button" class="action-btn download" onclick="event.stopPropagation(); downloadDocument(${doc.id})" title="دانلود">
1332
+ <i class="fas fa-download"></i>
1333
+ </button>
1334
+ <button type="button" class="action-btn delete" onclick="event.stopPropagation(); deleteDocument(${doc.id})" title="حذف">
1335
+ <i class="fas fa-trash"></i>
1336
+ </button>
1337
+ </div>
1338
+ </td>
1339
+ </tr>
1340
+ `).join('');
1341
+ }
1342
+
1343
+ function setupEventListeners() {
1344
+ // Filter inputs
1345
+ document.getElementById('searchFilter').addEventListener('input', debounce(applyFilters, 300));
1346
+ document.getElementById('categoryFilter').addEventListener('change', applyFilters);
1347
+ document.getElementById('statusFilter').addEventListener('change', applyFilters);
1348
+ document.getElementById('qualityFilter').addEventListener('change', applyFilters);
1349
+
1350
+ // Modal events
1351
+ document.addEventListener('click', (e) => {
1352
+ const modal = document.getElementById('documentModal');
1353
+ if (e.target === modal) {
1354
+ closeModal();
1355
+ }
1356
+ });
1357
+
1358
+ document.addEventListener('keydown', (e) => {
1359
+ if (e.key === 'Escape') {
1360
+ closeModal();
1361
+ }
1362
+ });
1363
+ }
1364
+
1365
+ async function applyFilters() {
1366
+ currentFilters = {
1367
+ search: document.getElementById('searchFilter').value.trim(),
1368
+ category: document.getElementById('categoryFilter').value,
1369
+ status: document.getElementById('statusFilter').value,
1370
+ min_quality: document.getElementById('qualityFilter').value
1371
+ };
1372
+
1373
+ currentPage = 1; // Reset to first page
1374
+ await loadDocuments();
1375
+ }
1376
+
1377
+ function resetFilters() {
1378
+ document.getElementById('searchFilter').value = '';
1379
+ document.getElementById('categoryFilter').value = '';
1380
+ document.getElementById('statusFilter').value = '';
1381
+ document.getElementById('qualityFilter').value = '';
1382
+
1383
+ currentFilters = {};
1384
+ currentPage = 1;
1385
+
1386
+ loadDocuments();
1387
+ showToast('فیلترها پاک شدند', 'info', 'بازنشانی');
1388
+ }
1389
+
1390
+ async function refreshDocuments() {
1391
+ await loadDocuments();
1392
+ showToast('لیست اسناد به‌روزرسانی شد', 'success', 'به‌روزرسانی');
1393
+ }
1394
+
1395
+ async function viewDocument(documentId) {
1396
+ try {
1397
+ let document;
1398
+
1399
+ if (isOnline) {
1400
+ document = await window.legalAPI.getDocument(documentId);
1401
+ } else {
1402
+ // Find in local documents
1403
+ document = documents.find(d => d.id === documentId);
1404
+ }
1405
+
1406
+ if (!document) {
1407
+ showToast('سند یافت نشد', 'error', 'خطا');
1408
+ return;
1409
+ }
1410
+
1411
+ showDocumentModal(document);
1412
+
1413
+ } catch (error) {
1414
+ console.error('Failed to load document:', error);
1415
+ showToast('خطا در بارگذاری جزئیات سند', 'error', 'خطا');
1416
+ }
1417
+ }
1418
+
1419
+ function showDocumentModal(document) {
1420
+ const modal = document.getElementById('documentModal');
1421
+ const modalTitle = document.getElementById('modalTitle');
1422
+ const documentMetadata = document.getElementById('documentMetadata');
1423
+ const ocrText = document.getElementById('ocrText');
1424
+
1425
+ modalTitle.textContent = document.original_filename || document.filename;
1426
+
1427
+ // Update metadata
1428
+ documentMetadata.innerHTML = `
1429
+ <div class="metadata-item">
1430
+ <div class="metadata-label">نام فایل اصلی</div>
1431
+ <div class="metadata-value">${document.original_filename || document.filename}</div>
1432
+ </div>
1433
+ <div class="metadata-item">
1434
+ <div class="metadata-label">نام فایل سیستم</div>
1435
+ <div class="metadata-value">${document.filename}</div>
1436
+ </div>
1437
+ <div class="metadata-item">
1438
+ <div class="metadata-label">اندازه فایل</div>
1439
+ <div class="metadata-value">${formatFileSize(document.file_size)}</div>
1440
+ </div>
1441
+ <div class="metadata-item">
1442
+ <div class="metadata-label">دسته‌بندی</div>
1443
+ <div class="metadata-value">${document.category}</div>
1444
+ </div>
1445
+ <div class="metadata-item">
1446
+ <div class="metadata-label">امتیاز کیفیت</div>
1447
+ <div class="metadata-value">${document.quality_score ? document.quality_score.toFixed(1) : '0.0'} / 10</div>
1448
+ </div>
1449
+ <div class="metadata-item">
1450
+ <div class="metadata-label">وضعیت</div>
1451
+ <div class="metadata-value">${document.getStatusText ? document.getStatusText() : document.status}</div>
1452
+ </div>
1453
+ <div class="metadata-item">
1454
+ <div class="metadata-label">تاریخ ایجاد</div>
1455
+ <div class="metadata-value">${formatDate(document.created_at)}</div>
1456
+ </div>
1457
+ <div class="metadata-item">
1458
+ <div class="metadata-label">خلاصه محتوا</div>
1459
+ <div class="metadata-value">${document.summary || 'در دسترس نیست'}</div>
1460
+ </div>
1461
+ `;
1462
+
1463
+ // Update OCR text
1464
+ ocrText.textContent = document.ocr_text || 'متن استخراج شده در دسترس نیست.';
1465
+
1466
+ modal.classList.add('show');
1467
+ }
1468
+
1469
+ function closeModal() {
1470
+ const modal = document.getElementById('documentModal');
1471
+ modal.classList.remove('show');
1472
+ }
1473
+
1474
+ async function deleteDocument(documentId) {
1475
+ if (!confirm('آیا از حذف این سند اطمینان دارید؟')) {
1476
+ return;
1477
+ }
1478
+
1479
+ try {
1480
+ if (isOnline) {
1481
+ await window.legalAPI.deleteDocument(documentId);
1482
+ }
1483
+
1484
+ // Remove from local array
1485
+ documents = documents.filter(doc => doc.id !== documentId);
1486
+ totalDocuments = Math.max(0, totalDocuments - 1);
1487
+
1488
+ displayDocuments();
1489
+ updateStats();
1490
+ showToast('سند با موفقیت حذف شد', 'success', 'حذف موفق');
1491
+
1492
+ } catch (error) {
1493
+ console.error('Failed to delete document:', error);
1494
+ showToast('خطا در حذف سند', 'error', 'خطا');
1495
+ }
1496
+ }
1497
+
1498
+ function downloadDocument(documentId) {
1499
+ const doc = documents.find(d => d.id === documentId);
1500
+ if (!doc) return;
1501
+
1502
+ showToast(`دانلود ${doc.original_filename} شروع شد`, 'info', 'دانلود');
1503
+ }
1504
+
1505
+ function exportDocuments() {
1506
+ showToast('فایل Excel در حال آماده‌سازی...', 'info', 'خروجی Excel');
1507
+
1508
+ setTimeout(() => {
1509
+ showToast('فایل Excel آماده شد و دانلود شروع شد', 'success', 'خروجی موفق');
1510
+ }, 2000);
1511
+ }
1512
+
1513
+ function updateStats() {
1514
+ const processedCount = documents.filter(doc => doc.status === 'processed').length;
1515
+ const pendingCount = documents.filter(doc => doc.status === 'processing' || doc.status === 'pending').length;
1516
+ const errorCount = documents.filter(doc => doc.status === 'error').length;
1517
+
1518
+ document.getElementById('totalCount').textContent = totalDocuments;
1519
+ document.getElementById('processedCount').textContent = processedCount;
1520
+ document.getElementById('pendingCount').textContent = pendingCount;
1521
+ document.getElementById('errorCount').textContent = errorCount;
1522
+
1523
+ // Update badge
1524
+ const badge = document.getElementById('totalDocumentsBadge');
1525
+ if (badge) {
1526
+ badge.textContent = totalDocuments;
1527
+ }
1528
+ }
1529
+
1530
+ function updatePagination(pagination) {
1531
+ const paginationContainer = document.getElementById('pagination');
1532
+ const totalPages = pagination.pages;
1533
+
1534
+ if (totalPages <= 1) {
1535
+ paginationContainer.style.display = 'none';
1536
+ return;
1537
+ }
1538
+
1539
+ paginationContainer.style.display = 'flex';
1540
+
1541
+ let paginationHTML = '';
1542
+
1543
+ // Previous button
1544
+ paginationHTML += `
1545
+ <button class="pagination-btn" ${currentPage <= 1 ? 'disabled' : ''}
1546
+ onclick="changePage(${currentPage - 1})">
1547
+ <i class="fas fa-chevron-right"></i>
1548
+ </button>
1549
+ `;
1550
+
1551
+ // Page numbers
1552
+ const startPage = Math.max(1, currentPage - 2);
1553
+ const endPage = Math.min(totalPages, currentPage + 2);
1554
+
1555
+ for (let i = startPage; i <= endPage; i++) {
1556
+ paginationHTML += `
1557
+ <button class="pagination-btn ${i === currentPage ? 'active' : ''}"
1558
+ onclick="changePage(${i})">
1559
+ ${i}
1560
+ </button>
1561
+ `;
1562
+ }
1563
+
1564
+ // Next button
1565
+ paginationHTML += `
1566
+ <button class="pagination-btn" ${currentPage >= totalPages ? 'disabled' : ''}
1567
+ onclick="changePage(${currentPage + 1})">
1568
+ <i class="fas fa-chevron-left"></i>
1569
+ </button>
1570
+ `;
1571
+
1572
+ paginationContainer.innerHTML = paginationHTML;
1573
+ }
1574
+
1575
+ async function changePage(page) {
1576
+ currentPage = page;
1577
+ await loadDocuments();
1578
+ }
1579
+
1580
+ function showLoading(show) {
1581
+ const loading = document.getElementById('loadingContainer');
1582
+ const table = document.getElementById('documentsTable');
1583
+ const emptyState = document.getElementById('emptyState');
1584
+
1585
+ if (show) {
1586
+ loading.style.display = 'block';
1587
+ table.style.display = 'none';
1588
+ emptyState.style.display = 'none';
1589
+ } else {
1590
+ loading.style.display = 'none';
1591
+ }
1592
+ }
1593
+
1594
+ console.log('📄 Documents Management Page Ready!');
1595
+ </script>
1596
+ </body>
1597
+ </html>
app/frontend/enhanced_analytics_dashboard.html ADDED
@@ -0,0 +1,1174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>داشبورد تحلیلی پیشرفته | سامانه هوشمند حقوقی</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
12
+ <style>
13
+ :root {
14
+ /* رنگ‌بندی مدرن و هارمونیک */
15
+ --text-primary: #0f172a;
16
+ --text-secondary: #475569;
17
+ --text-muted: #64748b;
18
+ --text-light: #ffffff;
19
+
20
+ /* پس‌زمینه‌های بهبود یافته */
21
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
22
+ --card-bg: rgba(255, 255, 255, 0.95);
23
+ --glass-bg: rgba(255, 255, 255, 0.9);
24
+ --glass-border: rgba(148, 163, 184, 0.2);
25
+ --sidebar-bg: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
26
+
27
+ /* گرادیان‌های مدرن */
28
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
29
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
30
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
31
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
32
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
33
+
34
+ /* سایه‌های ملایم */
35
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
36
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
37
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
38
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
39
+ --shadow-glow-primary: 0 0 20px rgba(59, 130, 246, 0.15);
40
+ --shadow-glow-success: 0 0 20px rgba(16, 185, 129, 0.15);
41
+ --shadow-glow-warning: 0 0 20px rgba(245, 158, 11, 0.15);
42
+ --shadow-glow-danger: 0 0 20px rgba(239, 68, 68, 0.15);
43
+
44
+ /* متغیرهای کامپکت */
45
+ --sidebar-width: 260px;
46
+ --border-radius: 12px;
47
+ --border-radius-sm: 8px;
48
+ --border-radius-lg: 16px;
49
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
50
+ --transition-fast: all 0.15s ease-in-out;
51
+
52
+ /* فونت‌های کامپکت */
53
+ --font-size-xs: 0.7rem;
54
+ --font-size-sm: 0.8rem;
55
+ --font-size-base: 0.9rem;
56
+ --font-size-lg: 1.1rem;
57
+ --font-size-xl: 1.25rem;
58
+ --font-size-2xl: 1.5rem;
59
+ }
60
+
61
+ * {
62
+ margin: 0;
63
+ padding: 0;
64
+ box-sizing: border-box;
65
+ }
66
+
67
+ body {
68
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
69
+ background: var(--body-bg);
70
+ color: var(--text-primary);
71
+ line-height: 1.6;
72
+ overflow-x: hidden;
73
+ font-size: var(--font-size-base);
74
+ }
75
+
76
+ /* اسکرول‌بار مدرن */
77
+ ::-webkit-scrollbar {
78
+ width: 6px;
79
+ height: 6px;
80
+ }
81
+
82
+ ::-webkit-scrollbar-track {
83
+ background: rgba(0, 0, 0, 0.02);
84
+ border-radius: 10px;
85
+ }
86
+
87
+ ::-webkit-scrollbar-thumb {
88
+ background: rgba(0, 0, 0, 0.1);
89
+ border-radius: 10px;
90
+ }
91
+
92
+ ::-webkit-scrollbar-thumb:hover {
93
+ background: rgba(0, 0, 0, 0.2);
94
+ }
95
+
96
+ /* Layout */
97
+ .dashboard-container {
98
+ display: flex;
99
+ min-height: 100vh;
100
+ }
101
+
102
+ /* Sidebar */
103
+ .sidebar {
104
+ width: var(--sidebar-width);
105
+ background: var(--sidebar-bg);
106
+ padding: 1.5rem;
107
+ position: fixed;
108
+ height: 100vh;
109
+ overflow-y: auto;
110
+ z-index: 1000;
111
+ }
112
+
113
+ .sidebar-header {
114
+ text-align: center;
115
+ margin-bottom: 2rem;
116
+ padding-bottom: 1rem;
117
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
118
+ }
119
+
120
+ .sidebar-header h2 {
121
+ color: var(--text-light);
122
+ font-size: var(--font-size-xl);
123
+ font-weight: 600;
124
+ margin-bottom: 0.5rem;
125
+ }
126
+
127
+ .sidebar-header p {
128
+ color: rgba(255, 255, 255, 0.7);
129
+ font-size: var(--font-size-sm);
130
+ }
131
+
132
+ .nav-menu {
133
+ list-style: none;
134
+ }
135
+
136
+ .nav-item {
137
+ margin-bottom: 0.5rem;
138
+ }
139
+
140
+ .nav-link {
141
+ display: flex;
142
+ align-items: center;
143
+ padding: 0.75rem 1rem;
144
+ color: rgba(255, 255, 255, 0.8);
145
+ text-decoration: none;
146
+ border-radius: var(--border-radius-sm);
147
+ transition: var(--transition-smooth);
148
+ font-size: var(--font-size-sm);
149
+ }
150
+
151
+ .nav-link:hover,
152
+ .nav-link.active {
153
+ background: rgba(255, 255, 255, 0.1);
154
+ color: var(--text-light);
155
+ }
156
+
157
+ .nav-link i {
158
+ margin-left: 0.75rem;
159
+ width: 16px;
160
+ text-align: center;
161
+ }
162
+
163
+ /* Main Content */
164
+ .main-content {
165
+ flex: 1;
166
+ margin-right: var(--sidebar-width);
167
+ padding: 2rem;
168
+ }
169
+
170
+ .page-header {
171
+ margin-bottom: 2rem;
172
+ }
173
+
174
+ .page-title {
175
+ font-size: var(--font-size-2xl);
176
+ font-weight: 700;
177
+ color: var(--text-primary);
178
+ margin-bottom: 0.5rem;
179
+ }
180
+
181
+ .page-subtitle {
182
+ color: var(--text-secondary);
183
+ font-size: var(--font-size-base);
184
+ }
185
+
186
+ /* Cards */
187
+ .cards-grid {
188
+ display: grid;
189
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
190
+ gap: 1.5rem;
191
+ margin-bottom: 2rem;
192
+ }
193
+
194
+ .card {
195
+ background: var(--card-bg);
196
+ border-radius: var(--border-radius-lg);
197
+ padding: 1.5rem;
198
+ box-shadow: var(--shadow-sm);
199
+ border: 1px solid var(--glass-border);
200
+ transition: var(--transition-smooth);
201
+ }
202
+
203
+ .card:hover {
204
+ box-shadow: var(--shadow-md);
205
+ transform: translateY(-2px);
206
+ }
207
+
208
+ .card-header {
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: space-between;
212
+ margin-bottom: 1rem;
213
+ }
214
+
215
+ .card-title {
216
+ font-size: var(--font-size-lg);
217
+ font-weight: 600;
218
+ color: var(--text-primary);
219
+ }
220
+
221
+ .card-icon {
222
+ width: 40px;
223
+ height: 40px;
224
+ border-radius: 50%;
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ font-size: 1.2rem;
229
+ color: white;
230
+ }
231
+
232
+ .card-icon.primary { background: var(--primary-gradient); }
233
+ .card-icon.success { background: var(--success-gradient); }
234
+ .card-icon.warning { background: var(--warning-gradient); }
235
+ .card-icon.danger { background: var(--danger-gradient); }
236
+
237
+ .metric-value {
238
+ font-size: 2rem;
239
+ font-weight: 700;
240
+ margin-bottom: 0.5rem;
241
+ }
242
+
243
+ .metric-label {
244
+ color: var(--text-secondary);
245
+ font-size: var(--font-size-sm);
246
+ }
247
+
248
+ .metric-change {
249
+ display: flex;
250
+ align-items: center;
251
+ font-size: var(--font-size-sm);
252
+ margin-top: 0.5rem;
253
+ }
254
+
255
+ .metric-change.positive {
256
+ color: #10b981;
257
+ }
258
+
259
+ .metric-change.negative {
260
+ color: #ef4444;
261
+ }
262
+
263
+ /* Charts */
264
+ .charts-grid {
265
+ display: grid;
266
+ grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
267
+ gap: 1.5rem;
268
+ margin-bottom: 2rem;
269
+ }
270
+
271
+ .chart-card {
272
+ background: var(--card-bg);
273
+ border-radius: var(--border-radius-lg);
274
+ padding: 1.5rem;
275
+ box-shadow: var(--shadow-sm);
276
+ border: 1px solid var(--glass-border);
277
+ }
278
+
279
+ .chart-container {
280
+ position: relative;
281
+ height: 300px;
282
+ }
283
+
284
+ /* Alerts */
285
+ .alerts-section {
286
+ margin-bottom: 2rem;
287
+ }
288
+
289
+ .alert {
290
+ padding: 1rem;
291
+ border-radius: var(--border-radius-sm);
292
+ margin-bottom: 1rem;
293
+ display: flex;
294
+ align-items: center;
295
+ }
296
+
297
+ .alert.warning {
298
+ background: rgba(245, 158, 11, 0.1);
299
+ border: 1px solid rgba(245, 158, 11, 0.3);
300
+ color: #92400e;
301
+ }
302
+
303
+ .alert.error {
304
+ background: rgba(239, 68, 68, 0.1);
305
+ border: 1px solid rgba(239, 68, 68, 0.3);
306
+ color: #991b1b;
307
+ }
308
+
309
+ .alert.success {
310
+ background: rgba(16, 185, 129, 0.1);
311
+ border: 1px solid rgba(16, 185, 129, 0.3);
312
+ color: #065f46;
313
+ }
314
+
315
+ .alert i {
316
+ margin-left: 0.75rem;
317
+ font-size: 1.2rem;
318
+ }
319
+
320
+ /* Tables */
321
+ .table-container {
322
+ background: var(--card-bg);
323
+ border-radius: var(--border-radius-lg);
324
+ padding: 1.5rem;
325
+ box-shadow: var(--shadow-sm);
326
+ border: 1px solid var(--glass-border);
327
+ margin-bottom: 2rem;
328
+ }
329
+
330
+ .table-header {
331
+ display: flex;
332
+ justify-content: space-between;
333
+ align-items: center;
334
+ margin-bottom: 1rem;
335
+ }
336
+
337
+ .table-title {
338
+ font-size: var(--font-size-lg);
339
+ font-weight: 600;
340
+ color: var(--text-primary);
341
+ }
342
+
343
+ table {
344
+ width: 100%;
345
+ border-collapse: collapse;
346
+ }
347
+
348
+ th, td {
349
+ padding: 0.75rem;
350
+ text-align: right;
351
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
352
+ }
353
+
354
+ th {
355
+ font-weight: 600;
356
+ color: var(--text-secondary);
357
+ font-size: var(--font-size-sm);
358
+ }
359
+
360
+ td {
361
+ color: var(--text-primary);
362
+ }
363
+
364
+ /* Loading */
365
+ .loading {
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ padding: 2rem;
370
+ color: var(--text-secondary);
371
+ }
372
+
373
+ .loading i {
374
+ margin-left: 0.5rem;
375
+ animation: spin 1s linear infinite;
376
+ }
377
+
378
+ @keyframes spin {
379
+ from { transform: rotate(0deg); }
380
+ to { transform: rotate(360deg); }
381
+ }
382
+
383
+ /* Responsive */
384
+ @media (max-width: 768px) {
385
+ .sidebar {
386
+ transform: translateX(-100%);
387
+ transition: var(--transition-smooth);
388
+ }
389
+
390
+ .sidebar.open {
391
+ transform: translateX(0);
392
+ }
393
+
394
+ .main-content {
395
+ margin-right: 0;
396
+ padding: 1rem;
397
+ }
398
+
399
+ .cards-grid {
400
+ grid-template-columns: 1fr;
401
+ }
402
+
403
+ .charts-grid {
404
+ grid-template-columns: 1fr;
405
+ }
406
+ }
407
+
408
+ /* Utility Classes */
409
+ .text-center { text-align: center; }
410
+ .text-right { text-align: right; }
411
+ .text-left { text-align: left; }
412
+ .mb-1 { margin-bottom: 0.5rem; }
413
+ .mb-2 { margin-bottom: 1rem; }
414
+ .mb-3 { margin-bottom: 1.5rem; }
415
+ .mt-1 { margin-top: 0.5rem; }
416
+ .mt-2 { margin-top: 1rem; }
417
+ .mt-3 { margin-top: 1.5rem; }
418
+ </style>
419
+ </head>
420
+ <body>
421
+ <div class="dashboard-container">
422
+ <!-- Sidebar -->
423
+ <aside class="sidebar">
424
+ <div class="sidebar-header">
425
+ <h2>📊 تحلیلات پیشرفته</h2>
426
+ <p>داشبورد هوشمند</p>
427
+ </div>
428
+ <nav class="nav-menu">
429
+ <div class="nav-item">
430
+ <a href="#overview" class="nav-link active" onclick="showSection('overview')">
431
+ <i class="fas fa-chart-line"></i>
432
+ نمای کلی
433
+ </a>
434
+ </div>
435
+ <div class="nav-item">
436
+ <a href="#trends" class="nav-link" onclick="showSection('trends')">
437
+ <i class="fas fa-chart-area"></i>
438
+ روندها
439
+ </a>
440
+ </div>
441
+ <div class="nav-item">
442
+ <a href="#predictions" class="nav-link" onclick="showSection('predictions')">
443
+ <i class="fas fa-crystal-ball"></i>
444
+ پیش‌بینی‌ها
445
+ </a>
446
+ </div>
447
+ <div class="nav-item">
448
+ <a href="#quality" class="nav-link" onclick="showSection('quality')">
449
+ <i class="fas fa-award"></i>
450
+ کیفیت
451
+ </a>
452
+ </div>
453
+ <div class="nav-item">
454
+ <a href="#health" class="nav-link" onclick="showSection('health')">
455
+ <i class="fas fa-heartbeat"></i>
456
+ سلامت سیستم
457
+ </a>
458
+ </div>
459
+ <div class="nav-item">
460
+ <a href="#clustering" class="nav-link" onclick="showSection('clustering')">
461
+ <i class="fas fa-sitemap"></i>
462
+ خوشه‌بندی
463
+ </a>
464
+ </div>
465
+ </nav>
466
+ </aside>
467
+
468
+ <!-- Main Content -->
469
+ <main class="main-content">
470
+ <!-- Overview Section -->
471
+ <section id="overview" class="section active">
472
+ <div class="page-header">
473
+ <h1 class="page-title">📊 نمای کلی تحلیلات</h1>
474
+ <p class="page-subtitle">معیارهای زنده و آمار سیستم</p>
475
+ </div>
476
+
477
+ <!-- Real-time Metrics -->
478
+ <div class="cards-grid">
479
+ <div class="card">
480
+ <div class="card-header">
481
+ <h3 class="card-title">کل اسناد</h3>
482
+ <div class="card-icon primary">
483
+ <i class="fas fa-file-alt"></i>
484
+ </div>
485
+ </div>
486
+ <div class="metric-value" id="total-documents">-</div>
487
+ <div class="metric-label">تعداد کل اسناد پردازش شده</div>
488
+ <div class="metric-change positive" id="documents-change">
489
+ <i class="fas fa-arrow-up"></i>
490
+ <span>+12%</span>
491
+ </div>
492
+ </div>
493
+
494
+ <div class="card">
495
+ <div class="card-header">
496
+ <h3 class="card-title">پردازش امروز</h3>
497
+ <div class="card-icon success">
498
+ <i class="fas fa-clock"></i>
499
+ </div>
500
+ </div>
501
+ <div class="metric-value" id="processed-today">-</div>
502
+ <div class="metric-label">اسناد پردازش شده امروز</div>
503
+ <div class="metric-change positive" id="today-change">
504
+ <i class="fas fa-arrow-up"></i>
505
+ <span>+8%</span>
506
+ </div>
507
+ </div>
508
+
509
+ <div class="card">
510
+ <div class="card-header">
511
+ <h3 class="card-title">زمان پردازش</h3>
512
+ <div class="card-icon warning">
513
+ <i class="fas fa-stopwatch"></i>
514
+ </div>
515
+ </div>
516
+ <div class="metric-value" id="avg-processing-time">-</div>
517
+ <div class="metric-label">میانگین زمان پردازش (ثانیه)</div>
518
+ <div class="metric-change negative" id="time-change">
519
+ <i class="fas fa-arrow-down"></i>
520
+ <span>-5%</span>
521
+ </div>
522
+ </div>
523
+
524
+ <div class="card">
525
+ <div class="card-header">
526
+ <h3 class="card-title">سلامت سیستم</h3>
527
+ <div class="card-icon success">
528
+ <i class="fas fa-heartbeat"></i>
529
+ </div>
530
+ </div>
531
+ <div class="metric-value" id="system-health">-</div>
532
+ <div class="metric-label">درصد سلامت کلی سیستم</div>
533
+ <div class="metric-change positive" id="health-change">
534
+ <i class="fas fa-arrow-up"></i>
535
+ <span>+2%</span>
536
+ </div>
537
+ </div>
538
+ </div>
539
+
540
+ <!-- Charts -->
541
+ <div class="charts-grid">
542
+ <div class="chart-card">
543
+ <h3 class="card-title mb-2">روند پردازش</h3>
544
+ <div class="chart-container">
545
+ <canvas id="processing-chart"></canvas>
546
+ </div>
547
+ </div>
548
+
549
+ <div class="chart-card">
550
+ <h3 class="card-title mb-2">کیفیت اسناد</h3>
551
+ <div class="chart-container">
552
+ <canvas id="quality-chart"></canvas>
553
+ </div>
554
+ </div>
555
+ </div>
556
+
557
+ <!-- Alerts -->
558
+ <div class="alerts-section">
559
+ <h3 class="card-title mb-2">هشدارها و توصیه‌ها</h3>
560
+ <div id="alerts-container">
561
+ <!-- Alerts will be populated here -->
562
+ </div>
563
+ </div>
564
+ </section>
565
+
566
+ <!-- Trends Section -->
567
+ <section id="trends" class="section">
568
+ <div class="page-header">
569
+ <h1 class="page-title">📈 تحلیل روندها</h1>
570
+ <p class="page-subtitle">تحلیل روندهای سیستم و پیش‌بینی‌ها</p>
571
+ </div>
572
+
573
+ <div class="charts-grid">
574
+ <div class="chart-card">
575
+ <h3 class="card-title mb-2">روند حجم اسناد</h3>
576
+ <div class="chart-container">
577
+ <canvas id="volume-trend-chart"></canvas>
578
+ </div>
579
+ </div>
580
+
581
+ <div class="chart-card">
582
+ <h3 class="card-title mb-2">روند کیفیت</h3>
583
+ <div class="chart-container">
584
+ <canvas id="quality-trend-chart"></canvas>
585
+ </div>
586
+ </div>
587
+ </div>
588
+
589
+ <div class="table-container">
590
+ <div class="table-header">
591
+ <h3 class="table-title">تجزیه و تحلیل روندها</h3>
592
+ </div>
593
+ <table id="trends-table">
594
+ <thead>
595
+ <tr>
596
+ <th>معیار</th>
597
+ <th>جهت</th>
598
+ <th>تغییر</th>
599
+ <th>اعتماد</th>
600
+ <th>توصیه</th>
601
+ </tr>
602
+ </thead>
603
+ <tbody id="trends-tbody">
604
+ <!-- Trends data will be populated here -->
605
+ </tbody>
606
+ </table>
607
+ </div>
608
+ </section>
609
+
610
+ <!-- Predictions Section -->
611
+ <section id="predictions" class="section">
612
+ <div class="page-header">
613
+ <h1 class="page-title">🔮 پیش‌بینی‌ها</h1>
614
+ <p class="page-subtitle">پیش‌بینی‌های هوشمند و توصیه‌های بهینه‌سازی</p>
615
+ </div>
616
+
617
+ <div class="cards-grid">
618
+ <div class="card">
619
+ <div class="card-header">
620
+ <h3 class="card-title">پیش‌بینی 24 ساعته</h3>
621
+ <div class="card-icon primary">
622
+ <i class="fas fa-calendar-day"></i>
623
+ </div>
624
+ </div>
625
+ <div id="forecast-24h">
626
+ <!-- 24h forecast data -->
627
+ </div>
628
+ </div>
629
+
630
+ <div class="card">
631
+ <div class="card-header">
632
+ <h3 class="card-title">ساعات اوج</h3>
633
+ <div class="card-icon warning">
634
+ <i class="fas fa-chart-bar"></i>
635
+ </div>
636
+ </div>
637
+ <div id="peak-hours">
638
+ <!-- Peak hours data -->
639
+ </div>
640
+ </div>
641
+ </div>
642
+
643
+ <div class="table-container">
644
+ <div class="table-header">
645
+ <h3 class="table-title">توصیه‌های بهینه‌سازی</h3>
646
+ </div>
647
+ <div id="optimization-recommendations">
648
+ <!-- Optimization recommendations -->
649
+ </div>
650
+ </div>
651
+ </section>
652
+
653
+ <!-- Quality Section -->
654
+ <section id="quality" class="section">
655
+ <div class="page-header">
656
+ <h1 class="page-title">🏆 تحلیل کیفیت</h1>
657
+ <p class="page-subtitle">ارزیابی کیفیت اسناد و فرصت‌های بهبود</p>
658
+ </div>
659
+
660
+ <div class="cards-grid">
661
+ <div class="card">
662
+ <div class="card-header">
663
+ <h3 class="card-title">امتیاز کلی کیفیت</h3>
664
+ <div class="card-icon success">
665
+ <i class="fas fa-star"></i>
666
+ </div>
667
+ </div>
668
+ <div class="metric-value" id="overall-quality">-</div>
669
+ <div class="metric-label">میانگین کیفیت اسناد</div>
670
+ </div>
671
+
672
+ <div class="card">
673
+ <div class="card-header">
674
+ <h3 class="card-title">مشکلات رایج</h3>
675
+ <div class="card-icon warning">
676
+ <i class="fas fa-exclamation-triangle"></i>
677
+ </div>
678
+ </div>
679
+ <div id="common-issues">
680
+ <!-- Common issues -->
681
+ </div>
682
+ </div>
683
+ </div>
684
+
685
+ <div class="chart-card">
686
+ <h3 class="card-title mb-2">توزیع کیفیت</h3>
687
+ <div class="chart-container">
688
+ <canvas id="quality-distribution-chart"></canvas>
689
+ </div>
690
+ </div>
691
+ </section>
692
+
693
+ <!-- System Health Section -->
694
+ <section id="health" class="section">
695
+ <div class="page-header">
696
+ <h1 class="page-title">💚 سلامت سیستم</h1>
697
+ <p class="page-subtitle">نظارت بر سلامت اجزای سیستم</p>
698
+ </div>
699
+
700
+ <div class="cards-grid">
701
+ <div class="card">
702
+ <div class="card-header">
703
+ <h3 class="card-title">سلامت کلی</h3>
704
+ <div class="card-icon success">
705
+ <i class="fas fa-heartbeat"></i>
706
+ </div>
707
+ </div>
708
+ <div class="metric-value" id="overall-health">-</div>
709
+ <div class="metric-label">درصد سلامت سیستم</div>
710
+ </div>
711
+
712
+ <div class="card">
713
+ <div class="card-header">
714
+ <h3 class="card-title">پایگاه داده</h3>
715
+ <div class="card-icon primary">
716
+ <i class="fas fa-database"></i>
717
+ </div>
718
+ </div>
719
+ <div class="metric-value" id="db-health">-</div>
720
+ <div class="metric-label">سلامت پایگاه داده</div>
721
+ </div>
722
+
723
+ <div class="card">
724
+ <div class="card-header">
725
+ <h3 class="card-title">پردازش OCR</h3>
726
+ <div class="card-icon warning">
727
+ <i class="fas fa-eye"></i>
728
+ </div>
729
+ </div>
730
+ <div class="metric-value" id="ocr-health">-</div>
731
+ <div class="metric-label">سلامت پردازش OCR</div>
732
+ </div>
733
+
734
+ <div class="card">
735
+ <div class="card-header">
736
+ <h3 class="card-title">موتور AI</h3>
737
+ <div class="card-icon success">
738
+ <i class="fas fa-brain"></i>
739
+ </div>
740
+ </div>
741
+ <div class="metric-value" id="ai-health">-</div>
742
+ <div class="metric-label">سلامت موتور هوش مصنوعی</div>
743
+ </div>
744
+ </div>
745
+
746
+ <div class="table-container">
747
+ <div class="table-header">
748
+ <h3 class="table-title">هشدارهای سیستم</h3>
749
+ </div>
750
+ <div id="system-alerts">
751
+ <!-- System alerts -->
752
+ </div>
753
+ </div>
754
+ </section>
755
+
756
+ <!-- Clustering Section -->
757
+ <section id="clustering" class="section">
758
+ <div class="page-header">
759
+ <h1 class="page-title">🔗 خوشه‌بندی اسناد</h1>
760
+ <p class="page-subtitle">تحلیل خوشه‌های اسناد و شباهت‌ها</p>
761
+ </div>
762
+
763
+ <div class="cards-grid">
764
+ <div class="card">
765
+ <div class="card-header">
766
+ <h3 class="card-title">تعداد خوشه‌ها</h3>
767
+ <div class="card-icon primary">
768
+ <i class="fas fa-sitemap"></i>
769
+ </div>
770
+ </div>
771
+ <div class="metric-value" id="cluster-count">-</div>
772
+ <div class="metric-label">تعداد خوشه‌های شناسایی شده</div>
773
+ </div>
774
+
775
+ <div class="card">
776
+ <div class="card-header">
777
+ <h3 class="card-title">کیفیت خوشه‌بندی</h3>
778
+ <div class="card-icon success">
779
+ <i class="fas fa-chart-pie"></i>
780
+ </div>
781
+ </div>
782
+ <div class="metric-value" id="clustering-quality">-</div>
783
+ <div class="metric-label">امتیاز کیفیت خوشه‌بندی</div>
784
+ </div>
785
+ </div>
786
+
787
+ <div class="chart-card">
788
+ <h3 class="card-title mb-2">توزیع خوشه‌ها</h3>
789
+ <div class="chart-container">
790
+ <canvas id="clusters-chart"></canvas>
791
+ </div>
792
+ </div>
793
+
794
+ <div class="table-container">
795
+ <div class="table-header">
796
+ <h3 class="table-title">جزئیات خوشه‌ها</h3>
797
+ </div>
798
+ <div id="clusters-details">
799
+ <!-- Clusters details -->
800
+ </div>
801
+ </div>
802
+ </section>
803
+ </main>
804
+ </div>
805
+
806
+ <script>
807
+ // Global variables
808
+ let charts = {};
809
+ let currentSection = 'overview';
810
+ const API_BASE = 'http://localhost:8000/api';
811
+
812
+ // Initialize dashboard
813
+ document.addEventListener('DOMContentLoaded', function() {
814
+ initializeDashboard();
815
+ });
816
+
817
+ function initializeDashboard() {
818
+ loadRealTimeMetrics();
819
+ loadTrends();
820
+ loadPredictions();
821
+ loadQualityReport();
822
+ loadSystemHealth();
823
+ loadClusteringData();
824
+
825
+ // Auto-refresh every 30 seconds
826
+ setInterval(loadRealTimeMetrics, 30000);
827
+ }
828
+
829
+ function showSection(sectionId) {
830
+ // Hide all sections
831
+ document.querySelectorAll('.section').forEach(section => {
832
+ section.style.display = 'none';
833
+ });
834
+
835
+ // Show selected section
836
+ document.getElementById(sectionId).style.display = 'block';
837
+
838
+ // Update navigation
839
+ document.querySelectorAll('.nav-link').forEach(link => {
840
+ link.classList.remove('active');
841
+ });
842
+ event.target.classList.add('active');
843
+
844
+ currentSection = sectionId;
845
+ }
846
+
847
+ async function loadRealTimeMetrics() {
848
+ try {
849
+ const response = await fetch(`${API_BASE}/enhanced-analytics/real-time-metrics`);
850
+ const data = await response.json();
851
+
852
+ if (data) {
853
+ document.getElementById('total-documents').textContent = data.total_documents.toLocaleString();
854
+ document.getElementById('processed-today').textContent = data.processed_today.toLocaleString();
855
+ document.getElementById('avg-processing-time').textContent = data.avg_processing_time.toFixed(2);
856
+ document.getElementById('system-health').textContent = data.system_health.toFixed(1) + '%';
857
+
858
+ // Update charts
859
+ updateProcessingChart(data);
860
+ updateQualityChart(data);
861
+ }
862
+ } catch (error) {
863
+ console.error('Error loading real-time metrics:', error);
864
+ showError('خطا در بارگذاری معیارهای زنده');
865
+ }
866
+ }
867
+
868
+ async function loadTrends() {
869
+ try {
870
+ const response = await fetch(`${API_BASE}/enhanced-analytics/trends`, {
871
+ method: 'POST',
872
+ headers: {
873
+ 'Content-Type': 'application/json',
874
+ },
875
+ body: JSON.stringify({
876
+ metric: 'processing_time',
877
+ time_period: '7d'
878
+ })
879
+ });
880
+ const data = await response.json();
881
+
882
+ if (data) {
883
+ updateTrendsTable(data);
884
+ updateTrendCharts(data);
885
+ }
886
+ } catch (error) {
887
+ console.error('Error loading trends:', error);
888
+ }
889
+ }
890
+
891
+ async function loadPredictions() {
892
+ try {
893
+ const response = await fetch(`${API_BASE}/enhanced-analytics/predictive-insights`);
894
+ const data = await response.json();
895
+
896
+ if (data) {
897
+ updatePredictionsUI(data);
898
+ }
899
+ } catch (error) {
900
+ console.error('Error loading predictions:', error);
901
+ }
902
+ }
903
+
904
+ async function loadQualityReport() {
905
+ try {
906
+ const response = await fetch(`${API_BASE}/enhanced-analytics/quality-report`);
907
+ const data = await response.json();
908
+
909
+ if (data) {
910
+ document.getElementById('overall-quality').textContent = (data.overall_quality_score * 100).toFixed(1) + '%';
911
+ updateQualityDistribution(data);
912
+ updateCommonIssues(data);
913
+ }
914
+ } catch (error) {
915
+ console.error('Error loading quality report:', error);
916
+ }
917
+ }
918
+
919
+ async function loadSystemHealth() {
920
+ try {
921
+ const response = await fetch(`${API_BASE}/enhanced-analytics/system-health`);
922
+ const data = await response.json();
923
+
924
+ if (data) {
925
+ document.getElementById('overall-health').textContent = data.overall_health.toFixed(1) + '%';
926
+ document.getElementById('db-health').textContent = data.component_health.database.toFixed(1) + '%';
927
+ document.getElementById('ocr-health').textContent = data.component_health.ocr_pipeline.toFixed(1) + '%';
928
+ document.getElementById('ai-health').textContent = data.component_health.ai_engine.toFixed(1) + '%';
929
+
930
+ updateSystemAlerts(data.alerts);
931
+ }
932
+ } catch (error) {
933
+ console.error('Error loading system health:', error);
934
+ }
935
+ }
936
+
937
+ async function loadClusteringData() {
938
+ try {
939
+ const response = await fetch(`${API_BASE}/enhanced-analytics/clustering`, {
940
+ method: 'POST',
941
+ headers: {
942
+ 'Content-Type': 'application/json',
943
+ },
944
+ body: JSON.stringify({
945
+ n_clusters: 5
946
+ })
947
+ });
948
+ const data = await response.json();
949
+
950
+ if (data) {
951
+ document.getElementById('cluster-count').textContent = Object.keys(data.clusters).length;
952
+ document.getElementById('clustering-quality').textContent = (data.silhouette_score * 100).toFixed(1) + '%';
953
+ updateClustersChart(data);
954
+ }
955
+ } catch (error) {
956
+ console.error('Error loading clustering data:', error);
957
+ }
958
+ }
959
+
960
+ function updateProcessingChart(data) {
961
+ const ctx = document.getElementById('processing-chart');
962
+ if (charts.processing) {
963
+ charts.processing.destroy();
964
+ }
965
+
966
+ charts.processing = new Chart(ctx, {
967
+ type: 'line',
968
+ data: {
969
+ labels: ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'],
970
+ datasets: [{
971
+ label: 'زمان پردازش (ثانیه)',
972
+ data: [2.1, 2.3, 2.0, 2.5, 2.2, 2.4, 2.1],
973
+ borderColor: '#3b82f6',
974
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
975
+ tension: 0.4
976
+ }]
977
+ },
978
+ options: {
979
+ responsive: true,
980
+ maintainAspectRatio: false,
981
+ plugins: {
982
+ legend: {
983
+ position: 'top',
984
+ }
985
+ },
986
+ scales: {
987
+ y: {
988
+ beginAtZero: true
989
+ }
990
+ }
991
+ }
992
+ });
993
+ }
994
+
995
+ function updateQualityChart(data) {
996
+ const ctx = document.getElementById('quality-chart');
997
+ if (charts.quality) {
998
+ charts.quality.destroy();
999
+ }
1000
+
1001
+ charts.quality = new Chart(ctx, {
1002
+ type: 'doughnut',
1003
+ data: {
1004
+ labels: ['عالی', 'خوب', 'متوسط', 'ضعیف'],
1005
+ datasets: [{
1006
+ data: [45, 30, 20, 5],
1007
+ backgroundColor: [
1008
+ '#10b981',
1009
+ '#3b82f6',
1010
+ '#f59e0b',
1011
+ '#ef4444'
1012
+ ]
1013
+ }]
1014
+ },
1015
+ options: {
1016
+ responsive: true,
1017
+ maintainAspectRatio: false,
1018
+ plugins: {
1019
+ legend: {
1020
+ position: 'bottom',
1021
+ }
1022
+ }
1023
+ }
1024
+ });
1025
+ }
1026
+
1027
+ function updateTrendsTable(data) {
1028
+ const tbody = document.getElementById('trends-tbody');
1029
+ tbody.innerHTML = '';
1030
+
1031
+ const trends = [
1032
+ { metric: 'حجم اسناد', direction: 'صعودی', change: '+15%', confidence: '95%', recommendation: 'افزایش ظرفیت سیستم' },
1033
+ { metric: 'کیفیت پردازش', direction: 'صعودی', change: '+8%', confidence: '87%', recommendation: 'حفظ روند فعلی' },
1034
+ { metric: 'زمان پردازش', direction: 'نزولی', change: '-5%', confidence: '92%', recommendation: 'بهینه‌سازی الگوریتم‌ها' }
1035
+ ];
1036
+
1037
+ trends.forEach(trend => {
1038
+ const row = document.createElement('tr');
1039
+ row.innerHTML = `
1040
+ <td>${trend.metric}</td>
1041
+ <td><span class="badge ${trend.direction === 'صعودی' ? 'success' : 'warning'}">${trend.direction}</span></td>
1042
+ <td>${trend.change}</td>
1043
+ <td>${trend.confidence}</td>
1044
+ <td>${trend.recommendation}</td>
1045
+ `;
1046
+ tbody.appendChild(row);
1047
+ });
1048
+ }
1049
+
1050
+ function updatePredictionsUI(data) {
1051
+ const forecastDiv = document.getElementById('forecast-24h');
1052
+ const predictions = data.next_24h_forecast || {};
1053
+
1054
+ forecastDiv.innerHTML = `
1055
+ <div class="metric-value">${predictions.expected_documents || 0}</div>
1056
+ <div class="metric-label">اسناد پیش‌بینی شده</div>
1057
+ <div class="mt-2">
1058
+ <strong>ساعات اوج:</strong> ${(predictions.peak_hours || []).join(', ')}
1059
+ </div>
1060
+ `;
1061
+
1062
+ const recommendationsDiv = document.getElementById('optimization-recommendations');
1063
+ const recommendations = data.system_optimization_suggestions || [];
1064
+
1065
+ recommendationsDiv.innerHTML = recommendations.map(rec =>
1066
+ `<div class="alert alert-info">${rec}</div>`
1067
+ ).join('');
1068
+ }
1069
+
1070
+ function updateQualityDistribution(data) {
1071
+ const ctx = document.getElementById('quality-distribution-chart');
1072
+ if (charts.qualityDistribution) {
1073
+ charts.qualityDistribution.destroy();
1074
+ }
1075
+
1076
+ const distribution = data.quality_distribution || {};
1077
+
1078
+ charts.qualityDistribution = new Chart(ctx, {
1079
+ type: 'bar',
1080
+ data: {
1081
+ labels: Object.keys(distribution),
1082
+ datasets: [{
1083
+ label: 'تعداد اسناد',
1084
+ data: Object.values(distribution),
1085
+ backgroundColor: '#3b82f6'
1086
+ }]
1087
+ },
1088
+ options: {
1089
+ responsive: true,
1090
+ maintainAspectRatio: false,
1091
+ plugins: {
1092
+ legend: {
1093
+ position: 'top',
1094
+ }
1095
+ }
1096
+ }
1097
+ });
1098
+ }
1099
+
1100
+ function updateCommonIssues(data) {
1101
+ const issuesDiv = document.getElementById('common-issues');
1102
+ const issues = data.common_issues || [];
1103
+
1104
+ issuesDiv.innerHTML = issues.map(issue => `
1105
+ <div class="alert alert-${issue.severity === 'high' ? 'error' : 'warning'}">
1106
+ <i class="fas fa-exclamation-triangle"></i>
1107
+ <strong>${issue.type}:</strong> ${issue.description}
1108
+ </div>
1109
+ `).join('');
1110
+ }
1111
+
1112
+ function updateSystemAlerts(alerts) {
1113
+ const alertsContainer = document.getElementById('system-alerts');
1114
+
1115
+ alertsContainer.innerHTML = alerts.map(alert => `
1116
+ <div class="alert alert-${alert.severity === 'high' ? 'error' : 'warning'}">
1117
+ <i class="fas fa-${alert.type === 'error' ? 'exclamation-circle' : 'exclamation-triangle'}"></i>
1118
+ <strong>${alert.component}:</strong> ${alert.message}
1119
+ </div>
1120
+ `).join('');
1121
+ }
1122
+
1123
+ function updateClustersChart(data) {
1124
+ const ctx = document.getElementById('clusters-chart');
1125
+ if (charts.clusters) {
1126
+ charts.clusters.destroy();
1127
+ }
1128
+
1129
+ const clusters = data.clusters || {};
1130
+ const labels = Object.keys(clusters);
1131
+ const sizes = Object.values(clusters).map(cluster => cluster.length);
1132
+
1133
+ charts.clusters = new Chart(ctx, {
1134
+ type: 'pie',
1135
+ data: {
1136
+ labels: labels,
1137
+ datasets: [{
1138
+ data: sizes,
1139
+ backgroundColor: [
1140
+ '#3b82f6',
1141
+ '#10b981',
1142
+ '#f59e0b',
1143
+ '#ef4444',
1144
+ '#8b5cf6'
1145
+ ]
1146
+ }]
1147
+ },
1148
+ options: {
1149
+ responsive: true,
1150
+ maintainAspectRatio: false,
1151
+ plugins: {
1152
+ legend: {
1153
+ position: 'bottom',
1154
+ }
1155
+ }
1156
+ }
1157
+ });
1158
+ }
1159
+
1160
+ function showError(message) {
1161
+ const alertsContainer = document.getElementById('alerts-container');
1162
+ alertsContainer.innerHTML = `
1163
+ <div class="alert alert-error">
1164
+ <i class="fas fa-exclamation-circle"></i>
1165
+ ${message}
1166
+ </div>
1167
+ `;
1168
+ }
1169
+
1170
+ // Initialize dashboard when page loads
1171
+ document.addEventListener('DOMContentLoaded', initializeDashboard);
1172
+ </script>
1173
+ </body>
1174
+ </html>
app/frontend/improved_legal_dashboard.html ADDED
The diff for this file is too large to render. See raw diff
 
app/frontend/index.html ADDED
@@ -0,0 +1,1728 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>داشبورد مدیریتی حقوقی | سامانه هوشمند</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
12
+
13
+ <!-- Load API Client and Core System -->
14
+ <script src="js/api-client.js"></script>
15
+ <script src="js/core.js"></script>
16
+ <script src="js/api-connection-test.js"></script>
17
+ <script src="js/file-upload-handler.js"></script>
18
+ <script src="js/document-crud.js"></script>
19
+ <script src="js/scraping-control.js"></script>
20
+ <script src="js/notifications.js"></script>
21
+ <style>
22
+ :root {
23
+ /* رنگ‌بندی مدرن و هارمونیک */
24
+ --text-primary: #0f172a;
25
+ --text-secondary: #475569;
26
+ --text-muted: #64748b;
27
+ --text-light: #ffffff;
28
+
29
+ /* پس‌زمینه‌های بهبود یافته */
30
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
31
+ --card-bg: rgba(255, 255, 255, 0.95);
32
+ --glass-bg: rgba(255, 255, 255, 0.9);
33
+ --glass-border: rgba(148, 163, 184, 0.2);
34
+
35
+ /* گرادیان‌های مدرن */
36
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
37
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
38
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
39
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
40
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
41
+
42
+ /* سایه‌های ملایم */
43
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
44
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
45
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
46
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
47
+ --shadow-glow-primary: 0 0 20px rgba(59, 130, 246, 0.15);
48
+
49
+ /* متغیرهای کامپکت */
50
+ --sidebar-width: 260px;
51
+ --border-radius: 12px;
52
+ --border-radius-sm: 8px;
53
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
54
+ --transition-fast: all 0.15s ease-in-out;
55
+
56
+ /* فونت‌های کامپکت */
57
+ --font-size-xs: 0.7rem;
58
+ --font-size-sm: 0.8rem;
59
+ --font-size-base: 0.9rem;
60
+ --font-size-lg: 1.1rem;
61
+ --font-size-xl: 1.25rem;
62
+ --font-size-2xl: 1.5rem;
63
+ }
64
+
65
+ /* ریست و تنظیمات پایه */
66
+ * {
67
+ margin: 0;
68
+ padding: 0;
69
+ box-sizing: border-box;
70
+ }
71
+
72
+ body {
73
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
74
+ background: var(--body-bg);
75
+ color: var(--text-primary);
76
+ line-height: 1.6;
77
+ overflow-x: hidden;
78
+ font-size: var(--font-size-base);
79
+ }
80
+
81
+ /* اسکرول‌بار مدرن */
82
+ ::-webkit-scrollbar {
83
+ inline-size: 6px;
84
+ block-size: 6px;
85
+ }
86
+
87
+ ::-webkit-scrollbar-track {
88
+ background: rgba(0, 0, 0, 0.02);
89
+ border-radius: 10px;
90
+ }
91
+
92
+ ::-webkit-scrollbar-thumb {
93
+ background: var(--primary-gradient);
94
+ border-radius: 10px;
95
+ }
96
+
97
+ /* کانتینر اصلی */
98
+ .dashboard-container {
99
+ display: flex;
100
+ min-block-size: 100vh;
101
+ inline-size: 100%;
102
+ }
103
+
104
+ /* سایدبار کامپکت */
105
+ .sidebar {
106
+ inline-size: var(--sidebar-width);
107
+ background: linear-gradient(135deg,
108
+ rgba(248, 250, 252, 0.98) 0%,
109
+ rgba(241, 245, 249, 0.95) 25%,
110
+ rgba(226, 232, 240, 0.98) 50%,
111
+ rgba(203, 213, 225, 0.95) 75%,
112
+ rgba(148, 163, 184, 0.1) 100%);
113
+ backdrop-filter: blur(25px);
114
+ padding: 1rem 0;
115
+ position: fixed;
116
+ block-size: 100vh;
117
+ inset-inline-end: 0;
118
+ inset-block-start: 0;
119
+ z-index: 1000;
120
+ overflow-y: auto;
121
+ box-shadow: -8px 0 32px rgba(59, 130, 246, 0.12);
122
+ border-inline-start: 1px solid rgba(59, 130, 246, 0.15);
123
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
124
+ }
125
+
126
+ .sidebar-header {
127
+ padding: 0 1rem 1rem;
128
+ border-block-end: 1px solid rgba(59, 130, 246, 0.12);
129
+ margin-block-end: 1rem;
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: center;
133
+ background: linear-gradient(135deg,
134
+ rgba(255, 255, 255, 0.4) 0%,
135
+ rgba(248, 250, 252, 0.2) 100%);
136
+ margin: 0 0.5rem 1rem;
137
+ border-radius: var(--border-radius);
138
+ backdrop-filter: blur(10px);
139
+ }
140
+
141
+ .logo {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.6rem;
145
+ color: var(--text-primary);
146
+ text-decoration: none;
147
+ }
148
+
149
+ .logo-icon {
150
+ inline-size: 2rem;
151
+ block-size: 2rem;
152
+ background: var(--primary-gradient);
153
+ border-radius: var(--border-radius-sm);
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ font-size: 1rem;
158
+ color: white;
159
+ box-shadow: var(--shadow-glow-primary);
160
+ }
161
+
162
+ .logo-text {
163
+ font-size: var(--font-size-lg);
164
+ font-weight: 700;
165
+ background: var(--primary-gradient);
166
+ -webkit-background-clip: text;
167
+ background-clip: text;
168
+ -webkit-text-fill-color: transparent;
169
+ }
170
+
171
+ .nav-section {
172
+ margin-block-end: 1rem;
173
+ }
174
+
175
+ .nav-title {
176
+ padding: 0 1rem 0.4rem;
177
+ font-size: var(--font-size-xs);
178
+ font-weight: 600;
179
+ text-transform: uppercase;
180
+ letter-spacing: 0.5px;
181
+ color: var(--text-secondary);
182
+ }
183
+
184
+ .nav-menu {
185
+ list-style: none;
186
+ }
187
+
188
+ .nav-item {
189
+ margin: 0.15rem 0.5rem;
190
+ }
191
+
192
+ .nav-link {
193
+ display: flex;
194
+ align-items: center;
195
+ padding: 0.6rem 0.8rem;
196
+ color: var(--text-primary);
197
+ text-decoration: none;
198
+ border-radius: var(--border-radius-sm);
199
+ transition: var(--transition-smooth);
200
+ font-weight: 500;
201
+ font-size: var(--font-size-sm);
202
+ cursor: pointer;
203
+ border: 1px solid transparent;
204
+ }
205
+
206
+ .nav-link:hover {
207
+ color: var(--text-primary);
208
+ transform: translateX(-2px);
209
+ border-color: rgba(59, 130, 246, 0.15);
210
+ background: rgba(59, 130, 246, 0.05);
211
+ }
212
+
213
+ .nav-link.active {
214
+ background: var(--primary-gradient);
215
+ color: var(--text-light);
216
+ box-shadow: var(--shadow-md);
217
+ }
218
+
219
+ .nav-icon {
220
+ margin-inline-start: 0.6rem;
221
+ inline-size: 1rem;
222
+ text-align: center;
223
+ font-size: 0.9rem;
224
+ }
225
+
226
+ .nav-badge {
227
+ background: var(--danger-gradient);
228
+ color: white;
229
+ padding: 0.15rem 0.4rem;
230
+ border-radius: 10px;
231
+ font-size: var(--font-size-xs);
232
+ font-weight: 600;
233
+ margin-inline-end: auto;
234
+ min-inline-size: 1.2rem;
235
+ text-align: center;
236
+ }
237
+
238
+ /* محتوای اصلی */
239
+ .main-content {
240
+ flex: 1;
241
+ margin-inline-end: var(--sidebar-width);
242
+ padding: 1rem;
243
+ min-block-size: 100vh;
244
+ inline-size: calc(100% - var(--sidebar-width));
245
+ }
246
+
247
+ /* هدر کامپکت */
248
+ .dashboard-header {
249
+ display: flex;
250
+ justify-content: space-between;
251
+ align-items: center;
252
+ margin-block-end: 1.2rem;
253
+ padding: 0.8rem 0;
254
+ }
255
+
256
+ .dashboard-title {
257
+ font-size: var(--font-size-2xl);
258
+ font-weight: 800;
259
+ background: var(--primary-gradient);
260
+ -webkit-background-clip: text;
261
+ background-clip: text;
262
+ -webkit-text-fill-color: transparent;
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 0.6rem;
266
+ }
267
+
268
+ .header-actions {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 0.8rem;
272
+ }
273
+
274
+ .search-container {
275
+ position: relative;
276
+ }
277
+
278
+ .search-input {
279
+ inline-size: 280px;
280
+ padding: 0.6rem 1rem 0.6rem 2.2rem;
281
+ border: none;
282
+ border-radius: 20px;
283
+ background: var(--glass-bg);
284
+ backdrop-filter: blur(10px);
285
+ box-shadow: var(--shadow-sm);
286
+ font-family: inherit;
287
+ font-size: var(--font-size-sm);
288
+ color: var(--text-primary);
289
+ transition: var(--transition-smooth);
290
+ border: 1px solid var(--glass-border);
291
+ }
292
+
293
+ .search-input:focus {
294
+ outline: none;
295
+ box-shadow: var(--shadow-glow-primary);
296
+ background: var(--card-bg);
297
+ border-color: rgba(59, 130, 246, 0.3);
298
+ }
299
+
300
+ .search-icon {
301
+ position: absolute;
302
+ inset-inline-end: 0.8rem;
303
+ inset-block-start: 50%;
304
+ transform: translateY(-50%);
305
+ color: var(--text-secondary);
306
+ font-size: 0.9rem;
307
+ }
308
+
309
+ .user-profile {
310
+ display: flex;
311
+ align-items: center;
312
+ gap: 0.6rem;
313
+ padding: 0.4rem 0.8rem;
314
+ background: var(--glass-bg);
315
+ backdrop-filter: blur(10px);
316
+ border-radius: 18px;
317
+ box-shadow: var(--shadow-sm);
318
+ cursor: pointer;
319
+ transition: var(--transition-smooth);
320
+ border: 1px solid var(--glass-border);
321
+ }
322
+
323
+ .user-profile:hover {
324
+ box-shadow: var(--shadow-md);
325
+ transform: translateY(-1px);
326
+ }
327
+
328
+ .user-avatar {
329
+ inline-size: 1.8rem;
330
+ block-size: 1.8rem;
331
+ border-radius: 50%;
332
+ background: var(--primary-gradient);
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ color: white;
337
+ font-weight: 600;
338
+ font-size: var(--font-size-sm);
339
+ }
340
+
341
+ .user-info {
342
+ display: flex;
343
+ flex-direction: column;
344
+ }
345
+
346
+ .user-name {
347
+ font-weight: 600;
348
+ color: var(--text-primary);
349
+ font-size: var(--font-size-sm);
350
+ }
351
+
352
+ .user-role {
353
+ font-size: var(--font-size-xs);
354
+ color: var(--text-secondary);
355
+ }
356
+
357
+ /* کارت‌های آمار کامپکت */
358
+ .stats-grid {
359
+ display: grid;
360
+ grid-template-columns: repeat(4, 1fr);
361
+ gap: 1rem;
362
+ margin-block-end: 1.2rem;
363
+ }
364
+
365
+ .stat-card {
366
+ background: var(--card-bg);
367
+ backdrop-filter: blur(10px);
368
+ border-radius: var(--border-radius);
369
+ padding: 1.2rem;
370
+ box-shadow: var(--shadow-md);
371
+ border: 1px solid rgba(255, 255, 255, 0.3);
372
+ position: relative;
373
+ overflow: hidden;
374
+ transition: var(--transition-smooth);
375
+ min-block-size: 130px;
376
+ }
377
+
378
+ .stat-card::before {
379
+ content: '';
380
+ position: absolute;
381
+ inset-block-start: 0;
382
+ inset-inline-start: 0;
383
+ inset-inline-end: 0;
384
+ block-size: 4px;
385
+ background: var(--primary-gradient);
386
+ }
387
+
388
+ .stat-card.primary::before { background: var(--primary-gradient); }
389
+ .stat-card.success::before { background: var(--success-gradient); }
390
+ .stat-card.danger::before { background: var(--danger-gradient); }
391
+ .stat-card.warning::before { background: var(--warning-gradient); }
392
+
393
+ .stat-card:hover {
394
+ transform: translateY(-6px) scale(1.02);
395
+ box-shadow: var(--shadow-lg);
396
+ }
397
+
398
+ .stat-header {
399
+ display: flex;
400
+ justify-content: space-between;
401
+ align-items: flex-start;
402
+ margin-block-end: 0.8rem;
403
+ }
404
+
405
+ .stat-icon {
406
+ inline-size: 2.2rem;
407
+ block-size: 2.2rem;
408
+ border-radius: var(--border-radius-sm);
409
+ display: flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ font-size: 1.1rem;
413
+ box-shadow: var(--shadow-sm);
414
+ transition: var(--transition-smooth);
415
+ }
416
+
417
+ .stat-icon.primary { background: var(--primary-gradient); color: white; }
418
+ .stat-icon.success { background: var(--success-gradient); color: white; }
419
+ .stat-icon.danger { background: var(--danger-gradient); color: white; }
420
+ .stat-icon.warning { background: var(--warning-gradient); color: white; }
421
+
422
+ .stat-content {
423
+ flex: 1;
424
+ }
425
+
426
+ .stat-title {
427
+ font-size: var(--font-size-xs);
428
+ color: var(--text-secondary);
429
+ font-weight: 600;
430
+ margin-block-end: 0.3rem;
431
+ }
432
+
433
+ .stat-value {
434
+ font-size: var(--font-size-xl);
435
+ font-weight: 800;
436
+ color: var(--text-primary);
437
+ line-height: 1;
438
+ margin-block-end: 0.3rem;
439
+ }
440
+
441
+ .stat-extra {
442
+ font-size: var(--font-size-xs);
443
+ color: var(--text-muted);
444
+ margin-block-end: 0.3rem;
445
+ }
446
+
447
+ .stat-change {
448
+ display: flex;
449
+ align-items: center;
450
+ gap: 0.25rem;
451
+ font-size: var(--font-size-xs);
452
+ font-weight: 700;
453
+ }
454
+
455
+ .stat-change.positive { color: #059669; }
456
+ .stat-change.negative { color: #dc2626; }
457
+
458
+ /* نمودارها */
459
+ .charts-section {
460
+ display: grid;
461
+ grid-template-columns: 2fr 1fr;
462
+ gap: 1.5rem;
463
+ margin-block-end: 1.5rem;
464
+ }
465
+
466
+ .chart-card {
467
+ background: var(--card-bg);
468
+ backdrop-filter: blur(10px);
469
+ border-radius: var(--border-radius);
470
+ padding: 1.5rem;
471
+ box-shadow: var(--shadow-md);
472
+ border: 1px solid rgba(255, 255, 255, 0.3);
473
+ transition: var(--transition-smooth);
474
+ }
475
+
476
+ .chart-card:hover {
477
+ transform: translateY(-4px);
478
+ box-shadow: var(--shadow-lg);
479
+ }
480
+
481
+ .chart-header {
482
+ display: flex;
483
+ justify-content: space-between;
484
+ align-items: center;
485
+ margin-block-end: 1.2rem;
486
+ }
487
+
488
+ .chart-title {
489
+ font-size: var(--font-size-lg);
490
+ font-weight: 700;
491
+ color: var(--text-primary);
492
+ }
493
+
494
+ .chart-filters {
495
+ display: flex;
496
+ gap: 0.3rem;
497
+ }
498
+
499
+ .chart-filter {
500
+ padding: 0.3rem 0.8rem;
501
+ border: none;
502
+ border-radius: 12px;
503
+ background: rgba(59, 130, 246, 0.08);
504
+ color: var(--text-secondary);
505
+ font-family: inherit;
506
+ font-size: var(--font-size-xs);
507
+ font-weight: 500;
508
+ cursor: pointer;
509
+ transition: var(--transition-fast);
510
+ }
511
+
512
+ .chart-filter:hover {
513
+ background: rgba(59, 130, 246, 0.12);
514
+ }
515
+
516
+ .chart-filter.active {
517
+ background: var(--primary-gradient);
518
+ color: white;
519
+ box-shadow: var(--shadow-glow-primary);
520
+ }
521
+
522
+ .chart-container {
523
+ block-size: 280px;
524
+ position: relative;
525
+ }
526
+
527
+ .chart-placeholder {
528
+ block-size: 100%;
529
+ display: flex;
530
+ align-items: center;
531
+ justify-content: center;
532
+ flex-direction: column;
533
+ color: var(--text-muted);
534
+ background: rgba(0, 0, 0, 0.02);
535
+ border-radius: var(--border-radius-sm);
536
+ border: 2px dashed rgba(0, 0, 0, 0.1);
537
+ }
538
+
539
+ .chart-placeholder i {
540
+ font-size: 3rem;
541
+ margin-block-end: 1rem;
542
+ opacity: 0.3;
543
+ }
544
+
545
+ /* دسترسی سریع */
546
+ .quick-access-section {
547
+ margin-block-end: 1.5rem;
548
+ }
549
+
550
+ .quick-access-grid {
551
+ display: grid;
552
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
553
+ gap: 1rem;
554
+ padding: 1rem 0;
555
+ }
556
+
557
+ .quick-access-item {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 1rem;
561
+ padding: 1rem;
562
+ background: rgba(255, 255, 255, 0.7);
563
+ border-radius: var(--border-radius-sm);
564
+ text-decoration: none;
565
+ color: var(--text-primary);
566
+ transition: var(--transition-smooth);
567
+ border: 1px solid rgba(59, 130, 246, 0.1);
568
+ position: relative;
569
+ overflow: hidden;
570
+ }
571
+
572
+ .quick-access-item::before {
573
+ content: '';
574
+ position: absolute;
575
+ inset-block-start: 0;
576
+ inset-inline-start: 0;
577
+ inset-block-end: 0;
578
+ inline-size: 4px;
579
+ background: var(--primary-gradient);
580
+ opacity: 0;
581
+ transition: var(--transition-smooth);
582
+ }
583
+
584
+ .quick-access-item:hover {
585
+ background: rgba(255, 255, 255, 0.9);
586
+ transform: translateY(-2px);
587
+ box-shadow: var(--shadow-md);
588
+ border-color: rgba(59, 130, 246, 0.3);
589
+ }
590
+
591
+ .quick-access-item:hover::before {
592
+ opacity: 1;
593
+ }
594
+
595
+ .quick-access-icon {
596
+ inline-size: 3rem;
597
+ block-size: 3rem;
598
+ background: var(--primary-gradient);
599
+ border-radius: var(--border-radius-sm);
600
+ display: flex;
601
+ align-items: center;
602
+ justify-content: center;
603
+ color: white;
604
+ font-size: 1.2rem;
605
+ flex-shrink: 0;
606
+ box-shadow: var(--shadow-sm);
607
+ transition: var(--transition-smooth);
608
+ }
609
+
610
+ .quick-access-item:hover .quick-access-icon {
611
+ transform: scale(1.1);
612
+ box-shadow: var(--shadow-md);
613
+ }
614
+
615
+ .quick-access-content h3 {
616
+ font-size: var(--font-size-base);
617
+ font-weight: 600;
618
+ color: var(--text-primary);
619
+ margin-block-end: 0.3rem;
620
+ }
621
+
622
+ .quick-access-content p {
623
+ font-size: var(--font-size-sm);
624
+ color: var(--text-secondary);
625
+ margin: 0;
626
+ }
627
+
628
+ /* Toast Notifications */
629
+ .toast-container {
630
+ position: fixed;
631
+ inset-block-start: 1rem;
632
+ inset-inline-start: 1rem;
633
+ z-index: 10001;
634
+ display: flex;
635
+ flex-direction: column;
636
+ gap: 0.5rem;
637
+ }
638
+
639
+ .toast {
640
+ background: var(--card-bg);
641
+ border-radius: var(--border-radius-sm);
642
+ padding: 1rem 1.5rem;
643
+ box-shadow: var(--shadow-lg);
644
+ border-inline-start: 4px solid;
645
+ display: flex;
646
+ align-items: center;
647
+ gap: 0.8rem;
648
+ min-inline-size: 300px;
649
+ transform: translateX(-100%);
650
+ transition: all 0.3s ease;
651
+ }
652
+
653
+ .toast.show {
654
+ transform: translateX(0);
655
+ }
656
+
657
+ .toast.success { border-inline-start-color: #10b981; }
658
+ .toast.error { border-inline-start-color: #ef4444; }
659
+ .toast.warning { border-inline-start-color: #f59e0b; }
660
+ .toast.info { border-inline-start-color: #3b82f6; }
661
+
662
+ .toast-icon {
663
+ font-size: 1.2rem;
664
+ }
665
+
666
+ .toast.success .toast-icon { color: #10b981; }
667
+ .toast.error .toast-icon { color: #ef4444; }
668
+ .toast.warning .toast-icon { color: #f59e0b; }
669
+ .toast.info .toast-icon { color: #3b82f6; }
670
+
671
+ .toast-content {
672
+ flex: 1;
673
+ }
674
+
675
+ .toast-title {
676
+ font-weight: 600;
677
+ font-size: var(--font-size-sm);
678
+ margin-block-end: 0.2rem;
679
+ }
680
+
681
+ .toast-message {
682
+ font-size: var(--font-size-xs);
683
+ color: var(--text-secondary);
684
+ }
685
+
686
+ .toast-close {
687
+ background: none;
688
+ border: none;
689
+ color: var(--text-secondary);
690
+ cursor: pointer;
691
+ font-size: 1rem;
692
+ transition: var(--transition-fast);
693
+ }
694
+
695
+ .toast-close:hover {
696
+ color: var(--text-primary);
697
+ }
698
+
699
+ /* Connection Status */
700
+ .connection-status {
701
+ position: fixed;
702
+ inset-block-end: 1rem;
703
+ inset-inline-start: 1rem;
704
+ background: var(--card-bg);
705
+ border-radius: var(--border-radius-sm);
706
+ padding: 0.5rem 1rem;
707
+ box-shadow: var(--shadow-sm);
708
+ display: flex;
709
+ align-items: center;
710
+ gap: 0.5rem;
711
+ font-size: var(--font-size-xs);
712
+ border-inline-start: 3px solid;
713
+ z-index: 1000;
714
+ }
715
+
716
+ .connection-status.online {
717
+ border-inline-start-color: #10b981;
718
+ color: #047857;
719
+ }
720
+
721
+ .connection-status.offline {
722
+ border-inline-start-color: #ef4444;
723
+ color: #b91c1c;
724
+ }
725
+
726
+ .status-indicator {
727
+ inline-size: 8px;
728
+ block-size: 8px;
729
+ border-radius: 50%;
730
+ }
731
+
732
+ .connection-status.online .status-indicator {
733
+ background: #10b981;
734
+ animation: pulse 2s infinite;
735
+ }
736
+
737
+ .connection-status.offline .status-indicator {
738
+ background: #ef4444;
739
+ }
740
+
741
+ @keyframes pulse {
742
+ 0%, 100% { opacity: 1; }
743
+ 50% { opacity: 0.5; }
744
+ }
745
+
746
+ /* دکمه منوی موبایل */
747
+ .mobile-menu-toggle {
748
+ display: none;
749
+ background: var(--glass-bg);
750
+ border: 1px solid var(--glass-border);
751
+ padding: 0.5rem;
752
+ border-radius: var(--border-radius-sm);
753
+ color: var(--text-primary);
754
+ font-size: 1rem;
755
+ cursor: pointer;
756
+ transition: var(--transition-fast);
757
+ }
758
+
759
+ .mobile-menu-toggle:hover {
760
+ background: var(--primary-gradient);
761
+ color: white;
762
+ }
763
+
764
+ /* واکنش‌گرایی */
765
+ @media (max-inline-size: 992px) {
766
+ .mobile-menu-toggle {
767
+ display: block;
768
+ }
769
+
770
+ .sidebar {
771
+ transform: translateX(100%);
772
+ position: fixed;
773
+ z-index: 10000;
774
+ }
775
+
776
+ .sidebar.open {
777
+ transform: translateX(0);
778
+ }
779
+
780
+ .main-content {
781
+ margin-inline-end: 0;
782
+ inline-size: 100%;
783
+ padding: 1rem;
784
+ }
785
+
786
+ .dashboard-header {
787
+ flex-direction: column;
788
+ align-items: flex-start;
789
+ gap: 0.8rem;
790
+ }
791
+
792
+ .header-actions {
793
+ width: 100%;
794
+ justify-content: space-between;
795
+ flex-direction: column;
796
+ gap: 0.8rem;
797
+ }
798
+
799
+ .search-container {
800
+ inline-size: 100%;
801
+ }
802
+
803
+ .search-input {
804
+ inline-size: 100%;
805
+ }
806
+
807
+ .stats-grid {
808
+ grid-template-columns: repeat(2, 1fr);
809
+ }
810
+
811
+ .charts-section {
812
+ grid-template-columns: 1fr;
813
+ }
814
+ }
815
+
816
+ @media (max-inline-size: 768px) {
817
+ .main-content {
818
+ padding: 0.8rem;
819
+ }
820
+
821
+ .stats-grid {
822
+ grid-template-columns: 1fr;
823
+ gap: 0.6rem;
824
+ }
825
+
826
+ .stat-card {
827
+ min-block-size: 100px;
828
+ padding: 0.8rem;
829
+ }
830
+
831
+ .stat-value {
832
+ font-size: var(--font-size-lg);
833
+ }
834
+
835
+ .chart-container {
836
+ block-size: 220px;
837
+ }
838
+ }
839
+ </style>
840
+ </head>
841
+ <body>
842
+ <div class="dashboard-container">
843
+ <!-- سایدبار -->
844
+ <aside class="sidebar" id="sidebar">
845
+ <div class="sidebar-header">
846
+ <div class="logo">
847
+ <div class="logo-icon">
848
+ <i class="fas fa-scale-balanced"></i>
849
+ </div>
850
+ <div class="logo-text">سامانه حقوقی</div>
851
+ </div>
852
+ </div>
853
+
854
+ <nav>
855
+ <div class="nav-section">
856
+ <h6 class="nav-title">داشبورد</h6>
857
+ <ul class="nav-menu">
858
+ <li class="nav-item">
859
+ <a href="index.html" class="nav-link active">
860
+ <i class="fas fa-chart-pie nav-icon"></i>
861
+ <span>نمای کلی</span>
862
+ </a>
863
+ </li>
864
+ <li class="nav-item">
865
+ <a href="enhanced_analytics_dashboard.html" class="nav-link">
866
+ <i class="fas fa-chart-area nav-icon"></i>
867
+ <span>داشبورد پیشرفته</span>
868
+ </a>
869
+ </li>
870
+ </ul>
871
+ </div>
872
+
873
+ <div class="nav-section">
874
+ <h6 class="nav-title">مدیریت اسناد</h6>
875
+ <ul class="nav-menu">
876
+ <li class="nav-item">
877
+ <a href="documents.html" class="nav-link">
878
+ <i class="fas fa-file-alt nav-icon"></i>
879
+ <span>مدیریت اسناد</span>
880
+ <span class="nav-badge" id="totalDocumentsBadge">6</span>
881
+ </a>
882
+ </li>
883
+
884
+ <li class="nav-item">
885
+ <a href="upload.html" class="nav-link">
886
+ <i class="fas fa-cloud-upload-alt nav-icon"></i>
887
+ <span>آپلود فایل</span>
888
+ </a>
889
+ </li>
890
+
891
+ <li class="nav-item">
892
+ <a href="search.html" class="nav-link">
893
+ <i class="fas fa-search nav-icon"></i>
894
+ <span>جستجو</span>
895
+ </a>
896
+ </li>
897
+ </ul>
898
+ </div>
899
+
900
+ <div class="nav-section">
901
+ <h6 class="nav-title">ابزارها</h6>
902
+ <ul class="nav-menu">
903
+ <li class="nav-item">
904
+ <a href="scraping.html" class="nav-link">
905
+ <i class="fas fa-globe nav-icon"></i>
906
+ <span>استخراج محتوا</span>
907
+ </a>
908
+ </li>
909
+
910
+ <li class="nav-item">
911
+ <a href="scraping_dashboard.html" class="nav-link">
912
+ <i class="fas fa-spider nav-icon"></i>
913
+ <span>داشبورد اسکرپینگ</span>
914
+ </a>
915
+ </li>
916
+
917
+ <li class="nav-item">
918
+ <a href="analytics.html" class="nav-link">
919
+ <i class="fas fa-chart-line nav-icon"></i>
920
+ <span>آمار و تحلیل</span>
921
+ </a>
922
+ </li>
923
+
924
+ <li class="nav-item">
925
+ <a href="reports.html" class="nav-link">
926
+ <i class="fas fa-file-export nav-icon"></i>
927
+ <span>گزارش‌ها</span>
928
+ </a>
929
+ </li>
930
+ </ul>
931
+ </div>
932
+
933
+ <div class="nav-section">
934
+ <h6 class="nav-title">تنظیمات و ابزارهای توسعه</h6>
935
+ <ul class="nav-menu">
936
+ <li class="nav-item">
937
+ <a href="settings.html" class="nav-link">
938
+ <i class="fas fa-cog nav-icon"></i>
939
+ <span>تنظیمات</span>
940
+ </a>
941
+ </li>
942
+ <li class="nav-item">
943
+ <a href="dev/api-test.html" class="nav-link">
944
+ <i class="fas fa-code nav-icon"></i>
945
+ <span>تست API</span>
946
+ </a>
947
+ </li>
948
+ <li class="nav-item">
949
+ <a href="#" class="nav-link">
950
+ <i class="fas fa-sign-out-alt nav-icon"></i>
951
+ <span>خروج</span>
952
+ </a>
953
+ </li>
954
+ </ul>
955
+ </div>
956
+ </nav>
957
+ </aside>
958
+
959
+ <!-- محتوای اصلی -->
960
+ <main class="main-content" id="mainContent">
961
+ <!-- هدر -->
962
+ <header class="dashboard-header">
963
+ <div>
964
+ <h1 class="dashboard-title">
965
+ <i class="fas fa-chart-pie"></i>
966
+ <span>داشبورد مدیریتی حقوقی</span>
967
+ </h1>
968
+ </div>
969
+
970
+ <div class="header-actions">
971
+ <button type="button" class="mobile-menu-toggle" id="mobileMenuToggle" aria-label="منوی موبایل">
972
+ <i class="fas fa-bars"></i>
973
+ </button>
974
+
975
+ <div class="search-container">
976
+ <input type="text" class="search-input" id="searchInput" placeholder="جستجو در اسناد، قوانین، پرونده‌ها...">
977
+ <i class="fas fa-search search-icon"></i>
978
+ </div>
979
+
980
+ <div class="user-profile">
981
+ <div class="user-avatar">ح</div>
982
+ <div class="user-info">
983
+ <span class="user-name">حسین محمدی</span>
984
+ <span class="user-role">وکیل پایه یک</span>
985
+ </div>
986
+ <i class="fas fa-chevron-down"></i>
987
+ </div>
988
+ </div>
989
+ </header>
990
+
991
+ <!-- کارت‌های آمار -->
992
+ <section aria-labelledby="stats-section">
993
+ <h2 id="stats-section" class="sr-only">آمار و ارقام کلیدی</h2>
994
+ <div class="stats-grid">
995
+ <div class="stat-card primary">
996
+ <div class="stat-header">
997
+ <div class="stat-content">
998
+ <div class="stat-title">کل اسناد جمع‌آوری شده</div>
999
+ <div class="stat-value" id="totalDocuments">6</div>
1000
+ <div class="stat-extra">در پایگاه داده سیستم</div>
1001
+ <div class="stat-change positive">
1002
+ <i class="fas fa-arrow-up"></i>
1003
+ <span>+15.2%</span>
1004
+ </div>
1005
+ </div>
1006
+ <div class="stat-icon primary">
1007
+ <i class="fas fa-file-alt"></i>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <div class="stat-card success">
1013
+ <div class="stat-header">
1014
+ <div class="stat-content">
1015
+ <div class="stat-title">اسناد پردازش شده</div>
1016
+ <div class="stat-value" id="processedDocuments">4</div>
1017
+ <div class="stat-extra">با موفقیت پردازش شده</div>
1018
+ <div class="stat-change positive">
1019
+ <i class="fas fa-arrow-up"></i>
1020
+ <span>+23.1%</span>
1021
+ </div>
1022
+ </div>
1023
+ <div class="stat-icon success">
1024
+ <i class="fas fa-check-circle"></i>
1025
+ </div>
1026
+ </div>
1027
+ </div>
1028
+
1029
+ <div class="stat-card danger">
1030
+ <div class="stat-header">
1031
+ <div class="stat-content">
1032
+ <div class="stat-title">اسناد دارای خطا</div>
1033
+ <div class="stat-value" id="errorDocuments">1</div>
1034
+ <div class="stat-extra">نیازمند بررسی</div>
1035
+ <div class="stat-change negative">
1036
+ <i class="fas fa-arrow-down"></i>
1037
+ <span>-8.3%</span>
1038
+ </div>
1039
+ </div>
1040
+ <div class="stat-icon danger">
1041
+ <i class="fas fa-triangle-exclamation"></i>
1042
+ </div>
1043
+ </div>
1044
+ </div>
1045
+
1046
+ <div class="stat-card warning">
1047
+ <div class="stat-header">
1048
+ <div class="stat-content">
1049
+ <div class="stat-title">امتیاز کیفی میانگین</div>
1050
+ <div class="stat-value" id="averageQuality">8.1</div>
1051
+ <div class="stat-extra">از 10 امتیاز</div>
1052
+ <div class="stat-change positive">
1053
+ <i class="fas fa-arrow-up"></i>
1054
+ <span>+2.1%</span>
1055
+ </div>
1056
+ </div>
1057
+ <div class="stat-icon warning">
1058
+ <i class="fas fa-star"></i>
1059
+ </div>
1060
+ </div>
1061
+ </div>
1062
+ </div>
1063
+ </section>
1064
+
1065
+ <!-- نمودارها -->
1066
+ <section class="charts-section">
1067
+ <div class="chart-card">
1068
+ <div class="chart-header">
1069
+ <h2 class="chart-title">روند پردازش اسناد</h2>
1070
+ <div class="chart-filters">
1071
+ <button type="button" class="chart-filter" onclick="updateChart('daily')">روزانه</button>
1072
+ <button type="button" class="chart-filter active" onclick="updateChart('weekly')">هفتگی</button>
1073
+ <button type="button" class="chart-filter" onclick="updateChart('monthly')">ماهانه</button>
1074
+ </div>
1075
+ </div>
1076
+ <div class="chart-container" id="documentsChart">
1077
+ <div class="chart-placeholder" id="chartPlaceholder">
1078
+ <i class="fas fa-chart-line"></i>
1079
+ <p>نمودار روند پردازش</p>
1080
+ <small>Chart.js در حال بارگذاری...</small>
1081
+ </div>
1082
+ <canvas id="documentsChartCanvas" style="display: none;"></canvas>
1083
+ </div>
1084
+ </div>
1085
+
1086
+ <div class="chart-card">
1087
+ <div class="chart-header">
1088
+ <h2 class="chart-title">توزیع وضعیت اسناد</h2>
1089
+ <div class="chart-filters">
1090
+ <button type="button" class="chart-filter active" onclick="updateStatusChart('status')">وضعیت</button>
1091
+ <button type="button" class="chart-filter" onclick="updateStatusChart('category')">دسته‌بندی</button>
1092
+ </div>
1093
+ </div>
1094
+ <div class="chart-container" id="statusChart">
1095
+ <div class="chart-placeholder" id="statusPlaceholder">
1096
+ <i class="fas fa-chart-pie"></i>
1097
+ <p>نمودار توزیع وضعیت</p>
1098
+ <small>Chart.js در حال بارگذاری...</small>
1099
+ </div>
1100
+ <canvas id="statusChartCanvas" style="display: none;"></canvas>
1101
+ </div>
1102
+ </div>
1103
+ </section>
1104
+
1105
+ <!-- دسترسی سریع -->
1106
+ <section class="quick-access-section">
1107
+ <div class="chart-card">
1108
+ <div class="chart-header">
1109
+ <h2 class="chart-title">
1110
+ <i class="fas fa-bolt"></i>
1111
+ دسترسی سریع
1112
+ </h2>
1113
+ </div>
1114
+ <div class="quick-access-grid">
1115
+ <a href="upload.html" class="quick-access-item">
1116
+ <div class="quick-access-icon">
1117
+ <i class="fas fa-cloud-upload-alt"></i>
1118
+ </div>
1119
+ <div class="quick-access-content">
1120
+ <h3>آپلود سند جدید</h3>
1121
+ <p>آپلود و پردازش اسناد PDF</p>
1122
+ </div>
1123
+ </a>
1124
+
1125
+ <a href="documents.html" class="quick-access-item">
1126
+ <div class="quick-access-icon">
1127
+ <i class="fas fa-folder-open"></i>
1128
+ </div>
1129
+ <div class="quick-access-content">
1130
+ <h3>مدیریت اسناد</h3>
1131
+ <p>مشاهده و ویرایش اسناد</p>
1132
+ </div>
1133
+ </a>
1134
+
1135
+ <a href="search.html" class="quick-access-item">
1136
+ <div class="quick-access-icon">
1137
+ <i class="fas fa-search"></i>
1138
+ </div>
1139
+ <div class="quick-access-content">
1140
+ <h3>جستجو در اسناد</h3>
1141
+ <p>جستجوی هوشمند در محتوا</p>
1142
+ </div>
1143
+ </a>
1144
+
1145
+ <a href="scraping.html" class="quick-access-item">
1146
+ <div class="quick-access-icon">
1147
+ <i class="fas fa-globe"></i>
1148
+ </div>
1149
+ <div class="quick-access-content">
1150
+ <h3>استخراج از وب</h3>
1151
+ <p>دریافت محتوا از وب‌سایت‌ها</p>
1152
+ </div>
1153
+ </a>
1154
+
1155
+ <a href="analytics.html" class="quick-access-item">
1156
+ <div class="quick-access-icon">
1157
+ <i class="fas fa-chart-line"></i>
1158
+ </div>
1159
+ <div class="quick-access-content">
1160
+ <h3>آمار و تحلیل</h3>
1161
+ <p>تحلیل عملکرد و آمار</p>
1162
+ </div>
1163
+ </a>
1164
+
1165
+ <a href="reports.html" class="quick-access-item">
1166
+ <div class="quick-access-icon">
1167
+ <i class="fas fa-file-export"></i>
1168
+ </div>
1169
+ <div class="quick-access-content">
1170
+ <h3>گزارش‌گیری</h3>
1171
+ <p>تولید گزارش‌های تفصیلی</p>
1172
+ </div>
1173
+ </a>
1174
+ </div>
1175
+ </div>
1176
+ </section>
1177
+ </main>
1178
+ </div>
1179
+
1180
+ <!-- Toast Container -->
1181
+ <div class="toast-container" id="toastContainer"></div>
1182
+
1183
+ <!-- Connection Status -->
1184
+ <div class="connection-status online" id="connectionStatus">
1185
+ <div class="status-indicator"></div>
1186
+ <span>متصل به سرور</span>
1187
+ </div>
1188
+
1189
+ <script>
1190
+ // Global variables
1191
+ let documentsChart = null;
1192
+ let statusChart = null;
1193
+ let chartJsLoaded = false;
1194
+ let isOnline = false;
1195
+
1196
+ // API Configuration
1197
+ const API_ENDPOINTS = {
1198
+ // Dashboard endpoints
1199
+ dashboardSummary: '/api/dashboard/summary',
1200
+ chartsData: '/api/dashboard/charts-data',
1201
+ aiSuggestions: '/api/dashboard/ai-suggestions',
1202
+ trainAI: '/api/dashboard/ai-feedback',
1203
+ performanceMetrics: '/api/dashboard/performance-metrics',
1204
+ trends: '/api/dashboard/trends',
1205
+
1206
+ // Documents endpoints
1207
+ documents: '/api/documents',
1208
+ documentSearch: '/api/documents/search/',
1209
+ categories: '/api/documents/categories/',
1210
+ sources: '/api/documents/sources/',
1211
+
1212
+ // OCR endpoints
1213
+ ocrProcess: '/api/ocr/process',
1214
+ ocrProcessAndSave: '/api/ocr/process-and-save',
1215
+ ocrBatchProcess: '/api/ocr/batch-process',
1216
+ ocrQualityMetrics: '/api/ocr/quality-metrics',
1217
+ ocrModels: '/api/ocr/models',
1218
+ ocrStatus: '/api/ocr/status',
1219
+
1220
+ // Analytics endpoints
1221
+ analyticsOverview: '/api/analytics/overview',
1222
+ analyticsTrends: '/api/analytics/trends',
1223
+ analyticsSimilarity: '/api/analytics/similarity',
1224
+ analyticsPerformance: '/api/analytics/performance',
1225
+ analyticsEntities: '/api/analytics/entities',
1226
+ analyticsQuality: '/api/analytics/quality-analysis',
1227
+
1228
+ // Scraping endpoints
1229
+ scrapingStart: '/api/scraping/scrape',
1230
+ scrapingStatus: '/api/scraping/status',
1231
+ scrapingItems: '/api/scraping/items',
1232
+ scrapingStatistics: '/api/scraping/statistics',
1233
+ ratingSummary: '/api/scraping/rating/summary',
1234
+
1235
+ // System endpoints
1236
+ health: '/api/health'
1237
+ };
1238
+
1239
+ // Initialize when page loads
1240
+ document.addEventListener('DOMContentLoaded', function() {
1241
+ console.log('🏠 Dashboard loading...');
1242
+ initializeDashboard();
1243
+ });
1244
+
1245
+ async function initializeDashboard() {
1246
+ try {
1247
+ // Test connection first
1248
+ isOnline = await testConnection();
1249
+
1250
+ // Setup Chart.js loading check
1251
+ setTimeout(() => {
1252
+ chartJsLoaded = typeof Chart !== 'undefined';
1253
+ console.log('Chart.js loaded:', chartJsLoaded);
1254
+
1255
+ if (chartJsLoaded) {
1256
+ initializeCharts();
1257
+ } else {
1258
+ console.warn('Chart.js not loaded, keeping placeholders');
1259
+ showToast('Chart.js بارگذاری نشد - نمودارها غیرفعال هستند', 'warning', 'هشدار');
1260
+ }
1261
+ }, 1000);
1262
+
1263
+ setupEventListeners();
1264
+ await loadInitialData();
1265
+ showToast('داشبورد با موفقیت بارگذاری شد', 'success', 'خوش آمدید');
1266
+
1267
+ } catch (error) {
1268
+ console.error('Failed to initialize dashboard:', error);
1269
+ isOnline = false;
1270
+ setupEventListeners();
1271
+ showToast('حالت آفلاین فعال است', 'warning', 'اتصال ناموفق');
1272
+ }
1273
+ }
1274
+
1275
+ async function testConnection() {
1276
+ try {
1277
+ // Try to connect to API if available
1278
+ if (window.legalAPI && window.legalAPI.healthCheck) {
1279
+ await window.legalAPI.healthCheck();
1280
+ return true;
1281
+ } else {
1282
+ // Fallback to simple fetch
1283
+ const response = await fetch(API_ENDPOINTS.health);
1284
+ return response.ok;
1285
+ }
1286
+ } catch (error) {
1287
+ console.log('API connection failed, using offline mode');
1288
+ return false;
1289
+ }
1290
+ }
1291
+
1292
+ // Initialize charts if Chart.js is available
1293
+ function initializeCharts() {
1294
+ if (!chartJsLoaded) return;
1295
+
1296
+ try {
1297
+ // Hide placeholders and show canvases
1298
+ document.getElementById('chartPlaceholder').style.display = 'none';
1299
+ document.getElementById('statusPlaceholder').style.display = 'none';
1300
+ document.getElementById('documentsChartCanvas').style.display = 'block';
1301
+ document.getElementById('statusChartCanvas').style.display = 'block';
1302
+
1303
+ // Processing trends chart
1304
+ const documentsCtx = document.getElementById('documentsChartCanvas');
1305
+ if (documentsCtx) {
1306
+ documentsChart = new Chart(documentsCtx, {
1307
+ type: 'line',
1308
+ data: {
1309
+ labels: ['هفته 1', 'هفته 2', 'هفته 3', 'هفته 4'],
1310
+ datasets: [
1311
+ {
1312
+ label: 'پردازش شده',
1313
+ data: [85, 92, 78, 95],
1314
+ borderColor: '#10b981',
1315
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
1316
+ tension: 0.4,
1317
+ borderWidth: 3,
1318
+ pointRadius: 6,
1319
+ pointHoverRadius: 8
1320
+ },
1321
+ {
1322
+ label: 'آپلود شده',
1323
+ data: [95, 105, 88, 110],
1324
+ borderColor: '#3b82f6',
1325
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
1326
+ tension: 0.4,
1327
+ borderWidth: 3,
1328
+ pointRadius: 6,
1329
+ pointHoverRadius: 8
1330
+ }
1331
+ ]
1332
+ },
1333
+ options: {
1334
+ responsive: true,
1335
+ maintainAspectRatio: false,
1336
+ plugins: {
1337
+ legend: {
1338
+ position: 'top',
1339
+ labels: {
1340
+ usePointStyle: true,
1341
+ padding: 20,
1342
+ font: {
1343
+ family: 'Vazirmatn',
1344
+ size: 12
1345
+ }
1346
+ }
1347
+ }
1348
+ },
1349
+ scales: {
1350
+ y: {
1351
+ beginAtZero: true,
1352
+ grid: {
1353
+ color: 'rgba(0, 0, 0, 0.05)'
1354
+ },
1355
+ ticks: {
1356
+ font: {
1357
+ family: 'Vazirmatn'
1358
+ }
1359
+ }
1360
+ },
1361
+ x: {
1362
+ grid: {
1363
+ color: 'rgba(0, 0, 0, 0.05)'
1364
+ },
1365
+ ticks: {
1366
+ font: {
1367
+ family: 'Vazirmatn'
1368
+ }
1369
+ }
1370
+ }
1371
+ },
1372
+ interaction: {
1373
+ intersect: false,
1374
+ mode: 'index'
1375
+ }
1376
+ }
1377
+ });
1378
+ }
1379
+
1380
+ // Status distribution chart
1381
+ const statusCtx = document.getElementById('statusChartCanvas');
1382
+ if (statusCtx) {
1383
+ statusChart = new Chart(statusCtx, {
1384
+ type: 'doughnut',
1385
+ data: {
1386
+ labels: ['پردازش شده', 'در حال پردازش', 'خطا', 'آپلود شده'],
1387
+ datasets: [{
1388
+ data: [4, 1, 1, 0],
1389
+ backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#3b82f6'],
1390
+ borderColor: '#ffffff',
1391
+ borderWidth: 3,
1392
+ hoverBorderWidth: 5
1393
+ }]
1394
+ },
1395
+ options: {
1396
+ responsive: true,
1397
+ maintainAspectRatio: false,
1398
+ plugins: {
1399
+ legend: {
1400
+ position: 'bottom',
1401
+ labels: {
1402
+ usePointStyle: true,
1403
+ padding: 15,
1404
+ font: {
1405
+ family: 'Vazirmatn',
1406
+ size: 11
1407
+ }
1408
+ }
1409
+ }
1410
+ },
1411
+ cutout: '60%'
1412
+ }
1413
+ });
1414
+ }
1415
+
1416
+ console.log('Charts initialized successfully');
1417
+ showToast('نمودارها بارگذاری شدند', 'success', 'موفقیت');
1418
+
1419
+ } catch (error) {
1420
+ console.error('Chart initialization failed:', error);
1421
+ showToast('خطا در بارگذاری نمودارها', 'error', 'خطا');
1422
+ }
1423
+ }
1424
+
1425
+ // Setup event listeners
1426
+ function setupEventListeners() {
1427
+ // Mobile menu toggle
1428
+ const mobileMenuToggle = document.getElementById('mobileMenuToggle');
1429
+ const sidebar = document.getElementById('sidebar');
1430
+
1431
+ if (mobileMenuToggle && sidebar) {
1432
+ mobileMenuToggle.addEventListener('click', () => {
1433
+ sidebar.classList.toggle('open');
1434
+ });
1435
+
1436
+ // Close sidebar when clicking outside on mobile
1437
+ document.addEventListener('click', (e) => {
1438
+ if (window.innerWidth <= 992 &&
1439
+ sidebar.classList.contains('open') &&
1440
+ !sidebar.contains(e.target) &&
1441
+ !mobileMenuToggle.contains(e.target)) {
1442
+ sidebar.classList.remove('open');
1443
+ }
1444
+ });
1445
+ }
1446
+
1447
+ // Search functionality
1448
+ const searchInput = document.getElementById('searchInput');
1449
+ if (searchInput) {
1450
+ searchInput.addEventListener('input', function(e) {
1451
+ const searchTerm = e.target.value.trim();
1452
+ if (searchTerm.length > 2) {
1453
+ showToast(`جستجو برای: ${searchTerm}`, 'info', 'جستجو');
1454
+ }
1455
+ });
1456
+ }
1457
+ }
1458
+
1459
+ // Load initial data
1460
+ async function loadInitialData() {
1461
+ try {
1462
+ showToast('در حال بارگذاری داده‌ها...', 'info');
1463
+
1464
+ if (isOnline) {
1465
+ // Try to load real data from API
1466
+ await Promise.all([
1467
+ loadDashboardStats(),
1468
+ loadChartsData(),
1469
+ updateDocumentsBadge()
1470
+ ]);
1471
+ showToast('داده‌ها از سرور بارگذاری شدند', 'success');
1472
+ } else {
1473
+ // Use mock data in offline mode
1474
+ loadMockData();
1475
+ showToast('داده‌های آزمایشی بارگذاری شدند', 'info');
1476
+ }
1477
+
1478
+ } catch (error) {
1479
+ console.error('Error loading initial data:', error);
1480
+ // Fallback to mock data
1481
+ loadMockData();
1482
+ showToast('خطا در بارگذاری - از داده���های آزمایشی استفاده شد', 'warning');
1483
+ }
1484
+ }
1485
+
1486
+ // Load dashboard statistics from API
1487
+ async function loadDashboardStats() {
1488
+ try {
1489
+ const response = await fetch(API_ENDPOINTS.dashboardSummary);
1490
+ if (!response.ok) throw new Error('Failed to load dashboard stats');
1491
+
1492
+ const stats = await response.json();
1493
+
1494
+ // Update UI with real data
1495
+ document.getElementById('totalDocuments').textContent = stats.total_documents || 0;
1496
+ document.getElementById('processedDocuments').textContent = stats.processed_documents || 0;
1497
+ document.getElementById('errorDocuments').textContent = stats.error_documents || 0;
1498
+ document.getElementById('averageQuality').textContent = (stats.average_quality || 0).toFixed(1);
1499
+
1500
+ console.log('Dashboard stats loaded from API');
1501
+ } catch (error) {
1502
+ console.error('Failed to load dashboard stats:', error);
1503
+ throw error;
1504
+ }
1505
+ }
1506
+
1507
+ // Load charts data from API
1508
+ async function loadChartsData() {
1509
+ try {
1510
+ const response = await fetch(API_ENDPOINTS.chartsData);
1511
+ if (!response.ok) throw new Error('Failed to load charts data');
1512
+
1513
+ const chartsData = await response.json();
1514
+ console.log('Charts data loaded from API');
1515
+ return chartsData;
1516
+ } catch (error) {
1517
+ console.error('Failed to load charts data:', error);
1518
+ throw error;
1519
+ }
1520
+ }
1521
+
1522
+ // Update documents badge
1523
+ async function updateDocumentsBadge() {
1524
+ try {
1525
+ const response = await fetch(API_ENDPOINTS.documents);
1526
+ if (!response.ok) throw new Error('Failed to load documents count');
1527
+
1528
+ const data = await response.json();
1529
+ const badge = document.getElementById('totalDocumentsBadge');
1530
+ if (badge && data.total_count !== undefined) {
1531
+ badge.textContent = data.total_count;
1532
+ }
1533
+ } catch (error) {
1534
+ console.error('Failed to update documents badge:', error);
1535
+ throw error;
1536
+ }
1537
+ }
1538
+
1539
+ // Load mock data for offline mode
1540
+ function loadMockData() {
1541
+ // Update stats with mock data
1542
+ document.getElementById('totalDocuments').textContent = '6';
1543
+ document.getElementById('processedDocuments').textContent = '4';
1544
+ document.getElementById('errorDocuments').textContent = '1';
1545
+ document.getElementById('averageQuality').textContent = '8.1';
1546
+
1547
+ // Update badge
1548
+ const badge = document.getElementById('totalDocumentsBadge');
1549
+ if (badge) {
1550
+ badge.textContent = '6';
1551
+ }
1552
+ }
1553
+
1554
+ // Chart update functions
1555
+ function updateChart(period) {
1556
+ if (!chartJsLoaded || !documentsChart) {
1557
+ showToast('نمودارها در دسترس نیستند', 'warning', 'هشدار');
1558
+ return;
1559
+ }
1560
+
1561
+ // Update active filter
1562
+ document.querySelectorAll('.chart-filter').forEach(btn => {
1563
+ btn.classList.remove('active');
1564
+ });
1565
+ event.target.classList.add('active');
1566
+
1567
+ // Mock data for different periods
1568
+ const data = {
1569
+ daily: {
1570
+ labels: ['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه'],
1571
+ processed: [12, 19, 8, 15, 22, 18, 14],
1572
+ uploaded: [15, 23, 12, 18, 25, 21, 16]
1573
+ },
1574
+ weekly: {
1575
+ labels: ['هفته 1', 'هفته 2', 'هفته 3', 'هفته 4'],
1576
+ processed: [85, 92, 78, 95],
1577
+ uploaded: [95, 105, 88, 110]
1578
+ },
1579
+ monthly: {
1580
+ labels: ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور'],
1581
+ processed: [340, 380, 290, 420, 380, 450],
1582
+ uploaded: [380, 420, 320, 460, 410, 490]
1583
+ }
1584
+ };
1585
+
1586
+ const selectedData = data[period] || data.weekly;
1587
+
1588
+ documentsChart.data.labels = selectedData.labels;
1589
+ documentsChart.data.datasets[0].data = selectedData.processed;
1590
+ documentsChart.data.datasets[1].data = selectedData.uploaded;
1591
+ documentsChart.update('active');
1592
+
1593
+ showToast(`نمودار به حالت ${period === 'daily' ? 'روزانه' : period === 'weekly' ? 'هفتگی' : 'ماهانه'} تغییر کرد`, 'info', 'بروزرسانی');
1594
+ }
1595
+
1596
+ function updateStatusChart(type) {
1597
+ if (!chartJsLoaded || !statusChart) {
1598
+ showToast('نمودارها در دسترس نیستند', 'warning', 'هشدار');
1599
+ return;
1600
+ }
1601
+
1602
+ // Update active filter
1603
+ const chartCard = event.target.closest('.chart-card');
1604
+ chartCard.querySelectorAll('.chart-filter').forEach(btn => {
1605
+ btn.classList.remove('active');
1606
+ });
1607
+ event.target.classList.add('active');
1608
+
1609
+ const data = {
1610
+ status: {
1611
+ labels: ['پردازش شده', 'در حال پردازش', 'خطا', 'آپلود شده'],
1612
+ data: [4, 1, 1, 0],
1613
+ colors: ['#10b981', '#f59e0b', '#ef4444', '#3b82f6']
1614
+ },
1615
+ category: {
1616
+ labels: ['قراردادها', 'دادخواست‌ها', 'احکام قضایی', 'آرای دیوان', 'سایر'],
1617
+ data: [1, 1, 1, 1, 2],
1618
+ colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']
1619
+ }
1620
+ };
1621
+
1622
+ const selectedData = data[type] || data.status;
1623
+
1624
+ statusChart.data.labels = selectedData.labels;
1625
+ statusChart.data.datasets[0].data = selectedData.data;
1626
+ statusChart.data.datasets[0].backgroundColor = selectedData.colors;
1627
+ statusChart.update('active');
1628
+
1629
+ showToast(`نمودار به حالت ${type === 'status' ? 'وضعیت' : 'دسته‌بندی'} تغییر کرد`, 'info', 'بروزرسانی');
1630
+ }
1631
+
1632
+ function showToast(message, type = 'info', title = 'اعلان') {
1633
+ const toastContainer = document.getElementById('toastContainer');
1634
+ if (!toastContainer) return;
1635
+
1636
+ const toast = document.createElement('div');
1637
+ toast.className = `toast ${type}`;
1638
+
1639
+ const icons = {
1640
+ success: 'check-circle',
1641
+ error: 'exclamation-triangle',
1642
+ warning: 'exclamation-circle',
1643
+ info: 'info-circle'
1644
+ };
1645
+
1646
+ toast.innerHTML = `
1647
+ <div class="toast-icon">
1648
+ <i class="fas fa-${icons[type]}"></i>
1649
+ </div>
1650
+ <div class="toast-content">
1651
+ <div class="toast-title">${title}</div>
1652
+ <div class="toast-message">${message}</div>
1653
+ </div>
1654
+ <button type="button" class="toast-close" onclick="this.parentElement.remove()">
1655
+ <i class="fas fa-times"></i>
1656
+ </button>
1657
+ `;
1658
+
1659
+ toastContainer.appendChild(toast);
1660
+
1661
+ // Show toast
1662
+ setTimeout(() => toast.classList.add('show'), 100);
1663
+
1664
+ // Auto remove after 5 seconds
1665
+ setTimeout(() => {
1666
+ if (toast.parentElement) {
1667
+ toast.classList.remove('show');
1668
+ setTimeout(() => {
1669
+ if (toast.parentElement) {
1670
+ toast.remove();
1671
+ }
1672
+ }, 300);
1673
+ }
1674
+ }, 5000);
1675
+ }
1676
+
1677
+ // Connection status monitoring
1678
+ async function checkConnectionStatus() {
1679
+ try {
1680
+ const response = await fetch(API_ENDPOINTS.health);
1681
+ const status = response.ok;
1682
+
1683
+ const connectionStatus = document.getElementById('connectionStatus');
1684
+ if (connectionStatus) {
1685
+ if (status) {
1686
+ connectionStatus.className = 'connection-status online';
1687
+ connectionStatus.innerHTML = `
1688
+ <div class="status-indicator"></div>
1689
+ <span>متصل به سرور</span>
1690
+ `;
1691
+
1692
+ // Update online status and refresh data if needed
1693
+ if (!isOnline) {
1694
+ isOnline = true;
1695
+ loadInitialData(); // Refresh with real data
1696
+ }
1697
+ } else {
1698
+ throw new Error('Server not responding');
1699
+ }
1700
+ }
1701
+ } catch (error) {
1702
+ const connectionStatus = document.getElementById('connectionStatus');
1703
+ if (connectionStatus) {
1704
+ connectionStatus.className = 'connection-status offline';
1705
+ connectionStatus.innerHTML = `
1706
+ <div class="status-indicator"></div>
1707
+ <span>خطا در اتصال</span>
1708
+ `;
1709
+
1710
+ // Update offline status
1711
+ if (isOnline) {
1712
+ isOnline = false;
1713
+ showToast('اتصال قطع شد - حالت آفلاین فعال', 'warning', 'اتصال');
1714
+ }
1715
+ }
1716
+ }
1717
+ }
1718
+
1719
+ // Check connection status every 30 seconds
1720
+ setInterval(checkConnectionStatus, 30000);
1721
+
1722
+ // Initial connection check after 2 seconds
1723
+ setTimeout(checkConnectionStatus, 2000);
1724
+
1725
+ console.log('🏠 Legal Dashboard Index Page Ready!');
1726
+ </script>
1727
+ </body>
1728
+ </html>
app/frontend/js/api-client.js ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Legal Dashboard API Client
3
+ * =========================
4
+ *
5
+ * JavaScript client for communicating with the Legal Dashboard backend API.
6
+ * Handles all HTTP requests and data transformation between frontend and backend.
7
+ */
8
+
9
+ class LegalDashboardAPI {
10
+ constructor(baseUrl = '') {
11
+ this.baseUrl = baseUrl || window.location.origin;
12
+ this.apiBase = `${this.baseUrl}/api`;
13
+
14
+ // Request interceptor for common headers
15
+ this.defaultHeaders = {
16
+ 'Content-Type': 'application/json',
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Generic HTTP request handler with error handling
22
+ */
23
+ async request(endpoint, options = {}) {
24
+ const url = `${this.apiBase}${endpoint}`;
25
+
26
+ const config = {
27
+ ...options,
28
+ headers: {
29
+ ...this.defaultHeaders,
30
+ ...options.headers
31
+ }
32
+ };
33
+
34
+ try {
35
+ const response = await fetch(url, config);
36
+
37
+ if (!response.ok) {
38
+ const errorData = await response.json().catch(() => ({}));
39
+ throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
40
+ }
41
+
42
+ const contentType = response.headers.get('content-type');
43
+ if (contentType && contentType.includes('application/json')) {
44
+ return await response.json();
45
+ }
46
+
47
+ return await response.text();
48
+
49
+ } catch (error) {
50
+ console.error(`API Request failed: ${endpoint}`, error);
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Health check - verify backend is running
57
+ */
58
+ async healthCheck() {
59
+ return this.request('/health');
60
+ }
61
+
62
+ // ==================== DASHBOARD API ====================
63
+
64
+ /**
65
+ * Get dashboard summary statistics
66
+ */
67
+ async getDashboardSummary() {
68
+ const response = await this.request('/dashboard/summary');
69
+ return response.data;
70
+ }
71
+
72
+ /**
73
+ * Get processing trends for charts
74
+ */
75
+ async getProcessingTrends(period = 'weekly') {
76
+ const response = await this.request(`/dashboard/charts/processing-trends?period=${period}`);
77
+ return response.data;
78
+ }
79
+
80
+ /**
81
+ * Get status distribution for pie chart
82
+ */
83
+ async getStatusDistribution() {
84
+ const response = await this.request('/dashboard/charts/status-distribution');
85
+ return response.data;
86
+ }
87
+
88
+ /**
89
+ * Get category distribution for pie chart
90
+ */
91
+ async getCategoryDistribution() {
92
+ const response = await this.request('/dashboard/charts/category-distribution');
93
+ return response.data;
94
+ }
95
+
96
+ // ==================== DOCUMENTS API ====================
97
+
98
+ /**
99
+ * Get paginated list of documents with filtering
100
+ */
101
+ async getDocuments(params = {}) {
102
+ const searchParams = new URLSearchParams();
103
+
104
+ Object.entries(params).forEach(([key, value]) => {
105
+ if (value !== null && value !== undefined && value !== '') {
106
+ searchParams.append(key, value);
107
+ }
108
+ });
109
+
110
+ const endpoint = `/documents/?${searchParams.toString()}`;
111
+ return this.request(endpoint);
112
+ }
113
+
114
+ /**
115
+ * Get single document by ID
116
+ */
117
+ async getDocument(documentId) {
118
+ return this.request(`/documents/${documentId}`);
119
+ }
120
+
121
+ /**
122
+ * Delete document by ID
123
+ */
124
+ async deleteDocument(documentId) {
125
+ return this.request(`/documents/${documentId}`, {
126
+ method: 'DELETE'
127
+ });
128
+ }
129
+
130
+ // ==================== OCR API ====================
131
+
132
+ /**
133
+ * Upload files for OCR processing
134
+ */
135
+ async uploadFiles(files) {
136
+ const formData = new FormData();
137
+
138
+ // Add files to form data
139
+ files.forEach(file => {
140
+ formData.append('files', file);
141
+ });
142
+
143
+ return this.request('/ocr/upload', {
144
+ method: 'POST',
145
+ headers: {}, // Remove Content-Type to let browser set it for FormData
146
+ body: formData
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Extract text from PDF immediately (for testing)
152
+ */
153
+ async extractTextImmediate(file) {
154
+ const formData = new FormData();
155
+ formData.append('file', file);
156
+
157
+ return this.request('/ocr/extract', {
158
+ method: 'POST',
159
+ headers: {},
160
+ body: formData
161
+ });
162
+ }
163
+
164
+ // ==================== SCRAPING API ====================
165
+
166
+ /**
167
+ * Scrape content from website
168
+ */
169
+ async scrapeWebsite(scrapingRequest) {
170
+ return this.request('/scraping/scrape', {
171
+ method: 'POST',
172
+ body: JSON.stringify(scrapingRequest)
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Get scraping history
178
+ */
179
+ async getScrapingHistory(skip = 0, limit = 50) {
180
+ const response = await this.request(`/scraping/scrape/history?skip=${skip}&limit=${limit}`);
181
+ return response.data;
182
+ }
183
+
184
+ // ==================== ANALYTICS API ====================
185
+
186
+ /**
187
+ * Calculate similarity between two documents
188
+ */
189
+ async calculateSimilarity(doc1Id, doc2Id) {
190
+ const response = await this.request(`/analytics/similarity?doc1_id=${doc1Id}&doc2_id=${doc2Id}`);
191
+ return response.data;
192
+ }
193
+
194
+ /**
195
+ * Get quality analysis
196
+ */
197
+ async getQualityAnalysis() {
198
+ const response = await this.request('/analytics/quality-analysis');
199
+ return response.data;
200
+ }
201
+
202
+ /**
203
+ * Get performance metrics
204
+ */
205
+ async getPerformanceMetrics() {
206
+ const response = await this.request('/analytics/performance-metrics');
207
+ return response.data;
208
+ }
209
+ }
210
+
211
+ // ==================== DATA MODELS ====================
212
+
213
+ /**
214
+ * Document model to match backend structure
215
+ */
216
+ class DocumentModel {
217
+ constructor(backendData) {
218
+ this.id = backendData.id;
219
+ this.filename = backendData.filename;
220
+ this.original_filename = backendData.original_filename;
221
+ this.file_size = backendData.file_size;
222
+ this.file_path = backendData.file_path;
223
+ this.category = backendData.category;
224
+ this.quality_score = backendData.quality_score || 0;
225
+ this.confidence_score = backendData.confidence_score || 0;
226
+ this.status = backendData.status;
227
+ this.created_at = backendData.created_at;
228
+ this.processed_at = backendData.processed_at;
229
+ this.ocr_text = backendData.ocr_text;
230
+ this.summary = backendData.summary;
231
+ this.keywords = backendData.keywords || [];
232
+ this.extracted_entities = backendData.extracted_entities || [];
233
+ this.processing_time = backendData.processing_time || 0;
234
+ this.importance_score = backendData.importance_score || 0;
235
+ this.similarity_scores = backendData.similarity_scores || {};
236
+ this.legal_references = backendData.legal_references || [];
237
+ }
238
+
239
+ /**
240
+ * Format file size for display
241
+ */
242
+ getFormattedFileSize() {
243
+ return formatFileSize(this.file_size);
244
+ }
245
+
246
+ /**
247
+ * Get creation date formatted for Persian locale
248
+ */
249
+ getFormattedDate() {
250
+ return formatDate(this.created_at);
251
+ }
252
+
253
+ /**
254
+ * Get quality class for styling
255
+ */
256
+ getQualityClass() {
257
+ if (this.quality_score >= 8.5) return 'quality-excellent';
258
+ if (this.quality_score >= 6.5) return 'quality-good';
259
+ if (this.quality_score >= 4.5) return 'quality-average';
260
+ return 'quality-poor';
261
+ }
262
+
263
+ /**
264
+ * Get status text in Persian
265
+ */
266
+ getStatusText() {
267
+ const statusMap = {
268
+ 'processed': 'پردازش شده',
269
+ 'processing': 'در حال پردازش',
270
+ 'uploaded': 'آپلود شده',
271
+ 'pending': 'در انتظار',
272
+ 'error': 'خطا'
273
+ };
274
+ return statusMap[this.status] || this.status;
275
+ }
276
+
277
+ /**
278
+ * Get status icon
279
+ */
280
+ getStatusIcon() {
281
+ const iconMap = {
282
+ 'processed': 'check-circle',
283
+ 'processing': 'spinner fa-spin',
284
+ 'uploaded': 'cloud-upload-alt',
285
+ 'pending': 'clock',
286
+ 'error': 'exclamation-triangle'
287
+ };
288
+ return iconMap[this.status] || 'question-circle';
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Scraping result model
294
+ */
295
+ class ScrapingResultModel {
296
+ constructor(backendData) {
297
+ this.success = backendData.success;
298
+ this.url = backendData.url;
299
+ this.title = backendData.title;
300
+ this.content_length = backendData.content_length;
301
+ this.processing_time = backendData.processing_time;
302
+ this.data = backendData.data;
303
+ this.error = backendData.error;
304
+ }
305
+
306
+ isSuccessful() {
307
+ return this.success && !this.error;
308
+ }
309
+
310
+ getFormattedProcessingTime() {
311
+ return `${this.processing_time.toFixed(2)} ثانیه`;
312
+ }
313
+ }
314
+
315
+ // ==================== UTILITY FUNCTIONS ====================
316
+
317
+ /**
318
+ * Format file size in bytes to human readable format
319
+ */
320
+ function formatFileSize(bytes) {
321
+ if (bytes === 0) return '0 بایت';
322
+ const k = 1024;
323
+ const sizes = ['بایت', 'کیلوبایت', 'مگابایت', 'گیگابایت'];
324
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
325
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
326
+ }
327
+
328
+ /**
329
+ * Format date for Persian locale
330
+ */
331
+ function formatDate(dateString) {
332
+ if (!dateString) return 'نامشخص';
333
+ const date = new Date(dateString);
334
+ return date.toLocaleDateString('fa-IR', {
335
+ year: 'numeric',
336
+ month: 'long',
337
+ day: 'numeric',
338
+ hour: '2-digit',
339
+ minute: '2-digit'
340
+ });
341
+ }
342
+
343
+ /**
344
+ * Show toast notification
345
+ */
346
+ function showToast(message, type = 'info', title = 'اعلان') {
347
+ const toastContainer = document.getElementById('toastContainer');
348
+ if (!toastContainer) return;
349
+
350
+ const toast = document.createElement('div');
351
+ toast.className = `toast ${type}`;
352
+
353
+ const icons = {
354
+ success: 'check-circle',
355
+ error: 'exclamation-triangle',
356
+ warning: 'exclamation-circle',
357
+ info: 'info-circle'
358
+ };
359
+
360
+ toast.innerHTML = `
361
+ <div class="toast-icon">
362
+ <i class="fas fa-${icons[type]}"></i>
363
+ </div>
364
+ <div class="toast-content">
365
+ <div class="toast-title">${title}</div>
366
+ <div class="toast-message">${message}</div>
367
+ </div>
368
+ <button type="button" class="toast-close" onclick="this.parentElement.remove()">
369
+ <i class="fas fa-times"></i>
370
+ </button>
371
+ `;
372
+
373
+ toastContainer.appendChild(toast);
374
+
375
+ // Show toast
376
+ setTimeout(() => toast.classList.add('show'), 100);
377
+
378
+ // Auto remove after 5 seconds
379
+ setTimeout(() => {
380
+ if (toast.parentElement) {
381
+ toast.classList.remove('show');
382
+ setTimeout(() => {
383
+ if (toast.parentElement) {
384
+ toast.remove();
385
+ }
386
+ }, 300);
387
+ }
388
+ }, 5000);
389
+ }
390
+
391
+ /**
392
+ * Debounce function for search inputs
393
+ */
394
+ function debounce(func, wait) {
395
+ let timeout;
396
+ return function executedFunction(...args) {
397
+ const later = () => {
398
+ clearTimeout(timeout);
399
+ func(...args);
400
+ };
401
+ clearTimeout(timeout);
402
+ timeout = setTimeout(later, wait);
403
+ };
404
+ }
405
+
406
+ // ==================== GLOBAL API INSTANCE ====================
407
+
408
+ // Create global API instance
409
+ window.legalAPI = new LegalDashboardAPI();
410
+
411
+ // Test connection on load
412
+ document.addEventListener('DOMContentLoaded', async () => {
413
+ try {
414
+ await window.legalAPI.healthCheck();
415
+ console.log('✅ Backend connection successful');
416
+ } catch (error) {
417
+ console.warn('⚠️ Backend connection failed, using fallback mode:', error.message);
418
+ showToast('اتصال به سرور برقرار نشد. حالت آفلاین فعال است.', 'warning', 'هشدار اتصال');
419
+ }
420
+ });
421
+
422
+ console.log('🔗 Legal Dashboard API Client loaded');
app/frontend/js/api-connection-test.js ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API Connection Test Script
3
+ * =========================
4
+ *
5
+ * Automated test script to validate all backend API endpoints
6
+ * and provide detailed reporting on frontend-backend integration.
7
+ */
8
+
9
+ class APIConnectionTester {
10
+ constructor() {
11
+ this.baseURL = window.location.origin;
12
+ this.results = [];
13
+ this.startTime = null;
14
+ this.endTime = null;
15
+ }
16
+
17
+ /**
18
+ * Run comprehensive API tests
19
+ */
20
+ async runAllTests() {
21
+ console.log('🚀 Starting API Connection Tests...');
22
+ this.startTime = Date.now();
23
+
24
+ const tests = [
25
+ // System Health Tests
26
+ { name: 'Health Check', url: '/api/health', method: 'GET', category: 'System' },
27
+
28
+ // Dashboard Tests
29
+ { name: 'Dashboard Summary', url: '/api/dashboard/summary', method: 'GET', category: 'Dashboard' },
30
+ { name: 'Charts Data', url: '/api/dashboard/charts-data', method: 'GET', category: 'Dashboard' },
31
+ { name: 'AI Suggestions', url: '/api/dashboard/ai-suggestions', method: 'GET', category: 'Dashboard' },
32
+ { name: 'Performance Metrics', url: '/api/dashboard/performance-metrics', method: 'GET', category: 'Dashboard' },
33
+ { name: 'Trends', url: '/api/dashboard/trends', method: 'GET', category: 'Dashboard' },
34
+
35
+ // Documents Tests
36
+ { name: 'Documents List', url: '/api/documents?limit=5', method: 'GET', category: 'Documents' },
37
+ { name: 'Document Categories', url: '/api/documents/categories/', method: 'GET', category: 'Documents' },
38
+ { name: 'Document Sources', url: '/api/documents/sources/', method: 'GET', category: 'Documents' },
39
+ { name: 'Document Search', url: '/api/documents/search/?q=test', method: 'GET', category: 'Documents' },
40
+
41
+ // OCR Tests
42
+ { name: 'OCR Status', url: '/api/ocr/status', method: 'GET', category: 'OCR' },
43
+ { name: 'OCR Models', url: '/api/ocr/models', method: 'GET', category: 'OCR' },
44
+
45
+ // Analytics Tests
46
+ { name: 'Analytics Overview', url: '/api/analytics/overview', method: 'GET', category: 'Analytics' },
47
+ { name: 'Analytics Performance', url: '/api/analytics/performance', method: 'GET', category: 'Analytics' },
48
+ { name: 'Analytics Entities', url: '/api/analytics/entities?limit=10', method: 'GET', category: 'Analytics' },
49
+ { name: 'Analytics Quality', url: '/api/analytics/quality-analysis', method: 'GET', category: 'Analytics' },
50
+
51
+ // Scraping Tests
52
+ { name: 'Scraping Statistics', url: '/api/scraping/statistics', method: 'GET', category: 'Scraping' },
53
+ { name: 'Scraping Status', url: '/api/scraping/status', method: 'GET', category: 'Scraping' },
54
+ { name: 'Rating Summary', url: '/api/scraping/rating/summary', method: 'GET', category: 'Scraping' },
55
+ { name: 'Scraping Health', url: '/api/scraping/health', method: 'GET', category: 'Scraping' },
56
+
57
+ // Phase 2 - File Upload Tests
58
+ { name: 'OCR Upload', url: '/api/ocr/upload', method: 'POST', category: 'File Upload' },
59
+ { name: 'OCR Process', url: '/api/ocr/process', method: 'POST', category: 'File Upload' },
60
+ { name: 'OCR Quality Metrics', url: '/api/ocr/quality-metrics', method: 'GET', category: 'File Upload' },
61
+
62
+ // Phase 2 - Document Management Tests
63
+ { name: 'Create Document', url: '/api/documents', method: 'POST', category: 'Document Management' },
64
+ { name: 'Update Document', url: '/api/documents/1', method: 'PUT', category: 'Document Management' },
65
+ { name: 'Delete Document', url: '/api/documents/1', method: 'DELETE', category: 'Document Management' },
66
+
67
+ // Phase 2 - Advanced Scraping Tests
68
+ { name: 'Scraping Start', url: '/api/scraping/start', method: 'POST', category: 'Advanced Scraping' },
69
+ { name: 'Scraping Stop', url: '/api/scraping/stop', method: 'POST', category: 'Advanced Scraping' },
70
+ { name: 'Scraping Results', url: '/api/scraping/results', method: 'GET', category: 'Advanced Scraping' }
71
+ ];
72
+
73
+ console.log(`📋 Running ${tests.length} API tests...`);
74
+
75
+ for (const test of tests) {
76
+ await this.runSingleTest(test);
77
+ // Small delay to avoid overwhelming the server
78
+ await this.delay(100);
79
+ }
80
+
81
+ this.endTime = Date.now();
82
+ this.generateReport();
83
+ }
84
+
85
+ /**
86
+ * Run a single API test
87
+ */
88
+ async runSingleTest(test) {
89
+ const startTime = Date.now();
90
+ let result = {
91
+ name: test.name,
92
+ category: test.category,
93
+ url: test.url,
94
+ method: test.method,
95
+ success: false,
96
+ status: null,
97
+ responseTime: 0,
98
+ data: null,
99
+ error: null,
100
+ timestamp: new Date().toISOString()
101
+ };
102
+
103
+ try {
104
+ const response = await fetch(test.url, {
105
+ method: test.method,
106
+ headers: {
107
+ 'Content-Type': 'application/json'
108
+ }
109
+ });
110
+
111
+ result.status = response.status;
112
+ result.responseTime = Date.now() - startTime;
113
+
114
+ if (response.ok) {
115
+ result.success = true;
116
+ try {
117
+ result.data = await response.json();
118
+ } catch (e) {
119
+ result.data = 'Non-JSON response';
120
+ }
121
+ } else {
122
+ result.error = `${response.status}: ${response.statusText}`;
123
+ }
124
+
125
+ } catch (error) {
126
+ result.error = error.message;
127
+ result.responseTime = Date.now() - startTime;
128
+ }
129
+
130
+ this.results.push(result);
131
+
132
+ // Log result
133
+ const status = result.success ? '✅' : '❌';
134
+ console.log(`${status} ${test.name}: ${result.success ? 'PASS' : 'FAIL'} (${result.responseTime}ms)`);
135
+
136
+ return result;
137
+ }
138
+
139
+ /**
140
+ * Generate comprehensive test report
141
+ */
142
+ generateReport() {
143
+ const totalTests = this.results.length;
144
+ const passedTests = this.results.filter(r => r.success).length;
145
+ const failedTests = totalTests - passedTests;
146
+ const totalTime = this.endTime - this.startTime;
147
+ const avgResponseTime = this.results.reduce((sum, r) => sum + r.responseTime, 0) / totalTests;
148
+
149
+ console.log('\n📊 API Connection Test Report');
150
+ console.log('='.repeat(50));
151
+ console.log(`Total Tests: ${totalTests}`);
152
+ console.log(`Passed: ${passedTests} ✅`);
153
+ console.log(`Failed: ${failedTests} ❌`);
154
+ console.log(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
155
+ console.log(`Total Time: ${totalTime}ms`);
156
+ console.log(`Average Response Time: ${avgResponseTime.toFixed(0)}ms`);
157
+
158
+ // Group results by category
159
+ const categories = {};
160
+ this.results.forEach(result => {
161
+ if (!categories[result.category]) {
162
+ categories[result.category] = [];
163
+ }
164
+ categories[result.category].push(result);
165
+ });
166
+
167
+ console.log('\n📈 Results by Category:');
168
+ Object.entries(categories).forEach(([category, results]) => {
169
+ const passed = results.filter(r => r.success).length;
170
+ const total = results.length;
171
+ const rate = ((passed / total) * 100).toFixed(1);
172
+ console.log(`${category}: ${passed}/${total} (${rate}%)`);
173
+ });
174
+
175
+ // Show failed tests
176
+ const failedTests = this.results.filter(r => !r.success);
177
+ if (failedTests.length > 0) {
178
+ console.log('\n❌ Failed Tests:');
179
+ failedTests.forEach(test => {
180
+ console.log(` - ${test.name}: ${test.error}`);
181
+ });
182
+ }
183
+
184
+ // Show slow tests
185
+ const slowTests = this.results.filter(r => r.responseTime > 1000);
186
+ if (slowTests.length > 0) {
187
+ console.log('\n🐌 Slow Tests (>1s):');
188
+ slowTests.forEach(test => {
189
+ console.log(` - ${test.name}: ${test.responseTime}ms`);
190
+ });
191
+ }
192
+
193
+ this.displayResultsInUI();
194
+ }
195
+
196
+ /**
197
+ * Display results in the UI
198
+ */
199
+ displayResultsInUI() {
200
+ const container = document.getElementById('apiTestResults');
201
+ if (!container) {
202
+ console.warn('No #apiTestResults container found');
203
+ return;
204
+ }
205
+
206
+ const totalTests = this.results.length;
207
+ const passedTests = this.results.filter(r => r.success).length;
208
+ const failedTests = totalTests - passedTests;
209
+ const successRate = ((passedTests / totalTests) * 100).toFixed(1);
210
+
211
+ container.innerHTML = `
212
+ <div class="test-report">
213
+ <h3>API Connection Test Results</h3>
214
+ <div class="test-summary">
215
+ <div class="test-stat">
216
+ <span class="stat-label">Total Tests:</span>
217
+ <span class="stat-value">${totalTests}</span>
218
+ </div>
219
+ <div class="test-stat">
220
+ <span class="stat-label">Passed:</span>
221
+ <span class="stat-value success">${passedTests} ✅</span>
222
+ </div>
223
+ <div class="test-stat">
224
+ <span class="stat-label">Failed:</span>
225
+ <span class="stat-value error">${failedTests} ❌</span>
226
+ </div>
227
+ <div class="test-stat">
228
+ <span class="stat-label">Success Rate:</span>
229
+ <span class="stat-value">${successRate}%</span>
230
+ </div>
231
+ </div>
232
+
233
+ <div class="test-details">
234
+ <h4>Test Details:</h4>
235
+ <div class="test-list">
236
+ ${this.results.map(result => `
237
+ <div class="test-item ${result.success ? 'success' : 'error'}">
238
+ <span class="test-name">${result.name}</span>
239
+ <span class="test-status">${result.success ? 'PASS' : 'FAIL'}</span>
240
+ <span class="test-time">${result.responseTime}ms</span>
241
+ ${result.error ? `<span class="test-error">${result.error}</span>` : ''}
242
+ </div>
243
+ `).join('')}
244
+ </div>
245
+ </div>
246
+ </div>
247
+ `;
248
+ }
249
+
250
+ /**
251
+ * Test specific endpoint patterns
252
+ */
253
+ async testEndpointPatterns() {
254
+ console.log('\n🔍 Testing Endpoint Patterns...');
255
+
256
+ const patterns = [
257
+ // Test the broken endpoints that frontend is trying to call
258
+ { name: 'Frontend Dashboard Summary (BROKEN)', url: '/api/dashboard-summary', expected: false },
259
+ { name: 'Frontend Charts Data (BROKEN)', url: '/api/charts-data', expected: false },
260
+ { name: 'Frontend AI Suggestions (BROKEN)', url: '/api/ai-suggestions', expected: false },
261
+ { name: 'Frontend Train AI (BROKEN)', url: '/api/train-ai', expected: false },
262
+ { name: 'Frontend Scrape Trigger (BROKEN)', url: '/api/scrape-trigger', expected: false },
263
+
264
+ // Test the correct endpoints
265
+ { name: 'Backend Dashboard Summary (CORRECT)', url: '/api/dashboard/summary', expected: true },
266
+ { name: 'Backend Charts Data (CORRECT)', url: '/api/dashboard/charts-data', expected: true },
267
+ { name: 'Backend AI Suggestions (CORRECT)', url: '/api/dashboard/ai-suggestions', expected: true },
268
+ { name: 'Backend AI Feedback (CORRECT)', url: '/api/dashboard/ai-feedback', expected: true },
269
+ { name: 'Backend Scrape (CORRECT)', url: '/api/scraping/scrape', expected: true }
270
+ ];
271
+
272
+ for (const pattern of patterns) {
273
+ try {
274
+ const response = await fetch(pattern.url);
275
+ const actual = response.ok;
276
+ const status = actual === pattern.expected ? '✅' : '❌';
277
+ console.log(`${status} ${pattern.name}: ${actual ? 'EXISTS' : 'MISSING'} (Expected: ${pattern.expected ? 'EXISTS' : 'MISSING'})`);
278
+ } catch (error) {
279
+ const status = pattern.expected ? '❌' : '✅';
280
+ console.log(`${status} ${pattern.name}: MISSING (Expected: ${pattern.expected ? 'EXISTS' : 'MISSING'})`);
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Test file upload functionality
287
+ */
288
+ async testFileUpload() {
289
+ console.log('\n📁 Testing File Upload...');
290
+
291
+ // Create a test file
292
+ const testFile = new File(['Test PDF content'], 'test.pdf', { type: 'application/pdf' });
293
+ const formData = new FormData();
294
+ formData.append('file', testFile);
295
+ formData.append('title', 'Test Document');
296
+ formData.append('source', 'Test');
297
+ formData.append('category', 'Test');
298
+
299
+ try {
300
+ const response = await fetch('/api/ocr/process-and-save', {
301
+ method: 'POST',
302
+ body: formData
303
+ });
304
+
305
+ if (response.ok) {
306
+ console.log('✅ File upload endpoint is accessible');
307
+ const result = await response.json();
308
+ console.log('📄 Upload response:', result);
309
+ } else {
310
+ console.log('❌ File upload failed:', response.status, response.statusText);
311
+ }
312
+ } catch (error) {
313
+ console.log('❌ File upload error:', error.message);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Utility function for delays
319
+ */
320
+ delay(ms) {
321
+ return new Promise(resolve => setTimeout(resolve, ms));
322
+ }
323
+ }
324
+
325
+ // Global instance
326
+ window.apiTester = new APIConnectionTester();
327
+
328
+ // Auto-run tests when page loads
329
+ document.addEventListener('DOMContentLoaded', () => {
330
+ console.log('🔧 API Connection Tester loaded');
331
+
332
+ // Add test button if not exists
333
+ if (!document.getElementById('runAPITests')) {
334
+ const testButton = document.createElement('button');
335
+ testButton.id = 'runAPITests';
336
+ testButton.textContent = 'Run API Tests';
337
+ testButton.style.cssText = `
338
+ position: fixed;
339
+ top: 10px;
340
+ right: 10px;
341
+ z-index: 10000;
342
+ padding: 10px 20px;
343
+ background: #007bff;
344
+ color: white;
345
+ border: none;
346
+ border-radius: 5px;
347
+ cursor: pointer;
348
+ `;
349
+ testButton.onclick = () => {
350
+ window.apiTester.runAllTests();
351
+ };
352
+ document.body.appendChild(testButton);
353
+ }
354
+
355
+ // Add results container if not exists
356
+ if (!document.getElementById('apiTestResults')) {
357
+ const resultsContainer = document.createElement('div');
358
+ resultsContainer.id = 'apiTestResults';
359
+ resultsContainer.style.cssText = `
360
+ position: fixed;
361
+ top: 60px;
362
+ right: 10px;
363
+ width: 400px;
364
+ max-height: 500px;
365
+ overflow-y: auto;
366
+ background: white;
367
+ border: 1px solid #ccc;
368
+ border-radius: 5px;
369
+ padding: 15px;
370
+ z-index: 10000;
371
+ display: none;
372
+ `;
373
+ document.body.appendChild(resultsContainer);
374
+ }
375
+ });
376
+
377
+ // Export for use in other scripts
378
+ if (typeof module !== 'undefined' && module.exports) {
379
+ module.exports = APIConnectionTester;
380
+ }
app/frontend/js/core.js ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Legal Dashboard Core Module
3
+ * ==========================
4
+ *
5
+ * Shared core functionality for cross-page communication and data synchronization.
6
+ * This module provides event-driven updates and shared state management across all pages.
7
+ */
8
+
9
+ class DashboardCore {
10
+ constructor() {
11
+ this.eventBus = new EventTarget();
12
+ this.cache = new Map();
13
+ this.apiClient = null;
14
+ this.isInitialized = false;
15
+
16
+ // Initialize when DOM is ready
17
+ if (document.readyState === 'loading') {
18
+ document.addEventListener('DOMContentLoaded', () => this.initialize());
19
+ } else {
20
+ this.initialize();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Initialize the core module
26
+ */
27
+ initialize() {
28
+ if (this.isInitialized) return;
29
+
30
+ console.log('🚀 Initializing Dashboard Core...');
31
+
32
+ // Initialize API client
33
+ this.apiClient = new LegalDashboardAPI();
34
+
35
+ // Set up localStorage synchronization
36
+ this.setupLocalStorageSync();
37
+
38
+ // Set up periodic health checks
39
+ this.setupHealthChecks();
40
+
41
+ // Set up cross-page event listeners
42
+ this.setupEventListeners();
43
+
44
+ this.isInitialized = true;
45
+ console.log('✅ Dashboard Core initialized');
46
+
47
+ // Broadcast initialization event
48
+ this.broadcast('coreInitialized', { timestamp: Date.now() });
49
+ }
50
+
51
+ /**
52
+ * Broadcast events across pages
53
+ */
54
+ broadcast(eventName, data = {}) {
55
+ const event = new CustomEvent(eventName, {
56
+ detail: {
57
+ ...data,
58
+ timestamp: Date.now(),
59
+ source: window.location.pathname
60
+ }
61
+ });
62
+
63
+ this.eventBus.dispatchEvent(event);
64
+
65
+ // Also store in localStorage for cross-tab communication
66
+ this.storeEvent(eventName, data);
67
+
68
+ console.log(`📡 Broadcast: ${eventName}`, data);
69
+ }
70
+
71
+ /**
72
+ * Listen for cross-page events
73
+ */
74
+ listen(eventName, callback) {
75
+ const wrappedCallback = (event) => {
76
+ callback(event.detail);
77
+ };
78
+
79
+ this.eventBus.addEventListener(eventName, wrappedCallback);
80
+
81
+ // Return unsubscribe function
82
+ return () => {
83
+ this.eventBus.removeEventListener(eventName, wrappedCallback);
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Store event in localStorage for cross-tab communication
89
+ */
90
+ storeEvent(eventName, data) {
91
+ try {
92
+ const events = JSON.parse(localStorage.getItem('dashboard_events') || '[]');
93
+ events.push({
94
+ name: eventName,
95
+ data: data,
96
+ timestamp: Date.now(),
97
+ source: window.location.pathname
98
+ });
99
+
100
+ // Keep only last 50 events
101
+ if (events.length > 50) {
102
+ events.splice(0, events.length - 50);
103
+ }
104
+
105
+ localStorage.setItem('dashboard_events', JSON.stringify(events));
106
+ } catch (error) {
107
+ console.warn('Failed to store event in localStorage:', error);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Setup localStorage synchronization
113
+ */
114
+ setupLocalStorageSync() {
115
+ // Listen for storage changes (cross-tab communication)
116
+ window.addEventListener('storage', (event) => {
117
+ if (event.key === 'dashboard_events') {
118
+ try {
119
+ const events = JSON.parse(event.newValue || '[]');
120
+ const latestEvent = events[events.length - 1];
121
+
122
+ if (latestEvent && latestEvent.source !== window.location.pathname) {
123
+ // Re-broadcast event from other tab
124
+ this.eventBus.dispatchEvent(new CustomEvent(latestEvent.name, {
125
+ detail: latestEvent.data
126
+ }));
127
+ }
128
+ } catch (error) {
129
+ console.warn('Failed to process storage event:', error);
130
+ }
131
+ }
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Setup periodic health checks
137
+ */
138
+ setupHealthChecks() {
139
+ // Check API health every 30 seconds
140
+ setInterval(async () => {
141
+ try {
142
+ const health = await this.apiClient.healthCheck();
143
+ this.broadcast('healthUpdate', health);
144
+ } catch (error) {
145
+ this.broadcast('healthUpdate', { status: 'unhealthy', error: error.message });
146
+ }
147
+ }, 30000);
148
+ }
149
+
150
+ /**
151
+ * Setup common event listeners
152
+ */
153
+ setupEventListeners() {
154
+ // Listen for document uploads
155
+ this.listen('documentUploaded', (data) => {
156
+ this.handleDocumentUpload(data);
157
+ });
158
+
159
+ // Listen for document updates
160
+ this.listen('documentUpdated', (data) => {
161
+ this.handleDocumentUpdate(data);
162
+ });
163
+
164
+ // Listen for document deletions
165
+ this.listen('documentDeleted', (data) => {
166
+ this.handleDocumentDelete(data);
167
+ });
168
+
169
+ // Listen for scraping updates
170
+ this.listen('scrapingUpdate', (data) => {
171
+ this.handleScrapingUpdate(data);
172
+ });
173
+
174
+ // Listen for system health updates
175
+ this.listen('healthUpdate', (data) => {
176
+ this.handleHealthUpdate(data);
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Handle document upload events
182
+ */
183
+ handleDocumentUpload(data) {
184
+ console.log('📄 Document uploaded:', data);
185
+
186
+ // Update cache
187
+ this.cache.set(`document_${data.fileId}`, data);
188
+
189
+ // Refresh document lists on relevant pages
190
+ if (window.location.pathname.includes('documents.html') ||
191
+ window.location.pathname.includes('improved_legal_dashboard.html')) {
192
+ this.refreshDocumentList();
193
+ }
194
+
195
+ // Update dashboard stats
196
+ this.updateDashboardStats();
197
+
198
+ // Show notification
199
+ showToast(`فایل "${data.fileName}" با موفقیت آپلود شد`, 'success');
200
+ }
201
+
202
+ /**
203
+ * Handle document update events
204
+ */
205
+ handleDocumentUpdate(data) {
206
+ console.log('📝 Document updated:', data);
207
+
208
+ // Update cache
209
+ this.cache.set(`document_${data.documentId}`, data);
210
+
211
+ // Refresh document lists
212
+ this.refreshDocumentList();
213
+
214
+ // Show notification
215
+ showToast('سند با موفقیت به‌روزرسانی شد', 'success');
216
+ }
217
+
218
+ /**
219
+ * Handle document delete events
220
+ */
221
+ handleDocumentDelete(data) {
222
+ console.log('🗑️ Document deleted:', data);
223
+
224
+ // Remove from cache
225
+ this.cache.delete(`document_${data.documentId}`);
226
+
227
+ // Refresh document lists
228
+ this.refreshDocumentList();
229
+
230
+ // Update dashboard stats
231
+ this.updateDashboardStats();
232
+
233
+ // Show notification
234
+ showToast('سند با موفقیت حذف شد', 'info');
235
+ }
236
+
237
+ /**
238
+ * Handle scraping update events
239
+ */
240
+ handleScrapingUpdate(data) {
241
+ console.log('🕷️ Scraping update:', data);
242
+
243
+ // Update scraping dashboard if on that page
244
+ if (window.location.pathname.includes('scraping_dashboard.html')) {
245
+ this.refreshScrapingDashboard();
246
+ }
247
+
248
+ // Show notification
249
+ showToast(`وضعیت scraping: ${data.status}`, 'info');
250
+ }
251
+
252
+ /**
253
+ * Handle health update events
254
+ */
255
+ handleHealthUpdate(data) {
256
+ console.log('💓 Health update:', data);
257
+
258
+ // Update health indicators on all pages
259
+ this.updateHealthIndicators(data);
260
+ }
261
+
262
+ /**
263
+ * Refresh document list (if function exists)
264
+ */
265
+ refreshDocumentList() {
266
+ if (typeof loadDocuments === 'function') {
267
+ loadDocuments();
268
+ }
269
+
270
+ if (typeof refreshDocumentTable === 'function') {
271
+ refreshDocumentTable();
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Update dashboard statistics
277
+ */
278
+ async updateDashboardStats() {
279
+ try {
280
+ const summary = await this.apiClient.getDashboardSummary();
281
+ this.broadcast('dashboardStatsUpdated', summary);
282
+
283
+ // Update dashboard if on dashboard page
284
+ if (window.location.pathname.includes('improved_legal_dashboard.html')) {
285
+ if (typeof updateDashboardStats === 'function') {
286
+ updateDashboardStats(summary);
287
+ }
288
+ }
289
+ } catch (error) {
290
+ console.error('Failed to update dashboard stats:', error);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Refresh scraping dashboard
296
+ */
297
+ refreshScrapingDashboard() {
298
+ if (typeof loadScrapingData === 'function') {
299
+ loadScrapingData();
300
+ }
301
+
302
+ if (typeof updateScrapingStatus === 'function') {
303
+ updateScrapingStatus();
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Update health indicators
309
+ */
310
+ updateHealthIndicators(healthData) {
311
+ const healthElements = document.querySelectorAll('.health-indicator');
312
+
313
+ healthElements.forEach(element => {
314
+ const status = healthData.status || 'unknown';
315
+ element.className = `health-indicator ${status}`;
316
+ element.textContent = status === 'healthy' ? '🟢' : '🔴';
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Get cached data
322
+ */
323
+ getCachedData(key) {
324
+ return this.cache.get(key);
325
+ }
326
+
327
+ /**
328
+ * Set cached data
329
+ */
330
+ setCachedData(key, data) {
331
+ this.cache.set(key, data);
332
+ }
333
+
334
+ /**
335
+ * Clear cache
336
+ */
337
+ clearCache() {
338
+ this.cache.clear();
339
+ }
340
+
341
+ /**
342
+ * Get API client
343
+ */
344
+ getApiClient() {
345
+ return this.apiClient;
346
+ }
347
+
348
+ /**
349
+ * Force refresh all data
350
+ */
351
+ async forceRefresh() {
352
+ console.log('🔄 Force refreshing all data...');
353
+
354
+ try {
355
+ // Clear cache
356
+ this.clearCache();
357
+
358
+ // Refresh document list
359
+ this.refreshDocumentList();
360
+
361
+ // Update dashboard stats
362
+ await this.updateDashboardStats();
363
+
364
+ // Refresh scraping data
365
+ this.refreshScrapingDashboard();
366
+
367
+ this.broadcast('dataRefreshed', { timestamp: Date.now() });
368
+
369
+ showToast('داده‌ها با موفقیت به‌روزرسانی شدند', 'success');
370
+ } catch (error) {
371
+ console.error('Failed to force refresh:', error);
372
+ showToast('خطا در به‌روزرسانی داده‌ها', 'error');
373
+ }
374
+ }
375
+ }
376
+
377
+ // Global instance
378
+ const dashboardCore = new DashboardCore();
379
+
380
+ // Export for use in other modules
381
+ if (typeof module !== 'undefined' && module.exports) {
382
+ module.exports = DashboardCore;
383
+ }
384
+
385
+ // Make available globally
386
+ window.dashboardCore = dashboardCore;
387
+
388
+ console.log('📦 Dashboard Core module loaded');
app/frontend/js/document-crud.js ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Document CRUD Handler for Legal Dashboard
3
+ * Manages Create, Read, Update, Delete operations for documents
4
+ */
5
+
6
+ class DocumentCRUDHandler {
7
+ constructor() {
8
+ this.baseEndpoint = '/api/documents';
9
+ this.documents = [];
10
+ this.currentEditId = null;
11
+ this.searchQuery = '';
12
+ this.filters = {
13
+ status: 'all',
14
+ category: 'all',
15
+ dateFrom: '',
16
+ dateTo: ''
17
+ };
18
+
19
+ this.initializeEventListeners();
20
+ this.loadDocuments();
21
+ }
22
+
23
+ initializeEventListeners() {
24
+ // Create document button
25
+ const createBtn = document.getElementById('createDocumentBtn');
26
+ if (createBtn) {
27
+ createBtn.addEventListener('click', () => this.showCreateModal());
28
+ }
29
+
30
+ // Search input
31
+ const searchInput = document.getElementById('documentSearch');
32
+ if (searchInput) {
33
+ searchInput.addEventListener('input', (e) => {
34
+ this.searchQuery = e.target.value;
35
+ this.filterDocuments();
36
+ });
37
+ }
38
+
39
+ // Filter selects
40
+ const statusFilter = document.getElementById('statusFilter');
41
+ if (statusFilter) {
42
+ statusFilter.addEventListener('change', (e) => {
43
+ this.filters.status = e.target.value;
44
+ this.filterDocuments();
45
+ });
46
+ }
47
+
48
+ const categoryFilter = document.getElementById('categoryFilter');
49
+ if (categoryFilter) {
50
+ categoryFilter.addEventListener('change', (e) => {
51
+ this.filters.category = e.target.value;
52
+ this.filterDocuments();
53
+ });
54
+ }
55
+
56
+ // Date filters
57
+ const dateFromFilter = document.getElementById('dateFromFilter');
58
+ if (dateFromFilter) {
59
+ dateFromFilter.addEventListener('change', (e) => {
60
+ this.filters.dateFrom = e.target.value;
61
+ this.filterDocuments();
62
+ });
63
+ }
64
+
65
+ const dateToFilter = document.getElementById('dateToFilter');
66
+ if (dateToFilter) {
67
+ dateToFilter.addEventListener('change', (e) => {
68
+ this.filters.dateTo = e.target.value;
69
+ this.filterDocuments();
70
+ });
71
+ }
72
+ }
73
+
74
+ async loadDocuments() {
75
+ try {
76
+ const response = await fetchWithErrorHandling(this.baseEndpoint);
77
+ this.documents = response.documents || [];
78
+ this.renderDocuments();
79
+ } catch (error) {
80
+ console.error('Failed to load documents:', error);
81
+ this.showToast('خطا در بارگذاری اسناد', 'error');
82
+ }
83
+ }
84
+
85
+ async createDocument(documentData) {
86
+ try {
87
+ const response = await fetchWithErrorHandling(this.baseEndpoint, {
88
+ method: 'POST',
89
+ body: JSON.stringify(documentData)
90
+ });
91
+
92
+ this.showToast('سند با موفقیت ایجاد شد', 'success');
93
+ this.loadDocuments();
94
+ return response;
95
+ } catch (error) {
96
+ this.showToast(`خطا در ایجاد سند: ${error.message}`, 'error');
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ async updateDocument(id, documentData) {
102
+ try {
103
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/${id}`, {
104
+ method: 'PUT',
105
+ body: JSON.stringify(documentData)
106
+ });
107
+
108
+ this.showToast('سند با موفقیت به‌روزرسانی شد', 'success');
109
+ this.loadDocuments();
110
+ return response;
111
+ } catch (error) {
112
+ this.showToast(`خطا در به‌روزرسانی سند: ${error.message}`, 'error');
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ async deleteDocument(id) {
118
+ try {
119
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/${id}`, {
120
+ method: 'DELETE'
121
+ });
122
+
123
+ this.showToast('سند با موفقیت حذف شد', 'success');
124
+ this.loadDocuments();
125
+ return response;
126
+ } catch (error) {
127
+ this.showToast(`خطا در حذف سند: ${error.message}`, 'error');
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ async searchDocuments(query) {
133
+ try {
134
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/search?q=${encodeURIComponent(query)}`);
135
+ return response.results || [];
136
+ } catch (error) {
137
+ console.error('Search failed:', error);
138
+ return [];
139
+ }
140
+ }
141
+
142
+ renderDocuments() {
143
+ const container = document.getElementById('documentsList');
144
+ if (!container) return;
145
+
146
+ if (this.documents.length === 0) {
147
+ container.innerHTML = '<p class="no-documents">هیچ سندی یافت نشد</p>';
148
+ return;
149
+ }
150
+
151
+ const documentsHTML = this.documents.map(doc => this.renderDocumentItem(doc)).join('');
152
+ container.innerHTML = documentsHTML;
153
+ }
154
+
155
+ renderDocumentItem(doc) {
156
+ const statusClass = this.getStatusClass(doc.status);
157
+ const qualityColor = this.getQualityColor(doc.quality);
158
+
159
+ return `
160
+ <div class="document-item" data-id="${doc.id}">
161
+ <div class="document-header">
162
+ <h4 class="document-title" data-id="${doc.id}">${doc.title}</h4>
163
+ <div class="document-actions">
164
+ <button class="btn-edit" onclick="documentCRUDHandler.editDocument(${doc.id})">
165
+ <i class="fas fa-edit"></i>
166
+ </button>
167
+ <button class="btn-delete" onclick="documentCRUDHandler.confirmDelete(${doc.id})">
168
+ <i class="fas fa-trash"></i>
169
+ </button>
170
+ </div>
171
+ </div>
172
+ <div class="document-details">
173
+ <div class="document-info">
174
+ <span class="document-status ${statusClass}">${this.getStatusText(doc.status)}</span>
175
+ <span class="document-quality" style="color: ${qualityColor}">
176
+ کیفیت: ${(doc.quality || 0).toFixed(1)}%
177
+ </span>
178
+ <span class="document-date">${this.formatDate(doc.created_at)}</span>
179
+ </div>
180
+ <div class="document-content">
181
+ <p class="document-description">${doc.description || 'توضیحات موجود نیست'}</p>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ `;
186
+ }
187
+
188
+ showCreateModal() {
189
+ const modal = document.getElementById('createDocumentModal');
190
+ if (modal) {
191
+ modal.style.display = 'block';
192
+ this.resetCreateForm();
193
+ }
194
+ }
195
+
196
+ hideCreateModal() {
197
+ const modal = document.getElementById('createDocumentModal');
198
+ if (modal) {
199
+ modal.style.display = 'none';
200
+ }
201
+ }
202
+
203
+ resetCreateForm() {
204
+ const form = document.getElementById('createDocumentForm');
205
+ if (form) {
206
+ form.reset();
207
+ }
208
+ }
209
+
210
+ async handleCreateDocument(event) {
211
+ event.preventDefault();
212
+
213
+ const formData = new FormData(event.target);
214
+ const documentData = {
215
+ title: formData.get('title'),
216
+ description: formData.get('description'),
217
+ category: formData.get('category'),
218
+ status: 'pending'
219
+ };
220
+
221
+ try {
222
+ await this.createDocument(documentData);
223
+ this.hideCreateModal();
224
+ } catch (error) {
225
+ console.error('Create document failed:', error);
226
+ }
227
+ }
228
+
229
+ editDocument(id) {
230
+ const document = this.documents.find(doc => doc.id === id);
231
+ if (!document) return;
232
+
233
+ this.currentEditId = id;
234
+ this.showEditModal(document);
235
+ }
236
+
237
+ showEditModal(document) {
238
+ const modal = document.getElementById('editDocumentModal');
239
+ if (modal) {
240
+ // Populate form fields
241
+ const titleInput = modal.querySelector('#editTitle');
242
+ const descriptionInput = modal.querySelector('#editDescription');
243
+ const categoryInput = modal.querySelector('#editCategory');
244
+ const statusInput = modal.querySelector('#editStatus');
245
+
246
+ if (titleInput) titleInput.value = document.title;
247
+ if (descriptionInput) descriptionInput.value = document.description || '';
248
+ if (categoryInput) categoryInput.value = document.category || '';
249
+ if (statusInput) statusInput.value = document.status || 'pending';
250
+
251
+ modal.style.display = 'block';
252
+ }
253
+ }
254
+
255
+ hideEditModal() {
256
+ const modal = document.getElementById('editDocumentModal');
257
+ if (modal) {
258
+ modal.style.display = 'none';
259
+ this.currentEditId = null;
260
+ }
261
+ }
262
+
263
+ async handleEditDocument(event) {
264
+ event.preventDefault();
265
+
266
+ if (!this.currentEditId) return;
267
+
268
+ const formData = new FormData(event.target);
269
+ const documentData = {
270
+ title: formData.get('title'),
271
+ description: formData.get('description'),
272
+ category: formData.get('category'),
273
+ status: formData.get('status')
274
+ };
275
+
276
+ try {
277
+ await this.updateDocument(this.currentEditId, documentData);
278
+ this.hideEditModal();
279
+ } catch (error) {
280
+ console.error('Update document failed:', error);
281
+ }
282
+ }
283
+
284
+ confirmDelete(id) {
285
+ const document = this.documents.find(doc => doc.id === id);
286
+ if (!document) return;
287
+
288
+ const confirmed = confirm(`آیا از حذف سند "${document.title}" اطمینان دارید؟`);
289
+ if (confirmed) {
290
+ this.deleteDocument(id);
291
+ }
292
+ }
293
+
294
+ filterDocuments() {
295
+ let filtered = this.documents;
296
+
297
+ // Apply search filter
298
+ if (this.searchQuery) {
299
+ filtered = filtered.filter(doc =>
300
+ doc.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
301
+ (doc.description && doc.description.toLowerCase().includes(this.searchQuery.toLowerCase()))
302
+ );
303
+ }
304
+
305
+ // Apply status filter
306
+ if (this.filters.status !== 'all') {
307
+ filtered = filtered.filter(doc => doc.status === this.filters.status);
308
+ }
309
+
310
+ // Apply category filter
311
+ if (this.filters.category !== 'all') {
312
+ filtered = filtered.filter(doc => doc.category === this.filters.category);
313
+ }
314
+
315
+ // Apply date filters
316
+ if (this.filters.dateFrom) {
317
+ filtered = filtered.filter(doc =>
318
+ new Date(doc.created_at) >= new Date(this.filters.dateFrom)
319
+ );
320
+ }
321
+
322
+ if (this.filters.dateTo) {
323
+ filtered = filtered.filter(doc =>
324
+ new Date(doc.created_at) <= new Date(this.filters.dateTo)
325
+ );
326
+ }
327
+
328
+ this.renderFilteredDocuments(filtered);
329
+ }
330
+
331
+ renderFilteredDocuments(filtered) {
332
+ const container = document.getElementById('documentsList');
333
+ if (!container) return;
334
+
335
+ if (filtered.length === 0) {
336
+ container.innerHTML = '<p class="no-results">هیچ نتیجه‌ای یافت نشد</p>';
337
+ return;
338
+ }
339
+
340
+ const documentsHTML = filtered.map(doc => this.renderDocumentItem(doc)).join('');
341
+ container.innerHTML = documentsHTML;
342
+ }
343
+
344
+ getStatusClass(status) {
345
+ const statusMap = {
346
+ 'pending': 'status-pending',
347
+ 'processing': 'status-processing',
348
+ 'completed': 'status-completed',
349
+ 'error': 'status-error'
350
+ };
351
+ return statusMap[status] || 'status-unknown';
352
+ }
353
+
354
+ getStatusText(status) {
355
+ const statusMap = {
356
+ 'pending': 'در انتظار',
357
+ 'processing': 'در حال پردازش',
358
+ 'completed': 'تکمیل شده',
359
+ 'error': 'خطا'
360
+ };
361
+ return statusMap[status] || 'نامشخص';
362
+ }
363
+
364
+ getQualityColor(quality) {
365
+ if (quality >= 80) return '#28a745';
366
+ if (quality >= 60) return '#ffc107';
367
+ return '#dc3545';
368
+ }
369
+
370
+ formatDate(dateString) {
371
+ if (!dateString) return 'نامشخص';
372
+ const date = new Date(dateString);
373
+ return date.toLocaleDateString('fa-IR');
374
+ }
375
+
376
+ showToast(message, type = 'info') {
377
+ if (typeof showToast === 'function') {
378
+ showToast(message, type);
379
+ } else {
380
+ console.log(`${type.toUpperCase()}: ${message}`);
381
+ }
382
+ }
383
+ }
384
+
385
+ // Initialize document CRUD handler
386
+ const documentCRUDHandler = new DocumentCRUDHandler();
app/frontend/js/file-upload-handler.js ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * File Upload Handler for Legal Dashboard
3
+ * Manages document uploads, OCR processing, and real-time progress
4
+ */
5
+
6
+ class FileUploadHandler {
7
+ constructor() {
8
+ this.uploadEndpoint = '/api/ocr/upload';
9
+ this.processEndpoint = '/api/ocr/process';
10
+ this.maxFileSize = 10 * 1024 * 1024; // 10MB
11
+ this.allowedTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/tiff'];
12
+ this.currentUpload = null;
13
+ this.uploadQueue = [];
14
+
15
+ this.initializeEventListeners();
16
+ }
17
+
18
+ initializeEventListeners() {
19
+ // File input change
20
+ const fileInput = document.getElementById('documentUpload');
21
+ if (fileInput) {
22
+ fileInput.addEventListener('change', (e) => this.handleFileSelection(e));
23
+ }
24
+
25
+ // Upload button
26
+ const uploadBtn = document.getElementById('uploadButton');
27
+ if (uploadBtn) {
28
+ uploadBtn.addEventListener('click', () => this.startUpload());
29
+ }
30
+
31
+ // Drag and drop
32
+ const dropZone = document.getElementById('uploadDropZone');
33
+ if (dropZone) {
34
+ dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
35
+ dropZone.addEventListener('drop', (e) => this.handleDrop(e));
36
+ dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
37
+ }
38
+ }
39
+
40
+ handleFileSelection(event) {
41
+ const files = event.target.files;
42
+ if (files.length > 0) {
43
+ this.validateAndQueueFiles(files);
44
+ }
45
+ }
46
+
47
+ handleDragOver(event) {
48
+ event.preventDefault();
49
+ event.currentTarget.classList.add('drag-over');
50
+ }
51
+
52
+ handleDragLeave(event) {
53
+ event.preventDefault();
54
+ event.currentTarget.classList.remove('drag-over');
55
+ }
56
+
57
+ handleDrop(event) {
58
+ event.preventDefault();
59
+ event.currentTarget.classList.remove('drag-over');
60
+
61
+ const files = event.dataTransfer.files;
62
+ if (files.length > 0) {
63
+ this.validateAndQueueFiles(files);
64
+ }
65
+ }
66
+
67
+ validateAndQueueFiles(files) {
68
+ const validFiles = [];
69
+ const errors = [];
70
+
71
+ for (let file of files) {
72
+ // Check file size
73
+ if (file.size > this.maxFileSize) {
74
+ errors.push(`${file.name}: File too large (max 10MB)`);
75
+ continue;
76
+ }
77
+
78
+ // Check file type
79
+ if (!this.allowedTypes.includes(file.type)) {
80
+ errors.push(`${file.name}: Unsupported file type`);
81
+ continue;
82
+ }
83
+
84
+ validFiles.push(file);
85
+ }
86
+
87
+ // Show errors if any
88
+ if (errors.length > 0) {
89
+ this.showErrors(errors);
90
+ }
91
+
92
+ // Queue valid files
93
+ if (validFiles.length > 0) {
94
+ this.uploadQueue.push(...validFiles);
95
+ this.updateUploadQueue();
96
+ }
97
+ }
98
+
99
+ updateUploadQueue() {
100
+ const queueContainer = document.getElementById('uploadQueue');
101
+ if (!queueContainer) return;
102
+
103
+ if (this.uploadQueue.length === 0) {
104
+ queueContainer.innerHTML = '<p class="no-files">هیچ فایلی برای آپلود انتخاب نشده</p>';
105
+ return;
106
+ }
107
+
108
+ const queueHTML = this.uploadQueue.map((file, index) => `
109
+ <div class="queue-item" data-index="${index}">
110
+ <div class="file-info">
111
+ <span class="file-name">${file.name}</span>
112
+ <span class="file-size">${this.formatFileSize(file.size)}</span>
113
+ </div>
114
+ <div class="file-actions">
115
+ <button class="remove-file" onclick="fileUploadHandler.removeFromQueue(${index})">
116
+ <i class="fas fa-times"></i>
117
+ </button>
118
+ </div>
119
+ </div>
120
+ `).join('');
121
+
122
+ queueContainer.innerHTML = queueHTML;
123
+ }
124
+
125
+ removeFromQueue(index) {
126
+ this.uploadQueue.splice(index, 1);
127
+ this.updateUploadQueue();
128
+ }
129
+
130
+ async startUpload() {
131
+ if (this.uploadQueue.length === 0) {
132
+ this.showToast('لطفاً فایلی برای آپلود انتخاب کنید', 'warning');
133
+ return;
134
+ }
135
+
136
+ if (this.currentUpload) {
137
+ this.showToast('آپلود در حال انجام است، لطفاً صبر کنید', 'warning');
138
+ return;
139
+ }
140
+
141
+ this.currentUpload = true;
142
+ this.showUploadProgress();
143
+
144
+ try {
145
+ for (let i = 0; i < this.uploadQueue.length; i++) {
146
+ const file = this.uploadQueue[i];
147
+ await this.uploadFile(file, i + 1, this.uploadQueue.length);
148
+ }
149
+
150
+ this.showToast('تمام فایل‌ها با موفقیت آپلود شدند', 'success');
151
+ this.refreshDocumentsList();
152
+ } catch (error) {
153
+ this.showToast(`خطا در آپلود: ${error.message}`, 'error');
154
+ } finally {
155
+ this.currentUpload = false;
156
+ this.hideUploadProgress();
157
+ this.uploadQueue = [];
158
+ this.updateUploadQueue();
159
+ }
160
+ }
161
+
162
+ async uploadFile(file, current, total) {
163
+ return new Promise((resolve, reject) => {
164
+ const formData = new FormData();
165
+ formData.append('file', file);
166
+ formData.append('filename', file.name);
167
+
168
+ const xhr = new XMLHttpRequest();
169
+
170
+ // Progress tracking
171
+ xhr.upload.addEventListener('progress', (e) => {
172
+ if (e.lengthComputable) {
173
+ const percentComplete = (e.loaded / e.total) * 100;
174
+ this.updateProgress(percentComplete, current, total);
175
+ }
176
+ });
177
+
178
+ // Response handling
179
+ xhr.addEventListener('load', () => {
180
+ if (xhr.status === 200) {
181
+ try {
182
+ const response = JSON.parse(xhr.responseText);
183
+ this.handleUploadSuccess(response, file);
184
+ resolve(response);
185
+ } catch (error) {
186
+ reject(new Error('Invalid response format'));
187
+ }
188
+ } else {
189
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
190
+ }
191
+ });
192
+
193
+ xhr.addEventListener('error', () => {
194
+ reject(new Error('Network error during upload'));
195
+ });
196
+
197
+ xhr.addEventListener('abort', () => {
198
+ reject(new Error('Upload cancelled'));
199
+ });
200
+
201
+ // Start upload
202
+ xhr.open('POST', this.uploadEndpoint);
203
+ xhr.send(formData);
204
+ });
205
+ }
206
+
207
+ handleUploadSuccess(response, file) {
208
+ // Update dashboard stats
209
+ this.updateDashboardStats();
210
+
211
+ // Show success message
212
+ this.showToast(`${file.name} با موفقیت آپلود شد`, 'success');
213
+
214
+ // Process OCR if needed
215
+ if (response.document_id) {
216
+ this.processOCR(response.document_id, file.name);
217
+ }
218
+ }
219
+
220
+ async processOCR(documentId, fileName) {
221
+ try {
222
+ const response = await fetch(this.processEndpoint, {
223
+ method: 'POST',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ },
227
+ body: JSON.stringify({
228
+ document_id: documentId,
229
+ filename: fileName
230
+ })
231
+ });
232
+
233
+ if (!response.ok) {
234
+ throw new Error(`OCR processing failed: ${response.status}`);
235
+ }
236
+
237
+ const result = await response.json();
238
+ this.showOCRResult(result, fileName);
239
+ } catch (error) {
240
+ this.showToast(`خطا در پردازش OCR: ${error.message}`, 'error');
241
+ }
242
+ }
243
+
244
+ showOCRResult(result, fileName) {
245
+ const ocrResultsContainer = document.getElementById('ocrResults');
246
+ if (!ocrResultsContainer) return;
247
+
248
+ const resultHTML = `
249
+ <div class="ocr-result">
250
+ <h4>نتایج OCR - ${fileName}</h4>
251
+ <div class="ocr-content">
252
+ <p><strong>کیفیت:</strong> ${(result.quality || 0).toFixed(2)}%</p>
253
+ <p><strong>متن استخراج شده:</strong></p>
254
+ <div class="extracted-text">
255
+ ${result.text || 'متنی استخراج نشد'}
256
+ </div>
257
+ </div>
258
+ </div>
259
+ `;
260
+
261
+ ocrResultsContainer.insertAdjacentHTML('afterbegin', resultHTML);
262
+ }
263
+
264
+ updateProgress(percent, current, total) {
265
+ const progressBar = document.getElementById('uploadProgressBar');
266
+ const progressText = document.getElementById('uploadProgressText');
267
+
268
+ if (progressBar) {
269
+ progressBar.style.width = `${percent}%`;
270
+ }
271
+
272
+ if (progressText) {
273
+ progressText.textContent = `آپلود فایل ${current} از ${total} (${Math.round(percent)}%)`;
274
+ }
275
+ }
276
+
277
+ showUploadProgress() {
278
+ const progressContainer = document.getElementById('uploadProgress');
279
+ if (progressContainer) {
280
+ progressContainer.style.display = 'block';
281
+ }
282
+ }
283
+
284
+ hideUploadProgress() {
285
+ const progressContainer = document.getElementById('uploadProgress');
286
+ if (progressContainer) {
287
+ progressContainer.style.display = 'none';
288
+ }
289
+ }
290
+
291
+ updateDashboardStats() {
292
+ // Trigger dashboard stats refresh
293
+ if (typeof loadDashboardStats === 'function') {
294
+ loadDashboardStats();
295
+ }
296
+ }
297
+
298
+ refreshDocumentsList() {
299
+ // Trigger documents list refresh
300
+ if (typeof loadDocumentsList === 'function') {
301
+ loadDocumentsList();
302
+ }
303
+ }
304
+
305
+ showErrors(errors) {
306
+ const errorMessage = errors.join('\n');
307
+ this.showToast(errorMessage, 'error');
308
+ }
309
+
310
+ showToast(message, type = 'info') {
311
+ if (typeof showToast === 'function') {
312
+ showToast(message, type);
313
+ } else {
314
+ console.log(`${type.toUpperCase()}: ${message}`);
315
+ }
316
+ }
317
+
318
+ formatFileSize(bytes) {
319
+ if (bytes === 0) return '0 Bytes';
320
+ const k = 1024;
321
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
322
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
323
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
324
+ }
325
+ }
326
+
327
+ // Initialize file upload handler
328
+ const fileUploadHandler = new FileUploadHandler();
app/frontend/js/notifications.js ADDED
@@ -0,0 +1,617 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Notification System for Legal Dashboard
3
+ * ======================================
4
+ *
5
+ * Handles real-time notifications, WebSocket connections, and notification management.
6
+ */
7
+
8
+ class NotificationManager {
9
+ constructor() {
10
+ this.websocket = null;
11
+ this.reconnectAttempts = 0;
12
+ this.maxReconnectAttempts = 5;
13
+ this.reconnectDelay = 1000;
14
+ this.notifications = [];
15
+ this.unreadCount = 0;
16
+ this.isConnected = false;
17
+ this.userId = null;
18
+
19
+ // Initialize notification elements
20
+ this.initElements();
21
+
22
+ // Start WebSocket connection
23
+ this.connectWebSocket();
24
+
25
+ // Set up periodic cleanup
26
+ setInterval(() => this.cleanupExpiredNotifications(), 60000); // Every minute
27
+ }
28
+
29
+ initElements() {
30
+ // Create notification container if it doesn't exist
31
+ if (!document.getElementById('notification-container')) {
32
+ const container = document.createElement('div');
33
+ container.id = 'notification-container';
34
+ container.className = 'notification-container';
35
+ document.body.appendChild(container);
36
+ }
37
+
38
+ // Create notification bell if it doesn't exist
39
+ if (!document.getElementById('notification-bell')) {
40
+ const bell = document.createElement('div');
41
+ bell.id = 'notification-bell';
42
+ bell.className = 'notification-bell';
43
+ bell.innerHTML = `
44
+ <i class="fas fa-bell"></i>
45
+ <span class="notification-badge" id="notification-badge">0</span>
46
+ `;
47
+ bell.addEventListener('click', () => this.toggleNotificationPanel());
48
+ document.body.appendChild(bell);
49
+ }
50
+
51
+ // Create notification panel if it doesn't exist
52
+ if (!document.getElementById('notification-panel')) {
53
+ const panel = document.createElement('div');
54
+ panel.id = 'notification-panel';
55
+ panel.className = 'notification-panel hidden';
56
+ panel.innerHTML = `
57
+ <div class="notification-header">
58
+ <h3>Notifications</h3>
59
+ <button class="close-btn" onclick="notificationManager.closeNotificationPanel()">×</button>
60
+ </div>
61
+ <div class="notification-list" id="notification-list"></div>
62
+ <div class="notification-footer">
63
+ <button onclick="notificationManager.markAllAsRead()">Mark All as Read</button>
64
+ <button onclick="notificationManager.clearAll()">Clear All</button>
65
+ </div>
66
+ `;
67
+ document.body.appendChild(panel);
68
+ }
69
+
70
+ // Add CSS styles
71
+ this.addStyles();
72
+ }
73
+
74
+ addStyles() {
75
+ if (!document.getElementById('notification-styles')) {
76
+ const styles = document.createElement('style');
77
+ styles.id = 'notification-styles';
78
+ styles.textContent = `
79
+ .notification-container {
80
+ position: fixed;
81
+ top: 20px;
82
+ right: 20px;
83
+ z-index: 10000;
84
+ max-width: 400px;
85
+ }
86
+
87
+ .notification-bell {
88
+ position: fixed;
89
+ top: 20px;
90
+ right: 20px;
91
+ background: #007bff;
92
+ color: white;
93
+ padding: 10px;
94
+ border-radius: 50%;
95
+ cursor: pointer;
96
+ z-index: 10001;
97
+ box-shadow: 0 2px 10px rgba(0,0,0,0.2);
98
+ transition: all 0.3s ease;
99
+ }
100
+
101
+ .notification-bell:hover {
102
+ transform: scale(1.1);
103
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
104
+ }
105
+
106
+ .notification-badge {
107
+ position: absolute;
108
+ top: -5px;
109
+ right: -5px;
110
+ background: #dc3545;
111
+ color: white;
112
+ border-radius: 50%;
113
+ padding: 2px 6px;
114
+ font-size: 12px;
115
+ min-width: 18px;
116
+ text-align: center;
117
+ }
118
+
119
+ .notification-panel {
120
+ position: fixed;
121
+ top: 70px;
122
+ right: 20px;
123
+ width: 350px;
124
+ max-height: 500px;
125
+ background: white;
126
+ border-radius: 8px;
127
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
128
+ z-index: 10002;
129
+ overflow: hidden;
130
+ transition: all 0.3s ease;
131
+ }
132
+
133
+ .notification-panel.hidden {
134
+ transform: translateX(100%);
135
+ opacity: 0;
136
+ }
137
+
138
+ .notification-header {
139
+ padding: 15px;
140
+ border-bottom: 1px solid #eee;
141
+ display: flex;
142
+ justify-content: space-between;
143
+ align-items: center;
144
+ }
145
+
146
+ .notification-header h3 {
147
+ margin: 0;
148
+ color: #333;
149
+ }
150
+
151
+ .close-btn {
152
+ background: none;
153
+ border: none;
154
+ font-size: 20px;
155
+ cursor: pointer;
156
+ color: #666;
157
+ }
158
+
159
+ .notification-list {
160
+ max-height: 350px;
161
+ overflow-y: auto;
162
+ }
163
+
164
+ .notification-item {
165
+ padding: 15px;
166
+ border-bottom: 1px solid #f0f0f0;
167
+ cursor: pointer;
168
+ transition: background-color 0.2s ease;
169
+ }
170
+
171
+ .notification-item:hover {
172
+ background-color: #f8f9fa;
173
+ }
174
+
175
+ .notification-item.unread {
176
+ background-color: #e3f2fd;
177
+ border-left: 4px solid #2196f3;
178
+ }
179
+
180
+ .notification-title {
181
+ font-weight: bold;
182
+ margin-bottom: 5px;
183
+ color: #333;
184
+ }
185
+
186
+ .notification-message {
187
+ color: #666;
188
+ font-size: 14px;
189
+ margin-bottom: 5px;
190
+ }
191
+
192
+ .notification-meta {
193
+ font-size: 12px;
194
+ color: #999;
195
+ display: flex;
196
+ justify-content: space-between;
197
+ }
198
+
199
+ .notification-footer {
200
+ padding: 15px;
201
+ border-top: 1px solid #eee;
202
+ display: flex;
203
+ gap: 10px;
204
+ }
205
+
206
+ .notification-footer button {
207
+ flex: 1;
208
+ padding: 8px;
209
+ border: 1px solid #ddd;
210
+ background: white;
211
+ border-radius: 4px;
212
+ cursor: pointer;
213
+ font-size: 12px;
214
+ }
215
+
216
+ .notification-footer button:hover {
217
+ background: #f8f9fa;
218
+ }
219
+
220
+ .notification-toast {
221
+ background: white;
222
+ border-radius: 8px;
223
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
224
+ margin-bottom: 10px;
225
+ padding: 15px;
226
+ border-left: 4px solid #007bff;
227
+ animation: slideIn 0.3s ease;
228
+ }
229
+
230
+ .notification-toast.success {
231
+ border-left-color: #28a745;
232
+ }
233
+
234
+ .notification-toast.warning {
235
+ border-left-color: #ffc107;
236
+ }
237
+
238
+ .notification-toast.error {
239
+ border-left-color: #dc3545;
240
+ }
241
+
242
+ @keyframes slideIn {
243
+ from {
244
+ transform: translateX(100%);
245
+ opacity: 0;
246
+ }
247
+ to {
248
+ transform: translateX(0);
249
+ opacity: 1;
250
+ }
251
+ }
252
+
253
+ .notification-type-icon {
254
+ margin-right: 8px;
255
+ }
256
+
257
+ .notification-type-icon.info { color: #007bff; }
258
+ .notification-type-icon.success { color: #28a745; }
259
+ .notification-type-icon.warning { color: #ffc107; }
260
+ .notification-type-icon.error { color: #dc3545; }
261
+ `;
262
+ document.head.appendChild(styles);
263
+ }
264
+ }
265
+
266
+ connectWebSocket() {
267
+ try {
268
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
269
+ const wsUrl = `${protocol}//${window.location.host}/api/notifications/ws`;
270
+
271
+ this.websocket = new WebSocket(wsUrl);
272
+
273
+ this.websocket.onopen = () => {
274
+ console.log('WebSocket connected');
275
+ this.isConnected = true;
276
+ this.reconnectAttempts = 0;
277
+
278
+ // Send user authentication if available
279
+ const token = localStorage.getItem('auth_token');
280
+ if (token) {
281
+ this.websocket.send(JSON.stringify({
282
+ type: 'auth',
283
+ token: token
284
+ }));
285
+ }
286
+ };
287
+
288
+ this.websocket.onmessage = (event) => {
289
+ try {
290
+ const data = JSON.parse(event.data);
291
+ this.handleNotification(data);
292
+ } catch (error) {
293
+ console.error('Error parsing WebSocket message:', error);
294
+ }
295
+ };
296
+
297
+ this.websocket.onclose = () => {
298
+ console.log('WebSocket disconnected');
299
+ this.isConnected = false;
300
+ this.handleReconnect();
301
+ };
302
+
303
+ this.websocket.onerror = (error) => {
304
+ console.error('WebSocket error:', error);
305
+ this.isConnected = false;
306
+ };
307
+
308
+ } catch (error) {
309
+ console.error('Error connecting to WebSocket:', error);
310
+ this.handleReconnect();
311
+ }
312
+ }
313
+
314
+ handleReconnect() {
315
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
316
+ this.reconnectAttempts++;
317
+ console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
318
+
319
+ setTimeout(() => {
320
+ this.connectWebSocket();
321
+ }, this.reconnectDelay * this.reconnectAttempts);
322
+ } else {
323
+ console.error('Max reconnection attempts reached');
324
+ this.showToast('Connection lost. Please refresh the page.', 'error');
325
+ }
326
+ }
327
+
328
+ handleNotification(data) {
329
+ if (data.type === 'connection_established') {
330
+ console.log('Notification service connected');
331
+ this.userId = data.user_id;
332
+ return;
333
+ }
334
+
335
+ // Add notification to list
336
+ const notification = {
337
+ id: data.id || Date.now(),
338
+ type: data.type || 'info',
339
+ title: data.title || 'Notification',
340
+ message: data.message || '',
341
+ priority: data.priority || 'medium',
342
+ created_at: data.created_at || new Date().toISOString(),
343
+ metadata: data.metadata || {},
344
+ read: false
345
+ };
346
+
347
+ this.notifications.unshift(notification);
348
+ this.unreadCount++;
349
+ this.updateBadge();
350
+ this.showToast(notification);
351
+ this.updateNotificationPanel();
352
+
353
+ // Play notification sound if enabled
354
+ this.playNotificationSound();
355
+ }
356
+
357
+ showToast(notification) {
358
+ const container = document.getElementById('notification-container');
359
+ const toast = document.createElement('div');
360
+ toast.className = `notification-toast ${notification.type}`;
361
+
362
+ const icon = this.getNotificationIcon(notification.type);
363
+
364
+ toast.innerHTML = `
365
+ <div class="notification-title">
366
+ <i class="fas ${icon} notification-type-icon ${notification.type}"></i>
367
+ ${notification.title}
368
+ </div>
369
+ <div class="notification-message">${notification.message}</div>
370
+ <div class="notification-meta">
371
+ <span>${this.formatTime(notification.created_at)}</span>
372
+ <span>${notification.priority}</span>
373
+ </div>
374
+ `;
375
+
376
+ container.appendChild(toast);
377
+
378
+ // Auto-remove after 5 seconds
379
+ setTimeout(() => {
380
+ if (toast.parentNode) {
381
+ toast.parentNode.removeChild(toast);
382
+ }
383
+ }, 5000);
384
+ }
385
+
386
+ getNotificationIcon(type) {
387
+ const icons = {
388
+ 'info': 'fa-info-circle',
389
+ 'success': 'fa-check-circle',
390
+ 'warning': 'fa-exclamation-triangle',
391
+ 'error': 'fa-times-circle',
392
+ 'upload_complete': 'fa-upload',
393
+ 'ocr_complete': 'fa-file-text',
394
+ 'scraping_complete': 'fa-spider',
395
+ 'system_error': 'fa-exclamation-circle',
396
+ 'user_activity': 'fa-user'
397
+ };
398
+ return icons[type] || 'fa-bell';
399
+ }
400
+
401
+ formatTime(timestamp) {
402
+ const date = new Date(timestamp);
403
+ const now = new Date();
404
+ const diff = now - date;
405
+
406
+ if (diff < 60000) { // Less than 1 minute
407
+ return 'Just now';
408
+ } else if (diff < 3600000) { // Less than 1 hour
409
+ const minutes = Math.floor(diff / 60000);
410
+ return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
411
+ } else if (diff < 86400000) { // Less than 1 day
412
+ const hours = Math.floor(diff / 3600000);
413
+ return `${hours} hour${hours > 1 ? 's' : ''} ago`;
414
+ } else {
415
+ return date.toLocaleDateString();
416
+ }
417
+ }
418
+
419
+ updateBadge() {
420
+ const badge = document.getElementById('notification-badge');
421
+ if (badge) {
422
+ badge.textContent = this.unreadCount;
423
+ badge.style.display = this.unreadCount > 0 ? 'block' : 'none';
424
+ }
425
+ }
426
+
427
+ toggleNotificationPanel() {
428
+ const panel = document.getElementById('notification-panel');
429
+ if (panel) {
430
+ panel.classList.toggle('hidden');
431
+ if (!panel.classList.contains('hidden')) {
432
+ this.updateNotificationPanel();
433
+ }
434
+ }
435
+ }
436
+
437
+ closeNotificationPanel() {
438
+ const panel = document.getElementById('notification-panel');
439
+ if (panel) {
440
+ panel.classList.add('hidden');
441
+ }
442
+ }
443
+
444
+ updateNotificationPanel() {
445
+ const list = document.getElementById('notification-list');
446
+ if (!list) return;
447
+
448
+ list.innerHTML = '';
449
+
450
+ this.notifications.slice(0, 20).forEach(notification => {
451
+ const item = document.createElement('div');
452
+ item.className = `notification-item ${notification.read ? '' : 'unread'}`;
453
+ item.onclick = () => this.markAsRead(notification.id);
454
+
455
+ const icon = this.getNotificationIcon(notification.type);
456
+
457
+ item.innerHTML = `
458
+ <div class="notification-title">
459
+ <i class="fas ${icon} notification-type-icon ${notification.type}"></i>
460
+ ${notification.title}
461
+ </div>
462
+ <div class="notification-message">${notification.message}</div>
463
+ <div class="notification-meta">
464
+ <span>${this.formatTime(notification.created_at)}</span>
465
+ <span>${notification.priority}</span>
466
+ </div>
467
+ `;
468
+
469
+ list.appendChild(item);
470
+ });
471
+ }
472
+
473
+ markAsRead(notificationId) {
474
+ const notification = this.notifications.find(n => n.id === notificationId);
475
+ if (notification && !notification.read) {
476
+ notification.read = true;
477
+ this.unreadCount = Math.max(0, this.unreadCount - 1);
478
+ this.updateBadge();
479
+ this.updateNotificationPanel();
480
+
481
+ // Send to server
482
+ this.sendToServer('mark_read', { notification_id: notificationId });
483
+ }
484
+ }
485
+
486
+ markAllAsRead() {
487
+ this.notifications.forEach(notification => {
488
+ notification.read = true;
489
+ });
490
+ this.unreadCount = 0;
491
+ this.updateBadge();
492
+ this.updateNotificationPanel();
493
+
494
+ // Send to server
495
+ this.sendToServer('mark_all_read', {});
496
+ }
497
+
498
+ clearAll() {
499
+ this.notifications = [];
500
+ this.unreadCount = 0;
501
+ this.updateBadge();
502
+ this.updateNotificationPanel();
503
+
504
+ // Send to server
505
+ this.sendToServer('clear_all', {});
506
+ }
507
+
508
+ sendToServer(action, data) {
509
+ if (this.websocket && this.isConnected) {
510
+ this.websocket.send(JSON.stringify({
511
+ action: action,
512
+ ...data
513
+ }));
514
+ }
515
+ }
516
+
517
+ playNotificationSound() {
518
+ // Create audio context for notification sound
519
+ try {
520
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
521
+ const oscillator = audioContext.createOscillator();
522
+ const gainNode = audioContext.createGain();
523
+
524
+ oscillator.connect(gainNode);
525
+ gainNode.connect(audioContext.destination);
526
+
527
+ oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
528
+ oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1);
529
+
530
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
531
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
532
+
533
+ oscillator.start(audioContext.currentTime);
534
+ oscillator.stop(audioContext.currentTime + 0.2);
535
+ } catch (error) {
536
+ console.log('Could not play notification sound:', error);
537
+ }
538
+ }
539
+
540
+ cleanupExpiredNotifications() {
541
+ const now = new Date();
542
+ const expired = this.notifications.filter(notification => {
543
+ const created = new Date(notification.created_at);
544
+ const diff = now - created;
545
+ return diff > 24 * 60 * 60 * 1000; // 24 hours
546
+ });
547
+
548
+ expired.forEach(notification => {
549
+ const index = this.notifications.indexOf(notification);
550
+ if (index > -1) {
551
+ this.notifications.splice(index, 1);
552
+ }
553
+ });
554
+
555
+ this.updateNotificationPanel();
556
+ }
557
+
558
+ // Public methods for external use
559
+ showInfo(title, message, duration = 5000) {
560
+ this.showCustomNotification('info', title, message, duration);
561
+ }
562
+
563
+ showSuccess(title, message, duration = 5000) {
564
+ this.showCustomNotification('success', title, message, duration);
565
+ }
566
+
567
+ showWarning(title, message, duration = 5000) {
568
+ this.showCustomNotification('warning', title, message, duration);
569
+ }
570
+
571
+ showError(title, message, duration = 5000) {
572
+ this.showCustomNotification('error', title, message, duration);
573
+ }
574
+
575
+ showCustomNotification(type, title, message, duration) {
576
+ const notification = {
577
+ id: Date.now(),
578
+ type: type,
579
+ title: title,
580
+ message: message,
581
+ priority: 'medium',
582
+ created_at: new Date().toISOString(),
583
+ metadata: {},
584
+ read: false
585
+ };
586
+
587
+ this.notifications.unshift(notification);
588
+ this.unreadCount++;
589
+ this.updateBadge();
590
+ this.showToast(notification);
591
+ this.updateNotificationPanel();
592
+
593
+ if (duration > 0) {
594
+ setTimeout(() => {
595
+ const index = this.notifications.indexOf(notification);
596
+ if (index > -1) {
597
+ this.notifications.splice(index, 1);
598
+ this.updateNotificationPanel();
599
+ }
600
+ }, duration);
601
+ }
602
+ }
603
+ }
604
+
605
+ // Initialize notification manager when DOM is loaded
606
+ let notificationManager;
607
+
608
+ document.addEventListener('DOMContentLoaded', () => {
609
+ notificationManager = new NotificationManager();
610
+ });
611
+
612
+ // Global function for external use
613
+ window.showNotification = (type, title, message) => {
614
+ if (notificationManager) {
615
+ notificationManager.showCustomNotification(type, title, message);
616
+ }
617
+ };
app/frontend/js/scraping-control.js ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Scraping Control Panel for Legal Dashboard
3
+ * Manages web scraping operations, real-time monitoring, and results display
4
+ */
5
+
6
+ class ScrapingControlPanel {
7
+ constructor() {
8
+ this.baseEndpoint = '/api/scraping';
9
+ this.currentJob = null;
10
+ this.isRunning = false;
11
+ this.statusInterval = null;
12
+ this.logs = [];
13
+
14
+ this.initializeEventListeners();
15
+ this.loadScrapingStatus();
16
+ }
17
+
18
+ initializeEventListeners() {
19
+ // Start scraping button
20
+ const startBtn = document.getElementById('startScrapingBtn');
21
+ if (startBtn) {
22
+ startBtn.addEventListener('click', () => this.startScraping());
23
+ }
24
+
25
+ // Stop scraping button
26
+ const stopBtn = document.getElementById('stopScrapingBtn');
27
+ if (stopBtn) {
28
+ stopBtn.addEventListener('click', () => this.stopScraping());
29
+ }
30
+
31
+ // Refresh results button
32
+ const refreshBtn = document.getElementById('refreshResultsBtn');
33
+ if (refreshBtn) {
34
+ refreshBtn.addEventListener('click', () => this.loadScrapingResults());
35
+ }
36
+
37
+ // Clear logs button
38
+ const clearLogsBtn = document.getElementById('clearLogsBtn');
39
+ if (clearLogsBtn) {
40
+ clearLogsBtn.addEventListener('click', () => this.clearLogs());
41
+ }
42
+ }
43
+
44
+ async startScraping() {
45
+ if (this.isRunning) {
46
+ this.showToast('اسکرپینگ در حال انجام است', 'warning');
47
+ return;
48
+ }
49
+
50
+ const scrapingConfig = this.getScrapingConfig();
51
+ if (!scrapingConfig.url) {
52
+ this.showToast('لطفاً URL را وارد کنید', 'error');
53
+ return;
54
+ }
55
+
56
+ try {
57
+ this.isRunning = true;
58
+ this.updateStartButton(true);
59
+ this.addLog('شروع اسکرپینگ...', 'info');
60
+
61
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/start`, {
62
+ method: 'POST',
63
+ body: JSON.stringify(scrapingConfig)
64
+ });
65
+
66
+ this.currentJob = response.job_id;
67
+ this.showToast('اسکرپینگ شروع شد', 'success');
68
+ this.addLog(`شغل اسکرپینگ ایجاد شد: ${this.currentJob}`, 'success');
69
+
70
+ // Start monitoring
71
+ this.startStatusMonitoring();
72
+ } catch (error) {
73
+ this.showToast(`خطا در شروع اسکرپینگ: ${error.message}`, 'error');
74
+ this.addLog(`خطا در شروع اسکرپینگ: ${error.message}`, 'error');
75
+ this.isRunning = false;
76
+ this.updateStartButton(false);
77
+ }
78
+ }
79
+
80
+ async stopScraping() {
81
+ if (!this.isRunning) {
82
+ this.showToast('هیچ اسکرپینگی در حال انجام نیست', 'warning');
83
+ return;
84
+ }
85
+
86
+ try {
87
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/stop`, {
88
+ method: 'POST',
89
+ body: JSON.stringify({ job_id: this.currentJob })
90
+ });
91
+
92
+ this.showToast('اسکرپینگ متوقف شد', 'success');
93
+ this.addLog('اسکرپینگ متوقف شد', 'info');
94
+
95
+ this.isRunning = false;
96
+ this.currentJob = null;
97
+ this.updateStartButton(false);
98
+ this.stopStatusMonitoring();
99
+ } catch (error) {
100
+ this.showToast(`خطا در توقف اسکرپینگ: ${error.message}`, 'error');
101
+ this.addLog(`خطا در توقف اسکرپینگ: ${error.message}`, 'error');
102
+ }
103
+ }
104
+
105
+ async loadScrapingStatus() {
106
+ try {
107
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/status`);
108
+ this.updateStatusDisplay(response);
109
+ } catch (error) {
110
+ console.error('Failed to load scraping status:', error);
111
+ }
112
+ }
113
+
114
+ async loadScrapingResults() {
115
+ try {
116
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/results`);
117
+ this.renderResults(response);
118
+ } catch (error) {
119
+ console.error('Failed to load scraping results:', error);
120
+ this.showToast('خطا در بارگذاری نتایج', 'error');
121
+ }
122
+ }
123
+
124
+ async loadScrapingStatistics() {
125
+ try {
126
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/statistics`);
127
+ this.renderStatistics(response);
128
+ } catch (error) {
129
+ console.error('Failed to load scraping statistics:', error);
130
+ }
131
+ }
132
+
133
+ startStatusMonitoring() {
134
+ this.statusInterval = setInterval(async () => {
135
+ if (this.isRunning) {
136
+ await this.updateScrapingStatus();
137
+ }
138
+ }, 5000); // Update every 5 seconds
139
+ }
140
+
141
+ stopStatusMonitoring() {
142
+ if (this.statusInterval) {
143
+ clearInterval(this.statusInterval);
144
+ this.statusInterval = null;
145
+ }
146
+ }
147
+
148
+ async updateScrapingStatus() {
149
+ try {
150
+ const response = await fetchWithErrorHandling(`${this.baseEndpoint}/status`);
151
+ this.updateStatusDisplay(response);
152
+
153
+ // Check if scraping is complete
154
+ if (response.status === 'completed' || response.status === 'failed') {
155
+ this.isRunning = false;
156
+ this.currentJob = null;
157
+ this.updateStartButton(false);
158
+ this.stopStatusMonitoring();
159
+
160
+ if (response.status === 'completed') {
161
+ this.addLog('اسکرپینگ تکمیل شد', 'success');
162
+ this.loadScrapingResults();
163
+ } else {
164
+ this.addLog('اسکرپینگ با خطا مواجه شد', 'error');
165
+ }
166
+ }
167
+ } catch (error) {
168
+ console.error('Failed to update scraping status:', error);
169
+ }
170
+ }
171
+
172
+ updateStatusDisplay(status) {
173
+ const statusElement = document.getElementById('scrapingStatus');
174
+ const progressElement = document.getElementById('scrapingProgress');
175
+ const statsElement = document.getElementById('scrapingStats');
176
+
177
+ if (statusElement) {
178
+ statusElement.textContent = this.getStatusText(status.status);
179
+ statusElement.className = `status-indicator ${this.getStatusClass(status.status)}`;
180
+ }
181
+
182
+ if (progressElement && status.progress) {
183
+ progressElement.style.width = `${status.progress}%`;
184
+ progressElement.textContent = `${status.progress}%`;
185
+ }
186
+
187
+ if (statsElement) {
188
+ statsElement.innerHTML = `
189
+ <div class="stat-item">
190
+ <span class="stat-label">صفحات پردازش شده:</span>
191
+ <span class="stat-value">${status.pages_processed || 0}</span>
192
+ </div>
193
+ <div class="stat-item">
194
+ <span class="stat-label">نتایج یافت شده:</span>
195
+ <span class="stat-value">${status.items_found || 0}</span>
196
+ </div>
197
+ <div class="stat-item">
198
+ <span class="stat-label">زمان سپری شده:</span>
199
+ <span class="stat-value">${status.elapsed_time || '0s'}</span>
200
+ </div>
201
+ `;
202
+ }
203
+ }
204
+
205
+ renderResults(results) {
206
+ const container = document.getElementById('scrapingResults');
207
+ if (!container) return;
208
+
209
+ if (!results.items || results.items.length === 0) {
210
+ container.innerHTML = '<p class="no-results">هیچ نتیجه‌ای یافت نشد</p>';
211
+ return;
212
+ }
213
+
214
+ const resultsHTML = results.items.map(item => `
215
+ <div class="scraping-item">
216
+ <div class="item-header">
217
+ <h4 class="item-title">${item.title || 'بدون عنوان'}</h4>
218
+ <span class="item-rating">امتیاز: ${item.rating || 0}</span>
219
+ </div>
220
+ <div class="item-content">
221
+ <p class="item-url">
222
+ <a href="${item.url}" target="_blank">${item.url}</a>
223
+ </p>
224
+ <p class="item-description">${item.description || 'توضیحات موجود نیست'}</p>
225
+ <div class="item-meta">
226
+ <span class="item-date">${this.formatDate(item.date)}</span>
227
+ <span class="item-category">${item.category || 'نامشخص'}</span>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ `).join('');
232
+
233
+ container.innerHTML = resultsHTML;
234
+ }
235
+
236
+ renderStatistics(stats) {
237
+ const container = document.getElementById('scrapingStatistics');
238
+ if (!container) return;
239
+
240
+ const statsHTML = `
241
+ <div class="stats-grid">
242
+ <div class="stat-card">
243
+ <div class="stat-number">${stats.total_scraped || 0}</div>
244
+ <div class="stat-label">کل نتایج</div>
245
+ </div>
246
+ <div class="stat-card">
247
+ <div class="stat-number">${stats.success_rate || 0}%</div>
248
+ <div class="stat-label">نرخ موفقیت</div>
249
+ </div>
250
+ <div class="stat-card">
251
+ <div class="stat-number">${stats.average_speed || 0}</div>
252
+ <div class="stat-label">سرعت متوسط (صفحه/دقیقه)</div>
253
+ </div>
254
+ <div class="stat-card">
255
+ <div class="stat-number">${stats.average_rating || 0}</div>
256
+ <div class="stat-label">امتیاز متوسط</div>
257
+ </div>
258
+ </div>
259
+ `;
260
+
261
+ container.innerHTML = statsHTML;
262
+ }
263
+
264
+ getScrapingConfig() {
265
+ const urlInput = document.getElementById('scrapingUrl');
266
+ const depthInput = document.getElementById('scrapingDepth');
267
+ const maxPagesInput = document.getElementById('maxPages');
268
+ const filtersInput = document.getElementById('scrapingFilters');
269
+
270
+ return {
271
+ url: urlInput ? urlInput.value : '',
272
+ depth: depthInput ? parseInt(depthInput.value) : 1,
273
+ max_pages: maxPagesInput ? parseInt(maxPagesInput.value) : 100,
274
+ filters: filtersInput ? filtersInput.value : ''
275
+ };
276
+ }
277
+
278
+ updateStartButton(isRunning) {
279
+ const startBtn = document.getElementById('startScrapingBtn');
280
+ const stopBtn = document.getElementById('stopScrapingBtn');
281
+
282
+ if (startBtn) {
283
+ startBtn.disabled = isRunning;
284
+ startBtn.textContent = isRunning ? 'در حال اجرا...' : 'شروع اسکرپینگ';
285
+ }
286
+
287
+ if (stopBtn) {
288
+ stopBtn.disabled = !isRunning;
289
+ stopBtn.style.display = isRunning ? 'inline-block' : 'none';
290
+ }
291
+ }
292
+
293
+ addLog(message, type = 'info') {
294
+ const timestamp = new Date().toLocaleTimeString('fa-IR');
295
+ const logEntry = {
296
+ timestamp,
297
+ message,
298
+ type
299
+ };
300
+
301
+ this.logs.unshift(logEntry);
302
+ this.renderLogs();
303
+
304
+ // Keep only last 100 logs
305
+ if (this.logs.length > 100) {
306
+ this.logs = this.logs.slice(0, 100);
307
+ }
308
+ }
309
+
310
+ renderLogs() {
311
+ const container = document.getElementById('scrapingLogs');
312
+ if (!container) return;
313
+
314
+ const logsHTML = this.logs.map(log => `
315
+ <div class="log-entry ${log.type}">
316
+ <span class="log-timestamp">${log.timestamp}</span>
317
+ <span class="log-message">${log.message}</span>
318
+ </div>
319
+ `).join('');
320
+
321
+ container.innerHTML = logsHTML;
322
+ }
323
+
324
+ clearLogs() {
325
+ this.logs = [];
326
+ this.renderLogs();
327
+ this.showToast('لاگ‌ها پاک شدند', 'info');
328
+ }
329
+
330
+ getStatusClass(status) {
331
+ const statusMap = {
332
+ 'idle': 'status-idle',
333
+ 'running': 'status-running',
334
+ 'completed': 'status-completed',
335
+ 'failed': 'status-failed',
336
+ 'stopped': 'status-stopped'
337
+ };
338
+ return statusMap[status] || 'status-unknown';
339
+ }
340
+
341
+ getStatusText(status) {
342
+ const statusMap = {
343
+ 'idle': 'آماده',
344
+ 'running': 'در حال اجرا',
345
+ 'completed': 'تکمیل شده',
346
+ 'failed': 'ناموفق',
347
+ 'stopped': 'متوقف شده'
348
+ };
349
+ return statusMap[status] || 'نامشخص';
350
+ }
351
+
352
+ formatDate(dateString) {
353
+ if (!dateString) return 'نامشخص';
354
+ const date = new Date(dateString);
355
+ return date.toLocaleDateString('fa-IR');
356
+ }
357
+
358
+ showToast(message, type = 'info') {
359
+ if (typeof showToast === 'function') {
360
+ showToast(message, type);
361
+ } else {
362
+ console.log(`${type.toUpperCase()}: ${message}`);
363
+ }
364
+ }
365
+ }
366
+
367
+ // Initialize scraping control panel
368
+ const scrapingControlPanel = new ScrapingControlPanel();
app/frontend/reports.html ADDED
@@ -0,0 +1,917 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Legal Dashboard - Reports &amp; Analytics</title>
7
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
+ <!-- Load API Client and Core System -->
11
+ <script src="js/api-client.js"></script>
12
+ <script src="js/core.js"></script>
13
+ <script src="js/notifications.js"></script>
14
+ <style>
15
+ :root {
16
+ --primary-color: #007bff;
17
+ --success-color: #28a745;
18
+ --warning-color: #ffc107;
19
+ --danger-color: #dc3545;
20
+ --info-color: #17a2b8;
21
+ }
22
+
23
+ body {
24
+ background-color: #f8f9fa;
25
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
26
+ }
27
+
28
+ .navbar {
29
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
30
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
31
+ }
32
+
33
+ .navbar-brand {
34
+ font-weight: bold;
35
+ color: white !important;
36
+ }
37
+
38
+ .nav-link {
39
+ color: rgba(255,255,255,0.9) !important;
40
+ transition: color 0.3s ease;
41
+ }
42
+
43
+ .nav-link:hover {
44
+ color: white !important;
45
+ }
46
+
47
+ .main-content {
48
+ padding: 2rem 0;
49
+ }
50
+
51
+ .card {
52
+ border: none;
53
+ border-radius: 15px;
54
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
55
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
56
+ margin-bottom: 1.5rem;
57
+ }
58
+
59
+ .card:hover {
60
+ transform: translateY(-5px);
61
+ box-shadow: 0 8px 30px rgba(0,0,0,0.15);
62
+ }
63
+
64
+ .card-header {
65
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
66
+ color: white;
67
+ border-radius: 15px 15px 0 0 !important;
68
+ padding: 1.5rem;
69
+ }
70
+
71
+ .metric-card {
72
+ text-align: center;
73
+ padding: 2rem;
74
+ }
75
+
76
+ .metric-value {
77
+ font-size: 2.5rem;
78
+ font-weight: bold;
79
+ margin-bottom: 0.5rem;
80
+ }
81
+
82
+ .metric-label {
83
+ color: #666;
84
+ font-size: 0.9rem;
85
+ text-transform: uppercase;
86
+ letter-spacing: 1px;
87
+ }
88
+
89
+ .metric-change {
90
+ font-size: 0.8rem;
91
+ margin-top: 0.5rem;
92
+ }
93
+
94
+ .metric-change.positive {
95
+ color: var(--success-color);
96
+ }
97
+
98
+ .metric-change.negative {
99
+ color: var(--danger-color);
100
+ }
101
+
102
+ .chart-container {
103
+ position: relative;
104
+ height: 400px;
105
+ margin: 1rem 0;
106
+ }
107
+
108
+ .table-responsive {
109
+ border-radius: 10px;
110
+ overflow: hidden;
111
+ }
112
+
113
+ .table {
114
+ margin-bottom: 0;
115
+ }
116
+
117
+ .table thead th {
118
+ background-color: #f8f9fa;
119
+ border: none;
120
+ font-weight: 600;
121
+ color: #495057;
122
+ }
123
+
124
+ .table tbody tr {
125
+ transition: background-color 0.2s ease;
126
+ }
127
+
128
+ .table tbody tr:hover {
129
+ background-color: #f8f9fa;
130
+ }
131
+
132
+ .btn-export {
133
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
134
+ border: none;
135
+ color: white;
136
+ padding: 0.5rem 1.5rem;
137
+ border-radius: 25px;
138
+ transition: all 0.3s ease;
139
+ }
140
+
141
+ .btn-export:hover {
142
+ transform: translateY(-2px);
143
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
144
+ color: white;
145
+ }
146
+
147
+ .status-badge {
148
+ padding: 0.25rem 0.75rem;
149
+ border-radius: 20px;
150
+ font-size: 0.8rem;
151
+ font-weight: 500;
152
+ }
153
+
154
+ .status-badge.success {
155
+ background-color: #d4edda;
156
+ color: #155724;
157
+ }
158
+
159
+ .status-badge.warning {
160
+ background-color: #fff3cd;
161
+ color: #856404;
162
+ }
163
+
164
+ .status-badge.error {
165
+ background-color: #f8d7da;
166
+ color: #721c24;
167
+ }
168
+
169
+ .loading {
170
+ display: flex;
171
+ justify-content: center;
172
+ align-items: center;
173
+ height: 200px;
174
+ }
175
+
176
+ .spinner {
177
+ border: 4px solid #f3f3f3;
178
+ border-top: 4px solid var(--primary-color);
179
+ border-radius: 50%;
180
+ width: 40px;
181
+ height: 40px;
182
+ animation: spin 1s linear infinite;
183
+ }
184
+
185
+ @keyframes spin {
186
+ 0% { transform: rotate(0deg); }
187
+ 100% { transform: rotate(360deg); }
188
+ }
189
+
190
+ .filter-section {
191
+ background: white;
192
+ border-radius: 15px;
193
+ padding: 1.5rem;
194
+ margin-bottom: 2rem;
195
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
196
+ }
197
+
198
+ .date-range {
199
+ display: flex;
200
+ gap: 1rem;
201
+ align-items: center;
202
+ }
203
+
204
+ .date-input {
205
+ border: 1px solid #ddd;
206
+ border-radius: 8px;
207
+ padding: 0.5rem;
208
+ font-size: 0.9rem;
209
+ }
210
+
211
+ .refresh-btn {
212
+ background: var(--primary-color);
213
+ border: none;
214
+ color: white;
215
+ padding: 0.5rem 1rem;
216
+ border-radius: 8px;
217
+ cursor: pointer;
218
+ transition: all 0.3s ease;
219
+ }
220
+
221
+ .refresh-btn:hover {
222
+ background: #0056b3;
223
+ transform: translateY(-1px);
224
+ }
225
+
226
+ .export-section {
227
+ display: flex;
228
+ gap: 1rem;
229
+ flex-wrap: wrap;
230
+ margin-top: 1rem;
231
+ }
232
+
233
+ .progress-ring {
234
+ width: 120px;
235
+ height: 120px;
236
+ margin: 0 auto;
237
+ }
238
+
239
+ .progress-ring circle {
240
+ fill: none;
241
+ stroke-width: 8;
242
+ }
243
+
244
+ .progress-ring .bg {
245
+ stroke: #e9ecef;
246
+ }
247
+
248
+ .progress-ring .progress {
249
+ stroke: var(--primary-color);
250
+ stroke-linecap: round;
251
+ transition: stroke-dasharray 0.3s ease;
252
+ }
253
+
254
+ .progress-text {
255
+ text-align: center;
256
+ margin-top: 1rem;
257
+ }
258
+
259
+ .progress-value {
260
+ font-size: 1.5rem;
261
+ font-weight: bold;
262
+ color: var(--primary-color);
263
+ }
264
+
265
+ .progress-label {
266
+ font-size: 0.9rem;
267
+ color: #666;
268
+ }
269
+ </style>
270
+ </head>
271
+ <body>
272
+ <!-- Navigation -->
273
+ <nav class="navbar navbar-expand-lg navbar-dark">
274
+ <div class="container">
275
+ <a class="navbar-brand" href="#">
276
+ <i class="fas fa-chart-line me-2"></i>
277
+ Legal Dashboard - Reports
278
+ </a>
279
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-label="Toggle navigation">
280
+ <span class="navbar-toggler-icon"></span>
281
+ </button>
282
+ <div class="collapse navbar-collapse" id="navbarNav">
283
+ <ul class="navbar-nav ms-auto">
284
+ <li class="nav-item">
285
+ <a class="nav-link" href="index.html">
286
+ <i class="fas fa-home me-1"></i>Dashboard
287
+ </a>
288
+ </li>
289
+ <li class="nav-item">
290
+ <a class="nav-link" href="documents.html">
291
+ <i class="fas fa-file-alt me-1"></i>Documents
292
+ </a>
293
+ </li>
294
+ <li class="nav-item">
295
+ <a class="nav-link active" href="reports.html">
296
+ <i class="fas fa-chart-bar me-1"></i>Reports
297
+ </a>
298
+ </li>
299
+ <li class="nav-item">
300
+ <a class="nav-link" href="#" onclick="logout()">
301
+ <i class="fas fa-sign-out-alt me-1"></i>Logout
302
+ </a>
303
+ </li>
304
+ </ul>
305
+ </div>
306
+ </div>
307
+ </nav>
308
+
309
+ <!-- Main Content -->
310
+ <div class="container main-content">
311
+ <!-- Filter Section -->
312
+ <div class="filter-section">
313
+ <div class="row align-items-center">
314
+ <div class="col-md-6">
315
+ <h5 class="mb-3">
316
+ <i class="fas fa-filter me-2"></i>Filter Options
317
+ </h5>
318
+ <div class="date-range">
319
+ <label class="me-2">Date Range:</label>
320
+ <input type="date" id="startDate" class="date-input">
321
+ <span>to</span>
322
+ <input type="date" id="endDate" class="date-input">
323
+ <button type="button" class="refresh-btn" onclick="loadReports()">
324
+ <i class="fas fa-sync-alt me-1"></i>Refresh
325
+ </button>
326
+ </div>
327
+ </div>
328
+ <div class="col-md-6">
329
+ <div class="export-section">
330
+ <button type="button" class="btn btn-export" onclick="exportReport('summary')">
331
+ <i class="fas fa-download me-1"></i>Export Summary
332
+ </button>
333
+ <button type="button" class="btn btn-export" onclick="exportReport('user_activity')">
334
+ <i class="fas fa-users me-1"></i>Export User Activity
335
+ </button>
336
+ <button type="button" class="btn btn-export" onclick="exportReport('document_analytics')">
337
+ <i class="fas fa-file-alt me-1"></i>Export Document Analytics
338
+ </button>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
344
+ <!-- Summary Metrics -->
345
+ <div class="row" id="summaryMetrics">
346
+ <div class="col-md-3">
347
+ <div class="card metric-card">
348
+ <div class="metric-value" id="totalDocuments">-</div>
349
+ <div class="metric-label">Total Documents</div>
350
+ <div class="metric-change positive" id="documentsChange">+12% from last month</div>
351
+ </div>
352
+ </div>
353
+ <div class="col-md-3">
354
+ <div class="card metric-card">
355
+ <div class="metric-value" id="totalUsers">-</div>
356
+ <div class="metric-label">Active Users</div>
357
+ <div class="metric-change positive" id="usersChange">+5% from last month</div>
358
+ </div>
359
+ </div>
360
+ <div class="col-md-3">
361
+ <div class="card metric-card">
362
+ <div class="metric-value" id="successRate">-</div>
363
+ <div class="metric-label">Success Rate</div>
364
+ <div class="metric-change positive" id="successChange">+3% from last month</div>
365
+ </div>
366
+ </div>
367
+ <div class="col-md-3">
368
+ <div class="card metric-card">
369
+ <div class="metric-value" id="avgProcessingTime">-</div>
370
+ <div class="metric-label">Avg Processing Time</div>
371
+ <div class="metric-change negative" id="timeChange">+0.5s from last month</div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+
376
+ <!-- Charts Row -->
377
+ <div class="row">
378
+ <div class="col-md-8">
379
+ <div class="card">
380
+ <div class="card-header">
381
+ <h5 class="mb-0">
382
+ <i class="fas fa-chart-line me-2"></i>Document Processing Trends
383
+ </h5>
384
+ </div>
385
+ <div class="card-body">
386
+ <div class="chart-container">
387
+ <canvas id="trendsChart"></canvas>
388
+ </div>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ <div class="col-md-4">
393
+ <div class="card">
394
+ <div class="card-header">
395
+ <h5 class="mb-0">
396
+ <i class="fas fa-pie-chart me-2"></i>Processing Status
397
+ </h5>
398
+ </div>
399
+ <div class="card-body">
400
+ <div class="chart-container">
401
+ <canvas id="statusChart"></canvas>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ </div>
407
+
408
+ <!-- Performance Metrics -->
409
+ <div class="row">
410
+ <div class="col-md-6">
411
+ <div class="card">
412
+ <div class="card-header">
413
+ <h5 class="mb-0">
414
+ <i class="fas fa-tachometer-alt me-2"></i>System Performance
415
+ </h5>
416
+ </div>
417
+ <div class="card-body">
418
+ <div class="row">
419
+ <div class="col-6">
420
+ <div class="progress-ring">
421
+ <svg width="120" height="120">
422
+ <circle class="bg" cx="60" cy="60" r="50"></circle>
423
+ <circle class="progress" cx="60" cy="60" r="50"
424
+ stroke-dasharray="314" stroke-dashoffset="78.5"></circle>
425
+ </svg>
426
+ </div>
427
+ <div class="progress-text">
428
+ <div class="progress-value">75%</div>
429
+ <div class="progress-label">CPU Usage</div>
430
+ </div>
431
+ </div>
432
+ <div class="col-6">
433
+ <div class="progress-ring">
434
+ <svg width="120" height="120">
435
+ <circle class="bg" cx="60" cy="60" r="50"></circle>
436
+ <circle class="progress" cx="60" cy="60" r="50"
437
+ stroke-dasharray="314" stroke-dashoffset="157"></circle>
438
+ </svg>
439
+ </div>
440
+ <div class="progress-text">
441
+ <div class="progress-value">50%</div>
442
+ <div class="progress-label">Memory Usage</div>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ <div class="col-md-6">
450
+ <div class="card">
451
+ <div class="card-header">
452
+ <h5 class="mb-0">
453
+ <i class="fas fa-clock me-2"></i>Response Times
454
+ </h5>
455
+ </div>
456
+ <div class="card-body">
457
+ <div class="chart-container">
458
+ <canvas id="responseChart"></canvas>
459
+ </div>
460
+ </div>
461
+ </div>
462
+ </div>
463
+ </div>
464
+
465
+ <!-- User Activity Table -->
466
+ <div class="card">
467
+ <div class="card-header">
468
+ <h5 class="mb-0">
469
+ <i class="fas fa-users me-2"></i>User Activity
470
+ </h5>
471
+ </div>
472
+ <div class="card-body">
473
+ <div class="table-responsive">
474
+ <table class="table table-hover" id="userActivityTable">
475
+ <thead>
476
+ <tr>
477
+ <th scope="col">User</th>
478
+ <th scope="col">Documents Processed</th>
479
+ <th scope="col">Last Activity</th>
480
+ <th scope="col">Success Rate</th>
481
+ <th scope="col">Avg Processing Time</th>
482
+ </tr>
483
+ </thead>
484
+ <tbody id="userActivityBody">
485
+ <tr>
486
+ <td colspan="5" class="text-center">
487
+ <div class="loading">
488
+ <div class="spinner"></div>
489
+ </div>
490
+ </td>
491
+ </tr>
492
+ </tbody>
493
+ </table>
494
+ </div>
495
+ </div>
496
+ </div>
497
+
498
+ <!-- Document Analytics Table -->
499
+ <div class="card">
500
+ <div class="card-header">
501
+ <h5 class="mb-0">
502
+ <i class="fas fa-file-alt me-2"></i>Recent Document Analytics
503
+ </h5>
504
+ </div>
505
+ <div class="card-body">
506
+ <div class="table-responsive">
507
+ <table class="table table-hover" id="documentAnalyticsTable">
508
+ <thead>
509
+ <tr>
510
+ <th scope="col">Document</th>
511
+ <th scope="col">Processing Time</th>
512
+ <th scope="col">OCR Accuracy</th>
513
+ <th scope="col">File Size</th>
514
+ <th scope="col">Status</th>
515
+ <th scope="col">Created</th>
516
+ </tr>
517
+ </thead>
518
+ <tbody id="documentAnalyticsBody">
519
+ <tr>
520
+ <td colspan="6" class="text-center">
521
+ <div class="loading">
522
+ <div class="spinner"></div>
523
+ </div>
524
+ </td>
525
+ </tr>
526
+ </tbody>
527
+ </table>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ </div>
532
+
533
+ <!-- Scripts -->
534
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
535
+ <script src="js/notifications.js"></script>
536
+ <script>
537
+ // Global variables
538
+ let trendsChart, statusChart, responseChart;
539
+ let currentData = {};
540
+
541
+ // Initialize page
542
+ document.addEventListener('DOMContentLoaded', function() {
543
+ // Set default date range (last 30 days)
544
+ const endDate = new Date();
545
+ const startDate = new Date();
546
+ startDate.setDate(startDate.getDate() - 30);
547
+
548
+ document.getElementById('endDate').value = endDate.toISOString().split('T')[0];
549
+ document.getElementById('startDate').value = startDate.toISOString().split('T')[0];
550
+
551
+ // Load initial data
552
+ loadReports();
553
+ });
554
+
555
+ // Load reports data
556
+ async function loadReports() {
557
+ try {
558
+ showLoading();
559
+
560
+ // Load summary metrics
561
+ await loadSummaryMetrics();
562
+
563
+ // Load charts
564
+ await loadCharts();
565
+
566
+ // Load tables
567
+ await loadUserActivity();
568
+ await loadDocumentAnalytics();
569
+
570
+ hideLoading();
571
+
572
+ } catch (error) {
573
+ console.error('Error loading reports:', error);
574
+ showNotification('error', 'Error', 'Failed to load reports data');
575
+ hideLoading();
576
+ }
577
+ }
578
+
579
+ // Load summary metrics
580
+ async function loadSummaryMetrics() {
581
+ try {
582
+ const response = await fetch('/api/reports/summary', {
583
+ headers: {
584
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
585
+ }
586
+ });
587
+
588
+ if (!response.ok) throw new Error('Failed to load summary');
589
+
590
+ const data = await response.json();
591
+ currentData.summary = data;
592
+
593
+ // Update metrics
594
+ document.getElementById('totalDocuments').textContent = data.total_documents;
595
+ document.getElementById('totalUsers').textContent = data.total_users;
596
+ document.getElementById('successRate').textContent = `${data.success_rate.toFixed(1)}%`;
597
+ document.getElementById('avgProcessingTime').textContent = `${data.avg_processing_time.toFixed(1)}s`;
598
+
599
+ } catch (error) {
600
+ console.error('Error loading summary metrics:', error);
601
+ // Set default values
602
+ document.getElementById('totalDocuments').textContent = '0';
603
+ document.getElementById('totalUsers').textContent = '0';
604
+ document.getElementById('successRate').textContent = '0%';
605
+ document.getElementById('avgProcessingTime').textContent = '0s';
606
+ }
607
+ }
608
+
609
+ // Load charts
610
+ async function loadCharts() {
611
+ try {
612
+ // Load trends data
613
+ const trendsResponse = await fetch('/api/reports/trends?days=30', {
614
+ headers: {
615
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
616
+ }
617
+ });
618
+
619
+ if (trendsResponse.ok) {
620
+ const trendsData = await trendsResponse.json();
621
+ createTrendsChart(trendsData.daily_trends);
622
+ }
623
+
624
+ // Create status chart (mock data for now)
625
+ createStatusChart();
626
+
627
+ // Create response time chart (mock data for now)
628
+ createResponseChart();
629
+
630
+ } catch (error) {
631
+ console.error('Error loading charts:', error);
632
+ }
633
+ }
634
+
635
+ // Create trends chart
636
+ function createTrendsChart(data) {
637
+ const ctx = document.getElementById('trendsChart').getContext('2d');
638
+
639
+ if (trendsChart) {
640
+ trendsChart.destroy();
641
+ }
642
+
643
+ trendsChart = new Chart(ctx, {
644
+ type: 'line',
645
+ data: {
646
+ labels: data.map(d => d.date),
647
+ datasets: [{
648
+ label: 'Documents Processed',
649
+ data: data.map(d => d.documents_processed),
650
+ borderColor: '#667eea',
651
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
652
+ tension: 0.4
653
+ }, {
654
+ label: 'Success Rate (%)',
655
+ data: data.map(d => d.success_rate),
656
+ borderColor: '#28a745',
657
+ backgroundColor: 'rgba(40, 167, 69, 0.1)',
658
+ tension: 0.4,
659
+ yAxisID: 'y1'
660
+ }]
661
+ },
662
+ options: {
663
+ responsive: true,
664
+ maintainAspectRatio: false,
665
+ scales: {
666
+ y: {
667
+ beginAtZero: true,
668
+ title: {
669
+ display: true,
670
+ text: 'Documents'
671
+ }
672
+ },
673
+ y1: {
674
+ position: 'right',
675
+ beginAtZero: true,
676
+ max: 100,
677
+ title: {
678
+ display: true,
679
+ text: 'Success Rate (%)'
680
+ }
681
+ }
682
+ },
683
+ plugins: {
684
+ legend: {
685
+ position: 'top'
686
+ }
687
+ }
688
+ }
689
+ });
690
+ }
691
+
692
+ // Create status chart
693
+ function createStatusChart() {
694
+ const ctx = document.getElementById('statusChart').getContext('2d');
695
+
696
+ if (statusChart) {
697
+ statusChart.destroy();
698
+ }
699
+
700
+ statusChart = new Chart(ctx, {
701
+ type: 'doughnut',
702
+ data: {
703
+ labels: ['Completed', 'Processing', 'Failed', 'Pending'],
704
+ datasets: [{
705
+ data: [65, 15, 10, 10],
706
+ backgroundColor: [
707
+ '#28a745',
708
+ '#ffc107',
709
+ '#dc3545',
710
+ '#6c757d'
711
+ ]
712
+ }]
713
+ },
714
+ options: {
715
+ responsive: true,
716
+ maintainAspectRatio: false,
717
+ plugins: {
718
+ legend: {
719
+ position: 'bottom'
720
+ }
721
+ }
722
+ }
723
+ });
724
+ }
725
+
726
+ // Create response time chart
727
+ function createResponseChart() {
728
+ const ctx = document.getElementById('responseChart').getContext('2d');
729
+
730
+ if (responseChart) {
731
+ responseChart.destroy();
732
+ }
733
+
734
+ responseChart = new Chart(ctx, {
735
+ type: 'bar',
736
+ data: {
737
+ labels: ['Documents', 'OCR', 'Search', 'Analytics'],
738
+ datasets: [{
739
+ label: 'Response Time (ms)',
740
+ data: [150, 2500, 200, 300],
741
+ backgroundColor: [
742
+ '#667eea',
743
+ '#764ba2',
744
+ '#f093fb',
745
+ '#f5576c'
746
+ ]
747
+ }]
748
+ },
749
+ options: {
750
+ responsive: true,
751
+ maintainAspectRatio: false,
752
+ scales: {
753
+ y: {
754
+ beginAtZero: true,
755
+ title: {
756
+ display: true,
757
+ text: 'Time (ms)'
758
+ }
759
+ }
760
+ },
761
+ plugins: {
762
+ legend: {
763
+ display: false
764
+ }
765
+ }
766
+ }
767
+ });
768
+ }
769
+
770
+ // Load user activity
771
+ async function loadUserActivity() {
772
+ try {
773
+ const response = await fetch('/api/reports/user-activity?days=30', {
774
+ headers: {
775
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
776
+ }
777
+ });
778
+
779
+ if (!response.ok) throw new Error('Failed to load user activity');
780
+
781
+ const data = await response.json();
782
+ currentData.userActivity = data;
783
+
784
+ const tbody = document.getElementById('userActivityBody');
785
+ tbody.innerHTML = '';
786
+
787
+ data.forEach(user => {
788
+ const row = document.createElement('tr');
789
+ row.innerHTML = `
790
+ <td><strong>${user.username}</strong></td>
791
+ <td>${user.documents_processed}</td>
792
+ <td>${formatDate(user.last_activity)}</td>
793
+ <td><span class="status-badge success">${user.success_rate.toFixed(1)}%</span></td>
794
+ <td>${user.total_processing_time.toFixed(1)}s</td>
795
+ `;
796
+ tbody.appendChild(row);
797
+ });
798
+
799
+ } catch (error) {
800
+ console.error('Error loading user activity:', error);
801
+ document.getElementById('userActivityBody').innerHTML =
802
+ '<tr><td colspan="5" class="text-center text-muted">No data available</td></tr>';
803
+ }
804
+ }
805
+
806
+ // Load document analytics
807
+ async function loadDocumentAnalytics() {
808
+ try {
809
+ const response = await fetch('/api/reports/document-analytics?limit=20', {
810
+ headers: {
811
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
812
+ }
813
+ });
814
+
815
+ if (!response.ok) throw new Error('Failed to load document analytics');
816
+
817
+ const data = await response.json();
818
+ currentData.documentAnalytics = data;
819
+
820
+ const tbody = document.getElementById('documentAnalyticsBody');
821
+ tbody.innerHTML = '';
822
+
823
+ data.forEach(doc => {
824
+ const row = document.createElement('tr');
825
+ row.innerHTML = `
826
+ <td><strong>${doc.filename}</strong></td>
827
+ <td>${doc.processing_time.toFixed(1)}s</td>
828
+ <td>${doc.ocr_accuracy ? doc.ocr_accuracy.toFixed(1) + '%' : 'N/A'}</td>
829
+ <td>${formatFileSize(doc.file_size)}</td>
830
+ <td><span class="status-badge ${getStatusClass(doc.status)}">${doc.status}</span></td>
831
+ <td>${formatDate(doc.created_at)}</td>
832
+ `;
833
+ tbody.appendChild(row);
834
+ });
835
+
836
+ } catch (error) {
837
+ console.error('Error loading document analytics:', error);
838
+ document.getElementById('documentAnalyticsBody').innerHTML =
839
+ '<tr><td colspan="6" class="text-center text-muted">No data available</td></tr>';
840
+ }
841
+ }
842
+
843
+ // Export report
844
+ async function exportReport(type) {
845
+ try {
846
+ const response = await fetch(`/api/reports/export/csv?report_type=${type}`, {
847
+ headers: {
848
+ 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
849
+ }
850
+ });
851
+
852
+ if (!response.ok) throw new Error('Failed to export report');
853
+
854
+ const blob = await response.blob();
855
+ const url = window.URL.createObjectURL(blob);
856
+ const a = document.createElement('a');
857
+ a.href = url;
858
+ a.download = `${type}_report_${new Date().toISOString().split('T')[0]}.csv`;
859
+ document.body.appendChild(a);
860
+ a.click();
861
+ document.body.removeChild(a);
862
+ window.URL.revokeObjectURL(url);
863
+
864
+ showNotification('success', 'Export Successful', `${type} report has been downloaded`);
865
+
866
+ } catch (error) {
867
+ console.error('Error exporting report:', error);
868
+ showNotification('error', 'Export Failed', 'Failed to export report');
869
+ }
870
+ }
871
+
872
+ // Utility functions
873
+ function formatDate(dateString) {
874
+ if (!dateString || dateString === 'Never') return 'Never';
875
+ const date = new Date(dateString);
876
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
877
+ }
878
+
879
+ function formatFileSize(bytes) {
880
+ if (bytes === 0) return '0 Bytes';
881
+ const k = 1024;
882
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
883
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
884
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
885
+ }
886
+
887
+ function getStatusClass(status) {
888
+ switch (status.toLowerCase()) {
889
+ case 'completed': return 'success';
890
+ case 'processing': return 'warning';
891
+ case 'failed': return 'error';
892
+ default: return 'warning';
893
+ }
894
+ }
895
+
896
+ function showLoading() {
897
+ // Show loading indicators
898
+ const loadingElements = document.querySelectorAll('.loading');
899
+ loadingElements.forEach(el => el.style.display = 'flex');
900
+ }
901
+
902
+ function hideLoading() {
903
+ // Hide loading indicators
904
+ const loadingElements = document.querySelectorAll('.loading');
905
+ loadingElements.forEach(el => el.style.display = 'none');
906
+ }
907
+
908
+ function logout() {
909
+ localStorage.removeItem('auth_token');
910
+ window.location.href = 'index.html';
911
+ }
912
+
913
+ // Auto-refresh every 5 minutes
914
+ setInterval(loadReports, 300000);
915
+ </script>
916
+ </body>
917
+ </html>
app/frontend/scraping.html ADDED
@@ -0,0 +1,1925 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>سامانه اسکراپینگ حقوقی | داشبورد هوشمند</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+
12
+ <!-- Load API Client and Core System -->
13
+ <script src="js/api-client.js"></script>
14
+ <script src="js/core.js"></script>
15
+ <script src="js/notifications.js"></script>
16
+ <script src="js/scraping-control.js"></script>
17
+
18
+ <style>
19
+ :root {
20
+ /* رنگ‌بندی مدرن و هارمونیک - منطبق با index */
21
+ --text-primary: #0f172a;
22
+ --text-secondary: #475569;
23
+ --text-muted: #64748b;
24
+ --text-light: #ffffff;
25
+
26
+ /* پس‌زمینه‌های بهبود یافته */
27
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
28
+ --card-bg: rgba(255, 255, 255, 0.95);
29
+ --glass-bg: rgba(255, 255, 255, 0.9);
30
+ --glass-border: rgba(148, 163, 184, 0.2);
31
+
32
+ /* گرادیان‌های مدرن */
33
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
34
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
35
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
36
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
37
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
38
+ --purple-gradient: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
39
+
40
+ /* سایه‌های ملایم */
41
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
42
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
43
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
44
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
45
+ --shadow-glow-primary: 0 0 20px rgba(59, 130, 246, 0.15);
46
+ --shadow-glow-success: 0 0 20px rgba(16, 185, 129, 0.15);
47
+ --shadow-glow-warning: 0 0 20px rgba(245, 158, 11, 0.15);
48
+ --shadow-glow-purple: 0 0 20px rgba(139, 92, 246, 0.15);
49
+
50
+ /* متغیرهای کامپکت */
51
+ --sidebar-width: 260px;
52
+ --border-radius: 12px;
53
+ --border-radius-sm: 8px;
54
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
55
+ --transition-fast: all 0.15s ease-in-out;
56
+
57
+ /* فونت‌های کامپکت */
58
+ --font-size-xs: 0.7rem;
59
+ --font-size-sm: 0.8rem;
60
+ --font-size-base: 0.9rem;
61
+ --font-size-lg: 1.1rem;
62
+ --font-size-xl: 1.25rem;
63
+ --font-size-2xl: 1.5rem;
64
+ }
65
+
66
+ /* ریست و تنظیمات پایه */
67
+ * {
68
+ margin: 0;
69
+ padding: 0;
70
+ box-sizing: border-box;
71
+ }
72
+
73
+ body {
74
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
75
+ background: var(--body-bg);
76
+ color: var(--text-primary);
77
+ line-height: 1.6;
78
+ overflow-x: hidden;
79
+ font-size: var(--font-size-base);
80
+ }
81
+
82
+ /* اسکرول‌بار مدرن */
83
+ ::-webkit-scrollbar {
84
+ inline-size: 6px;
85
+ block-size: 6px;
86
+ }
87
+
88
+ ::-webkit-scrollbar-track {
89
+ background: rgba(0, 0, 0, 0.02);
90
+ border-radius: 10px;
91
+ }
92
+
93
+ ::-webkit-scrollbar-thumb {
94
+ background: var(--primary-gradient);
95
+ border-radius: 10px;
96
+ }
97
+
98
+ /* کانتینر اصلی */
99
+ .dashboard-container {
100
+ display: flex;
101
+ min-height: 100vh;
102
+ width: 100%;
103
+ }
104
+
105
+ /* سایدبار کامپکت - منطبق با index */
106
+ .sidebar {
107
+ width: var(--sidebar-width);
108
+ background: linear-gradient(135deg,
109
+ rgba(248, 250, 252, 0.98) 0%,
110
+ rgba(241, 245, 249, 0.95) 25%,
111
+ rgba(226, 232, 240, 0.98) 50%,
112
+ rgba(203, 213, 225, 0.95) 75%,
113
+ rgba(148, 163, 184, 0.1) 100%);
114
+ backdrop-filter: blur(25px);
115
+ padding: 1rem 0;
116
+ position: fixed;
117
+ height: 100vh;
118
+ right: 0;
119
+ top: 0;
120
+ z-index: 1000;
121
+ overflow-y: auto;
122
+ box-shadow: -8px 0 32px rgba(59, 130, 246, 0.12);
123
+ border-left: 1px solid rgba(59, 130, 246, 0.15);
124
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
125
+ }
126
+
127
+ .sidebar-header {
128
+ padding: 0 1rem 1rem;
129
+ border-bottom: 1px solid rgba(59, 130, 246, 0.12);
130
+ margin-bottom: 1rem;
131
+ display: flex;
132
+ justify-content: space-between;
133
+ align-items: center;
134
+ background: linear-gradient(135deg,
135
+ rgba(255, 255, 255, 0.4) 0%,
136
+ rgba(248, 250, 252, 0.2) 100%);
137
+ margin: 0 0.5rem 1rem;
138
+ border-radius: var(--border-radius);
139
+ backdrop-filter: blur(10px);
140
+ }
141
+
142
+ .logo {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 0.6rem;
146
+ color: var(--text-primary);
147
+ text-decoration: none;
148
+ }
149
+
150
+ .logo-icon {
151
+ width: 2rem;
152
+ height: 2rem;
153
+ background: var(--primary-gradient);
154
+ border-radius: var(--border-radius-sm);
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ font-size: 1rem;
159
+ color: white;
160
+ box-shadow: var(--shadow-glow-primary);
161
+ }
162
+
163
+ .logo-text {
164
+ font-size: var(--font-size-lg);
165
+ font-weight: 700;
166
+ background: var(--primary-gradient);
167
+ -webkit-background-clip: text;
168
+ -webkit-text-fill-color: transparent;
169
+ }
170
+
171
+ .nav-section {
172
+ margin-bottom: 1rem;
173
+ }
174
+
175
+ .nav-title {
176
+ padding: 0 1rem 0.4rem;
177
+ font-size: var(--font-size-xs);
178
+ font-weight: 600;
179
+ text-transform: uppercase;
180
+ letter-spacing: 0.5px;
181
+ color: var(--text-secondary);
182
+ }
183
+
184
+ .nav-menu {
185
+ list-style: none;
186
+ }
187
+
188
+ .nav-item {
189
+ margin: 0.15rem 0.5rem;
190
+ }
191
+
192
+ .nav-link {
193
+ display: flex;
194
+ align-items: center;
195
+ padding: 0.6rem 0.8rem;
196
+ color: var(--text-primary);
197
+ text-decoration: none;
198
+ border-radius: var(--border-radius-sm);
199
+ transition: var(--transition-smooth);
200
+ font-weight: 500;
201
+ font-size: var(--font-size-sm);
202
+ cursor: pointer;
203
+ border: 1px solid transparent;
204
+ }
205
+
206
+ .nav-link:hover {
207
+ color: var(--text-primary);
208
+ transform: translateX(-2px);
209
+ border-color: rgba(59, 130, 246, 0.15);
210
+ background: rgba(59, 130, 246, 0.05);
211
+ }
212
+
213
+ .nav-link.active {
214
+ background: var(--primary-gradient);
215
+ color: var(--text-light);
216
+ box-shadow: var(--shadow-md);
217
+ }
218
+
219
+ .nav-icon {
220
+ margin-left: 0.6rem;
221
+ width: 1rem;
222
+ text-align: center;
223
+ font-size: 0.9rem;
224
+ }
225
+
226
+ .nav-badge {
227
+ background: var(--danger-gradient);
228
+ color: white;
229
+ padding: 0.15rem 0.4rem;
230
+ border-radius: 10px;
231
+ font-size: var(--font-size-xs);
232
+ font-weight: 600;
233
+ margin-right: auto;
234
+ min-width: 1.2rem;
235
+ text-align: center;
236
+ }
237
+
238
+ /* محتوای اصلی */
239
+ .main-content {
240
+ flex: 1;
241
+ margin-right: var(--sidebar-width);
242
+ padding: 1rem;
243
+ min-height: 100vh;
244
+ width: calc(100% - var(--sidebar-width));
245
+ }
246
+
247
+ /* هدر کامپکت - منطبق با index */
248
+ .dashboard-header {
249
+ display: flex;
250
+ justify-content: space-between;
251
+ align-items: center;
252
+ margin-bottom: 1.2rem;
253
+ padding: 0.8rem 0;
254
+ }
255
+
256
+ .dashboard-title {
257
+ font-size: var(--font-size-2xl);
258
+ font-weight: 800;
259
+ background: var(--primary-gradient);
260
+ -webkit-background-clip: text;
261
+ -webkit-text-fill-color: transparent;
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 0.6rem;
265
+ }
266
+
267
+ .header-actions {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 0.8rem;
271
+ }
272
+
273
+ /* آمار کلی - منطبق با index */
274
+ .stats-grid {
275
+ display: grid;
276
+ grid-template-columns: repeat(4, 1fr);
277
+ gap: 1rem;
278
+ margin-bottom: 1.2rem;
279
+ }
280
+
281
+ .stat-card {
282
+ background: var(--card-bg);
283
+ backdrop-filter: blur(10px);
284
+ border-radius: var(--border-radius);
285
+ padding: 1.2rem;
286
+ box-shadow: var(--shadow-md);
287
+ border: 1px solid rgba(255, 255, 255, 0.3);
288
+ position: relative;
289
+ overflow: hidden;
290
+ transition: var(--transition-smooth);
291
+ min-height: 130px;
292
+ }
293
+
294
+ .stat-card::before {
295
+ content: '';
296
+ position: absolute;
297
+ top: 0;
298
+ left: 0;
299
+ right: 0;
300
+ height: 4px;
301
+ background: var(--primary-gradient);
302
+ }
303
+
304
+ .stat-card.primary::before { background: var(--primary-gradient); }
305
+ .stat-card.success::before { background: var(--success-gradient); }
306
+ .stat-card.danger::before { background: var(--danger-gradient); }
307
+ .stat-card.warning::before { background: var(--warning-gradient); }
308
+ .stat-card.purple::before { background: var(--purple-gradient); }
309
+
310
+ .stat-card:hover {
311
+ transform: translateY(-6px) scale(1.02);
312
+ box-shadow: var(--shadow-lg);
313
+ }
314
+
315
+ .stat-header {
316
+ display: flex;
317
+ justify-content: space-between;
318
+ align-items: flex-start;
319
+ margin-bottom: 0.8rem;
320
+ }
321
+
322
+ .stat-icon {
323
+ width: 2.2rem;
324
+ height: 2.2rem;
325
+ border-radius: var(--border-radius-sm);
326
+ display: flex;
327
+ align-items: center;
328
+ justify-content: center;
329
+ font-size: 1.1rem;
330
+ box-shadow: var(--shadow-sm);
331
+ transition: var(--transition-smooth);
332
+ }
333
+
334
+ .stat-icon.primary { background: var(--primary-gradient); color: white; }
335
+ .stat-icon.success { background: var(--success-gradient); color: white; }
336
+ .stat-icon.danger { background: var(--danger-gradient); color: white; }
337
+ .stat-icon.warning { background: var(--warning-gradient); color: white; }
338
+ .stat-icon.purple { background: var(--purple-gradient); color: white; }
339
+
340
+ .stat-content {
341
+ flex: 1;
342
+ }
343
+
344
+ .stat-title {
345
+ font-size: var(--font-size-xs);
346
+ color: var(--text-secondary);
347
+ font-weight: 600;
348
+ margin-bottom: 0.3rem;
349
+ }
350
+
351
+ .stat-value {
352
+ font-size: var(--font-size-xl);
353
+ font-weight: 800;
354
+ color: var(--text-primary);
355
+ line-height: 1;
356
+ margin-bottom: 0.3rem;
357
+ }
358
+
359
+ .stat-value.small {
360
+ font-size: var(--font-size-base);
361
+ }
362
+
363
+ .stat-extra {
364
+ font-size: var(--font-size-xs);
365
+ color: var(--text-muted);
366
+ margin-bottom: 0.3rem;
367
+ }
368
+
369
+ .stat-change {
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 0.25rem;
373
+ font-size: var(--font-size-xs);
374
+ font-weight: 700;
375
+ }
376
+
377
+ .stat-change.positive { color: #059669; }
378
+ .stat-change.negative { color: #dc2626; }
379
+
380
+ /* کارت‌های عملیات اصلی */
381
+ .control-section {
382
+ display: grid;
383
+ grid-template-columns: 2fr 1fr;
384
+ gap: 1.5rem;
385
+ margin-bottom: 1.5rem;
386
+ }
387
+
388
+ .control-card {
389
+ background: var(--card-bg);
390
+ backdrop-filter: blur(10px);
391
+ border-radius: var(--border-radius);
392
+ padding: 1.5rem;
393
+ box-shadow: var(--shadow-md);
394
+ border: 1px solid rgba(255, 255, 255, 0.3);
395
+ transition: var(--transition-smooth);
396
+ }
397
+
398
+ .control-card:hover {
399
+ transform: translateY(-4px);
400
+ box-shadow: var(--shadow-lg);
401
+ }
402
+
403
+ .control-header {
404
+ display: flex;
405
+ justify-content: between;
406
+ align-items: center;
407
+ margin-bottom: 1.2rem;
408
+ }
409
+
410
+ .control-title {
411
+ font-size: var(--font-size-lg);
412
+ font-weight: 700;
413
+ color: var(--text-primary);
414
+ display: flex;
415
+ align-items: center;
416
+ gap: 0.6rem;
417
+ }
418
+
419
+ /* مراجع آماده */
420
+ .sources-grid {
421
+ display: grid;
422
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
423
+ gap: 1rem;
424
+ max-height: 350px;
425
+ overflow-y: auto;
426
+ padding-left: 0.5rem;
427
+ }
428
+
429
+ .source-item {
430
+ background: var(--glass-bg);
431
+ border: 2px solid var(--glass-border);
432
+ border-radius: var(--border-radius-sm);
433
+ padding: 1rem;
434
+ cursor: pointer;
435
+ transition: var(--transition-smooth);
436
+ position: relative;
437
+ }
438
+
439
+ .source-item:hover {
440
+ border-color: rgba(59, 130, 246, 0.3);
441
+ background: rgba(59, 130, 246, 0.03);
442
+ transform: translateY(-2px);
443
+ box-shadow: var(--shadow-sm);
444
+ }
445
+
446
+ .source-item.selected {
447
+ border-color: #3b82f6;
448
+ background: rgba(59, 130, 246, 0.05);
449
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
450
+ }
451
+
452
+ .source-checkbox {
453
+ position: absolute;
454
+ top: 0.8rem;
455
+ left: 0.8rem;
456
+ width: 1.2rem;
457
+ height: 1.2rem;
458
+ cursor: pointer;
459
+ }
460
+
461
+ .source-info {
462
+ margin-right: 2rem;
463
+ }
464
+
465
+ .source-name {
466
+ font-weight: 600;
467
+ color: var(--text-primary);
468
+ margin-bottom: 0.3rem;
469
+ font-size: var(--font-size-sm);
470
+ }
471
+
472
+ .source-url {
473
+ font-size: var(--font-size-xs);
474
+ color: var(--text-muted);
475
+ margin-bottom: 0.5rem;
476
+ direction: ltr;
477
+ text-align: left;
478
+ }
479
+
480
+ .source-meta {
481
+ display: flex;
482
+ gap: 0.5rem;
483
+ flex-wrap: wrap;
484
+ }
485
+
486
+ .source-tag {
487
+ background: rgba(59, 130, 246, 0.1);
488
+ color: #3b82f6;
489
+ padding: 0.15rem 0.5rem;
490
+ border-radius: 10px;
491
+ font-size: var(--font-size-xs);
492
+ font-weight: 500;
493
+ }
494
+
495
+ .source-status {
496
+ width: 8px;
497
+ height: 8px;
498
+ border-radius: 50%;
499
+ margin-top: 0.4rem;
500
+ position: absolute;
501
+ top: 0.8rem;
502
+ right: 0.8rem;
503
+ }
504
+
505
+ .source-status.online {
506
+ background: #10b981;
507
+ box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
508
+ }
509
+
510
+ .source-status.offline {
511
+ background: #ef4444;
512
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2);
513
+ }
514
+
515
+ /* دکمه‌های عمل - منطبق با index */
516
+ .action-buttons {
517
+ display: flex;
518
+ gap: 0.8rem;
519
+ margin-bottom: 1.5rem;
520
+ flex-wrap: wrap;
521
+ }
522
+
523
+ .btn {
524
+ padding: 0.6rem 1.2rem;
525
+ border: none;
526
+ border-radius: var(--border-radius-sm);
527
+ font-family: inherit;
528
+ font-weight: 600;
529
+ cursor: pointer;
530
+ transition: var(--transition-smooth);
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 0.5rem;
534
+ text-decoration: none;
535
+ font-size: var(--font-size-sm);
536
+ min-width: 120px;
537
+ justify-content: center;
538
+ }
539
+
540
+ .btn-primary {
541
+ background: var(--primary-gradient);
542
+ color: white;
543
+ box-shadow: var(--shadow-sm);
544
+ }
545
+
546
+ .btn-primary:hover {
547
+ box-shadow: var(--shadow-md), var(--shadow-glow-primary);
548
+ transform: translateY(-2px);
549
+ }
550
+
551
+ .btn-success {
552
+ background: var(--success-gradient);
553
+ color: white;
554
+ }
555
+
556
+ .btn-success:hover {
557
+ box-shadow: var(--shadow-md), var(--shadow-glow-success);
558
+ transform: translateY(-2px);
559
+ }
560
+
561
+ .btn-warning {
562
+ background: var(--warning-gradient);
563
+ color: white;
564
+ }
565
+
566
+ .btn-warning:hover {
567
+ box-shadow: var(--shadow-md), var(--shadow-glow-warning);
568
+ transform: translateY(-2px);
569
+ }
570
+
571
+ .btn-secondary {
572
+ background: var(--glass-bg);
573
+ color: var(--text-primary);
574
+ border: 1px solid var(--glass-border);
575
+ }
576
+
577
+ .btn-secondary:hover {
578
+ background: rgba(59, 130, 246, 0.05);
579
+ border-color: rgba(59, 130, 246, 0.3);
580
+ transform: translateY(-2px);
581
+ }
582
+
583
+ .btn-auto {
584
+ background: var(--purple-gradient);
585
+ color: white;
586
+ font-size: var(--font-size-base);
587
+ padding: 0.8rem 1.5rem;
588
+ position: relative;
589
+ overflow: hidden;
590
+ animation: pulse-auto 3s ease-in-out infinite;
591
+ }
592
+
593
+ @keyframes pulse-auto {
594
+ 0%, 100% { box-shadow: var(--shadow-sm); }
595
+ 50% { box-shadow: var(--shadow-lg), var(--shadow-glow-purple); }
596
+ }
597
+
598
+ .btn-auto:hover {
599
+ box-shadow: var(--shadow-xl), var(--shadow-glow-purple);
600
+ transform: translateY(-3px) scale(1.02);
601
+ }
602
+
603
+ .btn:disabled {
604
+ opacity: 0.6;
605
+ cursor: not-allowed;
606
+ transform: none !important;
607
+ box-shadow: var(--shadow-sm) !important;
608
+ }
609
+
610
+ .btn-sm {
611
+ padding: 0.4rem 0.8rem;
612
+ font-size: var(--font-size-xs);
613
+ min-width: auto;
614
+ }
615
+
616
+ /* تنظیمات سریع */
617
+ .quick-settings {
618
+ background: rgba(59, 130, 246, 0.03);
619
+ border: 1px solid rgba(59, 130, 246, 0.1);
620
+ border-radius: var(--border-radius-sm);
621
+ padding: 1rem;
622
+ margin-top: 1rem;
623
+ }
624
+
625
+ .settings-title {
626
+ font-size: var(--font-size-sm);
627
+ font-weight: 600;
628
+ color: var(--text-primary);
629
+ margin-bottom: 0.8rem;
630
+ display: flex;
631
+ align-items: center;
632
+ gap: 0.5rem;
633
+ }
634
+
635
+ .settings-grid {
636
+ display: grid;
637
+ grid-template-columns: 1fr 1fr;
638
+ gap: 1rem;
639
+ }
640
+
641
+ .setting-item label {
642
+ display: block;
643
+ font-size: var(--font-size-xs);
644
+ font-weight: 600;
645
+ color: var(--text-secondary);
646
+ margin-bottom: 0.4rem;
647
+ }
648
+
649
+ .setting-item select {
650
+ width: 100%;
651
+ padding: 0.5rem;
652
+ border: 1px solid var(--glass-border);
653
+ border-radius: var(--border-radius-sm);
654
+ background: var(--glass-bg);
655
+ color: var(--text-primary);
656
+ font-family: inherit;
657
+ font-size: var(--font-size-xs);
658
+ }
659
+
660
+ /* وضعیت فعالیت */
661
+ .activity-status {
662
+ background: var(--card-bg);
663
+ border-radius: var(--border-radius);
664
+ padding: 1.5rem;
665
+ margin-bottom: 1.5rem;
666
+ box-shadow: var(--shadow-md);
667
+ border: 1px solid rgba(255, 255, 255, 0.3);
668
+ display: none;
669
+ }
670
+
671
+ .activity-status.show {
672
+ display: block;
673
+ }
674
+
675
+ .status-content {
676
+ display: flex;
677
+ align-items: center;
678
+ gap: 1rem;
679
+ }
680
+
681
+ .status-icon {
682
+ width: 3rem;
683
+ height: 3rem;
684
+ border-radius: 50%;
685
+ display: flex;
686
+ align-items: center;
687
+ justify-content: center;
688
+ font-size: 1.2rem;
689
+ flex-shrink: 0;
690
+ }
691
+
692
+ .status-loading {
693
+ background: rgba(59, 130, 246, 0.1);
694
+ color: #3b82f6;
695
+ border: 2px solid rgba(59, 130, 246, 0.2);
696
+ }
697
+
698
+ .status-success {
699
+ background: rgba(16, 185, 129, 0.1);
700
+ color: #10b981;
701
+ border: 2px solid rgba(16, 185, 129, 0.2);
702
+ }
703
+
704
+ .status-error {
705
+ background: rgba(239, 68, 68, 0.1);
706
+ color: #ef4444;
707
+ border: 2px solid rgba(239, 68, 68, 0.2);
708
+ }
709
+
710
+ .status-details {
711
+ flex: 1;
712
+ }
713
+
714
+ .status-title {
715
+ font-size: var(--font-size-lg);
716
+ font-weight: 600;
717
+ color: var(--text-primary);
718
+ margin-bottom: 0.3rem;
719
+ }
720
+
721
+ .status-message {
722
+ font-size: var(--font-size-sm);
723
+ color: var(--text-secondary);
724
+ margin-bottom: 0.8rem;
725
+ }
726
+
727
+ .status-progress {
728
+ width: 100%;
729
+ height: 6px;
730
+ background: rgba(0, 0, 0, 0.08);
731
+ border-radius: 3px;
732
+ overflow: hidden;
733
+ }
734
+
735
+ .status-progress-bar {
736
+ height: 100%;
737
+ background: var(--primary-gradient);
738
+ border-radius: 3px;
739
+ transition: width 0.3s ease;
740
+ position: relative;
741
+ }
742
+
743
+ .status-progress-bar::after {
744
+ content: '';
745
+ position: absolute;
746
+ top: 0;
747
+ left: 0;
748
+ right: 0;
749
+ bottom: 0;
750
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
751
+ animation: shimmer 2s infinite;
752
+ }
753
+
754
+ @keyframes shimmer {
755
+ 0% { transform: translateX(-100%); }
756
+ 100% { transform: translateX(100%); }
757
+ }
758
+
759
+ /* نتایج */
760
+ .results-section {
761
+ background: var(--card-bg);
762
+ border-radius: var(--border-radius);
763
+ box-shadow: var(--shadow-md);
764
+ margin-bottom: 1.5rem;
765
+ border: 1px solid rgba(255, 255, 255, 0.3);
766
+ overflow: hidden;
767
+ display: none;
768
+ }
769
+
770
+ .results-section.show {
771
+ display: block;
772
+ }
773
+
774
+ .results-header {
775
+ display: flex;
776
+ justify-content: space-between;
777
+ align-items: center;
778
+ padding: 1.5rem;
779
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
780
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.02), rgba(255, 255, 255, 0.05));
781
+ }
782
+
783
+ .results-title {
784
+ font-size: var(--font-size-lg);
785
+ font-weight: 700;
786
+ color: var(--text-primary);
787
+ display: flex;
788
+ align-items: center;
789
+ gap: 0.6rem;
790
+ }
791
+
792
+ .results-actions {
793
+ display: flex;
794
+ gap: 0.5rem;
795
+ }
796
+
797
+ .results-grid {
798
+ display: grid;
799
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
800
+ gap: 1rem;
801
+ padding: 1.5rem;
802
+ }
803
+
804
+ .result-card {
805
+ background: var(--glass-bg);
806
+ border: 1px solid var(--glass-border);
807
+ border-radius: var(--border-radius-sm);
808
+ padding: 1.5rem;
809
+ transition: var(--transition-smooth);
810
+ }
811
+
812
+ .result-card:hover {
813
+ border-color: rgba(16, 185, 129, 0.3);
814
+ transform: translateY(-2px);
815
+ box-shadow: var(--shadow-sm);
816
+ }
817
+
818
+ .result-source {
819
+ font-weight: 600;
820
+ color: var(--text-primary);
821
+ margin-bottom: 0.5rem;
822
+ font-size: var(--font-size-sm);
823
+ }
824
+
825
+ .result-meta {
826
+ display: flex;
827
+ gap: 1rem;
828
+ margin-bottom: 1rem;
829
+ font-size: var(--font-size-xs);
830
+ color: var(--text-muted);
831
+ flex-wrap: wrap;
832
+ }
833
+
834
+ .result-meta span {
835
+ display: flex;
836
+ align-items: center;
837
+ gap: 0.3rem;
838
+ }
839
+
840
+ .result-content {
841
+ font-size: var(--font-size-sm);
842
+ color: var(--text-secondary);
843
+ line-height: 1.5;
844
+ max-height: 100px;
845
+ overflow: hidden;
846
+ margin-bottom: 1rem;
847
+ }
848
+
849
+ .result-actions {
850
+ display: flex;
851
+ gap: 0.5rem;
852
+ padding-top: 1rem;
853
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
854
+ }
855
+
856
+ /* Toast Notifications - منطبق با index */
857
+ .toast-container {
858
+ position: fixed;
859
+ top: 1rem;
860
+ left: 1rem;
861
+ z-index: 10001;
862
+ display: flex;
863
+ flex-direction: column;
864
+ gap: 0.5rem;
865
+ }
866
+
867
+ .toast {
868
+ background: var(--card-bg);
869
+ border-radius: var(--border-radius-sm);
870
+ padding: 1rem 1.5rem;
871
+ box-shadow: var(--shadow-lg);
872
+ border-left: 4px solid;
873
+ display: flex;
874
+ align-items: center;
875
+ gap: 0.8rem;
876
+ min-width: 300px;
877
+ transform: translateX(-100%);
878
+ transition: all 0.3s ease;
879
+ }
880
+
881
+ .toast.show {
882
+ transform: translateX(0);
883
+ }
884
+
885
+ .toast.success { border-left-color: #10b981; }
886
+ .toast.error { border-left-color: #ef4444; }
887
+ .toast.warning { border-left-color: #f59e0b; }
888
+ .toast.info { border-left-color: #3b82f6; }
889
+
890
+ .toast-icon {
891
+ font-size: 1.2rem;
892
+ }
893
+
894
+ .toast.success .toast-icon { color: #10b981; }
895
+ .toast.error .toast-icon { color: #ef4444; }
896
+ .toast.warning .toast-icon { color: #f59e0b; }
897
+ .toast.info .toast-icon { color: #3b82f6; }
898
+
899
+ .toast-content {
900
+ flex: 1;
901
+ }
902
+
903
+ .toast-title {
904
+ font-weight: 600;
905
+ font-size: var(--font-size-sm);
906
+ margin-bottom: 0.2rem;
907
+ }
908
+
909
+ .toast-message {
910
+ font-size: var(--font-size-xs);
911
+ color: var(--text-secondary);
912
+ }
913
+
914
+ .toast-close {
915
+ background: none;
916
+ border: none;
917
+ color: var(--text-secondary);
918
+ cursor: pointer;
919
+ font-size: 1rem;
920
+ transition: var(--transition-fast);
921
+ }
922
+
923
+ .toast-close:hover {
924
+ color: var(--text-primary);
925
+ }
926
+
927
+ /* دکمه منوی موبایل */
928
+ .mobile-menu-toggle {
929
+ display: none;
930
+ background: var(--glass-bg);
931
+ border: 1px solid var(--glass-border);
932
+ padding: 0.5rem;
933
+ border-radius: var(--border-radius-sm);
934
+ color: var(--text-primary);
935
+ font-size: 1rem;
936
+ cursor: pointer;
937
+ transition: var(--transition-fast);
938
+ }
939
+
940
+ .mobile-menu-toggle:hover {
941
+ background: var(--primary-gradient);
942
+ color: white;
943
+ }
944
+
945
+ /* واکنش‌گرایی */
946
+ @media (max-width: 992px) {
947
+ .mobile-menu-toggle {
948
+ display: block;
949
+ }
950
+
951
+ .sidebar {
952
+ transform: translateX(100%);
953
+ position: fixed;
954
+ z-index: 10000;
955
+ }
956
+
957
+ .sidebar.open {
958
+ transform: translateX(0);
959
+ }
960
+
961
+ .main-content {
962
+ margin-right: 0;
963
+ width: 100%;
964
+ padding: 1rem;
965
+ }
966
+
967
+ .dashboard-header {
968
+ flex-direction: column;
969
+ align-items: flex-start;
970
+ gap: 0.8rem;
971
+ }
972
+
973
+ .header-actions {
974
+ width: 100%;
975
+ justify-content: space-between;
976
+ }
977
+
978
+ .stats-grid {
979
+ grid-template-columns: repeat(2, 1fr);
980
+ }
981
+
982
+ .control-section {
983
+ grid-template-columns: 1fr;
984
+ }
985
+
986
+ .sources-grid {
987
+ grid-template-columns: 1fr;
988
+ }
989
+
990
+ .settings-grid {
991
+ grid-template-columns: 1fr;
992
+ }
993
+ }
994
+
995
+ @media (max-width: 768px) {
996
+ .main-content {
997
+ padding: 0.8rem;
998
+ }
999
+
1000
+ .stats-grid {
1001
+ grid-template-columns: 1fr;
1002
+ gap: 0.6rem;
1003
+ }
1004
+
1005
+ .stat-card {
1006
+ min-height: 100px;
1007
+ padding: 0.8rem;
1008
+ }
1009
+
1010
+ .stat-value {
1011
+ font-size: var(--font-size-lg);
1012
+ }
1013
+
1014
+ .action-buttons {
1015
+ flex-direction: column;
1016
+ }
1017
+
1018
+ .btn {
1019
+ justify-content: center;
1020
+ width: 100%;
1021
+ }
1022
+
1023
+ .results-grid {
1024
+ grid-template-columns: 1fr;
1025
+ }
1026
+ }
1027
+ </style>
1028
+ </head>
1029
+ <body>
1030
+ <div class="dashboard-container">
1031
+ <!-- سایدبار -->
1032
+ <aside class="sidebar" id="sidebar">
1033
+ <div class="sidebar-header">
1034
+ <div class="logo">
1035
+ <div class="logo-icon">
1036
+ <i class="fas fa-spider"></i>
1037
+ </div>
1038
+ <div class="logo-text">سامانه اسکراپینگ</div>
1039
+ </div>
1040
+ </div>
1041
+
1042
+ <nav>
1043
+ <div class="nav-section">
1044
+ <h6 class="nav-title">داشبورد</h6>
1045
+ <ul class="nav-menu">
1046
+ <li class="nav-item">
1047
+ <a href="index.html" class="nav-link">
1048
+ <i class="fas fa-chart-pie nav-icon"></i>
1049
+ <span>نمای کلی</span>
1050
+ </a>
1051
+ </li>
1052
+ <li class="nav-item">
1053
+ <a href="scraping_dashboard.html" class="nav-link active">
1054
+ <i class="fas fa-spider nav-icon"></i>
1055
+ <span>اسکراپینگ خودکار</span>
1056
+ </a>
1057
+ </li>
1058
+ </ul>
1059
+ </div>
1060
+
1061
+ <div class="nav-section">
1062
+ <h6 class="nav-title">مدیریت اسناد</h6>
1063
+ <ul class="nav-menu">
1064
+ <li class="nav-item">
1065
+ <a href="documents.html" class="nav-link">
1066
+ <i class="fas fa-file-alt nav-icon"></i>
1067
+ <span>مدیریت اسناد</span>
1068
+ <span class="nav-badge">6</span>
1069
+ </a>
1070
+ </li>
1071
+ <li class="nav-item">
1072
+ <a href="upload.html" class="nav-link">
1073
+ <i class="fas fa-cloud-upload-alt nav-icon"></i>
1074
+ <span>آپلود فایل</span>
1075
+ </a>
1076
+ </li>
1077
+ <li class="nav-item">
1078
+ <a href="search.html" class="nav-link">
1079
+ <i class="fas fa-search nav-icon"></i>
1080
+ <span>جستجو</span>
1081
+ </a>
1082
+ </li>
1083
+ </ul>
1084
+ </div>
1085
+
1086
+ <div class="nav-section">
1087
+ <h6 class="nav-title">ابزارها</h6>
1088
+ <ul class="nav-menu">
1089
+ <li class="nav-item">
1090
+ <a href="scraping.html" class="nav-link">
1091
+ <i class="fas fa-globe nav-icon"></i>
1092
+ <span>استخراج محتوا</span>
1093
+ </a>
1094
+ </li>
1095
+ <li class="nav-item">
1096
+ <a href="analytics.html" class="nav-link">
1097
+ <i class="fas fa-chart-line nav-icon"></i>
1098
+ <span>آمار و تحلیل</span>
1099
+ </a>
1100
+ </li>
1101
+ <li class="nav-item">
1102
+ <a href="reports.html" class="nav-link">
1103
+ <i class="fas fa-file-export nav-icon"></i>
1104
+ <span>گزارش‌ها</span>
1105
+ </a>
1106
+ </li>
1107
+ </ul>
1108
+ </div>
1109
+
1110
+ <div class="nav-section">
1111
+ <h6 class="nav-title">تنظیمات</h6>
1112
+ <ul class="nav-menu">
1113
+ <li class="nav-item">
1114
+ <a href="settings.html" class="nav-link">
1115
+ <i class="fas fa-cog nav-icon"></i>
1116
+ <span>تنظیمات</span>
1117
+ </a>
1118
+ </li>
1119
+ <li class="nav-item">
1120
+ <a href="#" class="nav-link">
1121
+ <i class="fas fa-sign-out-alt nav-icon"></i>
1122
+ <span>خروج</span>
1123
+ </a>
1124
+ </li>
1125
+ </ul>
1126
+ </div>
1127
+ </nav>
1128
+ </aside>
1129
+
1130
+ <!-- محتوای اصلی -->
1131
+ <main class="main-content">
1132
+ <!-- هدر صفحه -->
1133
+ <header class="dashboard-header">
1134
+ <div>
1135
+ <h1 class="dashboard-title">
1136
+ <i class="fas fa-spider"></i>
1137
+ <span>سامانه اسکراپینگ حقوقی</span>
1138
+ </h1>
1139
+ </div>
1140
+ <div class="header-actions">
1141
+ <button type="button" class="mobile-menu-toggle" id="mobileMenuToggle" aria-label="منوی موبایل">
1142
+ <i class="fas fa-bars"></i>
1143
+ </button>
1144
+ </div>
1145
+ </header>
1146
+
1147
+ <!-- آمار کلی -->
1148
+ <section aria-labelledby="stats-section">
1149
+ <h2 id="stats-section" class="sr-only">آمار اسکراپینگ</h2>
1150
+ <div class="stats-grid">
1151
+ <div class="stat-card primary">
1152
+ <div class="stat-header">
1153
+ <div class="stat-content">
1154
+ <div class="stat-title">مراجع حقوقی</div>
1155
+ <div class="stat-value" id="totalSources">10</div>
1156
+ <div class="stat-extra">مراجع آماده</div>
1157
+ <div class="stat-change positive">
1158
+ <i class="fas fa-arrow-up"></i>
1159
+ <span>+100%</span>
1160
+ </div>
1161
+ </div>
1162
+ <div class="stat-icon primary">
1163
+ <i class="fas fa-globe"></i>
1164
+ </div>
1165
+ </div>
1166
+ </div>
1167
+
1168
+ <div class="stat-card success">
1169
+ <div class="stat-header">
1170
+ <div class="stat-content">
1171
+ <div class="stat-title">مراجع انتخاب شده</div>
1172
+ <div class="stat-value" id="selectedSources">0</div>
1173
+ <div class="stat-extra">آماده اسکراپینگ</div>
1174
+ <div class="stat-change positive">
1175
+ <i class="fas fa-arrow-up"></i>
1176
+ <span>+0%</span>
1177
+ </div>
1178
+ </div>
1179
+ <div class="stat-icon success">
1180
+ <i class="fas fa-check-circle"></i>
1181
+ </div>
1182
+ </div>
1183
+ </div>
1184
+
1185
+ <div class="stat-card warning">
1186
+ <div class="stat-header">
1187
+ <div class="stat-content">
1188
+ <div class="stat-title">محتوای استخراج شده</div>
1189
+ <div class="stat-value" id="scrapedItems">0</div>
1190
+ <div class="stat-extra">آیتم استخراج شده</div>
1191
+ <div class="stat-change positive">
1192
+ <i class="fas fa-arrow-up"></i>
1193
+ <span>+0%</span>
1194
+ </div>
1195
+ </div>
1196
+ <div class="stat-icon warning">
1197
+ <i class="fas fa-download"></i>
1198
+ </div>
1199
+ </div>
1200
+ </div>
1201
+
1202
+ <div class="stat-card purple">
1203
+ <div class="stat-header">
1204
+ <div class="stat-content">
1205
+ <div class="stat-title">آخرین بروزرسانی</div>
1206
+ <div class="stat-value small" id="lastUpdate">--</div>
1207
+ <div class="stat-extra">زمان آخرین فعالیت</div>
1208
+ <div class="stat-change positive">
1209
+ <i class="fas fa-clock"></i>
1210
+ <span>جدید</span>
1211
+ </div>
1212
+ </div>
1213
+ <div class="stat-icon purple">
1214
+ <i class="fas fa-clock"></i>
1215
+ </div>
1216
+ </div>
1217
+ </div>
1218
+ </div>
1219
+ </section>
1220
+
1221
+ <!-- کنترل عملیات -->
1222
+ <section class="control-section">
1223
+ <!-- مراجع آماده -->
1224
+ <div class="control-card">
1225
+ <div class="control-header">
1226
+ <h2 class="control-title">
1227
+ <i class="fas fa-list"></i>
1228
+ مراجع حقوقی آماده
1229
+ </h2>
1230
+ </div>
1231
+ <div class="sources-grid" id="sourcesGrid">
1232
+ <!-- مراجع اینجا لود می‌شوند -->
1233
+ </div>
1234
+ </div>
1235
+
1236
+ <!-- کنترل عملیات -->
1237
+ <div class="control-card">
1238
+ <div class="control-header">
1239
+ <h2 class="control-title">
1240
+ <i class="fas fa-cogs"></i>
1241
+ کنترل عملیات
1242
+ </h2>
1243
+ </div>
1244
+
1245
+ <div class="action-buttons">
1246
+ <button type="button" class="btn btn-auto" onclick="startAutoScraping()" id="autoScrapingBtn">
1247
+ <i class="fas fa-magic"></i>
1248
+ اسکراپینگ خودکار
1249
+ </button>
1250
+ <button type="button" class="btn btn-primary" onclick="startSelectedScraping()" id="selectedScrapingBtn">
1251
+ <i class="fas fa-play"></i>
1252
+ اسکراپینگ انتخابی
1253
+ </button>
1254
+ <button type="button" class="btn btn-warning" onclick="stopScraping()" id="stopScrapingBtn" disabled>
1255
+ <i class="fas fa-stop"></i>
1256
+ توقف
1257
+ </button>
1258
+ <button type="button" class="btn btn-secondary" onclick="refreshData()">
1259
+ <i class="fas fa-sync-alt"></i>
1260
+ بروزرسانی
1261
+ </button>
1262
+ </div>
1263
+
1264
+ <div class="action-buttons">
1265
+ <button type="button" class="btn btn-secondary" onclick="selectAllSources()">
1266
+ <i class="fas fa-check-double"></i>
1267
+ انتخاب همه
1268
+ </button>
1269
+ <button type="button" class="btn btn-secondary" onclick="clearAllSources()">
1270
+ <i class="fas fa-times"></i>
1271
+ پاک کردن انتخاب
1272
+ </button>
1273
+ <button type="button" class="btn btn-secondary" onclick="checkSourcesStatus()">
1274
+ <i class="fas fa-wifi"></i>
1275
+ بررسی وضعیت
1276
+ </button>
1277
+ </div>
1278
+
1279
+ <div class="quick-settings">
1280
+ <h4 class="settings-title">
1281
+ <i class="fas fa-sliders-h"></i>
1282
+ تنظیمات سریع
1283
+ </h4>
1284
+ <div class="settings-grid">
1285
+ <div class="setting-item">
1286
+ <label>عمق اسکراپینگ</label>
1287
+ <select id="scrapingDepth">
1288
+ <option value="1">سطح ۱ (صفحه اصلی)</option>
1289
+ <option value="2" selected>سطح ۲ (یک سطح عمق)</option>
1290
+ <option value="3">سطح ۳ (دو سطح عمق)</option>
1291
+ </select>
1292
+ </div>
1293
+ <div class="setting-item">
1294
+ <label>تاخیر بین درخواست‌ها</label>
1295
+ <select id="scrapingDelay">
1296
+ <option value="1">۱ ثانیه</option>
1297
+ <option value="2" selected>۲ ثانیه</option>
1298
+ <option value="3">۳ ثانیه</option>
1299
+ <option value="5">۵ ثانیه</option>
1300
+ </select>
1301
+ </div>
1302
+ </div>
1303
+ </div>
1304
+ </div>
1305
+ </section>
1306
+
1307
+ <!-- وضعیت فعالیت -->
1308
+ <div class="activity-status" id="activityStatus">
1309
+ <div class="status-content">
1310
+ <div class="status-icon status-loading" id="statusIcon">
1311
+ <i class="fas fa-spinner fa-spin"></i>
1312
+ </div>
1313
+ <div class="status-details">
1314
+ <div class="status-title" id="statusTitle">در حال اسکراپینگ...</div>
1315
+ <div class="status-message" id="statusMessage">در حال اتصال به مراجع...</div>
1316
+ <div class="status-progress">
1317
+ <div class="status-progress-bar" id="statusProgressBar"></div>
1318
+ </div>
1319
+ </div>
1320
+ </div>
1321
+ </div>
1322
+
1323
+ <!-- نتایج -->
1324
+ <div class="results-section" id="resultsSection">
1325
+ <div class="results-header">
1326
+ <h3 class="results-title">
1327
+ <i class="fas fa-check-circle"></i>
1328
+ نتایج اسکراپینگ
1329
+ </h3>
1330
+ <div class="results-actions">
1331
+ <button type="button" class="btn btn-success btn-sm" onclick="exportResults()">
1332
+ <i class="fas fa-download"></i>
1333
+ دانلود JSON
1334
+ </button>
1335
+ <button type="button" class="btn btn-primary btn-sm" onclick="saveToDatabase()">
1336
+ <i class="fas fa-save"></i>
1337
+ ذخیره
1338
+ </button>
1339
+ </div>
1340
+ </div>
1341
+ <div class="results-grid" id="resultsGrid">
1342
+ <!-- نتایج اینجا نمایش داده می‌شوند -->
1343
+ </div>
1344
+ </div>
1345
+ </main>
1346
+ </div>
1347
+
1348
+ <!-- Toast Container -->
1349
+ <div class="toast-container" id="toastContainer"></div>
1350
+
1351
+ <script>
1352
+ // داده‌های مراجع حقوقی
1353
+ const legalSources = [
1354
+ {
1355
+ id: 'divan',
1356
+ name: 'دیوان عدالت اداری',
1357
+ url: 'https://www.divan.gov.ir',
1358
+ category: 'قضایی',
1359
+ tags: ['دیوان', 'عدالت اداری', 'آرا'],
1360
+ status: 'online',
1361
+ priority: 1
1362
+ },
1363
+ {
1364
+ id: 'dadiran',
1365
+ name: 'پورتال ملی خدمات قضایی',
1366
+ url: 'https://www.dadiran.ir',
1367
+ category: 'خدمات قضایی',
1368
+ tags: ['دادگستری', 'خدمات', 'پورتال ملی'],
1369
+ status: 'online',
1370
+ priority: 1
1371
+ },
1372
+ {
1373
+ id: 'judiciary',
1374
+ name: 'قوه قضائیه',
1375
+ url: 'https://www.judiciary.ir',
1376
+ category: 'قضایی',
1377
+ tags: ['قوه قضائیه', 'اخبار', 'اطلاعیه'],
1378
+ status: 'online',
1379
+ priority: 1
1380
+ },
1381
+ {
1382
+ id: 'lawcenter',
1383
+ name: 'مرکز مطالعات و تحقیقات قوانین',
1384
+ url: 'https://www.lawcenter.gov.ir',
1385
+ category: 'تحقیقات',
1386
+ tags: ['قوانین', 'تحقیقات', 'مطالعات'],
1387
+ status: 'online',
1388
+ priority: 2
1389
+ },
1390
+ {
1391
+ id: 'mohaseabat',
1392
+ name: 'دیوان محاسبات کشور',
1393
+ url: 'https://www.mohaseabat.gov.ir',
1394
+ category: 'مالی',
1395
+ tags: ['محاسبات', 'مالی', 'گزارش'],
1396
+ status: 'online',
1397
+ priority: 2
1398
+ },
1399
+ {
1400
+ id: 'majlis',
1401
+ name: 'مجلس شورای اسلامی',
1402
+ url: 'https://www.majlis.ir',
1403
+ category: 'قانونگذاری',
1404
+ tags: ['مجلس', 'قوانین', 'لوایح'],
1405
+ status: 'online',
1406
+ priority: 1
1407
+ },
1408
+ {
1409
+ id: 'shora-negahban',
1410
+ name: 'شورای نگهبان',
1411
+ url: 'https://www.shora-rc.ir',
1412
+ category: 'نظارتی',
1413
+ tags: ['شورای نگهبان', 'نظارت', 'تایید'],
1414
+ status: 'online',
1415
+ priority: 2
1416
+ },
1417
+ {
1418
+ id: 'maslahat',
1419
+ name: 'مجمع تشخیص مصلحت نظام',
1420
+ url: 'https://www.maslahat.ir',
1421
+ category: 'مشاوره‌ای',
1422
+ tags: ['مجمع', 'مصلحت', 'مشاوره'],
1423
+ status: 'online',
1424
+ priority: 2
1425
+ },
1426
+ {
1427
+ id: 'dadgostary-tehran',
1428
+ name: 'دادگستری استان تهران',
1429
+ url: 'https://www.tehran-justice.ir',
1430
+ category: 'قضایی محلی',
1431
+ tags: ['دادگستری', 'تهران', 'محلی'],
1432
+ status: 'online',
1433
+ priority: 3
1434
+ },
1435
+ {
1436
+ id: 'kanoon-vokala',
1437
+ name: 'کانون وکلای دادگستری',
1438
+ url: 'https://www.iranbar.org',
1439
+ category: 'صنفی',
1440
+ tags: ['وکلا', 'کانون', 'صنفی'],
1441
+ status: 'online',
1442
+ priority: 3
1443
+ }
1444
+ ];
1445
+
1446
+ // متغیرهای عمومی
1447
+ let selectedSources = [];
1448
+ let scrapingActive = false;
1449
+ let scrapingResults = [];
1450
+ let scrapingProgress = 0;
1451
+
1452
+ // شروع برنامه
1453
+ document.addEventListener('DOMContentLoaded', function() {
1454
+ initializeDashboard();
1455
+ loadSources();
1456
+ updateStatistics();
1457
+ setupEventListeners();
1458
+ showToast('سامانه اسکراپینگ حقوقی آماده است', 'success', 'آماده');
1459
+ });
1460
+
1461
+ // مقداردهی اولیه
1462
+ function initializeDashboard() {
1463
+ console.log('🕷️ Legal Scraping Dashboard Initializing...');
1464
+
1465
+ // شبیه‌سازی داده‌های موجود
1466
+ const mockData = {
1467
+ scrapedItems: Math.floor(Math.random() * 1000) + 500,
1468
+ lastUpdate: new Date().toLocaleDateString('fa-IR')
1469
+ };
1470
+
1471
+ document.getElementById('scrapedItems').textContent = mockData.scrapedItems;
1472
+ document.getElementById('lastUpdate').textContent = mockData.lastUpdate;
1473
+ }
1474
+
1475
+ // Setup event listeners
1476
+ function setupEventListeners() {
1477
+ // Mobile menu toggle
1478
+ const mobileMenuToggle = document.getElementById('mobileMenuToggle');
1479
+ const sidebar = document.getElementById('sidebar');
1480
+
1481
+ if (mobileMenuToggle && sidebar) {
1482
+ mobileMenuToggle.addEventListener('click', () => {
1483
+ sidebar.classList.toggle('open');
1484
+ });
1485
+
1486
+ // Close sidebar when clicking outside on mobile
1487
+ document.addEventListener('click', (e) => {
1488
+ if (window.innerWidth <= 992 &&
1489
+ sidebar.classList.contains('open') &&
1490
+ !sidebar.contains(e.target) &&
1491
+ !mobileMenuToggle.contains(e.target)) {
1492
+ sidebar.classList.remove('open');
1493
+ }
1494
+ });
1495
+ }
1496
+ }
1497
+
1498
+ // بارگذاری مراجع
1499
+ function loadSources() {
1500
+ const sourcesGrid = document.getElementById('sourcesGrid');
1501
+ let html = '';
1502
+
1503
+ legalSources.forEach(source => {
1504
+ html += `
1505
+ <div class="source-item" onclick="toggleSource('${source.id}')" id="source-${source.id}">
1506
+ <input type="checkbox" class="source-checkbox" id="checkbox-${source.id}" onchange="toggleSource('${source.id}')">
1507
+ <div class="source-status ${source.status}"></div>
1508
+ <div class="source-info">
1509
+ <div class="source-name">${source.name}</div>
1510
+ <div class="source-url">${source.url}</div>
1511
+ <div class="source-meta">
1512
+ <span class="source-tag">${source.category}</span>
1513
+ ${source.tags.slice(0, 2).map(tag => `<span class="source-tag">${tag}</span>`).join('')}
1514
+ </div>
1515
+ </div>
1516
+ </div>
1517
+ `;
1518
+ });
1519
+
1520
+ sourcesGrid.innerHTML = html;
1521
+ }
1522
+
1523
+ // تغییر وضعیت انتخاب مرجع
1524
+ function toggleSource(sourceId) {
1525
+ const sourceElement = document.getElementById(`source-${sourceId}`);
1526
+ const checkbox = document.getElementById(`checkbox-${sourceId}`);
1527
+
1528
+ if (selectedSources.includes(sourceId)) {
1529
+ selectedSources = selectedSources.filter(id => id !== sourceId);
1530
+ sourceElement.classList.remove('selected');
1531
+ checkbox.checked = false;
1532
+ } else {
1533
+ selectedSources.push(sourceId);
1534
+ sourceElement.classList.add('selected');
1535
+ checkbox.checked = true;
1536
+ }
1537
+
1538
+ updateStatistics();
1539
+ }
1540
+
1541
+ // انتخاب همه مراجع
1542
+ function selectAllSources() {
1543
+ selectedSources = legalSources.map(source => source.id);
1544
+ legalSources.forEach(source => {
1545
+ const sourceElement = document.getElementById(`source-${source.id}`);
1546
+ const checkbox = document.getElementById(`checkbox-${source.id}`);
1547
+ sourceElement.classList.add('selected');
1548
+ checkbox.checked = true;
1549
+ });
1550
+ updateStatistics();
1551
+ showToast('همه مراجع انتخاب شدند', 'success', 'انتخاب');
1552
+ }
1553
+
1554
+ // پاک کردن انتخاب همه مراجع
1555
+ function clearAllSources() {
1556
+ selectedSources = [];
1557
+ legalSources.forEach(source => {
1558
+ const sourceElement = document.getElementById(`source-${source.id}`);
1559
+ const checkbox = document.getElementById(`checkbox-${source.id}`);
1560
+ sourceElement.classList.remove('selected');
1561
+ checkbox.checked = false;
1562
+ });
1563
+ updateStatistics();
1564
+ showToast('انتخاب همه مراجع پاک شد', 'info', 'پاک سازی');
1565
+ }
1566
+
1567
+ // بروزرسانی آمار
1568
+ function updateStatistics() {
1569
+ document.getElementById('selectedSources').textContent = selectedSources.length;
1570
+ }
1571
+
1572
+ // شروع اسکراپینگ خودکار
1573
+ function startAutoScraping() {
1574
+ if (scrapingActive) {
1575
+ showToast('اسکراپینگ در حال انجام است', 'warning', 'هشدار');
1576
+ return;
1577
+ }
1578
+
1579
+ // انتخاب خودکار مراجع بر اساس اولویت
1580
+ const autoSelectedSources = legalSources
1581
+ .filter(source => source.priority <= 2)
1582
+ .map(source => source.id);
1583
+
1584
+ if (autoSelectedSources.length === 0) {
1585
+ showToast('هیچ مرجع مناسبی برای اسکراپینگ خودکار یافت نشد', 'error', 'خطا');
1586
+ return;
1587
+ }
1588
+
1589
+ // بروزرسانی انتخاب‌ها
1590
+ clearAllSources();
1591
+ autoSelectedSources.forEach(sourceId => {
1592
+ toggleSource(sourceId);
1593
+ });
1594
+
1595
+ showToast(`${autoSelectedSources.length} مرجع به صورت خودکار انتخاب شد`, 'info', 'انتخاب خودکار');
1596
+
1597
+ setTimeout(() => {
1598
+ startScraping(autoSelectedSources);
1599
+ }, 1000);
1600
+ }
1601
+
1602
+ // شروع اسکراپینگ مراجع انتخابی
1603
+ function startSelectedScraping() {
1604
+ if (scrapingActive) {
1605
+ showToast('اسکراپینگ در حال انجام است', 'warning', 'هشدار');
1606
+ return;
1607
+ }
1608
+
1609
+ if (selectedSources.length === 0) {
1610
+ showToast('لطفاً حداقل یک مرجع انتخاب کنید', 'warning', 'هشدار');
1611
+ return;
1612
+ }
1613
+
1614
+ startScraping(selectedSources);
1615
+ }
1616
+
1617
+ // شروع فرآیند اسکراپینگ
1618
+ function startScraping(sources) {
1619
+ scrapingActive = true;
1620
+ scrapingResults = [];
1621
+ scrapingProgress = 0;
1622
+
1623
+ // بروزرسانی UI
1624
+ document.getElementById('autoScrapingBtn').disabled = true;
1625
+ document.getElementById('selectedScrapingBtn').disabled = true;
1626
+ document.getElementById('stopScrapingBtn').disabled = false;
1627
+
1628
+ // نمایش وضعیت
1629
+ const activityStatus = document.getElementById('activityStatus');
1630
+ activityStatus.classList.add('show');
1631
+
1632
+ updateScrapingStatus('loading', 'شروع اسکراپینگ...', 'در حال آماده‌سازی...', 0);
1633
+
1634
+ showToast(`شروع اسکراپینگ ${sources.length} مرجع`, 'info', 'شروع');
1635
+
1636
+ // شبیه‌سازی فرآیند اسکراپینگ
1637
+ performScraping(sources);
1638
+ }
1639
+
1640
+ // انجام اسکراپینگ (شبیه‌سازی)
1641
+ async function performScraping(sources) {
1642
+ const totalSteps = sources.length * 5; // 5 مرحله برای هر مرجع
1643
+ let currentStep = 0;
1644
+
1645
+ for (let i = 0; i < sources.length; i++) {
1646
+ const sourceId = sources[i];
1647
+ const source = legalSources.find(s => s.id === sourceId);
1648
+
1649
+ if (!source) continue;
1650
+
1651
+ // مراحل اسکراپینگ برای هر مرجع
1652
+ const steps = [
1653
+ { title: `اتصال به ${source.name}`, message: 'در حال برقراری ارتباط...' },
1654
+ { title: `دریافت صفحه ${source.name}`, message: 'در حال دانلود HTML...' },
1655
+ { title: `تجزیه محتوا ${source.name}`, message: 'در حال استخراج اطلاعات...' },
1656
+ { title: `پردازش داده‌ها ${source.name}`, message: 'در حال تمیز کردن متن...' },
1657
+ { title: `ذخیره نتایج ${source.name}`, message: 'در حال ذخیره در پایگاه داده...' }
1658
+ ];
1659
+
1660
+ for (let j = 0; j < steps.length; j++) {
1661
+ currentStep++;
1662
+ const progress = (currentStep / totalSteps) * 100;
1663
+
1664
+ updateScrapingStatus('loading', steps[j].title, steps[j].message, progress);
1665
+
1666
+ // شبیه‌سازی تاخیر
1667
+ await new Promise(resolve => setTimeout(resolve, 800));
1668
+
1669
+ if (!scrapingActive) {
1670
+ return; // متوقف شده
1671
+ }
1672
+ }
1673
+
1674
+ // تولید نتیجه تصادفی برای این مرجع
1675
+ const result = generateMockResult(source);
1676
+ scrapingResults.push(result);
1677
+ }
1678
+
1679
+ // تکمیل اسکراپینگ
1680
+ completeScrapingSuccess();
1681
+ }
1682
+
1683
+ // تولید نتیجه تصادفی
1684
+ function generateMockResult(source) {
1685
+ const sampleContents = [
1686
+ 'قانون جدید در زمینه حقوق شهروندی تصویب شد...',
1687
+ 'دیوان عدالت اداری رای مهمی در خصوص اختیارات دولتی صادر کرد...',
1688
+ 'آخرین تغییرات در قوانین مدنی و تجارت اعلام شد...',
1689
+ 'گزارش جدید از عملکرد دستگاه‌های قضایی منتشر شد...',
1690
+ 'رویه‌های جدید برای تسریع در روند قضایی ابلاغ شد...'
1691
+ ];
1692
+
1693
+ const randomContent = sampleContents[Math.floor(Math.random() * sampleContents.length)];
1694
+
1695
+ return {
1696
+ source: source.name,
1697
+ url: source.url,
1698
+ title: `آخرین اطلاعات از ${source.name}`,
1699
+ content: randomContent,
1700
+ wordCount: Math.floor(Math.random() * 500) + 100,
1701
+ scrapedAt: new Date().toISOString(),
1702
+ links: Math.floor(Math.random() * 10) + 5,
1703
+ status: 'success'
1704
+ };
1705
+ }
1706
+
1707
+ // تکمیل موفق اسکراپینگ
1708
+ function completeScrapingSuccess() {
1709
+ updateScrapingStatus('success', 'اسکراپینگ تکمیل شد', `${scrapingResults.length} مرجع با موفقیت پردازش شد`, 100);
1710
+
1711
+ setTimeout(() => {
1712
+ document.getElementById('activityStatus').classList.remove('show');
1713
+ displayResults();
1714
+ resetScrapingState();
1715
+
1716
+ showToast(`اسکراپینگ تکمیل شد - ${scrapingResults.length} نتیجه`, 'success', 'تکمیل');
1717
+
1718
+ // بروزرسانی آمار
1719
+ const currentScraped = parseInt(document.getElementById('scrapedItems').textContent);
1720
+ document.getElementById('scrapedItems').textContent = currentScraped + scrapingResults.length;
1721
+ document.getElementById('lastUpdate').textContent = new Date().toLocaleDateString('fa-IR');
1722
+ }, 2000);
1723
+ }
1724
+
1725
+ // نمایش نتایج
1726
+ function displayResults() {
1727
+ const resultsSection = document.getElementById('resultsSection');
1728
+ const resultsGrid = document.getElementById('resultsGrid');
1729
+
1730
+ let html = '';
1731
+ scrapingResults.forEach((result, index) => {
1732
+ html += `
1733
+ <div class="result-card">
1734
+ <div class="result-source">${result.source}</div>
1735
+ <div class="result-meta">
1736
+ <span><i class="fas fa-file-text"></i> ${result.wordCount} کلمه</span>
1737
+ <span><i class="fas fa-link"></i> ${result.links} لینک</span>
1738
+ <span><i class="fas fa-clock"></i> ${new Date(result.scrapedAt).toLocaleTimeString('fa-IR')}</span>
1739
+ </div>
1740
+ <div class="result-content">${result.content}</div>
1741
+ <div class="result-actions">
1742
+ <button class="btn btn-primary btn-sm" onclick="viewFullResult(${index})">
1743
+ <i class="fas fa-eye"></i>
1744
+ مشاهده کامل
1745
+ </button>
1746
+ <button class="btn btn-success btn-sm" onclick="saveResult(${index})">
1747
+ <i class="fas fa-save"></i>
1748
+ ذخیره
1749
+ </button>
1750
+ </div>
1751
+ </div>
1752
+ `;
1753
+ });
1754
+
1755
+ resultsGrid.innerHTML = html;
1756
+ resultsSection.classList.add('show');
1757
+ }
1758
+
1759
+ // بروزرسانی وضعیت اسکراپینگ
1760
+ function updateScrapingStatus(type, title, message, progress) {
1761
+ const statusIcon = document.getElementById('statusIcon');
1762
+ const statusTitle = document.getElementById('statusTitle');
1763
+ const statusMessage = document.getElementById('statusMessage');
1764
+ const progressBar = document.getElementById('statusProgressBar');
1765
+
1766
+ statusIcon.className = `status-icon status-${type}`;
1767
+ statusTitle.textContent = title;
1768
+ statusMessage.textContent = message;
1769
+ progressBar.style.width = `${progress}%`;
1770
+
1771
+ const icons = {
1772
+ loading: 'spinner fa-spin',
1773
+ success: 'check-circle',
1774
+ error: 'exclamation-triangle'
1775
+ };
1776
+
1777
+ statusIcon.innerHTML = `<i class="fas fa-${icons[type]}"></i>`;
1778
+ }
1779
+
1780
+ // توقف اسکراپینگ
1781
+ function stopScraping() {
1782
+ if (!scrapingActive) return;
1783
+
1784
+ scrapingActive = false;
1785
+
1786
+ updateScrapingStatus('error', 'اسکراپینگ متوقف شد', 'فرآیند توسط کاربر متوقف شد', scrapingProgress);
1787
+
1788
+ setTimeout(() => {
1789
+ document.getElementById('activityStatus').classList.remove('show');
1790
+ resetScrapingState();
1791
+ }, 2000);
1792
+
1793
+ showToast('اسکراپینگ متوقف شد', 'warning', 'توقف');
1794
+ }
1795
+
1796
+ // بازنشانی وضعیت اسکراپینگ
1797
+ function resetScrapingState() {
1798
+ scrapingActive = false;
1799
+ scrapingProgress = 0;
1800
+
1801
+ document.getElementById('autoScrapingBtn').disabled = false;
1802
+ document.getElementById('selectedScrapingBtn').disabled = false;
1803
+ document.getElementById('stopScrapingBtn').disabled = true;
1804
+ }
1805
+
1806
+ // بررسی وضعیت مراجع
1807
+ function checkSourcesStatus() {
1808
+ showToast('در حال بررسی وضعیت مراجع...', 'info', 'بررسی');
1809
+
1810
+ // شبیه‌سازی بررسی
1811
+ setTimeout(() => {
1812
+ const onlineCount = legalSources.filter(s => s.status === 'online').length;
1813
+ showToast(`${onlineCount} از ${legalSources.length} مرجع آنلاین هستند`, 'success', 'وضعیت مراجع');
1814
+ }, 1500);
1815
+ }
1816
+
1817
+ // بروزرسانی داده‌ها
1818
+ function refreshData() {
1819
+ showToast('در حال بروزرسانی...', 'info', 'بروزرسانی');
1820
+
1821
+ setTimeout(() => {
1822
+ updateStatistics();
1823
+ showToast('داده‌ها بروزرسانی شد', 'success', 'بروزرسانی');
1824
+ }, 1000);
1825
+ }
1826
+
1827
+ // صادرات نتایج
1828
+ function exportResults() {
1829
+ if (scrapingResults.length === 0) {
1830
+ showToast('نتیجه‌ای برای صادرات وجود ندارد', 'warning', 'هشدار');
1831
+ return;
1832
+ }
1833
+
1834
+ const dataStr = JSON.stringify(scrapingResults, null, 2);
1835
+ const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
1836
+
1837
+ const exportFileDefaultName = `legal-scraping-results-${new Date().toISOString().split('T')[0]}.json`;
1838
+
1839
+ const linkElement = document.createElement('a');
1840
+ linkElement.setAttribute('href', dataUri);
1841
+ linkElement.setAttribute('download', exportFileDefaultName);
1842
+ linkElement.click();
1843
+
1844
+ showToast('فایل JSON آماده دانلود است', 'success', 'صادرات');
1845
+ }
1846
+
1847
+ // ذخیره در پایگاه داده
1848
+ function saveToDatabase() {
1849
+ if (scrapingResults.length === 0) {
1850
+ showToast('نتیجه‌ای برای ذخیره وجود ندارد', 'warning', 'هشدار');
1851
+ return;
1852
+ }
1853
+
1854
+ // شبیه‌سازی ذخیره
1855
+ showToast('در حال ذخیره در پایگاه داده...', 'info', 'ذخیره');
1856
+
1857
+ setTimeout(() => {
1858
+ showToast(`${scrapingResults.length} نتیجه در پایگاه داده ذخیره شد`, 'success', 'ذخیره موفق');
1859
+ }, 2000);
1860
+ }
1861
+
1862
+ // مشاهده نتیجه کامل
1863
+ function viewFullResult(index) {
1864
+ const result = scrapingResults[index];
1865
+ if (!result) return;
1866
+
1867
+ showToast(`مشاهده نتیجه کامل: ${result.source}`, 'info', 'نمایش');
1868
+ }
1869
+
1870
+ // ذخیره نتیجه منفرد
1871
+ function saveResult(index) {
1872
+ const result = scrapingResults[index];
1873
+ if (!result) return;
1874
+
1875
+ showToast(`نتیجه ${result.source} ذخیره شد`, 'success', 'ذخیره');
1876
+ }
1877
+
1878
+ // نمایش Toast
1879
+ function showToast(message, type = 'info', title = 'اعلان') {
1880
+ const toastContainer = document.getElementById('toastContainer');
1881
+ if (!toastContainer) return;
1882
+
1883
+ const toast = document.createElement('div');
1884
+ toast.className = `toast ${type}`;
1885
+
1886
+ const icons = {
1887
+ success: 'check-circle',
1888
+ error: 'exclamation-triangle',
1889
+ warning: 'exclamation-circle',
1890
+ info: 'info-circle'
1891
+ };
1892
+
1893
+ toast.innerHTML = `
1894
+ <div class="toast-icon">
1895
+ <i class="fas fa-${icons[type]}"></i>
1896
+ </div>
1897
+ <div class="toast-content">
1898
+ <div class="toast-title">${title}</div>
1899
+ <div class="toast-message">${message}</div>
1900
+ </div>
1901
+ <button type="button" class="toast-close" onclick="this.parentElement.remove()">
1902
+ <i class="fas fa-times"></i>
1903
+ </button>
1904
+ `;
1905
+
1906
+ toastContainer.appendChild(toast);
1907
+
1908
+ setTimeout(() => toast.classList.add('show'), 100);
1909
+
1910
+ setTimeout(() => {
1911
+ if (toast.parentElement) {
1912
+ toast.classList.remove('show');
1913
+ setTimeout(() => {
1914
+ if (toast.parentElement) {
1915
+ toast.remove();
1916
+ }
1917
+ }, 300);
1918
+ }
1919
+ }, 5000);
1920
+ }
1921
+
1922
+ console.log('🕷️ Legal Scraping Dashboard Ready!');
1923
+ </script>
1924
+ </body>
1925
+ </html>
app/frontend/scraping_dashboard.html ADDED
@@ -0,0 +1,863 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Legal Dashboard - Scraping &amp; Rating System</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.css" rel="stylesheet">
10
+ <!-- Load API Client and Core System -->
11
+ <script src="js/api-client.js"></script>
12
+ <script src="js/core.js"></script>
13
+ <script src="js/notifications.js"></script>
14
+ <script src="js/scraping-control.js"></script>
15
+ <style>
16
+ :root {
17
+ --primary-color: #2c3e50;
18
+ --secondary-color: #3498db;
19
+ --success-color: #27ae60;
20
+ --warning-color: #f39c12;
21
+ --danger-color: #e74c3c;
22
+ --light-bg: #f8f9fa;
23
+ --dark-bg: #343a40;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
28
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
29
+ min-height: 100vh;
30
+ }
31
+
32
+ .navbar-custom {
33
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
34
+ }
35
+
36
+ .navbar-brand {
37
+ font-weight: bold;
38
+ font-size: 1.5rem;
39
+ }
40
+
41
+ .card {
42
+ border: none;
43
+ border-radius: 15px;
44
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
45
+ transition: transform 0.3s ease;
46
+ }
47
+
48
+ .card:hover {
49
+ transform: translateY(-5px);
50
+ }
51
+
52
+ .card-header {
53
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
54
+ color: white;
55
+ border-radius: 15px 15px 0 0 !important;
56
+ font-weight: bold;
57
+ }
58
+
59
+ .btn-primary {
60
+ background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
61
+ border: none;
62
+ border-radius: 25px;
63
+ padding: 10px 25px;
64
+ font-weight: bold;
65
+ }
66
+
67
+ .btn-success {
68
+ background: linear-gradient(135deg, var(--success-color), #2ecc71);
69
+ border: none;
70
+ border-radius: 25px;
71
+ }
72
+
73
+ .btn-warning {
74
+ background: linear-gradient(135deg, var(--warning-color), #f1c40f);
75
+ border: none;
76
+ border-radius: 25px;
77
+ }
78
+
79
+ .btn-danger {
80
+ background: linear-gradient(135deg, var(--danger-color), #c0392b);
81
+ border: none;
82
+ border-radius: 25px;
83
+ }
84
+
85
+ .progress {
86
+ height: 25px;
87
+ border-radius: 15px;
88
+ background-color: #e9ecef;
89
+ }
90
+
91
+ .progress-bar {
92
+ border-radius: 15px;
93
+ font-weight: bold;
94
+ }
95
+
96
+ .stats-card {
97
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
98
+ color: white;
99
+ border-radius: 15px;
100
+ padding: 20px;
101
+ margin-bottom: 20px;
102
+ }
103
+
104
+ .stats-number {
105
+ font-size: 2.5rem;
106
+ font-weight: bold;
107
+ }
108
+
109
+ .stats-label {
110
+ font-size: 0.9rem;
111
+ opacity: 0.8;
112
+ }
113
+
114
+ .table {
115
+ border-radius: 10px;
116
+ overflow: hidden;
117
+ }
118
+
119
+ .table thead th {
120
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
121
+ color: white;
122
+ border: none;
123
+ font-weight: bold;
124
+ }
125
+
126
+ .badge {
127
+ border-radius: 20px;
128
+ padding: 8px 15px;
129
+ font-weight: bold;
130
+ }
131
+
132
+ .alert {
133
+ border-radius: 15px;
134
+ border: none;
135
+ }
136
+
137
+ .form-control, .form-select {
138
+ border-radius: 10px;
139
+ border: 2px solid #e9ecef;
140
+ transition: border-color 0.3s ease;
141
+ }
142
+
143
+ .form-control:focus, .form-select:focus {
144
+ border-color: var(--secondary-color);
145
+ box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
146
+ }
147
+
148
+ .modal-content {
149
+ border-radius: 15px;
150
+ border: none;
151
+ }
152
+
153
+ .modal-header {
154
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
155
+ color: white;
156
+ border-radius: 15px 15px 0 0;
157
+ }
158
+
159
+ .loading-spinner {
160
+ display: inline-block;
161
+ width: 20px;
162
+ height: 20px;
163
+ border: 3px solid #f3f3f3;
164
+ border-top: 3px solid var(--secondary-color);
165
+ border-radius: 50%;
166
+ animation: spin 1s linear infinite;
167
+ }
168
+
169
+ @keyframes spin {
170
+ 0% { transform: rotate(0deg); }
171
+ 100% { transform: rotate(360deg); }
172
+ }
173
+
174
+ .notification {
175
+ position: fixed;
176
+ top: 20px;
177
+ right: 20px;
178
+ z-index: 9999;
179
+ border-radius: 10px;
180
+ padding: 15px 20px;
181
+ color: white;
182
+ font-weight: bold;
183
+ transform: translateX(400px);
184
+ transition: transform 0.3s ease;
185
+ }
186
+
187
+ .notification.show {
188
+ transform: translateX(0);
189
+ }
190
+
191
+ .notification.success {
192
+ background: linear-gradient(135deg, var(--success-color), #2ecc71);
193
+ }
194
+
195
+ .notification.warning {
196
+ background: linear-gradient(135deg, var(--warning-color), #f1c40f);
197
+ }
198
+
199
+ .notification.error {
200
+ background: linear-gradient(135deg, var(--danger-color), #c0392b);
201
+ }
202
+
203
+ .chart-container {
204
+ position: relative;
205
+ height: 300px;
206
+ margin: 20px 0;
207
+ }
208
+
209
+ .job-status {
210
+ padding: 10px 15px;
211
+ border-radius: 20px;
212
+ font-weight: bold;
213
+ font-size: 0.9rem;
214
+ }
215
+
216
+ .status-pending { background-color: #f8f9fa; color: #6c757d; }
217
+ .status-processing { background-color: #fff3cd; color: #856404; }
218
+ .status-completed { background-color: #d1ecf1; color: #0c5460; }
219
+ .status-failed { background-color: #f8d7da; color: #721c24; }
220
+
221
+ .rating-badge {
222
+ padding: 5px 10px;
223
+ border-radius: 15px;
224
+ font-weight: bold;
225
+ font-size: 0.8rem;
226
+ }
227
+
228
+ .rating-excellent { background-color: #d4edda; color: #155724; }
229
+ .rating-good { background-color: #d1ecf1; color: #0c5460; }
230
+ .rating-average { background-color: #fff3cd; color: #856404; }
231
+ .rating-poor { background-color: #f8d7da; color: #721c24; }
232
+ .rating-unrated { background-color: #e2e3e5; color: #383d41; }
233
+ </style>
234
+ </head>
235
+ <body>
236
+ <!-- Navigation -->
237
+ <nav class="navbar navbar-expand-lg navbar-dark navbar-custom">
238
+ <div class="container-fluid">
239
+ <a class="navbar-brand" href="#">
240
+ <i class="fas fa-spider me-2"></i>
241
+ Legal Dashboard - Scraping &amp; Rating
242
+ </a>
243
+ <div class="navbar-nav ms-auto">
244
+ <a class="nav-link" href="#" onclick="showNotification('System is running smoothly', 'success')">
245
+ <i class="fas fa-heartbeat me-1"></i>
246
+ System Health
247
+ </a>
248
+ </div>
249
+ </div>
250
+ </nav>
251
+
252
+ <div class="container-fluid mt-4">
253
+ <!-- Statistics Cards -->
254
+ <div class="row mb-4">
255
+ <div class="col-md-3">
256
+ <div class="stats-card text-center">
257
+ <div class="stats-number" id="totalItems">0</div>
258
+ <div class="stats-label">Total Items Scraped</div>
259
+ </div>
260
+ </div>
261
+ <div class="col-md-3">
262
+ <div class="stats-card text-center">
263
+ <div class="stats-number" id="activeJobs">0</div>
264
+ <div class="stats-label">Active Jobs</div>
265
+ </div>
266
+ </div>
267
+ <div class="col-md-3">
268
+ <div class="stats-card text-center">
269
+ <div class="stats-number" id="avgRating">0.0</div>
270
+ <div class="stats-label">Average Rating</div>
271
+ </div>
272
+ </div>
273
+ <div class="col-md-3">
274
+ <div class="stats-card text-center">
275
+ <div class="stats-number" id="totalRated">0</div>
276
+ <div class="stats-label">Items Rated</div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="row">
282
+ <!-- Scraping Control Panel -->
283
+ <div class="col-lg-4">
284
+ <div class="card mb-4">
285
+ <div class="card-header">
286
+ <i class="fas fa-spider me-2"></i>
287
+ Scraping Control Panel
288
+ </div>
289
+ <div class="card-body">
290
+ <form id="scrapingForm">
291
+ <div class="mb-3">
292
+ <label for="urls" class="form-label">URLs to Scrape</label>
293
+ <textarea class="form-control" id="urls" rows="4" placeholder="Enter URLs (one per line)&#10;Example:&#10;https://example.com/page1&#10;https://example.com/page2"></textarea>
294
+ </div>
295
+ <div class="mb-3">
296
+ <label for="strategy" class="form-label">Scraping Strategy</label>
297
+ <select class="form-select" id="strategy">
298
+ <option value="general">General</option>
299
+ <option value="legal_documents">Legal Documents</option>
300
+ <option value="news_articles">News Articles</option>
301
+ <option value="academic_papers">Academic Papers</option>
302
+ <option value="government_sites">Government Sites</option>
303
+ <option value="custom">Custom</option>
304
+ </select>
305
+ </div>
306
+ <div class="mb-3">
307
+ <label for="keywords" class="form-label">Keywords (optional)</label>
308
+ <input type="text" class="form-control" id="keywords" placeholder="Enter keywords separated by commas">
309
+ </div>
310
+ <div class="row">
311
+ <div class="col-md-6">
312
+ <div class="mb-3">
313
+ <label for="maxDepth" class="form-label">Max Depth</label>
314
+ <input type="number" class="form-control" id="maxDepth" value="1" min="1" max="5">
315
+ </div>
316
+ </div>
317
+ <div class="col-md-6">
318
+ <div class="mb-3">
319
+ <label for="delay" class="form-label">Delay (seconds)</label>
320
+ <input type="number" class="form-control" id="delay" value="1.0" min="0.1" max="10.0" step="0.1">
321
+ </div>
322
+ </div>
323
+ </div>
324
+ <button type="submit" class="btn btn-primary w-100" id="startScrapingBtn">
325
+ <i class="fas fa-play me-2"></i>
326
+ Start Scraping Job
327
+ </button>
328
+ </form>
329
+ </div>
330
+ </div>
331
+
332
+ <!-- Rating Controls -->
333
+ <div class="card mb-4">
334
+ <div class="card-header">
335
+ <i class="fas fa-star me-2"></i>
336
+ Rating Controls
337
+ </div>
338
+ <div class="card-body">
339
+ <button type="button" class="btn btn-success w-100 mb-2" onclick="rateAllItems()">
340
+ <i class="fas fa-star me-2"></i>
341
+ Rate All Unrated Items
342
+ </button>
343
+ <button type="button" class="btn btn-warning w-100 mb-2" onclick="getLowQualityItems()">
344
+ <i class="fas fa-exclamation-triangle me-2"></i>
345
+ Get Low Quality Items
346
+ </button>
347
+ <button type="button" class="btn btn-info w-100" onclick="refreshStatistics()">
348
+ <i class="fas fa-sync-alt me-2"></i>
349
+ Refresh Statistics
350
+ </button>
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ <!-- Active Jobs -->
356
+ <div class="col-lg-8">
357
+ <div class="card mb-4">
358
+ <div class="card-header d-flex justify-content-between align-items-center">
359
+ <span><i class="fas fa-tasks me-2"></i>Active Scraping Jobs</span>
360
+ <button type="button" class="btn btn-sm btn-outline-light" onclick="refreshJobs()" aria-label="Refresh jobs">
361
+ <i class="fas fa-sync-alt"></i>
362
+ </button>
363
+ </div>
364
+ <div class="card-body">
365
+ <div id="jobsContainer">
366
+ <div class="text-center text-muted">
367
+ <i class="fas fa-spinner fa-spin fa-2x mb-2"></i>
368
+ <p>Loading jobs...</p>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+
374
+ <!-- Charts -->
375
+ <div class="row">
376
+ <div class="col-md-6">
377
+ <div class="card">
378
+ <div class="card-header">
379
+ <i class="fas fa-chart-pie me-2"></i>
380
+ Rating Distribution
381
+ </div>
382
+ <div class="card-body">
383
+ <div class="chart-container">
384
+ <canvas id="ratingChart"></canvas>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ </div>
389
+ <div class="col-md-6">
390
+ <div class="card">
391
+ <div class="card-header">
392
+ <i class="fas fa-chart-bar me-2"></i>
393
+ Language Distribution
394
+ </div>
395
+ <div class="card-body">
396
+ <div class="chart-container">
397
+ <canvas id="languageChart"></canvas>
398
+ </div>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ </div>
403
+ </div>
404
+ </div>
405
+
406
+ <!-- Scraped Items Table -->
407
+ <div class="row mt-4">
408
+ <div class="col-12">
409
+ <div class="card">
410
+ <div class="card-header d-flex justify-content-between align-items-center">
411
+ <span><i class="fas fa-list me-2"></i>Scraped Items</span>
412
+ <div>
413
+ <button type="button" class="btn btn-sm btn-outline-light me-2" onclick="refreshItems()" aria-label="Refresh items">
414
+ <i class="fas fa-sync-alt"></i>
415
+ </button>
416
+ <select class="form-select form-select-sm d-inline-block w-auto" id="itemFilter">
417
+ <option value="">All Items</option>
418
+ <option value="completed">Completed</option>
419
+ <option value="failed">Failed</option>
420
+ <option value="rated">Rated</option>
421
+ </select>
422
+ </div>
423
+ </div>
424
+ <div class="card-body">
425
+ <div class="table-responsive">
426
+ <table class="table table-hover">
427
+ <thead>
428
+ <tr>
429
+ <th scope="col">Title</th>
430
+ <th scope="col">URL</th>
431
+ <th scope="col">Status</th>
432
+ <th scope="col">Rating</th>
433
+ <th scope="col">Language</th>
434
+ <th scope="col">Word Count</th>
435
+ <th scope="col">Actions</th>
436
+ </tr>
437
+ </thead>
438
+ <tbody id="itemsTableBody">
439
+ <tr>
440
+ <td colspan="7" class="text-center text-muted">
441
+ <i class="fas fa-spinner fa-spin me-2"></i>
442
+ Loading items...
443
+ </td>
444
+ </tr>
445
+ </tbody>
446
+ </table>
447
+ </div>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ </div>
453
+
454
+ <!-- Notification Container -->
455
+ <div id="notificationContainer"></div>
456
+
457
+ <!-- Scripts -->
458
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
459
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
460
+ <script>
461
+ // Global variables
462
+ let ratingChart = null;
463
+ let languageChart = null;
464
+ let refreshInterval = null;
465
+
466
+ // Initialize dashboard
467
+ document.addEventListener('DOMContentLoaded', function() {
468
+ loadDashboard();
469
+ startAutoRefresh();
470
+ });
471
+
472
+ // Load dashboard data
473
+ async function loadDashboard() {
474
+ try {
475
+ await Promise.all([
476
+ loadStatistics(),
477
+ loadJobs(),
478
+ loadItems(),
479
+ loadCharts()
480
+ ]);
481
+ } catch (error) {
482
+ console.error('Error loading dashboard:', error);
483
+ showNotification('Error loading dashboard data', 'error');
484
+ }
485
+ }
486
+
487
+ // Load statistics
488
+ async function loadStatistics() {
489
+ try {
490
+ const [scrapingStats, ratingSummary] = await Promise.all([
491
+ fetch('/api/scrape/statistics').then(r => r.json()),
492
+ fetch('/api/rating/summary').then(r => r.json())
493
+ ]);
494
+
495
+ document.getElementById('totalItems').textContent = scrapingStats.total_items || 0;
496
+ document.getElementById('activeJobs').textContent = scrapingStats.active_jobs || 0;
497
+ document.getElementById('avgRating').textContent = (ratingSummary.average_score || 0).toFixed(2);
498
+ document.getElementById('totalRated').textContent = ratingSummary.total_rated || 0;
499
+ } catch (error) {
500
+ console.error('Error loading statistics:', error);
501
+ }
502
+ }
503
+
504
+ // Load jobs
505
+ async function loadJobs() {
506
+ try {
507
+ const response = await fetch('/api/scrape/status');
508
+ const jobs = await response.json();
509
+
510
+ const container = document.getElementById('jobsContainer');
511
+
512
+ if (jobs.length === 0) {
513
+ container.innerHTML = '<div class="text-center text-muted"><p>No active jobs</p></div>';
514
+ return;
515
+ }
516
+
517
+ let html = '';
518
+ jobs.forEach(job => {
519
+ const progress = job.progress * 100;
520
+ html += `
521
+ <div class="card mb-3">
522
+ <div class="card-body">
523
+ <div class="d-flex justify-content-between align-items-center mb-2">
524
+ <h6 class="card-title mb-0">Job ${job.job_id}</h6>
525
+ <span class="job-status status-${job.status}">${job.status}</span>
526
+ </div>
527
+ <div class="progress mb-2">
528
+ <div class="progress-bar" role="progressbar" style="width: ${progress}%"
529
+ aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100">
530
+ ${progress.toFixed(1)}%
531
+ </div>
532
+ </div>
533
+ <div class="row text-center">
534
+ <div class="col-4">
535
+ <small class="text-muted">Total</small><br>
536
+ <strong>${job.total_items}</strong>
537
+ </div>
538
+ <div class="col-4">
539
+ <small class="text-muted">Completed</small><br>
540
+ <strong class="text-success">${job.completed_items}</strong>
541
+ </div>
542
+ <div class="col-4">
543
+ <small class="text-muted">Failed</small><br>
544
+ <strong class="text-danger">${job.failed_items}</strong>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ </div>
549
+ `;
550
+ });
551
+
552
+ container.innerHTML = html;
553
+ } catch (error) {
554
+ console.error('Error loading jobs:', error);
555
+ document.getElementById('jobsContainer').innerHTML =
556
+ '<div class="alert alert-danger">Error loading jobs</div>';
557
+ }
558
+ }
559
+
560
+ // Load items
561
+ async function loadItems() {
562
+ try {
563
+ const response = await fetch('/api/scrape/items?limit=50');
564
+ const items = await response.json();
565
+
566
+ const tbody = document.getElementById('itemsTableBody');
567
+ let html = '';
568
+
569
+ items.forEach(item => {
570
+ const ratingClass = getRatingClass(item.rating_score);
571
+ const ratingText = item.rating_score > 0 ? item.rating_score.toFixed(2) : 'Unrated';
572
+
573
+ html += `
574
+ <tr>
575
+ <td>
576
+ <strong>${item.title || 'No Title'}</strong>
577
+ <br><small class="text-muted">${item.domain}</small>
578
+ </td>
579
+ <td>
580
+ <a href="${item.url}" target="_blank" class="text-decoration-none">
581
+ ${item.url.substring(0, 50)}...
582
+ </a>
583
+ </td>
584
+ <td>
585
+ <span class="job-status status-${item.processing_status}">
586
+ ${item.processing_status}
587
+ </span>
588
+ </td>
589
+ <td>
590
+ <span class="rating-badge ${ratingClass}">
591
+ ${ratingText}
592
+ </span>
593
+ </td>
594
+ <td>
595
+ <span class="badge bg-info">${item.language}</span>
596
+ </td>
597
+ <td>
598
+ <span class="badge bg-secondary">${item.word_count}</span>
599
+ </td>
600
+ <td>
601
+ <button class="btn btn-sm btn-outline-primary me-1" onclick="viewItem('${item.id}')">
602
+ <i class="fas fa-eye"></i>
603
+ </button>
604
+ <button class="btn btn-sm btn-outline-success" onclick="rateItem('${item.id}')">
605
+ <i class="fas fa-star"></i>
606
+ </button>
607
+ </td>
608
+ </tr>
609
+ `;
610
+ });
611
+
612
+ tbody.innerHTML = html;
613
+ } catch (error) {
614
+ console.error('Error loading items:', error);
615
+ document.getElementById('itemsTableBody').innerHTML =
616
+ '<tr><td colspan="7" class="text-center text-danger">Error loading items</td></tr>';
617
+ }
618
+ }
619
+
620
+ // Load charts
621
+ async function loadCharts() {
622
+ try {
623
+ const [scrapingStats, ratingSummary] = await Promise.all([
624
+ fetch('/api/scrape/statistics').then(r => r.json()),
625
+ fetch('/api/rating/summary').then(r => r.json())
626
+ ]);
627
+
628
+ // Rating distribution chart
629
+ const ratingCtx = document.getElementById('ratingChart').getContext('2d');
630
+ if (ratingChart) ratingChart.destroy();
631
+
632
+ ratingChart = new Chart(ratingCtx, {
633
+ type: 'doughnut',
634
+ data: {
635
+ labels: Object.keys(ratingSummary.rating_level_distribution || {}),
636
+ datasets: [{
637
+ data: Object.values(ratingSummary.rating_level_distribution || {}),
638
+ backgroundColor: ['#28a745', '#17a2b8', '#ffc107', '#dc3545', '#6c757d']
639
+ }]
640
+ },
641
+ options: {
642
+ responsive: true,
643
+ maintainAspectRatio: false,
644
+ plugins: {
645
+ legend: {
646
+ position: 'bottom'
647
+ }
648
+ }
649
+ }
650
+ });
651
+
652
+ // Language distribution chart
653
+ const languageCtx = document.getElementById('languageChart').getContext('2d');
654
+ if (languageChart) languageChart.destroy();
655
+
656
+ languageChart = new Chart(languageCtx, {
657
+ type: 'bar',
658
+ data: {
659
+ labels: Object.keys(scrapingStats.language_distribution || {}),
660
+ datasets: [{
661
+ label: 'Items by Language',
662
+ data: Object.values(scrapingStats.language_distribution || {}),
663
+ backgroundColor: '#3498db'
664
+ }]
665
+ },
666
+ options: {
667
+ responsive: true,
668
+ maintainAspectRatio: false,
669
+ plugins: {
670
+ legend: {
671
+ display: false
672
+ }
673
+ }
674
+ }
675
+ });
676
+ } catch (error) {
677
+ console.error('Error loading charts:', error);
678
+ }
679
+ }
680
+
681
+ // Form submission
682
+ document.getElementById('scrapingForm').addEventListener('submit', async function(e) {
683
+ e.preventDefault();
684
+
685
+ const urls = document.getElementById('urls').value.split('\n').filter(url => url.trim());
686
+ const strategy = document.getElementById('strategy').value;
687
+ const keywords = document.getElementById('keywords').value.split(',').filter(k => k.trim());
688
+ const maxDepth = parseInt(document.getElementById('maxDepth').value);
689
+ const delay = parseFloat(document.getElementById('delay').value);
690
+
691
+ if (urls.length === 0) {
692
+ showNotification('Please enter at least one URL', 'warning');
693
+ return;
694
+ }
695
+
696
+ const startBtn = document.getElementById('startScrapingBtn');
697
+ const originalText = startBtn.innerHTML;
698
+ startBtn.innerHTML = '<span class="loading-spinner me-2"></span>Starting...';
699
+ startBtn.disabled = true;
700
+
701
+ try {
702
+ const response = await fetch('/api/scrape', {
703
+ method: 'POST',
704
+ headers: {
705
+ 'Content-Type': 'application/json'
706
+ },
707
+ body: JSON.stringify({
708
+ urls: urls,
709
+ strategy: strategy,
710
+ keywords: keywords.length > 0 ? keywords : null,
711
+ max_depth: maxDepth,
712
+ delay_between_requests: delay
713
+ })
714
+ });
715
+
716
+ const result = await response.json();
717
+
718
+ if (response.ok) {
719
+ showNotification('Scraping job started successfully', 'success');
720
+ document.getElementById('scrapingForm').reset();
721
+ loadJobs();
722
+ } else {
723
+ showNotification(result.detail || 'Failed to start scraping job', 'error');
724
+ }
725
+ } catch (error) {
726
+ console.error('Error starting scraping job:', error);
727
+ showNotification('Error starting scraping job', 'error');
728
+ } finally {
729
+ startBtn.innerHTML = originalText;
730
+ startBtn.disabled = false;
731
+ }
732
+ });
733
+
734
+ // Rate all items
735
+ async function rateAllItems() {
736
+ try {
737
+ const response = await fetch('/api/rating/rate-all', {
738
+ method: 'POST'
739
+ });
740
+ const result = await response.json();
741
+
742
+ if (response.ok) {
743
+ showNotification(`Rated ${result.rated_count} items successfully`, 'success');
744
+ loadStatistics();
745
+ loadItems();
746
+ } else {
747
+ showNotification(result.detail || 'Failed to rate items', 'error');
748
+ }
749
+ } catch (error) {
750
+ console.error('Error rating items:', error);
751
+ showNotification('Error rating items', 'error');
752
+ }
753
+ }
754
+
755
+ // Get low quality items
756
+ async function getLowQualityItems() {
757
+ try {
758
+ const response = await fetch('/api/rating/low-quality');
759
+ const result = await response.json();
760
+
761
+ if (response.ok) {
762
+ showNotification(`Found ${result.total_items} low quality items`, 'warning');
763
+ // You could display these in a modal or separate section
764
+ } else {
765
+ showNotification(result.detail || 'Failed to get low quality items', 'error');
766
+ }
767
+ } catch (error) {
768
+ console.error('Error getting low quality items:', error);
769
+ showNotification('Error getting low quality items', 'error');
770
+ }
771
+ }
772
+
773
+ // Rate specific item
774
+ async function rateItem(itemId) {
775
+ try {
776
+ const response = await fetch(`/api/rating/rate/${itemId}`, {
777
+ method: 'POST'
778
+ });
779
+ const result = await response.json();
780
+
781
+ if (response.ok) {
782
+ showNotification(`Item ${itemId} rated successfully`, 'success');
783
+ loadItems();
784
+ } else {
785
+ showNotification(result.detail || 'Failed to rate item', 'error');
786
+ }
787
+ } catch (error) {
788
+ console.error('Error rating item:', error);
789
+ showNotification('Error rating item', 'error');
790
+ }
791
+ }
792
+
793
+ // View item details
794
+ function viewItem(itemId) {
795
+ // This could open a modal with item details
796
+ showNotification(`Viewing item ${itemId}`, 'info');
797
+ }
798
+
799
+ // Refresh functions
800
+ function refreshStatistics() {
801
+ loadStatistics();
802
+ showNotification('Statistics refreshed', 'success');
803
+ }
804
+
805
+ function refreshJobs() {
806
+ loadJobs();
807
+ showNotification('Jobs refreshed', 'success');
808
+ }
809
+
810
+ function refreshItems() {
811
+ loadItems();
812
+ showNotification('Items refreshed', 'success');
813
+ }
814
+
815
+ // Auto refresh
816
+ function startAutoRefresh() {
817
+ refreshInterval = setInterval(() => {
818
+ loadStatistics();
819
+ loadJobs();
820
+ }, 10000); // Refresh every 10 seconds
821
+ }
822
+
823
+ // Utility functions
824
+ function getRatingClass(score) {
825
+ if (score >= 0.8) return 'rating-excellent';
826
+ if (score >= 0.6) return 'rating-good';
827
+ if (score >= 0.4) return 'rating-average';
828
+ if (score >= 0.2) return 'rating-poor';
829
+ return 'rating-unrated';
830
+ }
831
+
832
+ function showNotification(message, type = 'info') {
833
+ const container = document.getElementById('notificationContainer');
834
+ const notification = document.createElement('div');
835
+ notification.className = `notification ${type}`;
836
+ notification.innerHTML = `
837
+ <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'warning' ? 'exclamation-triangle' : type === 'error' ? 'times-circle' : 'info-circle'} me-2"></i>
838
+ ${message}
839
+ `;
840
+
841
+ container.appendChild(notification);
842
+
843
+ setTimeout(() => {
844
+ notification.classList.add('show');
845
+ }, 100);
846
+
847
+ setTimeout(() => {
848
+ notification.classList.remove('show');
849
+ setTimeout(() => {
850
+ container.removeChild(notification);
851
+ }, 300);
852
+ }, 5000);
853
+ }
854
+
855
+ // Cleanup on page unload
856
+ window.addEventListener('beforeunload', function() {
857
+ if (refreshInterval) {
858
+ clearInterval(refreshInterval);
859
+ }
860
+ });
861
+ </script>
862
+ </body>
863
+ </html>
app/frontend/search.html ADDED
@@ -0,0 +1,1584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>جستجو | سامانه حقوقی</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+
12
+ <!-- Load API Client -->
13
+ <script src="/static/js/api-client.js"></script>
14
+ <script src="/static/js/core.js"></script>
15
+
16
+ <style>
17
+ :root {
18
+ --text-primary: #0f172a;
19
+ --text-secondary: #475569;
20
+ --text-muted: #64748b;
21
+ --text-light: #ffffff;
22
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
23
+ --card-bg: rgba(255, 255, 255, 0.95);
24
+ --glass-bg: rgba(255, 255, 255, 0.9);
25
+ --glass-border: rgba(148, 163, 184, 0.2);
26
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
27
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
28
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
29
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
30
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
31
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
32
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
33
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
35
+ --sidebar-width: 260px;
36
+ --border-radius: 12px;
37
+ --border-radius-sm: 8px;
38
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
39
+ --transition-fast: all 0.15s ease-in-out;
40
+ --font-size-xs: 0.7rem;
41
+ --font-size-sm: 0.8rem;
42
+ --font-size-base: 0.9rem;
43
+ --font-size-lg: 1.1rem;
44
+ --font-size-xl: 1.25rem;
45
+ --font-size-2xl: 1.5rem;
46
+ }
47
+
48
+ * {
49
+ margin: 0;
50
+ padding: 0;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ body {
55
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
56
+ background: var(--body-bg);
57
+ color: var(--text-primary);
58
+ line-height: 1.6;
59
+ overflow-x: hidden;
60
+ font-size: var(--font-size-base);
61
+ }
62
+
63
+ ::-webkit-scrollbar {
64
+ width: 6px;
65
+ height: 6px;
66
+ }
67
+
68
+ ::-webkit-scrollbar-track {
69
+ background: rgba(0, 0, 0, 0.02);
70
+ border-radius: 10px;
71
+ }
72
+
73
+ ::-webkit-scrollbar-thumb {
74
+ background: var(--primary-gradient);
75
+ border-radius: 10px;
76
+ }
77
+
78
+ .dashboard-container {
79
+ display: flex;
80
+ min-height: 100vh;
81
+ width: 100%;
82
+ }
83
+
84
+ /* سایدبار مشابه صفحات قبلی */
85
+ .sidebar {
86
+ width: var(--sidebar-width);
87
+ background: linear-gradient(135deg,
88
+ rgba(248, 250, 252, 0.98) 0%,
89
+ rgba(241, 245, 249, 0.95) 25%,
90
+ rgba(226, 232, 240, 0.98) 50%,
91
+ rgba(203, 213, 225, 0.95) 75%,
92
+ rgba(148, 163, 184, 0.1) 100%);
93
+ backdrop-filter: blur(25px);
94
+ -webkit-backdrop-filter: blur(25px);
95
+ padding: 1rem 0;
96
+ position: fixed;
97
+ height: 100vh;
98
+ right: 0;
99
+ top: 0;
100
+ z-index: 1000;
101
+ overflow-y: auto;
102
+ box-shadow:
103
+ 0 0 0 1px rgba(59, 130, 246, 0.08),
104
+ -8px 0 32px rgba(59, 130, 246, 0.12),
105
+ inset 0 1px 0 rgba(255, 255, 255, 0.6);
106
+ border-left: 1px solid rgba(59, 130, 246, 0.15);
107
+ }
108
+
109
+ .sidebar-header {
110
+ padding: 0 1rem 1rem;
111
+ border-bottom: 1px solid rgba(59, 130, 246, 0.12);
112
+ margin-bottom: 1rem;
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ background: linear-gradient(135deg,
117
+ rgba(255, 255, 255, 0.4) 0%,
118
+ rgba(248, 250, 252, 0.2) 100%);
119
+ margin: 0 0.5rem 1rem;
120
+ border-radius: var(--border-radius);
121
+ backdrop-filter: blur(10px);
122
+ }
123
+
124
+ .logo {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 0.6rem;
128
+ color: var(--text-primary);
129
+ text-decoration: none;
130
+ }
131
+
132
+ .logo-icon {
133
+ width: 2rem;
134
+ height: 2rem;
135
+ background: var(--primary-gradient);
136
+ border-radius: var(--border-radius-sm);
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ font-size: 1rem;
141
+ color: white;
142
+ }
143
+
144
+ .logo-text {
145
+ font-size: var(--font-size-lg);
146
+ font-weight: 700;
147
+ background: var(--primary-gradient);
148
+ -webkit-background-clip: text;
149
+ -webkit-text-fill-color: transparent;
150
+ }
151
+
152
+ .nav-section {
153
+ margin-bottom: 1rem;
154
+ }
155
+
156
+ .nav-title {
157
+ padding: 0 1rem 0.4rem;
158
+ font-size: var(--font-size-xs);
159
+ font-weight: 600;
160
+ text-transform: uppercase;
161
+ letter-spacing: 0.5px;
162
+ color: var(--text-secondary);
163
+ }
164
+
165
+ .nav-menu {
166
+ list-style: none;
167
+ }
168
+
169
+ .nav-item {
170
+ margin: 0.15rem 0.5rem;
171
+ }
172
+
173
+ .nav-link {
174
+ display: flex;
175
+ align-items: center;
176
+ padding: 0.6rem 0.8rem;
177
+ color: var(--text-primary);
178
+ text-decoration: none;
179
+ border-radius: var(--border-radius-sm);
180
+ transition: var(--transition-smooth);
181
+ font-weight: 500;
182
+ font-size: var(--font-size-sm);
183
+ cursor: pointer;
184
+ border: 1px solid transparent;
185
+ }
186
+
187
+ .nav-link:hover {
188
+ color: var(--text-primary);
189
+ transform: translateX(-2px);
190
+ border-color: rgba(59, 130, 246, 0.15);
191
+ background: rgba(59, 130, 246, 0.05);
192
+ }
193
+
194
+ .nav-link.active {
195
+ background: var(--primary-gradient);
196
+ color: var(--text-light);
197
+ box-shadow: var(--shadow-md);
198
+ }
199
+
200
+ .nav-icon {
201
+ margin-left: 0.6rem;
202
+ width: 1rem;
203
+ text-align: center;
204
+ font-size: 0.9rem;
205
+ }
206
+
207
+ .nav-badge {
208
+ background: var(--danger-gradient);
209
+ color: white;
210
+ padding: 0.15rem 0.4rem;
211
+ border-radius: 10px;
212
+ font-size: var(--font-size-xs);
213
+ font-weight: 600;
214
+ margin-right: auto;
215
+ min-width: 1.2rem;
216
+ text-align: center;
217
+ }
218
+
219
+ /* محتوای اصلی */
220
+ .main-content {
221
+ flex: 1;
222
+ margin-right: var(--sidebar-width);
223
+ padding: 1rem;
224
+ min-height: 100vh;
225
+ width: calc(100% - var(--sidebar-width));
226
+ }
227
+
228
+ .page-header {
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: center;
232
+ margin-bottom: 2rem;
233
+ padding: 1rem 0;
234
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
235
+ }
236
+
237
+ .page-title {
238
+ font-size: var(--font-size-2xl);
239
+ font-weight: 800;
240
+ background: var(--primary-gradient);
241
+ -webkit-background-clip: text;
242
+ -webkit-text-fill-color: transparent;
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 0.6rem;
246
+ }
247
+
248
+ .page-actions {
249
+ display: flex;
250
+ gap: 0.8rem;
251
+ }
252
+
253
+ .btn {
254
+ padding: 0.6rem 1.2rem;
255
+ border: none;
256
+ border-radius: var(--border-radius-sm);
257
+ font-family: inherit;
258
+ font-weight: 600;
259
+ cursor: pointer;
260
+ transition: var(--transition-smooth);
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 0.5rem;
264
+ text-decoration: none;
265
+ font-size: var(--font-size-sm);
266
+ }
267
+
268
+ .btn-primary {
269
+ background: var(--primary-gradient);
270
+ color: white;
271
+ box-shadow: var(--shadow-sm);
272
+ }
273
+
274
+ .btn-primary:hover {
275
+ box-shadow: var(--shadow-md);
276
+ transform: translateY(-1px);
277
+ }
278
+
279
+ .btn-outline {
280
+ background: transparent;
281
+ color: var(--text-primary);
282
+ border: 1px solid rgba(59, 130, 246, 0.2);
283
+ }
284
+
285
+ .btn-outline:hover {
286
+ background: rgba(59, 130, 246, 0.05);
287
+ border-color: rgba(59, 130, 246, 0.4);
288
+ }
289
+
290
+ /* جستجوی اصلی */
291
+ .search-main {
292
+ background: var(--card-bg);
293
+ border-radius: var(--border-radius);
294
+ padding: 2rem;
295
+ margin-bottom: 2rem;
296
+ box-shadow: var(--shadow-md);
297
+ border: 1px solid rgba(255, 255, 255, 0.3);
298
+ }
299
+
300
+ .search-form {
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: 1.5rem;
304
+ }
305
+
306
+ .search-primary {
307
+ position: relative;
308
+ }
309
+
310
+ .search-input {
311
+ width: 100%;
312
+ padding: 1rem 3rem 1rem 1rem;
313
+ border: 2px solid var(--glass-border);
314
+ border-radius: var(--border-radius);
315
+ background: var(--glass-bg);
316
+ color: var(--text-primary);
317
+ font-family: inherit;
318
+ font-size: var(--font-size-lg);
319
+ transition: var(--transition-smooth);
320
+ }
321
+
322
+ .search-input:focus {
323
+ outline: none;
324
+ border-color: #3b82f6;
325
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
326
+ background: var(--card-bg);
327
+ }
328
+
329
+ .search-btn {
330
+ position: absolute;
331
+ left: 0.5rem;
332
+ top: 50%;
333
+ transform: translateY(-50%);
334
+ background: var(--primary-gradient);
335
+ color: white;
336
+ border: none;
337
+ padding: 0.8rem 1.2rem;
338
+ border-radius: var(--border-radius-sm);
339
+ cursor: pointer;
340
+ font-weight: 600;
341
+ transition: var(--transition-smooth);
342
+ }
343
+
344
+ .search-btn:hover {
345
+ transform: translateY(-50%) scale(1.05);
346
+ box-shadow: var(--shadow-lg);
347
+ }
348
+
349
+ /* فیلترهای پیشرفته */
350
+ .search-filters {
351
+ display: grid;
352
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
353
+ gap: 1rem;
354
+ padding: 1.5rem;
355
+ background: rgba(59, 130, 246, 0.03);
356
+ border-radius: var(--border-radius);
357
+ border: 1px solid rgba(59, 130, 246, 0.1);
358
+ }
359
+
360
+ .filter-group {
361
+ display: flex;
362
+ flex-direction: column;
363
+ gap: 0.5rem;
364
+ }
365
+
366
+ .filter-label {
367
+ font-size: var(--font-size-sm);
368
+ font-weight: 600;
369
+ color: var(--text-primary);
370
+ }
371
+
372
+ .filter-select,
373
+ .filter-input {
374
+ padding: 0.6rem 0.8rem;
375
+ border: 1px solid var(--glass-border);
376
+ border-radius: var(--border-radius-sm);
377
+ background: var(--glass-bg);
378
+ color: var(--text-primary);
379
+ font-family: inherit;
380
+ font-size: var(--font-size-sm);
381
+ transition: var(--transition-smooth);
382
+ }
383
+
384
+ .filter-select:focus,
385
+ .filter-input:focus {
386
+ outline: none;
387
+ border-color: #3b82f6;
388
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
389
+ }
390
+
391
+ .search-actions {
392
+ display: flex;
393
+ gap: 1rem;
394
+ justify-content: flex-end;
395
+ }
396
+
397
+ .btn-sm {
398
+ padding: 0.4rem 0.8rem;
399
+ font-size: var(--font-size-xs);
400
+ }
401
+
402
+ /* نتایج جستجو */
403
+ .search-results {
404
+ background: var(--card-bg);
405
+ border-radius: var(--border-radius);
406
+ box-shadow: var(--shadow-md);
407
+ border: 1px solid rgba(255, 255, 255, 0.3);
408
+ overflow: hidden;
409
+ display: none;
410
+ }
411
+
412
+ .search-results.show {
413
+ display: block;
414
+ }
415
+
416
+ .results-header {
417
+ display: flex;
418
+ justify-content: space-between;
419
+ align-items: center;
420
+ padding: 1rem;
421
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
422
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.02), rgba(255, 255, 255, 0.1));
423
+ }
424
+
425
+ .results-title {
426
+ font-size: var(--font-size-lg);
427
+ font-weight: 700;
428
+ color: var(--text-primary);
429
+ }
430
+
431
+ .results-stats {
432
+ display: flex;
433
+ gap: 1rem;
434
+ font-size: var(--font-size-sm);
435
+ color: var(--text-muted);
436
+ }
437
+
438
+ .result-item {
439
+ padding: 1.5rem;
440
+ border-bottom: 1px solid rgba(0, 0, 0, 0.03);
441
+ transition: var(--transition-smooth);
442
+ cursor: pointer;
443
+ }
444
+
445
+ .result-item:hover {
446
+ background: rgba(59, 130, 246, 0.02);
447
+ transform: translateX(-3px);
448
+ }
449
+
450
+ .result-item:last-child {
451
+ border-bottom: none;
452
+ }
453
+
454
+ .result-header {
455
+ display: flex;
456
+ justify-content: space-between;
457
+ align-items: flex-start;
458
+ margin-bottom: 1rem;
459
+ }
460
+
461
+ .result-title {
462
+ font-size: var(--font-size-lg);
463
+ font-weight: 600;
464
+ color: var(--text-primary);
465
+ margin-bottom: 0.5rem;
466
+ display: flex;
467
+ align-items: center;
468
+ gap: 0.5rem;
469
+ }
470
+
471
+ .result-icon {
472
+ width: 1.5rem;
473
+ height: 1.5rem;
474
+ background: var(--primary-gradient);
475
+ border-radius: var(--border-radius-sm);
476
+ display: flex;
477
+ align-items: center;
478
+ justify-content: center;
479
+ color: white;
480
+ font-size: var(--font-size-xs);
481
+ }
482
+
483
+ .result-meta {
484
+ display: flex;
485
+ gap: 1rem;
486
+ font-size: var(--font-size-sm);
487
+ color: var(--text-muted);
488
+ margin-bottom: 1rem;
489
+ }
490
+
491
+ .result-snippet {
492
+ color: var(--text-secondary);
493
+ line-height: 1.6;
494
+ margin-bottom: 1rem;
495
+ }
496
+
497
+ .result-highlight {
498
+ background: rgba(245, 158, 11, 0.2);
499
+ color: var(--text-primary);
500
+ font-weight: 600;
501
+ padding: 0.1rem 0.2rem;
502
+ border-radius: 3px;
503
+ }
504
+
505
+ .result-actions {
506
+ display: flex;
507
+ gap: 0.5rem;
508
+ }
509
+
510
+ .result-btn {
511
+ padding: 0.3rem 0.6rem;
512
+ border: none;
513
+ border-radius: var(--border-radius-sm);
514
+ cursor: pointer;
515
+ transition: var(--transition-fast);
516
+ font-size: var(--font-size-xs);
517
+ font-weight: 500;
518
+ }
519
+
520
+ .result-btn.primary {
521
+ background: rgba(59, 130, 246, 0.1);
522
+ color: #3b82f6;
523
+ }
524
+
525
+ .result-btn.secondary {
526
+ background: rgba(16, 185, 129, 0.1);
527
+ color: #10b981;
528
+ }
529
+
530
+ .result-btn:hover {
531
+ transform: scale(1.05);
532
+ }
533
+
534
+ /* نتایج خالی */
535
+ .empty-results {
536
+ display: none;
537
+ padding: 3rem;
538
+ text-align: center;
539
+ color: var(--text-secondary);
540
+ }
541
+
542
+ .empty-results.show {
543
+ display: block;
544
+ }
545
+
546
+ .empty-icon {
547
+ font-size: 4rem;
548
+ margin-bottom: 1rem;
549
+ opacity: 0.3;
550
+ color: var(--text-muted);
551
+ }
552
+
553
+ .empty-title {
554
+ font-size: var(--font-size-xl);
555
+ font-weight: 600;
556
+ margin-bottom: 0.5rem;
557
+ color: var(--text-primary);
558
+ }
559
+
560
+ .empty-description {
561
+ font-size: var(--font-size-base);
562
+ margin-bottom: 1.5rem;
563
+ max-width: 400px;
564
+ margin-left: auto;
565
+ margin-right: auto;
566
+ }
567
+
568
+ /* صفحه‌بندی */
569
+ .pagination {
570
+ display: flex;
571
+ justify-content: center;
572
+ align-items: center;
573
+ gap: 0.4rem;
574
+ padding: 1rem;
575
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
576
+ }
577
+
578
+ .pagination-btn {
579
+ padding: 0.4rem 0.6rem;
580
+ border: 1px solid var(--glass-border);
581
+ background: var(--glass-bg);
582
+ color: var(--text-primary);
583
+ border-radius: var(--border-radius-sm);
584
+ cursor: pointer;
585
+ transition: var(--transition-fast);
586
+ font-size: var(--font-size-xs);
587
+ font-family: inherit;
588
+ font-weight: 500;
589
+ }
590
+
591
+ .pagination-btn:hover:not(:disabled) {
592
+ background: var(--primary-gradient);
593
+ color: white;
594
+ border-color: transparent;
595
+ transform: translateY(-1px);
596
+ }
597
+
598
+ .pagination-btn.active {
599
+ background: var(--primary-gradient);
600
+ color: white;
601
+ border-color: transparent;
602
+ }
603
+
604
+ .pagination-btn:disabled {
605
+ opacity: 0.4;
606
+ cursor: not-allowed;
607
+ }
608
+
609
+ /* لودینگ */
610
+ .loading-container {
611
+ display: none;
612
+ padding: 3rem;
613
+ text-align: center;
614
+ color: var(--text-secondary);
615
+ }
616
+
617
+ .loading-container.show {
618
+ display: block;
619
+ }
620
+
621
+ .loading-spinner {
622
+ width: 2rem;
623
+ height: 2rem;
624
+ border: 2px solid rgba(59, 130, 246, 0.1);
625
+ border-radius: 50%;
626
+ border-top-color: #3b82f6;
627
+ animation: spin 1s linear infinite;
628
+ margin: 0 auto 1rem;
629
+ }
630
+
631
+ @keyframes spin {
632
+ to { transform: rotate(360deg); }
633
+ }
634
+
635
+ /* Toast Notifications */
636
+ .toast-container {
637
+ position: fixed;
638
+ top: 1rem;
639
+ left: 1rem;
640
+ z-index: 10001;
641
+ display: flex;
642
+ flex-direction: column;
643
+ gap: 0.5rem;
644
+ }
645
+
646
+ .toast {
647
+ background: var(--card-bg);
648
+ border-radius: var(--border-radius-sm);
649
+ padding: 1rem 1.5rem;
650
+ box-shadow: var(--shadow-lg);
651
+ border-left: 4px solid;
652
+ display: flex;
653
+ align-items: center;
654
+ gap: 0.8rem;
655
+ min-width: 300px;
656
+ transform: translateX(-100%);
657
+ transition: all 0.3s ease;
658
+ }
659
+
660
+ .toast.show {
661
+ transform: translateX(0);
662
+ }
663
+
664
+ .toast.success { border-left-color: #10b981; }
665
+ .toast.error { border-left-color: #ef4444; }
666
+ .toast.warning { border-left-color: #f59e0b; }
667
+ .toast.info { border-left-color: #3b82f6; }
668
+
669
+ .toast-icon {
670
+ font-size: 1.2rem;
671
+ }
672
+
673
+ .toast.success .toast-icon { color: #10b981; }
674
+ .toast.error .toast-icon { color: #ef4444; }
675
+ .toast.warning .toast-icon { color: #f59e0b; }
676
+ .toast.info .toast-icon { color: #3b82f6; }
677
+
678
+ .toast-content {
679
+ flex: 1;
680
+ }
681
+
682
+ .toast-title {
683
+ font-weight: 600;
684
+ font-size: var(--font-size-sm);
685
+ margin-bottom: 0.2rem;
686
+ }
687
+
688
+ .toast-message {
689
+ font-size: var(--font-size-xs);
690
+ color: var(--text-secondary);
691
+ }
692
+
693
+ .toast-close {
694
+ background: none;
695
+ border: none;
696
+ color: var(--text-secondary);
697
+ cursor: pointer;
698
+ font-size: 1rem;
699
+ transition: var(--transition-fast);
700
+ }
701
+
702
+ .toast-close:hover {
703
+ color: var(--text-primary);
704
+ }
705
+
706
+ /* واکنش‌گرایی */
707
+ @media (max-width: 992px) {
708
+ .sidebar {
709
+ transform: translateX(100%);
710
+ transition: transform 0.3s ease;
711
+ }
712
+
713
+ .sidebar.open {
714
+ transform: translateX(0);
715
+ }
716
+
717
+ .main-content {
718
+ margin-right: 0;
719
+ width: 100%;
720
+ padding: 1rem;
721
+ }
722
+
723
+ .search-filters {
724
+ grid-template-columns: 1fr;
725
+ }
726
+
727
+ .search-actions {
728
+ justify-content: center;
729
+ flex-wrap: wrap;
730
+ }
731
+
732
+ .results-header {
733
+ flex-direction: column;
734
+ align-items: flex-start;
735
+ gap: 1rem;
736
+ }
737
+
738
+ .result-header {
739
+ flex-direction: column;
740
+ align-items: flex-start;
741
+ gap: 0.5rem;
742
+ }
743
+
744
+ .result-meta {
745
+ flex-direction: column;
746
+ gap: 0.5rem;
747
+ }
748
+ }
749
+
750
+ @media (max-width: 768px) {
751
+ .main-content {
752
+ padding: 0.8rem;
753
+ }
754
+
755
+ .search-main {
756
+ padding: 1.5rem;
757
+ }
758
+
759
+ .search-input {
760
+ font-size: var(--font-size-base);
761
+ padding: 0.8rem 2.5rem 0.8rem 0.8rem;
762
+ }
763
+
764
+ .search-btn {
765
+ padding: 0.6rem 0.8rem;
766
+ }
767
+
768
+ .result-item {
769
+ padding: 1rem;
770
+ }
771
+ }
772
+ </style>
773
+ </head>
774
+ <body>
775
+ <div class="dashboard-container">
776
+ <!-- سایدبار -->
777
+ <aside class="sidebar" id="sidebar">
778
+ <div class="sidebar-header">
779
+ <a href="/" class="logo">
780
+ <div class="logo-icon">
781
+ <i class="fas fa-scale-balanced"></i>
782
+ </div>
783
+ <div class="logo-text">سامانه حقوقی</div>
784
+ </a>
785
+ </div>
786
+
787
+ <nav>
788
+ <div class="nav-section">
789
+ <h6 class="nav-title">داشبورد</h6>
790
+ <ul class="nav-menu">
791
+ <li class="nav-item">
792
+ <a href="/" class="nav-link">
793
+ <i class="fas fa-chart-pie nav-icon"></i>
794
+ <span>نمای کلی</span>
795
+ </a>
796
+ </li>
797
+ </ul>
798
+ </div>
799
+
800
+ <div class="nav-section">
801
+ <h6 class="nav-title">مدیریت اسناد</h6>
802
+ <ul class="nav-menu">
803
+ <li class="nav-item">
804
+ <a href="/static/documents.html" class="nav-link">
805
+ <i class="fas fa-file-alt nav-icon"></i>
806
+ <span>مدیریت اسناد</span>
807
+ <span class="nav-badge" id="totalDocumentsBadge">6</span>
808
+ </a>
809
+ </li>
810
+
811
+ <li class="nav-item">
812
+ <a href="/static/upload.html" class="nav-link">
813
+ <i class="fas fa-cloud-upload-alt nav-icon"></i>
814
+ <span>آپلود فایل</span>
815
+ </a>
816
+ </li>
817
+
818
+ <li class="nav-item">
819
+ <a href="/static/search.html" class="nav-link active">
820
+ <i class="fas fa-search nav-icon"></i>
821
+ <span>جستجو</span>
822
+ </a>
823
+ </li>
824
+ </ul>
825
+ </div>
826
+
827
+ <div class="nav-section">
828
+ <h6 class="nav-title">ابزارها</h6>
829
+ <ul class="nav-menu">
830
+ <li class="nav-item">
831
+ <a href="/static/scraping.html" class="nav-link">
832
+ <i class="fas fa-globe nav-icon"></i>
833
+ <span>استخراج محتوا</span>
834
+ </a>
835
+ </li>
836
+
837
+ <li class="nav-item">
838
+ <a href="/static/analytics.html" class="nav-link">
839
+ <i class="fas fa-chart-line nav-icon"></i>
840
+ <span>آمار و تحلیل</span>
841
+ </a>
842
+ </li>
843
+
844
+ <li class="nav-item">
845
+ <a href="/static/reports.html" class="nav-link">
846
+ <i class="fas fa-file-export nav-icon"></i>
847
+ <span>گزارش‌ها</span>
848
+ </a>
849
+ </li>
850
+ </ul>
851
+ </div>
852
+
853
+ <div class="nav-section">
854
+ <h6 class="nav-title">تنظیمات</h6>
855
+ <ul class="nav-menu">
856
+ <li class="nav-item">
857
+ <a href="/static/settings.html" class="nav-link">
858
+ <i class="fas fa-cog nav-icon"></i>
859
+ <span>تنظیمات</span>
860
+ </a>
861
+ </li>
862
+ <li class="nav-item">
863
+ <a href="#" class="nav-link">
864
+ <i class="fas fa-sign-out-alt nav-icon"></i>
865
+ <span>خروج</span>
866
+ </a>
867
+ </li>
868
+ </ul>
869
+ </div>
870
+ </nav>
871
+ </aside>
872
+
873
+ <!-- محتوای اصلی -->
874
+ <main class="main-content">
875
+ <!-- هدر صفحه -->
876
+ <header class="page-header">
877
+ <h1 class="page-title">
878
+ <i class="fas fa-search"></i>
879
+ جستجوی پیشرفته اسناد
880
+ </h1>
881
+ <div class="page-actions">
882
+ <button type="button" class="btn btn-outline" onclick="clearSearch()">
883
+ <i class="fas fa-undo"></i>
884
+ پاک کردن
885
+ </button>
886
+ <a href="/static/documents.html" class="btn btn-primary">
887
+ <i class="fas fa-list"></i>
888
+ مشاهده همه اسناد
889
+ </a>
890
+ </div>
891
+ </header>
892
+
893
+ <!-- جستجوی اصلی -->
894
+ <section class="search-main">
895
+ <form class="search-form" onsubmit="performSearch(event)">
896
+ <div class="search-primary">
897
+ <input type="text"
898
+ class="search-input"
899
+ id="searchQuery"
900
+ name="q"
901
+ placeholder="جستجو در اسناد، قوانین، آرا..."
902
+ autocomplete="off">
903
+ <button type="submit" class="search-btn">
904
+ <i class="fas fa-search"></i>
905
+ جستجو
906
+ </button>
907
+ </div>
908
+
909
+ <div class="search-filters">
910
+ <div class="filter-group">
911
+ <label class="filter-label" for="categoryFilter">دسته‌بندی</label>
912
+ <select class="filter-select" id="categoryFilter" name="category">
913
+ <option value="">همه دسته‌ها</option>
914
+ <option value="قراردادها">قراردادها</option>
915
+ <option value="دادخواست‌ها">دادخواست‌ها</option>
916
+ <option value="احکام قضایی">احکام قضایی</option>
917
+ <option value="آرای دیوان">آرای دیوان</option>
918
+ <option value="سایر">سایر</option>
919
+ </select>
920
+ </div>
921
+
922
+ <div class="filter-group">
923
+ <label class="filter-label" for="statusFilter">وضعیت</label>
924
+ <select class="filter-select" id="statusFilter" name="status">
925
+ <option value="">همه وضعیت‌ها</option>
926
+ <option value="processed">پردازش شده</option>
927
+ <option value="processing">در حال پردازش</option>
928
+ <option value="pending">در انتظار</option>
929
+ <option value="error">خطا</option>
930
+ </select>
931
+ </div>
932
+
933
+ <div class="filter-group">
934
+ <label class="filter-label" for="dateFromFilter">از تاریخ</label>
935
+ <input type="date" class="filter-input" id="dateFromFilter" name="date_from">
936
+ </div>
937
+
938
+ <div class="filter-group">
939
+ <label class="filter-label" for="dateToFilter">تا تاریخ</label>
940
+ <input type="date" class="filter-input" id="dateToFilter" name="date_to">
941
+ </div>
942
+
943
+ <div class="filter-group">
944
+ <label class="filter-label" for="qualityFilter">حداقل کیفیت</label>
945
+ <select class="filter-select" id="qualityFilter" name="min_quality">
946
+ <option value="">همه کیفیت‌ها</option>
947
+ <option value="8.5">عالی (8.5+)</option>
948
+ <option value="6.5">خوب (6.5+)</option>
949
+ <option value="4.5">متوسط (4.5+)</option>
950
+ <option value="0">ضعیف (کمتر از 4.5)</option>
951
+ </select>
952
+ </div>
953
+
954
+ <div class="filter-group">
955
+ <label class="filter-label" for="sortFilter">مرتب‌سازی</label>
956
+ <select class="filter-select" id="sortFilter" name="sort">
957
+ <option value="relevance">مرتبط‌ترین</option>
958
+ <option value="date_desc">جدیدترین</option>
959
+ <option value="date_asc">قدیمی‌ترین</option>
960
+ <option value="quality_desc">بهترین کیفیت</option>
961
+ <option value="size_desc">بزرگترین فایل</option>
962
+ </select>
963
+ </div>
964
+ </div>
965
+
966
+ <div class="search-actions">
967
+ <button type="submit" class="btn btn-primary">
968
+ <i class="fas fa-search"></i>
969
+ جستجو
970
+ </button>
971
+ <button type="button" class="btn btn-outline btn-sm" onclick="resetFilters()">
972
+ <i class="fas fa-undo"></i>
973
+ پاک کردن فیلترها
974
+ </button>
975
+ <button type="button" class="btn btn-outline btn-sm" onclick="exportResults()">
976
+ <i class="fas fa-download"></i>
977
+ خروجی نتایج
978
+ </button>
979
+ </div>
980
+ </form>
981
+ </section>
982
+
983
+ <!-- لودینگ -->
984
+ <section class="loading-container" id="loadingContainer">
985
+ <div class="loading-spinner"></div>
986
+ <p>در حال جستجو...</p>
987
+ </section>
988
+
989
+ <!-- نتایج جستجو -->
990
+ <section class="search-results" id="searchResults">
991
+ <div class="results-header">
992
+ <h3 class="results-title">نتایج جستجو</h3>
993
+ <div class="results-stats">
994
+ <span>تعداد نتایج: <strong id="resultsCount">0</strong></span>
995
+ <span>زمان جستجو: <strong id="searchTime">0.00</strong> ثانیه</span>
996
+ </div>
997
+ </div>
998
+
999
+ <div id="resultsContainer">
1000
+ <!-- نتایج اینجا نمایش داده می‌شوند -->
1001
+ </div>
1002
+
1003
+ <div class="pagination" id="pagination">
1004
+ <!-- صفحه‌بندی اینجا نمایش داده می‌شود -->
1005
+ </div>
1006
+ </section>
1007
+
1008
+ <!-- نتایج خالی -->
1009
+ <section class="empty-results" id="emptyResults">
1010
+ <i class="fas fa-search empty-icon"></i>
1011
+ <div class="empty-title">نتیجه‌ای یافت نشد</div>
1012
+ <div class="empty-description">
1013
+ متأسفانه برای جستجوی شما نتیجه‌ای یافت نشد. لطفاً کلمات کلیدی دیگری امتحان کنید یا فیلترها را تغییر دهید.
1014
+ </div>
1015
+ <button type="button" class="btn btn-primary" onclick="clearSearch()">
1016
+ <i class="fas fa-search"></i>
1017
+ جستجوی جدید
1018
+ </button>
1019
+ </section>
1020
+ </main>
1021
+ </div>
1022
+
1023
+ <!-- Toast Container -->
1024
+ <div class="toast-container" id="toastContainer"></div>
1025
+
1026
+ <script>
1027
+ // Global variables
1028
+ let currentResults = [];
1029
+ let currentPage = 1;
1030
+ let itemsPerPage = 10;
1031
+ let totalResults = 0;
1032
+ let searchStartTime = 0;
1033
+ let isOnline = false;
1034
+
1035
+ // Initialize page
1036
+ document.addEventListener('DOMContentLoaded', function() {
1037
+ console.log('🔍 Search page loading...');
1038
+ initializeSearchPage();
1039
+ });
1040
+
1041
+ async function initializeSearchPage() {
1042
+ try {
1043
+ // Test backend connection
1044
+ isOnline = await testConnection();
1045
+
1046
+ // Setup event listeners
1047
+ setupEventListeners();
1048
+
1049
+ // Check for URL parameters
1050
+ checkUrlParameters();
1051
+
1052
+ showToast('صفحه جستجو آماده است', 'success', 'آماده');
1053
+
1054
+ } catch (error) {
1055
+ console.error('Failed to initialize search page:', error);
1056
+
1057
+ // Fallback mode
1058
+ isOnline = false;
1059
+ setupEventListeners();
1060
+
1061
+ showToast('حالت آفلاین فعال است', 'warning', 'اتصال ناموفق');
1062
+ }
1063
+ }
1064
+
1065
+ async function testConnection() {
1066
+ try {
1067
+ await window.legalAPI.healthCheck();
1068
+ return true;
1069
+ } catch (error) {
1070
+ return false;
1071
+ }
1072
+ }
1073
+
1074
+ function setupEventListeners() {
1075
+ // Form submission
1076
+ document.querySelector('.search-form').addEventListener('submit', performSearch);
1077
+
1078
+ // Real-time search with debounce
1079
+ const searchInput = document.getElementById('searchQuery');
1080
+ let searchTimeout;
1081
+ searchInput.addEventListener('input', function(e) {
1082
+ clearTimeout(searchTimeout);
1083
+ searchTimeout = setTimeout(() => {
1084
+ if (e.target.value.trim().length > 2) {
1085
+ performQuickSearch(e.target.value.trim());
1086
+ }
1087
+ }, 500);
1088
+ });
1089
+
1090
+ // Filter changes
1091
+ ['categoryFilter', 'statusFilter', 'qualityFilter', 'sortFilter'].forEach(id => {
1092
+ document.getElementById(id).addEventListener('change', debounce(performCurrentSearch, 300));
1093
+ });
1094
+
1095
+ ['dateFromFilter', 'dateToFilter'].forEach(id => {
1096
+ document.getElementById(id).addEventListener('change', debounce(performCurrentSearch, 500));
1097
+ });
1098
+ }
1099
+
1100
+ function checkUrlParameters() {
1101
+ const urlParams = new URLSearchParams(window.location.search);
1102
+ const query = urlParams.get('q');
1103
+
1104
+ if (query) {
1105
+ document.getElementById('searchQuery').value = query;
1106
+ performSearch();
1107
+ }
1108
+ }
1109
+
1110
+ async function performSearch(event) {
1111
+ if (event) {
1112
+ event.preventDefault();
1113
+ }
1114
+
1115
+ const form = document.querySelector('.search-form');
1116
+ const formData = new FormData(form);
1117
+
1118
+ const searchParams = {
1119
+ q: formData.get('q').trim(),
1120
+ category: formData.get('category'),
1121
+ status: formData.get('status'),
1122
+ date_from: formData.get('date_from'),
1123
+ date_to: formData.get('date_to'),
1124
+ min_quality: formData.get('min_quality'),
1125
+ sort: formData.get('sort') || 'relevance',
1126
+ page: currentPage,
1127
+ limit: itemsPerPage
1128
+ };
1129
+
1130
+ // Validate search query
1131
+ if (!searchParams.q || searchParams.q.length < 2) {
1132
+ showToast('لطفاً حداقل 2 کاراکتر برای جستجو وارد کنید', 'warning', 'هشدار');
1133
+ return;
1134
+ }
1135
+
1136
+ showLoading(true);
1137
+ searchStartTime = performance.now();
1138
+
1139
+ try {
1140
+ let results;
1141
+
1142
+ if (isOnline) {
1143
+ // Real API search
1144
+ results = await window.legalAPI.searchDocuments(searchParams);
1145
+ } else {
1146
+ // Mock search results
1147
+ results = await generateMockSearchResults(searchParams);
1148
+ }
1149
+
1150
+ const searchTime = ((performance.now() - searchStartTime) / 1000).toFixed(2);
1151
+ displaySearchResults(results, searchTime);
1152
+
1153
+ console.log('Search completed:', results);
1154
+
1155
+ } catch (error) {
1156
+ console.error('Search failed:', error);
1157
+ showToast('خطا در جستجو. لطفاً دوباره تلاش کنید.', 'error', 'خطا');
1158
+ showEmptyResults();
1159
+ } finally {
1160
+ showLoading(false);
1161
+ }
1162
+ }
1163
+
1164
+ async function performQuickSearch(query) {
1165
+ if (!query || query.length < 3) return;
1166
+
1167
+ try {
1168
+ const searchParams = { q: query, limit: 5 };
1169
+
1170
+ let results;
1171
+ if (isOnline) {
1172
+ results = await window.legalAPI.searchDocuments(searchParams);
1173
+ } else {
1174
+ results = await generateMockSearchResults(searchParams);
1175
+ }
1176
+
1177
+ // Could implement quick search dropdown here
1178
+ console.log('Quick search results:', results);
1179
+
1180
+ } catch (error) {
1181
+ console.error('Quick search failed:', error);
1182
+ }
1183
+ }
1184
+
1185
+ async function performCurrentSearch() {
1186
+ const form = document.querySelector('.search-form');
1187
+ const formData = new FormData(form);
1188
+
1189
+ if (formData.get('q').trim()) {
1190
+ await performSearch();
1191
+ }
1192
+ }
1193
+
1194
+ async function generateMockSearchResults(searchParams) {
1195
+ // Simulate API delay
1196
+ await new Promise(resolve => setTimeout(resolve, 800));
1197
+
1198
+ const mockDocuments = [
1199
+ {
1200
+ id: 1,
1201
+ filename: "contract_001.pdf",
1202
+ original_filename: "قرارداد خرید املاک.pdf",
1203
+ file_size: 1024000,
1204
+ category: "قراردادها",
1205
+ quality_score: 8.5,
1206
+ status: "processed",
1207
+ created_at: "2024-01-15T10:30:00Z",
1208
+ ocr_text: "قرارداد خرید و فروش ملک واقع در تهران منطقه 3...",
1209
+ summary: "قرارداد خرید املاک مسکونی در منطقه 3 تهران",
1210
+ relevance_score: 0.95
1211
+ },
1212
+ {
1213
+ id: 2,
1214
+ filename: "lawsuit_002.pdf",
1215
+ original_filename: "دادخواست طلاق.pdf",
1216
+ file_size: 512000,
1217
+ category: "دادخواست‌ها",
1218
+ quality_score: 7.8,
1219
+ status: "processed",
1220
+ created_at: "2024-01-14T14:20:00Z",
1221
+ ocr_text: "دادخواست طلاق به درخواست زوجه...",
1222
+ summary: "درخواست طلاق توافقی با تقسیم اموال",
1223
+ relevance_score: 0.87
1224
+ },
1225
+ {
1226
+ id: 3,
1227
+ filename: "judgment_003.pdf",
1228
+ original_filename: "حکم دادگاه عمومی.pdf",
1229
+ file_size: 768000,
1230
+ category: "احکام قضایی",
1231
+ quality_score: 9.2,
1232
+ status: "processed",
1233
+ created_at: "2024-01-13T09:15:00Z",
1234
+ ocr_text: "حکم دادگاه عمومی حقوقی تهران...",
1235
+ summary: "حکم پرونده ملکی در دادگاه عمومی تهران",
1236
+ relevance_score: 0.82
1237
+ }
1238
+ ];
1239
+
1240
+ // Filter results based on search parameters
1241
+ let filteredResults = mockDocuments;
1242
+
1243
+ if (searchParams.category) {
1244
+ filteredResults = filteredResults.filter(doc => doc.category === searchParams.category);
1245
+ }
1246
+
1247
+ if (searchParams.status) {
1248
+ filteredResults = filteredResults.filter(doc => doc.status === searchParams.status);
1249
+ }
1250
+
1251
+ if (searchParams.min_quality) {
1252
+ const minQuality = parseFloat(searchParams.min_quality);
1253
+ filteredResults = filteredResults.filter(doc => doc.quality_score >= minQuality);
1254
+ }
1255
+
1256
+ // Apply sorting
1257
+ if (searchParams.sort) {
1258
+ switch (searchParams.sort) {
1259
+ case 'date_desc':
1260
+ filteredResults.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
1261
+ break;
1262
+ case 'date_asc':
1263
+ filteredResults.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
1264
+ break;
1265
+ case 'quality_desc':
1266
+ filteredResults.sort((a, b) => b.quality_score - a.quality_score);
1267
+ break;
1268
+ case 'relevance':
1269
+ default:
1270
+ filteredResults.sort((a, b) => b.relevance_score - a.relevance_score);
1271
+ break;
1272
+ }
1273
+ }
1274
+
1275
+ return {
1276
+ documents: filteredResults,
1277
+ pagination: {
1278
+ page: searchParams.page || 1,
1279
+ limit: searchParams.limit || 10,
1280
+ total: filteredResults.length,
1281
+ pages: Math.ceil(filteredResults.length / (searchParams.limit || 10))
1282
+ }
1283
+ };
1284
+ }
1285
+
1286
+ function displaySearchResults(results, searchTime) {
1287
+ currentResults = results.documents || [];
1288
+ totalResults = results.pagination?.total || 0;
1289
+
1290
+ const resultsContainer = document.getElementById('resultsContainer');
1291
+ const searchResults = document.getElementById('searchResults');
1292
+ const emptyResults = document.getElementById('emptyResults');
1293
+
1294
+ // Update stats
1295
+ document.getElementById('resultsCount').textContent = totalResults;
1296
+ document.getElementById('searchTime').textContent = searchTime;
1297
+
1298
+ if (currentResults.length === 0) {
1299
+ searchResults.classList.remove('show');
1300
+ emptyResults.classList.add('show');
1301
+ return;
1302
+ }
1303
+
1304
+ // Display results
1305
+ emptyResults.classList.remove('show');
1306
+ searchResults.classList.add('show');
1307
+
1308
+ const searchQuery = document.getElementById('searchQuery').value.toLowerCase();
1309
+
1310
+ resultsContainer.innerHTML = currentResults.map(doc => {
1311
+ const snippet = generateSnippet(doc, searchQuery);
1312
+ const relevanceScore = ((doc.relevance_score || 0.8) * 100).toFixed(0);
1313
+
1314
+ return `
1315
+ <div class="result-item" onclick="viewDocument(${doc.id})">
1316
+ <div class="result-header">
1317
+ <div>
1318
+ <div class="result-title">
1319
+ <div class="result-icon">
1320
+ <i class="fas fa-file-pdf"></i>
1321
+ </div>
1322
+ ${highlightText(doc.original_filename || doc.filename, searchQuery)}
1323
+ </div>
1324
+ <div class="result-meta">
1325
+ <span><i class="fas fa-tag"></i> ${doc.category}</span>
1326
+ <span><i class="fas fa-calendar"></i> ${formatDate(doc.created_at)}</span>
1327
+ <span><i class="fas fa-star"></i> کیفیت: ${doc.quality_score?.toFixed(1) || '0.0'}</span>
1328
+ <span><i class="fas fa-percentage"></i> ارتباط: ${relevanceScore}%</span>
1329
+ </div>
1330
+ </div>
1331
+ <div class="result-actions">
1332
+ <button type="button" class="result-btn primary" onclick="event.stopPropagation(); viewDocument(${doc.id})" title="مشاهده">
1333
+ <i class="fas fa-eye"></i>
1334
+ </button>
1335
+ <button type="button" class="result-btn secondary" onclick="event.stopPropagation(); downloadDocument(${doc.id})" title="دانلود">
1336
+ <i class="fas fa-download"></i>
1337
+ </button>
1338
+ </div>
1339
+ </div>
1340
+ <div class="result-snippet">
1341
+ ${snippet}
1342
+ </div>
1343
+ </div>
1344
+ `;
1345
+ }).join('');
1346
+
1347
+ // Setup pagination
1348
+ setupPagination(results.pagination);
1349
+ }
1350
+
1351
+ function generateSnippet(doc, searchQuery) {
1352
+ const text = doc.summary || doc.ocr_text || 'محتوای متنی در دسترس نیست.';
1353
+ const maxLength = 200;
1354
+
1355
+ if (!searchQuery || text.length <= maxLength) {
1356
+ return highlightText(text.substring(0, maxLength) + (text.length > maxLength ? '...' : ''), searchQuery);
1357
+ }
1358
+
1359
+ // Try to find the search term in the text
1360
+ const lowerText = text.toLowerCase();
1361
+ const queryIndex = lowerText.indexOf(searchQuery);
1362
+
1363
+ if (queryIndex === -1) {
1364
+ return highlightText(text.substring(0, maxLength) + '...', searchQuery);
1365
+ }
1366
+
1367
+ // Extract snippet around the search term
1368
+ const start = Math.max(0, queryIndex - 80);
1369
+ const end = Math.min(text.length, queryIndex + searchQuery.length + 80);
1370
+
1371
+ let snippet = text.substring(start, end);
1372
+ if (start > 0) snippet = '...' + snippet;
1373
+ if (end < text.length) snippet = snippet + '...';
1374
+
1375
+ return highlightText(snippet, searchQuery);
1376
+ }
1377
+
1378
+ function highlightText(text, query) {
1379
+ if (!query || query.length < 2) return text;
1380
+
1381
+ const regex = new RegExp(`(${escapeRegExp(query)})`, 'gi');
1382
+ return text.replace(regex, '<span class="result-highlight">$1</span>');
1383
+ }
1384
+
1385
+ function escapeRegExp(string) {
1386
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1387
+ }
1388
+
1389
+ function setupPagination(pagination) {
1390
+ const paginationContainer = document.getElementById('pagination');
1391
+ const totalPages = pagination.pages;
1392
+
1393
+ if (totalPages <= 1) {
1394
+ paginationContainer.style.display = 'none';
1395
+ return;
1396
+ }
1397
+
1398
+ paginationContainer.style.display = 'flex';
1399
+
1400
+ let paginationHTML = '';
1401
+
1402
+ // Previous button
1403
+ paginationHTML += `
1404
+ <button class="pagination-btn" ${currentPage <= 1 ? 'disabled' : ''}
1405
+ onclick="changePage(${currentPage - 1})">
1406
+ <i class="fas fa-chevron-right"></i>
1407
+ </button>
1408
+ `;
1409
+
1410
+ // Page numbers
1411
+ const startPage = Math.max(1, currentPage - 2);
1412
+ const endPage = Math.min(totalPages, currentPage + 2);
1413
+
1414
+ for (let i = startPage; i <= endPage; i++) {
1415
+ paginationHTML += `
1416
+ <button class="pagination-btn ${i === currentPage ? 'active' : ''}"
1417
+ onclick="changePage(${i})">
1418
+ ${i}
1419
+ </button>
1420
+ `;
1421
+ }
1422
+
1423
+ // Next button
1424
+ paginationHTML += `
1425
+ <button class="pagination-btn" ${currentPage >= totalPages ? 'disabled' : ''}
1426
+ onclick="changePage(${currentPage + 1})">
1427
+ <i class="fas fa-chevron-left"></i>
1428
+ </button>
1429
+ `;
1430
+
1431
+ paginationContainer.innerHTML = paginationHTML;
1432
+ }
1433
+
1434
+ async function changePage(page) {
1435
+ currentPage = page;
1436
+ await performSearch();
1437
+
1438
+ // Scroll to top of results
1439
+ document.getElementById('searchResults').scrollIntoView({
1440
+ behavior: 'smooth',
1441
+ block: 'start'
1442
+ });
1443
+ }
1444
+
1445
+ function showEmptyResults() {
1446
+ document.getElementById('searchResults').classList.remove('show');
1447
+ document.getElementById('emptyResults').classList.add('show');
1448
+ }
1449
+
1450
+ function showLoading(show) {
1451
+ const loading = document.getElementById('loadingContainer');
1452
+ const results = document.getElementById('searchResults');
1453
+ const empty = document.getElementById('emptyResults');
1454
+
1455
+ if (show) {
1456
+ loading.classList.add('show');
1457
+ results.classList.remove('show');
1458
+ empty.classList.remove('show');
1459
+ } else {
1460
+ loading.classList.remove('show');
1461
+ }
1462
+ }
1463
+
1464
+ function resetFilters() {
1465
+ // Reset all filter fields except search query
1466
+ document.getElementById('categoryFilter').value = '';
1467
+ document.getElementById('statusFilter').value = '';
1468
+ document.getElementById('dateFromFilter').value = '';
1469
+ document.getElementById('dateToFilter').value = '';
1470
+ document.getElementById('qualityFilter').value = '';
1471
+ document.getElementById('sortFilter').value = 'relevance';
1472
+
1473
+ showToast('فیلترها پاک شدند', 'info', 'بازنشانی');
1474
+
1475
+ // Perform search again if there's a query
1476
+ const query = document.getElementById('searchQuery').value.trim();
1477
+ if (query) {
1478
+ performCurrentSearch();
1479
+ }
1480
+ }
1481
+
1482
+ function clearSearch() {
1483
+ document.querySelector('.search-form').reset();
1484
+ document.getElementById('searchResults').classList.remove('show');
1485
+ document.getElementById('emptyResults').classList.remove('show');
1486
+ document.getElementById('searchQuery').focus();
1487
+
1488
+ showToast('جستجو پاک شد', 'info', 'پاک کردن');
1489
+ }
1490
+
1491
+ function exportResults() {
1492
+ if (currentResults.length === 0) {
1493
+ showToast('نتیجه‌ای برای خروجی وجود ندارد', 'warning', 'هشدار');
1494
+ return;
1495
+ }
1496
+
1497
+ showToast('فایل نتایج در حال آماده‌سازی...', 'info', 'خروجی');
1498
+
1499
+ setTimeout(() => {
1500
+ showToast('خروجی نتایج آماده شد', 'success', 'خروجی موفق');
1501
+ }, 2000);
1502
+ }
1503
+
1504
+ function viewDocument(documentId) {
1505
+ showToast(`مشاهده سند شماره ${documentId}`, 'info', 'مشاهده سند');
1506
+ // Could redirect to document detail page
1507
+ }
1508
+
1509
+ function downloadDocument(documentId) {
1510
+ showToast(`دانلود سند شماره ${documentId} شروع شد`, 'info', 'دانلود');
1511
+ }
1512
+
1513
+ // Utility functions
1514
+ function formatDate(dateString) {
1515
+ if (!dateString) return 'نامشخص';
1516
+ const date = new Date(dateString);
1517
+ return date.toLocaleDateString('fa-IR', {
1518
+ year: 'numeric',
1519
+ month: 'long',
1520
+ day: 'numeric'
1521
+ });
1522
+ }
1523
+
1524
+ function debounce(func, wait) {
1525
+ let timeout;
1526
+ return function executedFunction(...args) {
1527
+ const later = () => {
1528
+ clearTimeout(timeout);
1529
+ func(...args);
1530
+ };
1531
+ clearTimeout(timeout);
1532
+ timeout = setTimeout(later, wait);
1533
+ };
1534
+ }
1535
+
1536
+ function showToast(message, type = 'info', title = 'اعلان') {
1537
+ const toastContainer = document.getElementById('toastContainer');
1538
+ if (!toastContainer) return;
1539
+
1540
+ const toast = document.createElement('div');
1541
+ toast.className = `toast ${type}`;
1542
+
1543
+ const icons = {
1544
+ success: 'check-circle',
1545
+ error: 'exclamation-triangle',
1546
+ warning: 'exclamation-circle',
1547
+ info: 'info-circle'
1548
+ };
1549
+
1550
+ toast.innerHTML = `
1551
+ <div class="toast-icon">
1552
+ <i class="fas fa-${icons[type]}"></i>
1553
+ </div>
1554
+ <div class="toast-content">
1555
+ <div class="toast-title">${title}</div>
1556
+ <div class="toast-message">${message}</div>
1557
+ </div>
1558
+ <button type="button" class="toast-close" onclick="this.parentElement.remove()">
1559
+ <i class="fas fa-times"></i>
1560
+ </button>
1561
+ `;
1562
+
1563
+ toastContainer.appendChild(toast);
1564
+
1565
+ // Show toast
1566
+ setTimeout(() => toast.classList.add('show'), 100);
1567
+
1568
+ // Auto remove after 5 seconds
1569
+ setTimeout(() => {
1570
+ if (toast.parentElement) {
1571
+ toast.classList.remove('show');
1572
+ setTimeout(() => {
1573
+ if (toast.parentElement) {
1574
+ toast.remove();
1575
+ }
1576
+ }, 300);
1577
+ }
1578
+ }, 5000);
1579
+ }
1580
+
1581
+ console.log('🔍 Search Page Ready!');
1582
+ </script>
1583
+ </body>
1584
+ </html>
app/frontend/settings.html ADDED
@@ -0,0 +1,2183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>تنظیمات | سامانه حقوقی</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+
12
+ <!-- Load API Client -->
13
+ <script src="/static/js/api-client.js"></script>
14
+ <script src="/static/js/core.js"></script>
15
+
16
+ <style>
17
+ :root {
18
+ --text-primary: #0f172a;
19
+ --text-secondary: #475569;
20
+ --text-muted: #64748b;
21
+ --text-light: #ffffff;
22
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
23
+ --card-bg: rgba(255, 255, 255, 0.95);
24
+ --glass-bg: rgba(255, 255, 255, 0.9);
25
+ --glass-border: rgba(148, 163, 184, 0.2);
26
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
27
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
28
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
29
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
30
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
31
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
32
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
33
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
34
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
35
+ --sidebar-width: 260px;
36
+ --border-radius: 12px;
37
+ --border-radius-sm: 8px;
38
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
39
+ --transition-fast: all 0.15s ease-in-out;
40
+ --font-size-xs: 0.7rem;
41
+ --font-size-sm: 0.8rem;
42
+ --font-size-base: 0.9rem;
43
+ --font-size-lg: 1.1rem;
44
+ --font-size-xl: 1.25rem;
45
+ --font-size-2xl: 1.5rem;
46
+ }
47
+
48
+ * {
49
+ margin: 0;
50
+ padding: 0;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ body {
55
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
56
+ background: var(--body-bg);
57
+ color: var(--text-primary);
58
+ line-height: 1.6;
59
+ overflow-x: hidden;
60
+ font-size: var(--font-size-base);
61
+ }
62
+
63
+ ::-webkit-scrollbar {
64
+ width: 6px;
65
+ height: 6px;
66
+ }
67
+
68
+ ::-webkit-scrollbar-track {
69
+ background: rgba(0, 0, 0, 0.02);
70
+ border-radius: 10px;
71
+ }
72
+
73
+ ::-webkit-scrollbar-thumb {
74
+ background: var(--primary-gradient);
75
+ border-radius: 10px;
76
+ }
77
+
78
+ .dashboard-container {
79
+ display: flex;
80
+ min-height: 100vh;
81
+ width: 100%;
82
+ }
83
+
84
+ /* سایدبار مشابه صفحات قبلی */
85
+ .sidebar {
86
+ width: var(--sidebar-width);
87
+ background: linear-gradient(135deg,
88
+ rgba(248, 250, 252, 0.98) 0%,
89
+ rgba(241, 245, 249, 0.95) 25%,
90
+ rgba(226, 232, 240, 0.98) 50%,
91
+ rgba(203, 213, 225, 0.95) 75%,
92
+ rgba(148, 163, 184, 0.1) 100%);
93
+ backdrop-filter: blur(25px);
94
+ -webkit-backdrop-filter: blur(25px);
95
+ padding: 1rem 0;
96
+ position: fixed;
97
+ height: 100vh;
98
+ right: 0;
99
+ top: 0;
100
+ z-index: 1000;
101
+ overflow-y: auto;
102
+ box-shadow:
103
+ 0 0 0 1px rgba(59, 130, 246, 0.08),
104
+ -8px 0 32px rgba(59, 130, 246, 0.12),
105
+ inset 0 1px 0 rgba(255, 255, 255, 0.6);
106
+ border-left: 1px solid rgba(59, 130, 246, 0.15);
107
+ }
108
+
109
+ .sidebar-header {
110
+ padding: 0 1rem 1rem;
111
+ border-bottom: 1px solid rgba(59, 130, 246, 0.12);
112
+ margin-bottom: 1rem;
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ background: linear-gradient(135deg,
117
+ rgba(255, 255, 255, 0.4) 0%,
118
+ rgba(248, 250, 252, 0.2) 100%);
119
+ margin: 0 0.5rem 1rem;
120
+ border-radius: var(--border-radius);
121
+ backdrop-filter: blur(10px);
122
+ }
123
+
124
+ .logo {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 0.6rem;
128
+ color: var(--text-primary);
129
+ text-decoration: none;
130
+ }
131
+
132
+ .logo-icon {
133
+ width: 2rem;
134
+ height: 2rem;
135
+ background: var(--primary-gradient);
136
+ border-radius: var(--border-radius-sm);
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+ font-size: 1rem;
141
+ color: white;
142
+ }
143
+
144
+ .logo-text {
145
+ font-size: var(--font-size-lg);
146
+ font-weight: 700;
147
+ background: var(--primary-gradient);
148
+ -webkit-background-clip: text;
149
+ -webkit-text-fill-color: transparent;
150
+ }
151
+
152
+ .nav-section {
153
+ margin-bottom: 1rem;
154
+ }
155
+
156
+ .nav-title {
157
+ padding: 0 1rem 0.4rem;
158
+ font-size: var(--font-size-xs);
159
+ font-weight: 600;
160
+ text-transform: uppercase;
161
+ letter-spacing: 0.5px;
162
+ color: var(--text-secondary);
163
+ }
164
+
165
+ .nav-menu {
166
+ list-style: none;
167
+ }
168
+
169
+ .nav-item {
170
+ margin: 0.15rem 0.5rem;
171
+ }
172
+
173
+ .nav-link {
174
+ display: flex;
175
+ align-items: center;
176
+ padding: 0.6rem 0.8rem;
177
+ color: var(--text-primary);
178
+ text-decoration: none;
179
+ border-radius: var(--border-radius-sm);
180
+ transition: var(--transition-smooth);
181
+ font-weight: 500;
182
+ font-size: var(--font-size-sm);
183
+ cursor: pointer;
184
+ border: 1px solid transparent;
185
+ }
186
+
187
+ .nav-link:hover {
188
+ color: var(--text-primary);
189
+ transform: translateX(-2px);
190
+ border-color: rgba(59, 130, 246, 0.15);
191
+ background: rgba(59, 130, 246, 0.05);
192
+ }
193
+
194
+ .nav-link.active {
195
+ background: var(--primary-gradient);
196
+ color: var(--text-light);
197
+ box-shadow: var(--shadow-md);
198
+ }
199
+
200
+ .nav-icon {
201
+ margin-left: 0.6rem;
202
+ width: 1rem;
203
+ text-align: center;
204
+ font-size: 0.9rem;
205
+ }
206
+
207
+ .nav-badge {
208
+ background: var(--danger-gradient);
209
+ color: white;
210
+ padding: 0.15rem 0.4rem;
211
+ border-radius: 10px;
212
+ font-size: var(--font-size-xs);
213
+ font-weight: 600;
214
+ margin-right: auto;
215
+ min-width: 1.2rem;
216
+ text-align: center;
217
+ }
218
+
219
+ /* محتوای اصلی */
220
+ .main-content {
221
+ flex: 1;
222
+ margin-right: var(--sidebar-width);
223
+ padding: 1rem;
224
+ min-height: 100vh;
225
+ width: calc(100% - var(--sidebar-width));
226
+ }
227
+
228
+ .page-header {
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: center;
232
+ margin-bottom: 2rem;
233
+ padding: 1rem 0;
234
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
235
+ }
236
+
237
+ .page-title {
238
+ font-size: var(--font-size-2xl);
239
+ font-weight: 800;
240
+ background: var(--primary-gradient);
241
+ -webkit-background-clip: text;
242
+ -webkit-text-fill-color: transparent;
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 0.6rem;
246
+ }
247
+
248
+ .page-actions {
249
+ display: flex;
250
+ gap: 0.8rem;
251
+ }
252
+
253
+ .btn {
254
+ padding: 0.6rem 1.2rem;
255
+ border: none;
256
+ border-radius: var(--border-radius-sm);
257
+ font-family: inherit;
258
+ font-weight: 600;
259
+ cursor: pointer;
260
+ transition: var(--transition-smooth);
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 0.5rem;
264
+ text-decoration: none;
265
+ font-size: var(--font-size-sm);
266
+ }
267
+
268
+ .btn-primary {
269
+ background: var(--primary-gradient);
270
+ color: white;
271
+ box-shadow: var(--shadow-sm);
272
+ }
273
+
274
+ .btn-primary:hover {
275
+ box-shadow: var(--shadow-md);
276
+ transform: translateY(-1px);
277
+ }
278
+
279
+ .btn-outline {
280
+ background: transparent;
281
+ color: var(--text-primary);
282
+ border: 1px solid rgba(59, 130, 246, 0.2);
283
+ }
284
+
285
+ .btn-outline:hover {
286
+ background: rgba(59, 130, 246, 0.05);
287
+ border-color: rgba(59, 130, 246, 0.4);
288
+ }
289
+
290
+ .btn-success {
291
+ background: var(--success-gradient);
292
+ color: white;
293
+ }
294
+
295
+ .btn-danger {
296
+ background: var(--danger-gradient);
297
+ color: white;
298
+ }
299
+
300
+ .btn-sm {
301
+ padding: 0.4rem 0.8rem;
302
+ font-size: var(--font-size-xs);
303
+ }
304
+
305
+ /* تنظیمات اصلی */
306
+ .settings-layout {
307
+ display: grid;
308
+ grid-template-columns: 250px 1fr;
309
+ gap: 2rem;
310
+ }
311
+
312
+ .settings-nav {
313
+ background: var(--card-bg);
314
+ border-radius: var(--border-radius);
315
+ padding: 1rem;
316
+ box-shadow: var(--shadow-sm);
317
+ border: 1px solid rgba(255, 255, 255, 0.3);
318
+ height: fit-content;
319
+ position: sticky;
320
+ top: 1rem;
321
+ }
322
+
323
+ .settings-nav-title {
324
+ font-size: var(--font-size-lg);
325
+ font-weight: 700;
326
+ color: var(--text-primary);
327
+ margin-bottom: 1rem;
328
+ padding-bottom: 0.8rem;
329
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
330
+ }
331
+
332
+ .settings-nav-list {
333
+ list-style: none;
334
+ }
335
+
336
+ .settings-nav-item {
337
+ margin-bottom: 0.3rem;
338
+ }
339
+
340
+ .settings-nav-link {
341
+ display: flex;
342
+ align-items: center;
343
+ gap: 0.8rem;
344
+ padding: 0.8rem;
345
+ color: var(--text-secondary);
346
+ text-decoration: none;
347
+ border-radius: var(--border-radius-sm);
348
+ transition: var(--transition-smooth);
349
+ font-size: var(--font-size-sm);
350
+ font-weight: 500;
351
+ }
352
+
353
+ .settings-nav-link:hover {
354
+ background: rgba(59, 130, 246, 0.05);
355
+ color: var(--text-primary);
356
+ transform: translateX(-3px);
357
+ }
358
+
359
+ .settings-nav-link.active {
360
+ background: var(--primary-gradient);
361
+ color: white;
362
+ box-shadow: var(--shadow-sm);
363
+ }
364
+
365
+ .settings-nav-icon {
366
+ font-size: var(--font-size-base);
367
+ width: 1.2rem;
368
+ text-align: center;
369
+ }
370
+
371
+ .settings-content {
372
+ background: var(--card-bg);
373
+ border-radius: var(--border-radius);
374
+ box-shadow: var(--shadow-md);
375
+ border: 1px solid rgba(255, 255, 255, 0.3);
376
+ overflow: hidden;
377
+ }
378
+
379
+ .settings-section {
380
+ display: none;
381
+ padding: 2rem;
382
+ }
383
+
384
+ .settings-section.active {
385
+ display: block;
386
+ }
387
+
388
+ .section-header {
389
+ margin-bottom: 2rem;
390
+ padding-bottom: 1rem;
391
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
392
+ }
393
+
394
+ .section-title {
395
+ font-size: var(--font-size-xl);
396
+ font-weight: 700;
397
+ color: var(--text-primary);
398
+ margin-bottom: 0.5rem;
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 0.6rem;
402
+ }
403
+
404
+ .section-description {
405
+ font-size: var(--font-size-sm);
406
+ color: var(--text-secondary);
407
+ line-height: 1.6;
408
+ }
409
+
410
+ /* فرم تنظیمات */
411
+ .settings-form {
412
+ display: flex;
413
+ flex-direction: column;
414
+ gap: 2rem;
415
+ }
416
+
417
+ .form-group {
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 0.8rem;
421
+ }
422
+
423
+ .form-row {
424
+ display: grid;
425
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
426
+ gap: 1rem;
427
+ }
428
+
429
+ .form-label {
430
+ font-size: var(--font-size-sm);
431
+ font-weight: 600;
432
+ color: var(--text-primary);
433
+ display: flex;
434
+ align-items: center;
435
+ gap: 0.3rem;
436
+ }
437
+
438
+ .form-description {
439
+ font-size: var(--font-size-xs);
440
+ color: var(--text-muted);
441
+ margin-top: 0.2rem;
442
+ }
443
+
444
+ .form-input,
445
+ .form-textarea,
446
+ .form-select {
447
+ padding: 0.8rem 1rem;
448
+ border: 2px solid var(--glass-border);
449
+ border-radius: var(--border-radius-sm);
450
+ background: var(--glass-bg);
451
+ color: var(--text-primary);
452
+ font-family: inherit;
453
+ font-size: var(--font-size-sm);
454
+ transition: var(--transition-smooth);
455
+ }
456
+
457
+ .form-input:focus,
458
+ .form-textarea:focus,
459
+ .form-select:focus {
460
+ outline: none;
461
+ border-color: #3b82f6;
462
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
463
+ background: var(--card-bg);
464
+ }
465
+
466
+ .form-textarea {
467
+ min-height: 100px;
468
+ resize: vertical;
469
+ }
470
+
471
+ /* سوئیچ‌ها */
472
+ .switch-group {
473
+ display: flex;
474
+ justify-content: space-between;
475
+ align-items: center;
476
+ padding: 1rem;
477
+ background: rgba(59, 130, 246, 0.02);
478
+ border-radius: var(--border-radius-sm);
479
+ border: 1px solid rgba(59, 130, 246, 0.1);
480
+ }
481
+
482
+ .switch-info {
483
+ flex: 1;
484
+ }
485
+
486
+ .switch-label {
487
+ font-size: var(--font-size-sm);
488
+ font-weight: 600;
489
+ color: var(--text-primary);
490
+ margin-bottom: 0.3rem;
491
+ }
492
+
493
+ .switch-description {
494
+ font-size: var(--font-size-xs);
495
+ color: var(--text-secondary);
496
+ }
497
+
498
+ .switch {
499
+ position: relative;
500
+ width: 50px;
501
+ height: 24px;
502
+ }
503
+
504
+ .switch input {
505
+ opacity: 0;
506
+ width: 0;
507
+ height: 0;
508
+ }
509
+
510
+ .slider {
511
+ position: absolute;
512
+ cursor: pointer;
513
+ top: 0;
514
+ left: 0;
515
+ right: 0;
516
+ bottom: 0;
517
+ background-color: #ccc;
518
+ transition: var(--transition-smooth);
519
+ border-radius: 24px;
520
+ }
521
+
522
+ .slider:before {
523
+ position: absolute;
524
+ content: "";
525
+ height: 18px;
526
+ width: 18px;
527
+ left: 3px;
528
+ bottom: 3px;
529
+ background-color: white;
530
+ transition: var(--transition-smooth);
531
+ border-radius: 50%;
532
+ }
533
+
534
+ input:checked + .slider {
535
+ background: var(--primary-gradient);
536
+ }
537
+
538
+ input:checked + .slider:before {
539
+ transform: translateX(26px);
540
+ }
541
+
542
+ /* کارت‌های اطلاعات */
543
+ .info-cards {
544
+ display: grid;
545
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
546
+ gap: 1rem;
547
+ margin-bottom: 2rem;
548
+ }
549
+
550
+ .info-card {
551
+ background: var(--glass-bg);
552
+ border-radius: var(--border-radius-sm);
553
+ padding: 1.5rem;
554
+ text-align: center;
555
+ border: 1px solid rgba(0, 0, 0, 0.05);
556
+ transition: var(--transition-smooth);
557
+ }
558
+
559
+ .info-card:hover {
560
+ transform: translateY(-2px);
561
+ box-shadow: var(--shadow-sm);
562
+ }
563
+
564
+ .info-card-icon {
565
+ font-size: 2rem;
566
+ margin-bottom: 0.8rem;
567
+ color: var(--text-primary);
568
+ }
569
+
570
+ .info-card-title {
571
+ font-size: var(--font-size-sm);
572
+ font-weight: 600;
573
+ color: var(--text-primary);
574
+ margin-bottom: 0.3rem;
575
+ }
576
+
577
+ .info-card-value {
578
+ font-size: var(--font-size-lg);
579
+ font-weight: 700;
580
+ color: var(--text-secondary);
581
+ }
582
+
583
+ /* بخش آپلود */
584
+ .upload-zone {
585
+ border: 2px dashed rgba(59, 130, 246, 0.3);
586
+ border-radius: var(--border-radius);
587
+ padding: 2rem;
588
+ text-align: center;
589
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.02), rgba(255, 255, 255, 0.1));
590
+ transition: var(--transition-smooth);
591
+ cursor: pointer;
592
+ }
593
+
594
+ .upload-zone:hover {
595
+ border-color: rgba(59, 130, 246, 0.6);
596
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(255, 255, 255, 0.15));
597
+ }
598
+
599
+ .upload-icon {
600
+ font-size: 3rem;
601
+ color: rgba(59, 130, 246, 0.6);
602
+ margin-bottom: 1rem;
603
+ }
604
+
605
+ .upload-text {
606
+ font-size: var(--font-size-base);
607
+ color: var(--text-secondary);
608
+ margin-bottom: 0.5rem;
609
+ }
610
+
611
+ .upload-hint {
612
+ font-size: var(--font-size-xs);
613
+ color: var(--text-muted);
614
+ }
615
+
616
+ /* جدول */
617
+ .settings-table {
618
+ width: 100%;
619
+ border-collapse: separate;
620
+ border-spacing: 0;
621
+ background: var(--card-bg);
622
+ border-radius: var(--border-radius);
623
+ overflow: hidden;
624
+ box-shadow: var(--shadow-xs);
625
+ }
626
+
627
+ .settings-table thead {
628
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.03), rgba(255, 255, 255, 0.1));
629
+ }
630
+
631
+ .settings-table th {
632
+ padding: 1rem;
633
+ text-align: right;
634
+ font-weight: 600;
635
+ color: var(--text-primary);
636
+ font-size: var(--font-size-sm);
637
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
638
+ }
639
+
640
+ .settings-table td {
641
+ padding: 0.8rem 1rem;
642
+ border-bottom: 1px solid rgba(0, 0, 0, 0.03);
643
+ font-size: var(--font-size-sm);
644
+ }
645
+
646
+ .settings-table tbody tr {
647
+ transition: all 0.2s ease;
648
+ }
649
+
650
+ .settings-table tbody tr:hover {
651
+ background: rgba(59, 130, 246, 0.02);
652
+ }
653
+
654
+ /* دکمه‌های عمل */
655
+ .form-actions {
656
+ display: flex;
657
+ gap: 1rem;
658
+ justify-content: flex-end;
659
+ padding-top: 2rem;
660
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
661
+ }
662
+
663
+ /* Toast Notifications */
664
+ .toast-container {
665
+ position: fixed;
666
+ top: 1rem;
667
+ left: 1rem;
668
+ z-index: 10001;
669
+ display: flex;
670
+ flex-direction: column;
671
+ gap: 0.5rem;
672
+ }
673
+
674
+ .toast {
675
+ background: var(--card-bg);
676
+ border-radius: var(--border-radius-sm);
677
+ padding: 1rem 1.5rem;
678
+ box-shadow: var(--shadow-lg);
679
+ border-left: 4px solid;
680
+ display: flex;
681
+ align-items: center;
682
+ gap: 0.8rem;
683
+ min-width: 300px;
684
+ transform: translateX(-100%);
685
+ transition: all 0.3s ease;
686
+ }
687
+
688
+ .toast.show {
689
+ transform: translateX(0);
690
+ }
691
+
692
+ .toast.success { border-left-color: #10b981; }
693
+ .toast.error { border-left-color: #ef4444; }
694
+ .toast.warning { border-left-color: #f59e0b; }
695
+ .toast.info { border-left-color: #3b82f6; }
696
+
697
+ .toast-icon {
698
+ font-size: 1.2rem;
699
+ }
700
+
701
+ .toast.success .toast-icon { color: #10b981; }
702
+ .toast.error .toast-icon { color: #ef4444; }
703
+ .toast.warning .toast-icon { color: #f59e0b; }
704
+ .toast.info .toast-icon { color: #3b82f6; }
705
+
706
+ .toast-content {
707
+ flex: 1;
708
+ }
709
+
710
+ .toast-title {
711
+ font-weight: 600;
712
+ font-size: var(--font-size-sm);
713
+ margin-bottom: 0.2rem;
714
+ }
715
+
716
+ .toast-message {
717
+ font-size: var(--font-size-xs);
718
+ color: var(--text-secondary);
719
+ }
720
+
721
+ .toast-close {
722
+ background: none;
723
+ border: none;
724
+ color: var(--text-secondary);
725
+ cursor: pointer;
726
+ font-size: 1rem;
727
+ transition: var(--transition-fast);
728
+ }
729
+
730
+ .toast-close:hover {
731
+ color: var(--text-primary);
732
+ }
733
+
734
+ /* واکنش‌گرایی */
735
+ @media (max-width: 992px) {
736
+ .sidebar {
737
+ transform: translateX(100%);
738
+ transition: transform 0.3s ease;
739
+ }
740
+
741
+ .sidebar.open {
742
+ transform: translateX(0);
743
+ }
744
+
745
+ .main-content {
746
+ margin-right: 0;
747
+ width: 100%;
748
+ padding: 1rem;
749
+ }
750
+
751
+ .settings-layout {
752
+ grid-template-columns: 1fr;
753
+ gap: 1rem;
754
+ }
755
+
756
+ .settings-nav {
757
+ position: static;
758
+ order: 2;
759
+ }
760
+
761
+ .settings-content {
762
+ order: 1;
763
+ }
764
+
765
+ .form-row {
766
+ grid-template-columns: 1fr;
767
+ }
768
+ }
769
+
770
+ @media (max-width: 768px) {
771
+ .main-content {
772
+ padding: 0.8rem;
773
+ }
774
+
775
+ .settings-section {
776
+ padding: 1.5rem;
777
+ }
778
+
779
+ .info-cards {
780
+ grid-template-columns: 1fr;
781
+ }
782
+
783
+ .form-actions {
784
+ flex-direction: column;
785
+ }
786
+ }
787
+ </style>
788
+ </head>
789
+ <body>
790
+ <div class="dashboard-container">
791
+ <!-- سایدبار -->
792
+ <aside class="sidebar" id="sidebar">
793
+ <div class="sidebar-header">
794
+ <a href="/" class="logo">
795
+ <div class="logo-icon">
796
+ <i class="fas fa-scale-balanced"></i>
797
+ </div>
798
+ <div class="logo-text">سامانه حقوقی</div>
799
+ </a>
800
+ </div>
801
+
802
+ <nav>
803
+ <div class="nav-section">
804
+ <h6 class="nav-title">داشبورد</h6>
805
+ <ul class="nav-menu">
806
+ <li class="nav-item">
807
+ <a href="/" class="nav-link">
808
+ <i class="fas fa-chart-pie nav-icon"></i>
809
+ <span>نمای کلی</span>
810
+ </a>
811
+ </li>
812
+ </ul>
813
+ </div>
814
+
815
+ <div class="nav-section">
816
+ <h6 class="nav-title">مدیریت اسناد</h6>
817
+ <ul class="nav-menu">
818
+ <li class="nav-item">
819
+ <a href="/static/documents.html" class="nav-link">
820
+ <i class="fas fa-file-alt nav-icon"></i>
821
+ <span>مدیریت اسناد</span>
822
+ <span class="nav-badge" id="totalDocumentsBadge">6</span>
823
+ </a>
824
+ </li>
825
+
826
+ <li class="nav-item">
827
+ <a href="/static/upload.html" class="nav-link">
828
+ <i class="fas fa-cloud-upload-alt nav-icon"></i>
829
+ <span>آپلود فایل</span>
830
+ </a>
831
+ </li>
832
+
833
+ <li class="nav-item">
834
+ <a href="/static/search.html" class="nav-link">
835
+ <i class="fas fa-search nav-icon"></i>
836
+ <span>جستجو</span>
837
+ </a>
838
+ </li>
839
+ </ul>
840
+ </div>
841
+
842
+ <div class="nav-section">
843
+ <h6 class="nav-title">ابزارها</h6>
844
+ <ul class="nav-menu">
845
+ <li class="nav-item">
846
+ <a href="/static/scraping.html" class="nav-link">
847
+ <i class="fas fa-globe nav-icon"></i>
848
+ <span>استخراج محتوا</span>
849
+ </a>
850
+ </li>
851
+
852
+ <li class="nav-item">
853
+ <a href="/static/analytics.html" class="nav-link">
854
+ <i class="fas fa-chart-line nav-icon"></i>
855
+ <span>آمار و تحلیل</span>
856
+ </a>
857
+ </li>
858
+
859
+ <li class="nav-item">
860
+ <a href="/static/reports.html" class="nav-link">
861
+ <i class="fas fa-file-export nav-icon"></i>
862
+ <span>گزارش‌ها</span>
863
+ </a>
864
+ </li>
865
+ </ul>
866
+ </div>
867
+
868
+ <div class="nav-section">
869
+ <h6 class="nav-title">تنظیمات</h6>
870
+ <ul class="nav-menu">
871
+ <li class="nav-item">
872
+ <a href="/static/settings.html" class="nav-link active">
873
+ <i class="fas fa-cog nav-icon"></i>
874
+ <span>تنظیمات</span>
875
+ </a>
876
+ </li>
877
+ <li class="nav-item">
878
+ <a href="#" class="nav-link">
879
+ <i class="fas fa-sign-out-alt nav-icon"></i>
880
+ <span>خروج</span>
881
+ </a>
882
+ </li>
883
+ </ul>
884
+ </div>
885
+ </nav>
886
+ </aside>
887
+
888
+ <!-- محتوای اصلی -->
889
+ <main class="main-content">
890
+ <!-- هدر صفحه -->
891
+ <header class="page-header">
892
+ <h1 class="page-title">
893
+ <i class="fas fa-cog"></i>
894
+ تنظیمات سیستم
895
+ </h1>
896
+ <div class="page-actions">
897
+ <button type="button" class="btn btn-outline" onclick="exportSettings()">
898
+ <i class="fas fa-download"></i>
899
+ خروجی تنظیمات
900
+ </button>
901
+ <button type="button" class="btn btn-primary" onclick="saveAllSettings()">
902
+ <i class="fas fa-save"></i>
903
+ ذخیره همه تغییرات
904
+ </button>
905
+ </div>
906
+ </header>
907
+
908
+ <!-- تنظیمات اصلی -->
909
+ <div class="settings-layout">
910
+ <!-- منوی تنظیمات -->
911
+ <nav class="settings-nav">
912
+ <h3 class="settings-nav-title">دسته‌بندی تنظیمات</h3>
913
+ <ul class="settings-nav-list">
914
+ <li class="settings-nav-item">
915
+ <a href="#general" class="settings-nav-link active" onclick="showSection('general', this)">
916
+ <i class="fas fa-sliders-h settings-nav-icon"></i>
917
+ <span>تنظیمات عمومی</span>
918
+ </a>
919
+ </li>
920
+ <li class="settings-nav-item">
921
+ <a href="#ocr" class="settings-nav-link" onclick="showSection('ocr', this)">
922
+ <i class="fas fa-eye settings-nav-icon"></i>
923
+ <span>تنظیمات OCR</span>
924
+ </a>
925
+ </li>
926
+ <li class="settings-nav-item">
927
+ <a href="#storage" class="settings-nav-link" onclick="showSection('storage', this)">
928
+ <i class="fas fa-database settings-nav-icon"></i>
929
+ <span>ذخیره‌سازی</span>
930
+ </a>
931
+ </li>
932
+ <li class="settings-nav-item">
933
+ <a href="#notifications" class="settings-nav-link" onclick="showSection('notifications', this)">
934
+ <i class="fas fa-bell settings-nav-icon"></i>
935
+ <span>اعلان‌ها</span>
936
+ </a>
937
+ </li>
938
+ <li class="settings-nav-item">
939
+ <a href="#security" class="settings-nav-link" onclick="showSection('security', this)">
940
+ <i class="fas fa-shield-alt settings-nav-icon"></i>
941
+ <span>امنیت</span>
942
+ </a>
943
+ </li>
944
+ <li class="settings-nav-item">
945
+ <a href="#api" class="settings-nav-link" onclick="showSection('api', this)">
946
+ <i class="fas fa-plug settings-nav-icon"></i>
947
+ <span>API و ادغام</span>
948
+ </a>
949
+ </li>
950
+ <li class="settings-nav-item">
951
+ <a href="#backup" class="settings-nav-link" onclick="showSection('backup', this)">
952
+ <i class="fas fa-history settings-nav-icon"></i>
953
+ <span>پشتیبان‌گیری</span>
954
+ </a>
955
+ </li>
956
+ <li class="settings-nav-item">
957
+ <a href="#system" class="settings-nav-link" onclick="showSection('system', this)">
958
+ <i class="fas fa-server settings-nav-icon"></i>
959
+ <span>اطلاعات سیستم</span>
960
+ </a>
961
+ </li>
962
+ </ul>
963
+ </nav>
964
+
965
+ <!-- محتوای تنظیمات -->
966
+ <div class="settings-content">
967
+ <!-- تنظیمات عمومی -->
968
+ <section class="settings-section active" id="general">
969
+ <div class="section-header">
970
+ <h2 class="section-title">
971
+ <i class="fas fa-sliders-h"></i>
972
+ تنظیمات عمومی
973
+ </h2>
974
+ <p class="section-description">
975
+ تنظیمات کلی سیستم و رفتار پیش‌فرض برنامه را در این بخش تغییر دهید.
976
+ </p>
977
+ </div>
978
+
979
+ <form class="settings-form" onsubmit="saveGeneralSettings(event)">
980
+ <div class="form-group">
981
+ <div class="form-row">
982
+ <div>
983
+ <label class="form-label" for="systemLanguage">
984
+ <i class="fas fa-language"></i>
985
+ زبان سیستم
986
+ </label>
987
+ <select class="form-select" id="systemLanguage" name="language">
988
+ <option value="fa">فارسی</option>
989
+ <option value="en">انگلیسی</option>
990
+ <option value="ar">عربی</option>
991
+ </select>
992
+ <div class="form-description">زبان پیش‌فرض رابط کاربری</div>
993
+ </div>
994
+
995
+ <div>
996
+ <label class="form-label" for="timezone">
997
+ <i class="fas fa-clock"></i>
998
+ منطقه زمانی
999
+ </label>
1000
+ <select class="form-select" id="timezone" name="timezone">
1001
+ <option value="Asia/Tehran">تهران (UTC+3:30)</option>
1002
+ <option value="UTC">UTC (UTC+0:00)</option>
1003
+ <option value="Asia/Dubai">دبی (UTC+4:00)</option>
1004
+ </select>
1005
+ <div class="form-description">منطقه زمانی پیش‌فرض سیستم</div>
1006
+ </div>
1007
+ </div>
1008
+ </div>
1009
+
1010
+ <div class="form-group">
1011
+ <div class="switch-group">
1012
+ <div class="switch-info">
1013
+ <div class="switch-label">حالت تاریک</div>
1014
+ <div class="switch-description">فعال‌سازی حالت تاریک برای رابط کاربری</div>
1015
+ </div>
1016
+ <label class="switch">
1017
+ <input type="checkbox" id="darkMode" name="darkMode">
1018
+ <span class="slider"></span>
1019
+ </label>
1020
+ </div>
1021
+
1022
+ <div class="switch-group">
1023
+ <div class="switch-info">
1024
+ <div class="switch-label">اعلان‌های دسکتاپ</div>
1025
+ <div class="switch-description">نمایش اعلان‌ها در دسکتاپ سیستم‌عامل</div>
1026
+ </div>
1027
+ <label class="switch">
1028
+ <input type="checkbox" id="desktopNotifications" name="desktopNotifications" checked>
1029
+ <span class="slider"></span>
1030
+ </label>
1031
+ </div>
1032
+
1033
+ <div class="switch-group">
1034
+ <div class="switch-info">
1035
+ <div class="switch-label">ذخیره خودکار</div>
1036
+ <div class="switch-description">ذخیره خودکار تغییرات هر 5 دقیقه</div>
1037
+ </div>
1038
+ <label class="switch">
1039
+ <input type="checkbox" id="autoSave" name="autoSave" checked>
1040
+ <span class="slider"></span>
1041
+ </label>
1042
+ </div>
1043
+ </div>
1044
+
1045
+ <div class="form-actions">
1046
+ <button type="button" class="btn btn-outline">بازنشانی</button>
1047
+ <button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
1048
+ </div>
1049
+ </form>
1050
+ </section>
1051
+
1052
+ <!-- تنظیمات OCR -->
1053
+ <section class="settings-section" id="ocr">
1054
+ <div class="section-header">
1055
+ <h2 class="section-title">
1056
+ <i class="fas fa-eye"></i>
1057
+ تنظیمات OCR
1058
+ </h2>
1059
+ <p class="section-description">
1060
+ تنظیمات مربوط به سیستم تشخیص نویسه نوری (OCR) و کیفیت پردازش متن.
1061
+ </p>
1062
+ </div>
1063
+
1064
+ <form class="settings-form" onsubmit="saveOCRSettings(event)">
1065
+ <div class="form-group">
1066
+ <div class="form-row">
1067
+ <div>
1068
+ <label class="form-label" for="ocrEngine">
1069
+ <i class="fas fa-cogs"></i>
1070
+ موتور OCR
1071
+ </label>
1072
+ <select class="form-select" id="ocrEngine" name="engine">
1073
+ <option value="tesseract">Tesseract</option>
1074
+ <option value="google">Google Vision API</option>
1075
+ <option value="azure">Azure Computer Vision</option>
1076
+ <option value="aws">AWS Textract</option>
1077
+ </select>
1078
+ <div class="form-description">موتور پیش‌فرض تشخیص متن</div>
1079
+ </div>
1080
+
1081
+ <div>
1082
+ <label class="form-label" for="ocrLanguage">
1083
+ <i class="fas fa-language"></i>
1084
+ زبان پردازش
1085
+ </label>
1086
+ <select class="form-select" id="ocrLanguage" name="language">
1087
+ <option value="fas">فارسی</option>
1088
+ <option value="eng">انگلیسی</option>
1089
+ <option value="ara">عربی</option>
1090
+ <option value="auto">تشخیص خودکار</option>
1091
+ </select>
1092
+ <div class="form-description">زبان اصلی اسناد برای پردازش</div>
1093
+ </div>
1094
+ </div>
1095
+ </div>
1096
+
1097
+ <div class="form-group">
1098
+ <div class="form-row">
1099
+ <div>
1100
+ <label class="form-label" for="ocrQuality">
1101
+ <i class="fas fa-sliders-h"></i>
1102
+ کیفیت پردازش
1103
+ </label>
1104
+ <select class="form-select" id="ocrQuality" name="quality">
1105
+ <option value="fast">سریع</option>
1106
+ <option value="balanced" selected>متعادل</option>
1107
+ <option value="accurate">دقیق</option>
1108
+ </select>
1109
+ <div class="form-description">تعادل بین سرعت و دقت</div>
1110
+ </div>
1111
+
1112
+ <div>
1113
+ <label class="form-label" for="confidenceThreshold">
1114
+ <i class="fas fa-percentage"></i>
1115
+ آستانه اطمینان
1116
+ </label>
1117
+ <input type="number" class="form-input" id="confidenceThreshold" name="confidence" value="75" min="0" max="100">
1118
+ <div class="form-description">حداقل درصد اطمینان برای پذیرش متن</div>
1119
+ </div>
1120
+ </div>
1121
+ </div>
1122
+
1123
+ <div class="form-group">
1124
+ <div class="switch-group">
1125
+ <div class="switch-info">
1126
+ <div class="switch-label">پیش‌پردازش تصویر</div>
1127
+ <div class="switch-description">بهبود کیفیت تصویر قبل از پردازش OCR</div>
1128
+ </div>
1129
+ <label class="switch">
1130
+ <input type="checkbox" id="imagePreprocessing" name="preprocessing" checked>
1131
+ <span class="slider"></span>
1132
+ </label>
1133
+ </div>
1134
+
1135
+ <div class="switch-group">
1136
+ <div class="switch-info">
1137
+ <div class="switch-label">تشخیص جدول</div>
1138
+ <div class="switch-description">تشخیص و استخراج جداول از اسناد</div>
1139
+ </div>
1140
+ <label class="switch">
1141
+ <input type="checkbox" id="tableDetection" name="tables" checked>
1142
+ <span class="slider"></span>
1143
+ </label>
1144
+ </div>
1145
+ </div>
1146
+
1147
+ <div class="form-actions">
1148
+ <button type="button" class="btn btn-outline">بازنشانی</button>
1149
+ <button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
1150
+ </div>
1151
+ </form>
1152
+ </section>
1153
+
1154
+ <!-- تنظیمات ذخیره‌سازی -->
1155
+ <section class="settings-section" id="storage">
1156
+ <div class="section-header">
1157
+ <h2 class="section-title">
1158
+ <i class="fas fa-database"></i>
1159
+ تنظیمات ذخیره‌سازی
1160
+ </h2>
1161
+ <p class="section-description">
1162
+ مدیریت فضای ذخیره‌سازی، پایگاه داده و فایل‌های سیستم.
1163
+ </p>
1164
+ </div>
1165
+
1166
+ <div class="info-cards">
1167
+ <div class="info-card">
1168
+ <div class="info-card-icon">
1169
+ <i class="fas fa-hdd" style="color: #3b82f6;"></i>
1170
+ </div>
1171
+ <div class="info-card-title">فضای کل</div>
1172
+ <div class="info-card-value">500 GB</div>
1173
+ </div>
1174
+
1175
+ <div class="info-card">
1176
+ <div class="info-card-icon">
1177
+ <i class="fas fa-chart-pie" style="color: #10b981;"></i>
1178
+ </div>
1179
+ <div class="info-card-title">استفاده شده</div>
1180
+ <div class="info-card-value">127 GB</div>
1181
+ </div>
1182
+
1183
+ <div class="info-card">
1184
+ <div class="info-card-icon">
1185
+ <i class="fas fa-file-alt" style="color: #f59e0b;"></i>
1186
+ </div>
1187
+ <div class="info-card-title">تعداد فایل‌ها</div>
1188
+ <div class="info-card-value">1,247</div>
1189
+ </div>
1190
+
1191
+ <div class="info-card">
1192
+ <div class="info-card-icon">
1193
+ <i class="fas fa-clock" style="color: #ef4444;"></i>
1194
+ </div>
1195
+ <div class="info-card-title">آخرین پشتیبان</div>
1196
+ <div class="info-card-value">2 ساعت پیش</div>
1197
+ </div>
1198
+ </div>
1199
+
1200
+ <form class="settings-form" onsubmit="saveStorageSettings(event)">
1201
+ <div class="form-group">
1202
+ <div class="form-row">
1203
+ <div>
1204
+ <label class="form-label" for="maxFileSize">
1205
+ <i class="fas fa-weight-hanging"></i>
1206
+ حداکثر اندازه فایل (MB)
1207
+ </label>
1208
+ <input type="number" class="form-input" id="maxFileSize" name="maxFileSize" value="50" min="1" max="500">
1209
+ <div class="form-description">حداکثر اندازه مجاز برای آپلود فایل</div>
1210
+ </div>
1211
+
1212
+ <div>
1213
+ <label class="form-label" for="retentionPeriod">
1214
+ <i class="fas fa-calendar"></i>
1215
+ مدت نگهداری (روز)
1216
+ </label>
1217
+ <input type="number" class="form-input" id="retentionPeriod" name="retention" value="365" min="30" max="3650">
1218
+ <div class="form-description">مدت نگهداری فایل‌ها قبل از حذف خودکار</div>
1219
+ </div>
1220
+ </div>
1221
+ </div>
1222
+
1223
+ <div class="form-group">
1224
+ <div class="switch-group">
1225
+ <div class="switch-info">
1226
+ <div class="switch-label">فشرده‌سازی خودکار</div>
1227
+ <div class="switch-description">فشرده‌سازی فایل‌ها برای صرفه‌جویی در فضا</div>
1228
+ </div>
1229
+ <label class="switch">
1230
+ <input type="checkbox" id="autoCompression" name="compression" checked>
1231
+ <span class="slider"></span>
1232
+ </label>
1233
+ </div>
1234
+
1235
+ <div class="switch-group">
1236
+ <div class="switch-info">
1237
+ <div class="switch-label">حذف خودکار فایل‌های موقت</div>
1238
+ <div class="switch-description">پاک‌سازی روزانه فایل‌های موقت و کش</div>
1239
+ </div>
1240
+ <label class="switch">
1241
+ <input type="checkbox" id="autoCleanup" name="cleanup" checked>
1242
+ <span class="slider"></span>
1243
+ </label>
1244
+ </div>
1245
+ </div>
1246
+
1247
+ <div class="form-actions">
1248
+ <button type="button" class="btn btn-danger" onclick="cleanupStorage()">پاک‌سازی فوری</button>
1249
+ <button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
1250
+ </div>
1251
+ </form>
1252
+ </section>
1253
+
1254
+ <!-- تنظیمات اعلان‌ها -->
1255
+ <section class="settings-section" id="notifications">
1256
+ <div class="section-header">
1257
+ <h2 class="section-title">
1258
+ <i class="fas fa-bell"></i>
1259
+ تنظیمات اعلان‌ها
1260
+ </h2>
1261
+ <p class="section-description">
1262
+ مدیریت نوع، زمان و روش ارسال اعلان‌های سیستم.
1263
+ </p>
1264
+ </div>
1265
+
1266
+ <form class="settings-form" onsubmit="saveNotificationSettings(event)">
1267
+ <div class="form-group">
1268
+ <div class="switch-group">
1269
+ <div class="switch-info">
1270
+ <div class="switch-label">اعلان پایان پردازش</div>
1271
+ <div class="switch-description">اعلان زمانی که پردازش سند تکمیل شود</div>
1272
+ </div>
1273
+ <label class="switch">
1274
+ <input type="checkbox" id="processCompleteNotif" name="processComplete" checked>
1275
+ <span class="slider"></span>
1276
+ </label>
1277
+ </div>
1278
+
1279
+ <div class="switch-group">
1280
+ <div class="switch-info">
1281
+ <div class="switch-label">اعلان خطاها</div>
1282
+ <div class="switch-description">اعلان در صورت بروز خطا در پردازش</div>
1283
+ </div>
1284
+ <label class="switch">
1285
+ <input type="checkbox" id="errorNotif" name="errors" checked>
1286
+ <span class="slider"></span>
1287
+ </label>
1288
+ </div>
1289
+
1290
+ <div class="switch-group">
1291
+ <div class="switch-info">
1292
+ <div class="switch-label">اعلان بروزرسانی</div>
1293
+ <div class="switch-description">اعلان زمانی که نسخه جدید در دسترس باشد</div>
1294
+ </div>
1295
+ <label class="switch">
1296
+ <input type="checkbox" id="updateNotif" name="updates" checked>
1297
+ <span class="slider"></span>
1298
+ </label>
1299
+ </div>
1300
+
1301
+ <div class="switch-group">
1302
+ <div class="switch-info">
1303
+ <div class="switch-label">اعلان گزارش‌های روزانه</div>
1304
+ <div class="switch-description">ارسال خلاصه عملکرد روزانه</div>
1305
+ </div>
1306
+ <label class="switch">
1307
+ <input type="checkbox" id="dailyReportNotif" name="dailyReport">
1308
+ <span class="slider"></span>
1309
+ </label>
1310
+ </div>
1311
+ </div>
1312
+
1313
+ <div class="form-group">
1314
+ <div class="form-row">
1315
+ <div>
1316
+ <label class="form-label" for="notificationEmail">
1317
+ <i class="fas fa-envelope"></i>
1318
+ ایمیل اعلان‌ها
1319
+ </label>
1320
+ <input type="email" class="form-input" id="notificationEmail" name="email" placeholder="[email protected]">
1321
+ <div class="form-description">آدرس ایمیل برای دریافت اعلان‌ها</div>
1322
+ </div>
1323
+
1324
+ <div>
1325
+ <label class="form-label" for="notificationTime">
1326
+ <i class="fas fa-clock"></i>
1327
+ ساعت ارسال گزارش روزانه
1328
+ </label>
1329
+ <input type="time" class="form-input" id="notificationTime" name="time" value="09:00">
1330
+ <div class="form-description">ساعت ارسال گزارش روزانه</div>
1331
+ </div>
1332
+ </div>
1333
+ </div>
1334
+
1335
+ <div class="form-actions">
1336
+ <button type="button" class="btn btn-outline">تست اعلان</button>
1337
+ <button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
1338
+ </div>
1339
+ </form>
1340
+ </section>
1341
+
1342
+ <!-- تنظیمات امنیت -->
1343
+ <section class="settings-section" id="security">
1344
+ <div class="section-header">
1345
+ <h2 class="section-title">
1346
+ <i class="fas fa-shield-alt"></i>
1347
+ تنظیمات امنیت
1348
+ </h2>
1349
+ <p class="section-description">
1350
+ تنظیمات مربوط به امنیت سیستم، احراز هویت و کنترل دسترسی.
1351
+ </p>
1352
+ </div>
1353
+
1354
+ <form class="settings-form" onsubmit="saveSecuritySettings(event)">
1355
+ <div class="form-group">
1356
+ <div class="form-row">
1357
+ <div>
1358
+ <label class="form-label" for="sessionTimeout">
1359
+ <i class="fas fa-clock"></i>
1360
+ مدت انقضای نشست (دقیقه)
1361
+ </label>
1362
+ <input type="number" class="form-input" id="sessionTimeout" name="sessionTimeout" value="60" min="15" max="480">
1363
+ <div class="form-description">مدت زمان انقضای خودکار نشست کاربر</div>
1364
+ </div>
1365
+
1366
+ <div>
1367
+ <label class="form-label" for="maxLoginAttempts">
1368
+ <i class="fas fa-lock"></i>
1369
+ حداکثر تلاش ورود
1370
+ </label>
1371
+ <input type="number" class="form-input" id="maxLoginAttempts" name="maxAttempts" value="5" min="3" max="20">
1372
+ <div class="form-description">تعداد تلاش مجاز برای ورود قبل از مسدودسازی</div>
1373
+ </div>
1374
+ </div>
1375
+ </div>
1376
+
1377
+ <div class="form-group">
1378
+ <div class="switch-group">
1379
+ <div class="switch-info">
1380
+ <div class="switch-label">احراز هویت دو مرحله‌ای</div>
1381
+ <div class="switch-description">فعال‌سازی 2FA برای امنیت بیشتر</div>
1382
+ </div>
1383
+ <label class="switch">
1384
+ <input type="checkbox" id="twoFactorAuth" name="twoFactor">
1385
+ <span class="slider"></span>
1386
+ </label>
1387
+ </div>
1388
+
1389
+ <div class="switch-group">
1390
+ <div class="switch-info">
1391
+ <div class="switch-label">رمزنگاری فایل‌ها</div>
1392
+ <div class="switch-description">رمزنگاری خودکار فایل‌های آپلود شده</div>
1393
+ </div>
1394
+ <label class="switch">
1395
+ <input type="checkbox" id="fileEncryption" name="encryption" checked>
1396
+ <span class="slider"></span>
1397
+ </label>
1398
+ </div>
1399
+
1400
+ <div class="switch-group">
1401
+ <div class="switch-info">
1402
+ <div class="switch-label">لاگ فعالیت‌ها</div>
1403
+ <div class="switch-description">ثبت کامل فعالیت‌های کاربران</div>
1404
+ </div>
1405
+ <label class="switch">
1406
+ <input type="checkbox" id="activityLogging" name="logging" checked>
1407
+ <span class="slider"></span>
1408
+ </label>
1409
+ </div>
1410
+ </div>
1411
+
1412
+ <div class="form-actions">
1413
+ <button type="button" class="btn btn-danger" onclick="clearSecurityLogs()">پاک کردن لاگ‌ها</button>
1414
+ <button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
1415
+ </div>
1416
+ </form>
1417
+ </section>
1418
+
1419
+ <!-- تنظیمات API -->
1420
+ <section class="settings-section" id="api">
1421
+ <div class="section-header">
1422
+ <h2 class="section-title">
1423
+ <i class="fas fa-plug"></i>
1424
+ API و ادغام
1425
+ </h2>
1426
+ <p class="section-description">
1427
+ مدیریت کلیدهای API، وب‌هوک‌ها و ادغام با سیستم‌های خارجی.
1428
+ </p>
1429
+ </div>
1430
+
1431
+ <form class="settings-form" onsubmit="saveAPISettings(event)">
1432
+ <div class="form-group">
1433
+ <div class="form-row">
1434
+ <div>
1435
+ <label class="form-label" for="apiKey">
1436
+ <i class="fas fa-key"></i>
1437
+ کلید API اصلی
1438
+ </label>
1439
+ <input type="password" class="form-input" id="apiKey" name="apiKey" value="sk_****************************" readonly>
1440
+ <div class="form-description">کلید API برای دسترسی به خدمات</div>
1441
+ </div>
1442
+
1443
+ <div>
1444
+ <label class="form-label" for="webhookUrl">
1445
+ <i class="fas fa-link"></i>
1446
+ آدرس Webhook
1447
+ </label>
1448
+ <input type="url" class="form-input" id="webhookUrl" name="webhook" placeholder="https://example.com/webhook">
1449
+ <div class="form-description">آدرس برای ارسال رویدادها</div>
1450
+ </div>
1451
+ </div>
1452
+ </div>
1453
+
1454
+ <div class="form-group">
1455
+ <label class="form-label">
1456
+ <i class="fas fa-cogs"></i>
1457
+ کلیدهای API خارجی
1458
+ </label>
1459
+ <table class="settings-table">
1460
+ <thead>
1461
+ <tr>
1462
+ <th>سرویس</th>
1463
+ <th>کلید API</th>
1464
+ <th>وضعیت</th>
1465
+ <th>عملیات</th>
1466
+ </tr>
1467
+ </thead>
1468
+ <tbody>
1469
+ <tr>
1470
+ <td>Google Vision API</td>
1471
+ <td>AIza**********************</td>
1472
+ <td><span style="color: #10b981;">فعال</span></td>
1473
+ <td>
1474
+ <button type="button" class="btn btn-outline btn-sm">ویرایش</button>
1475
+ </td>
1476
+ </tr>
1477
+ <tr>
1478
+ <td>Azure Computer Vision</td>
1479
+ <td>غیرفعال</td>
1480
+ <td><span style="color: #ef4444;">غیرفعال</span></td>
1481
+ <td>
1482
+ <button type="button" class="btn btn-primary btn-sm">تنظیم</button>
1483
+ </td>
1484
+ </tr>
1485
+ <tr>
1486
+ <td>AWS Textract</td>
1487
+ <td>غیرفعال</td>
1488
+ <td><span style="color: #ef4444;">غیرفعال</span></td>
1489
+ <td>
1490
+ <button type="button" class="btn btn-primary btn-sm">تنظیم</button>
1491
+ </td>
1492
+ </tr>
1493
+ </tbody>
1494
+ </table>
1495
+ </div>
1496
+
1497
+ <div class="form-actions">
1498
+ <button type="button" class="btn btn-outline" onclick="generateNewAPIKey()">تولید کلید جدید</button>
1499
+ <button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
1500
+ </div>
1501
+ </form>
1502
+ </section>
1503
+
1504
+ <!-- تنظیمات پشتیبان‌گیری -->
1505
+ <section class="settings-section" id="backup">
1506
+ <div class="section-header">
1507
+ <h2 class="section-title">
1508
+ <i class="fas fa-history"></i>
1509
+ پشتیبان‌گیری و بازیابی
1510
+ </h2>
1511
+ <p class="section-description">
1512
+ مدیریت پشتیبان‌گیری خودکار، بازیابی داده‌ها و تنظیمات مرتبط.
1513
+ </p>
1514
+ </div>
1515
+
1516
+ <div class="upload-zone" onclick="document.getElementById('backupFile').click()">
1517
+ <div class="upload-icon">
1518
+ <i class="fas fa-cloud-upload-alt"></i>
1519
+ </div>
1520
+ <div class="upload-text">بازیابی از فایل پشتیبان</div>
1521
+ <div class="upload-hint">فایل .backup را اینجا رها کنید یا کلیک کنید</div>
1522
+ <input type="file" id="backupFile" accept=".backup" style="display: none;" onchange="restoreBackup(this)">
1523
+ </div>
1524
+
1525
+ <form class="settings-form" onsubmit="saveBackupSettings(event)">
1526
+ <div class="form-group">
1527
+ <div class="form-row">
1528
+ <div>
1529
+ <label class="form-label" for="backupFrequency">
1530
+ <i class="fas fa-clock"></i>
1531
+ دوره پشتیبان‌گیری
1532
+ </label>
1533
+ <select class="form-select" id="backupFrequency" name="frequency">
1534
+ <option value="daily" selected>روزانه</option>
1535
+ <option value="weekly">هفتگی</option>
1536
+ <option value="monthly">ماهانه</option>
1537
+ <option value="manual">دستی</option>
1538
+ </select>
1539
+ <div class="form-description">دوره زمانی پشتیبان‌گیری خودکار</div>
1540
+ </div>
1541
+
1542
+ <div>
1543
+ <label class="form-label" for="backupRetention">
1544
+ <i class="fas fa-calendar"></i>
1545
+ نگهداری پشتیبان (روز)
1546
+ </label>
1547
+ <input type="number" class="form-input" id="backupRetention" name="retention" value="30" min="7" max="365">
1548
+ <div class="form-description">مدت نگهداری فایل‌های پشتیبان</div>
1549
+ </div>
1550
+ </div>
1551
+ </div>
1552
+
1553
+ <div class="form-group">
1554
+ <div class="switch-group">
1555
+ <div class="switch-info">
1556
+ <div class="switch-label">پشتیبان‌گیری خودکار</div>
1557
+ <div class="switch-description">فعال‌سازی پشتیبان‌گیری خودکار بر اساس دوره تعیین شده</div>
1558
+ </div>
1559
+ <label class="switch">
1560
+ <input type="checkbox" id="autoBackup" name="autoBackup" checked>
1561
+ <span class="slider"></span>
1562
+ </label>
1563
+ </div>
1564
+
1565
+ <div class="switch-group">
1566
+ <div class="switch-info">
1567
+ <div class="switch-label">فشرده‌سازی پشتیبان</div>
1568
+ <div class="switch-description">فشرده‌سازی فایل‌های پشتیبان برای کاهش حجم</div>
1569
+ </div>
1570
+ <label class="switch">
1571
+ <input type="checkbox" id="compressBackup" name="compress" checked>
1572
+ <span class="slider"></span>
1573
+ </label>
1574
+ </div>
1575
+ </div>
1576
+
1577
+ <div class="form-actions">
1578
+ <button type="button" class="btn btn-success" onclick="createBackup()">ایجاد پشتیبان فوری</button>
1579
+ <button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
1580
+ </div>
1581
+ </form>
1582
+ </section>
1583
+
1584
+ <!-- اطلاعات سیستم -->
1585
+ <section class="settings-section" id="system">
1586
+ <div class="section-header">
1587
+ <h2 class="section-title">
1588
+ <i class="fas fa-server"></i>
1589
+ اطلاعات سیستم
1590
+ </h2>
1591
+ <p class="section-description">
1592
+ اطلاعات فنی سیستم، نسخه برنامه و وضعیت سرویس‌ها.
1593
+ </p>
1594
+ </div>
1595
+
1596
+ <div class="info-cards">
1597
+ <div class="info-card">
1598
+ <div class="info-card-icon">
1599
+ <i class="fas fa-microchip" style="color: #3b82f6;"></i>
1600
+ </div>
1601
+ <div class="info-card-title">استفاده CPU</div>
1602
+ <div class="info-card-value">23%</div>
1603
+ </div>
1604
+
1605
+ <div class="info-card">
1606
+ <div class="info-card-icon">
1607
+ <i class="fas fa-memory" style="color: #10b981;"></i>
1608
+ </div>
1609
+ <div class="info-card-title">استفاده RAM</div>
1610
+ <div class="info-card-value">1.2 GB</div>
1611
+ </div>
1612
+
1613
+ <div class="info-card">
1614
+ <div class="info-card-icon">
1615
+ <i class="fas fa-network-wired" style="color: #f59e0b;"></i>
1616
+ </div>
1617
+ <div class="info-card-title">وضعیت شبکه</div>
1618
+ <div class="info-card-value">آنلاین</div>
1619
+ </div>
1620
+
1621
+ <div class="info-card">
1622
+ <div class="info-card-icon">
1623
+ <i class="fas fa-code-branch" style="color: #ef4444;"></i>
1624
+ </div>
1625
+ <div class="info-card-title">نسخه سیستم</div>
1626
+ <div class="info-card-value">v2.1.4</div>
1627
+ </div>
1628
+ </div>
1629
+
1630
+ <table class="settings-table">
1631
+ <thead>
1632
+ <tr>
1633
+ <th>جزئیات سیستم</th>
1634
+ <th>مقدار</th>
1635
+ </tr>
1636
+ </thead>
1637
+ <tbody>
1638
+ <tr>
1639
+ <td>سیستم‌عامل</td>
1640
+ <td>Ubuntu 22.04 LTS</td>
1641
+ </tr>
1642
+ <tr>
1643
+ <td>نسخه Python</td>
1644
+ <td>3.11.2</td>
1645
+ </tr>
1646
+ <tr>
1647
+ <td>پایگاه داده</td>
1648
+ <td>PostgreSQL 15.2</td>
1649
+ </tr>
1650
+ <tr>
1651
+ <td>وب سرور</td>
1652
+ <td>Nginx 1.18.0</td>
1653
+ </tr>
1654
+ <tr>
1655
+ <td>زمان راه‌اندازی</td>
1656
+ <td>72 ساعت و 15 دقیقه</td>
1657
+ </tr>
1658
+ <tr>
1659
+ <td>آخرین بروزرسانی</td>
1660
+ <td>2024-01-15 14:30:00</td>
1661
+ </tr>
1662
+ </tbody>
1663
+ </table>
1664
+
1665
+ <div class="form-actions">
1666
+ <button type="button" class="btn btn-outline" onclick="checkUpdates()">بررسی بروزرسانی</button>
1667
+ <button type="button" class="btn btn-danger" onclick="restartSystem()">راه‌اندازی مجدد</button>
1668
+ </div>
1669
+ </section>
1670
+ </div>
1671
+ </div>
1672
+ </main>
1673
+ </div>
1674
+
1675
+ <!-- Toast Container -->
1676
+ <div class="toast-container" id="toastContainer"></div>
1677
+
1678
+ <script>
1679
+ // Global variables
1680
+ let isOnline = false;
1681
+ let settings = {};
1682
+
1683
+ // Initialize page
1684
+ document.addEventListener('DOMContentLoaded', function() {
1685
+ console.log('⚙️ Settings page loading...');
1686
+ initializeSettingsPage();
1687
+ });
1688
+
1689
+ async function initializeSettingsPage() {
1690
+ try {
1691
+ // Test backend connection
1692
+ isOnline = await testConnection();
1693
+
1694
+ // Load current settings
1695
+ await loadCurrentSettings();
1696
+
1697
+ showToast('صفحه تنظیمات آماده است', 'success', 'آماده');
1698
+
1699
+ } catch (error) {
1700
+ console.error('Failed to initialize settings page:', error);
1701
+
1702
+ // Fallback mode
1703
+ isOnline = false;
1704
+ loadDefaultSettings();
1705
+
1706
+ showToast('حالت آفلاین فعال است', 'warning', 'اتصال ناموفق');
1707
+ }
1708
+ }
1709
+
1710
+ async function testConnection() {
1711
+ try {
1712
+ await window.legalAPI.healthCheck();
1713
+ return true;
1714
+ } catch (error) {
1715
+ return false;
1716
+ }
1717
+ }
1718
+
1719
+ async function loadCurrentSettings() {
1720
+ try {
1721
+ if (isOnline) {
1722
+ settings = await window.legalAPI.getSettings();
1723
+ } else {
1724
+ settings = getDefaultSettings();
1725
+ }
1726
+
1727
+ // Apply settings to form elements
1728
+ applySettingsToForms();
1729
+
1730
+ } catch (error) {
1731
+ console.error('Failed to load settings:', error);
1732
+ settings = getDefaultSettings();
1733
+ applySettingsToForms();
1734
+ }
1735
+ }
1736
+
1737
+ function loadDefaultSettings() {
1738
+ settings = getDefaultSettings();
1739
+ applySettingsToForms();
1740
+ }
1741
+
1742
+ function getDefaultSettings() {
1743
+ return {
1744
+ general: {
1745
+ language: 'fa',
1746
+ timezone: 'Asia/Tehran',
1747
+ darkMode: false,
1748
+ desktopNotifications: true,
1749
+ autoSave: true
1750
+ },
1751
+ ocr: {
1752
+ engine: 'tesseract',
1753
+ language: 'fas',
1754
+ quality: 'balanced',
1755
+ confidence: 75,
1756
+ preprocessing: true,
1757
+ tables: true
1758
+ },
1759
+ storage: {
1760
+ maxFileSize: 50,
1761
+ retention: 365,
1762
+ compression: true,
1763
+ cleanup: true
1764
+ },
1765
+ notifications: {
1766
+ processComplete: true,
1767
+ errors: true,
1768
+ updates: true,
1769
+ dailyReport: false,
1770
+ email: '',
1771
+ time: '09:00'
1772
+ },
1773
+ security: {
1774
+ sessionTimeout: 60,
1775
+ maxAttempts: 5,
1776
+ twoFactor: false,
1777
+ encryption: true,
1778
+ logging: true
1779
+ },
1780
+ api: {
1781
+ apiKey: 'sk_****************************',
1782
+ webhook: ''
1783
+ },
1784
+ backup: {
1785
+ frequency: 'daily',
1786
+ retention: 30,
1787
+ autoBackup: true,
1788
+ compress: true
1789
+ }
1790
+ };
1791
+ }
1792
+
1793
+ function applySettingsToForms() {
1794
+ // Apply general settings
1795
+ if (settings.general) {
1796
+ setFormValue('systemLanguage', settings.general.language);
1797
+ setFormValue('timezone', settings.general.timezone);
1798
+ setFormValue('darkMode', settings.general.darkMode);
1799
+ setFormValue('desktopNotifications', settings.general.desktopNotifications);
1800
+ setFormValue('autoSave', settings.general.autoSave);
1801
+ }
1802
+
1803
+ // Apply OCR settings
1804
+ if (settings.ocr) {
1805
+ setFormValue('ocrEngine', settings.ocr.engine);
1806
+ setFormValue('ocrLanguage', settings.ocr.language);
1807
+ setFormValue('ocrQuality', settings.ocr.quality);
1808
+ setFormValue('confidenceThreshold', settings.ocr.confidence);
1809
+ setFormValue('imagePreprocessing', settings.ocr.preprocessing);
1810
+ setFormValue('tableDetection', settings.ocr.tables);
1811
+ }
1812
+
1813
+ // Apply storage settings
1814
+ if (settings.storage) {
1815
+ setFormValue('maxFileSize', settings.storage.maxFileSize);
1816
+ setFormValue('retentionPeriod', settings.storage.retention);
1817
+ setFormValue('autoCompression', settings.storage.compression);
1818
+ setFormValue('autoCleanup', settings.storage.cleanup);
1819
+ }
1820
+
1821
+ // Apply notification settings
1822
+ if (settings.notifications) {
1823
+ setFormValue('processCompleteNotif', settings.notifications.processComplete);
1824
+ setFormValue('errorNotif', settings.notifications.errors);
1825
+ setFormValue('updateNotif', settings.notifications.updates);
1826
+ setFormValue('dailyReportNotif', settings.notifications.dailyReport);
1827
+ setFormValue('notificationEmail', settings.notifications.email);
1828
+ setFormValue('notificationTime', settings.notifications.time);
1829
+ }
1830
+
1831
+ // Apply security settings
1832
+ if (settings.security) {
1833
+ setFormValue('sessionTimeout', settings.security.sessionTimeout);
1834
+ setFormValue('maxLoginAttempts', settings.security.maxAttempts);
1835
+ setFormValue('twoFactorAuth', settings.security.twoFactor);
1836
+ setFormValue('fileEncryption', settings.security.encryption);
1837
+ setFormValue('activityLogging', settings.security.logging);
1838
+ }
1839
+
1840
+ // Apply API settings
1841
+ if (settings.api) {
1842
+ setFormValue('apiKey', settings.api.apiKey);
1843
+ setFormValue('webhookUrl', settings.api.webhook);
1844
+ }
1845
+
1846
+ // Apply backup settings
1847
+ if (settings.backup) {
1848
+ setFormValue('backupFrequency', settings.backup.frequency);
1849
+ setFormValue('backupRetention', settings.backup.retention);
1850
+ setFormValue('autoBackup', settings.backup.autoBackup);
1851
+ setFormValue('compressBackup', settings.backup.compress);
1852
+ }
1853
+ }
1854
+
1855
+ function setFormValue(elementId, value) {
1856
+ const element = document.getElementById(elementId);
1857
+ if (!element) return;
1858
+
1859
+ if (element.type === 'checkbox') {
1860
+ element.checked = value;
1861
+ } else {
1862
+ element.value = value;
1863
+ }
1864
+ }
1865
+
1866
+ // Navigation functions
1867
+ function showSection(sectionId, linkElement) {
1868
+ // Hide all sections
1869
+ document.querySelectorAll('.settings-section').forEach(section => {
1870
+ section.classList.remove('active');
1871
+ });
1872
+
1873
+ // Remove active class from all nav links
1874
+ document.querySelectorAll('.settings-nav-link').forEach(link => {
1875
+ link.classList.remove('active');
1876
+ });
1877
+
1878
+ // Show selected section
1879
+ const section = document.getElementById(sectionId);
1880
+ if (section) {
1881
+ section.classList.add('active');
1882
+ }
1883
+
1884
+ // Add active class to clicked nav link
1885
+ if (linkElement) {
1886
+ linkElement.classList.add('active');
1887
+ }
1888
+ }
1889
+
1890
+ // Form submission handlers
1891
+ function saveGeneralSettings(event) {
1892
+ event.preventDefault();
1893
+ const form = event.target;
1894
+ const formData = new FormData(form);
1895
+
1896
+ settings.general = {
1897
+ language: formData.get('language'),
1898
+ timezone: formData.get('timezone'),
1899
+ darkMode: formData.get('darkMode') === 'on',
1900
+ desktopNotifications: formData.get('desktopNotifications') === 'on',
1901
+ autoSave: formData.get('autoSave') === 'on'
1902
+ };
1903
+
1904
+ saveSettings('general');
1905
+ }
1906
+
1907
+ function saveOCRSettings(event) {
1908
+ event.preventDefault();
1909
+ const form = event.target;
1910
+ const formData = new FormData(form);
1911
+
1912
+ settings.ocr = {
1913
+ engine: formData.get('engine'),
1914
+ language: formData.get('language'),
1915
+ quality: formData.get('quality'),
1916
+ confidence: parseInt(formData.get('confidence')),
1917
+ preprocessing: formData.get('preprocessing') === 'on',
1918
+ tables: formData.get('tables') === 'on'
1919
+ };
1920
+
1921
+ saveSettings('ocr');
1922
+ }
1923
+
1924
+ function saveStorageSettings(event) {
1925
+ event.preventDefault();
1926
+ const form = event.target;
1927
+ const formData = new FormData(form);
1928
+
1929
+ settings.storage = {
1930
+ maxFileSize: parseInt(formData.get('maxFileSize')),
1931
+ retention: parseInt(formData.get('retention')),
1932
+ compression: formData.get('compression') === 'on',
1933
+ cleanup: formData.get('cleanup') === 'on'
1934
+ };
1935
+
1936
+ saveSettings('storage');
1937
+ }
1938
+
1939
+ function saveNotificationSettings(event) {
1940
+ event.preventDefault();
1941
+ const form = event.target;
1942
+ const formData = new FormData(form);
1943
+
1944
+ settings.notifications = {
1945
+ processComplete: formData.get('processComplete') === 'on',
1946
+ errors: formData.get('errors') === 'on',
1947
+ updates: formData.get('updates') === 'on',
1948
+ dailyReport: formData.get('dailyReport') === 'on',
1949
+ email: formData.get('email'),
1950
+ time: formData.get('time')
1951
+ };
1952
+
1953
+ saveSettings('notifications');
1954
+ }
1955
+
1956
+ function saveSecuritySettings(event) {
1957
+ event.preventDefault();
1958
+ const form = event.target;
1959
+ const formData = new FormData(form);
1960
+
1961
+ settings.security = {
1962
+ sessionTimeout: parseInt(formData.get('sessionTimeout')),
1963
+ maxAttempts: parseInt(formData.get('maxAttempts')),
1964
+ twoFactor: formData.get('twoFactor') === 'on',
1965
+ encryption: formData.get('encryption') === 'on',
1966
+ logging: formData.get('logging') === 'on'
1967
+ };
1968
+
1969
+ saveSettings('security');
1970
+ }
1971
+
1972
+ function saveAPISettings(event) {
1973
+ event.preventDefault();
1974
+ const form = event.target;
1975
+ const formData = new FormData(form);
1976
+
1977
+ settings.api = {
1978
+ apiKey: formData.get('apiKey'),
1979
+ webhook: formData.get('webhook')
1980
+ };
1981
+
1982
+ saveSettings('api');
1983
+ }
1984
+
1985
+ function saveBackupSettings(event) {
1986
+ event.preventDefault();
1987
+ const form = event.target;
1988
+ const formData = new FormData(form);
1989
+
1990
+ settings.backup = {
1991
+ frequency: formData.get('frequency'),
1992
+ retention: parseInt(formData.get('retention')),
1993
+ autoBackup: formData.get('autoBackup') === 'on',
1994
+ compress: formData.get('compress') === 'on'
1995
+ };
1996
+
1997
+ saveSettings('backup');
1998
+ }
1999
+
2000
+ async function saveSettings(section) {
2001
+ try {
2002
+ if (isOnline) {
2003
+ await window.legalAPI.updateSettings(settings);
2004
+ }
2005
+
2006
+ showToast(`تنظیمات ${getSectionName(section)} ذخیره شد`, 'success', 'ذخیره موفق');
2007
+
2008
+ } catch (error) {
2009
+ console.error('Failed to save settings:', error);
2010
+ showToast('خطا در ذخیره تنظیمات', 'error', 'خطا');
2011
+ }
2012
+ }
2013
+
2014
+ function getSectionName(section) {
2015
+ const names = {
2016
+ general: 'عمومی',
2017
+ ocr: 'OCR',
2018
+ storage: 'ذخیره‌سازی',
2019
+ notifications: 'اعلان‌ها',
2020
+ security: 'امنیت',
2021
+ api: 'API',
2022
+ backup: 'پشتیبان‌گیری'
2023
+ };
2024
+ return names[section] || section;
2025
+ }
2026
+
2027
+ function saveAllSettings() {
2028
+ showToast('در حال ذخیره همه تنظیمات...', 'info', 'ذخیره');
2029
+
2030
+ setTimeout(async () => {
2031
+ try {
2032
+ if (isOnline) {
2033
+ await window.legalAPI.updateSettings(settings);
2034
+ }
2035
+ showToast('همه تنظیمات با موفقیت ذخیره شدند', 'success', 'ذخیره موفق');
2036
+ } catch (error) {
2037
+ showToast('خطا در ذخیره تنظیمات', 'error', 'خطا');
2038
+ }
2039
+ }, 1000);
2040
+ }
2041
+
2042
+ // Utility functions
2043
+ function exportSettings() {
2044
+ const settingsJSON = JSON.stringify(settings, null, 2);
2045
+ const blob = new Blob([settingsJSON], { type: 'application/json' });
2046
+ const url = URL.createObjectURL(blob);
2047
+
2048
+ const a = document.createElement('a');
2049
+ a.href = url;
2050
+ a.download = `legal-system-settings-${new Date().toISOString().split('T')[0]}.json`;
2051
+ document.body.appendChild(a);
2052
+ a.click();
2053
+ document.body.removeChild(a);
2054
+ URL.revokeObjectURL(url);
2055
+
2056
+ showToast('فایل تنظیمات دانلود شد', 'success', 'خروجی موفق');
2057
+ }
2058
+
2059
+ function cleanupStorage() {
2060
+ if (!confirm('آیا از پاک‌سازی فوری فایل‌های موقت اطمینان دارید؟')) {
2061
+ return;
2062
+ }
2063
+
2064
+ showToast('پاک‌سازی شروع شد...', 'info', 'پاک‌سازی');
2065
+
2066
+ setTimeout(() => {
2067
+ showToast('فایل‌های موقت پاک شدند. 2.3 GB فضا آزاد شد', 'success', 'پاک‌سازی موفق');
2068
+ }, 3000);
2069
+ }
2070
+
2071
+ function generateNewAPIKey() {
2072
+ if (!confirm('آیا از تولید کلید API جدید اطمینان دارید؟ کلید قبلی غیرفعال خواهد شد.')) {
2073
+ return;
2074
+ }
2075
+
2076
+ const newKey = 'sk_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
2077
+ document.getElementById('apiKey').value = newKey;
2078
+
2079
+ showToast('کلید API جدید تولید شد', 'success', 'تولید موفق');
2080
+ }
2081
+
2082
+ function createBackup() {
2083
+ showToast('ایجاد پشتیبان شروع شد...', 'info', 'پشتیبان‌گیری');
2084
+
2085
+ setTimeout(() => {
2086
+ showToast('پشتیبان با موفقیت ایجاد شد و در مسیر /backups/ ذخیره شد', 'success', 'پشتیبان‌گیری موفق');
2087
+ }, 5000);
2088
+ }
2089
+
2090
+ function restoreBackup(input) {
2091
+ const file = input.files[0];
2092
+ if (!file) return;
2093
+
2094
+ if (!file.name.endsWith('.backup')) {
2095
+ showToast('لطفاً فایل پشتیبان معتبر انتخاب کنید', 'warning', 'فایل نامعتبر');
2096
+ return;
2097
+ }
2098
+
2099
+ showToast('بازیابی از پشتیبان شروع شد...', 'info', 'بازیابی');
2100
+
2101
+ setTimeout(() => {
2102
+ showToast('سیستم با موفقیت از پشتیبان بازیابی شد', 'success', 'بازیابی موفق');
2103
+ }, 8000);
2104
+ }
2105
+
2106
+ function clearSecurityLogs() {
2107
+ if (!confirm('آیا از پاک کردن همه لاگ‌های امنیتی اطمینان دارید؟')) {
2108
+ return;
2109
+ }
2110
+
2111
+ showToast('لاگ‌های امنیتی پاک شدند', 'info', 'پاک‌سازی');
2112
+ }
2113
+
2114
+ function checkUpdates() {
2115
+ showToast('در حال بررسی بروزرسانی...', 'info', 'بررسی');
2116
+
2117
+ setTimeout(() => {
2118
+ const hasUpdate = Math.random() > 0.7;
2119
+ if (hasUpdate) {
2120
+ showToast('نسخه جدید v2.2.0 در دسترس است', 'success', 'بروزرسانی موجود');
2121
+ } else {
2122
+ showToast('سیستم به‌روز است', 'info', 'به‌روز');
2123
+ }
2124
+ }, 2000);
2125
+ }
2126
+
2127
+ function restartSystem() {
2128
+ if (!confirm('آیا از راه‌اندازی مجدد سیستم اطمینان دارید؟ این عمل ممکن است چند دقیقه طول بکشد.')) {
2129
+ return;
2130
+ }
2131
+
2132
+ showToast('سیستم در حال راه‌اندازی مجدد...', 'warning', 'راه‌اندازی مجدد');
2133
+ }
2134
+
2135
+ function showToast(message, type = 'info', title = 'اعلان') {
2136
+ const toastContainer = document.getElementById('toastContainer');
2137
+ if (!toastContainer) return;
2138
+
2139
+ const toast = document.createElement('div');
2140
+ toast.className = `toast ${type}`;
2141
+
2142
+ const icons = {
2143
+ success: 'check-circle',
2144
+ error: 'exclamation-triangle',
2145
+ warning: 'exclamation-circle',
2146
+ info: 'info-circle'
2147
+ };
2148
+
2149
+ toast.innerHTML = `
2150
+ <div class="toast-icon">
2151
+ <i class="fas fa-${icons[type]}"></i>
2152
+ </div>
2153
+ <div class="toast-content">
2154
+ <div class="toast-title">${title}</div>
2155
+ <div class="toast-message">${message}</div>
2156
+ </div>
2157
+ <button type="button" class="toast-close" onclick="this.parentElement.remove()">
2158
+ <i class="fas fa-times"></i>
2159
+ </button>
2160
+ `;
2161
+
2162
+ toastContainer.appendChild(toast);
2163
+
2164
+ // Show toast
2165
+ setTimeout(() => toast.classList.add('show'), 100);
2166
+
2167
+ // Auto remove after 5 seconds
2168
+ setTimeout(() => {
2169
+ if (toast.parentElement) {
2170
+ toast.classList.remove('show');
2171
+ setTimeout(() => {
2172
+ if (toast.parentElement) {
2173
+ toast.remove();
2174
+ }
2175
+ }, 300);
2176
+ }
2177
+ }, 5000);
2178
+ }
2179
+
2180
+ console.log('⚙️ Settings Page Ready!');
2181
+ </script>
2182
+ </body>
2183
+ </html>
app/frontend/upload.html ADDED
@@ -0,0 +1,1343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>آپلود فایل | سامانه حقوقی</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+
12
+ <!-- Load API Client -->
13
+ <script src="/static/js/api-client.js"></script>
14
+ <script src="/static/js/core.js"></script>
15
+
16
+ <style>
17
+ /* استفاده از همان CSS از upload.html قبلی */
18
+ :root {
19
+ --text-primary: #0f172a;
20
+ --text-secondary: #475569;
21
+ --text-muted: #64748b;
22
+ --text-light: #ffffff;
23
+ --body-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 50%, #cbd5e1 100%);
24
+ --card-bg: rgba(255, 255, 255, 0.95);
25
+ --glass-bg: rgba(255, 255, 255, 0.9);
26
+ --glass-border: rgba(148, 163, 184, 0.2);
27
+ --primary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
28
+ --secondary-gradient: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
29
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #047857 100%);
30
+ --warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
31
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
32
+ --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
33
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
34
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.1);
35
+ --shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.12);
36
+ --sidebar-width: 260px;
37
+ --border-radius: 12px;
38
+ --border-radius-sm: 8px;
39
+ --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
40
+ --transition-fast: all 0.15s ease-in-out;
41
+ --font-size-xs: 0.7rem;
42
+ --font-size-sm: 0.8rem;
43
+ --font-size-base: 0.9rem;
44
+ --font-size-lg: 1.1rem;
45
+ --font-size-xl: 1.25rem;
46
+ --font-size-2xl: 1.5rem;
47
+ }
48
+
49
+ /* Simplified CSS - using key styles only */
50
+ * {
51
+ margin: 0;
52
+ padding: 0;
53
+ box-sizing: border-box;
54
+ }
55
+
56
+ body {
57
+ font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif;
58
+ background: var(--body-bg);
59
+ color: var(--text-primary);
60
+ line-height: 1.6;
61
+ overflow-x: hidden;
62
+ font-size: var(--font-size-base);
63
+ }
64
+
65
+ .dashboard-container {
66
+ display: flex;
67
+ min-block-size: 100vh;
68
+ inline-size: 100%;
69
+ }
70
+
71
+ /* سایدبار کامپکت */
72
+ .sidebar {
73
+ inline-size: var(--sidebar-width);
74
+ background: linear-gradient(135deg,
75
+ rgba(248, 250, 252, 0.98) 0%,
76
+ rgba(241, 245, 249, 0.95) 25%,
77
+ rgba(226, 232, 240, 0.98) 50%,
78
+ rgba(203, 213, 225, 0.95) 75%,
79
+ rgba(148, 163, 184, 0.1) 100%);
80
+ backdrop-filter: blur(25px);
81
+ padding: 1rem 0;
82
+ position: fixed;
83
+ block-size: 100vh;
84
+ inset-inline-end: 0;
85
+ inset-block-start: 0;
86
+ z-index: 1000;
87
+ overflow-y: auto;
88
+ box-shadow: -8px 0 32px rgba(59, 130, 246, 0.12);
89
+ border-inline-start: 1px solid rgba(59, 130, 246, 0.15);
90
+ }
91
+
92
+ .sidebar-header {
93
+ padding: 0 1rem 1rem;
94
+ border-block-end: 1px solid rgba(59, 130, 246, 0.12);
95
+ margin-block-end: 1rem;
96
+ display: flex;
97
+ align-items: center;
98
+ background: linear-gradient(135deg,
99
+ rgba(255, 255, 255, 0.4) 0%,
100
+ rgba(248, 250, 252, 0.2) 100%);
101
+ margin: 0 0.5rem 1rem;
102
+ border-radius: var(--border-radius);
103
+ backdrop-filter: blur(10px);
104
+ }
105
+
106
+ .logo {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 0.6rem;
110
+ color: var(--text-primary);
111
+ text-decoration: none;
112
+ }
113
+
114
+ .logo-icon {
115
+ inline-size: 2rem;
116
+ block-size: 2rem;
117
+ background: var(--primary-gradient);
118
+ border-radius: var(--border-radius-sm);
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ font-size: 1rem;
123
+ color: white;
124
+ }
125
+
126
+ .logo-text {
127
+ font-size: var(--font-size-lg);
128
+ font-weight: 700;
129
+ background: var(--primary-gradient);
130
+ -webkit-background-clip: text;
131
+ -webkit-text-fill-color: transparent;
132
+ }
133
+
134
+ .nav-section {
135
+ margin-block-end: 1rem;
136
+ }
137
+
138
+ .nav-title {
139
+ padding: 0 1rem 0.4rem;
140
+ font-size: var(--font-size-xs);
141
+ font-weight: 600;
142
+ text-transform: uppercase;
143
+ letter-spacing: 0.5px;
144
+ color: var(--text-secondary);
145
+ }
146
+
147
+ .nav-menu {
148
+ list-style: none;
149
+ }
150
+
151
+ .nav-item {
152
+ margin: 0.15rem 0.5rem;
153
+ }
154
+
155
+ .nav-link {
156
+ display: flex;
157
+ align-items: center;
158
+ padding: 0.6rem 0.8rem;
159
+ color: var(--text-primary);
160
+ text-decoration: none;
161
+ border-radius: var(--border-radius-sm);
162
+ transition: var(--transition-smooth);
163
+ font-weight: 500;
164
+ font-size: var(--font-size-sm);
165
+ cursor: pointer;
166
+ border: 1px solid transparent;
167
+ }
168
+
169
+ .nav-link:hover {
170
+ transform: translateX(-2px);
171
+ border-color: rgba(59, 130, 246, 0.15);
172
+ background: rgba(59, 130, 246, 0.05);
173
+ }
174
+
175
+ .nav-link.active {
176
+ background: var(--primary-gradient);
177
+ color: var(--text-light);
178
+ box-shadow: var(--shadow-md);
179
+ }
180
+
181
+ .nav-icon {
182
+ margin-inline-start: 0.6rem;
183
+ inline-size: 1rem;
184
+ text-align: center;
185
+ font-size: 0.9rem;
186
+ }
187
+
188
+ .nav-badge {
189
+ background: var(--danger-gradient);
190
+ color: white;
191
+ padding: 0.15rem 0.4rem;
192
+ border-radius: 10px;
193
+ font-size: var(--font-size-xs);
194
+ font-weight: 600;
195
+ margin-inline-end: auto;
196
+ min-inline-size: 1.2rem;
197
+ text-align: center;
198
+ }
199
+
200
+ /* محتوای اصلی */
201
+ .main-content {
202
+ flex: 1;
203
+ margin-inline-end: var(--sidebar-width);
204
+ padding: 1rem;
205
+ min-block-size: 100vh;
206
+ inline-size: calc(100% - var(--sidebar-width));
207
+ }
208
+
209
+ .page-header {
210
+ display: flex;
211
+ justify-content: space-between;
212
+ align-items: center;
213
+ margin-block-end: 2rem;
214
+ padding: 1rem 0;
215
+ border-block-end: 1px solid rgba(0, 0, 0, 0.1);
216
+ }
217
+
218
+ .page-title {
219
+ font-size: var(--font-size-2xl);
220
+ font-weight: 800;
221
+ background: var(--primary-gradient);
222
+ -webkit-background-clip: text;
223
+ -webkit-text-fill-color: transparent;
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 0.6rem;
227
+ }
228
+
229
+ .page-actions {
230
+ display: flex;
231
+ gap: 0.8rem;
232
+ }
233
+
234
+ .btn {
235
+ padding: 0.6rem 1.2rem;
236
+ border: none;
237
+ border-radius: var(--border-radius-sm);
238
+ font-family: inherit;
239
+ font-weight: 600;
240
+ cursor: pointer;
241
+ transition: var(--transition-smooth);
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 0.5rem;
245
+ text-decoration: none;
246
+ font-size: var(--font-size-sm);
247
+ }
248
+
249
+ .btn-outline {
250
+ background: transparent;
251
+ color: var(--text-primary);
252
+ border: 1px solid rgba(59, 130, 246, 0.2);
253
+ }
254
+
255
+ .btn-outline:hover {
256
+ background: rgba(59, 130, 246, 0.05);
257
+ border-color: rgba(59, 130, 246, 0.4);
258
+ }
259
+
260
+ /* آپلود فایل */
261
+ .upload-section {
262
+ background: var(--card-bg);
263
+ backdrop-filter: blur(10px);
264
+ border-radius: var(--border-radius);
265
+ padding: 2rem;
266
+ margin-block-end: 2rem;
267
+ box-shadow: var(--shadow-md);
268
+ border: 1px solid rgba(255, 255, 255, 0.3);
269
+ }
270
+
271
+ .upload-area {
272
+ border: 3px dashed rgba(59, 130, 246, 0.3);
273
+ border-radius: var(--border-radius);
274
+ padding: 3rem;
275
+ text-align: center;
276
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.02), rgba(255, 255, 255, 0.1));
277
+ transition: var(--transition-smooth);
278
+ cursor: pointer;
279
+ position: relative;
280
+ }
281
+
282
+ .upload-area:hover,
283
+ .upload-area.dragover {
284
+ border-color: rgba(59, 130, 246, 0.6);
285
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(255, 255, 255, 0.15));
286
+ transform: scale(1.01);
287
+ box-shadow: var(--shadow-lg);
288
+ }
289
+
290
+ .upload-icon {
291
+ font-size: 4rem;
292
+ color: rgba(59, 130, 246, 0.6);
293
+ margin-bottom: 1.5rem;
294
+ transition: var(--transition-smooth);
295
+ }
296
+
297
+ .upload-area:hover .upload-icon {
298
+ color: rgba(59, 130, 246, 0.8);
299
+ transform: scale(1.1);
300
+ }
301
+
302
+ .upload-title {
303
+ font-size: var(--font-size-xl);
304
+ font-weight: 700;
305
+ color: var(--text-primary);
306
+ margin-bottom: 0.8rem;
307
+ }
308
+
309
+ .upload-description {
310
+ color: var(--text-secondary);
311
+ margin-bottom: 1.5rem;
312
+ font-size: var(--font-size-base);
313
+ line-height: 1.6;
314
+ }
315
+
316
+ .upload-input {
317
+ position: absolute;
318
+ top: 0;
319
+ left: 0;
320
+ width: 100%;
321
+ height: 100%;
322
+ opacity: 0;
323
+ cursor: pointer;
324
+ }
325
+
326
+ .upload-btn {
327
+ background: var(--primary-gradient);
328
+ color: white;
329
+ border: none;
330
+ padding: 0.8rem 2rem;
331
+ border-radius: var(--border-radius-sm);
332
+ font-weight: 600;
333
+ cursor: pointer;
334
+ transition: var(--transition-smooth);
335
+ font-size: var(--font-size-base);
336
+ display: flex;
337
+ align-items: center;
338
+ gap: 0.5rem;
339
+ margin: 0 auto;
340
+ box-shadow: var(--shadow-sm);
341
+ }
342
+
343
+ .upload-btn:hover {
344
+ box-shadow: var(--shadow-lg);
345
+ transform: translateY(-2px);
346
+ }
347
+
348
+ /* فایل‌های آپلود شده */
349
+ .upload-files {
350
+ margin-top: 2rem;
351
+ display: none;
352
+ }
353
+
354
+ .upload-files.has-files {
355
+ display: block;
356
+ }
357
+
358
+ .files-header {
359
+ display: flex;
360
+ justify-content: space-between;
361
+ align-items: center;
362
+ margin-bottom: 1rem;
363
+ padding-bottom: 0.8rem;
364
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
365
+ }
366
+
367
+ .files-title {
368
+ font-size: var(--font-size-lg);
369
+ font-weight: 700;
370
+ color: var(--text-primary);
371
+ }
372
+
373
+ .files-actions {
374
+ display: flex;
375
+ gap: 0.6rem;
376
+ }
377
+
378
+ .btn-sm {
379
+ padding: 0.4rem 0.8rem;
380
+ font-size: var(--font-size-xs);
381
+ }
382
+
383
+ .btn-success {
384
+ background: var(--success-gradient);
385
+ color: white;
386
+ }
387
+
388
+ .btn-danger {
389
+ background: var(--danger-gradient);
390
+ color: white;
391
+ }
392
+
393
+ .file-item {
394
+ display: flex;
395
+ align-items: center;
396
+ gap: 1rem;
397
+ padding: 1rem;
398
+ background: rgba(255, 255, 255, 0.7);
399
+ border-radius: var(--border-radius-sm);
400
+ margin-bottom: 0.8rem;
401
+ border: 1px solid rgba(0, 0, 0, 0.05);
402
+ transition: var(--transition-smooth);
403
+ position: relative;
404
+ overflow: hidden;
405
+ }
406
+
407
+ .file-item::before {
408
+ content: '';
409
+ position: absolute;
410
+ top: 0;
411
+ left: 0;
412
+ bottom: 0;
413
+ width: 4px;
414
+ background: var(--primary-gradient);
415
+ }
416
+
417
+ .file-item:hover {
418
+ background: rgba(255, 255, 255, 0.9);
419
+ transform: translateX(-3px);
420
+ box-shadow: var(--shadow-sm);
421
+ }
422
+
423
+ .file-icon {
424
+ width: 3rem;
425
+ height: 3rem;
426
+ background: var(--danger-gradient);
427
+ border-radius: var(--border-radius-sm);
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: center;
431
+ color: white;
432
+ font-size: 1.2rem;
433
+ flex-shrink: 0;
434
+ box-shadow: var(--shadow-sm);
435
+ }
436
+
437
+ .file-info {
438
+ flex: 1;
439
+ }
440
+
441
+ .file-name {
442
+ font-weight: 600;
443
+ color: var(--text-primary);
444
+ font-size: var(--font-size-base);
445
+ margin-bottom: 0.3rem;
446
+ }
447
+
448
+ .file-details {
449
+ display: flex;
450
+ gap: 1rem;
451
+ font-size: var(--font-size-sm);
452
+ color: var(--text-secondary);
453
+ }
454
+
455
+ .file-progress {
456
+ width: 100%;
457
+ height: 6px;
458
+ background: rgba(0, 0, 0, 0.08);
459
+ border-radius: 3px;
460
+ margin-top: 0.8rem;
461
+ overflow: hidden;
462
+ }
463
+
464
+ .file-progress-bar {
465
+ height: 100%;
466
+ background: var(--success-gradient);
467
+ border-radius: 3px;
468
+ transition: width 0.3s ease;
469
+ position: relative;
470
+ }
471
+
472
+ .file-progress-bar::after {
473
+ content: '';
474
+ position: absolute;
475
+ top: 0;
476
+ left: 0;
477
+ right: 0;
478
+ bottom: 0;
479
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
480
+ animation: shimmer 2s infinite;
481
+ }
482
+
483
+ @keyframes shimmer {
484
+ 0% { transform: translateX(-100%); }
485
+ 100% { transform: translateX(100%); }
486
+ }
487
+
488
+ .file-status {
489
+ font-size: var(--font-size-xs);
490
+ font-weight: 600;
491
+ padding: 0.3rem 0.8rem;
492
+ border-radius: 15px;
493
+ display: flex;
494
+ align-items: center;
495
+ gap: 0.3rem;
496
+ white-space: nowrap;
497
+ }
498
+
499
+ .file-status.uploading {
500
+ background: rgba(245, 158, 11, 0.1);
501
+ color: #b45309;
502
+ }
503
+
504
+ .file-status.processing {
505
+ background: rgba(59, 130, 246, 0.1);
506
+ color: #1d4ed8;
507
+ }
508
+
509
+ .file-status.success {
510
+ background: rgba(16, 185, 129, 0.1);
511
+ color: #047857;
512
+ }
513
+
514
+ .file-status.error {
515
+ background: rgba(239, 68, 68, 0.1);
516
+ color: #b91c1c;
517
+ }
518
+
519
+ .file-actions {
520
+ display: flex;
521
+ gap: 0.4rem;
522
+ flex-shrink: 0;
523
+ }
524
+
525
+ .action-btn {
526
+ width: 2rem;
527
+ height: 2rem;
528
+ border: none;
529
+ border-radius: var(--border-radius-sm);
530
+ cursor: pointer;
531
+ transition: var(--transition-fast);
532
+ display: flex;
533
+ align-items: center;
534
+ justify-content: center;
535
+ font-size: var(--font-size-xs);
536
+ }
537
+
538
+ .action-btn.retry {
539
+ background: rgba(245, 158, 11, 0.1);
540
+ color: #f59e0b;
541
+ }
542
+
543
+ .action-btn.remove {
544
+ background: rgba(239, 68, 68, 0.1);
545
+ color: #ef4444;
546
+ }
547
+
548
+ .action-btn:hover {
549
+ transform: scale(1.1);
550
+ }
551
+
552
+ /* Toast Notifications */
553
+ .toast-container {
554
+ position: fixed;
555
+ top: 1rem;
556
+ left: 1rem;
557
+ z-index: 10001;
558
+ display: flex;
559
+ flex-direction: column;
560
+ gap: 0.5rem;
561
+ }
562
+
563
+ .toast {
564
+ background: var(--card-bg);
565
+ border-radius: var(--border-radius-sm);
566
+ padding: 1rem 1.5rem;
567
+ box-shadow: var(--shadow-lg);
568
+ border-left: 4px solid;
569
+ display: flex;
570
+ align-items: center;
571
+ gap: 0.8rem;
572
+ min-width: 300px;
573
+ transform: translateX(-100%);
574
+ transition: all 0.3s ease;
575
+ }
576
+
577
+ .toast.show {
578
+ transform: translateX(0);
579
+ }
580
+
581
+ .toast.success { border-left-color: #10b981; }
582
+ .toast.error { border-left-color: #ef4444; }
583
+ .toast.warning { border-left-color: #f59e0b; }
584
+ .toast.info { border-left-color: #3b82f6; }
585
+
586
+ .toast-icon {
587
+ font-size: 1.2rem;
588
+ }
589
+
590
+ .toast.success .toast-icon { color: #10b981; }
591
+ .toast.error .toast-icon { color: #ef4444; }
592
+ .toast.warning .toast-icon { color: #f59e0b; }
593
+ .toast.info .toast-icon { color: #3b82f6; }
594
+
595
+ .toast-content {
596
+ flex: 1;
597
+ }
598
+
599
+ .toast-title {
600
+ font-weight: 600;
601
+ font-size: var(--font-size-sm);
602
+ margin-bottom: 0.2rem;
603
+ }
604
+
605
+ .toast-message {
606
+ font-size: var(--font-size-xs);
607
+ color: var(--text-secondary);
608
+ }
609
+
610
+ .toast-close {
611
+ background: none;
612
+ border: none;
613
+ color: var(--text-secondary);
614
+ cursor: pointer;
615
+ font-size: 1rem;
616
+ transition: var(--transition-fast);
617
+ }
618
+
619
+ .toast-close:hover {
620
+ color: var(--text-primary);
621
+ }
622
+
623
+ /* واکنش‌گرایی */
624
+ @media (max-width: 992px) {
625
+ .sidebar {
626
+ transform: translateX(100%);
627
+ transition: transform 0.3s ease;
628
+ }
629
+
630
+ .sidebar.open {
631
+ transform: translateX(0);
632
+ }
633
+
634
+ .main-content {
635
+ margin-right: 0;
636
+ width: 100%;
637
+ padding: 1rem;
638
+ }
639
+ }
640
+ </style>
641
+ </head>
642
+ <body>
643
+ <div class="dashboard-container">
644
+ <!-- سایدبار -->
645
+ <aside class="sidebar" id="sidebar">
646
+ <div class="sidebar-header">
647
+ <a href="/" class="logo">
648
+ <div class="logo-icon">
649
+ <i class="fas fa-scale-balanced"></i>
650
+ </div>
651
+ <div class="logo-text">سامانه حقوقی</div>
652
+ </a>
653
+ </div>
654
+
655
+ <nav>
656
+ <div class="nav-section">
657
+ <h6 class="nav-title">داشبورد</h6>
658
+ <ul class="nav-menu">
659
+ <li class="nav-item">
660
+ <a href="/" class="nav-link">
661
+ <i class="fas fa-chart-pie nav-icon"></i>
662
+ <span>نمای کلی</span>
663
+ </a>
664
+ </li>
665
+ </ul>
666
+ </div>
667
+
668
+ <div class="nav-section">
669
+ <h6 class="nav-title">مدیریت اسناد</h6>
670
+ <ul class="nav-menu">
671
+ <li class="nav-item">
672
+ <a href="/static/documents.html" class="nav-link">
673
+ <i class="fas fa-file-alt nav-icon"></i>
674
+ <span>مدیریت اسناد</span>
675
+ <span class="nav-badge" id="totalDocumentsBadge">6</span>
676
+ </a>
677
+ </li>
678
+
679
+ <li class="nav-item">
680
+ <a href="/static/upload.html" class="nav-link active">
681
+ <i class="fas fa-cloud-upload-alt nav-icon"></i>
682
+ <span>آپلود فایل</span>
683
+ </a>
684
+ </li>
685
+
686
+ <li class="nav-item">
687
+ <a href="/static/search.html" class="nav-link">
688
+ <i class="fas fa-search nav-icon"></i>
689
+ <span>جستجو</span>
690
+ </a>
691
+ </li>
692
+ </ul>
693
+ </div>
694
+
695
+ <div class="nav-section">
696
+ <h6 class="nav-title">ابزارها</h6>
697
+ <ul class="nav-menu">
698
+ <li class="nav-item">
699
+ <a href="/static/scraping.html" class="nav-link">
700
+ <i class="fas fa-globe nav-icon"></i>
701
+ <span>استخراج محتوا</span>
702
+ </a>
703
+ </li>
704
+
705
+ <li class="nav-item">
706
+ <a href="/static/analytics.html" class="nav-link">
707
+ <i class="fas fa-chart-line nav-icon"></i>
708
+ <span>آمار و تحلیل</span>
709
+ </a>
710
+ </li>
711
+
712
+ <li class="nav-item">
713
+ <a href="/static/reports.html" class="nav-link">
714
+ <i class="fas fa-file-export nav-icon"></i>
715
+ <span>گزارش‌ها</span>
716
+ </a>
717
+ </li>
718
+ </ul>
719
+ </div>
720
+
721
+ <div class="nav-section">
722
+ <h6 class="nav-title">تنظیمات</h6>
723
+ <ul class="nav-menu">
724
+ <li class="nav-item">
725
+ <a href="/static/settings.html" class="nav-link">
726
+ <i class="fas fa-cog nav-icon"></i>
727
+ <span>تنظیمات</span>
728
+ </a>
729
+ </li>
730
+ <li class="nav-item">
731
+ <a href="#" class="nav-link">
732
+ <i class="fas fa-sign-out-alt nav-icon"></i>
733
+ <span>خروج</span>
734
+ </a>
735
+ </li>
736
+ </ul>
737
+ </div>
738
+ </nav>
739
+ </aside>
740
+
741
+ <!-- محتوای اصلی -->
742
+ <main class="main-content">
743
+ <!-- هدر صفحه -->
744
+ <header class="page-header">
745
+ <h1 class="page-title">
746
+ <i class="fas fa-cloud-upload-alt"></i>
747
+ آپلود فایل‌های حقوقی
748
+ </h1>
749
+ <div class="page-actions">
750
+ <a href="/static/documents.html" class="btn btn-outline">
751
+ <i class="fas fa-list"></i>
752
+ مشاهده اسناد
753
+ </a>
754
+ </div>
755
+ </header>
756
+
757
+ <!-- بخش آپلود اصلی -->
758
+ <section class="upload-section">
759
+ <div class="upload-area" id="uploadArea">
760
+ <div class="upload-icon">
761
+ <i class="fas fa-cloud-upload-alt"></i>
762
+ </div>
763
+ <h2 class="upload-title">آپلود اسناد PDF</h2>
764
+ <p class="upload-description">
765
+ فایل‌های PDF خود را بکشید و اینجا رها کنید یا کلیک کنید تا انتخاب کنید.<br>
766
+ چندین فایل را می‌توانید همزمان آپلود کنید.
767
+ </p>
768
+ <input type="file" class="upload-input" id="fileInput" multiple accept=".pdf">
769
+ <button type="button" class="upload-btn" onclick="document.getElementById('fileInput').click()">
770
+ <i class="fas fa-folder-open"></i>
771
+ انتخاب فایل‌ها
772
+ </button>
773
+ </div>
774
+ </section>
775
+
776
+ <!-- فایل‌های آپلود شده -->
777
+ <section class="upload-files" id="uploadFiles">
778
+ <div class="files-header">
779
+ <h3 class="files-title">فایل‌های آپلود شده</h3>
780
+ <div class="files-actions">
781
+ <button type="button" class="btn btn-success btn-sm" onclick="processAllFiles()">
782
+ <i class="fas fa-play"></i>
783
+ پردازش همه
784
+ </button>
785
+ <button type="button" class="btn btn-danger btn-sm" onclick="clearAllFiles()">
786
+ <i class="fas fa-trash"></i>
787
+ پاک کردن همه
788
+ </button>
789
+ </div>
790
+ </div>
791
+ <div id="filesContainer">
792
+ <!-- فایل‌ها اینجا نمایش داده می‌شوند -->
793
+ </div>
794
+ </section>
795
+ </main>
796
+ </div>
797
+
798
+ <!-- Toast Container -->
799
+ <div class="toast-container" id="toastContainer"></div>
800
+
801
+ <script>
802
+ // Global variables
803
+ let uploadedFiles = new Map();
804
+ let fileCounter = 0;
805
+ let isOnline = false;
806
+
807
+ // Initialize page
808
+ document.addEventListener('DOMContentLoaded', function() {
809
+ console.log('📤 Upload page loading...');
810
+ initializeUploadPage();
811
+ });
812
+
813
+ async function initializeUploadPage() {
814
+ try {
815
+ // Test backend connection
816
+ isOnline = await testConnection();
817
+
818
+ // Setup event listeners
819
+ setupEventListeners();
820
+
821
+ showToast('صفحه آپلود آماده است', 'success', 'آماده');
822
+
823
+ } catch (error) {
824
+ console.error('Failed to initialize upload page:', error);
825
+
826
+ // Fallback mode
827
+ isOnline = false;
828
+ setupEventListeners();
829
+
830
+ showToast('حالت آفلاین فعال است', 'warning', 'اتصال ناموفق');
831
+ }
832
+ }
833
+
834
+ async function testConnection() {
835
+ try {
836
+ await window.legalAPI.healthCheck();
837
+ return true;
838
+ } catch (error) {
839
+ return false;
840
+ }
841
+ }
842
+
843
+ // Setup event listeners
844
+ function setupEventListeners() {
845
+ const uploadArea = document.getElementById('uploadArea');
846
+ const fileInput = document.getElementById('fileInput');
847
+
848
+ // Drag and drop events
849
+ uploadArea.addEventListener('dragover', handleDragOver);
850
+ uploadArea.addEventListener('dragleave', handleDragLeave);
851
+ uploadArea.addEventListener('drop', handleDrop);
852
+
853
+ // File input change
854
+ fileInput.addEventListener('change', handleFileSelect);
855
+
856
+ // Click to upload
857
+ uploadArea.addEventListener('click', (e) => {
858
+ if (!e.target.closest('.upload-btn') && !e.target.classList.contains('upload-input')) {
859
+ fileInput.click();
860
+ }
861
+ });
862
+ }
863
+
864
+ // Drag and drop handlers
865
+ function handleDragOver(e) {
866
+ e.preventDefault();
867
+ e.currentTarget.classList.add('dragover');
868
+ }
869
+
870
+ function handleDragLeave(e) {
871
+ e.currentTarget.classList.remove('dragover');
872
+ }
873
+
874
+ function handleDrop(e) {
875
+ e.preventDefault();
876
+ e.currentTarget.classList.remove('dragover');
877
+
878
+ const files = Array.from(e.dataTransfer.files).filter(file =>
879
+ file.type === 'application/pdf'
880
+ );
881
+
882
+ if (files.length > 0) {
883
+ handleFileUpload(files);
884
+ } else {
885
+ showToast('فقط فایل‌های PDF پذیرفته می‌شوند', 'warning', 'هشدار');
886
+ }
887
+ }
888
+
889
+ // File selection handler
890
+ function handleFileSelect(e) {
891
+ const files = Array.from(e.target.files);
892
+ if (files.length > 0) {
893
+ handleFileUpload(files);
894
+ }
895
+ }
896
+
897
+ // File upload processing
898
+ async function handleFileUpload(files) {
899
+ const uploadFilesSection = document.getElementById('uploadFiles');
900
+ const filesContainer = document.getElementById('filesContainer');
901
+
902
+ uploadFilesSection.classList.add('has-files');
903
+
904
+ let validFiles = [];
905
+ let invalidFiles = 0;
906
+
907
+ for (const file of files) {
908
+ // Validate file
909
+ const validation = validateFile(file);
910
+ if (!validation.valid) {
911
+ showToast(`${file.name}: ${validation.error}`, 'error', 'خطای اعتبارسنجی');
912
+ invalidFiles++;
913
+ continue;
914
+ }
915
+
916
+ validFiles.push(file);
917
+ const fileId = generateFileId();
918
+ const fileItem = createFileItem(file, fileId);
919
+ filesContainer.appendChild(fileItem);
920
+
921
+ // Store file info
922
+ uploadedFiles.set(fileId, {
923
+ file: file,
924
+ status: 'ready',
925
+ progress: 0
926
+ });
927
+ }
928
+
929
+ if (validFiles.length > 0) {
930
+ showToast(`${validFiles.length} فایل آماده پردازش شد`, 'success', 'آپلود موفق');
931
+
932
+ // Start upload to backend if online
933
+ if (isOnline) {
934
+ await uploadFilesToBackend(validFiles);
935
+ } else {
936
+ // Simulate upload in offline mode
937
+ validFiles.forEach((file, index) => {
938
+ const fileId = Array.from(uploadedFiles.keys())[uploadedFiles.size - validFiles.length + index];
939
+ setTimeout(() => {
940
+ simulateFileUpload(fileId);
941
+ }, Math.random() * 1000);
942
+ });
943
+ }
944
+ }
945
+
946
+ if (invalidFiles > 0) {
947
+ showToast(`${invalidFiles} فایل نامعتبر حذف شد`, 'warning', 'هشدار');
948
+ }
949
+
950
+ // Clear file input
951
+ document.getElementById('fileInput').value = '';
952
+ }
953
+
954
+ // Upload files to backend
955
+ async function uploadFilesToBackend(files) {
956
+ try {
957
+ const response = await window.legalAPI.uploadFiles(files);
958
+
959
+ if (response.results) {
960
+ response.results.forEach((result, index) => {
961
+ const fileId = Array.from(uploadedFiles.keys())[uploadedFiles.size - files.length + index];
962
+ const fileData = uploadedFiles.get(fileId);
963
+
964
+ if (result.success) {
965
+ // Real upload successful - monitor progress
966
+ monitorUploadProgress(fileId, result.document_id);
967
+ } else {
968
+ // Upload failed
969
+ updateFileStatus(fileId, 'error', result.error || 'خطا در آپلود');
970
+ showFileRetryOption(fileId);
971
+ }
972
+ });
973
+ }
974
+
975
+ showToast(`${response.successful_uploads} فایل با موفقیت آپلود شد`, 'success', 'آپلود موفق');
976
+
977
+ } catch (error) {
978
+ console.error('Upload to backend failed:', error);
979
+ showToast('خطا در ارسال فایل‌ها به سرور', 'error', 'خطای آپلود');
980
+
981
+ // Fallback to simulation
982
+ files.forEach((file, index) => {
983
+ const fileId = Array.from(uploadedFiles.keys())[uploadedFiles.size - files.length + index];
984
+ setTimeout(() => {
985
+ simulateFileUpload(fileId);
986
+ }, Math.random() * 1000);
987
+ });
988
+ }
989
+ }
990
+
991
+ // Monitor upload progress for real backend uploads
992
+ async function monitorUploadProgress(fileId, documentId) {
993
+ const fileData = uploadedFiles.get(fileId);
994
+ if (!fileData) return;
995
+
996
+ updateFileStatus(fileId, 'uploading', 'در حال آپلود...');
997
+
998
+ // Simulate upload progress
999
+ let progress = 0;
1000
+ const uploadInterval = setInterval(() => {
1001
+ progress += Math.random() * 15;
1002
+ if (progress >= 100) {
1003
+ progress = 100;
1004
+ clearInterval(uploadInterval);
1005
+
1006
+ // Start monitoring OCR processing
1007
+ setTimeout(() => {
1008
+ monitorProcessingStatus(fileId, documentId);
1009
+ }, 500);
1010
+ }
1011
+
1012
+ const progressBar = document.querySelector(`#file-${fileId} .file-progress-bar`);
1013
+ if (progressBar) {
1014
+ progressBar.style.width = `${progress}%`;
1015
+ }
1016
+ }, 200);
1017
+ }
1018
+
1019
+ // Monitor processing status from backend
1020
+ async function monitorProcessingStatus(fileId, documentId) {
1021
+ const fileData = uploadedFiles.get(fileId);
1022
+ if (!fileData) return;
1023
+
1024
+ updateFileStatus(fileId, 'processing', 'در حال پردازش OCR...');
1025
+
1026
+ // Poll for processing status
1027
+ const pollInterval = setInterval(async () => {
1028
+ try {
1029
+ const document = await window.legalAPI.getDocument(documentId);
1030
+
1031
+ if (document.status === 'processed') {
1032
+ clearInterval(pollInterval);
1033
+ updateFileStatus(fileId, 'success', 'پردازش تکمیل شد');
1034
+ showToast(`${fileData.file.name} با موفقیت پردازش شد`, 'success', 'پردازش موفق');
1035
+ } else if (document.status === 'error') {
1036
+ clearInterval(pollInterval);
1037
+ updateFileStatus(fileId, 'error', 'خطا در پردازش');
1038
+ showFileRetryOption(fileId);
1039
+ showToast(`خطا در پردازش ${fileData.file.name}`, 'error', 'خطای پردازش');
1040
+ }
1041
+ // Continue polling for other statuses (processing, pending)
1042
+
1043
+ } catch (error) {
1044
+ console.error('Failed to check processing status:', error);
1045
+ // Don't clear interval yet, might be temporary network issue
1046
+ }
1047
+ }, 3000); // Poll every 3 seconds
1048
+
1049
+ // Stop polling after 5 minutes
1050
+ setTimeout(() => {
1051
+ clearInterval(pollInterval);
1052
+ if (fileData.status !== 'success' && fileData.status !== 'error') {
1053
+ updateFileStatus(fileId, 'processing', 'پردازش ادامه دارد...');
1054
+ }
1055
+ }, 300000);
1056
+ }
1057
+
1058
+ // Validate file
1059
+ function validateFile(file) {
1060
+ if (file.type !== 'application/pdf') {
1061
+ return { valid: false, error: 'فقط فایل‌های PDF پذیرفته می‌شوند' };
1062
+ }
1063
+
1064
+ if (file.size > 50 * 1024 * 1024) { // 50MB
1065
+ return { valid: false, error: 'حجم فایل نباید بیشتر از 50 مگابایت باشد' };
1066
+ }
1067
+
1068
+ if (file.size < 1024) { // 1KB
1069
+ return { valid: false, error: 'فایل خیلی کوچک است' };
1070
+ }
1071
+
1072
+ return { valid: true };
1073
+ }
1074
+
1075
+ // Create file item UI
1076
+ function createFileItem(file, fileId) {
1077
+ const fileItem = document.createElement('div');
1078
+ fileItem.className = 'file-item';
1079
+ fileItem.id = `file-${fileId}`;
1080
+
1081
+ fileItem.innerHTML = `
1082
+ <div class="file-icon">
1083
+ <i class="fas fa-file-pdf"></i>
1084
+ </div>
1085
+ <div class="file-info">
1086
+ <div class="file-name">${file.name}</div>
1087
+ <div class="file-details">
1088
+ <div class="file-size">
1089
+ <i class="fas fa-weight-hanging"></i>
1090
+ ${formatFileSize(file.size)}
1091
+ </div>
1092
+ <div class="file-type">
1093
+ <i class="fas fa-file-pdf"></i>
1094
+ PDF Document
1095
+ </div>
1096
+ </div>
1097
+ <div class="file-progress">
1098
+ <div class="file-progress-bar" style="width: 0%"></div>
1099
+ </div>
1100
+ </div>
1101
+ <div class="file-status uploading">
1102
+ <i class="fas fa-clock"></i>
1103
+ آماده
1104
+ </div>
1105
+ <div class="file-actions">
1106
+ <button type="button" class="action-btn retry" onclick="retryUpload('${fileId}')" title="تلاش مجدد" style="display: none;">
1107
+ <i class="fas fa-redo"></i>
1108
+ </button>
1109
+ <button type="button" class="action-btn remove" onclick="removeFile('${fileId}')" title="حذف">
1110
+ <i class="fas fa-times"></i>
1111
+ </button>
1112
+ </div>
1113
+ `;
1114
+
1115
+ return fileItem;
1116
+ }
1117
+
1118
+ // Simulate file upload and processing (offline mode)
1119
+ function simulateFileUpload(fileId) {
1120
+ const fileData = uploadedFiles.get(fileId);
1121
+ if (!fileData) return;
1122
+
1123
+ const fileItem = document.getElementById(`file-${fileId}`);
1124
+ const statusElement = fileItem.querySelector('.file-status');
1125
+ const progressBar = fileItem.querySelector('.file-progress-bar');
1126
+
1127
+ // Phase 1: Uploading
1128
+ updateFileStatus(fileId, 'uploading', 'در حال آپلود...');
1129
+
1130
+ let progress = 0;
1131
+ const uploadInterval = setInterval(() => {
1132
+ progress += Math.random() * 15;
1133
+ if (progress >= 100) {
1134
+ progress = 100;
1135
+ clearInterval(uploadInterval);
1136
+
1137
+ // Phase 2: Processing
1138
+ setTimeout(() => {
1139
+ simulateProcessing(fileId);
1140
+ }, 500);
1141
+ }
1142
+
1143
+ progressBar.style.width = `${progress}%`;
1144
+ }, 200);
1145
+ }
1146
+
1147
+ // Simulate OCR processing (offline mode)
1148
+ function simulateProcessing(fileId) {
1149
+ const fileData = uploadedFiles.get(fileId);
1150
+ if (!fileData) return;
1151
+
1152
+ updateFileStatus(fileId, 'processing', 'در حال پردازش OCR...');
1153
+
1154
+ // Simulate processing time based on file size
1155
+ const fileSize = fileData.file.size;
1156
+ const processingTime = Math.min(fileSize / (1024 * 1024) * 1000 + 2000, 8000); // 2-8 seconds
1157
+
1158
+ setTimeout(() => {
1159
+ // Random success/failure (90% success rate)
1160
+ const success = Math.random() > 0.1;
1161
+
1162
+ if (success) {
1163
+ updateFileStatus(fileId, 'success', 'پردازش تکمیل شد');
1164
+ showToast(`${fileData.file.name} با موفقیت پردازش شد`, 'success', 'پردازش موفق');
1165
+ } else {
1166
+ updateFileStatus(fileId, 'error', 'خطا در پردازش');
1167
+ showFileRetryOption(fileId);
1168
+ showToast(`خطا در پردازش ${fileData.file.name}`, 'error', 'خطای پردازش');
1169
+ }
1170
+ }, processingTime);
1171
+ }
1172
+
1173
+ // Update file status
1174
+ function updateFileStatus(fileId, status, message) {
1175
+ const fileData = uploadedFiles.get(fileId);
1176
+ if (!fileData) return;
1177
+
1178
+ fileData.status = status;
1179
+
1180
+ const fileItem = document.getElementById(`file-${fileId}`);
1181
+ if (!fileItem) return;
1182
+
1183
+ const statusElement = fileItem.querySelector('.file-status');
1184
+ const progressBar = fileItem.querySelector('.file-progress-bar');
1185
+
1186
+ statusElement.className = `file-status ${status}`;
1187
+
1188
+ const icons = {
1189
+ ready: 'clock',
1190
+ uploading: 'spinner fa-spin',
1191
+ processing: 'cog fa-spin',
1192
+ success: 'check-circle',
1193
+ error: 'exclamation-triangle'
1194
+ };
1195
+
1196
+ statusElement.innerHTML = `
1197
+ <i class="fas fa-${icons[status]}"></i>
1198
+ ${message}
1199
+ `;
1200
+
1201
+ switch (status) {
1202
+ case 'success':
1203
+ progressBar.style.width = '100%';
1204
+ progressBar.style.background = 'var(--success-gradient)';
1205
+ break;
1206
+ case 'error':
1207
+ progressBar.style.width = '100%';
1208
+ progressBar.style.background = 'var(--danger-gradient)';
1209
+ break;
1210
+ }
1211
+ }
1212
+
1213
+ // Show retry option for failed files
1214
+ function showFileRetryOption(fileId) {
1215
+ const fileItem = document.getElementById(`file-${fileId}`);
1216
+ if (!fileItem) return;
1217
+
1218
+ const retryBtn = fileItem.querySelector('.action-btn.retry');
1219
+ retryBtn.style.display = 'flex';
1220
+ }
1221
+
1222
+ // Retry file upload
1223
+ async function retryUpload(fileId) {
1224
+ const fileData = uploadedFiles.get(fileId);
1225
+ if (!fileData) return;
1226
+
1227
+ const fileItem = document.getElementById(`file-${fileId}`);
1228
+ const retryBtn = fileItem.querySelector('.action-btn.retry');
1229
+
1230
+ retryBtn.style.display = 'none';
1231
+ fileData.status = 'ready';
1232
+
1233
+ // Reset progress bar
1234
+ const progressBar = fileItem.querySelector('.file-progress-bar');
1235
+ progressBar.style.width = '0%';
1236
+ progressBar.style.background = 'var(--success-gradient)';
1237
+
1238
+ if (isOnline) {
1239
+ // Retry with real backend
1240
+ await uploadFilesToBackend([fileData.file]);
1241
+ } else {
1242
+ // Retry with simulation
1243
+ setTimeout(() => {
1244
+ simulateFileUpload(fileId);
1245
+ }, 500);
1246
+ }
1247
+ }
1248
+
1249
+ // Remove file
1250
+ function removeFile(fileId) {
1251
+ const fileData = uploadedFiles.get(fileId);
1252
+ if (!fileData) return;
1253
+
1254
+ if (!confirm(`آیا از حذف "${fileData.file.name}" اطمینان دارید؟`)) {
1255
+ return;
1256
+ }
1257
+
1258
+ const fileItem = document.getElementById(`file-${fileId}`);
1259
+ if (fileItem) {
1260
+ fileItem.style.transform = 'translateX(100%)';
1261
+ fileItem.style.opacity = '0';
1262
+
1263
+ setTimeout(() => {
1264
+ fileItem.remove();
1265
+ uploadedFiles.delete(fileId);
1266
+
1267
+ // Hide section if no files
1268
+ if (uploadedFiles.size === 0) {
1269
+ document.getElementById('uploadFiles').classList.remove('has-files');
1270
+ }
1271
+ }, 300);
1272
+ }
1273
+
1274
+ showToast(`${fileData.file.name} حذف شد`, 'info', 'حذف فایل');
1275
+ }
1276
+
1277
+ // Process all files
1278
+ async function processAllFiles() {
1279
+ let readyFiles = [];
1280
+
1281
+ uploadedFiles.forEach((fileData, fileId) => {
1282
+ if (fileData.status === 'ready') {
1283
+ readyFiles.push({fileId, file: fileData.file});
1284
+ }
1285
+ });
1286
+
1287
+ if (readyFiles.length === 0) {
1288
+ showToast('هیچ فایل آماده‌ای برای پردازش وجود ندارد', 'warning', 'هشدار');
1289
+ return;
1290
+ }
1291
+
1292
+ if (isOnline) {
1293
+ // Process with real backend
1294
+ const files = readyFiles.map(item => item.file);
1295
+ await uploadFilesToBackend(files);
1296
+ } else {
1297
+ // Process with simulation
1298
+ readyFiles.forEach(({fileId}) => {
1299
+ setTimeout(() => {
1300
+ simulateFileUpload(fileId);
1301
+ }, Math.random() * 1000);
1302
+ });
1303
+ }
1304
+
1305
+ showToast(`پردازش ${readyFiles.length} فایل شروع شد`, 'info', 'شروع پردازش');
1306
+ }
1307
+
1308
+ // Clear all files
1309
+ function clearAllFiles() {
1310
+ if (uploadedFiles.size === 0) {
1311
+ showToast('هیچ فایلی برای حذف وجود ندارد', 'warning', 'هشدار');
1312
+ return;
1313
+ }
1314
+
1315
+ if (!confirm('آیا از حذف همه فایل‌ها اطمینان دارید؟')) {
1316
+ return;
1317
+ }
1318
+
1319
+ const filesContainer = document.getElementById('filesContainer');
1320
+ filesContainer.innerHTML = '';
1321
+ uploadedFiles.clear();
1322
+
1323
+ document.getElementById('uploadFiles').classList.remove('has-files');
1324
+ showToast('همه فایل‌ها حذف شدند', 'info', 'پاک کردن');
1325
+ }
1326
+
1327
+ // Utility functions
1328
+ function generateFileId() {
1329
+ return `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1330
+ }
1331
+
1332
+ function formatFileSize(bytes) {
1333
+ if (bytes === 0) return '0 بایت';
1334
+ const k = 1024;
1335
+ const sizes = ['بایت', 'کیلوبایت', 'مگابایت', 'گیگابایت'];
1336
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1337
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1338
+ }
1339
+
1340
+ console.log('📤 Upload Page Ready!');
1341
+ </script>
1342
+ </body>
1343
+ </html>