photum/frontend/pages/AccessCodeManagement.tsx
João Vitor 888ae9eb62 feat: Adicionar sistema de código de acesso, upload de foto de perfil e melhorias de UI
- Adicionar modal de verificação de código de acesso na página inicial para cadastro

- Adicionar modal de seleção profissional/cliente após verificação do código

- Criar página AccessCodeManagement para CEO/SUPERADMIN gerar e gerenciar códigos de acesso

- Adicionar upload de foto de perfil no formulário de cadastro de profissional

- Adicionar upload de foto de perfil no modal de edição de perfil

- Remover botão 'Entrar/Cadastrar' do header nas rotas de login e cadastro

- Separar campos Conta e PIX no cadastro de profissional (apenas PIX obrigatório)

- Adicionar campo de descrição de equipamentos no cadastro de profissional

- Remover edição manual de 'Gastos Captação' (campo calculado)

- Converter página de códigos de acesso para formato de tabela estilo Excel

- Restaurar fallback de login do usuário demo quando o backend estiver indisponível

- Padronizar espaçamentos e fontes do header nas páginas administrativas

- Adicionar novo item de menu 'Códigos de Acesso' para CEO/SUPERADMIN
2025-12-18 15:12:20 -03:00

293 lines
11 KiB
TypeScript

import React, { useState } from "react";
import { Button } from "../components/Button";
import { Copy, Trash2, Plus, Calendar, Key, Check } from "lucide-react";
interface AccessCodeManagementProps {
onNavigate: (page: string) => void;
}
interface AccessCode {
id: string;
code: string;
expiresIn: number; // dias
createdAt: Date;
expiresAt: Date;
isActive: boolean;
}
export const AccessCodeManagement: React.FC<AccessCodeManagementProps> = ({
onNavigate,
}) => {
const [accessCodes, setAccessCodes] = useState<AccessCode[]>([
{
id: "1",
code: "PHOTUM2025",
expiresIn: 30,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
isActive: true,
},
]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [newCodeDays, setNewCodeDays] = useState("7");
const [copiedCode, setCopiedCode] = useState<string | null>(null);
const generateCode = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let code = "";
for (let i = 0; i < 10; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
};
const handleCreateCode = () => {
const days = parseInt(newCodeDays);
if (isNaN(days) || days < 1 || days > 365) {
alert("Por favor, insira um número válido de dias entre 1 e 365");
return;
}
const newCode: AccessCode = {
id: Date.now().toString(),
code: generateCode(),
expiresIn: days,
createdAt: new Date(),
expiresAt: new Date(Date.now() + days * 24 * 60 * 60 * 1000),
isActive: true,
};
setAccessCodes([newCode, ...accessCodes]);
setShowCreateModal(false);
setNewCodeDays("7");
};
const handleDeleteCode = (id: string) => {
if (confirm("Tem certeza que deseja excluir este código de acesso?")) {
setAccessCodes(accessCodes.filter((code) => code.id !== id));
}
};
const handleCopyCode = (code: string) => {
navigator.clipboard.writeText(code);
setCopiedCode(code);
setTimeout(() => setCopiedCode(null), 2000);
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
};
const getDaysRemaining = (expiresAt: Date) => {
const now = new Date();
const diffTime = expiresAt.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
const isExpired = (expiresAt: Date) => {
return new Date() > expiresAt;
};
return (
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-6 sm:mb-8">
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black mb-2">
Gerenciar Códigos de Acesso
</h1>
<p className="text-sm sm:text-base text-gray-600">
Crie e gerencie códigos de acesso temporários para novos cadastros
</p>
</div>
{/* Botão Criar Código */}
<div className="mb-6">
<Button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2"
>
<Plus size={20} />
Gerar Novo Código
</Button>
</div>
{/* Tabela de Códigos */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
{accessCodes.length === 0 ? (
<div className="p-12 text-center">
<Key size={48} className="mx-auto text-gray-300 mb-4" />
<p className="text-gray-500 text-lg mb-2">
Nenhum código de acesso criado
</p>
<p className="text-gray-400 text-sm">
Clique em "Gerar Novo Código" para criar um código de acesso
</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Código
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Validade (dias)
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Data de Criação
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
Data de Expiração
</th>
<th className="px-6 py-4 text-center text-xs font-semibold text-gray-700 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{accessCodes.map((code) => {
const expired = isExpired(code.expiresAt);
const daysRemaining = getDaysRemaining(code.expiresAt);
return (
<tr
key={code.id}
className={`hover:bg-gray-50 transition-colors ${
expired ? "bg-red-50/30" : ""
}`}
>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`inline-flex px-3 py-1 rounded-full text-xs font-semibold ${
expired
? "bg-red-100 text-red-700"
: daysRemaining <= 3
? "bg-yellow-100 text-yellow-700"
: "bg-green-100 text-green-700"
}`}
>
{expired
? "Expirado"
: daysRemaining === 1
? "1 dia"
: `${daysRemaining} dias`}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
<code className="text-lg font-bold text-[#492E61] tracking-wider">
{code.code}
</code>
<button
onClick={() => handleCopyCode(code.code)}
className="p-1.5 hover:bg-gray-100 rounded transition-colors"
title="Copiar código"
>
{copiedCode === code.code ? (
<Check size={16} className="text-green-500" />
) : (
<Copy size={16} className="text-gray-400" />
)}
</button>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
{code.expiresIn} {code.expiresIn === 1 ? "dia" : "dias"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{formatDate(code.createdAt)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{formatDate(code.expiresAt)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<button
onClick={() => handleDeleteCode(code.id)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-red-50 text-red-600 hover:bg-red-100 rounded-lg transition-colors text-sm font-medium"
title="Excluir código"
>
<Trash2 size={16} />
Excluir
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Modal de Criar Código */}
{showCreateModal && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
onClick={() => setShowCreateModal(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Gerar Novo Código de Acesso
</h2>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Validade do Código (em dias) *
</label>
<input
type="number"
min="1"
max="365"
value={newCodeDays}
onChange={(e) => setNewCodeDays(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#492E61] focus:border-transparent"
placeholder="Ex: 7"
/>
<p className="text-sm text-gray-500 mt-2">
O código será válido por {newCodeDays} dia(s) a partir da
criação
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<p className="text-sm text-blue-800">
<strong>Dica:</strong> O código será gerado automaticamente e
poderá ser compartilhado com os usuários que desejam se
cadastrar.
</p>
</div>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => setShowCreateModal(false)}
className="flex-1"
>
Cancelar
</Button>
<Button onClick={handleCreateCode} className="flex-1">
Gerar Código
</Button>
</div>
</div>
</div>
)}
</div>
);
};