refactor: update app.vue and various components to enhance UI consistency, replace color classes for improved accessibility, and refine layout for better user experience

This commit is contained in:
Jennie Robinson Faber 2025-09-10 11:02:54 +01:00
parent 7b4fb6c2fd
commit 24e8b7a3a8
41 changed files with 2395 additions and 1603 deletions

View file

@ -21,52 +21,25 @@
</h1>
</div>
<!-- Section 1: Organization Information -->
<!-- Section 1: Cooperative Information -->
<div class="section-card">
<h2 class="section-title">1. Organization Information</h2>
<div class="space-y-6">
<UFormField label="Organization Name" class="form-group-large">
<UFormField label="Cooperative Name" class="form-group-large">
<UInput
v-model="formData.orgName"
placeholder="Enter your organization name"
placeholder="Enter your cooperative name"
size="xl"
class="w-full"
:error="validationErrors.orgName"
@input="debouncedAutoSave" />
</UFormField>
<div class="flex flex-row gap-4 space-x-4">
<UFormField label="Organization Type" class="form-group-large">
<USelect
v-model="formData.orgType"
:items="orgTypeOptions"
placeholder="Select organization type..."
size="xl"
class="w-full"
:error="validationErrors.orgType"
@change="autoSave" />
</UFormField>
<UFormField
label="Number of Members/Staff"
class="form-group-large">
<UInput
v-model="formData.memberCount"
type="number"
min="2"
placeholder="e.g., 5"
size="xl"
:error="validationErrors.memberCount"
@change="autoSave" />
</UFormField>
</div>
</div>
</div>
<!-- Section 2: Core Values -->
<div class="section-card">
<div class="flex flex-row justify-between items-center">
<h2 class="section-title">2. Guiding Principles & Values</h2>
<h2 class="section-title">2. Values</h2>
<div class="flex flex-row gap-2 items-center no-print no-pdf">
<USwitch
v-model="sectionsEnabled.values"
@ -80,7 +53,7 @@
<div class="space-y-6" v-show="sectionsEnabled.values">
<UFormField
label="Select Core Values (check all that apply)"
label="Select core values (check all that apply)"
class="form-group-large">
<div class="values-grid">
<div
@ -110,12 +83,12 @@
</UFormField>
<UFormField
label="Additional Values or Principles"
label="Additional values or principles"
class="form-group-large">
<UTextarea
v-model="formData.customValues"
:rows="3"
placeholder="Add any additional values specific to your organization..."
placeholder="Add any additional values specific to your cooperative..."
size="xl"
class="w-full"
@input="debouncedAutoSave" />
@ -230,13 +203,14 @@
</UFormField>
<UFormField
label="Mediator/Facilitator Structure"
label="Mediator/facilitator structure"
class="form-group-large">
<USelect
v-model="formData.mediatorType"
:items="mediatorTypeOptions"
placeholder="Select mediator structure..."
size="xl"
class="w-full"
:error="validationErrors.mediatorType"
@change="autoSave" />
</UFormField>
@ -381,7 +355,7 @@
<div class="space-y-6">
<UFormField
label="Available Actions (check all that apply)"
label="Available actions (check all that apply)"
class="form-group-large">
<div class="checkbox-group mt-4 space-y-3">
<div
@ -484,7 +458,7 @@
v-model="formData.training"
:rows="3"
class="w-full"
placeholder="Describe any training needed for members, facilitators, or committee members..."
placeholder="Describe any training needed for member-workers, facilitators, or committee members..."
size="xl"
@input="debouncedAutoSave" />
</UFormField>
@ -672,18 +646,18 @@
</UFormField>
<UFormField
label="Staff Liaison for Conflict Resolution Committee"
label="Member Liaison for Conflict Resolution Committee"
class="form-group-large">
<UInput
v-model="formData.staffLiaison"
placeholder="Title/role of designated staff liaison"
placeholder="Title/role of designated member liaison"
size="xl"
class="w-full md:w-1/2"
@input="debouncedAutoSave" />
</UFormField>
<UFormField
label="Board Chair Role in Conflict Resolution"
label="Elected Board Chair Role in Conflict Resolution"
class="form-group-large">
<USelect
v-model="formData.boardChairRole"
@ -762,7 +736,7 @@
v-model="formData.requireExternalAdvice"
id="require-external-advice"
label="Require external legal advice for complex complaints"
help="Seek external expertise for multi-party or staff/director complaints"
help="Seek external expertise for multi-party or member-coordinator complaints"
@change="autoSave" />
</UFormField>
</div>
@ -893,7 +867,7 @@
</template>
<script setup>
import { ref, watch, computed, onMounted } from "vue";
import { ref, watch, computed } from "vue";
// Import centralized coop info
const { coopInfo, updateCoopInfo, getOrgName } = useCoopInfo();
@ -913,21 +887,7 @@ useHead({
],
});
// Import PDF export composable
const { exportToPDF } = usePdfExportBasic();
const showPreview = ref(false);
const copySuccess = ref(false);
// Options for dropdowns (using simple string arrays like the working membership template)
const orgTypeOptions = [
"Worker Cooperative",
"Consumer Cooperative",
"Nonprofit",
"Collective",
"Community Group",
"Other",
];
const approachOptions = [
{
@ -1014,17 +974,17 @@ const reflectionPeriodOptions = [
];
const internalAdvisorOptions = [
"Single Board-appointed advisor",
"Rotating Board members",
"Single elected advisor",
"Rotating member representatives",
"External neutral advisor",
"Committee-designated advisor",
"Staff member with training",
"Trained member facilitator",
];
const boardChairRoleOptions = [
"First contact for ED complaints",
"First contact for coordinator complaints",
"Appeals reviewer",
"Final decision maker",
"Participates in collective decision",
"Advisory role only",
"Not involved in conflicts",
];
@ -1089,21 +1049,23 @@ const coreValues = ref([
]);
const conflictTypes = ref([
{ label: "Interpersonal disputes between members", checked: true },
{ label: "Interpersonal disputes between member-workers", checked: true },
{ label: "Code of Conduct violations", checked: true },
{ label: "Work allocation and responsibility disagreements", checked: true },
{ label: "Decision-making process conflicts", checked: true },
{ label: "Harassment or discrimination", checked: false },
{ label: "Work performance issues", checked: false },
{ label: "Conflicts of interest", checked: false },
{ label: "Member-owner responsibility disputes", checked: false },
{ label: "Collective ownership tensions", checked: false },
{ label: "External organization disputes", checked: false },
{ label: "Financial disagreements", checked: false },
]);
const reportReceivers = ref([
{ label: "Designated conflict resolution committee", checked: true },
{ label: "Any board member", checked: false },
{ label: "Executive Director(s)", checked: false },
{ label: "Designated staff liaison", checked: false },
{ label: "Any member", checked: false },
{ label: "Any elected board member", checked: false },
{ label: "Administrative Coordinator(s)", checked: false },
{ label: "Designated member liaison", checked: false },
{ label: "Any member-worker", checked: false },
]);
const processSteps = ref([
@ -1124,7 +1086,7 @@ const availableActions = ref([
{ label: "Temporary suspension", checked: true },
{ label: "Role/responsibility changes", checked: false },
{ label: "Mediated agreement", checked: false },
{ label: "Removal from organization", checked: true },
{ label: "Removal from the cooperative", checked: true },
{ label: "Restorative circle/process", checked: false },
]);
@ -1168,7 +1130,7 @@ const formData = ref({
documentDirectResolution: true,
internalAdvisorType: "Single Board-appointed advisor",
staffLiaison: "",
boardChairRole: "First contact for ED complaints",
boardChairRole: "First contact for coordinator complaints",
formalAcknowledgmentTime: "Within 1 week",
formalReviewTime: "1 month",
requireExternalAdvice: true,
@ -1183,165 +1145,12 @@ const formData = ref({
// Validation logic
const validationErrors = ref({});
const validateForm = () => {
const errors = {};
// Required text fields
if (!formData.value.orgName?.trim()) {
errors.orgName = "Organization name is required";
}
if (!formData.value.orgType?.trim()) {
errors.orgType = "Organization type is required";
}
if (!formData.value.memberCount?.toString().trim()) {
errors.memberCount = "Number of members/staff is required";
}
if (!formData.value.approach?.trim()) {
errors.approach = "Primary resolution approach is required";
}
if (!formData.value.mediatorType?.trim()) {
errors.mediatorType = "Mediator/facilitator structure is required";
}
if (!formData.value.initialResponse?.trim()) {
errors.initialResponse = "Initial response time is required";
}
if (!formData.value.resolutionTarget?.trim()) {
errors.resolutionTarget = "Target resolution time is required";
}
if (!formData.value.reviewSchedule?.trim()) {
errors.reviewSchedule = "Policy review schedule is required";
}
if (!formData.value.amendments?.trim()) {
errors.amendments = "Amendment process is required";
}
// Required checkbox groups (must have at least one checked)
const checkedConflictTypes = conflictTypes.value.filter(
(item) => item.checked
);
if (checkedConflictTypes.length === 0) {
errors.conflictTypes = "Please select at least one type of conflict";
}
// Note: Guiding Principles & Values section is optional - no validation needed
const checkedReportReceivers = reportReceivers.value.filter(
(item) => item.checked
);
if (checkedReportReceivers.length === 0) {
errors.reportReceivers = "Please select at least one report receiver";
}
const checkedProcessSteps = processSteps.value.filter((item) => item.checked);
if (checkedProcessSteps.length === 0) {
errors.processSteps = "Please select at least one process step";
}
const checkedAvailableActions = availableActions.value.filter(
(item) => item.checked
);
if (checkedAvailableActions.length === 0) {
errors.availableActions = "Please select at least one available action";
}
// Note: Special circumstances section is optional - no validation needed
validationErrors.value = errors;
const isValid = Object.keys(errors).length === 0;
// Provide user feedback
if (isValid) {
alert("✅ Form is complete and ready for export!");
} else {
const errorCount = Object.keys(errors).length;
alert(
`❌ Please complete ${errorCount} required field${
errorCount > 1 ? "s" : ""
} before exporting.`
);
}
return isValid;
};
// Completion percentage computation
const completionPercentage = computed(() => {
const allInputs = [
formData.value.orgName,
formData.value.orgType,
formData.value.memberCount,
formData.value.approach,
formData.value.mediatorType,
formData.value.initialResponse,
formData.value.resolutionTarget,
formData.value.reviewSchedule,
formData.value.amendments,
];
const checkboxInputs = [
...coreValues.value,
...conflictTypes.value,
...reportReceivers.value,
...processSteps.value,
...availableActions.value,
...specialCircumstances.value,
];
const filledInputs = allInputs.filter(
(val) => val && val.toString().trim() !== ""
).length;
const checkedBoxes = checkboxInputs.filter((item) => item.checked).length;
const totalFields = allInputs.length + checkboxInputs.length;
const completedFields = filledInputs + checkedBoxes;
return Math.round((completedFields / totalFields) * 100);
});
// Load saved data
const loadSavedData = () => {
if (process.client) {
const saved = localStorage.getItem("conflict-resolution-framework-data");
if (saved) {
try {
const parsedData = JSON.parse(saved);
// Load form data
if (parsedData.formData) {
formData.value = { ...formData.value, ...parsedData.formData };
}
// Load checkbox arrays
if (parsedData.coreValues) coreValues.value = parsedData.coreValues;
if (parsedData.conflictTypes)
conflictTypes.value = parsedData.conflictTypes;
if (parsedData.reportReceivers)
reportReceivers.value = parsedData.reportReceivers;
if (parsedData.processSteps)
processSteps.value = parsedData.processSteps;
if (parsedData.availableActions)
availableActions.value = parsedData.availableActions;
if (parsedData.specialCircumstances)
specialCircumstances.value = parsedData.specialCircumstances;
if (parsedData.communicationChannels)
communicationChannels.value = parsedData.communicationChannels;
if (parsedData.formalComplaintElements)
formalComplaintElements.value = parsedData.formalComplaintElements;
if (parsedData.sectionsEnabled)
sectionsEnabled.value = parsedData.sectionsEnabled;
} catch (error) {
console.error("Error loading saved data:", error);
}
}
}
};
// Auto-save functionality
const autoSave = () => {
// Clear validation errors when users start correcting fields
clearValidationErrors();
if (process.client) {
if (typeof window !== "undefined") {
const dataToSave = {
formData: formData.value,
coreValues: coreValues.value,
@ -1383,7 +1192,7 @@ const markdownToHtml = (markdown) => {
.replace(/<\/li>\s*<ul>/g, "</li>")
.replace(/<\/ul>\s*<li>/g, "<li>")
// Tables (basic support)
.replace(/^\|(.+)\|$/gm, (match, content) => {
.replace(/^\|(.+)\|$/gm, (_, content) => {
const cells = content.split("|").map((cell) => cell.trim());
if (cells.every((cell) => cell.match(/^-+$/))) {
return ""; // Skip separator rows
@ -1471,29 +1280,309 @@ watch(
{ deep: true }
);
// Export data for the ExportOptions component
const exportData = computed(() => ({
formData: formData.value,
orgName: getOrgName(),
orgType: formData.value.orgType,
memberCount: formData.value.memberCount,
sectionsEnabled: sectionsEnabled.value,
coreValues: formData.value.coreValues,
principles: formData.value.principles,
policies: {
memberInvolvement: formData.value.memberInvolvement,
communicationGuidelines: formData.value.communicationGuidelines,
processSteps: formData.value.processSteps,
escalationCriteria: formData.value.escalationCriteria,
mediation: formData.value.mediation,
finalDecision: formData.value.finalDecision,
learning: formData.value.learning,
emergencyProcedures: formData.value.emergencyProcedures,
annualReview: formData.value.annualReview,
},
exportedAt: new Date().toISOString(),
section: "conflict-resolution-framework",
}));
// Generate the complete policy document for preview and export
const generatePolicyDocument = () => {
const cooperativeName = formData.value.orgName || "[Cooperative Name]";
let content = `# ${cooperativeName} Conflict Resolution Policy\n\n`;
content += `*Framework Created: ${
formData.value.createdDate || new Date().toISOString().split("T")[0]
}*\n`;
if (formData.value.reviewDate) {
content += `*Next Review: ${formData.value.reviewDate}*\n`;
}
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`;
const selectedValues = coreValues.value.filter((v) => v.checked);
if (selectedValues.length > 0) {
selectedValues.forEach((value) => {
content += `- **${value.label}**\n`;
});
content += `\n`;
}
if (formData.value.customValues) {
content += `${formData.value.customValues}\n\n`;
}
}
// Resolution Philosophy
const approachDescriptions = {
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.value.approach &&
approachDescriptions[formData.value.approach]
) {
content += `## Our Approach\n\n`;
content += `${approachDescriptions[formData.value.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 (sectionsEnabled.value.reflection) {
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.value.customReflectionPrompts) {
content += `### Additional Reflection Prompts\n\n`;
content += `${formData.value.customReflectionPrompts}\n\n`;
}
const reflectionTiming =
formData.value.reflectionPeriod || "Before any escalation";
content += `**Reflection Timing:** ${reflectionTiming}\n\n`;
}
// 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 += `### 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.value.documentDirectResolution) {
content += `7. **Keep a written record** of the resolution agreed to by both parties.\n\n`;
} else {
content += `\n`;
}
// Communication Channels
const selectedChannels = communicationChannels.value.filter(
(c) => c.checked
);
if (selectedChannels.length > 0) {
content += `### Escalating Communication Bandwidth\n\n`;
content += `Whenever a misunderstanding or conflict arises, **escalate the bandwidth of the channel**:\n\n`;
selectedChannels.forEach((channel, index) => {
content += `${index + 1}. ${channel.label}\n`;
});
content += `\n`;
}
if (formData.value.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
const selectedReceivers = reportReceivers.value.filter((r) => r.checked);
if (selectedReceivers.length > 0) {
content += `### Initial Contact Options\n\n`;
content += `You can report conflicts to any of the following:\n\n`;
selectedReceivers.forEach((receiver) => {
content += `- ${receiver.label}\n`;
});
content += `\n`;
}
// Mediator Structure
if (formData.value.mediatorType) {
content += `### Mediation/Facilitation\n\n`;
content += `**Structure:** ${formData.value.mediatorType}\n\n`;
if (formData.value.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.value.initialResponse) {
content += `- **Initial Response:** ${formData.value.initialResponse}\n`;
}
if (formData.value.resolutionTarget) {
content += `- **Target Resolution:** ${formData.value.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
const selectedElements = formalComplaintElements.value.filter(
(e) => e.checked
);
if (selectedElements.length > 0) {
content += `### Written Complaint Requirements\n\n`;
content += `The formal complaint must include:\n\n`;
selectedElements.forEach((element, index) => {
content += `${index + 1}. ${element.label}\n`;
});
content += `\n`;
}
// Formal Process Timeline
content += `### Formal Process Timeline\n\n`;
if (formData.value.formalAcknowledgmentTime) {
content += `- **Acknowledgment:** ${formData.value.formalAcknowledgmentTime}\n`;
}
if (formData.value.formalReviewTime) {
content += `- **Review Completion:** ${formData.value.formalReviewTime}\n\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`;
}
// 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`;
});
content += `\n`;
}
if (formData.value.appealProcess) {
content += `### Appeals Process\n\n`;
content += `Parties may request review of decisions through our appeals process.\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`;
}
}
// External Resources (if enabled)
if (sectionsEnabled.value.externalResources) {
content += `## External Resources\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`;
}
if (formData.value.additionalResources) {
content += `### Additional Resources\n\n`;
content += `${formData.value.additionalResources}\n\n`;
}
}
// Implementation
content += `## Policy Management\n\n`;
if (formData.value.training) {
content += `### Training Requirements\n\n`;
content += `${formData.value.training}\n\n`;
}
content += `### Review and Updates\n\n`;
if (formData.value.reviewSchedule) {
content += `This policy will be reviewed ${formData.value.reviewSchedule.toLowerCase()}.\n\n`;
}
if (formData.value.amendments) {
content += `**Amendment Process:** ${formData.value.amendments}\n\n`;
}
// Acknowledgments
if (formData.value.acknowledgments) {
content += `### Acknowledgments\n\n`;
content += `${formData.value.acknowledgments}\n\n`;
}
return content;
};
// Export data for the ExportOptions component - structured to match ExportOptions expectations
const exportData = computed(() => {
// Get selected values for arrays
const selectedCoreValues = coreValues.value
.filter((v) => v.checked)
.map((v) => v.label);
const selectedConflictTypes = conflictTypes.value
.filter((c) => c.checked)
.map((c) => c.label);
const selectedProcessSteps = processSteps.value
.filter((s) => s.checked)
.map((s) => s.label);
const selectedActions = availableActions.value
.filter((a) => a.checked)
.map((a) => a.label);
const selectedReceivers = reportReceivers.value
.filter((r) => r.checked)
.map((r) => r.label);
const selectedChannels = communicationChannels.value
.filter((c) => c.checked)
.map((c) => c.label);
const selectedComplaintElements = formalComplaintElements.value
.filter((e) => e.checked)
.map((e) => e.label);
const selectedCircumstances = specialCircumstances.value
.filter((c) => c.checked)
.map((c) => c.label);
return {
section: "conflict-resolution-framework",
// Enhanced formData with processed arrays
formData: {
...formData.value,
// Add processed arrays as lists for the formatter
coreValuesList: selectedCoreValues,
conflictTypesList: selectedConflictTypes,
processStepsList: selectedProcessSteps,
actionsList: selectedActions,
receiversList: selectedReceivers,
channelsList: selectedChannels,
complaintElementsList: selectedComplaintElements,
circumstancesList: selectedCircumstances,
},
sectionsEnabled: sectionsEnabled.value,
reportReceivers: reportReceivers.value,
coreValues: coreValues.value,
conflictTypes: conflictTypes.value,
processSteps: processSteps.value,
availableActions: availableActions.value,
specialCircumstances: specialCircumstances.value,
communicationChannels: communicationChannels.value,
formalComplaintElements: formalComplaintElements.value,
exportedAt: new Date().toISOString(),
};
});
</script>
<style scoped>