Files
LynkedUpPro_CRM/api/storm-forecast.js
T
Satyam-Rastogi 12bd2fe0de feat(storm-intel): Phase 4+5 — Tomorrow.io forecast overlay and canvasser zone assignment
- Tomorrow.io 7-day forecast strip with weather icons, storm score bar, and per-day risk color coding
- High-risk alert banner in forecast strip when any day scores >=65
- api/storm-forecast.js: Vercel proxy that reads TOMORROW_API_KEY server-side; graceful empty response when key unset
- useStormForecast hook: USE_MOCK=false with realistic MOCK_FORECAST fallback
- ZoneDrawingLayer: click-to-place polygon vertices using react-leaflet useMapEvents; disables double-click zoom while active; shows live vertex dots + dashed preview polygon
- ZoneLayer: renders stormZones as colored semi-transparent polygons with agent initials at centroid
- ZoneAssignmentModal: name + canvasser picker + color selector + optional storm link; portaled modal
- StormIntelPage: Draw Zone button (Admin/Owner only); drawing state/cancel flow; stormZones displayed on map; zone summary strip in sidebar; StormDetailDrawer has working "Draw Canvasser Zone" action
- FieldStormZonePage: canvasser mobile view at /field/storm-zones showing assigned zones with map, linked storm details, conversion window badge, and peak window alert
- Layout: Storm Zones entry in FIELD_AGENT nav
- App: /field/storm-zones route (FIELD_AGENT + Admin + Owner)
2026-05-18 18:06:17 +05:30

118 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Proxies Tomorrow.io daily weather forecast for the Plano TX area.
* Set TOMORROW_API_KEY in Vercel env vars (and .env locally for vercel dev).
*
* Response: array of 7-day daily forecast objects, normalised for the frontend.
* Cached 1 hour — free tier allows 500 calls/day.
*/
const PLANO_LAT = 33.055;
const PLANO_LNG = -96.752;
// Tomorrow.io weather code → label + hail risk flag
function interpretCode(code) {
const map = {
1000: { label: 'Clear', hailRisk: false },
1001: { label: 'Cloudy', hailRisk: false },
1100: { label: 'Mostly Clear', hailRisk: false },
1101: { label: 'Partly Cloudy', hailRisk: false },
1102: { label: 'Mostly Cloudy', hailRisk: false },
2000: { label: 'Fog', hailRisk: false },
4000: { label: 'Drizzle', hailRisk: false },
4001: { label: 'Rain', hailRisk: false },
4200: { label: 'Light Rain', hailRisk: false },
4201: { label: 'Heavy Rain', hailRisk: false },
5000: { label: 'Snow', hailRisk: false },
5001: { label: 'Flurries', hailRisk: false },
5100: { label: 'Light Snow', hailRisk: false },
5101: { label: 'Heavy Snow', hailRisk: false },
6000: { label: 'Freezing Drizzle', hailRisk: true },
6001: { label: 'Freezing Rain', hailRisk: true },
6200: { label: 'Lt Freezing Rain', hailRisk: true },
6201: { label: 'Hvy Freezing Rain',hailRisk: true },
7000: { label: 'Ice Pellets', hailRisk: true },
7101: { label: 'Heavy Hail', hailRisk: true },
7102: { label: 'Light Hail', hailRisk: true },
8000: { label: 'Thunderstorm', hailRisk: true },
};
return map[code] ?? { label: 'Mixed', hailRisk: false };
}
export default async function handler(req, res) {
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=7200');
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const key = process.env.TOMORROW_API_KEY;
if (!key) {
console.warn('[storm-forecast] TOMORROW_API_KEY not set');
return res.status(200).json([]);
}
try {
const fields = [
'precipitationProbability',
'weatherCode',
'windSpeedAvg',
'windGustMax',
'temperatureAvg',
'precipitationIntensityAvg',
'humidityAvg',
].join(',');
const url = `https://api.tomorrow.io/v4/weather/forecast?location=${PLANO_LAT},${PLANO_LNG}&apikey=${key}&timesteps=1d&fields=${fields}`;
const resp = await fetch(url, {
headers: { Accept: 'application/json' },
});
if (!resp.ok) {
const txt = await resp.text();
console.warn('[storm-forecast] Tomorrow.io error', resp.status, txt);
return res.status(200).json([]);
}
const data = await resp.json();
const daily = data?.timelines?.daily ?? [];
const forecast = daily.slice(0, 7).map(day => {
const v = day.values ?? {};
// Daily aggregated fields may come back as plain names or with Avg/Max suffix
const precipProb = v.precipitationProbability ?? v.precipitationProbabilityAvg ?? 0;
const weatherCode = v.weatherCode ?? v.weatherCodeMax ?? 1000;
const windGust = v.windGustMax ?? v.windGustAvg ?? 0;
const windSpeed = v.windSpeedAvg ?? 0;
const temp = v.temperatureAvg ?? 0;
const humidity = v.humidityAvg ?? 0;
const { label, hailRisk } = interpretCode(weatherCode);
// Storm severity: 0100 score for how dangerous this day looks
const stormScore = Math.min(100, Math.round(
precipProb * 0.5 +
(hailRisk ? 35 : 0) +
(windGust > 40 ? 15 : windGust > 25 ? 8 : 0)
));
return {
date: day.time,
precipProb: Math.round(precipProb),
weatherCode,
weatherLabel: label,
hailRisk,
windGust: Math.round(windGust),
windSpeed: Math.round(windSpeed),
tempF: Math.round(temp),
humidity: Math.round(humidity),
stormScore,
};
});
return res.status(200).json(forecast);
} catch (err) {
console.error('[storm-forecast] error:', err.message);
return res.status(200).json([]);
}
}