chore: update application configuration and UI components for improved styling and functionality
This commit is contained in:
parent
0af6b17792
commit
37ab8d7bab
54 changed files with 23293 additions and 1666 deletions
|
|
@ -58,7 +58,7 @@ export const useFixtures = () => {
|
|||
category: 'Services',
|
||||
subcategory: 'Development',
|
||||
targetPct: 65,
|
||||
targetMonthlyAmount: 7800,
|
||||
targetMonthlyAmount: 0,
|
||||
certainty: 'Committed',
|
||||
payoutDelayDays: 30,
|
||||
terms: 'Net 30',
|
||||
|
|
@ -73,7 +73,7 @@ export const useFixtures = () => {
|
|||
category: 'Product',
|
||||
subcategory: 'Digital Tools',
|
||||
targetPct: 20,
|
||||
targetMonthlyAmount: 2400,
|
||||
targetMonthlyAmount: 0,
|
||||
certainty: 'Probable',
|
||||
payoutDelayDays: 14,
|
||||
terms: 'Platform payout',
|
||||
|
|
@ -88,7 +88,7 @@ export const useFixtures = () => {
|
|||
category: 'Grant',
|
||||
subcategory: 'Government',
|
||||
targetPct: 10,
|
||||
targetMonthlyAmount: 1200,
|
||||
targetMonthlyAmount: 0,
|
||||
certainty: 'Committed',
|
||||
payoutDelayDays: 45,
|
||||
terms: 'Quarterly disbursement',
|
||||
|
|
@ -103,7 +103,7 @@ export const useFixtures = () => {
|
|||
category: 'Donation',
|
||||
subcategory: 'Individual',
|
||||
targetPct: 3,
|
||||
targetMonthlyAmount: 360,
|
||||
targetMonthlyAmount: 0,
|
||||
certainty: 'Aspirational',
|
||||
payoutDelayDays: 3,
|
||||
terms: 'Immediate',
|
||||
|
|
@ -118,7 +118,7 @@ export const useFixtures = () => {
|
|||
category: 'Other',
|
||||
subcategory: 'Professional Services',
|
||||
targetPct: 2,
|
||||
targetMonthlyAmount: 240,
|
||||
targetMonthlyAmount: 0,
|
||||
certainty: 'Probable',
|
||||
payoutDelayDays: 21,
|
||||
terms: 'Net 21',
|
||||
|
|
@ -163,21 +163,21 @@ export const useFixtures = () => {
|
|||
{
|
||||
id: 'overhead-1',
|
||||
name: 'Coworking Space',
|
||||
amount: 800,
|
||||
amount: 0,
|
||||
category: 'Workspace',
|
||||
recurring: true
|
||||
},
|
||||
{
|
||||
id: 'overhead-2',
|
||||
name: 'Tools & Software',
|
||||
amount: 420,
|
||||
amount: 0,
|
||||
category: 'Technology',
|
||||
recurring: true
|
||||
},
|
||||
{
|
||||
id: 'overhead-3',
|
||||
name: 'Business Insurance',
|
||||
amount: 180,
|
||||
amount: 0,
|
||||
category: 'Legal & Compliance',
|
||||
recurring: true
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ export const useFixtures = () => {
|
|||
{
|
||||
id: 'production-1',
|
||||
name: 'Development Kits',
|
||||
amount: 500,
|
||||
amount: 0,
|
||||
category: 'Hardware',
|
||||
period: '2024-01'
|
||||
}
|
||||
|
|
|
|||
191
composables/usePdfExport.ts
Normal file
191
composables/usePdfExport.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
export const usePdfExport = () => {
|
||||
const generatePDF = async (
|
||||
element: HTMLElement,
|
||||
filename: string = "document.pdf",
|
||||
options: any = {}
|
||||
): Promise<void> => {
|
||||
// Default options optimized for document templates
|
||||
const defaultOptions = {
|
||||
margin: [0.5, 0.5, 0.5, 0.5], // top, left, bottom, right in inches
|
||||
filename,
|
||||
image: { type: "jpeg", quality: 0.98 },
|
||||
html2canvas: {
|
||||
scale: 2, // Higher scale for better quality
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
letterRendering: true,
|
||||
logging: false,
|
||||
dpi: 300,
|
||||
height: undefined,
|
||||
width: undefined,
|
||||
},
|
||||
jsPDF: {
|
||||
unit: "in",
|
||||
format: "letter",
|
||||
orientation: "portrait",
|
||||
compress: true,
|
||||
},
|
||||
pagebreak: {
|
||||
mode: ["avoid-all", "css", "legacy"],
|
||||
before: ".page-break-before",
|
||||
after: ".page-break-after",
|
||||
avoid: ".no-page-break",
|
||||
},
|
||||
};
|
||||
|
||||
// Merge provided options with defaults
|
||||
const finalOptions = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
html2canvas: {
|
||||
...defaultOptions.html2canvas,
|
||||
...(options.html2canvas || {}),
|
||||
},
|
||||
jsPDF: {
|
||||
...defaultOptions.jsPDF,
|
||||
...(options.jsPDF || {}),
|
||||
},
|
||||
pagebreak: {
|
||||
...defaultOptions.pagebreak,
|
||||
...(options.pagebreak || {}),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// Dynamic import for client-side only
|
||||
if (process.server) {
|
||||
throw new Error("PDF generation is only available on the client side");
|
||||
}
|
||||
|
||||
// Import html2pdf dynamically with better error handling
|
||||
let html2pdf;
|
||||
try {
|
||||
const module = await import("html2pdf.js");
|
||||
html2pdf = module.default || module;
|
||||
} catch (importError) {
|
||||
console.error("Failed to import html2pdf.js:", importError);
|
||||
throw new Error(
|
||||
"Failed to load PDF library. Please refresh the page and try again."
|
||||
);
|
||||
}
|
||||
|
||||
// Clone the element to avoid modifying the original
|
||||
const clonedElement = element.cloneNode(true) as HTMLElement;
|
||||
|
||||
// Apply PDF-specific styling to the clone while preserving font choices
|
||||
const currentFontClass = element.className.match(/font-[\w-]+/)?.[0];
|
||||
let fontFamily = '"Times New Roman", "Times", serif'; // default
|
||||
|
||||
// Preserve selected font for PDF
|
||||
if (currentFontClass) {
|
||||
if (currentFontClass.includes("source-serif")) {
|
||||
fontFamily = '"Source Serif 4", "Times New Roman", serif';
|
||||
} else if (currentFontClass.includes("ubuntu")) {
|
||||
fontFamily =
|
||||
'"Ubuntu", -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
} else if (currentFontClass.includes("inter")) {
|
||||
fontFamily = '"Inter", -apple-system, BlinkMacSystemFont, sans-serif';
|
||||
}
|
||||
}
|
||||
|
||||
clonedElement.style.fontFamily = fontFamily;
|
||||
clonedElement.style.fontSize = "11pt";
|
||||
clonedElement.style.lineHeight = "1.5";
|
||||
clonedElement.style.color = "#000000";
|
||||
clonedElement.style.backgroundColor = "#ffffff";
|
||||
clonedElement.style.width = "8.5in";
|
||||
clonedElement.style.maxWidth = "8.5in";
|
||||
clonedElement.style.padding = "0.5in";
|
||||
clonedElement.style.boxSizing = "border-box";
|
||||
|
||||
// Hide export controls in the clone
|
||||
const exportControls = clonedElement.querySelector(".export-controls");
|
||||
if (exportControls) {
|
||||
(exportControls as HTMLElement).style.display = "none";
|
||||
}
|
||||
|
||||
// Ensure proper font loading by adding a slight delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Generate the PDF with better error handling
|
||||
console.log("Starting PDF generation with options:", finalOptions);
|
||||
|
||||
if (typeof html2pdf !== "function") {
|
||||
throw new Error(
|
||||
"html2pdf is not a function. Library may not have loaded correctly."
|
||||
);
|
||||
}
|
||||
|
||||
const pdfInstance = html2pdf();
|
||||
|
||||
if (!pdfInstance || typeof pdfInstance.set !== "function") {
|
||||
throw new Error("html2pdf instance is invalid");
|
||||
}
|
||||
|
||||
await pdfInstance.set(finalOptions).from(clonedElement).save();
|
||||
} catch (error: any) {
|
||||
console.error("PDF generation failed:", error);
|
||||
const errorMessage =
|
||||
error?.message || error?.toString() || "Unknown error";
|
||||
throw new Error(`PDF generation failed: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
const generatePDFFromContent = async (
|
||||
htmlContent: string,
|
||||
filename: string = "document.pdf",
|
||||
options: any = {}
|
||||
): Promise<void> => {
|
||||
if (process.server) {
|
||||
throw new Error("PDF generation is only available on the client side");
|
||||
}
|
||||
|
||||
// Create a temporary element with the HTML content
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
tempDiv.style.position = "absolute";
|
||||
tempDiv.style.left = "-9999px";
|
||||
tempDiv.style.top = "-9999px";
|
||||
tempDiv.style.width = "8.5in";
|
||||
tempDiv.style.fontFamily = '"Times New Roman", "Times", serif';
|
||||
tempDiv.style.fontSize = "11pt";
|
||||
tempDiv.style.lineHeight = "1.5";
|
||||
tempDiv.style.color = "#000000";
|
||||
tempDiv.style.backgroundColor = "#ffffff";
|
||||
|
||||
document.body.appendChild(tempDiv);
|
||||
|
||||
try {
|
||||
await generatePDF(tempDiv, filename, options);
|
||||
} finally {
|
||||
document.body.removeChild(tempDiv);
|
||||
}
|
||||
};
|
||||
|
||||
const exportDocumentAsPDF = async (
|
||||
selector: string = ".document-page",
|
||||
filename?: string
|
||||
): Promise<void> => {
|
||||
if (process.server) {
|
||||
throw new Error("PDF generation is only available on the client side");
|
||||
}
|
||||
|
||||
const element = document.querySelector(selector) as HTMLElement;
|
||||
if (!element) {
|
||||
throw new Error(`Element with selector "${selector}" not found`);
|
||||
}
|
||||
|
||||
// Generate filename based on current date if not provided
|
||||
const defaultFilename = `document-${
|
||||
new Date().toISOString().split("T")[0]
|
||||
}.pdf`;
|
||||
|
||||
await generatePDF(element, filename || defaultFilename);
|
||||
};
|
||||
|
||||
return {
|
||||
generatePDF,
|
||||
generatePDFFromContent,
|
||||
exportDocumentAsPDF,
|
||||
};
|
||||
};
|
||||
534
composables/usePdfExportBasic.ts
Normal file
534
composables/usePdfExportBasic.ts
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
export const usePdfExportBasic = () => {
|
||||
const exportToPDF = async (elementSelector: string, filename: string) => {
|
||||
// Only run on client side
|
||||
if (process.server || typeof window === "undefined") {
|
||||
throw new Error("PDF generation is only available on the client side");
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Starting professional PDF export...");
|
||||
|
||||
// Get the element to export
|
||||
const element = document.querySelector(elementSelector);
|
||||
if (!element) {
|
||||
throw new Error(`Element with selector "${elementSelector}" not found`);
|
||||
}
|
||||
|
||||
// Use jsPDF directly for better control and professional output
|
||||
const { jsPDF } = await import("jspdf");
|
||||
|
||||
// Extract form data from localStorage (similar to membership agreement)
|
||||
const savedData = localStorage.getItem("membership-agreement-data");
|
||||
const formData = savedData ? JSON.parse(savedData) : {};
|
||||
|
||||
// Create PDF with professional styling
|
||||
const pdf = new jsPDF({
|
||||
orientation: "portrait",
|
||||
unit: "in",
|
||||
format: "letter",
|
||||
});
|
||||
|
||||
// Helper function for page management (from revenue worksheet)
|
||||
const checkPageBreak = (
|
||||
currentY: number,
|
||||
neededSpace: number = 0.5
|
||||
): number => {
|
||||
if (currentY + neededSpace > 10) {
|
||||
pdf.addPage();
|
||||
return 1;
|
||||
}
|
||||
return currentY;
|
||||
};
|
||||
|
||||
// Helper function for section headers
|
||||
const addSectionHeader = (title: string, yPos: number): number => {
|
||||
// Force page break if not enough space for section header + some content
|
||||
if (yPos > 8.5) {
|
||||
pdf.addPage();
|
||||
yPos = 1;
|
||||
}
|
||||
pdf.setFillColor(240, 240, 240);
|
||||
pdf.rect(0.75, yPos - 0.1, 7, 0.4, "F");
|
||||
pdf.setFontSize(14);
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.setTextColor(0, 0, 0);
|
||||
pdf.text(title, 1, yPos + 0.15);
|
||||
return yPos + 0.5;
|
||||
};
|
||||
|
||||
// Format date helper
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return "[_____]";
|
||||
return new Date(dateStr).toLocaleDateString();
|
||||
};
|
||||
|
||||
// Header with professional styling
|
||||
pdf.setFillColor(0, 0, 0);
|
||||
pdf.rect(0, 0, 8.5, 1.2, "F");
|
||||
|
||||
pdf.setFontSize(20);
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.setTextColor(255, 255, 255);
|
||||
pdf.text("MEMBERSHIP AGREEMENT", 4.25, 0.6, { align: "center" });
|
||||
|
||||
pdf.setFontSize(12);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
pdf.text(formData.cooperativeName || "Worker Cooperative", 4.25, 0.9, {
|
||||
align: "center",
|
||||
});
|
||||
|
||||
// Reset text color
|
||||
pdf.setTextColor(0, 0, 0);
|
||||
|
||||
// Document info
|
||||
let yPos = 1.5;
|
||||
pdf.setFontSize(11);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
|
||||
const now = new Date();
|
||||
const generatedDate = now.toLocaleDateString();
|
||||
pdf.text(`Generated: ${generatedDate}`, 1, yPos);
|
||||
yPos += 0.3;
|
||||
|
||||
// Helper function for consistent body text
|
||||
const addBodyText = (
|
||||
text: string,
|
||||
yPos: number,
|
||||
indent: number = 1
|
||||
): number => {
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
const lines = pdf.splitTextToSize(text, 6.5);
|
||||
pdf.text(lines, indent, yPos);
|
||||
return yPos + lines.length * 0.15;
|
||||
};
|
||||
|
||||
// Helper function for subsection headers
|
||||
const addSubsectionHeader = (title: string, yPos: number): number => {
|
||||
yPos = checkPageBreak(yPos, 0.3);
|
||||
pdf.setFontSize(11);
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text(title, 1, yPos);
|
||||
return yPos + 0.2;
|
||||
};
|
||||
|
||||
// Helper function for bullet lists
|
||||
const addBulletList = (
|
||||
items: string[],
|
||||
yPos: number,
|
||||
indent: number = 1.2
|
||||
): number => {
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
items.forEach((item) => {
|
||||
yPos = checkPageBreak(yPos, 0.2);
|
||||
const lines = pdf.splitTextToSize(item, 6.5);
|
||||
pdf.text(lines, indent, yPos);
|
||||
yPos += lines.length * 0.18; // Increased line height
|
||||
});
|
||||
return yPos + 0.15; // Increased spacing after list
|
||||
};
|
||||
|
||||
// Section 1: Who We Are
|
||||
yPos = addSectionHeader("1. Who We Are", yPos);
|
||||
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text("Date Established:", 1, yPos);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
pdf.text(formatDate(formData.dateEstablished), 2.5, yPos);
|
||||
yPos += 0.3;
|
||||
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text("Our Purpose:", 1, yPos);
|
||||
yPos += 0.15;
|
||||
yPos = addBodyText(formData.purpose || "[Purpose to be filled in]", yPos);
|
||||
yPos += 0.15;
|
||||
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text("Our Core Values:", 1, yPos);
|
||||
yPos += 0.15;
|
||||
yPos = addBodyText(
|
||||
formData.coreValues || "[Core values to be filled in]",
|
||||
yPos
|
||||
);
|
||||
yPos += 0.2;
|
||||
|
||||
// Section 2: Membership
|
||||
yPos = addSectionHeader("2. Membership", yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Who Can Be a Member", yPos);
|
||||
yPos = addBodyText("Any person who:", yPos);
|
||||
yPos += 0.1;
|
||||
|
||||
const memberCriteria = [
|
||||
"• Shares our values and purpose",
|
||||
"• Contributes labour to the cooperative (by doing actual work, not just investing money)",
|
||||
"• Commits to collective decision-making",
|
||||
"• Participates in governance responsibilities",
|
||||
];
|
||||
yPos = addBulletList(memberCriteria, yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Becoming a Member", yPos);
|
||||
yPos = addBodyText(
|
||||
"New members join through a consent process, which means existing members must agree that adding this person won't harm the cooperative.",
|
||||
yPos
|
||||
);
|
||||
yPos += 0.15;
|
||||
|
||||
const membershipSteps = [
|
||||
`1. Trial period of ${
|
||||
formData.trialPeriodMonths || "[___]"
|
||||
} months working together`,
|
||||
"2. Values alignment conversation",
|
||||
"3. Consent decision by current members",
|
||||
`4. Optional - Equal buy-in contribution of $${
|
||||
formData.buyInAmount || "[___]"
|
||||
} (can be paid over time or waived based on need)`,
|
||||
];
|
||||
yPos = addBulletList(membershipSteps, yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Leaving the Cooperative", yPos);
|
||||
yPos = addBodyText(
|
||||
`Members can leave anytime with ${
|
||||
formData.noticeDays || "[___]"
|
||||
} days notice. The cooperative will:`,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.1;
|
||||
|
||||
const leavingSteps = [
|
||||
`• Pay out their share of any surplus within ${
|
||||
formData.surplusPayoutDays || "[___]"
|
||||
} days`,
|
||||
`• Return their buy-in contribution within ${
|
||||
formData.buyInReturnDays || "[___]"
|
||||
} days`,
|
||||
"• Maintain respectful ongoing relationships when possible",
|
||||
];
|
||||
yPos = addBulletList(leavingSteps, yPos);
|
||||
yPos += 0.1;
|
||||
|
||||
// Section 3: How We Make Decisions
|
||||
yPos = addSectionHeader("3. How We Make Decisions", yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Consent-Based Decisions", yPos);
|
||||
yPos = addBodyText(
|
||||
"We use consent, not consensus. This means we move forward when no one has a principled objection that would harm the cooperative. An objection must explain how the proposal would contradict our values or threaten our sustainability.",
|
||||
yPos
|
||||
);
|
||||
yPos += 0.15;
|
||||
|
||||
yPos = addSubsectionHeader("Day-to-Day Decisions", yPos);
|
||||
yPos = addBodyText(
|
||||
`Decisions under $${
|
||||
formData.dayToDayLimit || "[___]"
|
||||
} can be made by any member. Just tell others what you did at the next meeting.`,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.15;
|
||||
|
||||
yPos = addSubsectionHeader("Regular Decisions", yPos);
|
||||
yPos = addBodyText(
|
||||
`Decisions between $${formData.regularDecisionMin || "[___]"} and $${
|
||||
formData.regularDecisionMax || "[___]"
|
||||
} need consent from members present at a meeting (minimum 2 members).`,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.15;
|
||||
|
||||
yPos = addSubsectionHeader("Major Decisions", yPos);
|
||||
yPos = addBodyText("These require consent from all members:", yPos);
|
||||
yPos += 0.1;
|
||||
|
||||
const majorDecisions = [
|
||||
"• Adding or removing members",
|
||||
"• Changing this agreement",
|
||||
`• Taking on debt over $${formData.majorDebtThreshold || "[___]"}`,
|
||||
"• Fundamental changes to our purpose or structure",
|
||||
"• Dissolution of the cooperative",
|
||||
];
|
||||
yPos = addBulletList(majorDecisions, yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Meeting Structure", yPos);
|
||||
const meetingItems = [
|
||||
`• We meet ${
|
||||
formData.meetingFrequency || "[___]"
|
||||
} to make decisions together`,
|
||||
`• Emergency meetings need ${
|
||||
formData.emergencyNoticeHours || "[___]"
|
||||
} hours notice`,
|
||||
"• All members can add items to the agenda",
|
||||
"• We keep simple records of what we decide",
|
||||
];
|
||||
yPos = addBulletList(meetingItems, yPos);
|
||||
|
||||
// Section 4: Money and Labour
|
||||
yPos = addSectionHeader("4. Money and Labour", yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Equal Ownership", yPos);
|
||||
yPos = addBodyText(
|
||||
"Each member owns an equal share of the cooperative, regardless of when they joined or how much money they put in.",
|
||||
yPos
|
||||
);
|
||||
yPos += 0.15;
|
||||
|
||||
yPos = addSubsectionHeader("Paying Ourselves", yPos);
|
||||
const paymentItems = [
|
||||
`• Base hourly rate: $${
|
||||
formData.baseRate || "[___]"
|
||||
}/hour for all members`,
|
||||
`• Monthly draw: $${
|
||||
formData.monthlyDraw || "[___]"
|
||||
} per month (if applicable)`,
|
||||
`• Payment date: ${formData.paymentDay || "[___]"}th of each month`,
|
||||
`• Surplus sharing: ${
|
||||
formData.surplusFrequency || "[___]"
|
||||
}ly based on hours worked`,
|
||||
];
|
||||
yPos = addBulletList(paymentItems, yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Work Expectations", yPos);
|
||||
const workItems = [
|
||||
`• Target hours per week: ${formData.targetHours || "[___]"} hours`,
|
||||
"• All work counts equally - admin, client work, business development",
|
||||
"• We track hours honestly and transparently",
|
||||
"• Flexible scheduling based on personal needs and business requirements",
|
||||
];
|
||||
yPos = addBulletList(workItems, yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Financial Transparency", yPos);
|
||||
const transparencyItems = [
|
||||
"• All members can access all financial records anytime",
|
||||
"• Monthly financial updates shared with everyone",
|
||||
"• Annual financial review conducted together",
|
||||
"• No secret salaries or hidden expenses",
|
||||
];
|
||||
yPos = addBulletList(transparencyItems, yPos);
|
||||
|
||||
// Section 5: Roles and Responsibilities
|
||||
yPos = addSectionHeader("5. Roles and Responsibilities", yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Rotating Roles", yPos);
|
||||
yPos = addBodyText(
|
||||
`We rotate operational roles every ${
|
||||
formData.roleRotationMonths || "[___]"
|
||||
} months to share knowledge and prevent burnout. Current roles include:`,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.1;
|
||||
|
||||
const roles = [
|
||||
"• Financial coordinator (bookkeeping, invoicing, payments)",
|
||||
"• Client relationship manager (main point of contact)",
|
||||
"• Operations coordinator (scheduling, project management)",
|
||||
"• Business development (marketing, new client outreach)",
|
||||
];
|
||||
yPos = addBulletList(roles, yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Shared Responsibilities", yPos);
|
||||
yPos = addBodyText("All members participate in:", yPos);
|
||||
yPos += 0.1;
|
||||
|
||||
const sharedResponsibilities = [
|
||||
"• Weekly planning and check-in meetings",
|
||||
"• Monthly financial review",
|
||||
"• Annual strategic planning",
|
||||
"• Conflict resolution when needed",
|
||||
"• Onboarding new members",
|
||||
];
|
||||
yPos = addBulletList(sharedResponsibilities, yPos);
|
||||
|
||||
// Section 6: Conflict and Care
|
||||
yPos = addSectionHeader("6. Conflict and Care", yPos);
|
||||
|
||||
yPos = addSubsectionHeader("When Conflict Happens", yPos);
|
||||
const conflictSteps = [
|
||||
"1. Direct conversation between parties (if comfortable)",
|
||||
"2. Bring in a neutral member as mediator",
|
||||
"3. Full group conversation if needed",
|
||||
"4. External mediation if we can't resolve it ourselves",
|
||||
"5. As a last resort, consent process about membership",
|
||||
];
|
||||
yPos = addBulletList(conflictSteps, yPos);
|
||||
|
||||
yPos = addSubsectionHeader("Care Commitments", yPos);
|
||||
const careItems = [
|
||||
"• We check in about capacity and wellbeing regularly",
|
||||
"• We adjust workload when someone is struggling",
|
||||
"• We celebrate successes and support through failures",
|
||||
"• We maintain boundaries between work and personal relationships",
|
||||
"• We commit to direct, kind communication",
|
||||
];
|
||||
yPos = addBulletList(careItems, yPos);
|
||||
|
||||
// Section 7: Changing This Agreement
|
||||
yPos = addSectionHeader("7. Changing This Agreement", yPos);
|
||||
yPos = addBodyText(
|
||||
`This agreement gets reviewed every ${
|
||||
formData.reviewFrequency || "[___]"
|
||||
} and can be changed anytime with consent from all members. We'll update it as we learn what works for us.`,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.2;
|
||||
|
||||
// Section 8: If We Need to Close
|
||||
yPos = addSectionHeader("8. If We Need to Close", yPos);
|
||||
yPos = addBodyText("If the cooperative dissolves:", yPos);
|
||||
yPos += 0.1;
|
||||
|
||||
const dissolutionItems = [
|
||||
"• Pay all debts and obligations first",
|
||||
"• Return member buy-ins",
|
||||
`• Donate remaining assets to ${
|
||||
formData.assetDonationTarget || "[organization to be determined]"
|
||||
}`,
|
||||
"• Close all legal and financial accounts",
|
||||
"• Celebrate what we built together",
|
||||
];
|
||||
yPos = addBulletList(dissolutionItems, yPos);
|
||||
|
||||
// Section 9: Legal Bits
|
||||
yPos = addSectionHeader("9. Legal Bits", yPos);
|
||||
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text("Legal Structure:", 1, yPos);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
pdf.text(formData.legalStructure || "[To be determined]", 2.5, yPos);
|
||||
yPos += 0.2;
|
||||
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text("Registered Location:", 1, yPos);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
pdf.text(formData.registeredLocation || "[To be determined]", 2.5, yPos);
|
||||
yPos += 0.2;
|
||||
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text("Fiscal Year End:", 1, yPos);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
pdf.text(
|
||||
`${formData.fiscalYearEndMonth || "December"} ${
|
||||
formData.fiscalYearEndDay || "31"
|
||||
}`,
|
||||
2.5,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.3;
|
||||
|
||||
// Section 10: Agreement Review
|
||||
yPos = addSectionHeader("10. Agreement Review", yPos);
|
||||
|
||||
yPos = addBodyText(
|
||||
`Last Updated: ${formatDate(formData.lastUpdated)}`,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.1;
|
||||
yPos = addBodyText(
|
||||
`Next Review: ${
|
||||
formatDate(formData.nextReview) || "[To be scheduled]"
|
||||
}`,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.2;
|
||||
|
||||
// Current Members section (if any members are entered)
|
||||
if (
|
||||
formData.members &&
|
||||
formData.members.length > 0 &&
|
||||
formData.members.some((m: any) => m.name)
|
||||
) {
|
||||
yPos = addSectionHeader("Current Members", yPos);
|
||||
|
||||
formData.members.forEach((member: any, index: number) => {
|
||||
if (member.name) {
|
||||
yPos = checkPageBreak(yPos, 0.8);
|
||||
|
||||
pdf.setFontSize(11);
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text(`${index + 1}. ${member.name}`, 1, yPos);
|
||||
yPos += 0.25;
|
||||
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
|
||||
if (member.email) {
|
||||
pdf.text(`Email: ${member.email}`, 1.2, yPos);
|
||||
yPos += 0.18;
|
||||
}
|
||||
|
||||
if (member.joinDate) {
|
||||
pdf.text(`Joined: ${formatDate(member.joinDate)}`, 1.2, yPos);
|
||||
yPos += 0.18;
|
||||
}
|
||||
|
||||
if (member.role) {
|
||||
pdf.text(`Role: ${member.role}`, 1.2, yPos);
|
||||
yPos += 0.18;
|
||||
}
|
||||
|
||||
yPos += 0.25;
|
||||
}
|
||||
});
|
||||
yPos += 0.2;
|
||||
}
|
||||
|
||||
// Signature section
|
||||
yPos = checkPageBreak(yPos, 2.5);
|
||||
|
||||
pdf.setFontSize(12);
|
||||
pdf.setFont("helvetica", "bold");
|
||||
pdf.text("Member Signatures", 1, yPos);
|
||||
yPos += 0.3;
|
||||
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont("helvetica", "normal");
|
||||
pdf.text(
|
||||
"By signing below, we agree to these terms and commit to working together as equals in this cooperative.",
|
||||
1,
|
||||
yPos
|
||||
);
|
||||
yPos += 0.3;
|
||||
|
||||
// Create signature lines based on actual members (minimum 2, maximum 8)
|
||||
const membersWithNames = formData.members
|
||||
? formData.members.filter((m: any) => m.name)
|
||||
: [];
|
||||
const numSignatureLines = Math.max(
|
||||
2,
|
||||
Math.min(8, membersWithNames.length || 4)
|
||||
);
|
||||
const signatureLineWidth = 5;
|
||||
|
||||
for (let i = 0; i < numSignatureLines; i++) {
|
||||
yPos = checkPageBreak(yPos, 0.6);
|
||||
|
||||
// Pre-fill member name if available, otherwise leave blank
|
||||
const memberName = membersWithNames[i]?.name || "";
|
||||
|
||||
if (memberName) {
|
||||
pdf.setFont("helvetica", "normal");
|
||||
pdf.setFontSize(10);
|
||||
pdf.text(memberName, 1, yPos);
|
||||
yPos += 0.2;
|
||||
}
|
||||
|
||||
// Simple signature line (1px thin line)
|
||||
pdf.setLineWidth(0.01); // Very thin line
|
||||
pdf.line(1, yPos, 1 + signatureLineWidth, yPos);
|
||||
pdf.setLineWidth(0.2); // Reset to default
|
||||
yPos += 0.4;
|
||||
}
|
||||
|
||||
console.log("Saving PDF...");
|
||||
pdf.save(filename);
|
||||
|
||||
console.log("PDF saved successfully!");
|
||||
} catch (error: any) {
|
||||
console.error("Basic PDF generation error:", error);
|
||||
throw new Error(`PDF generation failed: ${error.message || error}`);
|
||||
}
|
||||
};
|
||||
|
||||
return { exportToPDF };
|
||||
};
|
||||
186
composables/usePdfExportSafe.ts
Normal file
186
composables/usePdfExportSafe.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
export const usePdfExportSafe = () => {
|
||||
const exportToPDF = async (elementSelector: string, filename: string) => {
|
||||
// Only run on client side
|
||||
if (process.server || typeof window === "undefined") {
|
||||
throw new Error("PDF generation is only available on the client side");
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import for client-side only
|
||||
const html2pdf = (await import("html2pdf.js")).default;
|
||||
|
||||
// Get the element to export
|
||||
const element = document.querySelector(elementSelector);
|
||||
if (!element) {
|
||||
throw new Error(`Element with selector "${elementSelector}" not found`);
|
||||
}
|
||||
|
||||
// Create a completely clean version of the content
|
||||
const createCleanContent = () => {
|
||||
const cleanDiv = document.createElement("div");
|
||||
cleanDiv.style.cssText = `
|
||||
width: 8.5in;
|
||||
padding: 0.5in;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #000000;
|
||||
background: #ffffff;
|
||||
`;
|
||||
|
||||
// Extract text content and basic structure
|
||||
const extractContent = (sourceEl: Element, targetEl: HTMLElement) => {
|
||||
const children = sourceEl.children;
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
|
||||
// Skip export controls
|
||||
if (
|
||||
child.classList.contains("export-controls") ||
|
||||
child.classList.contains("no-pdf") ||
|
||||
child.classList.contains("no-print")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let newEl: HTMLElement;
|
||||
|
||||
// Create appropriate element based on tag
|
||||
switch (child.tagName.toLowerCase()) {
|
||||
case "h1":
|
||||
newEl = document.createElement("h1");
|
||||
newEl.style.cssText =
|
||||
"font-size: 24px; font-weight: bold; margin: 20px 0 10px 0; color: #000;";
|
||||
break;
|
||||
case "h2":
|
||||
newEl = document.createElement("h2");
|
||||
newEl.style.cssText =
|
||||
"font-size: 20px; font-weight: bold; margin: 16px 0 8px 0; color: #000;";
|
||||
break;
|
||||
case "h3":
|
||||
newEl = document.createElement("h3");
|
||||
newEl.style.cssText =
|
||||
"font-size: 16px; font-weight: bold; margin: 12px 0 6px 0; color: #000;";
|
||||
break;
|
||||
case "p":
|
||||
newEl = document.createElement("p");
|
||||
newEl.style.cssText = "margin: 8px 0; color: #000;";
|
||||
break;
|
||||
case "ul":
|
||||
newEl = document.createElement("ul");
|
||||
newEl.style.cssText =
|
||||
"margin: 8px 0; padding-left: 20px; color: #000;";
|
||||
break;
|
||||
case "ol":
|
||||
newEl = document.createElement("ol");
|
||||
newEl.style.cssText =
|
||||
"margin: 8px 0; padding-left: 20px; color: #000;";
|
||||
break;
|
||||
case "li":
|
||||
newEl = document.createElement("li");
|
||||
newEl.style.cssText = "margin: 4px 0; color: #000;";
|
||||
break;
|
||||
case "input":
|
||||
newEl = document.createElement("span");
|
||||
const inputEl = child as HTMLInputElement;
|
||||
newEl.textContent = inputEl.value || "[_____]";
|
||||
newEl.style.cssText =
|
||||
"border-bottom: 1px solid #000; padding: 2px; color: #000;";
|
||||
break;
|
||||
case "textarea":
|
||||
newEl = document.createElement("span");
|
||||
const textareaEl = child as HTMLTextAreaElement;
|
||||
newEl.textContent = textareaEl.value || "[_____]";
|
||||
newEl.style.cssText =
|
||||
"border: 1px solid #000; padding: 4px; display: inline-block; color: #000;";
|
||||
break;
|
||||
case "select":
|
||||
newEl = document.createElement("span");
|
||||
const selectEl = child as HTMLSelectElement;
|
||||
newEl.textContent = selectEl.value || "[_____]";
|
||||
newEl.style.cssText =
|
||||
"border: 1px solid #000; padding: 2px; color: #000;";
|
||||
break;
|
||||
default:
|
||||
newEl = document.createElement("div");
|
||||
newEl.style.cssText = "color: #000;";
|
||||
}
|
||||
|
||||
// Set text content for elements that should have it
|
||||
if (
|
||||
child.tagName.toLowerCase() !== "input" &&
|
||||
child.tagName.toLowerCase() !== "textarea" &&
|
||||
child.tagName.toLowerCase() !== "select"
|
||||
) {
|
||||
// Get only direct text content, not from children
|
||||
const directText = Array.from(child.childNodes)
|
||||
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.textContent)
|
||||
.join("");
|
||||
|
||||
if (directText.trim()) {
|
||||
newEl.appendChild(document.createTextNode(directText));
|
||||
}
|
||||
}
|
||||
|
||||
targetEl.appendChild(newEl);
|
||||
|
||||
// Recursively process children
|
||||
if (child.children.length > 0) {
|
||||
extractContent(child, newEl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extractContent(element, cleanDiv);
|
||||
return cleanDiv;
|
||||
};
|
||||
|
||||
const cleanElement = createCleanContent();
|
||||
|
||||
// Temporarily add to DOM
|
||||
cleanElement.style.position = "absolute";
|
||||
cleanElement.style.left = "-9999px";
|
||||
cleanElement.style.top = "-9999px";
|
||||
document.body.appendChild(cleanElement);
|
||||
|
||||
// Simple options - no complex CSS processing
|
||||
const options = {
|
||||
margin: 0.5,
|
||||
filename: filename,
|
||||
image: { type: "jpeg", quality: 0.98 },
|
||||
html2canvas: {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
logging: false,
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
jsPDF: {
|
||||
unit: "in",
|
||||
format: "letter",
|
||||
orientation: "portrait",
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Generating PDF with clean content...");
|
||||
|
||||
try {
|
||||
// Generate PDF from the clean element
|
||||
await html2pdf().set(options).from(cleanElement).save();
|
||||
console.log("PDF generated successfully!");
|
||||
} finally {
|
||||
// Clean up
|
||||
if (cleanElement.parentNode) {
|
||||
document.body.removeChild(cleanElement);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("PDF generation error:", error);
|
||||
throw new Error(`PDF generation failed: ${error.message || error}`);
|
||||
}
|
||||
};
|
||||
|
||||
return { exportToPDF };
|
||||
};
|
||||
170
composables/usePdfExportSimple.ts
Normal file
170
composables/usePdfExportSimple.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
export const usePdfExportSimple = () => {
|
||||
const exportToPDF = async (elementSelector: string, filename: string) => {
|
||||
// Only run on client side
|
||||
if (process.server || typeof window === "undefined") {
|
||||
throw new Error("PDF generation is only available on the client side");
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import for client-side only
|
||||
const html2pdf = (await import("html2pdf.js")).default;
|
||||
|
||||
// Get the element to export
|
||||
const element = document.querySelector(elementSelector);
|
||||
if (!element) {
|
||||
throw new Error(`Element with selector "${elementSelector}" not found`);
|
||||
}
|
||||
|
||||
// Clone the element to avoid modifying the original
|
||||
const clonedElement = element.cloneNode(true) as HTMLElement;
|
||||
|
||||
// Fix CSS compatibility issues by applying only computed styles
|
||||
const fixCSSCompatibility = (el: HTMLElement) => {
|
||||
// Get all elements including the root
|
||||
const allElements = [el, ...el.querySelectorAll("*")] as HTMLElement[];
|
||||
|
||||
allElements.forEach((elem) => {
|
||||
// Get computed styles
|
||||
const computedStyle = window.getComputedStyle(elem);
|
||||
|
||||
// Clear all existing styles to start fresh
|
||||
elem.removeAttribute("style");
|
||||
elem.removeAttribute("class");
|
||||
|
||||
// Apply only essential computed styles that are safe
|
||||
elem.style.display = computedStyle.display;
|
||||
elem.style.position = computedStyle.position;
|
||||
elem.style.width = computedStyle.width;
|
||||
elem.style.height = computedStyle.height;
|
||||
elem.style.margin = computedStyle.margin;
|
||||
elem.style.padding = computedStyle.padding;
|
||||
elem.style.fontSize = computedStyle.fontSize;
|
||||
elem.style.fontWeight = computedStyle.fontWeight;
|
||||
elem.style.fontFamily = computedStyle.fontFamily;
|
||||
elem.style.lineHeight = computedStyle.lineHeight;
|
||||
elem.style.textAlign = computedStyle.textAlign;
|
||||
|
||||
// Apply safe color values - convert any complex colors to simple ones
|
||||
const safeColor =
|
||||
computedStyle.color.includes("oklch") ||
|
||||
computedStyle.color.includes("oklab")
|
||||
? "#000000"
|
||||
: computedStyle.color;
|
||||
const safeBgColor =
|
||||
computedStyle.backgroundColor.includes("oklch") ||
|
||||
computedStyle.backgroundColor.includes("oklab")
|
||||
? "transparent"
|
||||
: computedStyle.backgroundColor;
|
||||
|
||||
elem.style.color = safeColor;
|
||||
elem.style.backgroundColor = safeBgColor;
|
||||
elem.style.borderWidth = computedStyle.borderWidth;
|
||||
elem.style.borderStyle = computedStyle.borderStyle;
|
||||
elem.style.borderColor = "#cccccc"; // Safe fallback color
|
||||
});
|
||||
};
|
||||
|
||||
// Apply CSS fixes
|
||||
fixCSSCompatibility(clonedElement);
|
||||
|
||||
// Temporarily add to DOM for processing
|
||||
clonedElement.style.position = "absolute";
|
||||
clonedElement.style.left = "-9999px";
|
||||
clonedElement.style.top = "-9999px";
|
||||
document.body.appendChild(clonedElement);
|
||||
|
||||
// Simple options for better compatibility
|
||||
const options = {
|
||||
margin: 0.5,
|
||||
filename: filename,
|
||||
image: { type: "jpeg", quality: 0.98 },
|
||||
html2canvas: {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
logging: false,
|
||||
ignoreElements: (element: Element) => {
|
||||
// Skip elements that might cause issues
|
||||
return (
|
||||
element.classList.contains("no-pdf") ||
|
||||
element.classList.contains("export-controls")
|
||||
);
|
||||
},
|
||||
onclone: (clonedDoc: Document) => {
|
||||
// Remove ALL existing stylesheets to avoid oklch() issues
|
||||
const stylesheets = clonedDoc.querySelectorAll(
|
||||
'style, link[rel="stylesheet"]'
|
||||
);
|
||||
stylesheets.forEach((sheet) => sheet.remove());
|
||||
|
||||
// Add only safe, basic CSS
|
||||
const safeStyle = clonedDoc.createElement("style");
|
||||
safeStyle.textContent = `
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #000000;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.export-controls, .no-pdf {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Basic typography */
|
||||
h1 { font-size: 24px; font-weight: bold; margin: 20px 0 10px 0; }
|
||||
h2 { font-size: 20px; font-weight: bold; margin: 16px 0 8px 0; }
|
||||
h3 { font-size: 16px; font-weight: bold; margin: 12px 0 6px 0; }
|
||||
p { margin: 8px 0; }
|
||||
ul, ol { margin: 8px 0; padding-left: 20px; }
|
||||
li { margin: 4px 0; }
|
||||
|
||||
/* Form elements */
|
||||
input, textarea, select {
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
`;
|
||||
clonedDoc.head.appendChild(safeStyle);
|
||||
},
|
||||
},
|
||||
jsPDF: {
|
||||
unit: "in",
|
||||
format: "letter",
|
||||
orientation: "portrait",
|
||||
},
|
||||
};
|
||||
|
||||
console.log("Generating PDF with html2pdf...");
|
||||
|
||||
// Wait a moment for DOM changes to settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
try {
|
||||
// Generate and save the PDF using the cloned element
|
||||
await html2pdf().set(options).from(clonedElement).save();
|
||||
console.log("PDF generated successfully!");
|
||||
} finally {
|
||||
// Clean up the cloned element
|
||||
if (clonedElement.parentNode) {
|
||||
document.body.removeChild(clonedElement);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("PDF generation error:", error);
|
||||
throw new Error(`PDF generation failed: ${error.message || error}`);
|
||||
}
|
||||
};
|
||||
|
||||
return { exportToPDF };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue