feat(frontend): schedule history dialog with rollback, wire ConfigHistorySheet
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
History,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useActiveSchedule } from "@/hooks/use-channels";
|
import { useActiveSchedule } from "@/hooks/use-channels";
|
||||||
@@ -29,6 +30,7 @@ interface ChannelCardProps {
|
|||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
onMoveUp: () => void;
|
onMoveUp: () => void;
|
||||||
onMoveDown: () => void;
|
onMoveDown: () => void;
|
||||||
|
onScheduleHistory: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useScheduleStatus(channelId: string) {
|
function useScheduleStatus(channelId: string) {
|
||||||
@@ -69,6 +71,7 @@ export function ChannelCard({
|
|||||||
onExport,
|
onExport,
|
||||||
onMoveUp,
|
onMoveUp,
|
||||||
onMoveDown,
|
onMoveDown,
|
||||||
|
onScheduleHistory,
|
||||||
}: ChannelCardProps) {
|
}: ChannelCardProps) {
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
const blockCount = Object.values(channel.schedule_config.day_blocks).reduce(
|
const blockCount = Object.values(channel.schedule_config.day_blocks).reduce(
|
||||||
@@ -185,6 +188,15 @@ export function ChannelCard({
|
|||||||
>
|
>
|
||||||
<CalendarDays className="size-3.5" />
|
<CalendarDays className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onScheduleHistory}
|
||||||
|
title="Schedule history"
|
||||||
|
className="text-zinc-600 hover:text-zinc-200"
|
||||||
|
>
|
||||||
|
<History className="size-3.5" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
asChild
|
asChild
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { RecyclePolicyEditor } from "./recycle-policy-editor";
|
|||||||
import { WebhookEditor } from "./webhook-editor";
|
import { WebhookEditor } from "./webhook-editor";
|
||||||
import { AccessSettingsEditor } from "./access-settings-editor";
|
import { AccessSettingsEditor } from "./access-settings-editor";
|
||||||
import { LogoEditor } from "./logo-editor";
|
import { LogoEditor } from "./logo-editor";
|
||||||
|
import { ConfigHistorySheet } from "./config-history-sheet";
|
||||||
import { useChannelForm } from "@/hooks/use-channel-form";
|
import { useChannelForm } from "@/hooks/use-channel-form";
|
||||||
import { channelFormSchema, extractErrors } from "@/lib/schemas";
|
import { channelFormSchema, extractErrors } from "@/lib/schemas";
|
||||||
import type { FieldErrors } from "@/lib/schemas";
|
import type { FieldErrors } from "@/lib/schemas";
|
||||||
@@ -748,7 +749,13 @@ export function EditChannelSheet({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* TODO: ConfigHistorySheet — wired in Task 16 */}
|
{channel && (
|
||||||
|
<ConfigHistorySheet
|
||||||
|
channelId={channel.id}
|
||||||
|
open={configHistoryOpen}
|
||||||
|
onOpenChange={setConfigHistoryOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { useScheduleHistory, useRollbackSchedule } from '@/hooks/use-channels'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
channelId: string
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmtDateRange = (from: string, until: string) =>
|
||||||
|
`${new Date(from).toLocaleDateString()} – ${new Date(until).toLocaleDateString()}`
|
||||||
|
|
||||||
|
export function ScheduleHistoryDialog({ channelId, open, onOpenChange }: Props) {
|
||||||
|
const { data: entries } = useScheduleHistory(channelId)
|
||||||
|
const rollback = useRollbackSchedule()
|
||||||
|
const [confirmId, setConfirmId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Schedule history</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-2 mt-2 max-h-[60vh] overflow-y-auto">
|
||||||
|
{(entries ?? []).map((entry, i) => (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
className="flex items-center gap-3 p-3 rounded border border-border"
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
Gen #{entry.generation}
|
||||||
|
{i === 0 && (
|
||||||
|
<span className="ml-2 text-xs text-green-400 bg-green-950 px-1.5 py-0.5 rounded">
|
||||||
|
active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{fmtDateRange(entry.valid_from, entry.valid_until)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{i > 0 && (
|
||||||
|
confirmId === entry.id ? (
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
<span className="text-amber-400 whitespace-nowrap">Roll back to gen #{entry.generation}?</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={rollback.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
rollback.mutate({ channelId, genId: entry.id })
|
||||||
|
setConfirmId(null)
|
||||||
|
onOpenChange(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" onClick={() => setConfirmId(null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setConfirmId(entry.id)}
|
||||||
|
>
|
||||||
|
Rollback to here
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(entries ?? []).length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-8">
|
||||||
|
No schedule history yet. Generate a schedule to get started.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from "./components/import-channel-dialog";
|
} from "./components/import-channel-dialog";
|
||||||
import { IptvExportDialog } from "./components/iptv-export-dialog";
|
import { IptvExportDialog } from "./components/iptv-export-dialog";
|
||||||
import { TranscodeSettingsDialog } from "./components/transcode-settings-dialog";
|
import { TranscodeSettingsDialog } from "./components/transcode-settings-dialog";
|
||||||
|
import { ScheduleHistoryDialog } from "./components/schedule-history-dialog";
|
||||||
import type {
|
import type {
|
||||||
ChannelResponse,
|
ChannelResponse,
|
||||||
ProgrammingBlock,
|
ProgrammingBlock,
|
||||||
@@ -59,6 +60,7 @@ export default function DashboardPage() {
|
|||||||
const [editChannel, setEditChannel] = useState<ChannelResponse | null>(null);
|
const [editChannel, setEditChannel] = useState<ChannelResponse | null>(null);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<ChannelResponse | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<ChannelResponse | null>(null);
|
||||||
const [scheduleChannel, setScheduleChannel] = useState<ChannelResponse | null>(null);
|
const [scheduleChannel, setScheduleChannel] = useState<ChannelResponse | null>(null);
|
||||||
|
const [scheduleHistoryChannelId, setScheduleHistoryChannelId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleCreate = (data: {
|
const handleCreate = (data: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -186,6 +188,7 @@ export default function DashboardPage() {
|
|||||||
onExport={() => exportChannel(channel)}
|
onExport={() => exportChannel(channel)}
|
||||||
onMoveUp={() => handleMoveUp(channel.id)}
|
onMoveUp={() => handleMoveUp(channel.id)}
|
||||||
onMoveDown={() => handleMoveDown(channel.id)}
|
onMoveDown={() => handleMoveDown(channel.id)}
|
||||||
|
onScheduleHistory={() => setScheduleHistoryChannelId(channel.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -246,6 +249,14 @@ export default function DashboardPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{scheduleHistoryChannelId && (
|
||||||
|
<ScheduleHistoryDialog
|
||||||
|
channelId={scheduleHistoryChannelId}
|
||||||
|
open={!!scheduleHistoryChannelId}
|
||||||
|
onOpenChange={open => !open && setScheduleHistoryChannelId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{deleteTarget && (
|
{deleteTarget && (
|
||||||
<DeleteChannelDialog
|
<DeleteChannelDialog
|
||||||
channelName={deleteTarget.name}
|
channelName={deleteTarget.name}
|
||||||
|
|||||||
Reference in New Issue
Block a user