forked from git.gladyson/openmonetis
refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,99 +1,98 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { CategoryDetailHeader } from "@/components/categorias/category-detail-header";
|
||||
import { LancamentosPage } from "@/components/lancamentos/page/lancamentos-page";
|
||||
import MonthNavigation from "@/components/month-picker/month-navigation";
|
||||
import { fetchCategoryDetails } from "@/lib/dashboard/categories/category-details";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
import { fetchCategoryDetails } from "@/lib/dashboard/categories/category-details";
|
||||
import {
|
||||
buildOptionSets,
|
||||
buildSluggedFilters,
|
||||
fetchLancamentoFilterSources,
|
||||
buildOptionSets,
|
||||
buildSluggedFilters,
|
||||
fetchLancamentoFilterSources,
|
||||
} from "@/lib/lancamentos/page-helpers";
|
||||
import { displayPeriod, parsePeriodParam } from "@/lib/utils/period";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ categoryId: string }>;
|
||||
searchParams?: PageSearchParams;
|
||||
params: Promise<{ categoryId: string }>;
|
||||
searchParams?: PageSearchParams;
|
||||
};
|
||||
|
||||
const getSingleParam = (
|
||||
params: Record<string, string | string[] | undefined> | undefined,
|
||||
key: string
|
||||
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;
|
||||
const value = params?.[key];
|
||||
if (!value) return null;
|
||||
return Array.isArray(value) ? (value[0] ?? null) : value;
|
||||
};
|
||||
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { categoryId } = await params;
|
||||
const userId = await getUserId();
|
||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||
const { categoryId } = await params;
|
||||
const userId = await getUserId();
|
||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||
|
||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
||||
const periodoParam = getSingleParam(resolvedSearchParams, "periodo");
|
||||
const { period: selectedPeriod } = parsePeriodParam(periodoParam);
|
||||
|
||||
const [detail, filterSources, estabelecimentos] =
|
||||
await Promise.all([
|
||||
fetchCategoryDetails(userId, categoryId, selectedPeriod),
|
||||
fetchLancamentoFilterSources(userId),
|
||||
getRecentEstablishmentsAction(),
|
||||
]);
|
||||
const [detail, filterSources, estabelecimentos] = await Promise.all([
|
||||
fetchCategoryDetails(userId, categoryId, selectedPeriod),
|
||||
fetchLancamentoFilterSources(userId),
|
||||
getRecentEstablishmentsAction(),
|
||||
]);
|
||||
|
||||
if (!detail) {
|
||||
notFound();
|
||||
}
|
||||
if (!detail) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const sluggedFilters = buildSluggedFilters(filterSources);
|
||||
const {
|
||||
pagadorOptions,
|
||||
splitPagadorOptions,
|
||||
defaultPagadorId,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
pagadorFilterOptions,
|
||||
categoriaFilterOptions,
|
||||
contaCartaoFilterOptions,
|
||||
} = buildOptionSets({
|
||||
...sluggedFilters,
|
||||
pagadorRows: filterSources.pagadorRows,
|
||||
});
|
||||
const sluggedFilters = buildSluggedFilters(filterSources);
|
||||
const {
|
||||
pagadorOptions,
|
||||
splitPagadorOptions,
|
||||
defaultPagadorId,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
pagadorFilterOptions,
|
||||
categoriaFilterOptions,
|
||||
contaCartaoFilterOptions,
|
||||
} = buildOptionSets({
|
||||
...sluggedFilters,
|
||||
pagadorRows: filterSources.pagadorRows,
|
||||
});
|
||||
|
||||
const currentPeriodLabel = displayPeriod(detail.period);
|
||||
const previousPeriodLabel = displayPeriod(detail.previousPeriod);
|
||||
const currentPeriodLabel = displayPeriod(detail.period);
|
||||
const previousPeriodLabel = displayPeriod(detail.previousPeriod);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
<CategoryDetailHeader
|
||||
category={detail.category}
|
||||
currentPeriodLabel={currentPeriodLabel}
|
||||
previousPeriodLabel={previousPeriodLabel}
|
||||
currentTotal={detail.currentTotal}
|
||||
previousTotal={detail.previousTotal}
|
||||
percentageChange={detail.percentageChange}
|
||||
transactionCount={detail.transactions.length}
|
||||
/>
|
||||
<LancamentosPage
|
||||
currentUserId={userId}
|
||||
lancamentos={detail.transactions}
|
||||
pagadorOptions={pagadorOptions}
|
||||
splitPagadorOptions={splitPagadorOptions}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
contaOptions={contaOptions}
|
||||
cartaoOptions={cartaoOptions}
|
||||
categoriaOptions={categoriaOptions}
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||
selectedPeriod={detail.period}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
<CategoryDetailHeader
|
||||
category={detail.category}
|
||||
currentPeriodLabel={currentPeriodLabel}
|
||||
previousPeriodLabel={previousPeriodLabel}
|
||||
currentTotal={detail.currentTotal}
|
||||
previousTotal={detail.previousTotal}
|
||||
percentageChange={detail.percentageChange}
|
||||
transactionCount={detail.transactions.length}
|
||||
/>
|
||||
<LancamentosPage
|
||||
currentUserId={userId}
|
||||
lancamentos={detail.transactions}
|
||||
pagadorOptions={pagadorOptions}
|
||||
splitPagadorOptions={splitPagadorOptions}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
contaOptions={contaOptions}
|
||||
cartaoOptions={cartaoOptions}
|
||||
categoriaOptions={categoriaOptions}
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||
selectedPeriod={detail.period}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
"use server";
|
||||
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { categorias } from "@/db/schema";
|
||||
import {
|
||||
type ActionResult,
|
||||
handleActionError,
|
||||
revalidateForEntity,
|
||||
type ActionResult,
|
||||
handleActionError,
|
||||
revalidateForEntity,
|
||||
} from "@/lib/actions/helpers";
|
||||
import { getUser } from "@/lib/auth/server";
|
||||
import { CATEGORY_TYPES } from "@/lib/categorias/constants";
|
||||
import { db } from "@/lib/db";
|
||||
import { uuidSchema } from "@/lib/schemas/common";
|
||||
import { normalizeIconInput } from "@/lib/utils/string";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
const categoryBaseSchema = z.object({
|
||||
name: z
|
||||
.string({ message: "Informe o nome da categoria." })
|
||||
.trim()
|
||||
.min(1, "Informe o nome da categoria."),
|
||||
type: z.enum(CATEGORY_TYPES, {
|
||||
message: "Tipo de categoria inválido.",
|
||||
}),
|
||||
icon: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100, "O ícone deve ter no máximo 100 caracteres.")
|
||||
.nullish()
|
||||
.transform((value) => normalizeIconInput(value)),
|
||||
name: z
|
||||
.string({ message: "Informe o nome da categoria." })
|
||||
.trim()
|
||||
.min(1, "Informe o nome da categoria."),
|
||||
type: z.enum(CATEGORY_TYPES, {
|
||||
message: "Tipo de categoria inválido.",
|
||||
}),
|
||||
icon: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100, "O ícone deve ter no máximo 100 caracteres.")
|
||||
.nullish()
|
||||
.transform((value) => normalizeIconInput(value)),
|
||||
});
|
||||
|
||||
const createCategorySchema = categoryBaseSchema;
|
||||
const updateCategorySchema = categoryBaseSchema.extend({
|
||||
id: uuidSchema("Categoria"),
|
||||
id: uuidSchema("Categoria"),
|
||||
});
|
||||
const deleteCategorySchema = z.object({
|
||||
id: uuidSchema("Categoria"),
|
||||
id: uuidSchema("Categoria"),
|
||||
});
|
||||
|
||||
type CategoryCreateInput = z.infer<typeof createCategorySchema>;
|
||||
@@ -43,134 +43,134 @@ type CategoryUpdateInput = z.infer<typeof updateCategorySchema>;
|
||||
type CategoryDeleteInput = z.infer<typeof deleteCategorySchema>;
|
||||
|
||||
export async function createCategoryAction(
|
||||
input: CategoryCreateInput
|
||||
input: CategoryCreateInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = createCategorySchema.parse(input);
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = createCategorySchema.parse(input);
|
||||
|
||||
await db.insert(categorias).values({
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
icon: data.icon,
|
||||
userId: user.id,
|
||||
});
|
||||
await db.insert(categorias).values({
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
icon: data.icon,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
revalidateForEntity("categorias");
|
||||
revalidateForEntity("categorias");
|
||||
|
||||
return { success: true, message: "Categoria criada com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
return { success: true, message: "Categoria criada com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCategoryAction(
|
||||
input: CategoryUpdateInput
|
||||
input: CategoryUpdateInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = updateCategorySchema.parse(input);
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = updateCategorySchema.parse(input);
|
||||
|
||||
// Buscar categoria antes de atualizar para verificar restrições
|
||||
const categoria = await db.query.categorias.findFirst({
|
||||
columns: { id: true, name: true },
|
||||
where: and(eq(categorias.id, data.id), eq(categorias.userId, user.id)),
|
||||
});
|
||||
// Buscar categoria antes de atualizar para verificar restrições
|
||||
const categoria = await db.query.categorias.findFirst({
|
||||
columns: { id: true, name: true },
|
||||
where: and(eq(categorias.id, data.id), eq(categorias.userId, user.id)),
|
||||
});
|
||||
|
||||
if (!categoria) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
if (!categoria) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
|
||||
// Bloquear edição das categorias protegidas
|
||||
const categoriasProtegidas = [
|
||||
"Transferência interna",
|
||||
"Saldo inicial",
|
||||
"Pagamentos",
|
||||
];
|
||||
if (categoriasProtegidas.includes(categoria.name)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `A categoria '${categoria.name}' é protegida e não pode ser editada.`,
|
||||
};
|
||||
}
|
||||
// Bloquear edição das categorias protegidas
|
||||
const categoriasProtegidas = [
|
||||
"Transferência interna",
|
||||
"Saldo inicial",
|
||||
"Pagamentos",
|
||||
];
|
||||
if (categoriasProtegidas.includes(categoria.name)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `A categoria '${categoria.name}' é protegida e não pode ser editada.`,
|
||||
};
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(categorias)
|
||||
.set({
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
icon: data.icon,
|
||||
})
|
||||
.where(and(eq(categorias.id, data.id), eq(categorias.userId, user.id)))
|
||||
.returning();
|
||||
const [updated] = await db
|
||||
.update(categorias)
|
||||
.set({
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
icon: data.icon,
|
||||
})
|
||||
.where(and(eq(categorias.id, data.id), eq(categorias.userId, user.id)))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
if (!updated) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
|
||||
revalidateForEntity("categorias");
|
||||
revalidateForEntity("categorias");
|
||||
|
||||
return { success: true, message: "Categoria atualizada com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
return { success: true, message: "Categoria atualizada com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCategoryAction(
|
||||
input: CategoryDeleteInput
|
||||
input: CategoryDeleteInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = deleteCategorySchema.parse(input);
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = deleteCategorySchema.parse(input);
|
||||
|
||||
// Buscar categoria antes de deletar para verificar restrições
|
||||
const categoria = await db.query.categorias.findFirst({
|
||||
columns: { id: true, name: true },
|
||||
where: and(eq(categorias.id, data.id), eq(categorias.userId, user.id)),
|
||||
});
|
||||
// Buscar categoria antes de deletar para verificar restrições
|
||||
const categoria = await db.query.categorias.findFirst({
|
||||
columns: { id: true, name: true },
|
||||
where: and(eq(categorias.id, data.id), eq(categorias.userId, user.id)),
|
||||
});
|
||||
|
||||
if (!categoria) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
if (!categoria) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
|
||||
// Bloquear remoção das categorias protegidas
|
||||
const categoriasProtegidas = [
|
||||
"Transferência interna",
|
||||
"Saldo inicial",
|
||||
"Pagamentos",
|
||||
];
|
||||
if (categoriasProtegidas.includes(categoria.name)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `A categoria '${categoria.name}' é protegida e não pode ser removida.`,
|
||||
};
|
||||
}
|
||||
// Bloquear remoção das categorias protegidas
|
||||
const categoriasProtegidas = [
|
||||
"Transferência interna",
|
||||
"Saldo inicial",
|
||||
"Pagamentos",
|
||||
];
|
||||
if (categoriasProtegidas.includes(categoria.name)) {
|
||||
return {
|
||||
success: false,
|
||||
error: `A categoria '${categoria.name}' é protegida e não pode ser removida.`,
|
||||
};
|
||||
}
|
||||
|
||||
const [deleted] = await db
|
||||
.delete(categorias)
|
||||
.where(and(eq(categorias.id, data.id), eq(categorias.userId, user.id)))
|
||||
.returning({ id: categorias.id });
|
||||
const [deleted] = await db
|
||||
.delete(categorias)
|
||||
.where(and(eq(categorias.id, data.id), eq(categorias.userId, user.id)))
|
||||
.returning({ id: categorias.id });
|
||||
|
||||
if (!deleted) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
if (!deleted) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Categoria não encontrada.",
|
||||
};
|
||||
}
|
||||
|
||||
revalidateForEntity("categorias");
|
||||
revalidateForEntity("categorias");
|
||||
|
||||
return { success: true, message: "Categoria removida com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
return { success: true, message: "Categoria removida com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import type { CategoryType } from "@/components/categorias/types";
|
||||
import { categorias, type Categoria } from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { CategoryType } from "@/components/categorias/types";
|
||||
import { type Categoria, categorias } from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
export type CategoryData = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: CategoryType;
|
||||
icon: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
type: CategoryType;
|
||||
icon: string | null;
|
||||
};
|
||||
|
||||
export async function fetchCategoriesForUser(
|
||||
userId: string
|
||||
userId: string,
|
||||
): Promise<CategoryData[]> {
|
||||
const categoryRows = await db.query.categorias.findMany({
|
||||
where: eq(categorias.userId, userId),
|
||||
});
|
||||
const categoryRows = await db.query.categorias.findMany({
|
||||
where: eq(categorias.userId, userId),
|
||||
});
|
||||
|
||||
return categoryRows.map((category: Categoria) => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
type: category.type as CategoryType,
|
||||
icon: category.icon,
|
||||
}));
|
||||
return categoryRows.map((category: Categoria) => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
type: category.type as CategoryType,
|
||||
icon: category.icon,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="flex flex-col gap-6 px-6">
|
||||
<Card className="h-auto">
|
||||
<CardContent className="space-y-2.5">
|
||||
<div className="space-y-2">
|
||||
{/* Selected categories and counter */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-8 w-32 rounded-md" />
|
||||
<Skeleton className="h-8 w-40 rounded-md" />
|
||||
<Skeleton className="h-8 w-36 rounded-md" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0 pt-1.5">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-14" />
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<main className="flex flex-col gap-6 px-6">
|
||||
<Card className="h-auto">
|
||||
<CardContent className="space-y-2.5">
|
||||
<div className="space-y-2">
|
||||
{/* Selected categories and counter */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-8 w-32 rounded-md" />
|
||||
<Skeleton className="h-8 w-40 rounded-md" />
|
||||
<Skeleton className="h-8 w-36 rounded-md" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0 pt-1.5">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-6 w-14" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category selector button */}
|
||||
<Skeleton className="h-9 w-full rounded-md" />
|
||||
</div>
|
||||
{/* Category selector button */}
|
||||
<Skeleton className="h-9 w-full rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<Skeleton className="h-[450px] w-full rounded-lg" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
{/* Chart */}
|
||||
<Skeleton className="h-[450px] w-full rounded-lg" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import { fetchCategoryHistory } from "@/lib/dashboard/categories/category-histor
|
||||
import { getCurrentPeriod } from "@/lib/utils/period";
|
||||
|
||||
export default async function HistoricoCategoriasPage() {
|
||||
const user = await getUser();
|
||||
const currentPeriod = getCurrentPeriod();
|
||||
const user = await getUser();
|
||||
const currentPeriod = getCurrentPeriod();
|
||||
|
||||
const data = await fetchCategoryHistory(user.id, currentPeriod);
|
||||
const data = await fetchCategoryHistory(user.id, currentPeriod);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<CategoryHistoryWidget data={data} />
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main>
|
||||
<CategoryHistoryWidget data={data} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import PageDescription from "@/components/page-description";
|
||||
import { RiPriceTag3Line } from "@remixicon/react";
|
||||
import PageDescription from "@/components/page-description";
|
||||
|
||||
export const metadata = {
|
||||
title: "Categorias | Opensheets",
|
||||
title: "Categorias | Opensheets",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-6 px-6">
|
||||
<PageDescription
|
||||
icon={<RiPriceTag3Line />}
|
||||
title="Categorias"
|
||||
subtitle="Gerencie suas categorias de despesas e receitas acompanhando o histórico de desempenho dos últimos 9 meses, permitindo ajustes financeiros precisos conforme necessário."
|
||||
/>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<section className="space-y-6 px-6">
|
||||
<PageDescription
|
||||
icon={<RiPriceTag3Line />}
|
||||
title="Categorias"
|
||||
subtitle="Gerencie suas categorias de despesas e receitas acompanhando o histórico de desempenho dos últimos 9 meses, permitindo ajustes financeiros precisos conforme necessário."
|
||||
/>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,57 +5,54 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
* Layout: Header + Tabs + Grid de cards
|
||||
*/
|
||||
export default function CategoriasLoading() {
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<div className="w-full space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<div className="w-full space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 border-b">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
className="h-10 w-32 rounded-t-2xl bg-foreground/10"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Tabs */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 border-b">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
className="h-10 w-32 rounded-t-2xl bg-foreground/10"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Grid de cards de categorias */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-2xl border p-6 space-y-4"
|
||||
>
|
||||
{/* Ícone + Nome */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="size-12 rounded-2xl bg-foreground/10" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-5 w-full rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Grid de cards de categorias */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl border p-6 space-y-4">
|
||||
{/* Ícone + Nome */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="size-12 rounded-2xl bg-foreground/10" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-5 w-full rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Descrição */}
|
||||
{i % 3 === 0 && (
|
||||
<Skeleton className="h-4 w-full rounded-2xl bg-foreground/10" />
|
||||
)}
|
||||
{/* Descrição */}
|
||||
{i % 3 === 0 && (
|
||||
<Skeleton className="h-4 w-full rounded-2xl bg-foreground/10" />
|
||||
)}
|
||||
|
||||
{/* Botões de ação */}
|
||||
<div className="flex gap-2 pt-2 border-t">
|
||||
<Skeleton className="h-9 flex-1 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
{/* Botões de ação */}
|
||||
<div className="flex gap-2 pt-2 border-t">
|
||||
<Skeleton className="h-9 flex-1 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { getUserId } from "@/lib/auth/server";
|
||||
import { fetchCategoriesForUser } from "./data";
|
||||
|
||||
export default async function Page() {
|
||||
const userId = await getUserId();
|
||||
const categories = await fetchCategoriesForUser(userId);
|
||||
const userId = await getUserId();
|
||||
const categories = await fetchCategoriesForUser(userId);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<CategoriesPage categories={categories} />
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<CategoriesPage categories={categories} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user