ปรับปรุง phase 6 ให้ครบถ้วน
This commit is contained in:
parent
93570d38d8
commit
a196b8f9a8
14 changed files with 747 additions and 40 deletions
|
|
@ -335,6 +335,69 @@ public class DocumentsController(
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>FR-12 — detail ของเอกสาร: ข้อมูลหัวเรื่อง + template + ค่าล่าสุดของทุก field
|
||||
/// (นิยามจาก TemplateField merge กับ DocumentFieldValue — รองรับ template เปลี่ยนนิยามหลังเอกสารสร้างแล้ว)</summary>
|
||||
[HttpGet("{documentId:guid}")]
|
||||
[ProducesResponseType(typeof(DocumentDetail), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<DocumentDetail>> GetDetail(Guid documentId, CancellationToken ct)
|
||||
{
|
||||
var doc = await db.Documents
|
||||
.AsNoTracking()
|
||||
.Where(d => d.Id == documentId)
|
||||
.Select(d => new
|
||||
{
|
||||
d.Id,
|
||||
d.FileName,
|
||||
d.CreatedAt,
|
||||
d.TemplateId,
|
||||
TemplateName = d.Template != null ? d.Template.Name : null,
|
||||
TemplateIsActive = d.Template != null && d.Template.IsActive,
|
||||
})
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (doc is null)
|
||||
return NotFound($"ไม่พบ document {documentId}");
|
||||
|
||||
var fields = new List<DocumentFieldItem>();
|
||||
if (doc.TemplateId is not null)
|
||||
{
|
||||
// โหลดสองข้างแยกแล้ว merge ใน memory (แบบเดียวกับ FieldSyncService) —
|
||||
// ไม่ใช้ Join เพราะ inner join จะทิ้ง orphan ทั้งสองฝั่งเงียบๆ
|
||||
var defs = await db.TemplateFields
|
||||
.AsNoTracking()
|
||||
.Where(f => f.TemplateId == doc.TemplateId)
|
||||
.OrderBy(f => f.SortOrder)
|
||||
.Select(f => new { f.Key, f.Label, f.Type, f.IsRequired })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var values = await db.DocumentFieldValues
|
||||
.AsNoTracking()
|
||||
.Where(v => v.DocumentId == documentId)
|
||||
.ToDictionaryAsync(v => v.FieldKey, ct);
|
||||
|
||||
foreach (var def in defs)
|
||||
{
|
||||
values.TryGetValue(def.Key, out var value);
|
||||
fields.Add(new DocumentFieldItem(
|
||||
def.Key, def.Label, def.Type, def.IsRequired,
|
||||
value?.Value ?? string.Empty,
|
||||
value?.SourceVersionId, value?.UpdatedByUserId, value?.UpdatedAt));
|
||||
}
|
||||
|
||||
// ค่าที่เก็บไว้แต่ key ไม่มีในนิยามปัจจุบัน (replace ไฟล์แล้ว tag หาย) — ต่อท้ายโดยไม่มีนิยาม
|
||||
foreach (var (key, value) in values.Where(kv => defs.All(d => d.Key != kv.Key)).OrderBy(kv => kv.Key))
|
||||
{
|
||||
fields.Add(new DocumentFieldItem(
|
||||
key, null, null, null,
|
||||
value.Value,
|
||||
value.SourceVersionId, value.UpdatedByUserId, value.UpdatedAt));
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new DocumentDetail(
|
||||
doc.Id, doc.FileName, doc.CreatedAt, doc.TemplateId, doc.TemplateName, doc.TemplateIsActive, fields));
|
||||
}
|
||||
|
||||
/// <summary>FR-6 — version history เรียงใหม่ → เก่า (พร้อม snapshot fields ต่อ version)</summary>
|
||||
[HttpGet("{documentId:guid}/versions")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<VersionListItem>), StatusCodes.Status200OK)]
|
||||
|
|
|
|||
|
|
@ -314,6 +314,39 @@ public class TemplatesController(
|
|||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>FR-12 — รายการเอกสารที่สร้างจาก template นี้ (เรียงตามกิจกรรมล่าสุด)
|
||||
/// ใช้ได้แม้ template ถูก deactivate (เอกสารเก่ายังต้องเข้าถึงได้ตาม policy FR-9)</summary>
|
||||
[HttpGet("{id:guid}/documents")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<TemplateDocumentListItem>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<IReadOnlyList<TemplateDocumentListItem>>> ListDocuments(Guid id, CancellationToken ct)
|
||||
{
|
||||
if (!await db.Templates.AnyAsync(t => t.Id == id, ct))
|
||||
return NotFound($"ไม่พบ template {id}");
|
||||
|
||||
// cast (int?) ใน Max จำเป็น — ทำให้ SQL MAX คืน NULL บนชุดว่างได้ (pattern เดียวกับ UploadVersion)
|
||||
// LatestActivity ต้อง compute ใน projection — EF จะยกเข้า SQL ORDER BY ให้
|
||||
var rows = await db.Documents
|
||||
.AsNoTracking()
|
||||
.Where(d => d.TemplateId == id)
|
||||
.Select(d => new
|
||||
{
|
||||
d.Id,
|
||||
d.FileName,
|
||||
d.CreatedAt,
|
||||
VersionCount = d.Versions.Count,
|
||||
LatestVersionNumber = d.Versions.Max(v => (int?)v.VersionNumber) ?? 0,
|
||||
LatestVersionAt = d.Versions.Max(v => (DateTimeOffset?)v.CreatedAt),
|
||||
LatestActivity = d.Versions.Max(v => (DateTimeOffset?)v.CreatedAt) ?? d.CreatedAt,
|
||||
})
|
||||
.OrderByDescending(x => x.LatestActivity)
|
||||
.ThenBy(x => x.FileName)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Ok(rows.Select(r =>
|
||||
new TemplateDocumentListItem(r.Id, r.FileName, r.CreatedAt, r.VersionCount, r.LatestVersionNumber, r.LatestVersionAt)).ToList());
|
||||
}
|
||||
|
||||
private static TemplateDetail ToDetail(Template template) => new(
|
||||
template.Id, template.Name, template.Description, template.IsActive,
|
||||
template.CreatedAt, template.UpdatedAt, FieldDtos(template));
|
||||
|
|
|
|||
|
|
@ -42,3 +42,37 @@ public record VersionListItem(
|
|||
|
||||
/// <summary>response ของ GET .../download-link — presigned URL อายุสั้นให้เปิดในเบราว์เซอร์ของผู้ใช้</summary>
|
||||
public record VersionDownloadLinkResponse(string Url, DateTimeOffset ExpiresAt);
|
||||
|
||||
/// <summary>item ของ GET /api/templates/{templateId}/documents — รายการเอกสารของ template หนึ่งตัว (FR-12)</summary>
|
||||
public record TemplateDocumentListItem(
|
||||
Guid DocumentId,
|
||||
string FileName,
|
||||
DateTimeOffset CreatedAt,
|
||||
int VersionCount,
|
||||
int LatestVersionNumber,
|
||||
DateTimeOffset? LatestVersionAt);
|
||||
|
||||
/// <summary>field หนึ่งตัวของ document (FR-12) — นิยามจาก TemplateField + ค่าล่าสุดจาก DocumentFieldValue
|
||||
/// Label/Type/IsRequired เป็น null เมื่อมีค่าเก็บไว้แต่ key ไม่มีในนิยาม template ปัจจุบัน
|
||||
/// (เช่น replace ไฟล์แล้ว tag หาย) — SourceVersionId/UpdatedByUserId/UpdatedAt เป็น null เมื่อ
|
||||
/// field เพิ่มเข้า template หลังเอกสารนี้ถูกสร้าง (ยังไม่เคยมีค่า)</summary>
|
||||
public record DocumentFieldItem(
|
||||
string Key,
|
||||
string? Label,
|
||||
string? Type,
|
||||
bool? IsRequired,
|
||||
string Value,
|
||||
Guid? SourceVersionId,
|
||||
string? UpdatedByUserId,
|
||||
DateTimeOffset? UpdatedAt);
|
||||
|
||||
/// <summary>response ของ GET /api/documents/{documentId} (FR-12) —
|
||||
/// TemplateId null คือเอกสาร track เองจาก add-in (ไม่มีนิยาม field, Fields ว่าง)</summary>
|
||||
public record DocumentDetail(
|
||||
Guid Id,
|
||||
string FileName,
|
||||
DateTimeOffset CreatedAt,
|
||||
Guid? TemplateId,
|
||||
string? TemplateName,
|
||||
bool TemplateIsActive,
|
||||
IReadOnlyList<DocumentFieldItem> Fields);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue