import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import api from "../api/client"; import { useAuth } from "../auth/AuthContext"; import SearchBar from "../components/SearchBar"; import ConfirmDialog from "../components/ConfirmDialog"; import { getLocalizedValue, getLanguageName, normalizeColor, formatDuration, } from "./melodyUtils"; const MELODY_TYPES = ["", "orthodox", "catholic", "all"]; const MELODY_TONES = ["", "normal", "festive", "cheerful", "lamentation"]; // All available columns with their defaults const ALL_COLUMNS = [ { key: "status", label: "Status", defaultOn: true }, { key: "color", label: "Color", defaultOn: true }, { key: "name", label: "Name", defaultOn: true, alwaysOn: true }, { key: "description", label: "Description", defaultOn: false }, { key: "type", label: "Type", defaultOn: true }, { key: "tone", label: "Tone", defaultOn: true }, { key: "totalNotes", label: "Total Notes", defaultOn: true }, { key: "minSpeed", label: "Min Speed", defaultOn: false }, { key: "maxSpeed", label: "Max Speed", defaultOn: false }, { key: "tags", label: "Tags", defaultOn: false }, { key: "speed", label: "Speed", defaultOn: false }, { key: "duration", label: "Duration", defaultOn: false }, { key: "totalRunDuration", label: "Total Run", defaultOn: false }, { key: "pauseDuration", label: "Pause", defaultOn: false }, { key: "infiniteLoop", label: "Infinite", defaultOn: false }, { key: "noteAssignments", label: "Note Assignments", defaultOn: false }, { key: "isTrueRing", label: "True Ring", defaultOn: true }, { key: "docId", label: "Document ID", defaultOn: false }, { key: "pid", label: "PID", defaultOn: false }, ]; function getDefaultVisibleColumns() { const saved = localStorage.getItem("melodyListColumns"); if (saved) { try { return JSON.parse(saved); } catch { // ignore } } return ALL_COLUMNS.filter((c) => c.defaultOn).map((c) => c.key); } export default function MelodyList() { const [melodies, setMelodies] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [search, setSearch] = useState(""); const [typeFilter, setTypeFilter] = useState(""); const [toneFilter, setToneFilter] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [displayLang, setDisplayLang] = useState("en"); const [melodySettings, setMelodySettings] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const [unpublishTarget, setUnpublishTarget] = useState(null); const [actionLoading, setActionLoading] = useState(null); const [visibleColumns, setVisibleColumns] = useState(getDefaultVisibleColumns); const [showColumnPicker, setShowColumnPicker] = useState(false); const columnPickerRef = useRef(null); const navigate = useNavigate(); const { hasPermission } = useAuth(); const canEdit = hasPermission("melodies", "edit"); useEffect(() => { api.get("/settings/melody").then((ms) => { setMelodySettings(ms); setDisplayLang(ms.primary_language || "en"); }); }, []); // Close column picker on outside click useEffect(() => { const handleClick = (e) => { if (columnPickerRef.current && !columnPickerRef.current.contains(e.target)) { setShowColumnPicker(false); } }; if (showColumnPicker) { document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); } }, [showColumnPicker]); const fetchMelodies = async () => { setLoading(true); setError(""); try { const params = new URLSearchParams(); if (search) params.set("search", search); if (typeFilter) params.set("type", typeFilter); if (toneFilter) params.set("tone", toneFilter); if (statusFilter) params.set("status", statusFilter); const qs = params.toString(); const data = await api.get(`/melodies${qs ? `?${qs}` : ""}`); setMelodies(data.melodies); setTotal(data.total); } catch (err) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { fetchMelodies(); }, [search, typeFilter, toneFilter, statusFilter]); const handleDelete = async () => { if (!deleteTarget) return; try { await api.delete(`/melodies/${deleteTarget.id}`); setDeleteTarget(null); fetchMelodies(); } catch (err) { setError(err.message); setDeleteTarget(null); } }; const handlePublish = async (row) => { setActionLoading(row.id); try { await api.post(`/melodies/${row.id}/publish`); fetchMelodies(); } catch (err) { setError(err.message); } finally { setActionLoading(null); } }; const handleUnpublish = async () => { if (!unpublishTarget) return; setActionLoading(unpublishTarget.id); try { await api.post(`/melodies/${unpublishTarget.id}/unpublish`); setUnpublishTarget(null); fetchMelodies(); } catch (err) { setError(err.message); setUnpublishTarget(null); } finally { setActionLoading(null); } }; const toggleColumn = (key) => { const col = ALL_COLUMNS.find((c) => c.key === key); if (col?.alwaysOn) return; setVisibleColumns((prev) => { const next = prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]; localStorage.setItem("melodyListColumns", JSON.stringify(next)); return next; }); }; const isVisible = (key) => visibleColumns.includes(key); const getDisplayName = (nameVal) => getLocalizedValue(nameVal, displayLang, "Untitled"); const renderCellValue = (key, row) => { const info = row.information || {}; const ds = row.default_settings || {}; switch (key) { case "status": return ( {row.status === "published" ? "Live" : "Draft"} ); case "color": return info.color ? ( ) : ( ); case "name": return (
{getDisplayName(info.name)} {isVisible("description") && (

{getLocalizedValue(info.description, displayLang) || "-"}

)}
); case "type": return {row.type}; case "tone": return {info.melodyTone || "-"}; case "totalNotes": return info.totalNotes ?? "-"; case "minSpeed": return info.minSpeed ?? "-"; case "maxSpeed": return info.maxSpeed ?? "-"; case "tags": return info.customTags?.length > 0 ? (
{info.customTags.map((tag) => ( {tag} ))}
) : ( "-" ); case "speed": return ds.speed != null ? `${ds.speed}%` : "-"; case "duration": return ds.duration != null ? formatDuration(ds.duration) : "-"; case "totalRunDuration": return ds.totalRunDuration ?? "-"; case "pauseDuration": return ds.pauseDuration ?? "-"; case "infiniteLoop": return ( {ds.infiniteLoop ? "Yes" : "No"} ); case "noteAssignments": return ds.noteAssignments?.length > 0 ? ds.noteAssignments.join(", ") : "-"; case "isTrueRing": return ( {info.isTrueRing ? "Yes" : "No"} ); case "docId": return ( {row.id} ); case "pid": return row.pid || "-"; default: return "-"; } }; // Build visible column list (description is rendered inside name, not as its own column) const activeColumns = ALL_COLUMNS.filter( (c) => c.key !== "description" && isVisible(c.key) ); const languages = melodySettings?.available_languages || ["en"]; const selectClass = "px-3 py-2 rounded-md text-sm cursor-pointer border"; return (

Melodies

{canEdit && ( )}
{languages.length > 1 && ( )} {/* Column visibility dropdown */}
{showColumnPicker && (
{ALL_COLUMNS.map((col) => ( ))}
)}
{total} {total === 1 ? "melody" : "melodies"}
{error && (
{error}
)} {loading ? (
Loading...
) : melodies.length === 0 ? (
No melodies found.
) : (
{activeColumns.map((col) => ( ))} {canEdit && ( {melodies.map((row) => ( navigate(`/melodies/${row.id}`)} className="cursor-pointer transition-colors" style={{ borderBottom: "1px solid var(--border-secondary)" }} onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "var(--bg-card-hover)")} onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")} > {activeColumns.map((col) => ( ))} {canEdit && ( )} ))}
{col.key === "color" ? "" : col.label} )}
{renderCellValue(col.key, row)}
e.stopPropagation()} > {row.status === "draft" ? ( ) : ( )}
)} setDeleteTarget(null)} /> setUnpublishTarget(null)} />
); }