Merge pull request #24 from felipegcoutinho/feat/melhorias-gerais-do-app
Refatora hooks compartilhados e ajusta month picker
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -5,6 +5,22 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
|
|||||||
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
||||||
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.7.7] - 2026-03-05
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Períodos e navegação mensal: `useMonthPeriod` passou a usar os helpers centrais de período (`YYYY-MM`), o month-picker foi simplificado e o rótulo visual agora segue o formato `Março 2026`.
|
||||||
|
- Hooks e organização: hooks locais de calculadora, month-picker, logo picker e sidebar foram movidos para perto das respectivas features, deixando `/hooks` focado nos hooks realmente compartilhados.
|
||||||
|
- Estado de formulários e responsividade: `useFormState` ganhou APIs explícitas de reset/substituição no lugar do setter cru, e `useIsMobile` foi atualizado para assinatura estável com `useSyncExternalStore`, reduzindo a troca estrutural inicial no sidebar entre mobile e desktop.
|
||||||
|
- Navegação e estrutura compartilhada: `components/navbar` e `components/sidebar` foram consolidados em `components/navigation/*`, componentes globais migraram para `components/shared/*` e os imports foram padronizados no projeto.
|
||||||
|
- Dashboard e relatórios: a análise de parcelas foi movida para `/relatorios/analise-parcelas`, ações rápidas e widgets do dashboard foram refinados, e os cards de relatórios ganharam ajustes para evitar overflow no mobile.
|
||||||
|
- Pré-lançamentos e lançamentos: tabs e cards da inbox ficaram mais consistentes no mobile, itens descartados podem voltar para `Pendente` e compras feitas no dia do fechamento do cartão agora entram na próxima fatura.
|
||||||
|
- Tipografia e exportações: suporte a `SF Pro` foi removido, a validação de fontes ficou centralizada em `public/fonts/font_index.ts` e as exportações em PDF/CSV/Excel receberam melhor branding e apresentação.
|
||||||
|
- Calculadora e diálogos: o arraste ficou mais estável, os bloqueios de fechamento externo foram reforçados e o display interno foi reorganizado para uso mais consistente.
|
||||||
|
- Também houve ajustes menores de responsividade, espaçamento e acabamento visual em telas mobile, modais e detalhes de interface.
|
||||||
|
|
||||||
## [1.7.6] - 2026-03-02
|
## [1.7.6] - 2026-03-02
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { account, pagadores, tokensApi } from "@/db/schema";
|
|||||||
import { auth } from "@/lib/auth/config";
|
import { auth } from "@/lib/auth/config";
|
||||||
import { db, schema } from "@/lib/db";
|
import { db, schema } from "@/lib/db";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||||
|
import { DEFAULT_FONT_KEY, FONT_KEYS } from "@/public/fonts/font_index";
|
||||||
|
|
||||||
type ActionResponse<T = void> = {
|
type ActionResponse<T = void> = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -52,28 +53,12 @@ const deleteAccountSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const VALID_FONTS = [
|
|
||||||
"ai-sans",
|
|
||||||
"anthropic-sans",
|
|
||||||
"fira-code",
|
|
||||||
"fira-sans",
|
|
||||||
"geist",
|
|
||||||
"ibm-plex-mono",
|
|
||||||
"inter",
|
|
||||||
"jetbrains-mono",
|
|
||||||
"reddit-sans",
|
|
||||||
"roboto",
|
|
||||||
"sf-pro-display",
|
|
||||||
"sf-pro-rounded",
|
|
||||||
"ubuntu",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const updatePreferencesSchema = z.object({
|
const updatePreferencesSchema = z.object({
|
||||||
disableMagnetlines: z.boolean(),
|
disableMagnetlines: z.boolean(),
|
||||||
extratoNoteAsColumn: z.boolean(),
|
extratoNoteAsColumn: z.boolean(),
|
||||||
lancamentosColumnOrder: z.array(z.string()).nullable(),
|
lancamentosColumnOrder: z.array(z.string()).nullable(),
|
||||||
systemFont: z.enum(VALID_FONTS).default("ai-sans"),
|
systemFont: z.enum(FONT_KEYS).default(DEFAULT_FONT_KEY),
|
||||||
moneyFont: z.enum(VALID_FONTS).default("ai-sans"),
|
moneyFont: z.enum(FONT_KEYS).default(DEFAULT_FONT_KEY),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
import { tokensApi } from "@/db/schema";
|
import { tokensApi } from "@/db/schema";
|
||||||
import { db, schema } from "@/lib/db";
|
import { db, schema } from "@/lib/db";
|
||||||
|
import { type FontKey, normalizeFontKey } from "@/public/fonts/font_index";
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
disableMagnetlines: boolean;
|
disableMagnetlines: boolean;
|
||||||
extratoNoteAsColumn: boolean;
|
extratoNoteAsColumn: boolean;
|
||||||
lancamentosColumnOrder: string[] | null;
|
lancamentosColumnOrder: string[] | null;
|
||||||
systemFont: string;
|
systemFont: FontKey;
|
||||||
moneyFont: string;
|
moneyFont: FontKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiToken {
|
export interface ApiToken {
|
||||||
@@ -43,7 +44,13 @@ export async function fetchUserPreferences(
|
|||||||
.where(eq(schema.preferenciasUsuario.userId, userId))
|
.where(eq(schema.preferenciasUsuario.userId, userId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
return result[0] || null;
|
if (!result[0]) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result[0],
|
||||||
|
systemFont: normalizeFontKey(result[0].systemFont),
|
||||||
|
moneyFont: normalizeFontKey(result[0].moneyFont),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchApiTokens(userId: string): Promise<ApiToken[]> {
|
export async function fetchApiTokens(userId: string): Promise<ApiToken[]> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiSettings2Line } from "@remixicon/react";
|
import { RiSettings2Line } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Ajustes | OpenMonetis",
|
title: "Ajustes | OpenMonetis",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { UpdatePasswordForm } from "@/components/ajustes/update-password-form";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { auth } from "@/lib/auth/config";
|
import { auth } from "@/lib/auth/config";
|
||||||
|
import { DEFAULT_FONT_KEY } from "@/public/fonts/font_index";
|
||||||
import { fetchAjustesPageData } from "./data";
|
import { fetchAjustesPageData } from "./data";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
@@ -75,8 +76,8 @@ export default async function Page() {
|
|||||||
lancamentosColumnOrder={
|
lancamentosColumnOrder={
|
||||||
userPreferences?.lancamentosColumnOrder ?? null
|
userPreferences?.lancamentosColumnOrder ?? null
|
||||||
}
|
}
|
||||||
systemFont={userPreferences?.systemFont ?? "ai-sans"}
|
systemFont={userPreferences?.systemFont ?? DEFAULT_FONT_KEY}
|
||||||
moneyFont={userPreferences?.moneyFont ?? "ai-sans"}
|
moneyFont={userPreferences?.moneyFont ?? DEFAULT_FONT_KEY}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiTodoLine } from "@remixicon/react";
|
import { RiTodoLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Anotações | OpenMonetis",
|
title: "Anotações | OpenMonetis",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiCalendarEventLine } from "@remixicon/react";
|
import { RiCalendarEventLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Calendário | OpenMonetis",
|
title: "Calendário | OpenMonetis",
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
lancamentos,
|
lancamentos,
|
||||||
pagadores,
|
pagadores,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import { buildInvoicePaymentNote } from "@/lib/accounts/constants";
|
|
||||||
import { revalidateForEntity } from "@/lib/actions/helpers";
|
import { revalidateForEntity } from "@/lib/actions/helpers";
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
|
import { buildInvoicePaymentNote } from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
INVOICE_PAYMENT_STATUS,
|
INVOICE_PAYMENT_STATUS,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { and, desc, eq, type SQL, sum } from "drizzle-orm";
|
import { and, desc, eq, type SQL, sum } from "drizzle-orm";
|
||||||
import { cartoes, faturas, lancamentos } from "@/db/schema";
|
import { cartoes, faturas, lancamentos } from "@/db/schema";
|
||||||
import { buildInvoicePaymentNote } from "@/lib/accounts/constants";
|
import { buildInvoicePaymentNote } from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
INVOICE_PAYMENT_STATUS,
|
INVOICE_PAYMENT_STATUS,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
FilterSkeleton,
|
FilterSkeleton,
|
||||||
InvoiceSummaryCardSkeleton,
|
InvoiceSummaryCardSkeleton,
|
||||||
TransactionsTableSkeleton,
|
TransactionsTableSkeleton,
|
||||||
} from "@/components/skeletons";
|
} from "@/components/shared/skeletons";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiBankCard2Line } from "@remixicon/react";
|
import { RiBankCard2Line } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Cartões | OpenMonetis",
|
title: "Cartões | OpenMonetis",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiPriceTag3Line } from "@remixicon/react";
|
import { RiPriceTag3Line } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Categorias | OpenMonetis",
|
title: "Categorias | OpenMonetis",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiHistoryLine } from "@remixicon/react";
|
import { RiHistoryLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Cartões | OpenMonetis",
|
title: "Cartões | OpenMonetis",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { and, desc, eq, lt, type SQL, sql } from "drizzle-orm";
|
import { and, desc, eq, lt, type SQL, sql } from "drizzle-orm";
|
||||||
import { contas, lancamentos, pagadores } from "@/db/schema";
|
import { contas, lancamentos, pagadores } from "@/db/schema";
|
||||||
import { INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
|
import { INITIAL_BALANCE_NOTE } from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
AccountStatementCardSkeleton,
|
AccountStatementCardSkeleton,
|
||||||
FilterSkeleton,
|
FilterSkeleton,
|
||||||
TransactionsTableSkeleton,
|
TransactionsTableSkeleton,
|
||||||
} from "@/components/skeletons";
|
} from "@/components/shared/skeletons";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { categorias, contas, lancamentos, pagadores } from "@/db/schema";
|
import { categorias, contas, lancamentos, pagadores } from "@/db/schema";
|
||||||
import {
|
|
||||||
INITIAL_BALANCE_CATEGORY_NAME,
|
|
||||||
INITIAL_BALANCE_CONDITION,
|
|
||||||
INITIAL_BALANCE_NOTE,
|
|
||||||
INITIAL_BALANCE_PAYMENT_METHOD,
|
|
||||||
INITIAL_BALANCE_TRANSACTION_TYPE,
|
|
||||||
} from "@/lib/accounts/constants";
|
|
||||||
import {
|
import {
|
||||||
type ActionResult,
|
type ActionResult,
|
||||||
handleActionError,
|
handleActionError,
|
||||||
revalidateForEntity,
|
revalidateForEntity,
|
||||||
} from "@/lib/actions/helpers";
|
} from "@/lib/actions/helpers";
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
|
import {
|
||||||
|
INITIAL_BALANCE_CATEGORY_NAME,
|
||||||
|
INITIAL_BALANCE_CONDITION,
|
||||||
|
INITIAL_BALANCE_NOTE,
|
||||||
|
INITIAL_BALANCE_PAYMENT_METHOD,
|
||||||
|
INITIAL_BALANCE_TRANSACTION_TYPE,
|
||||||
|
} from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||||
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
|
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { and, eq, ilike, not, sql } from "drizzle-orm";
|
import { and, eq, ilike, not, sql } from "drizzle-orm";
|
||||||
import { contas, lancamentos, pagadores } from "@/db/schema";
|
import { contas, lancamentos, pagadores } from "@/db/schema";
|
||||||
import { INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
|
import { INITIAL_BALANCE_NOTE } from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { loadLogoOptions } from "@/lib/logo/options";
|
import { loadLogoOptions } from "@/lib/logo/options";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiBankLine } from "@remixicon/react";
|
import { RiBankLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Contas | OpenMonetis",
|
title: "Contas | OpenMonetis",
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { DashboardGridSkeleton } from "@/components/skeletons";
|
import { DashboardGridSkeleton } from "@/components/shared/skeletons";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
/**
|
|
||||||
* Loading state para a página do dashboard
|
|
||||||
* Estrutura: Welcome Banner → Month Picker → Section Cards → Widget Grid
|
|
||||||
*/
|
|
||||||
export default function DashboardLoading() {
|
export default function DashboardLoading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4">
|
<main className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
orcamentos,
|
orcamentos,
|
||||||
pagadores,
|
pagadores,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
|
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiSparklingLine } from "@remixicon/react";
|
import { RiSparklingLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Insights | OpenMonetis",
|
title: "Insights | OpenMonetis",
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import {
|
|||||||
lancamentos,
|
lancamentos,
|
||||||
pagadores,
|
pagadores,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
|
import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers";
|
||||||
|
import type { ActionResult } from "@/lib/actions/types";
|
||||||
|
import { getUser } from "@/lib/auth/server";
|
||||||
import {
|
import {
|
||||||
INITIAL_BALANCE_CONDITION,
|
INITIAL_BALANCE_CONDITION,
|
||||||
INITIAL_BALANCE_NOTE,
|
INITIAL_BALANCE_NOTE,
|
||||||
INITIAL_BALANCE_PAYMENT_METHOD,
|
INITIAL_BALANCE_PAYMENT_METHOD,
|
||||||
INITIAL_BALANCE_TRANSACTION_TYPE,
|
INITIAL_BALANCE_TRANSACTION_TYPE,
|
||||||
} from "@/lib/accounts/constants";
|
} from "@/lib/contas/constants";
|
||||||
import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers";
|
|
||||||
import type { ActionResult } from "@/lib/actions/types";
|
|
||||||
import { getUser } from "@/lib/auth/server";
|
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
LANCAMENTO_CONDITIONS,
|
LANCAMENTO_CONDITIONS,
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import {
|
|||||||
generateAnticipationNote,
|
generateAnticipationNote,
|
||||||
} from "@/lib/installments/anticipation-helpers";
|
} from "@/lib/installments/anticipation-helpers";
|
||||||
import type {
|
import type {
|
||||||
|
InstallmentAnticipationWithRelations,
|
||||||
CancelAnticipationInput,
|
CancelAnticipationInput,
|
||||||
CreateAnticipationInput,
|
CreateAnticipationInput,
|
||||||
EligibleInstallment,
|
EligibleInstallment,
|
||||||
InstallmentAnticipationWithRelations,
|
|
||||||
} from "@/lib/installments/anticipation-types";
|
} from "@/lib/installments/anticipation-types";
|
||||||
import { uuidSchema } from "@/lib/schemas/common";
|
import { uuidSchema } from "@/lib/schemas/common";
|
||||||
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
|
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
|
||||||
@@ -94,7 +94,7 @@ export async function getEligibleInstallmentsAction(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const eligibleInstallments: EligibleInstallment[] = rows.map((row) => ({
|
const eligibleInstallments: EligibleInstallment[] = rows.map((row: any) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
amount: row.amount,
|
amount: row.amount,
|
||||||
@@ -110,10 +110,11 @@ export async function getEligibleInstallmentsAction(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
message: "Parcelas elegíveis carregadas.",
|
||||||
data: eligibleInstallments,
|
data: eligibleInstallments,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(error) as ActionResult<EligibleInstallment[]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ export async function createInstallmentAnticipationAction(
|
|||||||
|
|
||||||
// 2. Calcular valor total
|
// 2. Calcular valor total
|
||||||
const totalAmountCents = installments.reduce(
|
const totalAmountCents = installments.reduce(
|
||||||
(sum, inst) => sum + Number(inst.amount) * 100,
|
(sum: number, inst: any) => sum + Number(inst.amount) * 100,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const totalAmount = totalAmountCents / 100;
|
const totalAmount = totalAmountCents / 100;
|
||||||
@@ -181,7 +182,7 @@ export async function createInstallmentAnticipationAction(
|
|||||||
const firstInstallment = installments[0];
|
const firstInstallment = installments[0];
|
||||||
|
|
||||||
// 4. Criar lançamento e antecipação em transação
|
// 4. Criar lançamento e antecipação em transação
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx: any) => {
|
||||||
// 4.1. Criar o lançamento de antecipação (com desconto aplicado)
|
// 4.1. Criar o lançamento de antecipação (com desconto aplicado)
|
||||||
const [newLancamento] = await tx
|
const [newLancamento] = await tx
|
||||||
.insert(lancamentos)
|
.insert(lancamentos)
|
||||||
@@ -205,7 +206,7 @@ export async function createInstallmentAnticipationAction(
|
|||||||
note:
|
note:
|
||||||
data.note ||
|
data.note ||
|
||||||
generateAnticipationNote(
|
generateAnticipationNote(
|
||||||
installments.map((inst) => ({
|
installments.map((inst: any) => ({
|
||||||
id: inst.id,
|
id: inst.id,
|
||||||
name: inst.name,
|
name: inst.name,
|
||||||
amount: inst.amount,
|
amount: inst.amount,
|
||||||
@@ -329,10 +330,13 @@ export async function getInstallmentAnticipationsAction(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
message: "Antecipações carregadas.",
|
||||||
data: anticipations,
|
data: anticipations,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(
|
||||||
|
error,
|
||||||
|
) as ActionResult<InstallmentAnticipationWithRelations[]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +351,7 @@ export async function cancelInstallmentAnticipationAction(
|
|||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
const data = cancelAnticipationSchema.parse(input);
|
const data = cancelAnticipationSchema.parse(input);
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx: any) => {
|
||||||
// 1. Buscar antecipação usando query builder
|
// 1. Buscar antecipação usando query builder
|
||||||
const anticipationRows = await tx
|
const anticipationRows = await tx
|
||||||
.select({
|
.select({
|
||||||
@@ -469,9 +473,12 @@ export async function getAnticipationDetailsAction(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
message: "Detalhes da antecipação carregados.",
|
||||||
data: anticipation,
|
data: anticipation,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return handleActionError(error);
|
return handleActionError(
|
||||||
|
error,
|
||||||
|
) as ActionResult<InstallmentAnticipationWithRelations>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
lancamentos,
|
lancamentos,
|
||||||
pagadores,
|
pagadores,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import { INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
|
import { INITIAL_BALANCE_NOTE } from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
|
|
||||||
export async function fetchLancamentos(filters: SQL[]) {
|
export async function fetchLancamentos(filters: SQL[]) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiArrowLeftRightLine } from "@remixicon/react";
|
import { RiArrowLeftRightLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Lançamentos | OpenMonetis",
|
title: "Lançamentos | OpenMonetis",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
FilterSkeleton,
|
FilterSkeleton,
|
||||||
TransactionsTableSkeleton,
|
TransactionsTableSkeleton,
|
||||||
} from "@/components/skeletons";
|
} from "@/components/shared/skeletons";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FontProvider } from "@/components/font-provider";
|
import { FontProvider } from "@/components/font-provider";
|
||||||
import { AppNavbar } from "@/components/navbar/app-navbar";
|
import { AppNavbar } from "@/components/navigation/navbar/app-navbar";
|
||||||
import { PrivacyProvider } from "@/components/privacy-provider";
|
import { PrivacyProvider } from "@/components/privacy-provider";
|
||||||
import { getUserSession } from "@/lib/auth/server";
|
import { getUserSession } from "@/lib/auth/server";
|
||||||
import { fetchDashboardNotifications } from "@/lib/dashboard/notifications";
|
import { fetchDashboardNotifications } from "@/lib/dashboard/notifications";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
orcamentos,
|
orcamentos,
|
||||||
pagadores,
|
pagadores,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/contas/constants";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiBarChart2Line } from "@remixicon/react";
|
import { RiBarChart2Line } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Orçamentos | OpenMonetis",
|
title: "Orçamentos | OpenMonetis",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiGroupLine } from "@remixicon/react";
|
import { RiGroupLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Pagadores | OpenMonetis",
|
title: "Pagadores | OpenMonetis",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { revalidatePath } from "next/cache";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { preLancamentos } from "@/db/schema";
|
import { preLancamentos } from "@/db/schema";
|
||||||
import { handleActionError } from "@/lib/actions/helpers";
|
import { handleActionError, revalidateForEntity } from "@/lib/actions/helpers";
|
||||||
import type { ActionResult } from "@/lib/actions/types";
|
import type { ActionResult } from "@/lib/actions/types";
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
@@ -17,6 +16,10 @@ const discardInboxSchema = z.object({
|
|||||||
inboxItemId: z.string().uuid("ID do item inválido"),
|
inboxItemId: z.string().uuid("ID do item inválido"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const restoreDiscardedInboxSchema = z.object({
|
||||||
|
inboxItemId: z.string().uuid("ID do item inválido"),
|
||||||
|
});
|
||||||
|
|
||||||
const bulkDiscardSchema = z.object({
|
const bulkDiscardSchema = z.object({
|
||||||
inboxItemIds: z.array(z.string().uuid()).min(1, "Selecione ao menos um item"),
|
inboxItemIds: z.array(z.string().uuid()).min(1, "Selecione ao menos um item"),
|
||||||
});
|
});
|
||||||
@@ -30,9 +33,7 @@ const bulkDeleteInboxSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function revalidateInbox() {
|
function revalidateInbox() {
|
||||||
revalidatePath("/pre-lancamentos");
|
revalidateForEntity("inbox");
|
||||||
revalidatePath("/lancamentos");
|
|
||||||
revalidatePath("/dashboard");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,6 +167,54 @@ export async function bulkDiscardInboxItemsAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restoreDiscardedInboxItemAction(
|
||||||
|
input: z.infer<typeof restoreDiscardedInboxSchema>,
|
||||||
|
): Promise<ActionResult> {
|
||||||
|
try {
|
||||||
|
const user = await getUser();
|
||||||
|
const data = restoreDiscardedInboxSchema.parse(input);
|
||||||
|
|
||||||
|
const [item] = await db
|
||||||
|
.select({ id: preLancamentos.id })
|
||||||
|
.from(preLancamentos)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(preLancamentos.id, data.inboxItemId),
|
||||||
|
eq(preLancamentos.userId, user.id),
|
||||||
|
eq(preLancamentos.status, "discarded"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Item não encontrado ou não está descartado.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(preLancamentos)
|
||||||
|
.set({
|
||||||
|
status: "pending",
|
||||||
|
discardedAt: null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(preLancamentos.id, data.inboxItemId),
|
||||||
|
eq(preLancamentos.userId, user.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
revalidateInbox();
|
||||||
|
|
||||||
|
return { success: true, message: "Item voltou para pendentes." };
|
||||||
|
} catch (error) {
|
||||||
|
return handleActionError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteInboxItemAction(
|
export async function deleteInboxItemAction(
|
||||||
input: z.infer<typeof deleteInboxSchema>,
|
input: z.infer<typeof deleteInboxSchema>,
|
||||||
): Promise<ActionResult> {
|
): Promise<ActionResult> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiAtLine } from "@remixicon/react";
|
import { RiAtLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Pré-Lançamentos | OpenMonetis",
|
title: "Pré-Lançamentos | OpenMonetis",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiSecurePaymentLine } from "@remixicon/react";
|
import { RiSecurePaymentLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Análise de Parcelas | OpenMonetis",
|
title: "Análise de Parcelas | OpenMonetis",
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiStore2Line } from "@remixicon/react";
|
import { RiStore2Line } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Top Estabelecimentos | OpenMonetis",
|
title: "Top Estabelecimentos | OpenMonetis",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col gap-4 px-6">
|
<main className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Skeleton className="h-8 w-48" />
|
<Skeleton className="h-8 w-48" />
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { EstablishmentsList } from "@/components/top-estabelecimentos/establishments-list";
|
import { EstablishmentsList } from "@/components/relatorios/estabelecimentos/establishments-list";
|
||||||
import { HighlightsCards } from "@/components/top-estabelecimentos/highlights-cards";
|
import { HighlightsCards } from "@/components/relatorios/estabelecimentos/highlights-cards";
|
||||||
import { PeriodFilterButtons } from "@/components/top-estabelecimentos/period-filter";
|
import { PeriodFilterButtons } from "@/components/relatorios/estabelecimentos/period-filter";
|
||||||
import { SummaryCards } from "@/components/top-estabelecimentos/summary-cards";
|
import { SummaryCards } from "@/components/relatorios/estabelecimentos/summary-cards";
|
||||||
import { TopCategories } from "@/components/top-estabelecimentos/top-categories";
|
import { TopCategories } from "@/components/relatorios/estabelecimentos/top-categories";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { getUser } from "@/lib/auth/server";
|
import { getUser } from "@/lib/auth/server";
|
||||||
import {
|
import {
|
||||||
fetchTopEstabelecimentosData,
|
fetchTopEstabelecimentosData,
|
||||||
type PeriodFilter,
|
type PeriodFilter,
|
||||||
} from "@/lib/top-estabelecimentos/fetch-data";
|
} from "@/lib/relatorios/estabelecimentos/fetch-data";
|
||||||
import { parsePeriodParam } from "@/lib/utils/period";
|
import { parsePeriodParam } from "@/lib/utils/period";
|
||||||
|
|
||||||
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
type PageSearchParams = Promise<Record<string, string | string[] | undefined>>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiFileChartLine } from "@remixicon/react";
|
import { RiFileChartLine } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Tendências | OpenMonetis",
|
title: "Tendências | OpenMonetis",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CategoryReportSkeleton } from "@/components/skeletons/category-report-skeleton";
|
import { CategoryReportSkeleton } from "@/components/shared/skeletons/category-report-skeleton";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { RiBankCard2Line } from "@remixicon/react";
|
import { RiBankCard2Line } from "@remixicon/react";
|
||||||
import PageDescription from "@/components/page-description";
|
import PageDescription from "@/components/shared/page-description";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Uso de Cartões | OpenMonetis",
|
title: "Uso de Cartões | OpenMonetis",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
DEFAULT_LANCAMENTOS_COLUMN_ORDER,
|
DEFAULT_LANCAMENTOS_COLUMN_ORDER,
|
||||||
LANCAMENTOS_COLUMN_LABELS,
|
LANCAMENTOS_COLUMN_LABELS,
|
||||||
} from "@/lib/lancamentos/column-order";
|
} from "@/lib/lancamentos/column-order";
|
||||||
import { FONT_OPTIONS, getFontVariable } from "@/public/fonts/font_index";
|
import { FONT_OPTIONS } from "@/public/fonts/font_index";
|
||||||
|
|
||||||
interface PreferencesFormProps {
|
interface PreferencesFormProps {
|
||||||
disableMagnetlines: boolean;
|
disableMagnetlines: boolean;
|
||||||
@@ -189,14 +189,6 @@ export function PreferencesForm({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p
|
|
||||||
className="text-sm text-muted-foreground pt-1"
|
|
||||||
style={{
|
|
||||||
fontFamily: getFontVariable(selectedSystemFont),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Suas finanças em um só lugar
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fonte de valores */}
|
{/* Fonte de valores */}
|
||||||
@@ -223,14 +215,6 @@ export function PreferencesForm({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p
|
|
||||||
className="text-sm text-muted-foreground pt-1 tabular-nums"
|
|
||||||
style={{
|
|
||||||
fontFamily: getFontVariable(selectedMoneyFont),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
R$ 1.234,56
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
RiArchiveLine,
|
RiArchiveLine,
|
||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBin5Line,
|
RiDeleteBin5Line,
|
||||||
RiEyeLine,
|
RiFileList2Line,
|
||||||
RiInboxUnarchiveLine,
|
RiInboxUnarchiveLine,
|
||||||
RiPencilLine,
|
RiPencilLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
@@ -60,7 +60,7 @@ export function NoteCard({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "detalhes",
|
label: "detalhes",
|
||||||
icon: <RiEyeLine className="size-4" aria-hidden />,
|
icon: <RiFileList2Line className="size-4" aria-hidden />,
|
||||||
onClick: onDetails,
|
onClick: onDetails,
|
||||||
variant: "default" as const,
|
variant: "default" as const,
|
||||||
},
|
},
|
||||||
@@ -115,7 +115,9 @@ export function NoteCard({
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`leading-relaxed ${
|
className={`leading-relaxed ${
|
||||||
task.completed ? "text-muted-foreground" : "text-foreground"
|
task.completed
|
||||||
|
? "text-muted-foreground line-through"
|
||||||
|
: "text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.text}
|
{task.text}
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ export function NoteDetailsDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{isTask ? (
|
{isTask ? (
|
||||||
<div className="max-h-[320px] overflow-auto space-y-3">
|
<Card className="max-h-[320px] overflow-auto gap-2 p-2">
|
||||||
{sortedTasks.map((task) => (
|
{sortedTasks.map((task) => (
|
||||||
<Card
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
className="flex gap-3 p-3 flex-row items-center"
|
className="flex items-center gap-3 px-3 py-1.5 space-y-1 rounded-md hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border ${
|
||||||
@@ -91,14 +91,16 @@ export function NoteDetailsDialog({
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`text-sm ${
|
className={`text-sm ${
|
||||||
task.completed ? "text-muted-foreground" : "text-foreground"
|
task.completed
|
||||||
|
? "text-muted-foreground line-through"
|
||||||
|
: "text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{task.text}
|
{task.text}
|
||||||
</span>
|
</span>
|
||||||
</Card>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="max-h-[320px] overflow-auto whitespace-pre-line wrap-break-word text-sm text-foreground">
|
<div className="max-h-[320px] overflow-auto whitespace-pre-line wrap-break-word text-sm text-foreground">
|
||||||
{note.description}
|
{note.description}
|
||||||
|
|||||||
@@ -85,17 +85,17 @@ export function NoteDialog({
|
|||||||
|
|
||||||
const initialState = buildInitialValues(note);
|
const initialState = buildInitialValues(note);
|
||||||
|
|
||||||
const { formState, updateField, setFormState } =
|
const { formState, resetForm, updateField } =
|
||||||
useFormState<NoteFormValues>(initialState);
|
useFormState<NoteFormValues>(initialState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setFormState(buildInitialValues(note));
|
resetForm(buildInitialValues(note));
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setNewTaskText("");
|
setNewTaskText("");
|
||||||
requestAnimationFrame(() => titleRef.current?.focus());
|
requestAnimationFrame(() => titleRef.current?.focus());
|
||||||
}
|
}
|
||||||
}, [dialogOpen, note, setFormState]);
|
}, [dialogOpen, note, resetForm]);
|
||||||
|
|
||||||
const dialogTitle = mode === "create" ? "Nova anotação" : "Editar anotação";
|
const dialogTitle = mode === "create" ? "Nova anotação" : "Editar anotação";
|
||||||
const description =
|
const description =
|
||||||
@@ -338,7 +338,7 @@ export function NoteDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sortedTasks.length > 0 && (
|
{sortedTasks.length > 0 && (
|
||||||
<div className="space-y-1 max-h-[240px] overflow-y-auto pr-1">
|
<div className="space-y-1 max-h-[300px] overflow-y-auto pr-1 mt-4 rounded-md p-2 bg-card ">
|
||||||
{sortedTasks.map((task) => (
|
{sortedTasks.map((task) => (
|
||||||
<div
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
deleteNoteAction,
|
deleteNoteAction,
|
||||||
} from "@/app/(dashboard)/anotacoes/actions";
|
} from "@/app/(dashboard)/anotacoes/actions";
|
||||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/shared/empty-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useDraggableDialog } from "@/hooks/use-draggable-dialog";
|
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
import { useDraggableDialog } from "./use-draggable-dialog";
|
||||||
|
|
||||||
type Variant = React.ComponentProps<typeof Button>["variant"];
|
type Variant = React.ComponentProps<typeof Button>["variant"];
|
||||||
type Size = React.ComponentProps<typeof Button>["size"];
|
type Size = React.ComponentProps<typeof Button>["size"];
|
||||||
@@ -50,8 +50,11 @@ export function CalculatorDialogContent({
|
|||||||
return (
|
return (
|
||||||
<DialogContent
|
<DialogContent
|
||||||
ref={contentRefCallback}
|
ref={contentRefCallback}
|
||||||
className="p-4 sm:max-w-sm"
|
className="p-5 sm:max-w-sm sm:p-6"
|
||||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
|
onFocusOutside={(e) => e.preventDefault()}
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<DialogHeader
|
<DialogHeader
|
||||||
className="cursor-grab select-none space-y-2 active:cursor-grabbing"
|
className="cursor-grab select-none space-y-2 active:cursor-grabbing"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { RiCheckLine, RiFileCopyLine } from "@remixicon/react";
|
import { RiCheckLine, RiFileCopyLine } from "@remixicon/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
export type CalculatorDisplayProps = {
|
export type CalculatorDisplayProps = {
|
||||||
history: string | null;
|
history: string | null;
|
||||||
@@ -7,6 +8,7 @@ export type CalculatorDisplayProps = {
|
|||||||
resultText: string | null;
|
resultText: string | null;
|
||||||
copied: boolean;
|
copied: boolean;
|
||||||
onCopy: () => void;
|
onCopy: () => void;
|
||||||
|
isResultView: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CalculatorDisplay({
|
export function CalculatorDisplay({
|
||||||
@@ -15,14 +17,27 @@ export function CalculatorDisplay({
|
|||||||
resultText,
|
resultText,
|
||||||
copied,
|
copied,
|
||||||
onCopy,
|
onCopy,
|
||||||
|
isResultView,
|
||||||
}: CalculatorDisplayProps) {
|
}: CalculatorDisplayProps) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border bg-muted px-4 py-5 text-right">
|
<div className="flex h-24 flex-col rounded-xl border bg-muted px-4 py-4 text-right">
|
||||||
{history && (
|
<div className="min-h-5 truncate text-sm text-muted-foreground">
|
||||||
<div className="text-sm text-muted-foreground">{history}</div>
|
{history ?? (
|
||||||
)}
|
<span
|
||||||
<div className="flex items-center justify-end gap-2">
|
className="pointer-events-none opacity-0 select-none"
|
||||||
<div className="text-right text-3xl font-semibold tracking-tight tabular-nums">
|
aria-hidden
|
||||||
|
>
|
||||||
|
0 + 0
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex items-end justify-end gap-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"truncate text-right font-semibold tracking-tight tabular-nums leading-none transition-all",
|
||||||
|
isResultView ? "text-2xl" : "text-3xl",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{expression}
|
{expression}
|
||||||
</div>
|
</div>
|
||||||
{resultText && (
|
{resultText && (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { CalculatorButtonConfig } from "@/components/calculadora/use-calculator-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { CalculatorButtonConfig } from "@/hooks/use-calculator-state";
|
|
||||||
import type { Operator } from "@/lib/utils/calculator";
|
import type { Operator } from "@/lib/utils/calculator";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad";
|
import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad";
|
||||||
|
import { useCalculatorKeyboard } from "@/components/calculadora/use-calculator-keyboard";
|
||||||
|
import { useCalculatorState } from "@/components/calculadora/use-calculator-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useCalculatorKeyboard } from "@/hooks/use-calculator-keyboard";
|
|
||||||
import { useCalculatorState } from "@/hooks/use-calculator-state";
|
|
||||||
import { CalculatorDisplay } from "./calculator-display";
|
import { CalculatorDisplay } from "./calculator-display";
|
||||||
|
|
||||||
type CalculatorProps = {
|
type CalculatorProps = {
|
||||||
@@ -64,6 +64,7 @@ export default function Calculator({
|
|||||||
resultText={resultText}
|
resultText={resultText}
|
||||||
copied={copied}
|
copied={copied}
|
||||||
onCopy={copyToClipboard}
|
onCopy={copyToClipboard}
|
||||||
|
isResultView={Boolean(history)}
|
||||||
/>
|
/>
|
||||||
<CalculatorKeypad buttons={buttons} activeOperator={operator} />
|
<CalculatorKeypad buttons={buttons} activeOperator={operator} />
|
||||||
{onSelectValue && (
|
{onSelectValue && (
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ function clampPosition(
|
|||||||
elementWidth: number,
|
elementWidth: number,
|
||||||
elementHeight: number,
|
elementHeight: number,
|
||||||
): Position {
|
): Position {
|
||||||
const maxX = window.innerWidth - MIN_VISIBLE_PX;
|
// Dialog starts centered (left/top 50% + translate(-50%, -50%)).
|
||||||
const minX = MIN_VISIBLE_PX - elementWidth;
|
// Clamp offsets so at least MIN_VISIBLE_PX remains visible on each axis.
|
||||||
const maxY = window.innerHeight - MIN_VISIBLE_PX;
|
const halfViewportWidth = window.innerWidth / 2;
|
||||||
const minY = MIN_VISIBLE_PX - elementHeight;
|
const halfViewportHeight = window.innerHeight / 2;
|
||||||
|
const halfElementWidth = elementWidth / 2;
|
||||||
|
const halfElementHeight = elementHeight / 2;
|
||||||
|
|
||||||
|
const minX = MIN_VISIBLE_PX - (halfViewportWidth + halfElementWidth);
|
||||||
|
const maxX = halfViewportWidth + halfElementWidth - MIN_VISIBLE_PX;
|
||||||
|
const minY = MIN_VISIBLE_PX - (halfViewportHeight + halfElementHeight);
|
||||||
|
const maxY = halfViewportHeight + halfElementHeight - MIN_VISIBLE_PX;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: Math.min(Math.max(x, minX), maxX),
|
x: Math.min(Math.max(x, minX), maxX),
|
||||||
@@ -21,11 +28,14 @@ function clampPosition(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTranslate(el: HTMLElement, x: number, y: number) {
|
function applyPosition(el: HTMLElement, x: number, y: number) {
|
||||||
if (x === 0 && y === 0) {
|
if (x === 0 && y === 0) {
|
||||||
el.style.translate = "";
|
el.style.translate = "";
|
||||||
|
el.style.transform = "";
|
||||||
} else {
|
} else {
|
||||||
el.style.translate = `${x}px ${y}px`;
|
// Keep the dialog's centered baseline (-50%, -50%) and only add drag offset.
|
||||||
|
el.style.translate = `calc(-50% + ${x}px) calc(-50% + ${y}px)`;
|
||||||
|
el.style.transform = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,18 +66,28 @@ export function useDraggableDialog() {
|
|||||||
const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight);
|
const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight);
|
||||||
|
|
||||||
offset.current = clamped;
|
offset.current = clamped;
|
||||||
applyTranslate(el, clamped.x, clamped.y);
|
applyPosition(el, clamped.x, clamped.y);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onPointerUp = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
const onPointerUp = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||||
dragStart.current = null;
|
dragStart.current = null;
|
||||||
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
if ((e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) {
|
||||||
|
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerCancel = useCallback(() => {
|
||||||
|
dragStart.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onLostPointerCapture = useCallback(() => {
|
||||||
|
dragStart.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetPosition = useCallback(() => {
|
const resetPosition = useCallback(() => {
|
||||||
offset.current = { x: 0, y: 0 };
|
offset.current = { x: 0, y: 0 };
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
applyTranslate(contentRef.current, 0, 0);
|
applyPosition(contentRef.current, 0, 0);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -75,6 +95,8 @@ export function useDraggableDialog() {
|
|||||||
onPointerDown,
|
onPointerDown,
|
||||||
onPointerMove,
|
onPointerMove,
|
||||||
onPointerUp,
|
onPointerUp,
|
||||||
|
onPointerCancel,
|
||||||
|
onLostPointerCapture,
|
||||||
style: { touchAction: "none" as const, cursor: "grab" },
|
style: { touchAction: "none" as const, cursor: "grab" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
updateCardAction,
|
updateCardAction,
|
||||||
} from "@/app/(dashboard)/cartoes/actions";
|
} from "@/app/(dashboard)/cartoes/actions";
|
||||||
import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker";
|
import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker";
|
||||||
|
import { useLogoSelection } from "@/components/logo-picker/use-logo-selection";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -25,7 +26,6 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useControlledState } from "@/hooks/use-controlled-state";
|
import { useControlledState } from "@/hooks/use-controlled-state";
|
||||||
import { useFormState } from "@/hooks/use-form-state";
|
import { useFormState } from "@/hooks/use-form-state";
|
||||||
import { useLogoSelection } from "@/hooks/use-logo-selection";
|
|
||||||
import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo";
|
import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo";
|
||||||
import { formatLimitInput } from "@/lib/utils/currency";
|
import { formatLimitInput } from "@/lib/utils/currency";
|
||||||
import { CardFormFields } from "./card-form-fields";
|
import { CardFormFields } from "./card-form-fields";
|
||||||
@@ -100,16 +100,16 @@ export function CardDialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Use form state hook for form management
|
// Use form state hook for form management
|
||||||
const { formState, updateField, updateFields, setFormState } =
|
const { formState, resetForm, updateField, updateFields } =
|
||||||
useFormState<CardFormValues>(initialState);
|
useFormState<CardFormValues>(initialState);
|
||||||
|
|
||||||
// Reset form when dialog opens
|
// Reset form when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
}
|
}
|
||||||
}, [dialogOpen, initialState, setFormState]);
|
}, [dialogOpen, initialState, resetForm]);
|
||||||
|
|
||||||
// Close logo dialog when main dialog closes
|
// Close logo dialog when main dialog closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -173,7 +173,7 @@ export function CardDialog({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ export function CardDialog({
|
|||||||
toast.error(result.error);
|
toast.error(result.error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[card?.id, formState, initialState, mode, setDialogOpen, setFormState],
|
[card?.id, formState, initialState, mode, resetForm, setDialogOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = mode === "create" ? "Novo cartão" : "Editar cartão";
|
const title = mode === "create" ? "Novo cartão" : "Editar cartão";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
RiChat3Line,
|
RiChat3Line,
|
||||||
RiDeleteBin5Line,
|
RiDeleteBin5Line,
|
||||||
RiEyeLine,
|
RiFileList2Line,
|
||||||
RiPencilLine,
|
RiPencilLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@@ -143,7 +143,7 @@ export function CardItem({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "ver fatura",
|
label: "ver fatura",
|
||||||
icon: <RiEyeLine className="size-4" aria-hidden />,
|
icon: <RiFileList2Line className="size-4" aria-hidden />,
|
||||||
onClick: onInvoice,
|
onClick: onInvoice,
|
||||||
className: "text-primary",
|
className: "text-primary",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { deleteCardAction } from "@/app/(dashboard)/cartoes/actions";
|
import { deleteCardAction } from "@/app/(dashboard)/cartoes/actions";
|
||||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/shared/empty-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|||||||
@@ -76,16 +76,16 @@ export function CategoryDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use form state hook for form management
|
// Use form state hook for form management
|
||||||
const { formState, updateField, setFormState } =
|
const { formState, resetForm, updateField } =
|
||||||
useFormState<CategoryFormValues>(initialState);
|
useFormState<CategoryFormValues>(initialState);
|
||||||
|
|
||||||
// Reset form when dialog opens
|
// Reset form when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
}
|
}
|
||||||
}, [dialogOpen, setFormState, initialState]);
|
}, [dialogOpen, initialState, resetForm]);
|
||||||
|
|
||||||
// Clear error when dialog closes
|
// Clear error when dialog closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -123,7 +123,7 @@ export function CategoryDialog({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
updateAccountAction,
|
updateAccountAction,
|
||||||
} from "@/app/(dashboard)/contas/actions";
|
} from "@/app/(dashboard)/contas/actions";
|
||||||
import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker";
|
import { LogoPickerDialog, LogoPickerTrigger } from "@/components/logo-picker";
|
||||||
|
import { useLogoSelection } from "@/components/logo-picker/use-logo-selection";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -25,7 +26,6 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useControlledState } from "@/hooks/use-controlled-state";
|
import { useControlledState } from "@/hooks/use-controlled-state";
|
||||||
import { useFormState } from "@/hooks/use-form-state";
|
import { useFormState } from "@/hooks/use-form-state";
|
||||||
import { useLogoSelection } from "@/hooks/use-logo-selection";
|
|
||||||
import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo";
|
import { deriveNameFromLogo, normalizeLogo } from "@/lib/logo";
|
||||||
import { formatInitialBalanceInput } from "@/lib/utils/currency";
|
import { formatInitialBalanceInput } from "@/lib/utils/currency";
|
||||||
|
|
||||||
@@ -126,16 +126,16 @@ export function AccountDialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Use form state hook for form management
|
// Use form state hook for form management
|
||||||
const { formState, updateField, updateFields, setFormState } =
|
const { formState, resetForm, updateField, updateFields } =
|
||||||
useFormState<AccountFormValues>(initialState);
|
useFormState<AccountFormValues>(initialState);
|
||||||
|
|
||||||
// Reset form when dialog opens
|
// Reset form when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
}
|
}
|
||||||
}, [dialogOpen, initialState, setFormState]);
|
}, [dialogOpen, initialState, resetForm]);
|
||||||
|
|
||||||
// Close logo dialog when main dialog closes
|
// Close logo dialog when main dialog closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -190,7 +190,7 @@ export function AccountDialog({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ export function AccountDialog({
|
|||||||
toast.error(result.error);
|
toast.error(result.error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[account?.id, formState, initialState, mode, setDialogOpen, setFormState],
|
[account?.id, formState, initialState, mode, resetForm, setDialogOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = mode === "create" ? "Nova conta" : "Editar conta";
|
const title = mode === "create" ? "Nova conta" : "Editar conta";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toast } from "sonner";
|
|||||||
import { deleteAccountAction } from "@/app/(dashboard)/contas/actions";
|
import { deleteAccountAction } from "@/app/(dashboard)/contas/actions";
|
||||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { AccountCard } from "@/components/contas/account-card";
|
import { AccountCard } from "@/components/contas/account-card";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/shared/empty-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { getCurrentPeriod } from "@/lib/utils/period";
|
import { getCurrentPeriod } from "@/lib/utils/period";
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export function BoletosWidget({ boletos }: BoletosWidgetProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="max-w-md"
|
className="max-w-[calc(100%-2rem)] sm:max-w-md"
|
||||||
onEscapeKeyDown={(event) => {
|
onEscapeKeyDown={(event) => {
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import {
|
|||||||
sortableKeyboardCoordinates,
|
sortableKeyboardCoordinates,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import {
|
import {
|
||||||
RiArrowDownLine,
|
RiAddCircleLine,
|
||||||
RiArrowUpLine,
|
|
||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiCloseLine,
|
RiCloseLine,
|
||||||
RiDragMove2Line,
|
RiDragMove2Line,
|
||||||
@@ -201,11 +200,11 @@ export function DashboardGridEditable({
|
|||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
{!isEditing ? (
|
{!isEditing ? (
|
||||||
<div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-2 px-1">
|
<div className="flex w-full min-w-0 flex-col gap-1 px-1 sm:w-auto sm:flex-row sm:items-center sm:gap-2">
|
||||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
Ações rápidas
|
Ações rápidas
|
||||||
</span>
|
</span>
|
||||||
<div className="-mb-1 flex items-center gap-2 overflow-x-auto pb-1 sm:mb-0 sm:overflow-visible sm:pb-0">
|
<div className="-mb-1 grid w-full grid-cols-3 gap-1 pb-1 sm:mb-0 sm:flex sm:w-auto sm:items-center sm:gap-2 sm:overflow-visible sm:pb-0">
|
||||||
<LancamentoDialog
|
<LancamentoDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
pagadorOptions={quickActionOptions.pagadorOptions}
|
pagadorOptions={quickActionOptions.pagadorOptions}
|
||||||
@@ -218,9 +217,16 @@ export function DashboardGridEditable({
|
|||||||
defaultPeriod={period}
|
defaultPeriod={period}
|
||||||
defaultTransactionType="Receita"
|
defaultTransactionType="Receita"
|
||||||
trigger={
|
trigger={
|
||||||
<Button size="sm" variant="outline" className="gap-2">
|
<Button
|
||||||
<RiArrowUpLine className="size-4 text-success/80" />
|
size="sm"
|
||||||
Nova receita
|
variant="outline"
|
||||||
|
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-0.5">
|
||||||
|
<RiAddCircleLine className="size-3.5 shrink-0 text-success/80" />
|
||||||
|
</span>
|
||||||
|
<span className="sm:hidden">Receita</span>
|
||||||
|
<span className="hidden sm:inline">Nova receita</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -236,18 +242,30 @@ export function DashboardGridEditable({
|
|||||||
defaultPeriod={period}
|
defaultPeriod={period}
|
||||||
defaultTransactionType="Despesa"
|
defaultTransactionType="Despesa"
|
||||||
trigger={
|
trigger={
|
||||||
<Button size="sm" variant="outline" className="gap-2">
|
<Button
|
||||||
<RiArrowDownLine className="size-4 text-destructive/80" />
|
size="sm"
|
||||||
Nova despesa
|
variant="outline"
|
||||||
|
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-0.5">
|
||||||
|
<RiAddCircleLine className="size-3.5 shrink-0 text-destructive/80" />
|
||||||
|
</span>
|
||||||
|
<span className="sm:hidden">Despesa</span>
|
||||||
|
<span className="hidden sm:inline">Nova despesa</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<NoteDialog
|
<NoteDialog
|
||||||
mode="create"
|
mode="create"
|
||||||
trigger={
|
trigger={
|
||||||
<Button size="sm" variant="outline" className="gap-2">
|
<Button
|
||||||
<RiTodoLine className="size-4 text-info/80" />
|
size="sm"
|
||||||
Nova anotação
|
variant="outline"
|
||||||
|
className="h-12 w-full min-w-0 flex-col justify-center gap-0.5 px-1.5 text-sm whitespace-normal sm:h-8 sm:w-auto sm:flex-row sm:gap-2 sm:px-3 sm:whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<RiTodoLine className="size-3.5 shrink-0 text-info/80" />
|
||||||
|
<span className="sm:hidden">Anotação</span>
|
||||||
|
<span className="hidden sm:inline">Nova anotação</span>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -257,7 +275,7 @@ export function DashboardGridEditable({
|
|||||||
<div />
|
<div />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex w-full items-center justify-end gap-2 sm:w-auto">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -281,22 +299,23 @@ export function DashboardGridEditable({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="grid w-full grid-cols-2 gap-2 sm:flex sm:w-auto">
|
||||||
<WidgetSettingsDialog
|
<WidgetSettingsDialog
|
||||||
hiddenWidgets={hiddenWidgets}
|
hiddenWidgets={hiddenWidgets}
|
||||||
onToggleWidget={handleToggleWidget}
|
onToggleWidget={handleToggleWidget}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
|
triggerClassName="w-full sm:w-auto"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleStartEditing}
|
onClick={handleStartEditing}
|
||||||
className="gap-2"
|
className="w-full gap-2 sm:w-auto"
|
||||||
>
|
>
|
||||||
<RiDragMove2Line className="size-4" />
|
<RiDragMove2Line className="size-4" />
|
||||||
Reordenar
|
Reordenar
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function InstallmentGroupCard({
|
|||||||
{group.cartaoLogo && (
|
{group.cartaoLogo && (
|
||||||
<img
|
<img
|
||||||
src={`/logos/${group.cartaoLogo}`}
|
src={`/logos/${group.cartaoLogo}`}
|
||||||
alt={group.cartaoName}
|
alt={group.cartaoName ?? "Cartão"}
|
||||||
className="h-6 w-auto object-contain rounded"
|
className="h-6 w-auto object-contain rounded"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
InstallmentAnalysisData,
|
InstallmentAnalysisData,
|
||||||
InstallmentGroup,
|
InstallmentGroup,
|
||||||
PendingInvoice,
|
|
||||||
} from "@/lib/dashboard/expenses/installment-analysis";
|
} from "@/lib/dashboard/expenses/installment-analysis";
|
||||||
|
|
||||||
export type { InstallmentAnalysisData, InstallmentGroup, PendingInvoice };
|
export type { InstallmentAnalysisData, InstallmentGroup };
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ export function InvoicesWidget({ invoices }: InvoicesWidgetProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="max-w-md"
|
className="max-w-[calc(100%-2rem)] sm:max-w-md"
|
||||||
onEscapeKeyDown={(event) => {
|
onEscapeKeyDown={(event) => {
|
||||||
if (modalState === "processing") {
|
if (modalState === "processing") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RiEyeLine, RiPencilLine, RiTodoLine } from "@remixicon/react";
|
import { RiFileList2Line, RiPencilLine, RiTodoLine } from "@remixicon/react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { NoteDetailsDialog } from "@/components/anotacoes/note-details-dialog";
|
import { NoteDetailsDialog } from "@/components/anotacoes/note-details-dialog";
|
||||||
import { NoteDialog } from "@/components/anotacoes/note-dialog";
|
import { NoteDialog } from "@/components/anotacoes/note-dialog";
|
||||||
@@ -100,13 +100,10 @@ export function NotesWidget({ notes }: NotesWidgetProps) {
|
|||||||
{buildDisplayTitle(note.title)}
|
{buildDisplayTitle(note.title)}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1 flex items-center gap-2">
|
<div className="mt-1 flex items-center gap-2">
|
||||||
<Badge
|
<Badge variant="outline" className="h-5 px-1.5 text-[10px]">
|
||||||
variant="secondary"
|
|
||||||
className="h-5 px-1.5 text-[10px]"
|
|
||||||
>
|
|
||||||
{getTasksSummary(note)}
|
{getTasksSummary(note)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
<p className="truncate text-[11px] text-muted-foreground">
|
||||||
{DATE_FORMATTER.format(new Date(note.createdAt))}
|
{DATE_FORMATTER.format(new Date(note.createdAt))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +128,7 @@ export function NotesWidget({ notes }: NotesWidgetProps) {
|
|||||||
note.title,
|
note.title,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
<RiEyeLine className="size-4" />
|
<RiFileList2Line className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -14,24 +14,31 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { widgetsConfig } from "@/lib/dashboard/widgets/widgets-config";
|
import { widgetsConfig } from "@/lib/dashboard/widgets/widgets-config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type WidgetSettingsDialogProps = {
|
type WidgetSettingsDialogProps = {
|
||||||
hiddenWidgets: string[];
|
hiddenWidgets: string[];
|
||||||
onToggleWidget: (widgetId: string) => void;
|
onToggleWidget: (widgetId: string) => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
triggerClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WidgetSettingsDialog({
|
export function WidgetSettingsDialog({
|
||||||
hiddenWidgets,
|
hiddenWidgets,
|
||||||
onToggleWidget,
|
onToggleWidget,
|
||||||
onReset,
|
onReset,
|
||||||
|
triggerClassName,
|
||||||
}: WidgetSettingsDialogProps) {
|
}: WidgetSettingsDialogProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="gap-2">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className={cn("gap-2", triggerClassName)}
|
||||||
|
>
|
||||||
<RiSettings4Line className="size-4" />
|
<RiSettings4Line className="size-4" />
|
||||||
Widgets
|
Widgets
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import {
|
|||||||
saveInsightsAction,
|
saveInsightsAction,
|
||||||
} from "@/app/(dashboard)/insights/actions";
|
} from "@/app/(dashboard)/insights/actions";
|
||||||
import { DEFAULT_MODEL } from "@/app/(dashboard)/insights/data";
|
import { DEFAULT_MODEL } from "@/app/(dashboard)/insights/data";
|
||||||
|
import { EmptyState } from "@/components/shared/empty-state";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import type { InsightsResponse } from "@/lib/schemas/insights";
|
import type { InsightsResponse } from "@/lib/schemas/insights";
|
||||||
import { EmptyState } from "../empty-state";
|
|
||||||
import { InsightsGrid } from "./insights-grid";
|
import { InsightsGrid } from "./insights-grid";
|
||||||
import { ModelSelector } from "./model-selector";
|
import { ModelSelector } from "./model-selector";
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ interface AnticipateInstallmentsDialogProps {
|
|||||||
|
|
||||||
type AnticipationFormValues = {
|
type AnticipationFormValues = {
|
||||||
anticipationPeriod: string;
|
anticipationPeriod: string;
|
||||||
discount: number;
|
discount: string;
|
||||||
pagadorId: string;
|
pagadorId: string;
|
||||||
categoriaId: string;
|
categoriaId: string;
|
||||||
note: string;
|
note: string;
|
||||||
@@ -92,10 +92,10 @@ export function AnticipateInstallmentsDialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Use form state hook for form management
|
// Use form state hook for form management
|
||||||
const { formState, updateField, setFormState } =
|
const { formState, replaceForm, updateField } =
|
||||||
useFormState<AnticipationFormValues>({
|
useFormState<AnticipationFormValues>({
|
||||||
anticipationPeriod: defaultPeriod,
|
anticipationPeriod: defaultPeriod,
|
||||||
discount: 0,
|
discount: "0",
|
||||||
pagadorId: "",
|
pagadorId: "",
|
||||||
categoriaId: "",
|
categoriaId: "",
|
||||||
note: "",
|
note: "",
|
||||||
@@ -110,23 +110,25 @@ export function AnticipateInstallmentsDialog({
|
|||||||
|
|
||||||
getEligibleInstallmentsAction(seriesId)
|
getEligibleInstallmentsAction(seriesId)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.success && result.data) {
|
if (!result.success) {
|
||||||
setEligibleInstallments(result.data);
|
|
||||||
|
|
||||||
// Pré-preencher pagador e categoria da primeira parcela
|
|
||||||
if (result.data.length > 0) {
|
|
||||||
const first = result.data[0];
|
|
||||||
setFormState({
|
|
||||||
anticipationPeriod: defaultPeriod,
|
|
||||||
discount: 0,
|
|
||||||
pagadorId: first.pagadorId ?? "",
|
|
||||||
categoriaId: first.categoriaId ?? "",
|
|
||||||
note: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(result.error || "Erro ao carregar parcelas");
|
toast.error(result.error || "Erro ao carregar parcelas");
|
||||||
setEligibleInstallments([]);
|
setEligibleInstallments([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installments = result.data ?? [];
|
||||||
|
setEligibleInstallments(installments);
|
||||||
|
|
||||||
|
// Pré-preencher pagador e categoria da primeira parcela
|
||||||
|
if (installments.length > 0) {
|
||||||
|
const first = installments[0];
|
||||||
|
replaceForm({
|
||||||
|
anticipationPeriod: defaultPeriod,
|
||||||
|
discount: "0",
|
||||||
|
pagadorId: first.pagadorId ?? "",
|
||||||
|
categoriaId: first.categoriaId ?? "",
|
||||||
|
note: "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -138,7 +140,7 @@ export function AnticipateInstallmentsDialog({
|
|||||||
setIsLoadingInstallments(false);
|
setIsLoadingInstallments(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [dialogOpen, seriesId, defaultPeriod, setFormState]);
|
}, [defaultPeriod, dialogOpen, replaceForm, seriesId]);
|
||||||
|
|
||||||
const totalAmount = useMemo(() => {
|
const totalAmount = useMemo(() => {
|
||||||
return eligibleInstallments
|
return eligibleInstallments
|
||||||
@@ -268,9 +270,7 @@ export function AnticipateInstallmentsDialog({
|
|||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
id="anticipation-discount"
|
id="anticipation-discount"
|
||||||
value={formState.discount}
|
value={formState.discount}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => updateField("discount", value)}
|
||||||
updateField("discount", value ?? 0)
|
|
||||||
}
|
|
||||||
placeholder="R$ 0,00"
|
placeholder="R$ 0,00"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -59,14 +59,15 @@ export function AnticipationHistoryDialog({
|
|||||||
try {
|
try {
|
||||||
const result = await getInstallmentAnticipationsAction(seriesId);
|
const result = await getInstallmentAnticipationsAction(seriesId);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (!result.success) {
|
||||||
setAnticipations(result.data);
|
|
||||||
} else {
|
|
||||||
toast.error(
|
toast.error(
|
||||||
result.error || "Erro ao carregar histórico de antecipações",
|
result.error || "Erro ao carregar histórico de antecipações",
|
||||||
);
|
);
|
||||||
setAnticipations([]);
|
setAnticipations([]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAnticipations(result.data ?? []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao buscar antecipações:", error);
|
console.error("Erro ao buscar antecipações:", error);
|
||||||
toast.error("Erro ao carregar histórico de antecipações");
|
toast.error("Erro ao carregar histórico de antecipações");
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ export function LancamentoDetailsDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="p-0 sm:max-w-xl">
|
<DialogContent className="p-0 sm:max-w-xl sm:border-0 sm:p-2">
|
||||||
<div className="gap-2 space-y-4 py-6">
|
<div className="gap-2 space-y-4 py-4">
|
||||||
<CardHeader className="flex flex-row items-start border-b">
|
<CardHeader className="flex flex-row items-start border-b sm:border-b-0">
|
||||||
<div>
|
<div>
|
||||||
<DialogTitle className="group flex items-center gap-2 text-lg">
|
<DialogTitle className="group flex items-center gap-2 text-lg">
|
||||||
#{lancamento.id}
|
#{lancamento.id}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { MonthPicker } from "@/components/ui/monthpicker";
|
import { MonthPicker } from "@/components/ui/month-picker";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { MonthPicker } from "@/components/ui/monthpicker";
|
import { MonthPicker } from "@/components/ui/month-picker";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -36,7 +36,6 @@ import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers";
|
|||||||
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
|
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
|
||||||
import { getTodayDateString } from "@/lib/utils/date";
|
import { getTodayDateString } from "@/lib/utils/date";
|
||||||
import { displayPeriod } from "@/lib/utils/period";
|
import { displayPeriod } from "@/lib/utils/period";
|
||||||
import type { SelectOption } from "../../types";
|
|
||||||
import {
|
import {
|
||||||
CategoriaSelectContent,
|
CategoriaSelectContent,
|
||||||
ContaCartaoSelectContent,
|
ContaCartaoSelectContent,
|
||||||
@@ -45,6 +44,7 @@ import {
|
|||||||
TransactionTypeSelectContent,
|
TransactionTypeSelectContent,
|
||||||
} from "../select-items";
|
} from "../select-items";
|
||||||
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
||||||
|
import type { SelectOption } from "../types";
|
||||||
|
|
||||||
/** Payment methods sem Boleto para este modal */
|
/** Payment methods sem Boleto para este modal */
|
||||||
const MASS_ADD_PAYMENT_METHODS = LANCAMENTO_PAYMENT_METHODS.filter(
|
const MASS_ADD_PAYMENT_METHODS = LANCAMENTO_PAYMENT_METHODS.filter(
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
|
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
|
||||||
|
import {
|
||||||
|
getPrimaryPdfColor,
|
||||||
|
loadExportLogoDataUrl,
|
||||||
|
} from "@/lib/utils/export-branding";
|
||||||
import type { LancamentoItem } from "./types";
|
import type { LancamentoItem } from "./types";
|
||||||
|
|
||||||
interface LancamentosExportProps {
|
interface LancamentosExportProps {
|
||||||
@@ -51,6 +55,17 @@ export function LancamentosExport({
|
|||||||
return "-";
|
return "-";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNameWithInstallment = (lancamento: LancamentoItem) => {
|
||||||
|
const isInstallment =
|
||||||
|
lancamento.condition.trim().toLowerCase() === "parcelado";
|
||||||
|
|
||||||
|
if (!isInstallment || !lancamento.installmentCount) {
|
||||||
|
return lancamento.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lancamento.name} (${lancamento.currentInstallment ?? 1}/${lancamento.installmentCount})`;
|
||||||
|
};
|
||||||
|
|
||||||
const exportToCSV = () => {
|
const exportToCSV = () => {
|
||||||
try {
|
try {
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
@@ -71,7 +86,7 @@ export function LancamentosExport({
|
|||||||
lancamentos.forEach((lancamento) => {
|
lancamentos.forEach((lancamento) => {
|
||||||
const row = [
|
const row = [
|
||||||
formatDate(lancamento.purchaseDate),
|
formatDate(lancamento.purchaseDate),
|
||||||
lancamento.name,
|
getNameWithInstallment(lancamento),
|
||||||
lancamento.transactionType,
|
lancamento.transactionType,
|
||||||
lancamento.condition,
|
lancamento.condition,
|
||||||
lancamento.paymentMethod,
|
lancamento.paymentMethod,
|
||||||
@@ -129,7 +144,7 @@ export function LancamentosExport({
|
|||||||
lancamentos.forEach((lancamento) => {
|
lancamentos.forEach((lancamento) => {
|
||||||
const row = [
|
const row = [
|
||||||
formatDate(lancamento.purchaseDate),
|
formatDate(lancamento.purchaseDate),
|
||||||
lancamento.name,
|
getNameWithInstallment(lancamento),
|
||||||
lancamento.transactionType,
|
lancamento.transactionType,
|
||||||
lancamento.condition,
|
lancamento.condition,
|
||||||
lancamento.paymentMethod,
|
lancamento.paymentMethod,
|
||||||
@@ -145,7 +160,7 @@ export function LancamentosExport({
|
|||||||
|
|
||||||
ws["!cols"] = [
|
ws["!cols"] = [
|
||||||
{ wch: 12 }, // Data
|
{ wch: 12 }, // Data
|
||||||
{ wch: 30 }, // Nome
|
{ wch: 42 }, // Nome
|
||||||
{ wch: 15 }, // Tipo
|
{ wch: 15 }, // Tipo
|
||||||
{ wch: 15 }, // Condição
|
{ wch: 15 }, // Condição
|
||||||
{ wch: 20 }, // Pagamento
|
{ wch: 20 }, // Pagamento
|
||||||
@@ -168,14 +183,33 @@ export function LancamentosExport({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportToPDF = () => {
|
const exportToPDF = async () => {
|
||||||
try {
|
try {
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
|
|
||||||
const doc = new jsPDF({ orientation: "landscape" });
|
const doc = new jsPDF({ orientation: "landscape" });
|
||||||
|
const primaryColor = getPrimaryPdfColor();
|
||||||
|
const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([
|
||||||
|
loadExportLogoDataUrl("/logo_small.png"),
|
||||||
|
loadExportLogoDataUrl("/logo_text.png"),
|
||||||
|
]);
|
||||||
|
let brandingEndX = 14;
|
||||||
|
|
||||||
|
if (smallLogoDataUrl) {
|
||||||
|
doc.addImage(smallLogoDataUrl, "PNG", brandingEndX, 7.5, 8, 8);
|
||||||
|
brandingEndX += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textLogoDataUrl) {
|
||||||
|
doc.addImage(textLogoDataUrl, "PNG", brandingEndX, 8, 30, 8);
|
||||||
|
brandingEndX += 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleX = brandingEndX > 14 ? brandingEndX + 4 : 14;
|
||||||
|
|
||||||
|
doc.setFont("courier", "normal");
|
||||||
doc.setFontSize(16);
|
doc.setFontSize(16);
|
||||||
doc.text("Lançamentos", 14, 15);
|
doc.text("Lançamentos", titleX, 15);
|
||||||
|
|
||||||
doc.setFontSize(10);
|
doc.setFontSize(10);
|
||||||
const periodParts = period.split("-");
|
const periodParts = period.split("-");
|
||||||
@@ -197,8 +231,15 @@ export function LancamentosExport({
|
|||||||
periodParts.length === 2
|
periodParts.length === 2
|
||||||
? `${monthNames[Number.parseInt(periodParts[1], 10) - 1]}/${periodParts[0]}`
|
? `${monthNames[Number.parseInt(periodParts[1], 10) - 1]}/${periodParts[0]}`
|
||||||
: period;
|
: period;
|
||||||
doc.text(`Período: ${formattedPeriod}`, 14, 22);
|
doc.text(`Período: ${formattedPeriod}`, titleX, 22);
|
||||||
doc.text(`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, 14, 27);
|
doc.text(
|
||||||
|
`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`,
|
||||||
|
titleX,
|
||||||
|
27,
|
||||||
|
);
|
||||||
|
doc.setDrawColor(...primaryColor);
|
||||||
|
doc.setLineWidth(0.5);
|
||||||
|
doc.line(14, 31, doc.internal.pageSize.getWidth() - 14, 31);
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
[
|
[
|
||||||
@@ -216,7 +257,7 @@ export function LancamentosExport({
|
|||||||
|
|
||||||
const body = lancamentos.map((lancamento) => [
|
const body = lancamentos.map((lancamento) => [
|
||||||
formatDate(lancamento.purchaseDate),
|
formatDate(lancamento.purchaseDate),
|
||||||
lancamento.name,
|
getNameWithInstallment(lancamento),
|
||||||
lancamento.transactionType,
|
lancamento.transactionType,
|
||||||
lancamento.condition,
|
lancamento.condition,
|
||||||
lancamento.paymentMethod,
|
lancamento.paymentMethod,
|
||||||
@@ -229,26 +270,28 @@ export function LancamentosExport({
|
|||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
head: headers,
|
head: headers,
|
||||||
body: body,
|
body: body,
|
||||||
startY: 32,
|
startY: 35,
|
||||||
|
tableWidth: "auto",
|
||||||
styles: {
|
styles: {
|
||||||
|
font: "courier",
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
cellPadding: 2,
|
cellPadding: 2,
|
||||||
},
|
},
|
||||||
headStyles: {
|
headStyles: {
|
||||||
fillColor: [59, 130, 246],
|
fillColor: primaryColor,
|
||||||
textColor: 255,
|
textColor: 255,
|
||||||
fontStyle: "bold",
|
fontStyle: "bold",
|
||||||
},
|
},
|
||||||
columnStyles: {
|
columnStyles: {
|
||||||
0: { cellWidth: 20 }, // Data
|
0: { cellWidth: 24 }, // Data
|
||||||
1: { cellWidth: 40 }, // Nome
|
1: { cellWidth: 58 }, // Nome
|
||||||
2: { cellWidth: 25 }, // Tipo
|
2: { cellWidth: 22 }, // Tipo
|
||||||
3: { cellWidth: 25 }, // Condição
|
3: { cellWidth: 22 }, // Condição
|
||||||
4: { cellWidth: 30 }, // Pagamento
|
4: { cellWidth: 28 }, // Pagamento
|
||||||
5: { cellWidth: 25 }, // Valor
|
5: { cellWidth: 24 }, // Valor
|
||||||
6: { cellWidth: 30 }, // Categoria
|
6: { cellWidth: 30 }, // Categoria
|
||||||
7: { cellWidth: 30 }, // Conta/Cartão
|
7: { cellWidth: 30 }, // Conta/Cartão
|
||||||
8: { cellWidth: 30 }, // Pagador
|
8: { cellWidth: 31 }, // Pagador
|
||||||
},
|
},
|
||||||
didParseCell: (cellData) => {
|
didParseCell: (cellData) => {
|
||||||
if (cellData.section === "body" && cellData.column.index === 5) {
|
if (cellData.section === "body" && cellData.column.index === 5) {
|
||||||
@@ -262,7 +305,7 @@ export function LancamentosExport({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
margin: { top: 32 },
|
margin: { top: 35 },
|
||||||
});
|
});
|
||||||
|
|
||||||
doc.save(getFileName("pdf"));
|
doc.save(getFileName("pdf"));
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { RiArrowDownFill, RiCheckLine } from "@remixicon/react";
|
import { RiArrowDownFill, RiCheckLine } from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
calculateLastInstallmentDate,
|
calculateLastInstallmentDate,
|
||||||
formatCurrentInstallment,
|
|
||||||
formatLastInstallmentDate,
|
|
||||||
formatPurchaseDate,
|
formatPurchaseDate,
|
||||||
|
formatLastInstallmentDate,
|
||||||
|
formatCurrentInstallment,
|
||||||
} from "@/lib/installments/utils";
|
} from "@/lib/installments/utils";
|
||||||
|
|
||||||
type InstallmentTimelineProps = {
|
type InstallmentTimelineProps = {
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ export function LancamentosFilters({
|
|||||||
|
|
||||||
<div className="flex w-full gap-2 md:w-auto">
|
<div className="flex w-full gap-2 md:w-auto">
|
||||||
{exportButton && (
|
{exportButton && (
|
||||||
<div className="flex-1 md:flex-none [&>*]:w-full [&>*]:md:w-auto">
|
<div className="flex-1 md:flex-none *:w-full *:md:w-auto">
|
||||||
{exportButton}
|
{exportButton}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -291,13 +291,13 @@ export function LancamentosFilters({
|
|||||||
<DrawerTrigger asChild>
|
<DrawerTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex-1 md:flex-none text-sm border-dashed relative"
|
className="flex-1 md:flex-none text-sm border-dashed relative bg-transparent"
|
||||||
aria-label="Abrir filtros"
|
aria-label="Abrir filtros"
|
||||||
>
|
>
|
||||||
<RiFilter3Line className="size-4" />
|
<RiFilter3Line className="size-4" />
|
||||||
Filtros
|
Filtros
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<span className="absolute -top-1 -right-1 size-2 rounded-full bg-primary" />
|
<span className="absolute -top-1 -right-1 size-3 rounded-full bg-primary" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
RiChat1Line,
|
RiChat1Line,
|
||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBin5Line,
|
RiDeleteBin5Line,
|
||||||
RiEyeLine,
|
|
||||||
RiFileCopyLine,
|
RiFileCopyLine,
|
||||||
|
RiFileList2Line,
|
||||||
RiGroupLine,
|
RiGroupLine,
|
||||||
RiHistoryLine,
|
RiHistoryLine,
|
||||||
RiMoreFill,
|
RiMoreFill,
|
||||||
@@ -31,8 +31,8 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { CategoryIcon } from "@/components/categorias/category-icon";
|
import { CategoryIcon } from "@/components/categorias/category-icon";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
|
||||||
import MoneyValues from "@/components/money-values";
|
import MoneyValues from "@/components/money-values";
|
||||||
|
import { EmptyState } from "@/components/shared/empty-state";
|
||||||
import { TypeBadge } from "@/components/type-badge";
|
import { TypeBadge } from "@/components/type-badge";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -588,7 +588,7 @@ const buildColumns = ({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onSelect={() => handleViewDetails(row.original)}
|
onSelect={() => handleViewDetails(row.original)}
|
||||||
>
|
>
|
||||||
<RiEyeLine className="size-4" />
|
<RiFileList2Line className="size-4" />
|
||||||
Detalhes
|
Detalhes
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{row.original.userId === currentUserId && (
|
{row.original.userId === currentUserId && (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface UseLogoSelectionProps {
|
|||||||
* mode: 'create',
|
* mode: 'create',
|
||||||
* currentLogo: formState.logo,
|
* currentLogo: formState.logo,
|
||||||
* currentName: formState.name,
|
* currentName: formState.name,
|
||||||
* onUpdate: (updates) => setFormState(prev => ({ ...prev, ...updates }))
|
* onUpdate: (updates) => updateFields(updates)
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -3,61 +3,37 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useTransition } from "react";
|
import { useEffect, useMemo, useTransition } from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { useMonthPeriod } from "@/hooks/use-month-period";
|
import { getNextPeriod, getPreviousPeriod } from "@/lib/utils/period";
|
||||||
import LoadingSpinner from "./loading-spinner";
|
import LoadingSpinner from "./loading-spinner";
|
||||||
import NavigationButton from "./nav-button";
|
import NavigationButton from "./nav-button";
|
||||||
import ReturnButton from "./return-button";
|
import ReturnButton from "./return-button";
|
||||||
|
import { useMonthPeriod } from "./use-month-period";
|
||||||
|
|
||||||
export default function MonthNavigation() {
|
export default function MonthNavigation() {
|
||||||
const {
|
const { period, currentMonth, currentYear, defaultPeriod, buildHref } =
|
||||||
monthNames,
|
useMonthPeriod();
|
||||||
currentMonth,
|
|
||||||
currentYear,
|
|
||||||
defaultMonth,
|
|
||||||
defaultYear,
|
|
||||||
buildHref,
|
|
||||||
} = useMonthPeriod();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const currentMonthLabel = useMemo(
|
const currentMonthLabel = useMemo(
|
||||||
() => currentMonth.charAt(0).toUpperCase() + currentMonth.slice(1),
|
() =>
|
||||||
[currentMonth],
|
`${currentMonth.charAt(0).toUpperCase()}${currentMonth.slice(1)} ${currentYear}`,
|
||||||
|
[currentMonth, currentYear],
|
||||||
);
|
);
|
||||||
|
const prevTarget = useMemo(
|
||||||
const currentMonthIndex = useMemo(
|
() => buildHref(getPreviousPeriod(period)),
|
||||||
() => monthNames.indexOf(currentMonth),
|
[buildHref, period],
|
||||||
[monthNames, currentMonth],
|
);
|
||||||
|
const nextTarget = useMemo(
|
||||||
|
() => buildHref(getNextPeriod(period)),
|
||||||
|
[buildHref, period],
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevTarget = useMemo(() => {
|
|
||||||
let idx = currentMonthIndex - 1;
|
|
||||||
let year = currentYear;
|
|
||||||
if (idx < 0) {
|
|
||||||
idx = monthNames.length - 1;
|
|
||||||
year = (parseInt(currentYear, 10) - 1).toString();
|
|
||||||
}
|
|
||||||
return buildHref(monthNames[idx], year);
|
|
||||||
}, [currentMonthIndex, currentYear, monthNames, buildHref]);
|
|
||||||
|
|
||||||
const nextTarget = useMemo(() => {
|
|
||||||
let idx = currentMonthIndex + 1;
|
|
||||||
let year = currentYear;
|
|
||||||
if (idx >= monthNames.length) {
|
|
||||||
idx = 0;
|
|
||||||
year = (parseInt(currentYear, 10) + 1).toString();
|
|
||||||
}
|
|
||||||
return buildHref(monthNames[idx], year);
|
|
||||||
}, [currentMonthIndex, currentYear, monthNames, buildHref]);
|
|
||||||
|
|
||||||
const returnTarget = useMemo(
|
const returnTarget = useMemo(
|
||||||
() => buildHref(defaultMonth, defaultYear),
|
() => buildHref(defaultPeriod),
|
||||||
[buildHref, defaultMonth, defaultYear],
|
[buildHref, defaultPeriod],
|
||||||
);
|
);
|
||||||
|
const isDifferentFromCurrent = period !== defaultPeriod;
|
||||||
const isDifferentFromCurrent =
|
|
||||||
currentMonth !== defaultMonth || currentYear !== defaultYear.toString();
|
|
||||||
|
|
||||||
// Prefetch otimizado: apenas meses adjacentes (M-1, M+1) e mês atual
|
// Prefetch otimizado: apenas meses adjacentes (M-1, M+1) e mês atual
|
||||||
// Isso melhora a performance da navegação sem sobrecarregar o cliente
|
// Isso melhora a performance da navegação sem sobrecarregar o cliente
|
||||||
@@ -91,10 +67,9 @@ export default function MonthNavigation() {
|
|||||||
<div
|
<div
|
||||||
className="mx-1 space-x-1 capitalize font-semibold"
|
className="mx-1 space-x-1 capitalize font-semibold"
|
||||||
aria-current={!isDifferentFromCurrent ? "date" : undefined}
|
aria-current={!isDifferentFromCurrent ? "date" : undefined}
|
||||||
aria-label={`Período selecionado: ${currentMonthLabel} de ${currentYear}`}
|
aria-label={`Período selecionado: ${currentMonthLabel}`}
|
||||||
>
|
>
|
||||||
<span>{currentMonthLabel}</span>
|
<span>{currentMonthLabel}</span>
|
||||||
<span>{currentYear}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPending && <LoadingSpinner />}
|
{isPending && <LoadingSpinner />}
|
||||||
|
|||||||
90
components/month-picker/use-month-period.ts
Normal file
90
components/month-picker/use-month-period.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatPeriod,
|
||||||
|
formatPeriodForUrl,
|
||||||
|
formatPeriodParam,
|
||||||
|
MONTH_NAMES,
|
||||||
|
parsePeriodParam,
|
||||||
|
} from "@/lib/utils/period";
|
||||||
|
|
||||||
|
const PERIOD_PARAM = "periodo";
|
||||||
|
|
||||||
|
export function useMonthPeriod() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const periodFromParams = searchParams.get(PERIOD_PARAM);
|
||||||
|
const referenceDate = useMemo(() => new Date(), []);
|
||||||
|
const defaultPeriod = useMemo(
|
||||||
|
() =>
|
||||||
|
formatPeriod(referenceDate.getFullYear(), referenceDate.getMonth() + 1),
|
||||||
|
[referenceDate],
|
||||||
|
);
|
||||||
|
const { period, monthName, year } = useMemo(
|
||||||
|
() => parsePeriodParam(periodFromParams, referenceDate),
|
||||||
|
[periodFromParams, referenceDate],
|
||||||
|
);
|
||||||
|
const defaultMonth = useMemo(
|
||||||
|
() => MONTH_NAMES[referenceDate.getMonth()] ?? "",
|
||||||
|
[referenceDate],
|
||||||
|
);
|
||||||
|
const defaultYear = useMemo(
|
||||||
|
() => referenceDate.getFullYear().toString(),
|
||||||
|
[referenceDate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildHref = useCallback(
|
||||||
|
(targetPeriod: string) => {
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
params.set(PERIOD_PARAM, formatPeriodForUrl(targetPeriod));
|
||||||
|
|
||||||
|
return `${pathname}?${params.toString()}`;
|
||||||
|
},
|
||||||
|
[pathname, searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildHrefFromMonth = useCallback(
|
||||||
|
(month: string, nextYear: string | number) => {
|
||||||
|
const parsedYear = Number.parseInt(String(nextYear).trim(), 10);
|
||||||
|
if (Number.isNaN(parsedYear)) {
|
||||||
|
return buildHref(defaultPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
const param = formatPeriodParam(month, parsedYear);
|
||||||
|
const parsed = parsePeriodParam(param, referenceDate);
|
||||||
|
return buildHref(parsed.period);
|
||||||
|
},
|
||||||
|
[buildHref, defaultPeriod, referenceDate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const replacePeriod = useCallback(
|
||||||
|
(targetPeriod: string) => {
|
||||||
|
if (!targetPeriod) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.replace(buildHref(targetPeriod), { scroll: false });
|
||||||
|
},
|
||||||
|
[buildHref, router],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pathname,
|
||||||
|
period,
|
||||||
|
currentMonth: monthName,
|
||||||
|
currentYear: year.toString(),
|
||||||
|
defaultPeriod,
|
||||||
|
defaultMonth,
|
||||||
|
defaultYear,
|
||||||
|
buildHref,
|
||||||
|
buildHrefFromMonth,
|
||||||
|
replacePeriod,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PERIOD_PARAM as MONTH_PERIOD_PARAM };
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { CalculatorDialogContent } from "@/components/calculadora/calculator-dialog";
|
|
||||||
import { usePrivacyMode } from "@/components/privacy-provider";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { cn } from "@/lib/utils/ui";
|
|
||||||
|
|
||||||
const itemClass =
|
|
||||||
"flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer";
|
|
||||||
|
|
||||||
export function NavToolsDropdown() {
|
|
||||||
const { privacyMode, toggle } = usePrivacyMode();
|
|
||||||
const [calcOpen, setCalcOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={calcOpen} onOpenChange={setCalcOpen}>
|
|
||||||
<ul className="grid w-52 gap-0.5 p-2">
|
|
||||||
<li>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<button type="button" className={cn(itemClass)}>
|
|
||||||
<span className="text-muted-foreground shrink-0">
|
|
||||||
<RiCalculatorLine className="size-4" />
|
|
||||||
</span>
|
|
||||||
<span className="flex-1 text-left">calculadora</span>
|
|
||||||
</button>
|
|
||||||
</DialogTrigger>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button type="button" onClick={toggle} className={cn(itemClass)}>
|
|
||||||
<span className="text-muted-foreground shrink-0">
|
|
||||||
{privacyMode ? (
|
|
||||||
<RiEyeOffLine className="size-4" />
|
|
||||||
) : (
|
|
||||||
<RiEyeLine className="size-4" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="flex-1 text-left">privacidade</span>
|
|
||||||
{privacyMode && (
|
|
||||||
<Badge
|
|
||||||
variant="secondary"
|
|
||||||
className="text-[10px] px-1.5 py-0 h-4"
|
|
||||||
>
|
|
||||||
Ativo
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<CalculatorDialogContent open={calcOpen} />
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type MobileToolsProps = {
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function MobileTools({ onClose }: MobileToolsProps) {
|
|
||||||
const { privacyMode, toggle } = usePrivacyMode();
|
|
||||||
const [calcOpen, setCalcOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={calcOpen} onOpenChange={setCalcOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
||||||
>
|
|
||||||
<span className="text-muted-foreground shrink-0">
|
|
||||||
<RiCalculatorLine className="size-4" />
|
|
||||||
</span>
|
|
||||||
<span className="flex-1 text-left">calculadora</span>
|
|
||||||
</button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
toggle();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors text-muted-foreground hover:text-foreground hover:bg-accent"
|
|
||||||
>
|
|
||||||
<span className="text-muted-foreground shrink-0">
|
|
||||||
{privacyMode ? (
|
|
||||||
<RiEyeOffLine className="size-4" />
|
|
||||||
) : (
|
|
||||||
<RiEyeLine className="size-4" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="flex-1 text-left">privacidade</span>
|
|
||||||
{privacyMode && (
|
|
||||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4">
|
|
||||||
Ativo
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<CalculatorDialogContent open={calcOpen} />
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AnimatedThemeToggler } from "@/components/animated-theme-toggler";
|
import { AnimatedThemeToggler } from "@/components/animated-theme-toggler";
|
||||||
|
import { Logo } from "@/components/logo";
|
||||||
import { NotificationBell } from "@/components/notificacoes/notification-bell";
|
import { NotificationBell } from "@/components/notificacoes/notification-bell";
|
||||||
import { RefreshPageButton } from "@/components/refresh-page-button";
|
import { RefreshPageButton } from "@/components/shared/refresh-page-button";
|
||||||
import type { DashboardNotificationsSnapshot } from "@/lib/dashboard/notifications";
|
import type { DashboardNotificationsSnapshot } from "@/lib/dashboard/notifications";
|
||||||
import { Logo } from "../logo";
|
|
||||||
import { NavMenu } from "./nav-menu";
|
import { NavMenu } from "./nav-menu";
|
||||||
import { NavbarUser } from "./navbar-user";
|
import { NavbarUser } from "./navbar-user";
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
RiFileChartLine,
|
RiFileChartLine,
|
||||||
RiGroupLine,
|
RiGroupLine,
|
||||||
RiPriceTag3Line,
|
RiPriceTag3Line,
|
||||||
|
RiSecurePaymentLine,
|
||||||
RiSparklingLine,
|
RiSparklingLine,
|
||||||
RiStore2Line,
|
RiStore2Line,
|
||||||
RiTodoLine,
|
RiTodoLine,
|
||||||
@@ -111,6 +112,11 @@ export const NAV_SECTIONS: NavSection[] = [
|
|||||||
icon: <RiBankCard2Line className="size-4" />,
|
icon: <RiBankCard2Line className="size-4" />,
|
||||||
preservePeriod: true,
|
preservePeriod: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/relatorios/analise-parcelas",
|
||||||
|
label: "análise de parcelas",
|
||||||
|
icon: <RiSecurePaymentLine className="size-4" />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "/relatorios/estabelecimentos",
|
href: "/relatorios/estabelecimentos",
|
||||||
label: "estabelecimentos",
|
label: "estabelecimentos",
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { RiDashboardLine, RiMenuLine } from "@remixicon/react";
|
import { RiDashboardLine, RiMenuLine } from "@remixicon/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { CalculatorDialogContent } from "@/components/calculadora/calculator-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog } from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuContent,
|
NavigationMenuContent,
|
||||||
@@ -26,7 +28,9 @@ import { MobileTools, NavToolsDropdown } from "./nav-tools";
|
|||||||
|
|
||||||
export function NavMenu() {
|
export function NavMenu() {
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
|
const [calculatorOpen, setCalculatorOpen] = useState(false);
|
||||||
const close = () => setSheetOpen(false);
|
const close = () => setSheetOpen(false);
|
||||||
|
const openCalculator = () => setCalculatorOpen(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -56,7 +60,7 @@ export function NavMenu() {
|
|||||||
Ferramentas
|
Ferramentas
|
||||||
</NavigationMenuTrigger>
|
</NavigationMenuTrigger>
|
||||||
<NavigationMenuContent>
|
<NavigationMenuContent>
|
||||||
<NavToolsDropdown />
|
<NavToolsDropdown onOpenCalculator={openCalculator} />
|
||||||
</NavigationMenuContent>
|
</NavigationMenuContent>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
@@ -114,10 +118,14 @@ export function NavMenu() {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
<MobileSectionLabel label="Ferramentas" />
|
<MobileSectionLabel label="Ferramentas" />
|
||||||
<MobileTools onClose={close} />
|
<MobileTools onClose={close} onOpenCalculator={openCalculator} />
|
||||||
</nav>
|
</nav>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
||||||
|
<Dialog open={calculatorOpen} onOpenChange={setCalculatorOpen}>
|
||||||
|
<CalculatorDialogContent open={calculatorOpen} />
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
106
components/navigation/navbar/nav-tools.tsx
Normal file
106
components/navigation/navbar/nav-tools.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { RiCalculatorLine, RiEyeLine, RiEyeOffLine } from "@remixicon/react";
|
||||||
|
import { usePrivacyMode } from "@/components/privacy-provider";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils/ui";
|
||||||
|
|
||||||
|
const itemClass =
|
||||||
|
"flex w-full items-center gap-2.5 rounded-sm px-2 py-2 text-sm text-foreground hover:bg-accent transition-colors cursor-pointer";
|
||||||
|
|
||||||
|
type NavToolsDropdownProps = {
|
||||||
|
onOpenCalculator: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NavToolsDropdown({ onOpenCalculator }: NavToolsDropdownProps) {
|
||||||
|
const { privacyMode, toggle } = usePrivacyMode();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="grid w-52 gap-0.5 p-2">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(itemClass)}
|
||||||
|
onClick={onOpenCalculator}
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
<RiCalculatorLine className="size-4" />
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">calculadora</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" onClick={toggle} className={cn(itemClass)}>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
{privacyMode ? (
|
||||||
|
<RiEyeOffLine className="size-4" />
|
||||||
|
) : (
|
||||||
|
<RiEyeLine className="size-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">privacidade</span>
|
||||||
|
{privacyMode && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px] px-1.5 py-0 h-4 text-success"
|
||||||
|
>
|
||||||
|
Ativo
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type MobileToolsProps = {
|
||||||
|
onClose: () => void;
|
||||||
|
onOpenCalculator: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MobileTools({ onClose, onOpenCalculator }: MobileToolsProps) {
|
||||||
|
const { privacyMode, toggle } = usePrivacyMode();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
onOpenCalculator();
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
<RiCalculatorLine className="size-4" />
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">calculadora</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
toggle();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||||
|
>
|
||||||
|
<span className="text-muted-foreground shrink-0">
|
||||||
|
{privacyMode ? (
|
||||||
|
<RiEyeOffLine className="size-4" />
|
||||||
|
) : (
|
||||||
|
<RiEyeLine className="size-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-left">privacidade</span>
|
||||||
|
{privacyMode && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px] px-1.5 py-0 h-4 text-success"
|
||||||
|
>
|
||||||
|
Ativo
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { FeedbackDialogBody } from "@/components/feedback/feedback-dialog";
|
import { FeedbackDialogBody } from "@/components/feedback/feedback-dialog";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -24,7 +25,6 @@ import { authClient } from "@/lib/auth/client";
|
|||||||
import { getAvatarSrc } from "@/lib/pagadores/utils";
|
import { getAvatarSrc } from "@/lib/pagadores/utils";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
import { version } from "@/package.json";
|
import { version } from "@/package.json";
|
||||||
import { Badge } from "../ui/badge";
|
|
||||||
|
|
||||||
const itemClass =
|
const itemClass =
|
||||||
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent";
|
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors hover:bg-accent";
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Logo } from "@/components/logo";
|
import { Logo } from "@/components/logo";
|
||||||
import { NavMain } from "@/components/sidebar/nav-main";
|
import { NavMain } from "@/components/navigation/sidebar/nav-main";
|
||||||
import { NavSecondary } from "@/components/sidebar/nav-secondary";
|
import { NavSecondary } from "@/components/navigation/sidebar/nav-secondary";
|
||||||
import { NavUser } from "@/components/sidebar/nav-user";
|
import { NavUser } from "@/components/navigation/sidebar/nav-user";
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
RiFundsLine,
|
RiFundsLine,
|
||||||
RiGroupLine,
|
RiGroupLine,
|
||||||
RiPriceTag3Line,
|
RiPriceTag3Line,
|
||||||
|
RiSecurePaymentLine,
|
||||||
RiSettings2Line,
|
RiSettings2Line,
|
||||||
RiSparklingLine,
|
RiSparklingLine,
|
||||||
RiTodoLine,
|
RiTodoLine,
|
||||||
@@ -165,6 +166,11 @@ export function createSidebarNavData(
|
|||||||
url: "/relatorios/uso-cartoes",
|
url: "/relatorios/uso-cartoes",
|
||||||
icon: RiBankCard2Line,
|
icon: RiBankCard2Line,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Análise de Parcelas",
|
||||||
|
url: "/relatorios/analise-parcelas",
|
||||||
|
icon: RiSecurePaymentLine,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -85,16 +85,16 @@ export function BudgetDialog({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use form state hook for form management
|
// Use form state hook for form management
|
||||||
const { formState, updateField, setFormState } =
|
const { formState, resetForm, updateField } =
|
||||||
useFormState<BudgetFormValues>(initialState);
|
useFormState<BudgetFormValues>(initialState);
|
||||||
|
|
||||||
// Reset form when dialog opens
|
// Reset form when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
}
|
}
|
||||||
}, [dialogOpen, setFormState, initialState]);
|
}, [dialogOpen, initialState, resetForm]);
|
||||||
|
|
||||||
// Clear error when dialog closes
|
// Clear error when dialog closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -153,7 +153,7 @@ export function BudgetDialog({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
duplicatePreviousMonthBudgetsAction,
|
duplicatePreviousMonthBudgetsAction,
|
||||||
} from "@/app/(dashboard)/orcamentos/actions";
|
} from "@/app/(dashboard)/orcamentos/actions";
|
||||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/shared/empty-state";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
import { BudgetCard } from "./budget-card";
|
import { BudgetCard } from "./budget-card";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
RiDeleteBin5Line,
|
RiDeleteBin5Line,
|
||||||
RiEyeLine,
|
RiFileList2Line,
|
||||||
RiMailSendLine,
|
RiMailSendLine,
|
||||||
RiPencilLine,
|
RiPencilLine,
|
||||||
RiVerifiedBadgeFill,
|
RiVerifiedBadgeFill,
|
||||||
@@ -101,7 +101,7 @@ export function PagadorCard({ pagador, onEdit, onRemove }: PagadorCardProps) {
|
|||||||
href={`/pagadores/${pagador.id}`}
|
href={`/pagadores/${pagador.id}`}
|
||||||
className={`text-primary flex items-center gap-1 font-medium transition-opacity hover:opacity-80`}
|
className={`text-primary flex items-center gap-1 font-medium transition-opacity hover:opacity-80`}
|
||||||
>
|
>
|
||||||
<RiEyeLine className="size-4" aria-hidden />
|
<RiFileList2Line className="size-4" aria-hidden />
|
||||||
detalhes
|
detalhes
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function PagadorDialog({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Use form state hook for form management
|
// Use form state hook for form management
|
||||||
const { formState, updateField, setFormState } =
|
const { formState, resetForm, updateField } =
|
||||||
useFormState<PagadorFormValues>(initialState);
|
useFormState<PagadorFormValues>(initialState);
|
||||||
|
|
||||||
const availableAvatars = useMemo(() => {
|
const availableAvatars = useMemo(() => {
|
||||||
@@ -111,10 +111,10 @@ export function PagadorDialog({
|
|||||||
// Reset form when dialog opens
|
// Reset form when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
}
|
}
|
||||||
}, [dialogOpen, initialState, setFormState]);
|
}, [dialogOpen, initialState, resetForm]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(event: React.FormEvent<HTMLFormElement>) => {
|
(event: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -160,7 +160,7 @@ export function PagadorDialog({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
setFormState(initialState);
|
resetForm(initialState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ export function PagadorDialog({
|
|||||||
toast.error(result.error);
|
toast.error(result.error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[formState, initialState, mode, pagador?.id, setDialogOpen, setFormState],
|
[formState, initialState, mode, pagador?.id, resetForm, setDialogOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = mode === "create" ? "Novo pagador" : "Editar pagador";
|
const title = mode === "create" ? "Novo pagador" : "Editar pagador";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { format } from "date-fns";
|
|||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { MonthPicker } from "@/components/ui/monthpicker";
|
import { MonthPicker } from "@/components/ui/month-picker";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
RiArrowGoBackLine,
|
||||||
RiCheckLine,
|
RiCheckLine,
|
||||||
RiDeleteBinLine,
|
RiDeleteBinLine,
|
||||||
RiEyeLine,
|
RiFileList2Line,
|
||||||
RiMoreLine,
|
RiMoreLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
@@ -37,6 +38,7 @@ interface InboxCardProps {
|
|||||||
onDiscard?: (item: InboxItem) => void;
|
onDiscard?: (item: InboxItem) => void;
|
||||||
onViewDetails?: (item: InboxItem) => void;
|
onViewDetails?: (item: InboxItem) => void;
|
||||||
onDelete?: (item: InboxItem) => void;
|
onDelete?: (item: InboxItem) => void;
|
||||||
|
onRestoreToPending?: (item: InboxItem) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLogoPath(logo: string): string {
|
function resolveLogoPath(logo: string): string {
|
||||||
@@ -79,6 +81,7 @@ export function InboxCard({
|
|||||||
onDiscard,
|
onDiscard,
|
||||||
onViewDetails,
|
onViewDetails,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onRestoreToPending,
|
||||||
}: InboxCardProps) {
|
}: InboxCardProps) {
|
||||||
const matchedLogo = useMemo(
|
const matchedLogo = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -161,7 +164,7 @@ export function InboxCard({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => onViewDetails?.(item)}>
|
<DropdownMenuItem onClick={() => onViewDetails?.(item)}>
|
||||||
<RiEyeLine className="mr-2 size-4" />
|
<RiFileList2Line className="mr-2 size-4" />
|
||||||
Ver detalhes
|
Ver detalhes
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => onProcess?.(item)}>
|
<DropdownMenuItem onClick={() => onProcess?.(item)}>
|
||||||
@@ -204,16 +207,30 @@ export function InboxCard({
|
|||||||
{formattedStatusDate}
|
{formattedStatusDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{onDelete && (
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<Button
|
{item.status === "discarded" && onRestoreToPending && (
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon-sm"
|
variant="ghost"
|
||||||
className="ml-auto text-muted-foreground hover:text-destructive"
|
size="icon-sm"
|
||||||
onClick={() => onDelete(item)}
|
className="text-muted-foreground hover:text-foreground"
|
||||||
>
|
onClick={() => onRestoreToPending(item)}
|
||||||
<RiDeleteBinLine className="size-4" />
|
aria-label="Voltar para pendente"
|
||||||
</Button>
|
title="Voltar para pendente"
|
||||||
)}
|
>
|
||||||
|
<RiArrowGoBackLine className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onDelete && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
className="text-muted-foreground hover:text-destructive"
|
||||||
|
onClick={() => onDelete(item)}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
) : (
|
) : (
|
||||||
<CardFooter className="gap-2 pt-3 pb-4">
|
<CardFooter className="gap-2 pt-3 pb-4">
|
||||||
@@ -226,10 +243,12 @@ export function InboxCard({
|
|||||||
Processar
|
Processar
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="icon-sm"
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
onClick={() => onDiscard?.(item)}
|
onClick={() => onDiscard?.(item)}
|
||||||
className="text-muted-foreground hover:text-destructive hover:border-destructive"
|
className="text-muted-foreground hover:text-destructive"
|
||||||
|
aria-label="Descartar notificação"
|
||||||
|
title="Descartar notificação"
|
||||||
>
|
>
|
||||||
<RiDeleteBinLine className="size-4" />
|
<RiDeleteBinLine className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user