Merge pull request #31 from rede5/feature/ui-improvements-and-access-codes
feat: Adicionar sistema de código de acesso, upload de foto de perfil…
This commit is contained in:
commit
3a0a2041a2
8 changed files with 633 additions and 45 deletions
|
|
@ -20,6 +20,7 @@ import { SettingsPage } from "./pages/Settings";
|
||||||
import { CourseManagement } from "./pages/CourseManagement";
|
import { CourseManagement } from "./pages/CourseManagement";
|
||||||
import { InspirationPage } from "./pages/Inspiration";
|
import { InspirationPage } from "./pages/Inspiration";
|
||||||
import { UserApproval } from "./pages/UserApproval";
|
import { UserApproval } from "./pages/UserApproval";
|
||||||
|
import { AccessCodeManagement } from "./pages/AccessCodeManagement";
|
||||||
import { PrivacyPolicy } from "./pages/PrivacyPolicy";
|
import { PrivacyPolicy } from "./pages/PrivacyPolicy";
|
||||||
import { TermsOfUse } from "./pages/TermsOfUse";
|
import { TermsOfUse } from "./pages/TermsOfUse";
|
||||||
import { LGPD } from "./pages/LGPD";
|
import { LGPD } from "./pages/LGPD";
|
||||||
|
|
@ -520,6 +521,18 @@ const AppContent: React.FC = () => {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/codigos-acesso"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute
|
||||||
|
allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]}
|
||||||
|
>
|
||||||
|
<PageWrapper>
|
||||||
|
<AccessCodeManagement onNavigate={(page) => {}} />
|
||||||
|
</PageWrapper>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Rota padrão - redireciona para home */}
|
{/* Rota padrão - redireciona para home */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|
|
||||||
|
|
@ -287,25 +287,6 @@ export const FotForm: React.FC<FotFormProps> = ({ onCancel, onSubmit, token, exi
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gastos Captação */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Gastos Captação
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">R$</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
value={formData.gastos_captacao}
|
|
||||||
onChange={(e) => setFormData({ ...formData, gastos_captacao: e.target.value })}
|
|
||||||
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
||||||
placeholder="0,00"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pre Venda Checkbox */}
|
{/* Pre Venda Checkbox */}
|
||||||
<div className="flex items-center h-full pt-6">
|
<div className="flex items-center h-full pt-6">
|
||||||
<label className="flex items-center space-x-3 cursor-pointer">
|
<label className="flex items-center space-x-3 cursor-pointer">
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
phone: "",
|
phone: "",
|
||||||
avatar: user?.avatar || "",
|
avatar: user?.avatar || "",
|
||||||
});
|
});
|
||||||
|
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||||
|
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
|
@ -64,6 +66,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
{ name: "Equipe", path: "equipe" },
|
{ name: "Equipe", path: "equipe" },
|
||||||
{ name: "Cadastro de FOT", path: "cursos" },
|
{ name: "Cadastro de FOT", path: "cursos" },
|
||||||
{ name: "Aprovação de Cadastros", path: "aprovacao-cadastros" },
|
{ name: "Aprovação de Cadastros", path: "aprovacao-cadastros" },
|
||||||
|
{ name: "Códigos de Acesso", path: "codigos-acesso" },
|
||||||
{ name: "Financeiro", path: "financeiro" },
|
{ name: "Financeiro", path: "financeiro" },
|
||||||
];
|
];
|
||||||
case UserRole.EVENT_OWNER:
|
case UserRole.EVENT_OWNER:
|
||||||
|
|
@ -99,6 +102,32 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
)}&background=random&color=fff&size=128`;
|
)}&background=random&color=fff&size=128`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// Validate file size (max 5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
alert("A imagem deve ter no máximo 5MB");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert("Por favor, selecione uma imagem válida");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvatarFile(file);
|
||||||
|
|
||||||
|
// Create preview URL
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setAvatarPreview(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3">
|
<nav className="fixed w-full z-50 bg-white shadow-sm py-2 sm:py-3">
|
||||||
|
|
@ -261,6 +290,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
!['entrar', 'cadastro', 'cadastro-profissional'].includes(currentPage) && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
@ -322,6 +352,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -454,6 +485,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
!['entrar', 'cadastro', 'cadastro-profissional'].includes(currentPage) && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
@ -507,6 +539,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -649,13 +682,23 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
||||||
</button>
|
</button>
|
||||||
<div className="w-24 h-24 mx-auto mb-4 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden relative group">
|
<div className="w-24 h-24 mx-auto mb-4 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center border-4 border-white/30 overflow-hidden relative group">
|
||||||
<img
|
<img
|
||||||
src={getAvatarSrc(profileData)}
|
src={avatarPreview || getAvatarSrc(profileData)}
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer">
|
<label
|
||||||
|
htmlFor="avatar-upload"
|
||||||
|
className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer"
|
||||||
|
>
|
||||||
<Camera size={28} className="text-white" />
|
<Camera size={28} className="text-white" />
|
||||||
</div>
|
</label>
|
||||||
|
<input
|
||||||
|
id="avatar-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleAvatarChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-white mb-1">
|
<h2 className="text-2xl font-bold text-white mb-1">
|
||||||
Editar Perfil
|
Editar Perfil
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export interface ProfessionalData {
|
||||||
email: string;
|
email: string;
|
||||||
senha: string;
|
senha: string;
|
||||||
confirmarSenha: string;
|
confirmarSenha: string;
|
||||||
|
avatar?: File | null;
|
||||||
funcaoId: string;
|
funcaoId: string;
|
||||||
cep: string;
|
cep: string;
|
||||||
rua: string;
|
rua: string;
|
||||||
|
|
@ -25,11 +26,13 @@ export interface ProfessionalData {
|
||||||
cpfCnpj: string;
|
cpfCnpj: string;
|
||||||
banco: string;
|
banco: string;
|
||||||
agencia: string;
|
agencia: string;
|
||||||
contaPix: string;
|
conta: string;
|
||||||
|
pix: string;
|
||||||
carroDisponivel: string;
|
carroDisponivel: string;
|
||||||
possuiEstudio: string;
|
possuiEstudio: string;
|
||||||
qtdEstudios: string;
|
qtdEstudios: string;
|
||||||
tipoCartao: string;
|
tipoCartao: string;
|
||||||
|
equipamentos: string;
|
||||||
observacao: string;
|
observacao: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,11 +46,13 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
||||||
const [isLoadingFunctions, setIsLoadingFunctions] = useState(false);
|
const [isLoadingFunctions, setIsLoadingFunctions] = useState(false);
|
||||||
const [functionsError, setFunctionsError] = useState(false);
|
const [functionsError, setFunctionsError] = useState(false);
|
||||||
const [isLoadingCep, setIsLoadingCep] = useState(false);
|
const [isLoadingCep, setIsLoadingCep] = useState(false);
|
||||||
|
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||||
const [formData, setFormData] = useState<ProfessionalData>({
|
const [formData, setFormData] = useState<ProfessionalData>({
|
||||||
nome: "",
|
nome: "",
|
||||||
email: "",
|
email: "",
|
||||||
senha: "",
|
senha: "",
|
||||||
confirmarSenha: "",
|
confirmarSenha: "",
|
||||||
|
avatar: null,
|
||||||
funcaoId: "",
|
funcaoId: "",
|
||||||
cep: "",
|
cep: "",
|
||||||
rua: "",
|
rua: "",
|
||||||
|
|
@ -60,11 +65,13 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
||||||
cpfCnpj: "",
|
cpfCnpj: "",
|
||||||
banco: "",
|
banco: "",
|
||||||
agencia: "",
|
agencia: "",
|
||||||
contaPix: "",
|
conta: "",
|
||||||
|
pix: "",
|
||||||
carroDisponivel: "nao",
|
carroDisponivel: "nao",
|
||||||
possuiEstudio: "nao",
|
possuiEstudio: "nao",
|
||||||
qtdEstudios: "0",
|
qtdEstudios: "0",
|
||||||
tipoCartao: "",
|
tipoCartao: "",
|
||||||
|
equipamentos: "",
|
||||||
observacao: "",
|
observacao: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -91,6 +98,19 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
setFormData((prev) => ({ ...prev, avatar: file }));
|
||||||
|
// Create preview URL
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setAvatarPreview(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCepBlur = async () => {
|
const handleCepBlur = async () => {
|
||||||
const cep = formData.cep.replace(/\D/g, "");
|
const cep = formData.cep.replace(/\D/g, "");
|
||||||
if (cep.length !== 8) return;
|
if (cep.length !== 8) return;
|
||||||
|
|
@ -214,6 +234,57 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
||||||
onChange={(e) => handleChange("nome", e.target.value)}
|
onChange={(e) => handleChange("nome", e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Foto de Perfil
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{avatarPreview ? (
|
||||||
|
<div className="relative w-24 h-24 rounded-full overflow-hidden border-4 border-[#B9CF33] shadow-lg">
|
||||||
|
<img
|
||||||
|
src={avatarPreview}
|
||||||
|
alt="Preview"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setAvatarPreview(null);
|
||||||
|
setFormData((prev) => ({ ...prev, avatar: null }));
|
||||||
|
}}
|
||||||
|
className="absolute top-0 right-0 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"
|
||||||
|
title="Remover foto"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center border-2 border-dashed border-gray-300">
|
||||||
|
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="cursor-pointer inline-flex items-center px-4 py-2 bg-[#B9CF33] text-white rounded-lg hover:opacity-90 transition-opacity">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Escolher Foto
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleAvatarChange}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">PNG, JPG ou JPEG até 5MB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="E-mail *"
|
label="E-mail *"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -419,30 +490,38 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="Banco *"
|
label="Banco"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
|
||||||
value={formData.banco}
|
value={formData.banco}
|
||||||
onChange={(e) => handleChange("banco", e.target.value)}
|
onChange={(e) => handleChange("banco", e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Agência *"
|
label="Agência"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
|
||||||
value={formData.agencia}
|
value={formData.agencia}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value.replace(/\D/g, "");
|
const value = e.target.value.replace(/\D/g, "");
|
||||||
handleChange("agencia", value);
|
handleChange("agencia", value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="Conta / Pix *"
|
label="Conta"
|
||||||
|
type="text"
|
||||||
|
value={formData.conta}
|
||||||
|
onChange={(e) => handleChange("conta", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="PIX *"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
value={formData.contaPix}
|
placeholder="E-mail, telefone, CPF ou chave aleatória"
|
||||||
onChange={(e) => handleChange("contaPix", e.target.value)}
|
value={formData.pix}
|
||||||
|
onChange={(e) => handleChange("pix", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -503,6 +582,19 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Equipamentos
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.equipamentos}
|
||||||
|
onChange={(e) => handleChange("equipamentos", e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent resize-none"
|
||||||
|
placeholder="Descreva os equipamentos que você possui (câmeras, lentes, flashes, etc.)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Tipo de Cartão (Cartão de Memória) *"
|
label="Tipo de Cartão (Cartão de Memória) *"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
|
|
@ -116,15 +116,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async (email: string, password?: string) => {
|
const login = async (email: string, password?: string) => {
|
||||||
// 1. Check for Demo/Mock users first - REMOVED to force API usage
|
// 1. Try Real API first
|
||||||
// const mockUser = MOCK_USERS.find(u => u.email === email);
|
|
||||||
// if (mockUser) {
|
|
||||||
// await new Promise(resolve => setTimeout(resolve, 800)); // Simulate delay
|
|
||||||
// setUser({ ...mockUser, ativo: true });
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 2. Try Real API
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/login`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -166,6 +158,16 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login error:', err);
|
console.error('Login error:', err);
|
||||||
|
|
||||||
|
// 2. Fallback to Demo/Mock users if API fails
|
||||||
|
const mockUser = MOCK_USERS.find(u => u.email === email);
|
||||||
|
if (mockUser) {
|
||||||
|
console.log('Using demo user:', mockUser.email);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
|
||||||
|
setUser({ ...mockUser, ativo: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
293
frontend/pages/AccessCodeManagement.tsx
Normal file
293
frontend/pages/AccessCodeManagement.tsx
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,11 +1,49 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
interface HomeProps {
|
interface HomeProps {
|
||||||
onEnter: () => void;
|
onEnter: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Home: React.FC<HomeProps> = ({ onEnter }) => {
|
export const Home: React.FC<HomeProps> = ({ onEnter }) => {
|
||||||
|
const [showAccessCodeModal, setShowAccessCodeModal] = useState(false);
|
||||||
|
const [accessCode, setAccessCode] = useState("");
|
||||||
|
const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false);
|
||||||
|
const [codeError, setCodeError] = useState("");
|
||||||
|
|
||||||
|
// Código mockado - em produção, verificar com o backend
|
||||||
|
const MOCK_ACCESS_CODE = "PHOTUM2025";
|
||||||
|
|
||||||
|
const handleRegisterClick = () => {
|
||||||
|
setShowAccessCodeModal(true);
|
||||||
|
setAccessCode("");
|
||||||
|
setCodeError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyCode = () => {
|
||||||
|
if (accessCode.trim() === "") {
|
||||||
|
setCodeError("Por favor, digite o código de acesso");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessCode.toUpperCase() === MOCK_ACCESS_CODE) {
|
||||||
|
setShowAccessCodeModal(false);
|
||||||
|
setShowProfessionalPrompt(true);
|
||||||
|
} else {
|
||||||
|
setCodeError("Código de acesso inválido ou expirado");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfessionalChoice = (isProfessional: boolean) => {
|
||||||
|
setShowProfessionalPrompt(false);
|
||||||
|
if (isProfessional) {
|
||||||
|
window.location.href = "/cadastro-profissional";
|
||||||
|
} else {
|
||||||
|
window.location.href = "/cadastro";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="min-h-screen flex items-center justify-center relative overflow-hidden"
|
className="min-h-screen flex items-center justify-center relative overflow-hidden"
|
||||||
|
|
@ -69,7 +107,7 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => (window.location.href = "/cadastro")}
|
onClick={handleRegisterClick}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|
@ -78,6 +116,132 @@ export const Home: React.FC<HomeProps> = ({ onEnter }) => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal de Código de Acesso */}
|
||||||
|
{showAccessCodeModal && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||||
|
onClick={() => setShowAccessCodeModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8 animate-fade-in-scale"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
|
Código de Acesso
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAccessCodeModal(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Digite o código de acesso fornecido pela empresa para continuar
|
||||||
|
com o cadastro.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Código de Acesso *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={accessCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAccessCode(e.target.value.toUpperCase());
|
||||||
|
setCodeError("");
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => e.key === "Enter" && handleVerifyCode()}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent uppercase"
|
||||||
|
placeholder="Digite o código"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{codeError && (
|
||||||
|
<p className="text-red-500 text-sm mt-2">{codeError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-sm text-yellow-800">
|
||||||
|
<strong>Atenção:</strong> O código de acesso é fornecido pela
|
||||||
|
empresa e tem validade temporária.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowAccessCodeModal(false)}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleVerifyCode} className="flex-1">
|
||||||
|
Verificar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Escolha de Tipo de Cadastro */}
|
||||||
|
{showProfessionalPrompt && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||||
|
onClick={() => setShowProfessionalPrompt(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8 animate-fade-in-scale"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
|
Tipo de Cadastro
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowProfessionalPrompt(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Você é um profissional (fotógrafo/cinegrafista) ou um cliente?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleProfessionalChoice(true)}
|
||||||
|
className="w-full p-4 border-2 border-[#492E61] bg-[#492E61]/5 hover:bg-[#492E61]/10 rounded-xl transition-colors text-left group"
|
||||||
|
>
|
||||||
|
<h3 className="font-semibold text-[#492E61] mb-1">
|
||||||
|
Sou Profissional
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Cadastro para fotógrafos e cinegrafistas
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleProfessionalChoice(false)}
|
||||||
|
className="w-full p-4 border-2 border-[#B9CF33] bg-[#B9CF33]/5 hover:bg-[#B9CF33]/10 rounded-xl transition-colors text-left group"
|
||||||
|
>
|
||||||
|
<h3 className="font-semibold text-[#492E61] mb-1">
|
||||||
|
Sou Cliente
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Cadastro para solicitar serviços
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,10 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
||||||
banco: professionalData.banco,
|
banco: professionalData.banco,
|
||||||
carro_disponivel: professionalData.carroDisponivel === "sim",
|
carro_disponivel: professionalData.carroDisponivel === "sim",
|
||||||
cidade: professionalData.cidade,
|
cidade: professionalData.cidade,
|
||||||
conta_pix: professionalData.contaPix,
|
conta_pix: professionalData.pix, // Usando o campo PIX separado
|
||||||
cpf_cnpj_titular: professionalData.cpfCnpj,
|
cpf_cnpj_titular: professionalData.cpfCnpj,
|
||||||
endereco: `${professionalData.cep}, ${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`,
|
endereco: `${professionalData.cep}, ${professionalData.rua}, ${professionalData.numero} - ${professionalData.bairro}`,
|
||||||
equipamentos: "", // Campo não está no form explícito, talvez observação ou outro?
|
equipamentos: professionalData.equipamentos,
|
||||||
extra_por_equipamento: false, // Default
|
extra_por_equipamento: false, // Default
|
||||||
funcao_profissional_id: professionalData.funcaoId,
|
funcao_profissional_id: professionalData.funcaoId,
|
||||||
observacao: professionalData.observacao,
|
observacao: professionalData.observacao,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue