Spaces:
Paused
Paused
Upload 25 files
Browse files- app/frontend/README.md +242 -0
- app/frontend/analytics.html +1912 -0
- app/frontend/dev/api-test.html +274 -0
- app/frontend/dev/comprehensive-test.html +767 -0
- app/frontend/dev/functional-test.html +885 -0
- app/frontend/dev/integration-test.html +385 -0
- app/frontend/dev/real-api-test.html +674 -0
- app/frontend/dev/test_integration.html +164 -0
- app/frontend/documents.html +1597 -0
- app/frontend/enhanced_analytics_dashboard.html +1174 -0
- app/frontend/improved_legal_dashboard.html +0 -0
- app/frontend/index.html +1728 -0
- app/frontend/js/api-client.js +422 -0
- app/frontend/js/api-connection-test.js +380 -0
- app/frontend/js/core.js +388 -0
- app/frontend/js/document-crud.js +386 -0
- app/frontend/js/file-upload-handler.js +328 -0
- app/frontend/js/notifications.js +617 -0
- app/frontend/js/scraping-control.js +368 -0
- app/frontend/reports.html +917 -0
- app/frontend/scraping.html +1925 -0
- app/frontend/scraping_dashboard.html +863 -0
- app/frontend/search.html +1584 -0
- app/frontend/settings.html +2183 -0
- app/frontend/upload.html +1343 -0
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 & 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 & 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 & 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) Example: https://example.com/page1 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>
|