Adds Dockerfile for frontend and backend, along with Nginx configuration and .dockerignore files. This enables containerized deployment and proper handling of static assets and API proxying. Updates mockDb.ts to correctly type ticket attachments.
329 lines
9.9 KiB
TypeScript
329 lines
9.9 KiB
TypeScript
|
|
import { Family, Payment, AppSettings, User, AlertDefinition, Condo, Notice, NoticeRead, Ticket, TicketAttachment } 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)
|
|
});
|
|
},
|
|
|
|
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}`);
|
|
},
|
|
|
|
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}`);
|
|
},
|
|
|
|
// --- SEEDING ---
|
|
seedPayments: () => {
|
|
// No-op in remote mode
|
|
}
|
|
};
|