221 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|