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:
Krao Hasanee
2026-05-13 14:20:38 -04:00
parent c9e7816e28
commit eee0885811
117 changed files with 17592 additions and 4057 deletions
+239
View File
@@ -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`);
}