335 lines
9.7 KiB
TypeScript
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: Number(((member.hoursPerWeek || 0) * 4.33).toFixed(2)),
|
|
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,
|
|
};
|
|
};
|