ca-grow-ops-manager/backend/src/services/branding.service.ts
fullsizemalt 4bdbfc82ca Add tinypdf-plus integration with PDF generation and branding
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>
2026-01-08 12:07:35 -08:00

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();