33 lines
10 KiB
React
33 lines
10 KiB
React
import React,{useEffect,useMemo,useState}from'react';
|
|
import{WorkshopStockOperation}from'./workshop-stock-operation.js';
|
|
|
|
const day=value=>new Date(value).toISOString().slice(0,10);
|
|
const display=value=>new Intl.DateTimeFormat('pt-MZ',{dateStyle:'short',timeStyle:'short',timeZone:'Africa/Maputo'}).format(new Date(value));
|
|
const local=value=>{const d=new Date(value);return new Date(d.getTime()+120*60000).toISOString().slice(0,16);};
|
|
const utc=value=>new Date(value+':00+02:00').toISOString();
|
|
export default function WorkshopScheduleWorkspace({api,context,can}){
|
|
const now=new Date(),[filters,setFilters]=useState({from:day(now),to:day(new Date(now.getTime()+7*86400000)),state:'',search:'',per_page:25,page:1}),[applied,setApplied]=useState(null),[data,setData]=useState(null),[options,setOptions]=useState(null),[optionQuery,setOptionQuery]=useState({search:'',page:1,per_page:25}),[optionSearch,setOptionSearch]=useState(''),[form,setForm]=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]),manage=can('workshop.assign',context.unit?.id),locked=busy||command.uncertain;
|
|
const query=value=>new URLSearchParams(Object.fromEntries(Object.entries(value).filter(([,v])=>v!==''&&v!==null))).toString();
|
|
async function load(f=applied??filters){setData(await api('/api/workshop-schedule?'+query(f)));}
|
|
useEffect(()=>{setApplied(filters);let current=true;api('/api/workshop-schedule?'+query(filters)).then(v=>{if(current)setData(v);}).catch(e=>{if(current)setError(e.message);});return()=>{current=false;};},[context.token]);
|
|
useEffect(()=>{if(!manage)return;let current=true;api('/api/workshop-schedule/options?'+query(optionQuery)).then(v=>{if(current)setOptions(v.orders);}).catch(e=>{if(current)setError(e.message);});return()=>{current=false;};},[context.token,optionQuery,manage]);
|
|
const start=()=>{setForm({action:'book',booking_key:crypto.randomUUID(),version:0,order_id:'',technician_id:'',starts_at:'',ends_at:'',reason:''});setPreview(null);setNotice('');setError('');};
|
|
const select=(row,action)=>{setForm({action,booking_key:row.booking_key,version:row.version,order_id:row.order_id,technician_id:row.technician_id,starts_at:local(row.starts_at),ends_at:local(row.ends_at),reason:''});setPreview(null);setError('');};
|
|
const update=(key,value)=>{setPreview(null);setForm(f=>({...f,[key]:value}));};
|
|
async function prepare(e){e.preventDefault();setBusy(true);setError('');try{const input={...form,order_id:Number(form.order_id),technician_id:Number(form.technician_id),starts_at:utc(form.starts_at),ends_at:utc(form.ends_at)};const result=await api('/api/workshop-schedule/preview','POST',input,undefined,crypto.randomUUID());setPreview(result);}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/workshop-schedule/confirm',{preview:preview.preview},crypto.randomUUID());setNotice(`Agendamento v${r.version} ${r.state==='cancelled'?'cancelado':'confirmado'}${r.replayed?' · recibo recuperado':''}.`);setPreview(null);setForm(null);await load();}catch(e){setError(e.message);if(!command.uncertain)setPreview(null);}finally{setBusy(false);}}
|
|
async function openHistory(key,page=1){setError('');try{const result=await api('/api/workshop-schedule/'+key+'/history?page='+page);setHistory(result.events);setHistoryKey(key);}catch(e){setError(e.message);}}
|
|
const reminders={within24h:'Nas próximas24h',in_progress:'Intervalo em curso',elapsed:'Intervalo decorrido — não presume trabalho concluído',planned:'Planeado',cancelled:'Cancelado'};
|
|
return <section className="panel" data-sgf-operation-uncertain={command.uncertain||preview?'true':undefined}><h2>Agenda e disponibilidade</h2><p>Horários de Moçambique (UTC+02). Liga ordens já existentes ao técnico atribuído e ao equipamento. Agendar não autoriza trabalho nem altera stock, custos ou facturação. Lembretes apenas neste ecrã.</p>{error&&<p role="alert" className="alert error">{error}</p>}{notice&&<p role="status">{notice}</p>}
|
|
<form onSubmit={e=>{e.preventDefault();setApplied(filters);load(filters).catch(e=>setError(e.message));}}><fieldset disabled={locked}><div className="form-grid"><label>Desde<input type="date" required value={filters.from} onChange={e=>setFilters({...filters,from:e.target.value,page:1})}/></label><label>Até<input type="date" required value={filters.to} onChange={e=>setFilters({...filters,to:e.target.value,page:1})}/></label><label>Ordem ou equipamento<input value={filters.search} onChange={e=>setFilters({...filters,search:e.target.value,page:1})}/></label><label>Estado<select value={filters.state} onChange={e=>setFilters({...filters,state:e.target.value,page:1})}><option value="">Todos</option><option value="scheduled">Agendados</option><option value="cancelled">Cancelados</option></select></label><label>Por página<select value={filters.per_page} onChange={e=>setFilters({...filters,per_page:Number(e.target.value),page:1})}><option value="25">25</option><option value="100">100</option></select></label></div><button>Consultar agenda</button></fieldset></form>
|
|
{manage&&<button disabled={locked||Boolean(form)} onClick={start}>Novo agendamento</button>}
|
|
{data&&<><div className="table-scroll"><table><thead><tr><th>Ordem/equipamento</th><th>Técnico</th><th>Intervalo</th><th>Situação</th><th>Acções</th></tr></thead><tbody>{data.bookings.data.map(row=><tr key={row.id}><td>{row.reference}<small>{row.equipment}</small></td><td>{row.technician}</td><td>{display(row.starts_at)}<small>até {display(row.ends_at)}</small></td><td>{reminders[row.reminder]}</td><td><button disabled={locked} onClick={()=>openHistory(row.booking_key)}>Histórico</button>{manage&&row.state==='scheduled'&&<><button disabled={locked||Boolean(form)} onClick={()=>select(row,'reschedule')}>Reagendar</button><button disabled={locked||Boolean(form)} onClick={()=>select(row,'cancel')}>Cancelar</button></>}</td></tr>)}</tbody></table></div>{!data.bookings.data.length&&<p>Sem agendamentos visíveis nos filtros seleccionados.</p>}<div className="actions"><button disabled={locked||data.bookings.current_page<=1} onClick={()=>{const f={...applied,page:data.bookings.current_page-1};setApplied(f);load(f).catch(e=>setError(e.message));}}>Anterior</button><span>Página {data.bookings.current_page}/{data.bookings.last_page}</span><button disabled={locked||data.bookings.current_page>=data.bookings.last_page} onClick={()=>{const f={...applied,page:data.bookings.current_page+1};setApplied(f);load(f).catch(e=>setError(e.message));}}>Seguinte</button></div></>}
|
|
{form&&<section className="panel"><h3>{form.action==='book'?'Novo agendamento':form.action==='cancel'?'Cancelar agendamento':'Reagendar'}</h3>{form.action==='book'&&<form onSubmit={e=>{e.preventDefault();setOptionQuery({...optionQuery,search:optionSearch,page:1});}}><fieldset disabled={locked||Boolean(preview)}><label>Pesquisar ordem<input value={optionSearch} onChange={e=>setOptionSearch(e.target.value)}/></label><button>Pesquisar</button>{options&&<><button type="button" disabled={options.current_page<=1} onClick={()=>setOptionQuery({...optionQuery,page:options.current_page-1})}>Ordens anteriores</button><button type="button" disabled={options.current_page>=options.last_page} onClick={()=>setOptionQuery({...optionQuery,page:options.current_page+1})}>Ordens seguintes</button></>}</fieldset></form>}
|
|
<form onSubmit={prepare}><fieldset disabled={locked||Boolean(preview)}>{form.action==='book'?<label>Ordem<select value={form.order_id} required onChange={e=>{const row=options?.data.find(o=>o.id===Number(e.target.value));setForm({...form,order_id:e.target.value,technician_id:row?.assignment?.technician_id??''});}}><option value="">Escolher ordem…</option>{options?.data.map(o=><option key={o.id} value={o.id} disabled={!o.assignment}>{o.reference} · {o.equipment} · {o.assignment?.name??'atribuir técnico na ordem primeiro'}</option>)}</select></label>:<p>Ordem #{form.order_id} · versão {form.version}</p>}<p>O técnico é o já atribuído na ordem; alterar a agenda não reatribui trabalho.</p><label>Início<input type="datetime-local" required disabled={form.action==='cancel'} value={form.starts_at} onChange={e=>update('starts_at',e.target.value)}/></label><label>Fim<input type="datetime-local" required disabled={form.action==='cancel'} value={form.ends_at} onChange={e=>update('ends_at',e.target.value)}/></label><label>Fundamento<textarea required minLength={10} maxLength={2000} value={form.reason} onChange={e=>update('reason',e.target.value)}/></label><button>Conferir disponibilidade e impacto</button></fieldset></form>
|
|
{preview&&<div role="status"><h4>Impacto antes da confirmação</h4><p>{preview.impact.order_reference} · {display(preview.impact.starts_at)} até {display(preview.impact.ends_at)}. {preview.impact.state==='cancelled'?'Liberta o intervalo reservado.':'Reserva o técnico e equipamento neste intervalo.'}</p><p>Sem movimento físico, efeito financeiro ou envio de mensagens.</p><button disabled={locked} onClick={()=>confirm()}>Confirmar esta decisão</button></div>}{command.uncertain&&<button disabled={busy} onClick={()=>confirm(true)}>Recuperar a mesma confirmação</button>}<button disabled={locked} onClick={()=>{setForm(null);setPreview(null);}}>Descartar</button>
|
|
</section>}
|
|
{history&&<section className="panel"><h3>Histórico do agendamento</h3>{history.data.map(row=><article key={row.id}><strong>Versão {row.version} · {row.state==='cancelled'?'Cancelado':'Agendado'}</strong><p>{display(row.starts_at)} — {display(row.ends_at)}</p><p>Autor #{row.actor_id} · {display(row.created_at)} · {row.reason}</p></article>)}<button disabled={history.current_page<=1||locked} onClick={()=>openHistory(historyKey,history.current_page-1)}>Anterior</button><button disabled={history.current_page>=history.last_page||locked} onClick={()=>openHistory(historyKey,history.current_page+1)}>Seguinte</button></section>}
|
|
</section>;
|
|
}
|