"use client"; import { useTranslations } from "next-intl"; import { useRef, useState } from "react"; import { getBulkOfferDestinations } from "@/actions/catalog-bulk"; import { createCatalogPackage, getCatalogPackage, listCatalogPackages, previewCatalogPackage, publishCatalogPackage, saveCatalogPackage, } from "@/actions/catalog-packages"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { type CatalogPackage, type PackageListRow, type PackagePreview, type PackagePublication, type PackageSnapshot, savePackageSchema, } from "@/features/catalog/domain/packages"; import { useUnsavedChanges } from "@/hooks/use-unsaved-changes"; import { CatalogPackagePreview } from "./catalog-package-preview"; import { PackageCategoryPicker } from "./package-category-picker"; import { packageChanges, packagePublicationIsCurrent, } from "./package-editor-helpers"; import { PackageSnapshotEditor } from "./package-snapshot-editor"; export function CatalogPackagesDialog({ canEdit }: { canEdit: boolean }) { const t = useTranslations("pages.admin.catalog.packages"); const [open, setOpen] = useState(false); const [busy, setBusy] = useState(false); const busyRef = useRef(false); const [error, setError] = useState(""); const [notice, setNotice] = useState(""); const [packages, setPackages] = useState([]); const [categories, setCategories] = useState< Array<{ id: number; caption: string }> >([]); const [listQuery, setListQuery] = useState(""); const [listPage, setListPage] = useState(0); const [saved, setSaved] = useState(null); const [draft, setDraft] = useState(null); const [name, setName] = useState(""); const [target, setTarget] = useState(-1); const [creating, setCreating] = useState(false); const [mode, setMode] = useState<"update" | "copy">("update"); const [pageIds, setPageIds] = useState([]); const [revision, setRevision] = useState(0); const [tab, setTab] = useState<"edit" | "preview" | "review">("edit"); const [previewBaseline, setPreviewBaseline] = useState(false); const [review, setReview] = useState(null); const [reviewPage, setReviewPage] = useState(0); const [publication, setPublication] = useState( null, ); const dirty = creating ? Boolean(name || pageIds.length || mode !== "update" || target !== -1) : Boolean( saved && draft && (saved.name !== name || saved.targetParentId !== target || JSON.stringify(saved.draft) !== JSON.stringify(draft)), ); useUnsavedChanges(open && dirty, t("discardChanges")); const editable = canEdit && saved?.status === "draft" && !publication; const matches = packages.filter((row) => `${row.name} ${row.id}`.toLowerCase().includes(listQuery.toLowerCase()), ); const changes = saved && draft ? packageChanges(saved.baseline, draft) : []; function invalidate() { setReview(null); setPublication(null); setNotice(""); setRevision((value) => value + 1); } function mayLeave() { return !busyRef.current && (!dirty || window.confirm(t("discardChanges"))); } async function run(operation: () => Promise) { if (busyRef.current) return; busyRef.current = true; setBusy(true); setError(""); try { await operation(); } catch (reason) { setError(reason instanceof Error ? reason.message : t("requestFailed")); } finally { busyRef.current = false; setBusy(false); } } function selectPackage(value: CatalogPackage) { setSaved(value); setDraft(value.draft); setName(value.name); setTarget(value.targetParentId); setCreating(false); setReview(null); setPublication(null); setTab("edit"); setRevision((value) => value + 1); setNotice(""); } async function refreshList() { const result = await listCatalogPackages(); if (!result.ok) throw new Error(result.error); setPackages(result.data); } async function start() { setOpen(true); await run(async () => { const results = await Promise.allSettled([ refreshList(), getBulkOfferDestinations(), ]); if (results[0].status === "rejected") throw results[0].reason; const destinations = results[1]; if (destinations.status === "rejected") throw destinations.reason; if (!destinations.value.ok) throw new Error(destinations.value.error); setCategories(destinations.value.data.pages); }); } function newPackage() { if (!mayLeave()) return; setSaved(null); setDraft(null); setName(""); setTarget(-1); setMode("update"); setPageIds([]); setCreating(true); setReview(null); setPublication(null); setNotice(""); setError(""); } async function openPackage(id: string) { if (!mayLeave()) return; await run(async () => { const result = await getCatalogPackage(id); if (!result.ok) throw new Error(result.error); selectPackage(result.data); }); } async function save() { if (!saved || !draft || !editable) return; const input = savePackageSchema.safeParse({ id: saved.id, version: saved.version, name, draft, targetParentId: target, }); if (!input.success) { setError(t("invalidFields")); return; } await run(async () => { const result = await saveCatalogPackage(input.data); if (!result.ok) throw new Error(result.error); selectPackage(result.data); setNotice(t("draftSaved")); await refreshList(); }); } async function requestReview() { if (!saved || dirty || !editable) return; setReview(null); await run(async () => { const result = await previewCatalogPackage(saved.id); if (!result.ok) throw new Error(result.error); if (result.data.version !== saved.version) { setReview(null); throw new Error(t("staleReview")); } setReview(result.data); setReviewPage(0); setTab("review"); }); } async function publish() { if ( !saved || !review || dirty || !packagePublicationIsCurrent(saved, review, dirty) || !editable || !window.confirm(t("confirmPublish")) ) return; await run(async () => { const result = await publishCatalogPackage({ id: saved.id, version: saved.version, fingerprint: review.fingerprint, }); if (!result.ok) { setReview(null); throw new Error(result.error); } setPublication(result.data); setReview(null); setNotice( t("publishedSuccess", { pages: result.data.pageIds.length, offers: result.data.offerCount, }), ); const current = await getCatalogPackage(saved.id).catch(() => null); if (current?.ok) { setSaved(current.data); setDraft(current.data.draft); } await refreshList(); }); } return ( <> { if (next || mayLeave()) { setOpen(next); if (!next) { setSaved(null); setDraft(null); setCreating(false); } } }} > {t("title")} {t("description")} {error && (

{error}

)} {notice && (

{notice}

)} {busy && (

{t("working")}

)}
{creating && (

{t("createPackage")}

{t(mode === "copy" ? "copyHint" : "updateHint")}

{t("sourceCategories")}

{t("packageLimits")}

{mode === "copy" && (

{t("targetParent")}

setTarget(ids[0])} root />
)}
)} {!creating && !saved && (

{t("choosePackage")}

)} {saved && draft && ( <>
{t(saved.status)} · v{saved.version} {t(saved.mode)} {dirty && {t("unsaved")}}
{saved.mode === "copy" && (
{t("targetParent")}:{" "} {categories.find((page) => page.id === target) ?.caption ?? (target === -1 ? t("rootCategory") : `#${target}`)} { setTarget(ids[0]); invalidate(); }} root />
)}
{tab === "edit" && ( { setDraft(value); invalidate(); }} /> )} {tab === "preview" && (
)} {tab === "review" && (

{t("changes")}

{t( saved.mode === "copy" ? "copyReview" : "updateReview", )}

{changes .slice(reviewPage * 20, (reviewPage + 1) * 20) .map((change) => ( ))}
{t("entry")} {t("field")} {t("before")} {t("after")}
{t(change.kind)} #{change.id} {change.label} {t(change.field)} {change.before} {change.after}
{!changes.length && (

{t("noChanges")}

)}
{review && (

{t("publicationImpact", { pages: review.changedPages, offers: review.changedOffers, })}

{review.conflicts.length ? ( <>

{t("conflicts")}

    {[...new Set(review.conflicts)] .slice(0, 50) .map((conflict) => (
  • {conflict}
  • ))}
) : (

{t("readyToPublish")}

)}
)}
)} {publication?.warnings?.map((warning) => (

{warning}

))} {editable && (
{review && !review.conflicts.length && ( )}

{t("saveHint")}

)} )}
); }