Add period companion wellbeing tools
All checks were successful
Build Org Backend / build (push) Successful in 19s
All checks were successful
Build Org Backend / build (push) Successful in 19s
This commit is contained in:
@@ -17,15 +17,20 @@ import org.zaine.app.dto.PeriodCompanionDTO.DailyLogDTO;
|
|||||||
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DashboardDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.DashboardDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.InsightsDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.InsightsDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodEntryRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.PeriodEntryRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PredictionDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.PredictionDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.SettingsDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.SettingsRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SymptomDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.SymptomDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.WellbeingDTO;
|
||||||
import org.zaine.app.service.PeriodCompanionService;
|
import org.zaine.app.service.PeriodCompanionService;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -117,6 +122,30 @@ public class PeriodCompanionController {
|
|||||||
return periodCompanionService.getInsights();
|
return periodCompanionService.getInsights();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Get cycle-aware wellbeing guidance")
|
||||||
|
@GetMapping("/wellbeing")
|
||||||
|
public WellbeingDTO getWellbeing() {
|
||||||
|
return periodCompanionService.getWellbeing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Get fasting make-up logs")
|
||||||
|
@GetMapping("/fasting-logs")
|
||||||
|
public List<FastingLogDTO> getFastingLogs() {
|
||||||
|
return periodCompanionService.getFastingLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Create or update a fasting make-up log")
|
||||||
|
@PostMapping("/fasting-logs")
|
||||||
|
public ResponseEntity<FastingLogDTO> saveFastingLog(@RequestBody FastingLogRequest request) {
|
||||||
|
return ResponseEntity.ok(periodCompanionService.saveFastingLog(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Create or update a daily reflection")
|
||||||
|
@PostMapping("/reflections")
|
||||||
|
public ResponseEntity<ReflectionDTO> saveReflection(@RequestBody ReflectionRequest request) {
|
||||||
|
return ResponseEntity.ok(periodCompanionService.saveReflection(request));
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "Get period companion settings")
|
@Operation(summary = "Get period companion settings")
|
||||||
@GetMapping("/settings")
|
@GetMapping("/settings")
|
||||||
public SettingsDTO getSettings() {
|
public SettingsDTO getSettings() {
|
||||||
|
|||||||
@@ -89,4 +89,19 @@ public final class PeriodCompanionDTO {
|
|||||||
public record CreateSymptomRequest(String name) {}
|
public record CreateSymptomRequest(String name) {}
|
||||||
public record SettingsDTO(int averageCycleLength, int averagePeriodLength, OffsetDateTime updatedAt) {}
|
public record SettingsDTO(int averageCycleLength, int averagePeriodLength, OffsetDateTime updatedAt) {}
|
||||||
public record SettingsRequest(Integer averageCycleLength, Integer averagePeriodLength) {}
|
public record SettingsRequest(Integer averageCycleLength, Integer averagePeriodLength) {}
|
||||||
|
public record FastingLogDTO(Long id, LocalDate fastDate, String status, String notes, OffsetDateTime createdAt, OffsetDateTime updatedAt) {}
|
||||||
|
public record FastingLogRequest(LocalDate fastDate, String status, String notes) {}
|
||||||
|
public record ReflectionDTO(Long id, LocalDate date, String prompt, String response, OffsetDateTime createdAt, OffsetDateTime updatedAt) {}
|
||||||
|
public record ReflectionRequest(LocalDate date, String prompt, String response) {}
|
||||||
|
public record WellbeingDTO(
|
||||||
|
String phase,
|
||||||
|
String bodyInsight,
|
||||||
|
String supportiveAction,
|
||||||
|
String spiritualReminder,
|
||||||
|
String fiqhNote,
|
||||||
|
ReflectionDTO todayReflection,
|
||||||
|
List<ReflectionDTO> recentReflections,
|
||||||
|
List<FastingLogDTO> fastingLogs,
|
||||||
|
long missedFastCount,
|
||||||
|
long madeUpFastCount) {}
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/main/java/org/zaine/app/model/PeriodDailyReflection.java
Normal file
58
src/main/java/org/zaine/app/model/PeriodDailyReflection.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package org.zaine.app.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "pc_daily_reflections")
|
||||||
|
public class PeriodDailyReflection {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private LocalDate date;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String prompt;
|
||||||
|
|
||||||
|
@Column(name = "response", columnDefinition = "text")
|
||||||
|
private String response;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private OffsetDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
void onInsert() {
|
||||||
|
OffsetDateTime now = OffsetDateTime.now();
|
||||||
|
this.createdAt = now;
|
||||||
|
this.updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
void onUpdate() {
|
||||||
|
this.updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public LocalDate getDate() { return date; }
|
||||||
|
public void setDate(LocalDate date) { this.date = date; }
|
||||||
|
public String getPrompt() { return prompt; }
|
||||||
|
public void setPrompt(String prompt) { this.prompt = prompt; }
|
||||||
|
public String getResponse() { return response; }
|
||||||
|
public void setResponse(String response) { this.response = response; }
|
||||||
|
public OffsetDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
}
|
||||||
58
src/main/java/org/zaine/app/model/PeriodFastingLog.java
Normal file
58
src/main/java/org/zaine/app/model/PeriodFastingLog.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package org.zaine.app.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "pc_fasting_logs")
|
||||||
|
public class PeriodFastingLog {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "fast_date", nullable = false, unique = true)
|
||||||
|
private LocalDate fastDate;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "notes", columnDefinition = "text")
|
||||||
|
private String notes;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private OffsetDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
void onInsert() {
|
||||||
|
OffsetDateTime now = OffsetDateTime.now();
|
||||||
|
this.createdAt = now;
|
||||||
|
this.updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
void onUpdate() {
|
||||||
|
this.updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public LocalDate getFastDate() { return fastDate; }
|
||||||
|
public void setFastDate(LocalDate fastDate) { this.fastDate = fastDate; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
public String getNotes() { return notes; }
|
||||||
|
public void setNotes(String notes) { this.notes = notes; }
|
||||||
|
public OffsetDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public OffsetDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.zaine.app.repositories;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.zaine.app.model.PeriodDailyReflection;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface PeriodDailyReflectionRepository extends JpaRepository<PeriodDailyReflection, Long> {
|
||||||
|
Optional<PeriodDailyReflection> findByDate(LocalDate date);
|
||||||
|
List<PeriodDailyReflection> findTop14ByOrderByDateDesc();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.zaine.app.repositories;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.zaine.app.model.PeriodFastingLog;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface PeriodFastingLogRepository extends JpaRepository<PeriodFastingLog, Long> {
|
||||||
|
Optional<PeriodFastingLog> findByFastDate(LocalDate fastDate);
|
||||||
|
List<PeriodFastingLog> findTop50ByOrderByFastDateDesc();
|
||||||
|
long countByStatus(String status);
|
||||||
|
}
|
||||||
@@ -10,26 +10,35 @@ import org.zaine.app.dto.PeriodCompanionDTO.CycleTrendDTO;
|
|||||||
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.DailyLogRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.DashboardDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.DashboardDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.FastingLogRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.InsightsDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.InsightsDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.MoodTrendDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.MoodTrendDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.PeriodCycleRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PeriodEntryRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.PeriodEntryRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.PredictionDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.PredictionDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.ReflectionRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.SettingsDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SettingsRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.SettingsRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageRequest;
|
import org.zaine.app.dto.PeriodCompanionDTO.SupportMessageRequest;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SymptomDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.SymptomDTO;
|
||||||
import org.zaine.app.dto.PeriodCompanionDTO.SymptomTrendDTO;
|
import org.zaine.app.dto.PeriodCompanionDTO.SymptomTrendDTO;
|
||||||
|
import org.zaine.app.dto.PeriodCompanionDTO.WellbeingDTO;
|
||||||
import org.zaine.app.model.PeriodCycle;
|
import org.zaine.app.model.PeriodCycle;
|
||||||
|
import org.zaine.app.model.PeriodDailyReflection;
|
||||||
import org.zaine.app.model.PeriodDailyLog;
|
import org.zaine.app.model.PeriodDailyLog;
|
||||||
|
import org.zaine.app.model.PeriodFastingLog;
|
||||||
import org.zaine.app.model.PeriodPredictionSnapshot;
|
import org.zaine.app.model.PeriodPredictionSnapshot;
|
||||||
import org.zaine.app.model.PeriodSettings;
|
import org.zaine.app.model.PeriodSettings;
|
||||||
import org.zaine.app.model.PeriodSupportMessage;
|
import org.zaine.app.model.PeriodSupportMessage;
|
||||||
import org.zaine.app.model.PeriodSymptom;
|
import org.zaine.app.model.PeriodSymptom;
|
||||||
import org.zaine.app.repositories.PeriodCycleRepository;
|
import org.zaine.app.repositories.PeriodCycleRepository;
|
||||||
|
import org.zaine.app.repositories.PeriodDailyReflectionRepository;
|
||||||
import org.zaine.app.repositories.PeriodDailyLogRepository;
|
import org.zaine.app.repositories.PeriodDailyLogRepository;
|
||||||
|
import org.zaine.app.repositories.PeriodFastingLogRepository;
|
||||||
import org.zaine.app.repositories.PeriodPredictionSnapshotRepository;
|
import org.zaine.app.repositories.PeriodPredictionSnapshotRepository;
|
||||||
import org.zaine.app.repositories.PeriodSettingsRepository;
|
import org.zaine.app.repositories.PeriodSettingsRepository;
|
||||||
import org.zaine.app.repositories.PeriodSupportMessageRepository;
|
import org.zaine.app.repositories.PeriodSupportMessageRepository;
|
||||||
@@ -51,6 +60,8 @@ import java.util.stream.Collectors;
|
|||||||
public class PeriodCompanionService {
|
public class PeriodCompanionService {
|
||||||
private final PeriodCycleRepository cycleRepository;
|
private final PeriodCycleRepository cycleRepository;
|
||||||
private final PeriodDailyLogRepository dailyLogRepository;
|
private final PeriodDailyLogRepository dailyLogRepository;
|
||||||
|
private final PeriodDailyReflectionRepository dailyReflectionRepository;
|
||||||
|
private final PeriodFastingLogRepository fastingLogRepository;
|
||||||
private final PeriodSymptomRepository symptomRepository;
|
private final PeriodSymptomRepository symptomRepository;
|
||||||
private final PeriodPredictionSnapshotRepository predictionRepository;
|
private final PeriodPredictionSnapshotRepository predictionRepository;
|
||||||
private final PeriodSettingsRepository settingsRepository;
|
private final PeriodSettingsRepository settingsRepository;
|
||||||
@@ -60,6 +71,8 @@ public class PeriodCompanionService {
|
|||||||
public PeriodCompanionService(
|
public PeriodCompanionService(
|
||||||
PeriodCycleRepository cycleRepository,
|
PeriodCycleRepository cycleRepository,
|
||||||
PeriodDailyLogRepository dailyLogRepository,
|
PeriodDailyLogRepository dailyLogRepository,
|
||||||
|
PeriodDailyReflectionRepository dailyReflectionRepository,
|
||||||
|
PeriodFastingLogRepository fastingLogRepository,
|
||||||
PeriodSymptomRepository symptomRepository,
|
PeriodSymptomRepository symptomRepository,
|
||||||
PeriodPredictionSnapshotRepository predictionRepository,
|
PeriodPredictionSnapshotRepository predictionRepository,
|
||||||
PeriodSettingsRepository settingsRepository,
|
PeriodSettingsRepository settingsRepository,
|
||||||
@@ -67,6 +80,8 @@ public class PeriodCompanionService {
|
|||||||
PeriodPredictionService predictionService) {
|
PeriodPredictionService predictionService) {
|
||||||
this.cycleRepository = cycleRepository;
|
this.cycleRepository = cycleRepository;
|
||||||
this.dailyLogRepository = dailyLogRepository;
|
this.dailyLogRepository = dailyLogRepository;
|
||||||
|
this.dailyReflectionRepository = dailyReflectionRepository;
|
||||||
|
this.fastingLogRepository = fastingLogRepository;
|
||||||
this.symptomRepository = symptomRepository;
|
this.symptomRepository = symptomRepository;
|
||||||
this.predictionRepository = predictionRepository;
|
this.predictionRepository = predictionRepository;
|
||||||
this.settingsRepository = settingsRepository;
|
this.settingsRepository = settingsRepository;
|
||||||
@@ -230,6 +245,76 @@ public class PeriodCompanionService {
|
|||||||
return new InsightsDTO(cycleTrends, symptomTrends, moodTrends);
|
return new InsightsDTO(cycleTrends, symptomTrends, moodTrends);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WellbeingDTO getWellbeing() {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
||||||
|
PeriodSettings settings = getOrCreateSettings();
|
||||||
|
String phase = predictionService.currentCycle(
|
||||||
|
cycles,
|
||||||
|
today,
|
||||||
|
settings.getAverageCycleLength(),
|
||||||
|
settings.getAveragePeriodLength()).phase();
|
||||||
|
ReflectionDTO todayReflection = dailyReflectionRepository.findByDate(today)
|
||||||
|
.map(this::toReflectionDto)
|
||||||
|
.orElseGet(() -> new ReflectionDTO(null, today, defaultReflectionPrompt(phase), null, null, null));
|
||||||
|
|
||||||
|
return new WellbeingDTO(
|
||||||
|
phase,
|
||||||
|
bodyInsight(phase),
|
||||||
|
supportiveAction(phase),
|
||||||
|
spiritualReminder(phase),
|
||||||
|
fiqhNote(phase),
|
||||||
|
todayReflection,
|
||||||
|
dailyReflectionRepository.findTop14ByOrderByDateDesc().stream().map(this::toReflectionDto).toList(),
|
||||||
|
getFastingLogs(),
|
||||||
|
fastingLogRepository.countByStatus("MISSED"),
|
||||||
|
fastingLogRepository.countByStatus("MADE_UP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FastingLogDTO> getFastingLogs() {
|
||||||
|
return fastingLogRepository.findTop50ByOrderByFastDateDesc().stream().map(this::toFastingLogDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FastingLogDTO saveFastingLog(FastingLogRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "fasting log payload is required");
|
||||||
|
}
|
||||||
|
LocalDate fastDate = requiredDate(request.fastDate(), "fastDate");
|
||||||
|
String status = requiredText(request.status(), "status").toUpperCase(Locale.ROOT);
|
||||||
|
if (!status.equals("MISSED") && !status.equals("MADE_UP")) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "status must be MISSED or MADE_UP");
|
||||||
|
}
|
||||||
|
PeriodFastingLog log = fastingLogRepository.findByFastDate(fastDate).orElseGet(PeriodFastingLog::new);
|
||||||
|
log.setFastDate(fastDate);
|
||||||
|
log.setStatus(status);
|
||||||
|
log.setNotes(blankToNull(request.notes()));
|
||||||
|
return toFastingLogDto(fastingLogRepository.save(log));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReflectionDTO saveReflection(ReflectionRequest request) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "reflection payload is required");
|
||||||
|
}
|
||||||
|
LocalDate date = request.date() == null ? LocalDate.now() : request.date();
|
||||||
|
String prompt = blankToNull(request.prompt());
|
||||||
|
if (prompt == null) {
|
||||||
|
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
||||||
|
PeriodSettings settings = getOrCreateSettings();
|
||||||
|
String phase = predictionService.currentCycle(
|
||||||
|
cycles,
|
||||||
|
date,
|
||||||
|
settings.getAverageCycleLength(),
|
||||||
|
settings.getAveragePeriodLength()).phase();
|
||||||
|
prompt = defaultReflectionPrompt(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
PeriodDailyReflection reflection = dailyReflectionRepository.findByDate(date).orElseGet(PeriodDailyReflection::new);
|
||||||
|
reflection.setDate(date);
|
||||||
|
reflection.setPrompt(prompt);
|
||||||
|
reflection.setResponse(blankToNull(request.response()));
|
||||||
|
return toReflectionDto(dailyReflectionRepository.save(reflection));
|
||||||
|
}
|
||||||
|
|
||||||
public SettingsDTO getSettings() {
|
public SettingsDTO getSettings() {
|
||||||
return toSettingsDto(getOrCreateSettings());
|
return toSettingsDto(getOrCreateSettings());
|
||||||
}
|
}
|
||||||
@@ -366,10 +451,71 @@ public class PeriodCompanionService {
|
|||||||
return new SupportMessageDTO(message.getId(), message.getAuthorName(), message.getMessage(), message.getCreatedAt());
|
return new SupportMessageDTO(message.getId(), message.getAuthorName(), message.getMessage(), message.getCreatedAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FastingLogDTO toFastingLogDto(PeriodFastingLog log) {
|
||||||
|
return new FastingLogDTO(log.getId(), log.getFastDate(), log.getStatus(), log.getNotes(), log.getCreatedAt(), log.getUpdatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReflectionDTO toReflectionDto(PeriodDailyReflection reflection) {
|
||||||
|
return new ReflectionDTO(
|
||||||
|
reflection.getId(),
|
||||||
|
reflection.getDate(),
|
||||||
|
reflection.getPrompt(),
|
||||||
|
reflection.getResponse(),
|
||||||
|
reflection.getCreatedAt(),
|
||||||
|
reflection.getUpdatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
private SettingsDTO toSettingsDto(PeriodSettings settings) {
|
private SettingsDTO toSettingsDto(PeriodSettings settings) {
|
||||||
return new SettingsDTO(settings.getAverageCycleLength(), settings.getAveragePeriodLength(), settings.getUpdatedAt());
|
return new SettingsDTO(settings.getAverageCycleLength(), settings.getAveragePeriodLength(), settings.getUpdatedAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String bodyInsight(String phase) {
|
||||||
|
return switch (phase) {
|
||||||
|
case "Menstrual" -> "Energy can be lower during hayd. Rest, warmth, hydration, and lighter plans may feel better.";
|
||||||
|
case "Follicular" -> "Energy often rebuilds after bleeding. This can be a good time for planning, movement, and fresh routines.";
|
||||||
|
case "Ovulation" -> "Some people feel more social and alert around ovulation, while others notice cramps or tenderness.";
|
||||||
|
case "Luteal" -> "Mood, sleep, cravings, and focus can shift in the luteal phase. Gentler expectations can help.";
|
||||||
|
default -> "Track a few cycles and the guidance will become more specific to your rhythm.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String supportiveAction(String phase) {
|
||||||
|
return switch (phase) {
|
||||||
|
case "Menstrual" -> "Choose one kind thing for the body today: heat, a slow walk, extra water, or an early night.";
|
||||||
|
case "Follicular" -> "Pick one task that benefits future you while your energy is returning.";
|
||||||
|
case "Ovulation" -> "Use the clearer moments for connection, but keep space for rest if your body asks for it.";
|
||||||
|
case "Luteal" -> "Reduce friction where possible: prepare simple meals, lower the pace, and write down what feels heavy.";
|
||||||
|
default -> "Make one small note about what your body is telling you today.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String spiritualReminder(String phase) {
|
||||||
|
return switch (phase) {
|
||||||
|
case "Menstrual" -> "Even when salah and fasting pause, dhikr, dua, gratitude, and reflection remain open doors.";
|
||||||
|
case "Follicular" -> "As energy returns, renew intentions gently rather than trying to catch up all at once.";
|
||||||
|
case "Ovulation" -> "Use moments of ease for shukr and sincere dua.";
|
||||||
|
case "Luteal" -> "When emotions feel louder, small consistent remembrance can be grounding.";
|
||||||
|
default -> "Notice the body as an amanah and meet it with mercy.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fiqhNote(String phase) {
|
||||||
|
if ("Menstrual".equals(phase)) {
|
||||||
|
return "Hayd-related rulings can vary by madhhab and situation. For spotting, istihadah, ghusl, or confusing bleeding, check a trusted scholar or teacher.";
|
||||||
|
}
|
||||||
|
return "For fasting make-ups, spotting, istihadah, and ghusl questions, use this as a reminder tool and confirm details with trusted fiqh guidance.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultReflectionPrompt(String phase) {
|
||||||
|
return switch (phase) {
|
||||||
|
case "Menstrual" -> "What would mercy toward your body look like today?";
|
||||||
|
case "Follicular" -> "What intention do you want to rebuild gently this week?";
|
||||||
|
case "Ovulation" -> "Where can you use today’s energy with gratitude and balance?";
|
||||||
|
case "Luteal" -> "What can you simplify before your next period begins?";
|
||||||
|
default -> "What did you notice in your body and heart today?";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private void validateRange(LocalDate from, LocalDate to) {
|
private void validateRange(LocalDate from, LocalDate to) {
|
||||||
requiredDate(from, "from");
|
requiredDate(from, "from");
|
||||||
requiredDate(to, "to");
|
requiredDate(to, "to");
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS pc_fasting_logs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
fast_date DATE NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT chk_pc_fasting_logs_status CHECK (status IN ('MISSED', 'MADE_UP'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pc_fasting_logs_fast_date
|
||||||
|
ON pc_fasting_logs (fast_date DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pc_daily_reflections (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
date DATE NOT NULL UNIQUE,
|
||||||
|
prompt VARCHAR(255) NOT NULL,
|
||||||
|
response TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pc_daily_reflections_date
|
||||||
|
ON pc_daily_reflections (date DESC);
|
||||||
|
|
||||||
|
INSERT INTO pc_symptoms (name)
|
||||||
|
VALUES
|
||||||
|
('Acne'),
|
||||||
|
('Appetite changes'),
|
||||||
|
('Constipation'),
|
||||||
|
('Diarrhea'),
|
||||||
|
('Dizziness'),
|
||||||
|
('Insomnia'),
|
||||||
|
('Joint pain'),
|
||||||
|
('Mood swings'),
|
||||||
|
('Pelvic pain'),
|
||||||
|
('Sleepiness'),
|
||||||
|
('Spotting'),
|
||||||
|
('Tenderness'),
|
||||||
|
('Digestive discomfort'),
|
||||||
|
('Sugar cravings')
|
||||||
|
ON CONFLICT (name) DO NOTHING;
|
||||||
Reference in New Issue
Block a user