Introduce a new feature to test SMTP configuration directly from the settings page. This involves adding a new API endpoint and corresponding UI elements to trigger and display the results of an SMTP test. Additionally, this commit refactors the Docker setup by consolidating Dockerfiles and removing unnecessary configuration files. The goal is to streamline the build process and reduce image size and complexity.
351 lines
11 KiB
TypeScript
351 lines
11 KiB
TypeScript
|
|
import { Family, Payment, AppSettings, User, AlertDefinition, Condo, Notice, NoticeRead, Ticket, TicketAttachment, TicketComment, SmtpConfig } from '../types';
|
|
|
|
// --- CONFIGURATION TOGGLE ---
|
|
const FORCE_LOCAL_DB = false;
|
|
const API_URL = '/api';
|
|
|
|
const STORAGE_KEYS = {
|
|
TOKEN: 'condo_auth_token',
|
|
USER: 'condo_user_info',
|
|
ACTIVE_CONDO_ID: 'condo_active_id',
|
|
};
|
|
|
|
const getAuthHeaders = () => {
|
|
const token = localStorage.getItem(STORAGE_KEYS.TOKEN);
|
|
return token ? { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
|
|
};
|
|
|
|
const request = async <T>(endpoint: string, options: RequestInit = {}): Promise<T> => {
|
|
const response = await fetch(`${API_URL}${endpoint}`, {
|
|
...options,
|
|
headers: {
|
|
...getAuthHeaders(),
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
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();
|
|
};
|
|
|
|
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', {
|
|
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;
|
|
},
|
|
|
|
logout: () => {
|
|
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
window.location.href = '#/login';
|
|
},
|
|
|
|
getCurrentUser: (): User | null => {
|
|
const u = localStorage.getItem(STORAGE_KEYS.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)
|
|
});
|
|
},
|
|
|
|
// --- SETTINGS (Global) ---
|
|
|
|
getSettings: async (): Promise<AppSettings> => {
|
|
return request<AppSettings>('/settings');
|
|
},
|
|
|
|
updateSettings: async (settings: AppSettings): Promise<void> => {
|
|
await request('/settings', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(settings)
|
|
});
|
|
},
|
|
|
|
testSmtpConfig: async (config: SmtpConfig): Promise<void> => {
|
|
await request('/settings/smtp-test', {
|
|
method: 'POST',
|
|
body: JSON.stringify(config)
|
|
});
|
|
},
|
|
|
|
getAvailableYears: async (): Promise<number[]> => {
|
|
return request<number[]>('/years');
|
|
},
|
|
|
|
// --- FAMILIES ---
|
|
|
|
getFamilies: async (condoId?: string): Promise<Family[]> => {
|
|
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 })
|
|
});
|
|
},
|
|
|
|
updateFamily: async (family: Family): Promise<Family> => {
|
|
return request<Family>(`/families/${family.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(family)
|
|
});
|
|
},
|
|
|
|
deleteFamily: async (familyId: string): Promise<void> => {
|
|
await request(`/families/${familyId}`, { method: 'DELETE' });
|
|
},
|
|
|
|
// --- PAYMENTS ---
|
|
|
|
getPaymentsByFamily: async (familyId: string): Promise<Payment[]> => {
|
|
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)
|
|
});
|
|
},
|
|
|
|
deleteUser: async (id: string) => {
|
|
await 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}`;
|
|
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)
|
|
});
|
|
}
|
|
},
|
|
|
|
deleteAlert: async (id: string) => {
|
|
await 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);
|
|
},
|
|
|
|
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 })
|
|
});
|
|
},
|
|
|
|
updateTicket: async (id: string, data: { status: string, priority: string }) => {
|
|
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}`);
|
|
},
|
|
|
|
getTicketComments: async (ticketId: string): Promise<TicketComment[]> => {
|
|
return request<TicketComment[]>(`/tickets/${ticketId}/comments`);
|
|
},
|
|
|
|
addTicketComment: async (ticketId: string, text: string): Promise<void> => {
|
|
await request(`/tickets/${ticketId}/comments`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ text })
|
|
});
|
|
},
|
|
|
|
// --- SEEDING ---
|
|
seedPayments: () => {
|
|
// No-op in remote mode
|
|
}
|
|
};
|