almeira-web/src/components/ProgrammingMatrixEditor.tsx

221 lines
12 KiB
TypeScript

'use client';
import { useState, useRef, useEffect, useCallback } from 'react';
import { Save, MousePointerClick, CalendarDays } from 'lucide-react';
const days = [
{ key: 'mon', label: 'Lunes' },
{ key: 'tue', label: 'Martes' },
{ key: 'wed', label: 'Miércoles' },
{ key: 'thu', label: 'Jueves' },
{ key: 'fri', label: 'Viernes' },
{ key: 'sat', label: 'Sábado' },
{ key: 'sun', label: 'Domingo' }
];
const slots = Array.from({length: 48}).map((_, i) => {
const h = Math.floor(i/2).toString().padStart(2, '0');
const m = i % 2 === 0 ? '00' : '30';
return `${h}:${m}`;
});
type Selection = {
day: string;
startIdx: number;
endIdx: number;
} | null;
export default function ProgrammingMatrixEditor({ initialData, tab, onSaveAction }: { initialData: Record<string, string>, tab: string, onSaveAction: (data: Record<string, string>) => void }) {
const [matrix, setMatrix] = useState<Record<string, string>>(initialData);
const [isDragging, setIsDragging] = useState(false);
const [selection, setSelection] = useState<Selection>(null);
const [dragStart, setDragStart] = useState<{day: string, idx: number} | null>(null);
const [bulkInput, setBulkInput] = useState('');
const formRef = useRef<HTMLFormElement>(null);
// Calc rowSpans to cleanly render unified cells visually
const getSpans = useCallback(() => {
const spans: Record<string, number[]> = {};
days.forEach(d => {
const list = slots.map(time => matrix[`${time}_${d.key}`] || '');
const spanList = new Array(48).fill(1);
for(let i=0; i<48; i++){
if (spanList[i] === 0) continue;
let count = 1;
const val = list[i];
while(i + count < 48 && list[i + count] === val && val !== '') {
spanList[i + count] = 0;
count++;
}
spanList[i] = count;
}
spans[d.key] = spanList;
});
return spans;
}, [matrix]);
const spans = getSpans();
const handleMouseDown = (day: string, idx: number) => {
setDragStart({ day, idx });
setIsDragging(true);
setSelection({ day, startIdx: idx, endIdx: idx });
setBulkInput(matrix[`${slots[idx]}_${day}`] || '');
};
const handleMouseEnter = (day: string, idx: number) => {
if (isDragging && dragStart && dragStart.day === day) {
setSelection({
day,
startIdx: Math.min(dragStart.idx, idx),
endIdx: Math.max(dragStart.idx, idx)
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
useEffect(() => {
window.addEventListener('mouseup', handleMouseUp);
return () => window.removeEventListener('mouseup', handleMouseUp);
}, []);
const handleApplyBulk = () => {
if (!selection) return;
const newMatrix = { ...matrix };
for (let i = selection.startIdx; i <= selection.endIdx; i++) {
newMatrix[`${slots[i]}_${selection.day}`] = bulkInput;
}
setMatrix(newMatrix);
setSelection(null); // Close modal
};
const handleClearBulk = () => {
if (!selection) return;
const newMatrix = { ...matrix };
for (let i = selection.startIdx; i <= selection.endIdx; i++) {
newMatrix[`${slots[i]}_${selection.day}`] = '';
}
setMatrix(newMatrix);
setSelection(null);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSaveAction(matrix);
};
return (
<form onSubmit={handleSubmit} className="bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl flex flex-col h-[75vh] relative select-none">
{/* Context Tooltip / Modal para la Selección */}
{selection && !isDragging && (
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-50 bg-slate-950/90 backdrop-blur-xl border border-cyan-500/30 p-4 rounded-2xl shadow-[0_10px_40px_rgba(0,0,0,0.5)] flex flex-col gap-3 min-w-[300px]">
<h4 className="text-cyan-400 font-bold uppercase tracking-widest text-xs">
Editando {slots[selection.startIdx]} a {slots[selection.endIdx]} ({days.find(d=>d.key===selection.day)?.label})
</h4>
<input
autoFocus
type="text"
value={bulkInput}
onChange={e => setBulkInput(e.target.value)}
onKeyDown={e => { if(e.key === 'Enter') { e.preventDefault(); handleApplyBulk(); } }}
placeholder="Nombre del Programa..."
className="bg-slate-900 border border-slate-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-cyan-400"
/>
<div className="flex gap-2">
<button type="button" onClick={handleApplyBulk} className="flex-1 bg-cyan-600 hover:bg-cyan-500 text-white font-bold rounded-lg py-2 transition-colors">Unificar Franjas</button>
<button type="button" onClick={handleClearBulk} className="px-4 bg-slate-800 hover:bg-slate-700 text-pink-400 font-bold rounded-lg py-2 transition-colors">Borrar</button>
</div>
</div>
)}
<div className="p-4 bg-slate-950 border-b border-slate-800 flex items-center justify-between shrink-0">
<div className="flex items-center gap-2 text-slate-400 text-sm font-medium">
<MousePointerClick size={16} /> <b>Consejo:</b> Haz clic y arrastra hacia abajo para unificar intervalos de 30 mins.
</div>
</div>
<div className="flex-1 overflow-auto custom-scrollbar bg-slate-900/50">
<table className="w-full text-left border-collapse min-w-[1000px]">
<thead className="sticky top-0 z-30 ring-1 ring-slate-800 outline outline-1 outline-slate-900">
<tr>
<th className="p-3 w-20 text-center font-black tracking-widest text-slate-500 uppercase text-[10px] border-r border-b border-slate-800 bg-slate-950 relative z-40">Hora</th>
{days.map(d => (
<th key={d.key} className="p-3 text-center font-bold tracking-widest text-white uppercase text-xs border-r border-b border-slate-800 bg-slate-900 sticky top-0 bg-opacity-95 backdrop-blur-sm z-30">
{d.label}
</th>
))}
</tr>
</thead>
<tbody onMouseLeave={handleMouseUp}>
{slots.map((time, idx) => {
const isHalfHour = time.endsWith('30');
return (
<tr key={idx}>
<td className={`p-2 border-r border-slate-800 bg-slate-950 text-center font-mono font-black tracking-widest sticky left-0 z-20 transition-colors pointer-events-none ${isHalfHour ? 'text-slate-600 text-[10px] border-b border-dashed border-slate-800/50' : 'text-slate-400 text-xs border-b border-solid'}`}>
{time}
</td>
{days.map(d => {
const isSelected = selection && selection.day === d.key && idx >= selection.startIdx && idx <= selection.endIdx;
// Visual Unification logic during non-edit
const span = spans[d.key][idx];
const isMergedChild = span === 0;
const cellVal = matrix[`${time}_${d.key}`];
const hasProgram = !!cellVal;
// Only apply unification hiding if NOT actively selecting this day
const hideForMerge = isMergedChild && (!selection || selection.day !== d.key);
if (hideForMerge) return null;
// Force span to 1 when dragging this column to allow seeing individual cells
const effectiveSpan = (selection && selection.day === d.key) ? 1 : span;
return (
<td
key={d.key}
rowSpan={effectiveSpan}
onMouseDown={() => handleMouseDown(d.key, idx)}
onMouseEnter={() => handleMouseEnter(d.key, idx)}
className={`
border-r border-slate-800/50 relative cursor-crosshair transition-colors duration-75
${isHalfHour && effectiveSpan === 1 ? 'border-b border-dashed border-slate-800/30' : 'border-b border-solid'}
${isSelected ? 'bg-cyan-900/60 shadow-[inset_0_0_10px_rgba(6,182,212,0.5)]' : 'hover:bg-slate-800/60'}
${hasProgram && !isSelected ? 'bg-slate-800/40 text-cyan-50' : ''}
`}
style={{ height: effectiveSpan > 1 ? `${effectiveSpan * 32}px` : '32px' }}
>
<div className={`w-full h-full flex items-center justify-center p-1 text-[11px] font-bold text-center leading-tight overflow-hidden ${hasProgram ? 'text-white' : 'text-slate-700'}`}>
{cellVal || ''}
</div>
{effectiveSpan > 1 && (
<div className="absolute top-1 right-1 bg-cyan-500/20 text-cyan-300 text-[8px] px-1 rounded uppercase tracking-widest pointer-events-none">
{effectiveSpan / 2}H
</div>
)}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
</div>
<div className="p-6 border-t border-slate-800 bg-slate-950/50 shrink-0">
<button type="submit" className={`w-full relative flex items-center justify-center gap-2 group overflow-hidden font-black uppercase tracking-widest py-4 px-12 rounded-xl transition-all border ${tab === 'tv' ? 'border-cyan-500/50 hover:border-cyan-400 bg-cyan-950 text-cyan-50' : 'border-rose-500/50 hover:border-rose-400 bg-rose-950 text-rose-50'}`}>
<div className={`absolute inset-0 bg-gradient-to-r ${tab === 'tv' ? 'from-cyan-600 to-blue-600' : 'from-rose-600 to-orange-500'} opacity-80 group-hover:opacity-100 transition-opacity`}></div>
<Save size={20} className="relative z-10" />
<span className="relative z-10">Guardar Parrilla / Sincronizar</span>
</button>
</div>
</form>
);
}