app/composables/useStorSync.ts

335 lines
9.7 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: Number(((member.hoursPerMonth || 0) / 4.33).toFixed(2)),
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
);
// 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;
});
};
// 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);
// 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;
});
};
// 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,
membersStore.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: Number(((member.hoursPerMonth || 0) / 4.33).toFixed(2)),
}));
}
return membersStore.members;
});
// Getter functions for backward compatibility
const getStreams = () => unifiedStreams.value;
const getMembers = () => unifiedMembers.value;
return {
syncToLegacyStores,
syncFromLegacyStores,
initSync,
getStreams,
getMembers,
unifiedStreams,
unifiedMembers,
};
};