2026-02-26 00:17:32 +03:30
|
|
|
/**
|
|
|
|
|
* Crop Zoning API
|
|
|
|
|
* @see CROP_ZONING_APIS.md
|
|
|
|
|
*/
|
|
|
|
|
|
2026-04-01 17:28:05 +03:30
|
|
|
import type { Feature, FeatureCollection, Polygon } from "geojson";
|
|
|
|
|
import { apiClient } from "../client";
|
|
|
|
|
import type {
|
|
|
|
|
RecommendationTaskInitResponse,
|
|
|
|
|
RecommendationTaskStatus,
|
|
|
|
|
RecommendationTaskStatusResponse,
|
|
|
|
|
} from "./recommendationTask";
|
|
|
|
|
import { normalizeRecommendationTaskStatus } from "./recommendationTask";
|
2026-02-26 00:17:32 +03:30
|
|
|
|
2026-04-01 17:28:05 +03:30
|
|
|
const PREFIX = "/api/crop-zoning";
|
2026-04-01 17:58:42 +03:30
|
|
|
const AREA_CACHE_KEY_PREFIX = "crop-zoning:area";
|
|
|
|
|
const AREA_CACHE_VERSION = "v1";
|
|
|
|
|
const AREA_CACHE_TTL_MS = 1000 * 60 * 60 * 6;
|
2026-02-26 00:17:32 +03:30
|
|
|
|
|
|
|
|
export interface Product {
|
2026-04-01 17:28:05 +03:30
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
color: string;
|
2026-02-26 00:17:32 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ZoneInitialData {
|
2026-04-01 17:28:05 +03:30
|
|
|
zoneId: string;
|
2026-04-01 17:58:42 +03:30
|
|
|
zoneUuid?: string;
|
2026-04-01 17:28:05 +03:30
|
|
|
geometry: Polygon;
|
2026-04-01 17:58:42 +03:30
|
|
|
center?: {
|
|
|
|
|
latitude: number;
|
|
|
|
|
longitude: number;
|
|
|
|
|
};
|
|
|
|
|
area_sqm?: number;
|
|
|
|
|
area_hectares?: number;
|
|
|
|
|
sequence?: number;
|
|
|
|
|
processing_status?: "pending" | "processing" | "completed" | "failed";
|
|
|
|
|
processing_error?: string;
|
2026-02-26 00:37:00 +03:30
|
|
|
/** اگر null/خالی/uncultivable باشد، زون غیرقابل کشت و خاکستری نمایش داده میشود */
|
2026-04-01 17:28:05 +03:30
|
|
|
crop?: string | null;
|
|
|
|
|
matchPercent?: number | null;
|
|
|
|
|
waterNeed?: string | null;
|
|
|
|
|
estimatedProfit?: string | null;
|
2026-04-01 17:58:42 +03:30
|
|
|
waterNeedLayer?: {
|
|
|
|
|
level: "low" | "medium" | "high";
|
|
|
|
|
value?: string;
|
|
|
|
|
color: string;
|
|
|
|
|
};
|
|
|
|
|
soilQualityLayer?: {
|
|
|
|
|
level: "low" | "medium" | "high";
|
|
|
|
|
score?: number;
|
|
|
|
|
color: string;
|
|
|
|
|
};
|
|
|
|
|
cultivationRiskLayer?: {
|
|
|
|
|
level: "low" | "medium" | "high";
|
|
|
|
|
color: string;
|
|
|
|
|
};
|
2026-02-26 00:17:32 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ZonesInitialResponse {
|
2026-04-01 17:28:05 +03:30
|
|
|
total_area_hectares: number;
|
|
|
|
|
total_area_sqm: number;
|
|
|
|
|
zone_count: number;
|
|
|
|
|
zones: ZoneInitialData[];
|
2026-02-26 00:17:32 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AreaResponse {
|
2026-04-01 17:28:05 +03:30
|
|
|
area: Feature<Polygon> | null;
|
2026-02-26 00:17:32 +03:30
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:28:05 +03:30
|
|
|
export interface CropZoningAreaTask {
|
|
|
|
|
status?:
|
|
|
|
|
| "IDLE"
|
|
|
|
|
| "PENDING"
|
|
|
|
|
| "PROCESSING"
|
|
|
|
|
| "SUCCESS"
|
|
|
|
|
| "FAILURE"
|
|
|
|
|
| "pending"
|
|
|
|
|
| "processing"
|
|
|
|
|
| "success"
|
|
|
|
|
| "completed"
|
|
|
|
|
| "failure"
|
|
|
|
|
| "failed";
|
|
|
|
|
stage?: string;
|
|
|
|
|
stage_label?: string;
|
|
|
|
|
area_uuid?: string;
|
|
|
|
|
total_zones?: number;
|
|
|
|
|
completed_zones?: number;
|
|
|
|
|
processing_zones?: number;
|
|
|
|
|
pending_zones?: number;
|
|
|
|
|
failed_zones?: number;
|
|
|
|
|
remaining_zones?: number;
|
|
|
|
|
progress_percent?: number;
|
|
|
|
|
message?: string;
|
|
|
|
|
failed_zone_errors?: string[];
|
|
|
|
|
cell_side_km?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CropZoningAreaResult extends AreaResponse {
|
|
|
|
|
status?: string;
|
|
|
|
|
task?: CropZoningAreaTask | null;
|
|
|
|
|
zones?: ZoneInitialData[];
|
2026-04-01 17:58:42 +03:30
|
|
|
pagination?: {
|
|
|
|
|
page: number;
|
|
|
|
|
page_size: number;
|
|
|
|
|
total_pages: number;
|
|
|
|
|
total_zones: number;
|
|
|
|
|
returned_zones: number;
|
|
|
|
|
has_next: boolean;
|
|
|
|
|
has_previous: boolean;
|
|
|
|
|
};
|
2026-04-01 17:28:05 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type CropZoningAreaResponse =
|
|
|
|
|
| CropZoningAreaResult
|
|
|
|
|
| RecommendationTaskInitResponse;
|
|
|
|
|
|
2026-02-26 00:17:32 +03:30
|
|
|
export interface ZoneDetailData {
|
2026-04-01 17:28:05 +03:30
|
|
|
zoneId: string;
|
|
|
|
|
crop: string;
|
|
|
|
|
matchPercent: number;
|
|
|
|
|
waterNeed: string;
|
|
|
|
|
estimatedProfit: string;
|
|
|
|
|
reason: string;
|
|
|
|
|
criteria: { name: string; value: number }[];
|
|
|
|
|
area_hectares?: number;
|
2026-02-26 00:17:32 +03:30
|
|
|
}
|
|
|
|
|
|
2026-02-26 00:37:00 +03:30
|
|
|
/** دیتای نیاز آبی هر زون — لایهٔ نیاز آبی */
|
|
|
|
|
export interface ZoneWaterNeedData {
|
2026-04-01 17:28:05 +03:30
|
|
|
zoneId: string;
|
|
|
|
|
geometry: Polygon;
|
|
|
|
|
level: "low" | "medium" | "high";
|
|
|
|
|
value?: string;
|
|
|
|
|
color: string;
|
2026-02-26 00:37:00 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** دیتای کیفیت خاک هر زون — لایهٔ کیفیت خاک */
|
|
|
|
|
export interface ZoneSoilQualityData {
|
2026-04-01 17:28:05 +03:30
|
|
|
zoneId: string;
|
|
|
|
|
geometry: Polygon;
|
|
|
|
|
level: "low" | "medium" | "high";
|
|
|
|
|
score?: number;
|
|
|
|
|
color: string;
|
2026-02-26 00:37:00 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** دیتای ریسک کشت هر زون — لایهٔ ریسک کشت */
|
|
|
|
|
export interface ZoneCultivationRiskData {
|
2026-04-01 17:28:05 +03:30
|
|
|
zoneId: string;
|
|
|
|
|
geometry: Polygon;
|
|
|
|
|
level: "low" | "medium" | "high";
|
|
|
|
|
color: string;
|
2026-02-26 00:37:00 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** دادهٔ نمایشی هر زون روی نقشه — خروجی تبدیل از تمام لایهها */
|
|
|
|
|
export interface ZoneMapData {
|
2026-04-01 17:28:05 +03:30
|
|
|
zoneId: string;
|
|
|
|
|
geometry: Polygon;
|
|
|
|
|
color: string;
|
|
|
|
|
tooltipContent: string;
|
|
|
|
|
cultivable: boolean;
|
|
|
|
|
zoneInitialData?: ZoneInitialData;
|
2026-02-26 00:37:00 +03:30
|
|
|
}
|
|
|
|
|
|
2026-02-26 00:17:32 +03:30
|
|
|
interface ApiResponse<T> {
|
2026-04-01 17:28:05 +03:30
|
|
|
status: string;
|
|
|
|
|
data: T;
|
2026-02-26 00:17:32 +03:30
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:58:42 +03:30
|
|
|
interface CachedAreaEntry {
|
|
|
|
|
expiresAt: number;
|
|
|
|
|
value: CropZoningAreaResult;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 00:17:32 +03:30
|
|
|
async function unwrap<T>(promise: Promise<ApiResponse<T>>): Promise<T> {
|
2026-04-01 17:28:05 +03:30
|
|
|
const res = await promise;
|
|
|
|
|
return res.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeTaskInitResponse(
|
|
|
|
|
task: RecommendationTaskInitResponse,
|
|
|
|
|
): RecommendationTaskInitResponse {
|
|
|
|
|
return {
|
|
|
|
|
...task,
|
|
|
|
|
status: normalizeRecommendationTaskStatus(task.status),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeAreaResult(
|
|
|
|
|
result: CropZoningAreaResult,
|
|
|
|
|
): CropZoningAreaResult {
|
|
|
|
|
return {
|
|
|
|
|
...result,
|
|
|
|
|
task: result.task
|
|
|
|
|
? {
|
|
|
|
|
...result.task,
|
|
|
|
|
status: normalizeRecommendationTaskStatus(result.task.status),
|
|
|
|
|
}
|
|
|
|
|
: result.task,
|
|
|
|
|
};
|
2026-02-26 00:17:32 +03:30
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:58:42 +03:30
|
|
|
function getAreaCacheUserKey(): string {
|
|
|
|
|
if (typeof window === "undefined") return "server";
|
|
|
|
|
|
|
|
|
|
const token = localStorage.getItem("auth_token");
|
|
|
|
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
return "guest";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return btoa(token).slice(0, 24);
|
|
|
|
|
} catch {
|
|
|
|
|
return token.slice(0, 24);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAreaCacheKey(
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid: string,
|
2026-04-01 17:58:42 +03:30
|
|
|
page: number,
|
|
|
|
|
pageSize: number,
|
|
|
|
|
): string {
|
|
|
|
|
return [
|
|
|
|
|
AREA_CACHE_KEY_PREFIX,
|
|
|
|
|
AREA_CACHE_VERSION,
|
|
|
|
|
getAreaCacheUserKey(),
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid,
|
2026-04-01 17:58:42 +03:30
|
|
|
page,
|
|
|
|
|
pageSize,
|
|
|
|
|
].join(":");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readCachedArea(
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid: string,
|
2026-04-01 17:58:42 +03:30
|
|
|
page: number,
|
|
|
|
|
pageSize: number,
|
|
|
|
|
): CropZoningAreaResult | null {
|
|
|
|
|
if (typeof window === "undefined") return null;
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-02 23:44:24 +03:30
|
|
|
const raw = localStorage.getItem(getAreaCacheKey(farmUuid, page, pageSize));
|
2026-04-01 17:58:42 +03:30
|
|
|
|
|
|
|
|
if (!raw) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = JSON.parse(raw) as CachedAreaEntry;
|
|
|
|
|
|
|
|
|
|
if (!parsed?.expiresAt || parsed.expiresAt < Date.now()) {
|
2026-04-02 23:44:24 +03:30
|
|
|
localStorage.removeItem(getAreaCacheKey(farmUuid, page, pageSize));
|
2026-04-01 17:58:42 +03:30
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return normalizeAreaResult(parsed.value);
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function writeCachedArea(
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid: string,
|
2026-04-01 17:58:42 +03:30
|
|
|
page: number,
|
|
|
|
|
pageSize: number,
|
|
|
|
|
value: CropZoningAreaResult,
|
|
|
|
|
): void {
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const payload: CachedAreaEntry = {
|
|
|
|
|
expiresAt: Date.now() + AREA_CACHE_TTL_MS,
|
|
|
|
|
value,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
localStorage.setItem(
|
2026-04-02 23:44:24 +03:30
|
|
|
getAreaCacheKey(farmUuid, page, pageSize),
|
2026-04-01 17:58:42 +03:30
|
|
|
JSON.stringify(payload),
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore storage quota and serialization errors.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 01:56:34 +03:30
|
|
|
function logAreaRequest(
|
|
|
|
|
phase: "cache-hit" | "request" | "response",
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
): void {
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
|
|
|
|
console.log(`[crop-zoning][area][${phase}]`, payload);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 00:17:32 +03:30
|
|
|
export const cropZoningService = {
|
|
|
|
|
getProducts(): Promise<{ products: Product[] }> {
|
2026-04-01 17:28:05 +03:30
|
|
|
return unwrap(
|
|
|
|
|
apiClient.get<ApiResponse<{ products: Product[] }>>(
|
|
|
|
|
`${PREFIX}/products/`,
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-02-26 00:17:32 +03:30
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getZonesInitial(body: {
|
2026-04-01 17:28:05 +03:30
|
|
|
zones: FeatureCollection<Polygon>;
|
|
|
|
|
products?: string[];
|
2026-02-26 00:17:32 +03:30
|
|
|
}): Promise<ZonesInitialResponse> {
|
2026-04-01 17:28:05 +03:30
|
|
|
return unwrap(
|
|
|
|
|
apiClient.post<ApiResponse<ZonesInitialResponse>>(
|
|
|
|
|
`${PREFIX}/zones/initial/`,
|
|
|
|
|
body,
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-02-26 00:17:32 +03:30
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getZoneDetails(zoneId: string): Promise<ZoneDetailData> {
|
2026-04-01 17:28:05 +03:30
|
|
|
return unwrap(
|
|
|
|
|
apiClient.get<ApiResponse<ZoneDetailData>>(
|
|
|
|
|
`${PREFIX}/zones/${zoneId}/details/`,
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-02-26 00:17:32 +03:30
|
|
|
},
|
|
|
|
|
|
2026-04-01 17:58:42 +03:30
|
|
|
getArea(
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid: string,
|
2026-04-01 17:58:42 +03:30
|
|
|
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) {
|
2026-04-02 23:44:24 +03:30
|
|
|
const cached = readCachedArea(farmUuid, page, pageSize);
|
2026-04-01 17:58:42 +03:30
|
|
|
|
|
|
|
|
if (cached) {
|
2026-04-02 01:56:34 +03:30
|
|
|
logAreaRequest("cache-hit", {
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid,
|
2026-04-02 01:56:34 +03:30
|
|
|
page,
|
|
|
|
|
pageSize,
|
|
|
|
|
pagination: cached.pagination ?? null,
|
|
|
|
|
taskStatus: cached.task?.status ?? null,
|
|
|
|
|
zonesCount: cached.zones?.length ?? 0,
|
|
|
|
|
});
|
2026-04-01 17:58:42 +03:30
|
|
|
return Promise.resolve(cached);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 23:44:24 +03:30
|
|
|
const params = new URLSearchParams({ farm_uuid: farmUuid });
|
2026-04-01 17:58:42 +03:30
|
|
|
|
|
|
|
|
params.set("page", String(page));
|
|
|
|
|
params.set("page_size", String(pageSize));
|
|
|
|
|
|
2026-04-02 01:56:34 +03:30
|
|
|
const endpoint = `${PREFIX}/area/?${params.toString()}`;
|
|
|
|
|
|
|
|
|
|
logAreaRequest("request", {
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid,
|
2026-04-02 01:56:34 +03:30
|
|
|
page,
|
|
|
|
|
pageSize,
|
|
|
|
|
endpoint,
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-01 17:28:05 +03:30
|
|
|
return unwrap(
|
2026-04-02 01:56:34 +03:30
|
|
|
apiClient.get<ApiResponse<CropZoningAreaResponse>>(endpoint),
|
2026-04-01 17:58:42 +03:30
|
|
|
).then((response) => {
|
|
|
|
|
if ("task_id" in response) {
|
2026-04-02 01:56:34 +03:30
|
|
|
logAreaRequest("response", {
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid,
|
2026-04-02 01:56:34 +03:30
|
|
|
page,
|
|
|
|
|
pageSize,
|
|
|
|
|
taskId: response.task_id,
|
|
|
|
|
status: response.status,
|
|
|
|
|
});
|
2026-04-01 17:58:42 +03:30
|
|
|
return normalizeTaskInitResponse(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalized = normalizeAreaResult(response);
|
|
|
|
|
const taskStatus = normalized.task?.status?.toLowerCase();
|
|
|
|
|
|
2026-04-02 01:56:34 +03:30
|
|
|
logAreaRequest("response", {
|
2026-04-02 23:44:24 +03:30
|
|
|
farmUuid,
|
2026-04-02 01:56:34 +03:30
|
|
|
page,
|
|
|
|
|
pageSize,
|
|
|
|
|
taskStatus: normalized.task?.status ?? null,
|
|
|
|
|
pagination: normalized.pagination ?? null,
|
|
|
|
|
zonesCount: normalized.zones?.length ?? 0,
|
|
|
|
|
hasArea: Boolean(normalized.area),
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-01 17:58:42 +03:30
|
|
|
if (
|
|
|
|
|
normalized.area &&
|
|
|
|
|
taskStatus !== "pending" &&
|
|
|
|
|
taskStatus !== "processing" &&
|
|
|
|
|
taskStatus !== "failure" &&
|
|
|
|
|
taskStatus !== "failed"
|
|
|
|
|
) {
|
2026-04-02 23:44:24 +03:30
|
|
|
writeCachedArea(farmUuid, page, pageSize, normalized);
|
2026-04-01 17:58:42 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return normalized;
|
|
|
|
|
});
|
2026-04-01 17:28:05 +03:30
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getAreaStatus(
|
|
|
|
|
taskId: string,
|
|
|
|
|
): Promise<RecommendationTaskStatusResponse<CropZoningAreaResult>> {
|
|
|
|
|
return unwrap(
|
|
|
|
|
apiClient.get<
|
|
|
|
|
ApiResponse<RecommendationTaskStatusResponse<CropZoningAreaResult>>
|
|
|
|
|
>(`${PREFIX}/area/status/${taskId}/`),
|
|
|
|
|
).then((response) => ({
|
|
|
|
|
...response,
|
|
|
|
|
status: normalizeRecommendationTaskStatus(response.status),
|
|
|
|
|
result: response.result
|
|
|
|
|
? normalizeAreaResult(response.result)
|
|
|
|
|
: undefined,
|
|
|
|
|
}));
|
2026-02-26 00:37:00 +03:30
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** نیاز آبی هر منطقه — برای لایهٔ نیاز آبی */
|
2026-04-01 17:28:05 +03:30
|
|
|
getZonesWaterNeed(body: {
|
|
|
|
|
zones: FeatureCollection<Polygon>;
|
|
|
|
|
}): Promise<{ zones: ZoneWaterNeedData[] }> {
|
2026-02-26 00:37:00 +03:30
|
|
|
return unwrap(
|
2026-04-01 17:28:05 +03:30
|
|
|
apiClient.post<ApiResponse<{ zones: ZoneWaterNeedData[] }>>(
|
|
|
|
|
`${PREFIX}/zones/water-need/`,
|
|
|
|
|
body,
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-02-26 00:37:00 +03:30
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** کیفیت خاک هر منطقه — برای لایهٔ کیفیت خاک */
|
2026-04-01 17:28:05 +03:30
|
|
|
getZonesSoilQuality(body: {
|
|
|
|
|
zones: FeatureCollection<Polygon>;
|
|
|
|
|
}): Promise<{ zones: ZoneSoilQualityData[] }> {
|
2026-02-26 00:37:00 +03:30
|
|
|
return unwrap(
|
2026-04-01 17:28:05 +03:30
|
|
|
apiClient.post<ApiResponse<{ zones: ZoneSoilQualityData[] }>>(
|
|
|
|
|
`${PREFIX}/zones/soil-quality/`,
|
|
|
|
|
body,
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-02-26 00:37:00 +03:30
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/** ریسک کشت هر منطقه — برای لایهٔ ریسک کشت */
|
|
|
|
|
getZonesCultivationRisk(body: {
|
2026-04-01 17:28:05 +03:30
|
|
|
zones: FeatureCollection<Polygon>;
|
2026-02-26 00:37:00 +03:30
|
|
|
}): Promise<{ zones: ZoneCultivationRiskData[] }> {
|
|
|
|
|
return unwrap(
|
|
|
|
|
apiClient.post<ApiResponse<{ zones: ZoneCultivationRiskData[] }>>(
|
|
|
|
|
`${PREFIX}/zones/cultivation-risk/`,
|
2026-04-01 17:28:05 +03:30
|
|
|
body,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
};
|