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,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
|
||||
}
|
||||
Reference in New Issue
Block a user