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