Files
tunarr/web/src/components/slot_scheduler/EditTimeSlotDialogContent.tsx
Christian Benincasa 7b0133cac2 checkpoint
2026-03-19 16:05:36 -04:00

302 lines
9.3 KiB
TypeScript

import type {
CustomShowProgramOption,
FillerProgramOption,
} from '@/helpers/slotSchedulerUtil';
import { OneDayMillis } from '@/helpers/slotSchedulerUtil';
import type { TimeSlotViewModel } from '@/model/TimeSlotModels.ts';
import {
Box,
Button,
DialogActions,
DialogContent,
FormControl,
InputLabel,
MenuItem,
Select,
Stack,
Tab,
Tabs,
} from '@mui/material';
import { TimePicker } from '@mui/x-date-pickers';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { find, isNil, map } from 'lodash-es';
import { useCallback, useState } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { useSlotProgramOptionsContext } from '../../hooks/programming_controls/useSlotProgramOptions.ts';
import { useFillerLists } from '../../hooks/useFillerLists.ts';
import { useTimeSlotFormContext } from '../../hooks/useTimeSlotFormContext.ts';
import { TabPanel } from '../TabPanel.tsx';
import { EditSlotProgrammingForm } from './EditSlotProgrammingForm.tsx';
import { SlotFillerDialogPanel } from './LegacySlotFillerDialogPanel.tsx';
import { TimeSlotConfigDialogPanel } from './TimeSlotConfigDialogPanel.tsx';
const DaysOfWeekMenuItems = [
{ value: 0, name: 'Sunday' },
{ value: 1, name: 'Monday' },
{ value: 2, name: 'Tuesday' },
{ value: 3, name: 'Wednesday' },
{ value: 4, name: 'Thursday' },
{ value: 5, name: 'Friday' },
{ value: 6, name: 'Saturday' },
];
type EditTimeSlotDialogContentProps = {
slot: TimeSlotViewModel;
index: number;
onClose: () => void;
};
export const EditTimeSlotDialogContent = ({
slot,
index,
onClose,
}: EditTimeSlotDialogContentProps) => {
const { getValues: getSlotFormValues, slotArray } = useTimeSlotFormContext();
const currentPeriod = getSlotFormValues('period');
const { data: fillerLists } = useFillerLists();
const programOptions = useSlotProgramOptionsContext();
const formMethods = useForm<TimeSlotViewModel>({
defaultValues: slot,
reValidateMode: 'onChange',
});
const {
control,
getValues,
formState: { isValid, isSubmitting },
} = formMethods;
const updateSlotDay = useCallback(
(newDayOfWeek: number, originalOnChange: (...args: unknown[]) => void) => {
const startTimeOfDay = getValues('startTime') % OneDayMillis;
const newStartTime = startTimeOfDay + newDayOfWeek * OneDayMillis;
originalOnChange(newStartTime);
},
[getValues],
);
const updateSlotTime = useCallback(
(
fieldValue: Dayjs | null,
originalOnChange: (...args: unknown[]) => void,
) => {
if (!fieldValue) return;
const h = fieldValue.hour();
const m = fieldValue.minute();
const multiplier = Math.floor(getValues('startTime') / OneDayMillis);
const millis = dayjs.duration({ hours: h, minutes: m }).asMilliseconds();
originalOnChange(millis + multiplier * OneDayMillis);
},
[getValues],
);
const slotType = formMethods.watch('type');
const [tab, setTab] = useState(0);
const newSlotForType = useCallback(
(type: TimeSlotViewModel['type']) => {
const startTime = getValues('startTime');
const opt = find(
programOptions,
(opt): opt is CustomShowProgramOption => opt.type === 'custom-show',
);
return match(type)
.returnType<TimeSlotViewModel>()
.with('custom-show', () => {
return {
startTime,
type: 'custom-show',
order: 'next',
direction: 'asc',
customShowId: opt!.customShowId,
title: opt!.description,
customShow: null,
isMissing: false,
};
})
.with('movie', () => ({
startTime,
type: 'movie',
order: 'alphanumeric',
direction: 'asc',
title: 'Movies',
}))
.with('filler', () => {
const opt = programOptions.find(
(opt): opt is FillerProgramOption => opt.type === 'filler',
);
return {
type: 'filler',
order: 'shuffle_prefer_short',
decayFactor: 0.5,
durationWeighting: 'linear',
recoveryFactor: 0.05,
fillerListId: opt?.value ?? '',
startTime,
fillerList: null,
isMissing: false,
};
})
.with('flex', () => ({
type: 'flex',
startTime,
}))
.with('redirect', () => {
const opt = programOptions.find((opt) => opt.type === 'redirect');
return {
startTime,
type: 'redirect',
channelId: opt?.channelId ?? '',
order: 'next',
direction: 'asc',
title: `Redirect to Channel ${opt?.channelName ?? ''}`,
};
})
.with('show', () => {
const opt = programOptions.find((opt) => opt.type === 'show');
return {
startTime,
type: 'show' as const,
showId: opt?.showId ?? '',
order: 'next',
direction: 'asc',
title: opt?.description ?? '',
show: null,
seasonFilter: [],
};
})
.with('smart-collection', () => {
const opt = programOptions.find(
(opt) => opt.type === 'smart-collection',
);
return {
startTime,
type: 'smart-collection' as const,
order: 'next',
direction: 'asc',
smartCollectionId: opt?.collectionId ?? '',
smartCollection: null,
isMissing: false,
};
})
.exhaustive();
},
[getValues, programOptions],
);
const commit = () => {
slotArray.update(index, getValues());
onClose();
};
return (
<>
<DialogContent>
<Box
sx={{
// pt: 2,
display: 'flex',
flexDirection: 'column',
gap: '1rem',
}}
>
<Tabs
value={tab}
onChange={(_, tab: number) => setTab(tab)}
sx={{ borderBottom: 1, borderColor: 'divider' }}
>
<Tab label="Programming" />
<Tab
label="Filler"
disabled={slotType === 'flex' || fillerLists.length === 0}
/>
<Tab label="Config" />
</Tabs>
<TabPanel value={tab} index={0}>
<Stack gap={2} useFlexGap>
<Stack direction="row" gap={1}>
{currentPeriod === 'week' && (
<FormControl fullWidth>
<InputLabel>Day</InputLabel>
<Controller
control={control}
name={`startTime`}
render={({ field }) => (
<Select
{...field}
fullWidth
value={Math.floor(field.value / OneDayMillis)}
label="Day"
onChange={(e) =>
updateSlotDay(
e.target.value as number,
field.onChange,
)
}
>
{map(DaysOfWeekMenuItems, ({ value, name }) => (
<MenuItem key={value} value={value}>
{name}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
)}
<Controller
control={control}
name={`startTime`}
render={({ field, fieldState: { error } }) => {
return (
<TimePicker
reduceAnimations
{...field}
value={dayjs().startOf(currentPeriod).add(field.value)}
onChange={(value) =>
updateSlotTime(value, field.onChange)
}
label="Start Time"
closeOnSelect={false}
slotProps={{
textField: {
error: !isNil(error),
},
}}
/>
);
}}
/>
</Stack>
<FormProvider {...formMethods}>
<EditSlotProgrammingForm newSlotForType={newSlotForType} />
</FormProvider>
</Stack>
</TabPanel>
<FormProvider {...formMethods}>
<TabPanel value={tab} index={1}>
<SlotFillerDialogPanel />
</TabPanel>
<TabPanel value={tab} index={2}>
<TimeSlotConfigDialogPanel />
</TabPanel>
</FormProvider>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => onClose()}>Cancel</Button>
<Button
disabled={!isValid || isSubmitting}
onClick={() => commit()}
variant="contained"
>
Save
</Button>
</DialogActions>
</>
);
};