mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
feat(lancamentos): filtros de status e anexo; feedback visual de fatura paga
- Novos filtros no drawer: somente pagos, somente não pagos, com anexo - Filtros de tipo/condição/pagamento agora usam slugs na URL (sem acentos) - Coluna de liquidação: lançamentos de cartão com fatura paga exibem ícone verde com tooltip — diferenciando do estado pendente - EstabelecimentoInput: popover respeita largura do input ao abrir - slugify extraído para shared/utils/string.ts - INVOICE_PAYMENT_CATEGORY_NAME adicionado em categories/constants.ts - SETTLED_FILTER_VALUES adicionado em transactions/constants.ts - establishment-logo.tsx removido (não utilizado) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,8 @@ const EMPTY_FILTERS: TransactionSearchFilters = {
|
|||||||
categoryFilter: null,
|
categoryFilter: null,
|
||||||
accountCardFilter: null,
|
accountCardFilter: null,
|
||||||
searchFilter: null,
|
searchFilter: null,
|
||||||
|
settledFilter: null,
|
||||||
|
attachmentFilter: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const createEmptySlugMaps = (): SlugMaps => ({
|
const createEmptySlugMaps = (): SlugMaps => ({
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const exportTransactionsSchema: z.ZodType<TransactionsExportContext> = z.object(
|
|||||||
categoryFilter: z.string().nullable(),
|
categoryFilter: z.string().nullable(),
|
||||||
accountCardFilter: z.string().nullable(),
|
accountCardFilter: z.string().nullable(),
|
||||||
searchFilter: z.string().nullable(),
|
searchFilter: z.string().nullable(),
|
||||||
|
settledFilter: z.string().nullable(),
|
||||||
|
attachmentFilter: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
accountId: z.string().min(1).nullable().optional(),
|
accountId: z.string().min(1).nullable().optional(),
|
||||||
cardId: z.string().min(1).nullable().optional(),
|
cardId: z.string().min(1).nullable().optional(),
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ export function EstabelecimentoInput({
|
|||||||
}: EstabelecimentoInputProps) {
|
}: EstabelecimentoInputProps) {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [searchValue, setSearchValue] = React.useState("");
|
const [searchValue, setSearchValue] = React.useState("");
|
||||||
|
const [width, setWidth] = React.useState<number | undefined>();
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!open || !containerRef.current) return;
|
||||||
|
setWidth(containerRef.current.offsetWidth);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const handleSelect = (selectedValue: string) => {
|
const handleSelect = (selectedValue: string) => {
|
||||||
onChange(selectedValue);
|
onChange(selectedValue);
|
||||||
@@ -50,7 +57,6 @@ export function EstabelecimentoInput({
|
|||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
setSearchValue(newValue);
|
setSearchValue(newValue);
|
||||||
|
|
||||||
// Open popover when user types and there are suggestions
|
|
||||||
if (newValue.length > 0 && estabelecimentos.length > 0) {
|
if (newValue.length > 0 && estabelecimentos.length > 0) {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
@@ -68,7 +74,7 @@ export function EstabelecimentoInput({
|
|||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen} modal>
|
<Popover open={open} onOpenChange={setOpen} modal>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div className="relative">
|
<div ref={containerRef} className="relative w-full">
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
value={value}
|
value={value}
|
||||||
@@ -86,7 +92,8 @@ export function EstabelecimentoInput({
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
{estabelecimentos.length > 0 && (
|
{estabelecimentos.length > 0 && (
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="p-0 w-[--radix-popover-trigger-width]"
|
className="p-0"
|
||||||
|
style={width ? { width } : undefined}
|
||||||
align="start"
|
align="start"
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
// Re-export from shared — componente movido para src/shared/components/entity-avatar/
|
|
||||||
export { EstablishmentLogo as EstabelecimentoLogo } from "@/shared/components/entity-avatar";
|
|
||||||
@@ -210,7 +210,7 @@ function buildColumns({
|
|||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="line-clamp-2 max-w-[180px] font-medium truncate">
|
<span className="line-clamp-2 max-w-[180px] font-semibold truncate">
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -570,12 +570,44 @@ function buildColumns({
|
|||||||
paymentMethod === "Transferência bancária" ||
|
paymentMethod === "Transferência bancária" ||
|
||||||
paymentMethod === "Pré-Pago | VR/VA";
|
paymentMethod === "Pré-Pago | VR/VA";
|
||||||
|
|
||||||
if (!canToggleSettlement)
|
if (!canToggleSettlement) {
|
||||||
|
const invoicePaid = Boolean(row.original.isSettled);
|
||||||
return (
|
return (
|
||||||
<span className="flex size-7 shrink-0 items-center justify-center">
|
<Tooltip>
|
||||||
<RiBankCard2Line className="size-4 text-muted-foreground/30" />
|
<TooltipTrigger asChild>
|
||||||
</span>
|
<span className="inline-flex">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
disabled
|
||||||
|
className={cn(
|
||||||
|
"transition-colors",
|
||||||
|
invoicePaid
|
||||||
|
? "bg-success/10 text-success"
|
||||||
|
: "text-muted-foreground/30",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{invoicePaid ? (
|
||||||
|
<RiCheckboxCircleFill className="size-4" />
|
||||||
|
) : (
|
||||||
|
<RiBankCard2Line className="size-4" />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">
|
||||||
|
{invoicePaid
|
||||||
|
? "Fatura paga"
|
||||||
|
: "Lançamento de cartão de crédito"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-48 text-center">
|
||||||
|
{invoicePaid
|
||||||
|
? "Fatura paga"
|
||||||
|
: "Lançamentos de cartão de crédito são liquidados ao pagar a fatura"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const readOnly = row.original.readonly;
|
const readOnly = row.original.readonly;
|
||||||
const loading = isSettlementLoading(row.original.id);
|
const loading = isSettlementLoading(row.original.id);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
PAYMENT_METHODS,
|
PAYMENT_METHODS,
|
||||||
|
SETTLED_FILTER_VALUES,
|
||||||
TRANSACTION_CONDITIONS,
|
TRANSACTION_CONDITIONS,
|
||||||
TRANSACTION_TYPES,
|
TRANSACTION_TYPES,
|
||||||
} from "@/features/transactions/constants";
|
} from "@/features/transactions/constants";
|
||||||
@@ -50,6 +51,8 @@ import {
|
|||||||
SelectLabel,
|
SelectLabel,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
} from "@/shared/components/ui/select";
|
} from "@/shared/components/ui/select";
|
||||||
|
import { Switch } from "@/shared/components/ui/switch";
|
||||||
|
import { slugify } from "@/shared/utils/string";
|
||||||
import { cn } from "@/shared/utils/ui";
|
import { cn } from "@/shared/utils/ui";
|
||||||
import {
|
import {
|
||||||
AccountCardSelectContent,
|
AccountCardSelectContent,
|
||||||
@@ -66,9 +69,6 @@ import type {
|
|||||||
|
|
||||||
const FILTER_EMPTY_VALUE = "__all";
|
const FILTER_EMPTY_VALUE = "__all";
|
||||||
|
|
||||||
const buildStaticOptions = (values: readonly string[]) =>
|
|
||||||
values.map((value) => ({ value, label: value }));
|
|
||||||
|
|
||||||
interface FilterSelectProps {
|
interface FilterSelectProps {
|
||||||
param: string;
|
param: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
@@ -263,7 +263,9 @@ export function TransactionsFilters({
|
|||||||
searchParams.get("payment") ||
|
searchParams.get("payment") ||
|
||||||
searchParams.get("payer") ||
|
searchParams.get("payer") ||
|
||||||
searchParams.get("category") ||
|
searchParams.get("category") ||
|
||||||
searchParams.get("accountCard");
|
searchParams.get("accountCard") ||
|
||||||
|
searchParams.get("settled") ||
|
||||||
|
searchParams.get("hasAttachment");
|
||||||
|
|
||||||
const handleResetFilters = () => {
|
const handleResetFilters = () => {
|
||||||
handleReset();
|
handleReset();
|
||||||
@@ -327,7 +329,10 @@ export function TransactionsFilters({
|
|||||||
<FilterSelect
|
<FilterSelect
|
||||||
param="type"
|
param="type"
|
||||||
placeholder="Todos"
|
placeholder="Todos"
|
||||||
options={buildStaticOptions(TRANSACTION_TYPES)}
|
options={TRANSACTION_TYPES.map((v) => ({
|
||||||
|
value: slugify(v),
|
||||||
|
label: v,
|
||||||
|
}))}
|
||||||
widthClass="w-full border-dashed"
|
widthClass="w-full border-dashed"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
getParamValue={getParamValue}
|
getParamValue={getParamValue}
|
||||||
@@ -345,7 +350,10 @@ export function TransactionsFilters({
|
|||||||
<FilterSelect
|
<FilterSelect
|
||||||
param="condition"
|
param="condition"
|
||||||
placeholder="Todas"
|
placeholder="Todas"
|
||||||
options={buildStaticOptions(TRANSACTION_CONDITIONS)}
|
options={TRANSACTION_CONDITIONS.map((v) => ({
|
||||||
|
value: slugify(v),
|
||||||
|
label: v,
|
||||||
|
}))}
|
||||||
widthClass="w-full border-dashed"
|
widthClass="w-full border-dashed"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
getParamValue={getParamValue}
|
getParamValue={getParamValue}
|
||||||
@@ -363,7 +371,10 @@ export function TransactionsFilters({
|
|||||||
<FilterSelect
|
<FilterSelect
|
||||||
param="payment"
|
param="payment"
|
||||||
placeholder="Todos"
|
placeholder="Todos"
|
||||||
options={buildStaticOptions(PAYMENT_METHODS)}
|
options={PAYMENT_METHODS.map((v) => ({
|
||||||
|
value: slugify(v),
|
||||||
|
label: v,
|
||||||
|
}))}
|
||||||
widthClass="w-full border-dashed"
|
widthClass="w-full border-dashed"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
getParamValue={getParamValue}
|
getParamValue={getParamValue}
|
||||||
@@ -547,6 +558,76 @@ export function TransactionsFilters({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm font-medium">Status</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
htmlFor="filter-pago"
|
||||||
|
className="text-sm text-muted-foreground cursor-pointer"
|
||||||
|
>
|
||||||
|
Somente pagos
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
id="filter-pago"
|
||||||
|
checked={
|
||||||
|
searchParams.get("settled") ===
|
||||||
|
SETTLED_FILTER_VALUES.PAID
|
||||||
|
}
|
||||||
|
disabled={isPending}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleFilterChange(
|
||||||
|
"settled",
|
||||||
|
checked ? SETTLED_FILTER_VALUES.PAID : null,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
htmlFor="filter-nao-pago"
|
||||||
|
className="text-sm text-muted-foreground cursor-pointer"
|
||||||
|
>
|
||||||
|
Somente não pagos
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
id="filter-nao-pago"
|
||||||
|
checked={
|
||||||
|
searchParams.get("settled") ===
|
||||||
|
SETTLED_FILTER_VALUES.UNPAID
|
||||||
|
}
|
||||||
|
disabled={isPending}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleFilterChange(
|
||||||
|
"settled",
|
||||||
|
checked ? SETTLED_FILTER_VALUES.UNPAID : null,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
htmlFor="filter-has-attachment"
|
||||||
|
className="text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
Com anexo
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
id="filter-has-attachment"
|
||||||
|
checked={searchParams.get("hasAttachment") === "true"}
|
||||||
|
disabled={isPending}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleFilterChange(
|
||||||
|
"hasAttachment",
|
||||||
|
checked ? "true" : null,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DrawerFooter>
|
<DrawerFooter>
|
||||||
|
|||||||
@@ -19,3 +19,8 @@ export const PAYMENT_METHODS = [
|
|||||||
"Pré-Pago | VR/VA",
|
"Pré-Pago | VR/VA",
|
||||||
"Transferência bancária",
|
"Transferência bancária",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export const SETTLED_FILTER_VALUES = {
|
||||||
|
PAID: "pago",
|
||||||
|
UNPAID: "nao-pago",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export type TransactionExportFilters = {
|
|||||||
categoryFilter: string | null;
|
categoryFilter: string | null;
|
||||||
accountCardFilter: string | null;
|
accountCardFilter: string | null;
|
||||||
searchFilter: string | null;
|
searchFilter: string | null;
|
||||||
|
settledFilter: string | null;
|
||||||
|
attachmentFilter: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TransactionsExportContext = {
|
export type TransactionsExportContext = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export function useTransactionAttachments(transactionId: string) {
|
|||||||
`/api/transactions/${transactionId}/attachments`,
|
`/api/transactions/${transactionId}/attachments`,
|
||||||
),
|
),
|
||||||
enabled: Boolean(transactionId),
|
enabled: Boolean(transactionId),
|
||||||
staleTime: 30_000,
|
staleTime: 50 * 60 * 1000, // 50 min — presigned URLs duram 1h
|
||||||
|
gcTime: 60 * 60 * 1000, // 1h — mantém cache enquanto URL é válida
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import type { SQL } from "drizzle-orm";
|
import type { SQL } from "drizzle-orm";
|
||||||
import { and, eq, ilike, isNotNull, or } from "drizzle-orm";
|
import { and, eq, ilike, isNotNull, or, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
cards,
|
cards,
|
||||||
type categories,
|
type categories,
|
||||||
financialAccounts,
|
financialAccounts,
|
||||||
type payers,
|
type payers,
|
||||||
|
transactionAttachments,
|
||||||
transactions,
|
transactions,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import type { SelectOption } from "@/features/transactions/components/types";
|
import type { SelectOption } from "@/features/transactions/components/types";
|
||||||
import {
|
import {
|
||||||
PAYMENT_METHODS,
|
PAYMENT_METHODS,
|
||||||
|
SETTLED_FILTER_VALUES,
|
||||||
TRANSACTION_CONDITIONS,
|
TRANSACTION_CONDITIONS,
|
||||||
TRANSACTION_TYPES,
|
TRANSACTION_TYPES,
|
||||||
} from "@/features/transactions/constants";
|
} from "@/features/transactions/constants";
|
||||||
@@ -19,6 +21,7 @@ import {
|
|||||||
PAYER_ROLE_THIRD_PARTY,
|
PAYER_ROLE_THIRD_PARTY,
|
||||||
} from "@/shared/lib/payers/constants";
|
} from "@/shared/lib/payers/constants";
|
||||||
import { toDateOnlyString } from "@/shared/utils/date";
|
import { toDateOnlyString } from "@/shared/utils/date";
|
||||||
|
import { slugify } from "@/shared/utils/string";
|
||||||
|
|
||||||
type PayerRow = typeof payers.$inferSelect;
|
type PayerRow = typeof payers.$inferSelect;
|
||||||
type AccountRow = typeof financialAccounts.$inferSelect;
|
type AccountRow = typeof financialAccounts.$inferSelect;
|
||||||
@@ -40,6 +43,8 @@ export type TransactionSearchFilters = {
|
|||||||
categoryFilter: string | null;
|
categoryFilter: string | null;
|
||||||
accountCardFilter: string | null;
|
accountCardFilter: string | null;
|
||||||
searchFilter: string | null;
|
searchFilter: string | null;
|
||||||
|
settledFilter: string | null;
|
||||||
|
attachmentFilter: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BaseSluggedOption = {
|
type BaseSluggedOption = {
|
||||||
@@ -127,6 +132,8 @@ export const extractTransactionSearchFilters = (
|
|||||||
categoryFilter: getSingleParam(params, "category"),
|
categoryFilter: getSingleParam(params, "category"),
|
||||||
accountCardFilter: getSingleParam(params, "accountCard"),
|
accountCardFilter: getSingleParam(params, "accountCard"),
|
||||||
searchFilter: getSingleParam(params, "q"),
|
searchFilter: getSingleParam(params, "q"),
|
||||||
|
settledFilter: getSingleParam(params, "settled"),
|
||||||
|
attachmentFilter: getSingleParam(params, "hasAttachment"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resolveTransactionPagination = (
|
export const resolveTransactionPagination = (
|
||||||
@@ -152,15 +159,17 @@ export const resolveTransactionPagination = (
|
|||||||
const normalizeLabel = (value: string | null | undefined) =>
|
const normalizeLabel = (value: string | null | undefined) =>
|
||||||
value?.trim().length ? value.trim() : "Sem descrição";
|
value?.trim().length ? value.trim() : "Sem descrição";
|
||||||
|
|
||||||
const slugify = (value: string) => {
|
const typeSlugToValue = Object.fromEntries(
|
||||||
const base = value
|
TRANSACTION_TYPES.map((t) => [slugify(t), t]),
|
||||||
.normalize("NFD")
|
) as Record<string, (typeof TRANSACTION_TYPES)[number]>;
|
||||||
.replace(/[\u0300-\u036f]/g, "")
|
|
||||||
.toLowerCase()
|
const conditionSlugToValue = Object.fromEntries(
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
TRANSACTION_CONDITIONS.map((c) => [slugify(c), c]),
|
||||||
.replace(/^-+|-+$/g, "");
|
) as Record<string, (typeof TRANSACTION_CONDITIONS)[number]>;
|
||||||
return base || "item";
|
|
||||||
};
|
const paymentSlugToValue = Object.fromEntries(
|
||||||
|
PAYMENT_METHODS.map((m) => [slugify(m), m]),
|
||||||
|
) as Record<string, (typeof PAYMENT_METHODS)[number]>;
|
||||||
|
|
||||||
const createSlugGenerator = () => {
|
const createSlugGenerator = () => {
|
||||||
const seen = new Map<string, number>();
|
const seen = new Map<string, number>();
|
||||||
@@ -338,16 +347,20 @@ export const buildTransactionWhere = ({
|
|||||||
where.push(eq(transactions.accountId, accountId));
|
where.push(eq(transactions.accountId, accountId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidTransaction(filters.transactionFilter)) {
|
const typeValue = typeSlugToValue[filters.transactionFilter ?? ""] ?? null;
|
||||||
where.push(eq(transactions.transactionType, filters.transactionFilter));
|
if (isValidTransaction(typeValue)) {
|
||||||
|
where.push(eq(transactions.transactionType, typeValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidCondition(filters.conditionFilter)) {
|
const conditionValue =
|
||||||
where.push(eq(transactions.condition, filters.conditionFilter));
|
conditionSlugToValue[filters.conditionFilter ?? ""] ?? null;
|
||||||
|
if (isValidCondition(conditionValue)) {
|
||||||
|
where.push(eq(transactions.condition, conditionValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidPaymentMethod(filters.paymentFilter)) {
|
const paymentValue = paymentSlugToValue[filters.paymentFilter ?? ""] ?? null;
|
||||||
where.push(eq(transactions.paymentMethod, filters.paymentFilter));
|
if (isValidPaymentMethod(paymentValue)) {
|
||||||
|
where.push(eq(transactions.paymentMethod, paymentValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payerId && filters.payerFilter) {
|
if (!payerId && filters.payerFilter) {
|
||||||
@@ -377,6 +390,18 @@ export const buildTransactionWhere = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters.settledFilter === SETTLED_FILTER_VALUES.PAID) {
|
||||||
|
where.push(eq(transactions.isSettled, true));
|
||||||
|
} else if (filters.settledFilter === SETTLED_FILTER_VALUES.UNPAID) {
|
||||||
|
where.push(eq(transactions.isSettled, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.attachmentFilter === "true") {
|
||||||
|
where.push(
|
||||||
|
sql`EXISTS (SELECT 1 FROM ${transactionAttachments} WHERE ${transactionAttachments.transactionId} = ${transactions.id})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const searchPattern = buildSearchPattern(filters.searchFilter);
|
const searchPattern = buildSearchPattern(filters.searchFilter);
|
||||||
if (searchPattern) {
|
if (searchPattern) {
|
||||||
where.push(
|
where.push(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export const CATEGORY_TYPES = ["receita", "despesa"] as const;
|
export const CATEGORY_TYPES = ["receita", "despesa"] as const;
|
||||||
|
|
||||||
|
export const INVOICE_PAYMENT_CATEGORY_NAME = "Pagamentos";
|
||||||
|
|
||||||
export type CategoryType = (typeof CATEGORY_TYPES)[number];
|
export type CategoryType = (typeof CATEGORY_TYPES)[number];
|
||||||
|
|
||||||
export const CATEGORY_TYPE_LABEL: Record<CategoryType, string> = {
|
export const CATEGORY_TYPE_LABEL: Record<CategoryType, string> = {
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
* Utility functions for string normalization and manipulation
|
* Utility functions for string normalization and manipulation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export function slugify(value: string): string {
|
||||||
|
const base = value
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
return base || "item";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capitalizes the first letter of a string
|
* Capitalizes the first letter of a string
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user