Fix file sharing load speed and move error; misc updates
- Remove recursive directory size calculations (single Seafile API call per list) - Remove 'Used in this location' usage display - Fix move using v2 per-type endpoints instead of broken batch endpoint - Send entry type from frontend for correct move routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
const W = 792;
|
||||
const H = 612;
|
||||
const MARGIN = 36;
|
||||
const ACCENT = [245, 165, 35];
|
||||
const DARK = [18, 18, 18];
|
||||
const HEADER_H = 64;
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const d = new Date(`${dateStr}T12:00:00`);
|
||||
return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
async function blobToDataUrl(blob) {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => resolve(event.target?.result || null);
|
||||
reader.onerror = () => resolve(null);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
async function resolvePhoto(source) {
|
||||
if (!source) return null;
|
||||
if (source instanceof File || source instanceof Blob) {
|
||||
return blobToDataUrl(source);
|
||||
}
|
||||
if (typeof source === 'string') return source.startsWith('data:') ? source : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function getFormat(dataUrl) {
|
||||
return dataUrl?.startsWith('data:image/png') ? 'PNG' : 'JPEG';
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve) => {
|
||||
if (!src) return resolve(null);
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
const PX_PER_PT = 300 / 72;
|
||||
|
||||
function toPrintJpeg(dataUrl, img, displayPtW, displayPtH, quality = 0.82) {
|
||||
if (!dataUrl || dataUrl.startsWith('data:image/png')) return dataUrl;
|
||||
const tW = Math.round(displayPtW * PX_PER_PT);
|
||||
const tH = Math.round(displayPtH * PX_PER_PT);
|
||||
if (img.naturalWidth <= tW && img.naturalHeight <= tH) return dataUrl;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = tW;
|
||||
canvas.height = tH;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, tW, tH);
|
||||
return canvas.toDataURL('image/jpeg', quality);
|
||||
}
|
||||
|
||||
function addHeader(doc, title, pageNum, totalPages) {
|
||||
doc.setFillColor(255, 255, 255);
|
||||
doc.rect(0, 0, W, HEADER_H, 'F');
|
||||
doc.setFillColor(...ACCENT);
|
||||
doc.rect(0, 0, 4, HEADER_H, 'F');
|
||||
doc.setDrawColor(220, 220, 220);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(0, HEADER_H, W, HEADER_H);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(...DARK);
|
||||
doc.text(title.toUpperCase(), MARGIN, HEADER_H / 2 + 4);
|
||||
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(140, 140, 140);
|
||||
doc.text(`${pageNum} / ${totalPages}`, W - MARGIN, HEADER_H / 2 + 4, { align: 'right' });
|
||||
}
|
||||
|
||||
function drawCoverImage(doc, dataUrl, img, x, y, w, h) {
|
||||
if (!dataUrl || !img) return;
|
||||
const srcAspect = img.naturalWidth / img.naturalHeight;
|
||||
const boxAspect = w / h;
|
||||
let drawW;
|
||||
let drawH;
|
||||
let drawX;
|
||||
let drawY;
|
||||
|
||||
if (srcAspect > boxAspect) {
|
||||
drawW = w;
|
||||
drawH = w / srcAspect;
|
||||
drawX = x;
|
||||
drawY = y + (h - drawH) / 2;
|
||||
} else {
|
||||
drawH = h;
|
||||
drawW = h * srcAspect;
|
||||
drawX = x + (w - drawW) / 2;
|
||||
drawY = y;
|
||||
}
|
||||
|
||||
const printableDataUrl = toPrintJpeg(dataUrl, img, drawW, drawH);
|
||||
doc.addImage(printableDataUrl, getFormat(printableDataUrl), drawX, drawY, drawW, drawH);
|
||||
}
|
||||
|
||||
export async function generateSurveyMakerPdf(data) {
|
||||
const { clientName, siteAddress, surveyDate, preparedBy, projectName, signs } = data;
|
||||
const doc = new jsPDF({ orientation: 'landscape', format: 'letter', unit: 'pt', compress: true });
|
||||
|
||||
const signPhotos = await Promise.all((signs || []).map(async (sign) => {
|
||||
const mainDataUrl = await resolvePhoto(sign.mainPhoto);
|
||||
const contextDataUrls = await Promise.all([
|
||||
resolvePhoto(sign.contextPhoto1),
|
||||
resolvePhoto(sign.contextPhoto2),
|
||||
resolvePhoto(sign.contextPhoto3),
|
||||
]);
|
||||
const mainImage = mainDataUrl ? await loadImage(mainDataUrl) : null;
|
||||
const contextImages = await Promise.all(contextDataUrls.map(async (dataUrl) => (dataUrl ? loadImage(dataUrl) : null)));
|
||||
|
||||
return { mainDataUrl, mainImage, contextDataUrls, contextImages };
|
||||
}));
|
||||
|
||||
const totalPages = Math.max(1, 1 + signs.length);
|
||||
|
||||
doc.setFillColor(255, 255, 255);
|
||||
doc.rect(0, 0, W, H, 'F');
|
||||
doc.setFillColor(...ACCENT);
|
||||
doc.rect(0, 0, W, 5, 'F');
|
||||
doc.rect(0, H - 5, W, 5, 'F');
|
||||
|
||||
doc.setFontSize(30);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(...DARK);
|
||||
doc.text(projectName || 'Sign Survey', MARGIN, 88);
|
||||
|
||||
const details = [
|
||||
['Client', clientName || '-'],
|
||||
['Site Address', siteAddress || '-'],
|
||||
['Survey Date', formatDate(surveyDate)],
|
||||
['Prepared By', preparedBy || '-'],
|
||||
['Sign Count', String(signs.length)],
|
||||
];
|
||||
|
||||
let y = 148;
|
||||
details.forEach(([label, value]) => {
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(150, 150, 150);
|
||||
doc.text(label.toUpperCase(), MARGIN, y);
|
||||
y += 16;
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(40, 40, 40);
|
||||
const lines = doc.splitTextToSize(value, 320);
|
||||
doc.text(lines, MARGIN, y);
|
||||
y += lines.length * 16 + 18;
|
||||
});
|
||||
|
||||
if (signPhotos[0]?.mainDataUrl && signPhotos[0]?.mainImage) {
|
||||
const photoX = 410;
|
||||
const photoY = 110;
|
||||
const photoW = W - photoX - MARGIN;
|
||||
const photoH = 360;
|
||||
drawCoverImage(doc, signPhotos[0].mainDataUrl, signPhotos[0].mainImage, photoX, photoY, photoW, photoH);
|
||||
}
|
||||
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(140, 140, 140);
|
||||
doc.text('1 / ' + totalPages, W - MARGIN, H - 18, { align: 'right' });
|
||||
|
||||
for (let index = 0; index < signs.length; index += 1) {
|
||||
const sign = signs[index];
|
||||
const photo = signPhotos[index];
|
||||
doc.addPage();
|
||||
addHeader(doc, sign.signName || `Sign ${index + 1}`, index + 2, totalPages);
|
||||
|
||||
const top = HEADER_H + 20;
|
||||
const leftW = 270;
|
||||
const rightX = MARGIN + leftW + 20;
|
||||
const rightW = W - rightX - MARGIN;
|
||||
const mainPhotoH = 280;
|
||||
const contextGap = 10;
|
||||
const contextPhotoY = top + mainPhotoH + 12;
|
||||
const contextPhotoW = (rightW - contextGap * 2) / 3;
|
||||
const contextPhotoH = H - contextPhotoY - 36;
|
||||
|
||||
doc.setFontSize(18);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(...DARK);
|
||||
doc.text(sign.signName || `Sign ${index + 1}`, MARGIN, top + 10);
|
||||
|
||||
let textY = top + 42;
|
||||
const blocks = [
|
||||
['Measurements', sign.measurements || '-'],
|
||||
['Notes', sign.notes || '-'],
|
||||
];
|
||||
|
||||
blocks.forEach(([label, value]) => {
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(150, 150, 150);
|
||||
doc.text(label.toUpperCase(), MARGIN, textY);
|
||||
textY += 16;
|
||||
doc.setFontSize(11);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(50, 50, 50);
|
||||
const lines = doc.splitTextToSize(value, leftW);
|
||||
doc.text(lines, MARGIN, textY);
|
||||
textY += lines.length * 14 + 24;
|
||||
});
|
||||
|
||||
if (photo?.mainDataUrl && photo?.mainImage) {
|
||||
drawCoverImage(doc, photo.mainDataUrl, photo.mainImage, rightX, top, rightW, mainPhotoH);
|
||||
} else {
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(170, 170, 170);
|
||||
doc.text('No Main Photo', rightX + rightW / 2, top + mainPhotoH / 2, { align: 'center' });
|
||||
}
|
||||
|
||||
for (let contextIndex = 0; contextIndex < 3; contextIndex += 1) {
|
||||
const x = rightX + contextIndex * (contextPhotoW + contextGap);
|
||||
|
||||
const contextDataUrl = photo?.contextDataUrls?.[contextIndex];
|
||||
const contextImage = photo?.contextImages?.[contextIndex];
|
||||
if (contextDataUrl && contextImage) {
|
||||
drawCoverImage(doc, contextDataUrl, contextImage, x, contextPhotoY, contextPhotoW, contextPhotoH);
|
||||
} else {
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(180, 180, 180);
|
||||
doc.text(`Context ${contextIndex + 1}`, x + contextPhotoW / 2, contextPhotoY + contextPhotoH / 2, { align: 'center' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const safe = (value) => (value || '').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, ' ');
|
||||
const filename = [safe(projectName || 'Survey'), safe(clientName), formatDate(surveyDate).replace(/[^a-zA-Z0-9]/g, '')].filter(Boolean).join('.');
|
||||
doc.save(`${filename || 'SurveyMaker'}.pdf`);
|
||||
}
|
||||
Reference in New Issue
Block a user