photum/frontend/pages/Profile.tsx

489 lines
20 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
User, Mail, Phone, MapPin, DollarSign, Briefcase,
Camera, FileText, Check, CreditCard, Save, ChevronRight
} from "lucide-react";
import { Navbar } from "../components/Navbar";
import { Button } from "../components/Button";
import { useAuth } from "../contexts/AuthContext";
import { getFunctions } from "../services/apiService";
import { toast } from "react-hot-toast";
// --- Helper Components ---
interface InputFieldProps {
label: string;
icon?: any;
value?: any;
className?: string;
onChange?: (e: any) => void;
type?: string;
placeholder?: string;
maxLength?: number;
required?: boolean;
name?: string;
disabled?: boolean;
}
const InputField = ({ label, icon: Icon, className, ...props }: InputFieldProps) => (
<div className={className}>
<label className="block text-sm font-medium text-gray-700 mb-2">{label}</label>
<div className="relative">
{Icon && (
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
<Icon size={20} />
</div>
)}
<input
className={`w-full ${Icon ? 'pl-10' : 'pl-4'} pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#492E61] focus:border-transparent transition-all`}
{...props}
/>
</div>
</div>
);
const Toggle = ({ label, checked, onChange }: { label: string, checked: boolean, onChange: (val: boolean) => void }) => (
<label className="flex items-center p-4 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50 transition-colors">
<div className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#492E61]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#492E61]"></div>
</div>
<span className="ml-3 text-sm font-medium text-gray-700">{label}</span>
</label>
);
const SidebarItem = ({ id, label, icon: Icon, active, onClick }: any) => (
<button
onClick={onClick}
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl transition-all duration-200 mb-1 ${
active
? "bg-[#492E61] text-white shadow-md"
: "text-gray-600 hover:bg-gray-50 hover:text-[#492E61]"
}`}
>
<div className="flex items-center gap-3">
<Icon size={20} className={active ? "text-white" : "text-gray-400"} />
<span className="font-medium">{label}</span>
</div>
{active && <ChevronRight size={16} className="text-white/80" />}
</button>
);
// --- Main Component ---
export const ProfilePage: React.FC = () => {
const { user, token } = useAuth();
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [activeTab, setActiveTab] = useState("personal");
const [functions, setFunctions] = useState<any[]>([]);
// Form State
const [formData, setFormData] = useState<any>({
id: "",
nome: "",
email: "",
whatsapp: "",
cpf_cnpj_titular: "",
endereco: "",
cidade: "",
uf: "",
banco: "",
agencia: "",
conta_pix: "",
carro_disponivel: false,
tem_estudio: false,
qtd_estudio: 0,
tipo_cartao: "",
equipamentos: "",
extra_por_equipamento: false,
funcoes_ids: [],
avatar_url: ""
});
// Fetch Data
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
if (!token) return;
const response = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/me`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) {
if (response.status === 404) toast.error("Perfil profissional não encontrado.");
else throw new Error("Falha ao carregar perfil");
return;
}
const data = await response.json();
setFormData({
...data,
carro_disponivel: data.carro_disponivel || false,
tem_estudio: data.tem_estudio || false,
extra_por_equipamento: data.extra_por_equipamento || false,
qtd_estudio: data.qtd_estudio || 0,
funcoes_ids: data.functions ? data.functions.map((f: any) => f.id) : []
});
const funcsRes = await getFunctions();
if (funcsRes.data) setFunctions(funcsRes.data);
} catch (error) {
console.error(error);
toast.error("Erro ao carregar dados");
} finally {
setIsLoading(false);
}
};
fetchData();
}, [token]);
const handleChange = (field: string, value: any) => {
setFormData((prev: any) => ({ ...prev, [field]: value }));
};
const handleFunctionToggle = (funcId: string) => {
setFormData((prev: any) => {
const currentIds = prev.funcoes_ids || [];
return currentIds.includes(funcId)
? { ...prev, funcoes_ids: currentIds.filter((id: string) => id !== funcId) }
: { ...prev, funcoes_ids: [...currentIds, funcId] };
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
try {
if (!token) throw new Error("Usuário não autenticado");
const res = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/${formData.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(formData)
});
if (!res.ok) throw new Error("Erro ao salvar");
toast.success("Perfil atualizado!");
} catch (error) {
console.error(error);
toast.error("Erro ao salvar alterações");
} finally {
setIsSaving(false);
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#492E61] border-t-transparent rounded-full animate-spin"></div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<Navbar onNavigate={(page) => window.location.href = `/${page}`} currentPage="profile" />
<div className="pt-28 sm:pt-32 lg:pt-36 pb-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Meu Perfil</h1>
<p className="text-gray-600">Gerencie suas informações pessoais e profissionais.</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* Sidebar (Desktop) */}
<div className="hidden lg:block lg:col-span-1">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 sticky top-32">
<nav className="space-y-1">
<SidebarItem
id="personal"
label="Dados Pessoais"
icon={User}
active={activeTab === 'personal'}
onClick={() => setActiveTab('personal')}
/>
<SidebarItem
id="address"
label="Endereço & Contato"
icon={MapPin}
active={activeTab === 'address'}
onClick={() => setActiveTab('address')}
/>
<SidebarItem
id="bank"
label="Dados Bancários"
icon={CreditCard}
active={activeTab === 'bank'}
onClick={() => setActiveTab('bank')}
/>
<SidebarItem
id="equipment"
label="Profissional"
icon={Camera}
active={activeTab === 'equipment'}
onClick={() => setActiveTab('equipment')}
/>
</nav>
</div>
</div>
{/* Mobile Tabs */}
<div className="lg:hidden mb-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-2 overflow-x-auto">
<div className="flex gap-2 min-w-max">
{['personal', 'address', 'bank', 'equipment'].map(id => (
<button
key={id}
onClick={() => setActiveTab(id)}
className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors ${
activeTab === id ? 'bg-[#492E61] text-white' : 'bg-gray-50 text-gray-600'
}`}
>
{id === 'personal' && 'Dados Pessoais'}
{id === 'address' && 'Endereço'}
{id === 'bank' && 'Banco'}
{id === 'equipment' && 'Profissional'}
</button>
))}
</div>
</div>
</div>
{/* Content Area */}
<div className="lg:col-span-3">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 md:p-8">
<form onSubmit={handleSubmit} className="space-y-8 animate-in fade-in duration-300">
<div className="flex items-center justify-between pb-6 border-b border-gray-100">
<h2 className="text-xl font-bold text-gray-800">
{activeTab === "personal" && "Informações Pessoais"}
{activeTab === "address" && "Endereço e Contato"}
{activeTab === "bank" && "Dados Bancários"}
{activeTab === "equipment" && "Perfil Profissional"}
</h2>
<div className="hidden sm:block">
<Button type="submit" disabled={isSaving}>
{isSaving ? "Salvando..." : "Salvar Alterações"}
</Button>
</div>
</div>
{/* --- PERSONAL --- */}
{activeTab === "personal" && (
<div className="space-y-8">
{/* Avatar Upload */}
<div className="flex items-center gap-6">
<div className="relative group">
<div className="w-24 h-24 rounded-full bg-gray-100 flex items-center justify-center overflow-hidden border-4 border-white shadow-lg">
{formData.avatar_url ? (
<img src={formData.avatar_url} alt="Profile" className="w-full h-full object-cover" />
) : (
<User size={40} className="text-gray-400" />
)}
</div>
<label className="absolute bottom-0 right-0 p-2 bg-[#492E61] rounded-full text-white cursor-pointer hover:bg-[#5a3a7a] transition-all shadow-md transform group-hover:scale-110">
<Camera size={16} />
<input type="file" className="hidden" accept="image/*" disabled />
</label>
</div>
<div>
<h3 className="font-bold text-lg text-gray-900">Foto de Perfil</h3>
<p className="text-sm text-gray-500">Recomendado: 400x400px. Max: 2MB.</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<InputField
label="Nome Completo"
icon={User}
value={formData.nome}
onChange={(e) => handleChange("nome", e.target.value)}
required
/>
<InputField
label="CPF/CNPJ"
icon={FileText}
value={formData.cpf_cnpj_titular}
onChange={(e) => handleChange("cpf_cnpj_titular", e.target.value)}
/>
</div>
</div>
)}
{/* --- ADDRESS --- */}
{activeTab === "address" && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<InputField
label="Email"
icon={Mail}
type="email"
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
required
/>
<InputField
label="WhatsApp"
icon={Phone}
value={formData.whatsapp}
onChange={(e) => handleChange("whatsapp", e.target.value)}
/>
<div className="md:col-span-2">
<InputField
label="Endereço"
icon={MapPin}
value={formData.endereco}
onChange={(e) => handleChange("endereco", e.target.value)}
/>
</div>
<InputField
label="Cidade"
icon={MapPin}
value={formData.cidade}
onChange={(e) => handleChange("cidade", e.target.value)}
/>
<InputField
label="UF"
icon={MapPin}
value={formData.uf}
onChange={(e) => handleChange("uf", e.target.value)}
maxLength={2}
/>
</div>
</div>
)}
{/* --- BANK --- */}
{activeTab === "bank" && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<InputField
label="Banco"
icon={Briefcase}
value={formData.banco}
onChange={(e) => handleChange("banco", e.target.value)}
/>
<InputField
label="Agência"
icon={Briefcase}
value={formData.agencia}
onChange={(e) => handleChange("agencia", e.target.value)}
/>
<InputField
label="Chave PIX"
icon={DollarSign}
value={formData.conta_pix}
onChange={(e) => handleChange("conta_pix", e.target.value)}
/>
</div>
</div>
)}
{/* --- EQUIPMENT --- */}
{activeTab === "equipment" && (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">Funções Atuantes</label>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
{functions.map((func) => (
<label key={func.id} className={`flex items-center p-3 rounded-xl border cursor-pointer transition-all ${
formData.funcoes_ids?.includes(func.id)
? "border-[#492E61] bg-[#492E61]/5 text-[#492E61] ring-1 ring-[#492E61]"
: "border-gray-200 hover:border-gray-300 hover:bg-gray-50"
}`}>
<div className={`w-5 h-5 rounded border mr-3 flex items-center justify-center transition-colors ${
formData.funcoes_ids?.includes(func.id) ? "bg-[#492E61] border-[#492E61]" : "border-gray-300 bg-white"
}`}>
{formData.funcoes_ids?.includes(func.id) && <Check size={14} className="text-white" />}
</div>
<span className="text-sm font-medium">{func.nome}</span>
<input
type="checkbox"
className="sr-only"
checked={formData.funcoes_ids?.includes(func.id) || false}
onChange={() => handleFunctionToggle(func.id)}
/>
</label>
))}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 border-t border-gray-100">
<Toggle
label="Possui carro disponível?"
checked={formData.carro_disponivel}
onChange={(checked) => handleChange("carro_disponivel", checked)}
/>
<Toggle
label="Cobra extra por equipamento?"
checked={formData.extra_por_equipamento}
onChange={(checked) => handleChange("extra_por_equipamento", checked)}
/>
<Toggle
label="Possui estúdio?"
checked={formData.tem_estudio}
onChange={(checked) => handleChange("tem_estudio", checked)}
/>
{formData.tem_estudio && (
<InputField
label="Qtd. Estúdios"
icon={Briefcase}
type="number"
value={formData.qtd_estudio}
onChange={(e) => handleChange("qtd_estudio", parseInt(e.target.value))}
/>
)}
<div className="md:col-span-2">
<InputField
label="Tipo de Cartão de Memória"
icon={Briefcase}
value={formData.tipo_cartao}
onChange={(e) => handleChange("tipo_cartao", e.target.value)}
placeholder="Ex: SD, CF Express..."
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">Lista de Equipamentos</label>
<textarea
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 transition-all resize-y min-h-[120px]"
value={formData.equipamentos || ""}
onChange={(e) => handleChange("equipamentos", e.target.value)}
placeholder="Liste suas câmeras, lentes e flashes..."
/>
</div>
</div>
</div>
)}
<div className="pt-6 sm:hidden">
<Button type="submit" disabled={isSaving} className="w-full">
{isSaving ? "Salvando..." : "Salvar Alterações"}
</Button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
);
};