mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #1352 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
This commit is contained in:
@@ -627,6 +627,9 @@ export class BookingTransitionService {
|
|||||||
file: { id: string; name: string; url: string } | null;
|
file: { id: string; name: string; url: string } | null;
|
||||||
reviewStatus: "PENDING" | "APPROVED" | "QUERIED" | null;
|
reviewStatus: "PENDING" | "APPROVED" | "QUERIED" | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
uploadedAt: string | null;
|
||||||
|
reviewedAt: string | null;
|
||||||
|
reviewedByName: string | null;
|
||||||
}>;
|
}>;
|
||||||
allApproved: boolean;
|
allApproved: boolean;
|
||||||
phase?: string | null;
|
phase?: string | null;
|
||||||
@@ -652,6 +655,9 @@ export class BookingTransitionService {
|
|||||||
const reviewByKey = new Map(
|
const reviewByKey = new Map(
|
||||||
reviews.map((r) => [`${r.settingCode}:${r.fileKey}`, r]),
|
reviews.map((r) => [`${r.settingCode}:${r.fileKey}`, r]),
|
||||||
);
|
);
|
||||||
|
const reviewerNames = await this.bookingsRepository.resolveStaffNames(
|
||||||
|
reviews.map((r) => r.reviewedByStaffId),
|
||||||
|
);
|
||||||
|
|
||||||
const documents: Awaited<
|
const documents: Awaited<
|
||||||
ReturnType<BookingTransitionService["getClearanceView"]>
|
ReturnType<BookingTransitionService["getClearanceView"]>
|
||||||
@@ -680,6 +686,11 @@ export class BookingTransitionService {
|
|||||||
file: file ? { id: file.id, name: file.name, url: file.url } : null,
|
file: file ? { id: file.id, name: file.name, url: file.url } : null,
|
||||||
reviewStatus: review?.status ?? null,
|
reviewStatus: review?.status ?? null,
|
||||||
note: review?.note ?? null,
|
note: review?.note ?? null,
|
||||||
|
uploadedAt: file?.createdAt ? file.createdAt.toISOString() : null,
|
||||||
|
reviewedAt: review?.reviewedAt ? review.reviewedAt.toISOString() : null,
|
||||||
|
reviewedByName: review?.reviewedByStaffId
|
||||||
|
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -700,6 +711,11 @@ export class BookingTransitionService {
|
|||||||
file: { id: f.id, name: f.name, url: f.url },
|
file: { id: f.id, name: f.name, url: f.url },
|
||||||
reviewStatus: review?.status ?? null,
|
reviewStatus: review?.status ?? null,
|
||||||
note: review?.note ?? null,
|
note: review?.note ?? null,
|
||||||
|
uploadedAt: f.createdAt ? f.createdAt.toISOString() : null,
|
||||||
|
reviewedAt: review?.reviewedAt ? review.reviewedAt.toISOString() : null,
|
||||||
|
reviewedByName: review?.reviewedByStaffId
|
||||||
|
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { computeFacets, FacetBucket } from '../../common/utils/facets.util';
|
import { computeFacets, FacetBucket } from '../../common/utils/facets.util';
|
||||||
|
import { resolveIamUserNames } from '../../common/utils/iam-user-name.util';
|
||||||
import { wagonsPerUnitForSize } from '../rule-engine/container-type.util';
|
import { wagonsPerUnitForSize } from '../rule-engine/container-type.util';
|
||||||
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
||||||
import { Contract } from '../contracts/entities/contract.entity';
|
import { Contract } from '../contracts/entities/contract.entity';
|
||||||
@@ -660,6 +661,13 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
await repo.save(repo.create({ ...input, status: 'PENDING' }));
|
await repo.save(repo.create({ ...input, status: 'PENDING' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Display names for reviewer staff ids — one query for the whole set. */
|
||||||
|
async resolveStaffNames(
|
||||||
|
staffIds: (string | null | undefined)[],
|
||||||
|
): Promise<Map<string, string>> {
|
||||||
|
return resolveIamUserNames(this.dataSource, staffIds);
|
||||||
|
}
|
||||||
|
|
||||||
/** GL marks a document APPROVED or QUERIED (with an optional note). */
|
/** GL marks a document APPROVED or QUERIED (with an optional note). */
|
||||||
async setDocumentReviewStatus(
|
async setDocumentReviewStatus(
|
||||||
bookingId: string,
|
bookingId: string,
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ export interface BookingClearanceView {
|
|||||||
file: { id: string; name: string; url: string } | null;
|
file: { id: string; name: string; url: string } | null;
|
||||||
reviewStatus: 'PENDING' | 'APPROVED' | 'QUERIED' | null;
|
reviewStatus: 'PENDING' | 'APPROVED' | 'QUERIED' | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
uploadedAt: string | null;
|
||||||
|
reviewedAt: string | null;
|
||||||
|
reviewedByName: string | null;
|
||||||
}>;
|
}>;
|
||||||
allApproved: boolean;
|
allApproved: boolean;
|
||||||
phase?: string | null;
|
phase?: string | null;
|
||||||
@@ -185,6 +188,9 @@ export class BookingClearanceService {
|
|||||||
const fileByCode = new Map(files.map((f) => [f.code, f]));
|
const fileByCode = new Map(files.map((f) => [f.code, f]));
|
||||||
const reviews = await this.bookingsRepository.findDocumentReviews(bookingId);
|
const reviews = await this.bookingsRepository.findDocumentReviews(bookingId);
|
||||||
const reviewByKey = new Map(reviews.map((r) => [`${r.settingCode}:${r.fileKey}`, r]));
|
const reviewByKey = new Map(reviews.map((r) => [`${r.settingCode}:${r.fileKey}`, r]));
|
||||||
|
const reviewerNames = await this.bookingsRepository.resolveStaffNames(
|
||||||
|
reviews.map((r) => r.reviewedByStaffId),
|
||||||
|
);
|
||||||
|
|
||||||
const documents: BookingClearanceView['documents'] = [];
|
const documents: BookingClearanceView['documents'] = [];
|
||||||
|
|
||||||
@@ -208,6 +214,11 @@ export class BookingClearanceService {
|
|||||||
file: file ? { id: file.id, name: file.name, url: file.url } : null,
|
file: file ? { id: file.id, name: file.name, url: file.url } : null,
|
||||||
reviewStatus: review?.status ?? null,
|
reviewStatus: review?.status ?? null,
|
||||||
note: review?.note ?? null,
|
note: review?.note ?? null,
|
||||||
|
uploadedAt: file?.createdAt ? file.createdAt.toISOString() : null,
|
||||||
|
reviewedAt: review?.reviewedAt ? review.reviewedAt.toISOString() : null,
|
||||||
|
reviewedByName: review?.reviewedByStaffId
|
||||||
|
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -227,6 +238,11 @@ export class BookingClearanceService {
|
|||||||
file: { id: f.id, name: f.name, url: f.url },
|
file: { id: f.id, name: f.name, url: f.url },
|
||||||
reviewStatus: review?.status ?? null,
|
reviewStatus: review?.status ?? null,
|
||||||
note: review?.note ?? null,
|
note: review?.note ?? null,
|
||||||
|
uploadedAt: f.createdAt ? f.createdAt.toISOString() : null,
|
||||||
|
reviewedAt: review?.reviewedAt ? review.reviewedAt.toISOString() : null,
|
||||||
|
reviewedByName: review?.reviewedByStaffId
|
||||||
|
? (reviewerNames.get(review.reviewedByStaffId) ?? null)
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type { Freight } from "@edr/types";
|
|||||||
import { isViewable } from "@edr/ui-common";
|
import { isViewable } from "@edr/ui-common";
|
||||||
|
|
||||||
import { SectionCard } from "./SectionCard";
|
import { SectionCard } from "./SectionCard";
|
||||||
|
import { formatDateTime } from "@/lib/format";
|
||||||
import { bookingsService } from "@/services/bookings.service";
|
import { bookingsService } from "@/services/bookings.service";
|
||||||
import {
|
import {
|
||||||
downloadBookingFile,
|
downloadBookingFile,
|
||||||
@@ -550,6 +551,11 @@ function DocReviewCard({
|
|||||||
<Text fz="12px" c="edr-muted" truncate>
|
<Text fz="12px" c="edr-muted" truncate>
|
||||||
{hasFile ? doc.file!.name : "Not uploaded by customer"}
|
{hasFile ? doc.file!.name : "Not uploaded by customer"}
|
||||||
</Text>
|
</Text>
|
||||||
|
{hasFile && doc.uploadedAt ? (
|
||||||
|
<Text fz="11px" c="dimmed">
|
||||||
|
Uploaded {formatDateTime(doc.uploadedAt)}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -578,9 +584,36 @@ function DocReviewCard({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
{hasFile && (
|
||||||
|
<Tooltip label="Download">
|
||||||
|
<Box
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
void downloadBookingFile(doc.file!.id, doc.file!.name)
|
||||||
|
}
|
||||||
|
c="edr-green"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
background: "transparent",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download size={15} />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{doc.reviewedAt && (status === "APPROVED" || status === "QUERIED") && (
|
||||||
|
<Text fz="11.5px" c="dimmed" mt={6}>
|
||||||
|
{status === "APPROVED" ? "Approved" : "Queried"} by{" "}
|
||||||
|
{doc.reviewedByName ?? "staff"} · {formatDateTime(doc.reviewedAt)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{status === "QUERIED" && doc.note && (
|
{status === "QUERIED" && doc.note && (
|
||||||
<Alert
|
<Alert
|
||||||
mt="sm"
|
mt="sm"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -813,6 +813,12 @@ export interface ClearanceDocument {
|
|||||||
file: { id: string; name: string; url: string } | null;
|
file: { id: string; name: string; url: string } | null;
|
||||||
reviewStatus: DocumentReviewStatus | null;
|
reviewStatus: DocumentReviewStatus | null;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
|
/** When the current file version was uploaded — a re-upload (query response) refreshes it. */
|
||||||
|
uploadedAt?: string | null;
|
||||||
|
/** When the latest review decision (approve/query) was recorded. */
|
||||||
|
reviewedAt?: string | null;
|
||||||
|
/** Display name of the staff member who recorded the decision. */
|
||||||
|
reviewedByName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user