293 lines
14 KiB
TypeScript
293 lines
14 KiB
TypeScript
import React, { useState, useRef } from "react";
|
||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||
import {
|
||
faBoxesStacked,
|
||
faCheckCircle,
|
||
faCheckToSlot,
|
||
faClipboardQuestion,
|
||
faShuffle,
|
||
} from "@fortawesome/free-solid-svg-icons";
|
||
import { ShipmentRequest } from "@/interfaces/interfaces";
|
||
|
||
interface ShipmentItemsAccordionProps {
|
||
selectedShipment: ShipmentRequest;
|
||
buttonStates: { [itemId: number]: any };
|
||
setButtonStates: React.Dispatch<React.SetStateAction<{ [itemId: number]: any }>>;
|
||
parentModalHandle: () => void;
|
||
}
|
||
|
||
export default function ShipmentItemsAccordion({
|
||
selectedShipment,
|
||
buttonStates,
|
||
setButtonStates,
|
||
parentModalHandle,
|
||
}: ShipmentItemsAccordionProps) {
|
||
// Track which accordion item (by id) is open. Default to first item.
|
||
const [openAccordion, setOpenAccordion] = useState<number | null>(
|
||
selectedShipment.items[0]?.id || null
|
||
);
|
||
// Track completed items by their id.
|
||
const [completedItems, setCompletedItems] = useState<{ [id: number]: boolean }>({});
|
||
// Ref to store a hold timer for touch devices.
|
||
const holdTimerRef = useRef<number | null>(null);
|
||
|
||
// Modal state
|
||
const [modalPositionOpen, setModalPositionOpen] = useState(false);
|
||
const [currentItem, setCurrentItem] = useState<any>(null);
|
||
const [fromPosition, setFromPosition] = useState<string>("");
|
||
const [toPosition, setToPosition] = useState<string>("");
|
||
// modalMode: "full" or "select-only"
|
||
const [modalMode, setModalMode] = useState<"full" | "select-only">("full");
|
||
// modalKey: "neniNaPozici" | "doskladnit" | "zmenitPozici"
|
||
const [modalKey, setModalKey] = useState<string | null>(null);
|
||
|
||
// Helper to get or initialize the button state for an item
|
||
const getItemState = (itemId: number) =>
|
||
buttonStates[itemId] || {
|
||
neniNaPozici: { value: false, details: [] },
|
||
doskladnit: { value: false, details: [] },
|
||
zmenitPozici: { value: false, details: [] },
|
||
};
|
||
|
||
// Generic update function for button state for a given item and key.
|
||
const updateButtonState = (itemId: number, key: string, details: any[] = []) => {
|
||
setButtonStates((prev) => {
|
||
const currentState = getItemState(itemId);
|
||
const currentButtonState = currentState[key] || { value: false, details: [] };
|
||
const toggledValue = !currentButtonState.value;
|
||
return {
|
||
...prev,
|
||
[itemId]: {
|
||
...currentState,
|
||
[key]: { value: toggledValue, details: toggledValue ? details : [] },
|
||
},
|
||
};
|
||
});
|
||
};
|
||
|
||
// Open the modal with appropriate mode and key
|
||
const handleOpenModal = (
|
||
item: any,
|
||
key: "neniNaPozici" | "doskladnit" | "zmenitPozici",
|
||
mode: "full" | "select-only"
|
||
) => {
|
||
setCurrentItem(item);
|
||
setModalKey(key);
|
||
setModalMode(mode);
|
||
setModalPositionOpen(true);
|
||
// Optionally notify parent modal
|
||
// parentModalHandle();
|
||
};
|
||
|
||
// Confirm handler for the modal
|
||
const handleConfirmModal = () => {
|
||
if (currentItem && modalKey) {
|
||
if (modalMode === "full") {
|
||
updateButtonState(currentItem.id, modalKey, [
|
||
{ from: fromPosition, to: toPosition },
|
||
]);
|
||
} else {
|
||
// select-only: only pass the "from" detail
|
||
updateButtonState(currentItem.id, modalKey, [{ position: fromPosition }]);
|
||
}
|
||
}
|
||
// Reset modal state
|
||
setModalPositionOpen(false);
|
||
setCurrentItem(null);
|
||
setFromPosition("");
|
||
setToPosition("");
|
||
setModalMode("full");
|
||
setModalKey(null);
|
||
// Optionally notify parent modal
|
||
// parentModalHandle();
|
||
};
|
||
|
||
// Toggle the open state of an accordion item.
|
||
const handleToggle = (id: number) => {
|
||
if (openAccordion === id) {
|
||
setOpenAccordion(null);
|
||
} else {
|
||
setOpenAccordion(id);
|
||
}
|
||
};
|
||
|
||
// Mark an item as completed.
|
||
const handleComplete = (id: number) => {
|
||
setCompletedItems((prev) => ({ ...prev, [id]: true }));
|
||
if (openAccordion === id) {
|
||
setOpenAccordion(null);
|
||
}
|
||
};
|
||
|
||
// Touch event handlers for long press.
|
||
const onTouchStartHandler = (id: number) => {
|
||
holdTimerRef.current = window.setTimeout(() => {
|
||
handleComplete(id);
|
||
}, 1000);
|
||
};
|
||
|
||
const onTouchEndHandler = () => {
|
||
if (holdTimerRef.current) {
|
||
clearTimeout(holdTimerRef.current);
|
||
holdTimerRef.current = null;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{selectedShipment.items.map((item) => {
|
||
const itemState = getItemState(item.id);
|
||
const isOpen = openAccordion === item.id;
|
||
const isCompleted = completedItems[item.id];
|
||
// If an item is not open, it appears "greyed out"
|
||
const headerOpacity = isOpen ? "opacity-100" : "opacity-50";
|
||
const buttonFunctionality = isOpen ? "" : "pointer-events-none";
|
||
|
||
return (
|
||
<div key={item.id} className="space-y-2">
|
||
<div
|
||
className={`collapse collapse-arrow border border-t-2 border-warning border-b-0 border-l-0 border-r-0 bg-base-100 ${headerOpacity}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={isOpen}
|
||
onChange={() => handleToggle(item.id)}
|
||
className="peer"
|
||
/>
|
||
<div
|
||
className="collapse-title flex justify-between items-center text-base-content px-4 py-2 cursor-pointer"
|
||
onDoubleClick={() => handleComplete(item.id)}
|
||
onTouchStart={() => onTouchStartHandler(item.id)}
|
||
onTouchEnd={onTouchEndHandler}
|
||
>
|
||
<div>
|
||
<p className="font-semibold text-base-content">
|
||
{item.quantity}× {item.name}
|
||
</p>
|
||
<p className="text-sm text-base-content">{item.item_note}</p>
|
||
<div className="flex justify-between text-sm text-base-content mt-1">
|
||
<span>{item.model_number}</span>
|
||
<span>
|
||
{(item.price / item.quantity).toFixed(2)} {selectedShipment.currency}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
{isCompleted && (
|
||
<FontAwesomeIcon icon={faCheckCircle} className="text-success" />
|
||
)}
|
||
</div>
|
||
<div className="collapse-content bg-base-200 text-base-content p-4">
|
||
<div className="flex flex-wrap gap-4 border border-base-300 rounded bg-base-100 p-3">
|
||
{Object.entries(item.stockData).flatMap(([stockName, stockArray]) =>
|
||
stockArray.map((stock, idx) => (
|
||
<div key={`${stockName}-${idx}`} className="text-sm">
|
||
<p className="font-semibold">{stockName}</p>
|
||
<p>Location: {stock.location}</p>
|
||
<p>Count: {stock.count} ks</p>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className={`flex flex-wrap items-center gap-2 text-base-content ${headerOpacity}`}>
|
||
<button
|
||
onClick={() =>
|
||
handleOpenModal(item, "neniNaPozici", "select-only")
|
||
}
|
||
className={`btn btn-outline btn-sm ${!isOpen ? "pointer-events-none" : ""} ${itemState.neniNaPozici.value ? "btn-success" : ""} w-full sm:w-auto`}
|
||
>
|
||
<FontAwesomeIcon icon={faClipboardQuestion} className="mr-2" />
|
||
Není na pozici
|
||
</button>
|
||
<button
|
||
onClick={() =>
|
||
handleOpenModal(item, "doskladnit", "select-only")
|
||
}
|
||
className={`btn btn-outline btn-sm ${!isOpen ? "pointer-events-none" : ""} ${itemState.doskladnit.value ? "btn-success" : ""} w-full sm:w-auto`}
|
||
>
|
||
<FontAwesomeIcon icon={faBoxesStacked} className="mr-2" />
|
||
Doplnit
|
||
</button>
|
||
<button
|
||
onClick={() =>
|
||
handleOpenModal(item, "zmenitPozici", "full")
|
||
}
|
||
className={`btn btn-outline btn-sm ${!isOpen ? "pointer-events-none" : ""} ${itemState.zmenitPozici.value ? "btn-success" : ""} w-full sm:w-auto`}
|
||
>
|
||
<FontAwesomeIcon icon={faShuffle} className="mr-2" />
|
||
Změnit pozici
|
||
</button>
|
||
<button
|
||
onClick={() => handleComplete(item.id)}
|
||
className={`btn btn-outline btn-sm ml-auto ${buttonFunctionality} w-full sm:w-auto`}
|
||
>
|
||
<FontAwesomeIcon icon={faCheckToSlot} className="text-lg" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
|
||
{/* Modal for changing position */}
|
||
<div className={`modal ${modalPositionOpen ? "modal-open" : ""}`}>
|
||
<div className="modal-box max-w-md">
|
||
<h3 className="font-bold text-lg">Change Position</h3>
|
||
<div className="space-y-4 mt-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-base-content mb-1">
|
||
From Position
|
||
</label>
|
||
<select
|
||
className="select select-bordered w-full"
|
||
value={fromPosition}
|
||
onChange={(e) => setFromPosition(e.target.value)}
|
||
>
|
||
<option value="">Select position</option>
|
||
{currentItem &&
|
||
Object.entries(currentItem.stockData).flatMap(
|
||
([stockName, stockArray]) =>
|
||
stockArray.map((stock, idx) => (
|
||
<option
|
||
key={`${stockName}-${idx}`}
|
||
value={`${stockName}--${stock.location}`}
|
||
>
|
||
<strong>{stockName}</strong> --- {stock.location}
|
||
</option>
|
||
))
|
||
)}
|
||
</select>
|
||
</div>
|
||
{modalMode === "full" && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-base-content mb-1">
|
||
To Position
|
||
</label>
|
||
<input
|
||
type="text"
|
||
className="input input-bordered w-full"
|
||
value={toPosition}
|
||
onChange={(e) => setToPosition(e.target.value)}
|
||
placeholder="Enter new position"
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="modal-action mt-4 flex justify-end gap-2">
|
||
<button className="btn btn-primary btn-sm" onClick={handleConfirmModal}>
|
||
Confirm
|
||
</button>
|
||
<button
|
||
className="btn btn-outline btn-sm"
|
||
onClick={() => setModalPositionOpen(false)}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|