Added estimaded salary to dashboard

This commit is contained in:
Jose Selesan
2026-07-22 12:31:58 -03:00
parent 09f00aa3b9
commit 4784242a35
9 changed files with 206 additions and 30 deletions

View File

@@ -9,9 +9,9 @@ import {
import type { Quote } from "@/lib/api";
import {
useFetchQuotes,
useMonthlyPayedTotal,
useMonthlyTotals,
useQuotes,
useSettings,
useWallets,
} from "@/lib/queries";
import { RelativeTime } from "@/lib/time";
@@ -131,15 +131,15 @@ export function DashboardPage() {
const bna = quotes?.find((q) => q.type === "BNA");
const now = new Date();
const { data: monthlyExpenses } = useMonthlyPayedTotal(
now.getFullYear(),
now.getMonth() + 1,
);
const { data: monthlyTotals } = useMonthlyTotals(
now.getFullYear(),
now.getMonth() + 1,
);
const { data: settings } = useSettings();
const salaryMultiplier =
settings?.find((s) => s.key === "salary_multiplier")?.value ?? "4500";
const isFetching = isLoading || isPending;
const { data: wallets } = useWallets();
@@ -173,9 +173,12 @@ export function DashboardPage() {
</p>
</div>
<div className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">Gastos del mes</p>
<p className="text-sm text-muted-foreground">Sueldo</p>
<p className="text-2xl font-bold">
${priceFormatter.format(monthlyExpenses?.total ?? 0)}
$
{priceFormatter.format(
(belo ? Number(belo.buy) : 0) * Number(salaryMultiplier),
)}
</p>
</div>
<QuoteCard
@@ -202,12 +205,11 @@ export function DashboardPage() {
<h2 className="text-lg font-semibold">Billeteras</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{wallets.map((w) => (
<div
key={w.id}
className="rounded-lg border p-4"
>
<div key={w.id} className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">{w.name}</p>
<p className="text-sm text-muted-foreground/60 text-xs">{w.currency}</p>
<p className="text-sm text-muted-foreground/60 text-xs">
{w.currency}
</p>
<p className="text-2xl font-bold tabular-nums mt-1">
{walletBalanceFormatter.format(w.balance)}
</p>

View File

@@ -339,7 +339,12 @@ export type Wallet = {
export type WalletMovement = {
id: number;
walletId: number;
type: "DEPOSIT" | "WITHDRAWAL" | "TRANSFER_IN" | "TRANSFER_OUT" | "ADJUSTMENT";
type:
| "DEPOSIT"
| "WITHDRAWAL"
| "TRANSFER_IN"
| "TRANSFER_OUT"
| "ADJUSTMENT";
amount: number;
description: string | null;
referenceWalletId: number | null;
@@ -390,10 +395,7 @@ export function deleteWallet(id: number): Promise<void> {
return mutator<void>(`/api/wallets/${id}`, "DELETE");
}
export function depositWallet(
id: number,
data: DepositInput,
): Promise<Wallet> {
export function depositWallet(id: number, data: DepositInput): Promise<Wallet> {
return mutator<Wallet>(`/api/wallets/${id}/deposit`, "POST", data);
}
@@ -418,9 +420,7 @@ export function setDefaultWallet(id: number): Promise<Wallet> {
return mutator<Wallet>(`/api/wallets/${id}/default`, "PUT");
}
export function getWalletMovements(
id: number,
): Promise<WalletMovement[]> {
export function getWalletMovements(id: number): Promise<WalletMovement[]> {
return fetcher<WalletMovement[]>(`/api/wallets/${id}/movements`);
}
@@ -447,6 +447,21 @@ export function getWalletMovementsFiltered(
);
}
// Settings
export type Setting = {
key: string;
value: string;
};
export function getSettings(): Promise<Setting[]> {
return fetcher<Setting[]>("/api/settings");
}
export function updateSetting(key: string, value: string): Promise<Setting> {
return mutator<Setting>("/api/settings", "PUT", { key, value });
}
// Telegram
export type TelegramStatus = {

View File

@@ -6,23 +6,23 @@ import {
} from "@tanstack/react-query";
import {
type AdjustBalanceInput,
adjustBalance as adjustBalanceApi,
type CreateNonPeriodicExpenseInput,
type CreatePeriodicExpenseInput,
type CreateWalletInput,
type DepositInput,
type TransferInput,
type WalletMovementsFilter,
createNonPeriodicExpense as createNonPeriodicExpenseApi,
createPeriodicExpense as createPeriodicExpenseApi,
createWallet as createWalletApi,
type DepositInput,
deletePeriodicExpense as deletePeriodicExpenseApi,
deleteWallet as deleteWalletApi,
depositWallet as depositWalletApi,
adjustBalance as adjustBalanceApi,
fetchQuotes,
generateMonthlyExpense as generateMonthlyExpenseApi,
generateTelegramCode,
getDailyMinMax,
getDailyQuotes,
getExpenses,
generateTelegramCode,
getHistoricalMinMax,
getMonthlyPayedTotal,
getMonthlyTotals,
@@ -30,23 +30,25 @@ import {
getPeriodicExpenses,
getQuoteHistory,
getQuotes,
getSettings,
getTelegramStatus,
getTotalPending,
getWalletMovements,
getWalletMovementsFiltered,
getWallets,
setDefaultWallet as setDefaultWalletApi,
importExpenses,
importPeriodicExpenses,
type PayExpenseInput,
payExpense as payExpenseApi,
setDefaultWallet as setDefaultWalletApi,
type TransferInput,
transferWallet as transferWalletApi,
type UpdateExpenseInput,
updateExpense as updateExpenseApi,
updatePeriodicExpense as updatePeriodicExpenseApi,
createWallet as createWalletApi,
updateSetting as updateSettingApi,
updateWallet as updateWalletApi,
deleteWallet as deleteWalletApi,
transferWallet as transferWalletApi,
type WalletMovementsFilter,
} from "./api";
export const quoteKeys = {
@@ -422,6 +424,31 @@ export function useWalletMovementsFiltered(filter: WalletMovementsFilter) {
});
}
// Settings
export const settingKeys = {
all: ["settings"] as const,
};
export function useSettings() {
return useQuery({
queryKey: settingKeys.all,
queryFn: getSettings,
staleTime: 60_000,
});
}
export function useUpdateSetting() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ key, value }: { key: string; value: string }) =>
updateSettingApi(key, value),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: settingKeys.all });
},
});
}
// Telegram
export function useTelegramStatus() {

View File

@@ -1,12 +1,14 @@
import { createFileRoute } from "@tanstack/react-router";
import { Check, Copy, RefreshCw } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAuth } from "@/lib/auth-context";
import {
useGenerateTelegramCode,
useSettings,
useTelegramStatus,
useUpdateSetting,
} from "@/lib/queries";
export const Route = createFileRoute("/_authenticated/settings")({
@@ -126,11 +128,79 @@ function SettingsPage() {
<hr className="border-border" />
<SalaryMultiplierSection />
<TelegramSection />
</div>
);
}
function SalaryMultiplierSection() {
const { data: settings, isLoading } = useSettings();
const updateSetting = useUpdateSetting();
const current =
settings?.find((s) => s.key === "salary_multiplier")?.value ?? "4500";
const [localValue, setLocalValue] = useState(current);
const [success, setSuccess] = useState(false);
useEffect(() => {
if (settings) {
const val = settings.find((s) => s.key === "salary_multiplier")?.value;
if (val) setLocalValue(val);
}
}, [settings]);
async function handleSave() {
setSuccess(false);
await updateSetting.mutateAsync({
key: "salary_multiplier",
value: localValue,
});
setSuccess(true);
setTimeout(() => setSuccess(false), 2000);
}
return (
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold tracking-tight">
Multiplicador de sueldo
</h2>
<p className="text-sm text-muted-foreground">
Valor para calcular el sueldo (Sueldo = BELO bid x este valor)
</p>
</div>
{isLoading ? (
<p className="text-sm text-muted-foreground">Cargando...</p>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Input
type="number"
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
className="w-32"
/>
<Button
type="button"
onClick={handleSave}
disabled={updateSetting.isPending || localValue === current}
>
{updateSetting.isPending ? "Guardando..." : "Guardar"}
</Button>
</div>
{success && (
<p className="text-sm text-emerald-600">
Multiplicador actualizado correctamente
</p>
)}
</div>
)}
</div>
);
}
function TelegramSection() {
const { data: status, isLoading } = useTelegramStatus();
const {