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:
@@ -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) =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user