UPDATE
This commit is contained in:
@@ -1,10 +1,25 @@
|
||||
import { apiClient } from '../client'
|
||||
import type { ApiError } from '../client'
|
||||
|
||||
const PREFIX = '/api/pest-detection'
|
||||
const DETECTION_PREFIX = '/api/pest-detection'
|
||||
const DISEASE_PREFIX = '/api/pest-disease'
|
||||
|
||||
export interface RiskCard {
|
||||
id?: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
stats?: string
|
||||
avatarColor?: string
|
||||
avatarIcon?: string
|
||||
chipText?: string
|
||||
chipColor?: string
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface PestRiskSummary {
|
||||
disease_risk?: Record<string, unknown>
|
||||
pest_risk?: Record<string, unknown>
|
||||
diseaseRisk?: Record<string, unknown>
|
||||
pestRisk?: Record<string, unknown>
|
||||
drivers?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
@@ -14,6 +29,12 @@ interface ApiResponse<T> {
|
||||
result?: T
|
||||
}
|
||||
|
||||
function isRouteMismatchError(error: unknown): boolean {
|
||||
const statusCode = (error as ApiError | undefined)?.code
|
||||
|
||||
return statusCode === 404 || statusCode === 405
|
||||
}
|
||||
|
||||
function extract<T>(res: ApiResponse<T> | T): T {
|
||||
if (res && typeof res === 'object') {
|
||||
if ('data' in res) return (res as ApiResponse<T>).data
|
||||
@@ -23,22 +44,87 @@ function extract<T>(res: ApiResponse<T> | T): T {
|
||||
return res as T
|
||||
}
|
||||
|
||||
function toKpiCard(card?: Record<string, unknown>): Record<string, unknown> {
|
||||
function toKpiCard(
|
||||
card: RiskCard | Record<string, unknown> | undefined,
|
||||
fallback: { title: string; icon: string },
|
||||
): Record<string, unknown> {
|
||||
if (!card || typeof card !== 'object') return {}
|
||||
|
||||
return { kpis: [card] }
|
||||
if ('title' in card || 'stats' in card) {
|
||||
return { kpis: [card] }
|
||||
}
|
||||
|
||||
const level = String(card.level ?? '').toLowerCase()
|
||||
const score = typeof card.score === 'number' ? card.score : Number(card.score ?? 0)
|
||||
const percentage = Number.isFinite(score) ? Math.round(score <= 1 ? score * 100 : score) : 0
|
||||
|
||||
const levelLabelMap: Record<string, string> = {
|
||||
low: 'پایین',
|
||||
medium: 'متوسط',
|
||||
moderate: 'متوسط',
|
||||
high: 'بالا'
|
||||
}
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
low: 'success',
|
||||
medium: 'warning',
|
||||
moderate: 'warning',
|
||||
high: 'error'
|
||||
}
|
||||
|
||||
const stats = level ? levelLabelMap[level] ?? String(card.level) : percentage ? `${percentage}%` : '-'
|
||||
|
||||
return {
|
||||
kpis: [
|
||||
{
|
||||
id: String(card.id ?? fallback.title),
|
||||
title: fallback.title,
|
||||
subtitle: 'پیش بینی هوشمند',
|
||||
stats,
|
||||
avatarColor: colorMap[level] ?? 'primary',
|
||||
avatarIcon: fallback.icon,
|
||||
chipText: `${percentage}%`,
|
||||
chipColor: colorMap[level] ?? 'warning'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const pestDetectionDomainService = {
|
||||
async getRiskSummary(farmUuid: string): Promise<PestRiskSummary> {
|
||||
const res = await apiClient.get<ApiResponse<PestRiskSummary> | PestRiskSummary>(
|
||||
`${PREFIX}/risk-summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
let res: ApiResponse<Record<string, unknown>> | Record<string, unknown>
|
||||
|
||||
try {
|
||||
res = await apiClient.post<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${DETECTION_PREFIX}/risk-summary/`,
|
||||
{ farm_uuid: farmUuid }
|
||||
)
|
||||
} catch (error) {
|
||||
if (!isRouteMismatchError(error)) throw error
|
||||
|
||||
try {
|
||||
res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${DETECTION_PREFIX}/risk-summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
} catch (fallbackError) {
|
||||
if (!isRouteMismatchError(fallbackError)) throw fallbackError
|
||||
|
||||
res = await apiClient.post<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${DISEASE_PREFIX}/risk-summary/`,
|
||||
{ farm_uuid: farmUuid }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const data = extract(res)
|
||||
const diseaseRisk = (data?.diseaseRisk as Record<string, unknown> | undefined) ?? (data?.disease_risk as Record<string, unknown> | undefined)
|
||||
const pestRisk = (data?.pestRisk as Record<string, unknown> | undefined) ?? (data?.pest_risk as Record<string, unknown> | undefined)
|
||||
const drivers = (data?.drivers as Record<string, unknown> | undefined) ?? {}
|
||||
|
||||
return {
|
||||
disease_risk: toKpiCard(data?.disease_risk),
|
||||
pest_risk: toKpiCard(data?.pest_risk)
|
||||
diseaseRisk: toKpiCard(diseaseRisk, { title: 'ریسک بیماری', icon: 'tabler-biohazard' }),
|
||||
pestRisk: toKpiCard(pestRisk, { title: 'ریسک آفات', icon: 'tabler-bug' }),
|
||||
drivers
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import { apiClient } from '../client'
|
||||
const PREFIX = '/api/soil'
|
||||
|
||||
export interface SoilSummary {
|
||||
summaryKpis?: Record<string, unknown>
|
||||
avg_soil_moisture?: Record<string, unknown>
|
||||
sensorRadarChart?: Record<string, unknown>
|
||||
sensorComparisonChart?: Record<string, unknown>
|
||||
anomalyDetectionCard?: Record<string, unknown>
|
||||
soilMoistureHeatmap?: Record<string, unknown>
|
||||
}
|
||||
@@ -16,37 +15,112 @@ interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
function extract<T>(res: ApiResponse<T> | T): T {
|
||||
interface StatusResponse<T> {
|
||||
status: string
|
||||
data: T
|
||||
}
|
||||
|
||||
type HeatmapPoint = {
|
||||
x: string
|
||||
y: number
|
||||
}
|
||||
|
||||
type HeatmapSeries = {
|
||||
name: string
|
||||
data: HeatmapPoint[]
|
||||
}
|
||||
|
||||
function extract<T>(res: ApiResponse<T> | StatusResponse<T> | T): T {
|
||||
return res && typeof res === 'object' && 'data' in res ? (res as ApiResponse<T>).data : (res as T)
|
||||
}
|
||||
|
||||
function getNumericValue(value: unknown): number {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value
|
||||
|
||||
const parsed = Number(value)
|
||||
|
||||
return Number.isFinite(parsed) ? parsed : 0
|
||||
}
|
||||
|
||||
function normalizeHeatmapSeries(data: Record<string, unknown>): HeatmapSeries[] {
|
||||
const legacySeries = Array.isArray(data.series) ? (data.series as HeatmapSeries[]) : []
|
||||
|
||||
if (legacySeries.length > 0) return legacySeries
|
||||
|
||||
const gridCells = Array.isArray(data.grid_cells) ? (data.grid_cells as Array<Record<string, unknown>>) : []
|
||||
|
||||
if (gridCells.length === 0) return []
|
||||
|
||||
const grouped = new Map<string, HeatmapPoint[]>()
|
||||
|
||||
gridCells.forEach((cell, index) => {
|
||||
const rowLabel = String(
|
||||
cell.zone_name ?? cell.zone ?? cell.row_label ?? cell.row ?? cell.y_label ?? `ردیف ${index + 1}`
|
||||
)
|
||||
const columnLabel = String(
|
||||
cell.time_label ?? cell.col_label ?? cell.column_label ?? cell.x_label ?? cell.col ?? cell.x ?? `${index + 1}`
|
||||
)
|
||||
const rawValue = cell.value ?? cell.moisture ?? cell.moisture_percent ?? cell.intensity ?? cell.y
|
||||
const value = getNumericValue(rawValue)
|
||||
const current = grouped.get(rowLabel) ?? []
|
||||
|
||||
current.push({ x: columnLabel, y: value })
|
||||
grouped.set(rowLabel, current)
|
||||
})
|
||||
|
||||
return Array.from(grouped.entries()).map(([name, points]) => ({
|
||||
name,
|
||||
data: points
|
||||
}))
|
||||
}
|
||||
|
||||
function buildHealthScoreKpi(summary: Record<string, unknown>): Record<string, unknown> {
|
||||
const healthScore = getNumericValue(summary.healthScore)
|
||||
const profileSource = String(summary.profileSource ?? 'مرجع خاک')
|
||||
const healthLanguage =
|
||||
summary.healthLanguage && typeof summary.healthLanguage === 'object'
|
||||
? (summary.healthLanguage as Record<string, unknown>)
|
||||
: {}
|
||||
const chipText = String(healthLanguage.short_chip_text ?? summary.avgSoilMoistureStatus ?? '-')
|
||||
|
||||
let chipColor: 'success' | 'warning' | 'error' = 'success'
|
||||
|
||||
if (healthScore < 45) chipColor = 'error'
|
||||
else if (healthScore < 70) chipColor = 'warning'
|
||||
|
||||
return {
|
||||
id: 'soil_health_score',
|
||||
title: 'سلامت خاک',
|
||||
subtitle: profileSource,
|
||||
stats: `${Math.round(healthScore)} / 100`,
|
||||
avatarColor: chipColor === 'error' ? 'error' : chipColor === 'warning' ? 'warning' : 'success',
|
||||
avatarIcon: 'tabler-activity-heartbeat',
|
||||
chipText,
|
||||
chipColor
|
||||
}
|
||||
}
|
||||
|
||||
export const soilService = {
|
||||
async getSummary(farmUuid: string): Promise<SoilSummary> {
|
||||
const res = await apiClient.get<ApiResponse<SoilSummary> | SoilSummary>(
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
return extract(res)
|
||||
const data = extract(res)
|
||||
|
||||
return {
|
||||
summaryKpis: { kpis: [buildHealthScoreKpi(data)] }
|
||||
}
|
||||
},
|
||||
|
||||
async getAvgMoisture(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
const res = await apiClient.get<StatusResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/avg-moisture/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
return extract(res)
|
||||
},
|
||||
const data = extract(res)
|
||||
|
||||
async getSensorRadarChart(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/sensor-radar-chart/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
return extract(res)
|
||||
},
|
||||
|
||||
async getSensorComparisonChart(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/sensor-comparison-chart/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
return extract(res)
|
||||
return {
|
||||
kpis: [data]
|
||||
}
|
||||
},
|
||||
|
||||
async getAnomalies(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
@@ -60,6 +134,11 @@ export const soilService = {
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/moisture-heatmap/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
return extract(res)
|
||||
const data = extract(res)
|
||||
|
||||
return {
|
||||
...data,
|
||||
series: normalizeHeatmapSeries(data)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { apiClient } from '../client'
|
||||
|
||||
const PREFIX = '/api/water'
|
||||
const WATER_PREFIX = '/api/water'
|
||||
const WEATHER_PREFIX = '/api/weather'
|
||||
|
||||
export interface WaterSummary {
|
||||
farmWeatherCard?: Record<string, unknown>
|
||||
waterNeedPrediction?: Record<string, unknown>
|
||||
water_stress_index?: Record<string, unknown>
|
||||
waterStressIndex?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
@@ -14,36 +15,66 @@ interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
function extract<T>(res: ApiResponse<T> | T): T {
|
||||
interface StatusResponse<T> {
|
||||
status: string
|
||||
data: T
|
||||
}
|
||||
|
||||
function extract<T>(res: ApiResponse<T> | StatusResponse<T> | T): T {
|
||||
return res && typeof res === 'object' && 'data' in res ? (res as ApiResponse<T>).data : (res as T)
|
||||
}
|
||||
|
||||
function withFarmUuid(endpoint: string, farmUuid?: string): string {
|
||||
if (!farmUuid) return endpoint
|
||||
|
||||
const separator = endpoint.includes('?') ? '&' : '?'
|
||||
|
||||
return `${endpoint}${separator}farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
}
|
||||
|
||||
function normalizeSummary(data: Record<string, unknown>): WaterSummary {
|
||||
return {
|
||||
farmWeatherCard: (data.farmWeatherCard as Record<string, unknown> | undefined) ?? {},
|
||||
waterNeedPrediction: (data.waterNeedPrediction as Record<string, unknown> | undefined) ?? {},
|
||||
waterStressIndex:
|
||||
(data.waterStressIndex as Record<string, unknown> | undefined) ??
|
||||
(data.water_stress_index as Record<string, unknown> | undefined) ??
|
||||
{},
|
||||
}
|
||||
}
|
||||
|
||||
export const waterService = {
|
||||
async getSummary(farmUuid: string): Promise<WaterSummary> {
|
||||
const res = await apiClient.get<ApiResponse<WaterSummary> | WaterSummary>(
|
||||
`${PREFIX}/summary/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
async getSummary(farmUuid?: string): Promise<WaterSummary> {
|
||||
const res = await apiClient.get<StatusResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
withFarmUuid(`${WATER_PREFIX}/summary/`, farmUuid)
|
||||
)
|
||||
return normalizeSummary(extract(res))
|
||||
},
|
||||
|
||||
async getCard(farmUuid?: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<StatusResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
withFarmUuid(`${WATER_PREFIX}/card/`, farmUuid)
|
||||
)
|
||||
return extract(res)
|
||||
},
|
||||
|
||||
async getCard(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/card/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
async getNeedPrediction(farmUuid?: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<StatusResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
withFarmUuid(`${WATER_PREFIX}/need-prediction/`, farmUuid)
|
||||
)
|
||||
return extract(res)
|
||||
},
|
||||
|
||||
async getNeedPrediction(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/need-prediction/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
return extract(res)
|
||||
},
|
||||
async getWeatherFarmCard(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const res = await apiClient.post<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${WEATHER_PREFIX}/farm-card/`,
|
||||
{ farm_uuid: farmUuid }
|
||||
)
|
||||
|
||||
async getStressIndex(farmUuid: string): Promise<Record<string, unknown>> {
|
||||
const res = await apiClient.get<ApiResponse<Record<string, unknown>> | Record<string, unknown>>(
|
||||
`${PREFIX}/stress-index/?farm_uuid=${encodeURIComponent(farmUuid)}`
|
||||
)
|
||||
return extract(res)
|
||||
return extract(res)
|
||||
} catch {
|
||||
return this.getCard(farmUuid)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user