From 22d0ecb330354ea25f002d463cc62047dc480e8d Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 2 Jun 2026 22:03:00 +0330 Subject: [PATCH] feat: doctor reply + diagnosis + tracking code per health request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- DrSousan.Api/Models/Models.cs | 20 +++-- DrSousan.Api/Pages/Index.cshtml | 20 ++++- DrSousan.Api/Program.cs | 77 ++++++++++++++--- DrSousan.Api/wwwroot/admin/index.html | 117 ++++++++++++++++++++------ 4 files changed, 187 insertions(+), 47 deletions(-) diff --git a/DrSousan.Api/Models/Models.cs b/DrSousan.Api/Models/Models.cs index 61aee68..b13f3a8 100644 --- a/DrSousan.Api/Models/Models.cs +++ b/DrSousan.Api/Models/Models.cs @@ -171,17 +171,23 @@ public class PatientVisit 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; + [MaxLength(20)] public string TrackingCode { get; set; } = ""; // e.g. DR-A3F7K2 + [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; + // Doctor response + public string Diagnosis { get; set; } = ""; // پزشک: تشخیص + public string DoctorReply { get; set; } = ""; // پزشک: پاسخ/توضیح + public DateTime? RepliedAt { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } // ─── DTOs ───────────────────────────────────────────────────────────────────── public record LoginRequest(string Username, string Password); +public record DoctorReplyDto(string? Diagnosis, string? DoctorReply); public record ChangePasswordRequest(string CurrentPassword, string NewPassword); public record SettingDto(string Key, string Value); public record BulkSettingsDto(Dictionary Settings); diff --git a/DrSousan.Api/Pages/Index.cshtml b/DrSousan.Api/Pages/Index.cshtml index c2256e8..2c62658 100644 --- a/DrSousan.Api/Pages/Index.cshtml +++ b/DrSousan.Api/Pages/Index.cshtml @@ -791,6 +791,7 @@ + @@ -844,15 +845,30 @@ }) }); if (!res.ok) throw new Error(); - btn.textContent = '✓ درخواست شما ثبت شد'; + const data = await res.json(); + const code = data.trackingCode || ''; + btn.textContent = '✓ درخواست ثبت شد'; btn.style.background = '#2D7A4F'; + // Show tracking code to user + const trackBox = document.getElementById('booking-tracking'); + if (trackBox && code) { + trackBox.innerHTML = ` +
+

✓ درخواست شما با موفقیت ثبت شد

+

کد رهگیری شما برای پیگیری پاسخ پزشک:

+
${code}
+

این کد را نزد خود نگه دارید. در اسرع وقت با شما تماس می‌گیریم.

