import React, { useEffect, useMemo, useState } from "react";
import { ResponsiveContainer, PieChart, Pie, Cell, Tooltip as RTooltip, BarChart, Bar, XAxis, YAxis, CartesianGrid, LineChart, Line, Legend } from "recharts";
// =====================
// UTILITIES & CONSTANTS
// =====================
const CATEGORIES = [
{ key: "hayriya", name: "Hayriya", percent: 2.5, source: "Bank" },
{ key: "otaOnaga", name: "Ota-onaga", percent: 10, source: "Bank" },
{ key: "kelajak", name: "Kelajak uchun", percent: 8.75, source: "Bank" },
{ key: "oyin", name: "O'yin-kulgi", percent: 8.75, source: "Bank" },
{ key: "kapital", name: "Kapital", percent: 30.8, source: "Bank" },
{ key: "kattaOrzu", name: "Katta orzular", percent: 3.85, source: "Bank" },
{ key: "kichikOrzu", name: "Kichik orzular", percent: 3.85, source: "Uyda" },
{ key: "shaxsiy", name: "Shaxsiy hisob", percent: 31.5, source: "Uyda" },
];
const DEFAULT_BUSINESSES = [
{ id: "nini", name: "NINI bog'cha", active: true },
{ id: "restoran", name: "Restoran", active: true },
{ id: "online", name: "Onlayn savdo", active: true },
];
const thousand = (n) =>
(n ?? 0).toLocaleString("uz-UZ", { maximumFractionDigits: 0 });
const currency = (n) => `${thousand(n)} so'm`;
const ls = {
get: (k, v) => {
try {
const raw = localStorage.getItem(k);
return raw ? JSON.parse(raw) : v;
} catch (e) {
return v;
}
},
set: (k, v) => localStorage.setItem(k, JSON.stringify(v)),
};
const nowYYYYMM = () => {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
};
// =====================
// ROOT COMPONENT
// =====================
export default function App() {
const [activeTab, setActiveTab] = useState("dashboard");
// Global settings/state saved in LS
const [month, setMonth] = useState(ls.get("month", nowYYYYMM()));
const [incomePlan, setIncomePlan] = useState(ls.get("incomePlan", 10000000));
const [businesses, setBusinesses] = useState(ls.get("businesses", DEFAULT_BUSINESSES));
const [allowOverspend, setAllowOverspend] = useState(ls.get("allowOverspend", false));
// Data collections by month
const [incomes, setIncomes] = useState(ls.get("incomes", {})); // {YYYY-MM: [{date,business,amount, note}]}
const [expenses, setExpenses] = useState(ls.get("expenses", {})); // {YYYY-MM: [{date,category,amount,source,note}]}
const [transfers, setTransfers] = useState(ls.get("transfers", {})); // {YYYY-MM: [{date, from, to, amount, note}]}
const [goals, setGoals] = useState(ls.get("goals", [])); // static list (progress tied to transfers)
const [debts, setDebts] = useState(ls.get("debts", { given: [], taken: [] }));
// Autosave
useEffect(() => ls.set("month", month), [month]);
useEffect(() => ls.set("incomePlan", incomePlan), [incomePlan]);
useEffect(() => ls.set("businesses", businesses), [businesses]);
useEffect(() => ls.set("allowOverspend", allowOverspend), [allowOverspend]);
useEffect(() => ls.set("incomes", incomes), [incomes]);
useEffect(() => ls.set("expenses", expenses), [expenses]);
useEffect(() => ls.set("transfers", transfers), [transfers]);
useEffect(() => ls.set("goals", goals), [goals]);
useEffect(() => ls.set("debts", debts), [debts]);
// Derived: category planned amounts
const plans = useMemo(() => {
const obj = {};
CATEGORIES.forEach((c) => (obj[c.key] = Math.round(incomePlan * (c.percent / 100))));
return obj;
}, [incomePlan]);
// Get month arrays helper
const getArr = (dict) => dict[month] ?? [];
// Sum helpers
const sumBy = (arr, key) => arr.reduce((s, x) => s + (x[key] ? Number(x[key]) : 0), 0);
// Actual spend per category
const spendByCategory = useMemo(() => {
const e = getArr(expenses);
const res = Object.fromEntries(CATEGORIES.map((c) => [c.key, 0]));
e.forEach((x) => {
res[x.category] = (res[x.category] || 0) + Number(x.amount);
});
return res;
}, [expenses, month]);
// Transfers impact: from category to goals (only from Kapital by rule, but UI allows generic – we enforce rule on UI)
const transfersByCategory = useMemo(() => {
const t = getArr(transfers);
const out = Object.fromEntries(CATEGORIES.map((c) => [c.key, 0]));
const incoming = Object.fromEntries(CATEGORIES.map((c) => [c.key, 0]));
t.forEach((x) => {
out[x.from] = (out[x.from] || 0) + Number(x.amount);
incoming[x.to] = (incoming[x.to] || 0) + Number(x.amount);
});
return { out, incoming };
}, [transfers, month]);
// Balances (per category)
const balances = useMemo(() => {
const obj = {};
CATEGORIES.forEach((c) => {
const used = (spendByCategory[c.key] || 0) + (transfersByCategory.out[c.key] || 0);
obj[c.key] = plans[c.key] - used;
});
return obj;
}, [plans, spendByCategory, transfersByCategory]);
const monthDaysLeft = useMemo(() => {
const [y, m] = month.split("-").map(Number);
const today = new Date();
const lastDay = new Date(y, m, 0).getDate();
let day = today.getFullYear() === y && today.getMonth() + 1 === m ? today.getDate() : 1;
// remaining days including today
return lastDay - day + 1;
}, [month]);
const totalIncomeFact = useMemo(() => sumBy(getArr(incomes), "amount"), [incomes, month]);
const totalExpenseFact = useMemo(() => sumBy(getArr(expenses), "amount"), [expenses, month]);
// Charts data
const pieData = CATEGORIES.map((c) => ({ name: c.name, value: plans[c.key] }));
const planVsFactData = CATEGORIES.map((c) => ({
name: c.name,
Reja: plans[c.key],
Fakt: spendByCategory[c.key] || 0,
}));
const COLORS = ["#8884d8", "#82ca9d", "#ffc658", "#8dd1e1", "#a4de6c", "#d0ed57", "#ffc0cb", "#d88884"]; // default palette
// ============ UI HELPERS ============
const Section = ({ title, children, right }) => (
{title}
{right}
{children}
);
const Input = ({ label, ...props }) => (
);
const Select = ({ label, children, ...props }) => (
);
const Button = ({ children, className = "", ...props }) => (
);
// ============ ACTION HANDLERS ============
const addIncome = (row) => {
const m = { ...incomes };
const arr = m[month] ? [...m[month]] : [];
arr.push(row);
m[month] = arr;
setIncomes(m);
};
const addExpense = (row) => {
// Overspend validation
const nextUsed = (spendByCategory[row.category] || 0) + Number(row.amount);
const willBalance = plans[row.category] - nextUsed - (transfersByCategory.out[row.category] || 0);
if (!allowOverspend && willBalance < 0) {
alert("Bu toifadagi reja mablag'i yetarli emas (overspend taqiqlangan). Sozlamalardan yoqishingiz mumkin.");
return;
}
const m = { ...expenses };
const arr = m[month] ? [...m[month]] : [];
arr.push(row);
m[month] = arr;
setExpenses(m);
};
const addTransfer = (row) => {
// Enforce rule: only from Kapital -> Goal (special pseudo-category "goal:
") or to other categories if needed
if (row.from !== "kapital") {
alert("Qoidaga ko'ra ko'chirish faqat 'Kapital' toifasidan amalga oshiriladi.");
return;
}
const m = { ...transfers };
const arr = m[month] ? [...m[month]] : [];
arr.push(row);
m[month] = arr;
setTransfers(m);
};
const goalProgress = (goalId) => {
// Sum of transfers to pseudo target `goal:` in all months
let total = 0;
Object.keys(transfers).forEach((mm) => {
(transfers[mm] || []).forEach((t) => {
if (t.to === `goal:${goalId}`) total += Number(t.amount);
});
});
return total;
};
const exportCSV = () => {
const rows = [];
rows.push(["MONTH", month]);
rows.push([]);
rows.push(["PLAN", "Percent", "Amount"]);
CATEGORIES.forEach((c) => rows.push([c.name, c.percent, plans[c.key]]));
rows.push([]);
rows.push(["INCOMES", "date", "business", "amount", "note"]);
getArr(incomes).forEach((r) => rows.push(["", r.date, r.business, r.amount, r.note || ""]));
rows.push([]);
rows.push(["EXPENSES", "date", "category", "amount", "source", "note"]);
getArr(expenses).forEach((r) => rows.push(["", r.date, r.category, r.amount, r.source, r.note || ""]));
rows.push([]);
rows.push(["TRANSFERS", "date", "from", "to", "amount", "note"]);
getArr(transfers).forEach((r) => rows.push(["", r.date, r.from, r.to, r.amount, r.note || ""]));
const csv = rows.map((r) => r.map((x) => `"${(x ?? "").toString().replaceAll('"', '""')}"`).join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `moliyaviy_dastur_${month}.csv`;
a.click();
URL.revokeObjectURL(url);
};
// ============ SUB VIEWS ============
const Dashboard = () => (
}
> a + b, 0)) / Math.max(1, monthDaysLeft))))} />
{pieData.map((entry, index) => (
|
))}
currency(v)} />
{CATEGORIES.map((c) => {
const used = (spendByCategory[c.key] || 0) + (transfersByCategory.out[c.key] || 0);
const pct = Math.min(100, Math.round((used / Math.max(1, plans[c.key])) * 100));
const over = used > plans[c.key];
return (
{c.name} {currency(used)} / {currency(plans[c.key])} ({pct}%)
Qoldiq: {currency(balances[c.key])}
);
})}
);
}
function FaktView() {
const [inc, setInc] = useState({ date: todayStr(), business: businesses[0]?.id || "", amount: 0, note: "" });
const [exp, setExp] = useState({ date: todayStr(), category: CATEGORIES[0].key, amount: 0, source: "Bank", note: "" });
return (
);
}
function BusinessView() {
const [name, setName] = useState("");
return (
);
}
function DebtsView() {
const [tab, setTab] = useState("given");
const [row, setRow] = useState({ who: "", start: todayStr(), principal: 0, rate: 0, termMonths: 12, schedule: "oylik", nextDue: todayStr(), note: "" });
const add = () => {
const copy = { ...debts };
copy[tab] = [...copy[tab], row];
setDebts(copy);
setRow({ who: "", start: todayStr(), principal: 0, rate: 0, termMonths: 12, schedule: "oylik", nextDue: todayStr(), note: "" });
};
const series = (list) => list.map((d, i) => ({ name: d.who || `${tab} ${i+1}`, Qoldiq: Math.max(0, d.principal) }));
return (