feat: Initialize ComFi project with Vite

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.
This commit is contained in:
MMrp89
2026-02-09 20:28:37 -03:00
parent 1e6a56d866
commit 1a57ac7754
28 changed files with 6070 additions and 8 deletions

255
components/SettingsView.tsx Normal file
View File

@@ -0,0 +1,255 @@
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>
);
};