+
`; + trackBox.style.display = 'block'; + } setTimeout(() => { btn.textContent = 'ارسال و رزرو نوبت'; btn.style.background = ''; btn.disabled = false; e.target.reset(); document.getElementById('booking-category').value = 'beauty'; - }, 3500); + if (trackBox) { trackBox.innerHTML=''; trackBox.style.display='none'; } + }, 8000); } catch { btn.textContent = 'خطا — دوباره تلاش کنید'; btn.style.background = '#c62828'; diff --git a/DrSousan.Api/Program.cs b/DrSousan.Api/Program.cs index f092e45..eaf03b5 100644 --- a/DrSousan.Api/Program.cs +++ b/DrSousan.Api/Program.cs @@ -131,17 +131,32 @@ await using (var scope = app.Services.CreateAsyncScope()) 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 '' + "Id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "TrackingCode" TEXT NOT NULL DEFAULT '', + "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, + "Diagnosis" TEXT NOT NULL DEFAULT '', + "DoctorReply" TEXT NOT NULL DEFAULT '', + "RepliedAt" TEXT, + "CreatedAt" TEXT NOT NULL DEFAULT '' ) """); } catch { } + // Add new columns to existing HealthRequests table (safe migration) + foreach (var col in new[] { + ("TrackingCode", "TEXT NOT NULL DEFAULT ''"), + ("Diagnosis", "TEXT NOT NULL DEFAULT ''"), + ("DoctorReply", "TEXT NOT NULL DEFAULT ''"), + ("RepliedAt", "TEXT") }) + { + try { await db.Database.ExecuteSqlRawAsync( + $"ALTER TABLE HealthRequests ADD COLUMN \"{col.Item1}\" {col.Item2}"); } + catch { } + } await SeedAsync(db); } @@ -703,20 +718,47 @@ patientsGroup.MapDelete("/visits/{visitId:int}", async (int visitId, AppDbContex // ── Health Requests (public submit / admin manage) ──────────────────────────── app.MapPost("/api/health-request", async (HealthRequest req, AppDbContext db) => { - req.CreatedAt = DateTime.UtcNow; - req.IsHandled = false; + req.CreatedAt = DateTime.UtcNow; + req.IsHandled = false; + req.TrackingCode = "DR-" + GenerateTrackingCode(); db.HealthRequests.Add(req); await db.SaveChangesAsync(); - return Results.Ok(new { message = "درخواست شما ثبت شد" }); + return Results.Ok(new { message = "درخواست شما ثبت شد", trackingCode = req.TrackingCode, id = req.Id }); }); -app.MapGet("/api/health-requests", async (bool? handled, AppDbContext db) => +// Public: look up own request by tracking code +app.MapGet("/api/health-request/track/{code}", async (string code, AppDbContext db) => +{ + var r = await db.HealthRequests.FirstOrDefaultAsync(x => x.TrackingCode == code); + if (r is null) return Results.NotFound(new { message = "کد رهگیری یافت نشد" }); + return Results.Ok(new { + r.TrackingCode, r.FullName, r.Category, r.Message, r.IsHandled, + r.Diagnosis, r.DoctorReply, r.RepliedAt, r.CreatedAt + }); +}); + +app.MapGet("/api/health-requests", async (bool? handled, string? phone, AppDbContext db) => { var q = db.HealthRequests.AsQueryable(); - if (handled.HasValue) q = q.Where(r => r.IsHandled == handled); + if (handled.HasValue) q = q.Where(r => r.IsHandled == handled); + if (!string.IsNullOrEmpty(phone)) q = q.Where(r => r.PhoneNumber == phone); return Results.Ok(await q.OrderByDescending(r => r.CreatedAt).ToListAsync()); }).RequireAuthorization(); +// Doctor reply: set diagnosis + reply text + mark handled +app.MapPut("/api/health-requests/{id:int}/reply", async (int id, DoctorReplyDto dto, AppDbContext db) => +{ + var r = await db.HealthRequests.FindAsync(id); + if (r is null) return Results.NotFound(); + r.Diagnosis = dto.Diagnosis ?? r.Diagnosis; + r.DoctorReply = dto.DoctorReply ?? r.DoctorReply; + r.IsHandled = true; + r.RepliedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + return Results.Ok(r); +}).RequireAuthorization(); + +// Mark handled without reply app.MapPut("/api/health-requests/{id:int}", async (int id, AppDbContext db) => { var r = await db.HealthRequests.FindAsync(id); @@ -808,6 +850,15 @@ static string Slugify(string text) return text.Trim('-'); } +static string GenerateTrackingCode() +{ + const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + var rng = System.Security.Cryptography.RandomNumberGenerator.Create(); + var bytes = new byte[6]; + rng.GetBytes(bytes); + return new string(bytes.Select(b => chars[b % chars.Length]).ToArray()); +} + static int EstimateReadingTime(string content) { if (string.IsNullOrEmpty(content)) return 1; diff --git a/DrSousan.Api/wwwroot/admin/index.html b/DrSousan.Api/wwwroot/admin/index.html index 66e53eb..1984cc6 100644 --- a/DrSousan.Api/wwwroot/admin/index.html +++ b/DrSousan.Api/wwwroot/admin/index.html @@ -722,7 +722,7 @@ tr:hover td{background:#FAFBFC}
- +
نامتلفندستهپیامتاریخوضعیتعملیات
کد رهگیرینامتلفندستهپیامتاریخوضعیتعملیات
@@ -861,7 +861,7 @@ tr:hover td{background:#FAFBFC}