feat(flights): integrate mission-planner into Flights page

- Port mission-planner flight planning to main app (Tailwind, react-leaflet v5, react-i18next)
- Add FlightMap with click-to-add waypoints, draggable markers, polyline with arrows
- Add FlightParamsPanel with action modes, waypoint list (drag-reorder), altitude chart, wind, JSON import/export
- Add FlightListSidebar with create/delete and telemetry date
- Add collapsible left panel with quick action mode shortcuts
- Add work area / no-go zone drawing via manual mouse events (L.rectangle)
- Add AltitudeDialog and JsonEditorDialog (Tailwind modals)
- Add battery/time/distance calculations per waypoint segment
- Add satellite/classic map toggle and mini-map on point drag
- Add Camera FOV and Communication Addr fields
- Add current position display under location search
- Merge mission-planner translations under flights.planner.*
- Gitignore .superpowers session data
This commit is contained in:
Oleksandr Hutsul
2026-04-17 00:31:24 +03:00
parent 567092188d
commit 274800e508
21 changed files with 1489 additions and 131 deletions
+150
View File
@@ -0,0 +1,150 @@
import { useTranslation } from 'react-i18next'
import WaypointList from './WaypointList'
import AltitudeChart from './AltitudeChart'
import WindEffect from './WindEffect'
import type { FlightPoint, CalculatedPointInfo, ActionMode, WindParams } from './types'
import type { Aircraft } from '../../types'
interface Props {
points: FlightPoint[]
calculatedPointInfo: CalculatedPointInfo[]
aircrafts: Aircraft[]
initialAltitude: number
actionMode: ActionMode
wind: WindParams
locationInput: string
currentPosition: { lat: number; lng: number }
totalDistance: string
totalTime: string
batteryStatus: { label: string; color: string }
onInitialAltitudeChange: (v: number) => void
onActionModeChange: (mode: ActionMode) => void
onWindChange: (w: WindParams) => void
onLocationInputChange: (v: string) => void
onLocationSearch: () => void
onReorderPoints: (points: FlightPoint[]) => void
onEditPoint: (point: FlightPoint) => void
onRemovePoint: (id: string) => void
onSave: () => void
onUpload: () => void
onEditJson: () => void
onExport: () => void
}
export default function FlightParamsPanel({
points, calculatedPointInfo, aircrafts, initialAltitude, actionMode, wind,
locationInput, currentPosition, totalDistance, totalTime, batteryStatus,
onInitialAltitudeChange, onActionModeChange, onWindChange,
onLocationInputChange, onLocationSearch, onReorderPoints, onEditPoint, onRemovePoint,
onSave, onUpload, onEditJson, onExport,
}: Props) {
const { t } = useTranslation()
const modeBtn = (mode: ActionMode, label: string, color: 'orange' | 'green' | 'red') => {
const active = actionMode === mode
const colorMap = {
orange: { border: 'border-az-orange', text: 'text-az-orange', bg: 'bg-az-orange/20', hover: 'hover:bg-az-orange/10' },
green: { border: 'border-az-green', text: 'text-az-green', bg: 'bg-az-green/20', hover: 'hover:bg-az-green/10' },
red: { border: 'border-az-red', text: 'text-az-red', bg: 'bg-az-red/20', hover: 'hover:bg-az-red/10' },
}[color]
return (
<button
onClick={() => onActionModeChange(mode)}
className={`flex-1 px-2.5 py-1 rounded border text-[11px] ${colorMap.border} ${colorMap.text} ${active ? colorMap.bg : colorMap.hover}`}
>{label}</button>
)
}
return (
<div className="p-2 space-y-2 text-xs overflow-y-auto flex-1">
<div className="flex gap-1">
{modeBtn('points', t('flights.planner.addPoints'), 'orange')}
{modeBtn('workArea', t('flights.planner.workArea'), 'green')}
{modeBtn('prohibitedArea', t('flights.planner.prohibitedArea'), 'red')}
</div>
<div>
<label className="text-az-muted block mb-0.5 text-[9px]">{t('flights.planner.location')}</label>
<input
value={locationInput}
onChange={e => onLocationInputChange(e.target.value)}
onKeyDown={e => e.key === 'Enter' && onLocationSearch()}
placeholder="47.242, 35.024"
className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text outline-none focus:border-az-orange"
/>
<div className="text-az-muted text-[9px] mt-0.5">
{t('flights.planner.currentLocation')}: {currentPosition.lat.toFixed(6)}, {currentPosition.lng.toFixed(6)}
</div>
</div>
<div>
<label className="text-az-muted block mb-0.5 text-[9px]">{t('flights.aircraft')}</label>
<select className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text">
{aircrafts.map(a => <option key={a.id} value={a.id}>{a.model}</option>)}
</select>
</div>
<div>
<label className="text-az-muted block mb-0.5 text-[9px]">{t('flights.planner.initialAltitude')}</label>
<input type="number" value={initialAltitude}
onChange={e => onInitialAltitudeChange(Number(e.target.value))}
className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text outline-none focus:border-az-orange"
/>
</div>
<div>
<label className="text-az-muted block mb-0.5 text-[9px]">{t('flights.planner.cameraFov')}</label>
<input type="text" placeholder={t('flights.planner.cameraFovPlaceholder')}
className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text outline-none focus:border-az-orange"
/>
</div>
<div>
<label className="text-az-muted block mb-0.5 text-[9px]">{t('flights.planner.commAddr')}</label>
<input type="text" placeholder={t('flights.planner.commAddrPlaceholder')}
className="w-full bg-az-bg border border-az-border rounded px-2 py-1 text-az-text outline-none focus:border-az-orange"
/>
</div>
<div>
<label className="text-az-muted block mb-1 text-[9px]">{t('flights.waypoints')}</label>
<WaypointList
points={points}
calculatedPointInfo={calculatedPointInfo}
onReorder={onReorderPoints}
onEdit={onEditPoint}
onRemove={onRemovePoint}
/>
</div>
{points.length > 1 && (
<div className="bg-az-header rounded px-2 py-1 flex gap-2 text-[10px]">
<span>{totalDistance}</span>
<span>{totalTime}</span>
<span style={{ color: batteryStatus.color }}>{batteryStatus.label}</span>
</div>
)}
<AltitudeChart points={points} />
<WindEffect wind={wind} onChange={onWindChange} />
<div className="flex gap-1">
<button onClick={onSave} className="flex-1 px-2.5 py-1 rounded border border-az-green text-az-green text-[11px] hover:bg-az-green/10">
{t('flights.planner.save')}
</button>
<button onClick={onUpload} className="flex-1 px-2.5 py-1 rounded border border-az-blue text-az-blue text-[11px] hover:bg-az-blue/10">
{t('flights.planner.upload')}
</button>
</div>
<div className="flex gap-1">
<button onClick={onEditJson} className="flex-1 px-2.5 py-1 rounded border border-az-muted text-az-text text-[11px] hover:border-az-text hover:text-white">
{t('flights.planner.editAsJson')}
</button>
<button onClick={onExport} className="flex-1 px-2.5 py-1 rounded border border-az-muted text-az-text text-[11px] hover:border-az-text hover:text-white">
{t('flights.planner.exportMapData')}
</button>
</div>
</div>
)
}