shoc-frontend-new/src/domain/work-orders/mappers/work-order-board-detail-mapper.ts
Arthur Bassi 93c5a35b46 chore: merge origin/dev into slide-over media branch
Sync with latest dev (CompDoc/completedDate from PR #45) and resolve
conflicts while preserving slide-over detail, workOrderVersion on media
mutations, and the consolidated list page hook.
2026-08-11 16:46:32 -03:00

289 lines
9.8 KiB
TypeScript

import { DOC_STATUS_API } from "@/domain/work-orders/types/work-order-board";
import type {
BoardMediaCategory,
WorkOrderBoardCompletion,
WorkOrderBoardDetail,
WorkOrderBoardMediaItem,
} from "@/domain/work-orders/types/work-order-board-detail";
import { BOARD_MEDIA_CATEGORY_API } from "@/domain/work-orders/types/work-order-board-detail";
import type { CompletionDocStatus } from "@/domain/work-orders/types/work-order-table-row";
import type {
WorkOrderAuditEntry,
WorkOrderComment,
} from "@/domain/work-orders/types/work-order-detail";
import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper";
import type {
FileCategory,
MediaCategory,
MediaFile,
} from "@/domain/work-orders/types/work-order-media";
function asRecord(raw: unknown): Record<string, unknown> {
return typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
}
function readString(record: Record<string, unknown>, ...keys: string[]): string {
for (const key of keys) {
const value = record[key];
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
}
return "";
}
function readNumber(record: Record<string, unknown>, ...keys: string[]): number | null {
for (const key of keys) {
const value = record[key];
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
}
return null;
}
function readBoolean(record: Record<string, unknown>, ...keys: string[]): boolean {
for (const key of keys) {
const value = record[key];
if (typeof value === "boolean") {
return value;
}
}
return false;
}
function readStringArray(record: Record<string, unknown>, ...keys: string[]): string[] {
for (const key of keys) {
const value = record[key];
if (!Array.isArray(value)) {
continue;
}
const mapped = value
.map((entry) => {
if (typeof entry === "string") {
return entry;
}
if (typeof entry === "number" || typeof entry === "boolean") {
return String(entry);
}
return "";
})
.filter(Boolean);
if (mapped.length > 0) {
return mapped;
}
}
return [];
}
function mapDocStatus(raw: unknown): CompletionDocStatus {
const numeric = typeof raw === "number" ? raw : Number(raw);
if (numeric === DOC_STATUS_API.Yes) return "Yes";
if (numeric === DOC_STATUS_API.No) return "No";
if (numeric === DOC_STATUS_API.NotNeeded) return "NN";
if (raw === "Yes" || raw === "No" || raw === "NN") return raw;
if (raw === "NotNeeded") return "NN";
return "";
}
function mapMediaCategory(raw: unknown): BoardMediaCategory {
const numeric = typeof raw === "number" ? raw : Number(raw);
if (numeric === BOARD_MEDIA_CATEGORY_API.Before) return "Before";
if (numeric === BOARD_MEDIA_CATEGORY_API.After) return "After";
if (numeric === BOARD_MEDIA_CATEGORY_API.Extra) return "Extra";
if (numeric === BOARD_MEDIA_CATEGORY_API.Completion) return "Completion";
return "Extra";
}
export function mapBoardDetailComment(raw: unknown): WorkOrderComment {
const item = asRecord(raw);
const id = readNumber(item, "id");
const authorId = readString(item, "authorId", "AuthorId") || null;
return {
id: id ?? (readString(item, "id") || `comment-${readString(item, "time")}`),
createdDate: readString(item, "time", "createdDate", "CreatedDate"),
commenttext: readString(item, "text", "commenttext", "Commenttext"),
commentType: readString(item, "commentType", "CommentType") || "internal",
userName: readString(item, "userName", "UserName", "authorName", "AuthorName"),
authorId,
commenter: readString(item, "commenter", "Commenter") || null,
recordType: readString(item, "recordType", "RecordType") || null,
mentions: readStringArray(item, "mentions", "Mentions"),
};
}
export function mapBoardDetailAuditEntry(raw: unknown, index: number): WorkOrderAuditEntry {
const item = asRecord(raw);
const time = readString(item, "time", "createdAt", "CreatedAt");
const typeRaw = readString(item, "type", "Type").toLowerCase();
const type: "manual" | "system" = typeRaw === "system" ? "system" : "manual";
const dispatcherId = readString(item, "dispatcherId", "DispatcherId");
const userName = readString(item, "userName", "UserName");
return {
id: `${time || "audit"}-${index}`,
fieldName: readString(item, "fieldName", "FieldName"),
oldValue: readString(item, "oldValue", "OldValue"),
newValue: readString(item, "newValue", "NewValue"),
action: readString(item, "action", "Action"),
createdAt: time,
userName: type === "system" ? "System" : userName,
type,
dispatcherId: dispatcherId || undefined,
};
}
export type UploadableMediaCategory = "Before" | "After" | "Extra";
export function buildMediaFormData(file: File, category?: UploadableMediaCategory): FormData {
const form = new FormData();
if (category) {
form.append("category", category);
}
form.append("file", file);
return form;
}
export function buildCompletionDocFormData(params: {
file: File;
workOrderVersion: string;
signOffName?: string;
signOffSignature?: string;
}): FormData {
const form = new FormData();
form.append("file", params.file);
form.append("workOrderVersion", params.workOrderVersion);
if (params.signOffName) {
form.append("signOffName", params.signOffName);
}
if (params.signOffSignature) {
form.append("signOffSignature", params.signOffSignature);
}
return form;
}
export function mapCompletion(raw: unknown): WorkOrderBoardCompletion {
const item = asRecord(raw);
const templateRaw = item.template ?? item.Template;
const templateRecord = templateRaw != null ? asRecord(templateRaw) : null;
const signoffsRaw = item.dispatchSignoffs ?? item.DispatchSignoffs;
const signoffs = Array.isArray(signoffsRaw)
? signoffsRaw.map((entry) => {
const s = asRecord(entry);
return {
dispatchId: readNumber(s, "dispatchId", "DispatchId"),
signoffType: readString(s, "signoffType", "SignoffType"),
name: readString(s, "name", "Name"),
signedAt: readString(s, "signedAt", "SignedAt"),
};
})
: [];
return {
docStatus: mapDocStatus(item.docStatus ?? item.DocStatus),
template: templateRecord
? {
id: readNumber(templateRecord, "id", "Id") ?? 0,
name: readString(templateRecord, "name", "Name"),
serviceKey: readString(templateRecord, "serviceKey", "ServiceKey"),
workOrderType: readNumber(templateRecord, "workOrderType", "WorkOrderType"),
templateUrl: readString(templateRecord, "templateUrl", "TemplateUrl"),
isActive: readBoolean(templateRecord, "isActive", "IsActive"),
}
: null,
signOffName: readString(item, "signOffName", "SignOffName"),
signOffAttachment: readString(item, "signOffAttachment", "SignOffAttachment"),
signOffSignature: readString(item, "signOffSignature", "SignOffSignature"),
dispatchSignoffs: signoffs,
};
}
export function mapBoardDetailMediaItem(raw: unknown): WorkOrderBoardMediaItem {
const item = asRecord(raw);
const id = readNumber(item, "id");
const isLegacyFlag = readBoolean(item, "isLegacy", "IsLegacy");
return {
id: String(id ?? (readString(item, "id") || cryptoRandomId())),
category: mapMediaCategory(item.category ?? item.Category),
url: readString(item, "url", "Url"),
fileName: readString(item, "fileName", "FileName") || "file",
uploadedAt: readString(item, "uploadedAt", "UploadedAt"),
isLegacy: isLegacyFlag || (id != null && id <= 0),
};
}
function cryptoRandomId(): string {
return `media-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
function classifyFileName(fileName: string, url: string): FileCategory {
const name = `${fileName} ${url}`.toLowerCase();
if (/\.(jpe?g|png|gif|webp|bmp)(\?|$)/i.test(name) || name.includes("image/")) {
return "image";
}
if (/\.(mp4|mov|webm|m4v)(\?|$)/i.test(name) || name.includes("video/")) {
return "video";
}
if (/\.pdf(\?|$)/i.test(name)) {
return "pdf";
}
if (/\.(docx?|xlsx?|pptx?)(\?|$)/i.test(name)) {
return "doc";
}
return "other";
}
function boardCategoryToMediaCategory(category: BoardMediaCategory): MediaCategory {
if (category === "Extra") return "Other";
if (category === "Completion") return "Report";
return "Photo";
}
/** Adapt board detail media items for existing MediaUploader / Extras UI. */
export function boardMediaItemsToMediaFiles(items: WorkOrderBoardMediaItem[]): MediaFile[] {
return items.map((item) => ({
id: item.id,
name: item.fileName,
fileType: classifyFileName(item.fileName, item.url),
url: item.url,
category: boardCategoryToMediaCategory(item.category),
purpose: item.category,
isLegacy: item.isLegacy || Number(item.id) <= 0,
uploadStatus: "done" as const,
}));
}
export function mapWorkOrderBoardDetail(raw: unknown): WorkOrderBoardDetail {
const root = asRecord(raw);
const infoRaw = root.info ?? root.Info ?? root;
const commentsRaw = root.comments ?? root.Comments;
const auditRaw = root.audit ?? root.Audit;
const mediaRaw = root.media ?? root.Media;
const completionRaw = root.completion ?? root.Completion;
const comments = Array.isArray(commentsRaw) ? commentsRaw.map(mapBoardDetailComment) : [];
const audit = Array.isArray(auditRaw)
? auditRaw.map((entry, index) => mapBoardDetailAuditEntry(entry, index))
: [];
const media = Array.isArray(mediaRaw) ? mediaRaw.map(mapBoardDetailMediaItem) : [];
return {
info: mapWorkOrderTableRow(infoRaw),
completion: mapCompletion(completionRaw ?? {}),
comments,
audit,
media,
};
}