import { DragDropContext, Draggable, DraggableStyle, Droppable, DropResult } from "@hello-pangea/dnd"; import AddIcon from "@mui/icons-material/Add"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import { Alert, AlertTitle, Box, Button, ButtonGroup, FormControlLabel, Snackbar, SnackbarCloseReason, Stack, Switch, Tab, Tabs, Typography, useTheme, } from "@mui/material"; import Grid from '@mui/material/Grid'; import { LoadingPlaceholder, LoadingProgressIcon } from "@/core/ui/LoadingWidget/LoadingWidget"; import { useProfileQuery } from "@/components/User"; import { Day } from "@/components/Routines/models/Day"; import { Slot } from "@/components/Routines/models/Slot"; import { SlotEntry } from "@/components/Routines/models/SlotEntry"; import { useAddDayQuery, useAddSlotEntryQuery, useAddSlotQuery, useDeleteSlotQuery, useEditDayOrderQuery, useEditSlotsQuery, useRoutineDetailQuery } from "@/components/Routines/queries"; import { DayForm } from "@/components/Routines/widgets/forms/DayForm"; import { DraggableSlotItem } from "@/components/Routines/widgets/slots/DraggableSlotItem"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { SNACKBAR_AUTO_HIDE_DURATION, WEIGHT_UNIT_KG, WEIGHT_UNIT_LB } from "@/core/lib/consts"; export const DayDragAndDropGrid = (props: { routineId: number, selectedDayIndex: number | null, setSelectedDayIndex: (day: number | null) => void }) => { const { t } = useTranslation(); const routineQuery = useRoutineDetailQuery(props.routineId); const editDayOrderQuery = useEditDayOrderQuery(props.routineId); const addDayQuery = useAddDayQuery(props.routineId); const getListStyle = (isDraggingOver: boolean) => ({ background: isDraggingOver ? "lightblue" : undefined, }); const getItemStyle = (isDragging: boolean, draggableStyle: DraggableStyle) => ({ backgroundColor: "white", margin: 4, ...draggableStyle, opacity: 1, }); const onDragStart = () => { props.setSelectedDayIndex(null); }; const onDragEnd = (result: DropResult) => { // Item was dropped outside the list if (!result.destination) { return; } const updatedDays = Array.from(routineQuery.data!.days); const [movedDay] = updatedDays.splice(result.source.index, 1); updatedDays.splice(result.destination.index, 0, movedDay); props.setSelectedDayIndex(result.destination.index); routineQuery.data!.days = updatedDays; editDayOrderQuery.mutate(updatedDays.map((day, index) => (Day.clone(day, { order: index + 1 })))); }; const handleAddDay = async () => { const newDay = new Day({ routineId: props.routineId, name: `${t('routines.newDay')} ${routineQuery.data!.days.length + 1}`, order: routineQuery.data!.days.length + 1, }); await addDayQuery.mutateAsync(newDay); props.setSelectedDayIndex(routineQuery.data!.days.length); }; if (routineQuery.isLoading) { return ; } return ( {(provided, snapshot) => ( {routineQuery.data!.days.map((day, index) => {(provided, snapshot) => ( } iconPosition="start" onClick={() => props.setSelectedDayIndex(index)} /> )} )} {provided.placeholder} )} {routineQuery.data!.days.length === 0 && {t('routines.routineHasNoDays')} {t('nothingHereYetAction')} } ); }; const useSlotDeletion = (day: Day, routineId: number) => { const deleteSlotQuery = useDeleteSlotQuery(routineId); const [openSnackbar, setOpenSnackbar] = useState(false); const [slotToDelete, setSlotToDelete] = useState(null); const handleCloseSnackbar = ( event: React.SyntheticEvent | Event, reason?: SnackbarCloseReason, ) => { if (slotToDelete !== null) { if (reason === 'timeout') { // Delete on the server deleteSlotQuery.mutate(slotToDelete.id!); setSlotToDelete(null); } else if (reason !== 'clickaway') { // Undo the deletion - re-add the slot using its sort value day.slots = [...day.slots, slotToDelete].sort((a, b) => a.order - b.order); setSlotToDelete(null); } } setOpenSnackbar(false); }; const handleDeleteSlot = (slotId: number) => { const slotIndex = day.slots.findIndex(slot => slot.id === slotId); if (slotIndex !== -1) { const updatedSlots = [...day.slots]; const [deletedSlot] = updatedSlots.splice(slotIndex, 1); day.slots = updatedSlots; setSlotToDelete(deletedSlot); setOpenSnackbar(true); } }; return { openSnackbar, handleCloseSnackbar, handleDeleteSlot }; }; type SlotGroup = { exerciseId: number | null, exerciseName: string | null, slots: { slot: Slot, originalIndex: number }[], }; export const groupSlotsByExercise = (slots: Slot[]): SlotGroup[] => { const groups: SlotGroup[] = []; for (let i = 0; i < slots.length; i++) { const slot = slots[i]; const primaryEntry = slot.entries.length === 1 ? slot.entries[0] : null; const exerciseId = primaryEntry?.exerciseId ?? null; const lastGroup = groups[groups.length - 1]; if (lastGroup && exerciseId !== null && lastGroup.exerciseId === exerciseId) { lastGroup.slots.push({ slot, originalIndex: i }); } else { groups.push({ exerciseId, exerciseName: primaryEntry?.exercise?.getTranslation()?.name ?? null, slots: [{ slot, originalIndex: i }], }); } } return groups; }; const SlotGroupContainer = (props: { group: SlotGroup, routineId: number, onDuplicate: (slotId: number) => void, children: React.ReactNode, }) => { const theme = useTheme(); const [t] = useTranslation(); if (props.group.slots.length <= 1) { return <>{props.children}; } const lastSlot = props.group.slots[props.group.slots.length - 1].slot; return ( {props.group.exerciseName} {props.children} ); }; export const DayDetails = (props: { day: Day, routineId: number, setSelectedDayIndex: (day: number | null) => void }) => { const { t } = useTranslation(); const addSlotEntryQuery = useAddSlotEntryQuery(props.routineId); const addSlotQuery = useAddSlotQuery(props.routineId); const editSlotOrderQuery = useEditSlotsQuery(props.routineId); const userProfileQuery = useProfileQuery(); const [showAutocompleterForSlot, setShowAutocompleterForSlot] = useState(null); const [simpleMode, setSimpleMode] = useState(true); const { openSnackbar, handleCloseSnackbar, handleDeleteSlot } = useSlotDeletion(props.day, props.routineId); const handleAddSlotEntry = (slotId: number) => { const slot = props.day.slots.find(s => s.id === slotId); if (slot === undefined) { console.warn(`Could not find slot with id ${slotId} to add config`); return; } if (showAutocompleterForSlot === slotId) { setShowAutocompleterForSlot(null); } else { setShowAutocompleterForSlot(slotId); } }; const handleDuplicateSlot = async (slotId: number) => { const sourceIndex = props.day.slots.findIndex(s => s.id === slotId); const sourceSlot = props.day.slots[sourceIndex]; if (!sourceSlot || sourceSlot.entries.length === 0) { return; } // Use array index for ordering (1-based), insert after source const insertOrder = sourceIndex + 2; // First, bump the order of all slots after the source (wait for completion) const slotsToUpdate = props.day.slots .slice(sourceIndex + 1) .map((s, i) => Slot.clone(s, { order: insertOrder + 1 + i })); if (slotsToUpdate.length > 0) { await editSlotOrderQuery.mutateAsync(slotsToUpdate); } // Then create the new slot at the correct position const entry = sourceSlot.entries[0]; const newSlot = await addSlotQuery.mutateAsync(new Slot({ dayId: props.day.id!, order: insertOrder, })); // Finally add the exercise entry await addSlotEntryQuery.mutateAsync(new SlotEntry({ slotId: newSlot.id!, exerciseId: entry.exerciseId, type: 'normal', order: 1, weightUnitId: entry.weightUnitId, })); }; const handleAddSlot = () => addSlotQuery.mutate(new Slot({ dayId: props.day.id!, order: props.day.slots.length + 1 })); const onDragEnd = (result: DropResult) => { if (!result.destination) { return; } const updatedSlots = Array.from(props.day.slots); const [movedSlot] = updatedSlots.splice(result.source.index, 1); updatedSlots.splice(result.destination.index, 0, movedSlot); editSlotOrderQuery.mutate(updatedSlots.map((slot, index) => (Slot.clone(slot, { order: index + 1 })))); props.day.slots = updatedSlots; }; const getListStyle = (isDraggingOver: boolean) => ({ background: isDraggingOver ? "lightblue" : undefined, }); return (<> {(!props.day.isRest && props.day.slots.length > 0) && <> setSimpleMode(!simpleMode)} />} label={t('routines.simpleMode')} /> } {(provided, snapshot) => (
{groupSlotsByExercise(props.day.slots).map((group) => {group.slots.map(({ slot, originalIndex }, indexInGroup) => { addSlotEntryQuery.mutate(new SlotEntry({ slotId: slot.id!, exerciseId: exercise.id!, type: 'normal', order: slot.entries.length + 1, weightUnitId: userProfileQuery.data!.useMetric ? WEIGHT_UNIT_KG : WEIGHT_UNIT_LB, })); setShowAutocompleterForSlot(null); }} /> )} )} {provided.placeholder}
)}
{t('undo')} } > Set successfully deleted {!props.day.isRest && } ); };