feat(sprint-6): Phase 5 — Full grow calendar (sensors, photos, feeding, harvest traceability)
- V9 migration: grow_entries, grow_stage_logs, sensor_readings, grow_photos, feeding_logs - 5 entities + GrowStage enum (7 stages) + SensorReadingType enum - GrowCalendarService: CRUD + stage advancement + harvest-to-batch linking - GrowCalendarController: 8 endpoints (/api/v1/grow/*) - Frontend: /grow list + /grow/[id] detail (timeline, sensor charts, photo gallery, feeding log) - Sensor chart (Recharts line: temp + humidity over time) - Harvest completion links grow entry → batch (full traceability) - React Query hooks for all grow operations - Full i18n (de/en) with 7 grow stage labels - Sidebar navigation updated with Anbau/Grow entry
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package de.cannamanage.service;
|
||||
|
||||
import de.cannamanage.domain.entity.*;
|
||||
import de.cannamanage.domain.enums.GrowStage;
|
||||
import de.cannamanage.domain.enums.SensorReadingType;
|
||||
import de.cannamanage.service.repository.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GrowCalendarService {
|
||||
|
||||
private final GrowEntryRepository growEntryRepository;
|
||||
private final GrowStageLogRepository growStageLogRepository;
|
||||
private final SensorReadingRepository sensorReadingRepository;
|
||||
private final GrowPhotoRepository growPhotoRepository;
|
||||
private final FeedingLogRepository feedingLogRepository;
|
||||
private final BatchRepository batchRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<GrowEntry> getGrowEntries() {
|
||||
return growEntryRepository.findAllByOrderByStartedAtDesc();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public GrowEntry getGrowEntry(UUID id) {
|
||||
return growEntryRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Grow entry not found"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GrowEntry createGrowEntry(String name, UUID strainId, String notes, Instant expectedHarvestAt) {
|
||||
GrowEntry entry = new GrowEntry();
|
||||
entry.setName(name);
|
||||
entry.setStrainId(strainId);
|
||||
entry.setStatus(GrowStage.SEEDLING);
|
||||
entry.setStartedAt(Instant.now());
|
||||
entry.setExpectedHarvestAt(expectedHarvestAt);
|
||||
entry.setNotes(notes);
|
||||
|
||||
GrowEntry saved = growEntryRepository.save(entry);
|
||||
|
||||
// Create initial stage log
|
||||
GrowStageLog stageLog = new GrowStageLog();
|
||||
stageLog.setGrowEntryId(saved.getId());
|
||||
stageLog.setStage(GrowStage.SEEDLING);
|
||||
stageLog.setStartedAt(Instant.now());
|
||||
growStageLogRepository.save(stageLog);
|
||||
|
||||
log.debug("Created grow entry: {} with stage SEEDLING", saved.getId());
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GrowEntry advanceStage(UUID entryId, GrowStage newStage) {
|
||||
GrowEntry entry = getGrowEntry(entryId);
|
||||
|
||||
// Close the current stage log
|
||||
List<GrowStageLog> logs = growStageLogRepository.findByGrowEntryIdOrderByStartedAtAsc(entryId);
|
||||
if (!logs.isEmpty()) {
|
||||
GrowStageLog currentLog = logs.get(logs.size() - 1);
|
||||
if (currentLog.getEndedAt() == null) {
|
||||
currentLog.setEndedAt(Instant.now());
|
||||
growStageLogRepository.save(currentLog);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new stage log
|
||||
GrowStageLog newLog = new GrowStageLog();
|
||||
newLog.setGrowEntryId(entryId);
|
||||
newLog.setStage(newStage);
|
||||
newLog.setStartedAt(Instant.now());
|
||||
growStageLogRepository.save(newLog);
|
||||
|
||||
// Update entry status
|
||||
entry.setStatus(newStage);
|
||||
log.debug("Advanced grow entry {} to stage {}", entryId, newStage);
|
||||
return growEntryRepository.save(entry);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SensorReading addSensorReading(UUID entryId, SensorReadingType type, BigDecimal value, String unit) {
|
||||
// Verify entry exists
|
||||
getGrowEntry(entryId);
|
||||
|
||||
SensorReading reading = new SensorReading();
|
||||
reading.setGrowEntryId(entryId);
|
||||
reading.setReadingType(type);
|
||||
reading.setValue(value);
|
||||
reading.setUnit(unit);
|
||||
reading.setRecordedAt(Instant.now());
|
||||
return sensorReadingRepository.save(reading);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GrowPhoto addPhoto(UUID entryId, String filePath, String caption) {
|
||||
getGrowEntry(entryId);
|
||||
|
||||
GrowPhoto photo = new GrowPhoto();
|
||||
photo.setGrowEntryId(entryId);
|
||||
photo.setFilePath(filePath);
|
||||
photo.setCaption(caption);
|
||||
photo.setTakenAt(Instant.now());
|
||||
return growPhotoRepository.save(photo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeedingLog addFeedingLog(UUID entryId, String nutrientName, BigDecimal amountMl,
|
||||
BigDecimal waterLiters, BigDecimal phAfter, BigDecimal ecAfter, String notes) {
|
||||
getGrowEntry(entryId);
|
||||
|
||||
FeedingLog feeding = new FeedingLog();
|
||||
feeding.setGrowEntryId(entryId);
|
||||
feeding.setNutrientName(nutrientName);
|
||||
feeding.setAmountMl(amountMl);
|
||||
feeding.setWaterLiters(waterLiters);
|
||||
feeding.setPhAfter(phAfter);
|
||||
feeding.setEcAfter(ecAfter);
|
||||
feeding.setFedAt(Instant.now());
|
||||
feeding.setNotes(notes);
|
||||
return feedingLogRepository.save(feeding);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GrowEntry completeHarvest(UUID entryId, BigDecimal grams, UUID batchId) {
|
||||
GrowEntry entry = getGrowEntry(entryId);
|
||||
|
||||
if (batchId != null) {
|
||||
batchRepository.findById(batchId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Batch not found"));
|
||||
entry.setLinkedBatchId(batchId);
|
||||
}
|
||||
|
||||
entry.setHarvestedGrams(grams);
|
||||
entry.setActualHarvestAt(Instant.now());
|
||||
entry.setStatus(GrowStage.COMPLETE);
|
||||
|
||||
// Close stage log
|
||||
List<GrowStageLog> logs = growStageLogRepository.findByGrowEntryIdOrderByStartedAtAsc(entryId);
|
||||
if (!logs.isEmpty()) {
|
||||
GrowStageLog currentLog = logs.get(logs.size() - 1);
|
||||
if (currentLog.getEndedAt() == null) {
|
||||
currentLog.setEndedAt(Instant.now());
|
||||
growStageLogRepository.save(currentLog);
|
||||
}
|
||||
}
|
||||
|
||||
// Create COMPLETE stage log
|
||||
GrowStageLog completeLog = new GrowStageLog();
|
||||
completeLog.setGrowEntryId(entryId);
|
||||
completeLog.setStage(GrowStage.COMPLETE);
|
||||
completeLog.setStartedAt(Instant.now());
|
||||
growStageLogRepository.save(completeLog);
|
||||
|
||||
log.debug("Completed harvest for grow entry {}: {}g", entryId, grams);
|
||||
return growEntryRepository.save(entry);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<GrowStageLog> getStageLogs(UUID entryId) {
|
||||
return growStageLogRepository.findByGrowEntryIdOrderByStartedAtAsc(entryId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<SensorReading> getSensorReadings(UUID entryId) {
|
||||
return sensorReadingRepository.findByGrowEntryIdOrderByRecordedAtDesc(entryId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<GrowPhoto> getPhotos(UUID entryId) {
|
||||
return growPhotoRepository.findByGrowEntryIdOrderByTakenAtDesc(entryId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<FeedingLog> getFeedingLogs(UUID entryId) {
|
||||
return feedingLogRepository.findByGrowEntryIdOrderByFedAtDesc(entryId);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.FeedingLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FeedingLogRepository extends JpaRepository<FeedingLog, UUID> {
|
||||
List<FeedingLog> findByGrowEntryIdOrderByFedAtDesc(UUID growEntryId);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.GrowEntry;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GrowEntryRepository extends JpaRepository<GrowEntry, UUID> {
|
||||
List<GrowEntry> findAllByOrderByStartedAtDesc();
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.GrowPhoto;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GrowPhotoRepository extends JpaRepository<GrowPhoto, UUID> {
|
||||
List<GrowPhoto> findByGrowEntryIdOrderByTakenAtDesc(UUID growEntryId);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.GrowStageLog;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface GrowStageLogRepository extends JpaRepository<GrowStageLog, UUID> {
|
||||
List<GrowStageLog> findByGrowEntryIdOrderByStartedAtAsc(UUID growEntryId);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package de.cannamanage.service.repository;
|
||||
|
||||
import de.cannamanage.domain.entity.SensorReading;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface SensorReadingRepository extends JpaRepository<SensorReading, UUID> {
|
||||
List<SensorReading> findByGrowEntryIdOrderByRecordedAtDesc(UUID growEntryId);
|
||||
}
|
||||
Reference in New Issue
Block a user