add goverment booking

This commit is contained in:
Marshal
2026-07-31 00:41:51 +00:00
parent 34e4a7d13f
commit 65f18b4796
35 changed files with 913 additions and 88 deletions

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString, MinLength } from 'class-validator';
export class SaveSignatureDto {
@ApiProperty()
@@ -13,6 +13,15 @@ export class SaveSignatureDto {
@IsString()
@MinLength(20)
signatureImageBase64!: string;
@ApiPropertyOptional({
description:
'Company stamp/seal image as base64 (with or without data URL prefix). Omit to keep the existing saved stamp.',
})
@IsOptional()
@IsString()
@MinLength(20)
stampImageBase64?: string;
}
export class SavedSignatureDto {
@@ -21,4 +30,7 @@ export class SavedSignatureDto {
@ApiProperty({ nullable: true })
signatureImageUrl!: string | null;
@ApiProperty({ nullable: true })
stampImageUrl!: string | null;
}

View File

@@ -22,4 +22,11 @@ export class SavedSignature extends BaseEntity {
@ManyToOne(() => FileRecord, { nullable: true })
@JoinColumn({ name: 'signature_file_id' })
signatureFile?: FileRecord | null;
@Column({ name: 'stamp_file_id', type: 'uuid', nullable: true })
stampFileId?: string | null;
@ManyToOne(() => FileRecord, { nullable: true })
@JoinColumn({ name: 'stamp_file_id' })
stampFile?: FileRecord | null;
}

View File

@@ -33,6 +33,7 @@ export class SignaturesController {
userId,
signerDisplayName: dto.signerDisplayName,
signatureImageBase64: dto.signatureImageBase64,
stampImageBase64: dto.stampImageBase64,
});
return this.signaturesService.getForUser(userId);
}

View File

@@ -16,7 +16,7 @@ export class SignaturesRepository extends BaseRepository<SavedSignature> {
findByUserId(userId: string): Promise<SavedSignature | null> {
return this.repository.findOne({
where: { userId } as never,
relations: ['signatureFile'],
relations: ['signatureFile', 'stampFile'],
});
}

View File

@@ -13,6 +13,8 @@ export interface UpsertSignatureInput {
userId: string;
signerDisplayName: string;
signatureImageBase64: string;
/** Optional company stamp/seal; omitted = keep the existing saved stamp. */
stampImageBase64?: string;
}
@Injectable()
@@ -31,15 +33,65 @@ export class SignaturesService {
return {
signerDisplayName: saved.signerDisplayName,
signatureImageUrl: await this.inlineImageUrl(saved.signatureFile?.url),
stampImageUrl: await this.inlineImageUrl(saved.stampFile?.url),
};
}
/** Insert or update the user's reusable signature, storing the image in MinIO. */
/** Insert or update the user's reusable signature (and optional stamp), storing the images in MinIO. */
async upsertForUser(input: UpsertSignatureInput): Promise<SavedSignature> {
const buffer = this.decodeSignatureImage(input.signatureImageBase64);
const file: Express.Multer.File = {
fieldname: 'signature',
originalname: `signature-${input.userId}.png`,
// Capture the previously referenced files so we can remove them only AFTER
// the saved_signatures row is repointed — deleting first would violate the
// FK constraint (saved_signatures.*_file_id -> files.id).
const existing = await this.signaturesRepository.findByUserId(input.userId);
const previousFileId = existing?.signatureFileId ?? null;
const previousStampFileId = existing?.stampFileId ?? null;
const fileRecord = await this.filesService.upload({
resourceId: input.userId,
resource: 'saved_signatures',
code: 'signature',
file: this.toUploadFile('signature', input.userId, input.signatureImageBase64),
});
const stampRecord = input.stampImageBase64
? await this.filesService.upload({
resourceId: input.userId,
resource: 'saved_signatures',
code: 'stamp',
file: this.toUploadFile('stamp', input.userId, input.stampImageBase64),
})
: null;
const saved = await this.signaturesRepository.upsert({
userId: input.userId,
signerDisplayName: input.signerDisplayName,
signatureFileId: fileRecord.id,
// Omitted stamp keeps whatever was saved before.
...(stampRecord ? { stampFileId: stampRecord.id } : {}),
});
const staleIds = [
previousFileId !== fileRecord.id ? previousFileId : null,
stampRecord && previousStampFileId !== stampRecord.id
? previousStampFileId
: null,
].filter((id): id is string => Boolean(id));
if (staleIds.length) {
await this.dataSource.getRepository(FileRecord).delete(staleIds);
}
return saved;
}
private toUploadFile(
kind: 'signature' | 'stamp',
userId: string,
base64: string,
): Express.Multer.File {
const buffer = this.decodeSignatureImage(base64);
return {
fieldname: kind,
originalname: `${kind}-${userId}.png`,
encoding: '7bit',
mimetype: 'image/png',
size: buffer.length,
@@ -49,33 +101,6 @@ export class SignaturesService {
filename: '',
path: '',
};
// Capture the previously referenced file so we can remove it only AFTER the
// saved_signatures row is repointed — deleting it first would violate the
// FK constraint (saved_signatures.signature_file_id -> files.id).
const existing = await this.signaturesRepository.findByUserId(input.userId);
const previousFileId = existing?.signatureFileId ?? null;
const fileRecord = await this.filesService.upload({
resourceId: input.userId,
resource: 'saved_signatures',
code: 'signature',
file,
});
const saved = await this.signaturesRepository.upsert({
userId: input.userId,
signerDisplayName: input.signerDisplayName,
signatureFileId: fileRecord.id,
});
if (previousFileId && previousFileId !== fileRecord.id) {
await this.dataSource
.getRepository(FileRecord)
.delete({ id: previousFileId });
}
return saved;
}
private async inlineImageUrl(