diff --git a/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py b/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py index cc39073..45b541e 100644 --- a/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py +++ b/backend/apps/jobs/management/commands/seed_sukawarna_kandang4.py @@ -283,6 +283,7 @@ class Command(BaseCommand): weight = AVG_WEIGHT[i] prev_weight = AVG_WEIGHT[i - 1] if i > 0 else DOC_IN_WEIGHT adg = _round(weight - prev_weight, 2) if weight else 0.0 + area_cm2 = _round(4.8 * (weight ** 0.66), 1) if weight else 0.0 cw, _ = ChickenWeight.objects.update_or_create( cycle=cycle, date=day, @@ -293,6 +294,7 @@ class Command(BaseCommand): "chicken_count": SAMPLE_COUNT[i], "uniformity": UNIFORMITY[i], "average_daily_gain": adg, + "chicken_area": area_cm2, }, ) cw_by_date[day] = cw diff --git a/backend/apps/jobs/management/commands/seed_sukawarna_kandang5.py b/backend/apps/jobs/management/commands/seed_sukawarna_kandang5.py index 32e976e..8c0d287 100644 --- a/backend/apps/jobs/management/commands/seed_sukawarna_kandang5.py +++ b/backend/apps/jobs/management/commands/seed_sukawarna_kandang5.py @@ -147,16 +147,18 @@ class Command(BaseCommand): cw_by_date = {} for row in sheets["chicken_weight"]: + avg_wt = float(as_number(row.get("average_weight"))) obj, _ = ChickenWeight.objects.update_or_create( cycle=cycle, date=row["date"], defaults={ "age": int(as_number(row.get("age"))), "doc_weight": int(as_number(row.get("doc_weight"))), - "average_weight": float(as_number(row.get("average_weight"))), + "average_weight": avg_wt, "chicken_count": int(as_number(row.get("chicken_count"))), "uniformity": float(as_number(row.get("uniformity"))), "average_daily_gain": float(as_number(row.get("average_daily_gain"))), + "chicken_area": round(4.8 * (avg_wt ** 0.66), 1) if avg_wt > 0 else 0.0, }, ) cw_by_date[row["date"]] = obj diff --git a/backend/apps/operations/migrations/0021_chickenweight_chicken_area_and_more.py b/backend/apps/operations/migrations/0021_chickenweight_chicken_area_and_more.py new file mode 100644 index 0000000..5fac660 --- /dev/null +++ b/backend/apps/operations/migrations/0021_chickenweight_chicken_area_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.17 on 2026-10-07 08:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('operations', '0020_drop_orphan_structured_data'), + ] + + operations = [ + migrations.AddField( + model_name='chickenweight', + name='chicken_area', + field=models.FloatField(default=0.0), + ), + migrations.AlterField( + model_name='chickencounting', + name='cc_id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='chickenweight', + name='cw_id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='feedsacks', + name='fs_id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='iotpanel', + name='panel_id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='kpi', + name='kpi_id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='manualinput', + name='manual_id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + ] diff --git a/backend/apps/operations/models.py b/backend/apps/operations/models.py index 3fb88a4..4401e3b 100644 --- a/backend/apps/operations/models.py +++ b/backend/apps/operations/models.py @@ -105,6 +105,7 @@ class ChickenWeight(models.Model): chicken_count = models.IntegerField() uniformity = models.FloatField() average_daily_gain = models.FloatField() + chicken_area = models.FloatField(default=0.0) cycle = models.ForeignKey(Cycle, on_delete=models.CASCADE, related_name="chicken_weights") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/backend/apps/operations/services/chicken_counting_edge.py b/backend/apps/operations/services/chicken_counting_edge.py index e3e1a02..9106065 100644 --- a/backend/apps/operations/services/chicken_counting_edge.py +++ b/backend/apps/operations/services/chicken_counting_edge.py @@ -116,10 +116,24 @@ def aggregate_weight_by_date( weights = [float(f.get("predicted_weight_g") or 0) for f in floors] unifs = [float(f.get("uniformity") or 0) for f in floors] samples = [int(f.get("n_valid_samples") or 0) for f in floors] + areas = [ + float( + f.get("area_cm2_median") + or f.get("area_cm2") + or f.get("predicted_area_cm2") + or f.get("bbox_area_cm2") + or f.get("area") + or 0 + ) + for f in floors + ] + valid_areas = [a for a in areas if a > 0] + avg_area = sum(valid_areas) / len(valid_areas) if valid_areas else 0.0 by_date[day] = { "average_weight": sum(weights) / len(weights), "uniformity": sum(unifs) / len(unifs), "chicken_count": max(samples) if samples else 0, + "chicken_area": round(avg_area, 2), } return by_date @@ -309,6 +323,7 @@ def sync_chicken_weight_for_cycle( uniformity = 0.0 chicken_count = int(cycle.doc_in_count) adg = 0.0 + chicken_area = 0.0 else: metrics = weight_by_date.get(day) if metrics is None: @@ -317,6 +332,7 @@ def sync_chicken_weight_for_cycle( uniformity = float(metrics["uniformity"]) chicken_count = int(metrics["chicken_count"]) adg = round(average_weight - prev_weight, 2) + chicken_area = float(metrics.get("chicken_area", 0.0)) defaults = { "age": age, @@ -325,6 +341,7 @@ def sync_chicken_weight_for_cycle( "chicken_count": chicken_count, "uniformity": uniformity, "average_daily_gain": adg, + "chicken_area": chicken_area, } ChickenWeight.objects.update_or_create( cycle=cycle, diff --git a/backend/database/ERD_Dashboard_Chicken_Counting.md b/backend/database/ERD_Dashboard_Chicken_Counting.md index c10b351..b5aa229 100644 --- a/backend/database/ERD_Dashboard_Chicken_Counting.md +++ b/backend/database/ERD_Dashboard_Chicken_Counting.md @@ -175,12 +175,7 @@ erDiagram int chicken_count "NOT NULL (n_weight_samples)" float uniformity "NOT NULL (Flock weight uniformity %)" float average_daily_gain "NOT NULL (ADG in grams/day)" - float area_cm2_median "Median body area in cm^2" - float minor_axis_cm_median "Median minor axis / width in cm" - float major_axis_cm_median "Median major axis / length in cm" - float gompertz_baseline_g "Theoretical Gompertz curve baseline" - float vision_gain_g "Allometric vision gain residual" - float rejection_rate "MAD outlier filter rejection percentage" + float chicken_area "NOT NULL DEFAULT 0.0 (Median chicken area in cm^2)" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } @@ -308,8 +303,7 @@ Our edge pipeline aggregates floor camera runs and pushes clean daily rollups di - `chicken_count` $\leftarrow \sum \text{n\_weight\_samples}$ (valid non-outlier tracks). - `uniformity` $\leftarrow \text{flock size uniformity percentage}$. - `average_daily_gain` $\leftarrow \text{daily weight gain in grams/day}$. - - `area_cm2_median`, `minor_axis_cm_median`, `major_axis_cm_median` $\leftarrow \text{calibrated physical dimensions}$. - - `gompertz_baseline_g`, `vision_gain_g` $\leftarrow \text{audited growth model residual breakdown}$. + - `chicken_area` $\leftarrow \text{calibrated median chicken body area in cm²}$. --- diff --git a/backend/database/ERD_dashboard.json b/backend/database/ERD_dashboard.json index f7794ed..5cc5cbf 100644 --- a/backend/database/ERD_dashboard.json +++ b/backend/database/ERD_dashboard.json @@ -1290,6 +1290,22 @@ "parent": "PF_pHT-a65sNfpEKlwq7-159", "label": "average_daily_gain float NOT NULL" }, + { + "id": "PF_pHT-a65sNfpEKlwq7-cw-area", + "type": "node", + "parent": "PF_pHT-a65sNfpEKlwq7-66" + }, + { + "id": "PF_pHT-a65sNfpEKlwq7-cw-area-col1", + "type": "node", + "parent": "PF_pHT-a65sNfpEKlwq7-cw-area" + }, + { + "id": "PF_pHT-a65sNfpEKlwq7-cw-area-col2", + "type": "node", + "parent": "PF_pHT-a65sNfpEKlwq7-cw-area", + "label": "chicken_area float NOT NULL" + }, { "id": "PF_pHT-a65sNfpEKlwq7-321", "type": "node", @@ -2537,4 +2553,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/backend/database/ERD_dashboard.png b/backend/database/ERD_dashboard.png index 2ba6374..e3f6589 100644 Binary files a/backend/database/ERD_dashboard.png and b/backend/database/ERD_dashboard.png differ diff --git a/backend/database/erd_schema.sql b/backend/database/erd_schema.sql index 63a08a7..9a9ca33 100644 --- a/backend/database/erd_schema.sql +++ b/backend/database/erd_schema.sql @@ -147,8 +147,9 @@ CREATE INDEX IF NOT EXISTS idx_cycles_close_requested_by_id ON cycles (close_req -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS flock ( flock_id SERIAL PRIMARY KEY, - flock_name VARCHAR(30) NOT NULL, - kandang_id INTEGER NOT NULL REFERENCES kandang (kandang_id) ON DELETE CASCADE, + flock_name VARCHAR(30) NOT NULL, + iot_api_url VARCHAR(500) NOT NULL DEFAULT '', + kandang_id INTEGER NOT NULL REFERENCES kandang (kandang_id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -259,8 +260,9 @@ CREATE TABLE IF NOT EXISTS chicken_weight ( doc_weight INTEGER NOT NULL, average_weight FLOAT NOT NULL, chicken_count INTEGER NOT NULL, - uniformity FLOAT NOT NULL, - average_daily_gain FLOAT NOT NULL, + uniformity FLOAT NOT NULL, + average_daily_gain FLOAT NOT NULL, + chicken_area FLOAT NOT NULL DEFAULT 0.0, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, cycle_id INTEGER NOT NULL REFERENCES cycles (cycle_id) ON DELETE CASCADE diff --git a/components/WeightMonitoringPage.tsx b/components/WeightMonitoringPage.tsx index 6cb1ae3..872b8e2 100644 --- a/components/WeightMonitoringPage.tsx +++ b/components/WeightMonitoringPage.tsx @@ -154,6 +154,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings age: fallback.age, uniformity: fallback.uniformity, average_daily_gain: fallback.average_daily_gain, + chicken_area: fallback.chicken_area, cycle: fallback.cycle, kandang: fallback.kandang, }; @@ -322,7 +323,15 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings - {['Tanggal', 'Umur', 'DOC', 'AVG', 'Uniformity', 'ADG'].map((h) => ( + {[ + 'Tanggal', + 'Umur', + 'DOC', + 'AVG', + 'Uniformity', + 'ADG', + 'Luas Ayam (IOT)', + ].map((h) => ( @@ -332,7 +341,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings {rows.length === 0 ? ( - @@ -351,6 +360,11 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings + )) )} diff --git a/components/weight/DailyDetailPanel.tsx b/components/weight/DailyDetailPanel.tsx index 146b44d..eb51a5b 100644 --- a/components/weight/DailyDetailPanel.tsx +++ b/components/weight/DailyDetailPanel.tsx @@ -86,6 +86,15 @@ const DailyDetailPanel: React.FC = ({ row, docWeight }) = value={`${formatNumber(row.chicken_count, 0)} ekor`} icon={} /> + 0 + ? `${formatNumber(row.chicken_area, 2, 0, true)} cm²` + : '-' + } + icon={} + /> {deviation != null && (
{h}
+ Belum ada data berat untuk siklus ini.
{formatNumber(row.average_daily_gain, 2, 2, true)} + {row.chicken_area && row.chicken_area > 0 + ? `${formatNumber(row.chicken_area, 2, 0, true)} cm²` + : '-'} +