vat_wms/resources/js/Pages/Expedice.tsx

535 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, {
useEffect,
useState,
useRef,
forwardRef,
useImperativeHandle,
} from "react";
import { usePage } from "@inertiajs/react";
import axios from "axios";
import {
faSquareCheck,
faTruck,
faXmark,
faBarcode, faArrowLeft, faQuestionCircle,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Item, ShipmentRequest } from "@/interfaces/interfaces";
import WarehouseExpediceDialog from "@/Components/WarehouseExpediceDialog";
import PdaView from "@/Pages/PdaView";
// Define an interface for the imperative handle exposed by a row.
interface RowHandle {
triggerAnimation: () => void;
}
// ------------------- ParcelRow (for marking as processed) -------------------
interface ParcelRowProps {
shipment: ShipmentRequest;
onProcess: (shipment: ShipmentRequest) => void;
onOpenDialog: (shipment: ShipmentRequest, type: "parcels") => void;
}
const ParcelRow = forwardRef<RowHandle, ParcelRowProps>(
({ shipment, onProcess, onOpenDialog }, ref) => {
// "none" | "bounce" | "fade"
const [animationPhase, setAnimationPhase] = useState<
"none" | "bounce" | "fade"
>("none");
// Expose a method to trigger the animation from the parent (dialog)
useImperativeHandle(ref, () => ({
triggerAnimation: () => {
setAnimationPhase("bounce");
setTimeout(() => {
setAnimationPhase("fade");
}, 1000);
setTimeout(() => {
onProcess(shipment);
}, 2000);
},
}));
const handleRowClick = () => {
onOpenDialog(shipment, "parcels");
};
const handleProcess = (e: React.MouseEvent) => {
e.stopPropagation();
setAnimationPhase("bounce");
setTimeout(() => {
setAnimationPhase("fade");
}, 1000);
setTimeout(() => {
onProcess(shipment);
}, 2000);
};
/* Determine classes based on the animation phase:
- "none": border-base-300, bg-base-100, text-base-content
- "bounce": border-primary, bg-primary, text-primary-content
- "fade": border-primary, bg-primary, text-primary-content, opacity-0
*/
const containerClasses = `
p-4 border rounded-lg shadow flex flex-col space-y-3 transition-all duration-1000 cursor-pointer
${
animationPhase === "none"
? "border-base-300 bg-base-100 text-base-content opacity-100"
: animationPhase === "bounce"
? "border-primary bg-primary text-primary-content opacity-100"
: "border-primary bg-primary text-primary-content opacity-0"
}
`;
return (
<div onClick={handleRowClick} className={containerClasses}>
<div>
<p className="font-bold">{shipment.shipment_reference}</p>
<div className="mt-2 space-y-2">
{shipment.items.map((item) => (
<div key={item.id} className="border-t-2 border-warning pt-2 space-y-1">
<p className="text-base-content">
{item.quantity}× {item.name}
</p>
<div className="flex justify-between text-sm text-base-content">
<p>{item.model_number}</p>
<p>
{(item.price / item.quantity).toFixed(2)}{" "}
{shipment.currency}
</p>
</div>
<div
className={`mt-2 flex flex-wrap gap-2 p-2 border rounded-lg transition-colors duration-300 ${
animationPhase === "bounce"
? "bg-primary text-primary-content"
: "bg-base-200 text-base-content"
} border-base-300`}
>
{Object.entries(item.stockData).map(
([stockName, stockArray]) =>
stockArray.map((stock, idx) => (
<div key={`${stockName}-${idx}`} className="text-sm">
<p className="font-semibold">{stockName}</p>
<p>Sklad: {stock.location}</p>
<p>{stock.count} ks</p>
</div>
))
)}
</div>
</div>
))}
</div>
</div>
{animationPhase === "bounce" ? (
<FontAwesomeIcon
icon={faSquareCheck}
className="w-8 h-8 text-primary animate-bounce"
/>
) : (
<button
onClick={handleProcess}
className="btn btn-primary btn-sm self-end"
>
Process
</button>
)}
</div>
);
}
);
// ------------------- ProcessedRow (for marking as unprocessed) -------------------
interface ProcessedRowProps {
shipment: ShipmentRequest;
onUnprocess: (shipment: ShipmentRequest) => void;
onOpenDialog: (shipment: ShipmentRequest, type: "processed") => void;
}
const ProcessedRow = forwardRef<RowHandle, ProcessedRowProps>(
({ shipment, onUnprocess, onOpenDialog }, ref) => {
const [animationPhase, setAnimationPhase] = useState<
"none" | "bounce" | "fade"
>("none");
useImperativeHandle(ref, () => ({
triggerAnimation: () => {
setAnimationPhase("bounce");
setTimeout(() => {
setAnimationPhase("fade");
}, 1000);
setTimeout(() => {
onUnprocess(shipment);
}, 2000);
},
}));
const handleRowClick = () => {
onOpenDialog(shipment, "processed");
};
const handleUnprocess = (e: React.MouseEvent) => {
e.stopPropagation();
setAnimationPhase("bounce");
setTimeout(() => {
setAnimationPhase("fade");
}, 1000);
setTimeout(() => {
onUnprocess(shipment);
}, 2000);
};
/* Determine classes based on the animation phase:
- "none": border-base-300, bg-base-100, text-base-content
- "bounce": border-error, bg-error, text-error-content
- "fade": border-error, bg-error, text-error-content, opacity-0
*/
const containerClasses = `
p-4 border rounded-lg shadow flex flex-col space-y-3 transition-all duration-1000 cursor-pointer
${
animationPhase === "none"
? "border-base-300 bg-base-100 text-base-content opacity-100"
: animationPhase === "bounce"
? "border-error bg-error text-error-content opacity-100"
: "border-error bg-error text-error-content opacity-0"
}
`;
return (
<div onClick={handleRowClick} className={containerClasses}>
<div>
<p className="font-bold">{shipment.shipment_reference}</p>
<div className="mt-2 space-y-2">
{shipment.items.map((item) => (
<div key={item.id} className="border-t-2 border-warning pt-2 space-y-1">
<p className="text-base-content">
{item.quantity}× {item.name}
</p>
<div className="flex justify-between text-sm text-base-content">
<p>{item.model_number}</p>
<p>
{(item.price / item.quantity).toFixed(2)}{" "}
{shipment.currency}
</p>
</div>
<div
className={`mt-2 flex flex-wrap gap-2 p-2 border rounded-lg transition-colors duration-300 ${
animationPhase === "bounce"
? "bg-error text-error-content"
: "bg-base-200 text-base-content"
} border-base-300`}
>
{Object.entries(item.stockData).map(
([stockName, stockArray]) =>
stockArray.map((stock, idx) => (
<div key={`${stockName}-${idx}`} className="text-sm">
<p className="font-semibold">{stockName}</p>
<p>Sklad: {stock.location}</p>
<p>{stock.count} ks</p>
</div>
))
)}
</div>
</div>
))}
</div>
</div>
{animationPhase === "bounce" ? (
<FontAwesomeIcon
icon={faXmark}
className="w-8 h-8 text-error animate-bounce"
/>
) : (
<button
onClick={handleUnprocess}
className="btn btn-error btn-sm self-end"
>
Unprocess
</button>
)}
</div>
);
}
);
type InertiaProps = {
auth: any; // adjust per your auth type
selectedBatchID: number;
};
// ------------------- Main Component -------------------
export default function WarehouseExpedicePage() {
const { selectedBatchID } = usePage<InertiaProps>().props;
// States for shipments
const [parcels, setParcels] = useState<ShipmentRequest[]>([]);
const [parcelsOther, setParcelsOther] = useState<ShipmentRequest[]>([]);
const [processed, setProcessed] = useState<ShipmentRequest[]>([]);
const [loading, setLoading] = useState(true);
// Refs to control individual row animations
const parcelRowRefs = useRef<{ [key: number]: RowHandle | null }>({});
const processedRowRefs = useRef<{ [key: number]: RowHandle | null }>({});
// Dialog state
const [selectedShipment, setSelectedShipment] =
useState<ShipmentRequest | null>(null);
const [selectedType, setSelectedType] = useState<
"parcels" | "processed" | null
>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const handleCopyToClipboard = () => {
const copyString = parcels.map((p) => p.shipment_reference).join(",");
const copyStringOther = parcelsOther.map((p) => p.shipment_reference).join(
","
);
const copyStringProcessed = processed
.map((p) => p.shipment_reference)
.join(",");
navigator.clipboard
.writeText([copyString, copyStringOther, copyStringProcessed].filter(Boolean).join(","))
.then(() => {
alert("Parcels copied to clipboard!");
})
.catch((err) => {
console.error("Failed to copy:", err);
});
};
useEffect(() => {
axios
.post("/api/expediceListWMS", { batch_id: selectedBatchID })
.then((response) => {
setParcels(response.data.batch_items.shipments);
setParcelsOther(response.data.batch_items.shipments_other);
setLoading(false);
})
.catch((error) => {
console.error("Error fetching data:", error);
setLoading(false);
});
}, [selectedBatchID]);
// Move a shipment from Parcels to Processed
const markAsProcessed = (shipment: ShipmentRequest) => {
setParcels((prev) => prev.filter((s) => s.id !== shipment.id));
setProcessed((prev) => [...prev, shipment]);
};
// Move a shipment from Processed back to Parcels
const markAsUnprocessed = (shipment: ShipmentRequest) => {
setProcessed((prev) => prev.filter((s) => s.id !== shipment.id));
setParcels((prev) => [...prev, shipment]);
};
// Open the dialog for a shipment
const openDialog = (
shipment: ShipmentRequest,
type: "parcels" | "processed"
) => {
setSelectedShipment(shipment);
setSelectedType(type);
setIsDialogOpen(true);
};
// Close the dialog
const closeDialog = () => {
setIsDialogOpen(false);
setSelectedShipment(null);
setSelectedType(null);
};
const pdaDialogRef = useRef<HTMLDialogElement>(null);
const openPdaModal = () => {
pdaDialogRef.current?.showModal();
};
const closePdaModal = () => {
pdaDialogRef.current?.close();
};
const [buttonStates, setButtonStates] = useState<{ [itemId: number]: any }>(
{}
);
// Handle processing/unprocessing via the dialog action button
const handleDialogProcess = () => {
if (selectedShipment && selectedType === "parcels") {
const filteredButtonStates = selectedShipment.items.reduce(
(acc: any, item: Item) => {
if (buttonStates[item.id]) {
acc[item.id] = buttonStates[item.id];
}
return acc;
},
{}
);
axios
.post("/api/warehouseExpedice/passOptions", {
shipment_request_id: selectedShipment.id,
buttonStates: filteredButtonStates,
shipment_items: selectedShipment.items,
})
.catch((error) => {
console.error("Error sending button states:", error);
});
if (parcelRowRefs.current[selectedShipment.id]) {
parcelRowRefs.current[selectedShipment.id]?.triggerAnimation();
} else {
markAsProcessed(selectedShipment);
}
} else if (selectedShipment && selectedType === "processed") {
if (processedRowRefs.current[selectedShipment.id]) {
processedRowRefs.current[selectedShipment.id]?.triggerAnimation();
} else {
markAsUnprocessed(selectedShipment);
}
}
closeDialog();
};
if (loading) {
return <div className="p-4 text-base-content">Loading...</div>;
}
return (
<>
{/* Top bar */}
<div className="flex justify-between items-center bg-base-100 p-4 shadow">
<a className="link" href={route('dashboard')}><FontAwesomeIcon icon={faArrowLeft} /> Back</a>
<button className="btn btn-ghost">
<FontAwesomeIcon icon={faQuestionCircle} /> Help
</button>
</div>
{/* Open the modal using document.getElementById('ID').showModal() method */}
<button className="btn" onClick={()=>openPdaModal()}>open modal</button>
{/* PDA VIEW DIALOG */}
<dialog id="pdaViewModal" className="modal" ref={pdaDialogRef}>
<div className="modal-box max-w-4xl max-h-[90vh] overflow-hidden p-0">
<PdaView closeParent={closePdaModal} />
</div>
<form method="dialog" className="modal-backdrop">
<button className="btn">Close</button>
</form>
</dialog>
{/* Tabs */}
<div className="tabs tabs-box w-full">
{/* Parcels Tab */}
<label className="tab flex-1">
<input
type="radio"
name="expedice_tabs"
className="tab"
aria-label="Parcels"
defaultChecked
/>
<div className="flex items-center justify-center gap-2 w-full">
<FontAwesomeIcon icon={faTruck} className="w-5 h-5" />
<span className="text-base-content">Parcels</span>
</div>
</label>
<div className="tab-content bg-base-100 border-base-300 p-6">
<div className="space-y-3">
{parcels.length === 0 ? (
<p className="text-base-content">No parcels available.</p>
) : (
parcels.map((shipment) => (
<ParcelRow
ref={(el) => {
parcelRowRefs.current[shipment.id] = el;
}}
key={shipment.id}
shipment={shipment}
onProcess={markAsProcessed}
onOpenDialog={openDialog}
/>
))
)}
</div>
</div>
{/* Nahravacky Tab */}
<label className="tab flex-1">
<input
type="radio"
name="expedice_tabs"
className="tab"
aria-label="Nahravacky"
/>
<div className="flex items-center justify-center gap-2 w-full">
<FontAwesomeIcon icon={faBarcode} className="w-5 h-5" />
<span className="text-base-content">Nahravacky</span>
</div>
</label>
<div className="tab-content bg-base-100 border-base-300 p-6">
<div className="space-y-3">
{parcelsOther.length === 0 ? (
<p className="text-base-content">No parcels available.</p>
) : (
parcelsOther.map((shipment) => (
<ParcelRow
ref={(el) => {
parcelRowRefs.current[shipment.id] = el;
}}
key={shipment.id}
shipment={shipment}
onProcess={markAsProcessed}
onOpenDialog={openDialog}
/>
))
)}
</div>
</div>
{/* Processed Tab (15% width) */}
<label className="tab w-[15%] flex-none">
<input
type="radio"
name="expedice_tabs"
className="tab"
aria-label="Processed"
/>
<div className="flex items-center justify-center w-full">
<FontAwesomeIcon icon={faSquareCheck} className="w-5 h-5" />
</div>
</label>
<div className="tab-content bg-base-100 border-base-300 p-6">
<div className="space-y-3">
{processed.length === 0 ? (
<p className="text-base-content">No processed shipments.</p>
) : (
processed.map((shipment) => (
<ProcessedRow
ref={(el) => {
processedRowRefs.current[shipment.id] = el;
}}
key={shipment.id}
shipment={shipment}
onUnprocess={markAsUnprocessed}
onOpenDialog={openDialog}
/>
))
)}
</div>
</div>
</div>
{/* Shipment Details Dialog */}
{selectedShipment && (
<WarehouseExpediceDialog
selectedShipment={selectedShipment}
isDialogOpen={isDialogOpen}
closeDialog={closeDialog}
handleDialogProcess={handleDialogProcess}
selectedType={selectedType}
buttonStates={buttonStates}
setButtonStates={setButtonStates}
/>
)}
</>
);
}