mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-03-10 04:51:47 +00:00
feat: implementar sistema de preferências do usuário e refatorar changelog
Adiciona sistema completo de preferências de usuário: - Cria tabela userPreferences no schema com campos disableMagnetlines, periodMonthsBefore e periodMonthsAfter - Implementa página de Ajustes com abas (Preferências, Alterar nome, Senha, E-mail, Deletar conta) - Adiciona componente PreferencesForm para configuração de magnetlines e períodos de exibição - Propaga periodPreferences para todos os componentes de lançamentos e calendário Refatora sistema de changelog: - Remove implementação anterior baseada em JSON estático - Adiciona nova página de changelog dinâmica em app/(dashboard)/changelog - Adiciona componente changelog-list.tsx - Remove arquivos obsoletos (changelog-notification, actions, data, utils, scripts) Adiciona controle de saldo inicial em contas: - Novo campo excludeInitialBalanceFromIncome em contas - Permite excluir saldo inicial do cálculo de receitas - Atualiza queries de lançamentos para respeitar esta configuração Melhorias adicionais: - Adiciona componente ui/accordion.tsx do shadcn/ui - Refatora formatPeriodLabel para displayPeriod centralizado - Propaga estabelecimentos para componentes de lançamentos - Remove variável DB_PROVIDER obsoleta do .env.example e documentação - Adiciona 6 migrações de banco de dados (0003-0008)
This commit is contained in:
@@ -35,6 +35,8 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { useControlledState } from "@/hooks/use-controlled-state";
|
||||
import { useFormState } from "@/hooks/use-form-state";
|
||||
import type { EligibleInstallment } from "@/lib/installments/anticipation-types";
|
||||
import type { PeriodPreferences } from "@/lib/user-preferences/period";
|
||||
import { createMonthOptions } from "@/lib/utils/period";
|
||||
import { RiLoader4Line } from "@remixicon/react";
|
||||
import {
|
||||
useCallback,
|
||||
@@ -53,6 +55,7 @@ interface AnticipateInstallmentsDialogProps {
|
||||
categorias: Array<{ id: string; name: string; icon: string | null }>;
|
||||
pagadores: Array<{ id: string; name: string }>;
|
||||
defaultPeriod: string;
|
||||
periodPreferences: PeriodPreferences;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
@@ -65,57 +68,6 @@ type AnticipationFormValues = {
|
||||
note: string;
|
||||
};
|
||||
|
||||
type SelectOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const monthFormatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const formatPeriodLabel = (period: string) => {
|
||||
const [year, month] = period.split("-").map(Number);
|
||||
if (!year || !month) {
|
||||
return period;
|
||||
}
|
||||
const date = new Date(year, month - 1, 1);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return period;
|
||||
}
|
||||
const label = monthFormatter.format(date);
|
||||
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||
};
|
||||
|
||||
const buildPeriodOptions = (currentValue?: string): SelectOption[] => {
|
||||
const now = new Date();
|
||||
const options: SelectOption[] = [];
|
||||
|
||||
// Adiciona opções de 3 meses no passado até 6 meses no futuro
|
||||
for (let offset = -3; offset <= 6; offset += 1) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() + offset, 1);
|
||||
const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}`;
|
||||
options.push({ value, label: formatPeriodLabel(value) });
|
||||
}
|
||||
|
||||
// Adiciona o valor atual se não estiver na lista
|
||||
if (
|
||||
currentValue &&
|
||||
!options.some((option) => option.value === currentValue)
|
||||
) {
|
||||
options.push({
|
||||
value: currentValue,
|
||||
label: formatPeriodLabel(currentValue),
|
||||
});
|
||||
}
|
||||
|
||||
return options.sort((a, b) => a.value.localeCompare(b.value));
|
||||
};
|
||||
|
||||
export function AnticipateInstallmentsDialog({
|
||||
trigger,
|
||||
seriesId,
|
||||
@@ -123,6 +75,7 @@ export function AnticipateInstallmentsDialog({
|
||||
categorias,
|
||||
pagadores,
|
||||
defaultPeriod,
|
||||
periodPreferences,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AnticipateInstallmentsDialogProps) {
|
||||
@@ -152,8 +105,13 @@ export function AnticipateInstallmentsDialog({
|
||||
});
|
||||
|
||||
const periodOptions = useMemo(
|
||||
() => buildPeriodOptions(formState.anticipationPeriod),
|
||||
[formState.anticipationPeriod]
|
||||
() =>
|
||||
createMonthOptions(
|
||||
formState.anticipationPeriod,
|
||||
periodPreferences.monthsBefore,
|
||||
periodPreferences.monthsAfter
|
||||
),
|
||||
[formState.anticipationPeriod, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
|
||||
);
|
||||
|
||||
// Buscar parcelas elegíveis ao abrir o dialog
|
||||
|
||||
@@ -110,10 +110,14 @@ export function LancamentoDetailsDialog({
|
||||
<span className="capitalize">
|
||||
<Badge
|
||||
variant={getTransactionBadgeVariant(
|
||||
lancamento.transactionType
|
||||
lancamento.categoriaName === "Saldo inicial"
|
||||
? "Saldo inicial"
|
||||
: lancamento.transactionType
|
||||
)}
|
||||
>
|
||||
{lancamento.transactionType}
|
||||
{lancamento.categoriaName === "Saldo inicial"
|
||||
? "Saldo Inicial"
|
||||
: lancamento.transactionType}
|
||||
</Badge>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LancamentoFormState } from "@/lib/lancamentos/form-helpers";
|
||||
import type { PeriodPreferences } from "@/lib/user-preferences/period";
|
||||
import type { LancamentoItem, SelectOption } from "../../types";
|
||||
|
||||
export type FormState = LancamentoFormState;
|
||||
@@ -17,6 +18,7 @@ export interface LancamentoDialogProps {
|
||||
estabelecimentos: string[];
|
||||
lancamento?: LancamentoItem;
|
||||
defaultPeriod?: string;
|
||||
periodPreferences: PeriodPreferences;
|
||||
defaultCartaoId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
defaultPurchaseDate?: string | null;
|
||||
|
||||
@@ -58,6 +58,7 @@ export function LancamentoDialog({
|
||||
estabelecimentos,
|
||||
lancamento,
|
||||
defaultPeriod,
|
||||
periodPreferences,
|
||||
defaultCartaoId,
|
||||
defaultPaymentMethod,
|
||||
defaultPurchaseDate,
|
||||
@@ -125,8 +126,13 @@ export function LancamentoDialog({
|
||||
}, [categoriaOptions, formState.transactionType]);
|
||||
|
||||
const monthOptions = useMemo(
|
||||
() => createMonthOptions(formState.period),
|
||||
[formState.period]
|
||||
() =>
|
||||
createMonthOptions(
|
||||
formState.period,
|
||||
periodPreferences.monthsBefore,
|
||||
periodPreferences.monthsAfter
|
||||
),
|
||||
[formState.period, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
|
||||
);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
|
||||
@@ -31,8 +31,18 @@ export function PaymentMethodSection({
|
||||
"Dinheiro",
|
||||
"Boleto",
|
||||
"Cartão de débito",
|
||||
"Pré-Pago | VR/VA",
|
||||
"Transferência bancária",
|
||||
].includes(formState.paymentMethod);
|
||||
|
||||
// Filtrar contas apenas do tipo "Pré-Pago | VR/VA" quando forma de pagamento for "Pré-Pago | VR/VA"
|
||||
const filteredContaOptions =
|
||||
formState.paymentMethod === "Pré-Pago | VR/VA"
|
||||
? contaOptions.filter(
|
||||
(option) => option.accountType === "Pré-Pago | VR/VA"
|
||||
)
|
||||
: contaOptions;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isUpdateMode ? (
|
||||
@@ -56,7 +66,9 @@ export function PaymentMethodSection({
|
||||
>
|
||||
<SelectValue placeholder="Selecione" className="w-full">
|
||||
{formState.paymentMethod && (
|
||||
<PaymentMethodSelectContent label={formState.paymentMethod} />
|
||||
<PaymentMethodSelectContent
|
||||
label={formState.paymentMethod}
|
||||
/>
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
@@ -138,7 +150,7 @@ export function PaymentMethodSection({
|
||||
<SelectValue placeholder="Selecione">
|
||||
{formState.contaId &&
|
||||
(() => {
|
||||
const selectedOption = contaOptions.find(
|
||||
const selectedOption = filteredContaOptions.find(
|
||||
(opt) => opt.value === formState.contaId
|
||||
);
|
||||
return selectedOption ? (
|
||||
@@ -152,14 +164,14 @@ export function PaymentMethodSection({
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{contaOptions.length === 0 ? (
|
||||
{filteredContaOptions.length === 0 ? (
|
||||
<div className="px-2 py-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nenhuma conta cadastrada
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
contaOptions.map((option) => (
|
||||
filteredContaOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
@@ -246,7 +258,7 @@ export function PaymentMethodSection({
|
||||
<SelectValue placeholder="Selecione">
|
||||
{formState.contaId &&
|
||||
(() => {
|
||||
const selectedOption = contaOptions.find(
|
||||
const selectedOption = filteredContaOptions.find(
|
||||
(opt) => opt.value === formState.contaId
|
||||
);
|
||||
return selectedOption ? (
|
||||
@@ -260,14 +272,14 @@ export function PaymentMethodSection({
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{contaOptions.length === 0 ? (
|
||||
{filteredContaOptions.length === 0 ? (
|
||||
<div className="px-2 py-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nenhuma conta cadastrada
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
contaOptions.map((option) => (
|
||||
filteredContaOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<ContaCartaoSelectContent
|
||||
label={option.label}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers";
|
||||
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
|
||||
import { getTodayDateString } from "@/lib/utils/date";
|
||||
import { createMonthOptions } from "@/lib/utils/period";
|
||||
import type { PeriodPreferences } from "@/lib/user-preferences/period";
|
||||
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -51,6 +52,7 @@ interface MassAddDialogProps {
|
||||
categoriaOptions: SelectOption[];
|
||||
estabelecimentos: string[];
|
||||
selectedPeriod: string;
|
||||
periodPreferences: PeriodPreferences;
|
||||
defaultPagadorId?: string | null;
|
||||
}
|
||||
|
||||
@@ -91,6 +93,7 @@ export function MassAddDialog({
|
||||
categoriaOptions,
|
||||
estabelecimentos,
|
||||
selectedPeriod,
|
||||
periodPreferences,
|
||||
defaultPagadorId,
|
||||
}: MassAddDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -119,8 +122,13 @@ export function MassAddDialog({
|
||||
|
||||
// Period options
|
||||
const periodOptions = useMemo(
|
||||
() => createMonthOptions(selectedPeriod, 3),
|
||||
[selectedPeriod]
|
||||
() =>
|
||||
createMonthOptions(
|
||||
selectedPeriod,
|
||||
periodPreferences.monthsBefore,
|
||||
periodPreferences.monthsAfter
|
||||
),
|
||||
[selectedPeriod, periodPreferences.monthsBefore, periodPreferences.monthsAfter]
|
||||
);
|
||||
|
||||
// Categorias agrupadas e filtradas por tipo de transação
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
LancamentoItem,
|
||||
SelectOption,
|
||||
} from "../types";
|
||||
import type { PeriodPreferences } from "@/lib/user-preferences/period";
|
||||
|
||||
interface LancamentosPageProps {
|
||||
lancamentos: LancamentoItem[];
|
||||
@@ -39,6 +40,7 @@ interface LancamentosPageProps {
|
||||
contaCartaoFilterOptions: ContaCartaoFilterOption[];
|
||||
selectedPeriod: string;
|
||||
estabelecimentos: string[];
|
||||
periodPreferences: PeriodPreferences;
|
||||
allowCreate?: boolean;
|
||||
defaultCartaoId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
@@ -59,6 +61,7 @@ export function LancamentosPage({
|
||||
contaCartaoFilterOptions,
|
||||
selectedPeriod,
|
||||
estabelecimentos,
|
||||
periodPreferences,
|
||||
allowCreate = true,
|
||||
defaultCartaoId,
|
||||
defaultPaymentMethod,
|
||||
@@ -114,7 +117,7 @@ export function LancamentosPage({
|
||||
return;
|
||||
}
|
||||
|
||||
const supportedMethods = ["Pix", "Boleto", "Dinheiro", "Cartão de débito"];
|
||||
const supportedMethods = ["Pix", "Boleto", "Dinheiro", "Cartão de débito", "Pré-Pago | VR/VA", "Transferência bancária"];
|
||||
if (!supportedMethods.includes(item.paymentMethod)) {
|
||||
return;
|
||||
}
|
||||
@@ -354,6 +357,7 @@ export function LancamentosPage({
|
||||
categoriaOptions={categoriaOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
defaultPeriod={selectedPeriod}
|
||||
periodPreferences={periodPreferences}
|
||||
defaultCartaoId={defaultCartaoId}
|
||||
defaultPaymentMethod={defaultPaymentMethod}
|
||||
lockCartaoSelection={lockCartaoSelection}
|
||||
@@ -379,6 +383,7 @@ export function LancamentosPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
lancamento={lancamentoToCopy ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
periodPreferences={periodPreferences}
|
||||
/>
|
||||
|
||||
<LancamentoDialog
|
||||
@@ -394,6 +399,7 @@ export function LancamentosPage({
|
||||
estabelecimentos={estabelecimentos}
|
||||
lancamento={selectedLancamento ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
periodPreferences={periodPreferences}
|
||||
onBulkEditRequest={handleBulkEditRequest}
|
||||
/>
|
||||
|
||||
@@ -473,6 +479,7 @@ export function LancamentosPage({
|
||||
categoriaOptions={categoriaOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
selectedPeriod={selectedPeriod}
|
||||
periodPreferences={periodPreferences}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
/>
|
||||
) : null}
|
||||
@@ -508,6 +515,7 @@ export function LancamentosPage({
|
||||
name: p.label,
|
||||
}))}
|
||||
defaultPeriod={selectedPeriod}
|
||||
periodPreferences={periodPreferences}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import type { InstallmentAnticipationWithRelations } from "@/lib/installments/anticipation-types";
|
||||
import { displayPeriod } from "@/lib/utils/period";
|
||||
import { RiCalendarCheckLine, RiCloseLine, RiEyeLine } from "@remixicon/react";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
@@ -26,24 +27,6 @@ interface AnticipationCardProps {
|
||||
onCanceled?: () => void;
|
||||
}
|
||||
|
||||
const monthFormatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const formatPeriodLabel = (period: string) => {
|
||||
const [year, month] = period.split("-").map(Number);
|
||||
if (!year || !month) {
|
||||
return period;
|
||||
}
|
||||
const date = new Date(year, month - 1, 1);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return period;
|
||||
}
|
||||
const label = monthFormatter.format(date);
|
||||
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||
};
|
||||
|
||||
export function AnticipationCard({
|
||||
anticipation,
|
||||
onViewLancamento,
|
||||
@@ -93,7 +76,7 @@ export function AnticipationCard({
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{formatPeriodLabel(anticipation.anticipationPeriod)}
|
||||
{displayPeriod(anticipation.anticipationPeriod)}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
@@ -288,16 +288,20 @@ const buildColumns = ({
|
||||
{
|
||||
accessorKey: "transactionType",
|
||||
header: "Transação",
|
||||
cell: ({ row }) => (
|
||||
<TypeBadge
|
||||
type={
|
||||
row.original.transactionType as
|
||||
| "Despesa"
|
||||
| "Receita"
|
||||
| "Transferência"
|
||||
}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const type =
|
||||
row.original.categoriaName === "Saldo inicial"
|
||||
? "Saldo inicial"
|
||||
: row.original.transactionType;
|
||||
|
||||
return (
|
||||
<TypeBadge
|
||||
type={
|
||||
type as "Despesa" | "Receita" | "Transferência" | "Saldo inicial"
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "amount",
|
||||
|
||||
@@ -43,6 +43,7 @@ export type SelectOption = {
|
||||
avatarUrl?: string | null;
|
||||
logo?: string | null;
|
||||
icon?: string | null;
|
||||
accountType?: string | null;
|
||||
};
|
||||
|
||||
export type LancamentoFilterOption = {
|
||||
|
||||
Reference in New Issue
Block a user