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:
parent
864a81065c
commit
f1889b3a70
17 changed files with 922 additions and 1004 deletions
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue