src/app/features/template-export/template-export-api/template-export-api.service.ts
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,
          };
        }),
      );
  }
}

results matching ""

    No results matching ""