Introduces SMTP configuration settings and alert definitions to enable automated email notifications. This includes new types for `SmtpConfig` and `AlertDefinition`, and integrates these into the settings page and mock database. Adds styling for select elements and scrollbar hiding in the main HTML. Updates mock database logic to potentially support local development without a backend.
443 lines
15 KiB
TypeScript
443 lines
15 KiB
TypeScript
import { Family, Payment, AppSettings, User, AuthResponse, AlertDefinition } from '../types';
|
|
|
|
// --- CONFIGURATION TOGGLE ---
|
|
// TRUE = WORK MODE (Localstorage, no backend required)
|
|
// FALSE = COMMIT MODE (Real API calls)
|
|
const FORCE_LOCAL_DB = false;
|
|
|
|
const API_URL = '/api';
|
|
|
|
const STORAGE_KEYS = {
|
|
SETTINGS: 'condo_settings',
|
|
FAMILIES: 'condo_families',
|
|
PAYMENTS: 'condo_payments',
|
|
TOKEN: 'condo_auth_token',
|
|
USER: 'condo_user_info',
|
|
USERS_LIST: 'condo_users_list',
|
|
ALERTS: 'condo_alerts_def'
|
|
};
|
|
|
|
const getLocal = <T>(key: string, defaultVal: T): T => {
|
|
try {
|
|
const item = localStorage.getItem(key);
|
|
return item ? JSON.parse(item) : defaultVal;
|
|
} catch {
|
|
return defaultVal;
|
|
}
|
|
};
|
|
|
|
const setLocal = (key: string, val: any) => {
|
|
localStorage.setItem(key, JSON.stringify(val));
|
|
};
|
|
|
|
// --- AUTH HELPERS ---
|
|
|
|
const getAuthHeaders = () => {
|
|
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
|
|
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
|
};
|
|
|
|
// --- SERVICE IMPLEMENTATION ---
|
|
|
|
export const CondoService = {
|
|
|
|
login: async (email, password) => {
|
|
if (FORCE_LOCAL_DB) {
|
|
// MOCK LOGIN for Preview
|
|
await new Promise(resolve => setTimeout(resolve, 600)); // Fake delay
|
|
|
|
// Allow any login, but give admin rights to specific email or generic
|
|
const role = email.includes('admin') || email === 'fcarra79@gmail.com' ? 'admin' : 'user';
|
|
|
|
const mockUser: User = {
|
|
id: 'local-user-' + Math.random().toString(36).substr(2, 9),
|
|
email,
|
|
name: email.split('@')[0],
|
|
role: role as any,
|
|
familyId: null,
|
|
receiveAlerts: true
|
|
};
|
|
|
|
localStorage.setItem(STORAGE_KEYS.TOKEN, 'mock-local-token-' + Date.now());
|
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(mockUser));
|
|
return { token: localStorage.getItem(STORAGE_KEYS.TOKEN)!, user: mockUser };
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password })
|
|
});
|
|
if (!res.ok) throw new Error('Login fallito');
|
|
const data = await res.json();
|
|
localStorage.setItem(STORAGE_KEYS.TOKEN, data.token);
|
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(data.user));
|
|
return data;
|
|
} catch (e) {
|
|
console.warn("Backend unavailable or login failed");
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
logout: () => {
|
|
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
window.location.href = '#/login';
|
|
},
|
|
|
|
getCurrentUser: (): User | null => {
|
|
return getLocal<User | null>(STORAGE_KEYS.USER, null);
|
|
},
|
|
|
|
updateProfile: async (data: Partial<User> & { password?: string }) => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const currentUser = getLocal<User | null>(STORAGE_KEYS.USER, null);
|
|
if (!currentUser) throw new Error("Not logged in");
|
|
|
|
// Update current user session
|
|
const updatedUser = { ...currentUser, ...data };
|
|
delete (updatedUser as any).password;
|
|
setLocal(STORAGE_KEYS.USER, updatedUser);
|
|
|
|
// Update in users list if it exists
|
|
const users = getLocal<User[]>(STORAGE_KEYS.USERS_LIST, []);
|
|
const userIndex = users.findIndex(u => u.id === currentUser.id || u.email === currentUser.email);
|
|
if (userIndex >= 0) {
|
|
users[userIndex] = { ...users[userIndex], ...data };
|
|
delete (users[userIndex] as any).password; // mock logic: don't store pw
|
|
setLocal(STORAGE_KEYS.USERS_LIST, users);
|
|
}
|
|
return { success: true, user: updatedUser };
|
|
}
|
|
|
|
const res = await fetch(`${API_URL}/profile`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (!res.ok) throw new Error('Failed to update profile');
|
|
const response = await res.json();
|
|
|
|
// Update local session
|
|
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(response.user));
|
|
return response;
|
|
},
|
|
|
|
getSettings: async (): Promise<AppSettings> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
return getLocal<AppSettings>(STORAGE_KEYS.SETTINGS, {
|
|
defaultMonthlyQuota: 100,
|
|
condoName: 'Condominio (Anteprima)',
|
|
currentYear: new Date().getFullYear(),
|
|
smtpConfig: {
|
|
host: '', port: 587, user: '', pass: '', secure: false, fromEmail: ''
|
|
}
|
|
});
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/settings`, { headers: getAuthHeaders() });
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
return getLocal<AppSettings>(STORAGE_KEYS.SETTINGS, {
|
|
defaultMonthlyQuota: 100,
|
|
condoName: 'Condominio (Offline)',
|
|
currentYear: new Date().getFullYear()
|
|
});
|
|
}
|
|
},
|
|
|
|
updateSettings: async (settings: AppSettings): Promise<void> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
setLocal(STORAGE_KEYS.SETTINGS, settings);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/settings`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(settings)
|
|
});
|
|
if (!res.ok) throw new Error('API Error');
|
|
} catch (e) {
|
|
setLocal(STORAGE_KEYS.SETTINGS, settings);
|
|
}
|
|
},
|
|
|
|
getAvailableYears: async (): Promise<number[]> => {
|
|
// Shared logic works for both because it falls back to calculating from payments
|
|
if (FORCE_LOCAL_DB) {
|
|
const payments = getLocal<Payment[]>(STORAGE_KEYS.PAYMENTS, []);
|
|
const settings = getLocal<AppSettings>(STORAGE_KEYS.SETTINGS, { currentYear: new Date().getFullYear() } as AppSettings);
|
|
const years = new Set(payments.map(p => p.forYear));
|
|
years.add(settings.currentYear);
|
|
return Array.from(years).sort((a, b) => b - a);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/years`, { headers: getAuthHeaders() });
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
const payments = getLocal<Payment[]>(STORAGE_KEYS.PAYMENTS, []);
|
|
const settings = getLocal<AppSettings>(STORAGE_KEYS.SETTINGS, { currentYear: new Date().getFullYear() } as AppSettings);
|
|
const years = new Set(payments.map(p => p.forYear));
|
|
years.add(settings.currentYear);
|
|
return Array.from(years).sort((a, b) => b - a);
|
|
}
|
|
},
|
|
|
|
getFamilies: async (): Promise<Family[]> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
return getLocal<Family[]>(STORAGE_KEYS.FAMILIES, []);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/families`, { headers: getAuthHeaders() });
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
return getLocal<Family[]>(STORAGE_KEYS.FAMILIES, []);
|
|
}
|
|
},
|
|
|
|
addFamily: async (familyData: Omit<Family, 'id' | 'balance'>): Promise<Family> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const families = getLocal<Family[]>(STORAGE_KEYS.FAMILIES, []);
|
|
const newFamily = { ...familyData, id: crypto.randomUUID(), balance: 0 };
|
|
setLocal(STORAGE_KEYS.FAMILIES, [...families, newFamily]);
|
|
return newFamily;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/families`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(familyData)
|
|
});
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
updateFamily: async (family: Family): Promise<Family> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const families = getLocal<Family[]>(STORAGE_KEYS.FAMILIES, []);
|
|
const updated = families.map(f => f.id === family.id ? family : f);
|
|
setLocal(STORAGE_KEYS.FAMILIES, updated);
|
|
return family;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/families/${family.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(family)
|
|
});
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
deleteFamily: async (familyId: string): Promise<void> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const families = getLocal<Family[]>(STORAGE_KEYS.FAMILIES, []);
|
|
setLocal(STORAGE_KEYS.FAMILIES, families.filter(f => f.id !== familyId));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/families/${familyId}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (!res.ok) throw new Error('API Error');
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
getPaymentsByFamily: async (familyId: string): Promise<Payment[]> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const payments = getLocal<Payment[]>(STORAGE_KEYS.PAYMENTS, []);
|
|
return payments.filter(p => p.familyId === familyId);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/payments?familyId=${familyId}`, { headers: getAuthHeaders() });
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
const payments = getLocal<Payment[]>(STORAGE_KEYS.PAYMENTS, []);
|
|
return payments.filter(p => p.familyId === familyId);
|
|
}
|
|
},
|
|
|
|
addPayment: async (payment: Omit<Payment, 'id'>): Promise<Payment> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const payments = getLocal<Payment[]>(STORAGE_KEYS.PAYMENTS, []);
|
|
const newPayment = { ...payment, id: crypto.randomUUID() };
|
|
setLocal(STORAGE_KEYS.PAYMENTS, [...payments, newPayment]);
|
|
return newPayment;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/payments`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(payment)
|
|
});
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
getUsers: async (): Promise<User[]> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
return getLocal<User[]>(STORAGE_KEYS.USERS_LIST, []);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/users`, { headers: getAuthHeaders() });
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
},
|
|
|
|
createUser: async (userData: any) => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const users = getLocal<User[]>(STORAGE_KEYS.USERS_LIST, []);
|
|
const newUser = { ...userData, id: crypto.randomUUID() };
|
|
if (newUser.receiveAlerts === undefined) newUser.receiveAlerts = true;
|
|
// Don't save password in local mock
|
|
delete newUser.password;
|
|
setLocal(STORAGE_KEYS.USERS_LIST, [...users, newUser]);
|
|
return { success: true, id: newUser.id };
|
|
}
|
|
|
|
const res = await fetch(`${API_URL}/users`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(userData)
|
|
});
|
|
if (!res.ok) throw new Error('Failed to create user');
|
|
return res.json();
|
|
},
|
|
|
|
updateUser: async (id: string, userData: any) => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const users = getLocal<User[]>(STORAGE_KEYS.USERS_LIST, []);
|
|
const updatedUsers = users.map(u => u.id === id ? { ...u, ...userData, id } : u);
|
|
setLocal(STORAGE_KEYS.USERS_LIST, updatedUsers);
|
|
return { success: true };
|
|
}
|
|
|
|
const res = await fetch(`${API_URL}/users/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(userData)
|
|
});
|
|
if (!res.ok) throw new Error('Failed to update user');
|
|
return res.json();
|
|
},
|
|
|
|
deleteUser: async (id: string) => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const users = getLocal<User[]>(STORAGE_KEYS.USERS_LIST, []);
|
|
setLocal(STORAGE_KEYS.USERS_LIST, users.filter(u => u.id !== id));
|
|
return;
|
|
}
|
|
|
|
const res = await fetch(`${API_URL}/users/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (!res.ok) throw new Error('Failed to delete user');
|
|
},
|
|
|
|
// --- ALERTS SERVICE ---
|
|
|
|
getAlerts: async (): Promise<AlertDefinition[]> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
return getLocal<AlertDefinition[]>(STORAGE_KEYS.ALERTS, []);
|
|
}
|
|
try {
|
|
const res = await fetch(`${API_URL}/alerts`, { headers: getAuthHeaders() });
|
|
if (!res.ok) throw new Error('API Error');
|
|
return res.json();
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
},
|
|
|
|
saveAlert: async (alert: AlertDefinition): Promise<AlertDefinition> => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const alerts = getLocal<AlertDefinition[]>(STORAGE_KEYS.ALERTS, []);
|
|
const existingIndex = alerts.findIndex(a => a.id === alert.id);
|
|
let newAlerts;
|
|
if (existingIndex >= 0) {
|
|
newAlerts = alerts.map(a => a.id === alert.id ? alert : a);
|
|
} else {
|
|
newAlerts = [...alerts, { ...alert, id: alert.id || crypto.randomUUID() }];
|
|
}
|
|
setLocal(STORAGE_KEYS.ALERTS, newAlerts);
|
|
return alert;
|
|
}
|
|
|
|
const method = alert.id ? 'PUT' : 'POST';
|
|
const url = alert.id ? `${API_URL}/alerts/${alert.id}` : `${API_URL}/alerts`;
|
|
|
|
const res = await fetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
body: JSON.stringify(alert)
|
|
});
|
|
if (!res.ok) throw new Error('Failed to save alert');
|
|
return res.json();
|
|
},
|
|
|
|
deleteAlert: async (id: string) => {
|
|
if (FORCE_LOCAL_DB) {
|
|
const alerts = getLocal<AlertDefinition[]>(STORAGE_KEYS.ALERTS, []);
|
|
setLocal(STORAGE_KEYS.ALERTS, alerts.filter(a => a.id !== id));
|
|
return;
|
|
}
|
|
const res = await fetch(`${API_URL}/alerts/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
if (!res.ok) throw new Error('Failed to delete alert');
|
|
},
|
|
|
|
seedPayments: () => {
|
|
if (!FORCE_LOCAL_DB) return; // Don't seed if connected to DB
|
|
|
|
const families = getLocal<Family[]>(STORAGE_KEYS.FAMILIES, []);
|
|
if (families.length === 0) {
|
|
// Seed only if completely empty
|
|
const demoFamilies: Family[] = [
|
|
{ id: 'f1', name: 'Rossi Mario', unitNumber: 'A1', contactEmail: 'rossi@email.com', balance: 0 },
|
|
{ id: 'f2', name: 'Bianchi Luigi', unitNumber: 'A2', contactEmail: 'bianchi@email.com', balance: 0 },
|
|
{ id: 'f3', name: 'Verdi Anna', unitNumber: 'B1', contactEmail: 'verdi@email.com', balance: 0 },
|
|
];
|
|
setLocal(STORAGE_KEYS.FAMILIES, demoFamilies);
|
|
|
|
const demoUsers: User[] = [
|
|
{ id: 'u1', email: 'admin@condo.it', name: 'Amministratore', role: 'admin', phone: '3331234567', familyId: null, receiveAlerts: true },
|
|
{ id: 'u2', email: 'rossi@email.com', name: 'Mario Rossi', role: 'user', phone: '', familyId: 'f1', receiveAlerts: true }
|
|
];
|
|
setLocal(STORAGE_KEYS.USERS_LIST, demoUsers);
|
|
}
|
|
}
|
|
}; |