import { inject, Injectable } from "@angular/core";
import { FileService } from "../../file/file.service";
import { SafeUrl } from "@angular/platform-browser";
import { Entity } from "app/core/entity/model/entity";
import { Observable, of, throwError } from "rxjs";
import { HttpResponse } from "@angular/common/http";
import { NotAvailableOfflineError } from "../../../core/session/not-available-offline.error";
import { NAVIGATOR_TOKEN } from "../../../utils/di-tokens";
import { switchMap } from "rxjs/operators";
import { TemplateExport } from "../template-export.entity";
import { Logging } from "../../../core/logging/logging.service";
import { environment } from "../../../../environments/environment";
import {
TemplateExportComplement,
TemplateExportContextService,
} from "../template-export-context/template-export-context.service";
/**
* Format of API response body upon uploading a new template file.
*/
interface TemplateUploadResponseDto {
templateId: string;
}
/**
* Format of API request body to render a PDF from a template.
* TemplateId is provided via URL path.
*/
interface TemplateRenderRequestDto {
/**
* target file type (e.g. "pdf")
*/
convertTo: string;
/**
* The data used to fill placeholders in the template.
*/
data: Object;
/**
* Additional context data available in the template under the `{c.…}` prefix.
*/
complement?: TemplateExportComplement;
}
/**
* Format of API request body to render a batch of files from one template.
* `data` is an array of records; the backend will render each one and return a ZIP.
*/
interface TemplateRenderBatchRequestDto {
convertTo: string;
data: Object[];
/**
* Additional context data, shared by all records of the batch,
* available in the template under the `{c.…}` prefix.
*/
complement?: TemplateExportComplement;
}
export interface TemplateExportResult {
filename: string;
file: ArrayBuffer;
}
export interface TemplateExportBatchResult {
filename: string;
file: ArrayBuffer;
}
/**
* Interact with the PDF Template Generation API that uses File Templates to generate custom pdf documents.
*/
@Injectable({
providedIn: "root",
})
export class TemplateExportApiService extends FileService {
private navigator = inject<Navigator>(NAVIGATOR_TOKEN);
private readonly exportContext = inject(TemplateExportContextService);
readonly API_URL = environment.API_PROXY_PREFIX + "/v1/export";
/*
--- FileService methods ---
*/
/**
* Upload a new template file to the API.
* @param file to be uploaded to the API as a template
* @param entity
* @param property
* @return The template ID generated by the API
*/
uploadFile(
file: File,
entity: TemplateExport,
property: string,
): Observable<string> {
if (!this.navigator.onLine) {
return throwError(() => new NotAvailableOfflineError("File Attachments"));
}
const formData = new FormData();
formData.append("template", file, file.name);
return this.httpClient.post(this.API_URL + "/template", formData).pipe(
switchMap(async (res: TemplateUploadResponseDto) => {
entity.templateId = res.templateId;
await this.entityMapper.save(entity);
return res.templateId;
}),
);
}
protected override getShowFileUrl(
entity: TemplateExport,
property: string,
): string {
return this.API_URL + "/template/" + entity.getId();
}
loadFile(entity: Entity, property: string): Observable<SafeUrl> {
// should not be required for our use cases of the Template Export API
throw new Error("Method not implemented.");
}
removeFile(entity: Entity, property: string): Observable<any> {
// we do not do explicit file removal due to the design of the API
Logging.debug("skipping file removal for Template Export API");
return of(true);
}
removeAllFiles(entity: Entity): Observable<any> {
Logging.debug("skipping file removal for Template Export API");
return of(true);
}
/*
--- PDF Generation API methods ---
*/
/**
* Generate a PDF applying actual data to an existing template.
* @param template The TemplateExport entity to render
* @param data The data object (typically an entity) to be applied to the template
* @return An array buffer of the generated PDF
*/
generatePdfFromTemplate(
template: TemplateExport,
data: Object,
): Observable<TemplateExportResult> {
const complement = this.exportContext.getComplement();
return this.httpClient
.post(
this.API_URL + "/render/" + template.getId(),
{
convertTo: "pdf",
data: data,
...(complement ? { complement } : {}),
} as TemplateRenderRequestDto,
{ observe: "response", responseType: "arraybuffer" },
)
.pipe(
switchMap(async (res: HttpResponse<ArrayBuffer>) => {
// the API returns the filename in the Content-Disposition header as a URL-encoded string with special delimiters
const filenameMatch = decodeURIComponent(
res.headers.get("Content-Disposition"),
).match(/filename="(.+)"/);
const fileName =
filenameMatch && filenameMatch.length > 1
? filenameMatch[1]
: template.title;
return {
filename: fileName,
file: res.body,
};
}),
);
}
/**
* Generate output from one template for many records in a single request.
*
* Both modes go through the template engine's native batch rendering — the backend
* just forwards the request:
* - `mode: "zip"` (default): N independent files packaged in a ZIP archive.
* - `mode: "combined"`: N rendered files merged into a single multi-page PDF.
*
* @param template The TemplateExport entity to render
* @param dataList The array of data objects (typically entities) to apply to the template
* @param mode How the backend should aggregate the output
* @return The generated file (ZIP or PDF) and a derived filename
*/
generateBatchFromTemplate(
template: TemplateExport,
dataList: Object[],
mode: "zip" | "combined" = "zip",
): Observable<TemplateExportBatchResult> {
const fallbackExtension = mode === "combined" ? ".pdf" : ".zip";
const complement = this.exportContext.getComplement();
return this.httpClient
.post(
this.API_URL + "/render-batch/" + template.getId() + "?mode=" + mode,
{
convertTo: "pdf",
data: dataList,
...(complement ? { complement } : {}),
} as TemplateRenderBatchRequestDto,
{ observe: "response", responseType: "arraybuffer" },
)
.pipe(
switchMap(async (res: HttpResponse<ArrayBuffer>) => {
const disposition = res.headers.get("Content-Disposition");
const filenameMatch = disposition
? decodeURIComponent(disposition).match(/filename="?([^";]+)"?/)
: null;
const filename =
filenameMatch && filenameMatch.length > 1
? filenameMatch[1]
: template.title + fallbackExtension;
return {
filename,
file: res.body,
};
}),
);
}
}