({ field, onBack, onEditBoundary }: FieldDetailViewProps)
| 214 | const TEXTURE_COLORS = { sand: "#EAB947", silt: "#A0785A", clay: "#854F0B" }; |
| 215 | |
| 216 | const FieldDetailView = ({ field, onBack, onEditBoundary }: FieldDetailViewProps) => { |
| 217 | const [weather, setWeather] = useState<FieldWeather | null>(null); |
| 218 | const [loading, setLoading] = useState(true); |
| 219 | const [ndviStats, setNdviStats] = useState<NdviStats | null>(null); |
| 220 | const [ndviLoading, setNdviLoading] = useState(false); |
| 221 | const [aiAnalysis, setAiAnalysis] = useState<string>(""); |
| 222 | const [aiLoading, setAiLoading] = useState(false); |
| 223 | const [showAnalysis, setShowAnalysis] = useState(false); |
| 224 | const [soilData, setSoilData] = useState<SoilData | null>(null); |
| 225 | const [soilLoading, setSoilLoading] = useState(false); |
| 226 | const [aqiData, setAqiData] = useState<AqiData | null>(null); |
| 227 | const [soilMoisture, setSoilMoisture] = useState<number | null>(null); |
| 228 | |
| 229 | const areaAcres = haToAcres(field.area); |
| 230 | const urban = isUrbanField(field); |
| 231 | |
| 232 | const fieldCenter = useMemo(() => { |
| 233 | const coords = field.coordinates[0]; |
| 234 | return { |
| 235 | lat: coords.reduce((s, c) => s + c[1], 0) / coords.length, |
| 236 | lng: coords.reduce((s, c) => s + c[0], 0) / coords.length, |
| 237 | }; |
| 238 | }, [field]); |
| 239 | |
| 240 | // Fetch weather + AQI + soil moisture |
| 241 | useEffect(() => { |
| 242 | const fetchAll = async () => { |
| 243 | setLoading(true); |
| 244 | const { lat, lng } = fieldCenter; |
| 245 | try { |
| 246 | const [weatherRes, aqiRes, soilMoistRes] = await Promise.all([ |
| 247 | fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code`), |
| 248 | fetch(`https://air-quality-api.open-meteo.com/v1/air-quality?latitude=${lat}&longitude=${lng}¤t=pm2_5,pm10,european_aqi,us_aqi`), |
| 249 | fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&hourly=soil_moisture_0_to_7cm&forecast_days=1`), |
| 250 | ]); |
| 251 | const [wData, aqData, smData] = await Promise.all([weatherRes.json(), aqiRes.json(), soilMoistRes.json()]); |
| 252 | setWeather(wData.current || null); |
| 253 | if (aqData.current) setAqiData(aqData.current); |
| 254 | if (smData.hourly?.soil_moisture_0_to_7cm) { |
| 255 | const vals = smData.hourly.soil_moisture_0_to_7cm.filter((v: any) => v != null); |
| 256 | if (vals.length) setSoilMoisture(Math.round(vals[vals.length - 1] * 1000) / 10); |
| 257 | } |
| 258 | } catch { setWeather(null); } |
| 259 | finally { setLoading(false); } |
| 260 | }; |
| 261 | fetchAll(); |
| 262 | }, [field, fieldCenter]); |
| 263 | |
| 264 | // Fetch soil data |
| 265 | useEffect(() => { |
| 266 | const cached = getFreshLocalCacheValue<SoilData>(SOIL_CACHE_KEY, field.id, QUERY_CACHE_TTL_MS); |
| 267 | if (cached) { setSoilData(cached); return; } |
| 268 | const fetchSoil = async () => { |
| 269 | setSoilLoading(true); |
| 270 | try { |
| 271 | const data = await invokeWithRetry<any>( |
| 272 | "soil-data", |
| 273 | { lat: fieldCenter.lat, lon: fieldCenter.lng }, |
nothing calls this directly
no test coverage detected