feat: melhora a UX de lançamentos e ações rápidas

This commit is contained in:
Felipe Coutinho
2026-03-15 23:23:26 +00:00
parent 2712d4919a
commit df3d0134be
17 changed files with 137 additions and 72 deletions

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleFill, RiBankLine } from "@remixicon/react"; import { RiAddFill, RiBankLine } from "@remixicon/react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@@ -176,7 +176,7 @@ export function AccountsPage({
logoOptions={logoOptions} logoOptions={logoOptions}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Nova conta Nova conta
</Button> </Button>
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleFill, RiFileCopyLine, RiFundsLine } from "@remixicon/react"; import { RiAddFill, RiFileCopyLine, RiFundsLine } from "@remixicon/react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -115,7 +115,7 @@ export function BudgetsPage({
disabled={categories.length === 0} disabled={categories.length === 0}
className="w-full sm:w-auto" className="w-full sm:w-auto"
> >
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Novo orçamento Novo orçamento
</Button> </Button>
} }

View File

@@ -143,7 +143,7 @@ export function DayCell({ day, onSelect, onCreate }: DayCellProps) {
onClick={() => onSelect(day)} onClick={() => onSelect(day)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={cn( className={cn(
"flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border border-transparent bg-card/80 p-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-primary/10", "flex h-full cursor-pointer flex-col gap-1.5 rounded-lg border border-transparent bg-card/80 p-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:border-primary/40 hover:bg-primary/5 dark:hover:bg-accent transition-colors duration-300",
!day.isCurrentMonth && "opacity-60", !day.isCurrentMonth && "opacity-60",
day.isToday && "border-primary/70 bg-primary/5 hover:border-primary", day.isToday && "border-primary/70 bg-primary/5 hover:border-primary",
)} )}

View File

@@ -185,7 +185,7 @@ export function CardItem({
)} )}
</div> </div>
<div className="flex items-center justify-between border-y border-dashed py-3 text-xs font-medium text-muted-foreground sm:text-sm"> <div className="flex items-center justify-between border-y py-3 text-xs font-medium text-muted-foreground sm:text-sm">
<span> <span>
Fecha dia{" "} Fecha dia{" "}
<span className="font-semibold text-foreground"> <span className="font-semibold text-foreground">

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleFill, RiBankCard2Line } from "@remixicon/react"; import { RiAddFill, RiBankCard2Line } from "@remixicon/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -164,7 +164,7 @@ export function CardsPage({
logoOptions={logoOptions} logoOptions={logoOptions}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Novo cartão Novo cartão
</Button> </Button>
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { import {
RiAddCircleFill, RiAddFill,
RiDeleteBin5Line, RiDeleteBin5Line,
RiExternalLinkLine, RiExternalLinkLine,
RiPencilLine, RiPencilLine,
@@ -127,7 +127,7 @@ export function CategoriesPage({ categories }: CategoriesPageProps) {
defaultType={activeType} defaultType={activeType}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Nova categoria Nova categoria
</Button> </Button>
} }

View File

@@ -16,7 +16,7 @@ import {
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { import {
RiAddCircleFill, RiAddFill,
RiCheckLine, RiCheckLine,
RiCloseLine, RiCloseLine,
RiDragMove2Line, RiDragMove2Line,
@@ -218,7 +218,7 @@ export function DashboardGridEditable({
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" 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"> <span className="flex items-center gap-0.5">
<RiAddCircleFill className="size-3.5 shrink-0 text-success/80" /> <RiAddFill className="size-3.5 shrink-0 text-success/80" />
</span> </span>
<span className="sm:hidden">Receita</span> <span className="sm:hidden">Receita</span>
<span className="hidden sm:inline">Nova receita</span> <span className="hidden sm:inline">Nova receita</span>
@@ -243,7 +243,7 @@ export function DashboardGridEditable({
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" 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"> <span className="flex items-center gap-0.5">
<RiAddCircleFill className="size-3.5 shrink-0 text-destructive/80" /> <RiAddFill className="size-3.5 shrink-0 text-destructive/80" />
</span> </span>
<span className="sm:hidden">Despesa</span> <span className="sm:hidden">Despesa</span>
<span className="hidden sm:inline">Nova despesa</span> <span className="hidden sm:inline">Nova despesa</span>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleFill, RiTodoLine } from "@remixicon/react"; import { RiAddFill, RiTodoLine } from "@remixicon/react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { archiveNoteAction, deleteNoteAction } from "@/features/notes/actions"; import { archiveNoteAction, deleteNoteAction } from "@/features/notes/actions";
@@ -208,7 +208,7 @@ export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
onOpenChange={handleCreateOpenChange} onOpenChange={handleCreateOpenChange}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Nova anotação Nova anotação
</Button> </Button>
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { RiAddCircleFill } from "@remixicon/react"; import { RiAddFill } from "@remixicon/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useState, useTransition } from "react"; import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -126,7 +126,7 @@ export function PayersPage({ payers, avatarOptions }: PayersPageProps) {
avatarOptions={avatarOptions} avatarOptions={avatarOptions}
trigger={ trigger={
<Button className="w-full sm:w-auto"> <Button className="w-full sm:w-auto">
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Novo pagador Novo pagador
</Button> </Button>
} }

View File

@@ -77,7 +77,7 @@ export function CardTopExpenses({ data }: CardTopExpensesProps) {
</span> </span>
{expense.category && ( {expense.category && (
<Badge <Badge
variant="secondary" variant="outline"
className="h-5 max-w-full px-1.5 py-0 text-xs truncate" className="h-5 max-w-full px-1.5 py-0 text-xs truncate"
> >
{expense.category} {expense.category}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { import {
RiAddCircleFill, RiAddFill,
RiAlertLine, RiAlertLine,
RiCheckLine, RiCheckLine,
RiDeleteBinLine, RiDeleteBinLine,
@@ -154,7 +154,7 @@ export function ApiTokensForm({ tokens }: ApiTokensFormProps) {
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm"> <Button size="sm">
<RiAddCircleFill className="h-4 w-4 mr-1" /> <RiAddFill className="h-4 w-4 mr-1" />
Novo Token Novo Token
</Button> </Button>
</DialogTrigger> </DialogTrigger>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { import {
RiAddCircleFill, RiAddFill,
RiAlertLine, RiAlertLine,
RiDeleteBinLine, RiDeleteBinLine,
RiFingerprintLine, RiFingerprintLine,
@@ -210,8 +210,8 @@ export function PasskeysForm() {
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" disabled={isMutating || !passkeySupported}> <Button size="sm" disabled={isMutating || !passkeySupported}>
<RiAddCircleFill className="h-4 w-4 mr-1" /> <RiAddFill className="h-4 w-4 mr-1" />
Adicionar Nova Passkey
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>

View File

@@ -69,7 +69,7 @@ function InlinePeriodPicker({
return ( return (
<div className="-mt-1"> <div className="-mt-1">
<span className="text-xs text-muted-foreground">Invoice de </span> <span className="text-xs text-muted-foreground">Fatura de </span>
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<button <button
@@ -554,7 +554,7 @@ export function MassAddDialog({
); );
return selectedOption ? ( return selectedOption ? (
<PayerSelectContent <PayerSelectContent
label={selectedOption.label} label={selectedOption.label.split(" ")[0]}
avatarUrl={selectedOption.avatarUrl} avatarUrl={selectedOption.avatarUrl}
/> />
) : null; ) : null;

View File

@@ -39,7 +39,7 @@ function InlinePeriodPicker({
return ( return (
<div className="ml-1"> <div className="ml-1">
<span className="text-xs text-muted-foreground">Invoice de </span> <span className="text-xs text-muted-foreground">Fatura de </span>
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<button <button

View File

@@ -172,7 +172,11 @@ export function TransactionsPage({
throw new Error(result.error); throw new Error(result.error);
} }
toast.success(result.message); toast.success(
nextValue
? `"${item.name}" marcado como pago`
: `"${item.name}" desmarcado`,
);
} catch (error) { } catch (error) {
const message = const message =
error instanceof Error error instanceof Error

View File

@@ -77,6 +77,7 @@ export function EstabelecimentoInput({
required={required} required={required}
maxLength={maxLength} maxLength={maxLength}
autoComplete="off" autoComplete="off"
className={estabelecimentos.length > 0 ? "pr-8" : undefined}
/> />
{estabelecimentos.length > 0 && ( {estabelecimentos.length > 0 && (
<RiSearchLine className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" /> <RiSearchLine className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" />

View File

@@ -1,9 +1,16 @@
"use client"; "use client";
import { import {
RiAddCircleFill, RiAddCircleFill,
RiAddCircleLine, RiAddFill,
RiArrowLeftDoubleLine,
RiArrowLeftRightLine, RiArrowLeftRightLine,
RiArrowLeftSLine,
RiArrowRightDoubleLine,
RiArrowRightSLine,
RiBankCard2Line,
RiChat1Line, RiChat1Line,
RiCheckboxBlankCircleLine,
RiCheckboxCircleFill,
RiCheckLine, RiCheckLine,
RiDeleteBin5Line, RiDeleteBin5Line,
RiFileCopyLine, RiFileCopyLine,
@@ -12,8 +19,6 @@ import {
RiHistoryLine, RiHistoryLine,
RiMoreFill, RiMoreFill,
RiPencilLine, RiPencilLine,
RiThumbUpFill,
RiThumbUpLine,
RiTimeLine, RiTimeLine,
} from "@remixicon/react"; } from "@remixicon/react";
import { import {
@@ -271,7 +276,7 @@ const buildColumns = ({
{!noteAsColumn && hasNote ? ( {!noteAsColumn && hasNote ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="inline-flex rounded-full p-1 hover:bg-muted/60"> <span className="inline-flex rounded-full p-1 hover:bg-accent transition-colors duration-300">
<RiChat1Line <RiChat1Line
className="h-4 w-4 text-muted-foreground" className="h-4 w-4 text-muted-foreground"
aria-hidden aria-hidden
@@ -544,32 +549,49 @@ const buildColumns = ({
paymentMethod === "Cartão de débito" || paymentMethod === "Cartão de débito" ||
paymentMethod === "Transferência bancária" || paymentMethod === "Transferência bancária" ||
paymentMethod === "Pré-Pago | VR/VA"; paymentMethod === "Pré-Pago | VR/VA";
if (!canToggleSettlement)
return (
<span className="flex size-7 shrink-0 items-center justify-center">
<RiBankCard2Line className="size-4 text-muted-foreground/30" />
</span>
);
const readOnly = row.original.readonly; const readOnly = row.original.readonly;
const loading = isSettlementLoading(row.original.id); const loading = isSettlementLoading(row.original.id);
const settled = Boolean(row.original.isSettled); const settled = Boolean(row.original.isSettled);
const Icon = settled ? RiThumbUpFill : RiThumbUpLine;
return ( return (
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
variant={"outline"} variant="ghost"
size="icon-sm" size="icon-sm"
onClick={() => handleToggleSettlement(row.original)} onClick={() => handleToggleSettlement(row.original)}
disabled={loading || readOnly || !canToggleSettlement} disabled={loading || readOnly}
className={cn( className={cn(
"border-none", "transition-colors",
!canToggleSettlement && "opacity-70 ", settled
settled && "border-none", ? "bg-success/10 text-success hover:bg-success/20 hover:text-success"
: "text-muted-foreground hover:text-foreground",
)} )}
> >
{loading ? ( {loading ? (
<Spinner className="size-4" /> <Spinner className="size-4" />
) : settled ? (
<RiCheckboxCircleFill className="size-4" />
) : ( ) : (
<Icon className={cn("size-4", settled && "text-success")} /> <RiCheckboxBlankCircleLine className="size-4" />
)} )}
<span className="sr-only"> <span className="sr-only">
{settled ? "Desfazer pagamento" : "Marcar como pago"} {settled ? "Desfazer pagamento" : "Marcar como pago"}
</span> </span>
</Button> </Button>
</TooltipTrigger>
<TooltipContent side="top">
{settled ? "Desfazer pagamento" : "Marcar como pago"}
</TooltipContent>
</Tooltip>
); );
})()} })()}
@@ -866,14 +888,14 @@ export function TransactionsTable({
onClick={() => onCreate("Receita")} onClick={() => onCreate("Receita")}
className="w-full sm:w-auto" className="w-full sm:w-auto"
> >
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Nova Receita Nova Receita
</Button> </Button>
<Button <Button
onClick={() => onCreate("Despesa")} onClick={() => onCreate("Despesa")}
className="w-full sm:w-auto" className="w-full sm:w-auto"
> >
<RiAddCircleFill className="size-4" /> <RiAddFill className="size-4" />
Nova Despesa Nova Despesa
</Button> </Button>
</> </>
@@ -887,7 +909,7 @@ export function TransactionsTable({
size="icon" size="icon"
className="hidden size-9 sm:inline-flex" className="hidden size-9 sm:inline-flex"
> >
<RiAddCircleLine className="size-4" /> <RiAddCircleFill className="size-4" />
<span className="sr-only"> <span className="sr-only">
Adicionar múltiplos lançamentos Adicionar múltiplos lançamentos
</span> </span>
@@ -1014,7 +1036,17 @@ export function TransactionsTable({
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{rowModel.rows.map((row) => ( {rowModel.rows.map((row) => (
<TableRow key={row.id}> <TableRow
key={row.id}
className={cn(
row.original.paymentMethod === "Boleto" &&
row.original.dueDate &&
!row.original.isSettled &&
new Date(row.original.dueDate) < new Date()
? "bg-destructive/[0.03] hover:bg-destructive/[0.05]"
: undefined,
)}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}> <TableCell key={cell.id}>
{flexRender( {flexRender(
@@ -1032,7 +1064,7 @@ export function TransactionsTable({
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Exibindo {rowModel.rows.length} de {totalRows} lançamentos {totalRows} lançamentos
</span> </span>
<Select <Select
value={pagination.pageSize.toString()} value={pagination.pageSize.toString()}
@@ -1055,22 +1087,50 @@ export function TransactionsTable({
</Select> </Select>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
Página {table.getState().pagination.pageIndex + 1} de{" "}
{Math.max(table.getPageCount(), 1)}
</span>
<div className="flex items-center gap-1">
<Button <Button
variant="outline" variant="outline"
size="sm" size="icon-sm"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
aria-label="Primeira página"
>
<RiArrowLeftDoubleLine className="size-4" />
</Button>
<Button
variant="outline"
size="icon-sm"
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
aria-label="Página anterior"
> >
Anterior <RiArrowLeftSLine className="size-4" />
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="icon-sm"
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
aria-label="Próxima página"
> >
Próximo <RiArrowRightSLine className="size-4" />
</Button> </Button>
<Button
variant="outline"
size="icon-sm"
onClick={() =>
table.setPageIndex(table.getPageCount() - 1)
}
disabled={!table.getCanNextPage()}
aria-label="Última página"
>
<RiArrowRightDoubleLine className="size-4" />
</Button>
</div>
</div> </div>
</div> </div>
</> </>