dashboard
This commit is contained in:
parent
ba34d0e59b
commit
7215cc263a
@ -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 {}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user