migrate site id to varchar instead of int

This commit is contained in:
Alberto-Audrix committed 2026-09-15 11:12:09 +07:00
1 parent 61f954a3bd
commit 36639d8c38
16 files changed
+445 -38

No files matched your search

+14 -3
View File
@@ -30,22 +30,32 @@ class VisibilityScopeTests(TestCase):
self.active_a = ActiveSite.objects.create(
code="a",
name="Site A",
source_site_id="site-a-0001",
api_base_url="https://a.example/api/v1",
managed_by=self.gm,
)
self.active_b = ActiveSite.objects.create(
code="b",
name="Site B",
source_site_id="site-b-0001",
api_base_url="https://b.example/api/v1",
managed_by=self.gm_other,
)
self.site = Site.objects.create(
site_name="Site A", user=self.gm, active_site=self.active_a
site_id="site-a-0001",
site_name="Site A",
user=self.gm,
active_site=self.active_a,
)
self.site_other = Site.objects.create(
site_name="Site B", user=self.gm_other, active_site=self.active_b
site_id="site-b-0001",
site_name="Site B",
user=self.gm_other,
active_site=self.active_b,
)
self.orphan = Site.objects.create(
site_id="orphan-seed-0001", site_name="Orphan Seed", user=self.gm
)
self.orphan = Site.objects.create(site_name="Orphan Seed", user=self.gm)
Kandang.objects.create(kandang_name="K1", site=self.site)
Kandang.objects.create(kandang_name="K2", site=self.site_other)
@@ -95,6 +105,7 @@ class VisibilityScopeTests(TestCase):
ActiveSite.objects.create(
code="orphan-seed",
name="Orphan Seed",
source_site_id="orphan-seed-0001",
api_base_url="https://orphan.example/api/v1",
api_key="k",
managed_by=None,
@@ -0,0 +1,238 @@
# Convert HQ sites.site_id from bigint to farm-matching slug (varchar).
from django.db import migrations, models
import django.db.models.deletion
def _unique_slug(candidate: str, used: set[str]) -> str:
final = candidate[:64]
n = 0
while final in used or not final:
n += 1
suffix = f"-{n}"
final = f"{candidate[: 64 - len(suffix)]}{suffix}"
used.add(final)
return final
def forwards_convert_site_pk(apps, schema_editor):
from django.utils.text import slugify
connection = schema_editor.connection
Site = apps.get_model("farms", "Site")
Kandang = apps.get_model("farms", "Kandang")
ActiveSite = apps.get_model("sync", "ActiveSite")
sites = list(
Site.objects.all().values(
"site_id",
"site_name",
"user_id",
"active_site_id",
"created_at",
"updated_at",
)
)
kandangs = list(
Kandang.objects.all().values(
"kandang_id",
"kandang_name",
"feed_in_button_urls",
"site_id",
"created_at",
"updated_at",
)
)
active_source = {
row.pk: str(row.source_site_id).strip()
for row in ActiveSite.objects.all()
if str(row.source_site_id or "").strip()
}
used: set[str] = set()
mapping: dict[int, str] = {}
for row in sites:
old = row["site_id"]
candidate = active_source.get(row["active_site_id"]) or ""
if not candidate:
base = slugify(row["site_name"] or "")[:40] or "site"
candidate = f"{base}-legacy-{old}"
mapping[old] = _unique_slug(candidate, used)
with connection.cursor() as cursor:
if connection.vendor == "sqlite":
cursor.execute("PRAGMA foreign_keys=OFF")
cursor.execute(
"""
CREATE TABLE sites_new (
site_id varchar(64) NOT NULL PRIMARY KEY,
site_name varchar(30) NOT NULL,
user_id bigint NOT NULL REFERENCES user_access (user_id)
DEFERRABLE INITIALLY DEFERRED,
active_site_id bigint UNIQUE REFERENCES active_sites (active_site_id)
DEFERRABLE INITIALLY DEFERRED,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
)
"""
)
for row in sites:
cursor.execute(
"""
INSERT INTO sites_new
(site_id, site_name, user_id, active_site_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
[
mapping[row["site_id"]],
row["site_name"],
row["user_id"],
row["active_site_id"],
row["created_at"],
row["updated_at"],
],
)
cursor.execute("DROP TABLE sites")
cursor.execute("ALTER TABLE sites_new RENAME TO sites")
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_sites_user_id ON sites (user_id)"
)
cursor.execute(
"""
CREATE TABLE kandang_new (
kandang_id integer NOT NULL PRIMARY KEY AUTOINCREMENT,
kandang_name varchar(30) NOT NULL,
feed_in_button_urls text NOT NULL DEFAULT '[]',
site_id varchar(64) NOT NULL REFERENCES sites (site_id)
DEFERRABLE INITIALLY DEFERRED,
created_at datetime NOT NULL,
updated_at datetime NOT NULL
)
"""
)
import json
for row in kandangs:
urls = row["feed_in_button_urls"]
if not isinstance(urls, str):
urls = json.dumps(urls if urls is not None else [])
cursor.execute(
"""
INSERT INTO kandang_new
(kandang_id, kandang_name, feed_in_button_urls, site_id,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
[
row["kandang_id"],
row["kandang_name"],
urls,
mapping[row["site_id"]],
row["created_at"],
row["updated_at"],
],
)
cursor.execute("DROP TABLE kandang")
cursor.execute("ALTER TABLE kandang_new RENAME TO kandang")
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)"
)
cursor.execute("PRAGMA foreign_keys=ON")
elif connection.vendor == "postgresql":
cursor.execute(
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS site_id_new varchar(64)"
)
for old, new in mapping.items():
cursor.execute(
"UPDATE sites SET site_id_new = %s WHERE site_id = %s",
[new, old],
)
cursor.execute(
"ALTER TABLE kandang ADD COLUMN IF NOT EXISTS site_id_new varchar(64)"
)
cursor.execute(
"""
UPDATE kandang AS k
SET site_id_new = s.site_id_new
FROM sites AS s
WHERE k.site_id = s.site_id
"""
)
cursor.execute(
"""
ALTER TABLE kandang DROP CONSTRAINT IF EXISTS kandang_site_id_fkey
"""
)
# Django default name variants
cursor.execute(
"""
DO $$
DECLARE r RECORD;
BEGIN
FOR r IN
SELECT conname FROM pg_constraint
WHERE conrelid = 'kandang'::regclass
AND contype = 'f'
AND pg_get_constraintdef(oid) ILIKE '%%site_id%%'
LOOP
EXECUTE format('ALTER TABLE kandang DROP CONSTRAINT %I', r.conname);
END LOOP;
END $$;
"""
)
cursor.execute("ALTER TABLE kandang DROP COLUMN site_id")
cursor.execute(
"ALTER TABLE kandang RENAME COLUMN site_id_new TO site_id"
)
cursor.execute("ALTER TABLE sites DROP CONSTRAINT sites_pkey")
cursor.execute("ALTER TABLE sites DROP COLUMN site_id")
cursor.execute("ALTER TABLE sites RENAME COLUMN site_id_new TO site_id")
cursor.execute("ALTER TABLE sites ALTER COLUMN site_id SET NOT NULL")
cursor.execute("ALTER TABLE sites ADD PRIMARY KEY (site_id)")
cursor.execute("ALTER TABLE kandang ALTER COLUMN site_id SET NOT NULL")
cursor.execute(
"""
ALTER TABLE kandang
ADD CONSTRAINT kandang_site_id_fkey
FOREIGN KEY (site_id) REFERENCES sites (site_id) ON DELETE CASCADE
"""
)
cursor.execute(
"CREATE INDEX IF NOT EXISTS idx_kandang_site_id ON kandang (site_id)"
)
else:
raise NotImplementedError(
f"sites.site_id slug migration unsupported for {connection.vendor}"
)
class Migration(migrations.Migration):
dependencies = [
("farms", "0018_rename_city_site_to_active_site"),
("sync", "0007_uniq_source_site_id"),
]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.RunPython(forwards_convert_site_pk, migrations.RunPython.noop),
],
state_operations=[
migrations.AlterField(
model_name="site",
name="site_id",
field=models.SlugField(max_length=64, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name="kandang",
name="site",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="kandangs",
to="farms.site",
),
),
],
),
]
+34 -1
View File
@@ -1,14 +1,42 @@
from datetime import date
import secrets
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.text import slugify
from apps.accounts.models import User
def site_name_slug(site_name: str) -> str:
"""Slug portion of a site id (no numeric suffix)."""
code = slugify(str(site_name or "").strip())[:55]
if not code:
raise ValidationError({"site_name": "Nama site tidak valid untuk dijadikan id."})
return code
def site_id_from_name(site_name: str, *, numeric_suffix: int | None = None) -> str:
"""Site PK: `{name-slug}-{4-digit number}`, matching farm dashboard identity."""
base = site_name_slug(site_name)
if numeric_suffix is None:
numeric_suffix = secrets.randbelow(10_000)
return f"{base}-{numeric_suffix:04d}"
def allocate_site_id(site_name: str) -> str:
"""Pick an unused site_id for HQ-only creates (seeds / manual)."""
for _ in range(32):
candidate = site_id_from_name(site_name)
if not Site.objects.filter(pk=candidate).exists():
return candidate
raise ValidationError({"site_name": "Gagal membuat id site unik — coba lagi."})
class Site(models.Model):
site_id = models.BigAutoField(primary_key=True)
# Same shape as farm dashboard Site.site_id; when linked, equals ActiveSite.source_site_id.
site_id = models.SlugField(primary_key=True, max_length=64)
site_name = models.CharField(max_length=30)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sites")
# Linked when GM/GM Admin adds the site dashboard to their Pengaturan view list.
@@ -29,6 +57,11 @@ class Site(models.Model):
def __str__(self) -> str:
return self.site_name
def save(self, *args, **kwargs):
if not self.site_id:
self.site_id = allocate_site_id(self.site_name)
super().save(*args, **kwargs)
class Kandang(models.Model):
kandang_id = models.BigAutoField(primary_key=True)
+3 -1
View File
@@ -4,7 +4,9 @@ from apps.accounts.serializers import PkAsIdMixin
from apps.farms.models import Cycle, Flock, Kandang, Site
class SiteSerializer(PkAsIdMixin, serializers.ModelSerializer):
class SiteSerializer(serializers.ModelSerializer):
# Slug PK (matches farm site_id); do not use IntegerField PkAsIdMixin.
id = serializers.CharField(source="pk", read_only=True)
owner_name = serializers.CharField(source="user.display_name", read_only=True)
class Meta:
@@ -2,27 +2,56 @@
from __future__ import annotations
from apps.farms.models import Site
from django.db import transaction
from django.utils.text import slugify
from apps.farms.models import Kandang, Site
from apps.sync.models import ActiveSite
def desired_hq_site_id(active_site: ActiveSite) -> str:
"""HQ Site PK should match the farm site slug when known."""
source = str(active_site.source_site_id or "").strip()
if source:
return source[:64]
code = slugify(str(active_site.code or "").strip())[:64]
if code:
return code
raise ValueError("ActiveSite needs source_site_id or code to create an HQ site.")
@transaction.atomic
def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site:
"""
Ensure the active-site registry entry has an HQ Site for farm visibility.
Prefers an existing linked site, then an unlinked site owned by the
registrar with a matching name (so seeded Sukawarna attaches when
registered), otherwise creates a new Site.
Site.site_id matches ActiveSite.source_site_id (farm Site.site_id) when set.
Prefers an existing linked site, then a site already keyed by that id, then an
unlinked site owned by the registrar with a matching name (re-keyed if needed),
otherwise creates a new Site.
"""
existing = Site.objects.filter(active_site=active_site).first()
if existing:
return existing
owner = active_site.managed_by
if owner is None:
raise ValueError("ActiveSite.managed_by is required to create an HQ site.")
desired_id = desired_hq_site_id(active_site)
site_name = (active_site.name or active_site.code or "Site")[:30]
existing = Site.objects.filter(active_site=active_site).first()
if existing is not None:
if existing.pk == desired_id:
return existing
return _rekey_site(existing, desired_id=desired_id, site_name=site_name, owner=owner)
by_pk = Site.objects.filter(pk=desired_id).first()
if by_pk is not None:
by_pk.active_site = active_site
by_pk.user = owner
if by_pk.site_name != site_name:
by_pk.site_name = site_name
by_pk.save(update_fields=["active_site", "user", "site_name", "updated_at"])
return by_pk
orphan = (
Site.objects.filter(
user=owner, active_site__isnull=True, site_name__iexact=site_name
@@ -31,7 +60,6 @@ def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site:
.first()
)
if orphan is None and active_site.code:
# e.g. code "sukawarna" ↔ site_name "Sukawarna"
orphan = (
Site.objects.filter(
user=owner, active_site__isnull=True, site_name__iexact=active_site.code
@@ -40,10 +68,45 @@ def ensure_hq_site_for_active_site(active_site: ActiveSite) -> Site:
.first()
)
if orphan is not None:
if orphan.pk == desired_id:
orphan.active_site = active_site
orphan.save(update_fields=["active_site", "updated_at"])
return orphan
return _rekey_site(
orphan,
desired_id=desired_id,
site_name=site_name,
owner=owner,
active_site=active_site,
)
return Site.objects.create(
site_name=site_name, user=owner, active_site=active_site
site_id=desired_id,
site_name=site_name,
user=owner,
active_site=active_site,
)
def _rekey_site(
old: Site,
*,
desired_id: str,
site_name: str,
owner,
active_site: ActiveSite | None = None,
) -> Site:
"""Move an HQ site (and its kandangs) onto the farm slug primary key."""
link = active_site if active_site is not None else old.active_site
new_site = Site.objects.create(
site_id=desired_id,
site_name=site_name or old.site_name,
user=owner,
active_site=None,
)
Kandang.objects.filter(site=old).update(site=new_site)
old_active = old.active_site
old.delete()
new_site.active_site = link or old_active
new_site.save(update_fields=["active_site", "updated_at"])
return new_site
+6 -1
View File
@@ -12,14 +12,19 @@ from apps.sync.models import ActiveSite
def _gm_visible_site(*, owner: User, site_name: str, code: str) -> Site:
"""GM scope only sees sites linked to an ActiveSite they manage."""
site_id = f"{code}-0001"
active_site = ActiveSite.objects.create(
code=code,
name=site_name,
source_site_id=site_id,
api_base_url=f"https://{code}.example/api/v1",
managed_by=owner,
)
return Site.objects.create(
site_name=site_name, user=owner, active_site=active_site
site_id=site_id,
site_name=site_name,
user=owner,
active_site=active_site,
)
+21 -2
View File
@@ -7,6 +7,25 @@ from rest_framework.test import APIClient
from apps.accounts.models import User
from apps.farms.models import Cycle, Flock, Kandang, Site
from apps.operations.models import ChickenWeight, FeedSacks, IotPanel
from apps.sync.models import ActiveSite
def _linked_site(user: User, *, site_name: str = "Sukawarna", code: str = "sukawarna") -> Site:
"""GM-visible site linked to an ActiveSite (Pengaturan registry)."""
site_id = f"{code}-0001"
active = ActiveSite.objects.create(
code=code,
name=site_name,
source_site_id=site_id,
api_base_url=f"https://{code}.example/api/v1",
managed_by=user,
)
return Site.objects.create(
site_id=site_id,
site_name=site_name,
user=user,
active_site=active,
)
@override_settings(DASHBOARD_PUBLISH_HOUR=0)
@@ -15,7 +34,7 @@ class OperationsAPITests(TestCase):
self.user = User.objects.create_user(user_name="tester", password="secret")
self.client = APIClient()
self.client.force_authenticate(self.user)
site = Site.objects.create(site_name="Sukawarna", user=self.user)
site = _linked_site(self.user)
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site)
self.cycle = Cycle.objects.create(
kandang=self.kandang,
@@ -314,7 +333,7 @@ class DashboardVisibilityTests(TestCase):
self.user = User.objects.create_user(user_name="tester", password="secret")
self.client = APIClient()
self.client.force_authenticate(self.user)
site = Site.objects.create(site_name="Sukawarna", user=self.user)
site = _linked_site(self.user)
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site)
self.cycle = Cycle.objects.create(
kandang=self.kandang,
@@ -15,6 +15,24 @@ from apps.operations.services.chicken_counting_edge import (
location_prefix_for_kandang,
sync_chicken_counting_for_cycle,
)
from apps.sync.models import ActiveSite
def _linked_site(user: User, *, site_name: str = "Sukawarna", code: str = "sukawarna") -> Site:
site_id = f"{code}-0001"
active = ActiveSite.objects.create(
code=code,
name=site_name,
source_site_id=site_id,
api_base_url=f"https://{code}.example/api/v1",
managed_by=user,
)
return Site.objects.create(
site_id=site_id,
site_name=site_name,
user=user,
active_site=active,
)
DB_HISTORY_SAMPLE = [
{"date": "2026-09-02", "location": "K1-L1", "total": 14591},
@@ -133,7 +151,7 @@ class ChickenCountingSyncApiTests(TestCase):
self.user = User.objects.create_user(user_name="tester", password="secret")
self.client = APIClient()
self.client.force_authenticate(self.user)
site = Site.objects.create(site_name="Sukawarna", user=self.user)
site = _linked_site(self.user)
self.kandang = Kandang.objects.create(kandang_name="Kandang 1", site=site)
self.cycle = Cycle.objects.create(
kandang=self.kandang,
+1 -1
View File
@@ -20,7 +20,7 @@ class ActiveSite(models.Model):
blank=True,
default="",
db_index=True,
help_text="Farm-location Site.site_id (slug) from the registering dashboard",
help_text="Farm-location Site.site_id (slug); equals HQ sites.site_id when claimed",
)
api_base_url = models.URLField(
max_length=255,
+17 -2
View File
@@ -149,6 +149,7 @@ class ClaimActiveSiteTests(TestCase):
self.active = ActiveSite.objects.create(
code="sukawarna",
name="Sukawarna",
source_site_id="sukawarna-0001",
api_base_url="https://sukawarna.example/api/v1",
api_key="site-secret",
managed_by=None,
@@ -164,7 +165,18 @@ class ClaimActiveSiteTests(TestCase):
self.assertEqual(response.status_code, 200, response.data)
self.active.refresh_from_db()
self.assertEqual(self.active.managed_by_id, self.gm.pk)
self.assertTrue(Site.objects.filter(active_site=self.active, user=self.gm).exists())
hq = Site.objects.get(active_site=self.active, user=self.gm)
self.assertEqual(hq.pk, "sukawarna-0001")
self.assertEqual(hq.site_id, self.active.source_site_id)
def test_claimed_site_api_id_is_farm_slug_string(self):
self.client.force_authenticate(self.gm)
self.client.post("/api/v1/active-sites/", {"code": "sukawarna"}, format="json")
listed = self.client.get("/api/v1/sites/")
self.assertEqual(listed.status_code, 200)
row = next(r for r in listed.data["results"] if r["site_name"] == "Sukawarna")
self.assertEqual(row["id"], "sukawarna-0001")
self.assertIsInstance(row["id"], str)
def test_claim_unknown_code_404(self):
self.client.force_authenticate(self.gm)
@@ -191,7 +203,10 @@ class ClaimActiveSiteTests(TestCase):
self.active.source_site_id = "sukawarna-0001"
self.active.save(update_fields=["managed_by", "source_site_id"])
hq_site = Site.objects.create(
site_name="Sukawarna", user=self.gm, active_site=self.active
site_id="sukawarna-0001",
site_name="Sukawarna",
user=self.gm,
active_site=self.active,
)
self.client.force_authenticate(self.gm)
response = self.client.delete(f"/api/v1/active-sites/{self.active.pk}/")
+6 -3
View File
@@ -10,6 +10,7 @@
-- is_active true, is_superuser false. Product roles never use is_staff.)
--
-- user_access → sites → kandang → cycles
-- (sites.site_id = farm slug = active_sites.source_site_id when linked)
-- kandang → flock → iot_panel
-- user_access → api_keys
-- user_access → active_sites (managed_by)
@@ -103,10 +104,12 @@ CREATE INDEX IF NOT EXISTS idx_active_sites_managed_by_id ON active_sites (manag
CREATE INDEX IF NOT EXISTS idx_active_sites_source_site_id ON active_sites (source_site_id);
-- -----------------------------------------------------------------------------
-- 4. Sites
-- 4. Sites (HQ farm tree)
-- site_id is the farm dashboard Site.site_id slug (e.g. sukawarna-4821).
-- When linked, it equals active_sites.source_site_id.
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS sites (
site_id BIGSERIAL PRIMARY KEY,
site_id VARCHAR(64) PRIMARY KEY,
site_name VARCHAR(30) NOT NULL,
user_id BIGINT NOT NULL REFERENCES user_access (user_id) ON DELETE CASCADE,
active_site_id BIGINT UNIQUE REFERENCES active_sites (active_site_id) ON DELETE SET NULL,
@@ -123,7 +126,7 @@ CREATE TABLE IF NOT EXISTS kandang (
kandang_id BIGSERIAL PRIMARY KEY,
kandang_name VARCHAR(30) NOT NULL,
feed_in_button_urls JSONB NOT NULL DEFAULT '[]'::jsonb,
site_id BIGINT NOT NULL REFERENCES sites (site_id) ON DELETE CASCADE,
site_id VARCHAR(64) NOT NULL REFERENCES sites (site_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
);
+1 -1
View File
@@ -119,7 +119,7 @@ const Header: React.FC<HeaderProps> = ({
<i className="fa-solid fa-map-marker-alt text-gray-500" />
<select
value={selectedSiteId ?? ''}
onChange={(e) => setSelectedSiteId(e.target.value ? Number(e.target.value) : null)}
onChange={(e) => setSelectedSiteId(e.target.value ? e.target.value : null)}
className="px-3 py-1.5 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white text-sm text-gray-900"
>
{visibleSites.length === 0 && <option value="">Tidak ada site</option>}
+3 -3
View File
@@ -8,7 +8,7 @@ type FarmContextValue = {
kandangs: Kandang[];
flocks: Flock[];
cycles: Cycle[];
selectedSiteId: number | null;
selectedSiteId: string | null;
selectedKandangId: number | null;
selectedCycleId: number | null;
selectedSite: Site | null;
@@ -16,7 +16,7 @@ type FarmContextValue = {
selectedCycle: Cycle | null;
loading: boolean;
error: string | null;
setSelectedSiteId: (id: number | null) => void;
setSelectedSiteId: (id: string | null) => void;
setSelectedKandangId: (id: number | null) => void;
setSelectedCycleId: (id: number | null) => void;
reload: () => Promise<void>;
@@ -31,7 +31,7 @@ export const FarmProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [kandangs, setKandangs] = useState<Kandang[]>([]);
const [flocks, setFlocks] = useState<Flock[]>([]);
const [cycles, setCycles] = useState<Cycle[]>([]);
const [selectedSiteId, setSelectedSiteId] = useState<number | null>(null);
const [selectedSiteId, setSelectedSiteId] = useState<string | null>(null);
const [selectedKandangId, setSelectedKandangId] = useState<number | null>(null);
const [selectedCycleId, setSelectedCycleId] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
+3 -3
View File
@@ -191,11 +191,11 @@ export const api = {
list: async (query?: { user_id?: number }) =>
unwrapList(await get<Paginated<Site> | Site[]>('/sites/', query)),
create: (body: SiteWrite) => post<Site>('/sites/', body),
update: (id: number, body: Partial<SiteWrite>) => patch<Site>(`/sites/${id}/`, body),
remove: (id: number) => del(`/sites/${id}/`),
update: (id: string, body: Partial<SiteWrite>) => patch<Site>(`/sites/${id}/`, body),
remove: (id: string) => del(`/sites/${id}/`),
},
kandangs: {
list: async (query?: { site_id?: number }) =>
list: async (query?: { site_id?: string }) =>
unwrapList(await get<Paginated<Kandang> | Kandang[]>('/kandangs/', query)),
create: (body: KandangWrite) => post<Kandang>('/kandangs/', body),
update: (id: number, body: Partial<KandangWrite>) => patch<Kandang>(`/kandangs/${id}/`, body),
+4 -4
View File
@@ -65,7 +65,7 @@ export type ActiveSiteWrite = {
};
export type Site = {
id: number;
id: string;
site_name: string;
user: number;
owner_name?: string;
@@ -77,7 +77,7 @@ export type Site = {
export type Kandang = {
id: number;
kandang_name: string;
site: number;
site: string;
created_at: string;
updated_at: string;
};
@@ -268,7 +268,7 @@ export type KarungRequestResult = {
export type CycleFilter = {
cycle_id?: number;
kandang_id?: number;
site_id?: number;
site_id?: string;
flock_id?: number;
date?: string;
status?: CycleStatus;
@@ -277,7 +277,7 @@ export type CycleFilter = {
export type SiteWrite = { site_name: string; user: number };
export type KandangWrite = {
kandang_name: string;
site: number;
site: string;
};
export type FlockWrite = {
flock_name: string;
+2 -2
View File
@@ -3,7 +3,7 @@ import type { Cycle, KPI, Site } from '../types/api.ts';
export type KandangRankingRow = {
rank: number;
siteId: number;
siteId: string;
siteName: string;
kandangId: number;
kandangName: string;
@@ -95,7 +95,7 @@ export async function loadKandangRanking({
sites.map((site) => api.kandangs.list({ site_id: site.id }))
);
type KandangMeta = { siteId: number; siteName: string; kandangName: string };
type KandangMeta = { siteId: string; siteName: string; kandangName: string };
const kandangMeta = new Map<number, KandangMeta>();
kandangLists.forEach((list, i) => {
const site = sites[i];