diff --git a/src/main/java/org/zaine/app/controller/CalendarController.java b/src/main/java/org/zaine/app/controller/CalendarController.java new file mode 100644 index 0000000..c6737d4 --- /dev/null +++ b/src/main/java/org/zaine/app/controller/CalendarController.java @@ -0,0 +1,65 @@ +package org.zaine.app.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.zaine.app.dto.CalendarEventDTO; +import org.zaine.app.dto.CreateCalendarEventDTO; +import org.zaine.app.service.CalendarService; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/api/calendar") +@Tag(name = "Calendar", description = "Calendar events exposed to clients") +public class CalendarController { + + private final CalendarService calendarService; + + public CalendarController(CalendarService calendarService) { + this.calendarService = calendarService; + } + + @Operation(summary = "Get today's calendar events") + @GetMapping("/today") + public List getTodayEvents() { + return calendarService.getTodayEvents(); + } + + @Operation(summary = "Get calendar events for a month") + @GetMapping("/month") + public List getMonthEvents( + @RequestParam int year, + @RequestParam int month) { + return calendarService.getMonthEvents(year, month); + } + + @Operation(summary = "Get calendar events for a date range") + @GetMapping("/range") + public List getRangeEvents( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) { + return calendarService.getEventsForRange(from, to); + } + + @Operation(summary = "Get one calendar event") + @GetMapping("/events/{id}") + public CalendarEventDTO getEvent(@PathVariable Long id) { + return calendarService.getEvent(id); + } + + @Operation(summary = "Create a calendar event") + @PostMapping("/events") + public ResponseEntity createEvent(@RequestBody CreateCalendarEventDTO dto) { + return ResponseEntity.ok(calendarService.createEvent(dto)); + } +} diff --git a/src/main/java/org/zaine/app/controller/CheckinController.java b/src/main/java/org/zaine/app/controller/CheckinController.java new file mode 100644 index 0000000..1768a93 --- /dev/null +++ b/src/main/java/org/zaine/app/controller/CheckinController.java @@ -0,0 +1,44 @@ +package org.zaine.app.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.zaine.app.dto.EveningCheckinRequest; +import org.zaine.app.dto.MorningCheckinRequest; +import org.zaine.app.dto.TodayStatusDTO; +import org.zaine.app.service.CheckinService; + +@RestController +@RequestMapping("/api/checkin") +@Tag(name = "Check-in", description = "Daily morning and evening check-ins") +public class CheckinController { + + private final CheckinService checkinService; + + public CheckinController(CheckinService checkinService) { + this.checkinService = checkinService; + } + + @Operation(summary = "Get today's check-in status") + @GetMapping("/today-status") + public TodayStatusDTO getTodayStatus() { + return checkinService.getTodayStatus(); + } + + @Operation(summary = "Submit morning check-in") + @PostMapping("/morning") + public ResponseEntity submitMorning(@RequestBody MorningCheckinRequest request) { + return ResponseEntity.ok(checkinService.saveMorning(request)); + } + + @Operation(summary = "Submit evening check-in") + @PostMapping("/evening") + public ResponseEntity submitEvening(@RequestBody EveningCheckinRequest request) { + return ResponseEntity.ok(checkinService.saveEvening(request)); + } +} diff --git a/src/main/java/org/zaine/app/dto/CalendarEventDTO.java b/src/main/java/org/zaine/app/dto/CalendarEventDTO.java new file mode 100644 index 0000000..91f44c6 --- /dev/null +++ b/src/main/java/org/zaine/app/dto/CalendarEventDTO.java @@ -0,0 +1,15 @@ +package org.zaine.app.dto; + +import java.time.OffsetDateTime; + +public record CalendarEventDTO( + Long id, + String externalId, + String title, + String description, + String location, + OffsetDateTime startsAt, + OffsetDateTime endsAt, + boolean allDay, + String source) { +} diff --git a/src/main/java/org/zaine/app/dto/CreateCalendarEventDTO.java b/src/main/java/org/zaine/app/dto/CreateCalendarEventDTO.java new file mode 100644 index 0000000..7142bd6 --- /dev/null +++ b/src/main/java/org/zaine/app/dto/CreateCalendarEventDTO.java @@ -0,0 +1,14 @@ +package org.zaine.app.dto; + +import java.time.OffsetDateTime; + +public record CreateCalendarEventDTO( + String externalId, + String title, + String description, + String location, + OffsetDateTime startsAt, + OffsetDateTime endsAt, + boolean allDay, + String source) { +} diff --git a/src/main/java/org/zaine/app/dto/EveningCheckinRequest.java b/src/main/java/org/zaine/app/dto/EveningCheckinRequest.java new file mode 100644 index 0000000..df121db --- /dev/null +++ b/src/main/java/org/zaine/app/dto/EveningCheckinRequest.java @@ -0,0 +1,11 @@ +package org.zaine.app.dto; + +import java.time.LocalDate; + +public record EveningCheckinRequest( + LocalDate date, + Integer mood, + Integer stressLevel, + String reflection, + String bestThingToday) { +} diff --git a/src/main/java/org/zaine/app/dto/MorningCheckinRequest.java b/src/main/java/org/zaine/app/dto/MorningCheckinRequest.java new file mode 100644 index 0000000..5f19e5e --- /dev/null +++ b/src/main/java/org/zaine/app/dto/MorningCheckinRequest.java @@ -0,0 +1,12 @@ +package org.zaine.app.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public record MorningCheckinRequest( + LocalDate date, + BigDecimal sleepHours, + Integer energyLevel, + Integer mood, + String note) { +} diff --git a/src/main/java/org/zaine/app/dto/TodayStatusDTO.java b/src/main/java/org/zaine/app/dto/TodayStatusDTO.java new file mode 100644 index 0000000..24fe5f5 --- /dev/null +++ b/src/main/java/org/zaine/app/dto/TodayStatusDTO.java @@ -0,0 +1,18 @@ +package org.zaine.app.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; + +public record TodayStatusDTO( + LocalDate date, + boolean morningDone, + boolean eveningDone, + BigDecimal sleepHours, + Integer morningEnergyLevel, + Integer morningMoodLevel, + Integer eveningMoodLevel, + Integer stressLevel, + OffsetDateTime morningCompletedAt, + OffsetDateTime eveningCompletedAt) { +} diff --git a/src/main/java/org/zaine/app/model/CalendarEvent.java b/src/main/java/org/zaine/app/model/CalendarEvent.java new file mode 100644 index 0000000..846860c --- /dev/null +++ b/src/main/java/org/zaine/app/model/CalendarEvent.java @@ -0,0 +1,83 @@ +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.OffsetDateTime; + +@Entity +@Table(name = "calendar_events") +public class CalendarEvent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "external_id", unique = true) + private String externalId; + + @Column(nullable = false) + private String title; + + @Column + private String description; + + @Column + private String location; + + @Column(name = "starts_at", nullable = false) + private OffsetDateTime startsAt; + + @Column(name = "ends_at", nullable = false) + private OffsetDateTime endsAt; + + @Column(name = "all_day", nullable = false) + private boolean allDay; + + @Column(nullable = false) + private String source = "manual"; + + @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 String getExternalId() { return externalId; } + public void setExternalId(String externalId) { this.externalId = externalId; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getLocation() { return location; } + public void setLocation(String location) { this.location = location; } + public OffsetDateTime getStartsAt() { return startsAt; } + public void setStartsAt(OffsetDateTime startsAt) { this.startsAt = startsAt; } + public OffsetDateTime getEndsAt() { return endsAt; } + public void setEndsAt(OffsetDateTime endsAt) { this.endsAt = endsAt; } + public boolean isAllDay() { return allDay; } + public void setAllDay(boolean allDay) { this.allDay = allDay; } + public String getSource() { return source; } + public void setSource(String source) { this.source = source; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public OffsetDateTime getUpdatedAt() { return updatedAt; } +} diff --git a/src/main/java/org/zaine/app/model/DailyCheckin.java b/src/main/java/org/zaine/app/model/DailyCheckin.java new file mode 100644 index 0000000..813ad16 --- /dev/null +++ b/src/main/java/org/zaine/app/model/DailyCheckin.java @@ -0,0 +1,100 @@ +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.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; + +@Entity +@Table(name = "daily_checkins") +public class DailyCheckin { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private LocalDate date; + + @Column(name = "sleep_hours", precision = 4, scale = 2) + private BigDecimal sleepHours; + + @Column(name = "morning_energy_level") + private Integer morningEnergyLevel; + + @Column(name = "morning_mood_level") + private Integer morningMoodLevel; + + @Column(name = "morning_note") + private String morningNote; + + @Column(name = "morning_completed_at") + private OffsetDateTime morningCompletedAt; + + @Column(name = "evening_mood_level") + private Integer eveningMoodLevel; + + @Column(name = "stress_level") + private Integer stressLevel; + + @Column(name = "reflection", columnDefinition = "text") + private String reflection; + + @Column(name = "best_thing_today") + private String bestThingToday; + + @Column(name = "evening_completed_at") + private OffsetDateTime eveningCompletedAt; + + @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 BigDecimal getSleepHours() { return sleepHours; } + public void setSleepHours(BigDecimal sleepHours) { this.sleepHours = sleepHours; } + public Integer getMorningEnergyLevel() { return morningEnergyLevel; } + public void setMorningEnergyLevel(Integer morningEnergyLevel) { this.morningEnergyLevel = morningEnergyLevel; } + public Integer getMorningMoodLevel() { return morningMoodLevel; } + public void setMorningMoodLevel(Integer morningMoodLevel) { this.morningMoodLevel = morningMoodLevel; } + public String getMorningNote() { return morningNote; } + public void setMorningNote(String morningNote) { this.morningNote = morningNote; } + public OffsetDateTime getMorningCompletedAt() { return morningCompletedAt; } + public void setMorningCompletedAt(OffsetDateTime morningCompletedAt) { this.morningCompletedAt = morningCompletedAt; } + public Integer getEveningMoodLevel() { return eveningMoodLevel; } + public void setEveningMoodLevel(Integer eveningMoodLevel) { this.eveningMoodLevel = eveningMoodLevel; } + public Integer getStressLevel() { return stressLevel; } + public void setStressLevel(Integer stressLevel) { this.stressLevel = stressLevel; } + public String getReflection() { return reflection; } + public void setReflection(String reflection) { this.reflection = reflection; } + public String getBestThingToday() { return bestThingToday; } + public void setBestThingToday(String bestThingToday) { this.bestThingToday = bestThingToday; } + public OffsetDateTime getEveningCompletedAt() { return eveningCompletedAt; } + public void setEveningCompletedAt(OffsetDateTime eveningCompletedAt) { this.eveningCompletedAt = eveningCompletedAt; } + public OffsetDateTime getCreatedAt() { return createdAt; } + public OffsetDateTime getUpdatedAt() { return updatedAt; } +} diff --git a/src/main/java/org/zaine/app/repositories/CalendarEventRepository.java b/src/main/java/org/zaine/app/repositories/CalendarEventRepository.java new file mode 100644 index 0000000..af3ebb4 --- /dev/null +++ b/src/main/java/org/zaine/app/repositories/CalendarEventRepository.java @@ -0,0 +1,13 @@ +package org.zaine.app.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.zaine.app.model.CalendarEvent; + +import java.time.OffsetDateTime; +import java.util.List; + +public interface CalendarEventRepository extends JpaRepository { + List findByStartsAtLessThanAndEndsAtGreaterThanOrderByStartsAtAsc( + OffsetDateTime rangeEnd, + OffsetDateTime rangeStart); +} diff --git a/src/main/java/org/zaine/app/repositories/DailyCheckinRepository.java b/src/main/java/org/zaine/app/repositories/DailyCheckinRepository.java new file mode 100644 index 0000000..90be9f3 --- /dev/null +++ b/src/main/java/org/zaine/app/repositories/DailyCheckinRepository.java @@ -0,0 +1,11 @@ +package org.zaine.app.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.zaine.app.model.DailyCheckin; + +import java.time.LocalDate; +import java.util.Optional; + +public interface DailyCheckinRepository extends JpaRepository { + Optional findByDate(LocalDate date); +} diff --git a/src/main/java/org/zaine/app/service/CalendarService.java b/src/main/java/org/zaine/app/service/CalendarService.java new file mode 100644 index 0000000..b778c4a --- /dev/null +++ b/src/main/java/org/zaine/app/service/CalendarService.java @@ -0,0 +1,99 @@ +package org.zaine.app.service; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import org.zaine.app.dto.CalendarEventDTO; +import org.zaine.app.dto.CreateCalendarEventDTO; +import org.zaine.app.model.CalendarEvent; +import org.zaine.app.repositories.CalendarEventRepository; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.TemporalAdjusters; +import java.util.List; + +@Service +public class CalendarService { + + private final CalendarEventRepository calendarEventRepository; + + public CalendarService(CalendarEventRepository calendarEventRepository) { + this.calendarEventRepository = calendarEventRepository; + } + + public List getTodayEvents() { + LocalDate today = LocalDate.now(); + return getEventsForRange(today, today.plusDays(1)); + } + + public List getMonthEvents(int year, int month) { + LocalDate start = LocalDate.of(year, month, 1); + LocalDate end = start.with(TemporalAdjusters.firstDayOfNextMonth()); + return getEventsForRange(start, end); + } + + public List getEventsForRange(LocalDate from, LocalDate to) { + if (to.isBefore(from)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "to must be on or after from"); + } + + ZoneId zone = ZoneId.systemDefault(); + OffsetDateTime rangeStart = from.atStartOfDay(zone).toOffsetDateTime(); + OffsetDateTime rangeEnd = to.atStartOfDay(zone).toOffsetDateTime(); + + return calendarEventRepository + .findByStartsAtLessThanAndEndsAtGreaterThanOrderByStartsAtAsc(rangeEnd, rangeStart) + .stream() + .map(this::toDto) + .toList(); + } + + public CalendarEventDTO getEvent(Long id) { + CalendarEvent event = calendarEventRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Calendar event not found")); + return toDto(event); + } + + public CalendarEventDTO createEvent(CreateCalendarEventDTO dto) { + if (dto.title() == null || dto.title().isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "title is required"); + } + if (dto.startsAt() == null || dto.endsAt() == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "startsAt and endsAt are required"); + } + if (!dto.endsAt().isAfter(dto.startsAt())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "endsAt must be after startsAt"); + } + + CalendarEvent event = new CalendarEvent(); + event.setExternalId(blankToNull(dto.externalId())); + event.setTitle(dto.title().trim()); + event.setDescription(blankToNull(dto.description())); + event.setLocation(blankToNull(dto.location())); + event.setStartsAt(dto.startsAt()); + event.setEndsAt(dto.endsAt()); + event.setAllDay(dto.allDay()); + event.setSource(dto.source() == null || dto.source().isBlank() ? "manual" : dto.source().trim()); + + return toDto(calendarEventRepository.save(event)); + } + + private CalendarEventDTO toDto(CalendarEvent event) { + return new CalendarEventDTO( + event.getId(), + event.getExternalId(), + event.getTitle(), + event.getDescription(), + event.getLocation(), + event.getStartsAt(), + event.getEndsAt(), + event.isAllDay(), + event.getSource()); + } + + private String blankToNull(String value) { + return value == null || value.isBlank() ? null : value.trim(); + } +} diff --git a/src/main/java/org/zaine/app/service/CheckinService.java b/src/main/java/org/zaine/app/service/CheckinService.java new file mode 100644 index 0000000..d57157e --- /dev/null +++ b/src/main/java/org/zaine/app/service/CheckinService.java @@ -0,0 +1,97 @@ +package org.zaine.app.service; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import org.zaine.app.dto.EveningCheckinRequest; +import org.zaine.app.dto.MorningCheckinRequest; +import org.zaine.app.dto.TodayStatusDTO; +import org.zaine.app.model.DailyCheckin; +import org.zaine.app.repositories.DailyCheckinRepository; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; + +@Service +public class CheckinService { + + private final DailyCheckinRepository dailyCheckinRepository; + + public CheckinService(DailyCheckinRepository dailyCheckinRepository) { + this.dailyCheckinRepository = dailyCheckinRepository; + } + + public TodayStatusDTO getTodayStatus() { + return toStatus(getOrCreate(LocalDate.now())); + } + + public TodayStatusDTO saveMorning(MorningCheckinRequest request) { + LocalDate date = request.date() == null ? LocalDate.now() : request.date(); + validateScore(request.energyLevel(), "energyLevel"); + validateScore(request.mood(), "mood"); + validateSleepHours(request.sleepHours()); + + DailyCheckin checkin = getOrCreate(date); + checkin.setSleepHours(request.sleepHours()); + checkin.setMorningEnergyLevel(request.energyLevel()); + checkin.setMorningMoodLevel(request.mood()); + checkin.setMorningNote(blankToNull(request.note())); + checkin.setMorningCompletedAt(OffsetDateTime.now()); + + return toStatus(dailyCheckinRepository.save(checkin)); + } + + public TodayStatusDTO saveEvening(EveningCheckinRequest request) { + LocalDate date = request.date() == null ? LocalDate.now() : request.date(); + validateScore(request.mood(), "mood"); + validateScore(request.stressLevel(), "stressLevel"); + + DailyCheckin checkin = getOrCreate(date); + checkin.setEveningMoodLevel(request.mood()); + checkin.setStressLevel(request.stressLevel()); + checkin.setReflection(blankToNull(request.reflection())); + checkin.setBestThingToday(blankToNull(request.bestThingToday())); + checkin.setEveningCompletedAt(OffsetDateTime.now()); + + return toStatus(dailyCheckinRepository.save(checkin)); + } + + private DailyCheckin getOrCreate(LocalDate date) { + return dailyCheckinRepository.findByDate(date).orElseGet(() -> { + DailyCheckin checkin = new DailyCheckin(); + checkin.setDate(date); + return checkin; + }); + } + + private TodayStatusDTO toStatus(DailyCheckin checkin) { + return new TodayStatusDTO( + checkin.getDate(), + checkin.getMorningCompletedAt() != null, + checkin.getEveningCompletedAt() != null, + checkin.getSleepHours(), + checkin.getMorningEnergyLevel(), + checkin.getMorningMoodLevel(), + checkin.getEveningMoodLevel(), + checkin.getStressLevel(), + checkin.getMorningCompletedAt(), + checkin.getEveningCompletedAt()); + } + + private void validateScore(Integer value, String fieldName) { + if (value == null || value < 1 || value > 10) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, fieldName + " must be between 1 and 10"); + } + } + + private void validateSleepHours(BigDecimal sleepHours) { + if (sleepHours == null || sleepHours.compareTo(BigDecimal.ZERO) < 0 || sleepHours.compareTo(new BigDecimal("24")) > 0) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sleepHours must be between 0 and 24"); + } + } + + private String blankToNull(String value) { + return value == null || value.isBlank() ? null : value.trim(); + } +} diff --git a/src/main/resources/db/migration/V3__calendar_and_daily_checkins.sql b/src/main/resources/db/migration/V3__calendar_and_daily_checkins.sql new file mode 100644 index 0000000..7e39c74 --- /dev/null +++ b/src/main/resources/db/migration/V3__calendar_and_daily_checkins.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS calendar_events ( + id BIGSERIAL PRIMARY KEY, + external_id VARCHAR(255) UNIQUE, + title VARCHAR(255) NOT NULL, + description TEXT, + location VARCHAR(255), + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ NOT NULL, + all_day BOOLEAN NOT NULL DEFAULT FALSE, + source VARCHAR(64) NOT NULL DEFAULT 'manual', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_calendar_events_range + ON calendar_events (starts_at, ends_at); + +CREATE TABLE IF NOT EXISTS daily_checkins ( + id BIGSERIAL PRIMARY KEY, + date DATE NOT NULL UNIQUE, + sleep_hours NUMERIC(4, 2), + morning_energy_level INTEGER, + morning_mood_level INTEGER, + morning_note VARCHAR(255), + morning_completed_at TIMESTAMPTZ, + evening_mood_level INTEGER, + stress_level INTEGER, + reflection TEXT, + best_thing_today VARCHAR(255), + evening_completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT chk_daily_checkins_sleep_hours CHECK (sleep_hours IS NULL OR (sleep_hours >= 0 AND sleep_hours <= 24)), + CONSTRAINT chk_daily_checkins_morning_energy CHECK (morning_energy_level IS NULL OR (morning_energy_level BETWEEN 1 AND 10)), + CONSTRAINT chk_daily_checkins_morning_mood CHECK (morning_mood_level IS NULL OR (morning_mood_level BETWEEN 1 AND 10)), + CONSTRAINT chk_daily_checkins_evening_mood CHECK (evening_mood_level IS NULL OR (evening_mood_level BETWEEN 1 AND 10)), + CONSTRAINT chk_daily_checkins_stress CHECK (stress_level IS NULL OR (stress_level BETWEEN 1 AND 10)) +);