- 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
293 lines
11 KiB
TypeScript
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>
|
|
);
|
|
};
|