200 lines
8.1 KiB
TypeScript
200 lines
8.1 KiB
TypeScript
// components/modals/CountStockModal.tsx
|
||
import React from 'react'
|
||
import axios from 'axios'
|
||
import { toast } from 'react-hot-toast'
|
||
import { StockBatch, StockEntry } from "@/types"
|
||
|
||
interface Props {
|
||
onClose: () => void
|
||
selectedBatch: () => StockBatch
|
||
}
|
||
|
||
const CountStockModal: React.FC<Props> = ({ onClose, selectedBatch }) => {
|
||
const [quantity, setQuantity] = React.useState("")
|
||
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
|
||
const [selectedEntry, setSelectedEntry] = React.useState<StockEntry | null>(null)
|
||
|
||
const handleSelect = (entry: StockEntry) => {
|
||
setSelectedEntry(entry)
|
||
setIsDropdownOpen(false)
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
|
||
if (!selectedEntry) {
|
||
toast.error("Please select a product first.")
|
||
return
|
||
}
|
||
|
||
try {
|
||
const { data } = await axios.post(
|
||
"/api/pdaView/countStock",
|
||
{
|
||
entryId: selectedEntry.id,
|
||
quantity,
|
||
},
|
||
{ withCredentials: true }
|
||
)
|
||
|
||
// If the HTTP status was 200 but success===false,
|
||
// inspect data.error to decide which toast to show
|
||
if (!data.success) {
|
||
switch (data.error) {
|
||
case "already_counted":
|
||
toast.error("This item has already been counted.")
|
||
break
|
||
case "validation_failed":
|
||
toast.error("Validation failed. Please check your inputs.")
|
||
break
|
||
case "not_found":
|
||
toast.error("Could not find that stock entry.")
|
||
break
|
||
case "server_error":
|
||
default:
|
||
// show the message from the server if provided, otherwise fallback
|
||
toast.error(
|
||
data.message ?? "Something went wrong on the server."
|
||
)
|
||
break
|
||
}
|
||
return
|
||
}
|
||
|
||
// success === true:
|
||
toast.success("Stock counted!")
|
||
onClose()
|
||
} catch (err: any) {
|
||
// If the request itself failed (e.g. network or HTTP 500 that didn't return JSON):
|
||
// You can inspect err.response.status if you want, e.g. 409 → extract JSON, etc.
|
||
if (err.response && err.response.data) {
|
||
// Attempt to read the server’s JSON error payload
|
||
const payload = err.response.data
|
||
if (payload.error === "already_counted") {
|
||
toast.error("This item has already been counted.")
|
||
return
|
||
}
|
||
if (payload.error === "validation_failed") {
|
||
toast.error("Validation failed. Please check your inputs.")
|
||
return
|
||
}
|
||
if (payload.error === "not_found") {
|
||
toast.error("Could not find that stock entry.")
|
||
return
|
||
}
|
||
// Fallback to any message string
|
||
toast.error(payload.message || "Unknown error occurred.")
|
||
return
|
||
}
|
||
|
||
// Otherwise, a true “network” or unexpected error:
|
||
toast.error("Failed to count stock. Please try again.")
|
||
}
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<h3 className="font-bold text-lg">Count Stock</h3>
|
||
|
||
{/* Product Dropdown */}
|
||
<div className="form-control w-full">
|
||
<label className="label">
|
||
<span className="label-text">Product</span>
|
||
</label>
|
||
|
||
<div className="dropdown w-full">
|
||
<button
|
||
type="button"
|
||
className={`btn w-full justify-between ${selectedEntry && selectedEntry?.counted ? "pointer-events-none opacity-50" : "pointer-events-auto opacity-100"}`}
|
||
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||
>
|
||
{selectedEntry ? (
|
||
<div className="flex items-center space-x-2">
|
||
<img
|
||
src={selectedEntry.physical_item.image_url}
|
||
alt={selectedEntry.physical_item.name}
|
||
className="w-6 h-6 rounded-full"
|
||
/>
|
||
<span>
|
||
{selectedEntry.physical_item.name} ({selectedEntry.original_count_invoice})
|
||
</span>
|
||
</div>
|
||
) : (
|
||
"Select product ..."
|
||
)}
|
||
<svg
|
||
className="fill-current w-4 h-4"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
viewBox="0 0 20 20"
|
||
>
|
||
<path d="M5.516 7.548a.75.75 0 0 1 1.06 0L10 10.972l3.424-3.424a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06z" />
|
||
</svg>
|
||
</button>
|
||
|
||
<ul
|
||
tabIndex={0}
|
||
className={`dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full mt-1 ${
|
||
isDropdownOpen ? "block" : "hidden"
|
||
}`}
|
||
>
|
||
{selectedBatch.stock_entries.map((entry) => (
|
||
<li key={entry.id}>
|
||
<button
|
||
type="button"
|
||
disabled={entry.counted}
|
||
className={`flex items-center px-2 py-1 hover:bg-gray-100 rounded ${entry.counted ? "pointer-events-none opacity-50" : "pointer-events-auto opacity-100"}`}
|
||
onClick={() => handleSelect(entry)}
|
||
>
|
||
<img
|
||
src={entry.physical_item.image_url}
|
||
alt={entry.physical_item.name}
|
||
className="w-6 h-6 rounded-full mr-2"
|
||
/>
|
||
<span>
|
||
{entry.physical_item.name} ({entry.original_count_invoice}) {entry.counted ? " --- (Already counted)" : ""}
|
||
</span>
|
||
</button>
|
||
</li>
|
||
))}
|
||
{selectedBatch.stock_entries.length === 0 && (
|
||
<li>
|
||
<span className="px-2 py-1 text-gray-500">No items available</span>
|
||
</li>
|
||
)}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quantity Input */}
|
||
<div className="form-control w-full">
|
||
<label className="label">
|
||
<span className="label-text">Quantity</span>
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={quantity}
|
||
onChange={(e) => setQuantity(+e.target.value)}
|
||
className="input input-bordered w-full"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="modal-action flex justify-end space-x-2">
|
||
<button type="button" className="btn" onClick={onClose}>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="btn btn-primary"
|
||
disabled={!selectedEntry || quantity < 0}
|
||
>
|
||
Submit
|
||
</button>
|
||
</div>
|
||
</form>
|
||
)
|
||
}
|
||
|
||
export default CountStockModal
|