feat: add initial application structure with configuration, UI components, and state management
This commit is contained in:
parent
fadf94002c
commit
0af6b17792
56 changed files with 6137 additions and 129 deletions
131
stores/budget.ts
Normal file
131
stores/budget.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useBudgetStore = defineStore(
|
||||
"budget",
|
||||
() => {
|
||||
// Schema version for persistence
|
||||
const schemaVersion = "1.0";
|
||||
|
||||
// Monthly budget lines by period (YYYY-MM)
|
||||
const budgetLines = ref({});
|
||||
|
||||
// Overhead costs (recurring monthly)
|
||||
const overheadCosts = ref([]);
|
||||
|
||||
// Production costs (variable monthly)
|
||||
const productionCosts = ref([]);
|
||||
|
||||
// Current selected period
|
||||
const currentPeriod = ref("2024-01");
|
||||
|
||||
// Computed current budget
|
||||
const currentBudget = computed(() => {
|
||||
return (
|
||||
budgetLines.value[currentPeriod.value] || {
|
||||
period: currentPeriod.value,
|
||||
revenueByStream: {},
|
||||
payrollCosts: { memberHours: [], oncostApplied: 0 },
|
||||
overheadCosts: [],
|
||||
productionCosts: [],
|
||||
savingsChange: 0,
|
||||
net: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Actions
|
||||
function setBudgetLine(period, budgetData) {
|
||||
budgetLines.value[period] = {
|
||||
period,
|
||||
...budgetData,
|
||||
};
|
||||
}
|
||||
|
||||
function updateRevenue(period, streamId, type, amount) {
|
||||
if (!budgetLines.value[period]) {
|
||||
budgetLines.value[period] = { period, revenueByStream: {} };
|
||||
}
|
||||
if (!budgetLines.value[period].revenueByStream[streamId]) {
|
||||
budgetLines.value[period].revenueByStream[streamId] = {};
|
||||
}
|
||||
budgetLines.value[period].revenueByStream[streamId][type] = amount;
|
||||
}
|
||||
|
||||
// Wizard-required actions
|
||||
function addOverheadLine(cost) {
|
||||
// Allow creating a blank line so the user can fill it out in the UI
|
||||
const safeName = cost?.name ?? "";
|
||||
const safeAmountMonthly =
|
||||
typeof cost?.amountMonthly === "number" &&
|
||||
!Number.isNaN(cost.amountMonthly)
|
||||
? cost.amountMonthly
|
||||
: 0;
|
||||
|
||||
overheadCosts.value.push({
|
||||
id: Date.now().toString(),
|
||||
name: safeName,
|
||||
amount: safeAmountMonthly,
|
||||
category: cost?.category || "Operations",
|
||||
recurring: cost?.recurring ?? true,
|
||||
...cost,
|
||||
});
|
||||
}
|
||||
|
||||
function removeOverheadLine(id) {
|
||||
const index = overheadCosts.value.findIndex((c) => c.id === id);
|
||||
if (index > -1) {
|
||||
overheadCosts.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function addOverheadCost(cost) {
|
||||
addOverheadLine({ name: cost.name, amountMonthly: cost.amount, ...cost });
|
||||
}
|
||||
|
||||
function addProductionCost(cost) {
|
||||
productionCosts.value.push({
|
||||
id: Date.now().toString(),
|
||||
name: cost.name,
|
||||
amount: cost.amount,
|
||||
category: cost.category || "Production",
|
||||
period: cost.period,
|
||||
...cost,
|
||||
});
|
||||
}
|
||||
|
||||
function setCurrentPeriod(period) {
|
||||
currentPeriod.value = period;
|
||||
}
|
||||
|
||||
// Reset function
|
||||
function resetBudgetOverhead() {
|
||||
overheadCosts.value = [];
|
||||
productionCosts.value = [];
|
||||
}
|
||||
|
||||
return {
|
||||
budgetLines,
|
||||
overheadCosts,
|
||||
productionCosts,
|
||||
currentPeriod: readonly(currentPeriod),
|
||||
currentBudget,
|
||||
schemaVersion,
|
||||
setBudgetLine,
|
||||
updateRevenue,
|
||||
// Wizard actions
|
||||
addOverheadLine,
|
||||
removeOverheadLine,
|
||||
resetBudgetOverhead,
|
||||
// Legacy actions
|
||||
addOverheadCost,
|
||||
addProductionCost,
|
||||
setCurrentPeriod,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: "urgent-tools-budget",
|
||||
paths: ["overheadCosts", "productionCosts", "currentPeriod"],
|
||||
},
|
||||
}
|
||||
);
|
||||
114
stores/cash.ts
Normal file
114
stores/cash.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useCashStore = defineStore("cash", () => {
|
||||
// 13-week cash flow events
|
||||
const cashEvents = ref([]);
|
||||
|
||||
// Payment queue - staged payments within policy
|
||||
const paymentQueue = ref([]);
|
||||
|
||||
// Week that first breaches minimum cushion
|
||||
const firstBreachWeek = ref(null);
|
||||
|
||||
// Current cash and savings balances
|
||||
const currentCash = ref(5000);
|
||||
const currentSavings = ref(8000);
|
||||
|
||||
// Computed weekly projections
|
||||
const weeklyProjections = computed(() => {
|
||||
const weeks = [];
|
||||
let runningBalance = currentCash.value;
|
||||
|
||||
for (let week = 1; week <= 13; week++) {
|
||||
const weekEvents = cashEvents.value.filter((e) => e.week === week);
|
||||
const weekInflow = weekEvents
|
||||
.filter((e) => e.type === "Influx")
|
||||
.reduce((sum, e) => sum + e.amount, 0);
|
||||
const weekOutflow = weekEvents
|
||||
.filter((e) => e.type === "Outflow")
|
||||
.reduce((sum, e) => sum + e.amount, 0);
|
||||
|
||||
const net = weekInflow - weekOutflow;
|
||||
runningBalance += net;
|
||||
|
||||
weeks.push({
|
||||
number: week,
|
||||
inflow: weekInflow,
|
||||
outflow: weekOutflow,
|
||||
net,
|
||||
balance: runningBalance,
|
||||
cushion: runningBalance, // Will be calculated properly later
|
||||
breachesCushion: false, // Will be calculated properly later
|
||||
});
|
||||
}
|
||||
|
||||
return weeks;
|
||||
});
|
||||
|
||||
// Actions
|
||||
function addCashEvent(event) {
|
||||
cashEvents.value.push({
|
||||
id: Date.now().toString(),
|
||||
date: event.date,
|
||||
week: event.week,
|
||||
type: event.type, // Influx|Outflow
|
||||
amount: event.amount,
|
||||
sourceRef: event.sourceRef,
|
||||
policyTag: event.policyTag, // Payroll|Tax|Vendor|SavingsSweep
|
||||
...event,
|
||||
});
|
||||
}
|
||||
|
||||
function updateCashEvent(id, updates) {
|
||||
const event = cashEvents.value.find((e) => e.id === id);
|
||||
if (event) {
|
||||
Object.assign(event, updates);
|
||||
}
|
||||
}
|
||||
|
||||
function removeCashEvent(id) {
|
||||
const index = cashEvents.value.findIndex((e) => e.id === id);
|
||||
if (index > -1) {
|
||||
cashEvents.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function addToPaymentQueue(payment) {
|
||||
paymentQueue.value.push({
|
||||
id: Date.now().toString(),
|
||||
amount: payment.amount,
|
||||
recipient: payment.recipient,
|
||||
scheduledWeek: payment.scheduledWeek,
|
||||
priority: payment.priority,
|
||||
canStage: payment.canStage !== false,
|
||||
...payment,
|
||||
});
|
||||
}
|
||||
|
||||
function stagePayment(paymentId, newWeek) {
|
||||
const payment = paymentQueue.value.find((p) => p.id === paymentId);
|
||||
if (payment && payment.canStage) {
|
||||
payment.scheduledWeek = newWeek;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentBalances(cash, savings) {
|
||||
currentCash.value = cash;
|
||||
currentSavings.value = savings;
|
||||
}
|
||||
|
||||
return {
|
||||
cashEvents: readonly(cashEvents),
|
||||
paymentQueue: readonly(paymentQueue),
|
||||
firstBreachWeek: readonly(firstBreachWeek),
|
||||
currentCash: readonly(currentCash),
|
||||
currentSavings: readonly(currentSavings),
|
||||
weeklyProjections,
|
||||
addCashEvent,
|
||||
updateCashEvent,
|
||||
removeCashEvent,
|
||||
addToPaymentQueue,
|
||||
stagePayment,
|
||||
updateCurrentBalances,
|
||||
};
|
||||
});
|
||||
217
stores/members.ts
Normal file
217
stores/members.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useMembersStore = defineStore(
|
||||
"members",
|
||||
() => {
|
||||
// Member list and profiles
|
||||
const members = ref([]);
|
||||
|
||||
// Schema version for persistence
|
||||
const schemaVersion = "1.0";
|
||||
|
||||
// Capacity totals across all members
|
||||
const capacityTotals = computed(() => ({
|
||||
minHours: members.value.reduce(
|
||||
(sum, m) => sum + (m.capacity?.minHours || 0),
|
||||
0
|
||||
),
|
||||
targetHours: members.value.reduce(
|
||||
(sum, m) => sum + (m.capacity?.targetHours || 0),
|
||||
0
|
||||
),
|
||||
maxHours: members.value.reduce(
|
||||
(sum, m) => sum + (m.capacity?.maxHours || 0),
|
||||
0
|
||||
),
|
||||
}));
|
||||
|
||||
// Privacy flags - aggregate vs individual visibility
|
||||
const privacyFlags = ref({
|
||||
showIndividualNeeds: false,
|
||||
showIndividualCapacity: false,
|
||||
stewardOnlyDetails: true,
|
||||
});
|
||||
|
||||
// Normalize a member object to ensure required structure and sane defaults
|
||||
function normalizeMember(raw) {
|
||||
const normalized = {
|
||||
id: raw.id || Date.now().toString(),
|
||||
displayName: typeof raw.displayName === "string" ? raw.displayName : "",
|
||||
roleFocus: typeof raw.roleFocus === "string" ? raw.roleFocus : "",
|
||||
payRelationship: raw.payRelationship || "FullyPaid",
|
||||
capacity: {
|
||||
minHours: Number(raw.capacity?.minHours) || 0,
|
||||
targetHours: Number(raw.capacity?.targetHours) || 0,
|
||||
maxHours: Number(raw.capacity?.maxHours) || 0,
|
||||
},
|
||||
riskBand: raw.riskBand || "Medium",
|
||||
externalCoveragePct: Number(raw.externalCoveragePct ?? 0),
|
||||
privacyNeeds: raw.privacyNeeds || "aggregate_ok",
|
||||
deferredHours: Number(raw.deferredHours ?? 0),
|
||||
quarterlyDeferredCap: Number(raw.quarterlyDeferredCap ?? 240),
|
||||
...raw,
|
||||
};
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Initialize normalization for any persisted members
|
||||
if (Array.isArray(members.value) && members.value.length) {
|
||||
members.value = members.value.map(normalizeMember);
|
||||
}
|
||||
|
||||
// Validation details for debugging
|
||||
const validationDetails = computed(() =>
|
||||
members.value.map((m) => {
|
||||
const nameOk =
|
||||
typeof m.displayName === "string" && m.displayName.trim().length > 0;
|
||||
const relOk = Boolean(m.payRelationship);
|
||||
const target = Number(m.capacity?.targetHours);
|
||||
const targetOk = Number.isFinite(target) && target > 0;
|
||||
const pctRaw = m.externalCoveragePct;
|
||||
const pct =
|
||||
pctRaw === undefined || pctRaw === null ? 0 : Number(pctRaw);
|
||||
const pctOk =
|
||||
pctRaw === undefined ||
|
||||
pctRaw === null ||
|
||||
(Number.isFinite(pct) && pct >= 0 && pct <= 100);
|
||||
return {
|
||||
id: m.id,
|
||||
displayName: m.displayName,
|
||||
payRelationship: m.payRelationship,
|
||||
targetHours: m.capacity?.targetHours,
|
||||
externalCoveragePct: m.externalCoveragePct,
|
||||
checks: { nameOk, relOk, targetOk, pctOk },
|
||||
valid: nameOk && relOk && targetOk && pctOk,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Validation computed (robust against NaN/empty values)
|
||||
const isValid = computed(() => {
|
||||
// A member is valid if core required fields pass
|
||||
const isMemberValid = (m: any) => {
|
||||
const nameOk =
|
||||
typeof m.displayName === "string" && m.displayName.trim().length > 0;
|
||||
const relOk = Boolean(m.payRelationship);
|
||||
const target = Number(m.capacity?.targetHours);
|
||||
const targetOk = Number.isFinite(target) && target > 0;
|
||||
// External coverage is optional; when present it must be within 0..100
|
||||
const pctRaw = m.externalCoveragePct;
|
||||
const pct =
|
||||
pctRaw === undefined || pctRaw === null ? 0 : Number(pctRaw);
|
||||
const pctOk =
|
||||
pctRaw === undefined ||
|
||||
pctRaw === null ||
|
||||
(Number.isFinite(pct) && pct >= 0 && pct <= 100);
|
||||
return nameOk && relOk && targetOk && pctOk;
|
||||
};
|
||||
|
||||
// Require at least one valid member
|
||||
return members.value.some(isMemberValid);
|
||||
});
|
||||
|
||||
// Wizard-required actions
|
||||
function upsertMember(member) {
|
||||
const existingIndex = members.value.findIndex((m) => m.id === member.id);
|
||||
if (existingIndex > -1) {
|
||||
members.value[existingIndex] = normalizeMember({
|
||||
...members.value[existingIndex],
|
||||
...member,
|
||||
});
|
||||
} else {
|
||||
members.value.push(
|
||||
normalizeMember({
|
||||
id: member.id || Date.now().toString(),
|
||||
...member,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setCapacity(memberId, capacity) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member) {
|
||||
member.capacity = { ...member.capacity, ...capacity };
|
||||
}
|
||||
}
|
||||
|
||||
function setPayRelationship(memberId, payRelationship) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member) {
|
||||
member.payRelationship = payRelationship;
|
||||
}
|
||||
}
|
||||
|
||||
function setRiskBand(memberId, riskBand) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member) {
|
||||
member.riskBand = riskBand;
|
||||
}
|
||||
}
|
||||
|
||||
function setExternalCoveragePct(memberId, pct) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member && pct >= 0 && pct <= 100) {
|
||||
member.externalCoveragePct = pct;
|
||||
}
|
||||
}
|
||||
|
||||
function setPrivacy(memberId, privacyNeeds) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member) {
|
||||
member.privacyNeeds = privacyNeeds;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
function addMember(member) {
|
||||
upsertMember(member);
|
||||
}
|
||||
|
||||
function updateMember(id, updates) {
|
||||
const member = members.value.find((m) => m.id === id);
|
||||
if (member) {
|
||||
Object.assign(member, updates);
|
||||
}
|
||||
}
|
||||
|
||||
function removeMember(id) {
|
||||
const index = members.value.findIndex((m) => m.id === id);
|
||||
if (index > -1) {
|
||||
members.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset function
|
||||
function resetMembers() {
|
||||
members.value = [];
|
||||
}
|
||||
|
||||
return {
|
||||
members,
|
||||
capacityTotals,
|
||||
privacyFlags,
|
||||
validationDetails,
|
||||
isValid,
|
||||
schemaVersion,
|
||||
// Wizard actions
|
||||
upsertMember,
|
||||
setCapacity,
|
||||
setPayRelationship,
|
||||
setRiskBand,
|
||||
setExternalCoveragePct,
|
||||
setPrivacy,
|
||||
resetMembers,
|
||||
// Legacy actions
|
||||
addMember,
|
||||
updateMember,
|
||||
removeMember,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: "urgent-tools-members",
|
||||
paths: ["members", "privacyFlags"],
|
||||
},
|
||||
}
|
||||
);
|
||||
182
stores/policies.ts
Normal file
182
stores/policies.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const usePoliciesStore = defineStore(
|
||||
"policies",
|
||||
() => {
|
||||
// Schema version for persistence
|
||||
const schemaVersion = "1.0";
|
||||
|
||||
// Core policies
|
||||
const equalHourlyWage = ref(0);
|
||||
const payrollOncostPct = ref(25);
|
||||
const savingsTargetMonths = ref(3);
|
||||
const minCashCushionAmount = ref(3000);
|
||||
|
||||
// Deferred pay limits
|
||||
const deferredCapHoursPerQtr = ref(240);
|
||||
const deferredSunsetMonths = ref(12);
|
||||
|
||||
// Surplus distribution order
|
||||
const surplusOrder = ref([
|
||||
"Deferred",
|
||||
"Savings",
|
||||
"Hardship",
|
||||
"Training",
|
||||
"Patronage",
|
||||
"Retained",
|
||||
]);
|
||||
|
||||
// Payment priority order
|
||||
const paymentPriority = ref(["Payroll", "Taxes", "CriticalOps", "Vendors"]);
|
||||
|
||||
// Volunteer scope - allowed flows
|
||||
const volunteerScope = ref({
|
||||
allowedFlows: ["Care", "SharedLearning"],
|
||||
});
|
||||
|
||||
// Validation computed
|
||||
const isValid = computed(() => {
|
||||
return (
|
||||
equalHourlyWage.value > 0 &&
|
||||
payrollOncostPct.value >= 0 &&
|
||||
payrollOncostPct.value <= 100 &&
|
||||
savingsTargetMonths.value >= 0 &&
|
||||
minCashCushionAmount.value >= 0 &&
|
||||
deferredCapHoursPerQtr.value >= 0 &&
|
||||
deferredSunsetMonths.value >= 0
|
||||
);
|
||||
});
|
||||
|
||||
// Wizard-required actions
|
||||
function setEqualWage(amount) {
|
||||
if (amount > 0) {
|
||||
equalHourlyWage.value = amount;
|
||||
}
|
||||
}
|
||||
|
||||
function setOncostPct(pct) {
|
||||
if (pct >= 0 && pct <= 100) {
|
||||
payrollOncostPct.value = pct;
|
||||
}
|
||||
}
|
||||
|
||||
function setSavingsTargetMonths(months) {
|
||||
if (months >= 0) {
|
||||
savingsTargetMonths.value = months;
|
||||
}
|
||||
}
|
||||
|
||||
function setMinCashCushion(amount) {
|
||||
if (amount >= 0) {
|
||||
minCashCushionAmount.value = amount;
|
||||
}
|
||||
}
|
||||
|
||||
function setDeferredCap(hours) {
|
||||
if (hours >= 0) {
|
||||
deferredCapHoursPerQtr.value = hours;
|
||||
}
|
||||
}
|
||||
|
||||
function setDeferredSunset(months) {
|
||||
if (months >= 0) {
|
||||
deferredSunsetMonths.value = months;
|
||||
}
|
||||
}
|
||||
|
||||
function setVolunteerScope(allowedFlows) {
|
||||
volunteerScope.value = { allowedFlows: [...allowedFlows] };
|
||||
}
|
||||
|
||||
function setSurplusOrder(order) {
|
||||
surplusOrder.value = [...order];
|
||||
}
|
||||
|
||||
function setPaymentPriority(priority) {
|
||||
paymentPriority.value = [...priority];
|
||||
}
|
||||
|
||||
// Legacy actions
|
||||
function updatePolicy(key, value) {
|
||||
if (key === "equalHourlyWage") setEqualWage(value);
|
||||
else if (key === "payrollOncostPct") setOncostPct(value);
|
||||
else if (key === "savingsTargetMonths") setSavingsTargetMonths(value);
|
||||
else if (key === "minCashCushionAmount") setMinCashCushion(value);
|
||||
else if (key === "deferredCapHoursPerQtr") setDeferredCap(value);
|
||||
else if (key === "deferredSunsetMonths") setDeferredSunset(value);
|
||||
}
|
||||
|
||||
function updateSurplusOrder(newOrder) {
|
||||
setSurplusOrder(newOrder);
|
||||
}
|
||||
|
||||
function updatePaymentPriority(newOrder) {
|
||||
setPaymentPriority(newOrder);
|
||||
}
|
||||
|
||||
// Reset function
|
||||
function resetPolicies() {
|
||||
equalHourlyWage.value = 0;
|
||||
payrollOncostPct.value = 25;
|
||||
savingsTargetMonths.value = 3;
|
||||
minCashCushionAmount.value = 3000;
|
||||
deferredCapHoursPerQtr.value = 240;
|
||||
deferredSunsetMonths.value = 12;
|
||||
surplusOrder.value = [
|
||||
"Deferred",
|
||||
"Savings",
|
||||
"Hardship",
|
||||
"Training",
|
||||
"Patronage",
|
||||
"Retained",
|
||||
];
|
||||
paymentPriority.value = ["Payroll", "Taxes", "CriticalOps", "Vendors"];
|
||||
volunteerScope.value = { allowedFlows: ["Care", "SharedLearning"] };
|
||||
}
|
||||
|
||||
return {
|
||||
equalHourlyWage,
|
||||
payrollOncostPct,
|
||||
savingsTargetMonths,
|
||||
minCashCushionAmount,
|
||||
deferredCapHoursPerQtr,
|
||||
deferredSunsetMonths,
|
||||
surplusOrder,
|
||||
paymentPriority,
|
||||
volunteerScope,
|
||||
isValid,
|
||||
schemaVersion,
|
||||
// Wizard actions
|
||||
setEqualWage,
|
||||
setOncostPct,
|
||||
setSavingsTargetMonths,
|
||||
setMinCashCushion,
|
||||
setDeferredCap,
|
||||
setDeferredSunset,
|
||||
setVolunteerScope,
|
||||
setSurplusOrder,
|
||||
setPaymentPriority,
|
||||
resetPolicies,
|
||||
// Legacy actions
|
||||
updatePolicy,
|
||||
updateSurplusOrder,
|
||||
updatePaymentPriority,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: "urgent-tools-policies",
|
||||
paths: [
|
||||
"equalHourlyWage",
|
||||
"payrollOncostPct",
|
||||
"savingsTargetMonths",
|
||||
"minCashCushionAmount",
|
||||
"deferredCapHoursPerQtr",
|
||||
"deferredSunsetMonths",
|
||||
"surplusOrder",
|
||||
"paymentPriority",
|
||||
"volunteerScope",
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
95
stores/scenarios.ts
Normal file
95
stores/scenarios.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useScenariosStore = defineStore("scenarios", () => {
|
||||
// Scenario presets
|
||||
const presets = ref({
|
||||
current: {
|
||||
name: "Operate Current Plan",
|
||||
description: "Continue with existing revenue and capacity",
|
||||
settings: {},
|
||||
},
|
||||
quitDayJobs: {
|
||||
name: "Quit Day Jobs",
|
||||
description: "Members leave external work, increase co-op hours",
|
||||
settings: {
|
||||
targetHoursMultiplier: 1.5,
|
||||
externalCoverageReduction: 0.8,
|
||||
},
|
||||
},
|
||||
startProduction: {
|
||||
name: "Start Production",
|
||||
description: "Launch product development phase",
|
||||
settings: {
|
||||
productionCostsIncrease: 2000,
|
||||
effortHoursIncrease: 100,
|
||||
},
|
||||
},
|
||||
sixMonth: {
|
||||
name: "6-Month Plan",
|
||||
description: "Extended planning horizon",
|
||||
settings: {
|
||||
timeHorizonMonths: 6,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// What-if sliders state
|
||||
const sliders = ref({
|
||||
monthlyRevenue: 12000,
|
||||
paidHoursPerMonth: 320,
|
||||
winRatePct: 70,
|
||||
avgPayoutDelayDays: 30,
|
||||
hourlyWageAdjust: 0,
|
||||
});
|
||||
|
||||
// Selected scenario
|
||||
const activeScenario = ref("current");
|
||||
|
||||
// Computed scenario results (will be calculated by composables)
|
||||
const scenarioResults = computed(() => ({
|
||||
runway: 2.8,
|
||||
monthlyCosts: 8700,
|
||||
cashflowRisk: "medium",
|
||||
recommendations: [],
|
||||
}));
|
||||
|
||||
// Actions
|
||||
function setActiveScenario(scenarioKey) {
|
||||
activeScenario.value = scenarioKey;
|
||||
}
|
||||
|
||||
function updateSlider(key, value) {
|
||||
if (key in sliders.value) {
|
||||
sliders.value[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function resetSliders() {
|
||||
sliders.value = {
|
||||
monthlyRevenue: 12000,
|
||||
paidHoursPerMonth: 320,
|
||||
winRatePct: 70,
|
||||
avgPayoutDelayDays: 30,
|
||||
hourlyWageAdjust: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function saveCustomScenario(name, settings) {
|
||||
presets.value[name.toLowerCase().replace(/\s+/g, "")] = {
|
||||
name,
|
||||
description: "Custom scenario",
|
||||
settings,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
presets: readonly(presets),
|
||||
sliders,
|
||||
activeScenario: readonly(activeScenario),
|
||||
scenarioResults,
|
||||
setActiveScenario,
|
||||
updateSlider,
|
||||
resetSliders,
|
||||
saveCustomScenario,
|
||||
};
|
||||
});
|
||||
132
stores/session.ts
Normal file
132
stores/session.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useSessionStore = defineStore("session", () => {
|
||||
// Value Accounting session checklist state
|
||||
const checklist = ref({
|
||||
monthClosed: false,
|
||||
contributionsLogged: false,
|
||||
surplusCalculated: false,
|
||||
needsReviewed: false,
|
||||
});
|
||||
|
||||
// Draft distribution allocations
|
||||
const draftAllocations = ref({
|
||||
deferredRepay: 0,
|
||||
savings: 0,
|
||||
hardship: 0,
|
||||
training: 0,
|
||||
patronage: 0,
|
||||
retained: 0,
|
||||
});
|
||||
|
||||
// Session rationale text
|
||||
const rationale = ref("");
|
||||
|
||||
// Current session period
|
||||
const currentSession = ref("2024-01");
|
||||
|
||||
// Saved distribution records
|
||||
const savedRecords = ref([]);
|
||||
|
||||
// Available amounts for distribution
|
||||
const availableAmounts = ref({
|
||||
surplus: 0,
|
||||
deferredOwed: 0,
|
||||
savingsNeeded: 0,
|
||||
});
|
||||
|
||||
// Computed total allocated
|
||||
const totalAllocated = computed(() =>
|
||||
Object.values(draftAllocations.value).reduce(
|
||||
(sum, amount) => sum + amount,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
// Computed checklist completion
|
||||
const checklistComplete = computed(() =>
|
||||
Object.values(checklist.value).every(Boolean)
|
||||
);
|
||||
|
||||
// Actions
|
||||
function updateChecklistItem(key, value) {
|
||||
if (key in checklist.value) {
|
||||
checklist.value[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllocation(key, amount) {
|
||||
if (key in draftAllocations.value) {
|
||||
draftAllocations.value[key] = Number(amount) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
function resetAllocations() {
|
||||
Object.keys(draftAllocations.value).forEach((key) => {
|
||||
draftAllocations.value[key] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function updateRationale(text) {
|
||||
rationale.value = text;
|
||||
}
|
||||
|
||||
function saveSession() {
|
||||
const record = {
|
||||
id: Date.now().toString(),
|
||||
period: currentSession.value,
|
||||
date: new Date().toISOString(),
|
||||
allocations: { ...draftAllocations.value },
|
||||
rationale: rationale.value,
|
||||
checklist: { ...checklist.value },
|
||||
};
|
||||
|
||||
savedRecords.value.push(record);
|
||||
|
||||
// Reset for next session
|
||||
resetAllocations();
|
||||
rationale.value = "";
|
||||
Object.keys(checklist.value).forEach((key) => {
|
||||
checklist.value[key] = false;
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
function loadSession(period) {
|
||||
const record = savedRecords.value.find((r) => r.period === period);
|
||||
if (record) {
|
||||
currentSession.value = period;
|
||||
Object.assign(draftAllocations.value, record.allocations);
|
||||
rationale.value = record.rationale;
|
||||
Object.assign(checklist.value, record.checklist);
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentSession(period) {
|
||||
currentSession.value = period;
|
||||
}
|
||||
|
||||
function updateAvailableAmounts(amounts) {
|
||||
Object.assign(availableAmounts.value, amounts);
|
||||
}
|
||||
|
||||
return {
|
||||
checklist,
|
||||
draftAllocations,
|
||||
rationale,
|
||||
currentSession: readonly(currentSession),
|
||||
savedRecords: readonly(savedRecords),
|
||||
availableAmounts: readonly(availableAmounts),
|
||||
totalAllocated,
|
||||
checklistComplete,
|
||||
updateChecklistItem,
|
||||
updateAllocation,
|
||||
resetAllocations,
|
||||
updateRationale,
|
||||
saveSession,
|
||||
loadSession,
|
||||
setCurrentSession,
|
||||
updateAvailableAmounts,
|
||||
};
|
||||
});
|
||||
117
stores/streams.ts
Normal file
117
stores/streams.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useStreamsStore = defineStore(
|
||||
"streams",
|
||||
() => {
|
||||
// Schema version for persistence
|
||||
const schemaVersion = "1.0";
|
||||
|
||||
// Revenue streams with all properties
|
||||
const streams = ref([]);
|
||||
|
||||
// Computed totals
|
||||
const totalTargetPct = computed(() =>
|
||||
streams.value.reduce((sum, stream) => sum + (stream.targetPct || 0), 0)
|
||||
);
|
||||
|
||||
const totalMonthlyAmount = computed(() =>
|
||||
streams.value.reduce(
|
||||
(sum, stream) => sum + (stream.targetMonthlyAmount || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
// Validation computed
|
||||
const targetPctDeviation = computed(() => {
|
||||
const total = totalTargetPct.value;
|
||||
return Math.abs(100 - total);
|
||||
});
|
||||
|
||||
const hasValidStreams = computed(() => {
|
||||
return streams.value.every(
|
||||
(stream) =>
|
||||
stream.name &&
|
||||
stream.category &&
|
||||
stream.payoutDelayDays >= 0 &&
|
||||
(stream.targetPct >= 0 || stream.targetMonthlyAmount >= 0)
|
||||
);
|
||||
});
|
||||
|
||||
// Wizard-required actions
|
||||
function upsertStream(stream) {
|
||||
const existingIndex = streams.value.findIndex((s) => s.id === stream.id);
|
||||
if (existingIndex > -1) {
|
||||
streams.value[existingIndex] = {
|
||||
...streams.value[existingIndex],
|
||||
...stream,
|
||||
};
|
||||
} else {
|
||||
const newStream = {
|
||||
id: stream.id || Date.now().toString(),
|
||||
name: stream.name,
|
||||
category: stream.category,
|
||||
subcategory: stream.subcategory || "",
|
||||
targetPct: stream.targetPct || 0,
|
||||
targetMonthlyAmount: stream.targetMonthlyAmount || 0,
|
||||
certainty: stream.certainty || "Aspirational", // Committed|Probable|Aspirational
|
||||
payoutDelayDays: stream.payoutDelayDays || 0,
|
||||
terms: stream.terms || "",
|
||||
revenueSharePct: stream.revenueSharePct || 0,
|
||||
platformFeePct: stream.platformFeePct || 0,
|
||||
restrictions: stream.restrictions || "General", // Restricted|General
|
||||
seasonalityWeights:
|
||||
stream.seasonalityWeights || new Array(12).fill(1),
|
||||
effortHoursPerMonth: stream.effortHoursPerMonth || 0,
|
||||
...stream,
|
||||
};
|
||||
streams.value.push(newStream);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy actions
|
||||
function addStream(stream) {
|
||||
upsertStream(stream);
|
||||
}
|
||||
|
||||
function updateStream(id, updates) {
|
||||
const stream = streams.value.find((s) => s.id === id);
|
||||
if (stream) {
|
||||
Object.assign(stream, updates);
|
||||
}
|
||||
}
|
||||
|
||||
function removeStream(id) {
|
||||
const index = streams.value.findIndex((s) => s.id === id);
|
||||
if (index > -1) {
|
||||
streams.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset function
|
||||
function resetStreams() {
|
||||
streams.value = [];
|
||||
}
|
||||
|
||||
return {
|
||||
streams,
|
||||
totalTargetPct,
|
||||
totalMonthlyAmount,
|
||||
targetPctDeviation,
|
||||
hasValidStreams,
|
||||
schemaVersion,
|
||||
// Wizard actions
|
||||
upsertStream,
|
||||
resetStreams,
|
||||
// Legacy actions
|
||||
addStream,
|
||||
updateStream,
|
||||
removeStream,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: "urgent-tools-streams",
|
||||
paths: ["streams"],
|
||||
},
|
||||
}
|
||||
);
|
||||
28
stores/wizard.ts
Normal file
28
stores/wizard.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useWizardStore = defineStore(
|
||||
"wizard",
|
||||
() => {
|
||||
const currentStep = ref(1);
|
||||
|
||||
function setStep(step: number) {
|
||||
currentStep.value = Math.min(Math.max(1, step), 5);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentStep.value = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
setStep,
|
||||
reset,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: "urgent-tools-wizard",
|
||||
paths: ["currentStep"],
|
||||
},
|
||||
}
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue