dashboard

This commit is contained in:
Ricardo Araújo 2026-09-16 14:15:51 +01:00
parent ba34d0e59b
commit 7215cc263a
5 changed files with 90 additions and 27 deletions

View File

@ -30,6 +30,11 @@ class MedMapAPI {
return try await apiClient.request(url, method: .post, parameters: SignupRequest(email: email, password: password, role: role))
}
func logout() async throws {
let url = "\(baseURL)/api/auth/logout"
try await apiClient.requestVoid(url, method: .post, parameters: LogoutRequest())
}
// MARK: - Companies
func createCompany(request: CreateCompanyRequest) async throws -> CompanyDTO {
@ -113,3 +118,5 @@ class MedMapAPI {
return try await apiClient.request(url, method: .get)
}
}
private struct LogoutRequest: Encodable {}

View File

@ -9,6 +9,7 @@ final class DashboardViewModel: ViewModel {
@Published private(set) var prescriptions: [PrescriptionWithSchedulesResponse] = []
@Published private(set) var medications: [String: MedicationDTO] = [:]
@Published private(set) var errorMessage: String?
@Published private(set) var logoutErrorMessage: String?
private let api: MedMapAPI
@ -73,6 +74,23 @@ final class DashboardViewModel: ViewModel {
}
}
func logout() async {
guard !isLoading else { return }
isLoading = true
logoutErrorMessage = nil
defer { isLoading = false }
do {
try await api.logout()
appStateManager.resetNavigation()
appStateManager.navigateTo(state: .login)
} catch {
logoutErrorMessage = "Nao foi possivel terminar a sessao. Tente novamente."
fluxlog("Patient logout failed: \(error.localizedDescription)")
}
}
private var currentTime: String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")

View File

@ -4,6 +4,7 @@ struct DashboardView: View {
@StateObject private var viewModel = DashboardViewModel()
@State private var isShowingMonthlyHistory = false
@State private var isShowingLogoutConfirmation = false
var body: some View {
ZStack {
@ -44,7 +45,15 @@ struct DashboardView: View {
}
.sheet(isPresented: $isShowingMonthlyHistory) {
PatientMonthlyHistorySheet()
.presentationBackground(.clear)
.presentationBackground(.ultraThinMaterial)
}
.alert("Terminar sessao?", isPresented: $isShowingLogoutConfirmation) {
Button("Cancelar", role: .cancel) {}
Button("Terminar sessao", role: .destructive) {
Task { await viewModel.logout() }
}
} message: {
Text("Terá de iniciar sessao novamente para consultar a sua medicacao.")
}
}
@ -66,8 +75,28 @@ struct DashboardView: View {
.fill(.green)
.frame(width: 16, height: 16)
}
Button {
isShowingLogoutConfirmation = true
} label: {
Image(systemName: "power")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(Color.appTextPrimary.opacity(0.52))
.frame(width: 32, height: 32)
.background(.white.opacity(0.45))
.clipShape(Circle())
}
.buttonStyle(.plain)
.accessibilityLabel("Terminar sessao")
}
.padding(.horizontal, 32)
if let logoutErrorMessage = viewModel.logoutErrorMessage {
Text(logoutErrorMessage)
.font(.jakartaRegular12)
.foregroundStyle(.red)
.frame(maxWidth: .infinity)
}
}
// MARK: - Patient header

View File

@ -13,32 +13,28 @@ struct PatientMonthlyHistorySheet: View {
private let columns = Array(repeating: GridItem(.flexible(), spacing: 7), count: 7)
var body: some View {
ZStack {
Color.appBackgroundLight.opacity(0.22).ignoresSafeArea()
VStack(spacing: 0) {
Spacer(minLength: 0)
VStack(spacing: 0) {
Spacer(minLength: 0)
VStack(spacing: 20) {
header
content
}
.padding(20)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 22))
.shadow(color: .black.opacity(0.18), radius: 15, y: 8)
.padding(.horizontal, 32)
Spacer(minLength: 0)
Button(action: dismiss.callAsFunction) {
Image(systemName: "xmark")
.font(.system(size: 21, weight: .medium))
.foregroundStyle(Color.appAccent)
.frame(width: 44, height: 44)
}
.padding(.bottom, 48)
VStack(spacing: 24) {
header
content
}
.padding(24)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 22))
.shadow(color: .black.opacity(0.18), radius: 15, y: 8)
.padding(.horizontal, 20)
Spacer(minLength: 0)
Button(action: dismiss.callAsFunction) {
Image(systemName: "xmark")
.font(.system(size: 21, weight: .medium))
.foregroundStyle(Color.appAccent)
.frame(width: 44, height: 44)
}
.padding(.bottom, 48)
}
.task {
await viewModel.loadHistory()
@ -64,7 +60,7 @@ struct PatientMonthlyHistorySheet: View {
Text(viewModel.history?.periodLabel ?? "Histórico")
.font(.jakartaBold16)
.foregroundStyle(Color.appAccent)
.frame(minWidth: 94)
.frame(minWidth: 124)
Button {
Task { await viewModel.showNextMonth() }
@ -102,7 +98,7 @@ struct PatientMonthlyHistorySheet: View {
} else if let history = viewModel.history {
LazyVGrid(columns: columns, spacing: 11) {
ForEach(0..<history.leadingDays, id: \.self) { _ in
Color.clear.frame(height: 32)
Color.clear.frame(height: 38)
}
ForEach(history.days) { day in
CalendarDayView(day: day)

View File

@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import org.springframework.http.HttpHeaders;
@ -21,6 +22,8 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -98,4 +101,14 @@ public class RestAuthController {
return ResponseEntity.status(201).headers(headers).body(LoginResponse.from(created));
}
@Operation(summary = "Log out", description = "Invalidates the authenticated session and clears its session cookie.")
@ApiResponse(responseCode = "204", description = "Session invalidated")
@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request, HttpServletResponse response) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
new SecurityContextLogoutHandler().logout(request, response, authentication);
new CookieClearingLogoutHandler("JSESSIONID").logout(request, response, authentication);
return ResponseEntity.noContent().build();
}
}