feat(sprint-6): Phase 5 — Full grow calendar (sensors, photos, feeding, harvest traceability)
Deploy to Production / test (push) Has been cancelled
Deploy to Production / deploy (push) Has been cancelled

- 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:
Patrick Plate
2026-06-12 22:51:45 +02:00
parent 05933a08ca
commit 076fd6f9b3
34 changed files with 1843 additions and 2 deletions
@@ -0,0 +1,73 @@
package de.cannamanage.domain.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "feeding_logs")
public class FeedingLog {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id", nullable = false, updatable = false)
private UUID id;
@Column(name = "grow_entry_id", nullable = false)
private UUID growEntryId;
@Column(name = "nutrient_name", nullable = false, length = 100)
private String nutrientName;
@Column(name = "amount_ml", nullable = false, precision = 8, scale = 1)
private BigDecimal amountMl;
@Column(name = "water_liters", precision = 8, scale = 1)
private BigDecimal waterLiters;
@Column(name = "ph_after", precision = 4, scale = 2)
private BigDecimal phAfter;
@Column(name = "ec_after", precision = 6, scale = 2)
private BigDecimal ecAfter;
@Column(name = "fed_at", nullable = false)
private Instant fedAt;
@Column(name = "notes", columnDefinition = "TEXT")
private String notes;
@PrePersist
void onCreate() {
if (this.fedAt == null) this.fedAt = Instant.now();
}
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public UUID getGrowEntryId() { return growEntryId; }
public void setGrowEntryId(UUID growEntryId) { this.growEntryId = growEntryId; }
public String getNutrientName() { return nutrientName; }
public void setNutrientName(String nutrientName) { this.nutrientName = nutrientName; }
public BigDecimal getAmountMl() { return amountMl; }
public void setAmountMl(BigDecimal amountMl) { this.amountMl = amountMl; }
public BigDecimal getWaterLiters() { return waterLiters; }
public void setWaterLiters(BigDecimal waterLiters) { this.waterLiters = waterLiters; }
public BigDecimal getPhAfter() { return phAfter; }
public void setPhAfter(BigDecimal phAfter) { this.phAfter = phAfter; }
public BigDecimal getEcAfter() { return ecAfter; }
public void setEcAfter(BigDecimal ecAfter) { this.ecAfter = ecAfter; }
public Instant getFedAt() { return fedAt; }
public void setFedAt(Instant fedAt) { this.fedAt = fedAt; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
}
@@ -0,0 +1,85 @@
package de.cannamanage.domain.entity;
import de.cannamanage.domain.enums.GrowStage;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "grow_entries")
public class GrowEntry extends AbstractTenantEntity {
@Column(name = "name", nullable = false)
private String name;
@Column(name = "strain_id")
private UUID strainId;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 30)
private GrowStage status = GrowStage.SEEDLING;
@Column(name = "started_at", nullable = false)
private Instant startedAt;
@Column(name = "expected_harvest_at")
private Instant expectedHarvestAt;
@Column(name = "actual_harvest_at")
private Instant actualHarvestAt;
@Column(name = "harvested_grams", precision = 8, scale = 1)
private BigDecimal harvestedGrams;
@Column(name = "linked_batch_id")
private UUID linkedBatchId;
@Column(name = "notes", columnDefinition = "TEXT")
private String notes;
@Column(name = "updated_at")
private Instant updatedAt;
@PrePersist
void onCreateGrow() {
if (this.startedAt == null) this.startedAt = Instant.now();
this.updatedAt = Instant.now();
}
@PreUpdate
void onUpdate() {
this.updatedAt = Instant.now();
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public UUID getStrainId() { return strainId; }
public void setStrainId(UUID strainId) { this.strainId = strainId; }
public GrowStage getStatus() { return status; }
public void setStatus(GrowStage status) { this.status = status; }
public Instant getStartedAt() { return startedAt; }
public void setStartedAt(Instant startedAt) { this.startedAt = startedAt; }
public Instant getExpectedHarvestAt() { return expectedHarvestAt; }
public void setExpectedHarvestAt(Instant expectedHarvestAt) { this.expectedHarvestAt = expectedHarvestAt; }
public Instant getActualHarvestAt() { return actualHarvestAt; }
public void setActualHarvestAt(Instant actualHarvestAt) { this.actualHarvestAt = actualHarvestAt; }
public BigDecimal getHarvestedGrams() { return harvestedGrams; }
public void setHarvestedGrams(BigDecimal harvestedGrams) { this.harvestedGrams = harvestedGrams; }
public UUID getLinkedBatchId() { return linkedBatchId; }
public void setLinkedBatchId(UUID linkedBatchId) { this.linkedBatchId = linkedBatchId; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
public Instant getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
}
@@ -0,0 +1,48 @@
package de.cannamanage.domain.entity;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "grow_photos")
public class GrowPhoto {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id", nullable = false, updatable = false)
private UUID id;
@Column(name = "grow_entry_id", nullable = false)
private UUID growEntryId;
@Column(name = "file_path", nullable = false, length = 500)
private String filePath;
@Column(name = "caption", length = 255)
private String caption;
@Column(name = "taken_at", nullable = false)
private Instant takenAt;
@PrePersist
void onCreate() {
if (this.takenAt == null) this.takenAt = Instant.now();
}
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public UUID getGrowEntryId() { return growEntryId; }
public void setGrowEntryId(UUID growEntryId) { this.growEntryId = growEntryId; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public String getCaption() { return caption; }
public void setCaption(String caption) { this.caption = caption; }
public Instant getTakenAt() { return takenAt; }
public void setTakenAt(Instant takenAt) { this.takenAt = takenAt; }
}
@@ -0,0 +1,56 @@
package de.cannamanage.domain.entity;
import de.cannamanage.domain.enums.GrowStage;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "grow_stage_logs")
public class GrowStageLog {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id", nullable = false, updatable = false)
private UUID id;
@Column(name = "grow_entry_id", nullable = false)
private UUID growEntryId;
@Enumerated(EnumType.STRING)
@Column(name = "stage", nullable = false, length = 30)
private GrowStage stage;
@Column(name = "started_at", nullable = false)
private Instant startedAt;
@Column(name = "ended_at")
private Instant endedAt;
@Column(name = "notes", columnDefinition = "TEXT")
private String notes;
@PrePersist
void onCreate() {
if (this.startedAt == null) this.startedAt = Instant.now();
}
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public UUID getGrowEntryId() { return growEntryId; }
public void setGrowEntryId(UUID growEntryId) { this.growEntryId = growEntryId; }
public GrowStage getStage() { return stage; }
public void setStage(GrowStage stage) { this.stage = stage; }
public Instant getStartedAt() { return startedAt; }
public void setStartedAt(Instant startedAt) { this.startedAt = startedAt; }
public Instant getEndedAt() { return endedAt; }
public void setEndedAt(Instant endedAt) { this.endedAt = endedAt; }
public String getNotes() { return notes; }
public void setNotes(String notes) { this.notes = notes; }
}
@@ -0,0 +1,57 @@
package de.cannamanage.domain.entity;
import de.cannamanage.domain.enums.SensorReadingType;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "sensor_readings")
public class SensorReading {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id", nullable = false, updatable = false)
private UUID id;
@Column(name = "grow_entry_id", nullable = false)
private UUID growEntryId;
@Enumerated(EnumType.STRING)
@Column(name = "reading_type", nullable = false, length = 30)
private SensorReadingType readingType;
@Column(name = "value", nullable = false, precision = 8, scale = 2)
private BigDecimal value;
@Column(name = "unit", nullable = false, length = 10)
private String unit;
@Column(name = "recorded_at", nullable = false)
private Instant recordedAt;
@PrePersist
void onCreate() {
if (this.recordedAt == null) this.recordedAt = Instant.now();
}
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public UUID getGrowEntryId() { return growEntryId; }
public void setGrowEntryId(UUID growEntryId) { this.growEntryId = growEntryId; }
public SensorReadingType getReadingType() { return readingType; }
public void setReadingType(SensorReadingType readingType) { this.readingType = readingType; }
public BigDecimal getValue() { return value; }
public void setValue(BigDecimal value) { this.value = value; }
public String getUnit() { return unit; }
public void setUnit(String unit) { this.unit = unit; }
public Instant getRecordedAt() { return recordedAt; }
public void setRecordedAt(Instant recordedAt) { this.recordedAt = recordedAt; }
}
@@ -0,0 +1,11 @@
package de.cannamanage.domain.enums;
public enum GrowStage {
SEEDLING,
VEGETATIVE,
FLOWERING,
HARVEST,
DRYING,
CURING,
COMPLETE
}
@@ -0,0 +1,9 @@
package de.cannamanage.domain.enums;
public enum SensorReadingType {
TEMPERATURE,
HUMIDITY,
CO2,
PH,
EC
}