Merge pull request #1352 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-08-20 07:04:21 +03:00
committed by GitHub
6 changed files with 1394 additions and 448 deletions

View File

@@ -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,
}); });
} }

View File

@@ -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,

View File

@@ -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,
}); });
} }

View File

@@ -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

View File

@@ -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;
} }
/** /**