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:
Felipe Coutinho
2026-01-03 14:18:03 +00:00
parent 3eca48c71a
commit fd817683ca
87 changed files with 13582 additions and 1445 deletions

View File

@@ -68,148 +68,153 @@ export function UpdateEmailForm({ currentEmail, authProvider }: UpdateEmailFormP
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
{/* E-mail atual (apenas informativo) */}
<div className="space-y-2">
<Label htmlFor="currentEmail">E-mail atual</Label>
<Input
id="currentEmail"
type="email"
value={currentEmail}
disabled
className="bg-muted cursor-not-allowed"
aria-describedby="current-email-help"
/>
<p id="current-email-help" className="text-xs text-muted-foreground">
Este é seu e-mail atual cadastrado
</p>
</div>
{/* Senha de confirmação (apenas para usuários com login por e-mail/senha) */}
{!isGoogleAuth && (
<form onSubmit={handleSubmit} className="flex flex-col space-y-6">
<div className="space-y-4 max-w-md">
{/* E-mail atual (apenas informativo) */}
<div className="space-y-2">
<Label htmlFor="password">
Senha atual <span className="text-destructive">*</span>
<Label htmlFor="currentEmail">E-mail atual</Label>
<Input
id="currentEmail"
type="email"
value={currentEmail}
disabled
className="bg-muted cursor-not-allowed"
aria-describedby="current-email-help"
/>
<p id="current-email-help" className="text-xs text-muted-foreground">
Este é seu e-mail atual cadastrado
</p>
</div>
{/* Senha de confirmação (apenas para usuários com login por e-mail/senha) */}
{!isGoogleAuth && (
<div className="space-y-2">
<Label htmlFor="password">
Senha atual <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isPending}
placeholder="Digite sua senha para confirmar"
required
aria-required="true"
aria-describedby="password-help"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={showPassword ? "Ocultar senha" : "Mostrar senha"}
>
{showPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
</div>
<p id="password-help" className="text-xs text-muted-foreground">
Por segurança, confirme sua senha antes de alterar seu e-mail
</p>
</div>
)}
{/* Novo e-mail */}
<div className="space-y-2">
<Label htmlFor="newEmail">
Novo e-mail <span className="text-destructive">*</span>
</Label>
<Input
id="newEmail"
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
disabled={isPending}
placeholder="Digite o novo e-mail"
required
aria-required="true"
aria-describedby="new-email-help"
aria-invalid={!isEmailDifferent}
className={!isEmailDifferent ? "border-red-500 focus-visible:ring-red-500" : ""}
/>
{!isEmailDifferent && newEmail && (
<p className="text-xs text-red-600 dark:text-red-400 flex items-center gap-1" role="alert">
<RiCloseLine className="h-3.5 w-3.5" />
O novo e-mail deve ser diferente do atual
</p>
)}
{!newEmail && (
<p id="new-email-help" className="text-xs text-muted-foreground">
Digite o novo endereço de e-mail para sua conta
</p>
)}
</div>
{/* Confirmar novo e-mail */}
<div className="space-y-2">
<Label htmlFor="confirmEmail">
Confirmar novo e-mail <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
id="confirmEmail"
type="email"
value={confirmEmail}
onChange={(e) => setConfirmEmail(e.target.value)}
disabled={isPending}
placeholder="Digite sua senha para confirmar"
placeholder="Repita o novo e-mail"
required
aria-required="true"
aria-describedby="password-help"
aria-describedby="confirm-email-help"
aria-invalid={emailsMatch === false}
className={
emailsMatch === false
? "border-red-500 focus-visible:ring-red-500 pr-10"
: emailsMatch === true
? "border-green-500 focus-visible:ring-green-500 pr-10"
: ""
}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={showPassword ? "Ocultar senha" : "Mostrar senha"}
>
{showPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
{/* Indicador visual de match */}
{emailsMatch !== null && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{emailsMatch ? (
<RiCheckLine className="h-5 w-5 text-green-500" aria-label="Os e-mails coincidem" />
) : (
<RiCloseLine className="h-5 w-5 text-red-500" aria-label="Os e-mails não coincidem" />
)}
</div>
)}
</div>
<p id="password-help" className="text-xs text-muted-foreground">
Por segurança, confirme sua senha antes de alterar seu e-mail
</p>
</div>
)}
{/* Novo e-mail */}
<div className="space-y-2">
<Label htmlFor="newEmail">
Novo e-mail <span className="text-destructive">*</span>
</Label>
<Input
id="newEmail"
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
disabled={isPending}
placeholder="Digite o novo e-mail"
required
aria-required="true"
aria-describedby="new-email-help"
aria-invalid={!isEmailDifferent}
className={!isEmailDifferent ? "border-red-500 focus-visible:ring-red-500" : ""}
/>
{!isEmailDifferent && newEmail && (
<p className="text-xs text-red-600 dark:text-red-400 flex items-center gap-1" role="alert">
<RiCloseLine className="h-3.5 w-3.5" />
O novo e-mail deve ser diferente do atual
</p>
)}
{!newEmail && (
<p id="new-email-help" className="text-xs text-muted-foreground">
Digite o novo endereço de e-mail para sua conta
</p>
)}
</div>
{/* Confirmar novo e-mail */}
<div className="space-y-2">
<Label htmlFor="confirmEmail">
Confirmar novo e-mail <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="confirmEmail"
type="email"
value={confirmEmail}
onChange={(e) => setConfirmEmail(e.target.value)}
disabled={isPending}
placeholder="Repita o novo e-mail"
required
aria-required="true"
aria-describedby="confirm-email-help"
aria-invalid={emailsMatch === false}
className={
emailsMatch === false
? "border-red-500 focus-visible:ring-red-500 pr-10"
: emailsMatch === true
? "border-green-500 focus-visible:ring-green-500 pr-10"
: ""
}
/>
{/* Indicador visual de match */}
{emailsMatch !== null && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{emailsMatch ? (
<RiCheckLine className="h-5 w-5 text-green-500" aria-label="Os e-mails coincidem" />
) : (
<RiCloseLine className="h-5 w-5 text-red-500" aria-label="Os e-mails não coincidem" />
)}
</div>
{/* Mensagem de erro em tempo real */}
{emailsMatch === false && (
<p id="confirm-email-help" className="text-xs text-red-600 dark:text-red-400 flex items-center gap-1" role="alert">
<RiCloseLine className="h-3.5 w-3.5" />
Os e-mails não coincidem
</p>
)}
{emailsMatch === true && (
<p id="confirm-email-help" className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
<RiCheckLine className="h-3.5 w-3.5" />
Os e-mails coincidem
</p>
)}
</div>
{/* Mensagem de erro em tempo real */}
{emailsMatch === false && (
<p id="confirm-email-help" className="text-xs text-red-600 dark:text-red-400 flex items-center gap-1" role="alert">
<RiCloseLine className="h-3.5 w-3.5" />
Os e-mails não coincidem
</p>
)}
{emailsMatch === true && (
<p id="confirm-email-help" className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
<RiCheckLine className="h-3.5 w-3.5" />
Os e-mails coincidem
</p>
)}
</div>
<Button
type="submit"
disabled={isPending || emailsMatch === false || !isEmailDifferent}
>
{isPending ? "Atualizando..." : "Atualizar e-mail"}
</Button>
<div className="flex justify-end">
<Button
type="submit"
disabled={isPending || emailsMatch === false || !isEmailDifferent}
className="w-fit"
>
{isPending ? "Atualizando..." : "Atualizar e-mail"}
</Button>
</div>
</form>
);
}