diff --git a/README.md b/README.md index 7d21ed3..a02e13b 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The project follows a modular architecture with separate backend services and fr ### Backend Setup 1. Clone the repository: ``` - git clone https://github.com/yourusername/afyaquik.git + git clone https://github.com/brianbrix/afyaquik.git cd afyaquik ``` diff --git a/afyaquik-frontend/admin/src/App.tsx b/afyaquik-frontend/admin/src/App.tsx index b2fe09d..70aa267 100644 --- a/afyaquik-frontend/admin/src/App.tsx +++ b/afyaquik-frontend/admin/src/App.tsx @@ -49,6 +49,8 @@ import BillingItemEdit from "./billing/items/BillingItemEdit"; import CurrencyList from "./billing/currencies/CurrencyList"; import CurrencyCreate from "./billing/currencies/CurrencyCreate"; import CurrencyEdit from "./billing/currencies/CurrencyEdit"; +import PasswordResetRequestList from "./users/PasswordResetRequestList"; +import PasswordResetRequestAction from "./users/PasswordResetRequestAction"; const App = () => ( @@ -68,6 +70,7 @@ const App = () => ( + ); diff --git a/afyaquik-frontend/admin/src/stations/StationCreate.tsx b/afyaquik-frontend/admin/src/stations/StationCreate.tsx index 725543f..8602ff3 100644 --- a/afyaquik-frontend/admin/src/stations/StationCreate.tsx +++ b/afyaquik-frontend/admin/src/stations/StationCreate.tsx @@ -1,9 +1,12 @@ -import { Create, SimpleForm, TextInput } from 'react-admin'; +import {CheckboxGroupInput, Create, ReferenceArrayInput, SimpleForm, TextInput} from 'react-admin'; const StationCreate = () => ( + + + ); diff --git a/afyaquik-frontend/admin/src/stations/StationEdit.tsx b/afyaquik-frontend/admin/src/stations/StationEdit.tsx index 102475b..b8a525d 100644 --- a/afyaquik-frontend/admin/src/stations/StationEdit.tsx +++ b/afyaquik-frontend/admin/src/stations/StationEdit.tsx @@ -1,10 +1,13 @@ -import { Edit, SimpleForm, TextInput } from 'react-admin'; +import {CheckboxGroupInput, Edit, ReferenceArrayInput, SimpleForm, TextInput} from 'react-admin'; const StationEdit = () => ( + + + ); diff --git a/afyaquik-frontend/admin/src/users/PasswordResetRequestAction.tsx b/afyaquik-frontend/admin/src/users/PasswordResetRequestAction.tsx new file mode 100644 index 0000000..78b57c8 --- /dev/null +++ b/afyaquik-frontend/admin/src/users/PasswordResetRequestAction.tsx @@ -0,0 +1,21 @@ +import { + Edit, + SimpleForm, + TextInput, + PasswordInput +} from 'react-admin'; +import {confirmPasswordValidator, passwordValidator} from "../utils"; + + + +const PasswordResetRequestAction = () => ( + + + + + + + + +); +export default PasswordResetRequestAction; diff --git a/afyaquik-frontend/admin/src/users/PasswordResetRequestList.tsx b/afyaquik-frontend/admin/src/users/PasswordResetRequestList.tsx new file mode 100644 index 0000000..65758d7 --- /dev/null +++ b/afyaquik-frontend/admin/src/users/PasswordResetRequestList.tsx @@ -0,0 +1,17 @@ +import {List, Datagrid, TextField, EmailField, EditButton, BooleanField, TextInput, SelectInput} from 'react-admin'; + +const UserFilters = [ + , +]; +const PasswordResetRequestList = () => ( + + + + + + + + +); + +export default PasswordResetRequestList; diff --git a/afyaquik-frontend/admin/src/users/UserCreate.tsx b/afyaquik-frontend/admin/src/users/UserCreate.tsx index 700623b..5dee2e0 100644 --- a/afyaquik-frontend/admin/src/users/UserCreate.tsx +++ b/afyaquik-frontend/admin/src/users/UserCreate.tsx @@ -1,20 +1,37 @@ -import { Create, SimpleForm, TextInput, ReferenceArrayInput, CheckboxGroupInput,BooleanInput } from 'react-admin'; - -const UserCreate = () => ( - - - - - - - - - - - - - - -); +import { + Create, + SimpleForm, + TextInput, + ReferenceArrayInput, + CheckboxGroupInput, + BooleanInput, + PasswordInput +} from 'react-admin'; +import {generatePassword, passwordValidator} from "../utils"; +import {useState} from "react"; +const UserCreate = () => { + const [generatedPassword, setGeneratedPassword] = useState(""); + return ( + + + + + + + + + + +
+ + +
+ +
+
+ ); +}; export default UserCreate; diff --git a/afyaquik-frontend/admin/src/users/UserEdit.tsx b/afyaquik-frontend/admin/src/users/UserEdit.tsx index df1fb9b..4dd42f7 100644 --- a/afyaquik-frontend/admin/src/users/UserEdit.tsx +++ b/afyaquik-frontend/admin/src/users/UserEdit.tsx @@ -1,6 +1,18 @@ -import { Edit, SimpleForm, TextInput, ReferenceArrayInput, CheckboxGroupInput,BooleanInput } from 'react-admin'; +import { + Edit, + SimpleForm, + TextInput, + ReferenceArrayInput, + CheckboxGroupInput, + BooleanInput, + PasswordInput +} from 'react-admin'; +import {generatePassword, passwordValidator} from "../utils"; +import {useState} from "react"; -const UserEdit = () => ( +const UserEdit = () => { + const [generatedPassword, setGeneratedPassword] = useState(""); + return ( @@ -9,6 +21,12 @@ const UserEdit = () => ( +
+ + +
@@ -19,6 +37,6 @@ const UserEdit = () => (
-); +)}; export default UserEdit; diff --git a/afyaquik-frontend/admin/src/utils.ts b/afyaquik-frontend/admin/src/utils.ts new file mode 100644 index 0000000..a26c9f8 --- /dev/null +++ b/afyaquik-frontend/admin/src/utils.ts @@ -0,0 +1,25 @@ +export const generatePassword = () => { + const length = 12; + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"; + let password = ""; + for (let i = 0, n = charset.length; i < length; ++i) { + password += charset.charAt(Math.floor(Math.random() * n)); + } + return password; +}; + +export const passwordValidator = (value: string) => { + if (!value) return 'Password is required'; + if (value.length < 8) return 'Password must be at least 8 characters'; + if (!/[A-Z]/.test(value)) return 'Password must contain an uppercase letter'; + if (!/[a-z]/.test(value)) return 'Password must contain a lowercase letter'; + if (!/[0-9]/.test(value)) return 'Password must contain a number'; + if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) return 'Password must contain a special character'; + return undefined; +}; + +export const confirmPasswordValidator = (value: string, allValues: any) => { + if (!value) return 'Confirm password is required'; + if (value !== allValues.password) return 'Passwords do not match'; + return undefined; +}; diff --git a/afyaquik-frontend/auth/src/App.tsx b/afyaquik-frontend/auth/src/App.tsx index af09e12..54566e2 100644 --- a/afyaquik-frontend/auth/src/App.tsx +++ b/afyaquik-frontend/auth/src/App.tsx @@ -1,9 +1,10 @@ -import {Routes, Route, HashRouter} from 'react-router-dom'; +import {Routes, Route} from 'react-router-dom'; import LoginPage from './pages/LoginPage'; -import { AuthProvider } from './components/AuthProvider'; -import {AuthGuard, Header, ToastProvider} from "@afyaquik/shared"; +import {Header, ToastProvider} from "@afyaquik/shared"; import HomePage from "./pages/HomePage"; +import ProfilePage from "./pages/ProfilePage"; import React from "react"; +import ForgotPasswordPage from "./pages/ForgotPasswordPage"; export default function App() { return ( @@ -13,6 +14,8 @@ export default function App() { } /> } /> } /> + } /> + } /> diff --git a/afyaquik-frontend/auth/src/pages/ForgotPasswordPage.tsx b/afyaquik-frontend/auth/src/pages/ForgotPasswordPage.tsx new file mode 100644 index 0000000..272fcfd --- /dev/null +++ b/afyaquik-frontend/auth/src/pages/ForgotPasswordPage.tsx @@ -0,0 +1,51 @@ +import { apiRequest } from "@afyaquik/shared"; +import React, { useState } from "react"; + +const ForgotPasswordPage = () => { + const [username, setUsername] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + setSuccess(false); + try { + await apiRequest("/password-reset/request", { + method: "POST", + body: { username }, + }); + setSuccess(true); + } catch { + setError("Failed to request password reset. Please check your username."); + } + setLoading(false); + }; + + return ( +
+

Reset Password

+
+
+ + setUsername(e.target.value)} + required + /> +
+ {error &&

{error}

} + {success &&

Check your email for reset link!

} + +
+
+ ); +}; + +export default ForgotPasswordPage; diff --git a/afyaquik-frontend/auth/src/pages/LoginPage.tsx b/afyaquik-frontend/auth/src/pages/LoginPage.tsx index cd446e6..d89d761 100644 --- a/afyaquik-frontend/auth/src/pages/LoginPage.tsx +++ b/afyaquik-frontend/auth/src/pages/LoginPage.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { authService } from '../utils/authService'; -import {sendNotification} from "@afyaquik/shared"; +import {apiRequest, sendNotification} from "@afyaquik/shared"; export default function LoginPage() { const [username, setUsername] = useState(''); @@ -53,6 +53,13 @@ export default function LoginPage() { setError('Please select a role to continue.'); return; } + apiRequest(`/roles/byName/${selectedRole}`,{method:'GET'}) + .then(response=> + { + localStorage.setItem('allowedStations', response.stations) + localStorage.setItem('formattedStations', response.stations.join('||')) + } + ) localStorage.setItem('currentRole', selectedRole); @@ -127,6 +134,15 @@ export default function LoginPage() { )} + ); } diff --git a/afyaquik-frontend/auth/src/pages/ProfilePage.tsx b/afyaquik-frontend/auth/src/pages/ProfilePage.tsx new file mode 100644 index 0000000..eef4191 --- /dev/null +++ b/afyaquik-frontend/auth/src/pages/ProfilePage.tsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from "react"; +import { apiRequest, StepForm, StepConfig, useToast } from "@afyaquik/shared"; +import { Button } from "react-bootstrap"; + +const ProfilePage = () => { + const [profile, setProfile] = useState({}); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + const { showToast } = useToast(); + + useEffect(() => { + apiRequest("/users/me", { method: "GET" }) + .then((data) => setProfile(data)) + .finally(() => setLoading(false)); + }, []); + + const formConfig: StepConfig[] = [ + { + label: "Edit Profile", + fields: [ + { name: "username", label: "Username", type: "text", disabled: true }, + { name: "firstName", label: "First Name", type: "text", required: true }, + { name: "lastName", label: "Last Name", type: "text", required: true }, + { name: "email", label: "Email", type: "email", required: true }, + { name: "phone", label: "Phone", type: "text" }, + ], + }, + ]; + + if (loading) return
Loading...
; + + return ( +
+ {!editing ? ( +
+

Profile

+

Username: {profile.username}

+

First Name: {profile.firstName}

+

Last Name: {profile.lastName}

+

Email: {profile.email}

+

Phone: {profile.phone}

+ +
+ ) : ( + { + apiRequest(`/users/${profile.id}`, { method: "PUT", body: data }) + .then((updated) => { + setProfile(updated); + setEditing(false); + showToast("Profile updated successfully.", "success"); + }) + .catch(() => showToast("Failed to update profile.", "error")); + }} + submitButtonLabel="Save Changes" + /> + )} +
+ ); +}; + +export default ProfilePage; + diff --git a/afyaquik-frontend/doctor/src/App.tsx b/afyaquik-frontend/doctor/src/App.tsx index cbcdfac..8049436 100644 --- a/afyaquik-frontend/doctor/src/App.tsx +++ b/afyaquik-frontend/doctor/src/App.tsx @@ -9,6 +9,7 @@ import DoctorAppointmentDetailsPage from "./patient/DoctorAppointmentDetailsPage import DoctorAppointmentList from "./patient/DoctorAppointmentsList"; import DoctorTreatmentPlanAddPage from "./patient/DoctorTreatmentPlanAddPage"; import DoctorVisitAssign from "./patient/DoctorVisitAssign"; +import DoctorAppointmentEditPage from "./patient/DoctorAppointmentEditPage"; function App() { return ( @@ -23,6 +24,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/afyaquik-frontend/doctor/src/patient/DoctorAppointmentEditPage.tsx b/afyaquik-frontend/doctor/src/patient/DoctorAppointmentEditPage.tsx new file mode 100644 index 0000000..a60e09a --- /dev/null +++ b/afyaquik-frontend/doctor/src/patient/DoctorAppointmentEditPage.tsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import {StepForm, apiRequest, useToast, StepConfig} from "@afyaquik/shared"; +import { Button } from "react-bootstrap"; + +const DoctorAppointmentEditPage = () => { + const { id } = useParams(); + const appointmentId = Number(id); + const [defaultValues, setDefaultValues] = useState({}); + const [loading, setLoading] = useState(true); + const { showToast } = useToast(); + const navigate = useNavigate(); + + useEffect(() => { + apiRequest(`/appointments/${appointmentId}`, { method: "GET" }) + .then((data) => { + setDefaultValues(data); + }) + .finally(() => setLoading(false)); + }, [appointmentId]); + + const formConfig: StepConfig[] = [ + { + label: "Edit Appointment", + fields: [ + { + name: "notes", + label: "Appointment Notes", + type:'wysiwyg', + required: false, + }, + { + name: "status", + label: "Status", + type: "select", + options: [ + { label: "Scheduled", value: "SCHEDULED" }, + { label: "Completed", value: "COMPLETED" }, + { label: "Cancelled", value: "CANCELLED" }, + ], + required: true, + }, + ], + }, + ]; + + const handleConvertToVisit = () => { + apiRequest(`/appointments/${appointmentId}/convert-to-visit`, { method: "POST" }) + .then(() => { + showToast("Appointment converted to visit and sent to receptionist for reassignment.", "success"); + }) + .catch(() => showToast("Failed to convert appointment.", "error")); + }; + + if (loading) return
Loading...
; + + return ( +
+ { + // Merge all initial appointment details with new updates + const updatedData = { ...defaultValues, ...data }; + apiRequest(`/appointments/${appointmentId}`, { + method: "PUT", + body: updatedData, + }) + .then(() => { + showToast("Appointment updated.", "success"); + navigate(`/appointments/${appointmentId}/details`); + }) + .catch(() => showToast("Failed to update appointment.", "error")); + }} + submitButtonLabel="Save Changes" + /> + +
+ ); +}; + +export default DoctorAppointmentEditPage; diff --git a/afyaquik-frontend/doctor/src/patient/DoctorPatientVisitList.tsx b/afyaquik-frontend/doctor/src/patient/DoctorPatientVisitList.tsx index 84b93e2..ff7d949 100644 --- a/afyaquik-frontend/doctor/src/patient/DoctorPatientVisitList.tsx +++ b/afyaquik-frontend/doctor/src/patient/DoctorPatientVisitList.tsx @@ -6,7 +6,7 @@ interface AuthResponse { } const columns = [ - + { header: 'Patient Name', accessor: 'patientName' }, { header: 'Visit Type', accessor: 'visitType' }, { header: 'Date of Visit', accessor: 'visitDate', type: 'datetime' } @@ -64,7 +64,7 @@ const DoctorPatientVisitList = () => { data={patientVisits} // editView="index.html#/visits/#id/edit" detailsView="index.html#/visits/#id/details" - combinedSearchFieldsAndTerms={`patientAssignments.assignedOfficer.id=${assignedDoctorId}`} + combinedSearchFieldsAndTerms={`patientAssignments.nextStation.nam e=${localStorage.getItem('formattedStations')},patientAssignments.assignedOfficer.id=${assignedDoctorId}`} searchFields={searchFields} searchEntity="visits" dateFieldName={"patientAssignments.updatedAt"} diff --git a/afyaquik-frontend/pharmacy/src/patient-drug/PatientDrugList.tsx b/afyaquik-frontend/pharmacy/src/patient-drug/PatientDrugList.tsx index a2520ba..8b4ef5b 100644 --- a/afyaquik-frontend/pharmacy/src/patient-drug/PatientDrugList.tsx +++ b/afyaquik-frontend/pharmacy/src/patient-drug/PatientDrugList.tsx @@ -1,6 +1,5 @@ import {useEffect, useRef, useState} from "react"; import {apiRequest, DataTable, DataTableRef, useAlert, useToast} from "@afyaquik/shared"; -import { Button } from "react-bootstrap"; const columns = [ @@ -125,7 +124,7 @@ const PatientDrugList = ({visitId, data: initialData}:{visitId:number, data: Pat detailsTitle={'Dispense Drug'} detailsButtonEnabled={(drug: PatientDrug) => !drug.dispensed} searchEntity="patientDrugs" - combinedSearchFieldsAndTerms={`patientVisit.id=${visitId},deleted=false`} + combinedSearchFieldsAndTerms={`patientVisit.id=${visitId}`} dataEndpoint={`/search`} preventDeleteMultipleAction={deleteMultipleErrorAction} data={data} diff --git a/afyaquik-frontend/pharmacy/src/patient/PatientAssignmentList.tsx b/afyaquik-frontend/pharmacy/src/patient/PatientAssignmentList.tsx index 699c19c..aa1384c 100644 --- a/afyaquik-frontend/pharmacy/src/patient/PatientAssignmentList.tsx +++ b/afyaquik-frontend/pharmacy/src/patient/PatientAssignmentList.tsx @@ -29,10 +29,10 @@ const PatientAssignmentList = ({query}:{query?:string}) => { const userId = Number(localStorage.getItem("userId")); if (!query) { - query = `nextStation.name=PHARMACY,assignedOfficer.id=${userId}` + query = `nextStation.name=${localStorage.getItem('formattedStations')},assignedOfficer.id=${userId}` } else { - query = `${query},nextStation.name=PHARMACY,assignedOfficer.id=${userId}` + query = `${query},nextStation.name=${localStorage.getItem('formattedStations')},assignedOfficer.id=${userId}` } console.log("Assignments query", query) diff --git a/afyaquik-frontend/pharmacy/src/visit/VisitList.tsx b/afyaquik-frontend/pharmacy/src/visit/VisitList.tsx index 48ba380..ea1f3f9 100644 --- a/afyaquik-frontend/pharmacy/src/visit/VisitList.tsx +++ b/afyaquik-frontend/pharmacy/src/visit/VisitList.tsx @@ -29,7 +29,7 @@ const searchFields = [ const VisitList = () => { const userId = Number(localStorage.getItem("userId")); - const query=`patientAssignments.nextStation.name=PHARMACY,patientAssignments.assignedOfficer.id=${userId}`; + const query=`patientAssignments.nextStation.name=${localStorage.getItem('formattedStations')},patientAssignments.assignedOfficer.id=${userId}`; return ( { sendNotification( data.doctorId, "New Appointment", - `You have a new appointment with ${data.patient.firstName} on ${data.appointmentDateTime}.`, + `You have a new appointment with ${data.firstName} on ${data.appointmentDateTime}.`, `index.html#/appointments/${response.id}/details`, 'APPOINTMENT','DOCTOR' ); diff --git a/afyaquik-frontend/receptionist/src/appointment/AppointmentEditForm.tsx b/afyaquik-frontend/receptionist/src/appointment/AppointmentEditForm.tsx index 950c799..20ac1b9 100644 --- a/afyaquik-frontend/receptionist/src/appointment/AppointmentEditForm.tsx +++ b/afyaquik-frontend/receptionist/src/appointment/AppointmentEditForm.tsx @@ -97,7 +97,7 @@ const AppointmentEditForm = () => { apiRequest(`/appointments/${id}`, { method:'PUT' , body: data}) .then(response => { console.log(response) - window.location.href = `index.html#/patients/${response.id}/details`; + window.location.href = `index.html#/appointments/${response.id}/details`; }) .catch(err => console.error(err)); }} diff --git a/afyaquik-frontend/receptionist/src/patient/PatientRegisterForm.tsx b/afyaquik-frontend/receptionist/src/patient/PatientRegisterForm.tsx index 40ff5ae..8d75986 100644 --- a/afyaquik-frontend/receptionist/src/patient/PatientRegisterForm.tsx +++ b/afyaquik-frontend/receptionist/src/patient/PatientRegisterForm.tsx @@ -32,6 +32,7 @@ const formConfig: StepConfig[] = [ { label: 'Separated', value: 'SEPARATED' } ] }, ], + stepButtonLabel: 'Next', topComponents: [backtoList()] }, { diff --git a/afyaquik-frontend/shared/src/BaseHomePage.tsx b/afyaquik-frontend/shared/src/BaseHomePage.tsx index 58f12d3..9b21adb 100644 --- a/afyaquik-frontend/shared/src/BaseHomePage.tsx +++ b/afyaquik-frontend/shared/src/BaseHomePage.tsx @@ -17,7 +17,6 @@ const hasRole = (roles: string[]): boolean => { return true; } const currentRole = localStorage.getItem('currentRole'); - console.log("Current Roles", roles, currentRole) return roles.includes(currentRole as string) }; diff --git a/afyaquik-frontend/shared/src/DataTable.tsx b/afyaquik-frontend/shared/src/DataTable.tsx index 85a86e8..a77664f 100644 --- a/afyaquik-frontend/shared/src/DataTable.tsx +++ b/afyaquik-frontend/shared/src/DataTable.tsx @@ -205,6 +205,10 @@ function DataTable({ setIsSearching(true); if (!showDeletedRecords) { + if (searchTerm.length > 0) + { + searchTerm+=','; + } searchTerm+='deleted=false' } try { @@ -268,17 +272,15 @@ function DataTable({ setIsSearching(false); } }; - if (dataEndpoint) { - useEffect(() => { - if (searchTerm.length >= 3 || searchTerm.length === 0) { - let sortParam = sortField ? `${sortField},${sortDirection}` : 'createdAt,desc'; - if (dateFieldName) - sortParam = `${dateFieldName},desc`; - fetchData(currentPage, pageSize, sortParam); + useEffect(() => { + if (dataEndpoint && (searchTerm.length >= 3 || searchTerm.length === 0)) { + let sortParam = sortField ? `${sortField},${sortDirection}` : 'createdAt,desc'; + if (dateFieldName && !sortField) { + sortParam = `${dateFieldName},desc`; } - - }, [currentPage, pageSize, sortField, sortDirection, searchTerm, selectedFields, dateFieldValue]); - } + fetchData(currentPage, pageSize, sortParam); + } + }, [currentPage, pageSize, sortField, sortDirection, searchTerm, selectedFields, dateFieldValue]); const handleSort = (field: string) => { if (sortField === field) { @@ -448,14 +450,15 @@ function DataTable({ col.sortable !== false && handleSort(col.accessor)} + style={{ cursor: (col.sortable === true || col.sortable === undefined) ? 'pointer' : 'default' }} + onClick={() => (col.sortable === true || col.sortable === undefined) && handleSort(col.accessor)} > {col.header} {sortField === col.accessor && ( - sortDirection === 'asc' ? ' 🔼' : ' 🔽' + isSearching ? ' ⏳' : (sortDirection === 'asc' ? ' 🔼' : ' 🔽') )} + ))} {(editView || detailsView || (!showSelectionMode && (detailsButtonAction || editButtonAction))) && Actions} diff --git a/afyaquik-frontend/shared/src/Header.tsx b/afyaquik-frontend/shared/src/Header.tsx index 2d7f569..4f90c9d 100644 --- a/afyaquik-frontend/shared/src/Header.tsx +++ b/afyaquik-frontend/shared/src/Header.tsx @@ -36,6 +36,9 @@ const Header: React.FC = ({homeUrl, userRole}) => { } {isLoggedIn ? ( <> + + Profile + diff --git a/afyaquik-frontend/shared/src/patient/PatientList.tsx b/afyaquik-frontend/shared/src/patient/PatientList.tsx index 62dde3d..1f11b34 100644 --- a/afyaquik-frontend/shared/src/patient/PatientList.tsx +++ b/afyaquik-frontend/shared/src/patient/PatientList.tsx @@ -20,7 +20,7 @@ const PatientList = () => { const [patients, setPatients] = useState([]); useEffect(() => { - apiRequest("/patients/search", { method: 'POST', body: {} }) + apiRequest("/search", { method: 'POST', body: {} }) .then(data => { setPatients(data); }) diff --git a/src/main/java/com/afyaquik/appointments/entity/Appointment.java b/src/main/java/com/afyaquik/appointments/entity/Appointment.java index 1f1d926..7bfb8a5 100644 --- a/src/main/java/com/afyaquik/appointments/entity/Appointment.java +++ b/src/main/java/com/afyaquik/appointments/entity/Appointment.java @@ -1,6 +1,7 @@ package com.afyaquik.appointments.entity; import com.afyaquik.appointments.enums.AppointmentStatus; +import com.afyaquik.patients.entity.PatientVisit; import com.afyaquik.utils.SuperEntity; import com.afyaquik.patients.entity.Patient; import com.afyaquik.users.entity.User; @@ -41,4 +42,8 @@ public class Appointment extends SuperEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "doctor_id", nullable = false) private User doctor; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "patient_visit_id", nullable = true) + private PatientVisit patientVisit; } diff --git a/src/main/java/com/afyaquik/appointments/enums/AppointmentStatus.java b/src/main/java/com/afyaquik/appointments/enums/AppointmentStatus.java index 7f9b5fb..261cbc5 100644 --- a/src/main/java/com/afyaquik/appointments/enums/AppointmentStatus.java +++ b/src/main/java/com/afyaquik/appointments/enums/AppointmentStatus.java @@ -3,6 +3,7 @@ public enum AppointmentStatus { PENDING, CONFIRMED, + SCHEDULED, CANCELLED, COMPLETED } diff --git a/src/main/java/com/afyaquik/appointments/services/AppointmentService.java b/src/main/java/com/afyaquik/appointments/services/AppointmentService.java index 145b25b..dfe74a4 100644 --- a/src/main/java/com/afyaquik/appointments/services/AppointmentService.java +++ b/src/main/java/com/afyaquik/appointments/services/AppointmentService.java @@ -10,5 +10,5 @@ public interface AppointmentService { ListFetchDto getPatientAppointments(Long patientId, Pageable pageable); AppointmentDto updateAppointment(Long appointmentId, AppointmentDto dto); AppointmentDto getAppointmentDetails(Long appointmentId); - + void convertToVisit(Long appointmentId); } diff --git a/src/main/java/com/afyaquik/appointments/services/impl/AppointmentServiceImpl.java b/src/main/java/com/afyaquik/appointments/services/impl/AppointmentServiceImpl.java index 6a13e2f..0e5abc8 100644 --- a/src/main/java/com/afyaquik/appointments/services/impl/AppointmentServiceImpl.java +++ b/src/main/java/com/afyaquik/appointments/services/impl/AppointmentServiceImpl.java @@ -11,6 +11,9 @@ import com.afyaquik.patients.repository.PatientRepository; import com.afyaquik.users.entity.User; import com.afyaquik.users.repository.UsersRepository; +import com.afyaquik.patients.entity.PatientVisit; +import com.afyaquik.patients.repository.PatientVisitRepo; +import com.afyaquik.patients.enums.VisitType; import jakarta.persistence.EntityExistsException; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; @@ -29,6 +32,7 @@ public class AppointmentServiceImpl implements AppointmentService { private final PatientRepository patientRepo; private final UsersRepository usersRepo; private final AppointmentMapper appointmentMapper; + private final PatientVisitRepo patientVisitRepo; @Override @Transactional public AppointmentDto create(AppointmentDto appointmentDto) { @@ -82,12 +86,21 @@ public AppointmentDto updateAppointment(Long appointmentId, AppointmentDto dto) } } + // Prevent status change if appointment is already completed or cancelled + if ((appointment.getStatus() == AppointmentStatus.COMPLETED || appointment.getStatus() == AppointmentStatus.CANCELLED) + && dto.getStatus() != null && !dto.getStatus().equals(appointment.getStatus().name())) { + throw new IllegalStateException("Cannot change status of a completed or cancelled appointment."); + } + if (dto.getReason() != null) { appointment.setReason(dto.getReason()); } if (dto.getStatus() != null) { appointment.setStatus(AppointmentStatus.valueOf(dto.getStatus())); } + if (dto.getNotes()!=null) { + appointment.setNotes(dto.getNotes()); + } if (dto.getAppointmentDateTime() != null) { appointment.setAppointmentDateTime(dto.getAppointmentDateTime()); } @@ -102,4 +115,27 @@ public AppointmentDto getAppointmentDetails(Long appointmentId) { return appointmentMapper.toDto(appointmentRepo.findById(appointmentId) .orElseThrow(() -> new EntityNotFoundException("Appointment not found"))); } + + @Override + @Transactional + public void convertToVisit(Long appointmentId) { + Appointment appointment = appointmentRepo.findById(appointmentId) + .orElseThrow(() -> new EntityNotFoundException("Appointment not found")); + if (appointment.getStatus() == AppointmentStatus.COMPLETED || appointment.getStatus() == AppointmentStatus.CANCELLED) { + throw new IllegalStateException("Appointment status is already completed or cancelled.Cannot convert to visit."); + } + if (patientVisitRepo.existsByAppointment(appointment)) { + throw new EntityExistsException("Appointment already has a visit."); + } + PatientVisit visit = PatientVisit.builder() + .patient(appointment.getPatient()) + .visitType(VisitType.CONSULTATION) + .summaryReasonForVisit(appointment.getNotes()!= null ? appointment.getNotes() :appointment.getReason()) + .visitDate(appointment.getAppointmentDateTime().toLocalDate()) + .appointment(appointment) + .build(); + patientVisitRepo.save(visit); + appointment.setStatus(AppointmentStatus.COMPLETED); + appointmentRepo.save(appointment); + } } diff --git a/src/main/java/com/afyaquik/billing/config/CurrencyDataLoader.java b/src/main/java/com/afyaquik/billing/config/CurrencyDataLoader.java new file mode 100644 index 0000000..231a95c --- /dev/null +++ b/src/main/java/com/afyaquik/billing/config/CurrencyDataLoader.java @@ -0,0 +1,62 @@ +package com.afyaquik.billing.config; + +import com.afyaquik.billing.entity.Currency; +import com.afyaquik.billing.repository.CurrencyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class CurrencyDataLoader { + + private final CurrencyRepository currencyRepository; + + @Bean + @Transactional + public CommandLineRunner loadCurrencies() { + return args -> { + // Only run if no currencies exist + if (currencyRepository.count() == 0) { + // Create KES (Kenyan Shilling) as default active currency + Currency kes = Currency.builder() + .name("Kenyan Shilling") + .code("KES") + .symbol("KSh") + .active(true) + .build(); + + // Create other common currencies (inactive by default) + Currency usd = Currency.builder() + .name("US Dollar") + .code("USD") + .symbol("$") + .active(false) + .build(); + + Currency eur = Currency.builder() + .name("Euro") + .code("EUR") + .symbol("€") + .active(false) + .build(); + + Currency gbp = Currency.builder() + .name("British Pound") + .code("GBP") + .symbol("£") + .active(false) + .build(); + + // Save all currencies + currencyRepository.saveAll(List.of(kes, usd, eur, gbp)); + + System.out.println("Default currencies initialized with KES as active currency"); + } + }; + } +} diff --git a/src/main/java/com/afyaquik/patients/entity/PatientVisit.java b/src/main/java/com/afyaquik/patients/entity/PatientVisit.java index 24057df..f9d0d0d 100644 --- a/src/main/java/com/afyaquik/patients/entity/PatientVisit.java +++ b/src/main/java/com/afyaquik/patients/entity/PatientVisit.java @@ -1,5 +1,6 @@ package com.afyaquik.patients.entity; +import com.afyaquik.appointments.entity.Appointment; import com.afyaquik.billing.entity.Billing; import com.afyaquik.utils.SuperEntity; import com.afyaquik.patients.enums.Status; @@ -51,9 +52,12 @@ public class PatientVisit extends SuperEntity { @Enumerated(EnumType.STRING) private Status visitStatus= Status.PENDING; - @OneToOne(mappedBy = "patientVisit", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @OneToOne(mappedBy = "patientVisit", cascade = CascadeType.ALL, orphanRemoval = true) private Billing billing; + @OneToOne(mappedBy = "patientVisit", cascade = CascadeType.ALL, orphanRemoval = true) + private Appointment appointment; + public String getPatientName() { return this.patient.getFirstName() + " " + this.patient.getLastName(); diff --git a/src/main/java/com/afyaquik/patients/repository/PatientVisitRepo.java b/src/main/java/com/afyaquik/patients/repository/PatientVisitRepo.java index 3d4dd9a..6f9d6b6 100644 --- a/src/main/java/com/afyaquik/patients/repository/PatientVisitRepo.java +++ b/src/main/java/com/afyaquik/patients/repository/PatientVisitRepo.java @@ -1,5 +1,6 @@ package com.afyaquik.patients.repository; +import com.afyaquik.appointments.entity.Appointment; import com.afyaquik.patients.entity.Patient; import com.afyaquik.patients.entity.PatientVisit; import org.springframework.data.domain.Page; @@ -10,5 +11,6 @@ @Repository public interface PatientVisitRepo extends JpaRepository{ Page findAllByPatient(Pageable pageable, Patient patient); + boolean existsByAppointment(Appointment appointment); } diff --git a/src/main/java/com/afyaquik/pharmacy/utils/ExpiredDrugsDeactivator.java b/src/main/java/com/afyaquik/pharmacy/utils/ExpiredDrugsDeactivator.java index 026b34e..100dcea 100644 --- a/src/main/java/com/afyaquik/pharmacy/utils/ExpiredDrugsDeactivator.java +++ b/src/main/java/com/afyaquik/pharmacy/utils/ExpiredDrugsDeactivator.java @@ -12,8 +12,8 @@ public class ExpiredDrugsDeactivator { private final DrugInventoryService drugInventoryService; //cron to deactivate expired drugs -// @Scheduled(cron = "0 0 0 * * ?") // every day at midnight - @Scheduled(cron = "0 */2 * * * ?") + @Scheduled(cron = "0 0 0 * * ?") // every day at midnight +// @Scheduled(cron = "0 */2 * * * ?") public void deactivateExpiredDrugs() { log.info("Deactivating expired inventories"); drugInventoryService.deactivateExpiredDrugs(); diff --git a/src/main/java/com/afyaquik/users/config/StationDataLoader.java b/src/main/java/com/afyaquik/users/config/StationDataLoader.java new file mode 100644 index 0000000..a88a2e4 --- /dev/null +++ b/src/main/java/com/afyaquik/users/config/StationDataLoader.java @@ -0,0 +1,69 @@ +package com.afyaquik.users.config; + +import com.afyaquik.users.entity.Role; +import com.afyaquik.users.entity.Station; +import com.afyaquik.users.repository.RolesRepository; +import com.afyaquik.users.repository.StationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@Configuration +@RequiredArgsConstructor +public class StationDataLoader { + + private final StationRepository stationRepository; + private final RolesRepository rolesRepository; + + // Map of station names to their corresponding role names + private final Map> stationRoleMappings = Map.of( + "PHARMACY", List.of("PHARMACIST", "PHARMACY_TECH"), + "LAB", List.of("LAB_TECHNICIAN", "PATHOLOGIST"), + "TRIAGE", List.of("NURSE", "DOCTOR") + ); + + @Bean + @Transactional + public CommandLineRunner loadStations() { + return args -> { + // Process each station + for (Map.Entry> entry : stationRoleMappings.entrySet()) { + String stationName = entry.getKey(); + List roleNames = entry.getValue(); + + // Find or create the station + Station station = stationRepository.findByName(stationName) + .orElseGet(() -> { + Station newStation = Station.builder() + .name(stationName) + .build(); + return stationRepository.save(newStation); + }); + + // Get or create roles and associate them with the station + Set rolesToAdd = new HashSet<>(); + for (String roleName : roleNames) { + Role role = rolesRepository.findByName(roleName) + .orElseGet(() -> { + Role newRole = Role.builder() + .name(roleName) + .build(); + return rolesRepository.save(newRole); + }); + rolesToAdd.add(role); + } + + // Update station with roles + station.getAllowedRoles().addAll(rolesToAdd); + stationRepository.save(station); + + System.out.println("Updated station " + stationName + " with roles: " + + String.join(", ", roleNames)); + } + }; + } +} diff --git a/src/main/java/com/afyaquik/users/dto/PasswordResetRequestDto.java b/src/main/java/com/afyaquik/users/dto/PasswordResetRequestDto.java new file mode 100644 index 0000000..b289d2f --- /dev/null +++ b/src/main/java/com/afyaquik/users/dto/PasswordResetRequestDto.java @@ -0,0 +1,13 @@ +package com.afyaquik.users.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class PasswordResetRequestDto { + private Long userId; + private String username; + private String password; + private String confirmPassword; +} diff --git a/src/main/java/com/afyaquik/users/dto/RoleResponse.java b/src/main/java/com/afyaquik/users/dto/RoleResponse.java index 0548650..bfdef71 100644 --- a/src/main/java/com/afyaquik/users/dto/RoleResponse.java +++ b/src/main/java/com/afyaquik/users/dto/RoleResponse.java @@ -3,9 +3,12 @@ import lombok.Builder; import lombok.Data; +import java.util.List; + @Data @Builder public class RoleResponse { private Long id; private String name; + private List stations; } diff --git a/src/main/java/com/afyaquik/users/dto/StationDto.java b/src/main/java/com/afyaquik/users/dto/StationDto.java index 0fba804..f0c96e5 100644 --- a/src/main/java/com/afyaquik/users/dto/StationDto.java +++ b/src/main/java/com/afyaquik/users/dto/StationDto.java @@ -5,6 +5,9 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.HashSet; +import java.util.Set; + @Data @Builder @NoArgsConstructor @@ -12,4 +15,6 @@ public class StationDto { private Long id; private String name; + @Builder.Default + private Set allowedRoles = new HashSet<>(); } diff --git a/src/main/java/com/afyaquik/users/entity/PasswordRequestStatus.java b/src/main/java/com/afyaquik/users/entity/PasswordRequestStatus.java new file mode 100644 index 0000000..e5b6dda --- /dev/null +++ b/src/main/java/com/afyaquik/users/entity/PasswordRequestStatus.java @@ -0,0 +1,7 @@ +package com.afyaquik.users.entity; + +public enum PasswordRequestStatus { + PENDING, + COMPLETED, + EXPIRED +} diff --git a/src/main/java/com/afyaquik/users/entity/PasswordResetRequest.java b/src/main/java/com/afyaquik/users/entity/PasswordResetRequest.java new file mode 100644 index 0000000..a15798a --- /dev/null +++ b/src/main/java/com/afyaquik/users/entity/PasswordResetRequest.java @@ -0,0 +1,25 @@ +package com.afyaquik.users.entity; + +import com.afyaquik.utils.SuperEntity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "password_reset_requests") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PasswordResetRequest extends SuperEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + private PasswordRequestStatus status; + private LocalDateTime expiryDate; +} diff --git a/src/main/java/com/afyaquik/users/entity/Role.java b/src/main/java/com/afyaquik/users/entity/Role.java index 19643e4..9a4e7b5 100644 --- a/src/main/java/com/afyaquik/users/entity/Role.java +++ b/src/main/java/com/afyaquik/users/entity/Role.java @@ -4,6 +4,9 @@ import jakarta.persistence.*; import lombok.*; +import java.util.HashSet; +import java.util.Set; + @Entity @Table(name = "roles") @Getter @@ -15,6 +18,11 @@ public class Role extends SuperEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false, unique = true) private String name; + + @ManyToMany(mappedBy = "allowedRoles", fetch = FetchType.LAZY) + @Builder.Default + private Set stations = new HashSet<>(); } diff --git a/src/main/java/com/afyaquik/users/entity/Station.java b/src/main/java/com/afyaquik/users/entity/Station.java index 39aebf9..4ed992e 100644 --- a/src/main/java/com/afyaquik/users/entity/Station.java +++ b/src/main/java/com/afyaquik/users/entity/Station.java @@ -4,6 +4,9 @@ import jakarta.persistence.*; import lombok.*; +import java.util.HashSet; +import java.util.Set; + @Entity @Table(name = "stations") @NoArgsConstructor @@ -15,7 +18,16 @@ public class Station extends SuperEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false, unique = true) private String name; - + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "station_roles", + joinColumns = @JoinColumn(name = "station_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + @Builder.Default + private Set allowedRoles = new HashSet<>(); } diff --git a/src/main/java/com/afyaquik/users/repository/PasswordResetRequestRepository.java b/src/main/java/com/afyaquik/users/repository/PasswordResetRequestRepository.java new file mode 100644 index 0000000..39d4dbe --- /dev/null +++ b/src/main/java/com/afyaquik/users/repository/PasswordResetRequestRepository.java @@ -0,0 +1,21 @@ +package com.afyaquik.users.repository; + +import aj.org.objectweb.asm.commons.Remapper; +import com.afyaquik.users.entity.PasswordRequestStatus; +import com.afyaquik.users.entity.PasswordResetRequest; +import com.afyaquik.users.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface PasswordResetRequestRepository extends JpaRepository { + Optional findByUserAndStatus(User user, PasswordRequestStatus status); + + List findByStatusAndExpiryDateBefore(PasswordRequestStatus status, LocalDateTime expiryDateBefore); + Optional findTopByUserIdOrderByExpiryDateDesc(Long userId); +} + diff --git a/src/main/java/com/afyaquik/users/repository/UsersRepository.java b/src/main/java/com/afyaquik/users/repository/UsersRepository.java index 42e74ca..7963eae 100644 --- a/src/main/java/com/afyaquik/users/repository/UsersRepository.java +++ b/src/main/java/com/afyaquik/users/repository/UsersRepository.java @@ -26,6 +26,8 @@ public interface UsersRepository extends CrudRepository { List findByRolesIn(Collection roles); Set findByStationsIn(Collection stations); + Set findByStationsContaining(Station station); + @Query("SELECT u FROM User u") Page getAllUsersPaginated(Pageable pageable); } diff --git a/src/main/java/com/afyaquik/users/service/PasswordResetRequestService.java b/src/main/java/com/afyaquik/users/service/PasswordResetRequestService.java new file mode 100644 index 0000000..c2fadde --- /dev/null +++ b/src/main/java/com/afyaquik/users/service/PasswordResetRequestService.java @@ -0,0 +1,14 @@ +package com.afyaquik.users.service; + +import com.afyaquik.users.dto.PasswordResetRequestDto; +import org.springframework.transaction.annotation.Transactional; + +public interface PasswordResetRequestService { + void createPasswordResetRequest(PasswordResetRequestDto request); + void processPasswordReset(PasswordResetRequestDto request); + + @Transactional + void closeExpiredRequests(); + + String getLatestStatusForUser(Long userId); +} diff --git a/src/main/java/com/afyaquik/users/service/UserRoleService.java b/src/main/java/com/afyaquik/users/service/UserRoleService.java index 58cc716..d0aac48 100644 --- a/src/main/java/com/afyaquik/users/service/UserRoleService.java +++ b/src/main/java/com/afyaquik/users/service/UserRoleService.java @@ -10,6 +10,7 @@ public interface UserRoleService { RoleResponse updateRole(Long id, RoleRequest roleRequest); void deleteRole(Long id); List getAllRoles(); + RoleResponse getRoleByName(String name); RoleResponse getRole(Long id); } diff --git a/src/main/java/com/afyaquik/users/service/impl/PasswordResetRequestServiceImpl.java b/src/main/java/com/afyaquik/users/service/impl/PasswordResetRequestServiceImpl.java new file mode 100644 index 0000000..e9ca8d1 --- /dev/null +++ b/src/main/java/com/afyaquik/users/service/impl/PasswordResetRequestServiceImpl.java @@ -0,0 +1,87 @@ +package com.afyaquik.users.service.impl; + +import com.afyaquik.users.dto.PasswordResetRequestDto; +import com.afyaquik.users.entity.PasswordRequestStatus; +import com.afyaquik.users.entity.PasswordResetRequest; +import com.afyaquik.users.entity.User; +import com.afyaquik.users.repository.PasswordResetRequestRepository; +import com.afyaquik.users.repository.UsersRepository; +import com.afyaquik.users.service.PasswordResetRequestService; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PasswordResetRequestServiceImpl implements PasswordResetRequestService { + private final PasswordResetRequestRepository passwordResetRequestRepository; + private final UsersRepository usersRepository; + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional + public void createPasswordResetRequest(PasswordResetRequestDto requestDto) { + User user = usersRepository.findByUsername(requestDto.getUsername()) + .orElseThrow(() -> new EntityNotFoundException("User not found")); + if (passwordResetRequestRepository.findByUserAndStatus(user, PasswordRequestStatus.PENDING).isPresent()) { + throw new IllegalStateException("Password reset request already pending"); + } + java.time.LocalDateTime expiryDate = java.time.LocalDateTime.now().plusHours(3); + PasswordResetRequest resetRequest = PasswordResetRequest.builder() + .user(user) + .status(PasswordRequestStatus.PENDING) + .expiryDate(expiryDate) + .build(); + passwordResetRequestRepository.save(resetRequest); + } + + @Override + @Transactional + public void processPasswordReset(PasswordResetRequestDto requestDto) { + User user = usersRepository.findByUsername(requestDto.getUsername()) + .orElseThrow(() -> new EntityNotFoundException("User not found")); + PasswordResetRequest resetRequest = passwordResetRequestRepository.findByUserAndStatus(user, PasswordRequestStatus.PENDING) + .orElseThrow(() -> new EntityNotFoundException("Pending password reset request not found for: "+user.getUsername())); + if (resetRequest.getExpiryDate().isBefore(java.time.LocalDateTime.now())) { + throw new IllegalStateException("Reset token is invalid or expired"); + } + if (requestDto.getPassword()==null || requestDto.getConfirmPassword()==null){ + throw new IllegalArgumentException("Password and confirm password are required"); + } + if (!requestDto.getPassword().equals(requestDto.getConfirmPassword())) { + throw new IllegalArgumentException("Passwords do not match"); + } + user.setPasswordHash(passwordEncoder.encode(requestDto.getPassword())); + usersRepository.save(user); + resetRequest.setStatus(PasswordRequestStatus.COMPLETED); + passwordResetRequestRepository.save(resetRequest); + } + + @Transactional + @Override + public void closeExpiredRequests() { + List expiredRequests = passwordResetRequestRepository.findByStatusAndExpiryDateBefore( + PasswordRequestStatus.PENDING, java.time.LocalDateTime.now()); + for (PasswordResetRequest request : expiredRequests) { + request.setStatus(PasswordRequestStatus.EXPIRED); + } + passwordResetRequestRepository.saveAll(expiredRequests); + } + + @Scheduled(cron = "0 0 * * * *") // runs every hour + public void scheduledCloseExpiredRequests() { + closeExpiredRequests(); + } + + @Override + public String getLatestStatusForUser(Long userId) { + return passwordResetRequestRepository.findTopByUserIdOrderByExpiryDateDesc(userId) + .map(req -> req.getStatus().name()) + .orElse(null); + } +} diff --git a/src/main/java/com/afyaquik/users/service/impl/UserRoleServiceImpl.java b/src/main/java/com/afyaquik/users/service/impl/UserRoleServiceImpl.java index 7f59a54..aafa618 100644 --- a/src/main/java/com/afyaquik/users/service/impl/UserRoleServiceImpl.java +++ b/src/main/java/com/afyaquik/users/service/impl/UserRoleServiceImpl.java @@ -3,6 +3,7 @@ import com.afyaquik.users.dto.RoleRequest; import com.afyaquik.users.dto.RoleResponse; import com.afyaquik.users.entity.Role; +import com.afyaquik.users.entity.Station; import com.afyaquik.users.repository.RolesRepository; import com.afyaquik.users.service.UserRoleService; import jakarta.persistence.EntityNotFoundException; @@ -53,6 +54,12 @@ public List getAllRoles() { return rolesRepository.findAll().stream().map(role -> RoleResponse.builder().id(role.getId()).name(role.getName()).build()).toList(); } + @Override + public RoleResponse getRoleByName(String name) { + Role role = rolesRepository.findByName(name).orElseThrow(() -> new EntityNotFoundException("Role not found")); + return RoleResponse.builder().id(role.getId()).name(role.getName()).stations(role.getStations().stream().map(Station::getName).toList()).build(); + } + @Override public RoleResponse getRole(Long id) { return rolesRepository.findById(id).map(role -> RoleResponse.builder().id(role.getId()).name(role.getName()).build()).orElseThrow(() -> new EntityNotFoundException("Role not found")); diff --git a/src/main/java/com/afyaquik/users/service/impl/UserServiceImpl.java b/src/main/java/com/afyaquik/users/service/impl/UserServiceImpl.java index a4a195d..088e520 100644 --- a/src/main/java/com/afyaquik/users/service/impl/UserServiceImpl.java +++ b/src/main/java/com/afyaquik/users/service/impl/UserServiceImpl.java @@ -228,6 +228,9 @@ public UserResponse updateUserDetails(Long userId, UserDto request) { user.setEmail(request.getEmail()); user.setEnabled(request.getEnabled()); user.setAvailable(request.isAvailable()); + if (request.getPassword() != null) { + user.setPasswordHash(passwordEncoder.encode(request.getPassword())); + } AssignRolesRequest assignRolesRequest= new AssignRolesRequest(); assignRolesRequest.setRoles(request.getRoles()); assignRoles(user, assignRolesRequest); diff --git a/src/main/java/com/afyaquik/users/service/impl/UserStationServiceImpl.java b/src/main/java/com/afyaquik/users/service/impl/UserStationServiceImpl.java index 78ada7e..3b564dc 100644 --- a/src/main/java/com/afyaquik/users/service/impl/UserStationServiceImpl.java +++ b/src/main/java/com/afyaquik/users/service/impl/UserStationServiceImpl.java @@ -5,6 +5,7 @@ import com.afyaquik.users.entity.Role; import com.afyaquik.users.entity.Station; import com.afyaquik.users.entity.User; +import com.afyaquik.users.repository.RolesRepository; import com.afyaquik.users.repository.StationRepository; import com.afyaquik.users.repository.UsersRepository; import com.afyaquik.users.service.UserStationService; @@ -12,56 +13,147 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.HashSet; import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class UserStationServiceImpl implements UserStationService { private final StationRepository stationRepository; - private final UsersRepository usersRepository; + private final UsersRepository usersRepository; + private final RolesRepository rolesRepository; + @Override + @Transactional public StationDto createStation(StationDto stationDto) { - stationRepository.findByName(stationDto.getName()).ifPresent(station -> { - throw new EntityExistsException("Station already exists"); - }); - Station station = Station.builder().name(stationDto.getName()).build(); + // Check if station with the same name already exists + if (stationRepository.findByName(stationDto.getName()).isPresent()) { + throw new EntityExistsException("Station with name " + stationDto.getName() + " already exists"); + } + + // Create new station + Station station = Station.builder() + .name(stationDto.getName()) + .allowedRoles(new HashSet<>()) + .build(); + + // Set allowed roles if provided + if (stationDto.getAllowedRoles() != null && !stationDto.getAllowedRoles().isEmpty()) { + Set roles = stationDto.getAllowedRoles().stream() + .map(roleName -> rolesRepository.findByName(roleName) + .orElseThrow(()-> new EntityNotFoundException("Role not found with name: " + roleName))) + .collect(Collectors.toSet()); + station.setAllowedRoles(roles); + } + + // Save the station station = stationRepository.save(station); - return StationDto.builder().id(station.getId()).name(station.getName()).build(); + + // Convert to DTO and return + return toStationDto(station); } @Override + @Transactional public StationDto updateStation(Long id, StationDto stationDto) { - Station station = stationRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("Station not found")); - station.setName(stationDto.getName()); + // Find existing station + Station station = stationRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Station not found with id: " + id)); + + // Update name if provided and different + if (stationDto.getName() != null && !stationDto.getName().equals(station.getName())) { + // Check if new name is already taken + if (stationRepository.findByName(stationDto.getName()).isPresent()) { + throw new EntityExistsException("Another station with name " + stationDto.getName() + " already exists"); + } + station.setName(stationDto.getName()); + } + + // Update allowed roles if provided + if (stationDto.getAllowedRoles() != null) { + Set roles = stationDto.getAllowedRoles().stream() + .map(roleName -> rolesRepository.findByName(roleName) + .orElseThrow(()-> new EntityNotFoundException("Role not found with name: " + roleName))) + .collect(Collectors.toSet()); + + // Clear existing roles and add new ones + station.getAllowedRoles().clear(); + station.getAllowedRoles().addAll(roles); + } + + // Save the updated station station = stationRepository.save(station); - return StationDto.builder().id(station.getId()).name(station.getName()).build(); + return toStationDto(station); } @Override + @Transactional public void deleteStation(Long id) { - stationRepository.findById(id).orElseThrow(()-> new EntityNotFoundException("Station not found")); - stationRepository.deleteById(id); + Station station = stationRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Station not found with id: " + id)); + + // Remove station from all users first to avoid constraint violations + Set users = usersRepository.findByStationsContaining(station); + users.forEach(user -> user.getStations().remove(station)); + usersRepository.saveAll(users); + + // Now delete the station + stationRepository.delete(station); } @Override + @Transactional(readOnly = true) public StationDto getStation(Long id) { - return stationRepository.findById(id).map(station -> StationDto.builder().id(station.getId()).name(station.getName()).build()).orElseThrow(() -> new EntityNotFoundException("Station not found")); + return stationRepository.findById(id) + .map(this::toStationDto) + .orElseThrow(() -> new EntityNotFoundException("Station not found with id: " + id)); } @Override - public Set getStationUsers(String name) { - Set stations = new HashSet<>(); - Station station = stationRepository.findByName(name).orElseThrow(() -> new EntityNotFoundException("Station not found")); - stations.add(station); - Set users = usersRepository.findByStationsIn(stations); - return users.stream().map(user -> UserDto.builder().id(user.getId()).username(user.getUsername()).firstName(user.getFirstName()).lastName(user.getLastName()).secondName(user.getSecondName()).email(user.getEmail()).enabled(user.isEnabled()).roles(user.getRoles().stream().map(Role::getName).collect(java.util.stream.Collectors.toSet())).build()).collect(java.util.stream.Collectors.toSet()); + @Transactional(readOnly = true) + public Set getStationUsers(String stationName) { + Station station = stationRepository.findByName(stationName) + .orElseThrow(() -> new EntityNotFoundException("Station not found with name: " + stationName)); + return usersRepository.findByStationsContaining(station).stream() + .map(this::toUserDto) + .collect(Collectors.toSet()); } @Override + @Transactional(readOnly = true) public Set getAllStations() { - return stationRepository.findAll().stream().map(station -> StationDto.builder().id(station.getId()).name(station.getName()).build()).collect(java.util.stream.Collectors.toSet()); + return stationRepository.findAll().stream() + .map(this::toStationDto) + .collect(Collectors.toSet()); + } + + // Helper method to convert Station to StationDto + private StationDto toStationDto(Station station) { + return StationDto.builder() + .id(station.getId()) + .name(station.getName()) + .allowedRoles(station.getAllowedRoles().stream() + .map(Role::getName) + .collect(Collectors.toSet())) + .build(); + } + + // Helper method to convert User to UserDto + private UserDto toUserDto(User user) { + return UserDto.builder() + .id(user.getId()) + .username(user.getUsername()) + .firstName(user.getFirstName()) + .lastName(user.getLastName()) + .email(user.getEmail()) + .enabled(user.isEnabled()) + .roles(user.getRoles().stream() + .map(Role::getName) + .collect(Collectors.toSet())) + .build(); } } diff --git a/src/main/java/com/afyaquik/utils/mappers/users/PasswordResetRequestMapper.java b/src/main/java/com/afyaquik/utils/mappers/users/PasswordResetRequestMapper.java new file mode 100644 index 0000000..7775aea --- /dev/null +++ b/src/main/java/com/afyaquik/utils/mappers/users/PasswordResetRequestMapper.java @@ -0,0 +1,20 @@ +package com.afyaquik.utils.mappers.users; + +import com.afyaquik.users.dto.PasswordResetRequestDto; +import com.afyaquik.users.entity.PasswordResetRequest; +import com.afyaquik.utils.mappers.EntityMapper; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring") +public interface PasswordResetRequestMapper extends EntityMapper { + + @Mapping(target = "user.id", source = "userId") + PasswordResetRequest toEntity(PasswordResetRequestDto dto); + + @Override + @Mapping(target = "userId", source = "user.id") + PasswordResetRequestDto toDto(PasswordResetRequest entity); +} + diff --git a/src/main/java/com/afyaquik/utils/mappers/users/RoleMapper.java b/src/main/java/com/afyaquik/utils/mappers/users/RoleMapper.java index 1ccdd9e..2e16206 100644 --- a/src/main/java/com/afyaquik/utils/mappers/users/RoleMapper.java +++ b/src/main/java/com/afyaquik/utils/mappers/users/RoleMapper.java @@ -1,13 +1,32 @@ package com.afyaquik.utils.mappers.users; -import com.afyaquik.utils.mappers.EntityMapper; import com.afyaquik.users.dto.RoleResponse; import com.afyaquik.users.entity.Role; +import com.afyaquik.users.entity.Station; +import com.afyaquik.utils.mappers.EntityMapper; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import java.util.Set; +import java.util.stream.Collectors; + @Mapper(componentModel = "spring") public interface RoleMapper extends EntityMapper { + @Override + @Mapping(target = "stations", ignore = true) RoleResponse toDto(Role entity); + + @Named("mapStationsToStrings") + static Set mapStationsToStrings(Set stations) { + if (stations == null) { + return null; + } + return stations.stream() + .map(Station::getName) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/com/afyaquik/utils/mappers/users/StationMapper.java b/src/main/java/com/afyaquik/utils/mappers/users/StationMapper.java index 7e16430..d72152d 100644 --- a/src/main/java/com/afyaquik/utils/mappers/users/StationMapper.java +++ b/src/main/java/com/afyaquik/utils/mappers/users/StationMapper.java @@ -2,11 +2,32 @@ import com.afyaquik.utils.mappers.EntityMapper; import com.afyaquik.users.dto.StationDto; +import com.afyaquik.users.entity.Role; import com.afyaquik.users.entity.Station; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +import java.util.Set; +import java.util.stream.Collectors; @Mapper(componentModel = "spring") public interface StationMapper extends EntityMapper { + @Override + @Mapping(target = "allowedRoles", source = "allowedRoles", qualifiedByName = "mapRolesToStrings") StationDto toDto(Station station); + + @Mapping(target = "allowedRoles", ignore = true) + Station toEntity(StationDto dto); + + @Named("mapRolesToStrings") + default Set mapRolesToStrings(Set roles) { + if (roles == null) { + return null; + } + return roles.stream() + .map(Role::getName) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/com/afyaquik/utils/mappers/users/UserMapperRegistry.java b/src/main/java/com/afyaquik/utils/mappers/users/UserMapperRegistry.java index c2bef9d..5577f59 100644 --- a/src/main/java/com/afyaquik/utils/mappers/users/UserMapperRegistry.java +++ b/src/main/java/com/afyaquik/utils/mappers/users/UserMapperRegistry.java @@ -7,13 +7,14 @@ @Component public class UserMapperRegistry { @Autowired - UserMapperRegistry(RoleMapper roleMapper, UserMapper userMapper, StationMapper stationMapper, ApiPermissionMapper apiPermissionMapper, MapperRegistry mapperRegistry) + UserMapperRegistry(RoleMapper roleMapper, UserMapper userMapper, StationMapper stationMapper, ApiPermissionMapper apiPermissionMapper, PasswordResetRequestMapper passwordResetRequestMapper, MapperRegistry mapperRegistry) { { mapperRegistry.registerMapper("stations", stationMapper); mapperRegistry.registerMapper("users", userMapper); mapperRegistry.registerMapper("roles", roleMapper); mapperRegistry.registerMapper("apiPermissions", apiPermissionMapper); + mapperRegistry.registerMapper("passwordResetRequests", passwordResetRequestMapper); } } } diff --git a/src/main/java/com/afyaquik/utils/otherservices/impl/EntityUtilServiceImpl.java b/src/main/java/com/afyaquik/utils/otherservices/impl/EntityUtilServiceImpl.java index aa989b4..962ee84 100644 --- a/src/main/java/com/afyaquik/utils/otherservices/impl/EntityUtilServiceImpl.java +++ b/src/main/java/com/afyaquik/utils/otherservices/impl/EntityUtilServiceImpl.java @@ -13,6 +13,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.lang.reflect.Field; @@ -36,13 +38,41 @@ public class EntityUtilServiceImpl implements EntityUtilService { private final Map> joins = new HashMap<>(); + // Map of allowed roles for each entity + private static final Map> ENTITY_ROLE_MAP = Map.of( + "Patient", Set.of("ADMIN", "DOCTOR", "NURSE", "SUPER_ADMIN"), + "Billing", Set.of("ADMIN", "ACCOUNTANT", "SUPER_ADMIN","DOCTOR","RECEPTIONIST"), + "User", Set.of("ADMIN", "SUPERADMIN") + // Add more entities and allowed roles as needed + ); + + + private Set getCurrentUserRoles() { + return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); + } + + private boolean isRoleAllowedForEntity(String entity, Set userRoles) { + Set allowedRoles = ENTITY_ROLE_MAP.get(entity); + // If no allowed roles are specified, entity is public + if (allowedRoles == null) return true; + for (String role : userRoles) { + if (allowedRoles.contains(role)) return true; + } + return false; + } @Override - @Cacheable(value = "searchResults", key = "#searchDto.searchEntity + '-' + #searchDto.query + '-' + #searchDto.page + '-' + #searchDto.size + '-' + #searchDto.sort") + @Cacheable(value = "searchResults", key = "#searchDto.searchEntity + '-' + #searchDto.query + '-' + #searchDto.page + '-' + #searchDto.size + '-' + #searchDto.sort+ '-' + #searchDto.dateFilter") public SearchResponseDto search(SearchDto searchDto) { String entityKey = searchDto.getSearchEntity(); if (entityKey == null) throw new IllegalArgumentException("Search entity not specified"); + // Role-based entity search restriction + Set userRoles = getCurrentUserRoles(); + if (!isRoleAllowedForEntity(entityKey, userRoles)) { + throw new SecurityException("You do not have permission to search this entity."); + } + try { Class entityClass = resolveEntityClass(entityKey); Class dtoClass = resolveDtoClass(entityKey); @@ -198,7 +228,7 @@ private Predicate buildPredicates(SearchDto dto, CriteriaBuilder cb, Root roo for (String term : searchTerms) { term = term.trim(); List fieldPredicates = new ArrayList<>(); - if (!term.contains("=") && dto.getSearchFields() != null && !dto.getSearchFields().isEmpty()) { + if (!term.contains("=")) { term= term.toLowerCase(Locale.ROOT); for (String field : dto.getSearchFields()) { try { @@ -248,27 +278,55 @@ private Predicate buildPredicates(SearchDto dto, CriteriaBuilder cb, Root roo Path path = resolveJoinPath(root, field, entityClass); Class type = path.getJavaType(); - if (type.equals(String.class)) { - extraTermPredicates.add(cb.equal(cb.lower(path.as(String.class)), value.toLowerCase(Locale.ROOT))); - } else if (type.equals(LocalDateTime.class)) { - LocalDateTime parsed = parseDate(value); - if (parsed != null) - extraTermPredicates.add(cb.equal(path.as(LocalDateTime.class), parsed)); - } - else if (type.equals(Long.class)) { + // Check if the value contains || for OR operation + if (value.contains("||")) { + String[] orValues = value.split("\\|\\|"); + List orPredicates = new ArrayList<>(); + + for (String orValue : orValues) { + orValue = orValue.trim(); + if (type.equals(String.class)) { + orPredicates.add(cb.equal(cb.lower(path.as(String.class)), orValue.toLowerCase(Locale.ROOT))); + } else if (type.equals(LocalDateTime.class)) { + LocalDateTime parsed = parseDate(orValue); + if (parsed != null) { + orPredicates.add(cb.equal(path.as(LocalDateTime.class), parsed)); + } + } else if (type.equals(Long.class)) { + orPredicates.add(cb.equal(path.as(Long.class), Long.parseLong(orValue))); + } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { + orPredicates.add(cb.equal(path.as(Boolean.class), Boolean.parseBoolean(orValue))); + } else { + orPredicates.add(cb.equal(path.as(String.class), orValue)); + } + } + + if (!orPredicates.isEmpty()) { + extraTermPredicates.add(cb.or(orPredicates.toArray(new Predicate[0]))); + } + } else { + // logic for single value + if (type.equals(String.class)) { + extraTermPredicates.add(cb.equal(cb.lower(path.as(String.class)), value.toLowerCase(Locale.ROOT))); + } else if (type.equals(LocalDateTime.class)) { + LocalDateTime parsed = parseDate(value); + if (parsed != null) + extraTermPredicates.add(cb.equal(path.as(LocalDateTime.class), parsed)); + } else if (type.equals(Long.class)) { extraTermPredicates.add(cb.equal(path.as(Long.class), Long.parseLong(value))); + } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { + extraTermPredicates.add(cb.equal(path.as(Boolean.class), Boolean.parseBoolean(value))); + } else { + extraTermPredicates.add(cb.equal(path.as(String.class), value)); // fallback } - else if (type.equals(Boolean.class) || type.equals(boolean.class)) { - extraTermPredicates.add(cb.equal(path.as(Boolean.class), Boolean.parseBoolean(value))); - } - else { - extraTermPredicates.add(cb.equal(path.as(String.class), value)); // fallback } } catch (IllegalArgumentException e) { log.warn("Invalid search term: {}", term); } if (!extraTermPredicates.isEmpty()) { - termPredicates.add(cb.and(extraTermPredicates.toArray(new Predicate[0]))); + // If we have multiple conditions, combine them with AND + termPredicates.add(cb.and(extraTermPredicates.toArray(new Predicate[0]))); + } } @@ -305,6 +363,7 @@ private Class resolveEntityClass(String key) throws ClassNotFoundException { case "billings" -> Class.forName("com.afyaquik.billing.entity.Billing"); case "billingDetails" -> Class.forName("com.afyaquik.billing.entity.BillingDetail"); case "currencies" -> Class.forName("com.afyaquik.billing.entity.Currency"); + case "passwordResetRequests" -> Class.forName("com.afyaquik.users.entity.PasswordResetRequest"); default -> throw new ClassNotFoundException("No entity class for " + key); }; } @@ -334,6 +393,7 @@ private Class resolveDtoClass(String key) throws ClassNotFoundException { case "billings" -> Class.forName("com.afyaquik.billing.dto.BillingDto"); case "billingDetails" -> Class.forName("com.afyaquik.billing.dto.BillingDetailDto"); case "currencies" -> Class.forName("com.afyaquik.billing.dto.CurrencyDto"); + case "passwordResetRequests" -> Class.forName("com.afyaquik.users.dto.PasswordResetRequestDto"); default -> throw new ClassNotFoundException("No DTO class for " + key); }; } diff --git a/src/main/java/com/afyaquik/web/api/appointments/AppointmentController.java b/src/main/java/com/afyaquik/web/api/appointments/AppointmentController.java index 55a431e..1538879 100644 --- a/src/main/java/com/afyaquik/web/api/appointments/AppointmentController.java +++ b/src/main/java/com/afyaquik/web/api/appointments/AppointmentController.java @@ -38,4 +38,9 @@ public ResponseEntity update(@PathVariable Long appointmentId, @ public ResponseEntity getAppointmentDetails(@PathVariable Long appointmentId) { return ResponseEntity.ok(service.getAppointmentDetails(appointmentId)); } + @PostMapping("/{appointmentId}/convert-to-visit") + public ResponseEntity convertToVisit(@PathVariable Long appointmentId) { + service.convertToVisit(appointmentId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/afyaquik/web/api/search/EntityUtilController.java b/src/main/java/com/afyaquik/web/api/search/EntityUtilController.java index d1fec5d..12553fe 100644 --- a/src/main/java/com/afyaquik/web/api/search/EntityUtilController.java +++ b/src/main/java/com/afyaquik/web/api/search/EntityUtilController.java @@ -16,8 +16,6 @@ public class EntityUtilController { private final EntityUtilService entityUtilService; @PostMapping("/search") public ResponseEntity search(@RequestBody SearchDto searchDto) { - - SearchResponseDto response = entityUtilService.search(searchDto); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/afyaquik/web/api/users/PasswordResetRequestController.java b/src/main/java/com/afyaquik/web/api/users/PasswordResetRequestController.java new file mode 100644 index 0000000..ca45baf --- /dev/null +++ b/src/main/java/com/afyaquik/web/api/users/PasswordResetRequestController.java @@ -0,0 +1,38 @@ +package com.afyaquik.web.api.users; + +import com.afyaquik.users.dto.PasswordResetRequestDto; +import com.afyaquik.users.service.PasswordResetRequestService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/password-reset") +@RequiredArgsConstructor +public class PasswordResetRequestController { + private final PasswordResetRequestService passwordResetRequestService; + + @PostMapping("/request") + public ResponseEntity createPasswordResetRequest(@RequestParam PasswordResetRequestDto requestDto) { + passwordResetRequestService.createPasswordResetRequest(requestDto); + return ResponseEntity.ok().build(); + } + + @PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") + @PostMapping("/process") + public ResponseEntity processPasswordReset(@RequestBody PasswordResetRequestDto requestDto) { + passwordResetRequestService.processPasswordReset(requestDto); + return ResponseEntity.ok().build(); + } + + @GetMapping("/status/{userId}") + public ResponseEntity getPasswordResetStatus(@PathVariable Long userId) { + String status = passwordResetRequestService.getLatestStatusForUser(userId); + if (status != null) { + return ResponseEntity.ok(status); + } else { + return ResponseEntity.notFound().build(); + } + } +} diff --git a/src/main/java/com/afyaquik/web/api/users/RoleController.java b/src/main/java/com/afyaquik/web/api/users/RoleController.java index da69d83..32448ec 100644 --- a/src/main/java/com/afyaquik/web/api/users/RoleController.java +++ b/src/main/java/com/afyaquik/web/api/users/RoleController.java @@ -48,4 +48,10 @@ public ResponseEntity getAllRoles() { headers.add("Access-Control-Expose-Headers","Content-Range"); return ResponseEntity.ok().headers(headers).body(roles); } + @GetMapping("/byName/{name}") + @PreAuthorize("isAuthenticated()") + public ResponseEntity getRoleByName(@PathVariable String name) { + return ResponseEntity.ok(userRoleService.getRoleByName(name)); + } + } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 4da26f2..80b583a 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,6 +11,7 @@ spring: enabled: false jpa: + hibernate: ddl-auto: update show-sql: true