348 lines
14 KiB
TypeScript
348 lines
14 KiB
TypeScript
// components/modals/EditStockSections.tsx
|
|
import React from 'react'
|
|
import axios from 'axios'
|
|
import { toast } from 'react-hot-toast'
|
|
import { StockPosition, StockSection } from "@/types"
|
|
|
|
interface Props {
|
|
onClose: () => void
|
|
selectedPosition: StockPosition
|
|
}
|
|
|
|
interface EditableSection {
|
|
// Mirror of StockSection, with local-only flags
|
|
section_id?: number
|
|
section_symbol: string
|
|
section_name: string
|
|
capacity: number
|
|
retrievable: boolean
|
|
markedForDeletion: boolean
|
|
}
|
|
|
|
const EditStockSections: React.FC<Props> = ({ onClose, selectedPosition }) => {
|
|
|
|
// Local state for position capacity and editable sections
|
|
const [positionCapacity, setPositionCapacity] = React.useState<number>(selectedPosition.capacity)
|
|
const [sections, setSections] = React.useState<EditableSection[]>([])
|
|
const [loading, setLoading] = React.useState<boolean>(false)
|
|
const [validationError, setValidationError] = React.useState<string>("")
|
|
|
|
// Initialize sections state whenever the selected position changes
|
|
React.useEffect(() => {
|
|
const initialSections: EditableSection[] = selectedPosition.sections.map((sec) => ({
|
|
section_id: sec.section_id,
|
|
section_symbol: sec.section_symbol,
|
|
section_name: sec.section_name,
|
|
capacity: sec.capacity,
|
|
retrievable: sec.retrievable,
|
|
markedForDeletion: false,
|
|
}))
|
|
setSections(initialSections)
|
|
setPositionCapacity(selectedPosition.capacity)
|
|
setValidationError("")
|
|
}, [selectedPosition])
|
|
|
|
// Handler: change position capacity
|
|
const handlePositionCapacityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const val = parseInt(e.target.value, 10)
|
|
if (!isNaN(val) && val >= 0) {
|
|
setPositionCapacity(val)
|
|
// Clear any existing validation error to re-validate
|
|
setValidationError("")
|
|
}
|
|
}
|
|
|
|
// Handler: change a section's field (by index)
|
|
const handleSectionChange = (
|
|
index: number,
|
|
field: keyof Omit<EditableSection, "section_id" | "markedForDeletion">,
|
|
value: string | boolean
|
|
) => {
|
|
setSections((prev) => {
|
|
const updated = [...prev]
|
|
if (field === "capacity") {
|
|
const num = parseInt(value as string, 10)
|
|
updated[index].capacity = isNaN(num) ? 0 : num
|
|
} else if (field === "retrievable") {
|
|
updated[index].retrievable = value as boolean
|
|
} else {
|
|
updated[index][field] = value as string
|
|
}
|
|
return updated
|
|
})
|
|
setValidationError("")
|
|
}
|
|
|
|
// Handler: add a brand-new empty section
|
|
const handleAddSection = () => {
|
|
setSections((prev) => [
|
|
...prev,
|
|
{
|
|
section_symbol: `${prev.length + 1}`,
|
|
section_name: `Section ${prev.length + 1}`,
|
|
capacity: 0,
|
|
retrievable: true,
|
|
markedForDeletion: false,
|
|
},
|
|
])
|
|
setValidationError("")
|
|
}
|
|
|
|
// Handler: mark a section for deletion (or directly remove if never saved)
|
|
const handleRemoveSection = (index: number) => {
|
|
setSections((prev) => {
|
|
const updated = [...prev]
|
|
if (updated[index].section_id) {
|
|
// If it exists on the server, mark for deletion
|
|
updated[index].markedForDeletion = true
|
|
} else {
|
|
// If it's a new unsaved section, just drop it
|
|
updated.splice(index, 1)
|
|
}
|
|
return updated
|
|
})
|
|
setValidationError("")
|
|
}
|
|
|
|
// Compute total capacity of non-deleted sections
|
|
const totalSectionsCapacity = sections
|
|
.filter((s) => !s.markedForDeletion)
|
|
.reduce((sum, s) => sum + s.capacity, 0)
|
|
|
|
// Validation: total section capacity must not exceed position capacity
|
|
React.useEffect(() => {
|
|
if (totalSectionsCapacity > positionCapacity) {
|
|
setValidationError(
|
|
`Total section capacity (${totalSectionsCapacity}) exceeds position capacity (${positionCapacity}).`
|
|
)
|
|
} else {
|
|
setValidationError("")
|
|
}
|
|
}, [totalSectionsCapacity, positionCapacity])
|
|
|
|
// Handler: save all changes (position + sections)
|
|
const handleSave = async () => {
|
|
// Prevent saving if there's a validation error
|
|
if (validationError) {
|
|
toast.error(validationError)
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
const toastId = toast.loading("Saving changes…")
|
|
|
|
try {
|
|
// 1) Update the StockPosition's capacity if it has changed
|
|
if (positionCapacity !== selectedPosition.capacity) {
|
|
await axios.put(
|
|
`/api/stockPositions/${selectedPosition.position_id}`,
|
|
{
|
|
capacity: positionCapacity,
|
|
},
|
|
{ withCredentials: true }
|
|
)
|
|
}
|
|
|
|
// 2) Process each section: create, update, or delete
|
|
for (const sec of sections) {
|
|
if (sec.section_id && sec.markedForDeletion) {
|
|
// DELETE existing section
|
|
await axios.delete(`/api/stockSections/${sec.section_id}`, {
|
|
withCredentials: true,
|
|
})
|
|
} else if (sec.section_id) {
|
|
// UPDATE existing section
|
|
await axios.put(
|
|
`/api/stockSections/${sec.section_id}`,
|
|
{
|
|
section_name: sec.section_name,
|
|
capacity: sec.capacity,
|
|
retrievable: sec.retrievable,
|
|
},
|
|
{ withCredentials: true }
|
|
)
|
|
} else if (!sec.section_id && !sec.markedForDeletion) {
|
|
// CREATE new section
|
|
await axios.post(
|
|
`/api/stockSections`,
|
|
{
|
|
position_id: selectedPosition.position_id,
|
|
section_symbol: sec.section_symbol,
|
|
section_name: sec.section_name,
|
|
capacity: sec.capacity,
|
|
retrievable: sec.retrievable,
|
|
},
|
|
{ withCredentials: true }
|
|
)
|
|
}
|
|
// If a new section was added then immediately marked for deletion before save,
|
|
// we do nothing (skip).
|
|
}
|
|
|
|
toast.dismiss(toastId)
|
|
toast.success("Position and sections updated successfully.")
|
|
onClose()
|
|
} catch (err: any) {
|
|
toast.dismiss(toastId)
|
|
if (err.response && err.response.data && err.response.data.message) {
|
|
toast.error(`Error: ${err.response.data.message}`)
|
|
} else {
|
|
toast.error("Network or server error. Please try again.")
|
|
}
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<h3 className="font-bold text-lg"> Edit<span className="italic opacity-75 border-l-2 pl-2 ml-2">Position {selectedPosition.storage_address}</span> </h3>
|
|
|
|
{/* Position Capacity Input */}
|
|
<div className="form-control w-full">
|
|
<label className="label">
|
|
<span className="label-text">Position Capacity</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={positionCapacity || ""}
|
|
onChange={handlePositionCapacityChange}
|
|
className="input input-bordered w-full"
|
|
min="0"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Sections List */}
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<h4 className="font-semibold">Sections</h4>
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm btn-outline"
|
|
onClick={handleAddSection}
|
|
>
|
|
+ Add Section
|
|
</button>
|
|
</div>
|
|
|
|
{sections.map((sec, idx) => (
|
|
<div
|
|
key={sec.section_id ?? `new-${idx}`}
|
|
className={`border rounded p-3 ${
|
|
sec.markedForDeletion ? "opacity-50 italic" : ""
|
|
}`}
|
|
>
|
|
<div className="flex justify-between items-center mb-2">
|
|
<span className="font-medium">
|
|
{sec.section_id
|
|
? `Section ID: ${sec.section_id}`
|
|
: `New Section`}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="btn btn-error btn-xs"
|
|
onClick={() => handleRemoveSection(idx)}
|
|
>
|
|
{sec.section_id ? "Delete" : "Remove"}
|
|
</button>
|
|
</div>
|
|
{!sec.markedForDeletion && (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{/* Section Name */}
|
|
<div className="form-control">
|
|
<label className="label">
|
|
<span className="label-text">Name</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={sec.section_name}
|
|
onChange={(e) =>
|
|
handleSectionChange(idx, "section_name", e.target.value)
|
|
}
|
|
className="input input-bordered w-full"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Section Capacity */}
|
|
<div className="form-control">
|
|
<label className="label">
|
|
<span className="label-text">Capacity</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={sec.capacity}
|
|
onChange={(e) =>
|
|
handleSectionChange(idx, "capacity", e.target.value)
|
|
}
|
|
className="input input-bordered w-full"
|
|
min="0"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Retrievable Checkbox */}
|
|
<div className="form-control flex items-center pt-6">
|
|
<label className="label cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={sec.retrievable}
|
|
onChange={(e) =>
|
|
handleSectionChange(idx, "retrievable", e.target.checked)
|
|
}
|
|
className="checkbox checkbox-primary mr-2"
|
|
/>
|
|
<span className="label-text">Retrievable</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{sec.markedForDeletion && (
|
|
<div className="text-sm text-red-600">
|
|
This section will be deleted when you save.
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{sections.length === 0 && (
|
|
<div className="text-gray-500 italic">
|
|
No sections defined for this position.
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Validation Error */}
|
|
{validationError && (
|
|
<div className="text-red-600 font-medium">{validationError}</div>
|
|
)}
|
|
<div className="text-sm text-gray-600">
|
|
Total of section capacities: <strong>{totalSectionsCapacity}</strong>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex space-x-2 pt-4">
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary"
|
|
onClick={handleSave}
|
|
disabled={!!validationError || loading}
|
|
>
|
|
{loading ? "Saving…" : "Save Changes"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-outline"
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default EditStockSections
|