Autor #{r.actor_id} · {r.created_at} · {r.reason}
import React,{useEffect,useMemo,useState}from'react';import{WorkshopStockOperation}from'./workshop-stock-operation.js'; const qs=x=>new URLSearchParams(Object.fromEntries(Object.entries(x).filter(([,v])=>v!==''&&v!==null))).toString(); function Options({api,view,label,value,onChange,disabled,context}){const[q,setQ]=useState({search:'',page:1,per_page:25}),[search,setSearch]=useState(''),[p,setP]=useState(null);useEffect(()=>{let active=true;api('/api/commercial-quotes/options?'+qs({view,...q})).then(v=>{if(active)setP(v.records);}).catch(()=>{if(active)setP(null);});return()=>{active=false;};},[context.token,view,q]);return
;} const labels={draft:'Preparado — por rever',approved:'Revisto — aguarda cliente',rejected:'Revisão recusada',customer_accepted:'Aceite pelo cliente',customer_declined:'Recusado pelo cliente',converted:'Convertido em rascunho'}; export default function CommercialQuotesWorkspace({api,context,can,navigate,actorId}){ const[query,setQuery]=useState({page:1,per_page:25,state:'',search:''}),[search,setSearch]=useState(''),[data,setData]=useState(null),[form,setForm]=useState(null),[origin,setOrigin]=useState(null),[preview,setPreview]=useState(null),[error,setError]=useState(''),[notice,setNotice]=useState(''),[busy,setBusy]=useState(false),[history,setHistory]=useState(null),[historyKey,setHistoryKey]=useState(null);const command=useMemo(()=>new WorkshopStockOperation(api),[context.token]),has=right=>can('commercial_quotes.'+right,context.unit?.id),locked=busy||command.uncertain; const load=async()=>setData((await api('/api/commercial-quotes?'+qs(query))).quotes);useEffect(()=>{let active=true;api('/api/commercial-quotes?'+qs(query)).then(v=>{if(active)setData(v.quotes);}).catch(e=>{if(active)setError(e.message);});return()=>{active=false;};},[query,context.token]); function start(row=null,action='prepare'){setError('');setPreview(null);setOrigin(row);const content=row?.data;setForm({action,quote_key:row?.quote_key??crypto.randomUUID(),version:row?.version??0,reason:'',data:action==='prepare'?{client_id:content?.client_id??'',currency:'MZN',valid_until:content?.valid_until??'',source_order_id:content?.source_order_id??null,description:content?.description??'',lines:content?.lines?.map(l=>({service_id:l.service_id,quantity:l.quantity,unit_price:l.unit_price,description:l.description??''}))??[{service_id:'',quantity:'',unit_price:'',description:''}]}:action==='review'?{decision:''}:action==='customer'?{decision:'',representative:'',capacity:'',evidence:'',decided_on:''}:{}});} const update=(key,value)=>setForm(f=>({...f,data:{...f.data,[key]:value}}));const line=(i,key,value)=>setForm(f=>({...f,data:{...f.data,lines:f.data.lines.map((l,n)=>n===i?{...l,[key]:value}:l)}})); async function prepare(e){e.preventDefault();setBusy(true);setError('');try{const input=JSON.parse(JSON.stringify(form));if(input.action==='prepare'){input.data.client_id=Number(input.data.client_id);input.data.source_order_id=input.data.source_order_id?Number(input.data.source_order_id):null;input.data.lines=input.data.lines.map(l=>({...l,service_id:Number(l.service_id)}));}setPreview(await api('/api/commercial-quotes/preview','POST',input,undefined,crypto.randomUUID()));}catch(e){setError(e.message);}finally{setBusy(false);}} async function confirm(retry=false){setBusy(true);setError('');try{const r=retry?await command.retry():await command.send('/api/commercial-quotes/confirm',{preview:preview.preview},crypto.randomUUID());setNotice(`Orçamento v${r.version}: ${labels[r.state]}${r.replayed?' · recibo recuperado':''}.`);setForm(null);setPreview(null);await load();}catch(e){setError(e.message);if(!command.uncertain)setPreview(null);}finally{setBusy(false);}} async function viewHistory(key,page=1){try{const result=await api('/api/commercial-quotes/'+key+'/history?page='+page);setHistory(result.events);setHistoryKey(key);}catch(e){setError(e.message);}} returnProposta própria, revisão distinta e decisão documentada do cliente. Os valores são base antes de impostos: a conferência fiscal ocorre no seu percurso. Aceitar não emite factura, autoriza trabalho ou cria dívida/stock.
{error&&{error}
}{notice&&{notice}
}{has('prepare')&&} {data&&<>| Referência/cliente | Base proposta | Validade/estado | Acções |
|---|---|---|---|
| ORC-QA-{row.quote_key.slice(0,8).toUpperCase()}{row.client_name} · v{row.version} | {row.data.base} MZN | {row.data.valid_until}{row.expired?'Caducado · ':''}{labels[row.state]} | Versão para impressão{!row.conversion&&has('prepare')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&}{row.state==='draft'&&has('review')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&row.prepared_by!==actorId&&}{row.state==='approved'&&has('prepare')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&!row.expired&&}{row.state==='customer_accepted'&&has('convert')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&!row.expired&&}{row.conversion&&navigate&&} |
Sem orçamentos visíveis para estes filtros.
} Página {data.current_page}/{data.last_page} >} {form&&{preview.impact.reference} · {preview.impact.client} · base {preview.impact.base} MZN · {labels[preview.impact.state]}.
{preview.impact.lines.map((l,i)=>{l.service_name}: {l.quantity} × {l.unit_price} MZN
)}Sem emissão, dívida ou Stock automáticos. Validade {preview.impact.valid_until}.
Autor #{r.actor_id} · {r.created_at} · {r.reason}