import { useEffect, useRef, useState } from "react";
import {
DndContext,
closestCenter,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { BiCopy, BiDotsVertical, BiX } from "react-icons/bi";
import {
DengiButton,
DengiYesOrNoDialog,
LoadingSpinner,
} from "./trash-components";
import { motion, AnimatePresence } from "motion/react";
import {
BiCheck,
BiPlus,
BiRedo,
BiTime,
BiTrash,
BiUndo,
} from "react-icons/bi";
// import { ifstyle } from "../App";
import {
apiFormatDate,
convertApiToEditor,
convertEditorToApi,
formatDate,
} from "./dengi";
import {
EditorPrefillRequest,
EditorPublishRequest,
EditorRemoveRequest,
} from "./editor_req_mgr";
import { API_BASE_URL } from "../App";
// Configuration
const COLUMNS = [
[
{
key: "position",
label: "номер",
width: "w-9",
placeholder: "№",
useinput: true,
},
{
key: "subject",
label: "предмет",
width: "w-40",
placeholder: "предмет",
useinput: false,
},
{
key: "room",
label: "кабинет",
width: "w-14",
placeholder: "123",
useinput: false,
},
{
key: "time1",
label: "начало",
width: "w-16",
placeholder: "00:00",
useinput: true,
},
{
key: "time2",
label: "конец",
width: "w-16",
placeholder: "00:00",
useinput: true,
},
],
[
{
key: "position",
label: "номер",
width: "w-9",
placeholder: "№",
useinput: true,
},
{
key: "subject",
label: "предмет",
width: "w-40",
placeholder: "предмет",
useinput: false,
},
{
key: "room",
label: "кабинет",
width: "w-14",
placeholder: "123",
useinput: false,
},
],
[
{
key: "position",
label: "номер",
width: "w-9",
placeholder: "№",
useinput: true,
},
{
key: "time1",
label: "начало",
width: "w-16",
placeholder: "00:00",
useinput: true,
},
{
key: "time2",
label: "конец",
width: "w-16",
placeholder: "00:00",
useinput: true,
},
],
];
const INITIAL_DATA = [
// {
// id: "1",
// position: "1",
// subject: "Mathematics",
// time: "09:00",
// room: "A-1",
// },
// { id: "2", position: "2", subject: "Physics", time: "10:00", room: "B-2" },
// { id: "3", position: "3", subject: "Chemistry", time: "11:00", room: "A-3" },
// { id: "4", position: "4", subject: "Biology", time: "13:00", room: "C-1" },
];
function TableInput(props) {
return props.useinput ? : ;
}
function SortableRow({
item,
columns,
onUpdate,
onDelete,
onDuplicate,
setFocusEdit,
setHoverEdit,
pickmeMode,
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
{columns.map((col) => (
onUpdate(item.id, col.key, e.target.value)}
className={`px-1 py-3 text-center bg-transparent text-white border-r ${
pickmeMode ? "border-pink-400/50" : "border-zinc-700"
} last:border-r-0 last:rounded-tr-lg focus:outline-none focus:bg-gray-700/50 placeholder-gray-500 transition-colors hover:bg-gray-700/20 first:rounded-tl-lg max-h-min resize-none block min-w-0 grow ${
col.width
}`}
placeholder={col.placeholder}
onFocus={() => setFocusEdit(col.key)}
onBlur={() => setFocusEdit(null)}
onMouseOver={() => setHoverEdit(col.key)}
onMouseLeave={() => setHoverEdit(null)}
/>
))}
);
}
async function getPrefilledRasp(tab, date, adminId) {
let a = await EditorPrefillRequest(
API_BASE_URL,
tab,
adminId,
apiFormatDate(date)
);
if (a[0] == 1 && a[1]) {
return convertApiToEditor(a[1]);
}
}
export function TableEditor({
initialData = INITIAL_DATA,
onChange = () => {},
selectedTab,
columns = COLUMNS[selectedTab],
setChangesWereMade,
adminId,
date,
dengiCash,
pickmeMode = false,
}) {
const [items, setItems] = useState(initialData);
const [undoItems, setUndoItems] = useState([]);
const [redoItems, setRedoItems] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
let rejected = false;
//console.log("cleanup");
// cleanup on change
setUndoItems([]);
setRedoItems([]);
setLoading(true);
getPrefilledRasp(selectedTab, date, adminId).then((i) => {
if (rejected) return;
console.log(i || []);
setItems(i || []);
setLoading(false);
});
return () => {
rejected = true;
};
}, [
selectedTab,
formatDate(date) /* NOTE: we need to format to rely only on DD-MM-YY,
and forget about time at all*/,
]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 200,
tolerance: 8,
},
})
);
const updateItems = (newItems) => {
setItems(newItems);
onChange?.(newItems);
};
useEffect(() => setChangesWereMade(undoItems.length > 0), [undoItems]);
const handleDragEnd = (event) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
const newItems = arrayMove(items, oldIndex, newIndex);
// Swap positions: assign old positions to new order
const reorderedItems = newItems.map((item, idx) => ({
...item,
position: items[idx].position, // Use position from original array at this index
}));
setUndoItems([items, ...undoItems]);
updateItems(reorderedItems);
}
};
const updateTimeoutRef = useRef({});
const previousValueRef = useRef({});
const handleUpdate = (id, field, value) => {
const key = `${id}-${field}`;
if (!updateTimeoutRef.current[key]) {
const currentItem = items.find((item) => item.id === id);
previousValueRef.current[key] = currentItem[field];
}
if (updateTimeoutRef.current[key]) {
clearTimeout(updateTimeoutRef.current[key]);
}
const newItems = items.map((item) =>
item.id === id ? { ...item, [field]: value } : item
);
updateItems(newItems);
updateTimeoutRef.current[key] = setTimeout(() => {
// Create items array with the ORIGINAL value from before editing started
const itemsWithOriginalValue = items.map((item) =>
item.id === id
? { ...item, [field]: previousValueRef.current[key] }
: item
);
setUndoItems([itemsWithOriginalValue, ...undoItems]);
// Clean up
delete updateTimeoutRef.current[key];
delete previousValueRef.current[key];
}, 1000);
};
const handleUndo = () => {
if (isLoading) return;
setRedoItems([items, ...redoItems]);
updateItems(undoItems[0]);
setUndoItems(undoItems.slice(1));
};
const handleRedo = () => {
if (isLoading) return;
setUndoItems([items, ...undoItems]);
updateItems(redoItems[0]);
setRedoItems(redoItems.slice(1));
};
const handleDelete = (id) => {
if (isLoading) return;
const newItems = items.filter((item) => item.id !== id);
setUndoItems([items, ...undoItems]);
updateItems(newItems);
};
const handleDuplicate = (id) => {
const index = items.findIndex((item) => item.id === id);
if (index === -1) return;
const itemToDuplicate = items[index];
// Create new item with unique id
const newItem = {
...itemToDuplicate,
id: Date.now(), // or use uuid() or any unique id generator
};
// Insert the new item right after the duplicated item
const newItems = [
...items.slice(0, index + 1),
newItem,
...items.slice(index + 1),
];
// Reassign positions based on the original order
// The duplicate takes the next position, and everything shifts
const reorderedItems = newItems.map((item, idx) => ({
...item,
position: String(idx + 1), // or just (idx + 1) if position is a number
}));
setUndoItems([items, ...undoItems]);
updateItems(reorderedItems);
};
const handleAddRow = () => {
if (isLoading) return;
const newId = String(Date.now());
const sortedItems = [...items];
sortedItems.sort((a, b) => a.position - b.position);
const lastPosition =
items.length > 0
? String(
parseInt(sortedItems[sortedItems.length - 1].position || "0") + 1
)
: "1";
const newItem = { id: newId, position: lastPosition };
columns.forEach((col) => {
if (col.key !== "position") {
newItem[col.key] = "";
}
});
setUndoItems([items, ...undoItems]);
updateItems([...items, newItem]);
setTimeout(
() =>
window.scrollTo({
top: document.documentElement.scrollHeight - window.innerHeight,
left: window.scrollX, // preserve current X
behavior: "smooth",
}),
100
);
};
const [focusEdit, setFocusEdit] = useState(null);
const [hoverEdit, setHoverEdit] = useState(null);
const [deleteDiag, setDeleteDiag] = useState(false);
const handleClear = () => {
if (isLoading) return;
setUndoItems([items, ...undoItems]);
updateItems([]);
};
const handleChangesDefault = () => {
if (selectedTab != 0 || isLoading) return;
setLoading(true);
getPrefilledRasp(1, date, adminId).then((i) => {
setUndoItems([items, ...undoItems]);
setItems(i);
setLoading(false);
});
};
const handlePublish = () => {
if (isLoading) return;
setUndoItems([]);
setRedoItems([]);
console.log(convertEditorToApi(items));
dengiCash.fullClear(); // keep it clean
setLoading(true);
EditorPublishRequest(
API_BASE_URL,
selectedTab,
adminId,
apiFormatDate(date),
JSON.stringify(convertEditorToApi(items))
).then(() => {
setLoading(false);
});
};
const handleDeleteAll = () => {
// if (isLoading) return; // WARN!
setDeleteDiag(true);
};
let deleteDialogBusy = false; // NOTE: we don't need a state
return (
<>
,
noIcon: ,
bgDissmis: true,
yesBtnStyle: "bg-red-500! text-white! md:hover:bg-red-600!",
}}
onAnswer={(answ) => {
if (deleteDialogBusy) return;
if (!answ) setDeleteDiag(false);
else {
dengiCash.fullClear();
// scary
deleteDialogBusy = true;
EditorRemoveRequest(
API_BASE_URL,
selectedTab,
adminId,
apiFormatDate(date)
).then(() => {
deleteDialogBusy = false; // for 100% sure
setDeleteDiag(false);
location.hash = "/app";
});
}
}}
show={deleteDiag}
/>
{selectedTab == 0 && (
убрать зам.
)}
очистить
{!isLoading && (
{columns.map((col, i) => (
<>
setHoverEdit(col.key)}
// onMouseLeave={() => setHoverEdit(null)}
>
{col.label}
>
))}
item.id)}
strategy={verticalListSortingStrategy}
>
0 ? "sync" : "wait"}>
{items.map((item) => (
))}
{items.length === 0 && (
пусто
)}
{/*
*/}
)}
{isLoading && (
)}
удалить всё
сохранить
>
);
}