mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 16:00:56 +00:00
add file viewer functionality across various components
- Implemented a shared file viewer modal using `useFileViewer` hook to allow inline viewing of documents (images, PDFs, videos, etc.) across the application. - Updated `ContractClearanceReviewSection`, `ContractRequestDetailPage`, `ContractViewPage`, and booking-related components to utilize the new file viewer for document previews. - Added "Approve all" button in `ContractClearanceReviewSection` to bulk approve documents. - Enhanced document action buttons to include view and download options based on file type. - Introduced `isViewable` utility to determine if a file can be previewed inline. - Created `FileViewer` component to handle rendering of various file types and added appropriate fallback for unsupported formats.
This commit is contained in:
@@ -231,10 +231,14 @@ export class ContractTransitionService {
|
||||
|
||||
await this.contractsRepository.completeApprovalStep(step.id, actorId, 'APPROVED');
|
||||
|
||||
// Record who acted on this step, but DO NOT advance the contract status here —
|
||||
// approving one step (e.g. LINE_STAFF) must not finalize the chain while later
|
||||
// steps (e.g. DIRECTOR) are still pending. Status only moves to APPROVED once
|
||||
// every step in the chain is complete; until then the contract stays in
|
||||
// PENDING_APPROVAL so the next required role can act.
|
||||
const updates: Record<string, unknown> = {};
|
||||
const now = new Date();
|
||||
if (requiredRole === 'LINE_STAFF') {
|
||||
updates.status = 'APPROVED_PENDING_SIGNATURE';
|
||||
updates.approvedByStaffId = actorId;
|
||||
updates.approvedByStaffAt = now;
|
||||
} else if (requiredRole === 'DIRECTOR') {
|
||||
@@ -246,9 +250,7 @@ export class ContractTransitionService {
|
||||
}
|
||||
|
||||
const allDone = await this.contractsRepository.allApprovalStepsComplete(contractId);
|
||||
if (allDone) {
|
||||
updates.status = 'APPROVED';
|
||||
}
|
||||
updates.status = allDone ? 'APPROVED' : 'PENDING_APPROVAL';
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await this.contractsRepository.update(contractId, updates as never);
|
||||
@@ -368,9 +370,28 @@ export class ContractTransitionService {
|
||||
options: { signerUserId?: string },
|
||||
): Promise<void> {
|
||||
const role = dto.role as ContractSignerRole;
|
||||
const raw = dto.signatureImageBase64.includes(',')
|
||||
? dto.signatureImageBase64.split(',')[1]!
|
||||
: dto.signatureImageBase64;
|
||||
|
||||
// Resolve the signature image. The client may send a freshly-drawn image, or
|
||||
// omit it to reuse the signer's saved profile signature. Fall back to the
|
||||
// saved one whenever no image is supplied.
|
||||
let imageBase64 = dto.signatureImageBase64;
|
||||
let signerDisplayName = dto.signerDisplayName;
|
||||
if (!imageBase64 && options.signerUserId) {
|
||||
const saved = await this.signaturesService.getForUser(options.signerUserId);
|
||||
if (saved?.signatureImageUrl) {
|
||||
imageBase64 = saved.signatureImageUrl;
|
||||
signerDisplayName = signerDisplayName || saved.signerDisplayName;
|
||||
}
|
||||
}
|
||||
if (!imageBase64) {
|
||||
throw new BadRequestException(
|
||||
'No signature provided and no saved signature found on the profile.',
|
||||
);
|
||||
}
|
||||
|
||||
const raw = imageBase64.includes(',')
|
||||
? imageBase64.split(',')[1]!
|
||||
: imageBase64;
|
||||
const buffer = Buffer.from(raw, 'base64');
|
||||
const sigFile: Express.Multer.File = {
|
||||
fieldname: `signature_${role.toLowerCase()}`,
|
||||
@@ -395,17 +416,19 @@ export class ContractTransitionService {
|
||||
await this.contractsRepository.saveSignature({
|
||||
contractId: contract.id,
|
||||
role,
|
||||
signerDisplayName: dto.signerDisplayName,
|
||||
signerDisplayName,
|
||||
signedAt: new Date(),
|
||||
signatureFileId: fileRecord.id,
|
||||
consentText: dto.consentText ?? null,
|
||||
});
|
||||
|
||||
if (options.signerUserId) {
|
||||
// Only (re)save the reusable profile signature when the signer drew a NEW
|
||||
// image. Reusing the saved signature must not rewrite it with itself.
|
||||
if (options.signerUserId && dto.signatureImageBase64) {
|
||||
try {
|
||||
await this.signaturesService.upsertForUser({
|
||||
userId: options.signerUserId,
|
||||
signerDisplayName: dto.signerDisplayName,
|
||||
signerDisplayName,
|
||||
signatureImageBase64: dto.signatureImageBase64,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -42,6 +42,7 @@ import { ContractTransitionService } from './contract-transition.service';
|
||||
import { ContractClearanceService } from './contract-clearance.service';
|
||||
import { ContractBookingService } from './contract-booking.service';
|
||||
import { ClearanceMilestoneService } from './clearance-milestone.service';
|
||||
import { SignaturesService } from '../signatures/signatures.service';
|
||||
import { CreateContractDto } from './dto/create-contract.dto';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto';
|
||||
import { FilterContractDto } from './dto/filter-contract.dto';
|
||||
@@ -68,6 +69,7 @@ export class ContractsController {
|
||||
private readonly clearanceService: ContractClearanceService,
|
||||
private readonly contractBookingService: ContractBookingService,
|
||||
private readonly milestoneService: ClearanceMilestoneService,
|
||||
private readonly signaturesService: SignaturesService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@@ -313,6 +315,12 @@ export class ContractsController {
|
||||
}
|
||||
const { view, html, signatures } =
|
||||
await this.transitionService.getContractDocumentView(id);
|
||||
// The signer's reusable saved signature (if any) so the sign UI can offer
|
||||
// "Approve & sign" with the stored image instead of forcing a fresh draw.
|
||||
const signerId = resolveAuthUserId(user);
|
||||
const savedSignature = signerId
|
||||
? await this.signaturesService.getForUser(signerId)
|
||||
: null;
|
||||
return {
|
||||
contractId: view.bookingId,
|
||||
reference: view.reference,
|
||||
@@ -325,6 +333,7 @@ export class ContractsController {
|
||||
canSignStaff: view.canSignStaff,
|
||||
hasContractDocument: view.hasContractDocument,
|
||||
signatures,
|
||||
savedSignature,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,16 @@ export class SignContractDto {
|
||||
@IsIn(['CUSTOMER', 'STAFF', 'DIRECTOR', 'CEO'])
|
||||
role!: 'CUSTOMER' | 'STAFF' | 'DIRECTOR' | 'CEO';
|
||||
|
||||
@ApiProperty({ description: 'PNG signature image as base64 (with or without data URL prefix)' })
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'PNG signature image as base64 (with or without data URL prefix). ' +
|
||||
'Optional: when omitted, the signer\'s reusable saved signature from their ' +
|
||||
'profile is used instead.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(20)
|
||||
signatureImageBase64!: string;
|
||||
signatureImageBase64?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
|
||||
Reference in New Issue
Block a user