vat_wms/resources/js/Components/modals/SetStockModal.tsx

393 lines
18 KiB
TypeScript

import React from 'react'
import axios from 'axios'
import {toast} from 'react-hot-toast'
import {StockBatch, StockEntry, StockPosition, StockSection} from '@/types'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faCheckCircle, faTimesCircle} from '@fortawesome/free-solid-svg-icons'
interface Props {
onClose: () => void
selectedBatch: StockBatch
selectedSection: StockSection | null
selectedPosition: StockPosition | null
}
const SetStockModal: React.FC<Props> = ({onClose, selectedBatch, selectedSection, selectedPosition}) => {
const [quantity, setQuantity] = React.useState<string>('')
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
const [selectedEntry, setSelectedEntry] = React.useState<StockEntry | null>(null)
const [loading, setLoading] = React.useState(false)
const [isSuggestionsOpen, setIsSuggestionsOpen] = React.useState(false)
const [activeTab, setActiveTab] = React.useState<'batch' | 'item'>('item')
const handleSelect = (entry: StockEntry) => {
setSelectedEntry(entry)
setIsDropdownOpen(false)
}
const parsedQty = parseInt(quantity, 10)
const isNumber = !isNaN(parsedQty)
const maxItem = selectedEntry ? selectedEntry.count - selectedEntry.count_stocked : 0
const capacity = selectedSection ? selectedSection.capacity : 0
const exceedsItem = isNumber && parsedQty > maxItem
const exceedsCapacity = isNumber && parsedQty > capacity
const hasError = exceedsItem || exceedsCapacity
const readyToScan =
selectedEntry !== null &&
isNumber &&
parsedQty >= 1 &&
!exceedsItem &&
!exceedsCapacity
// Called when “Simulate Scan (ID=3)” is clicked
const handleScan = async () => {
if(activeTab === 'batch') {
if (!selectedBatch || !selectedPosition) return
}
else {
if (!selectedEntry || !selectedSection) return
if (!readyToScan) {
toast.error('Please fix validation errors before confirming.')
return
}
}
setLoading(true)
const toastId = toast.loading('Storing…')
try {
let response
if (activeTab === 'batch') {
response = await axios.post(
'/api/pdaView/setBatchSection',
{
batch_id: selectedBatch.id,
position_id: selectedPosition?.position_id,
},
{ withCredentials: true }
)
} else {
response = await axios.post(
'/api/pdaView/setStockSection',
{
entry_id: selectedEntry?.id,
section_id: selectedSection?.section_id,
count_to_be_stored: parsedQty,
},
{ withCredentials: true }
)
}
// now 'response' is defined no matter which branch ran
const { data } = response
console.log('data', data)
if (!data.success) {
toast.dismiss(toastId)
switch (data.error) {
case 'validation_failed':
toast.error('Validation failed. Check inputs.')
break
case 'not_found':
toast.error('Entry or section not found.')
break
case 'section_occupied':
toast.error('That section is already occupied.')
break
case 'insufficient_capacity':
toast.error('Not enough capacity in this section.')
break
case 'server_error':
default:
toast.error(data.message ?? 'Server error during storing.')
break
}
setLoading(false)
return
}
toast.dismiss(toastId)
toast.success('Stored successfully.')
resetAndClose()
} catch (err: any) {
toast.dismiss()
if (err.response && err.response.data) {
const payload = err.response.data
if (payload.error === 'validation_failed') {
toast.error('Validation failed. Check inputs.')
} else if (payload.error === 'not_found') {
toast.error('Entry or section not found.')
} else if (payload.error === 'section_occupied') {
toast.error('That section is already occupied.')
} else if (payload.error === 'insufficient_capacity') {
toast.error('Not enough capacity in this section.')
} else {
toast.error(payload.message || 'Unknown error occurred.')
}
} else {
toast.error('Network error. Please try again.')
}
setLoading(false)
}
}
const resetAndClose = () => {
setQuantity('')
setSelectedEntry(null)
setLoading(false)
setIsSuggestionsOpen(false)
setIsDropdownOpen(false)
onClose()
}
return (
<div className="space-y-4">
<h3 className="font-bold text-lg">Store Stock</h3>
<div className="tabs tabs-box">
<button
className={`tab ${activeTab === 'batch' ? 'tab-active' : ''}`}
onClick={() => setActiveTab('batch')}
>
Entire batch
</button>
<button
className={`tab ${activeTab === 'item' ? 'tab-active' : ''}`}
onClick={() => setActiveTab('item')}
>
Individual items
</button>
</div>
{activeTab === 'batch' && (
<div className="bg-base-100 border-base-300 p-6">
{(selectedSection || selectedPosition) && (
<div className="mb-2 flex items-center text-primary">
Selected position: {selectedPosition ? selectedPosition.storage_address : selectedSection ? selectedSection.position.storage_address : ""}
</div>
)}
<div className="flex items-center space-x-2 mt-2">
<span>{loading ? 'Storing…' : (selectedSection || selectedPosition) ? 'Waiting for confirm' : 'Waiting for section scan...'}</span>
<span className="loading loading-spinner text-primary"></span>
</div>
<div className="flex space-x-2">
<button type="button" className="btn" onClick={() => setIsSuggestionsOpen(true)}>
Suggested Sections
</button>
<button
type="button"
className={`btn btn-primary ${loading ? 'loading' : ''}`}
onClick={handleScan}
disabled={!selectedSection && !selectedPosition}
>
{loading ? 'Storing…' : selectedSection || selectedPosition ? 'Confirm' : 'Waiting for section scan...'}
</button>
</div>
</div>
)}
{activeTab === 'item' && (
<div className="bg-base-100 border-base-300 p-6">
{/* 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"
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.count_stocked} uskladneno | {selectedEntry.count-selectedEntry.count_stocked} zbyva) {selectedEntry.sections.length > 0 ? `[${selectedEntry.sections[0].storage_address}]` : ''}
</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) => {
const stocked = entry.count_stocked
const total = entry.count
const remaining = total - stocked
const selectable = remaining > 0
return (
<li
key={entry.id}
className={`${!selectable ? 'opacity-50 filter' : ''}`}
>
<button
type="button"
disabled={!selectable}
onClick={selectable ? () => handleSelect(entry) : undefined}
className={
`
flex items-center px-2 py-1 rounded
hover:border-gray-100
${!selectable ? 'cursor-not-allowed' : 'hover:border-gray-100'}
`
}
>
<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.count_stocked} uskladneno | {remaining} zbyva) {entry.sections.length > 0 ? `[${entry.sections[0].storage_address}]` : ''}
</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 to store</span>
</label>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(e.target.value)}
className={`input input-bordered w-full ${hasError ? 'border-red-500' : ''}`}
min="1"
max={selectedEntry ? maxItem : undefined}
required
/>
{selectedEntry && (
<span className="text-sm text-gray-500 mt-1">
Max items: {maxItem} | Capacity: {capacity}
</span>
)}
{/* Validation Messages */}
{(exceedsItem || exceedsCapacity) && (
<div className="mt-2 space-y-1">
{exceedsItem && (
<div className="flex items-center text-red-600">
<FontAwesomeIcon icon={faTimesCircle} className="mr-2" />
<span>Item quantity exceeded</span>
</div>
)}
{exceedsCapacity && (
<div className="flex items-center text-red-600">
<FontAwesomeIcon icon={faTimesCircle} className="mr-2" />
<span>Maximum capacity exceeded</span>
</div>
)}
</div>
)}
{/* Success indicator if valid */}
{readyToScan && (
<div className="mt-2 flex items-center text-green-600">
<FontAwesomeIcon icon={faCheckCircle} className="mr-2" />
<span>Ready to store</span>
</div>
)}
</div>
{selectedSection && (
<div className="mt-2 mb-2 flex items-center text-primary">
Selected section: {selectedSection.storage_address}
</div>
)}
{/* Buttons: Suggested Sections & Simulate Scan */}
<div className="flex space-x-2">
<button type="button" className="btn" onClick={() => setIsSuggestionsOpen(true)}>
Suggested Sections
</button>
<button
type="button"
className={`btn btn-primary ${loading ? 'loading' : ''}`}
onClick={handleScan}
disabled={!readyToScan || loading}
>
{loading ? 'Storing…' : selectedSection ? 'Confirm' : 'Waiting for section scan...'}
</button>
</div>
{/* Waiting for scan indicator */}
{readyToScan && !loading && (
<div className="flex items-center space-x-2 mt-2">
<span>{loading ? 'Storing…' : selectedSection ? 'Waiting for confirm' : 'Waiting for section scan...'}</span>
<span className="loading loading-spinner text-primary"></span>
</div>
)}
{/* Cancel Button */}
<div className="modal-action flex justify-end pt-2">
<button type="button" className="btn" onClick={resetAndClose}>
Cancel
</button>
</div>
{/* Suggested Sections Modal */}
{isSuggestionsOpen && (
<div className="modal modal-open">
<div className="modal-box">
<h3 className="font-bold text-lg">Suggested Sections</h3>
<ul className="mt-4 space-y-2">
<li>Section A (ID: 1)</li>
<li>Section B (ID: 2)</li>
<li>Section C (ID: 3)</li>
<li>Section D (ID: 4)</li>
</ul>
<div className="modal-action">
<button className="btn" onClick={() => setIsSuggestionsOpen(false)}>
Close
</button>
</div>
</div>
</div>
)}
</div>
)}
</div>
)
}
export default SetStockModal