Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
256 lines
15 KiB
TypeScript
256 lines
15 KiB
TypeScript
|
|
import React, { useState, useRef } from 'react';
|
|
import { useComFi } from '../contexts/ComFiContext';
|
|
import { useToast } from '../contexts/ToastContext';
|
|
import {
|
|
Building2, Save, UploadCloud, Camera, Palette, Tag,
|
|
Plus, Trash2, Bell, Lock, UserCog, Mail, Key
|
|
} from 'lucide-react';
|
|
import { Category } from '../types';
|
|
import { CustomSelect } from './CustomSelect';
|
|
|
|
export const SettingsView: React.FC = () => {
|
|
const { tenant, setTenant, categories, setCategories, currentUser } = useComFi();
|
|
const { addToast } = useToast();
|
|
const [activeTab, setActiveTab] = useState<'company' | 'categories' | 'security'>('company');
|
|
|
|
// Organization State
|
|
const [orgForm, setOrgForm] = useState(tenant);
|
|
const logoInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// Categories State
|
|
const [newCategory, setNewCategory] = useState({ name: '', type: 'expense' as 'expense' | 'income' });
|
|
|
|
// Handle Organization Save
|
|
const handleSaveOrg = () => {
|
|
setTenant(orgForm);
|
|
addToast({ type: 'success', title: 'Configurações Salvas', message: 'Dados da empresa atualizados.' });
|
|
};
|
|
|
|
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
setOrgForm({ ...orgForm, logo: reader.result as string });
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
// Handle Categories
|
|
const handleAddCategory = () => {
|
|
if (!newCategory.name) return;
|
|
const category: Category = {
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
name: newCategory.name,
|
|
type: newCategory.type,
|
|
color: newCategory.type === 'income' ? 'bg-green-50 text-green-600' : 'bg-red-50 text-red-600'
|
|
};
|
|
setCategories([...categories, category]);
|
|
setNewCategory({ name: '', type: 'expense' });
|
|
addToast({ type: 'success', title: 'Categoria Adicionada' });
|
|
};
|
|
|
|
const handleDeleteCategory = (id: string) => {
|
|
setCategories(categories.filter(c => c.id !== id));
|
|
addToast({ type: 'info', title: 'Categoria Removida' });
|
|
};
|
|
|
|
const inputClass = "w-full p-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-200 outline-none text-slate-800 text-sm";
|
|
const labelClass = "block text-xs font-bold text-slate-700 mb-1 uppercase tracking-wide";
|
|
|
|
return (
|
|
<div className="animate-fade-in max-w-4xl mx-auto pb-10">
|
|
<h1 className="text-2xl font-bold text-slate-800 mb-2">Configurações & Personalização</h1>
|
|
<p className="text-slate-500 mb-6">Gerencie dados da empresa, categorias financeiras e preferências.</p>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-slate-200 mb-6 overflow-x-auto">
|
|
<button
|
|
onClick={() => setActiveTab('company')}
|
|
className={`px-6 py-3 text-sm font-bold border-b-2 transition-colors whitespace-nowrap flex items-center gap-2 ${activeTab === 'company' ? 'border-primary-500 text-primary-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
|
|
>
|
|
<Building2 size={16}/> Minha Empresa
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('categories')}
|
|
className={`px-6 py-3 text-sm font-bold border-b-2 transition-colors whitespace-nowrap flex items-center gap-2 ${activeTab === 'categories' ? 'border-primary-500 text-primary-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
|
|
>
|
|
<Tag size={16}/> Categorias Financeiras
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('security')}
|
|
className={`px-6 py-3 text-sm font-bold border-b-2 transition-colors whitespace-nowrap flex items-center gap-2 ${activeTab === 'security' ? 'border-primary-500 text-primary-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
|
|
>
|
|
<Lock size={16}/> Segurança & Preferências
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-[2rem] shadow-sm border border-slate-100 p-8">
|
|
|
|
{/* ORGANIZATION SETTINGS */}
|
|
{activeTab === 'company' && (
|
|
<div className="space-y-6">
|
|
<div className="flex items-start gap-6">
|
|
<div className="flex flex-col items-center">
|
|
<div
|
|
onClick={() => logoInputRef.current?.click()}
|
|
className="w-32 h-32 rounded-2xl bg-slate-50 border-2 border-dashed border-slate-200 flex items-center justify-center cursor-pointer overflow-hidden hover:border-primary-300 relative group transition-colors"
|
|
>
|
|
{orgForm.logo ? (
|
|
<img src={orgForm.logo} alt="Logo" className="w-full h-full object-contain p-2" />
|
|
) : (
|
|
<div className="text-center">
|
|
<UploadCloud size={24} className="mx-auto text-slate-400 mb-1" />
|
|
<span className="text-[10px] text-slate-400 font-bold uppercase">Upload Logo</span>
|
|
</div>
|
|
)}
|
|
<div className="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<Camera className="text-white" size={24}/>
|
|
</div>
|
|
</div>
|
|
<input type="file" ref={logoInputRef} className="hidden" accept="image/*" onChange={handleLogoUpload} />
|
|
</div>
|
|
|
|
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="col-span-2">
|
|
<label className={labelClass}>Nome da Empresa (Razão Social)</label>
|
|
<input className={inputClass} value={orgForm.name} onChange={e => setOrgForm({...orgForm, name: e.target.value})} placeholder="Minha Empresa S.A." />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>CNPJ</label>
|
|
<input className={inputClass} value={orgForm.cnpj} onChange={e => setOrgForm({...orgForm, cnpj: e.target.value})} placeholder="00.000.000/0000-00" />
|
|
</div>
|
|
<div>
|
|
<label className={labelClass}>Telefone Comercial</label>
|
|
<input className={inputClass} value={orgForm.phone} onChange={e => setOrgForm({...orgForm, phone: e.target.value})} />
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className={labelClass}>Endereço Completo</label>
|
|
<input className={inputClass} value={orgForm.address} onChange={e => setOrgForm({...orgForm, address: e.target.value})} placeholder="Rua, Número, Bairro, Cidade - UF" />
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className={labelClass}>Email Financeiro</label>
|
|
<input className={inputClass} value={orgForm.email} onChange={e => setOrgForm({...orgForm, email: e.target.value})} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-6 border-t border-slate-50 flex justify-end">
|
|
<button onClick={handleSaveOrg} className="px-6 py-3 bg-primary-500 text-white font-bold rounded-xl hover:bg-primary-600 shadow-lg shadow-primary-200 flex items-center gap-2">
|
|
<Save size={18} /> Salvar Alterações
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* CATEGORIES SETTINGS */}
|
|
{activeTab === 'categories' && (
|
|
<div className="space-y-6">
|
|
<div className="flex gap-4 items-end bg-slate-50 p-4 rounded-xl border border-slate-100">
|
|
<div className="flex-1">
|
|
<label className={labelClass}>Nova Categoria</label>
|
|
<input className={inputClass} value={newCategory.name} onChange={e => setNewCategory({...newCategory, name: e.target.value})} placeholder="Ex: Transporte, Freelancers..." />
|
|
</div>
|
|
<div className="w-40">
|
|
<label className={labelClass}>Tipo</label>
|
|
<div className="relative">
|
|
<CustomSelect
|
|
value={newCategory.type}
|
|
onChange={(val) => setNewCategory({...newCategory, type: val})}
|
|
options={[
|
|
{ value: 'expense', label: 'Despesa' },
|
|
{ value: 'income', label: 'Receita' }
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button onClick={handleAddCategory} className="px-4 py-3 bg-slate-800 text-white rounded-xl hover:bg-slate-900 font-bold flex items-center justify-center">
|
|
<Plus size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div>
|
|
<h3 className="font-bold text-slate-800 mb-3 flex items-center gap-2 text-sm"><span className="w-2 h-2 rounded-full bg-red-500"></span> Despesas</h3>
|
|
<div className="space-y-2">
|
|
{categories.filter(c => c.type === 'expense').map(cat => (
|
|
<div key={cat.id} className="flex justify-between items-center p-3 bg-white border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group">
|
|
<span className="text-sm font-medium text-slate-700">{cat.name}</span>
|
|
<button onClick={() => handleDeleteCategory(cat.id)} className="text-slate-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all">
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-slate-800 mb-3 flex items-center gap-2 text-sm"><span className="w-2 h-2 rounded-full bg-green-500"></span> Receitas</h3>
|
|
<div className="space-y-2">
|
|
{categories.filter(c => c.type === 'income').map(cat => (
|
|
<div key={cat.id} className="flex justify-between items-center p-3 bg-white border border-slate-100 rounded-xl hover:bg-slate-50 transition-colors group">
|
|
<span className="text-sm font-medium text-slate-700">{cat.name}</span>
|
|
<button onClick={() => handleDeleteCategory(cat.id)} className="text-slate-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all">
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* SECURITY SETTINGS */}
|
|
{activeTab === 'security' && (
|
|
<div className="space-y-6 max-w-2xl">
|
|
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-100">
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="w-16 h-16 rounded-full bg-white border-4 border-white shadow-sm overflow-hidden">
|
|
<img src={currentUser.avatar} alt="User" className="w-full h-full object-cover" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-slate-800 text-lg">{currentUser.name}</h3>
|
|
<p className="text-sm text-slate-500">{currentUser.email}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className={labelClass}>Alterar Senha</label>
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<Key className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={16} />
|
|
<input type="password" className={`${inputClass} pl-10`} placeholder="Nova senha..." />
|
|
</div>
|
|
<button className="px-4 py-2 bg-slate-800 text-white rounded-xl text-sm font-bold hover:bg-slate-900">Atualizar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<h3 className="font-bold text-slate-800 text-sm uppercase tracking-wide">Notificações</h3>
|
|
<div className="flex items-center justify-between p-4 border border-slate-100 rounded-xl">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center"><Bell size={20}/></div>
|
|
<div>
|
|
<p className="font-bold text-slate-800 text-sm">Alertas por Email</p>
|
|
<p className="text-xs text-slate-400">Receba resumos semanais.</p>
|
|
</div>
|
|
</div>
|
|
<div className="relative inline-block w-12 mr-2 align-middle select-none transition duration-200 ease-in">
|
|
<input type="checkbox" name="toggle" id="toggle" className="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer border-slate-200 checked:right-0 checked:border-green-400"/>
|
|
<label htmlFor="toggle" className="toggle-label block overflow-hidden h-6 rounded-full bg-slate-200 cursor-pointer checked:bg-green-400"></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|