Backend changes: - Add tinypdf-plus dependency for TTF/OTF font support - Create PDF service with text, certificate, and label generation - Add PDF API endpoints (/api/pdf/*) - Add branding service for custom fonts and styling - Add branding API endpoints (/api/branding/*) - Add font registration endpoint for custom fonts Frontend changes: - Add PDF library with download utilities - Add PDFDownloadButton component for reusable PDF downloads - Add branding API client for managing branding configs - Update ReportsPage with PDF generation tab Co-Authored-By: Claude <noreply@anthropic.com>
143 lines
3.5 KiB
TypeScript
143 lines
3.5 KiB
TypeScript
/**
|
|
* Branding Service
|
|
* Manages custom branding settings for PDF generation
|
|
*/
|
|
|
|
import { randomUUID } from 'crypto';
|
|
import { promises as fs } from 'fs';
|
|
import path from 'path';
|
|
|
|
export interface BrandingConfig {
|
|
id: string;
|
|
name: string;
|
|
// Font references (stored separately)
|
|
titleFont?: string;
|
|
bodyFont?: string;
|
|
labelFont?: string;
|
|
// Colors
|
|
primaryColor?: string;
|
|
secondaryColor?: string;
|
|
accentColor?: string;
|
|
// Logo
|
|
logoData?: string; // base64 encoded image
|
|
// Metadata
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
class BrandingService {
|
|
private configs: Map<string, BrandingConfig> = new Map();
|
|
private storagePath: string;
|
|
|
|
constructor() {
|
|
this.storagePath = path.join(process.cwd(), 'storage', 'branding');
|
|
this.ensureStorageDirectory();
|
|
this.loadDefaultBranding();
|
|
}
|
|
|
|
private async ensureStorageDirectory() {
|
|
try {
|
|
await fs.mkdir(this.storagePath, { recursive: true });
|
|
} catch (error) {
|
|
console.warn('Could not create branding storage directory:', error);
|
|
}
|
|
}
|
|
|
|
private loadDefaultBranding() {
|
|
// Create a default branding configuration
|
|
const defaultBranding: BrandingConfig = {
|
|
id: 'default',
|
|
name: 'Default',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
this.configs.set('default', defaultBranding);
|
|
}
|
|
|
|
/**
|
|
* Get a branding configuration by ID
|
|
*/
|
|
getBranding(id: string): BrandingConfig | undefined {
|
|
return this.configs.get(id);
|
|
}
|
|
|
|
/**
|
|
* Get the default branding configuration
|
|
*/
|
|
getDefaultBranding(): BrandingConfig {
|
|
return this.configs.get('default')!;
|
|
}
|
|
|
|
/**
|
|
* Create or update a branding configuration
|
|
*/
|
|
async saveBranding(config: Partial<BrandingConfig>): Promise<BrandingConfig> {
|
|
const id = config.id || randomUUID();
|
|
const existing = this.configs.get(id);
|
|
|
|
const branding: BrandingConfig = {
|
|
id,
|
|
name: config.name || 'Custom Branding',
|
|
titleFont: config.titleFont,
|
|
bodyFont: config.bodyFont,
|
|
labelFont: config.labelFont,
|
|
primaryColor: config.primaryColor,
|
|
secondaryColor: config.secondaryColor,
|
|
accentColor: config.accentColor,
|
|
logoData: config.logoData,
|
|
createdAt: existing?.createdAt || new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
this.configs.set(id, branding);
|
|
|
|
// Persist to disk
|
|
await this.persistBranding(id, branding);
|
|
|
|
return branding;
|
|
}
|
|
|
|
/**
|
|
* List all branding configurations
|
|
*/
|
|
listBrandings(): BrandingConfig[] {
|
|
return Array.from(this.configs.values());
|
|
}
|
|
|
|
/**
|
|
* Delete a branding configuration
|
|
*/
|
|
async deleteBranding(id: string): Promise<boolean> {
|
|
if (id === 'default') {
|
|
throw new Error('Cannot delete default branding');
|
|
}
|
|
|
|
const deleted = this.configs.delete(id);
|
|
|
|
if (deleted) {
|
|
// Remove from disk
|
|
try {
|
|
const filePath = path.join(this.storagePath, `${id}.json`);
|
|
await fs.unlink(filePath);
|
|
} catch (error) {
|
|
console.warn(`Could not delete branding file for ${id}:`, error);
|
|
}
|
|
}
|
|
|
|
return deleted;
|
|
}
|
|
|
|
/**
|
|
* Persist branding configuration to disk
|
|
*/
|
|
private async persistBranding(id: string, branding: BrandingConfig) {
|
|
try {
|
|
const filePath = path.join(this.storagePath, `${id}.json`);
|
|
await fs.writeFile(filePath, JSON.stringify(branding, null, 2));
|
|
} catch (error) {
|
|
console.error(`Failed to persist branding ${id}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const brandingService = new BrandingService();
|