Remove GeminiLLM integration from ReportGenerationService, simplifying the report generation process by eliminating the markdown report generation logic. This change focuses on streamlining the service and enhancing maintainability.
Browse files- REPORT_API.md +113 -0
- src/services/report-generation.service.ts +0 -174
REPORT_API.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Report API - Integration Guide
|
| 2 |
+
|
| 3 |
+
## Endpoint
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
GET /api/reports/:session_id?type=full|summary|tools_only
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## Response Format
|
| 10 |
+
|
| 11 |
+
Trả về `MedicalReport` (JSON) - chỉ chứa dữ liệu y tế có ý nghĩa.
|
| 12 |
+
|
| 13 |
+
## JSON Mẫu
|
| 14 |
+
|
| 15 |
+
```json
|
| 16 |
+
{
|
| 17 |
+
"session_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 18 |
+
"user_id": "user123",
|
| 19 |
+
"created_at": "2024-01-15T10:30:00Z",
|
| 20 |
+
"updated_at": "2024-01-15T10:35:00Z",
|
| 21 |
+
|
| 22 |
+
"concerns": [
|
| 23 |
+
{
|
| 24 |
+
"description": "Da tôi bị nổi mẩn đỏ và ngứa",
|
| 25 |
+
"timestamp": "2024-01-15T10:30:00Z",
|
| 26 |
+
"has_image": true
|
| 27 |
+
}
|
| 28 |
+
],
|
| 29 |
+
|
| 30 |
+
"image_analysis": {
|
| 31 |
+
"top_conditions": [
|
| 32 |
+
{
|
| 33 |
+
"condition_name": "Viêm da dị ứng",
|
| 34 |
+
"confidence_percent": "85.3%",
|
| 35 |
+
"probability": 0.853
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"condition_name": "Chàm",
|
| 39 |
+
"confidence_percent": "72.1%",
|
| 40 |
+
"probability": 0.721
|
| 41 |
+
}
|
| 42 |
+
],
|
| 43 |
+
"model_type": "derm_cv"
|
| 44 |
+
},
|
| 45 |
+
|
| 46 |
+
"triage_assessment": [
|
| 47 |
+
{
|
| 48 |
+
"level": "routine",
|
| 49 |
+
"timestamp": "2024-01-15T10:30:00Z",
|
| 50 |
+
"red_flags": [],
|
| 51 |
+
"reasoning": "Triệu chứng nhẹ, không có dấu hiệu nguy hiểm"
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
|
| 55 |
+
"suspected_conditions": [
|
| 56 |
+
{
|
| 57 |
+
"condition_name": "Viêm da dị ứng",
|
| 58 |
+
"source": "cv_model",
|
| 59 |
+
"confidence": "high",
|
| 60 |
+
"occurrences": 1
|
| 61 |
+
}
|
| 62 |
+
],
|
| 63 |
+
|
| 64 |
+
"medical_guidelines": [
|
| 65 |
+
{
|
| 66 |
+
"content": "Điều trị viêm da dị ứng: Tránh tiếp xúc với chất gây dị ứng, sử dụng kem dưỡng ẩm...",
|
| 67 |
+
"relevance_score": 0.92,
|
| 68 |
+
"source": "Bộ Y Tế"
|
| 69 |
+
}
|
| 70 |
+
],
|
| 71 |
+
|
| 72 |
+
"recommendations": [
|
| 73 |
+
{
|
| 74 |
+
"action": "Đến khám bác sĩ da liễu trong vòng 1-2 ngày",
|
| 75 |
+
"timeframe": "1-2 ngày",
|
| 76 |
+
"home_care_advice": "Tránh gãi, dùng kem dưỡng ẩm",
|
| 77 |
+
"warning_signs": "Nếu vùng da lan rộng hoặc có mủ",
|
| 78 |
+
"timestamp": "2024-01-15T10:30:00Z"
|
| 79 |
+
}
|
| 80 |
+
],
|
| 81 |
+
|
| 82 |
+
"suggested_hospitals": [
|
| 83 |
+
{
|
| 84 |
+
"name": "Bệnh viện Da Liễu Trung ương",
|
| 85 |
+
"distance_km": 2.5,
|
| 86 |
+
"address": "123 Đường ABC, Quận XYZ",
|
| 87 |
+
"rating": 4.5,
|
| 88 |
+
"specialty_match": "high",
|
| 89 |
+
"condition": "Viêm da dị ứng"
|
| 90 |
+
}
|
| 91 |
+
]
|
| 92 |
+
}
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Ví dụ Sử dụng
|
| 96 |
+
|
| 97 |
+
```javascript
|
| 98 |
+
// Lấy report cho session
|
| 99 |
+
const response = await fetch('/api/reports/550e8400-e29b-41d4-a716-446655440000');
|
| 100 |
+
const medicalReport = await response.json();
|
| 101 |
+
|
| 102 |
+
// Sử dụng dữ liệu
|
| 103 |
+
console.log('Mối quan tâm:', medicalReport.concerns);
|
| 104 |
+
console.log('Bệnh nghi ngờ:', medicalReport.suspected_conditions);
|
| 105 |
+
console.log('Bệnh viện:', medicalReport.suggested_hospitals);
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
## Lưu ý
|
| 109 |
+
|
| 110 |
+
- Tất cả dữ liệu đều có ý nghĩa y tế (không có metadata kỹ thuật)
|
| 111 |
+
- `image_analysis` chỉ có khi có hình ảnh
|
| 112 |
+
- `suggested_hospitals` chỉ có khi emergency/urgent hoặc user yêu cầu
|
| 113 |
+
|
src/services/report-generation.service.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
|
| 2 |
import { logger } from '../utils/logger.js';
|
| 3 |
import { ConversationHistoryService } from './conversation-history.service.js';
|
| 4 |
import { ToolExecutionTrackerService } from './tool-execution-tracker.service.js';
|
| 5 |
-
import { GeminiLLM } from '../agent/gemini-llm.js';
|
| 6 |
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
|
| 8 |
/**
|
|
@@ -91,7 +90,6 @@ export class ReportGenerationService {
|
|
| 91 |
private supabaseClient: SupabaseClient;
|
| 92 |
private conversationService: ConversationHistoryService;
|
| 93 |
private toolTracker: ToolExecutionTrackerService;
|
| 94 |
-
private llm: GeminiLLM;
|
| 95 |
|
| 96 |
constructor(
|
| 97 |
supabaseClient: SupabaseClient,
|
|
@@ -101,7 +99,6 @@ export class ReportGenerationService {
|
|
| 101 |
this.supabaseClient = supabaseClient;
|
| 102 |
this.conversationService = conversationService;
|
| 103 |
this.toolTracker = toolTracker;
|
| 104 |
-
this.llm = new GeminiLLM();
|
| 105 |
}
|
| 106 |
|
| 107 |
/**
|
|
@@ -368,177 +365,6 @@ export class ReportGenerationService {
|
|
| 368 |
return 'medium';
|
| 369 |
}
|
| 370 |
|
| 371 |
-
/**
|
| 372 |
-
* Generate markdown report using LLM (Optional - chỉ dùng cho display)
|
| 373 |
-
*/
|
| 374 |
-
private async generateMarkdownReport(
|
| 375 |
-
reportContent: MedicalReport,
|
| 376 |
-
reportType: string
|
| 377 |
-
): Promise<string> {
|
| 378 |
-
const prompt = `Bạn là trợ lý y tế chuyên nghiệp. Hãy tạo một báo cáo tổng hợp đầy đủ về cuộc trò chuyện y tế dựa trên dữ liệu sau.
|
| 379 |
-
|
| 380 |
-
LOẠI BÁO CÁO: ${reportType === 'full' ? 'Báo cáo đầy đủ' : reportType === 'summary' ? 'Tóm tắt' : 'Chỉ công cụ'}
|
| 381 |
-
|
| 382 |
-
THÔNG TIN PHIÊN:
|
| 383 |
-
- Session ID: ${reportContent.session_info.session_id}
|
| 384 |
-
- Thời gian bắt đầu: ${reportContent.session_info.created_at}
|
| 385 |
-
- Thời gian cập nhật: ${reportContent.session_info.updated_at}
|
| 386 |
-
- Số lượng tin nhắn: ${reportContent.session_info.message_count}
|
| 387 |
-
|
| 388 |
-
LỊCH SỬ HỘI THOẠI:
|
| 389 |
-
${reportContent.conversation_timeline.map((msg, idx) => `
|
| 390 |
-
${idx + 1}. [${msg.role === 'user' ? 'Người dùng' : 'Hệ thống'}] (${msg.timestamp})
|
| 391 |
-
${msg.role === 'user' ? 'Câu hỏi/Triệu chứng:' : 'Phản hồi:'}
|
| 392 |
-
${msg.content}
|
| 393 |
-
${msg.image_url ? '📷 [Có hình ảnh đính kèm]' : ''}
|
| 394 |
-
${msg.triage_result ? `\n Mức độ khẩn cấp: ${msg.triage_result.triage_level}` : ''}
|
| 395 |
-
`).join('\n')}
|
| 396 |
-
|
| 397 |
-
THỰC THI CÔNG CỤ (TOOL EXECUTIONS):
|
| 398 |
-
${reportContent.tool_executions.map((exec, idx) => `
|
| 399 |
-
${idx + 1}. ${exec.tool_display_name || exec.tool_name}
|
| 400 |
-
- Thứ tự: ${exec.execution_order}
|
| 401 |
-
- Trạng thái: ${exec.status}
|
| 402 |
-
- Thời gian: ${exec.execution_time_ms}ms
|
| 403 |
-
- Input: ${JSON.stringify(exec.input_data, null, 2)}
|
| 404 |
-
- Output: ${JSON.stringify(exec.output_data, null, 2)}
|
| 405 |
-
`).join('\n')}
|
| 406 |
-
|
| 407 |
-
TÓM TẮT:
|
| 408 |
-
- Mối quan tâm chính: ${reportContent.summary.main_concerns.join(', ')}
|
| 409 |
-
- Các bệnh được đề xuất: ${reportContent.summary.top_conditions_suggested.map(c => `${c.name} (${c.confidence}, xuất hiện ${c.occurrences} lần)`).join(', ')}
|
| 410 |
-
- Mức độ khẩn cấp: ${reportContent.summary.triage_levels_identified.map(t => `${t.level} (${t.count} lần)`).join(', ')}
|
| 411 |
-
- Bệnh viện được đề xuất: ${reportContent.summary.hospitals_suggested.map(h => `${h.name} (${h.distance_km}km)`).join(', ') || 'Không có'}
|
| 412 |
-
- Số guideline đã truy xuất: ${reportContent.summary.key_guidelines_retrieved}
|
| 413 |
-
|
| 414 |
-
YÊU CẦU:
|
| 415 |
-
1. Tạo báo cáo markdown CHUYÊN NGHIỆP, DỄ ĐỌC, CẤU TRÚC RÕ RÀNG
|
| 416 |
-
2. Bao gồm TẤT CẢ thông tin quan trọng từ tools (CV top 3, RAG guidelines đầy đủ, triage reasoning)
|
| 417 |
-
3. Sử dụng tiếng Việt hoàn toàn
|
| 418 |
-
4. Format đẹp với markdown (tiêu đề, danh sách, bảng nếu cần)
|
| 419 |
-
5. Nhấn mạnh các thông tin quan trọng mà response message có thể đã bỏ qua
|
| 420 |
-
6. Bao gồm disclaimer y tế phù hợp
|
| 421 |
-
|
| 422 |
-
CẤU TRÚC BÁO CÁO:
|
| 423 |
-
# BÁO CÁO TỔNG HỢP - PHIÊN TƯ VẤN Y TẾ
|
| 424 |
-
|
| 425 |
-
## 1. THÔNG TIN PHIÊN
|
| 426 |
-
[Session info]
|
| 427 |
-
|
| 428 |
-
## 2. TÓM TẮT CUỘC HỘI THOẠI
|
| 429 |
-
[Summary of main concerns, conditions, triage levels]
|
| 430 |
-
|
| 431 |
-
## 3. CHI TIẾT HỘI THOẠI
|
| 432 |
-
[Full conversation timeline]
|
| 433 |
-
|
| 434 |
-
## 4. PHÂN TÍCH CÔNG CỤ (TOOLS ANALYSIS)
|
| 435 |
-
[Detailed tool execution results - CV top 3, RAG guidelines, etc.]
|
| 436 |
-
|
| 437 |
-
## 5. KẾT LUẬN VÀ KHUYẾN NGHỊ
|
| 438 |
-
[Final recommendations]
|
| 439 |
-
|
| 440 |
-
**Lưu ý:** Thông tin chỉ mang tính tham khảo, không thay thế bác sĩ.`;
|
| 441 |
-
|
| 442 |
-
try {
|
| 443 |
-
const generations = await this.llm._generate([prompt]);
|
| 444 |
-
return generations.generations[0][0].text.trim();
|
| 445 |
-
} catch (error) {
|
| 446 |
-
logger.error({ error }, 'Error generating markdown report');
|
| 447 |
-
// Fallback to simple markdown
|
| 448 |
-
return this.generateFallbackMarkdown(reportContent);
|
| 449 |
-
}
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
/**
|
| 453 |
-
* Generate fallback markdown if LLM fails (Optional)
|
| 454 |
-
*/
|
| 455 |
-
private generateFallbackMarkdown(
|
| 456 |
-
reportContent: MedicalReport
|
| 457 |
-
): string {
|
| 458 |
-
return `# BÁO CÁO TỔNG HỢP - PHIÊN TƯ VẤN Y TẾ
|
| 459 |
-
|
| 460 |
-
## 1. THÔNG TIN PHIÊN
|
| 461 |
-
- **Session ID:** ${reportContent.session_info.session_id}
|
| 462 |
-
- **Thời gian bắt đầu:** ${reportContent.session_info.created_at}
|
| 463 |
-
- **Thời gian cập nhật:** ${reportContent.session_info.updated_at}
|
| 464 |
-
- **Số lượng tin nhắn:** ${reportContent.session_info.message_count}
|
| 465 |
-
|
| 466 |
-
## 2. TÓM TẮT CUỘC HỘI THOẠI
|
| 467 |
-
|
| 468 |
-
### Mối quan tâm chính:
|
| 469 |
-
${reportContent.summary.main_concerns.map(c => `- ${c}`).join('\n')}
|
| 470 |
-
|
| 471 |
-
### Các bệnh được đề xuất:
|
| 472 |
-
${reportContent.summary.top_conditions_suggested.map(c =>
|
| 473 |
-
`- **${c.name}** (${c.confidence}, xuất hiện ${c.occurrences} lần, nguồn: ${c.source})`
|
| 474 |
-
).join('\n')}
|
| 475 |
-
|
| 476 |
-
### Mức độ khẩn cấp:
|
| 477 |
-
${reportContent.summary.triage_levels_identified.map(t =>
|
| 478 |
-
`- **${t.level}**: ${t.count} lần`
|
| 479 |
-
).join('\n')}
|
| 480 |
-
|
| 481 |
-
### Bệnh viện được đề xuất:
|
| 482 |
-
${reportContent.summary.hospitals_suggested.length > 0
|
| 483 |
-
? reportContent.summary.hospitals_suggested.map(h =>
|
| 484 |
-
`- **${h.name}** (${h.distance_km}km) - ${h.address}`
|
| 485 |
-
).join('\n')
|
| 486 |
-
: '- Không có bệnh viện nào được đề xuất'
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
-
### Số guideline đã truy xuất:
|
| 490 |
-
- ${reportContent.summary.key_guidelines_retrieved} guideline snippets
|
| 491 |
-
|
| 492 |
-
## 3. CHI TIẾT HỘI THOẠI
|
| 493 |
-
|
| 494 |
-
${reportContent.conversation_timeline.map((msg, idx) => `
|
| 495 |
-
### Tin nhắn ${idx + 1} - ${msg.role === 'user' ? 'Người dùng' : 'Hệ thống'}
|
| 496 |
-
|
| 497 |
-
**Thời gian:** ${msg.timestamp}
|
| 498 |
-
|
| 499 |
-
**Nội dung:**
|
| 500 |
-
${msg.content}
|
| 501 |
-
|
| 502 |
-
${msg.image_url ? '📷 *Có hình ảnh đính kèm*' : ''}
|
| 503 |
-
|
| 504 |
-
${msg.triage_result ? `
|
| 505 |
-
**Kết quả phân tích:**
|
| 506 |
-
- Mức độ khẩn cấp: ${msg.triage_result.triage_level}
|
| 507 |
-
- Tóm tắt triệu chứng: ${msg.triage_result.symptom_summary}
|
| 508 |
-
${msg.triage_result.suspected_conditions?.length > 0 ? `
|
| 509 |
-
- Bệnh nghi ngờ:
|
| 510 |
-
${msg.triage_result.suspected_conditions.map((c: any) => ` - ${c.name} (${c.confidence}, nguồn: ${c.source})`).join('\n')}
|
| 511 |
-
` : ''}
|
| 512 |
-
` : ''}
|
| 513 |
-
`).join('\n')}
|
| 514 |
-
|
| 515 |
-
## 4. PHÂN TÍCH CÔNG CỤ (TOOLS ANALYSIS)
|
| 516 |
-
|
| 517 |
-
${reportContent.tool_executions.map((exec, idx) => `
|
| 518 |
-
### ${idx + 1}. ${exec.tool_display_name || exec.tool_name}
|
| 519 |
-
|
| 520 |
-
**Thứ tự thực thi:** ${exec.execution_order}
|
| 521 |
-
**Trạng thái:** ${exec.status}
|
| 522 |
-
**Thời gian:** ${exec.execution_time_ms}ms
|
| 523 |
-
|
| 524 |
-
**Input:**
|
| 525 |
-
\`\`\`json
|
| 526 |
-
${JSON.stringify(exec.input_data, null, 2)}
|
| 527 |
-
\`\`\`
|
| 528 |
-
|
| 529 |
-
**Output:**
|
| 530 |
-
\`\`\`json
|
| 531 |
-
${JSON.stringify(exec.output_data, null, 2)}
|
| 532 |
-
\`\`\`
|
| 533 |
-
`).join('\n')}
|
| 534 |
-
|
| 535 |
-
## 5. KẾT LUẬN VÀ KHUYẾN NGHỊ
|
| 536 |
-
|
| 537 |
-
Dựa trên phân tích toàn bộ cuộc hội thoại và kết quả từ các công cụ, đây là báo cáo tổng hợp đầy đủ về phiên tư vấn y tế.
|
| 538 |
-
|
| 539 |
-
**Lưu ý quan trọng:** Thông tin trong báo cáo này chỉ mang tính tham khảo giáo dục, không thay thế việc khám và chẩn đoán của bác sĩ. Nếu bạn có triệu chứng nghiêm trọng, hãy đến cơ sở y tế ngay lập tức.`;
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
/**
|
| 543 |
* Save report to database
|
| 544 |
*/
|
|
|
|
| 2 |
import { logger } from '../utils/logger.js';
|
| 3 |
import { ConversationHistoryService } from './conversation-history.service.js';
|
| 4 |
import { ToolExecutionTrackerService } from './tool-execution-tracker.service.js';
|
|
|
|
| 5 |
import { v4 as uuidv4 } from 'uuid';
|
| 6 |
|
| 7 |
/**
|
|
|
|
| 90 |
private supabaseClient: SupabaseClient;
|
| 91 |
private conversationService: ConversationHistoryService;
|
| 92 |
private toolTracker: ToolExecutionTrackerService;
|
|
|
|
| 93 |
|
| 94 |
constructor(
|
| 95 |
supabaseClient: SupabaseClient,
|
|
|
|
| 99 |
this.supabaseClient = supabaseClient;
|
| 100 |
this.conversationService = conversationService;
|
| 101 |
this.toolTracker = toolTracker;
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
/**
|
|
|
|
| 365 |
return 'medium';
|
| 366 |
}
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
/**
|
| 369 |
* Save report to database
|
| 370 |
*/
|