import CancelIcon from "@mui/icons-material/Cancel"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import EditIcon from "@mui/icons-material/Edit"; import SaveIcon from "@mui/icons-material/Save"; import { Box, Card, CardContent, Typography } from '@mui/material'; import Grid from "@mui/material/Grid"; import { DataGrid, GridActionsCellItem, GridColDef, GridEventListener, GridRowEditStopReasons, GridRowId, GridRowModel, GridRowModes, GridRowModesModel, GridRowsProp } from "@mui/x-data-grid"; import { FormQueryErrors } from "@/core/ui/Widgets/FormError"; import { Exercise } from "@/components/Exercises"; import { RIR_VALUES_SELECT_LIST } from "@/components/Routines/models/BaseConfig"; import { WorkoutLog } from "@/components/Routines/models/WorkoutLog"; import { useDeleteRoutineLogQuery, useEditRoutineLogQuery } from "@/components/Routines/queries"; import { DateTime } from "luxon"; import React from "react"; import { useTranslation } from "react-i18next"; import { CartesianGrid, Legend, Scatter, ScatterChart, Tooltip, TooltipContentProps, XAxis, YAxis } from "recharts"; import { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"; import { generateChartColors } from "@/core/lib/colors"; import { PAGINATION_OPTIONS } from "@/core/lib/consts"; import { dateToLocale, luxonDateTimeToLocale } from "@/core/lib/date"; export const ExerciseLog = (props: { exercise: Exercise, routineId: number, logEntries: WorkoutLog[] | undefined }) => { const { t } = useTranslation(); const logEntries = props.logEntries ?? []; const deleteLogQuery = useDeleteRoutineLogQuery(props.routineId); const editLogQuery = useEditRoutineLogQuery(props.routineId); const initialRows: GridRowsProp = logEntries.map((logEntry: WorkoutLog) => ({ id: logEntry.id, date: logEntry.date, repetitions: logEntry.repetitions, weight: logEntry.weight, rir: logEntry.rir, entry: logEntry })); const [rows, setRows] = React.useState(initialRows); const [rowModesModel, setRowModesModel] = React.useState({}); const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { if (params.reason === GridRowEditStopReasons.rowFocusOut) { event.defaultMuiPrevented = true; } }; const handleEditClick = (id: GridRowId) => () => { setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); }; const handleSaveClick = (id: GridRowId) => () => { setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); }; const handleDeleteClick = (id: GridRowId) => () => { deleteLogQuery.mutate(id.toString()); setRows(rows.filter((row) => row.id !== id)); }; const handleCancelClick = (id: GridRowId) => () => { setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View, ignoreModifications: true }, }); const editedRow = rows.find((row) => row.id === id); if (editedRow!.isNew) { setRows(rows.filter((row) => row.id !== id)); } }; const processRowUpdate = (newRow: GridRowModel) => { const log = newRow.entry; if (log !== undefined) { log.date = newRow.date; log.repetitions = newRow.repetitions; log.weight = newRow.weight; log.rir = newRow.rir; editLogQuery.mutate(log); } const updatedRow = { ...newRow, isNew: false }; setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); return updatedRow; }; const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { setRowModesModel(newRowModesModel); }; const columns: GridColDef[] = [ { field: 'date', type: 'dateTime', flex: 1, editable: true, disableColumnMenu: true, headerName: t('date'), valueFormatter: (value?: Date) => { if (value == null) { return ''; } return `${dateToLocale(value)}`; }, }, { field: 'repetitions', type: 'number', disableColumnMenu: true, editable: true, headerName: t('routines.reps'), }, { field: 'weight', type: 'number', disableColumnMenu: true, editable: true, headerName: t('weight'), }, { field: 'rir', type: 'singleSelect', disableColumnMenu: true, editable: true, headerName: t('routines.rir'), // eslint-disable-next-line @typescript-eslint/no-explicit-any getOptionValue: (value: any) => value.value, // eslint-disable-next-line @typescript-eslint/no-explicit-any getOptionLabel: (value: any) => value.label, valueOptions: RIR_VALUES_SELECT_LIST, }, { field: 'actions', type: 'actions', headerName: 'Actions', width: 100, cellClassName: 'actions', getActions: ({ id }) => { const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; if (isInEditMode) { return [ } label={t('save')} onClick={handleSaveClick(id)} />, } label={t('cancel')} className="textPrimary" onClick={handleCancelClick(id)} color="inherit" />, ]; } return [ } label={t('edit')} className="textPrimary" onClick={handleEditClick(id)} color="inherit" />, } label={t('delete')} onClick={handleDeleteClick(id)} color="inherit" />, ]; }, }, ]; const initialState = { pagination: { paginationModel: { pageSize: 5, }, }, }; return <> {props.exercise.getTranslation().name} ; }; /* * Format the log entries so that they can be passed to the chart * * This is mostly due to the time, which needs to be a number to be shown * in the scatter plot */ const formatData = (data: WorkoutLog[]) => data.map((log) => { return { id: log.id, value: log.weight, time: log.date.getTime(), entry: log, }; }); const ExerciseLogTooltip = ({ active, payload }: TooltipContentProps) => { if (active) { // TODO: translate rir let rir = ''; if (payload?.[1].payload?.entry.rir) { rir = `, ${payload?.[1].payload?.entry.rir} RiR`; } return {luxonDateTimeToLocale(DateTime.fromMillis(payload?.[0].value as number))} {payload?.[1].payload?.entry.repetitions} × {payload?.[1].value}{payload?.[1].unit}{rir} ; } return null; }; export const TimeSeriesChart = (props: { data: WorkoutLog[] }) => { // Group by rep count // // We draw series based on the same reps, as otherwise the chart wouldn't // make much sense const result: Map = props.data.reduce(function (r, a) { r.set(a.repetitions, r.get(a.repetitions) || []); r.get(a.repetitions)!.push(a); return r; }, new Map()); const colorGenerator = generateChartColors(result.size); return ( luxonDateTimeToLocale(DateTime.fromMillis(unixTime))} type="number" /> {Array.from(result).map(([key, value]) => { const color = colorGenerator.next().value!; const formattedData = formatData(value); return ; } )} ); };