This commit is contained in:
2026-04-29 22:26:53 +03:30
parent 8f74e4f385
commit 04d678fda4
23 changed files with 2860 additions and 711 deletions
@@ -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
}
},
}
+100 -21
View File
@@ -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)
}
},
}
+51 -20
View File
@@ -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)
}
},
}