refactor: update app.vue and various components to improve routing paths, enhance UI consistency, and streamline layout for better user experience
This commit is contained in:
parent
b6e8d3b7ec
commit
78af43770c
29 changed files with 1699 additions and 1990 deletions
30
app.vue
30
app.vue
|
|
@ -15,7 +15,8 @@
|
|||
Urgent Tools
|
||||
</h1>
|
||||
</NuxtLink>
|
||||
<div class="absolute right-0">
|
||||
<div class="absolute right-0 flex items-center gap-2">
|
||||
<CurrencySelector />
|
||||
<ColorModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -24,7 +25,7 @@
|
|||
role="navigation"
|
||||
aria-label="Main navigation">
|
||||
<NuxtLink
|
||||
to="/coop-planner"
|
||||
to="/tools/coop-planner"
|
||||
class="px-3 py-2 text-sm text-black dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
||||
:class="{
|
||||
'bg-neutral-100 dark:bg-neutral-800': isCoopBuilderSection,
|
||||
|
|
@ -42,20 +43,20 @@
|
|||
Coach
|
||||
</NuxtLink> -->
|
||||
<NuxtLink
|
||||
to="/wizards"
|
||||
to="/tools/wizards"
|
||||
class="px-3 py-2 text-sm text-black dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
||||
:class="{
|
||||
'bg-neutral-100 dark:bg-neutral-800':
|
||||
$route.path === '/wizards',
|
||||
$route.path === '/tools/wizards',
|
||||
}">
|
||||
Wizards
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/resources"
|
||||
to="/tools/resources"
|
||||
class="px-3 py-2 text-sm text-black dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-md transition-colors"
|
||||
:class="{
|
||||
'bg-neutral-100 dark:bg-neutral-800':
|
||||
$route.path === '/resources',
|
||||
$route.path === '/tools/resources',
|
||||
}">
|
||||
More Resources & Templates
|
||||
</NuxtLink>
|
||||
|
|
@ -72,6 +73,7 @@
|
|||
<NuxtPage />
|
||||
</template>
|
||||
</NuxtLayout>
|
||||
<AppFooter />
|
||||
<NuxtRouteAnnouncer />
|
||||
</div>
|
||||
</UApp>
|
||||
|
|
@ -81,18 +83,16 @@
|
|||
const route = useRoute();
|
||||
const isCoopBuilderSection = computed(
|
||||
() =>
|
||||
route.path === "/coop-planner" ||
|
||||
route.path === "/coop-builder" ||
|
||||
route.path === "/" ||
|
||||
route.path === "/mix" ||
|
||||
route.path === "/budget" ||
|
||||
route.path === "/project-budget" ||
|
||||
route.path === "/settings" ||
|
||||
route.path === "/glossary"
|
||||
route.path === "/tools/coop-planner" ||
|
||||
route.path === "/tools/coop-builder" ||
|
||||
route.path === "/tools" ||
|
||||
route.path === "/tools/mix" ||
|
||||
route.path === "/tools/budget" ||
|
||||
route.path === "/tools/project-budget"
|
||||
);
|
||||
|
||||
const isWizardSection = computed(
|
||||
() => route.path === "/wizards" || route.path.startsWith("/templates/")
|
||||
() => route.path === "/tools/wizards" || route.path.startsWith("/tools/templates/")
|
||||
);
|
||||
|
||||
// Run migrations on app startup
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ h1, h2, h3, h4, h5, h6 {
|
|||
font-family: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
TEMPLATE DOCUMENT LAYOUT
|
||||
|
|
@ -124,7 +126,7 @@ html.dark .section-card::before {
|
|||
}
|
||||
|
||||
.inline-field {
|
||||
@apply inline-block mx-1 min-w-[120px] border-none bg-neutral-50 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 px-2 py-1 rounded;
|
||||
@apply inline-block mx-1;
|
||||
}
|
||||
|
||||
.inline-field:focus {
|
||||
|
|
|
|||
|
|
@ -85,13 +85,6 @@
|
|||
v-if="suggestedCategories.length > 0">
|
||||
Consider developing: {{ suggestedCategories.join(", ") }}
|
||||
</p>
|
||||
<p class="text-xs text-black dark:text-white">
|
||||
<NuxtLink
|
||||
to="/help#revenue-diversification"
|
||||
class="text-white dark:text-neutral-100 hover:text-white dark:hover:text-white underline">
|
||||
Learn how to develop these revenue streams →
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
57
components/AppFooter.vue
Normal file
57
components/AppFooter.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<footer class="bg-neutral-50 dark:bg-neutral-900 border-t border-neutral-200 dark:border-neutral-800 mt-20">
|
||||
<UContainer>
|
||||
<div class="py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<h3 class="text-lg font-mono uppercase font-bold text-black dark:text-white mb-4">
|
||||
Urgent Tools
|
||||
</h3>
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
Tools for cooperative planning, budgeting, and resource management.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-black dark:text-white mb-3">Tools</h4>
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
<NuxtLink to="/tools/coop-planner" class="text-sm text-neutral-600 dark:text-neutral-400 hover:text-black dark:hover:text-white transition-colors">
|
||||
Budget Builder
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/tools/wizards" class="text-sm text-neutral-600 dark:text-neutral-400 hover:text-black dark:hover:text-white transition-colors">
|
||||
Wizards
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/tools/resources" class="text-sm text-neutral-600 dark:text-neutral-400 hover:text-black dark:hover:text-white transition-colors">
|
||||
Resources & Templates
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<USeparator class="my-8" />
|
||||
|
||||
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-500">
|
||||
© {{ currentYear }} Urgent Tools. All rights reserved.
|
||||
</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="https://github.com" target="_blank" rel="noopener noreferrer" class="text-neutral-500 dark:text-neutral-500 hover:text-black dark:hover:text-white transition-colors">
|
||||
<UIcon name="i-simple-icons-github" class="w-5 h-5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
</script>
|
||||
|
|
@ -30,17 +30,17 @@ const coopBuilderItems = [
|
|||
{
|
||||
id: "coop-builder",
|
||||
name: "Settings",
|
||||
path: "/coop-builder",
|
||||
path: "/tools/coop-builder",
|
||||
},
|
||||
{
|
||||
id: "budget",
|
||||
name: "Studio Budget",
|
||||
path: "/budget",
|
||||
path: "/tools/budget",
|
||||
},
|
||||
{
|
||||
id: "project-budget",
|
||||
name: "Project Budget",
|
||||
path: "/project-budget",
|
||||
path: "/tools/project-budget",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
40
components/CurrencySelector.vue
Normal file
40
components/CurrencySelector.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<USelectMenu
|
||||
v-model="selectedCurrency"
|
||||
:items="selectOptions"
|
||||
value-key="value"
|
||||
:ui="{
|
||||
trigger: 'flex items-center gap-1.5 text-sm px-2.5 py-1.5 bg-white dark:bg-neutral-950 border border-neutral-200 dark:border-neutral-800 rounded-md hover:bg-neutral-50 dark:hover:bg-neutral-900 transition-colors',
|
||||
width: 'w-auto min-w-[120px]'
|
||||
}"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-xs font-mono">{{ getCurrencySymbol(selectedCurrency) }} {{ selectedCurrency }}</span>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { currencyOptions, getCurrencySymbol } from '~/utils/currency'
|
||||
|
||||
const { currency } = useCoopBuilder()
|
||||
|
||||
// Create options for USelectMenu
|
||||
const selectOptions = computed(() =>
|
||||
currencyOptions.map((curr) => ({
|
||||
label: `${curr.symbol} ${curr.code}`,
|
||||
value: curr.code,
|
||||
}))
|
||||
)
|
||||
|
||||
// Two-way binding with the store
|
||||
const selectedCurrency = computed({
|
||||
get: () => currency.value,
|
||||
set: (value: string) => {
|
||||
currency.value = value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -958,475 +958,13 @@ const formatMembershipAgreementAsMarkdown = (data: any): string => {
|
|||
|
||||
// Conflict Resolution Framework formatting - Complete document with all static and dynamic content
|
||||
const formatConflictResolutionAsText = (data: any): string => {
|
||||
const formData = data.formData || {};
|
||||
const cooperativeName = formData.orgName || "[Cooperative Name]";
|
||||
let content = `${cooperativeName.toUpperCase()} CONFLICT RESOLUTION POLICY\n${"=".repeat(
|
||||
cooperativeName.length + 30
|
||||
)}\n\n`;
|
||||
|
||||
content += `Framework Created: ${
|
||||
formData.createdDate || new Date().toISOString().split("T")[0]
|
||||
}\n`;
|
||||
if (formData.reviewDate) {
|
||||
content += `Next Review: ${formData.reviewDate}\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
|
||||
// Core Values section (if enabled)
|
||||
if (data.sectionsEnabled?.values !== false) {
|
||||
content += `OUR VALUES\n----------\n\n`;
|
||||
content += `This conflict resolution framework is guided by our core values:\n\n`;
|
||||
|
||||
if (formData.coreValuesList && formData.coreValuesList.length > 0) {
|
||||
formData.coreValuesList.forEach((value: string) => {
|
||||
content += `• ${value}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.customValues) {
|
||||
content += `${formData.customValues}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolution Philosophy
|
||||
const approachDescriptions: { [key: string]: string } = {
|
||||
restorative:
|
||||
"We use a restorative/loving justice approach that focuses on healing, understanding root causes, and repairing relationships rather than punishment.",
|
||||
mediation:
|
||||
"We use a mediation-first approach where neutral third-party facilitators help parties dialogue and find solutions.",
|
||||
progressive:
|
||||
"We use progressive discipline with clear escalation steps and defined consequences for violations.",
|
||||
hybrid:
|
||||
"We use a hybrid approach that combines multiple methods based on the type and severity of conflict.",
|
||||
};
|
||||
|
||||
if (formData.approach && approachDescriptions[formData.approach]) {
|
||||
content += `OUR APPROACH\n------------\n\n`;
|
||||
content += `${approachDescriptions[formData.approach]}\n\n`;
|
||||
content += `We do our best to resolve conflicts at the lowest possible escalation step (direct resolution), but agree to escalate conflicts (to assisted resolution) if they are not resolved.\n\n`;
|
||||
}
|
||||
|
||||
// Reflection Process (if enabled)
|
||||
if (data.sectionsEnabled?.reflection !== false) {
|
||||
content += `REFLECTION\n----------\n\n`;
|
||||
content += `Before engaging in direct resolution, we encourage taking time for reflection:\n\n`;
|
||||
content += `1. Set aside time to think through what happened. What was the other person's behaviour? How did it affect you? Distinguish other people's actions from your feelings about them.\n`;
|
||||
content += `2. Consider uncertainties or misunderstandings that may have occurred.\n`;
|
||||
content += `3. Distinguish disagreement from personal hostility. Disagreement and dissent are part of healthy discussion. Hostility is not.\n`;
|
||||
content += `4. Use your personal support system (friends, family, therapist, etc.) to work through and clarify your perspective.\n`;
|
||||
content += `5. Ask yourself what part you played, how you could have behaved differently, and what your needs are.\n\n`;
|
||||
|
||||
if (formData.customReflectionPrompts) {
|
||||
content += `Additional Reflection Prompts:\n${formData.customReflectionPrompts}\n\n`;
|
||||
}
|
||||
|
||||
const reflectionTiming =
|
||||
formData.reflectionPeriod || "Before any escalation";
|
||||
content += `Reflection Timing: ${reflectionTiming}\n\n`;
|
||||
}
|
||||
|
||||
// Direct Resolution (if enabled)
|
||||
if (data.sectionsEnabled?.directResolution !== false) {
|
||||
content += `DIRECT RESOLUTION\n-----------------\n\n`;
|
||||
content += `A direct resolution process occurs when individuals communicate their concerns and work together to resolve disputes without filing an informal or formal complaint.\n\n`;
|
||||
|
||||
content += `Have a Conversation\n-------------------\n\n`;
|
||||
content += `When there is a disagreement, the involved people should first communicate with each other about their concerns.\n\n`;
|
||||
|
||||
content += `1. Choose a time and place to meet that is private and agreeable to both.\n`;
|
||||
content += `2. Allow reasonable time for the conversation.\n`;
|
||||
content += `3. The point is mutual understanding, not determining who is right or wrong. This requires patience and willingness to listen without immediately dismissing the other person's perspective.\n`;
|
||||
content += `4. Express thoughts and feelings directly without belittling or dismissing. Use "I" statements and active listening techniques.\n`;
|
||||
content += `5. Communicate your wants and needs and make offers and requests.\n`;
|
||||
content += `6. Learn for the future. Ask questions like, "If what I/you said or did came across that way, what can we do to prevent this from happening in the future?"\n`;
|
||||
|
||||
if (formData.documentDirectResolution) {
|
||||
content += `7. Keep a written record of the resolution agreed to by both parties.\n\n`;
|
||||
} else {
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Communication Channels
|
||||
if (formData.channelsList && formData.channelsList.length > 0) {
|
||||
content += `Escalating Communication Bandwidth\n-----------------------------------\n\n`;
|
||||
content += `Whenever a misunderstanding or conflict arises, escalate the bandwidth of the channel:\n\n`;
|
||||
formData.channelsList.forEach((channel: string, index: number) => {
|
||||
content += `${index + 1}. ${channel}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.requireDirectAttempt) {
|
||||
content += `NOTE: Direct resolution must be attempted before escalating to assisted resolution, unless safety concerns prevent this.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Assisted Resolution
|
||||
content += `ASSISTED RESOLUTION\n-------------------\n\n`;
|
||||
content += `If talking things out doesn't work, you can ask a responsible contact person for help in writing.\n\n`;
|
||||
|
||||
// Responsible Contact People
|
||||
if (formData.receiversList && formData.receiversList.length > 0) {
|
||||
content += `Initial Contact Options:\n`;
|
||||
formData.receiversList.forEach((receiver: string) => {
|
||||
content += `• ${receiver}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Mediator Structure
|
||||
if (formData.mediatorType) {
|
||||
content += `Mediation/Facilitation Structure: ${formData.mediatorType}\n\n`;
|
||||
|
||||
if (formData.supportPeople) {
|
||||
content += `Support People: Parties may bring a trusted person for emotional support during mediation sessions.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline
|
||||
content += `Response Times:\n`;
|
||||
if (formData.initialResponse) {
|
||||
content += `• Initial Response: ${formData.initialResponse}\n`;
|
||||
}
|
||||
if (formData.resolutionTarget) {
|
||||
content += `• Target Resolution: ${formData.resolutionTarget}\n\n`;
|
||||
}
|
||||
|
||||
// Formal Complaints
|
||||
content += `FORMAL COMPLAINTS\n-----------------\n\n`;
|
||||
content += `If assisted resolution efforts do not result in an acceptable outcome within a reasonable timeframe, a formal complaint may be filed in writing.\n\n`;
|
||||
|
||||
// Required Elements
|
||||
if (
|
||||
formData.complaintElementsList &&
|
||||
formData.complaintElementsList.length > 0
|
||||
) {
|
||||
content += `Written Complaint Requirements:\n`;
|
||||
formData.complaintElementsList.forEach((element: string, index: number) => {
|
||||
content += `${index + 1}. ${element}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Formal Process Timeline
|
||||
content += `Formal Process Timeline:\n`;
|
||||
if (formData.formalAcknowledgmentTime) {
|
||||
content += `• Acknowledgment: ${formData.formalAcknowledgmentTime}\n`;
|
||||
}
|
||||
if (formData.formalReviewTime) {
|
||||
content += `• Review Completion: ${formData.formalReviewTime}\n\n`;
|
||||
}
|
||||
|
||||
if (formData.requireExternalAdvice) {
|
||||
content += `External Expertise: For complex complaints involving multiple parties or organizational leaders, external legal advice will be sought.\n\n`;
|
||||
}
|
||||
|
||||
// Settlement Documentation
|
||||
if (formData.requireMinutesOfSettlement) {
|
||||
content += `Reaching Agreement\n------------------\n\n`;
|
||||
content += `Any resolution agreed upon must be documented in "Minutes of Settlement" signed by both parties. These agreements will be kept confidential according to our privacy standards.\n\n`;
|
||||
}
|
||||
|
||||
// Consequences and Actions
|
||||
if (formData.actionsList && formData.actionsList.length > 0) {
|
||||
content += `POSSIBLE OUTCOMES\n-----------------\n\n`;
|
||||
content += `Depending on the situation, resolution may include:\n\n`;
|
||||
formData.actionsList.forEach((action: string) => {
|
||||
content += `• ${action}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.appealProcess) {
|
||||
content += `Appeals Process: Parties may request review of decisions through our appeals process.\n\n`;
|
||||
}
|
||||
|
||||
// Documentation and Privacy
|
||||
if (data.sectionsEnabled?.documentation !== false) {
|
||||
content += `DOCUMENTATION & PRIVACY\n-----------------------\n\n`;
|
||||
if (formData.docLevel) {
|
||||
content += `Documentation Level: ${formData.docLevel}\n\n`;
|
||||
}
|
||||
if (formData.confidentiality) {
|
||||
content += `Confidentiality: ${formData.confidentiality}\n\n`;
|
||||
}
|
||||
if (formData.retention) {
|
||||
content += `Record Retention: ${formData.retention}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// External Resources (if enabled)
|
||||
if (data.sectionsEnabled?.externalResources !== false) {
|
||||
content += `EXTERNAL RESOURCES\n------------------\n\n`;
|
||||
if (formData.includeHumanRights) {
|
||||
content += `Individuals who are not satisfied with the outcome of a harassment or discrimination complaint may file a complaint with the Canadian Human Rights Commission or their provincial human rights tribunal.\n\n`;
|
||||
}
|
||||
|
||||
if (formData.additionalResources) {
|
||||
content += `Additional Resources:\n${formData.additionalResources}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation
|
||||
content += `POLICY MANAGEMENT\n-----------------\n\n`;
|
||||
if (formData.training) {
|
||||
content += `Training Requirements:\n${formData.training}\n\n`;
|
||||
}
|
||||
|
||||
content += `Review and Updates:\n`;
|
||||
if (formData.reviewSchedule) {
|
||||
content += `This policy will be reviewed ${formData.reviewSchedule.toLowerCase()}.\n\n`;
|
||||
}
|
||||
if (formData.amendments) {
|
||||
content += `Amendment Process: ${formData.amendments}\n\n`;
|
||||
}
|
||||
|
||||
// Acknowledgments
|
||||
if (formData.acknowledgments) {
|
||||
content += `Acknowledgments:\n${formData.acknowledgments}\n\n`;
|
||||
}
|
||||
|
||||
return content;
|
||||
// Use the pre-generated content from the Vue component
|
||||
return data.content || "No content available";
|
||||
};
|
||||
|
||||
const formatConflictResolutionAsMarkdown = (data: any): string => {
|
||||
const formData = data.formData || {};
|
||||
const cooperativeName = formData.orgName || "[Cooperative Name]";
|
||||
let content = `# ${cooperativeName} Conflict Resolution Policy\n\n`;
|
||||
|
||||
content += `*Framework Created: ${
|
||||
formData.createdDate || new Date().toISOString().split("T")[0]
|
||||
}*\n`;
|
||||
if (formData.reviewDate) {
|
||||
content += `*Next Review: ${formData.reviewDate}*\n`;
|
||||
}
|
||||
content += `\n---\n\n`;
|
||||
|
||||
// Core Values section (if enabled)
|
||||
if (data.sectionsEnabled?.values !== false) {
|
||||
content += `## Our Values\n\n`;
|
||||
content += `This conflict resolution framework is guided by our core values:\n\n`;
|
||||
|
||||
if (formData.coreValuesList && formData.coreValuesList.length > 0) {
|
||||
formData.coreValuesList.forEach((value: string) => {
|
||||
content += `- **${value}**\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.customValues) {
|
||||
content += `${formData.customValues}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolution Philosophy
|
||||
const approachDescriptions: { [key: string]: string } = {
|
||||
restorative:
|
||||
"We use a **restorative/loving justice** approach that focuses on healing, understanding root causes, and repairing relationships rather than punishment.",
|
||||
mediation:
|
||||
"We use a **mediation-first** approach where neutral third-party facilitators help parties dialogue and find solutions.",
|
||||
progressive:
|
||||
"We use **progressive discipline** with clear escalation steps and defined consequences for violations.",
|
||||
hybrid:
|
||||
"We use a **hybrid approach** that combines multiple methods based on the type and severity of conflict.",
|
||||
};
|
||||
|
||||
if (formData.approach && approachDescriptions[formData.approach]) {
|
||||
content += `## Our Approach\n\n`;
|
||||
content += `${approachDescriptions[formData.approach]}\n\n`;
|
||||
content += `We do our best to resolve conflicts at the lowest possible escalation step (direct resolution), but agree to escalate conflicts (to assisted resolution) if they are not resolved.\n\n`;
|
||||
}
|
||||
|
||||
// Reflection Process (if enabled)
|
||||
if (data.sectionsEnabled?.reflection !== false) {
|
||||
content += `## Reflection\n\n`;
|
||||
content += `Before engaging in direct resolution, we encourage taking time for reflection:\n\n`;
|
||||
content += `1. **Set aside time to think** through what happened. What was the other person's behaviour? How did it affect you? *Distinguish other people's **actions** from your **feelings** about them.*\n`;
|
||||
content += `2. **Consider uncertainties** or misunderstandings that may have occurred.\n`;
|
||||
content += `3. **Distinguish disagreement from personal hostility.** Disagreement and dissent are part of healthy discussion. Hostility is not.\n`;
|
||||
content += `4. **Use your personal support system** (friends, family, therapist, etc.) to work through and clarify your perspective.\n`;
|
||||
content += `5. **Ask yourself** what part you played, how you could have behaved differently, and what your needs are.\n\n`;
|
||||
|
||||
if (formData.customReflectionPrompts) {
|
||||
content += `### Additional Reflection Prompts\n\n`;
|
||||
content += `${formData.customReflectionPrompts}\n\n`;
|
||||
}
|
||||
|
||||
const reflectionTiming =
|
||||
formData.reflectionPeriod || "Before any escalation";
|
||||
content += `**Reflection Timing:** ${reflectionTiming}\n\n`;
|
||||
}
|
||||
|
||||
// Direct Resolution (if enabled)
|
||||
if (data.sectionsEnabled?.directResolution !== false) {
|
||||
content += `## Direct Resolution\n\n`;
|
||||
content += `A *direct resolution* process occurs when individuals communicate their concerns and work together to resolve disputes without filing an informal or formal complaint.\n\n`;
|
||||
|
||||
content += `### Have a Conversation\n\n`;
|
||||
content += `When there is a disagreement, the involved people should first **communicate with each other** about their concerns.\n\n`;
|
||||
|
||||
content += `1. **Choose a time and place** to meet that is private and agreeable to both.\n`;
|
||||
content += `2. **Allow reasonable time** for the conversation.\n`;
|
||||
content += `3. **The point is mutual understanding**, not determining who is right or wrong. This requires patience and willingness to listen without immediately dismissing the other person's perspective.\n`;
|
||||
content += `4. **Express thoughts and feelings directly** without belittling or dismissing. Use "I" statements and active listening techniques.\n`;
|
||||
content += `5. **Communicate your wants and needs** and make offers and requests.\n`;
|
||||
content += `6. **Learn for the future.** Ask questions like, "If what I/you said or did came across that way, what can we do to prevent this from happening in the future?"\n`;
|
||||
|
||||
if (formData.documentDirectResolution) {
|
||||
content += `7. **Keep a written record** of the resolution agreed to by both parties.\n\n`;
|
||||
} else {
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Communication Channels
|
||||
if (formData.channelsList && formData.channelsList.length > 0) {
|
||||
content += `### Escalating Communication Bandwidth\n\n`;
|
||||
content += `Whenever a misunderstanding or conflict arises, **escalate the bandwidth of the channel**:\n\n`;
|
||||
formData.channelsList.forEach((channel: string, index: number) => {
|
||||
content += `${index + 1}. ${channel}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.requireDirectAttempt) {
|
||||
content += `> **Note:** Direct resolution must be attempted before escalating to assisted resolution, unless safety concerns prevent this.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Assisted Resolution
|
||||
content += `## Assisted Resolution\n\n`;
|
||||
content += `If talking things out doesn't work, you can ask a responsible contact person for help in writing.\n\n`;
|
||||
|
||||
// Responsible Contact People
|
||||
if (formData.receiversList && formData.receiversList.length > 0) {
|
||||
content += `### Initial Contact Options\n\n`;
|
||||
content += `You can report conflicts to any of the following:\n\n`;
|
||||
formData.receiversList.forEach((receiver: string) => {
|
||||
content += `- ${receiver}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Mediator Structure
|
||||
if (formData.mediatorType) {
|
||||
content += `### Mediation/Facilitation\n\n`;
|
||||
content += `**Structure:** ${formData.mediatorType}\n\n`;
|
||||
|
||||
if (formData.supportPeople) {
|
||||
content += `**Support People:** Parties may bring a trusted person for emotional support during mediation sessions.\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline
|
||||
content += `### Response Times\n\n`;
|
||||
if (formData.initialResponse) {
|
||||
content += `- **Initial Response:** ${formData.initialResponse}\n`;
|
||||
}
|
||||
if (formData.resolutionTarget) {
|
||||
content += `- **Target Resolution:** ${formData.resolutionTarget}\n\n`;
|
||||
}
|
||||
|
||||
// Formal Complaints
|
||||
content += `## Formal Complaints\n\n`;
|
||||
content += `If assisted resolution efforts do not result in an acceptable outcome within a reasonable timeframe, a *formal complaint* may be filed in writing.\n\n`;
|
||||
|
||||
// Required Elements
|
||||
if (
|
||||
formData.complaintElementsList &&
|
||||
formData.complaintElementsList.length > 0
|
||||
) {
|
||||
content += `### Written Complaint Requirements\n\n`;
|
||||
content += `The formal complaint must include:\n\n`;
|
||||
formData.complaintElementsList.forEach((element: string, index: number) => {
|
||||
content += `${index + 1}. ${element}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Formal Process Timeline
|
||||
content += `### Formal Process Timeline\n\n`;
|
||||
if (formData.formalAcknowledgmentTime) {
|
||||
content += `- **Acknowledgment:** ${formData.formalAcknowledgmentTime}\n`;
|
||||
}
|
||||
if (formData.formalReviewTime) {
|
||||
content += `- **Review Completion:** ${formData.formalReviewTime}\n\n`;
|
||||
}
|
||||
|
||||
if (formData.requireExternalAdvice) {
|
||||
content += `> **External Expertise:** For complex complaints involving multiple parties or organizational leaders, external legal advice will be sought.\n\n`;
|
||||
}
|
||||
|
||||
// Settlement Documentation
|
||||
if (formData.requireMinutesOfSettlement) {
|
||||
content += `### Reaching Agreement\n\n`;
|
||||
content += `Any resolution agreed upon must be documented in "Minutes of Settlement" signed by both parties. These agreements will be kept confidential according to our privacy standards.\n\n`;
|
||||
}
|
||||
|
||||
// Consequences and Actions
|
||||
if (formData.actionsList && formData.actionsList.length > 0) {
|
||||
content += `## Possible Outcomes\n\n`;
|
||||
content += `Depending on the situation, resolution may include:\n\n`;
|
||||
formData.actionsList.forEach((action: string) => {
|
||||
content += `- ${action}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.appealProcess) {
|
||||
content += `### Appeals Process\n\n`;
|
||||
content += `Parties may request review of decisions through our appeals process.\n\n`;
|
||||
}
|
||||
|
||||
// Documentation and Privacy
|
||||
if (data.sectionsEnabled?.documentation !== false) {
|
||||
content += `## Documentation & Privacy\n\n`;
|
||||
if (formData.docLevel) {
|
||||
content += `**Documentation Level:** ${formData.docLevel}\n\n`;
|
||||
}
|
||||
if (formData.confidentiality) {
|
||||
content += `**Confidentiality:** ${formData.confidentiality}\n\n`;
|
||||
}
|
||||
if (formData.retention) {
|
||||
content += `**Record Retention:** ${formData.retention}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// External Resources (if enabled)
|
||||
if (data.sectionsEnabled?.externalResources !== false) {
|
||||
content += `## External Resources\n\n`;
|
||||
if (formData.includeHumanRights) {
|
||||
content += `Individuals who are not satisfied with the outcome of a harassment or discrimination complaint may file a complaint with the [Canadian Human Rights Commission](https://www.chrc-ccdp.gc.ca/eng) or their provincial human rights tribunal.\n\n`;
|
||||
}
|
||||
|
||||
if (formData.additionalResources) {
|
||||
content += `### Additional Resources\n\n`;
|
||||
content += `${formData.additionalResources}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation
|
||||
content += `## Policy Management\n\n`;
|
||||
if (formData.training) {
|
||||
content += `### Training Requirements\n\n`;
|
||||
content += `${formData.training}\n\n`;
|
||||
}
|
||||
|
||||
content += `### Review and Updates\n\n`;
|
||||
if (formData.reviewSchedule) {
|
||||
content += `This policy will be reviewed ${formData.reviewSchedule.toLowerCase()}.\n\n`;
|
||||
}
|
||||
if (formData.amendments) {
|
||||
content += `**Amendment Process:** ${formData.amendments}\n\n`;
|
||||
}
|
||||
|
||||
// Acknowledgments
|
||||
if (formData.acknowledgments) {
|
||||
content += `### Acknowledgments\n\n`;
|
||||
content += `${formData.acknowledgments}\n\n`;
|
||||
}
|
||||
|
||||
return content;
|
||||
// Use the pre-generated content from the Vue component
|
||||
return data.content || "No content available";
|
||||
};
|
||||
|
||||
// Decision Framework formatting
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
<template>
|
||||
<UTooltip
|
||||
:text="definition"
|
||||
:ui="{
|
||||
background: 'bg-white dark:bg-neutral-900',
|
||||
ring: 'ring-1 ring-neutral-200 dark:ring-neutral-800',
|
||||
rounded: 'rounded-lg',
|
||||
shadow: 'shadow-lg',
|
||||
base: 'px-3 py-2 text-sm max-w-xs',
|
||||
}"
|
||||
:popper="{ arrow: true }">
|
||||
<template #text>
|
||||
<div class="space-y-2">
|
||||
<div class="font-medium">{{ term }}</div>
|
||||
<div class="text-neutral-600">{{ definition }}</div>
|
||||
<NuxtLink
|
||||
:to="`/glossary#${termId}`"
|
||||
class="text-primary-600 hover:text-primary-700 text-xs inline-flex items-center gap-1"
|
||||
@click="$emit('glossary-click')">
|
||||
<UIcon name="i-heroicons-book-open" class="w-3 h-3" />
|
||||
See full definition
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<span
|
||||
class="underline decoration-dotted decoration-neutral-400 hover:decoration-primary-500 cursor-help"
|
||||
:class="{ 'font-medium': emphasis }"
|
||||
:tabindex="0"
|
||||
:aria-describedby="`tooltip-${termId}`"
|
||||
@keydown.enter="$emit('glossary-click')"
|
||||
@keydown.space.prevent="$emit('glossary-click')">
|
||||
<slot>{{ term }}</slot>
|
||||
</span>
|
||||
</UTooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
term: string;
|
||||
termId: string;
|
||||
definition: string;
|
||||
emphasis?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
emphasis: false,
|
||||
});
|
||||
|
||||
defineEmits(["glossary-click"]);
|
||||
</script>
|
||||
|
|
@ -2,275 +2,296 @@
|
|||
<div class="mx-auto">
|
||||
<div class="relative">
|
||||
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<div class="relative bg-white dark:bg-neutral-950 border-1 border-black dark:border-neutral-400">
|
||||
<!-- Controls -->
|
||||
<div
|
||||
class="p-6 border-b-1 border-black dark:border-neutral-400 bg-neutral-100 dark:bg-neutral-950">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<UFormField label="Duration in months" class="">
|
||||
<UInputNumber
|
||||
id="duration"
|
||||
v-model="durationMonths"
|
||||
:min="3"
|
||||
:max="24"
|
||||
size="lg"
|
||||
class="w-full" />
|
||||
</UFormField>
|
||||
class="relative bg-white dark:bg-neutral-950 border-1 border-black dark:border-neutral-400">
|
||||
<!-- Controls -->
|
||||
<div
|
||||
class="p-6 border-b-1 border-black dark:border-neutral-400 bg-neutral-100 dark:bg-neutral-950">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<UFormField label="Duration in months" class="">
|
||||
<UInputNumber
|
||||
id="duration"
|
||||
v-model="durationMonths"
|
||||
:min="3"
|
||||
:max="24"
|
||||
size="lg"
|
||||
class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cost Summary -->
|
||||
<div
|
||||
class="p-6 border-b-1 border-black bg-neutral-100 dark:bg-neutral-950">
|
||||
<ul class="space-y-2">
|
||||
<!-- Two Column Layout -->
|
||||
<li class="pb-2">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<!-- Left Column: Detailed Breakdown -->
|
||||
<div
|
||||
class="text-base text-neutral-600 dark:text-neutral-200 space-y-1">
|
||||
<div class="font-bold font-display">
|
||||
Monthly payroll breakdown:
|
||||
</div>
|
||||
<div>
|
||||
Base hourly rate: {{ currency(theoreticalHourlyRate) }}/hour
|
||||
</div>
|
||||
<div class="pl-2 space-y-0.5">
|
||||
<!-- Cost Summary -->
|
||||
<div
|
||||
class="p-6 border-b-1 border-black bg-neutral-100 dark:bg-neutral-950">
|
||||
<ul class="space-y-2">
|
||||
<!-- Two Column Layout -->
|
||||
<li class="pb-2">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<!-- Left Column: Detailed Breakdown -->
|
||||
<div
|
||||
class="text-base text-neutral-600 dark:text-neutral-200 space-y-1">
|
||||
<div class="font-bold font-display">
|
||||
Monthly payroll breakdown:
|
||||
</div>
|
||||
<div>
|
||||
Base hourly rate: {{ currency(theoreticalHourlyRate) }}/hour
|
||||
</div>
|
||||
<div class="pl-2 space-y-0.5">
|
||||
<div
|
||||
v-for="member in props.members"
|
||||
:key="member.name"
|
||||
class="flex justify-between">
|
||||
<span
|
||||
>{{ member.name }} @ {{ member.hoursPerMonth }}h:</span
|
||||
>
|
||||
<span class="font-mono">{{
|
||||
currency(member.hoursPerMonth * theoreticalHourlyRate)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="member in props.members"
|
||||
:key="member.name"
|
||||
class="flex justify-between">
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-medium">
|
||||
<span>Total base pay:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(baseMonthlyPayroll)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span
|
||||
>{{ member.name }} @ {{ member.hoursPerMonth }}h:</span
|
||||
>Payroll taxes & benefits ({{
|
||||
percent(props.oncostRate)
|
||||
}}):</span
|
||||
>
|
||||
<span class="font-mono">{{
|
||||
currency(member.hoursPerMonth * theoreticalHourlyRate)
|
||||
currency(theoreticalOncosts)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-bold">
|
||||
<span>Total monthly payroll:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(baseMonthlyPayroll + theoreticalOncosts)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Complete Project Budget Estimate -->
|
||||
<div
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-medium">
|
||||
<span>Total base pay:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(baseMonthlyPayroll)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span
|
||||
>Payroll taxes & benefits ({{
|
||||
percent(props.oncostRate)
|
||||
}}):</span
|
||||
>
|
||||
<span class="font-mono">{{
|
||||
currency(theoreticalOncosts)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-bold">
|
||||
<span>Total monthly payroll:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(baseMonthlyPayroll + theoreticalOncosts)
|
||||
}}</span>
|
||||
class="text-base text-neutral-600 dark:text-neutral-200 space-y-1">
|
||||
<div class="font-bold font-display">
|
||||
Complete project budget estimate:
|
||||
</div>
|
||||
<p
|
||||
class="text-sm mb-2 italic text-neutral-500 dark:text-neutral-400">
|
||||
This uses a 1.8x multiplier, based on industry standards.
|
||||
</p>
|
||||
|
||||
<!-- Team Payroll -->
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold">Team Payroll:</span>
|
||||
<span class="font-mono font-bold">{{
|
||||
currency(projectBudget)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- External Resources -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>External resources:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(externalResources)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Freelancers, contractors, consultants, voice talent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools & Software -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Tools and software:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(toolsSoftware)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Licenses, subscriptions, cloud services, development
|
||||
tools/kits
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testing & QA -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Testing and QA:</span>
|
||||
<span class="font-mono">{{ currency(testingQA) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
User testing sessions, focus groups, QA contractors,
|
||||
playtesting, bug fixing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Marketing & Community -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Marketing and community:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(marketingCommunity)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Community building, promotional materials, launch
|
||||
preparation (minimum 10% for most funders)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Administration -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Administration:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(administration)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Legal, accounting, insurance, project-specific business
|
||||
costs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtotal -->
|
||||
<div
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-bold">
|
||||
<span>Subtotal:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(budgetSubtotal)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Contingency -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Contingency (10%):</span>
|
||||
<span class="font-mono">{{ currency(contingency) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-bold text-lg">
|
||||
<span>TOTAL PROJECT:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(totalProjectBudget)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Complete Project Budget Estimate -->
|
||||
<div
|
||||
class="text-base text-neutral-600 dark:text-neutral-200 space-y-1">
|
||||
<div class="font-bold font-display">
|
||||
Complete project budget estimate:
|
||||
</div>
|
||||
<p
|
||||
class="text-sm mb-2 italic text-neutral-500 dark:text-neutral-400">
|
||||
This uses a 1.8x multiplier, based on industry standards.
|
||||
</p>
|
||||
|
||||
<!-- Team Payroll -->
|
||||
<div class="flex justify-between">
|
||||
<span class="font-bold">Team Payroll:</span>
|
||||
<span class="font-mono font-bold">{{
|
||||
currency(projectBudget)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- External Resources -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>External resources:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(externalResources)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Freelancers, contractors, consultants, voice talent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tools & Software -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Tools and software:</span>
|
||||
<span class="font-mono">{{ currency(toolsSoftware) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Licenses, subscriptions, cloud services, development
|
||||
tools/kits
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Testing & QA -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Testing and QA:</span>
|
||||
<span class="font-mono">{{ currency(testingQA) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
User testing sessions, focus groups, QA contractors,
|
||||
playtesting, bug fixing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Marketing & Community -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Marketing and community:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(marketingCommunity)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Community building, promotional materials, launch
|
||||
preparation (minimum 10% for most funders)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Administration -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Administration:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(administration)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Legal, accounting, insurance, project-specific business
|
||||
costs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subtotal -->
|
||||
<div
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-bold">
|
||||
<span>Subtotal:</span>
|
||||
<span class="font-mono">{{ currency(budgetSubtotal) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Contingency -->
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Contingency (10%):</span>
|
||||
<span class="font-mono">{{ currency(contingency) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total -->
|
||||
<div
|
||||
class="border-t border-neutral-300 dark:border-neutral-600 pt-1 flex justify-between font-bold text-lg">
|
||||
<span>TOTAL PROJECT:</span>
|
||||
<span class="font-mono">{{
|
||||
currency(totalProjectBudget)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Break-Even Sketch -->
|
||||
<div
|
||||
class="text-black dark:text-white border-t border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-950">
|
||||
<div class="p-6 text-black dark:text-white">
|
||||
<h3 class="font-bold mb-4">Break-Even Sketch</h3>
|
||||
<!-- Inputs -->
|
||||
<div class="flex flex-wrap space-x-4 mb-6">
|
||||
<div>
|
||||
<label for="price" class="block font-bold text-sm mb-1"
|
||||
>Price per copy:</label
|
||||
>
|
||||
<UInput
|
||||
id="price"
|
||||
v-model="price"
|
||||
type="number"
|
||||
placeholder="20.00"
|
||||
size="lg"
|
||||
class="w-32"
|
||||
:ui="{ leading: 'pointer-events-none' }">
|
||||
<template #leading>
|
||||
<span class="text-sm font-mono">{{
|
||||
formatCurrency(0, {
|
||||
showSymbol: true,
|
||||
precision: 0,
|
||||
}).replace("0", "")
|
||||
}}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
<div>
|
||||
<label for="storeCut" class="block font-bold text-sm mb-1"
|
||||
>Store cut:</label
|
||||
>
|
||||
<UInput
|
||||
id="storeCut"
|
||||
v-model="storeCutInput"
|
||||
type="number"
|
||||
placeholder="30"
|
||||
size="lg"
|
||||
class="w-24"
|
||||
:ui="{ trailing: 'pointer-events-none' }">
|
||||
<template #trailing>
|
||||
<span class="text-sm font-mono">%</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
<div>
|
||||
<label for="reviewToSales" class="block font-bold text-sm mb-1"
|
||||
>Sales per review:</label
|
||||
>
|
||||
<UInputNumber
|
||||
id="reviewToSales"
|
||||
v-model="reviewToSales"
|
||||
:min="5"
|
||||
:max="100"
|
||||
size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outputs -->
|
||||
<ul class="space-y-2 mb-4">
|
||||
<li>
|
||||
At {{ currency(price) }} per copy after store fees, you'd need
|
||||
about
|
||||
<strong>{{ unitsToBreakEven.toLocaleString() }} sales</strong> to
|
||||
cover this budget.
|
||||
</li>
|
||||
<li>
|
||||
That's roughly
|
||||
<strong
|
||||
>{{ reviewsToBreakEven.toLocaleString() }} Steam reviews</strong
|
||||
>
|
||||
(≈ {{ reviewToSales }} sales per review).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="text-base text-neutral-600 dark:text-neutral-400">
|
||||
Assumes {{ percent(storeCutInput / 100) }} store fee. Taxes not
|
||||
included.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Break-Even Sketch -->
|
||||
<div
|
||||
class="text-black dark:text-white border-t border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-950">
|
||||
<div class="p-6 text-black dark:text-white">
|
||||
<h3 class="font-bold mb-4">Break-Even Sketch</h3>
|
||||
<p class="mb-4">
|
||||
There are so many ways to guess at the sales figures of published
|
||||
games. One is to use a review-to-sales ratio, formalized in the
|
||||
Boxleiter method and later discussed and refined by many others.
|
||||
<a
|
||||
href="https://app.sensortower.com/vgi/insights/article/how-to-estimate-steam-video-game-sales"
|
||||
>VGInsights</a
|
||||
>
|
||||
and <a href="https://gamediscover.co/">GameDiscoverCo</a> are good
|
||||
sources for more information on this method. Your chosen ratio
|
||||
will vary, typically between 30 and 70 reviews per sale, and is
|
||||
dependent on factors like genre, pricing, and market conditions at
|
||||
the time of release.
|
||||
</p>
|
||||
<!-- Inputs -->
|
||||
<div class="flex flex-wrap space-x-4 mb-6">
|
||||
<div>
|
||||
<label for="price" class="block font-bold text-sm mb-1"
|
||||
>Price per copy:</label
|
||||
>
|
||||
<UInput
|
||||
id="price"
|
||||
v-model="price"
|
||||
type="number"
|
||||
placeholder="20.00"
|
||||
size="lg"
|
||||
class="w-32"
|
||||
:ui="{ leading: 'pointer-events-none' }">
|
||||
<template #leading>
|
||||
<span class="text-sm font-mono">{{
|
||||
formatCurrency(0, {
|
||||
showSymbol: true,
|
||||
precision: 0,
|
||||
}).replace("0", "")
|
||||
}}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
<div>
|
||||
<label for="storeCut" class="block font-bold text-sm mb-1"
|
||||
>Store cut:</label
|
||||
>
|
||||
<UInput
|
||||
id="storeCut"
|
||||
v-model="storeCutInput"
|
||||
type="number"
|
||||
placeholder="30"
|
||||
size="lg"
|
||||
class="w-24"
|
||||
:ui="{ trailing: 'pointer-events-none' }">
|
||||
<template #trailing>
|
||||
<span class="text-sm font-mono">%</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
<div>
|
||||
<label for="reviewToSales" class="block font-bold text-sm mb-1"
|
||||
>Sales per review:</label
|
||||
>
|
||||
<UInputNumber
|
||||
id="reviewToSales"
|
||||
v-model="reviewToSales"
|
||||
:min="5"
|
||||
:max="100"
|
||||
size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outputs -->
|
||||
<ul class="space-y-2 mb-4">
|
||||
<li>
|
||||
At {{ currency(price) }} per copy after store fees, you'd need
|
||||
about
|
||||
<strong class="bg-yellow-200 dark:text-black"
|
||||
>{{ unitsToBreakEven.toLocaleString() }} sales</strong
|
||||
>
|
||||
to cover this budget.
|
||||
</li>
|
||||
<li>
|
||||
That's roughly
|
||||
<strong class="bg-yellow-200 dark:text-black"
|
||||
>{{ reviewsToBreakEven.toLocaleString() }} Steam
|
||||
reviews</strong
|
||||
>
|
||||
(≈ {{ reviewToSales }} sales per review).
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="text-base text-neutral-600 dark:text-neutral-400">
|
||||
Taxes not included.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,24 +35,7 @@
|
|||
This hourly rate applies to all paid work in your co-op
|
||||
</p>
|
||||
<div class="flex gap-4 items-start">
|
||||
<!-- Currency Selection -->
|
||||
<UFormField label="Currency" class="w-1/2">
|
||||
<USelect
|
||||
v-model="selectedCurrency"
|
||||
:items="currencySelectOptions"
|
||||
placeholder="Select currency"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
@update:model-value="updateCurrency">
|
||||
<template #leading>
|
||||
<span class="text-lg">{{
|
||||
getCurrencySymbol(selectedCurrency)
|
||||
}}</span>
|
||||
</template>
|
||||
</USelect>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Hourly Rate" class="w-1/2">
|
||||
<UFormField label="Hourly Rate" class="w-full">
|
||||
<UInput
|
||||
v-model="wageText"
|
||||
type="text"
|
||||
|
|
@ -62,7 +45,7 @@
|
|||
@update:model-value="validateAndSaveWage">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500 text-xl">{{
|
||||
getCurrencySymbol(selectedCurrency)
|
||||
getCurrencySymbol(selectedCurrency.value)
|
||||
}}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
|
|
@ -73,7 +56,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { currencyOptions, getCurrencySymbol } from "~/utils/currency";
|
||||
import { getCurrencySymbol } from "~/utils/currency";
|
||||
|
||||
const emit = defineEmits<{
|
||||
"save-status": [status: "saving" | "saved" | "error"];
|
||||
|
|
@ -87,7 +70,7 @@ const store = useCoopBuilderStore();
|
|||
const selectedPolicy = ref(coop.policy.value?.relationship || "equal-pay");
|
||||
const roleBands = ref(coop.policy.value?.roleBands || {});
|
||||
const wageText = ref(String(store.equalHourlyWage || ""));
|
||||
const selectedCurrency = ref(coop.currency.value || "EUR");
|
||||
const selectedCurrency = computed(() => coop.currency.value || "EUR");
|
||||
|
||||
function parseNumberInput(val: unknown): number {
|
||||
if (typeof val === "number") return val;
|
||||
|
|
@ -115,23 +98,11 @@ const policyOptions = [
|
|||
},
|
||||
];
|
||||
|
||||
// Currency options for USelect (simplified format)
|
||||
const currencySelectOptions = computed(() =>
|
||||
currencyOptions.map((currency) => ({
|
||||
label: `${currency.name} (${currency.code})`,
|
||||
value: currency.code,
|
||||
}))
|
||||
);
|
||||
|
||||
// Already initialized above with store values
|
||||
|
||||
// Removed uniqueRoles computed - no longer needed with simplified policies
|
||||
|
||||
function updateCurrency(value: string) {
|
||||
selectedCurrency.value = value;
|
||||
coop.setCurrency(value);
|
||||
emit("save-status", "saved");
|
||||
}
|
||||
|
||||
function updatePolicy(value: string) {
|
||||
coop.setPolicy(value as "equal-pay" | "needs-weighted" | "hours-weighted");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
<template>
|
||||
<div class="mb-12">
|
||||
<div class="">
|
||||
<div class="w-full mx-auto">
|
||||
<nav
|
||||
class="flex flex-wrap items-center space-x-1 font-mono uppercase justify-self-center"
|
||||
>
|
||||
class="flex flex-wrap items-center space-x-1 font-mono uppercase justify-self-center">
|
||||
<NuxtLink
|
||||
v-for="wizard in templateWizards"
|
||||
:key="wizard.id"
|
||||
|
|
@ -13,8 +12,7 @@
|
|||
isActive(wizard.path)
|
||||
? 'bg-black text-white dark:bg-white dark:text-black no-underline'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
">
|
||||
{{ wizard.name }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
|
@ -29,22 +27,22 @@ const templateWizards = [
|
|||
{
|
||||
id: "membership-agreement",
|
||||
name: "Membership Agreement",
|
||||
path: "/templates/membership-agreement",
|
||||
path: "/tools/templates/membership-agreement",
|
||||
},
|
||||
{
|
||||
id: "conflict-resolution-framework",
|
||||
name: "Conflict Resolution",
|
||||
path: "/templates/conflict-resolution-framework",
|
||||
path: "/tools/templates/conflict-resolution-framework",
|
||||
},
|
||||
{
|
||||
id: "tech-charter",
|
||||
name: "Tech Charter",
|
||||
path: "/templates/tech-charter",
|
||||
path: "/tools/templates/tech-charter",
|
||||
},
|
||||
{
|
||||
id: "decision-framework",
|
||||
name: "Decision Framework",
|
||||
path: "/templates/decision-framework",
|
||||
path: "/tools/templates/decision-framework",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const useSetupState = () => {
|
|||
}
|
||||
|
||||
const goToMemberManagement = () => {
|
||||
navigateTo('/settings#members')
|
||||
navigateTo('/coop-planner')
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Glossary</h2>
|
||||
<UInput
|
||||
v-model="searchQuery"
|
||||
placeholder="Search definitions..."
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
class="w-64"
|
||||
:ui="{ icon: { trailing: { pointer: '' } } }" />
|
||||
</div>
|
||||
|
||||
<UCard>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div
|
||||
v-for="letter in alphabeticalGroups"
|
||||
:key="letter.letter"
|
||||
class="space-y-4">
|
||||
<h3
|
||||
class="text-lg font-semibold text-primary-600 border-b border-neutral-200 pb-2">
|
||||
{{ letter.letter }}
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="term in letter.terms"
|
||||
:key="term.id"
|
||||
:id="term.id"
|
||||
class="scroll-mt-20">
|
||||
<dt class="font-medium text-neutral-900 mb-1">
|
||||
{{ term.term }}
|
||||
</dt>
|
||||
<dd class="text-neutral-600 text-sm leading-relaxed">
|
||||
{{ term.definition }}
|
||||
<span
|
||||
v-if="term.example"
|
||||
class="block mt-1 text-neutral-500 italic">
|
||||
Example: {{ term.example }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const searchQuery = ref("");
|
||||
|
||||
// Glossary terms based on CLAUDE.md definitions
|
||||
const glossaryTerms = ref([
|
||||
{
|
||||
id: "budget",
|
||||
term: "Budget",
|
||||
definition:
|
||||
"Month-by-month plan of money in and money out. Not exact dates.",
|
||||
example: "January budget shows €12,000 revenue and €9,900 costs",
|
||||
},
|
||||
{
|
||||
id: "concentration",
|
||||
term: "Concentration",
|
||||
definition:
|
||||
"Dependence on few revenue sources. UI shows top source percentage.",
|
||||
example: "If 65% comes from one client, concentration is high risk",
|
||||
},
|
||||
{
|
||||
id: "coverage",
|
||||
term: "Coverage",
|
||||
definition: "Funded paid hours divided by target hours across all members.",
|
||||
example: "208 funded hours ÷ 320 target hours = 65% coverage",
|
||||
},
|
||||
{
|
||||
id: "deferred-pay",
|
||||
term: "Deferred Pay",
|
||||
definition: "Unpaid hours the co-op owes later at the same wage.",
|
||||
example: "Alex worked 40 hours unpaid in January, owed €800 later",
|
||||
},
|
||||
{
|
||||
id: "equal-wage",
|
||||
term: "Equal Wage",
|
||||
definition: "Same hourly rate for all paid hours.",
|
||||
example: "Everyone gets €20/hour for paid work, regardless of role",
|
||||
},
|
||||
{
|
||||
id: "minimum-cash-cushion",
|
||||
term: "Minimum Cash Cushion",
|
||||
definition: "Lowest operating balance we agree not to breach.",
|
||||
example: "€3,000 minimum means never go below this amount",
|
||||
},
|
||||
{
|
||||
id: "on-costs",
|
||||
term: "On-costs",
|
||||
definition: "Employer taxes, benefits, and payroll fees on top of wages.",
|
||||
example: "€6,400 wages + 25% on-costs = €8,000 total payroll",
|
||||
},
|
||||
{
|
||||
id: "patronage",
|
||||
term: "Patronage",
|
||||
definition: "A way to share surplus based on recorded contributions.",
|
||||
example: "Extra profits shared based on hours worked or value added",
|
||||
},
|
||||
{
|
||||
id: "payout-delay",
|
||||
term: "Payout Delay",
|
||||
definition: "Time between earning money and receiving it.",
|
||||
example: "Platform sales have 14-day delay, grants have 45-day delay",
|
||||
},
|
||||
{
|
||||
id: "restricted-funds",
|
||||
term: "Restricted Funds",
|
||||
definition: "Money that can only be used for approved purposes.",
|
||||
example: "Grant money restricted to development costs only",
|
||||
},
|
||||
{
|
||||
id: "revenue-share",
|
||||
term: "Revenue Share",
|
||||
definition: "Percentage of earnings paid to platform or partner.",
|
||||
example: "App store takes 30% revenue share on sales",
|
||||
},
|
||||
{
|
||||
id: "runway",
|
||||
term: "Runway",
|
||||
definition:
|
||||
"Months until cash plus savings run out under the current plan.",
|
||||
example: "€13,000 available ÷ €4,600 monthly burn = 2.8 months runway",
|
||||
},
|
||||
{
|
||||
id: "savings-target",
|
||||
term: "Savings Target",
|
||||
definition: "Money held for stability. Aim to reach before ramping hours.",
|
||||
example: "3 months target = €13,800 for 3 months of expenses",
|
||||
},
|
||||
{
|
||||
id: "surplus",
|
||||
term: "Surplus",
|
||||
definition: "Money left over after all costs are paid.",
|
||||
example: "€12,000 revenue - €9,900 costs = €2,100 surplus",
|
||||
},
|
||||
]);
|
||||
|
||||
// Filter terms based on search
|
||||
const filteredTerms = computed(() => {
|
||||
if (!searchQuery.value) return glossaryTerms.value;
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return glossaryTerms.value.filter(
|
||||
(term) =>
|
||||
term.term.toLowerCase().includes(query) ||
|
||||
term.definition.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// Group terms alphabetically
|
||||
const alphabeticalGroups = computed(() => {
|
||||
const groups = new Map();
|
||||
|
||||
filteredTerms.value
|
||||
.sort((a, b) => a.term.localeCompare(b.term))
|
||||
.forEach((term) => {
|
||||
const letter = term.term[0].toUpperCase();
|
||||
if (!groups.has(letter)) {
|
||||
groups.set(letter, { letter, terms: [] });
|
||||
}
|
||||
groups.get(letter).terms.push(term);
|
||||
});
|
||||
|
||||
return Array.from(groups.values()).sort((a, b) =>
|
||||
a.letter.localeCompare(b.letter)
|
||||
);
|
||||
});
|
||||
|
||||
// SEO and accessibility
|
||||
useSeoMeta({
|
||||
title: "Glossary - Plain English Definitions",
|
||||
description: "Plain English definitions of co-op financial terms. No jargon.",
|
||||
});
|
||||
</script>
|
||||
191
pages/help.vue
191
pages/help.vue
|
|
@ -1,191 +0,0 @@
|
|||
<template>
|
||||
<div class="min-h-screen bg-neutral-50 py-8">
|
||||
<div class="container mx-auto max-w-4xl px-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-4xl font-bold mb-4">Budget Planning Help</h1>
|
||||
<p class="text-xl text-neutral-600">Learn how to build a sustainable financial plan for your co-op or studio</p>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="mb-8">
|
||||
<div class="border-4 border-black bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="p-4">
|
||||
<h2 class="text-xl font-bold mb-4">Quick Navigation</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<a href="#revenue-diversification" class="block p-3 bg-blue-50 border-2 border-blue-200 rounded hover:bg-blue-100 transition-colors">
|
||||
<span class="font-semibold">Revenue Diversification</span>
|
||||
<p class="text-sm text-neutral-600">How to develop multiple income streams</p>
|
||||
</a>
|
||||
<a href="#budget-categories" class="block p-3 bg-green-50 border-2 border-green-200 rounded hover:bg-green-100 transition-colors">
|
||||
<span class="font-semibold">Budget Categories</span>
|
||||
<p class="text-sm text-neutral-600">Understanding revenue and expense types</p>
|
||||
</a>
|
||||
<a href="#planning-tips" class="block p-3 bg-yellow-50 border-2 border-yellow-200 rounded hover:bg-yellow-100 transition-colors">
|
||||
<span class="font-semibold">Planning Tips</span>
|
||||
<p class="text-sm text-neutral-600">Best practices for financial planning</p>
|
||||
</a>
|
||||
<a href="#getting-started" class="block p-3 bg-purple-50 border-2 border-purple-200 rounded hover:bg-purple-100 transition-colors">
|
||||
<span class="font-semibold">Getting Started</span>
|
||||
<p class="text-sm text-neutral-600">Step-by-step setup guide</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Sections -->
|
||||
<div class="space-y-8">
|
||||
|
||||
<!-- Revenue Diversification -->
|
||||
<section id="revenue-diversification" class="border-4 border-black bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="bg-blue-500 text-white px-6 py-4">
|
||||
<h2 class="text-2xl font-bold">Revenue Diversification</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="prose max-w-none">
|
||||
<p class="text-lg mb-4">
|
||||
Building multiple revenue streams reduces risk and creates more stable income for your co-op.
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">How to Develop Games & Products</h3>
|
||||
<p class="mb-4">
|
||||
[Placeholder text - Add your content here about developing games and product revenue streams]
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">How to Develop Services & Contracts</h3>
|
||||
<p class="mb-4">
|
||||
[Placeholder text - Add your content here about developing service-based revenue streams]
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">How to Secure Grants & Funding</h3>
|
||||
<p class="mb-4">
|
||||
[Placeholder text - Add your content here about finding and securing grants]
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">Building Community Support</h3>
|
||||
<p class="mb-4">
|
||||
[Placeholder text - Add your content here about building community-supported revenue]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Budget Categories -->
|
||||
<section id="budget-categories" class="border-4 border-black bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="bg-green-500 text-white px-6 py-4">
|
||||
<h2 class="text-2xl font-bold">Budget Categories</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="prose max-w-none">
|
||||
<h3 class="text-xl font-semibold mb-3">Revenue Categories</h3>
|
||||
<ul class="mb-6 space-y-2">
|
||||
<li><strong>Games & Products:</strong> [Add description]</li>
|
||||
<li><strong>Services & Contracts:</strong> [Add description]</li>
|
||||
<li><strong>Grants & Funding:</strong> [Add description]</li>
|
||||
<li><strong>Community Support:</strong> [Add description]</li>
|
||||
<li><strong>Partnerships:</strong> [Add description]</li>
|
||||
<li><strong>Investment Income:</strong> [Add description]</li>
|
||||
<li><strong>In-Kind Contributions:</strong> [Add description]</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">Expense Categories</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><strong>Salaries & Benefits:</strong> [Add description]</li>
|
||||
<li><strong>Development Costs:</strong> [Add description]</li>
|
||||
<li><strong>Equipment & Technology:</strong> [Add description]</li>
|
||||
<li><strong>Marketing & Outreach:</strong> [Add description]</li>
|
||||
<li><strong>Office & Operations:</strong> [Add description]</li>
|
||||
<li><strong>Legal & Professional:</strong> [Add description]</li>
|
||||
<li><strong>Other Expenses:</strong> [Add description]</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Planning Tips -->
|
||||
<section id="planning-tips" class="border-4 border-black bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="bg-yellow-500 text-white px-6 py-4">
|
||||
<h2 class="text-2xl font-bold">Planning Tips</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="prose max-w-none">
|
||||
<h3 class="text-xl font-semibold mb-3">Concentration Risk</h3>
|
||||
<p class="mb-4">
|
||||
[Add content about managing concentration risk and why diversification matters]
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">Monthly vs Annual Planning</h3>
|
||||
<p class="mb-4">
|
||||
[Add content about balancing monthly budgets with annual strategic planning]
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">Setting Realistic Goals</h3>
|
||||
<p class="mb-4">
|
||||
[Add content about setting achievable revenue and expense targets]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Getting Started -->
|
||||
<section id="getting-started" class="border-4 border-black bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="bg-purple-500 text-white px-6 py-4">
|
||||
<h2 class="text-2xl font-bold">Getting Started</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="prose max-w-none">
|
||||
<h3 class="text-xl font-semibold mb-3">Step 1: Set Up Your Basic Budget</h3>
|
||||
<p class="mb-4">
|
||||
[Add step-by-step instructions for initial budget setup]
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">Step 2: Plan Your Revenue Streams</h3>
|
||||
<p class="mb-4">
|
||||
[Add guidance on planning initial revenue streams]
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mb-3">Step 3: Track and Adjust</h3>
|
||||
<p class="mb-4">
|
||||
[Add content about ongoing budget management]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Back to Budget -->
|
||||
<div class="mt-8 text-center">
|
||||
<NuxtLink
|
||||
to="/budget"
|
||||
class="inline-block px-6 py-3 bg-black text-white font-bold border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] transition-all"
|
||||
>
|
||||
Back to Budget
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Page meta
|
||||
useHead({
|
||||
title: 'Budget Planning Help - Urgent Tools',
|
||||
meta: [
|
||||
{ name: 'description', content: 'Learn how to build sustainable financial plans and develop diverse revenue streams for your co-op or studio.' }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Smooth scrolling for anchor links */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Ensure anchor links account for any fixed headers */
|
||||
section[id] {
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
||||
</style>
|
||||
760
pages/index.vue
760
pages/index.vue
|
|
@ -1,382 +1,432 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold">Compensation</h2>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs font-mono">
|
||||
Runway: {{ Math.round(metrics.runway) }}mo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="onExport"
|
||||
class="px-4 py-2 border-2 border-black bg-white font-bold uppercase text-sm hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
Export JSON
|
||||
</button>
|
||||
<button
|
||||
@click="onImport"
|
||||
class="px-4 py-2 border-2 border-black bg-white font-bold uppercase text-sm hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
Import JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-screen">
|
||||
<!-- Noise overlay and effects -->
|
||||
<div class="noise" />
|
||||
|
||||
<!-- Key Metrics Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<CoverageMeter
|
||||
:funded-paid-hours="Math.round(metrics.totalTargetHours * 0.65)"
|
||||
:target-hours="metrics.totalTargetHours"
|
||||
description="Funded hours vs target capacity across all members." />
|
||||
|
||||
<ReserveMeter
|
||||
:current-savings="savingsProgress.current"
|
||||
:savings-target-months="savingsProgress.targetMonths"
|
||||
:monthly-burn="getMonthlyBurn()"
|
||||
:description="`${savingsProgress.progressPct.toFixed(0)}% of savings target reached. ${savingsProgress.gap > 0 ? 'Gap: ' + $format.currency(savingsProgress.gap) : 'Target achieved!'}`" />
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Components with Wizard Styling -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Needs Coverage Bars -->
|
||||
<div class="border-2 border-black bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="border-b-2 border-black p-4">
|
||||
<h3 class="text-lg font-bold uppercase">Member Coverage</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<NeedsCoverageBars />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestone-Runway Overlay -->
|
||||
<div class="border-2 border-black bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="border-b-2 border-black p-4">
|
||||
<h3 class="text-lg font-bold uppercase">Runway vs Milestones</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<MilestoneRunwayOverlay />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts Section with Wizard Styling -->
|
||||
<div class="border-2 border-black bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="border-b-2 border-black p-4">
|
||||
<h3 class="text-lg font-bold uppercase">Alerts</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4">
|
||||
<!-- Concentration Risk Alert -->
|
||||
<div
|
||||
v-if="topSourcePct > 50"
|
||||
class="border-2 border-red-600 bg-red-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-red-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Revenue Concentration Risk</h4>
|
||||
<p class="text-sm mb-2">{{ topStreamName }} = {{ topSourcePct }}% of total → consider balancing</p>
|
||||
<button
|
||||
@click="handleAlertNavigation('/dashboard', 'concentration')"
|
||||
class="text-sm underline font-bold">
|
||||
VIEW DETAILS
|
||||
</button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<section class="coming-soon">
|
||||
<header>
|
||||
<div class="glitch-container">
|
||||
<div class="logo">coop.love</div>
|
||||
</div>
|
||||
</header>
|
||||
<p>
|
||||
A new space to celebrate values-driven cooperative game development
|
||||
around the world.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
Join our list to be first to know when we launch!
|
||||
</p>
|
||||
|
||||
<form
|
||||
action="https://buttondown.com/api/emails/embed-subscribe/coop.love"
|
||||
method="post"
|
||||
target="popupwindow"
|
||||
onsubmit="window.open('https://buttondown.com/coop.love', 'popupwindow')"
|
||||
class="embeddable-buttondown-form">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="bd-email"
|
||||
placeholder="your@email.com" />
|
||||
|
||||
<input type="submit" value="Subscribe" />
|
||||
</form>
|
||||
|
||||
<div class="powered-by">
|
||||
<p>A collaborative initiative powered by:</p>
|
||||
<p class="org-names">
|
||||
<a href="https://babyghosts.fund">Baby Ghosts</a> +
|
||||
<a href="https://gammaspace.ca">Gamma Space</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Cushion Breach Alert -->
|
||||
<div
|
||||
v-if="alerts.cushionBreach"
|
||||
class="border-2 border-orange-600 bg-orange-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-orange-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Cash Cushion Breach Forecast</h4>
|
||||
<p class="text-sm mb-2">Projected to breach minimum cushion in week {{ cushionForecast.firstBreachWeek || 'unknown' }}</p>
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
@click="handleAlertNavigation('/cash', 'breach-forecast')"
|
||||
class="text-sm underline font-bold">
|
||||
VIEW CALENDAR
|
||||
</button>
|
||||
<button
|
||||
@click="handleAlertNavigation('/budget', 'expenses')"
|
||||
class="text-sm underline font-bold">
|
||||
ADJUST BUDGET
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tools access -->
|
||||
<div class="tools-access">
|
||||
<NuxtLink to="/tools" class="tools-link">
|
||||
Access Urgent Tools →
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Savings Below Target Alert -->
|
||||
<div
|
||||
v-if="alerts.savingsBelowTarget"
|
||||
class="border-2 border-yellow-600 bg-yellow-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-yellow-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Savings Below Target</h4>
|
||||
<p class="text-sm mb-2">{{ savingsProgress.progressPct.toFixed(0) }}% of target reached. Build savings before increasing paid hours.</p>
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
@click="handleAlertNavigation('/budget', 'savings')"
|
||||
class="text-sm underline font-bold">
|
||||
VIEW PROGRESS
|
||||
</button>
|
||||
<button
|
||||
@click="handleAlertNavigation('/coop-builder', 'policies')"
|
||||
class="text-sm underline font-bold">
|
||||
ADJUST POLICIES
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Over-Deferred Member Alert -->
|
||||
<div
|
||||
v-if="deferredAlert.show"
|
||||
class="border-2 border-purple-600 bg-purple-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-purple-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Member Over-Deferred</h4>
|
||||
<p class="text-sm mb-2">{{ deferredAlert.description }}</p>
|
||||
<button
|
||||
@click="handleAlertNavigation('/coop-builder', 'members')"
|
||||
class="text-sm underline font-bold">
|
||||
REVIEW MEMBERS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success message when no alerts -->
|
||||
<div v-if="!alerts.cushionBreach && !alerts.savingsBelowTarget && topSourcePct <= 50 && !deferredAlert.show"
|
||||
class="text-center py-8">
|
||||
<span class="text-4xl font-bold">✓</span>
|
||||
<p class="font-bold uppercase mt-2">All systems looking good!</p>
|
||||
<p class="text-sm mt-1">No critical alerts at this time.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Quick Actions with Wizard Styling -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
@click="navigateTo('/cash-flow')"
|
||||
class="border-2 border-black bg-white p-4 text-left hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
<div class="font-bold uppercase mb-1">Cash Flow Analysis</div>
|
||||
<div class="text-sm">Detailed runway & one-time events</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="navigateTo('/budget')"
|
||||
class="border-2 border-black bg-white p-4 text-left hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
<div class="font-bold uppercase mb-1">Budget Planning</div>
|
||||
<div class="text-sm">Manage expenses & savings</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Dashboard page
|
||||
const { $format } = useNuxtApp();
|
||||
<script setup>
|
||||
useHead({
|
||||
title: 'coop.love ❤️ Cooperative Game Development',
|
||||
meta: [
|
||||
{ name: 'description', content: 'A new space to celebrate values-driven cooperative game development around the world.' }
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
// Use real store data instead of fixtures
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
const streamsStore = useStreamsStore();
|
||||
const budgetStore = useBudgetStore();
|
||||
const cashStore = useCashStore();
|
||||
<style scoped>
|
||||
/* Import Google Fonts for Courier Prime */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap");
|
||||
|
||||
// Runway composable with operating mode integration
|
||||
const { getDualModeRunway, getMonthlyBurn } = useRunway();
|
||||
|
||||
// Cushion forecast and savings progress
|
||||
const { savingsProgress, cushionForecast, alerts } = useCushionForecast();
|
||||
|
||||
|
||||
// Calculate metrics from real store data
|
||||
const metrics = computed(() => {
|
||||
const totalTargetHours = membersStore.members.reduce(
|
||||
(sum, member) => sum + (member.capacity?.targetHours || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const totalTargetRevenue = streamsStore.streams.reduce(
|
||||
(sum, stream) => sum + (stream.targetMonthlyAmount || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const totalOverheadCosts = budgetStore.overheadCosts.reduce(
|
||||
(sum, cost) => sum + (cost.amount || 0),
|
||||
0
|
||||
);
|
||||
|
||||
// Use integrated runway calculations that respect operating mode
|
||||
const currentMode = 'target'; // Always target mode now
|
||||
const monthlyBurn = getMonthlyBurn(currentMode);
|
||||
|
||||
// Use actual cash store values with fallback
|
||||
const cash = cashStore.currentCash || 50000;
|
||||
const savings = cashStore.currentSavings || 15000;
|
||||
const totalLiquid = cash + savings;
|
||||
|
||||
// Get dual-mode runway data
|
||||
const runwayData = getDualModeRunway(cash, savings);
|
||||
const runway = currentMode === 'target' ? runwayData.target : runwayData.minimum;
|
||||
|
||||
return {
|
||||
totalTargetHours,
|
||||
totalTargetRevenue,
|
||||
monthlyPayroll: runwayData.minBurn, // Use actual calculated payroll
|
||||
monthlyBurn,
|
||||
runway,
|
||||
runwayData, // Include dual-mode data
|
||||
finances: {
|
||||
currentBalances: {
|
||||
cash: cashStore.currentCash,
|
||||
savings: cashStore.currentSavings,
|
||||
totalLiquid,
|
||||
},
|
||||
policies: {
|
||||
equalHourlyWage: policiesStore.equalHourlyWage,
|
||||
payrollOncostPct: policiesStore.payrollOncostPct,
|
||||
savingsTargetMonths: policiesStore.savingsTargetMonths,
|
||||
minCashCushionAmount: policiesStore.minCashCushionAmount,
|
||||
},
|
||||
deferredLiabilities: {
|
||||
totalDeferred: membersStore.members.reduce(
|
||||
(sum, m) =>
|
||||
sum + (m.deferredHours || 0) * policiesStore.equalHourlyWage,
|
||||
0
|
||||
),
|
||||
},
|
||||
surplus: Math.max(0, totalTargetRevenue - monthlyBurn),
|
||||
savingsGap: Math.max(
|
||||
0,
|
||||
policiesStore.savingsTargetMonths * monthlyBurn -
|
||||
cashStore.currentSavings
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate concentration metrics
|
||||
const topSourcePct = computed(() => {
|
||||
if (streamsStore.streams.length === 0) return 0;
|
||||
const amounts = streamsStore.streams.map((s) => s.targetMonthlyAmount || 0);
|
||||
const total = amounts.reduce((sum, amt) => sum + amt, 0);
|
||||
return total > 0 ? Math.round((Math.max(...amounts) / total) * 100) : 0;
|
||||
});
|
||||
|
||||
const topStreamName = computed(() => {
|
||||
if (streamsStore.streams.length === 0) return 'No streams';
|
||||
const amounts = streamsStore.streams.map((s) => s.targetMonthlyAmount || 0);
|
||||
const maxAmount = Math.max(...amounts);
|
||||
const topStream = streamsStore.streams.find(s => (s.targetMonthlyAmount || 0) === maxAmount);
|
||||
return topStream?.name || 'Unknown stream';
|
||||
});
|
||||
|
||||
const concentrationStatus = computed(() => {
|
||||
if (topSourcePct.value > 50) return "red";
|
||||
if (topSourcePct.value > 35) return "yellow";
|
||||
return "green";
|
||||
});
|
||||
|
||||
const concentrationColor = computed(() => {
|
||||
if (topSourcePct.value > 50) return "text-red-600";
|
||||
if (topSourcePct.value > 35) return "text-yellow-600";
|
||||
return "text-green-600";
|
||||
});
|
||||
|
||||
function getRunwayColor(months: number): string {
|
||||
if (months >= 6) return 'text-green-600'
|
||||
if (months >= 3) return 'text-yellow-600'
|
||||
return 'text-red-600'
|
||||
:root {
|
||||
--accent-1: #333333;
|
||||
--accent-2: #555555;
|
||||
--accent-3: #777777;
|
||||
--light: #f5f5f5;
|
||||
--lighter: #ffffff;
|
||||
--text: #0a0a0a;
|
||||
--scanline: rgba(0, 0, 0, 0.05);
|
||||
--font-stack: "Courier Prime", "Consolas", "Menlo", "Monaco",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// Cash breach description
|
||||
const cashBreachDescription = computed(() => {
|
||||
// Check cash store for first breach week from projections
|
||||
const breachWeek = cashStore.weeklyProjections.find(
|
||||
(week) => week.breachesCushion
|
||||
.min-h-screen {
|
||||
background-color: var(--lighter);
|
||||
background-image: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(245, 245, 245, 0.6) 0%,
|
||||
rgba(250, 250, 250, 0.8) 70%,
|
||||
var(--lighter) 100%
|
||||
);
|
||||
if (breachWeek) {
|
||||
return `Week ${breachWeek.number} would drop below your minimum cushion.`;
|
||||
color: var(--text);
|
||||
font-family: var(--font-stack);
|
||||
line-height: 1.6;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.min-h-screen::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
rgba(0, 0, 0, 0) 50%,
|
||||
rgba(0, 0, 0, 0.08) 50%
|
||||
),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0.03),
|
||||
rgba(0, 0, 0, 0.05),
|
||||
rgba(0, 0, 0, 0.03)
|
||||
);
|
||||
background-size: 100% 3px, 3px 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.noise {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 1000 1000' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='5' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
opacity: 0.08;
|
||||
pointer-events: none;
|
||||
z-index: 9998;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 2rem 1rem;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--accent-3);
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.glitch-container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: var(--font-stack);
|
||||
font-size: clamp(3rem, 10vw, 7rem);
|
||||
font-weight: 400;
|
||||
color: var(--text);
|
||||
text-shadow: 0 0 5px rgba(214, 138, 229, 0.7),
|
||||
0 0 10px rgba(84, 194, 243, 0.7);
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
animation: flicker 4s infinite alternate;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo::before,
|
||||
.logo::after {
|
||||
content: "coop.love";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo::before {
|
||||
color: var(--accent-2);
|
||||
opacity: 0.5;
|
||||
animation: glitch-1 2s infinite alternate-reverse;
|
||||
}
|
||||
|
||||
.logo::after {
|
||||
color: var(--accent-3);
|
||||
opacity: 0.5;
|
||||
animation: glitch-2 3.5s infinite alternate-reverse;
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0%,
|
||||
19.999%,
|
||||
22%,
|
||||
62.999%,
|
||||
64%,
|
||||
64.999%,
|
||||
70%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
return "No cushion breach currently projected.";
|
||||
});
|
||||
20%,
|
||||
21.999%,
|
||||
63%,
|
||||
63.999%,
|
||||
65%,
|
||||
69.999% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
const onExport = () => {
|
||||
const data = exportAll();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "urgent-tools.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
@keyframes glitch-1 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
20% {
|
||||
transform: translate(-2px, 2px);
|
||||
}
|
||||
40% {
|
||||
transform: translate(-2px, -2px);
|
||||
}
|
||||
60% {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
80% {
|
||||
transform: translate(2px, -2px);
|
||||
}
|
||||
}
|
||||
|
||||
const onImport = async () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "application/json";
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
importAll(JSON.parse(text));
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
@keyframes glitch-2 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
25% {
|
||||
transform: translate(2px, -2px);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-2px, 2px);
|
||||
}
|
||||
75% {
|
||||
transform: translate(2px, 2px);
|
||||
}
|
||||
}
|
||||
|
||||
const { exportAll, importAll } = useFixtureIO();
|
||||
.coming-soon {
|
||||
text-align: center;
|
||||
margin: 4rem 0;
|
||||
padding: 3rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.embeddable-buttondown-form {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
// Deferred alert logic
|
||||
const deferredAlert = computed(() => {
|
||||
const maxDeferredRatio = 1.5; // From CLAUDE.md - flag if >1.5× monthly payroll
|
||||
const monthlyPayrollCost = getMonthlyBurn() * 0.7; // Estimate payroll as 70% of burn
|
||||
const totalDeferred = membersStore.members.reduce(
|
||||
(sum, m) => sum + (m.deferredHours || 0) * policiesStore.equalHourlyWage,
|
||||
0
|
||||
.embeddable-buttondown-form label {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
color: var(--accent-1);
|
||||
}
|
||||
|
||||
.embeddable-buttondown-form input[type="email"] {
|
||||
background: var(--lighter);
|
||||
border: 1px solid var(--accent-2);
|
||||
padding: 0.8rem;
|
||||
width: 100%;
|
||||
font-family: var(--font-stack);
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.embeddable-buttondown-form input[type="email"]:focus {
|
||||
border-color: var(--accent-1);
|
||||
outline: none;
|
||||
box-shadow: 0 0 5px rgba(51, 51, 51, 0.3);
|
||||
}
|
||||
|
||||
.embeddable-buttondown-form input[type="submit"] {
|
||||
background: var(--lighter);
|
||||
color: var(--accent-1);
|
||||
border: 1px solid var(--accent-1);
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-stack);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.embeddable-buttondown-form input[type="submit"]:hover {
|
||||
background: var(--accent-1);
|
||||
color: var(--lighter);
|
||||
text-shadow: none;
|
||||
box-shadow: 0 0 10px rgba(51, 51, 51, 0.3);
|
||||
}
|
||||
|
||||
.embeddable-buttondown-form input[type="submit"]:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.7s ease;
|
||||
}
|
||||
|
||||
const deferredRatio = monthlyPayrollCost > 0 ? totalDeferred / monthlyPayrollCost : 0;
|
||||
const show = deferredRatio > maxDeferredRatio;
|
||||
.embeddable-buttondown-form input[type="submit"]:hover:before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
const overDeferredMembers = membersStore.members.filter(m => {
|
||||
const memberDeferred = (m.deferredHours || 0) * policiesStore.equalHourlyWage;
|
||||
const memberMonthlyPay = m.monthlyPayPlanned || 0;
|
||||
return memberDeferred > memberMonthlyPay * 2; // Member has >2 months of pay deferred
|
||||
});
|
||||
.powered-by {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 2rem;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
description: show
|
||||
? `${overDeferredMembers.length} member(s) over deferred cap. Total: ${(deferredRatio * 100).toFixed(0)}% of monthly payroll.`
|
||||
: ''
|
||||
};
|
||||
});
|
||||
.powered-by p {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// Alert navigation with context
|
||||
function handleAlertNavigation(path: string, section?: string) {
|
||||
// Store alert context for target page to highlight relevant section
|
||||
if (section) {
|
||||
localStorage.setItem('urgent-tools-alert-context', JSON.stringify({ section, timestamp: Date.now() }));
|
||||
.org-names {
|
||||
color: var(--accent-1);
|
||||
}
|
||||
|
||||
.org-names a {
|
||||
color: #8a34d6;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.org-names a:hover {
|
||||
color: #d68ae5;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tools-access {
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--accent-3);
|
||||
}
|
||||
|
||||
.tools-link {
|
||||
background: var(--lighter);
|
||||
color: var(--accent-1);
|
||||
border: 1px solid var(--accent-1);
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-stack);
|
||||
}
|
||||
|
||||
.tools-link:hover {
|
||||
background: var(--accent-1);
|
||||
color: var(--lighter);
|
||||
text-shadow: none;
|
||||
box-shadow: 0 0 10px rgba(51, 51, 51, 0.3);
|
||||
}
|
||||
|
||||
.tools-link:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.7s ease;
|
||||
}
|
||||
|
||||
.tools-link:hover:before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.logo {
|
||||
width: 100%;
|
||||
}
|
||||
navigateTo(path);
|
||||
};
|
||||
</script>
|
||||
|
||||
header {
|
||||
padding: 1rem 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Policies & Privacy</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Wage & Costs</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2"
|
||||
>Equal Hourly Wage</label
|
||||
>
|
||||
<UInput
|
||||
v-model="policies.hourlyWage"
|
||||
type="number"
|
||||
:ui="{ wrapper: 'relative' }">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500">€</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2"
|
||||
>Payroll On-costs (%)</label
|
||||
>
|
||||
<UInput
|
||||
v-model="policies.payrollOncost"
|
||||
type="number"
|
||||
:ui="{ wrapper: 'relative' }">
|
||||
<template #trailing>
|
||||
<span class="text-neutral-500">%</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Cash Management</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2"
|
||||
>Savings Target (months)</label
|
||||
>
|
||||
<UInput
|
||||
v-model="policies.savingsTargetMonths"
|
||||
type="number"
|
||||
step="0.1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2"
|
||||
>Minimum Cash Cushion</label
|
||||
>
|
||||
<UInput
|
||||
v-model="policies.minCashCushion"
|
||||
type="number"
|
||||
:ui="{ wrapper: 'relative' }">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500">€</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Deferred Pay Limits</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2"
|
||||
>Cap (hours per quarter)</label
|
||||
>
|
||||
<UInput v-model="policies.deferredCapHours" type="number" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2"
|
||||
>Sunset (months)</label
|
||||
>
|
||||
<UInput v-model="policies.deferredSunsetMonths" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Distribution Order</h3>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-neutral-600">
|
||||
Order of surplus distribution priorities.
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in distributionOrder"
|
||||
:key="item"
|
||||
class="flex items-center justify-between p-2 bg-neutral-50 rounded">
|
||||
<span class="text-sm font-medium"
|
||||
>{{ index + 1 }}. {{ item }}</span
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-chevron-up" />
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-chevron-down" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<!-- Member Management Section -->
|
||||
<UCard id="members">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">Team Members</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<UBadge v-if="isSetupComplete" color="green" variant="subtle" size="xs">
|
||||
Synchronized with Setup
|
||||
</UBadge>
|
||||
<UButton variant="ghost" size="xs" @click="goToSetup">
|
||||
Edit in Setup
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-if="members.length === 0" class="text-center py-8 text-neutral-500">
|
||||
<p class="mb-4">No team members found.</p>
|
||||
<UButton @click="goToSetup" variant="outline">
|
||||
Add Members in Setup
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
class="p-4 border border-neutral-200 rounded-lg bg-neutral-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 class="font-medium">{{ member.displayName || member.name }}</h4>
|
||||
<div class="text-sm text-neutral-600 space-y-1">
|
||||
<div v-if="member.role">Role: {{ member.role }}</div>
|
||||
<div>Monthly Target: {{ $format.currency(member.monthlyPayPlanned || 0) }}</div>
|
||||
<div v-if="member.minMonthlyNeeds">Min Needs: {{ $format.currency(member.minMonthlyNeeds) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right text-sm text-neutral-600">
|
||||
<div>{{ Math.round((member.hoursPerMonth || member.hoursPerWeek * 4.33)) }} hrs/month</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<UButton color="primary"> Save Policies </UButton>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Store sync and setup state
|
||||
const { initSync, getMembers, unifiedMembers } = useStoreSync();
|
||||
const { isSetupComplete, goToSetup } = useSetupState();
|
||||
const coopStore = useCoopBuilderStore();
|
||||
const { $format } = useNuxtApp();
|
||||
|
||||
// Initialize synchronization on mount
|
||||
onMounted(async () => {
|
||||
await initSync();
|
||||
});
|
||||
|
||||
// Get reactive synchronized member data
|
||||
const members = unifiedMembers;
|
||||
|
||||
// Get synchronized policy data from setup
|
||||
const policies = computed({
|
||||
get: () => ({
|
||||
hourlyWage: coopStore.equalHourlyWage,
|
||||
payrollOncost: coopStore.payrollOncostPct,
|
||||
savingsTargetMonths: coopStore.savingsTargetMonths,
|
||||
minCashCushion: coopStore.minCashCushion,
|
||||
deferredCapHours: 240, // These fields might not be in coop store yet
|
||||
deferredSunsetMonths: 12,
|
||||
}),
|
||||
set: (newPolicies) => {
|
||||
// Update the CoopBuilder store when policies change
|
||||
coopStore.setEqualWage(newPolicies.hourlyWage);
|
||||
coopStore.setOncostPct(newPolicies.payrollOncost);
|
||||
coopStore.savingsTargetMonths = newPolicies.savingsTargetMonths;
|
||||
coopStore.minCashCushion = newPolicies.minCashCushion;
|
||||
}
|
||||
});
|
||||
|
||||
const distributionOrder = ref([
|
||||
"Deferred",
|
||||
"Savings",
|
||||
"Hardship",
|
||||
"Training",
|
||||
"Patronage",
|
||||
"Retained",
|
||||
]);
|
||||
</script>
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
</p>
|
||||
<div class="flex justify-center">
|
||||
<NuxtLink
|
||||
to="/coop-builder"
|
||||
to="/tools/coop-builder"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-black hover:bg-neutral-800 border-2 border-black transition-colors">
|
||||
Complete Setup Wizard
|
||||
</NuxtLink>
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
:disabled="isResetting">
|
||||
Start Over
|
||||
</button>
|
||||
<button class="export-btn primary" @click="navigateTo('/budget')">
|
||||
<button class="export-btn primary" @click="navigateTo('/tools/budget')">
|
||||
Go to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
381
pages/tools/index.vue
Normal file
381
pages/tools/index.vue
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold">Compensation</h2>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs font-mono">
|
||||
Runway: {{ Math.round(metrics.runway) }}mo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="onExport"
|
||||
class="px-4 py-2 border-2 border-black bg-white font-bold uppercase text-sm hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
Export JSON
|
||||
</button>
|
||||
<button
|
||||
@click="onImport"
|
||||
class="px-4 py-2 border-2 border-black bg-white font-bold uppercase text-sm hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
Import JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<CoverageMeter
|
||||
:funded-paid-hours="Math.round(metrics.totalTargetHours * 0.65)"
|
||||
:target-hours="metrics.totalTargetHours"
|
||||
description="Funded hours vs target capacity across all members." />
|
||||
|
||||
<ReserveMeter
|
||||
:current-savings="savingsProgress.current"
|
||||
:savings-target-months="savingsProgress.targetMonths"
|
||||
:monthly-burn="getMonthlyBurn()"
|
||||
:description="`${savingsProgress.progressPct.toFixed(0)}% of savings target reached. ${savingsProgress.gap > 0 ? 'Gap: ' + $format.currency(savingsProgress.gap) : 'Target achieved!'}`" />
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Components with Wizard Styling -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Needs Coverage Bars -->
|
||||
<div class="border-2 border-black bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="border-b-2 border-black p-4">
|
||||
<h3 class="text-lg font-bold uppercase">Member Coverage</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<NeedsCoverageBars />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestone-Runway Overlay -->
|
||||
<div class="border-2 border-black bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="border-b-2 border-black p-4">
|
||||
<h3 class="text-lg font-bold uppercase">Runway vs Milestones</h3>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<MilestoneRunwayOverlay />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts Section with Wizard Styling -->
|
||||
<div class="border-2 border-black bg-white shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
<div class="border-b-2 border-black p-4">
|
||||
<h3 class="text-lg font-bold uppercase">Alerts</h3>
|
||||
</div>
|
||||
<div class="p-4 space-y-4">
|
||||
<!-- Concentration Risk Alert -->
|
||||
<div
|
||||
v-if="topSourcePct > 50"
|
||||
class="border-2 border-red-600 bg-red-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-red-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Revenue Concentration Risk</h4>
|
||||
<p class="text-sm mb-2">{{ topStreamName }} = {{ topSourcePct }}% of total → consider balancing</p>
|
||||
<button
|
||||
@click="handleAlertNavigation('/tools/', 'concentration')"
|
||||
class="text-sm underline font-bold">
|
||||
VIEW DETAILS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cushion Breach Alert -->
|
||||
<div
|
||||
v-if="alerts.cushionBreach"
|
||||
class="border-2 border-orange-600 bg-orange-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-orange-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Cash Cushion Breach Forecast</h4>
|
||||
<p class="text-sm mb-2">Projected to breach minimum cushion in week {{ cushionForecast.firstBreachWeek || 'unknown' }}</p>
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
@click="handleAlertNavigation('/tools/cash', 'breach-forecast')"
|
||||
class="text-sm underline font-bold">
|
||||
VIEW CALENDAR
|
||||
</button>
|
||||
<button
|
||||
@click="handleAlertNavigation('/tools/budget', 'expenses')"
|
||||
class="text-sm underline font-bold">
|
||||
ADJUST BUDGET
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Savings Below Target Alert -->
|
||||
<div
|
||||
v-if="alerts.savingsBelowTarget"
|
||||
class="border-2 border-yellow-600 bg-yellow-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-yellow-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Savings Below Target</h4>
|
||||
<p class="text-sm mb-2">{{ savingsProgress.progressPct.toFixed(0) }}% of target reached. Build savings before increasing paid hours.</p>
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
@click="handleAlertNavigation('/tools/budget', 'savings')"
|
||||
class="text-sm underline font-bold">
|
||||
VIEW PROGRESS
|
||||
</button>
|
||||
<button
|
||||
@click="handleAlertNavigation('/tools/coop-builder', 'policies')"
|
||||
class="text-sm underline font-bold">
|
||||
ADJUST POLICIES
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Over-Deferred Member Alert -->
|
||||
<div
|
||||
v-if="deferredAlert.show"
|
||||
class="border-2 border-purple-600 bg-purple-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="text-purple-600 font-bold text-xl">!</span>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold uppercase mb-1">Member Over-Deferred</h4>
|
||||
<p class="text-sm mb-2">{{ deferredAlert.description }}</p>
|
||||
<button
|
||||
@click="handleAlertNavigation('/tools/coop-builder', 'members')"
|
||||
class="text-sm underline font-bold">
|
||||
REVIEW MEMBERS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success message when no alerts -->
|
||||
<div v-if="!alerts.cushionBreach && !alerts.savingsBelowTarget && topSourcePct <= 50 && !deferredAlert.show"
|
||||
class="text-center py-8">
|
||||
<span class="text-4xl font-bold">✓</span>
|
||||
<p class="font-bold uppercase mt-2">All systems looking good!</p>
|
||||
<p class="text-sm mt-1">No critical alerts at this time.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Quick Actions with Wizard Styling -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
@click="navigateTo('/tools/cash-flow')"
|
||||
class="border-2 border-black bg-white p-4 text-left hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
<div class="font-bold uppercase mb-1">Cash Flow Analysis</div>
|
||||
<div class="text-sm">Detailed runway & one-time events</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="navigateTo('/tools/budget')"
|
||||
class="border-2 border-black bg-white p-4 text-left hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] transition-shadow">
|
||||
<div class="font-bold uppercase mb-1">Budget Planning</div>
|
||||
<div class="text-sm">Manage expenses & savings</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Dashboard page
|
||||
const { $format } = useNuxtApp();
|
||||
|
||||
// Use real store data instead of fixtures
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
const streamsStore = useStreamsStore();
|
||||
const budgetStore = useBudgetStore();
|
||||
const cashStore = useCashStore();
|
||||
|
||||
// Runway composable with operating mode integration
|
||||
const { getDualModeRunway, getMonthlyBurn } = useRunway();
|
||||
|
||||
// Cushion forecast and savings progress
|
||||
const { savingsProgress, cushionForecast, alerts } = useCushionForecast();
|
||||
|
||||
|
||||
// Calculate metrics from real store data
|
||||
const metrics = computed(() => {
|
||||
const totalTargetHours = membersStore.members.reduce(
|
||||
(sum, member) => sum + (member.capacity?.targetHours || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const totalTargetRevenue = streamsStore.streams.reduce(
|
||||
(sum, stream) => sum + (stream.targetMonthlyAmount || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const totalOverheadCosts = budgetStore.overheadCosts.reduce(
|
||||
(sum, cost) => sum + (cost.amount || 0),
|
||||
0
|
||||
);
|
||||
|
||||
// Use integrated runway calculations that respect operating mode
|
||||
const currentMode = 'target'; // Always target mode now
|
||||
const monthlyBurn = getMonthlyBurn(currentMode);
|
||||
|
||||
// Use actual cash store values with fallback
|
||||
const cash = cashStore.currentCash || 50000;
|
||||
const savings = cashStore.currentSavings || 15000;
|
||||
const totalLiquid = cash + savings;
|
||||
|
||||
// Get dual-mode runway data
|
||||
const runwayData = getDualModeRunway(cash, savings);
|
||||
const runway = currentMode === 'target' ? runwayData.target : runwayData.minimum;
|
||||
|
||||
return {
|
||||
totalTargetHours,
|
||||
totalTargetRevenue,
|
||||
monthlyPayroll: runwayData.minBurn, // Use actual calculated payroll
|
||||
monthlyBurn,
|
||||
runway,
|
||||
runwayData, // Include dual-mode data
|
||||
finances: {
|
||||
currentBalances: {
|
||||
cash: cashStore.currentCash,
|
||||
savings: cashStore.currentSavings,
|
||||
totalLiquid,
|
||||
},
|
||||
policies: {
|
||||
equalHourlyWage: policiesStore.equalHourlyWage,
|
||||
payrollOncostPct: policiesStore.payrollOncostPct,
|
||||
savingsTargetMonths: policiesStore.savingsTargetMonths,
|
||||
minCashCushionAmount: policiesStore.minCashCushionAmount,
|
||||
},
|
||||
deferredLiabilities: {
|
||||
totalDeferred: membersStore.members.reduce(
|
||||
(sum, m) =>
|
||||
sum + (m.deferredHours || 0) * policiesStore.equalHourlyWage,
|
||||
0
|
||||
),
|
||||
},
|
||||
surplus: Math.max(0, totalTargetRevenue - monthlyBurn),
|
||||
savingsGap: Math.max(
|
||||
0,
|
||||
policiesStore.savingsTargetMonths * monthlyBurn -
|
||||
cashStore.currentSavings
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate concentration metrics
|
||||
const topSourcePct = computed(() => {
|
||||
if (streamsStore.streams.length === 0) return 0;
|
||||
const amounts = streamsStore.streams.map((s) => s.targetMonthlyAmount || 0);
|
||||
const total = amounts.reduce((sum, amt) => sum + amt, 0);
|
||||
return total > 0 ? Math.round((Math.max(...amounts) / total) * 100) : 0;
|
||||
});
|
||||
|
||||
const topStreamName = computed(() => {
|
||||
if (streamsStore.streams.length === 0) return 'No streams';
|
||||
const amounts = streamsStore.streams.map((s) => s.targetMonthlyAmount || 0);
|
||||
const maxAmount = Math.max(...amounts);
|
||||
const topStream = streamsStore.streams.find(s => (s.targetMonthlyAmount || 0) === maxAmount);
|
||||
return topStream?.name || 'Unknown stream';
|
||||
});
|
||||
|
||||
const concentrationStatus = computed(() => {
|
||||
if (topSourcePct.value > 50) return "red";
|
||||
if (topSourcePct.value > 35) return "yellow";
|
||||
return "green";
|
||||
});
|
||||
|
||||
const concentrationColor = computed(() => {
|
||||
if (topSourcePct.value > 50) return "text-red-600";
|
||||
if (topSourcePct.value > 35) return "text-yellow-600";
|
||||
return "text-green-600";
|
||||
});
|
||||
|
||||
function getRunwayColor(months: number): string {
|
||||
if (months >= 6) return 'text-green-600'
|
||||
if (months >= 3) return 'text-yellow-600'
|
||||
return 'text-red-600'
|
||||
}
|
||||
|
||||
|
||||
// Cash breach description
|
||||
const cashBreachDescription = computed(() => {
|
||||
// Check cash store for first breach week from projections
|
||||
const breachWeek = cashStore.weeklyProjections.find(
|
||||
(week) => week.breachesCushion
|
||||
);
|
||||
if (breachWeek) {
|
||||
return `Week ${breachWeek.number} would drop below your minimum cushion.`;
|
||||
}
|
||||
return "No cushion breach currently projected.";
|
||||
});
|
||||
|
||||
const onExport = () => {
|
||||
const data = exportAll();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "urgent-tools.json";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const onImport = async () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "application/json";
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
importAll(JSON.parse(text));
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const { exportAll, importAll } = useFixtureIO();
|
||||
|
||||
|
||||
// Deferred alert logic
|
||||
const deferredAlert = computed(() => {
|
||||
const maxDeferredRatio = 1.5; // From CLAUDE.md - flag if >1.5× monthly payroll
|
||||
const monthlyPayrollCost = getMonthlyBurn() * 0.7; // Estimate payroll as 70% of burn
|
||||
const totalDeferred = membersStore.members.reduce(
|
||||
(sum, m) => sum + (m.deferredHours || 0) * policiesStore.equalHourlyWage,
|
||||
0
|
||||
);
|
||||
|
||||
const deferredRatio = monthlyPayrollCost > 0 ? totalDeferred / monthlyPayrollCost : 0;
|
||||
const show = deferredRatio > maxDeferredRatio;
|
||||
|
||||
const overDeferredMembers = membersStore.members.filter(m => {
|
||||
const memberDeferred = (m.deferredHours || 0) * policiesStore.equalHourlyWage;
|
||||
const memberMonthlyPay = m.monthlyPayPlanned || 0;
|
||||
return memberDeferred > memberMonthlyPay * 2; // Member has >2 months of pay deferred
|
||||
});
|
||||
|
||||
return {
|
||||
show,
|
||||
description: show
|
||||
? `${overDeferredMembers.length} member(s) over deferred cap. Total: ${(deferredRatio * 100).toFixed(0)}% of monthly payroll.`
|
||||
: ''
|
||||
};
|
||||
});
|
||||
|
||||
// Alert navigation with context
|
||||
function handleAlertNavigation(path: string, section?: string) {
|
||||
// Store alert context for target page to highlight relevant section
|
||||
if (section) {
|
||||
localStorage.setItem('urgent-tools-alert-context', JSON.stringify({ section, timestamp: Date.now() }));
|
||||
}
|
||||
navigateTo(path);
|
||||
};
|
||||
</script>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
No team members set up yet.
|
||||
</p>
|
||||
<NuxtLink
|
||||
to="/coop-builder"
|
||||
to="/tools/coop-builder"
|
||||
class="px-4 py-2 border-2 border-black dark:border-white bg-white dark:bg-black text-black dark:text-white font-bold hover:bg-neutral-100 dark:hover:bg-neutral-900">
|
||||
Set up your team in Setup Wizard
|
||||
</NuxtLink>
|
||||
|
|
@ -76,6 +76,7 @@
|
|||
v-model="value.checked"
|
||||
:id="`core-value-${index}`"
|
||||
:label="value.label"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -124,6 +125,7 @@
|
|||
v-model="conflict.checked"
|
||||
:id="`conflict-type-${index}`"
|
||||
:label="conflict.label"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -157,6 +159,7 @@
|
|||
id="anonymous-reporting"
|
||||
label="Allow anonymous reporting"
|
||||
help="Members can report issues without revealing their identity"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
|
@ -191,6 +194,7 @@
|
|||
v-model="receiver.checked"
|
||||
:id="`report-receiver-${index}`"
|
||||
:label="receiver.label"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -221,6 +225,7 @@
|
|||
id="support-people"
|
||||
label="Allow support people in mediation sessions"
|
||||
help="Parties can bring a trusted person for emotional support"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
|
@ -283,6 +288,7 @@
|
|||
v-model="step.checked"
|
||||
:id="`process-step-${index}`"
|
||||
:label="`${index + 1}. ${step.label}`"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -378,6 +384,7 @@
|
|||
v-model="action.checked"
|
||||
:id="`available-action-${index}`"
|
||||
:label="action.label"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -396,6 +403,7 @@
|
|||
id="appeal-process"
|
||||
label="Include appeals process"
|
||||
help="Parties can request review of decisions"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
|
@ -439,6 +447,7 @@
|
|||
v-model="circumstance.checked"
|
||||
:id="`special-circumstance-${index}`"
|
||||
:label="circumstance.label"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -598,6 +607,7 @@
|
|||
v-model="channel.checked"
|
||||
:id="`comm-channel-${index}`"
|
||||
:label="channel.label"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -611,6 +621,7 @@
|
|||
id="require-direct-attempt"
|
||||
label="Require direct resolution attempt before escalation"
|
||||
help="Parties must try to resolve directly before filing complaints"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
|
|
@ -621,6 +632,7 @@
|
|||
id="document-direct-resolution"
|
||||
label="Require written record of direct resolution attempts"
|
||||
help="Parties should document outcomes of direct conversations"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
|
@ -699,6 +711,7 @@
|
|||
v-model="element.checked"
|
||||
:id="`complaint-element-${index}`"
|
||||
:label="element.label"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -737,6 +750,7 @@
|
|||
id="require-external-advice"
|
||||
label="Require external legal advice for complex complaints"
|
||||
help="Seek external expertise for multi-party or member-coordinator complaints"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
|
@ -753,6 +767,7 @@
|
|||
id="minutes-of-settlement"
|
||||
label="Require 'Minutes of Settlement' for resolved complaints"
|
||||
help="Agreements must be documented in writing and signed by both parties"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
|
|
@ -806,6 +821,7 @@
|
|||
id="include-human-rights"
|
||||
label="Include Human Rights Commission information"
|
||||
help="Reference external discrimination complaint options"
|
||||
class="w-full"
|
||||
@change="autoSave" />
|
||||
</UFormField>
|
||||
|
||||
|
|
@ -1041,7 +1057,7 @@ const coreValues = ref([
|
|||
{ label: "Mutual Care", checked: true },
|
||||
{ label: "Transparency", checked: true },
|
||||
{ label: "Accountability", checked: false },
|
||||
{ label: "Consent-Based", checked: false },
|
||||
{ label: "Consent", checked: false },
|
||||
{ label: "Anti-Oppression", checked: false },
|
||||
{ label: "Restorative Justice", checked: false },
|
||||
{ label: "Collective Liberation", checked: false },
|
||||
|
|
@ -1280,7 +1296,7 @@ watch(
|
|||
{ deep: true }
|
||||
);
|
||||
|
||||
// Generate the complete policy document for preview and export
|
||||
// Comprehensive generatePolicyDocument function with procedural structure
|
||||
const generatePolicyDocument = () => {
|
||||
const cooperativeName = formData.value.orgName || "[Cooperative Name]";
|
||||
let content = `# ${cooperativeName} Conflict Resolution Policy\n\n`;
|
||||
|
|
@ -1293,25 +1309,93 @@ const generatePolicyDocument = () => {
|
|||
}
|
||||
content += `\n---\n\n`;
|
||||
|
||||
// Core Values section (if enabled)
|
||||
if (sectionsEnabled.value.values) {
|
||||
content += `## Our Values\n\n`;
|
||||
content += `This conflict resolution framework is guided by our core values:\n\n`;
|
||||
// PURPOSE SECTION
|
||||
content += `## Purpose\n\n`;
|
||||
content += `Disagreements in groups are par for the course. But ignoring conflicts, or managing them poorly, can deeply harm individuals and our whole community.\n\n`;
|
||||
content += `Addressing conflict head-on is **a way of caring for each other**.\n\n`;
|
||||
content += `This policy aims to offer a straightforward, consistently enforced, and transparent approach to resolving conflicts and disputes that may emerge in relation to ${cooperativeName}'s programs, governance, or the actions of its members.\n\n`;
|
||||
|
||||
// GUIDING PRINCIPLES
|
||||
content += `## Guiding Principles\n\n`;
|
||||
content += `- All parties to a complaint will **actively participate** and strive to achieve a **collaborative** outcome at the earliest possible stage of the process\n`;
|
||||
content += `- Information about a complaint will only be given to parties directly involved and others on a need-to-know basis\n`;
|
||||
content += `- Parties will be provided with clear and understandable reasons for complaint decisions\n`;
|
||||
content += `- Complaints will be dealt with promptly and resolved as quickly as possible\n`;
|
||||
content += `- Review of complaints will be fair, impartial, and respectful, allowing all parties to have their perspectives heard\n`;
|
||||
content += `- The review will be thorough and as detailed as possible based on the information provided\n`;
|
||||
content += `- The process will be accessible and clearly communicated to all members\n\n`;
|
||||
|
||||
// Add selected values if enabled
|
||||
if (sectionsEnabled.value.values) {
|
||||
const selectedValues = coreValues.value.filter((v) => v.checked);
|
||||
if (selectedValues.length > 0) {
|
||||
content += `Additionally, this framework is guided by our core values:\n\n`;
|
||||
selectedValues.forEach((value) => {
|
||||
content += `- **${value.label}**\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.value.customValues) {
|
||||
content += `${formData.value.customValues}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolution Philosophy
|
||||
// DEFINITIONS SECTION
|
||||
content += `## Definitions\n\n`;
|
||||
content += `- **Conflict/Dispute**: Ongoing experiences of tension and misunderstandings, often leading to interpersonal discord. These terms are used interchangeably.\n`;
|
||||
content += `- **Complainant**: The individual lodging a complaint against another party, policy, or practice.\n`;
|
||||
content += `- **Respondent**: An individual against whom a complaint has been made.\n`;
|
||||
|
||||
// Add definitions based on selections
|
||||
const selectedReceivers = reportReceivers.value.filter((r) => r.checked);
|
||||
if (selectedReceivers.length > 0) {
|
||||
content += `- **Responsible Contact People**: Those accountable for assisting in conflict resolution (${selectedReceivers
|
||||
.map((r) => r.label)
|
||||
.join(
|
||||
", "
|
||||
)}). They act as neutral implementers of this policy, not advocates.\n`;
|
||||
}
|
||||
|
||||
if (formData.value.internalAdvisorType) {
|
||||
content += `- **Internal Advisor**: ${formData.value.internalAdvisorType} who facilitates the conflict resolution process as a neutral intermediary.\n`;
|
||||
}
|
||||
|
||||
if (formData.value.supportPeople) {
|
||||
content += `- **Support People**: Individuals not connected to the conflict whom parties may choose to have present for emotional support during mediation.\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
|
||||
// POLICY ROUTING TABLE
|
||||
content += `## Which Policy Applies?\n\n`;
|
||||
content += `| **Who Can File** | **Type of Complaint** | **Policy to Use** | **Initial Contact** |\n`;
|
||||
content += `|------------------|----------------------|-------------------|--------------------|\n`;
|
||||
|
||||
const selectedConflictTypes = conflictTypes.value.filter((c) => c.checked);
|
||||
selectedConflictTypes.forEach((conflict) => {
|
||||
let policy = "This policy";
|
||||
let contact = selectedReceivers[0]?.label || "Designated contact";
|
||||
|
||||
if (
|
||||
conflict.label.includes("Harassment") ||
|
||||
conflict.label.includes("discrimination")
|
||||
) {
|
||||
policy = "Code of Conduct / Human Rights";
|
||||
if (formData.value.includeHumanRights) {
|
||||
contact += " or Human Rights Tribunal";
|
||||
}
|
||||
} else if (conflict.label.includes("Code of Conduct")) {
|
||||
policy = "Code of Conduct";
|
||||
}
|
||||
|
||||
content += `| Members | ${conflict.label} | ${policy} | ${contact} |\n`;
|
||||
});
|
||||
|
||||
if (formData.value.anonymousReporting) {
|
||||
content += `| Any party | Anonymous reports | This policy | Anonymous reporting system |\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
|
||||
// RESOLUTION APPROACH
|
||||
const approachDescriptions = {
|
||||
restorative:
|
||||
"We use a **restorative/loving justice** approach that focuses on healing, understanding root causes, and repairing relationships rather than punishment.",
|
||||
|
|
@ -1332,9 +1416,9 @@ const generatePolicyDocument = () => {
|
|||
content += `We do our best to resolve conflicts at the lowest possible escalation step (direct resolution), but agree to escalate conflicts (to assisted resolution) if they are not resolved.\n\n`;
|
||||
}
|
||||
|
||||
// Reflection Process (if enabled)
|
||||
// REFLECTION PROCESS (if enabled)
|
||||
if (sectionsEnabled.value.reflection) {
|
||||
content += `## Reflection\n\n`;
|
||||
content += `## Reflection Process\n\n`;
|
||||
content += `Before engaging in direct resolution, we encourage taking time for reflection:\n\n`;
|
||||
content += `1. **Set aside time to think** through what happened. What was the other person's behaviour? How did it affect you? *Distinguish other people's **actions** from your **feelings** about them.*\n`;
|
||||
content += `2. **Consider uncertainties** or misunderstandings that may have occurred.\n`;
|
||||
|
|
@ -1352,20 +1436,20 @@ const generatePolicyDocument = () => {
|
|||
content += `**Reflection Timing:** ${reflectionTiming}\n\n`;
|
||||
}
|
||||
|
||||
// Direct Resolution (if enabled)
|
||||
// DIRECT RESOLUTION (if enabled)
|
||||
if (sectionsEnabled.value.directResolution) {
|
||||
content += `## Direct Resolution\n\n`;
|
||||
content += `A *direct resolution* process occurs when individuals communicate their concerns and work together to resolve disputes without filing an informal or formal complaint.\n\n`;
|
||||
content += `A *direct resolution* process occurs when individuals communicate their concerns and work together to resolve disputes without filing a formal complaint.\n\n`;
|
||||
|
||||
content += `### Have a Conversation\n\n`;
|
||||
content += `When there is a disagreement, the involved people should first **communicate with each other** about their concerns.\n\n`;
|
||||
|
||||
content += `1. **Choose a time and place** to meet that is private and agreeable to both.\n`;
|
||||
content += `2. **Allow reasonable time** for the conversation.\n`;
|
||||
content += `3. **The point is mutual understanding**, not determining who is right or wrong. This requires patience and willingness to listen without immediately dismissing the other person's perspective.\n`;
|
||||
content += `4. **Express thoughts and feelings directly** without belittling or dismissing. Use "I" statements and active listening techniques.\n`;
|
||||
content += `3. **The point is mutual understanding**, not determining who is right or wrong.\n`;
|
||||
content += `4. **Express thoughts and feelings directly** using "I" statements and active listening.\n`;
|
||||
content += `5. **Communicate your wants and needs** and make offers and requests.\n`;
|
||||
content += `6. **Learn for the future.** Ask questions like, "If what I/you said or did came across that way, what can we do to prevent this from happening in the future?"\n`;
|
||||
content += `6. **Learn for the future** – ask what can be done to prevent this from recurring.\n`;
|
||||
|
||||
if (formData.value.documentDirectResolution) {
|
||||
content += `7. **Keep a written record** of the resolution agreed to by both parties.\n\n`;
|
||||
|
|
@ -1391,43 +1475,175 @@ const generatePolicyDocument = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Assisted Resolution
|
||||
// RECEIVING REPORTS SECTION
|
||||
content += `## Receiving Reports\n\n`;
|
||||
|
||||
content += `### Document the Initial Incident Report\n\n`;
|
||||
content += `Collect the following information and enter it in the Incident Log:\n\n`;
|
||||
content += `| Field | Information to Collect |\n`;
|
||||
content += `|-------|------------------------|\n`;
|
||||
content += `| **Participant Name** | Name of individual(s) involved |\n`;
|
||||
content += `| **Issue/Violation** | Brief description of the behavior or conflict |\n`;
|
||||
content += `| **Date & Time** | When the incident occurred |\n`;
|
||||
content += `| **Circumstances** | Context or situation surrounding the incident |\n`;
|
||||
content += `| **Others Involved** | Names of any witnesses or additional participants |\n`;
|
||||
content += `| **Conversation Notes** | Summary of discussion with the complainant |\n\n`;
|
||||
content += `*Gather this information from the complainant – do not "interview" witnesses unless they approach staff.*\n\n`;
|
||||
|
||||
// SUPPORTING THE COMPLAINANT
|
||||
content += `### Supporting the Complainant\n\n`;
|
||||
content += `Follow these steps to help the complainant feel safe:\n\n`;
|
||||
content += `1. **Provide private space** for discussion (in digital spaces, use DM/private channels)\n`;
|
||||
content += `2. **Allow the complainant to decide** if further action should be taken\n`;
|
||||
content += `3. **Explain the process** – walk them through next steps per this policy\n`;
|
||||
content += `4. **Assure confidentiality** – their identity will not be disclosed without permission\n`;
|
||||
content += `5. **Confirm follow-up** – they will be informed about any actions taken\n\n`;
|
||||
|
||||
// ASSISTED RESOLUTION
|
||||
content += `## Assisted Resolution\n\n`;
|
||||
content += `If talking things out doesn't work, you can ask a responsible contact person for help in writing.\n\n`;
|
||||
content += `If direct resolution doesn't work, parties can request assistance from a responsible contact person.\n\n`;
|
||||
|
||||
// Process Steps
|
||||
const selectedSteps = processSteps.value.filter((s) => s.checked);
|
||||
if (selectedSteps.length > 0) {
|
||||
content += `### Resolution Process Steps\n\n`;
|
||||
selectedSteps.forEach((step, index) => {
|
||||
content += `${index + 1}. ${step.label}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Responsible Contact People
|
||||
const selectedReceivers = reportReceivers.value.filter((r) => r.checked);
|
||||
content += `### Responsible Contact People\n\n`;
|
||||
|
||||
if (selectedReceivers.length > 0) {
|
||||
content += `### Initial Contact Options\n\n`;
|
||||
content += `You can report conflicts to any of the following:\n\n`;
|
||||
content += `**Initial Contact Options:**\n`;
|
||||
selectedReceivers.forEach((receiver) => {
|
||||
content += `- ${receiver.label}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Contact People Structure
|
||||
if (formData.value.internalAdvisorType) {
|
||||
content += `**Internal Advisor:** ${formData.value.internalAdvisorType}\n`;
|
||||
}
|
||||
|
||||
if (formData.value.staffLiaison) {
|
||||
content += `**Member Liaison:** ${formData.value.staffLiaison}\n`;
|
||||
}
|
||||
|
||||
if (formData.value.boardChairRole) {
|
||||
content += `**Board Chair Role:** ${formData.value.boardChairRole}\n`;
|
||||
}
|
||||
|
||||
if (
|
||||
formData.value.internalAdvisorType ||
|
||||
formData.value.staffLiaison ||
|
||||
formData.value.boardChairRole
|
||||
) {
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Mediator Structure
|
||||
if (formData.value.mediatorType) {
|
||||
content += `### Mediation/Facilitation\n\n`;
|
||||
content += `**Structure:** ${formData.value.mediatorType}\n\n`;
|
||||
content += `**Mediation Structure:** ${formData.value.mediatorType}\n`;
|
||||
|
||||
if (formData.value.supportPeople) {
|
||||
content += `**Support People:** Parties may bring a trusted person for emotional support during mediation sessions.\n\n`;
|
||||
content += `**Support People:** Parties may bring a trusted person for emotional support during mediation sessions.\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
content += `### Response Times\n\n`;
|
||||
content += `### Response Timeline\n\n`;
|
||||
content += `| Stage | Timeframe |\n`;
|
||||
content += `|-------|----------|\n`;
|
||||
if (formData.value.initialResponse) {
|
||||
content += `- **Initial Response:** ${formData.value.initialResponse}\n`;
|
||||
content += `| Initial Response | ${formData.value.initialResponse} |\n`;
|
||||
}
|
||||
if (formData.value.resolutionTarget) {
|
||||
content += `- **Target Resolution:** ${formData.value.resolutionTarget}\n\n`;
|
||||
content += `| Target Resolution | ${formData.value.resolutionTarget} |\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
|
||||
// COMMITTEE MEETING PROCEDURES (if committee-based)
|
||||
if (
|
||||
formData.value.mediatorType &&
|
||||
formData.value.mediatorType.toLowerCase().includes("committee")
|
||||
) {
|
||||
content += `## Committee Meeting Procedures\n\n`;
|
||||
|
||||
content += `### Before the Meeting\n`;
|
||||
content += `- Notify respondent of complaint\n`;
|
||||
content += `- Allow respondent to provide their perspective\n`;
|
||||
content += `- Schedule meeting within ${
|
||||
formData.value.initialResponse || "specified timeframe"
|
||||
}\n\n`;
|
||||
|
||||
content += `### During the Meeting\n`;
|
||||
content += `Committee members should review the incident report and discuss:\n`;
|
||||
content += `- What happened?\n`;
|
||||
content += `- What are we doing about it?\n`;
|
||||
content += `- Who is implementing the decision?\n`;
|
||||
content += `- When will it be implemented?\n\n`;
|
||||
content += `*Neither the complainant nor respondent should attend the deliberation.*\n\n`;
|
||||
|
||||
content += `### After the Meeting\n`;
|
||||
content += `- Communicate decision to all parties\n`;
|
||||
content += `- Document all communications\n`;
|
||||
content += `- Follow up with complainant about outcomes\n`;
|
||||
content += `- Prepare report for organizational records\n\n`;
|
||||
}
|
||||
|
||||
// Formal Complaints
|
||||
// RESPONSE PROCEDURES MATRIX
|
||||
content += `## Response Procedures\n\n`;
|
||||
const selectedActions = availableActions.value.filter((a) => a.checked);
|
||||
|
||||
if (selectedActions.length > 0) {
|
||||
content += `### Response Matrix\n\n`;
|
||||
content += `| Issue Severity | Possible Response | Documentation Required |\n`;
|
||||
content += `|----------------|-------------------|------------------------|\n`;
|
||||
|
||||
// Create severity-based responses
|
||||
const hasVerbal = selectedActions.some((a) => a.label.includes("Verbal"));
|
||||
const hasWritten = selectedActions.some((a) => a.label.includes("Written"));
|
||||
const hasSuspension = selectedActions.some((a) =>
|
||||
a.label.includes("suspension")
|
||||
);
|
||||
const hasRemoval = selectedActions.some(
|
||||
(a) => a.label.includes("Removal") || a.label.includes("removal")
|
||||
);
|
||||
|
||||
if (hasVerbal) {
|
||||
content += `| First occurrence, minor | Verbal warning | Update incident log |\n`;
|
||||
}
|
||||
if (hasWritten) {
|
||||
content += `| Repeated behavior | Written warning | Formal documentation |\n`;
|
||||
}
|
||||
if (hasSuspension) {
|
||||
content += `| Serious violation | Temporary suspension | Full investigation report |\n`;
|
||||
}
|
||||
if (hasRemoval) {
|
||||
content += `| Severe/safety threat | Immediate removal | Complete documentation + notifications |\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
|
||||
content += `### Available Remedial Actions\n\n`;
|
||||
selectedActions.forEach((action) => {
|
||||
content += `- ${action.label}\n`;
|
||||
});
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.value.appealProcess) {
|
||||
content += `### Appeals Process\n\n`;
|
||||
content += `Parties may request review of decisions through our appeals process. Appeals must be submitted in writing within 30 days of the original decision.\n\n`;
|
||||
}
|
||||
|
||||
// FORMAL COMPLAINTS
|
||||
content += `## Formal Complaints\n\n`;
|
||||
content += `If assisted resolution efforts do not result in an acceptable outcome within a reasonable timeframe, a *formal complaint* may be filed in writing.\n\n`;
|
||||
content += `If assisted resolution does not result in an acceptable outcome, a formal complaint may be filed in writing.\n\n`;
|
||||
|
||||
// Required Elements
|
||||
const selectedElements = formalComplaintElements.value.filter(
|
||||
|
|
@ -1444,58 +1660,137 @@ const generatePolicyDocument = () => {
|
|||
|
||||
// Formal Process Timeline
|
||||
content += `### Formal Process Timeline\n\n`;
|
||||
content += `| Stage | Timeframe |\n`;
|
||||
content += `|-------|----------|\n`;
|
||||
if (formData.value.formalAcknowledgmentTime) {
|
||||
content += `- **Acknowledgment:** ${formData.value.formalAcknowledgmentTime}\n`;
|
||||
content += `| Acknowledgment of complaint | ${formData.value.formalAcknowledgmentTime} |\n`;
|
||||
}
|
||||
if (formData.value.formalReviewTime) {
|
||||
content += `- **Review Completion:** ${formData.value.formalReviewTime}\n\n`;
|
||||
content += `| Review completion | ${formData.value.formalReviewTime} |\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
|
||||
if (formData.value.requireExternalAdvice) {
|
||||
content += `> **External Expertise:** For complex complaints involving multiple parties or organizational leaders, external legal advice will be sought.\n\n`;
|
||||
}
|
||||
|
||||
// Settlement Documentation
|
||||
if (formData.value.requireMinutesOfSettlement) {
|
||||
content += `### Reaching Agreement\n\n`;
|
||||
content += `Any resolution agreed upon must be documented in "Minutes of Settlement" signed by both parties. These agreements will be kept confidential according to our privacy standards.\n\n`;
|
||||
// PREVENTING RETALIATION (if anti-retaliation is selected)
|
||||
const hasAntiRetaliation = specialCircumstances.value.some(
|
||||
(c) => c.checked && c.label.toLowerCase().includes("retaliation")
|
||||
);
|
||||
|
||||
if (hasAntiRetaliation) {
|
||||
content += `## Preventing Retaliation\n\n`;
|
||||
content += `**CRITICAL:** The privacy and safety of the complainant is paramount.\n\n`;
|
||||
content += `- **DO NOT** share details of the incident without express permission from the complainant\n`;
|
||||
content += `- **DO NOT** reveal the complainant's identity to the respondent or others\n`;
|
||||
content += `- **MONITOR** for any retaliatory behavior following a complaint\n`;
|
||||
content += `- **DOCUMENT** any instances of suspected retaliation\n`;
|
||||
content += `- **TREAT** retaliation as a separate, serious violation requiring immediate action\n\n`;
|
||||
}
|
||||
|
||||
// Consequences and Actions
|
||||
const selectedActions = availableActions.value.filter((a) => a.checked);
|
||||
if (selectedActions.length > 0) {
|
||||
content += `## Possible Outcomes\n\n`;
|
||||
content += `Depending on the situation, resolution may include:\n\n`;
|
||||
selectedActions.forEach((action) => {
|
||||
content += `- ${action.label}\n`;
|
||||
});
|
||||
// SETTLEMENT & DOCUMENTATION
|
||||
content += `## Settlement & Documentation\n\n`;
|
||||
|
||||
if (formData.value.requireMinutesOfSettlement) {
|
||||
content += `### Minutes of Settlement\n`;
|
||||
content += `Any resolution must be documented in "Minutes of Settlement" that:\n`;
|
||||
content += `- Clearly state the agreed-upon resolution\n`;
|
||||
content += `- Include commitments from all parties\n`;
|
||||
content += `- Are signed by both complainant and respondent\n`;
|
||||
content += `- Are kept according to our confidentiality standards\n\n`;
|
||||
}
|
||||
|
||||
// REGARDING APOLOGIES
|
||||
content += `### Regarding Apologies\n\n`;
|
||||
content += `We do not require or facilitate apologies unless explicitly requested by the complainant.\n\n`;
|
||||
content += `- Forced apologies can constitute continued harassment\n`;
|
||||
content += `- If offered, apologies should be brief and relayed through the mediator\n`;
|
||||
content += `- Apologies should not require a response from the recipient\n`;
|
||||
content += `- Pressing unwanted apologies may result in further disciplinary action\n\n`;
|
||||
|
||||
// DOCUMENTATION & PRIVACY
|
||||
if (sectionsEnabled.value.documentation) {
|
||||
content += `## Documentation & Privacy\n\n`;
|
||||
|
||||
content += `### Record Management\n\n`;
|
||||
content += `| Record Type | Retention Period | Access Level | Storage Location |\n`;
|
||||
content += `|-------------|------------------|--------------|------------------|\n`;
|
||||
content += `| Initial incident reports | Permanent | Committee only | Secure database |\n`;
|
||||
content += `| Investigation notes | ${
|
||||
formData.value.retention || "5 years"
|
||||
} | Designated roles | Confidential files |\n`;
|
||||
content += `| Resolution agreements | ${
|
||||
formData.value.conflictFileRetention ||
|
||||
formData.value.retention ||
|
||||
"5 years"
|
||||
} | Parties + committee | Secure archive |\n`;
|
||||
content += `| Committee meeting minutes | ${
|
||||
formData.value.retention || "5 years"
|
||||
} | Committee members | Meeting records |\n\n`;
|
||||
|
||||
if (formData.value.docLevel) {
|
||||
content += `**Documentation Level:** ${formData.value.docLevel}\n`;
|
||||
}
|
||||
if (formData.value.confidentiality) {
|
||||
content += `**General Confidentiality:** ${formData.value.confidentiality}\n`;
|
||||
}
|
||||
if (formData.value.settlementConfidentiality) {
|
||||
content += `**Settlement Confidentiality:** ${formData.value.settlementConfidentiality}\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
}
|
||||
|
||||
if (formData.value.appealProcess) {
|
||||
content += `### Appeals Process\n\n`;
|
||||
content += `Parties may request review of decisions through our appeals process.\n\n`;
|
||||
}
|
||||
// SPECIAL CIRCUMSTANCES (if enabled)
|
||||
if (sectionsEnabled.value.special) {
|
||||
const selectedCircumstances = specialCircumstances.value.filter(
|
||||
(c) => c.checked
|
||||
);
|
||||
if (selectedCircumstances.length > 0) {
|
||||
content += `## Special Circumstances\n\n`;
|
||||
|
||||
// Documentation and Privacy
|
||||
if (sectionsEnabled.value.documentation) {
|
||||
content += `## Documentation & Privacy\n\n`;
|
||||
if (formData.value.docLevel) {
|
||||
content += `**Documentation Level:** ${formData.value.docLevel}\n\n`;
|
||||
}
|
||||
if (formData.value.confidentiality) {
|
||||
content += `**Confidentiality:** ${formData.value.confidentiality}\n\n`;
|
||||
}
|
||||
if (formData.value.retention) {
|
||||
content += `**Record Retention:** ${formData.value.retention}\n\n`;
|
||||
const hasImmediateRemoval = selectedCircumstances.some(
|
||||
(c) =>
|
||||
c.label.toLowerCase().includes("immediate removal") ||
|
||||
c.label.toLowerCase().includes("safety")
|
||||
);
|
||||
|
||||
if (hasImmediateRemoval) {
|
||||
content += `### Immediate Safety Threats\n`;
|
||||
content += `When anyone's physical safety is threatened:\n`;
|
||||
content += `1. Immediately remove the offender from the space\n`;
|
||||
content += `2. Implement permanent ban if warranted\n`;
|
||||
content += `3. Notify relevant authorities if required\n`;
|
||||
content += `4. Document all actions taken\n`;
|
||||
content += `5. Inform stakeholders as appropriate while protecting victim privacy\n\n`;
|
||||
}
|
||||
|
||||
const hasTraumaInformed = selectedCircumstances.some((c) =>
|
||||
c.label.toLowerCase().includes("trauma")
|
||||
);
|
||||
|
||||
if (hasTraumaInformed) {
|
||||
content += `### Trauma-Informed Approach\n`;
|
||||
content += `All conflict resolution processes will incorporate trauma-informed principles:\n`;
|
||||
content += `- Recognize the impact of trauma on behavior\n`;
|
||||
content += `- Prioritize physical and emotional safety\n`;
|
||||
content += `- Provide choices and restore control\n`;
|
||||
content += `- Collaborate rather than prescribe solutions\n`;
|
||||
content += `- Build on strengths and resilience\n\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External Resources (if enabled)
|
||||
// EXTERNAL RESOURCES (if enabled)
|
||||
if (sectionsEnabled.value.externalResources) {
|
||||
content += `## External Resources\n\n`;
|
||||
content += `## External Resources & Redress\n\n`;
|
||||
|
||||
if (formData.value.includeHumanRights) {
|
||||
content += `Individuals who are not satisfied with the outcome of a harassment or discrimination complaint may file a complaint with the [Canadian Human Rights Commission](https://www.chrc-ccdp.gc.ca/eng) or their provincial human rights tribunal.\n\n`;
|
||||
content += `### Human Rights Complaints\n`;
|
||||
content += `Individuals who are not satisfied with the outcome of a harassment or discrimination complaint may file a complaint with:\n`;
|
||||
content += `- [Canadian Human Rights Commission](https://www.chrc-ccdp.gc.ca/eng)\n`;
|
||||
content += `- Provincial human rights tribunal\n`;
|
||||
content += `- Other relevant regulatory bodies\n\n`;
|
||||
}
|
||||
|
||||
if (formData.value.additionalResources) {
|
||||
|
|
@ -1504,20 +1799,30 @@ const generatePolicyDocument = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Implementation
|
||||
content += `## Policy Management\n\n`;
|
||||
// IMPLEMENTATION & TRAINING
|
||||
content += `## Implementation\n\n`;
|
||||
|
||||
if (formData.value.training) {
|
||||
content += `### Training Requirements\n\n`;
|
||||
content += `${formData.value.training}\n\n`;
|
||||
}
|
||||
|
||||
content += `### Review and Updates\n\n`;
|
||||
content += `### Policy Management\n\n`;
|
||||
content += `| Aspect | Details |\n`;
|
||||
content += `|--------|----------|\n`;
|
||||
if (formData.value.reviewSchedule) {
|
||||
content += `This policy will be reviewed ${formData.value.reviewSchedule.toLowerCase()}.\n\n`;
|
||||
content += `| Review Schedule | ${formData.value.reviewSchedule} |\n`;
|
||||
}
|
||||
if (formData.value.amendments) {
|
||||
content += `**Amendment Process:** ${formData.value.amendments}\n\n`;
|
||||
content += `| Amendment Process | ${formData.value.amendments} |\n`;
|
||||
}
|
||||
content += `| Last Updated | ${
|
||||
formData.value.createdDate || new Date().toISOString().split("T")[0]
|
||||
} |\n`;
|
||||
if (formData.value.reviewDate) {
|
||||
content += `| Next Review | ${formData.value.reviewDate} |\n`;
|
||||
}
|
||||
content += `\n`;
|
||||
|
||||
// Acknowledgments
|
||||
if (formData.value.acknowledgments) {
|
||||
|
|
@ -1558,6 +1863,8 @@ const exportData = computed(() => {
|
|||
|
||||
return {
|
||||
section: "conflict-resolution-framework",
|
||||
// Add the generated policy document content for exports
|
||||
content: generatePolicyDocument(),
|
||||
// Enhanced formData with processed arrays
|
||||
formData: {
|
||||
...formData.value,
|
||||
|
|
@ -440,7 +440,7 @@
|
|||
:key="step"
|
||||
class="flex items-start">
|
||||
<span
|
||||
class="text-neutral-900 dark:text-neutral-100 font-bold mr-3 mt-1"
|
||||
class="text-neutral-900 dark:text-neutral-100 font-bold mr-3"
|
||||
>→</span
|
||||
>
|
||||
<span class="text-neutral-700 dark:text-neutral-300">{{
|
||||
|
|
@ -453,7 +453,7 @@
|
|||
<div v-if="result.tips" class="section-card">
|
||||
<h3
|
||||
class="text-xl font-semibold text-neutral-900 dark:text-neutral-100 mb-4">
|
||||
Pro tips:
|
||||
Hot tips:
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
<li
|
||||
|
|
@ -461,7 +461,7 @@
|
|||
:key="tip"
|
||||
class="flex items-start">
|
||||
<span
|
||||
class="text-neutral-900 dark:text-neutral-100 font-bold mr-3 mt-1"
|
||||
class="text-neutral-900 dark:text-neutral-100 font-bold mr-3"
|
||||
>→</span
|
||||
>
|
||||
<span class="text-neutral-700 dark:text-neutral-300">{{
|
||||
|
|
@ -508,13 +508,6 @@
|
|||
<UButton @click="resetForm" size="lg" color="primary">
|
||||
Try Another Decision
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="printResult"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
color="primary">
|
||||
Print Recommendation
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -522,6 +515,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credits Section -->
|
||||
<div class="mt-12 py-8 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<div class="text-center text-sm text-neutral-600 dark:text-neutral-400">
|
||||
<p class="mb-2 font-medium">With inspiration from:</p>
|
||||
<div class="space-y-1">
|
||||
<p>Rocket Adrift</p>
|
||||
<p>Baby Ghosts Peer Accelerator curriculum</p>
|
||||
<p><a href="https://thedecider.app/" target="_blank" rel="noopener noreferrer" class="hover:text-neutral-900 dark:hover:text-neutral-100 underline">The Decider App</a></p>
|
||||
<p><a href="https://patterns.sociocracy30.org/index.html" target="_blank" rel="noopener noreferrer" class="hover:text-neutral-900 dark:hover:text-neutral-100 underline">Sociocracy 3.0</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Export Options at Bottom -->
|
||||
|
|
@ -571,7 +577,7 @@ const urgencyOptions = [
|
|||
{
|
||||
value: 5,
|
||||
title: "Crisis mode",
|
||||
description: "This decision is needed yesterday",
|
||||
description: "We should have decided yesterday!!",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -602,7 +608,7 @@ const expertiseOptions = [
|
|||
{
|
||||
value: "multiple",
|
||||
title: "Multiple experts",
|
||||
description: "Several people have relevant expertise",
|
||||
description: "Several people have expertise",
|
||||
},
|
||||
{
|
||||
value: "distributed",
|
||||
|
|
@ -620,7 +626,7 @@ const impactOptions = [
|
|||
{
|
||||
value: "narrow",
|
||||
title: "One person or small team",
|
||||
description: "Affects specific individuals or department",
|
||||
description: "Affects specific individuals or area",
|
||||
},
|
||||
{
|
||||
value: "wide",
|
||||
|
|
@ -633,7 +639,7 @@ const optionsOptions = [
|
|||
{
|
||||
value: "clear",
|
||||
title: "Clear choices",
|
||||
description: "We know our options and their trade-offs",
|
||||
description: "We know our options and their tradeoffs",
|
||||
},
|
||||
{
|
||||
value: "emerging",
|
||||
|
|
@ -768,7 +774,7 @@ function determineFramework() {
|
|||
method: "Strategic Delay",
|
||||
tagline: "Wait for clarity to emerge",
|
||||
reasoning:
|
||||
"It's not urgent, options aren't clear, and people aren't strongly invested. Sometimes the best decision is to not decide yet.",
|
||||
"It's not urgent, options aren't clear, and people aren't strongly invested. Sometimes the best decision is to hold off on deciding!",
|
||||
steps: [
|
||||
"Acknowledge the decision exists",
|
||||
"Set a future check-in date",
|
||||
|
|
@ -803,7 +809,7 @@ function determineFramework() {
|
|||
"This is a high-stakes, permanent decision affecting everyone who cares deeply. Take the time to get real alignment.",
|
||||
steps: [
|
||||
"Share context and constraints with everyone",
|
||||
"Gather all perspectives (async or sync)",
|
||||
"Gather all perspectives (async or live discussion)",
|
||||
"Identify shared values and concerns",
|
||||
"Iterate on proposals until everyone can support it",
|
||||
"Document the decision and everyone's commitment",
|
||||
|
|
@ -902,7 +908,7 @@ function determineFramework() {
|
|||
state.reversible === "high"
|
||||
) {
|
||||
return {
|
||||
method: "Controlled Randomness",
|
||||
method: "Roll the Dice",
|
||||
tagline: "Let chance break the tie",
|
||||
reasoning:
|
||||
"Options are equally good, stakes are low, and people aren't strongly invested. Save time and energy.",
|
||||
|
|
@ -207,7 +207,8 @@
|
|||
</li>
|
||||
<li>Values alignment conversation</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Optional - Equal buy-in contribution of $<UInput
|
||||
Optional - Equal buy-in contribution of {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.buyInAmount"
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
|
|
@ -307,7 +308,8 @@
|
|||
</h3>
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
|
||||
Decisions under $<UInput
|
||||
Decisions under {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.dayToDayLimit"
|
||||
type="number"
|
||||
placeholder="100"
|
||||
|
|
@ -325,13 +327,15 @@
|
|||
</h3>
|
||||
<p
|
||||
class="content-paragraph mb-3 leading-relaxed text-left flex items-baseline gap-2 flex-wrap">
|
||||
Decisions between $<UInput
|
||||
Decisions between {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.regularDecisionMin"
|
||||
type="number"
|
||||
placeholder="100"
|
||||
class="inline-field number-field w-10"
|
||||
@change="autoSave" />
|
||||
and $<UInput
|
||||
and {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.regularDecisionMax"
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
|
|
@ -355,7 +359,8 @@
|
|||
<li>Adding or removing members</li>
|
||||
<li>Changing this agreement</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Taking on debt over $<UInput
|
||||
Taking on debt over {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.majorDebtThreshold"
|
||||
type="number"
|
||||
placeholder="5000"
|
||||
|
|
@ -418,8 +423,8 @@
|
|||
</h3>
|
||||
<p class="content-paragraph mb-3 leading-relaxed text-left">
|
||||
Each member owns an equal share of
|
||||
{{ getDisplayName().toLowerCase() }},
|
||||
regardless of hours worked or tenure.
|
||||
{{ getDisplayName().toLowerCase() }}, regardless of hours
|
||||
worked or tenure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -450,7 +455,8 @@
|
|||
</p>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Base rate: $<UInput
|
||||
Base rate: {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.baseRate"
|
||||
type="number"
|
||||
placeholder="25"
|
||||
|
|
@ -458,7 +464,8 @@
|
|||
@change="autoSave" />/hour for all members
|
||||
</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Or: Equal monthly draw of $<UInput
|
||||
Or: Equal monthly draw of {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.monthlyDraw"
|
||||
type="number"
|
||||
placeholder="2000"
|
||||
|
|
@ -478,7 +485,8 @@
|
|||
</p>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Hourly rate: $<UInput
|
||||
Hourly rate: {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.hourlyRate"
|
||||
type="number"
|
||||
placeholder="25"
|
||||
|
|
@ -506,7 +514,8 @@
|
|||
</li>
|
||||
<li>Regular needs assessment and adjustment process</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Minimum guaranteed amount: $<UInput
|
||||
Minimum guaranteed amount: {{ currencySymbol
|
||||
}}<UInput
|
||||
v-model="formData.minGuaranteedPay"
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
|
|
@ -521,27 +530,24 @@
|
|||
<p class="content-paragraph font-semibold">
|
||||
Payment Schedule:
|
||||
</p>
|
||||
<ul class="content-list my-2 pl-6 list-disc">
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Paid on the
|
||||
<USelect
|
||||
v-model="formData.paymentDay"
|
||||
:items="dayOptions"
|
||||
placeholder="15th"
|
||||
class="inline-field"
|
||||
@change="autoSave" />
|
||||
<p class="">
|
||||
Paid on the
|
||||
<USelectMenu
|
||||
v-model="formData.paymentDay"
|
||||
:items="dayOptions"
|
||||
placeholder="15th"
|
||||
size="lg"
|
||||
class="w-48"
|
||||
@change="autoSave" />
|
||||
|
||||
of each month
|
||||
</li>
|
||||
<li class="flex items-baseline gap-2 flex-wrap">
|
||||
Surplus (profit) distributed equally every
|
||||
<UInput
|
||||
v-model="formData.surplusFrequency"
|
||||
placeholder="quarter"
|
||||
class="inline-field"
|
||||
@change="autoSave" />
|
||||
</li>
|
||||
</ul>
|
||||
of each month and the surplus (profit) distributed equally
|
||||
every
|
||||
<UInput
|
||||
v-model="formData.surplusFrequency"
|
||||
placeholder="quarter"
|
||||
class="inline-field"
|
||||
@change="autoSave" />.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -805,8 +811,8 @@
|
|||
|
||||
<div v-else class="text-neutral-600 dark:text-neutral-400 italic">
|
||||
<p class="content-paragraph">
|
||||
{{ getDisplayName() || "This cooperative" }} operates as
|
||||
an informal collective. If we decide to register legally in the
|
||||
{{ getDisplayName() || "This cooperative" }} operates as an
|
||||
informal collective. If we decide to register legally in the
|
||||
future, we'll update this section with our legal structure
|
||||
details.
|
||||
</p>
|
||||
|
|
@ -826,10 +832,15 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { getCurrencySymbol } from "~/utils/currency";
|
||||
|
||||
// Import centralized coop info
|
||||
const { coopInfo, updateCoopInfo, getDisplayName } = useCoopInfo();
|
||||
|
||||
// Get currency symbol from global coop builder
|
||||
const coop = useCoopBuilder();
|
||||
const currencySymbol = computed(() => getCurrencySymbol(coop.currency.value));
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
});
|
||||
|
|
@ -1092,10 +1103,13 @@ loadSavedData();
|
|||
watch(
|
||||
() => coopInfo.value,
|
||||
(newCoopInfo) => {
|
||||
formData.value.cooperativeName = newCoopInfo.cooperativeName || formData.value.cooperativeName;
|
||||
formData.value.dateEstablished = newCoopInfo.dateEstablished || formData.value.dateEstablished;
|
||||
formData.value.cooperativeName =
|
||||
newCoopInfo.cooperativeName || formData.value.cooperativeName;
|
||||
formData.value.dateEstablished =
|
||||
newCoopInfo.dateEstablished || formData.value.dateEstablished;
|
||||
formData.value.purpose = newCoopInfo.purpose || formData.value.purpose;
|
||||
formData.value.coreValues = newCoopInfo.coreValues || formData.value.coreValues;
|
||||
formData.value.coreValues =
|
||||
newCoopInfo.coreValues || formData.value.coreValues;
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
|
@ -1349,12 +1363,6 @@ const exportData = computed(() => ({
|
|||
|
||||
/* Template-specific styles not in main.css */
|
||||
|
||||
.template-content {
|
||||
max-width: none;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Fiscal year group styling */
|
||||
.fiscal-year-group {
|
||||
display: flex;
|
||||
|
|
@ -142,22 +142,14 @@
|
|||
<div
|
||||
v-if="principleWeights[principle.id] > 0"
|
||||
class="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-800">
|
||||
<label
|
||||
:class="[
|
||||
'flex items-center gap-3 cursor-pointer item-label-bg px-2 py-1',
|
||||
nonNegotiables.includes(principle.id)
|
||||
? 'selected'
|
||||
: '',
|
||||
]">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="nonNegotiables.includes(principle.id)"
|
||||
@change="toggleNonNegotiable(principle.id)"
|
||||
class="w-4 h-4" />
|
||||
<span class="text-sm font-medium text-white">
|
||||
Make this non-negotiable
|
||||
</span>
|
||||
</label>
|
||||
<UCheckbox
|
||||
:model-value="nonNegotiables.includes(principle.id)"
|
||||
@update:model-value="
|
||||
(checked) =>
|
||||
toggleNonNegotiableCheckbox(principle.id, checked)
|
||||
"
|
||||
label="Make this non-negotiable"
|
||||
class="item-label-bg px-2 py-1" />
|
||||
</div>
|
||||
|
||||
<!-- Show rubric description when selected -->
|
||||
|
|
@ -165,7 +157,7 @@
|
|||
v-if="principleWeights[principle.id] > 0"
|
||||
class="mt-4 p-3 item-label-bg selected border border-neutral-200">
|
||||
<div
|
||||
class="text-xs font-bold uppercase text-neutral-300 mb-1">
|
||||
class="text-xs font-bold uppercase text-neutral-800 dark:text-neutral-300 mb-1">
|
||||
Evaluation Criteria:
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
|
|
@ -213,7 +205,7 @@
|
|||
:class="[
|
||||
'relative px-5 py-2 border-2 transition-all focus:outline-none focus:ring-1 focus:ring-black cursor-pointer',
|
||||
constraints.sso === option.value
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black dark:!text-black'
|
||||
? 'constraint-selected border-black dark:border-white cursor-pointer !bg-black !text-white dark:!bg-white dark:!text-black'
|
||||
: 'border-neutral-300 hover:border-neutral-400 bg-white dark:bg-neutral-950 cursor-pointer',
|
||||
]">
|
||||
{{ option.label }}
|
||||
|
|
@ -855,6 +847,32 @@ const toggleIntegration = (integration) => {
|
|||
}
|
||||
};
|
||||
|
||||
const toggleIntegrationCheckbox = (integration, checked) => {
|
||||
if (checked) {
|
||||
if (!constraints.value.integrations.includes(integration)) {
|
||||
constraints.value.integrations.push(integration);
|
||||
}
|
||||
} else {
|
||||
const idx = constraints.value.integrations.indexOf(integration);
|
||||
if (idx !== -1) {
|
||||
constraints.value.integrations.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleNonNegotiableCheckbox = (principleId, checked) => {
|
||||
if (checked) {
|
||||
if (!nonNegotiables.value.includes(principleId)) {
|
||||
nonNegotiables.value.push(principleId);
|
||||
}
|
||||
} else {
|
||||
const idx = nonNegotiables.value.indexOf(principleId);
|
||||
if (idx !== -1) {
|
||||
nonNegotiables.value.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
if (confirm("Are you sure you want to clear all form data and start over?")) {
|
||||
charterPurpose.value =
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="template-container min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 py-8"
|
||||
>
|
||||
class="template-container min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100 py-8">
|
||||
<div class="max-w-6xl mx-auto px-4 relative">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||
Wizards
|
||||
</h1>
|
||||
<p class="text-neutral-700 dark:text-neutral-200">
|
||||
Fillable forms for cooperative documents. Data saves locally in your browser.
|
||||
Fillable forms for cooperative documents. Data saves locally in your
|
||||
browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -17,14 +17,14 @@
|
|||
<div
|
||||
v-for="template in templates"
|
||||
:key="template.id"
|
||||
class="template-card h-full flex flex-col"
|
||||
>
|
||||
<div class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
class="template-card h-full flex flex-col">
|
||||
<div
|
||||
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6 h-full flex flex-col"
|
||||
>
|
||||
class="absolute top-2 left-2 w-full h-full dither-shadow"></div>
|
||||
<div
|
||||
class="relative bg-white dark:bg-neutral-950 border border-black dark:border-white p-6 h-full flex flex-col">
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900 dark:text-white">
|
||||
<h3
|
||||
class="text-xl font-semibold text-neutral-900 dark:text-white">
|
||||
{{ template.name }}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
@ -35,8 +35,7 @@
|
|||
<span
|
||||
v-for="tag in template.tags"
|
||||
:key="tag"
|
||||
class="px-2 py-1 text-xs font-medium bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-900 border border-black dark:border-white dither-tag"
|
||||
>
|
||||
class="px-2 py-1 text-xs font-medium bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-900 border border-black dark:border-white dither-tag">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -51,8 +50,7 @@
|
|||
<NuxtLink
|
||||
:to="template.path"
|
||||
class="flex-1 px-4 py-2 bg-black dark:bg-white text-white dark:text-black text-center font-medium tracking-wider hover:underline"
|
||||
style="font-family: 'Ubuntu Mono', monospace"
|
||||
>
|
||||
style="font-family: 'Ubuntu Mono', monospace">
|
||||
START WIZARD
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
|
|
@ -60,8 +58,7 @@
|
|||
:to="template.path"
|
||||
class="px-4 py-2 bg-white dark:bg-neutral-950 text-black dark:text-white border border-black dark:border-white hover:bg-white dark:hover:bg-neutral-950 transition-colors bitmap-button"
|
||||
title="Continue from saved data"
|
||||
style="font-family: 'Ubuntu Mono', monospace"
|
||||
>
|
||||
style="font-family: 'Ubuntu Mono', monospace">
|
||||
RESUME
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
|
@ -81,7 +78,7 @@ const templates = [
|
|||
id: "membership-agreement",
|
||||
name: "Membership Agreement",
|
||||
description:
|
||||
"A comprehensive agreement outlining member rights, responsibilities, decision-making processes, and financial arrangements for worker cooperatives.",
|
||||
"An agreement outlining member rights, responsibilities, decision-making processes, and financial arrangements.",
|
||||
icon: "i-heroicons-user-group",
|
||||
path: "/templates/membership-agreement",
|
||||
tags: ["Legal", "Governance", "Membership"],
|
||||
|
|
@ -93,7 +90,7 @@ const templates = [
|
|||
id: "conflict-resolution-framework",
|
||||
name: "Conflict Resolution",
|
||||
description:
|
||||
"A customizable framework for handling conflicts with restorative justice principles, clear processes, and organizational values alignment.",
|
||||
"A framework for handling conflicts with restorative justice principles, clear processes, and organizational values alignment.",
|
||||
icon: "i-heroicons-scale",
|
||||
path: "/templates/conflict-resolution-framework",
|
||||
tags: ["Governance", "Process", "Care"],
|
||||
|
|
@ -105,7 +102,7 @@ const templates = [
|
|||
id: "tech-charter",
|
||||
name: "Technology Charter",
|
||||
description:
|
||||
"Build technology decisions on cooperative values. Define principles, technical constraints, and evaluation criteria for vendor selection.",
|
||||
"How do you decide what technology and tools align with your values? This wizard helps you define principles, technical constraints, and evaluation criteria for tech selection.",
|
||||
icon: "i-heroicons-cog-6-tooth",
|
||||
path: "/templates/tech-charter",
|
||||
tags: ["Technology", "Decision-Making", "Governance"],
|
||||
|
|
@ -117,7 +114,7 @@ const templates = [
|
|||
id: "decision-framework",
|
||||
name: "Decision Framework Helper",
|
||||
description:
|
||||
"Interactive tool to help determine the best decision-making approach based on urgency, expertise, stakes, and team dynamics.",
|
||||
"Need help deciding how to decide? This wizard guides you towards a decision-making approach based on urgency, expertise, stakes, and team dynamics.",
|
||||
icon: "i-heroicons-light-bulb",
|
||||
path: "/templates/decision-framework",
|
||||
tags: ["Decision-Making", "Process", "Governance"],
|
||||
|
|
@ -150,45 +147,6 @@ useHead({
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&family=Ubuntu+Mono:wght@400;700&display=swap");
|
||||
|
||||
.dither-shadow {
|
||||
background: black;
|
||||
background-image: radial-gradient(white 1px, transparent 1px);
|
||||
background-size: 2px 2px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dither-shadow {
|
||||
background: white;
|
||||
background-image: radial-gradient(black 1px, transparent 1px);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark) .dither-shadow {
|
||||
background: white;
|
||||
background-image: radial-gradient(black 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.dither-shadow-disabled {
|
||||
background: black;
|
||||
background-image: radial-gradient(white 1px, transparent 1px);
|
||||
background-size: 2px 2px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dither-shadow-disabled {
|
||||
background: white;
|
||||
background-image: radial-gradient(black 1px, transparent 1px);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark) .dither-shadow-disabled {
|
||||
background: white;
|
||||
background-image: radial-gradient(black 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.template-card {
|
||||
@apply relative;
|
||||
font-family: "Ubuntu", monospace;
|
||||
Loading…
Add table
Add a link
Reference in a new issue