Files
Condopay/pages/Settings.tsx
frakarr 79e249b638 feat: Setup project with Vite and React
Initializes the Condopay frontend project using Vite, React, and TypeScript. Includes basic project structure, dependencies, and configuration for Tailwind CSS and React Router.
2025-12-06 18:55:48 +01:00

646 lines
31 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { CondoService } from '../services/mockDb';
import { AppSettings, Family, User } from '../types';
import { Save, Building, Coins, Plus, Pencil, Trash2, X, CalendarCheck, AlertTriangle, UserCog, Mail, Phone, Lock, Shield, User as UserIcon } from 'lucide-react';
export const SettingsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'general' | 'families' | 'users'>('general');
const [loading, setLoading] = useState(true);
// General Settings State
const [settings, setSettings] = useState<AppSettings>({
defaultMonthlyQuota: 0,
condoName: '',
currentYear: new Date().getFullYear()
});
const [saving, setSaving] = useState(false);
const [successMsg, setSuccessMsg] = useState('');
// Families State
const [families, setFamilies] = useState<Family[]>([]);
const [showFamilyModal, setShowFamilyModal] = useState(false);
const [editingFamily, setEditingFamily] = useState<Family | null>(null);
const [familyForm, setFamilyForm] = useState({ name: '', unitNumber: '', contactEmail: '' });
// Users State
const [users, setUsers] = useState<User[]>([]);
const [showUserModal, setShowUserModal] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [userForm, setUserForm] = useState({
name: '',
email: '',
password: '',
phone: '',
role: 'user',
familyId: ''
});
useEffect(() => {
const fetchData = async () => {
try {
const [s, f, u] = await Promise.all([
CondoService.getSettings(),
CondoService.getFamilies(),
CondoService.getUsers()
]);
setSettings(s);
setFamilies(f);
setUsers(u);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// --- Settings Handlers ---
const handleSettingsSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setSuccessMsg('');
try {
await CondoService.updateSettings(settings);
setSuccessMsg('Impostazioni salvate con successo!');
setTimeout(() => setSuccessMsg(''), 3000);
} catch (e) {
console.error(e);
} finally {
setSaving(false);
}
};
const handleNewYear = async () => {
const nextYear = settings.currentYear + 1;
if (window.confirm(`Sei sicuro di voler chiudere l'anno ${settings.currentYear} e aprire il ${nextYear}? \n\nI dati vecchi non verranno cancellati, ma la visualizzazione di default passerà al nuovo anno con saldi azzerati.`)) {
setSaving(true);
try {
const newSettings = { ...settings, currentYear: nextYear };
await CondoService.updateSettings(newSettings);
setSettings(newSettings);
setSuccessMsg(`Anno ${nextYear} aperto con successo!`);
setTimeout(() => setSuccessMsg(''), 3000);
} catch(e) {
console.error(e);
} finally {
setSaving(false);
}
}
};
// --- Family Handlers ---
const openAddFamilyModal = () => {
setEditingFamily(null);
setFamilyForm({ name: '', unitNumber: '', contactEmail: '' });
setShowFamilyModal(true);
};
const openEditFamilyModal = (family: Family) => {
setEditingFamily(family);
setFamilyForm({
name: family.name,
unitNumber: family.unitNumber,
contactEmail: family.contactEmail || ''
});
setShowFamilyModal(true);
};
const handleDeleteFamily = async (id: string) => {
if (!window.confirm('Sei sicuro di voler eliminare questa famiglia? Tutti i dati e lo storico pagamenti verranno persi.')) {
return;
}
try {
await CondoService.deleteFamily(id);
setFamilies(families.filter(f => f.id !== id));
} catch (e) {
console.error(e);
}
};
const handleFamilySubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingFamily) {
const updatedFamily = {
...editingFamily,
name: familyForm.name,
unitNumber: familyForm.unitNumber,
contactEmail: familyForm.contactEmail
};
await CondoService.updateFamily(updatedFamily);
setFamilies(families.map(f => f.id === updatedFamily.id ? updatedFamily : f));
} else {
const newFamily = await CondoService.addFamily(familyForm);
setFamilies([...families, newFamily]);
}
setShowFamilyModal(false);
} catch (e) {
console.error(e);
}
};
// --- User Handlers ---
const openAddUserModal = () => {
setEditingUser(null);
setUserForm({ name: '', email: '', password: '', phone: '', role: 'user', familyId: '' });
setShowUserModal(true);
};
const openEditUserModal = (user: User) => {
setEditingUser(user);
setUserForm({
name: user.name || '',
email: user.email,
password: '',
phone: user.phone || '',
role: user.role || 'user',
familyId: user.familyId || ''
});
setShowUserModal(true);
};
const handleDeleteUser = async (id: string) => {
if(!window.confirm("Sei sicuro di voler eliminare questo utente?")) return;
try {
await CondoService.deleteUser(id);
setUsers(users.filter(u => u.id !== id));
} catch (e) {
console.error(e);
}
};
const handleUserSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingUser) {
await CondoService.updateUser(editingUser.id, userForm);
const updatedUsers = await CondoService.getUsers();
setUsers(updatedUsers);
} else {
await CondoService.createUser(userForm);
const updatedUsers = await CondoService.getUsers();
setUsers(updatedUsers);
}
setShowUserModal(false);
} catch (e) {
console.error(e);
alert("Errore nel salvataggio utente");
}
};
const getFamilyName = (id: string | null | undefined) => {
if (!id) return '-';
return families.find(f => f.id === id)?.name || 'Sconosciuta';
};
if (loading) return <div className="p-8 text-center text-slate-400">Caricamento...</div>;
return (
<div className="max-w-4xl mx-auto space-y-6 pb-20">
<div>
<h2 className="text-2xl font-bold text-slate-800">Impostazioni</h2>
<p className="text-slate-500 text-sm md:text-base">Gestisci configurazione, anagrafica e utenti.</p>
</div>
{/* Tabs - Scrollable on mobile */}
<div className="flex border-b border-slate-200 overflow-x-auto no-scrollbar pb-1">
<button
onClick={() => setActiveTab('general')}
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
activeTab === 'general' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
}`}
>
Generale
{activeTab === 'general' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
</button>
<button
onClick={() => setActiveTab('families')}
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
activeTab === 'families' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
}`}
>
Famiglie
{activeTab === 'families' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
</button>
<button
onClick={() => setActiveTab('users')}
className={`px-4 md:px-6 py-3 font-medium text-sm transition-all relative whitespace-nowrap flex-shrink-0 ${
activeTab === 'users' ? 'text-blue-600' : 'text-slate-500 hover:text-slate-700'
}`}
>
Utenti
{activeTab === 'users' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"></div>}
</button>
</div>
{activeTab === 'general' && (
<div className="space-y-6 animate-fade-in">
{/* General Data Form */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8 max-w-2xl">
<h3 className="text-lg font-bold text-slate-800 mb-4">Dati Generali</h3>
<form onSubmit={handleSettingsSubmit} className="space-y-5">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2 flex items-center gap-2">
<Building className="w-4 h-4" />
Nome Condominio
</label>
<input
type="text"
value={settings.condoName}
onChange={(e) => setSettings({ ...settings, condoName: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-blue-500 outline-none transition-all"
placeholder="Es. Condominio Roma"
required
/>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2 flex items-center gap-2">
<Coins className="w-4 h-4" />
Quota Mensile ()
</label>
<div className="relative">
<input
type="number"
step="0.01"
value={settings.defaultMonthlyQuota}
onChange={(e) => setSettings({ ...settings, defaultMonthlyQuota: parseFloat(e.target.value) })}
className="w-full border border-slate-300 rounded-lg px-4 py-2.5 pl-8 focus:ring-2 focus:ring-blue-500 outline-none transition-all"
required
/>
<span className="absolute left-3 top-2.5 text-slate-400"></span>
</div>
</div>
<div className="pt-4 flex flex-col-reverse md:flex-row items-center justify-between gap-4">
<span className={`text-sm font-medium h-5 ${successMsg ? 'text-green-600' : ''}`}>{successMsg}</span>
<button
type="submit"
disabled={saving}
className="w-full md:w-auto flex items-center justify-center gap-2 bg-blue-600 text-white px-6 py-2.5 rounded-lg font-medium hover:bg-blue-700 transition-all disabled:opacity-70"
>
<Save className="w-4 h-4" />
{saving ? '...' : 'Salva'}
</button>
</div>
</form>
</div>
{/* Fiscal Year Management */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8 max-w-2xl">
<h3 className="text-lg font-bold text-slate-800 mb-2 flex items-center gap-2">
<CalendarCheck className="w-5 h-5 text-slate-600" />
Anno Fiscale
</h3>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-6">
<p className="text-slate-600 text-sm">
Anno corrente: <span className="font-bold text-slate-900 text-lg">{settings.currentYear}</span>
</p>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-start gap-3 bg-amber-50 p-3 rounded-lg border border-amber-100">
<AlertTriangle className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" />
<p className="text-xs text-amber-800">
Chiudendo l'anno, il sistema passerà al <strong>{settings.currentYear + 1}</strong>. I dati storici rimarranno consultabili.
</p>
</div>
<button
type="button"
onClick={handleNewYear}
disabled={saving}
className="w-full md:w-auto self-end bg-slate-800 text-white px-4 py-2.5 rounded-lg font-medium hover:bg-slate-900 transition-all disabled:opacity-70 text-sm"
>
Chiudi {settings.currentYear} e apri {settings.currentYear + 1}
</button>
</div>
</div>
</div>
)}
{activeTab === 'families' && (
<div className="space-y-4 animate-fade-in">
<div className="flex justify-end">
<button
onClick={openAddFamilyModal}
className="w-full md:w-auto flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg font-medium shadow-sm transition-colors"
>
<Plus className="w-5 h-5" />
Aggiungi Famiglia
</button>
</div>
{/* Desktop Table */}
<div className="hidden md:block bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<table className="w-full text-left text-sm text-slate-600">
<thead className="bg-slate-50 text-slate-700 font-semibold border-b border-slate-200">
<tr>
<th className="px-6 py-4">Nome Famiglia</th>
<th className="px-6 py-4">Interno</th>
<th className="px-6 py-4">Email</th>
<th className="px-6 py-4 text-right">Azioni</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{families.map(family => (
<tr key={family.id} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-4 font-medium text-slate-900">{family.name}</td>
<td className="px-6 py-4">{family.unitNumber}</td>
<td className="px-6 py-4 text-slate-400">{family.contactEmail || '-'}</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<button onClick={() => openEditFamilyModal(family)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg"><Pencil className="w-4 h-4" /></button>
<button onClick={() => handleDeleteFamily(family.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg"><Trash2 className="w-4 h-4" /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Cards for Families */}
<div className="md:hidden space-y-3">
{families.map(family => (
<div key={family.id} className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm">
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="font-bold text-slate-800 text-lg">{family.name}</h4>
<p className="text-sm text-slate-500 font-medium">Interno: {family.unitNumber}</p>
</div>
</div>
<p className="text-sm text-slate-500 mb-4 flex items-center gap-2">
<Mail className="w-4 h-4" />
{family.contactEmail || 'Nessuna email'}
</p>
<div className="grid grid-cols-2 gap-3 border-t border-slate-100 pt-3">
<button onClick={() => openEditFamilyModal(family)} className="flex items-center justify-center gap-2 py-2 text-blue-600 bg-blue-50 rounded-lg text-sm font-bold">
<Pencil className="w-4 h-4" /> Modifica
</button>
<button onClick={() => handleDeleteFamily(family.id)} className="flex items-center justify-center gap-2 py-2 text-red-600 bg-red-50 rounded-lg text-sm font-bold">
<Trash2 className="w-4 h-4" /> Elimina
</button>
</div>
</div>
))}
</div>
</div>
)}
{activeTab === 'users' && (
<div className="space-y-4 animate-fade-in">
<div className="flex justify-end">
<button
onClick={openAddUserModal}
className="w-full md:w-auto flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg font-medium shadow-sm transition-colors"
>
<Plus className="w-5 h-5" />
Nuovo Utente
</button>
</div>
{/* Desktop Table */}
<div className="hidden md:block bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<table className="w-full text-left text-sm text-slate-600">
<thead className="bg-slate-50 text-slate-700 font-semibold border-b border-slate-200">
<tr>
<th className="px-6 py-4">Utente</th>
<th className="px-6 py-4">Contatti</th>
<th className="px-6 py-4">Ruolo</th>
<th className="px-6 py-4">Famiglia</th>
<th className="px-6 py-4 text-right">Azioni</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{users.map(user => (
<tr key={user.id} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-4 font-medium text-slate-900">{user.name || '-'}</td>
<td className="px-6 py-4">
<div className="font-medium">{user.email}</div>
{user.phone && <div className="text-xs text-slate-400 mt-0.5">{user.phone}</div>}
</td>
<td className="px-6 py-4">
<span className={`px-2 py-1 rounded-full text-xs font-bold uppercase
${user.role === 'admin' ? 'bg-purple-100 text-purple-700' : ''}
${user.role === 'poweruser' ? 'bg-orange-100 text-orange-700' : ''}
${user.role === 'user' ? 'bg-green-100 text-green-700' : ''}
`}>
{user.role}
</span>
</td>
<td className="px-6 py-4">{getFamilyName(user.familyId)}</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<button onClick={() => openEditUserModal(user)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg"><Pencil className="w-4 h-4" /></button>
<button onClick={() => handleDeleteUser(user.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg"><Trash2 className="w-4 h-4" /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Cards for Users */}
<div className="md:hidden space-y-3">
{users.map(user => (
<div key={user.id} className="bg-white p-5 rounded-xl border border-slate-200 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 p-4">
<span className={`px-2 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider
${user.role === 'admin' ? 'bg-purple-100 text-purple-700' : ''}
${user.role === 'poweruser' ? 'bg-orange-100 text-orange-700' : ''}
${user.role === 'user' ? 'bg-green-100 text-green-700' : ''}
`}>
{user.role}
</span>
</div>
<div className="mb-3 pr-16">
<h4 className="font-bold text-slate-800 text-lg flex items-center gap-2">
<UserIcon className="w-4 h-4 text-slate-400"/>
{user.name || 'Senza Nome'}
</h4>
<p className="text-sm text-slate-500 mt-1">{user.email}</p>
</div>
<div className="space-y-2 text-sm text-slate-600 bg-slate-50 p-3 rounded-lg mb-4">
<div className="flex justify-between">
<span className="text-slate-400">Famiglia:</span>
<span className="font-medium">{getFamilyName(user.familyId)}</span>
</div>
{user.phone && (
<div className="flex justify-between">
<span className="text-slate-400">Telefono:</span>
<span className="font-medium">{user.phone}</span>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-3">
<button onClick={() => openEditUserModal(user)} className="flex items-center justify-center gap-2 py-2 text-blue-600 bg-blue-50 rounded-lg text-sm font-bold">
<Pencil className="w-4 h-4" /> Modifica
</button>
<button onClick={() => handleDeleteUser(user.id)} className="flex items-center justify-center gap-2 py-2 text-red-600 bg-red-50 rounded-lg text-sm font-bold">
<Trash2 className="w-4 h-4" /> Elimina
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* Family Modal */}
{showFamilyModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden animate-in fade-in zoom-in duration-200">
<div className="bg-slate-50 px-6 py-4 border-b border-slate-100 flex justify-between items-center">
<h3 className="font-bold text-lg text-slate-800">
{editingFamily ? 'Modifica Famiglia' : 'Nuova Famiglia'}
</h3>
<button onClick={() => setShowFamilyModal(false)} className="text-slate-400 hover:text-slate-600 p-1">
<X className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleFamilySubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Nome Famiglia</label>
<input
type="text"
required
value={familyForm.name}
onChange={(e) => setFamilyForm({...familyForm, name: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="Es. Famiglia Rossi"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Interno / Scala</label>
<input
type="text"
required
value={familyForm.unitNumber}
onChange={(e) => setFamilyForm({...familyForm, unitNumber: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Email</label>
<input
type="email"
value={familyForm.contactEmail}
onChange={(e) => setFamilyForm({...familyForm, contactEmail: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div className="pt-4 flex gap-3">
<button type="button" onClick={() => setShowFamilyModal(false)} className="flex-1 px-4 py-3 border border-slate-300 rounded-lg font-medium text-slate-700 hover:bg-slate-50">Annulla</button>
<button type="submit" className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700">Salva</button>
</div>
</form>
</div>
</div>
)}
{/* User Modal */}
{showUserModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden overflow-y-auto max-h-[90vh] animate-in fade-in zoom-in duration-200">
<div className="bg-slate-50 px-6 py-4 border-b border-slate-100 flex justify-between items-center sticky top-0">
<h3 className="font-bold text-lg text-slate-800">
{editingUser ? 'Modifica Utente' : 'Nuovo Utente'}
</h3>
<button onClick={() => setShowUserModal(false)} className="text-slate-400 hover:text-slate-600 p-1">
<X className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleUserSubmit} className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Nome / Username</label>
<input
type="text"
required
value={userForm.name}
onChange={(e) => setUserForm({...userForm, name: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Email (Login)</label>
<input
type="email"
required
value={userForm.email}
onChange={(e) => setUserForm({...userForm, email: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Telefono</label>
<input
type="tel"
value={userForm.phone}
onChange={(e) => setUserForm({...userForm, phone: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Password</label>
<input
type="password"
required={!editingUser}
value={userForm.password}
onChange={(e) => setUserForm({...userForm, password: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none"
placeholder={editingUser ? "Lascia vuoto per non cambiare" : "Inserisci password"}
/>
</div>
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Ruolo</label>
<select
value={userForm.role}
onChange={(e) => setUserForm({...userForm, role: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none bg-white"
>
<option value="user">User</option>
<option value="poweruser">Poweruser</option>
<option value="admin">Admin</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Famiglia Collegata</label>
<select
value={userForm.familyId}
onChange={(e) => setUserForm({...userForm, familyId: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 focus:ring-2 focus:ring-blue-500 outline-none bg-white"
>
<option value="">Nessuna</option>
{families.map(f => (
<option key={f.id} value={f.id}>{f.name}</option>
))}
</select>
</div>
</div>
<div className="pt-4 flex gap-3">
<button type="button" onClick={() => setShowUserModal(false)} className="flex-1 px-4 py-3 border border-slate-300 rounded-lg font-medium text-slate-700">Annulla</button>
<button type="submit" className="flex-1 px-4 py-3 bg-blue-600 text-white rounded-lg font-medium">Salva</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};