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
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 };
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue