Files
iistwin/client/src/pages/finance/dashboard.tsx

278 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import { useLocation, useSearch } from "wouter";
import { FinanceLayout } from "@/components/finance/FinanceLayout";
import { FinanceNav, type FinanceTab } from "@/components/finance/FinanceNav";
import { FinanceBottomNav } from "@/components/finance/FinanceBottomNav";
import { ConnectionStatus } from "@/components/finance/ConnectionStatus";
import { DateRangeFilter, type DateRange as LocalDateRange } from "@/components/finance/DateRangeFilter";
import { SummaryCards } from "@/components/finance/SummaryCards";
import { DataTable } from "@/components/finance/DataTable";
import { CategoryTree } from "@/components/finance/CategoryTree";
import { CategoryManager } from "@/components/finance/CategoryManager";
import { FinanceSettingsPanel } from "@/components/finance/FinanceSettings";
import { Button } from "@/components/ui/button";
import { Loader2, RefreshCw } from "lucide-react";
import {
useIncome,
useExpenses,
useIncomeTreeSummary,
useExpenseTreeSummary,
useIncomeCategories,
useExpenseCategories,
useCreateIncomeCategory,
useUpdateIncomeCategory,
useDeleteIncomeCategory,
useCreateExpenseCategory,
useUpdateExpenseCategory,
useDeleteExpenseCategory,
useTableConfig,
useRefreshAll,
} from "@/hooks/useFinance";
function toISODate(d?: Date): string | undefined {
if (!d) return undefined;
return d.toISOString().split("T")[0];
}
function tabFromSearch(search: string): FinanceTab | null {
const tab = new URLSearchParams(search).get("tab");
const valid: FinanceTab[] = [
"dashboard",
"income",
"expenses",
"income-categories",
"expense-categories",
"settings",
];
return valid.includes(tab as FinanceTab) ? (tab as FinanceTab) : null;
}
export default function FinanceDashboard() {
const [search] = useSearch();
const [, navigate] = useLocation();
const [activeTab, setActiveTabState] = useState<FinanceTab>(() => tabFromSearch(search) ?? "dashboard");
const setActiveTab = (tab: FinanceTab) => {
setActiveTabState(tab);
navigate(tab === "dashboard" ? "/finance" : `/finance?tab=${tab}`, { replace: true });
};
const [dateRange, setDateRange] = useState<LocalDateRange>({});
const [incomePage, setIncomePage] = useState(1);
const [expensePage, setExpensePage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [incomeSort, setIncomeSort] = useState({ by: "data", dir: "desc" as "asc" | "desc" });
const [expenseSort, setExpenseSort] = useState({ by: "data", dir: "desc" as "asc" | "desc" });
const [incomeFilters, setIncomeFilters] = useState<Record<string, string>>({});
const [expenseFilters, setExpenseFilters] = useState<Record<string, string>>({});
const dr = { from: toISODate(dateRange.from), to: toISODate(dateRange.to) };
const { data: incomeData, isLoading: incomeLoading } = useIncome(
incomePage, pageSize, incomeSort.by, incomeSort.dir, incomeFilters, dr
);
const { data: expenseData, isLoading: expenseLoading } = useExpenses(
expensePage, pageSize, expenseSort.by, expenseSort.dir, expenseFilters, dr
);
const { data: incomeTree, isLoading: incomeTreeLoading, dataUpdatedAt: incomeTreeUpdatedAt } = useIncomeTreeSummary(dr);
const { data: expenseTree, isLoading: expenseTreeLoading } = useExpenseTreeSummary(dr);
const { data: incomeCats } = useIncomeCategories();
const { data: expenseCats } = useExpenseCategories();
const { data: tableConfig } = useTableConfig();
const refreshAll = useRefreshAll();
const createIncCat = useCreateIncomeCategory();
const updateIncCat = useUpdateIncomeCategory();
const deleteIncCat = useDeleteIncomeCategory();
const createExpCat = useCreateExpenseCategory();
const updateExpCat = useUpdateExpenseCategory();
const deleteExpCat = useDeleteExpenseCategory();
const incomeTotal = incomeTree?.grandTotal?.total ?? 0;
const expenseTotal = expenseTree?.grandTotal?.total ?? 0;
const incomeCount = incomeTree?.grandTotal?.count ?? 0;
const expenseCount = expenseTree?.grandTotal?.count ?? 0;
const columnLabels = tableConfig?.config?.columnLabels ?? {};
const incomeSumCol = tableConfig?.config?.incomeSumColumn ?? "summa";
const expenseSumCol = tableConfig?.config?.expenseSumColumn ?? "summadokumenta";
const handleIncomeSort = (col: string) => {
setIncomeSort((s) => ({
by: col,
dir: s.by === col && s.dir === "desc" ? "asc" : "desc",
}));
};
const handleExpenseSort = (col: string) => {
setExpenseSort((s) => ({
by: col,
dir: s.by === col && s.dir === "desc" ? "asc" : "desc",
}));
};
const visibleIncomeCols = ["data", "summa", "zakazchik", "dogovorzakazchika", "statya", "kommentarii"];
const visibleExpenseCols = ["data", "summadokumenta", "kontragent", "dogovorkontragenta", "statya", "kommentarii"];
return (
<FinanceLayout
mobileNav={<FinanceBottomNav active={activeTab} onChange={setActiveTab} />}
>
<div className="space-y-4">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h1 className="text-xl font-bold">Финансы</h1>
<div className="mt-1">
<ConnectionStatus />
</div>
</div>
<div className="flex items-center gap-2">
<DateRangeFilter value={dateRange} onChange={(r) => { setDateRange(r); setIncomePage(1); setExpensePage(1); }} />
<Button
size="sm"
variant="outline"
className="h-8 text-xs"
onClick={() => refreshAll.mutate()}
disabled={refreshAll.isPending}
>
{refreshAll.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <RefreshCw className="w-3.5 h-3.5" />}
</Button>
{incomeTreeUpdatedAt && (
<span className="text-[10px] text-muted-foreground whitespace-nowrap">
Обновлено: {new Date(incomeTreeUpdatedAt).toLocaleTimeString()}
</span>
)}
</div>
</div>
<FinanceNav active={activeTab} onChange={setActiveTab} />
{/* Dashboard */}
{activeTab === "dashboard" && (
<div className="space-y-4">
<SummaryCards
incomeTotal={incomeTotal}
expenseTotal={expenseTotal}
incomeCount={incomeCount}
expenseCount={expenseCount}
loading={incomeTreeLoading || expenseTreeLoading}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-semibold mb-2">Доходы по категориям</h3>
{incomeTreeLoading ? (
<div className="text-xs text-muted-foreground py-4">Загрузка...</div>
) : (
<CategoryTree
trees={incomeTree?.trees ?? []}
uncategorized={incomeTree?.uncategorized}
storageKey="omegaprom_finance_income_expanded"
onSelect={(id) => {
setActiveTab("income");
// could filter by category here in future
}}
/>
)}
</div>
<div>
<h3 className="text-sm font-semibold mb-2">Расходы по категориям</h3>
{expenseTreeLoading ? (
<div className="text-xs text-muted-foreground py-4">Загрузка...</div>
) : (
<CategoryTree
trees={expenseTree?.trees ?? []}
uncategorized={expenseTree?.uncategorized}
storageKey="omegaprom_finance_expense_expanded"
onSelect={(id) => {
setActiveTab("expenses");
}}
/>
)}
</div>
</div>
</div>
)}
{/* Income */}
{activeTab === "income" && (
<div className="space-y-3">
<h3 className="text-sm font-semibold">Доходы</h3>
<DataTable
data={incomeData}
loading={incomeLoading}
columnLabels={columnLabels}
sumColumn={incomeSumCol}
visibleColumns={visibleIncomeCols}
sortBy={incomeSort.by}
sortDir={incomeSort.dir}
onSort={handleIncomeSort}
filters={incomeFilters}
onFilterChange={(col, val) => {
setIncomeFilters((f) => ({ ...f, [col]: val }));
setIncomePage(1);
}}
page={incomePage}
onPageChange={setIncomePage}
pageSize={pageSize}
onPageSizeChange={setPageSize}
/>
</div>
)}
{/* Expenses */}
{activeTab === "expenses" && (
<div className="space-y-3">
<h3 className="text-sm font-semibold">Расходы</h3>
<DataTable
data={expenseData}
loading={expenseLoading}
columnLabels={columnLabels}
sumColumn={expenseSumCol}
visibleColumns={visibleExpenseCols}
sortBy={expenseSort.by}
sortDir={expenseSort.dir}
onSort={handleExpenseSort}
filters={expenseFilters}
onFilterChange={(col, val) => {
setExpenseFilters((f) => ({ ...f, [col]: val }));
setExpensePage(1);
}}
page={expensePage}
onPageChange={setExpensePage}
pageSize={pageSize}
onPageSizeChange={setPageSize}
/>
</div>
)}
{/* Income Categories */}
{activeTab === "income-categories" && (
<CategoryManager
categories={incomeCats?.categories ?? []}
onCreate={(c) => createIncCat.mutate(c)}
onUpdate={(id, c) => updateIncCat.mutate({ id, ...c })}
onDelete={(id) => deleteIncCat.mutate(id)}
loading={!incomeCats}
typeLabel="Категории доходов"
/>
)}
{/* Expense Categories */}
{activeTab === "expense-categories" && (
<CategoryManager
categories={expenseCats?.categories ?? []}
onCreate={(c) => createExpCat.mutate(c)}
onUpdate={(id, c) => updateExpCat.mutate({ id, ...c })}
onDelete={(id) => deleteExpCat.mutate(id)}
loading={!expenseCats}
typeLabel="Категории расходов"
/>
)}
{/* Settings */}
{activeTab === "settings" && <FinanceSettingsPanel />}
</div>
</FinanceLayout>
);
}