mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-03-10 04:51:47 +00:00
feat: adição de novos ícones SVG e configuração do ambiente
- Adicionados ícones SVG para ChatGPT, Claude, Gemini e OpenRouter - Implementados ícones para modos claro e escuro do ChatGPT - Criado script de inicialização para PostgreSQL com extensão pgcrypto - Adicionado script de configuração de ambiente que faz backup do .env - Configurado tsconfig.json para TypeScript com opções de compilação
This commit is contained in:
143
lib/installments/anticipation-helpers.ts
Normal file
143
lib/installments/anticipation-helpers.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import type { Lancamento } from "@/db/schema";
|
||||
import type { EligibleInstallment } from "./anticipation-types";
|
||||
|
||||
/**
|
||||
* Calcula o valor total de antecipação baseado nas parcelas selecionadas
|
||||
*/
|
||||
export function calculateTotalAnticipationAmount(
|
||||
installments: EligibleInstallment[]
|
||||
): number {
|
||||
return installments.reduce((sum, inst) => sum + Number(inst.amount), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se o período de antecipação é válido
|
||||
* O período não pode ser anterior ao período da primeira parcela selecionada
|
||||
*/
|
||||
export function validateAnticipationPeriod(
|
||||
period: string,
|
||||
installments: EligibleInstallment[]
|
||||
): boolean {
|
||||
if (installments.length === 0) return false;
|
||||
|
||||
const earliestPeriod = installments.reduce((earliest, inst) => {
|
||||
return inst.period < earliest ? inst.period : earliest;
|
||||
}, installments[0].period);
|
||||
|
||||
return period >= earliestPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata os números das parcelas antecipadas em uma string legível
|
||||
* Exemplo: "1, 2, 3" ou "5, 6, 7, 8"
|
||||
*/
|
||||
export function getAnticipatedInstallmentNumbers(
|
||||
installments: EligibleInstallment[]
|
||||
): string {
|
||||
const numbers = installments
|
||||
.map((inst) => inst.currentInstallment)
|
||||
.filter((num): num is number => num !== null)
|
||||
.sort((a, b) => a - b)
|
||||
.join(", ");
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata o resumo de parcelas antecipadas
|
||||
* Exemplo: "Parcelas 1-3 de 12" ou "Parcela 5 de 12"
|
||||
*/
|
||||
export function formatAnticipatedInstallmentsRange(
|
||||
installments: EligibleInstallment[]
|
||||
): string {
|
||||
const numbers = installments
|
||||
.map((inst) => inst.currentInstallment)
|
||||
.filter((num): num is number => num !== null)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
if (numbers.length === 0) return "";
|
||||
if (numbers.length === 1) {
|
||||
const total = installments[0]?.installmentCount ?? 0;
|
||||
return `Parcela ${numbers[0]} de ${total}`;
|
||||
}
|
||||
|
||||
const first = numbers[0];
|
||||
const last = numbers[numbers.length - 1];
|
||||
const total = installments[0]?.installmentCount ?? 0;
|
||||
|
||||
// Se as parcelas são consecutivas
|
||||
const isConsecutive = numbers.every((num, i) => {
|
||||
if (i === 0) return true;
|
||||
return num === numbers[i - 1]! + 1;
|
||||
});
|
||||
|
||||
if (isConsecutive) {
|
||||
return `Parcelas ${first}-${last} de ${total}`;
|
||||
} else {
|
||||
return `${numbers.length} parcelas de ${total}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se uma antecipação pode ser cancelada
|
||||
* Só pode cancelar se o lançamento de antecipação não foi pago
|
||||
*/
|
||||
export function canCancelAnticipation(lancamento: Lancamento): boolean {
|
||||
return lancamento.isSettled !== true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordena parcelas por número da parcela atual
|
||||
*/
|
||||
export function sortInstallmentsByNumber(
|
||||
installments: EligibleInstallment[]
|
||||
): EligibleInstallment[] {
|
||||
return [...installments].sort((a, b) => {
|
||||
const aNum = a.currentInstallment ?? 0;
|
||||
const bNum = b.currentInstallment ?? 0;
|
||||
return aNum - bNum;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula quantas parcelas restam após uma antecipação
|
||||
*/
|
||||
export function calculateRemainingInstallments(
|
||||
totalInstallments: number,
|
||||
anticipatedCount: number
|
||||
): number {
|
||||
return Math.max(0, totalInstallments - anticipatedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida se as parcelas selecionadas pertencem à mesma série
|
||||
*/
|
||||
export function validateInstallmentsSameSeries(
|
||||
installments: EligibleInstallment[],
|
||||
seriesId: string
|
||||
): boolean {
|
||||
// Esta validação será feita no servidor com os dados completos
|
||||
// Aqui apenas retorna true como placeholder
|
||||
return installments.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera descrição automática para o lançamento de antecipação
|
||||
*/
|
||||
export function generateAnticipationDescription(
|
||||
lancamentoName: string,
|
||||
installmentCount: number
|
||||
): string {
|
||||
return `Antecipação de ${installmentCount} ${
|
||||
installmentCount === 1 ? "parcela" : "parcelas"
|
||||
} - ${lancamentoName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata nota automática para antecipação
|
||||
*/
|
||||
export function generateAnticipationNote(
|
||||
installments: EligibleInstallment[]
|
||||
): string {
|
||||
const range = formatAnticipatedInstallmentsRange(installments);
|
||||
return `Antecipação: ${range}`;
|
||||
}
|
||||
66
lib/installments/anticipation-types.ts
Normal file
66
lib/installments/anticipation-types.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
Categoria,
|
||||
InstallmentAnticipation,
|
||||
Lancamento,
|
||||
Pagador,
|
||||
} from "@/db/schema";
|
||||
|
||||
/**
|
||||
* Parcela elegível para antecipação
|
||||
*/
|
||||
export type EligibleInstallment = {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: string;
|
||||
period: string;
|
||||
purchaseDate: Date;
|
||||
dueDate: Date | null;
|
||||
currentInstallment: number | null;
|
||||
installmentCount: number | null;
|
||||
paymentMethod: string;
|
||||
categoriaId: string | null;
|
||||
pagadorId: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Antecipação com dados completos
|
||||
*/
|
||||
export type InstallmentAnticipationWithRelations = InstallmentAnticipation & {
|
||||
lancamento: Lancamento;
|
||||
pagador: Pagador | null;
|
||||
categoria: Categoria | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Input para criar antecipação
|
||||
*/
|
||||
export type CreateAnticipationInput = {
|
||||
seriesId: string;
|
||||
installmentIds: string[];
|
||||
anticipationPeriod: string;
|
||||
pagadorId?: string;
|
||||
categoriaId?: string;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Input para cancelar antecipação
|
||||
*/
|
||||
export type CancelAnticipationInput = {
|
||||
anticipationId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resumo de antecipação para exibição
|
||||
*/
|
||||
export type AnticipationSummary = {
|
||||
id: string;
|
||||
totalAmount: string;
|
||||
installmentCount: number;
|
||||
anticipationPeriod: string;
|
||||
anticipationDate: Date;
|
||||
note: string | null;
|
||||
isSettled: boolean;
|
||||
lancamentoId: string;
|
||||
anticipatedInstallments: string[];
|
||||
};
|
||||
79
lib/installments/utils.ts
Normal file
79
lib/installments/utils.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Calcula a data da última parcela baseado no período da parcela atual
|
||||
* @param currentPeriod - Período da parcela atual no formato YYYY-MM (ex: "2025-11")
|
||||
* @param currentInstallment - Número da parcela atual
|
||||
* @param totalInstallments - Quantidade total de parcelas
|
||||
* @returns Data da última parcela
|
||||
*/
|
||||
export function calculateLastInstallmentDate(
|
||||
currentPeriod: string,
|
||||
currentInstallment: number,
|
||||
totalInstallments: number
|
||||
): Date {
|
||||
// Parse do período atual (formato: "YYYY-MM")
|
||||
const [yearStr, monthStr] = currentPeriod.split("-");
|
||||
const year = Number.parseInt(yearStr ?? "", 10);
|
||||
const monthIndex = Number.parseInt(monthStr ?? "", 10) - 1; // 0-indexed
|
||||
|
||||
if (Number.isNaN(year) || Number.isNaN(monthIndex)) {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
// Cria data do período atual (parcela atual)
|
||||
const currentDate = new Date(year, monthIndex, 1);
|
||||
|
||||
// Calcula quantas parcelas faltam (incluindo a atual)
|
||||
// Ex: parcela 2 de 6 -> restam 5 parcelas (2, 3, 4, 5, 6)
|
||||
const remainingInstallments = totalInstallments - currentInstallment + 1;
|
||||
|
||||
// Calcula quantos meses adicionar para chegar na última parcela
|
||||
// Ex: restam 5 parcelas -> adicionar 4 meses (parcela atual + 4 = 5 parcelas)
|
||||
const monthsToAdd = remainingInstallments - 1;
|
||||
|
||||
// Simplificando: monthsToAdd = totalInstallments - currentInstallment
|
||||
currentDate.setMonth(currentDate.getMonth() + monthsToAdd);
|
||||
|
||||
return currentDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata a data da última parcela no formato "Mês de Ano"
|
||||
* Exemplo: "Março de 2026"
|
||||
*/
|
||||
export function formatLastInstallmentDate(date: Date): string {
|
||||
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
|
||||
const formatted = formatter.format(date);
|
||||
// Capitaliza a primeira letra
|
||||
return formatted.charAt(0).toUpperCase() + formatted.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata a data de compra no formato "dia, dd mmm"
|
||||
* Exemplo: "qua, 24 set"
|
||||
*/
|
||||
export function formatPurchaseDate(date: Date): string {
|
||||
const formatter = new Intl.DateTimeFormat("pt-BR", {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formata o texto da parcela atual
|
||||
* Exemplo: "1 de 6"
|
||||
*/
|
||||
export function formatCurrentInstallment(
|
||||
current: number,
|
||||
total: number
|
||||
): string {
|
||||
return `${current} de ${total}`;
|
||||
}
|
||||
Reference in New Issue
Block a user