feat: Refactor API services and UI components

This commit refactors the API service to use a consistent `fetch` wrapper for all requests, improving error handling and authorization logic. It also updates UI components to reflect changes in API endpoints and data structures, particularly around notifications and extraordinary expenses. Docker configurations are removed as they are no longer relevant for this stage of development.
This commit is contained in:
2025-12-09 23:12:47 +01:00
parent 38a3402deb
commit 2a6da489aa
9 changed files with 449 additions and 920 deletions

View File

@@ -1,335 +1,287 @@
import {
Condo, Family, Payment, AppSettings, User, AuthResponse,
Ticket, TicketComment, ExtraordinaryExpense, Notice,
AlertDefinition, NoticeRead
} from '../types';
import { Family, Payment, AppSettings, User, AlertDefinition, Condo, Notice, NoticeRead, Ticket, TicketAttachment, TicketComment, SmtpConfig, ExtraordinaryExpense } from '../types';
const API_URL = '/api';
// --- CONFIGURATION TOGGLE ---
const FORCE_LOCAL_DB = false;
const API_URL = '/api';
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = localStorage.getItem('condo_token');
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers as any,
};
const STORAGE_KEYS = {
TOKEN: 'condo_auth_token',
USER: 'condo_user_info',
ACTIVE_CONDO_ID: 'condo_active_id',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const getAuthHeaders = () => {
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
return token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
};
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers,
});
const request = async <T>(endpoint: string, options: RequestInit = {}): Promise<T> => {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
...getAuthHeaders(),
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || response.statusText);
}
if (response.status === 401) {
CondoService.logout();
throw new Error("Unauthorized");
}
if (!response.ok) {
const errText = await response.text();
throw new Error(errText || `API Error: ${response.status}`);
}
return response.json();
};
// Handle empty responses
const text = await response.text();
return text ? JSON.parse(text) : undefined;
}
export const CondoService = {
// --- CONDO CONTEXT MANAGEMENT ---
getActiveCondoId: (): string | null => {
return localStorage.getItem(STORAGE_KEYS.ACTIVE_CONDO_ID);
},
setActiveCondo: (condoId: string) => {
localStorage.setItem(STORAGE_KEYS.ACTIVE_CONDO_ID, condoId);
window.location.reload();
},
getCondos: async (): Promise<Condo[]> => {
return request<Condo[]>('/condos');
},
getActiveCondo: async (): Promise<Condo | undefined> => {
const condos = await CondoService.getCondos();
const activeId = CondoService.getActiveCondoId();
if (!activeId && condos.length > 0) {
// Do not reload here, just set it silently or let the UI handle it
localStorage.setItem(STORAGE_KEYS.ACTIVE_CONDO_ID, condos[0].id);
return condos[0];
}
return condos.find(c => c.id === activeId);
},
saveCondo: async (condo: Condo): Promise<Condo> => {
// If no ID, it's a creation
if (!condo.id || condo.id.length < 5) { // Simple check if it's a new ID request
return request<Condo>('/condos', {
method: 'POST',
body: JSON.stringify(condo)
});
} else {
return request<Condo>(`/condos/${condo.id}`, {
method: 'PUT',
body: JSON.stringify(condo)
});
}
},
deleteCondo: async (id: string) => {
await request(`/condos/${id}`, { method: 'DELETE' });
if (localStorage.getItem(STORAGE_KEYS.ACTIVE_CONDO_ID) === id) {
localStorage.removeItem(STORAGE_KEYS.ACTIVE_CONDO_ID);
}
},
// --- NOTICES (BACHECA) ---
getNotices: async (condoId?: string): Promise<Notice[]> => {
let url = '/notices';
const activeId = condoId || CondoService.getActiveCondoId();
if (activeId) url += `?condoId=${activeId}`;
return request<Notice[]>(url);
},
saveNotice: async (notice: Notice): Promise<Notice> => {
if (!notice.id) {
return request<Notice>('/notices', {
method: 'POST',
body: JSON.stringify(notice)
});
} else {
return request<Notice>(`/notices/${notice.id}`, {
method: 'PUT',
body: JSON.stringify(notice)
});
}
},
deleteNotice: async (id: string) => {
await request(`/notices/${id}`, { method: 'DELETE' });
},
markNoticeAsRead: async (noticeId: string, userId: string) => {
await request(`/notices/${noticeId}/read`, {
method: 'POST',
body: JSON.stringify({ userId })
});
},
getNoticeReadStatus: async (noticeId: string): Promise<NoticeRead[]> => {
return request<NoticeRead[]>(`/notices/${noticeId}/reads`);
},
getUnreadNoticesForUser: async (userId: string, condoId: string): Promise<Notice[]> => {
return request<Notice[]>(`/notices/unread?userId=${userId}&condoId=${condoId}`);
},
// --- AUTH ---
login: async (email, password) => {
const data = await request<{token: string, user: User}>('/auth/login', {
// Auth & User
login: async (email: string, password: string): Promise<void> => {
const data = await request<AuthResponse>('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
localStorage.setItem(STORAGE_KEYS.TOKEN, data.token);
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(data.user));
// Set active condo if user belongs to a family
if (data.user.familyId) {
try {
const families = await CondoService.getFamilies(); // This will filter by user perms automatically on server
const fam = families.find(f => f.id === data.user.familyId);
if (fam) {
localStorage.setItem(STORAGE_KEYS.ACTIVE_CONDO_ID, fam.condoId);
}
} catch (e) { console.error("Could not set active condo on login", e); }
}
return data;
localStorage.setItem('condo_token', data.token);
localStorage.setItem('condo_user', JSON.stringify(data.user));
},
logout: () => {
localStorage.removeItem(STORAGE_KEYS.TOKEN);
localStorage.removeItem(STORAGE_KEYS.USER);
window.location.href = '#/login';
localStorage.removeItem('condo_token');
localStorage.removeItem('condo_user');
window.location.href = '/#/login';
},
getCurrentUser: (): User | null => {
const u = localStorage.getItem(STORAGE_KEYS.USER);
return u ? JSON.parse(u) : null;
const u = localStorage.getItem('condo_user');
return u ? JSON.parse(u) : null;
},
updateProfile: async (data: Partial<User> & { password?: string }) => {
return request<{success: true, user: User}>('/profile', {
method: 'PUT',
body: JSON.stringify(data)
});
updateProfile: async (data: any): Promise<void> => {
const res = await request<{success: boolean, user: User}>('/profile', {
method: 'PUT',
body: JSON.stringify(data)
});
if (res.user) {
localStorage.setItem('condo_user', JSON.stringify(res.user));
}
},
// --- SETTINGS (Global) ---
// Settings
getSettings: async (): Promise<AppSettings> => {
return request<AppSettings>('/settings');
return request<AppSettings>('/settings');
},
updateSettings: async (settings: AppSettings): Promise<void> => {
await request('/settings', {
method: 'PUT',
body: JSON.stringify(settings)
});
return request('/settings', {
method: 'PUT',
body: JSON.stringify(settings)
});
},
testSmtpConfig: async (config: SmtpConfig): Promise<void> => {
await request('/settings/smtp-test', {
testSmtpConfig: async (config: any): Promise<void> => {
return request('/settings/smtp-test', {
method: 'POST',
body: JSON.stringify(config)
});
},
getAvailableYears: async (): Promise<number[]> => {
return request<number[]>('/years');
return request<number[]>('/years');
},
// --- FAMILIES ---
// Condos
getCondos: async (): Promise<Condo[]> => {
return request<Condo[]>('/condos');
},
getActiveCondoId: (): string | undefined => {
return localStorage.getItem('active_condo_id') || undefined;
},
getActiveCondo: async (): Promise<Condo | undefined> => {
const id = localStorage.getItem('active_condo_id');
const condos = await CondoService.getCondos();
if (id) {
return condos.find(c => c.id === id);
}
return condos.length > 0 ? condos[0] : undefined;
},
setActiveCondo: (id: string) => {
localStorage.setItem('active_condo_id', id);
window.dispatchEvent(new Event('condo-updated'));
window.location.reload();
},
saveCondo: async (condo: Condo): Promise<Condo> => {
if (condo.id) {
await request(`/condos/${condo.id}`, {
method: 'PUT',
body: JSON.stringify(condo)
});
return condo;
} else {
return request<Condo>('/condos', {
method: 'POST',
body: JSON.stringify(condo)
});
}
},
deleteCondo: async (id: string): Promise<void> => {
return request(`/condos/${id}`, { method: 'DELETE' });
},
// Families
getFamilies: async (condoId?: string): Promise<Family[]> => {
let url = '/families';
const activeId = condoId || CondoService.getActiveCondoId();
if (activeId) url += `?condoId=${activeId}`;
return request<Family[]>(url);
let url = '/families';
const activeId = condoId || CondoService.getActiveCondoId();
if (activeId) url += `?condoId=${activeId}`;
return request<Family[]>(url);
},
addFamily: async (familyData: Omit<Family, 'id' | 'balance' | 'condoId'>): Promise<Family> => {
const activeCondoId = CondoService.getActiveCondoId();
if (!activeCondoId) throw new Error("Nessun condominio selezionato");
return request<Family>('/families', {
method: 'POST',
body: JSON.stringify({ ...familyData, condoId: activeCondoId })
});
addFamily: async (family: any): Promise<Family> => {
const activeId = CondoService.getActiveCondoId();
return request<Family>('/families', {
method: 'POST',
body: JSON.stringify({ ...family, condoId: activeId })
});
},
updateFamily: async (family: Family): Promise<Family> => {
return request<Family>(`/families/${family.id}`, {
method: 'PUT',
body: JSON.stringify(family)
});
updateFamily: async (family: Family): Promise<void> => {
return request(`/families/${family.id}`, {
method: 'PUT',
body: JSON.stringify(family)
});
},
deleteFamily: async (familyId: string): Promise<void> => {
await request(`/families/${familyId}`, { method: 'DELETE' });
deleteFamily: async (id: string): Promise<void> => {
return request(`/families/${id}`, { method: 'DELETE' });
},
// --- PAYMENTS ---
// Payments
seedPayments: () => { /* No-op for real backend */ },
getPaymentsByFamily: async (familyId: string): Promise<Payment[]> => {
return request<Payment[]>(`/payments?familyId=${familyId}`);
return request<Payment[]>(`/payments?familyId=${familyId}`);
},
getCondoPayments: async (condoId: string): Promise<Payment[]> => {
return request<Payment[]>(`/payments?condoId=${condoId}`);
},
addPayment: async (payment: Omit<Payment, 'id'>): Promise<Payment> => {
return request<Payment>('/payments', {
method: 'POST',
body: JSON.stringify(payment)
});
},
// --- USERS ---
getUsers: async (condoId?: string): Promise<User[]> => {
let url = '/users';
const activeId = condoId || CondoService.getActiveCondoId();
if (activeId) url += `?condoId=${activeId}`;
return request<User[]>(url);
},
createUser: async (userData: any) => {
return request('/users', {
method: 'POST',
body: JSON.stringify(userData)
});
},
updateUser: async (id: string, userData: any) => {
return request(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData)
addPayment: async (payment: any): Promise<Payment> => {
return request<Payment>('/payments', {
method: 'POST',
body: JSON.stringify(payment)
});
},
deleteUser: async (id: string) => {
await request(`/users/${id}`, { method: 'DELETE' });
// Users
getUsers: async (condoId?: string): Promise<User[]> => {
let url = '/users';
if (condoId) url += `?condoId=${condoId}`;
return request<User[]>(url);
},
// --- ALERTS ---
createUser: async (user: any): Promise<void> => {
return request('/users', {
method: 'POST',
body: JSON.stringify(user)
});
},
updateUser: async (id: string, user: any): Promise<void> => {
return request(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(user)
});
},
deleteUser: async (id: string): Promise<void> => {
return request(`/users/${id}`, { method: 'DELETE' });
},
// Alerts
getAlerts: async (condoId?: string): Promise<AlertDefinition[]> => {
let url = '/alerts';
const activeId = condoId || CondoService.getActiveCondoId();
if (activeId) url += `?condoId=${activeId}`;
if (condoId) url += `?condoId=${condoId}`;
return request<AlertDefinition[]>(url);
},
saveAlert: async (alert: AlertDefinition & { condoId?: string }): Promise<AlertDefinition> => {
const activeCondoId = CondoService.getActiveCondoId();
if (!alert.id) {
return request<AlertDefinition>('/alerts', {
method: 'POST',
body: JSON.stringify({ ...alert, condoId: activeCondoId })
});
} else {
return request<AlertDefinition>(`/alerts/${alert.id}`, {
method: 'PUT',
body: JSON.stringify(alert)
});
}
saveAlert: async (alert: AlertDefinition): Promise<AlertDefinition> => {
const activeId = CondoService.getActiveCondoId();
if (alert.id) {
await request(`/alerts/${alert.id}`, { method: 'PUT', body: JSON.stringify(alert) });
return alert;
} else {
return request('/alerts', {
method: 'POST',
body: JSON.stringify({ ...alert, condoId: activeId })
});
}
},
deleteAlert: async (id: string) => {
await request(`/alerts/${id}`, { method: 'DELETE' });
deleteAlert: async (id: string): Promise<void> => {
return request(`/alerts/${id}`, { method: 'DELETE' });
},
// --- TICKETS ---
getTickets: async (condoId?: string): Promise<Ticket[]> => {
let url = '/tickets';
const activeId = condoId || CondoService.getActiveCondoId();
if (activeId) url += `?condoId=${activeId}`;
return request<Ticket[]>(url);
// Notices
getNotices: async (condoId?: string): Promise<Notice[]> => {
let url = '/notices';
const activeId = condoId || CondoService.getActiveCondoId();
if (activeId) url += `?condoId=${activeId}`;
return request<Notice[]>(url);
},
createTicket: async (data: Omit<Partial<Ticket>, 'attachments'> & { attachments?: { fileName: string, fileType: string, data: string }[] }) => {
const activeId = CondoService.getActiveCondoId();
if(!activeId) throw new Error("No active condo");
return request('/tickets', {
method: 'POST',
body: JSON.stringify({ ...data, condoId: activeId })
});
getUnreadNoticesForUser: async (userId: string, condoId: string): Promise<Notice[]> => {
return request<Notice[]>(`/notices/unread?userId=${userId}&condoId=${condoId}`);
},
updateTicket: async (id: string, data: { status: string, priority: string }) => {
getNoticeReadStatus: async (noticeId: string): Promise<NoticeRead[]> => {
return request<NoticeRead[]>(`/notices/${noticeId}/read-status`);
},
markNoticeAsRead: async (noticeId: string, userId: string): Promise<void> => {
return request(`/notices/${noticeId}/read`, {
method: 'POST',
body: JSON.stringify({ userId })
});
},
saveNotice: async (notice: Notice): Promise<void> => {
if (notice.id) {
return request(`/notices/${notice.id}`, { method: 'PUT', body: JSON.stringify(notice) });
} else {
return request('/notices', { method: 'POST', body: JSON.stringify(notice) });
}
},
deleteNotice: async (id: string): Promise<void> => {
return request(`/notices/${id}`, { method: 'DELETE' });
},
// Tickets
getTickets: async (): Promise<Ticket[]> => {
const activeId = CondoService.getActiveCondoId();
return request<Ticket[]>(`/tickets?condoId=${activeId}`);
},
createTicket: async (data: any): Promise<void> => {
const activeId = CondoService.getActiveCondoId();
return request('/tickets', {
method: 'POST',
body: JSON.stringify({ ...data, condoId: activeId })
});
},
updateTicket: async (id: string, data: any): Promise<void> => {
return request(`/tickets/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
},
deleteTicket: async (id: string) => {
await request(`/tickets/${id}`, { method: 'DELETE' });
},
getTicketAttachment: async (ticketId: string, attachmentId: string): Promise<TicketAttachment> => {
return request<TicketAttachment>(`/tickets/${ticketId}/attachments/${attachmentId}`);
deleteTicket: async (id: string): Promise<void> => {
return request(`/tickets/${id}`, { method: 'DELETE' });
},
getTicketComments: async (ticketId: string): Promise<TicketComment[]> => {
@@ -337,14 +289,17 @@ export const CondoService = {
},
addTicketComment: async (ticketId: string, text: string): Promise<void> => {
await request(`/tickets/${ticketId}/comments`, {
return request(`/tickets/${ticketId}/comments`, {
method: 'POST',
body: JSON.stringify({ text })
});
},
// --- EXTRAORDINARY EXPENSES ---
getTicketAttachment: async (ticketId: string, attachmentId: string): Promise<any> => {
return request(`/tickets/${ticketId}/attachments/${attachmentId}`);
},
// Extraordinary Expenses
getExpenses: async (condoId?: string): Promise<ExtraordinaryExpense[]> => {
let url = '/expenses';
const activeId = condoId || CondoService.getActiveCondoId();
@@ -365,6 +320,13 @@ export const CondoService = {
});
},
updateExpense: async (id: string, data: any): Promise<void> => {
return request(`/expenses/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
},
getExpenseAttachment: async (expenseId: string, attachmentId: string): Promise<any> => {
return request(`/expenses/${expenseId}/attachments/${attachmentId}`);
},
@@ -379,10 +341,5 @@ export const CondoService = {
method: 'POST',
body: JSON.stringify({ amount, notes: 'PayPal Payment' })
});
},
// --- SEEDING ---
seedPayments: () => {
// No-op in remote mode
}
};
};