mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 20:13:15 +00:00
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.
289 lines
9.8 KiB
TypeScript
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,
|
|
};
|
|
}
|