feat: patient management system + health landing page
Backend:
- Patient model: name, phone, email, age, weight, height, gender,
blood type, disease history, allergies, medications, notes, category
- PatientVisit model: title, content, prescription, visit type,
visit/next-visit dates, linked to patient (cascade delete)
- HealthRequest model: public form submissions for beauty/health care
- Runtime SQLite migrations for all 3 new tables
- Full CRUD API: /api/patients, /api/patients/{id}/visits,
/api/health-requests (public POST + admin GET/PUT/DELETE)
Admin panel:
- 'پرونده بیماران' page: list, search, filter by category (beauty/health)
- Patient profile page: personal info + medical history + visits timeline
- Add/edit patient modal with all medical fields
- Add visit modal: type, date, clinical notes, prescription, next visit
- 'درخواستها' page: manage public health requests, mark as handled
- Badge counter for unhandled requests in sidebar
Frontend (SEO):
- New #health-care section with Schema.org MedicalClinic markup
- Two category cards: زیبایی پوست and سلامت عمومی
- Feature lists with checkmarks per category
- Inline request form that submits to /api/health-request
- Mobile responsive (single column on small screens)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,10 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
|
||||
public DbSet<BlogCategory> BlogCategories => Set<BlogCategory>();
|
||||
public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
|
||||
public DbSet<Comment> Comments => Set<Comment>();
|
||||
public DbSet<Faq> Faqs => Set<Faq>();
|
||||
public DbSet<Faq> Faqs => Set<Faq>();
|
||||
public DbSet<Patient> Patients => Set<Patient>();
|
||||
public DbSet<PatientVisit> PatientVisits => Set<PatientVisit>();
|
||||
public DbSet<HealthRequest> HealthRequests => Set<HealthRequest>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder mb)
|
||||
{
|
||||
|
||||
@@ -130,6 +130,56 @@ public class Faq
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Patient ──────────────────────────────────────────────────────────────────
|
||||
public class Patient
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(150)] public string FullName { get; set; } = "";
|
||||
[MaxLength(20)] public string PhoneNumber { get; set; } = "";
|
||||
[MaxLength(200)] public string Email { get; set; } = "";
|
||||
public int Age { get; set; }
|
||||
public decimal Weight { get; set; } // kg
|
||||
public decimal Height { get; set; } // cm
|
||||
[MaxLength(10)] public string Gender { get; set; } = ""; // مرد / زن
|
||||
[MaxLength(10)] public string BloodType { get; set; } = "";
|
||||
public string DiseaseHistory { get; set; } = "";
|
||||
public string Allergies { get; set; } = "";
|
||||
public string Medications { get; set; } = "";
|
||||
public string Notes { get; set; } = "";
|
||||
[MaxLength(20)] public string Category { get; set; } = "beauty"; // beauty | health
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public ICollection<PatientVisit> Visits { get; set; } = new List<PatientVisit>();
|
||||
}
|
||||
|
||||
// ─── Patient Visit / Doctor Note ──────────────────────────────────────────────
|
||||
public class PatientVisit
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int PatientId { get; set; }
|
||||
public Patient? Patient { get; set; }
|
||||
[MaxLength(300)] public string Title { get; set; } = "";
|
||||
public string Content { get; set; } = "";
|
||||
public string Prescription { get; set; } = ""; // دارو / تجویز
|
||||
[MaxLength(50)] public string VisitType { get; set; } = "ویزیت"; // ویزیت | آزمایش | پروسیجر
|
||||
public DateTime VisitDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? NextVisitDate { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── Health / Appointment Request (public form) ───────────────────────────────
|
||||
public class HealthRequest
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[MaxLength(150)] public string FullName { get; set; } = "";
|
||||
[MaxLength(20)] public string PhoneNumber { get; set; } = "";
|
||||
[MaxLength(200)] public string Email { get; set; } = "";
|
||||
public string Message { get; set; } = "";
|
||||
[MaxLength(20)] public string Category { get; set; } = "beauty"; // beauty | health
|
||||
public bool IsHandled { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// ─── DTOs ─────────────────────────────────────────────────────────────────────
|
||||
public record LoginRequest(string Username, string Password);
|
||||
public record ChangePasswordRequest(string CurrentPassword, string NewPassword);
|
||||
|
||||
@@ -172,6 +172,29 @@
|
||||
.faq-item[open] summary::after { content:"-"; }
|
||||
.faq-answer { padding:0 1.5rem 1.25rem; color:var(--mid); font-size:0.92rem; line-height:1.85; border-top:1px solid var(--border); }
|
||||
/* ─── Contact ──────────────────────────────────────────────── */
|
||||
/* ─── Health Care Landing ─────────────────────────────────── */
|
||||
#health-care { background:var(--section-bg); }
|
||||
.health-header { text-align:center; margin-bottom:3rem; }
|
||||
.health-cats { display:grid; grid-template-columns:1fr 1fr; gap:2rem; margin-bottom:2.5rem; }
|
||||
.health-cat-card { background:var(--white); border-radius:24px; padding:2.2rem; border:1px solid var(--border); display:flex; flex-direction:column; gap:1rem; }
|
||||
.health-cat-icon { width:60px; height:60px; border-radius:18px; display:flex; align-items:center; justify-content:center; }
|
||||
.health-cat-icon svg { width:28px; height:28px; }
|
||||
.health-cat-icon.beauty { background:#FCE4EC; color:#C2185B; }
|
||||
.health-cat-icon.health { background:#E3F2FD; color:#1565C0; }
|
||||
.health-cat-card h3 { font-size:1.15rem; font-weight:700; color:var(--dark); }
|
||||
.health-cat-card p { font-size:.88rem; color:var(--mid); line-height:1.8; }
|
||||
.health-cat-list { list-style:none; padding:0; display:flex; flex-direction:column; gap:.45rem; flex:1; }
|
||||
.health-cat-list li { font-size:.85rem; color:var(--mid); padding-right:1.2rem; position:relative; }
|
||||
.health-cat-list li::before { content:"✓"; position:absolute; right:0; color:var(--gold); font-weight:700; }
|
||||
.health-cta-btn { background:var(--gold); color:#fff; border:none; border-radius:12px; padding:.85rem 1.5rem; font-family:'Vazirmatn',sans-serif; font-size:.9rem; font-weight:600; cursor:pointer; transition:background .25s; margin-top:auto; }
|
||||
.health-cta-btn:hover { background:#a07840; }
|
||||
.health-cta-btn.health-btn { background:#1565C0; }
|
||||
.health-cta-btn.health-btn:hover { background:#0d47a1; }
|
||||
.health-form-wrap { margin-top:1rem; }
|
||||
.health-form-card { background:var(--white); border-radius:24px; padding:2.5rem; border:1px solid var(--border); max-width:680px; margin:0 auto; }
|
||||
.health-form-card h3 { font-size:1.1rem; font-weight:700; margin-bottom:.5rem; }
|
||||
#health-care .hidden { display:none; }
|
||||
/* ─── Contact ─────────────────────────────────────────────── */
|
||||
#contact { background:var(--white); }
|
||||
.contact-grid { display:grid; grid-template-columns:1fr 1.4fr; gap:4rem; align-items:start; }
|
||||
.contact-info-list { display:flex; flex-direction:column; gap:1.5rem; margin-top:2rem; }
|
||||
@@ -227,6 +250,7 @@
|
||||
.about-image-wrap { text-align:center; }
|
||||
.testimonials-grid { grid-template-columns:1fr; }
|
||||
.section-title { font-size:1.5rem; }
|
||||
.health-cats { grid-template-columns:1fr; }
|
||||
.faq-item summary { font-size:.9rem; padding:1rem 1.2rem; }
|
||||
.faq-answer { padding:0 1.2rem 1rem; font-size:.88rem; }
|
||||
}
|
||||
@@ -581,6 +605,72 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ HEALTH CARE LANDING ══════ -->
|
||||
<section id="health-care" itemscope itemtype="https://schema.org/MedicalClinic">
|
||||
<div class="container">
|
||||
<div class="health-header fade-in">
|
||||
<span class="section-label">مراقبت سلامت</span>
|
||||
<h2 class="section-title" itemprop="name">خدمات پزشکی دکتر آلطه</h2>
|
||||
<div class="divider"></div>
|
||||
<p class="section-desc" itemprop="description">ما در دو حوزه تخصصی <strong>زیبایی پوست</strong> و <strong>سلامت عمومی</strong> در کنار شما هستیم. از مشاوره اولیه تا پیگیری درمان، تیم ما آماده پاسخگویی است.</p>
|
||||
</div>
|
||||
|
||||
<!-- Category Cards -->
|
||||
<div class="health-cats fade-in">
|
||||
<div class="health-cat-card" itemprop="availableService" itemscope itemtype="https://schema.org/MedicalTherapy">
|
||||
<div class="health-cat-icon beauty">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>
|
||||
</div>
|
||||
<h3 itemprop="name">زیبایی پوست</h3>
|
||||
<p itemprop="description">بوتاکس، فیلر، لیزر، مزوتراپی، پاکسازی عمیق پوست و درمان تخصصی انواع مشکلات پوستی توسط متخصص.</p>
|
||||
<ul class="health-cat-list">
|
||||
<li>تزریق بوتاکس و فیلر</li>
|
||||
<li>لیزر موهای زائد و جوانسازی</li>
|
||||
<li>مزوتراپی و PRP</li>
|
||||
<li>درمان جای جوش و لک</li>
|
||||
<li>پاکسازی عمیق پوست</li>
|
||||
</ul>
|
||||
<button class="health-cta-btn" onclick="openHealthForm('beauty')">درخواست مشاوره زیبایی</button>
|
||||
</div>
|
||||
|
||||
<div class="health-cat-card" itemprop="availableService" itemscope itemtype="https://schema.org/MedicalTherapy">
|
||||
<div class="health-cat-icon health">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||
</div>
|
||||
<h3 itemprop="name">سلامت عمومی</h3>
|
||||
<p itemprop="description">معاینه، تشخیص و پیگیری بیماریهای عمومی، مشاوره تغذیه، مدیریت وزن و برنامهریزی سلامت فردی.</p>
|
||||
<ul class="health-cat-list">
|
||||
<li>معاینه و ویزیت تخصصی</li>
|
||||
<li>مشاوره و مدیریت وزن</li>
|
||||
<li>پیگیری بیماریهای مزمن</li>
|
||||
<li>برنامه سلامت شخصیسازیشده</li>
|
||||
<li>آزمایشات تخصصی</li>
|
||||
</ul>
|
||||
<button class="health-cta-btn health-btn" onclick="openHealthForm('health')">درخواست مراقبت سلامت</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health Request Form -->
|
||||
<div class="health-form-wrap fade-in hidden" id="healthFormWrap">
|
||||
<div class="health-form-card">
|
||||
<h3 id="healthFormTitle">درخواست مشاوره</h3>
|
||||
<p style="color:var(--mid);font-size:.88rem;margin-bottom:1.5rem">فرم زیر را تکمیل کنید. تیم ما در اسرع وقت با شما تماس میگیرد.</p>
|
||||
<input type="hidden" id="hf-category"/>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>نام و نام خانوادگی *</label><input id="hf-name" placeholder="نام شما" required/></div>
|
||||
<div class="form-group"><label>شماره موبایل *</label><input id="hf-phone" type="tel" placeholder="09xx-xxx-xxxx" required/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>ایمیل (اختیاری)</label><input id="hf-email" type="email" placeholder="email@example.com"/></div>
|
||||
<div class="form-group"><label>شرح درخواست یا مشکل</label><textarea id="hf-message" rows="4" placeholder="لطفاً مشکل یا درخواست خود را بنویسید ..."></textarea></div>
|
||||
<div style="display:flex;gap:.8rem;flex-wrap:wrap">
|
||||
<button class="form-submit" id="hf-submit" onclick="submitHealthForm()" style="flex:1">ارسال درخواست</button>
|
||||
<button onclick="document.getElementById('healthFormWrap').classList.add('hidden')" style="background:transparent;border:1.5px solid var(--border);border-radius:12px;padding:.8rem 1.5rem;cursor:pointer;font-family:inherit;font-size:.9rem">انصراف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════ CONTACT ══════ -->
|
||||
<section id="contact">
|
||||
<div class="container">
|
||||
@@ -758,6 +848,52 @@
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Health care request form
|
||||
function openHealthForm(category) {
|
||||
document.getElementById('hf-category').value = category;
|
||||
document.getElementById('healthFormTitle').textContent =
|
||||
category === 'health' ? 'درخواست مراقبت سلامت عمومی' : 'درخواست مشاوره زیبایی پوست';
|
||||
const wrap = document.getElementById('healthFormWrap');
|
||||
wrap.classList.remove('hidden');
|
||||
setTimeout(() => wrap.scrollIntoView({behavior:'smooth', block:'center'}), 100);
|
||||
}
|
||||
async function submitHealthForm() {
|
||||
const name = document.getElementById('hf-name').value.trim();
|
||||
const phone = document.getElementById('hf-phone').value.trim();
|
||||
if (!name || !phone) { alert('نام و شماره تلفن الزامی است'); return; }
|
||||
const btn = document.getElementById('hf-submit');
|
||||
btn.textContent = '...در حال ارسال';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const body = {
|
||||
fullName: name,
|
||||
phoneNumber: phone,
|
||||
email: document.getElementById('hf-email').value,
|
||||
message: document.getElementById('hf-message').value,
|
||||
category: document.getElementById('hf-category').value
|
||||
};
|
||||
const res = await fetch('/api/health-request', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
btn.textContent = '✓ درخواست شما ثبت شد';
|
||||
btn.style.background = '#2D7A4F';
|
||||
['hf-name','hf-phone','hf-email','hf-message'].forEach(id => document.getElementById(id).value='');
|
||||
setTimeout(() => {
|
||||
document.getElementById('healthFormWrap').classList.add('hidden');
|
||||
btn.textContent = 'ارسال درخواست';
|
||||
btn.style.background = '';
|
||||
btn.disabled = false;
|
||||
}, 3000);
|
||||
} catch {
|
||||
btn.textContent = 'خطا — دوباره تلاش کنید';
|
||||
btn.style.background = '#c62828';
|
||||
setTimeout(() => { btn.textContent='ارسال درخواست'; btn.style.background=''; btn.disabled=false; }, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
// Active nav link on scroll
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
window.addEventListener('scroll', () => {
|
||||
|
||||
@@ -89,6 +89,60 @@ await using (var scope = app.Services.CreateAsyncScope())
|
||||
""");
|
||||
}
|
||||
catch { /* already exists */ }
|
||||
// Ensure Patient tables exist
|
||||
try {
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS "Patients" (
|
||||
"Id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"FullName" TEXT NOT NULL DEFAULT '',
|
||||
"PhoneNumber" TEXT NOT NULL DEFAULT '',
|
||||
"Email" TEXT NOT NULL DEFAULT '',
|
||||
"Age" INTEGER NOT NULL DEFAULT 0,
|
||||
"Weight" REAL NOT NULL DEFAULT 0,
|
||||
"Height" REAL NOT NULL DEFAULT 0,
|
||||
"Gender" TEXT NOT NULL DEFAULT '',
|
||||
"BloodType" TEXT NOT NULL DEFAULT '',
|
||||
"DiseaseHistory" TEXT NOT NULL DEFAULT '',
|
||||
"Allergies" TEXT NOT NULL DEFAULT '',
|
||||
"Medications" TEXT NOT NULL DEFAULT '',
|
||||
"Notes" TEXT NOT NULL DEFAULT '',
|
||||
"Category" TEXT NOT NULL DEFAULT 'beauty',
|
||||
"IsActive" INTEGER NOT NULL DEFAULT 1,
|
||||
"CreatedAt" TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
""");
|
||||
} catch { }
|
||||
try {
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS "PatientVisits" (
|
||||
"Id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"PatientId" INTEGER NOT NULL,
|
||||
"Title" TEXT NOT NULL DEFAULT '',
|
||||
"Content" TEXT NOT NULL DEFAULT '',
|
||||
"Prescription" TEXT NOT NULL DEFAULT '',
|
||||
"VisitType" TEXT NOT NULL DEFAULT 'ویزیت',
|
||||
"VisitDate" TEXT NOT NULL DEFAULT '',
|
||||
"NextVisitDate" TEXT,
|
||||
"CreatedAt" TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY ("PatientId") REFERENCES "Patients"("Id") ON DELETE CASCADE
|
||||
)
|
||||
""");
|
||||
} catch { }
|
||||
try {
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS "HealthRequests" (
|
||||
"Id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"FullName" TEXT NOT NULL DEFAULT '',
|
||||
"PhoneNumber" TEXT NOT NULL DEFAULT '',
|
||||
"Email" TEXT NOT NULL DEFAULT '',
|
||||
"Message" TEXT NOT NULL DEFAULT '',
|
||||
"Category" TEXT NOT NULL DEFAULT 'beauty',
|
||||
"IsHandled" INTEGER NOT NULL DEFAULT 0,
|
||||
"CreatedAt" TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
""");
|
||||
} catch { }
|
||||
|
||||
await SeedAsync(db);
|
||||
}
|
||||
|
||||
@@ -566,6 +620,121 @@ app.MapDelete("/api/upload/{filename}", (string filename, IWebHostEnvironment en
|
||||
return Results.NoContent();
|
||||
}).RequireAuthorization();
|
||||
|
||||
// ── Patients (admin) ──────────────────────────────────────────────────────────
|
||||
var patientsGroup = app.MapGroup("/api/patients").RequireAuthorization();
|
||||
|
||||
patientsGroup.MapGet("/", async (string? category, string? search, AppDbContext db) =>
|
||||
{
|
||||
var q = db.Patients.Where(p => p.IsActive);
|
||||
if (!string.IsNullOrEmpty(category)) q = q.Where(p => p.Category == category);
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
q = q.Where(p => p.FullName.Contains(search) || p.PhoneNumber.Contains(search));
|
||||
return Results.Ok(await q.OrderByDescending(p => p.CreatedAt)
|
||||
.Select(p => new { p.Id, p.FullName, p.PhoneNumber, p.Email, p.Age, p.Gender,
|
||||
p.Category, p.BloodType, p.CreatedAt,
|
||||
VisitCount = db.PatientVisits.Count(v => v.PatientId == p.Id) })
|
||||
.ToListAsync());
|
||||
});
|
||||
|
||||
patientsGroup.MapGet("/{id:int}", async (int id, AppDbContext db) =>
|
||||
{
|
||||
var p = await db.Patients.Include(x => x.Visits.OrderByDescending(v => v.VisitDate))
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
return p is null ? Results.NotFound() : Results.Ok(p);
|
||||
});
|
||||
|
||||
patientsGroup.MapPost("/", async (Patient patient, AppDbContext db) =>
|
||||
{
|
||||
patient.CreatedAt = DateTime.UtcNow;
|
||||
db.Patients.Add(patient);
|
||||
await db.SaveChangesAsync();
|
||||
return Results.Created($"/api/patients/{patient.Id}", patient);
|
||||
});
|
||||
|
||||
patientsGroup.MapPut("/{id:int}", async (int id, Patient updated, AppDbContext db) =>
|
||||
{
|
||||
var p = await db.Patients.FindAsync(id);
|
||||
if (p is null) return Results.NotFound();
|
||||
p.FullName = updated.FullName; p.PhoneNumber = updated.PhoneNumber;
|
||||
p.Email = updated.Email; p.Age = updated.Age; p.Weight = updated.Weight;
|
||||
p.Height = updated.Height; p.Gender = updated.Gender; p.BloodType = updated.BloodType;
|
||||
p.DiseaseHistory = updated.DiseaseHistory; p.Allergies = updated.Allergies;
|
||||
p.Medications = updated.Medications; p.Notes = updated.Notes;
|
||||
p.Category = updated.Category; p.IsActive = updated.IsActive;
|
||||
await db.SaveChangesAsync();
|
||||
return Results.Ok(p);
|
||||
});
|
||||
|
||||
patientsGroup.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
|
||||
{
|
||||
var p = await db.Patients.FindAsync(id);
|
||||
if (p is null) return Results.NotFound();
|
||||
p.IsActive = false; // soft delete
|
||||
await db.SaveChangesAsync();
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
// Patient visits/notes
|
||||
patientsGroup.MapGet("/{id:int}/visits", async (int id, AppDbContext db) =>
|
||||
Results.Ok(await db.PatientVisits.Where(v => v.PatientId == id)
|
||||
.OrderByDescending(v => v.VisitDate).ToListAsync()));
|
||||
|
||||
patientsGroup.MapPost("/{id:int}/visits", async (int id, PatientVisit visit, AppDbContext db) =>
|
||||
{
|
||||
var patient = await db.Patients.FindAsync(id);
|
||||
if (patient is null) return Results.NotFound();
|
||||
visit.PatientId = id;
|
||||
visit.CreatedAt = DateTime.UtcNow;
|
||||
if (visit.VisitDate == default) visit.VisitDate = DateTime.UtcNow;
|
||||
db.PatientVisits.Add(visit);
|
||||
await db.SaveChangesAsync();
|
||||
return Results.Created($"/api/patients/{id}/visits/{visit.Id}", visit);
|
||||
});
|
||||
|
||||
patientsGroup.MapDelete("/visits/{visitId:int}", async (int visitId, AppDbContext db) =>
|
||||
{
|
||||
var v = await db.PatientVisits.FindAsync(visitId);
|
||||
if (v is null) return Results.NotFound();
|
||||
db.PatientVisits.Remove(v);
|
||||
await db.SaveChangesAsync();
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
// ── Health Requests (public submit / admin manage) ────────────────────────────
|
||||
app.MapPost("/api/health-request", async (HealthRequest req, AppDbContext db) =>
|
||||
{
|
||||
req.CreatedAt = DateTime.UtcNow;
|
||||
req.IsHandled = false;
|
||||
db.HealthRequests.Add(req);
|
||||
await db.SaveChangesAsync();
|
||||
return Results.Ok(new { message = "درخواست شما ثبت شد" });
|
||||
});
|
||||
|
||||
app.MapGet("/api/health-requests", async (bool? handled, AppDbContext db) =>
|
||||
{
|
||||
var q = db.HealthRequests.AsQueryable();
|
||||
if (handled.HasValue) q = q.Where(r => r.IsHandled == handled);
|
||||
return Results.Ok(await q.OrderByDescending(r => r.CreatedAt).ToListAsync());
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapPut("/api/health-requests/{id:int}", async (int id, AppDbContext db) =>
|
||||
{
|
||||
var r = await db.HealthRequests.FindAsync(id);
|
||||
if (r is null) return Results.NotFound();
|
||||
r.IsHandled = true;
|
||||
await db.SaveChangesAsync();
|
||||
return Results.Ok(r);
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapDelete("/api/health-requests/{id:int}", async (int id, AppDbContext db) =>
|
||||
{
|
||||
var r = await db.HealthRequests.FindAsync(id);
|
||||
if (r is null) return Results.NotFound();
|
||||
db.HealthRequests.Remove(r);
|
||||
await db.SaveChangesAsync();
|
||||
return Results.NoContent();
|
||||
}).RequireAuthorization();
|
||||
|
||||
// ── Sitemap ───────────────────────────────────────────────────────────────────
|
||||
app.MapGet("/sitemap.xml", async (AppDbContext db, HttpContext ctx) =>
|
||||
{
|
||||
|
||||
@@ -269,6 +269,15 @@ tr:hover td{background:#FAFBFC}
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
گزارش SEO
|
||||
</div>
|
||||
<div class="nav-section">مدیریت بیماران</div>
|
||||
<div class="nav-item" onclick="showPage('patients',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
پرونده بیماران
|
||||
</div>
|
||||
<div class="nav-item" onclick="showPage('healthrequests',this)" id="healthreqNavItem">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||
درخواستها <span id="healthreqBadge" style="display:none;background:#E53935;color:#fff;font-size:.65rem;padding:.1rem .4rem;border-radius:50px;margin-right:.3rem"></span>
|
||||
</div>
|
||||
<div class="nav-section">تنظیمات</div>
|
||||
<div class="nav-item" onclick="showPage('security',this)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
@@ -549,6 +558,84 @@ tr:hover td{background:#FAFBFC}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ PATIENTS PAGE ══ -->
|
||||
<div class="page" id="page-patients">
|
||||
<div class="card" style="margin-bottom:1.2rem">
|
||||
<div class="card-header">
|
||||
<div class="card-title">پرونده بیماران</div>
|
||||
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
|
||||
<select id="patientCatFilter" onchange="loadPatients()" style="border:1.5px solid var(--border);border-radius:8px;padding:.4rem .7rem;font-family:inherit;font-size:.82rem;background:var(--white)">
|
||||
<option value="">همه دستهها</option>
|
||||
<option value="beauty">زیبایی پوست</option>
|
||||
<option value="health">سلامت عمومی</option>
|
||||
</select>
|
||||
<input id="patientSearch" placeholder="جستجو نام / تلفن..." oninput="loadPatients()"
|
||||
style="border:1.5px solid var(--border);border-radius:8px;padding:.4rem .8rem;font-family:inherit;font-size:.82rem;outline:none;width:180px"/>
|
||||
<button class="btn btn-primary btn-sm" onclick="openPatientModal()">+ ثبت بیمار جدید</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>نام</th><th>تلفن</th><th>سن</th><th>جنسیت</th><th>دسته</th><th>ویزیتها</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="patientsTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ PATIENT PROFILE (sub-page) ══ -->
|
||||
<div class="page" id="page-patient-profile">
|
||||
<div style="display:flex;align-items:center;gap:.8rem;margin-bottom:1.2rem">
|
||||
<button class="btn btn-secondary btn-sm" onclick="showPage('patients',document.querySelector('.nav-item:has(+[onclick*=patients])'))">← بازگشت</button>
|
||||
<h2 id="profileName" style="font-size:1.1rem;font-weight:700"></h2>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.2rem">
|
||||
<!-- Profile card -->
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">اطلاعات شخصی</div>
|
||||
<button class="btn btn-secondary btn-sm" id="editPatientBtn" onclick="">✏️ ویرایش</button>
|
||||
</div>
|
||||
<div class="modal-body" id="profileInfo"></div>
|
||||
</div>
|
||||
<!-- Medical history -->
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">سابقه پزشکی</div></div>
|
||||
<div class="modal-body" id="profileMedical"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Visits timeline -->
|
||||
<div class="card" style="margin-top:1.2rem">
|
||||
<div class="card-header">
|
||||
<div class="card-title">سوابق ویزیت</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="openVisitModal()">+ ثبت ویزیت جدید</button>
|
||||
</div>
|
||||
<div id="visitsTimeline" style="padding:1.2rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ HEALTH REQUESTS PAGE ══ -->
|
||||
<div class="page" id="page-healthrequests">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">درخواستهای مراقبت سلامت</div>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<select id="reqFilter" onchange="loadHealthRequests()" style="border:1.5px solid var(--border);border-radius:8px;padding:.4rem .7rem;font-family:inherit;font-size:.82rem;background:var(--white)">
|
||||
<option value="">همه</option>
|
||||
<option value="false">بررسی نشده</option>
|
||||
<option value="true">بررسی شده</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary btn-sm" onclick="loadHealthRequests()">🔄 بازخوانی</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>نام</th><th>تلفن</th><th>دسته</th><th>پیام</th><th>تاریخ</th><th>وضعیت</th><th>عملیات</th></tr></thead>
|
||||
<tbody id="healthreqTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ COMMENTS PAGE ══ -->
|
||||
<div class="page" id="page-comments">
|
||||
<div class="card">
|
||||
@@ -640,6 +727,71 @@ tr:hover td{background:#FAFBFC}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Patient Modal -->
|
||||
<div class="modal-overlay hidden" id="patientModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="patientModalTitle">ثبت بیمار جدید</div>
|
||||
<button class="modal-close" onclick="closeModal('patientModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="pt-id"/>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.8rem">
|
||||
<div class="form-group"><label>نام و نام خانوادگی *</label><input id="pt-name" placeholder="نام کامل"/></div>
|
||||
<div class="form-group"><label>شماره تلفن *</label><input id="pt-phone" dir="ltr" placeholder="09xx"/></div>
|
||||
<div class="form-group"><label>ایمیل</label><input id="pt-email" dir="ltr" placeholder="email@example.com"/></div>
|
||||
<div class="form-group"><label>سن</label><input id="pt-age" type="number" min="0" max="120" placeholder="سال"/></div>
|
||||
<div class="form-group"><label>وزن (kg)</label><input id="pt-weight" type="number" step="0.1" placeholder="kg"/></div>
|
||||
<div class="form-group"><label>قد (cm)</label><input id="pt-height" type="number" step="0.1" placeholder="cm"/></div>
|
||||
<div class="form-group"><label>جنسیت</label>
|
||||
<select id="pt-gender"><option value="">انتخاب کنید</option><option value="زن">زن</option><option value="مرد">مرد</option></select>
|
||||
</div>
|
||||
<div class="form-group"><label>گروه خونی</label>
|
||||
<select id="pt-blood"><option value="">نامشخص</option><option>A+</option><option>A-</option><option>B+</option><option>B-</option><option>AB+</option><option>AB-</option><option>O+</option><option>O-</option></select>
|
||||
</div>
|
||||
<div class="form-group"><label>دسته درمانی</label>
|
||||
<select id="pt-cat"><option value="beauty">زیبایی پوست</option><option value="health">سلامت عمومی</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label>سابقه بیماری</label><textarea id="pt-history" rows="3" placeholder="دیابت، فشار خون، بیماری قلبی و ..."></textarea></div>
|
||||
<div class="form-group"><label>حساسیتها</label><textarea id="pt-allergy" rows="2" placeholder="حساسیت به دارو یا مواد غذایی ..."></textarea></div>
|
||||
<div class="form-group"><label>داروهای جاری</label><textarea id="pt-meds" rows="2" placeholder="داروهایی که بیمار مصرف میکند ..."></textarea></div>
|
||||
<div class="form-group"><label>یادداشت کلی</label><textarea id="pt-notes" rows="2" placeholder="نکات مهم ..."></textarea></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="savePatient()">ذخیره</button>
|
||||
<button class="btn btn-secondary" onclick="closeModal('patientModal')">انصراف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visit Modal -->
|
||||
<div class="modal-overlay hidden" id="visitModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">ثبت ویزیت / یادداشت پزشکی</div>
|
||||
<button class="modal-close" onclick="closeModal('visitModal')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="vt-patientId"/>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.8rem">
|
||||
<div class="form-group"><label>عنوان ویزیت</label><input id="vt-title" placeholder="مثال: ویزیت اول، جلسه لیزر ۲ ..."/></div>
|
||||
<div class="form-group"><label>نوع</label>
|
||||
<select id="vt-type"><option>ویزیت</option><option>آزمایش</option><option>پروسیجر</option><option>مشاوره</option><option>پیگیری</option></select>
|
||||
</div>
|
||||
<div class="form-group"><label>تاریخ ویزیت</label><input id="vt-date" type="datetime-local"/></div>
|
||||
<div class="form-group"><label>ویزیت بعدی</label><input id="vt-next" type="date" placeholder="اختیاری"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>شرح حال / یافتهها</label><textarea id="vt-content" rows="4" placeholder="معاینه، شکایت بیمار، یافتههای بالینی ..."></textarea></div>
|
||||
<div class="form-group"><label>تجویز / دستورالعمل</label><textarea id="vt-rx" rows="3" placeholder="دارو، دوز، توصیهها ..."></textarea></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="saveVisit()">ذخیره ویزیت</button>
|
||||
<button class="btn btn-secondary" onclick="closeModal('visitModal')">انصراف</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery Modal -->
|
||||
<div class="modal-overlay hidden" id="galleryModal">
|
||||
<div class="modal">
|
||||
@@ -889,7 +1041,7 @@ function toast(msg, type='success') {
|
||||
}
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||||
const pageTitles = {dashboard:'داشبورد',hero:'صفحه اصلی',about:'درباره من',contact:'تماس',services:'خدمات',gallery:'گالری',testimonials:'نظرات',blogposts:'مقالات',categories:'دستهبندیها',faqs:'سوالات متداول',seo:'گزارش SEO',comments:'مدیریت نظرات',security:'تغییر رمز عبور'};
|
||||
const pageTitles = {dashboard:'داشبورد',hero:'صفحه اصلی',about:'درباره من',contact:'تماس',services:'خدمات',gallery:'گالری',testimonials:'نظرات',blogposts:'مقالات',categories:'دستهبندیها',faqs:'سوالات متداول',seo:'گزارش SEO',comments:'مدیریت نظرات',security:'تغییر رمز عبور',patients:'پرونده بیماران','patient-profile':'پرونده بیمار',healthrequests:'درخواستهای سلامت'};
|
||||
|
||||
function showPage(name, el) {
|
||||
document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
|
||||
@@ -913,8 +1065,180 @@ function loadPage(name) {
|
||||
else if (name==='faqs') loadFaqs();
|
||||
else if (name==='seo') loadSeo();
|
||||
else if (name==='comments') loadComments();
|
||||
else if (name==='patients') loadPatients();
|
||||
else if (name==='healthrequests') loadHealthRequests();
|
||||
}
|
||||
|
||||
// ── Patients ──────────────────────────────────────────────────────────────────
|
||||
let patients = [], currentPatientId = null;
|
||||
|
||||
async function loadPatients() {
|
||||
const cat = document.getElementById('patientCatFilter').value;
|
||||
const search = document.getElementById('patientSearch').value;
|
||||
const params = new URLSearchParams();
|
||||
if (cat) params.set('category', cat);
|
||||
if (search) params.set('search', search);
|
||||
patients = await api('/api/patients?' + params) || [];
|
||||
const catLabel = {beauty:'زیبایی پوست', health:'سلامت عمومی'};
|
||||
document.getElementById('patientsTable').innerHTML = patients.map(p => `
|
||||
<tr>
|
||||
<td><strong>${p.fullName}</strong></td>
|
||||
<td dir="ltr">${p.phoneNumber}</td>
|
||||
<td>${p.age || '-'}</td>
|
||||
<td>${p.gender || '-'}</td>
|
||||
<td><span style="background:${p.category==='health'?'#E3F2FD':'#FCE4EC'};color:${p.category==='health'?'#1565C0':'#880E4F'};padding:2px 8px;border-radius:20px;font-size:.72rem">${catLabel[p.category]||p.category}</span></td>
|
||||
<td>${p.visitCount}</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-sm" onclick="openProfile(${p.id})">پرونده</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="editPatient(${p.id})">ویرایش</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deletePatient(${p.id})">حذف</button>
|
||||
</td>
|
||||
</tr>`).join('') || '<tr><td colspan="7" style="text-align:center;color:var(--light);padding:2rem">بیماری ثبت نشده</td></tr>';
|
||||
}
|
||||
|
||||
function openPatientModal() {
|
||||
['id','name','phone','email','age','weight','height'].forEach(f => document.getElementById(`pt-${f}`).value='');
|
||||
['gender','blood','cat'].forEach(f => document.getElementById(`pt-${f}`).selectedIndex=0);
|
||||
['history','allergy','meds','notes'].forEach(f => document.getElementById(`pt-${f}`).value='');
|
||||
document.getElementById('patientModalTitle').textContent='ثبت بیمار جدید';
|
||||
document.getElementById('patientModal').classList.remove('hidden');
|
||||
}
|
||||
function editPatient(id) {
|
||||
const p = patients.find(x=>x.id===id); if(!p) return;
|
||||
document.getElementById('pt-id').value=p.id;
|
||||
document.getElementById('pt-name').value=p.fullName||'';
|
||||
document.getElementById('pt-phone').value=p.phoneNumber||'';
|
||||
document.getElementById('pt-email').value=p.email||'';
|
||||
document.getElementById('pt-age').value=p.age||'';
|
||||
document.getElementById('pt-weight').value=p.weight||'';
|
||||
document.getElementById('pt-height').value=p.height||'';
|
||||
document.getElementById('pt-gender').value=p.gender||'';
|
||||
document.getElementById('pt-blood').value=p.bloodType||'';
|
||||
document.getElementById('pt-cat').value=p.category||'beauty';
|
||||
document.getElementById('patientModalTitle').textContent='ویرایش بیمار';
|
||||
document.getElementById('patientModal').classList.remove('hidden');
|
||||
}
|
||||
async function savePatient() {
|
||||
const id = document.getElementById('pt-id').value;
|
||||
const body={
|
||||
fullName:document.getElementById('pt-name').value,
|
||||
phoneNumber:document.getElementById('pt-phone').value,
|
||||
email:document.getElementById('pt-email').value,
|
||||
age:parseInt(document.getElementById('pt-age').value)||0,
|
||||
weight:parseFloat(document.getElementById('pt-weight').value)||0,
|
||||
height:parseFloat(document.getElementById('pt-height').value)||0,
|
||||
gender:document.getElementById('pt-gender').value,
|
||||
bloodType:document.getElementById('pt-blood').value,
|
||||
diseaseHistory:document.getElementById('pt-history').value,
|
||||
allergies:document.getElementById('pt-allergy').value,
|
||||
medications:document.getElementById('pt-meds').value,
|
||||
notes:document.getElementById('pt-notes').value,
|
||||
category:document.getElementById('pt-cat').value,
|
||||
isActive:true
|
||||
};
|
||||
if(id) await api(`/api/patients/${id}`,{method:'PUT',body:JSON.stringify(body)});
|
||||
else await api('/api/patients',{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('patientModal'); toast('ذخیره شد ✓'); loadPatients();
|
||||
}
|
||||
async function deletePatient(id){if(!confirm('حذف بیمار؟'))return;await api(`/api/patients/${id}`,{method:'DELETE'});toast('حذف شد','error');loadPatients();}
|
||||
|
||||
// Patient Profile
|
||||
async function openProfile(id) {
|
||||
currentPatientId = id;
|
||||
const p = await api(`/api/patients/${id}`);
|
||||
if(!p) return;
|
||||
document.getElementById('profileName').textContent = p.fullName + (p.category==='health' ? ' — سلامت عمومی' : ' — زیبایی پوست');
|
||||
document.getElementById('editPatientBtn').onclick = () => editPatient(id);
|
||||
document.getElementById('profileInfo').innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.7rem;font-size:.88rem">
|
||||
<div><b>تلفن:</b> <span dir="ltr">${p.phoneNumber||'-'}</span></div>
|
||||
<div><b>ایمیل:</b> ${p.email||'-'}</div>
|
||||
<div><b>سن:</b> ${p.age||'-'}</div>
|
||||
<div><b>جنسیت:</b> ${p.gender||'-'}</div>
|
||||
<div><b>وزن:</b> ${p.weight||'-'} kg</div>
|
||||
<div><b>قد:</b> ${p.height||'-'} cm</div>
|
||||
<div><b>گروه خونی:</b> ${p.bloodType||'-'}</div>
|
||||
<div><b>تاریخ ثبت:</b> ${new Date(p.createdAt).toLocaleDateString('fa-IR')}</div>
|
||||
</div>
|
||||
${p.notes ? `<p style="margin-top:.8rem;color:var(--mid);font-size:.85rem">${p.notes}</p>` : ''}
|
||||
`;
|
||||
document.getElementById('profileMedical').innerHTML = `
|
||||
<div style="font-size:.88rem;line-height:2">
|
||||
<div><b>سابقه بیماری:</b><br><span style="color:var(--mid)">${p.diseaseHistory||'—'}</span></div>
|
||||
<div style="margin-top:.8rem"><b>حساسیتها:</b><br><span style="color:var(--mid)">${p.allergies||'—'}</span></div>
|
||||
<div style="margin-top:.8rem"><b>داروها:</b><br><span style="color:var(--mid)">${p.medications||'—'}</span></div>
|
||||
</div>
|
||||
`;
|
||||
renderVisits(p.visits||[]);
|
||||
showPage('patient-profile', null);
|
||||
}
|
||||
function renderVisits(visits) {
|
||||
const tl = document.getElementById('visitsTimeline');
|
||||
if(!visits.length){tl.innerHTML='<p style="color:var(--light);text-align:center;padding:2rem">ویزیتی ثبت نشده</p>';return;}
|
||||
const typeColors={'ویزیت':'#1976D2','آزمایش':'#388E3C','پروسیجر':'#7B1FA2','مشاوره':'#F57C00','پیگیری':'#0288D1'};
|
||||
tl.innerHTML = visits.map(v => `
|
||||
<div style="border-right:3px solid ${typeColors[v.visitType]||'#999'};padding:.8rem 1rem .8rem 0;margin-bottom:1.2rem;padding-right:1rem">
|
||||
<div style="display:flex;align-items:center;gap:.6rem;margin-bottom:.4rem">
|
||||
<span style="background:${typeColors[v.visitType]||'#999'};color:#fff;padding:2px 10px;border-radius:20px;font-size:.72rem">${v.visitType}</span>
|
||||
<strong style="font-size:.9rem">${v.title}</strong>
|
||||
<span style="color:var(--light);font-size:.78rem;margin-right:auto">${new Date(v.visitDate).toLocaleDateString('fa-IR')}</span>
|
||||
<button class="btn btn-danger btn-sm" style="padding:.2rem .6rem;font-size:.7rem" onclick="deleteVisit(${v.id})">حذف</button>
|
||||
</div>
|
||||
${v.content?`<p style="font-size:.85rem;color:var(--dark);margin:.3rem 0">${v.content}</p>`:''}
|
||||
${v.prescription?`<div style="background:#F8F9FA;border-radius:8px;padding:.5rem .8rem;font-size:.82rem;margin-top:.4rem;color:#333"><b>تجویز:</b> ${v.prescription}</div>`:''}
|
||||
${v.nextVisitDate?`<p style="font-size:.78rem;color:var(--gold);margin-top:.4rem">📅 ویزیت بعدی: ${new Date(v.nextVisitDate).toLocaleDateString('fa-IR')}</p>`:''}
|
||||
</div>`).join('');
|
||||
}
|
||||
function openVisitModal() {
|
||||
document.getElementById('vt-patientId').value=currentPatientId;
|
||||
['title','content','rx'].forEach(f=>document.getElementById(`vt-${f}`).value='');
|
||||
document.getElementById('vt-type').selectedIndex=0;
|
||||
document.getElementById('vt-date').value=new Date().toISOString().slice(0,16);
|
||||
document.getElementById('vt-next').value='';
|
||||
document.getElementById('visitModal').classList.remove('hidden');
|
||||
}
|
||||
async function saveVisit() {
|
||||
const pid = document.getElementById('vt-patientId').value;
|
||||
const body={
|
||||
title:document.getElementById('vt-title').value,
|
||||
content:document.getElementById('vt-content').value,
|
||||
prescription:document.getElementById('vt-rx').value,
|
||||
visitType:document.getElementById('vt-type').value,
|
||||
visitDate:document.getElementById('vt-date').value||new Date().toISOString(),
|
||||
nextVisitDate:document.getElementById('vt-next').value||null
|
||||
};
|
||||
await api(`/api/patients/${pid}/visits`,{method:'POST',body:JSON.stringify(body)});
|
||||
closeModal('visitModal'); toast('ویزیت ثبت شد ✓');
|
||||
openProfile(parseInt(pid));
|
||||
}
|
||||
async function deleteVisit(vid){if(!confirm('حذف ویزیت؟'))return;await api(`/api/patients/visits/${vid}`,{method:'DELETE'});toast('حذف شد','error');openProfile(currentPatientId);}
|
||||
|
||||
// ── Health Requests ───────────────────────────────────────────────────────────
|
||||
async function loadHealthRequests() {
|
||||
const filter = document.getElementById('reqFilter').value;
|
||||
const params = filter !== '' ? `?handled=${filter}` : '';
|
||||
const reqs = await api('/api/health-requests' + params) || [];
|
||||
const pending = reqs.filter(r=>!r.isHandled).length;
|
||||
const badge = document.getElementById('healthreqBadge');
|
||||
if(pending>0){badge.textContent=pending;badge.style.display='inline';}else{badge.style.display='none';}
|
||||
const catLabel = {beauty:'زیبایی پوست', health:'سلامت عمومی'};
|
||||
document.getElementById('healthreqTable').innerHTML = reqs.map(r=>`
|
||||
<tr style="${!r.isHandled?'font-weight:600':'opacity:.7'}">
|
||||
<td>${r.fullName}</td>
|
||||
<td dir="ltr">${r.phoneNumber}</td>
|
||||
<td>${catLabel[r.category]||r.category}</td>
|
||||
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${r.message}">${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>
|
||||
${!r.isHandled?`<button class="btn btn-secondary btn-sm" onclick="handleReq(${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>';
|
||||
}
|
||||
async function handleReq(id){await api(`/api/health-requests/${id}`,{method:'PUT'});toast('علامتگذاری شد ✓');loadHealthRequests();}
|
||||
async function deleteReq(id){if(!confirm('حذف؟'))return;await api(`/api/health-requests/${id}`,{method:'DELETE'});toast('حذف شد','error');loadHealthRequests();}
|
||||
|
||||
// ── Comments ──────────────────────────────────────────────────────────────────
|
||||
async function loadComments() {
|
||||
const all = await api('/api/comments') || [];
|
||||
@@ -1738,6 +2062,7 @@ async function init(){
|
||||
loadDashboard();
|
||||
loadCategories();
|
||||
loadComments(); // populate pending badge on load
|
||||
loadHealthRequests(); // populate health requests badge on load
|
||||
}
|
||||
|
||||
// Auto-login if token exists
|
||||
|
||||
Reference in New Issue
Block a user