feat: doctor reply + diagnosis + tracking code per health request
Backend:
- HealthRequest model: TrackingCode (DR-XXXXXX), Diagnosis,
DoctorReply, RepliedAt fields
- Runtime migration: ALTER TABLE adds 4 new columns to existing DB
- POST /api/health-request: auto-generates tracking code, returns it
- PUT /api/health-requests/{id}/reply: doctor sets diagnosis + reply
- GET /api/health-request/track/{code}: public lookup by tracking code
- GET /api/health-requests?phone=: filter history by phone number
Admin panel:
- Request table shows tracking code column (gold badge)
- Detail modal (680px): tracking code header, patient info, full message
- Previous doctor reply shown in green box if exists
- Reply form: diagnosis input + textarea for doctor message
- History panel: all requests from same phone, click to switch
- 'پاسخ / مشاهده' button opens reply modal directly
Frontend:
- After form submit: shows tracking code in green box to user
(format: DR-XXXXXX, stays visible 8 seconds)
- Box auto-hides and form resets after timeout
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -722,7 +722,7 @@ tr:hover td{background:#FAFBFC}
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>نام</th><th>تلفن</th><th>دسته</th><th>پیام</th><th>تاریخ</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<thead><tr><th>کد رهگیری</th><th>نام</th><th>تلفن</th><th>دسته</th><th>پیام</th><th>تاریخ</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="healthreqTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -861,7 +861,7 @@ tr:hover td{background:#FAFBFC}
|
||||
<!-- Visit Modal -->
|
||||
<!-- Health Request Detail Modal -->
|
||||
<div class="modal-overlay hidden" id="reqDetailModal">
|
||||
<div class="modal" style="max-width:560px">
|
||||
<div class="modal" style="max-width:680px">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">جزئیات درخواست</div>
|
||||
<button class="modal-close" onclick="closeModal('reqDetailModal')">✕</button>
|
||||
@@ -1331,45 +1331,112 @@ async function loadHealthRequests() {
|
||||
const catLabel = {beauty:'زیبایی پوست', health:'سلامت عمومی'};
|
||||
document.getElementById('healthreqTable').innerHTML = _allReqs.map(r=>`
|
||||
<tr style="${!r.isHandled?'font-weight:600':'opacity:.75'}">
|
||||
<td><span style="font-family:monospace;font-size:.78rem;background:var(--gold-pale);color:var(--gold);padding:2px 7px;border-radius:6px">${r.trackingCode||'—'}</span></td>
|
||||
<td>${r.fullName}</td>
|
||||
<td dir="ltr">${r.phoneNumber}</td>
|
||||
<td>${catLabel[r.category]||r.category}</td>
|
||||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--mid);font-size:.85rem">${r.message||'—'}</td>
|
||||
<td>${new Date(r.createdAt).toLocaleDateString('fa-IR')}</td>
|
||||
<td><span style="background:${r.isHandled?'#E8F5E9':'#FFEBEE'};color:${r.isHandled?'#388E3C':'#C62828'};padding:2px 8px;border-radius:20px;font-size:.72rem">${r.isHandled?'بررسی شده':'جدید'}</span></td>
|
||||
<td dir="ltr" style="font-size:.85rem">${r.phoneNumber}</td>
|
||||
<td style="font-size:.82rem">${catLabel[r.category]||r.category}</td>
|
||||
<td style="max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--mid);font-size:.82rem">${r.message||'—'}</td>
|
||||
<td style="font-size:.82rem">${new Date(r.createdAt).toLocaleDateString('fa-IR')}</td>
|
||||
<td><span style="background:${r.isHandled?'#E8F5E9':'#FFEBEE'};color:${r.isHandled?'#388E3C':'#C62828'};padding:2px 8px;border-radius:20px;font-size:.72rem">${r.isHandled?'پاسخ داده شده':'جدید'}</span></td>
|
||||
<td style="white-space:nowrap">
|
||||
<button class="btn btn-secondary btn-sm" onclick="viewReq(${r.id})">مشاهده</button>
|
||||
${!r.isHandled?`<button class="btn btn-secondary btn-sm" onclick="handleReq(${r.id})">✓ بررسی شد</button>`:''}
|
||||
<button class="btn btn-secondary btn-sm" onclick="viewReq(${r.id})">پاسخ / مشاهده</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteReq(${r.id})">حذف</button>
|
||||
</td>
|
||||
</tr>`).join('') || '<tr><td colspan="7" style="text-align:center;color:var(--light);padding:2rem">درخواستی وجود ندارد</td></tr>';
|
||||
</tr>`).join('') || '<tr><td colspan="8" style="text-align:center;color:var(--light);padding:2rem">درخواستی وجود ندارد</td></tr>';
|
||||
}
|
||||
|
||||
function viewReq(id) {
|
||||
function lbl(text) {
|
||||
return `<span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.3rem">${text}</span>`;
|
||||
}
|
||||
|
||||
async function viewReq(id) {
|
||||
const r = _allReqs.find(x=>x.id===id); if(!r) return;
|
||||
const catLabel = {beauty:'زیبایی پوست', health:'سلامت عمومی'};
|
||||
|
||||
// Load history for same phone number
|
||||
const history = await api(`/api/health-requests?phone=${encodeURIComponent(r.phoneNumber)}`) || [];
|
||||
const histHtml = history.length > 1 ? `
|
||||
<div style="margin-top:1.5rem">
|
||||
<div style="font-size:.82rem;font-weight:600;color:var(--mid);margin-bottom:.6rem">📋 سابقه درخواستهای این شماره (${history.length} درخواست)</div>
|
||||
<div style="display:flex;flex-direction:column;gap:.5rem">
|
||||
${history.map(h=>`
|
||||
<div style="background:${h.id===r.id?'var(--gold-pale)':'var(--section-bg)'};border-radius:10px;padding:.6rem .9rem;font-size:.82rem;display:flex;align-items:center;gap:.8rem;cursor:pointer;border:1.5px solid ${h.id===r.id?'var(--gold)':'transparent'}" onclick="viewReq(${h.id})">
|
||||
<span style="color:var(--light);flex-shrink:0">${new Date(h.createdAt).toLocaleDateString('fa-IR')}</span>
|
||||
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${(h.message||'').substring(0,60)}${h.message?.length>60?'...':''}</span>
|
||||
<span style="background:${h.isHandled?'#E8F5E9':'#FFEBEE'};color:${h.isHandled?'#388E3C':'#C62828'};padding:2px 8px;border-radius:20px;font-size:.7rem;flex-shrink:0">${h.isHandled?'پاسخ داده شده':'جدید'}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
document.getElementById('reqDetailBody').innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1.2rem">
|
||||
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">نام</span><strong>${r.fullName}</strong></div>
|
||||
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">تلفن</span><strong dir="ltr">${r.phoneNumber}</strong></div>
|
||||
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">ایمیل</span><span>${r.email||'—'}</span></div>
|
||||
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">دسته</span>
|
||||
<span style="background:${r.category==='health'?'#E3F2FD':'var(--gold-pale)'};color:${r.category==='health'?'#1565C0':'var(--gold)'};padding:3px 10px;border-radius:20px;font-size:.78rem">${catLabel[r.category]||r.category}</span>
|
||||
<!-- Header: tracking code + status -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.2rem;flex-wrap:wrap;gap:.5rem">
|
||||
<div style="background:var(--gold-pale);border-radius:10px;padding:.4rem .9rem;font-size:.82rem;font-family:monospace;letter-spacing:.05em;color:var(--gold)">
|
||||
🔖 کد رهگیری: <strong>${r.trackingCode||'—'}</strong>
|
||||
</div>
|
||||
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">تاریخ ثبت</span><span>${new Date(r.createdAt).toLocaleDateString('fa-IR')}</span></div>
|
||||
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">وضعیت</span>
|
||||
<span style="background:${r.isHandled?'#E8F5E9':'#FFEBEE'};color:${r.isHandled?'#388E3C':'#C62828'};padding:3px 10px;border-radius:20px;font-size:.78rem">${r.isHandled?'بررسی شده':'جدید'}</span>
|
||||
<span style="background:${r.isHandled?'#E8F5E9':'#FFEBEE'};color:${r.isHandled?'#388E3C':'#C62828'};padding:4px 12px;border-radius:20px;font-size:.78rem">${r.isHandled?'✓ پاسخ داده شده':'⏳ در انتظار بررسی'}</span>
|
||||
</div>
|
||||
|
||||
<!-- Patient info -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.8rem;margin-bottom:1.2rem">
|
||||
<div>${lbl('نام')}<strong>${r.fullName}</strong></div>
|
||||
<div>${lbl('تلفن')}<strong dir="ltr">${r.phoneNumber}</strong></div>
|
||||
<div>${lbl('دسته')}
|
||||
<span style="background:${r.category==='health'?'#E3F2FD':'var(--gold-pale)'};color:${r.category==='health'?'#1565C0':'var(--gold)'};padding:2px 10px;border-radius:20px;font-size:.78rem">${catLabel[r.category]||r.category}</span>
|
||||
</div>
|
||||
<div>${lbl('تاریخ ثبت')}<span>${new Date(r.createdAt).toLocaleDateString('fa-IR')}</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Patient message -->
|
||||
<div style="margin-bottom:1.2rem">
|
||||
${lbl('پیام بیمار')}
|
||||
<div style="background:var(--section-bg);border-radius:10px;padding:.9rem 1rem;line-height:2;font-size:.88rem;white-space:pre-wrap;min-height:50px">${r.message||'—'}</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing reply (if any) -->
|
||||
${r.doctorReply||r.diagnosis ? `
|
||||
<div style="background:#E8F5E9;border-radius:12px;padding:1rem;margin-bottom:1.2rem;border-right:3px solid #388E3C">
|
||||
<div style="font-size:.78rem;color:#388E3C;font-weight:600;margin-bottom:.5rem">✓ پاسخ دکتر — ${r.repliedAt?new Date(r.repliedAt).toLocaleDateString('fa-IR'):''}</div>
|
||||
${r.diagnosis?`<div style="margin-bottom:.5rem">${lbl('تشخیص')}<strong>${r.diagnosis}</strong></div>`:''}
|
||||
${r.doctorReply?`<div>${lbl('پاسخ / توضیح')}<span style="font-size:.88rem">${r.doctorReply}</span></div>`:''}
|
||||
</div>` : ''}
|
||||
|
||||
<!-- Doctor reply form -->
|
||||
<div style="border-top:1px solid var(--border);padding-top:1.2rem">
|
||||
<div style="font-size:.88rem;font-weight:600;color:var(--mid);margin-bottom:.8rem">👩⚕️ پاسخ پزشک</div>
|
||||
<div class="form-group" style="margin-bottom:.8rem">
|
||||
<label style="font-size:.8rem">تشخیص</label>
|
||||
<input id="reply-diagnosis" value="${r.diagnosis||''}" placeholder="مثال: درماتیت آتوپیک، نیاز به مشاوره حضوری ..."/>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:1rem">
|
||||
<label style="font-size:.8rem">پاسخ / توضیح برای بیمار</label>
|
||||
<textarea id="reply-body" rows="4" placeholder="پاسخ خود را بنویسید. بیمار میتواند با کد رهگیری این پاسخ را ببیند...">${r.doctorReply||''}</textarea>
|
||||
</div>
|
||||
<div style="display:flex;gap:.6rem;flex-wrap:wrap">
|
||||
<button class="btn btn-primary" onclick="saveReply(${r.id})">💾 ذخیره پاسخ</button>
|
||||
${!r.isHandled?`<button class="btn btn-secondary" onclick="handleReq(${r.id})">✓ بررسی شد (بدون پاسخ)</button>`:''}
|
||||
<button class="btn btn-secondary" onclick="closeModal('reqDetailModal')">بستن</button>
|
||||
</div>
|
||||
</div>
|
||||
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.5rem">پیام / شرح درخواست</span>
|
||||
<div style="background:var(--section-bg);border-radius:12px;padding:1rem 1.2rem;line-height:2;font-size:.9rem;white-space:pre-wrap;min-height:60px">${r.message||'—'}</div>
|
||||
</div>
|
||||
${!r.isHandled?`<div style="margin-top:1.2rem"><button class="btn btn-primary" onclick="handleReq(${r.id});closeModal('reqDetailModal')">✓ علامتگذاری به عنوان بررسی شده</button></div>`:''}
|
||||
${histHtml}
|
||||
`;
|
||||
document.getElementById('reqDetailModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function handleReq(id){await api(`/api/health-requests/${id}`,{method:'PUT'});toast('علامتگذاری شد ✓');loadHealthRequests();}
|
||||
async function saveReply(id) {
|
||||
const diagnosis = document.getElementById('reply-diagnosis').value.trim();
|
||||
const doctorReply = document.getElementById('reply-body').value.trim();
|
||||
await api(`/api/health-requests/${id}/reply`, {
|
||||
method:'PUT',
|
||||
body: JSON.stringify({diagnosis, doctorReply})
|
||||
});
|
||||
toast('پاسخ ذخیره شد ✓');
|
||||
await loadHealthRequests();
|
||||
// Refresh view with updated data
|
||||
viewReq(id);
|
||||
}
|
||||
|
||||
async function handleReq(id){await api(`/api/health-requests/${id}`,{method:'PUT'});toast('علامتگذاری شد ✓');loadHealthRequests();closeModal('reqDetailModal');}
|
||||
async function deleteReq(id){if(!confirm('حذف؟'))return;await api(`/api/health-requests/${id}`,{method:'DELETE'});toast('حذف شد','error');loadHealthRequests();}
|
||||
|
||||
// ── Comments ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user