Replace box picker with free-text location in audit scan
"Where found" during inventory audit is now a plain text field (e.g. "каб. 305") stored on the object instead of assigning it to a box, since audit walks often use room/office labels that don't map to the box/shelf hierarchy used elsewhere. Box assignment via the regular object form is unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ export interface InventoryObject {
|
||||
created_at: string
|
||||
updated_at: string
|
||||
last_seen_at: string | null
|
||||
last_seen_location: string | null
|
||||
qr_code_url: string
|
||||
photos: Photo[]
|
||||
}
|
||||
@@ -43,7 +44,7 @@ export interface ObjectInput {
|
||||
}
|
||||
|
||||
export interface ObjectScanInput {
|
||||
box_id?: string | null
|
||||
location?: string | null
|
||||
}
|
||||
|
||||
export interface Box {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
const SESSION_START_KEY = 'inventory_audit_session_start'
|
||||
const LAST_BOX_KEY = 'inventory_audit_last_box_id'
|
||||
const LAST_LOCATION_KEY = 'inventory_audit_last_location'
|
||||
|
||||
function readSessionStart(): number {
|
||||
const raw = sessionStorage.getItem(SESSION_START_KEY)
|
||||
@@ -12,26 +12,26 @@ function readSessionStart(): number {
|
||||
}
|
||||
|
||||
/** Tracks the current audit walk: when it started (for the found/not-found split) and the
|
||||
* last room/box picked, so the next scanned object can default to the same location. */
|
||||
* last location typed in, so the next scanned object can default to the same location. */
|
||||
export function useAuditSession() {
|
||||
const [sessionStart, setSessionStart] = useState<number>(readSessionStart)
|
||||
const [lastBoxId, setLastBoxIdState] = useState<string | null>(() =>
|
||||
sessionStorage.getItem(LAST_BOX_KEY),
|
||||
const [lastLocation, setLastLocationState] = useState<string | null>(() =>
|
||||
sessionStorage.getItem(LAST_LOCATION_KEY),
|
||||
)
|
||||
|
||||
const restart = useCallback(() => {
|
||||
const now = Date.now()
|
||||
sessionStorage.setItem(SESSION_START_KEY, String(now))
|
||||
sessionStorage.removeItem(LAST_BOX_KEY)
|
||||
sessionStorage.removeItem(LAST_LOCATION_KEY)
|
||||
setSessionStart(now)
|
||||
setLastBoxIdState(null)
|
||||
setLastLocationState(null)
|
||||
}, [])
|
||||
|
||||
const setLastBoxId = useCallback((boxId: string | null) => {
|
||||
if (boxId) sessionStorage.setItem(LAST_BOX_KEY, boxId)
|
||||
else sessionStorage.removeItem(LAST_BOX_KEY)
|
||||
setLastBoxIdState(boxId)
|
||||
const setLastLocation = useCallback((location: string | null) => {
|
||||
if (location) sessionStorage.setItem(LAST_LOCATION_KEY, location)
|
||||
else sessionStorage.removeItem(LAST_LOCATION_KEY)
|
||||
setLastLocationState(location)
|
||||
}, [])
|
||||
|
||||
return { sessionStart, restart, lastBoxId, setLastBoxId }
|
||||
return { sessionStart, restart, lastLocation, setLastLocation }
|
||||
}
|
||||
|
||||
@@ -51,12 +51,9 @@ export function useDeleteObject() {
|
||||
export function useScanObject() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, boxId }: { id: string; boxId: string | null }) =>
|
||||
api.post<InventoryObject>(`/objects/${id}/scan`, { box_id: boxId } satisfies ObjectScanInput),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['objects'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['boxes'] })
|
||||
},
|
||||
mutationFn: ({ id, location }: { id: string; location: string | null }) =>
|
||||
api.post<InventoryObject>(`/objects/${id}/scan`, { location } satisfies ObjectScanInput),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['objects'] }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,20 +4,19 @@ import { Button } from '../../components/Button'
|
||||
import { PhotoGallery } from '../../components/PhotoGallery'
|
||||
import { QrScanner } from '../../components/QrScanner'
|
||||
import { useAuditSession } from '../../hooks/useAuditSession'
|
||||
import { useBox, useBoxesList } from '../../hooks/useBoxes'
|
||||
import { useBox } from '../../hooks/useBoxes'
|
||||
import { useObject, useObjectsList, useScanObject } from '../../hooks/useObjects'
|
||||
import { parseScannedTarget, type ScannedTarget } from '../../lib/qrUrl'
|
||||
|
||||
const RESCAN_COOLDOWN_MS = 4000
|
||||
|
||||
export function AuditPage() {
|
||||
const { sessionStart, restart, lastBoxId, setLastBoxId } = useAuditSession()
|
||||
const { sessionStart, restart, lastLocation, setLastLocation } = useAuditSession()
|
||||
const { data: objects } = useObjectsList('')
|
||||
const { data: boxes } = useBoxesList('')
|
||||
const scanObject = useScanObject()
|
||||
|
||||
const [overlay, setOverlay] = useState<ScannedTarget | null>(null)
|
||||
const [selectedBoxId, setSelectedBoxId] = useState('')
|
||||
const [locationInput, setLocationInput] = useState('')
|
||||
const [cameraError, setCameraError] = useState<string | null>(null)
|
||||
const cooldownRef = useRef<{ key: string; at: number } | null>(null)
|
||||
|
||||
@@ -33,10 +32,10 @@ export function AuditPage() {
|
||||
const cooldown = cooldownRef.current
|
||||
if (cooldown && cooldown.key === key && Date.now() - cooldown.at < RESCAN_COOLDOWN_MS) return
|
||||
|
||||
if (target.type === 'object') setSelectedBoxId(lastBoxId ?? '')
|
||||
if (target.type === 'object') setLocationInput(lastLocation ?? '')
|
||||
setOverlay(target)
|
||||
},
|
||||
[lastBoxId],
|
||||
[lastLocation],
|
||||
)
|
||||
|
||||
function closeOverlay() {
|
||||
@@ -46,9 +45,9 @@ export function AuditPage() {
|
||||
|
||||
async function confirmFound() {
|
||||
if (!overlay || overlay.type !== 'object') return
|
||||
const boxId = selectedBoxId || null
|
||||
await scanObject.mutateAsync({ id: overlay.id, boxId })
|
||||
setLastBoxId(boxId)
|
||||
const location = locationInput.trim() || null
|
||||
await scanObject.mutateAsync({ id: overlay.id, location })
|
||||
setLastLocation(location)
|
||||
closeOverlay()
|
||||
}
|
||||
|
||||
@@ -106,18 +105,14 @@ export function AuditPage() {
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Где нашли?</label>
|
||||
<select
|
||||
value={selectedBoxId}
|
||||
onChange={(e) => setSelectedBoxId(e.target.value)}
|
||||
<input
|
||||
type="text"
|
||||
value={locationInput}
|
||||
onChange={(e) => setLocationInput(e.target.value)}
|
||||
placeholder="например, каб. 305"
|
||||
autoFocus
|
||||
className="w-full rounded-md border border-slate-300 px-3 py-2 dark:border-slate-700 dark:bg-slate-950"
|
||||
>
|
||||
<option value="">— без ящика —</option>
|
||||
{boxes?.map((box) => (
|
||||
<option key={box.id} value={box.id}>
|
||||
{box.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -210,7 +205,9 @@ export function AuditPage() {
|
||||
<span>
|
||||
{o.name} <span className="text-slate-500 dark:text-slate-400">· {o.inventory_number}</span>
|
||||
</span>
|
||||
{o.box && <span className="shrink-0 text-slate-500 dark:text-slate-400">{o.box.name}</span>}
|
||||
{o.last_seen_location && (
|
||||
<span className="shrink-0 text-slate-500 dark:text-slate-400">{o.last_seen_location}</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
{foundObjects.length === 0 && (
|
||||
|
||||
Reference in New Issue
Block a user