refactor: remove CashFlowChart and UnifiedCashFlowDashboard components, update routing paths in app.vue, and enhance budget page with cumulative balance calculations and payroll explanation modal for improved user experience

This commit is contained in:
Jennie Robinson Faber 2025-09-10 07:42:56 +01:00
parent 864a81065c
commit f1889b3a70
17 changed files with 922 additions and 1004 deletions

View file

@ -1,76 +1,94 @@
/**
* Store Synchronization Composable
*
*
* Ensures that the legacy stores (streams, members, policies) always stay
* synchronized with the new CoopBuilderStore. This makes the setup interface
* the single source of truth while maintaining backward compatibility.
*/
export const useStoreSync = () => {
const coopStore = useCoopBuilderStore()
const streamsStore = useStreamsStore()
const membersStore = useMembersStore()
const policiesStore = usePoliciesStore()
const coopStore = useCoopBuilderStore();
const streamsStore = useStreamsStore();
const membersStore = useMembersStore();
const policiesStore = usePoliciesStore();
// Flags to prevent recursive syncing and duplicate watchers
let isSyncing = false
let watchersSetup = false
let isSyncing = false;
let watchersSetup = false;
// Sync CoopBuilder -> Legacy Stores
const syncToLegacyStores = () => {
if (isSyncing) return
isSyncing = true
if (isSyncing) return;
isSyncing = true;
// Sync streams
streamsStore.resetStreams()
streamsStore.resetStreams();
coopStore.streams.forEach((stream: any) => {
streamsStore.upsertStream({
id: stream.id,
name: stream.label,
category: stream.category || 'services',
category: stream.category || "services",
targetMonthlyAmount: stream.monthly,
certainty: stream.certainty || 'Probable',
certainty: stream.certainty || "Probable",
payoutDelayDays: 30,
terms: 'Net 30',
terms: "Net 30",
targetPct: 0,
revenueSharePct: 0,
platformFeePct: 0,
restrictions: 'General',
restrictions: "General",
seasonalityWeights: new Array(12).fill(1),
effortHoursPerMonth: 0
})
})
effortHoursPerMonth: 0,
});
});
// Sync members
membersStore.resetMembers()
membersStore.resetMembers();
coopStore.members.forEach((member: any) => {
membersStore.upsertMember({
id: member.id,
displayName: member.name,
role: member.role || '',
hoursPerWeek: Math.round((member.hoursPerMonth || 0) / 4.33),
role: member.role || "",
hoursPerWeek: Number(((member.hoursPerMonth || 0) / 4.33).toFixed(2)),
minMonthlyNeeds: member.minMonthlyNeeds || 0,
monthlyPayPlanned: member.monthlyPayPlanned || 0,
targetMonthlyPay: member.targetMonthlyPay || 0,
externalMonthlyIncome: member.externalMonthlyIncome || 0
})
})
externalMonthlyIncome: member.externalMonthlyIncome || 0,
});
});
// Sync policies - using individual update calls based on store structure
policiesStore.updatePolicy('equalHourlyWage', coopStore.equalHourlyWage)
policiesStore.updatePolicy('payrollOncostPct', coopStore.payrollOncostPct)
policiesStore.updatePolicy('savingsTargetMonths', coopStore.savingsTargetMonths)
policiesStore.updatePolicy('minCashCushionAmount', coopStore.minCashCushion)
policiesStore.updatePolicy("equalHourlyWage", coopStore.equalHourlyWage);
policiesStore.updatePolicy("payrollOncostPct", coopStore.payrollOncostPct);
policiesStore.updatePolicy(
"savingsTargetMonths",
coopStore.savingsTargetMonths
);
policiesStore.updatePolicy(
"minCashCushionAmount",
coopStore.minCashCushion
);
// Ensure pay policy relationship is kept in sync across stores
if (coopStore.policy?.relationship) {
if (typeof (policiesStore as any).setPayPolicy === "function") {
(policiesStore as any).setPayPolicy(coopStore.policy.relationship);
} else if (policiesStore.payPolicy) {
policiesStore.payPolicy.relationship = coopStore.policy.relationship;
}
if (membersStore.payPolicy) {
membersStore.payPolicy.relationship = coopStore.policy
.relationship as any;
}
}
// Reset flag after sync completes
nextTick(() => {
isSyncing = false
})
}
isSyncing = false;
});
};
// Sync Legacy Stores -> CoopBuilder
const syncFromLegacyStores = () => {
if (isSyncing) return
isSyncing = true
if (isSyncing) return;
isSyncing = true;
// Sync streams from legacy store
streamsStore.streams.forEach((stream: any) => {
coopStore.upsertStream({
@ -78,9 +96,9 @@ export const useStoreSync = () => {
label: stream.name,
monthly: stream.targetMonthlyAmount,
category: stream.category,
certainty: stream.certainty
})
})
certainty: stream.certainty,
});
});
// Sync members from legacy store
membersStore.members.forEach((member: any) => {
@ -88,165 +106,222 @@ export const useStoreSync = () => {
id: member.id,
name: member.displayName,
role: member.role,
hoursPerMonth: Math.round((member.hoursPerWeek || 0) * 4.33),
hoursPerMonth: Number(((member.hoursPerWeek || 0) * 4.33).toFixed(2)),
minMonthlyNeeds: member.minMonthlyNeeds,
monthlyPayPlanned: member.monthlyPayPlanned,
targetMonthlyPay: member.targetMonthlyPay,
externalMonthlyIncome: member.externalMonthlyIncome
})
})
externalMonthlyIncome: member.externalMonthlyIncome,
});
});
// Sync policies from legacy store
if (policiesStore.isValid) {
coopStore.setEqualWage(policiesStore.equalHourlyWage)
coopStore.setOncostPct(policiesStore.payrollOncostPct)
coopStore.savingsTargetMonths = policiesStore.savingsTargetMonths
coopStore.minCashCushion = policiesStore.minCashCushionAmount
coopStore.setEqualWage(policiesStore.equalHourlyWage);
coopStore.setOncostPct(policiesStore.payrollOncostPct);
coopStore.savingsTargetMonths = policiesStore.savingsTargetMonths;
coopStore.minCashCushion = policiesStore.minCashCushionAmount;
if (policiesStore.payPolicy?.relationship) {
coopStore.setPolicy(policiesStore.payPolicy.relationship as any)
coopStore.setPolicy(policiesStore.payPolicy.relationship as any);
// Keep members store aligned with legacy policy
if (membersStore.payPolicy) {
membersStore.payPolicy.relationship = policiesStore.payPolicy
.relationship as any;
}
}
}
// Also consider members store policy as a source of truth if set
if (membersStore.payPolicy?.relationship) {
coopStore.setPolicy(membersStore.payPolicy.relationship as any);
if (typeof (policiesStore as any).setPayPolicy === "function") {
(policiesStore as any).setPayPolicy(
membersStore.payPolicy.relationship as any
);
} else if (policiesStore.payPolicy) {
policiesStore.payPolicy.relationship = membersStore.payPolicy
.relationship as any;
}
}
// Reset flag after sync completes
nextTick(() => {
isSyncing = false
})
}
isSyncing = false;
});
};
// Watch for changes in CoopBuilder and sync to legacy stores
const setupCoopBuilderWatchers = () => {
// Watch streams changes
watch(() => coopStore.streams, () => {
if (!isSyncing) {
syncToLegacyStores()
}
}, { deep: true })
watch(
() => coopStore.streams,
() => {
if (!isSyncing) {
syncToLegacyStores();
}
},
{ deep: true }
);
// Watch members changes
watch(() => coopStore.members, () => {
if (!isSyncing) {
syncToLegacyStores()
}
}, { deep: true })
watch(
() => coopStore.members,
() => {
if (!isSyncing) {
syncToLegacyStores();
}
},
{ deep: true }
);
// Watch policy changes
watch(() => [
coopStore.equalHourlyWage,
coopStore.payrollOncostPct,
coopStore.savingsTargetMonths,
coopStore.minCashCushion,
coopStore.currency,
coopStore.policy.relationship
], () => {
if (!isSyncing) {
syncToLegacyStores()
watch(
() => [
coopStore.equalHourlyWage,
coopStore.payrollOncostPct,
coopStore.savingsTargetMonths,
coopStore.minCashCushion,
coopStore.currency,
coopStore.policy.relationship,
],
() => {
if (!isSyncing) {
syncToLegacyStores();
}
}
})
}
);
};
// Watch for changes in legacy stores and sync to CoopBuilder
const setupLegacyStoreWatchers = () => {
// Watch streams store changes
watch(() => streamsStore.streams, () => {
if (!isSyncing) {
syncFromLegacyStores()
}
}, { deep: true })
watch(
() => streamsStore.streams,
() => {
if (!isSyncing) {
syncFromLegacyStores();
}
},
{ deep: true }
);
// Watch members store changes
watch(() => membersStore.members, () => {
if (!isSyncing) {
syncFromLegacyStores()
}
}, { deep: true })
watch(
() => membersStore.members,
() => {
if (!isSyncing) {
syncFromLegacyStores();
}
},
{ deep: true }
);
// Watch policies store changes
watch(() => [
policiesStore.equalHourlyWage,
policiesStore.payrollOncostPct,
policiesStore.savingsTargetMonths,
policiesStore.minCashCushionAmount,
policiesStore.payPolicy?.relationship
], () => {
if (!isSyncing) {
syncFromLegacyStores()
watch(
() => [
policiesStore.equalHourlyWage,
policiesStore.payrollOncostPct,
policiesStore.savingsTargetMonths,
policiesStore.minCashCushionAmount,
policiesStore.payPolicy?.relationship,
membersStore.payPolicy?.relationship,
],
() => {
if (!isSyncing) {
syncFromLegacyStores();
}
}
})
}
);
};
// Initialize synchronization
const initSync = async () => {
// Wait for next tick to ensure stores are mounted
await nextTick()
await nextTick();
// Force store hydration by accessing $state
if (coopStore.$state) {
console.log('🔄 CoopBuilder store hydrated')
console.log("🔄 CoopBuilder store hydrated");
}
// Small delay to ensure localStorage is loaded
await new Promise(resolve => setTimeout(resolve, 10))
// Determine which store has data and sync accordingly
const coopHasData = coopStore.members.length > 0 || coopStore.streams.length > 0
const legacyHasData = streamsStore.streams.length > 0 || membersStore.members.length > 0
console.log('🔄 InitSync: CoopBuilder data:', coopHasData, 'Legacy data:', legacyHasData)
console.log('🔄 CoopBuilder members:', coopStore.members.length, 'streams:', coopStore.streams.length)
console.log('🔄 Legacy members:', membersStore.members.length, 'streams:', streamsStore.streams.length)
// Small delay to ensure localStorage is loaded
await new Promise((resolve) => setTimeout(resolve, 10));
// Determine which store has data and sync accordingly
const coopHasData =
coopStore.members.length > 0 || coopStore.streams.length > 0;
const legacyHasData =
streamsStore.streams.length > 0 || membersStore.members.length > 0;
console.log(
"🔄 InitSync: CoopBuilder data:",
coopHasData,
"Legacy data:",
legacyHasData
);
console.log(
"🔄 CoopBuilder members:",
coopStore.members.length,
"streams:",
coopStore.streams.length
);
console.log(
"🔄 Legacy members:",
membersStore.members.length,
"streams:",
streamsStore.streams.length
);
if (coopHasData && !legacyHasData) {
console.log('🔄 Syncing CoopBuilder → Legacy')
syncToLegacyStores()
console.log("🔄 Syncing CoopBuilder → Legacy");
syncToLegacyStores();
} else if (legacyHasData && !coopHasData) {
console.log('🔄 Syncing Legacy → CoopBuilder')
syncFromLegacyStores()
console.log("🔄 Syncing Legacy → CoopBuilder");
syncFromLegacyStores();
} else if (coopHasData && legacyHasData) {
console.log('🔄 Both have data, keeping in sync')
console.log("🔄 Both have data, keeping in sync");
// Both have data, ensure consistency by syncing from CoopBuilder (primary source)
syncToLegacyStores()
syncToLegacyStores();
} else {
console.log('🔄 No data in either store')
console.log("🔄 No data in either store");
}
// Set up watchers for ongoing sync (only once)
if (!watchersSetup) {
setupCoopBuilderWatchers()
setupLegacyStoreWatchers()
watchersSetup = true
setupCoopBuilderWatchers();
setupLegacyStoreWatchers();
watchersSetup = true;
}
// Return promise to allow awaiting
return Promise.resolve()
}
return Promise.resolve();
};
// Get unified streams data (prioritize CoopBuilder) - make reactive
const unifiedStreams = computed(() => {
if (coopStore.streams.length > 0) {
return coopStore.streams.map(stream => ({
return coopStore.streams.map((stream) => ({
...stream,
name: stream.label,
targetMonthlyAmount: stream.monthly
}))
targetMonthlyAmount: stream.monthly,
}));
}
return streamsStore.streams
})
return streamsStore.streams;
});
// Get unified members data (prioritize CoopBuilder) - make reactive
const unifiedMembers = computed(() => {
if (coopStore.members.length > 0) {
return coopStore.members.map(member => ({
return coopStore.members.map((member) => ({
...member,
displayName: member.name,
hoursPerWeek: Math.round((member.hoursPerMonth || 0) / 4.33)
}))
hoursPerWeek: Number(((member.hoursPerMonth || 0) / 4.33).toFixed(2)),
}));
}
return membersStore.members
})
return membersStore.members;
});
// Getter functions for backward compatibility
const getStreams = () => unifiedStreams.value
const getMembers = () => unifiedMembers.value
const getStreams = () => unifiedStreams.value;
const getMembers = () => unifiedMembers.value;
return {
syncToLegacyStores,
@ -255,6 +330,6 @@ export const useStoreSync = () => {
getStreams,
getMembers,
unifiedStreams,
unifiedMembers
}
}
unifiedMembers,
};
};