feat: full appointment feature (CRUD + Appointments tab)
Builds the complete appointment feature end-to-end, mirroring the medication feature structure. The model existed but was orphaned dead code (no handlers, routes, repo, or frontend) — this builds the whole stack. Backend: - models/appointment.rs: AppointmentData (deserializes the data blob, camelCase keys), AppointmentResponse (flat snake_case + From<Appointment>), Create/Update request structs, AppointmentRepository (create, find_by_user_filtered with optional status filter, find/update/delete by appointment_id). Mirrors the medication serialization pattern. Module added to models/mod.rs. - handlers/appointments.rs: full CRUD (create/list/get/update/delete) returning AppointmentResponse. Routes wired in app.rs (POST/GET /api/appointments, GET/POST /:id, POST /:id/delete). - 3 unit tests (AppointmentData deser, default status, response flatten). Frontend: - Appointment/Create/Update types; getAppointments/getAppointment/create/ update/deleteAppointment in api.ts; useAppointmentStore. - AppointmentsManager component (list with type+status chips, create/edit dialogs, delete confirm, empty state) as a 5th Dashboard tab. Verified: backend fmt/clippy 0 warnings, 26 tests pass; frontend build clean, 20 tests pass.
This commit is contained in:
parent
0921d73a6d
commit
4c4c29f72e
10 changed files with 1019 additions and 4 deletions
|
|
@ -7,6 +7,9 @@ import {
|
|||
DrugInteraction,
|
||||
AdherenceStats,
|
||||
Profile,
|
||||
Appointment,
|
||||
CreateAppointmentRequest,
|
||||
UpdateAppointmentRequest,
|
||||
} from '../types/api';
|
||||
import apiService from '../services/api';
|
||||
|
||||
|
|
@ -80,6 +83,17 @@ interface ProfileState {
|
|||
clearError: () => void;
|
||||
}
|
||||
|
||||
interface AppointmentState {
|
||||
appointments: Appointment[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
loadAppointments: (status?: string) => Promise<void>;
|
||||
createAppointment: (data: CreateAppointmentRequest) => Promise<void>;
|
||||
updateAppointment: (id: string, data: UpdateAppointmentRequest) => Promise<void>;
|
||||
deleteAppointment: (id: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
// Auth Store
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
devtools(
|
||||
|
|
@ -466,3 +480,70 @@ export const useProfileStore = create<ProfileState>()(
|
|||
clearError: () => set({ error: null }),
|
||||
})),
|
||||
);
|
||||
|
||||
// Appointment Store
|
||||
export const useAppointmentStore = create<AppointmentState>()(
|
||||
devtools((set) => ({
|
||||
appointments: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
loadAppointments: async (status) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const appointments = await apiService.getAppointments(status);
|
||||
set({ appointments, isLoading: false });
|
||||
} catch (error: any) {
|
||||
set({
|
||||
error: error.message || 'Failed to load appointments',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
createAppointment: async (data) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const appt = await apiService.createAppointment(data);
|
||||
set((state) => ({
|
||||
appointments: [...state.appointments, appt],
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({
|
||||
error: error.message || 'Failed to create appointment',
|
||||
isLoading: false,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateAppointment: async (id, data) => {
|
||||
try {
|
||||
const updated = await apiService.updateAppointment(id, data);
|
||||
set((state) => ({
|
||||
appointments: state.appointments.map((a) =>
|
||||
a.appointment_id === id ? updated : a,
|
||||
),
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to update appointment' });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteAppointment: async (id) => {
|
||||
try {
|
||||
await apiService.deleteAppointment(id);
|
||||
set((state) => ({
|
||||
appointments: state.appointments.filter((a) => a.appointment_id !== id),
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to delete appointment' });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
})),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue