Files
SGF/resources/js/PersonnelWorkspace.jsx
2026-11-11 15:04:01 +02:00

33 lines
23 KiB
React

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 <fieldset disabled={disabled}><legend>{kind==='worker'?'Colaborador fictício revisto':personnelKinds[kind]+' — fonte revista'}</legend><label>Pesquisar fonte<input value={search} onChange={e=>setSearch(e.target.value)}/></label><button type="button" onClick={()=>setQuery({page:1,per_page:25,search})}>Pesquisar {kind==='worker'?'colaborador':personnelKinds[kind]}</button>{error&&<p role="alert">{error}</p>}<label>Escolha explícita<select required={!optional} value={value} onChange={e=>{const r=records.find(r=>r.record_key===e.target.value);onChange(e.target.value,r?.effective??r);}}><option value="">{optional?'Sem ligação contratual escolhida':'Escolher…'}</option>{value&&!records.some(r=>r.record_key===value)&&<option value={value}>Fonte escolhida anteriormente</option>}{records.filter(r=>r.effective&&r.effective.data.status!=='cancelled'&&(kind!=='worker'||r.effective.data.status==='active')).map(r=><option key={r.record_key} value={r.record_key}>{r.effective.data.input.code??r.effective.data.input.reference??r.effective.data.input.date??(r.effective.data.input.valid_from+' a '+r.effective.data.input.valid_to)} · v{r.effective.version}</option>)}</select></label>{data&&<><button type="button" disabled={disabled||query.page<=1} onClick={()=>setQuery(q=>({...q,page:q.page-1}))}>Anterior — fontes</button><span>Página{data.records.current_page}/{data.records.last_page}</span><button type="button" disabled={disabled||query.page>=data.records.last_page} onClick={()=>setQuery(q=>({...q,page:q.page+1}))}>Seguinte — fontes</button></>}</fieldset>;
}
function Intervals({label,value,onChange,disabled,nullable=false}){return <fieldset disabled={disabled}><legend>{label}</legend>{nullable&&<label><input type="checkbox" checked={value===null} onChange={e=>onChange(e.target.checked?null:[])}/>Informação ainda desconhecida</label>}{value!==null&&<>{value.map((r,i)=><div className="form-grid" key={i}><label>Desde<input required type="time" value={clock(r.from)} onChange={e=>onChange(value.map((x,j)=>j===i?{...x,from:minutes(e.target.value)}:x))}/></label><label><input type="checkbox" checked={r.to===1440} onChange={e=>onChange(value.map((x,j)=>j===i?{...x,to:e.target.checked?1440:1020}:x))}/>Fim à meia-noite deste dia</label><label>Até<input readOnly={r.to===1440} required type={r.to===1440?'text':'time'} value={clock(r.to)} onChange={e=>onChange(value.map((x,j)=>j===i?{...x,to:minutes(e.target.value)}:x))}/></label><button type="button" onClick={()=>onChange(value.filter((_,j)=>i!==j))}>Remover intervalo</button></div>)}<button type="button" disabled={disabled||value.length>=8} onClick={()=>onChange([...value,{from:480,to:720}])}>Adicionar intervalo</button>{value.length===0&&<p>Conjunto explicitamente vazio. Não equivale a informação desconhecida.</p>}</>}</fieldset>;}
export function PersonnelResult({kind,data}){const i=data.input,r=data.result;return <section><h3>{personnelKinds[kind]} · {labels[data.status]}</h3><p>{data.dependencies.worker.code} · {i.reference??i.date??i.period??(i.valid_from+' a '+i.valid_to)}.</p>{kind==='contract'&&<dl className="overview-definitions"><dt>Modalidade interna</dt><dd>{{fixed:'Prazo explícito',open:'Sem termo declarado',unknown_end:'Fim desconhecido'}[i.mode]}</dd><dt>Vigência</dt><dd>{i.valid_from} a {i.valid_to??'Sem fim confirmado'}</dd><dt>Bruto mensal declarado</dt><dd>{i.gross} MZN</dd><dt>Dependentes revistos</dt><dd>{r.dependent_records}</dd></dl>}{kind==='day'&&<dl className="overview-definitions"><dt>Previsto</dt><dd>{duration(r.planned_minutes)}</dd><dt>Declarado</dt><dd>{duration(r.declared_minutes)}</dd><dt>Férias conferidas</dt><dd>{duration(r.leave_minutes)}</dd><dt>Por conferir</dt><dd>{duration(r.unaccounted_minutes)}</dd><dt>Fora do plano, sem autorização de horas extra</dt><dd>{duration(r.outside_plan_minutes)}</dd></dl>}{['entitlement','leave'].includes(kind)&&<><p>{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)}.</p><p>Minutos de orçamento fictício; sem direito anual legal presumido.</p></>}{kind==='report'&&<><p>{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.</p><div className="table-scroll"><table><thead><tr><th>Dia / versão</th><th>Previsto</th><th>Declarado</th><th>Férias</th><th>Por conferir</th></tr></thead><tbody>{r.days.map(d=><tr key={d.record_key}><td>{d.calculation.date} · v{d.version}</td><td>{duration(d.calculation.planned_minutes)}</td><td>{duration(d.calculation.declared_minutes)}</td><td>{duration(d.calculation.leave_minutes)}</td><td>{duration(d.calculation.unaccounted_minutes)}</td></tr>)}</tbody></table></div>{r.leave_balances.map(b=><p key={b.entitlement_key}>Condição v{b.entitlement_version}: disponível{duration(b.available_minutes)}, reservado{duration(b.reserved_minutes)}, gozado{duration(b.taken_minutes)}.</p>)}{r.payroll.map(p=><p key={p.record_key}>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.</p>)}</>}{data.refusal_conference&&<p>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)}.</>}</p>}<p>Evidência: {i.evidence}. Sem salário, dívida ou pagamento automático.</p></section>;}
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={})=><label key={k}>{label}<input required type={type} value={form.data[k]??''} onChange={e=>change(k,e.target.value)} {...extra}/></label>,source=(kind,key)=> <PersonnelSourcePicker kind={kind} workerKey={query.worker_key} context={context} api={api} value={form.data[key]} onChange={v=>change(key,v)} disabled={locked||Boolean(preview)||form.version>0}/>;
return <section className="panel" data-sgf-operation-uncertain={protect?'true':undefined}><p className="eyebrow">RECURSOS HUMANOS</p><h1>Contratos, assiduidade e férias QA</h1><p>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.</p>{error&&<p role="alert">{error}</p>}{notice&&<p role="status">{notice}</p>}<PersonnelSourcePicker kind="worker" api={api} context={context} value={query.worker_key??''} onChange={(key,row)=>{setWorker(row??null);setSelected(null);setData(null);setQuery({kind:query.kind,page:1,per_page:25,...(key?{worker_key:key}:{})});}} disabled={protect}/>{worker&&<p>Ficha de {worker.data.input.code}, cadastro revisto v{worker.version}. Consultas abaixo mantêm esta pessoa e o contexto.</p>}<nav aria-label="Funções de pessoal">{Object.entries(personnelKinds).map(([kind,label])=><button key={kind} disabled={protect||!query.worker_key} aria-current={query.kind===kind?'page':undefined} onClick={()=>{setSelected(null);setFilters({search:'',period:'',state:''});setQuery({kind,worker_key:query.worker_key,page:1,per_page:25});}}>{label}</button>)}</nav>{!query.worker_key&&<p>Escolha explicitamente um colaborador revisto para consultar ou preparar.</p>}
{query.worker_key&&<form data-sgf-read-filters="true" onSubmit={e=>{e.preventDefault();setSelected(null);setQuery(q=>({kind:q.kind,worker_key:q.worker_key,per_page:q.per_page,...filters,page:1}));}}><fieldset disabled={protect}><label>Pesquisar referência/código<input value={filters.search} onChange={e=>setFilters(f=>({...f,search:e.target.value}))}/></label>{['day','leave','report'].includes(query.kind)&&<label>Período<input type="month" value={filters.period} onChange={e=>setFilters(f=>({...f,period:e.target.value}))}/></label>}<label>Estado<select value={filters.state} onChange={e=>setFilters(f=>({...f,state:e.target.value}))}><option value="">Todos</option>{['prepared','approved','refused'].map(s=><option key={s} value={s}>{labels[s]}</option>)}</select></label><label>Por página<select value={query.per_page} onChange={e=>{setSelected(null);setQuery(q=>({...q,per_page:Number(e.target.value),page:1}));}}><option value="25">25</option><option value="100">100</option></select></label><button>Consultar</button></fieldset></form>}
{data&&<>{data.capabilities.prepare&&!query.record_key&&<button disabled={protect} onClick={()=>{setSelected(null);start('establish',false,true);}}>Novo registo — {personnelKinds[query.kind]}</button>}<p>Página{data.records.current_page}/{data.records.last_page} · {data.records.total} registo/versão(s).</p><div className="table-scroll"><table><thead><tr><th>Referência/dia/período</th><th>Versão e decisão</th><th>Estado revisto actual</th><th>Autor e fundamento</th><th>Escolha</th></tr></thead><tbody>{data.records.data.map(r=><tr key={r.id}><td>{r.data.input.reference??r.data.input.date??r.data.input.period??(r.data.input.valid_from+' a '+r.data.input.valid_to)}</td><td>v{r.version} · {labels[r.state]}<small>{r.data.input.operation}</small></td><td>{r.effective?labels[r.effective.data.status]:'Sem aprovação'}{r.effective&&<small>v{r.effective.version}</small>}</td><td>QA{r.actor_id} · {r.created_at}<small>{r.reason}</small></td><td><button disabled={protect} onClick={()=>{setSelected(r);setReceipt(null);}}>Escolher este registo</button></td></tr>)}</tbody></table></div>{!data.records.data.length&&<p>Sem registos neste âmbito/filtro.</p>}<button disabled={protect||query.page<=1} onClick={()=>{setSelected(null);setQuery(q=>({...q,page:q.page-1}));}}>Anterior</button><button disabled={protect||query.page>=data.records.last_page} onClick={()=>{setSelected(null);setQuery(q=>({...q,page:q.page+1}));}}>Seguinte</button>{query.record_key&&<button disabled={protect} onClick={()=>{setSelected(null);setQuery({kind:query.kind,worker_key:query.worker_key,page:1,per_page:25});}}>Voltar à lista actual</button>}</>}
{selected&&<section><h2>Registo escolhido · v{selected.version} · {labels[selected.state]}</h2><PersonnelResult kind={selected.kind} data={selected.data}/>{selected.current_balance&&<p><strong>Saldo efectivo nesta consulta:</strong> 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.</p>}<p>Antes v{selected.impact.before.version}, {labels[selected.impact.before.state]??'Sem registo'}; depois v{selected.impact.after.version}, {labels[selected.state]}.</p><button disabled={protect} onClick={()=>{setSelected(null);setQuery(q=>({...q,record_key:selected.record_key,page:1}));}}>Abrir histórico deste registo</button>{!query.record_key&&data.capabilities.prepare&&selected.effective?.data.status!=='cancelled'&&<><button disabled={protect} onClick={()=>start(selected.effective?'amend':'establish')}>Preparar {selected.effective?'correcção':'nova versão'}</button>{selected.effective&&<button disabled={protect} onClick={()=>start('cancel')}>Preparar cancelamento</button>}{selected.kind==='contract'&&selected.effective&&<button disabled={protect} onClick={()=>start('terminate')}>Preparar terminação</button>}{selected.kind==='leave'&&selected.effective?.data.status==='reserved'&&<button disabled={protect} onClick={()=>start('take')}>Declarar gozo sem automatismo</button>}</>}{!query.record_key&&data.capabilities.review&&selected.state==='prepared'&&<button disabled={protect} onClick={()=>start('',true)}>Rever preparação alheia</button>}{selected.state==='approved'&&<><button disabled={protect} onClick={()=>download(selected,true)}>Descarregar resumo legível desta versão</button><button disabled={protect} onClick={()=>download(selected)}>Descarregar CSV original conservado</button><p>O resumo deriva apenas desta versão revista; mantém o SHA de referência do original e identifica o seu próprio formato.</p></>}</section>}
{form&&<form onSubmit={prepare}><fieldset disabled={locked||Boolean(preview)}><h2>{form.action==='review'?'Revisão distinta':'Preparação individual'} · {personnelKinds[form.kind]} · v{form.version}</h2>{form.action==='review'?<label>Decisão<select required value={form.data.decision} onChange={e=>change('decision',e.target.value)}><option value="">Escolher…</option><option value="approved">Aprovar</option><option value="refused">Recusar</option></select></label>:<><p>Operação: {form.data.operation}. Pessoa e fontes são revalidadas na confirmação.</p><fieldset disabled={['cancel','take'].includes(form.data.operation)}>{form.kind==='contract'?<>{field('reference','Referência interna QA','text',{readOnly:form.version>0,pattern:'QA-[A-Z0-9-]{2,60}'})}<label>Modalidade interna<select value={form.data.mode} onChange={e=>change('mode',e.target.value)}><option value="fixed">Prazo explícito</option><option value="open">Sem termo declarado</option><option value="unknown_end">Fim desconhecido — vigência incompleta</option></select></label>{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')}<label><input type="checkbox" checked={form.data.minutes===null} onChange={e=>change('minutes',e.target.checked?null:0)}/>Saldo inicial explicitamente desconhecido</label><label>Orçamento QA em minutos<input type="number" min="0" max="527040" required={form.data.minutes!==null} disabled={form.data.minutes===null} value={form.data.minutes??''} onChange={e=>change('minutes',Number(e.target.value))}/></label></>:<>{form.kind==='leave'&&<>{source('entitlement','entitlement_key')}{source('day','day_key')}</>}{field('date','Dia local','date',{readOnly:form.version>0})}</>}</>}</fieldset>{['day','leave'].includes(form.kind)&&field('reference_on','Referência da declaração (não futura)','date')}{form.kind==='day'&&<><Intervals label="Intervalos previstos" value={form.data.planned} nullable onChange={v=>change('planned',v)} disabled={locked||Boolean(preview)||form.data.operation==='cancel'}/><Intervals label="Intervalos declarados" value={form.data.actual} nullable onChange={v=>change('actual',v)} disabled={locked||Boolean(preview)||form.data.operation==='cancel'}/><label><input type="checkbox" checked={form.data.leave_conferred} disabled={form.data.operation==='cancel'} onChange={e=>change('leave_conferred',e.target.checked)}/>Conferir explicitamente férias efectivas deste dia</label></>}{form.kind==='leave'&&<Intervals label="Intervalos do pedido" value={form.data.intervals} onChange={v=>change('intervals',v)} disabled={locked||Boolean(preview)||['cancel','take'].includes(form.data.operation)}/>}<label>Referência da evidência<textarea required minLength={10} maxLength={1000} value={form.data.evidence} onChange={e=>change('evidence',e.target.value)}/></label></>}<label>Fundamento individual<textarea required minLength={10} maxLength={2000} value={form.reason} onChange={e=>setForm(f=>({...f,reason:e.target.value}))}/></label><button>Conferir impacto</button></fieldset><button type="button" disabled={locked} onClick={()=>{setForm(null);setPreview(null);}}>Descartar formulário e prévia</button></form>}
{preview&&<section><h2>Impacto antes da confirmação</h2><p>Antes v{preview.impact.before.version}, {labels[preview.impact.before.state]??'Sem registo'}; depois v{preview.impact.after.version}, {labels[preview.impact.after.state]}.</p><PersonnelResult kind={form.kind} data={preview.impact.after.proposal}/><p>Fundamento: {form.reason}. Sem alteração salarial, dívida, pagamento ou lançamento.</p><button disabled={locked} onClick={()=>confirm()}>Confirmar esta decisão individual</button><p>Chave própria: <code>{preview.key}</code></p></section>}{op.uncertain&&<><p role="alert">Resultado incerto. Conserve corpo/chave e não inicie outra decisão.</p><button disabled={busy} onClick={()=>confirm(true)}>Recuperar a mesma confirmação</button></>}
<h2>Recibo técnico próprio</h2><label>Chave<input disabled={protect} value={receiptKey} onChange={e=>setReceiptKey(e.target.value)}/></label><button disabled={protect||!receiptKey} onClick={async()=>{setBusy(true);setError('');try{setReceipt(await api('/api/hr-personnel/receipts/'+encodeURIComponent(receiptKey)));}catch(e){setError(e.message);}finally{setBusy(false);}}}>Consultar recibo sem executar</button>{receipt&&<p>v{receipt.result.version} · {labels[receipt.result.state]} · {receipt.created_at} · {receipt.reason}. Não foi executada outra operação.</p>}
</section>;
}