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
{kind==='worker'?'Colaborador fictício revisto':personnelKinds[kind]+' — fonte revista'}{error&&

{error}

}{data&&<>Página{data.records.current_page}/{data.records.last_page}}
; } function Intervals({label,value,onChange,disabled,nullable=false}){return
{label}{nullable&&}{value!==null&&<>{value.map((r,i)=>
)}{value.length===0&&

Conjunto explicitamente vazio. Não equivale a informação desconhecida.

}}
;} 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.

{r.days.map(d=>)}
Dia / versãoPrevistoDeclaradoFériasPor conferir
{d.calculation.date} · v{d.version}{duration(d.calculation.planned_minutes)}{duration(d.calculation.declared_minutes)}{duration(d.calculation.leave_minutes)}{duration(d.calculation.unaccounted_minutes)}
{r.leave_balances.map(b=>

Condição v{b.entitlement_version}: disponível{duration(b.available_minutes)}, reservado{duration(b.reserved_minutes)}, gozado{duration(b.taken_minutes)}.

)}{r.payroll.map(p=>

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.

{error&&

{error}

}{notice&&

{notice}

}{setWorker(row??null);setSelected(null);setData(null);setQuery({kind:query.kind,page:1,per_page:25,...(key?{worker_key:key}:{})});}} disabled={protect}/>{worker&&

Ficha de {worker.data.input.code}, cadastro revisto v{worker.version}. Consultas abaixo mantêm esta pessoa e o contexto.

}{!query.worker_key&&

Escolha explicitamente um colaborador revisto para consultar ou preparar.

} {query.worker_key&&
{e.preventDefault();setSelected(null);setQuery(q=>({kind:q.kind,worker_key:q.worker_key,per_page:q.per_page,...filters,page:1}));}}>
{['day','leave','report'].includes(query.kind)&&}
} {data&&<>{data.capabilities.prepare&&!query.record_key&&}

Página{data.records.current_page}/{data.records.last_page} · {data.records.total} registo/versão(s).

{data.records.data.map(r=>)}
Referência/dia/períodoVersão e decisãoEstado revisto actualAutor e fundamentoEscolha
{r.data.input.reference??r.data.input.date??r.data.input.period??(r.data.input.valid_from+' a '+r.data.input.valid_to)}v{r.version} · {labels[r.state]}{r.data.input.operation}{r.effective?labels[r.effective.data.status]:'Sem aprovação'}{r.effective&&v{r.effective.version}}QA{r.actor_id} · {r.created_at}{r.reason}
{!data.records.data.length&&

Sem registos neste âmbito/filtro.

}{query.record_key&&}} {selected&&

Registo escolhido · v{selected.version} · {labels[selected.state]}

{selected.current_balance&&

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]}.

{!query.record_key&&data.capabilities.prepare&&selected.effective?.data.status!=='cancelled'&&<>{selected.effective&&}{selected.kind==='contract'&&selected.effective&&}{selected.kind==='leave'&&selected.effective?.data.status==='reserved'&&}}{!query.record_key&&data.capabilities.review&&selected.state==='prepared'&&}{selected.state==='approved'&&<>

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&&

{form.action==='review'?'Revisão distinta':'Preparação individual'} · {personnelKinds[form.kind]} · v{form.version}

{form.action==='review'?:<>

Operação: {form.data.operation}. Pessoa e fontes são revalidadas na confirmação.

{form.kind==='contract'?<>{field('reference','Referência interna QA','text',{readOnly:form.version>0,pattern:'QA-[A-Z0-9-]{2,60}'})}{field('valid_from','Início','date',{readOnly:form.version>0})}{field('valid_to','Fim explícito','date',{required:form.data.mode==='fixed',disabled:form.data.mode!=='fixed'})}{field('gross','Bruto mensal MZN','text',{pattern:'[0-9]{1,9}\\.[0-9]{2}',readOnly:form.data.operation==='terminate'})}:form.kind==='report'?field('period','Período da conferência','month',{readOnly:form.version>0}):<>{source('contract','contract_key')}{form.kind==='entitlement'?<>{field('valid_from','Vigência desde','date')}{field('valid_to','Vigência até','date')}:<>{form.kind==='leave'&&<>{source('entitlement','entitlement_key')}{source('day','day_key')}}{field('date','Dia local','date',{readOnly:form.version>0})}}}
{['day','leave'].includes(form.kind)&&field('reference_on','Referência da declaração (não futura)','date')}{form.kind==='day'&&<>change('planned',v)} disabled={locked||Boolean(preview)||form.data.operation==='cancel'}/>change('actual',v)} disabled={locked||Boolean(preview)||form.data.operation==='cancel'}/>}{form.kind==='leave'&&change('intervals',v)} disabled={locked||Boolean(preview)||['cancel','take'].includes(form.data.operation)}/>}