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

@ -1,52 +1,72 @@
<template>
<UModal
v-model:open="isOpen"
<UModal
v-model:open="isOpen"
title="Payroll Oncost Settings"
description="Configure payroll taxes and benefits percentage"
:dismissible="true"
>
:dismissible="true">
<template #body>
<div class="space-y-6">
<!-- Explanation -->
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<div class="flex items-start">
<UIcon name="i-heroicons-information-circle" class="h-5 w-5 text-blue-400 mt-0.5 mr-3 flex-shrink-0" />
<UIcon
name="i-heroicons-information-circle"
class="h-5 w-5 text-blue-400 mt-0.5 mr-3 flex-shrink-0" />
<div class="text-sm">
<p class="text-blue-800 dark:text-blue-200 font-medium mb-2">What are payroll oncosts?</p>
<p class="text-blue-800 dark:text-blue-200 font-medium mb-2">
What are payroll oncosts?
</p>
<p class="text-blue-700 dark:text-blue-300">
Payroll oncosts cover taxes, benefits, and other employee-related expenses beyond base wages.
This typically includes employer payroll taxes, worker's compensation, benefits contributions, and other statutory requirements.
Payroll oncosts cover taxes, benefits, and other
employee-related expenses beyond base wages. This typically
includes employer payroll taxes, worker's compensation, benefits
contributions, and other statutory requirements.
</p>
</div>
</div>
</div>
<!-- Current Settings Display -->
<div class="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Current Impact</h4>
<div class="bg-neutral-50 dark:bg-neutral-800 p-4 rounded-lg">
<h4 class="font-medium text-neutral-900 dark:text-white mb-3">
Current Impact
</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-gray-600 dark:text-gray-400">Base Payroll</div>
<div class="font-medium">{{ formatCurrency(basePayroll) }}/month</div>
<div class="text-neutral-600 dark:text-neutral-400">
Base Payroll
</div>
<div class="font-medium">
{{ formatCurrency(basePayroll) }}/month
</div>
</div>
<div>
<div class="text-gray-600 dark:text-gray-400">Oncosts ({{ currentOncostPct }}%)</div>
<div class="font-medium">{{ formatCurrency(currentOncostAmount) }}/month</div>
<div class="text-neutral-600 dark:text-neutral-400">
Oncosts ({{ currentOncostPct }}%)
</div>
<div class="font-medium">
{{ formatCurrency(currentOncostAmount) }}/month
</div>
</div>
</div>
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="text-gray-600 dark:text-gray-400 text-sm">Total Payroll Cost</div>
<div class="font-semibold text-lg">{{ formatCurrency(totalPayrollCost) }}/month</div>
<div
class="mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
<div class="text-neutral-600 dark:text-neutral-400 text-sm">
Total Payroll Cost
</div>
<div class="font-semibold text-lg">
{{ formatCurrency(totalPayrollCost) }}/month
</div>
</div>
</div>
<!-- Percentage Input -->
<div class="space-y-3">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<label
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
Oncost Percentage
</label>
<div class="flex items-center space-x-3">
<div class="flex-1">
<UInput
@ -56,10 +76,9 @@
max="100"
step="1"
placeholder="25"
class="text-center"
/>
class="text-center" />
</div>
<span class="text-sm text-gray-500">%</span>
<span class="text-sm text-neutral-500">%</span>
</div>
<!-- Slider for easier adjustment -->
@ -70,9 +89,8 @@
min="0"
max="50"
step="1"
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 slider"
/>
<div class="flex justify-between text-xs text-gray-500">
class="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer dark:bg-neutral-700 slider" />
<div class="flex justify-between text-xs text-neutral-500">
<span>0%</span>
<span>25%</span>
<span>50%</span>
@ -81,29 +99,44 @@
</div>
<!-- Preview of New Settings -->
<div v-if="newOncostPct !== currentOncostPct" class="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg">
<h4 class="font-medium text-green-800 dark:text-green-200 mb-3">Preview Changes</h4>
<div
v-if="newOncostPct !== currentOncostPct"
class="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg">
<h4 class="font-medium text-green-800 dark:text-green-200 mb-3">
Preview Changes
</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-green-700 dark:text-green-300">New Oncosts ({{ newOncostPct }}%)</div>
<div class="font-medium">{{ formatCurrency(newOncostAmount) }}/month</div>
<div class="text-green-700 dark:text-green-300">
New Oncosts ({{ newOncostPct }}%)
</div>
<div class="font-medium">
{{ formatCurrency(newOncostAmount) }}/month
</div>
</div>
<div>
<div class="text-green-700 dark:text-green-300">New Total Cost</div>
<div class="font-medium">{{ formatCurrency(newTotalCost) }}/month</div>
<div class="text-green-700 dark:text-green-300">
New Total Cost
</div>
<div class="font-medium">
{{ formatCurrency(newTotalCost) }}/month
</div>
</div>
</div>
<div class="mt-2 text-xs">
<span class="text-green-700 dark:text-green-300">
{{ newTotalCost > totalPayrollCost ? 'Increase' : 'Decrease' }} of
{{ formatCurrency(Math.abs(newTotalCost - totalPayrollCost)) }}/month
{{ newTotalCost > totalPayrollCost ? "Increase" : "Decrease" }} of
{{
formatCurrency(Math.abs(newTotalCost - totalPayrollCost))
}}/month
</span>
</div>
</div>
<!-- Common Oncost Ranges -->
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<label
class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
Common Ranges
</label>
<div class="flex flex-wrap gap-2">
@ -111,10 +144,9 @@
v-for="preset in commonRanges"
:key="preset.value"
size="xs"
color="gray"
color="neutral"
variant="outline"
@click="newOncostPct = preset.value"
>
@click="newOncostPct = preset.value">
{{ preset.label }}
</UButton>
</div>
@ -124,14 +156,13 @@
<template #footer="{ close }">
<div class="flex justify-end gap-3">
<UButton color="gray" variant="ghost" @click="handleCancel">
<UButton color="neutral" variant="ghost" @click="handleCancel">
Cancel
</UButton>
<UButton
color="primary"
<UButton
color="primary"
@click="handleSave"
:disabled="!isValidPercentage"
>
:disabled="!isValidPercentage">
Update Oncost Percentage
</UButton>
</div>
@ -140,120 +171,134 @@
</template>
<script setup lang="ts">
import { allocatePayroll as allocatePayrollImpl } from '~/types/members'
import { allocatePayroll as allocatePayrollImpl } from "~/types/members";
interface Props {
open: boolean
open: boolean;
}
interface Emits {
(e: 'update:open', value: boolean): void
(e: 'save', percentage: number): void
(e: "update:open", value: boolean): void;
(e: "save", percentage: number): void;
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// Modal state
const isOpen = computed({
get: () => props.open,
set: (value) => emit('update:open', value)
})
set: (value) => emit("update:open", value),
});
// Get current payroll data
const coopStore = useCoopBuilderStore()
const currentOncostPct = computed(() => coopStore.payrollOncostPct || 0)
const coopStore = useCoopBuilderStore();
const currentOncostPct = computed(() => coopStore.payrollOncostPct || 0);
// Calculate current payroll values using the same logic as the budget store
const { allocatePayroll } = useCoopBuilder()
const { allocatePayroll } = useCoopBuilder();
const basePayroll = computed(() => {
// Calculate base payroll the same way the budget store does
const totalHours = coopStore.members.reduce((sum, m) => sum + (m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0)), 0)
const hourlyWage = coopStore.equalHourlyWage || 0
const basePayrollBudget = totalHours * hourlyWage
const totalHours = coopStore.members.reduce(
(sum, m) =>
sum + (m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0)),
0
);
const hourlyWage = coopStore.equalHourlyWage || 0;
const basePayrollBudget = totalHours * hourlyWage;
if (basePayrollBudget > 0 && coopStore.members.length > 0) {
// Use policy-driven allocation to get actual member pay amounts
const payPolicy = {
relationship: coopStore.policy.relationship,
roleBands: coopStore.policy.roleBands
}
roleBands: coopStore.policy.roleBands,
};
// Convert members to the format expected by allocatePayroll
const membersForAllocation = coopStore.members.map(m => ({
const membersForAllocation = coopStore.members.map((m) => ({
...m,
displayName: m.name,
monthlyPayPlanned: m.monthlyPayPlanned || 0,
minMonthlyNeeds: m.minMonthlyNeeds || 0,
hoursPerMonth: m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0)
}))
hoursPerMonth:
m.hoursPerMonth || (m.hoursPerWeek ? m.hoursPerWeek * 4.33 : 0),
}));
// Use the imported allocatePayroll function
const allocatedMembers = allocatePayrollImpl(membersForAllocation, payPolicy, basePayrollBudget)
const allocatedMembers = allocatePayrollImpl(
membersForAllocation,
payPolicy,
basePayrollBudget
);
// Sum the allocated amounts for total payroll
return allocatedMembers.reduce((sum, m) => sum + (m.monthlyPayPlanned || 0), 0)
return allocatedMembers.reduce(
(sum, m) => sum + (m.monthlyPayPlanned || 0),
0
);
}
return 0
})
const currentOncostAmount = computed(() =>
basePayroll.value * (currentOncostPct.value / 100)
)
return 0;
});
const totalPayrollCost = computed(() =>
basePayroll.value + currentOncostAmount.value
)
const currentOncostAmount = computed(
() => basePayroll.value * (currentOncostPct.value / 100)
);
const totalPayrollCost = computed(
() => basePayroll.value + currentOncostAmount.value
);
// New percentage input
const newOncostPct = ref(currentOncostPct.value)
const newOncostPct = ref(currentOncostPct.value);
// Computed values for preview
const newOncostAmount = computed(() => basePayroll.value * (newOncostPct.value / 100))
const newTotalCost = computed(() => basePayroll.value + newOncostAmount.value)
const newOncostAmount = computed(
() => basePayroll.value * (newOncostPct.value / 100)
);
const newTotalCost = computed(() => basePayroll.value + newOncostAmount.value);
const isValidPercentage = computed(() =>
newOncostPct.value >= 0 && newOncostPct.value <= 100
)
const isValidPercentage = computed(
() => newOncostPct.value >= 0 && newOncostPct.value <= 100
);
// Common oncost ranges
const commonRanges = [
{ label: '0% (No oncosts)', value: 0 },
{ label: '15% (Basic)', value: 15 },
{ label: '25% (Standard)', value: 25 },
{ label: '35% (Comprehensive)', value: 35 }
]
{ label: "0% (No oncosts)", value: 0 },
{ label: "15% (Basic)", value: 15 },
{ label: "25% (Standard)", value: 25 },
{ label: "35% (Comprehensive)", value: 35 },
];
// Reset to current value when modal opens
watch(isOpen, (open) => {
if (open) {
newOncostPct.value = currentOncostPct.value
newOncostPct.value = currentOncostPct.value;
}
})
});
// Handlers
function handleCancel() {
newOncostPct.value = currentOncostPct.value
isOpen.value = false
newOncostPct.value = currentOncostPct.value;
isOpen.value = false;
}
function handleSave() {
if (isValidPercentage.value) {
emit('save', newOncostPct.value)
isOpen.value = false
emit("save", newOncostPct.value);
isOpen.value = false;
}
}
// Currency formatting
function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount)
}).format(amount);
}
</script>
@ -275,4 +320,4 @@ function formatCurrency(amount: number): string {
cursor: pointer;
border: none;
}
</style>
</style>