ajuste de layout mobile, melhorias e criação de novas funções. Detalhes adicionados no CHANGELOG.md
This commit is contained in:
committed by
Felipe Coutinho
parent
31fe752b7d
commit
ffde55f589
@@ -70,6 +70,8 @@ const VALID_FONTS = [
|
||||
|
||||
const updatePreferencesSchema = z.object({
|
||||
disableMagnetlines: z.boolean(),
|
||||
extratoNoteAsColumn: z.boolean(),
|
||||
lancamentosColumnOrder: z.array(z.string()).nullable(),
|
||||
systemFont: z.enum(VALID_FONTS).default("ai-sans"),
|
||||
moneyFont: z.enum(VALID_FONTS).default("ai-sans"),
|
||||
});
|
||||
@@ -417,6 +419,8 @@ export async function updatePreferencesAction(
|
||||
.update(schema.preferenciasUsuario)
|
||||
.set({
|
||||
disableMagnetlines: validated.disableMagnetlines,
|
||||
extratoNoteAsColumn: validated.extratoNoteAsColumn,
|
||||
lancamentosColumnOrder: validated.lancamentosColumnOrder,
|
||||
systemFont: validated.systemFont,
|
||||
moneyFont: validated.moneyFont,
|
||||
updatedAt: new Date(),
|
||||
@@ -427,6 +431,8 @@ export async function updatePreferencesAction(
|
||||
await db.insert(schema.preferenciasUsuario).values({
|
||||
userId: session.user.id,
|
||||
disableMagnetlines: validated.disableMagnetlines,
|
||||
extratoNoteAsColumn: validated.extratoNoteAsColumn,
|
||||
lancamentosColumnOrder: validated.lancamentosColumnOrder,
|
||||
systemFont: validated.systemFont,
|
||||
moneyFont: validated.moneyFont,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ import { db, schema } from "@/lib/db";
|
||||
|
||||
export interface UserPreferences {
|
||||
disableMagnetlines: boolean;
|
||||
extratoNoteAsColumn: boolean;
|
||||
lancamentosColumnOrder: string[] | null;
|
||||
systemFont: string;
|
||||
moneyFont: string;
|
||||
}
|
||||
@@ -32,6 +34,8 @@ export async function fetchUserPreferences(
|
||||
const result = await db
|
||||
.select({
|
||||
disableMagnetlines: schema.preferenciasUsuario.disableMagnetlines,
|
||||
extratoNoteAsColumn: schema.preferenciasUsuario.extratoNoteAsColumn,
|
||||
lancamentosColumnOrder: schema.preferenciasUsuario.lancamentosColumnOrder,
|
||||
systemFont: schema.preferenciasUsuario.systemFont,
|
||||
moneyFont: schema.preferenciasUsuario.moneyFont,
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RiArrowRightSLine } from "@remixicon/react";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
@@ -35,17 +36,28 @@ export default async function Page() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Tabs defaultValue="preferencias" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="preferencias">Preferências</TabsTrigger>
|
||||
<TabsTrigger value="companion">Companion</TabsTrigger>
|
||||
<TabsTrigger value="nome">Alterar nome</TabsTrigger>
|
||||
<TabsTrigger value="senha">Alterar senha</TabsTrigger>
|
||||
<TabsTrigger value="email">Alterar e-mail</TabsTrigger>
|
||||
<TabsTrigger value="changelog">Changelog</TabsTrigger>
|
||||
<TabsTrigger value="deletar" className="text-destructive">
|
||||
Deletar conta
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
{/* No mobile: rolagem horizontal + seta indicando mais opções à direita */}
|
||||
<div className="relative -mx-6 px-6 md:mx-0 md:px-0">
|
||||
<div className="overflow-x-auto overflow-y-hidden scroll-smooth md:overflow-visible [-webkit-overflow-scrolling:touch] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<TabsList className="inline-flex w-max flex-nowrap md:w-full">
|
||||
<TabsTrigger value="preferencias">Preferências</TabsTrigger>
|
||||
<TabsTrigger value="companion">Companion</TabsTrigger>
|
||||
<TabsTrigger value="nome">Alterar nome</TabsTrigger>
|
||||
<TabsTrigger value="senha">Alterar senha</TabsTrigger>
|
||||
<TabsTrigger value="email">Alterar e-mail</TabsTrigger>
|
||||
<TabsTrigger value="changelog">Changelog</TabsTrigger>
|
||||
<TabsTrigger value="deletar" className="text-destructive">
|
||||
Deletar conta
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute right-0 top-0 hidden h-9 w-10 items-center justify-end bg-gradient-to-l from-background to-transparent md:hidden"
|
||||
aria-hidden
|
||||
>
|
||||
<RiArrowRightSLine className="size-5 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="preferencias" className="mt-4">
|
||||
<Card className="p-6">
|
||||
@@ -61,6 +73,12 @@ export default async function Page() {
|
||||
disableMagnetlines={
|
||||
userPreferences?.disableMagnetlines ?? false
|
||||
}
|
||||
extratoNoteAsColumn={
|
||||
userPreferences?.extratoNoteAsColumn ?? false
|
||||
}
|
||||
lancamentosColumnOrder={
|
||||
userPreferences?.lancamentosColumnOrder ?? null
|
||||
}
|
||||
systemFont={userPreferences?.systemFont ?? "ai-sans"}
|
||||
moneyFont={userPreferences?.moneyFont ?? "ai-sans"}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RiPencilLine } from "@remixicon/react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchUserPreferences } from "@/app/(dashboard)/ajustes/data";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { CardDialog } from "@/components/cartoes/card-dialog";
|
||||
import type { Card } from "@/components/cartoes/types";
|
||||
@@ -51,12 +52,13 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [filterSources, logoOptions, invoiceData, estabelecimentos] =
|
||||
const [filterSources, logoOptions, invoiceData, estabelecimentos, userPreferences] =
|
||||
await Promise.all([
|
||||
fetchLancamentoFilterSources(userId),
|
||||
loadLogoOptions(),
|
||||
fetchInvoiceData(userId, cartaoId, selectedPeriod),
|
||||
getRecentEstablishmentsAction(),
|
||||
fetchUserPreferences(userId),
|
||||
]);
|
||||
const sluggedFilters = buildSluggedFilters(filterSources);
|
||||
const slugMaps = buildSlugMaps(sluggedFilters);
|
||||
@@ -182,6 +184,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate
|
||||
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.lancamentosColumnOrder ?? null}
|
||||
defaultCartaoId={card.id}
|
||||
defaultPaymentMethod="Cartão de crédito"
|
||||
lockCartaoSelection
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchUserPreferences } from "@/app/(dashboard)/ajustes/data";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { CategoryDetailHeader } from "@/components/categorias/category-detail-header";
|
||||
import { LancamentosPage } from "@/components/lancamentos/page/lancamentos-page";
|
||||
@@ -36,10 +37,11 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
||||
|
||||
const [detail, filterSources, estabelecimentos] = await Promise.all([
|
||||
const [detail, filterSources, estabelecimentos, userPreferences] = await Promise.all([
|
||||
fetchCategoryDetails(userId, categoryId, selectedPeriod),
|
||||
fetchLancamentoFilterSources(userId),
|
||||
getRecentEstablishmentsAction(),
|
||||
fetchUserPreferences(userId),
|
||||
]);
|
||||
|
||||
if (!detail) {
|
||||
@@ -92,6 +94,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
selectedPeriod={detail.period}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate={true}
|
||||
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.lancamentosColumnOrder ?? null}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RiPencilLine } from "@remixicon/react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchUserPreferences } from "@/app/(dashboard)/ajustes/data";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { AccountDialog } from "@/components/contas/account-dialog";
|
||||
import { AccountStatementCard } from "@/components/contas/account-statement-card";
|
||||
@@ -57,12 +58,13 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [filterSources, logoOptions, accountSummary, estabelecimentos] =
|
||||
const [filterSources, logoOptions, accountSummary, estabelecimentos, userPreferences] =
|
||||
await Promise.all([
|
||||
fetchLancamentoFilterSources(userId),
|
||||
loadLogoOptions(),
|
||||
fetchAccountSummary(userId, contaId, selectedPeriod),
|
||||
getRecentEstablishmentsAction(),
|
||||
fetchUserPreferences(userId),
|
||||
]);
|
||||
const sluggedFilters = buildSluggedFilters(filterSources);
|
||||
const slugMaps = buildSlugMaps(sluggedFilters);
|
||||
@@ -161,6 +163,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate={false}
|
||||
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.lancamentosColumnOrder ?? null}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fetchUserPreferences } from "@/app/(dashboard)/ajustes/data";
|
||||
import { LancamentosPage } from "@/components/lancamentos/page/lancamentos-page";
|
||||
import MonthNavigation from "@/components/month-picker/month-navigation";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
@@ -31,7 +32,10 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
|
||||
const searchFilters = extractLancamentoSearchFilters(resolvedSearchParams);
|
||||
|
||||
const filterSources = await fetchLancamentoFilterSources(userId);
|
||||
const [filterSources, userPreferences] = await Promise.all([
|
||||
fetchLancamentoFilterSources(userId),
|
||||
fetchUserPreferences(userId),
|
||||
]);
|
||||
|
||||
const sluggedFilters = buildSluggedFilters(filterSources);
|
||||
const slugMaps = buildSlugMaps(sluggedFilters);
|
||||
@@ -80,6 +84,8 @@ export default async function Page({ searchParams }: PageProps) {
|
||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.lancamentosColumnOrder ?? null}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async function DashboardLayout({
|
||||
/>
|
||||
<SidebarInset>
|
||||
<SiteHeader notificationsSnapshot={notificationsSnapshot} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-1 flex-col pt-12 md:pt-0">
|
||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6">
|
||||
{children}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
RiWallet3Line,
|
||||
} from "@remixicon/react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchUserPreferences } from "@/app/(dashboard)/ajustes/data";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page";
|
||||
import type {
|
||||
@@ -168,6 +169,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
shareRows,
|
||||
currentUserShare,
|
||||
estabelecimentos,
|
||||
userPreferences,
|
||||
] = await Promise.all([
|
||||
fetchPagadorLancamentos(filters),
|
||||
fetchPagadorMonthlyBreakdown({
|
||||
@@ -203,6 +205,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
sharesPromise,
|
||||
currentUserSharePromise,
|
||||
getRecentEstablishmentsAction(),
|
||||
fetchUserPreferences(userId),
|
||||
]);
|
||||
|
||||
const mappedLancamentos = mapLancamentosData(lancamentoRows);
|
||||
@@ -381,6 +384,8 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate={canEdit}
|
||||
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false}
|
||||
columnOrder={userPreferences?.lancamentosColumnOrder ?? null}
|
||||
importPagadorOptions={loggedUserOptionSets?.pagadorOptions}
|
||||
importSplitPagadorOptions={
|
||||
loggedUserOptionSets?.splitPagadorOptions
|
||||
|
||||
23
app/(dashboard)/relatorios/gastos-por-categoria/layout.tsx
Normal file
23
app/(dashboard)/relatorios/gastos-por-categoria/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { RiPieChartLine } from "@remixicon/react";
|
||||
import PageDescription from "@/components/page-description";
|
||||
|
||||
export const metadata = {
|
||||
title: "Gastos por categoria | OpenMonetis",
|
||||
};
|
||||
|
||||
export default function GastosPorCategoriaLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-6 px-6">
|
||||
<PageDescription
|
||||
icon={<RiPieChartLine />}
|
||||
title="Gastos por categoria"
|
||||
subtitle="Visualize suas despesas divididas por categoria no mês selecionado. Altere o mês para comparar períodos."
|
||||
/>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
30
app/(dashboard)/relatorios/gastos-por-categoria/loading.tsx
Normal file
30
app/(dashboard)/relatorios/gastos-por-categoria/loading.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function GastosPorCategoriaLoading() {
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<div className="h-14 animate-pulse rounded-xl bg-foreground/10" />
|
||||
|
||||
<div className="rounded-xl border p-4 md:p-6 space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 w-20 rounded-lg" />
|
||||
<Skeleton className="h-9 w-20 rounded-lg" />
|
||||
</div>
|
||||
<div className="space-y-3 pt-4">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center justify-between py-2 border-b border-dashed">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="size-10 rounded-lg" />
|
||||
<div className="space-y-1">
|
||||
<Skeleton className="h-4 w-28" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="h-5 w-20" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
100
app/(dashboard)/relatorios/gastos-por-categoria/page.tsx
Normal file
100
app/(dashboard)/relatorios/gastos-por-categoria/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
RiArrowDownSFill,
|
||||
RiArrowUpSFill,
|
||||
RiPieChartLine,
|
||||
} from "@remixicon/react";
|
||||
import MonthNavigation from "@/components/month-picker/month-navigation";
|
||||
import { ExpensesByCategoryWidgetWithChart } from "@/components/dashboard/expenses-by-category-widget-with-chart";
|
||||
import MoneyValues from "@/components/money-values";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
import { fetchExpensesByCategory } from "@/lib/dashboard/categories/expenses-by-category";
|
||||
import { calculatePercentageChange } from "@/lib/utils/math";
|
||||
import { parsePeriodParam } from "@/lib/utils/period";
|
||||
|
||||
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
||||
|
||||
type PageProps = {
|
||||
searchParams?: PageSearchParams;
|
||||
};
|
||||
|
||||
const getSingleParam = (
|
||||
params: Record<string, string | string[] | undefined> | undefined,
|
||||
key: string,
|
||||
) => {
|
||||
const value = params?.[key];
|
||||
if (!value) return null;
|
||||
return Array.isArray(value) ? (value[0] ?? null) : value;
|
||||
};
|
||||
|
||||
export default async function GastosPorCategoriaPage({
|
||||
searchParams,
|
||||
}: PageProps) {
|
||||
const userId = await getUserId();
|
||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||
|
||||
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
||||
|
||||
const data = await fetchExpensesByCategory(userId, selectedPeriod);
|
||||
const percentageChange = calculatePercentageChange(
|
||||
data.currentTotal,
|
||||
data.previousTotal,
|
||||
);
|
||||
const hasIncrease = percentageChange !== null && percentageChange > 0;
|
||||
const hasDecrease = percentageChange !== null && percentageChange < 0;
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<RiPieChartLine className="size-4 text-primary" />
|
||||
Resumo do mês
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Total de despesas no mês
|
||||
</p>
|
||||
<MoneyValues
|
||||
amount={data.currentTotal}
|
||||
className="text-2xl font-semibold"
|
||||
/>
|
||||
</div>
|
||||
{percentageChange !== null && (
|
||||
<span
|
||||
className={`flex items-center gap-0.5 text-sm ${
|
||||
hasIncrease
|
||||
? "text-destructive"
|
||||
: hasDecrease
|
||||
? "text-success"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{hasIncrease && <RiArrowUpSFill className="size-4" />}
|
||||
{hasDecrease && <RiArrowDownSFill className="size-4" />}
|
||||
{percentageChange > 0 ? "+" : ""}
|
||||
{percentageChange.toFixed(1)}% em relação ao mês anterior
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Mês anterior: <MoneyValues amount={data.previousTotal} />
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 md:p-6">
|
||||
<ExpensesByCategoryWidgetWithChart
|
||||
data={data}
|
||||
period={selectedPeriod}
|
||||
/>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user