refactor: remove deprecated components and streamline member coverage calculations, enhance budget management with improved payroll handling, and update UI elements for better clarity
This commit is contained in:
parent
983aeca2dc
commit
09d8794d72
42 changed files with 2166 additions and 2974 deletions
|
|
@ -12,22 +12,11 @@
|
|||
|
||||
<!-- Removed Tab Navigation - showing streams directly -->
|
||||
<div class="space-y-6">
|
||||
<!-- Export Controls -->
|
||||
<div class="flex justify-end">
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="gray"
|
||||
size="sm"
|
||||
@click="exportStreams">
|
||||
<UIcon name="i-heroicons-arrow-down-tray" class="mr-1" />
|
||||
Export
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-if="streams.length === 0"
|
||||
class="text-center py-12 border-4 border-dashed border-black rounded-xl bg-white shadow-lg">
|
||||
class="text-center py-12 border-2 border-dashed border-neutral-300 rounded-xl bg-white shadow-sm">
|
||||
<h4 class="font-medium text-neutral-900 mb-2">
|
||||
No revenue streams yet
|
||||
</h4>
|
||||
|
|
@ -47,56 +36,85 @@
|
|||
<div
|
||||
v-for="stream in streams"
|
||||
:key="stream.id"
|
||||
class="p-6 border-3 border-black rounded-xl bg-white shadow-md">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<UFormField label="Category" required>
|
||||
class="p-6 border-2 border-black rounded-xl bg-white shadow-md">
|
||||
<!-- First row: Category and Name with delete button -->
|
||||
<div class="flex gap-4 mb-4">
|
||||
<UFormField label="Category" required class="flex-1">
|
||||
<USelect
|
||||
v-model="stream.category"
|
||||
:items="categoryOptions"
|
||||
size="xl"
|
||||
class="text-xl font-bold w-full"
|
||||
@update:model-value="saveStream(stream)" />
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="saveCategoryChange(stream)" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Revenue source name" required>
|
||||
<USelectMenu
|
||||
v-model="stream.name"
|
||||
:items="nameOptionsByCategory[stream.category] || []"
|
||||
placeholder="Select or type a source name"
|
||||
creatable
|
||||
searchable
|
||||
size="xl"
|
||||
class="text-xl font-bold w-full"
|
||||
@update:model-value="saveStream(stream)" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Monthly amount" required>
|
||||
<UInput
|
||||
v-model="stream.targetMonthlyAmount"
|
||||
type="text"
|
||||
placeholder="5000"
|
||||
size="xl"
|
||||
class="text-xl font-black w-full"
|
||||
@update:model-value="validateAndSaveAmount($event, stream)"
|
||||
@blur="saveStream(stream)">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500 text-xl">$</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UFormField label="Name" required class="flex-1">
|
||||
<div class="flex gap-2">
|
||||
<USelectMenu
|
||||
v-model="stream.name"
|
||||
:items="nameOptionsByCategory[stream.category] || []"
|
||||
placeholder="Select or type a source name"
|
||||
creatable
|
||||
searchable
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="saveStream(stream)" />
|
||||
<UButton
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="error"
|
||||
@click="removeStream(stream.id)"
|
||||
:ui="{
|
||||
base: 'cursor-pointer hover:opacity-90 transition-opacity',
|
||||
}">
|
||||
<UIcon name="i-heroicons-trash" class="w-4 h-4" />
|
||||
</UButton>
|
||||
</div>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-6 pt-6 border-t-3 border-black">
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="solid"
|
||||
color="error"
|
||||
@click="removeStream(stream.id)"
|
||||
:ui="{
|
||||
base: 'cursor-pointer hover:opacity-90 transition-opacity',
|
||||
}">
|
||||
<UIcon name="i-heroicons-trash" class="w-4 h-4" />
|
||||
</UButton>
|
||||
<!-- Second row: Amount with toggle -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<UFormField :label="stream.amountType === 'annual' ? 'Annual amount' : 'Monthly amount'" required>
|
||||
<div class="flex gap-2">
|
||||
<UInput
|
||||
:value="stream.amountType === 'annual' ? stream.targetAnnualAmount : stream.targetMonthlyAmount"
|
||||
type="text"
|
||||
:placeholder="stream.amountType === 'annual' ? '60000' : '5000'"
|
||||
size="md"
|
||||
class="text-sm font-medium w-full"
|
||||
@update:model-value="validateAndSaveAmount($event, stream)"
|
||||
@blur="saveStream(stream)">
|
||||
<template #leading>
|
||||
<span class="text-neutral-500">{{ currencySymbol }}</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UButtonGroup size="md">
|
||||
<UButton
|
||||
:variant="stream.amountType === 'monthly' ? 'solid' : 'outline'"
|
||||
color="primary"
|
||||
@click="switchAmountType(stream, 'monthly')"
|
||||
class="text-xs">
|
||||
Monthly
|
||||
</UButton>
|
||||
<UButton
|
||||
:variant="stream.amountType === 'annual' ? 'solid' : 'outline'"
|
||||
color="primary"
|
||||
@click="switchAmountType(stream, 'annual')"
|
||||
class="text-xs">
|
||||
Annual
|
||||
</UButton>
|
||||
</UButtonGroup>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 mt-1">
|
||||
<template v-if="stream.amountType === 'annual'">
|
||||
{{ currencySymbol }}{{ Math.round((stream.targetAnnualAmount || 0) / 12) }} per month
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ currencySymbol }}{{ (stream.targetMonthlyAmount || 0) * 12 }} per year
|
||||
</template>
|
||||
</p>
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -115,21 +133,6 @@
|
|||
Add another stream
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div v-if="streams.length > 0" class="flex items-center gap-3 justify-end">
|
||||
<UButton
|
||||
@click="addRevenueStream"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
color="success"
|
||||
:ui="{
|
||||
base: 'cursor-pointer hover:scale-105 transition-transform',
|
||||
leadingIcon: 'hover:rotate-90 transition-transform',
|
||||
}">
|
||||
<UIcon name="i-heroicons-plus" class="mr-1" />
|
||||
Add stream
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -142,8 +145,9 @@ const emit = defineEmits<{
|
|||
"save-status": [status: "saving" | "saved" | "error"];
|
||||
}>();
|
||||
|
||||
// Store
|
||||
// Store and Currency
|
||||
const coop = useCoopBuilder();
|
||||
const { currencySymbol } = useCurrency();
|
||||
const streams = computed(() =>
|
||||
coop.streams.value.map(s => ({
|
||||
// Map store fields to component expectations
|
||||
|
|
@ -151,6 +155,8 @@ const streams = computed(() =>
|
|||
name: s.label,
|
||||
category: s.category || 'games',
|
||||
targetMonthlyAmount: s.monthly || 0,
|
||||
targetAnnualAmount: (s.annual || (s.monthly || 0) * 12),
|
||||
amountType: s.amountType || 'monthly',
|
||||
subcategory: '',
|
||||
targetPct: 0,
|
||||
certainty: s.certainty || 'Aspirational',
|
||||
|
|
@ -219,7 +225,12 @@ const nameOptionsByCategory: Record<string, string[]> = {
|
|||
|
||||
// Computed
|
||||
const totalMonthlyAmount = computed(() =>
|
||||
streams.value.reduce((sum, s) => sum + (s.targetMonthlyAmount || 0), 0)
|
||||
streams.value.reduce((sum, s) => {
|
||||
const monthly = s.amountType === 'annual'
|
||||
? Math.round((s.targetAnnualAmount || 0) / 12)
|
||||
: (s.targetMonthlyAmount || 0);
|
||||
return sum + monthly;
|
||||
}, 0)
|
||||
);
|
||||
|
||||
// Live-write with debounce
|
||||
|
|
@ -228,10 +239,16 @@ const debouncedSave = useDebounceFn((stream: any) => {
|
|||
|
||||
try {
|
||||
// Convert component format back to store format
|
||||
const monthly = stream.amountType === 'annual'
|
||||
? Math.round((stream.targetAnnualAmount || 0) / 12)
|
||||
: (stream.targetMonthlyAmount || 0);
|
||||
|
||||
const streamData = {
|
||||
id: stream.id,
|
||||
label: stream.name || '',
|
||||
monthly: stream.targetMonthlyAmount || 0,
|
||||
monthly: monthly,
|
||||
annual: stream.targetAnnualAmount || monthly * 12,
|
||||
amountType: stream.amountType || 'monthly',
|
||||
category: stream.category || 'games',
|
||||
certainty: stream.certainty || 'Aspirational'
|
||||
};
|
||||
|
|
@ -245,23 +262,87 @@ const debouncedSave = useDebounceFn((stream: any) => {
|
|||
}, 300);
|
||||
|
||||
function saveStream(stream: any) {
|
||||
if (stream.name && stream.category && stream.targetMonthlyAmount >= 0) {
|
||||
const hasValidAmount = stream.amountType === 'annual'
|
||||
? stream.targetAnnualAmount >= 0
|
||||
: stream.targetMonthlyAmount >= 0;
|
||||
|
||||
if (stream.name && stream.category && hasValidAmount) {
|
||||
debouncedSave(stream);
|
||||
}
|
||||
}
|
||||
|
||||
// Save category changes immediately even without a name
|
||||
function saveCategoryChange(stream: any) {
|
||||
// Always save category changes immediately
|
||||
saveStreamImmediate(stream);
|
||||
}
|
||||
|
||||
// Immediate save without debounce for UI responsiveness
|
||||
function saveStreamImmediate(stream: any) {
|
||||
try {
|
||||
// Convert component format back to store format
|
||||
const monthly = stream.amountType === 'annual'
|
||||
? Math.round((stream.targetAnnualAmount || 0) / 12)
|
||||
: (stream.targetMonthlyAmount || 0);
|
||||
|
||||
const streamData = {
|
||||
id: stream.id,
|
||||
label: stream.name || '',
|
||||
monthly: monthly,
|
||||
annual: stream.targetAnnualAmount || monthly * 12,
|
||||
amountType: stream.amountType || 'monthly',
|
||||
category: stream.category || 'games',
|
||||
certainty: stream.certainty || 'Aspirational'
|
||||
};
|
||||
|
||||
coop.upsertStream(streamData);
|
||||
} catch (error) {
|
||||
console.error("Failed to save stream:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Validation function for amount
|
||||
function validateAndSaveAmount(value: string, stream: any) {
|
||||
const numValue = parseFloat(value.replace(/[^\d.]/g, ""));
|
||||
stream.targetMonthlyAmount = isNaN(numValue) ? 0 : Math.max(0, numValue);
|
||||
const validValue = isNaN(numValue) ? 0 : Math.max(0, numValue);
|
||||
|
||||
if (stream.amountType === 'annual') {
|
||||
stream.targetAnnualAmount = validValue;
|
||||
stream.targetMonthlyAmount = Math.round(validValue / 12);
|
||||
} else {
|
||||
stream.targetMonthlyAmount = validValue;
|
||||
stream.targetAnnualAmount = validValue * 12;
|
||||
}
|
||||
|
||||
saveStream(stream);
|
||||
}
|
||||
|
||||
// Function to switch between annual and monthly
|
||||
function switchAmountType(stream: any, type: 'annual' | 'monthly') {
|
||||
stream.amountType = type;
|
||||
|
||||
// Recalculate values based on new type
|
||||
if (type === 'annual') {
|
||||
if (!stream.targetAnnualAmount) {
|
||||
stream.targetAnnualAmount = (stream.targetMonthlyAmount || 0) * 12;
|
||||
}
|
||||
} else {
|
||||
if (!stream.targetMonthlyAmount) {
|
||||
stream.targetMonthlyAmount = Math.round((stream.targetAnnualAmount || 0) / 12);
|
||||
}
|
||||
}
|
||||
|
||||
// Save immediately without debounce for instant UI update
|
||||
saveStreamImmediate(stream);
|
||||
}
|
||||
|
||||
function addRevenueStream() {
|
||||
const newStream = {
|
||||
id: Date.now().toString(),
|
||||
label: "",
|
||||
monthly: 0,
|
||||
annual: 0,
|
||||
amountType: "monthly",
|
||||
category: "games",
|
||||
certainty: "Aspirational"
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue