import React,{useEffect,useMemo,useRef,useState}from'react';
import{WorkshopStockOperation}from'./workshop-stock-operation.js';
import{personnelDownload,personnelReadableDownload}from'./statement-download.js';
const qs=q=>new URLSearchParams(Object.entries(q).filter(([,v])=>v!==''&&v!==null)).toString();
export const personnelKinds={contract:'Contratos',day:'Planos e assiduidade',entitlement:'Condições de férias',leave:'Pedidos e gozo de férias',report:'Conferência do período'};
const labels={prepared:'Preparado — por rever',approved:'Revisto e aprovado',refused:'Recusado',active:'Activo',terminated:'Terminado',cancelled:'Cancelado',reserved:'Reservado',taken:'Gozado'};
const duration=m=>m===null||m===undefined?'Desconhecido':`${Math.floor(m/60)}h ${m%60}min`,clock=m=>String(Math.floor(m/60)).padStart(2,'0')+':'+String(m%60).padStart(2,'0'),minutes=v=>Number(v.slice(0,2))*60+Number(v.slice(3));
export function checkedPersonnel(data,q){if(data.policy!=='SGF-HR-PERSONNEL-QA-v1'||data.kind!==q.kind||data.synthetic_only!==true||data.salary_effect!==false||!Array.isArray(data.records?.data)||data.records.current_page!==q.page||data.records.data.length>Number(q.per_page)||data.records.data.some(r=>r.kind!==q.kind||q.worker_key&&r.data.input.worker_key!==q.worker_key||q.record_key&&r.record_key!==q.record_key))throw Error('Resposta de outro âmbito, pessoa ou página. Consulte novamente.');return data;}
export function PersonnelSourcePicker({kind,workerKey,api,context,value,onChange,disabled,optional=false}){
const[query,setQuery]=useState({page:1,per_page:25,search:''}),[search,setSearch]=useState(''),[data,setData]=useState(null),[error,setError]=useState('');useEffect(()=>{let active=true;setData(null);setError('');if(kind!=='worker'&&!workerKey)return;const q={...query,kind:kind==='worker'?'worker':kind,...(workerKey?{worker_key:workerKey}:{})},path=kind==='worker'?'/api/remunerations':'/api/hr-personnel';api(path+'?'+qs(q)).then(r=>{if(active)setData(kind==='worker'?r:checkedPersonnel(r,q));}).catch(e=>{if(active)setError(e.message);});return()=>{active=false;};},[query,workerKey,context.token,kind]);const records=data?.records.data??[];
return
;
}
function Intervals({label,value,onChange,disabled,nullable=false}){return ;}
export function PersonnelResult({kind,data}){const i=data.input,r=data.result;return
{personnelKinds[kind]} · {labels[data.status]}
{data.dependencies.worker.code} · {i.reference??i.date??i.period??(i.valid_from+' a '+i.valid_to)}.
{kind==='contract'&&
Modalidade interna
{{fixed:'Prazo explícito',open:'Sem termo declarado',unknown_end:'Fim desconhecido'}[i.mode]}
Vigência
{i.valid_from} a {i.valid_to??'Sem fim confirmado'}
Bruto mensal declarado
{i.gross} MZN
Dependentes revistos
{r.dependent_records}
}{kind==='day'&&
Previsto
{duration(r.planned_minutes)}
Declarado
{duration(r.declared_minutes)}
Férias conferidas
{duration(r.leave_minutes)}
Por conferir
{duration(r.unaccounted_minutes)}
Fora do plano, sem autorização de horas extra
{duration(r.outside_plan_minutes)}
}{['entitlement','leave'].includes(kind)&&<>
{kind==='leave'?'Pedido: '+duration(r.minutes)+'. ':''}Saldo no resultado desta versão{duration((r.balance_after??r).available_minutes)}, reservado{duration((r.balance_after??r).reserved_minutes)}, gozado{duration((r.balance_after??r).taken_minutes)}.
Minutos de orçamento fictício; sem direito anual legal presumido.
>}{kind==='report'&&<>
{r.known_days} dias com registos; {r.known_conferred_days} conferidos; {r.other_calendar_days_unknown} outros dias sem cobertura comprovada. Mês completo: não demonstrado.
Processamento v{p.version}: {p.contract_link_proven?'Tem contrato escolhido na origem.':'Sem ligação contratual histórica comprovada.'} O processamento não foi recalculado.
)}>}{data.refusal_conference&&
Recusa conferida com as fontes actuais{data.refusal_conference.original_source_changed?' alteradas desde a preparação':''}. A proposta não foi aplicada.{data.refusal_conference.current_balance&&<> Saldo efectivo conferido: disponível{duration(data.refusal_conference.current_balance.available_minutes)}, reservado{duration(data.refusal_conference.current_balance.reserved_minutes)}.>}
}
Evidência: {i.evidence}. Sem salário, dívida ou pagamento automático.
;}
export default function PersonnelWorkspace({api,context,initialKind='contract'}){
const[worker,setWorker]=useState(null),[query,setQuery]=useState({kind:initialKind,page:1,per_page:25}),[filters,setFilters]=useState({search:'',period:'',state:''}),[data,setData]=useState(null),[selected,setSelected]=useState(null),[form,setForm]=useState(null),[preview,setPreview]=useState(null),[busy,setBusy]=useState(false),[error,setError]=useState(''),[notice,setNotice]=useState(''),[receiptKey,setReceiptKey]=useState(''),[receipt,setReceipt]=useState(null);const op=useMemo(()=>new WorkshopStockOperation(api),[context.token]),generation=useRef(context.token),mounted=useRef(true);generation.current=context.token;useEffect(()=>{mounted.current=true;return()=>{mounted.current=false;};},[context.token]);const locked=busy||op.uncertain,protect=locked||Boolean(form);
useEffect(()=>{if(!query.worker_key)return;let active=true;setBusy(true);setError('');setData(null);api('/api/hr-personnel?'+qs(query)).then(r=>{if(active)setData(checkedPersonnel(r,query));}).catch(e=>{if(active)setError(e.message);}).finally(()=>{if(active)setBusy(false);});return()=>{active=false;};},[context.token,query]);
function start(operation='establish',review=false,newRecord=false){const row=newRecord?null:selected,kind=query.kind,old=row?.effective?.data.input??row?.data.input;const defaults={contract:{reference:'QA-',mode:'fixed',valid_from:'',valid_to:'',gross:''},day:{contract_key:'',date:'',reference_on:'',planned:null,actual:null,leave_conferred:false},entitlement:{contract_key:'',valid_from:'',valid_to:'',minutes:null},leave:{contract_key:'',entitlement_key:'',day_key:'',date:'',reference_on:'',intervals:[]},report:{period:''}}[kind];setPreview(null);setError('');setForm({kind,record_key:row?.record_key??crypto.randomUUID(),version:row?.version??0,action:review?'review':'prepare',reason:'',data:review?{decision:''}:{...defaults,...old,worker_key:query.worker_key,operation,evidence:''}});}
const change=(k,v)=>{setPreview(null);setForm(f=>({...f,data:{...f.data,[k]:v}}));};
async function prepare(e){e.preventDefault();setBusy(true);setError('');try{const body={...form,data:{...form.data}};if(body.kind==='contract'&&body.action==='prepare'&&body.data.mode!=='fixed')body.data.valid_to=null;const key=crypto.randomUUID(),out=await api('/api/hr-personnel/preview','POST',body,undefined,key);setPreview({...out,key});}catch(e){setError(e.message);}finally{setBusy(false);}}
async function confirm(retry=false){setBusy(true);setError('');try{const out=retry?await op.retry():await op.send('/api/hr-personnel/confirm',{preview:preview.preview},preview.key);setReceiptKey(preview.key);setNotice(`v${out.version} · ${labels[out.state]}${out.replayed?' · recibo recuperado':''}`);setSelected(null);setForm(null);setPreview(null);setQuery(q=>({...q,page:1}));}catch(e){setError(e.message);if(!op.uncertain)setPreview(null);}finally{setBusy(false);}}
async function download(row,readable=false){const token=context.token;setBusy(true);setError('');try{const guard=()=>mounted.current&&generation.current===token;const out=readable?await personnelReadableDownload({id:row.id,token,sourceSha256:row.data.artifact_sha256,isCurrent:guard}):await personnelDownload({id:row.id,token,sha256:row.data.artifact_sha256,isCurrent:guard});const url=URL.createObjectURL(new Blob([out.bytes],{type:'text/csv;charset=utf-8'})),a=document.createElement('a');a.href=url;a.download=(readable?'sgf-rh-resumo-legivel-':'sgf-rh-interno-')+row.kind+'-v'+row.version+'.csv';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}catch(e){if(mounted.current&&generation.current===token)setError(e.message);}finally{if(mounted.current&&generation.current===token)setBusy(false);}}
const field=(k,label,type='text',extra={})=>,source=(kind,key)=> change(key,v)} disabled={locked||Boolean(preview)||form.version>0}/>;
return
RECURSOS HUMANOS
Contratos, assiduidade e férias QA
Fontes e decisões individuais, com revisão distinta. Registos internos fictícios; sem calendário laboral legal presumido nem alteração automática de remunerações. Resultados preparados são propostas; reserva e gozo só ficam efectivos após revisão alheia.
Saldo efectivo nesta consulta: disponível{duration(selected.current_balance.available_minutes)}, reservado{duration(selected.current_balance.reserved_minutes)}, gozado{duration(selected.current_balance.taken_minutes)}. A versão conservada mantém os valores da sua decisão.
}
Antes v{selected.impact.before.version}, {labels[selected.impact.before.state]??'Sem registo'}; depois v{selected.impact.after.version}, {labels[selected.state]}.
O resumo deriva apenas desta versão revista; mantém o SHA de referência do original e identifica o seu próprio formato.
>}}
{form&&}
{preview&&
Impacto antes da confirmação
Antes v{preview.impact.before.version}, {labels[preview.impact.before.state]??'Sem registo'}; depois v{preview.impact.after.version}, {labels[preview.impact.after.state]}.
Fundamento: {form.reason}. Sem alteração salarial, dívida, pagamento ou lançamento.
Chave própria: {preview.key}
}{op.uncertain&&<>
Resultado incerto. Conserve corpo/chave e não inicie outra decisão.
>}
Recibo técnico próprio
{receipt&&
v{receipt.result.version} · {labels[receipt.result.state]} · {receipt.created_at} · {receipt.reason}. Não foi executada outra operação.