import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; import Draggable from 'react-draggable'; import { Card, CardContent, Typography, CardActions, IconButton, Table, TableBody, TableRow, TableCell, Menu, MenuItem, CardMedia, Dialog, TextField, DialogActions, DialogContent, Button, } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import CloseIcon from '@mui/icons-material/Close'; import ReplayIcon from '@mui/icons-material/Replay'; import PublishIcon from '@mui/icons-material/Publish'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import PendingIcon from '@mui/icons-material/Pending'; import { useTranslation } from './LocalizationProvider'; import RemoveDialog from './RemoveDialog'; import PositionValue from './PositionValue'; import { useDeviceReadonly } from '../util/permissions'; import usePositionAttributes from '../attributes/usePositionAttributes'; import { devicesActions } from '../../store'; import { useCatch, useCatchCallback } from '../../reactHelper'; import { useAttributePreference } from '../util/preferences'; const useStyles = makeStyles((theme) => ({ card: { pointerEvents: 'auto', width: theme.dimensions.popupMaxWidth, }, media: { height: theme.dimensions.popupImageHeight, display: 'flex', justifyContent: 'flex-end', alignItems: 'flex-start', }, mediaButton: { color: theme.palette.primary.contrastText, mixBlendMode: 'difference', }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: theme.spacing(1, 1, 0, 2), }, content: { paddingTop: theme.spacing(1), paddingBottom: theme.spacing(1), maxHeight: theme.dimensions.cardContentMaxHeight, overflow: 'auto', }, delete: { color: theme.palette.error.main, }, icon: { width: '25px', height: '25px', filter: 'brightness(0) invert(1)', }, table: { '& .MuiTableCell-sizeSmall': { paddingLeft: 0, paddingRight: 0, }, }, cell: { borderBottom: 'none', }, actions: { justifyContent: 'space-between', }, root: ({ desktopPadding }) => ({ pointerEvents: 'none', position: 'fixed', zIndex: 5, left: '50%', [theme.breakpoints.up('md')]: { left: `calc(50% + ${desktopPadding} / 2)`, bottom: theme.spacing(3), }, [theme.breakpoints.down('md')]: { left: '50%', bottom: `calc(${theme.spacing(3)} + ${theme.dimensions.bottomBarHeight}px)`, }, transform: 'translateX(-50%)', }), })); const StatusRow = ({ name, content }) => { const classes = useStyles(); return ( {name} {content} ); }; const StatusCard = ({ deviceId, position, onClose, disableActions, desktopPadding = 0 }) => { const classes = useStyles({ desktopPadding }); const navigate = useNavigate(); const dispatch = useDispatch(); const t = useTranslation(); const deviceReadonly = useDeviceReadonly(); const shareDisabled = useSelector((state) => state.session.server.attributes.disableShare); const user = useSelector((state) => state.session.user); const device = useSelector((state) => state.devices.items[deviceId]); const deviceImage = device?.attributes?.deviceImage; const positionAttributes = usePositionAttributes(t); const positionItems = useAttributePreference('positionItems', 'speed,address,totalDistance,course'); const [anchorEl, setAnchorEl] = useState(null); const [removing, setRemoving] = useState(false); const [shared, setShared] = useState(null); const handleRemove = useCatch(async (removed) => { if (removed) { const response = await fetch('/api/devices'); if (response.ok) { dispatch(devicesActions.refresh(await response.json())); } else { throw Error(await response.text()); } } setRemoving(false); }); const handleGeofence = useCatchCallback(async () => { const newItem = { name: t('sharedGeofence'), area: `CIRCLE (${position.latitude} ${position.longitude}, 50)`, }; const response = await fetch('/api/geofences', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newItem), }); if (response.ok) { const item = await response.json(); const permissionResponse = await fetch('/api/permissions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deviceId: position.deviceId, geofenceId: item.id }), }); if (!permissionResponse.ok) { throw Error(await permissionResponse.text()); } navigate(`/settings/geofence/${item.id}`); } else { throw Error(await response.text()); } }, [navigate, position]); const handleShare = useCatchCallback(async () => { const expiration = dayjs().add(1, 'week').toISOString(); const response = await fetch('/api/devices/share', { method: 'POST', body: new URLSearchParams(`deviceId=${deviceId}&expiration=${expiration}`), }); if (response.ok) { const token = await response.text(); setShared(`${window.location.origin}?token=${token}`); } else { throw Error(await response.text()); } }, [deviceId, setShared]); return ( <>
{device && ( {deviceImage ? ( ) : (
{device.name}
)} {position && ( {positionItems.split(',').filter((key) => position.hasOwnProperty(key) || position.attributes.hasOwnProperty(key)).map((key) => ( )} /> ))}
)} setAnchorEl(e.currentTarget)} disabled={!position} > navigate('/replay')} disabled={disableActions || !position} > navigate(`/settings/device/${deviceId}/command`)} disabled={disableActions} > navigate(`/settings/device/${deviceId}`)} disabled={disableActions || deviceReadonly} > setRemoving(true)} disabled={disableActions || deviceReadonly} className={classes.delete} >
)}
{position && ( setAnchorEl(null)}> navigate(`/position/${position.id}`)}>{t('sharedShowDetails')} {t('sharedCreateGeofence')} {t('linkGoogleMaps')} {t('linkAppleMaps')} {t('linkStreetView')} {!shareDisabled && !user.temporary && {t('deviceShare')}} )} handleRemove(removed)} /> setShared(null)}> e.target.select()} /> ); }; export default StatusCard;