app/composables/usePdfExportBasic.ts

534 lines
18 KiB
TypeScript

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 };
};