Files
SGF/resources/js/StockReadWorkspace.jsx
T
2026-11-04 09:37:20 +02:00

44 lines
4.9 KiB
React

import React,{useEffect,useRef,useState} from 'react';
const labels={items:'Artigos',warehouses:'Armazéns',balances:'Existências físicas',movements:'Movimentos'};
export function StockReceiptOrigin({row,enabled,onOpen,loading}) {
if(!enabled||row.movement_type!=='receipt_in'||!Number.isSafeInteger(row.id)||row.id<1||typeof onOpen!=='function')return null;
return <button type="button" disabled={loading} onClick={()=>onOpen(row.id)}>Conferir recepção em Compras</button>;
}
export function milliQuantity(value) {
if(value===null||value===undefined)return 'Indeterminado';
const n=BigInt(value),negative=n<0n,absolute=negative?-n:n;
return `${negative?'-':''}${absolute/1000n},${String(absolute%1000n).padStart(3,'0')}`;
}
export default function StockReadWorkspace({api,context,openReceiptOrigin}) {
const initial={view:'balances',page:1,per_page:25,search:''};
const [query,setQuery]=useState(initial),[data,setData]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(false);
const generation=useRef(0);
useEffect(()=>{
const current=++generation.current;setData(null);setError('');setLoading(true);
const params=new URLSearchParams(Object.entries(query).filter(([,v])=>v!==undefined&&v!==''));
api(`/api/stock-read?${params}`).then(r=>{if(current===generation.current)setData(r);})
.catch(e=>{if(current===generation.current)setError(e.message);}).finally(()=>{if(current===generation.current)setLoading(false);});
return ()=>{generation.current++;};
},[context.token,query]);
function filter(e){e.preventDefault();const f=Object.fromEntries(new FormData(e.currentTarget));setQuery({view:query.view,page:1,per_page:Number(f.per_page),search:f.search,warehouse_id:f.warehouse_id||undefined});}
function page(n){setQuery({...query,page:n,snapshot:data.snapshot});}
const trace=query.view==='movements'&&data?.capabilities?.receipt_trace===true&&typeof openReceiptOrigin==='function';
return <section className="panel"><h2>Consulta de Stock</h2>
<p>Somente leitura. As restantes operações de Stock continuam indisponíveis neste percurso.</p>
<nav aria-label="Consultas de stock">{Object.entries(labels).map(([id,label])=><button key={id} disabled={loading} aria-current={query.view===id?'page':undefined} onClick={()=>setQuery({...initial,view:id})}>{label}</button>)}</nav>
<form onSubmit={filter} key={query.view}><label>Pesquisa por código ou nome<input name="search" maxLength={100}/></label>
{['balances','movements'].includes(query.view)&&<label>Identificador do armazém autorizado<input name="warehouse_id" type="number" min="1"/></label>}
<label>Registos por página<select name="per_page" defaultValue="25"><option value="25">25</option><option value="100">100</option></select></label>
<button disabled={loading}>Pesquisar</button></form>
{loading&&<p role="status">A consultar a fonte autorizada…</p>}
{error&&<div role="alert"><p>{error}</p><button onClick={()=>setQuery({...query,page:1,snapshot:undefined})}>Actualizar consulta</button></div>}
{data&&<><p role="status">{data.notice}</p><p>Referência de leitura: {data.observed_at}. {data.pagination.total} registos; página {data.pagination.page} de {data.pagination.last_page}.</p>
<div className="table-wrap"><table><thead><tr><th>Código / origem</th><th>Designação</th>{query.view!=='warehouses'&&<th>Unidade</th>}{['balances','movements'].includes(query.view)&&<><th>Armazém</th><th>{query.view==='balances'?'Físico':'Quantidade'}</th><th>{query.view==='balances'?'Disponível':'Tipo / data'}</th></>}{trace&&<th>Documento de origem</th>}</tr></thead>
<tbody>{data.rows.map(row=><tr key={row.id??`${row.stock_item_id}:${row.warehouse_id}`}><td>#{row.id??row.stock_item_id} · {row.code}{row.source_reference&&<small> · {row.source_reference}</small>}</td><td>{row.name}</td>{query.view!=='warehouses'&&<td>{row.unit}</td>}{['balances','movements'].includes(query.view)&&<><td>#{row.warehouse_id} · {row.warehouse_code} · {row.warehouse_name}</td><td>{milliQuantity(query.view==='balances'?row.physical_milli:row.quantity_milli)}</td><td>{query.view==='balances'?'Indeterminado':`${row.movement_type === 'workshop_return_in' ? 'Devolução de excedente' : row.movement_type} · ${row.moved_at}`}</td></>}{trace&&<td><StockReceiptOrigin row={row} enabled={trace} onOpen={openReceiptOrigin} loading={loading}/></td>}</tr>)}</tbody></table></div>
{data.rows.length===0&&<p>Sem registos elegíveis para estes filtros nesta fonte autorizada.</p>}
<button disabled={loading||query.page<=1} onClick={()=>page(query.page-1)}>Anterior</button><button disabled={loading||query.page>=data.pagination.last_page} onClick={()=>page(query.page+1)}>Seguinte</button>
<button disabled={loading} onClick={()=>setQuery({...query,page:1,snapshot:undefined})}>Nova referência de leitura</button></>}
</section>;
}