This commit is contained in:
2026-04-08 23:01:07 +03:30
parent 3f15890393
commit 6046bc3c28
6 changed files with 1273 additions and 140 deletions
+124 -73
View File
@@ -16,6 +16,11 @@ const PREFIX = "/api/crop-zoning";
const AREA_CACHE_KEY_PREFIX = "crop-zoning:area";
const AREA_CACHE_VERSION = "v1";
const AREA_CACHE_TTL_MS = 1000 * 60 * 60 * 6;
type CropZoningLayerEndpoint =
| "area"
| "water-need"
| "soil-quality"
| "cultivation-risk";
export interface Product {
id: string;
@@ -217,6 +222,7 @@ function getAreaCacheUserKey(): string {
}
function getAreaCacheKey(
endpoint: CropZoningLayerEndpoint,
farmUuid: string,
page: number,
pageSize: number,
@@ -225,6 +231,7 @@ function getAreaCacheKey(
AREA_CACHE_KEY_PREFIX,
AREA_CACHE_VERSION,
getAreaCacheUserKey(),
endpoint,
farmUuid,
page,
pageSize,
@@ -232,6 +239,7 @@ function getAreaCacheKey(
}
function readCachedArea(
endpoint: CropZoningLayerEndpoint,
farmUuid: string,
page: number,
pageSize: number,
@@ -239,7 +247,9 @@ function readCachedArea(
if (typeof window === "undefined") return null;
try {
const raw = localStorage.getItem(getAreaCacheKey(farmUuid, page, pageSize));
const raw = localStorage.getItem(
getAreaCacheKey(endpoint, farmUuid, page, pageSize),
);
if (!raw) {
return null;
@@ -248,7 +258,7 @@ function readCachedArea(
const parsed = JSON.parse(raw) as CachedAreaEntry;
if (!parsed?.expiresAt || parsed.expiresAt < Date.now()) {
localStorage.removeItem(getAreaCacheKey(farmUuid, page, pageSize));
localStorage.removeItem(getAreaCacheKey(endpoint, farmUuid, page, pageSize));
return null;
}
@@ -259,6 +269,7 @@ function readCachedArea(
}
function writeCachedArea(
endpoint: CropZoningLayerEndpoint,
farmUuid: string,
page: number,
pageSize: number,
@@ -273,7 +284,7 @@ function writeCachedArea(
};
localStorage.setItem(
getAreaCacheKey(farmUuid, page, pageSize),
getAreaCacheKey(endpoint, farmUuid, page, pageSize),
JSON.stringify(payload),
);
} catch {
@@ -290,6 +301,97 @@ function logAreaRequest(
console.log(`[crop-zoning][area][${phase}]`, payload);
}
function getLayerEndpointUrl(
endpoint: CropZoningLayerEndpoint,
params: URLSearchParams,
): string {
return `${PREFIX}/${endpoint}/?${params.toString()}`;
}
function getLayerArea(
endpoint: CropZoningLayerEndpoint,
farmUuid: string,
options?: { page?: number; pageSize?: number; useCache?: boolean },
): Promise<CropZoningAreaResponse> {
const page = options?.page ?? 1;
const pageSize = options?.pageSize ?? 10;
const useCache = options?.useCache ?? true;
if (useCache) {
const cached = readCachedArea(endpoint, farmUuid, page, pageSize);
if (cached) {
logAreaRequest("cache-hit", {
endpoint,
farmUuid,
page,
pageSize,
pagination: cached.pagination ?? null,
taskStatus: cached.task?.status ?? null,
zonesCount: cached.zones?.length ?? 0,
});
return Promise.resolve(cached);
}
}
const params = new URLSearchParams({ farm_uuid: farmUuid });
params.set("page", String(page));
params.set("page_size", String(pageSize));
const requestUrl = getLayerEndpointUrl(endpoint, params);
logAreaRequest("request", {
endpoint,
farmUuid,
page,
pageSize,
endpointUrl: requestUrl,
});
return unwrap(
apiClient.get<ApiResponse<CropZoningAreaResponse>>(requestUrl),
).then((response) => {
if ("task_id" in response) {
logAreaRequest("response", {
endpoint,
farmUuid,
page,
pageSize,
taskId: response.task_id,
status: response.status,
});
return normalizeTaskInitResponse(response);
}
const normalized = normalizeAreaResult(response);
const taskStatus = normalized.task?.status?.toLowerCase();
logAreaRequest("response", {
endpoint,
farmUuid,
page,
pageSize,
taskStatus: normalized.task?.status ?? null,
pagination: normalized.pagination ?? null,
zonesCount: normalized.zones?.length ?? 0,
hasArea: Boolean(normalized.area),
});
if (
normalized.area &&
taskStatus !== "pending" &&
taskStatus !== "processing" &&
taskStatus !== "failure" &&
taskStatus !== "failed"
) {
writeCachedArea(endpoint, farmUuid, page, pageSize, normalized);
}
return normalized;
});
}
export const cropZoningService = {
getProducts(): Promise<{ products: Product[] }> {
return unwrap(
@@ -323,79 +425,28 @@ export const cropZoningService = {
farmUuid: string,
options?: { page?: number; pageSize?: number; useCache?: boolean },
): Promise<CropZoningAreaResponse> {
const page = options?.page ?? 1;
const pageSize = options?.pageSize ?? 10;
const useCache = options?.useCache ?? true;
return getLayerArea("area", farmUuid, options);
},
if (useCache) {
const cached = readCachedArea(farmUuid, page, pageSize);
getWaterNeedArea(
farmUuid: string,
options?: { page?: number; pageSize?: number; useCache?: boolean },
): Promise<CropZoningAreaResponse> {
return getLayerArea("water-need", farmUuid, options);
},
if (cached) {
logAreaRequest("cache-hit", {
farmUuid,
page,
pageSize,
pagination: cached.pagination ?? null,
taskStatus: cached.task?.status ?? null,
zonesCount: cached.zones?.length ?? 0,
});
return Promise.resolve(cached);
}
}
getSoilQualityArea(
farmUuid: string,
options?: { page?: number; pageSize?: number; useCache?: boolean },
): Promise<CropZoningAreaResponse> {
return getLayerArea("soil-quality", farmUuid, options);
},
const params = new URLSearchParams({ farm_uuid: farmUuid });
params.set("page", String(page));
params.set("page_size", String(pageSize));
const endpoint = `${PREFIX}/area/?${params.toString()}`;
logAreaRequest("request", {
farmUuid,
page,
pageSize,
endpoint,
});
return unwrap(
apiClient.get<ApiResponse<CropZoningAreaResponse>>(endpoint),
).then((response) => {
if ("task_id" in response) {
logAreaRequest("response", {
farmUuid,
page,
pageSize,
taskId: response.task_id,
status: response.status,
});
return normalizeTaskInitResponse(response);
}
const normalized = normalizeAreaResult(response);
const taskStatus = normalized.task?.status?.toLowerCase();
logAreaRequest("response", {
farmUuid,
page,
pageSize,
taskStatus: normalized.task?.status ?? null,
pagination: normalized.pagination ?? null,
zonesCount: normalized.zones?.length ?? 0,
hasArea: Boolean(normalized.area),
});
if (
normalized.area &&
taskStatus !== "pending" &&
taskStatus !== "processing" &&
taskStatus !== "failure" &&
taskStatus !== "failed"
) {
writeCachedArea(farmUuid, page, pageSize, normalized);
}
return normalized;
});
getCultivationRiskArea(
farmUuid: string,
options?: { page?: number; pageSize?: number; useCache?: boolean },
): Promise<CropZoningAreaResponse> {
return getLayerArea("cultivation-risk", farmUuid, options);
},
getAreaStatus(
@@ -0,0 +1,72 @@
import { apiClient } from "../client";
const PREFIX = "/api/sensor-external-api";
export interface SensorExternalFarmSensor {
uuid: string;
sensor_catalog_uuid: string | null;
physical_device_uuid: string;
name: string;
sensor_type: string;
is_active: boolean;
specifications?: Record<string, unknown> | null;
power_source?: Record<string, unknown> | null;
created_at: string;
updated_at: string;
}
export interface SensorExternalCatalog {
uuid: string;
code: string;
name: string;
description?: string | null;
customizable_fields?: unknown[];
supported_power_sources?: unknown[];
returned_data_fields?: string[];
sample_payload?: Record<string, unknown> | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface SensorExternalRequestLog {
id: number;
farm_uuid: string;
sensor_catalog_uuid: string | null;
physical_device_uuid: string;
farm_sensor: SensorExternalFarmSensor | null;
sensor_catalog: SensorExternalCatalog | null;
payload: Record<string, unknown> | null;
created_at: string;
}
export interface SensorExternalRequestLogsResponse {
code: number;
msg: string;
count: number;
next: string | null;
previous: string | null;
data: SensorExternalRequestLog[];
}
export const sensorExternalApiService = {
listRequestLogs(params: {
farmUuid: string;
page?: number;
pageSize?: number;
}): Promise<SensorExternalRequestLogsResponse> {
const searchParams = new URLSearchParams({ farm_uuid: params.farmUuid });
if (typeof params.page === "number") {
searchParams.set("page", String(params.page));
}
if (typeof params.pageSize === "number") {
searchParams.set("page_size", String(params.pageSize));
}
return apiClient.get<SensorExternalRequestLogsResponse>(
`${PREFIX}/logs/?${searchParams.toString()}`,
);
},
};