mirror of
https://github.com/calli-eve/eve-pi.git
synced 2026-06-11 08:35:41 +02:00
581 lines
23 KiB
TypeScript
581 lines
23 KiB
TypeScript
import { AccessToken, PlanetWithInfo, Pin } from "@/types";
|
|
import { Box, Stack, Typography, useTheme, Paper, IconButton, Divider } from "@mui/material";
|
|
import { CharacterRow } from "../Characters/CharacterRow";
|
|
import { PlanetaryInteractionRow } from "../PlanetaryInteraction/PlanetaryInteractionRow";
|
|
import { SessionContext } from "@/app/context/Context";
|
|
import { useContext, useState, useEffect } from "react";
|
|
import { PlanRow } from "./PlanRow";
|
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
import { DragDropContext, Droppable, Draggable, DropResult } from "@hello-pangea/dnd";
|
|
import { planetCalculations } from "@/planets";
|
|
import { EvePraisalResult } from "@/eve-praisal";
|
|
import { STORAGE_IDS, PI_SCHEMATICS, PI_PRODUCT_VOLUMES, STORAGE_CAPACITIES } from "@/const";
|
|
import { DateTime } from "luxon";
|
|
import { PlanetCalculations, AlertState, StorageContent, StorageInfo } from "@/types/planet";
|
|
import { getProgramOutputPrediction } from "../PlanetaryInteraction/ExtractionSimulation";
|
|
|
|
interface AccountTotals {
|
|
monthlyEstimate: number;
|
|
storageValue: number;
|
|
planetCount: number;
|
|
characterCount: number;
|
|
runningExtractors: number;
|
|
totalExtractors: number;
|
|
}
|
|
|
|
const calculateAlertState = (planetDetails: PlanetCalculations, minExtractionRate: number): AlertState => {
|
|
const hasLowStorage = planetDetails.storageInfo.some(storage => storage.fillRate > 60);
|
|
const hasLowImports = planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24);
|
|
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
|
|
|
|
return {
|
|
expired: planetDetails.expired,
|
|
hasLowStorage,
|
|
hasLowImports,
|
|
hasLargeExtractorDifference: planetDetails.hasLargeExtractorDifference,
|
|
hasLowExtractionRate
|
|
};
|
|
};
|
|
|
|
const calculatePlanetDetails = (planet: PlanetWithInfo, piPrices: EvePraisalResult | undefined, balanceThreshold: number): PlanetCalculations => {
|
|
const { expired, extractors, localProduction: rawProduction, localImports, localExports: rawExports } = planetCalculations(planet);
|
|
|
|
// Convert localProduction to include factoryCount
|
|
const localProduction = new Map(Array.from(rawProduction).map(([key, value]) => [
|
|
key,
|
|
{
|
|
...value,
|
|
factoryCount: value.count || 1
|
|
}
|
|
]));
|
|
|
|
// Calculate extractor averages and check for large differences
|
|
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds
|
|
const extractorAverages = extractors
|
|
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
|
|
.map(e => {
|
|
const installDate = new Date(e.install_time ?? "");
|
|
const expiryDate = new Date(e.expiry_time ?? "");
|
|
const programDuration = (expiryDate.getTime() - installDate.getTime()) / 1000;
|
|
const cycles = Math.floor(programDuration / CYCLE_TIME);
|
|
|
|
const qtyPerCycle = e.extractor_details?.qty_per_cycle || 0;
|
|
const prediction = getProgramOutputPrediction(qtyPerCycle, CYCLE_TIME, cycles);
|
|
const totalOutput = prediction.reduce((sum, val) => sum + val, 0);
|
|
const averagePerHour = totalOutput / cycles * 2;
|
|
|
|
return {
|
|
typeId: e.extractor_details!.product_type_id!,
|
|
averagePerHour
|
|
};
|
|
});
|
|
|
|
const hasLargeExtractorDifference = extractorAverages.length === 2 &&
|
|
Math.abs(extractorAverages[0].averagePerHour - extractorAverages[1].averagePerHour) > balanceThreshold;
|
|
|
|
// Calculate storage info
|
|
const storageFacilities = planet.info.pins.filter((pin: Pin) =>
|
|
STORAGE_IDS().some(storage => storage.type_id === pin.type_id)
|
|
);
|
|
|
|
const storageInfo = storageFacilities.map((storage: Pin) => {
|
|
if (!storage || !storage.contents) return null;
|
|
|
|
const storageType = STORAGE_IDS().find(s => s.type_id === storage.type_id)?.name || 'Unknown';
|
|
const storageCapacity = STORAGE_CAPACITIES[storage.type_id] || 0;
|
|
|
|
const totalVolume = (storage.contents || [])
|
|
.reduce((sum: number, item: StorageContent) => {
|
|
const volume = PI_PRODUCT_VOLUMES[item.type_id] || 0;
|
|
return sum + (item.amount * volume);
|
|
}, 0);
|
|
|
|
const totalValue = (storage.contents || [])
|
|
.reduce((sum: number, item: StorageContent) => {
|
|
const price = piPrices?.appraisal.items.find((a) => a.typeID === item.type_id)?.prices.sell.min ?? 0;
|
|
return sum + (item.amount * price);
|
|
}, 0);
|
|
|
|
const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0;
|
|
|
|
return {
|
|
pin_id: storage.pin_id,
|
|
type: storageType,
|
|
type_id: storage.type_id,
|
|
capacity: storageCapacity,
|
|
used: totalVolume,
|
|
fillRate: fillRate,
|
|
value: totalValue
|
|
};
|
|
}).filter(Boolean) as StorageInfo[];
|
|
|
|
// Calculate import depletion times
|
|
const importDepletionTimes = localImports.map(i => {
|
|
// Find all storage facilities containing this import
|
|
const storagesWithImport = storageFacilities.filter((storage: Pin) =>
|
|
storage.contents?.some((content: StorageContent) => content.type_id === i.type_id)
|
|
);
|
|
|
|
// Get the total amount in all storage facilities
|
|
const totalAmount = storagesWithImport.reduce((sum: number, storage: Pin) => {
|
|
const content = storage.contents?.find((content: StorageContent) => content.type_id === i.type_id);
|
|
return sum + (content?.amount ?? 0);
|
|
}, 0);
|
|
|
|
// Calculate consumption rate per hour
|
|
const schematic = PI_SCHEMATICS.find(s => s.schematic_id === i.schematic_id);
|
|
const cycleTime = schematic?.cycle_time ?? 3600;
|
|
const consumptionPerHour = i.quantity * i.factoryCount * (3600 / cycleTime);
|
|
|
|
// Calculate time until depletion in hours, starting from last_update
|
|
const lastUpdate = DateTime.fromISO(planet.last_update);
|
|
const now = DateTime.now();
|
|
const hoursSinceUpdate = now.diff(lastUpdate, 'hours').hours;
|
|
const remainingAmount = Math.max(0, totalAmount - (consumptionPerHour * hoursSinceUpdate));
|
|
const hoursUntilDepletion = consumptionPerHour > 0 ? remainingAmount / consumptionPerHour : 0;
|
|
|
|
// Calculate monthly cost
|
|
const price = piPrices?.appraisal.items.find((a) => a.typeID === i.type_id)?.prices.sell.min ?? 0;
|
|
const monthlyCost = (consumptionPerHour * 24 * 30 * price) / 1000000; // Cost in millions
|
|
|
|
return {
|
|
typeId: i.type_id,
|
|
hoursUntilDepletion,
|
|
monthlyCost
|
|
};
|
|
});
|
|
|
|
// Convert localExports to match the LocalExport interface
|
|
const localExports = rawExports.map(e => {
|
|
const schematic = PI_SCHEMATICS.flatMap(s => s.outputs)
|
|
.find(s => s.type_id === e.typeId)?.schematic_id ?? 0;
|
|
const factoryCount = planet.info.pins
|
|
.filter(p => p.schematic_id === schematic)
|
|
.length;
|
|
|
|
return {
|
|
type_id: e.typeId,
|
|
schematic_id: schematic,
|
|
quantity: e.amount / factoryCount, // Convert total amount back to per-factory quantity
|
|
factoryCount
|
|
};
|
|
});
|
|
|
|
return {
|
|
expired,
|
|
extractors,
|
|
localProduction,
|
|
localImports,
|
|
localExports,
|
|
storageInfo,
|
|
extractorAverages,
|
|
hasLargeExtractorDifference,
|
|
importDepletionTimes,
|
|
visibility: 'visible' as const
|
|
};
|
|
};
|
|
|
|
const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => {
|
|
let totalMonthlyEstimate = 0;
|
|
let totalStorageValue = 0;
|
|
let totalPlanetCount = 0;
|
|
let totalCharacterCount = characters.length;
|
|
let runningExtractors = 0;
|
|
let totalExtractors = 0;
|
|
|
|
characters.forEach((character) => {
|
|
totalPlanetCount += character.planets.length;
|
|
character.planets.forEach((planet) => {
|
|
const { localExports, extractors } = planetCalculations(planet);
|
|
const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id);
|
|
|
|
// Count running and total extractors
|
|
if (!planetConfig?.excludeFromTotals) {
|
|
extractors.forEach(extractor => {
|
|
totalExtractors++;
|
|
if (extractor.expiry_time && new Date(extractor.expiry_time) > new Date()) {
|
|
runningExtractors++;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Calculate monthly estimate
|
|
if (!planetConfig?.excludeFromTotals) {
|
|
localExports.forEach((exportItem) => {
|
|
const valueInMillions = (((piPrices?.appraisal.items.find(
|
|
(a) => a.typeID === exportItem.typeId,
|
|
)?.prices.sell.min ?? 0) *
|
|
exportItem.amount) /
|
|
1000000) *
|
|
24 *
|
|
30;
|
|
totalMonthlyEstimate += valueInMillions;
|
|
});
|
|
}
|
|
|
|
if (!planetConfig?.excludeFromTotals) {
|
|
planet.info.pins
|
|
.filter(pin => STORAGE_IDS().some(storage => storage.type_id === pin.type_id))
|
|
.forEach(storage => {
|
|
storage.contents?.forEach(content => {
|
|
const valueInMillions = (piPrices?.appraisal.items.find(
|
|
(a) => a.typeID === content.type_id,
|
|
)?.prices.sell.min ?? 0) * content.amount / 1000000;
|
|
totalStorageValue += valueInMillions;
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return {
|
|
monthlyEstimate: totalMonthlyEstimate,
|
|
storageValue: totalStorageValue,
|
|
planetCount: totalPlanetCount,
|
|
characterCount: totalCharacterCount,
|
|
runningExtractors,
|
|
totalExtractors
|
|
};
|
|
};
|
|
|
|
export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { characters: AccessToken[], isCollapsed?: boolean }) => {
|
|
const theme = useTheme();
|
|
const [localIsCollapsed, setLocalIsCollapsed] = useState(false);
|
|
const { planMode, piPrices, alertMode, balanceThreshold, minExtractionRate } = useContext(SessionContext);
|
|
const accountName = characters.length > 0 ? (characters[0].account ?? "-") : "-";
|
|
const [characterOrder, setCharacterOrder] = useState<number[]>(() => {
|
|
try {
|
|
const saved = localStorage.getItem(`characterOrder-${accountName}`);
|
|
if (saved) {
|
|
const parsed: number[] = JSON.parse(saved);
|
|
const ids = characters.map(c => c.character.characterId);
|
|
const valid = parsed.filter(id => ids.includes(id));
|
|
const newIds = ids.filter(id => !valid.includes(id));
|
|
return [...valid, ...newIds];
|
|
}
|
|
} catch (_) { /* ignore corrupt localStorage */ }
|
|
return characters.map(c => c.character.characterId);
|
|
});
|
|
|
|
useEffect(() => {
|
|
const ids = characters.map(c => c.character.characterId);
|
|
setCharacterOrder(prev => {
|
|
const valid = prev.filter(id => ids.includes(id));
|
|
const newIds = ids.filter(id => !valid.includes(id));
|
|
return [...valid, ...newIds];
|
|
});
|
|
}, [characters]);
|
|
|
|
useEffect(() => {
|
|
if (characterOrder.length > 0) {
|
|
localStorage.setItem(`characterOrder-${accountName}`, JSON.stringify(characterOrder));
|
|
}
|
|
}, [characterOrder, accountName]);
|
|
|
|
const [collapsedCharacters, setCollapsedCharacters] = useState<Set<number>>(() => {
|
|
try {
|
|
const saved = localStorage.getItem(`collapsedCharacters-${accountName}`);
|
|
if (saved) return new Set<number>(JSON.parse(saved));
|
|
} catch (_) { /* ignore corrupt localStorage */ }
|
|
return new Set<number>();
|
|
});
|
|
|
|
const toggleCharacterCollapsed = (characterId: number) => {
|
|
setCollapsedCharacters(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(characterId)) next.delete(characterId);
|
|
else next.add(characterId);
|
|
localStorage.setItem(`collapsedCharacters-${accountName}`, JSON.stringify(Array.from(next)));
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const orderedCharacters = characterOrder
|
|
.map(id => characters.find(c => c.character.characterId === id))
|
|
.filter((c): c is AccessToken => c !== undefined);
|
|
|
|
const handleCharacterDragEnd = (result: DropResult) => {
|
|
if (!result.destination) return;
|
|
const items = Array.from(characterOrder);
|
|
const [moved] = items.splice(result.source.index, 1);
|
|
items.splice(result.destination.index, 0, moved);
|
|
setCharacterOrder(items);
|
|
};
|
|
|
|
const DragDropContextComponent = DragDropContext as any;
|
|
const DroppableComponent = Droppable as any;
|
|
const DraggableComponent = Draggable as any;
|
|
const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices);
|
|
|
|
// Calculate planet details and alert states for each planet
|
|
const planetDetails = characters.reduce((acc, character) => {
|
|
character.planets.forEach(planet => {
|
|
const details = calculatePlanetDetails(planet, piPrices, balanceThreshold);
|
|
acc[`${character.character.characterId}-${planet.planet_id}`] = {
|
|
...details,
|
|
alertState: calculateAlertState(details, minExtractionRate)
|
|
};
|
|
});
|
|
return acc;
|
|
}, {} as Record<string, PlanetCalculations & { alertState: AlertState }>);
|
|
|
|
// Update local collapse state when prop changes
|
|
useEffect(() => {
|
|
setLocalIsCollapsed(propIsCollapsed ?? false);
|
|
}, [propIsCollapsed]);
|
|
|
|
const getAlertVisibility = (alertState: AlertState) => {
|
|
if (!alertMode) return 'visible';
|
|
if (alertState.expired) return 'visible';
|
|
if (alertState.hasLowStorage) return 'visible';
|
|
if (alertState.hasLowImports) return 'visible';
|
|
if (alertState.hasLargeExtractorDifference) return 'visible';
|
|
if (alertState.hasLowExtractionRate) return 'visible';
|
|
return 'hidden';
|
|
};
|
|
|
|
// Check if any planet in the account has alerts
|
|
const hasAnyAlerts = Object.values(planetDetails).some(details => {
|
|
const alertState = calculateAlertState(details, minExtractionRate);
|
|
return alertState.expired ||
|
|
alertState.hasLowStorage ||
|
|
alertState.hasLowImports ||
|
|
alertState.hasLargeExtractorDifference ||
|
|
alertState.hasLowExtractionRate;
|
|
});
|
|
|
|
// If in alert mode and no alerts, hide the entire card
|
|
if (alertMode && !hasAnyAlerts) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Paper
|
|
elevation={2}
|
|
sx={{
|
|
padding: theme.custom.compactMode ? theme.spacing(1) : theme.spacing(2),
|
|
margin: theme.spacing(1),
|
|
display: 'flex',
|
|
alignItems: 'flex-start',
|
|
gap: 1,
|
|
backgroundColor: theme.palette.background.paper,
|
|
transition: 'all 0.2s ease-in-out',
|
|
cursor: 'grab',
|
|
'&:hover': {
|
|
boxShadow: theme.shadows[4],
|
|
},
|
|
'&:active': {
|
|
boxShadow: theme.shadows[8],
|
|
cursor: 'grabbing',
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ flex: 1 }}>
|
|
<Box
|
|
sx={{
|
|
backgroundColor: theme.palette.background.default,
|
|
borderRadius: 1,
|
|
padding: theme.spacing(1),
|
|
marginBottom: theme.spacing(2),
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
cursor: 'pointer',
|
|
'&:hover': {
|
|
backgroundColor: theme.palette.action.hover,
|
|
},
|
|
}}
|
|
onClick={() => setLocalIsCollapsed(!localIsCollapsed)}
|
|
>
|
|
<Box>
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.9rem",
|
|
fontWeight: 500,
|
|
color: theme.palette.text.primary,
|
|
}}
|
|
>
|
|
{characters.length > 0 && characters[0].account !== "-"
|
|
? `Account: ${characters[0].account}`
|
|
: "No account name"}
|
|
</Typography>
|
|
<Box sx={{
|
|
display: 'flex',
|
|
gap: 2,
|
|
flexWrap: 'wrap',
|
|
mt: 1,
|
|
alignItems: 'center'
|
|
}}>
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.8rem",
|
|
color: theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Monthly: {monthlyEstimate >= 1000
|
|
? `${(monthlyEstimate / 1000).toFixed(2)} B`
|
|
: `${monthlyEstimate.toFixed(2)} M`} ISK
|
|
</Typography>
|
|
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.8rem",
|
|
color: theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Storage: {storageValue >= 1000
|
|
? `${(storageValue / 1000).toFixed(2)} B`
|
|
: `${storageValue.toFixed(2)} M`} ISK
|
|
</Typography>
|
|
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.8rem",
|
|
color: theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Planets: {planetCount}
|
|
</Typography>
|
|
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.8rem",
|
|
color: theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Characters: {characterCount}
|
|
</Typography>
|
|
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.8rem",
|
|
color: runningExtractors < totalExtractors ? theme.palette.error.main : theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Extractors: {runningExtractors}/{totalExtractors}
|
|
</Typography>
|
|
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.8rem",
|
|
color: Object.values(planetDetails).some(d => d.alertState.hasLowStorage) ? theme.palette.error.main : theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Storage Alerts: {Object.values(planetDetails).filter(d => d.alertState.hasLowStorage).length}
|
|
</Typography>
|
|
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
|
|
<Typography
|
|
sx={{
|
|
fontSize: "0.8rem",
|
|
color: Object.values(planetDetails).some(d => d.alertState.hasLargeExtractorDifference) ? theme.palette.error.main : theme.palette.text.secondary,
|
|
}}
|
|
>
|
|
Balance Alerts: {Object.values(planetDetails).filter(d => d.alertState.hasLargeExtractorDifference).length}
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
<IconButton
|
|
size="small"
|
|
sx={{
|
|
transform: localIsCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
|
|
transition: 'transform 0.2s ease-in-out'
|
|
}}
|
|
>
|
|
{localIsCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
|
</IconButton>
|
|
</Box>
|
|
{!localIsCollapsed && (
|
|
<DragDropContextComponent onDragEnd={handleCharacterDragEnd}>
|
|
<DroppableComponent droppableId={`characters-${accountName}`} direction="vertical">
|
|
{(provided: any) => (
|
|
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
|
{orderedCharacters.map((c, index) => (
|
|
<DraggableComponent
|
|
key={c.character.characterId}
|
|
draggableId={`char-${c.character.characterId}`}
|
|
index={index}
|
|
>
|
|
{(provided: any, snapshot: any) => (
|
|
<Stack
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
direction="row"
|
|
alignItems="flex-start"
|
|
sx={{
|
|
opacity: snapshot.isDragging ? 0.8 : 1,
|
|
backgroundColor: snapshot.isDragging ? theme.palette.action.hover : 'transparent',
|
|
borderRadius: 1,
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
pt: 1,
|
|
gap: 0.5,
|
|
}}
|
|
>
|
|
<Box
|
|
{...provided.dragHandleProps}
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
cursor: 'grab',
|
|
px: 0.5,
|
|
color: theme.palette.text.disabled,
|
|
'&:hover': { color: theme.palette.text.secondary },
|
|
'&:active': { cursor: 'grabbing' },
|
|
}}
|
|
>
|
|
⠿
|
|
</Box>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => toggleCharacterCollapsed(c.character.characterId)}
|
|
sx={{
|
|
p: 0.25,
|
|
color: theme.palette.text.disabled,
|
|
'&:hover': { color: theme.palette.text.secondary },
|
|
transform: collapsedCharacters.has(c.character.characterId) ? 'rotate(0deg)' : 'rotate(90deg)',
|
|
transition: 'transform 0.2s ease-in-out',
|
|
}}
|
|
>
|
|
<ChevronRightIcon fontSize="small" />
|
|
</IconButton>
|
|
</Box>
|
|
<CharacterRow character={c} />
|
|
{!collapsedCharacters.has(c.character.characterId) && (
|
|
planMode ? (
|
|
<PlanRow character={c} />
|
|
) : (
|
|
<PlanetaryInteractionRow
|
|
character={c}
|
|
planetDetails={c.planets.reduce((acc, planet) => {
|
|
const details = planetDetails[`${c.character.characterId}-${planet.planet_id}`];
|
|
acc[planet.planet_id] = {
|
|
...details,
|
|
visibility: getAlertVisibility(details.alertState)
|
|
};
|
|
return acc;
|
|
}, {} as Record<number, PlanetCalculations & { visibility: string }>)}
|
|
/>
|
|
)
|
|
)}
|
|
</Stack>
|
|
)}
|
|
</DraggableComponent>
|
|
))}
|
|
{provided.placeholder}
|
|
</Box>
|
|
)}
|
|
</DroppableComponent>
|
|
</DragDropContextComponent>
|
|
)}
|
|
</Box>
|
|
</Paper>
|
|
);
|
|
};
|