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:
@@ -1,76 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { userUpdateLog } from "@/db/schema";
|
||||
import { successResult, type ActionResult } from "@/lib/actions/types";
|
||||
import { getUser } from "@/lib/auth/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { handleActionError } from "../actions/helpers";
|
||||
|
||||
export async function markUpdateAsRead(
|
||||
updateId: string
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
|
||||
// Check if already marked as read
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(userUpdateLog)
|
||||
.where(
|
||||
and(
|
||||
eq(userUpdateLog.userId, user.id),
|
||||
eq(userUpdateLog.updateId, updateId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return successResult("Já marcado como lido");
|
||||
}
|
||||
|
||||
await db.insert(userUpdateLog).values({
|
||||
userId: user.id,
|
||||
updateId,
|
||||
});
|
||||
|
||||
return successResult("Marcado como lido");
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function markAllUpdatesAsRead(
|
||||
updateIds: string[]
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
|
||||
// Get existing read updates
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(userUpdateLog)
|
||||
.where(eq(userUpdateLog.userId, user.id));
|
||||
|
||||
const existingIds = new Set(existing.map((log) => log.updateId));
|
||||
|
||||
// Filter out already read updates
|
||||
const newUpdateIds = updateIds.filter((id) => !existingIds.has(id));
|
||||
|
||||
if (newUpdateIds.length === 0) {
|
||||
return successResult("Todos já marcados como lidos");
|
||||
}
|
||||
|
||||
// Insert new read logs
|
||||
await db.insert(userUpdateLog).values(
|
||||
newUpdateIds.map((updateId) => ({
|
||||
userId: user.id,
|
||||
updateId,
|
||||
}))
|
||||
);
|
||||
|
||||
return successResult("Todas as atualizações marcadas como lidas");
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { db } from "@/lib/db";
|
||||
import { userUpdateLog } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export interface ChangelogEntry {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
date: string;
|
||||
icon: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface Changelog {
|
||||
version: string;
|
||||
generatedAt: string;
|
||||
entries: ChangelogEntry[];
|
||||
}
|
||||
|
||||
export function getChangelog(): Changelog {
|
||||
try {
|
||||
const changelogPath = path.join(process.cwd(), "public", "changelog.json");
|
||||
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
return {
|
||||
version: "1.0.0",
|
||||
generatedAt: new Date().toISOString(),
|
||||
entries: [],
|
||||
};
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(changelogPath, "utf-8");
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error("Error reading changelog:", error);
|
||||
return {
|
||||
version: "1.0.0",
|
||||
generatedAt: new Date().toISOString(),
|
||||
entries: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUnreadUpdates(userId: string) {
|
||||
const changelog = getChangelog();
|
||||
|
||||
if (changelog.entries.length === 0) {
|
||||
return {
|
||||
unreadCount: 0,
|
||||
unreadEntries: [],
|
||||
allEntries: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Get read updates from database
|
||||
const readLogs = await db
|
||||
.select()
|
||||
.from(userUpdateLog)
|
||||
.where(eq(userUpdateLog.userId, userId));
|
||||
|
||||
const readUpdateIds = new Set(readLogs.map((log) => log.updateId));
|
||||
|
||||
// Filter unread entries
|
||||
const unreadEntries = changelog.entries.filter(
|
||||
(entry) => !readUpdateIds.has(entry.id)
|
||||
);
|
||||
|
||||
return {
|
||||
unreadCount: unreadEntries.length,
|
||||
unreadEntries,
|
||||
allEntries: changelog.entries,
|
||||
};
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { ChangelogEntry } from "./data";
|
||||
|
||||
/**
|
||||
* Converte uma string de data para um formato compatível com Safari.
|
||||
* Safari não aceita "YYYY-MM-DD HH:mm:ss ±HHMM", requer "YYYY-MM-DDTHH:mm:ss±HHMM"
|
||||
*
|
||||
* @param dateString - String de data no formato "YYYY-MM-DD HH:mm:ss ±HHMM"
|
||||
* @returns Date object válido
|
||||
*/
|
||||
export function parseSafariCompatibleDate(dateString: string): Date {
|
||||
// Substitui o espaço entre data e hora por "T" (formato ISO 8601)
|
||||
// Exemplo: "2025-12-09 17:26:08 +0000" → "2025-12-09T17:26:08+0000"
|
||||
const isoString = dateString.replace(/(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+/, "$1T$2");
|
||||
return new Date(isoString);
|
||||
}
|
||||
|
||||
export function getCategoryLabel(category: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
feature: "Novidades",
|
||||
bugfix: "Correções",
|
||||
performance: "Performance",
|
||||
documentation: "Documentação",
|
||||
style: "Interface",
|
||||
refactor: "Melhorias",
|
||||
test: "Testes",
|
||||
chore: "Manutenção",
|
||||
other: "Outros",
|
||||
};
|
||||
return labels[category] || "Outros";
|
||||
}
|
||||
|
||||
export function groupEntriesByCategory(entries: ChangelogEntry[]) {
|
||||
return entries.reduce(
|
||||
(acc, entry) => {
|
||||
if (!acc[entry.category]) {
|
||||
acc[entry.category] = [];
|
||||
}
|
||||
acc[entry.category].push(entry);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ChangelogEntry[]>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { categorias, lancamentos, pagadores } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
||||
import { categorias, lancamentos, pagadores, contas } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
|
||||
import type { CategoryType } from "@/lib/categorias/constants";
|
||||
import { toNumber } from "@/lib/dashboard/common";
|
||||
import { db } from "@/lib/db";
|
||||
import { mapLancamentosData } from "@/lib/lancamentos/page-helpers";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
import { getPreviousPeriod } from "@/lib/utils/period";
|
||||
import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, isNull, or, sql, ne } from "drizzle-orm";
|
||||
|
||||
type MappedLancamentos = ReturnType<typeof mapLancamentosData>;
|
||||
|
||||
@@ -81,7 +81,17 @@ export async function fetchCategoryDetails(
|
||||
});
|
||||
|
||||
const filteredRows = currentRows.filter(
|
||||
(row) => row.pagador?.role === PAGADOR_ROLE_ADMIN
|
||||
(row) => {
|
||||
// Filtrar apenas pagadores admin
|
||||
if (row.pagador?.role !== PAGADOR_ROLE_ADMIN) return false;
|
||||
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
if (row.note === INITIAL_BALANCE_NOTE && row.conta?.excludeInitialBalanceFromIncome) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const transactions = mapLancamentosData(filteredRows);
|
||||
@@ -97,6 +107,7 @@ export async function fetchCategoryDetails(
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
@@ -104,7 +115,13 @@ export async function fetchCategoryDetails(
|
||||
eq(lancamentos.transactionType, transactionType),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
||||
sanitizedNote,
|
||||
eq(lancamentos.period, previousPeriod)
|
||||
eq(lancamentos.period, previousPeriod),
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
or(
|
||||
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(contas.excludeInitialBalanceFromIncome),
|
||||
eq(contas.excludeInitialBalanceFromIncome, false)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -65,14 +65,15 @@ export async function fetchCategoryHistory(
|
||||
userId: string,
|
||||
currentPeriod: string
|
||||
): Promise<CategoryHistoryData> {
|
||||
// Generate last 6 months including current
|
||||
// Generate last 8 months, current month, and next month (10 total)
|
||||
const periods: string[] = [];
|
||||
const monthLabels: string[] = [];
|
||||
|
||||
const [year, month] = currentPeriod.split("-").map(Number);
|
||||
const currentDate = new Date(year, month - 1, 1);
|
||||
|
||||
for (let i = 8; i >= 0; i--) {
|
||||
// Generate months from -8 to +1 (relative to current)
|
||||
for (let i = 8; i >= -1; i--) {
|
||||
const date = addMonths(currentDate, -i);
|
||||
const period = format(date, "yyyy-MM");
|
||||
const label = format(date, "MMM", { locale: ptBR }).toUpperCase();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { categorias, lancamentos, orcamentos, pagadores } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
||||
import { categorias, lancamentos, orcamentos, pagadores, contas } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
|
||||
import { db } from "@/lib/db";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
import { getPreviousPeriod } from "@/lib/utils/period";
|
||||
import { calculatePercentageChange } from "@/lib/utils/math";
|
||||
import { safeToNumber } from "@/lib/utils/number";
|
||||
import { and, eq, isNull, or, sql } from "drizzle-orm";
|
||||
import { and, eq, isNull, or, sql, ne } from "drizzle-orm";
|
||||
|
||||
export type CategoryIncomeItem = {
|
||||
categoryId: string;
|
||||
@@ -43,6 +43,7 @@ export async function fetchIncomeByCategory(
|
||||
.from(lancamentos)
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.leftJoin(
|
||||
orcamentos,
|
||||
and(
|
||||
@@ -61,6 +62,12 @@ export async function fetchIncomeByCategory(
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`
|
||||
),
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
or(
|
||||
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(contas.excludeInitialBalanceFromIncome),
|
||||
eq(contas.excludeInitialBalanceFromIncome, false)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -75,6 +82,7 @@ export async function fetchIncomeByCategory(
|
||||
.from(lancamentos)
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
@@ -85,6 +93,12 @@ export async function fetchIncomeByCategory(
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`
|
||||
),
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
or(
|
||||
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(contas.excludeInitialBalanceFromIncome),
|
||||
eq(contas.excludeInitialBalanceFromIncome, false)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { lancamentos, pagadores } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
||||
import { lancamentos, pagadores, contas } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
|
||||
import { db } from "@/lib/db";
|
||||
import { toNumber } from "@/lib/dashboard/common";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { and, eq, sql, or, isNull, ne } from "drizzle-orm";
|
||||
|
||||
export type MonthData = {
|
||||
month: string;
|
||||
@@ -79,6 +79,7 @@ export async function fetchIncomeExpenseBalance(
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
@@ -87,7 +88,13 @@ export async function fetchIncomeExpenseBalance(
|
||||
eq(pagadores.role, "admin"),
|
||||
sql`(${lancamentos.note} IS NULL OR ${
|
||||
lancamentos.note
|
||||
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`})`
|
||||
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`})`,
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
or(
|
||||
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(contas.excludeInitialBalanceFromIncome),
|
||||
eq(contas.excludeInitialBalanceFromIncome, false)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { lancamentos, pagadores } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/lib/accounts/constants";
|
||||
import { lancamentos, pagadores, contas } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, INITIAL_BALANCE_NOTE } from "@/lib/accounts/constants";
|
||||
import { db } from "@/lib/db";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
import {
|
||||
@@ -74,6 +74,7 @@ export async function fetchDashboardCardMetrics(
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
@@ -88,6 +89,12 @@ export async function fetchDashboardCardMetrics(
|
||||
`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`
|
||||
)
|
||||
)
|
||||
),
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
or(
|
||||
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(contas.excludeInitialBalanceFromIncome),
|
||||
eq(contas.excludeInitialBalanceFromIncome, false)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -12,4 +12,6 @@ export const LANCAMENTO_PAYMENT_METHODS = [
|
||||
"Pix",
|
||||
"Dinheiro",
|
||||
"Boleto",
|
||||
"Pré-Pago | VR/VA",
|
||||
"Transferência bancária",
|
||||
] as const;
|
||||
|
||||
@@ -110,7 +110,7 @@ export function buildLancamentoInitialState(
|
||||
isSettled:
|
||||
paymentMethod === "Cartão de crédito"
|
||||
? null
|
||||
: lancamento?.isSettled ?? false,
|
||||
: lancamento?.isSettled ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ export function applyFieldDependencies(
|
||||
updates.isSettled = null;
|
||||
} else {
|
||||
updates.cartaoId = undefined;
|
||||
updates.isSettled = currentState.isSettled ?? false;
|
||||
updates.isSettled = currentState.isSettled ?? true;
|
||||
}
|
||||
|
||||
// Clear boleto-specific fields if not boleto
|
||||
|
||||
@@ -80,7 +80,10 @@ export function formatCondition(value?: string | null): string {
|
||||
*/
|
||||
export function getTransactionBadgeVariant(type?: string | null): "default" | "destructive" | "secondary" {
|
||||
if (!type) return "secondary";
|
||||
return type.toLowerCase() === "receita" ? "default" : "destructive";
|
||||
const normalized = type.toLowerCase();
|
||||
return normalized === "receita" || normalized === "saldo inicial"
|
||||
? "default"
|
||||
: "destructive";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,6 +55,7 @@ type CategoriaSluggedOption = BaseSluggedOption & {
|
||||
type ContaSluggedOption = BaseSluggedOption & {
|
||||
kind: "conta";
|
||||
logo: string | null;
|
||||
accountType: string | null;
|
||||
};
|
||||
|
||||
type CartaoSluggedOption = BaseSluggedOption & {
|
||||
@@ -154,7 +155,8 @@ export const toOption = (
|
||||
slug?: string | null,
|
||||
avatarUrl?: string | null,
|
||||
logo?: string | null,
|
||||
icon?: string | null
|
||||
icon?: string | null,
|
||||
accountType?: string | null
|
||||
): SelectOption => ({
|
||||
value,
|
||||
label: normalizeLabel(label),
|
||||
@@ -164,6 +166,7 @@ export const toOption = (
|
||||
avatarUrl: avatarUrl ?? null,
|
||||
logo: logo ?? null,
|
||||
icon: icon ?? null,
|
||||
accountType: accountType ?? null,
|
||||
});
|
||||
|
||||
export const fetchLancamentoFilterSources = async (userId: string) => {
|
||||
@@ -234,6 +237,7 @@ export const buildSluggedFilters = ({
|
||||
slug: contaCartaoSlugger(label),
|
||||
kind: "conta" as const,
|
||||
logo: conta.logo ?? null,
|
||||
accountType: conta.accountType ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -468,8 +472,8 @@ export const buildOptionSets = ({
|
||||
: contaFiltersRaw;
|
||||
|
||||
const contaOptions = sortByLabel(
|
||||
contaOptionsSource.map(({ id, label, slug, logo }) =>
|
||||
toOption(id, label, undefined, undefined, slug, undefined, logo)
|
||||
contaOptionsSource.map(({ id, label, slug, logo, accountType }) =>
|
||||
toOption(id, label, undefined, undefined, slug, undefined, logo, undefined, accountType)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
32
lib/user-preferences/period.ts
Normal file
32
lib/user-preferences/period.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { db, schema } from "@/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export type PeriodPreferences = {
|
||||
monthsBefore: number;
|
||||
monthsAfter: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches period preferences for a user
|
||||
* @param userId - User ID
|
||||
* @returns Period preferences with defaults if not found
|
||||
*/
|
||||
export async function fetchUserPeriodPreferences(
|
||||
userId: string
|
||||
): Promise<PeriodPreferences> {
|
||||
const result = await db
|
||||
.select({
|
||||
periodMonthsBefore: schema.userPreferences.periodMonthsBefore,
|
||||
periodMonthsAfter: schema.userPreferences.periodMonthsAfter,
|
||||
})
|
||||
.from(schema.userPreferences)
|
||||
.where(eq(schema.userPreferences.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
const preferences = result[0];
|
||||
|
||||
return {
|
||||
monthsBefore: preferences?.periodMonthsBefore ?? 3,
|
||||
monthsAfter: preferences?.periodMonthsAfter ?? 3,
|
||||
};
|
||||
}
|
||||
@@ -55,6 +55,8 @@ export const getPaymentMethodIcon = (paymentMethod: string): ReactNode => {
|
||||
<RemixIcons.RiBankCardLine className={ICON_CLASS} aria-hidden />
|
||||
),
|
||||
debito: <RemixIcons.RiBankCardLine className={ICON_CLASS} aria-hidden />,
|
||||
prepagovrva: <RemixIcons.RiCouponLine className={ICON_CLASS} aria-hidden />,
|
||||
transferenciabancaria: <RemixIcons.RiExchangeLine className={ICON_CLASS} aria-hidden />,
|
||||
};
|
||||
|
||||
return registry[key] ?? null;
|
||||
|
||||
@@ -367,17 +367,24 @@ export type SelectOption = {
|
||||
/**
|
||||
* Creates month options for a select dropdown, centered around current month
|
||||
* @param currentValue - Current period value to ensure it's included in options
|
||||
* @param offsetRange - Number of months before/after current month (default: 3)
|
||||
* @param monthsBefore - Number of months before current month (default: 3)
|
||||
* @param monthsAfter - Number of months after current month (default: same as monthsBefore)
|
||||
* @returns Array of select options with formatted labels
|
||||
* @example
|
||||
* createMonthOptions() // -3 to +3
|
||||
* createMonthOptions(undefined, 3) // -3 to +3
|
||||
* createMonthOptions(undefined, 3, 6) // -3 to +6
|
||||
*/
|
||||
export function createMonthOptions(
|
||||
currentValue?: string,
|
||||
offsetRange: number = 3
|
||||
monthsBefore: number = 3,
|
||||
monthsAfter?: number
|
||||
): SelectOption[] {
|
||||
const now = new Date();
|
||||
const options: SelectOption[] = [];
|
||||
const after = monthsAfter ?? monthsBefore; // If not specified, use same as before
|
||||
|
||||
for (let offset = -offsetRange; offset <= offsetRange; offset += 1) {
|
||||
for (let offset = -monthsBefore; offset <= after; offset += 1) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() + offset, 1);
|
||||
const value = formatPeriod(date.getFullYear(), date.getMonth() + 1);
|
||||
options.push({ value, label: displayPeriod(value) });
|
||||
|
||||
Reference in New Issue
Block a user