260 lines
No EOL
8 KiB
TypeScript
260 lines
No EOL
8 KiB
TypeScript
/**
|
|
* 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()
|
|
|
|
// Flags to prevent recursive syncing and duplicate watchers
|
|
let isSyncing = false
|
|
let watchersSetup = false
|
|
|
|
// Sync CoopBuilder -> Legacy Stores
|
|
const syncToLegacyStores = () => {
|
|
if (isSyncing) return
|
|
isSyncing = true
|
|
// Sync streams
|
|
streamsStore.resetStreams()
|
|
coopStore.streams.forEach((stream: any) => {
|
|
streamsStore.upsertStream({
|
|
id: stream.id,
|
|
name: stream.label,
|
|
category: stream.category || 'services',
|
|
targetMonthlyAmount: stream.monthly,
|
|
certainty: stream.certainty || 'Probable',
|
|
payoutDelayDays: 30,
|
|
terms: 'Net 30',
|
|
targetPct: 0,
|
|
revenueSharePct: 0,
|
|
platformFeePct: 0,
|
|
restrictions: 'General',
|
|
seasonalityWeights: new Array(12).fill(1),
|
|
effortHoursPerMonth: 0
|
|
})
|
|
})
|
|
|
|
// Sync members
|
|
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),
|
|
minMonthlyNeeds: member.minMonthlyNeeds || 0,
|
|
monthlyPayPlanned: member.monthlyPayPlanned || 0,
|
|
targetMonthlyPay: member.targetMonthlyPay || 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)
|
|
|
|
// Reset flag after sync completes
|
|
nextTick(() => {
|
|
isSyncing = false
|
|
})
|
|
}
|
|
|
|
// Sync Legacy Stores -> CoopBuilder
|
|
const syncFromLegacyStores = () => {
|
|
if (isSyncing) return
|
|
isSyncing = true
|
|
// Sync streams from legacy store
|
|
streamsStore.streams.forEach((stream: any) => {
|
|
coopStore.upsertStream({
|
|
id: stream.id,
|
|
label: stream.name,
|
|
monthly: stream.targetMonthlyAmount,
|
|
category: stream.category,
|
|
certainty: stream.certainty
|
|
})
|
|
})
|
|
|
|
// Sync members from legacy store
|
|
membersStore.members.forEach((member: any) => {
|
|
coopStore.upsertMember({
|
|
id: member.id,
|
|
name: member.displayName,
|
|
role: member.role,
|
|
hoursPerMonth: Math.round((member.hoursPerWeek || 0) * 4.33),
|
|
minMonthlyNeeds: member.minMonthlyNeeds,
|
|
monthlyPayPlanned: member.monthlyPayPlanned,
|
|
targetMonthlyPay: member.targetMonthlyPay,
|
|
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
|
|
if (policiesStore.payPolicy?.relationship) {
|
|
coopStore.setPolicy(policiesStore.payPolicy.relationship as any)
|
|
}
|
|
}
|
|
|
|
// Reset flag after sync completes
|
|
nextTick(() => {
|
|
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 members changes
|
|
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 for changes in legacy stores and sync to CoopBuilder
|
|
const setupLegacyStoreWatchers = () => {
|
|
// Watch streams store changes
|
|
watch(() => streamsStore.streams, () => {
|
|
if (!isSyncing) {
|
|
syncFromLegacyStores()
|
|
}
|
|
}, { deep: true })
|
|
|
|
// Watch members store changes
|
|
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()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Initialize synchronization
|
|
const initSync = async () => {
|
|
// Wait for next tick to ensure stores are mounted
|
|
await nextTick()
|
|
|
|
// Force store hydration by accessing $state
|
|
if (coopStore.$state) {
|
|
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)
|
|
|
|
if (coopHasData && !legacyHasData) {
|
|
console.log('🔄 Syncing CoopBuilder → Legacy')
|
|
syncToLegacyStores()
|
|
} else if (legacyHasData && !coopHasData) {
|
|
console.log('🔄 Syncing Legacy → CoopBuilder')
|
|
syncFromLegacyStores()
|
|
} else if (coopHasData && legacyHasData) {
|
|
console.log('🔄 Both have data, keeping in sync')
|
|
// Both have data, ensure consistency by syncing from CoopBuilder (primary source)
|
|
syncToLegacyStores()
|
|
} else {
|
|
console.log('🔄 No data in either store')
|
|
}
|
|
|
|
// Set up watchers for ongoing sync (only once)
|
|
if (!watchersSetup) {
|
|
setupCoopBuilderWatchers()
|
|
setupLegacyStoreWatchers()
|
|
watchersSetup = true
|
|
}
|
|
|
|
// Return promise to allow awaiting
|
|
return Promise.resolve()
|
|
}
|
|
|
|
// Get unified streams data (prioritize CoopBuilder) - make reactive
|
|
const unifiedStreams = computed(() => {
|
|
if (coopStore.streams.length > 0) {
|
|
return coopStore.streams.map(stream => ({
|
|
...stream,
|
|
name: stream.label,
|
|
targetMonthlyAmount: stream.monthly
|
|
}))
|
|
}
|
|
return streamsStore.streams
|
|
})
|
|
|
|
// Get unified members data (prioritize CoopBuilder) - make reactive
|
|
const unifiedMembers = computed(() => {
|
|
if (coopStore.members.length > 0) {
|
|
return coopStore.members.map(member => ({
|
|
...member,
|
|
displayName: member.name,
|
|
hoursPerWeek: Math.round((member.hoursPerMonth || 0) / 4.33)
|
|
}))
|
|
}
|
|
return membersStore.members
|
|
})
|
|
|
|
// Getter functions for backward compatibility
|
|
const getStreams = () => unifiedStreams.value
|
|
const getMembers = () => unifiedMembers.value
|
|
|
|
return {
|
|
syncToLegacyStores,
|
|
syncFromLegacyStores,
|
|
initSync,
|
|
getStreams,
|
|
getMembers,
|
|
unifiedStreams,
|
|
unifiedMembers
|
|
}
|
|
} |