898 lines
35 KiB
TypeScript
898 lines
35 KiB
TypeScript
import { Link, router, useForm } from '@inertiajs/react';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import {
|
|
portalRequest,
|
|
PortalApiError,
|
|
newSubmissionKey,
|
|
} from '@/lib/portal-api';
|
|
import EducationSubmittedSnapshot, {
|
|
type EducationDraft,
|
|
type EducationDraftSummary,
|
|
} from '@/components/education-submitted-snapshot';
|
|
import {
|
|
Management,
|
|
Field,
|
|
control,
|
|
button,
|
|
Pagination,
|
|
} from '../management/shared';
|
|
import type { Paginated } from '../management/shared';
|
|
import {
|
|
TerritoryFields,
|
|
type TerritoryOption,
|
|
} from '@/components/investment-territory';
|
|
type Option = { id: string; name?: string; title?: string; code?: string };
|
|
type RecordRow = {
|
|
id: string;
|
|
qualification: string;
|
|
quantity: number;
|
|
status: string;
|
|
version_no: number;
|
|
valid_until: string;
|
|
period_start: string;
|
|
period_end: string;
|
|
source: string;
|
|
can_review?: boolean;
|
|
review_unavailable?: 'self' | 'expired' | null;
|
|
details_url?: string;
|
|
};
|
|
type WorkCounts = {
|
|
total: number;
|
|
submitted: number;
|
|
reviewed: number;
|
|
rejected: number;
|
|
expired: number;
|
|
to_review: number;
|
|
};
|
|
type Props = {
|
|
workQueue?: {
|
|
actor_id: number;
|
|
drafts: number | null;
|
|
demands: WorkCounts;
|
|
capacities: WorkCounts;
|
|
};
|
|
draftsAvailable?: boolean;
|
|
selectedDraft?: EducationDraft | null;
|
|
ownDrafts?: Paginated<EducationDraftSummary> | null;
|
|
demands: Paginated<RecordRow>;
|
|
capacities: Paginated<RecordRow>;
|
|
skills: Option[];
|
|
occupations: Option[];
|
|
providers: Option[];
|
|
courses: Option[];
|
|
projects: Option[];
|
|
territories: TerritoryOption[];
|
|
today: string;
|
|
filters: {
|
|
q?: string;
|
|
status?: string;
|
|
geo_location_id?: string;
|
|
scope?: 'all' | 'mine' | 'review';
|
|
};
|
|
indicators: {
|
|
demand_records: number;
|
|
capacity_records: number;
|
|
expired_demands: number;
|
|
pending_demands: number;
|
|
};
|
|
};
|
|
const territoryLevels = ['province', 'district', 'admin_post', 'locality'];
|
|
|
|
function territoryPath(
|
|
id: string,
|
|
territories: TerritoryOption[],
|
|
): TerritoryOption[] {
|
|
const byId = new Map(
|
|
territories.map((territory) => [territory.id, territory]),
|
|
);
|
|
const path: TerritoryOption[] = [];
|
|
let current = byId.get(id);
|
|
while (current && path.length < territoryLevels.length) {
|
|
if (territoryLevels.includes(current.location_type))
|
|
path.unshift(current);
|
|
current = current.parent_id ? byId.get(current.parent_id) : undefined;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
function Errors({ errors }: { errors: Record<string, string> }) {
|
|
return (
|
|
<>
|
|
{Object.entries(errors).map(([key, value]) => (
|
|
<p role="alert" key={key}>
|
|
{value}
|
|
</p>
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
function Select({
|
|
label,
|
|
value,
|
|
options,
|
|
onChange,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
options: Option[];
|
|
onChange: (value: string) => void;
|
|
}) {
|
|
return (
|
|
<Field label={label}>
|
|
<select
|
|
className={control}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
required
|
|
>
|
|
<option value="">Seleccione</option>
|
|
{value && !options.some((option) => option.id === value) && (
|
|
<option value={value}>
|
|
Selecção guardada não incluída nesta lista
|
|
</option>
|
|
)}
|
|
{options.map((o) => (
|
|
<option key={o.id} value={o.id}>
|
|
{o.code ? `${o.code} — ` : ''}
|
|
{o.name ?? o.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
);
|
|
}
|
|
function Catalog({
|
|
kind,
|
|
title,
|
|
providers,
|
|
}: {
|
|
kind: string;
|
|
title: string;
|
|
providers: Option[];
|
|
}) {
|
|
const form = useForm({ name: '', title: '', code: '', provider_id: '' });
|
|
return (
|
|
<details className="rounded border p-4">
|
|
<summary className="cursor-pointer font-semibold">{title}</summary>
|
|
<form
|
|
className="mt-4 space-y-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.post(`/management/education/catalog/${kind}`, {
|
|
onSuccess: () => form.reset(),
|
|
});
|
|
}}
|
|
>
|
|
<Field label="Designação">
|
|
<input
|
|
className={control}
|
|
required
|
|
value={
|
|
kind === 'courses'
|
|
? form.data.title
|
|
: form.data.name
|
|
}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
kind === 'courses' ? 'title' : 'name',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Field>
|
|
{kind !== 'providers' && (
|
|
<Field label="Código">
|
|
<input
|
|
className={control}
|
|
required
|
|
value={form.data.code}
|
|
onChange={(e) =>
|
|
form.setData('code', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
)}
|
|
{kind === 'courses' && (
|
|
<Select
|
|
label="Instituição"
|
|
value={form.data.provider_id}
|
|
options={providers}
|
|
onChange={(v) => form.setData('provider_id', v)}
|
|
/>
|
|
)}
|
|
<Errors errors={form.errors} />
|
|
<button className={button} disabled={form.processing}>
|
|
Criar cadastro interno
|
|
</button>
|
|
</form>
|
|
</details>
|
|
);
|
|
}
|
|
export function Intake({
|
|
kind,
|
|
props,
|
|
onSaved,
|
|
onDirty,
|
|
}: {
|
|
kind: 'demands' | 'capacities';
|
|
props: Props;
|
|
onSaved?: (draft: EducationDraft) => void;
|
|
onDirty?: (kind: 'demands' | 'capacities', dirty: boolean) => void;
|
|
}) {
|
|
const demand = kind === 'demands';
|
|
const initial =
|
|
props.selectedDraft?.kind === kind ? props.selectedDraft : null;
|
|
const [active, setActive] = useState<EducationDraft | null>(initial);
|
|
const [clientKey] = useState(newSubmissionKey);
|
|
const [draftBusy, setDraftBusy] = useState(false);
|
|
const [draftMessage, setDraftMessage] = useState('');
|
|
const [draftError, setDraftError] = useState('');
|
|
const [initialPath] = useState(() =>
|
|
territoryPath(
|
|
String(initial?.payload.geo_location_id ?? ''),
|
|
props.territories,
|
|
),
|
|
);
|
|
const [territoryResetKey, setTerritoryResetKey] = useState(0);
|
|
const form = useForm({
|
|
project_id: String(initial?.payload.project_id ?? ''),
|
|
course_id: String(initial?.payload.course_id ?? ''),
|
|
occupation_id: String(initial?.payload.occupation_id ?? ''),
|
|
skill_id: String(initial?.payload.skill_id ?? ''),
|
|
geo_location_id: String(initial?.payload.geo_location_id ?? ''),
|
|
employer: String(initial?.payload.employer ?? ''),
|
|
qualification: String(initial?.payload.qualification ?? ''),
|
|
quantity: String(initial?.payload.quantity ?? ''),
|
|
period_start: String(initial?.payload.period_start ?? ''),
|
|
period_end: String(initial?.payload.period_end ?? ''),
|
|
source: String(initial?.payload.source ?? ''),
|
|
reference_date: initial
|
|
? String(initial.payload.reference_date ?? '')
|
|
: props.today,
|
|
valid_until: String(initial?.payload.valid_until ?? ''),
|
|
});
|
|
useEffect(
|
|
() => onDirty?.(kind, form.isDirty),
|
|
[onDirty, kind, form.isDirty],
|
|
);
|
|
async function persist(submit = false) {
|
|
if (draftBusy) return;
|
|
setDraftBusy(true);
|
|
setDraftMessage('');
|
|
setDraftError('');
|
|
form.clearErrors();
|
|
const payload = { ...form.data };
|
|
try {
|
|
const url =
|
|
`/management/education/drafts/${kind}` +
|
|
(active ? `/${active.id}` : '') +
|
|
(submit ? '/submit' : '');
|
|
const saved = await portalRequest<EducationDraft>(url, {
|
|
method: submit || !active ? 'POST' : 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
version_no: active?.version_no ?? 0,
|
|
client_key: clientKey,
|
|
payload,
|
|
}),
|
|
});
|
|
setActive(saved);
|
|
form.setDefaults(payload);
|
|
onSaved?.(saved);
|
|
setDraftMessage(
|
|
submit
|
|
? 'Declaração submetida para revisão independente.'
|
|
: 'Rascunho privado guardado. Pode recuperá-lo na sua lista.',
|
|
);
|
|
} catch (failure) {
|
|
if (failure instanceof PortalApiError) {
|
|
form.setError(
|
|
Object.fromEntries(
|
|
Object.entries(failure.errors).map(
|
|
([key, messages]) => [
|
|
key,
|
|
messages[0] ?? failure.message,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
setDraftError(
|
|
failure.status === 409
|
|
? 'O rascunho foi alterado noutra sessão. As suas edições continuam neste formulário; abra a versão guardada antes de tentar novamente.'
|
|
: failure.message,
|
|
);
|
|
} else
|
|
setDraftError(
|
|
'Não foi possível guardar. As suas edições continuam neste formulário.',
|
|
);
|
|
} finally {
|
|
setDraftBusy(false);
|
|
}
|
|
}
|
|
if (active?.state === 'submitted')
|
|
return <EducationSubmittedSnapshot draft={active} />;
|
|
return (
|
|
<details className="rounded border p-4" open={initial !== null}>
|
|
<summary className="cursor-pointer font-semibold">
|
|
{demand
|
|
? 'Declarar procura de trabalhadores'
|
|
: 'Declarar vagas de formação'}
|
|
</summary>
|
|
<form
|
|
className="mt-4 grid gap-4 md:grid-cols-2"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if (active) {
|
|
void persist(true);
|
|
return;
|
|
}
|
|
form.post(`/management/education/${kind}`, {
|
|
onSuccess: () => {
|
|
form.reset();
|
|
setTerritoryResetKey((key) => key + 1);
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<fieldset
|
|
className="contents"
|
|
disabled={draftBusy || form.processing}
|
|
>
|
|
<Select
|
|
label={demand ? 'Projecto' : 'Curso'}
|
|
options={demand ? props.projects : props.courses}
|
|
value={
|
|
demand ? form.data.project_id : form.data.course_id
|
|
}
|
|
onChange={(v) =>
|
|
form.setData(demand ? 'project_id' : 'course_id', v)
|
|
}
|
|
/>
|
|
<Select
|
|
label="Ocupação"
|
|
options={props.occupations}
|
|
value={form.data.occupation_id}
|
|
onChange={(v) => form.setData('occupation_id', v)}
|
|
/>
|
|
<Select
|
|
label="Competência principal"
|
|
options={props.skills}
|
|
value={form.data.skill_id}
|
|
onChange={(v) => form.setData('skill_id', v)}
|
|
/>
|
|
<div className="md:col-span-2">
|
|
{initial?.payload.geo_location_id &&
|
|
form.data.geo_location_id ===
|
|
initial.payload.geo_location_id &&
|
|
initialPath.length === 0 && (
|
|
<p>
|
|
O local guardado já não consta do cadastro
|
|
disponível. A referência foi conservada;
|
|
escolha uma localização actual antes de
|
|
submeter.
|
|
</p>
|
|
)}
|
|
<TerritoryFields
|
|
key={territoryResetKey}
|
|
initial={initialPath}
|
|
disabled={draftBusy || form.processing}
|
|
endpoint="/management/territory-options"
|
|
onChange={(path) =>
|
|
form.setData(
|
|
'geo_location_id',
|
|
path[path.length - 1]?.id ?? '',
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
{demand && (
|
|
<Field label="Empregador declarante">
|
|
<input
|
|
className={control}
|
|
required
|
|
value={form.data.employer}
|
|
onChange={(e) =>
|
|
form.setData('employer', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
)}
|
|
<Field label="Qualificação exigida / desenvolvida">
|
|
<input
|
|
className={control}
|
|
required
|
|
value={form.data.qualification}
|
|
onChange={(e) =>
|
|
form.setData('qualification', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
<Field
|
|
label={
|
|
demand
|
|
? 'Trabalhadores necessários'
|
|
: 'Vagas disponíveis (não trabalhadores)'
|
|
}
|
|
>
|
|
<input
|
|
className={control}
|
|
type="number"
|
|
min="1"
|
|
required
|
|
value={form.data.quantity}
|
|
onChange={(e) =>
|
|
form.setData('quantity', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
{(
|
|
[
|
|
'period_start',
|
|
'period_end',
|
|
'reference_date',
|
|
'valid_until',
|
|
] as const
|
|
).map((key) => (
|
|
<Field
|
|
key={key}
|
|
label={
|
|
{
|
|
period_start: demand
|
|
? 'Início da necessidade'
|
|
: 'Início da formação',
|
|
period_end: demand
|
|
? 'Fim da necessidade'
|
|
: 'Conclusão prevista',
|
|
reference_date:
|
|
'Data de referência da fonte',
|
|
valid_until: 'Fonte válida até',
|
|
}[key]
|
|
}
|
|
>
|
|
<input
|
|
className={control}
|
|
type="date"
|
|
required
|
|
value={form.data[key]}
|
|
onChange={(e) =>
|
|
form.setData(key, e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
))}
|
|
<Field label="Fonte, referência da evidência e responsável pelos dados">
|
|
<textarea
|
|
className={control}
|
|
required
|
|
minLength={10}
|
|
value={form.data.source}
|
|
onChange={(e) =>
|
|
form.setData('source', e.target.value)
|
|
}
|
|
/>
|
|
</Field>
|
|
<div>
|
|
<Errors errors={form.errors} />
|
|
{draftError && <p role="alert">{draftError}</p>}
|
|
{draftMessage && <p role="status">{draftMessage}</p>}
|
|
{props.draftsAvailable && (
|
|
<>
|
|
<p className="text-sm">
|
|
Guardar rascunho conserva informação
|
|
incompleta apenas na sua conta; ainda não
|
|
declara trabalhadores ou vagas para análise.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
className={button}
|
|
disabled={draftBusy || form.processing}
|
|
onClick={() => void persist()}
|
|
>
|
|
Guardar rascunho
|
|
</button>
|
|
{active && (
|
|
<p className="text-sm">
|
|
Rascunho guardado na versão{' '}
|
|
{active.version_no}. A submissão usa os
|
|
campos actuais deste formulário.
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
<button className={button} disabled={form.processing}>
|
|
Submeter para revisão
|
|
</button>
|
|
</div>
|
|
</fieldset>
|
|
</form>
|
|
</details>
|
|
);
|
|
}
|
|
function Review({
|
|
row,
|
|
kind,
|
|
today,
|
|
}: {
|
|
row: RecordRow;
|
|
kind: string;
|
|
today: string;
|
|
}) {
|
|
return (
|
|
<article className="space-y-3 rounded border p-4">
|
|
<p className="font-semibold">
|
|
{row.qualification} · {row.quantity}{' '}
|
|
{kind === 'demands' ? 'trabalhadores' : 'vagas formativas'}
|
|
</p>
|
|
<p>
|
|
{row.period_start} a {row.period_end} ·{' '}
|
|
{row.valid_until < today
|
|
? 'Fonte expirada'
|
|
: (
|
|
{
|
|
submitted: 'Submetida',
|
|
reviewed: 'Revista internamente',
|
|
rejected: 'Rejeitada',
|
|
} as Record<string, string>
|
|
)[row.status]}
|
|
</p>
|
|
<p className="break-words">Fonte: {row.source}</p>
|
|
{row.review_unavailable === 'self' && (
|
|
<p>Submetida por si; a revisão cabe a outro gestor.</p>
|
|
)}
|
|
{row.details_url && (
|
|
<Link className="underline" href={row.details_url}>
|
|
{row.can_review
|
|
? 'Consultar e rever'
|
|
: 'Consultar declaração'}
|
|
</Link>
|
|
)}
|
|
</article>
|
|
);
|
|
}
|
|
export default function Index(props: Props) {
|
|
const [savedHere, setSavedHere] = useState<EducationDraftSummary[]>([]);
|
|
const [dirtyForms, setDirtyForms] = useState({
|
|
demands: false,
|
|
capacities: false,
|
|
});
|
|
const markDirty = useCallback(
|
|
(kind: 'demands' | 'capacities', dirty: boolean) => {
|
|
setDirtyForms((current) =>
|
|
current[kind] === dirty
|
|
? current
|
|
: { ...current, [kind]: dirty },
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
const saved = useCallback((draft: EducationDraft) => {
|
|
setSavedHere((current) => [
|
|
{
|
|
id: draft.id,
|
|
kind: draft.kind,
|
|
version_no: draft.version_no,
|
|
state: draft.state,
|
|
updated_at: draft.updated_at,
|
|
label: String(draft.payload.qualification ?? ''),
|
|
},
|
|
...current.filter((item) => item.id !== draft.id),
|
|
]);
|
|
if (draft.state === 'submitted')
|
|
router.reload({
|
|
only: [
|
|
'demands',
|
|
'capacities',
|
|
'indicators',
|
|
'ownDrafts',
|
|
'workQueue',
|
|
],
|
|
});
|
|
}, []);
|
|
const unsaved = dirtyForms.demands || dirtyForms.capacities;
|
|
useEffect(() => {
|
|
const warn = (event: BeforeUnloadEvent) => {
|
|
if (unsaved) {
|
|
event.preventDefault();
|
|
event.returnValue = '';
|
|
}
|
|
};
|
|
window.addEventListener('beforeunload', warn);
|
|
const unsubscribe = router.on('before', (event) => {
|
|
if (
|
|
unsaved &&
|
|
event.detail.visit.method === 'get' &&
|
|
event.detail.visit.preserveState !== true &&
|
|
!window.confirm(
|
|
'Há alterações por guardar nos formulários de Educação. Pretende sair sem as guardar?',
|
|
)
|
|
)
|
|
event.preventDefault();
|
|
});
|
|
return () => {
|
|
window.removeEventListener('beforeunload', warn);
|
|
unsubscribe();
|
|
};
|
|
}, [unsaved]);
|
|
const ownDrafts = [
|
|
...savedHere,
|
|
...(props.ownDrafts?.data ?? []).filter(
|
|
(item) => !savedHere.some((saved) => saved.id === item.id),
|
|
),
|
|
];
|
|
const search = useForm({
|
|
scope: props.filters.scope ?? 'all',
|
|
q: props.filters.q ?? '',
|
|
status: props.filters.status ?? '',
|
|
geo_location_id: props.filters.geo_location_id ?? '',
|
|
});
|
|
return (
|
|
<Management title="Competências e investimento em educação">
|
|
<p>
|
|
Declarações internas de procura e oferta. A revisão não
|
|
constitui acreditação, certificação ou validação institucional.
|
|
Não introduza dados pessoais de formandos.
|
|
</p>
|
|
<section
|
|
aria-label="Indicadores internos"
|
|
className="grid gap-4 md:grid-cols-4"
|
|
>
|
|
<p>
|
|
Declarações de procura:{' '}
|
|
<strong>{props.indicators.demand_records}</strong>
|
|
</p>
|
|
<p>
|
|
Declarações de oferta:{' '}
|
|
<strong>{props.indicators.capacity_records}</strong>
|
|
</p>
|
|
<p>
|
|
Procuras a rever:{' '}
|
|
<strong>{props.indicators.pending_demands}</strong>
|
|
</p>
|
|
<p>
|
|
Procuras com fonte expirada:{' '}
|
|
<strong>{props.indicators.expired_demands}</strong>
|
|
</p>
|
|
</section>
|
|
<p>
|
|
Indicadores globais de registos internos, sem aplicação dos
|
|
filtros. Não representam totais de trabalhadores, lacunas
|
|
validadas ou resultados educativos.
|
|
</p>
|
|
{props.workQueue && (
|
|
<section
|
|
aria-label="O meu acompanhamento em Educação"
|
|
className="space-y-3 rounded border p-4"
|
|
>
|
|
<h2 className="text-lg font-semibold">
|
|
O meu acompanhamento
|
|
</h2>
|
|
<p>
|
|
Contagens das minhas declarações, em todos os filtros.
|
|
</p>
|
|
{props.workQueue.drafts !== null && (
|
|
<p>
|
|
Rascunhos privados por concluir:{' '}
|
|
{props.workQueue.drafts}
|
|
</p>
|
|
)}
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{(['demands', 'capacities'] as const).map((kind) => {
|
|
const counts = props.workQueue?.[kind];
|
|
if (!counts) return null;
|
|
return (
|
|
<section key={kind} className="space-y-2">
|
|
<h3 className="font-semibold">
|
|
{kind === 'demands'
|
|
? 'Declarações de procura'
|
|
: 'Declarações de capacidade formativa'}
|
|
</h3>
|
|
<p>Submetidas por mim: {counts.total}</p>
|
|
<p>
|
|
Por rever, com fonte vigente:{' '}
|
|
{counts.submitted}
|
|
</p>
|
|
<p>
|
|
Revistas: {counts.reviewed} ·
|
|
Rejeitadas: {counts.rejected} · Fonte
|
|
expirada: {counts.expired}
|
|
</p>
|
|
<p>
|
|
Disponíveis para a minha revisão
|
|
independente: {counts.to_review}
|
|
</p>
|
|
</section>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
)}
|
|
<form
|
|
className="grid gap-4 md:grid-cols-4"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
search.get('/management/education', {
|
|
preserveState: true,
|
|
});
|
|
}}
|
|
>
|
|
<Field label="Âmbito das declarações">
|
|
<select
|
|
className={control}
|
|
value={search.data.scope}
|
|
onChange={(event) =>
|
|
search.setData({
|
|
...search.data,
|
|
scope: event.target.value as
|
|
| 'all'
|
|
| 'mine'
|
|
| 'review',
|
|
status:
|
|
event.target.value === 'review'
|
|
? ''
|
|
: search.data.status,
|
|
})
|
|
}
|
|
>
|
|
<option value="all">Todas as declarações</option>
|
|
<option value="mine">Submetidas por mim</option>
|
|
<option value="review">
|
|
Disponíveis para revisão independente
|
|
</option>
|
|
</select>
|
|
</Field>
|
|
<Field label="Pesquisar qualificação">
|
|
<input
|
|
className={control}
|
|
maxLength={150}
|
|
value={search.data.q}
|
|
onChange={(e) => search.setData('q', e.target.value)}
|
|
/>
|
|
</Field>
|
|
{search.data.scope !== 'review' && (
|
|
<Field label="Estado">
|
|
<select
|
|
className={control}
|
|
value={search.data.status}
|
|
onChange={(e) =>
|
|
search.setData('status', e.target.value)
|
|
}
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="submitted">
|
|
Por rever, fonte vigente
|
|
</option>
|
|
<option value="reviewed">
|
|
Revisto, fonte vigente
|
|
</option>
|
|
<option value="rejected">
|
|
Rejeitado, fonte vigente
|
|
</option>
|
|
<option value="expired">Fonte expirada</option>
|
|
</select>
|
|
</Field>
|
|
)}
|
|
<div className="md:col-span-2">
|
|
<TerritoryFields
|
|
initial={territoryPath(
|
|
search.data.geo_location_id,
|
|
props.territories,
|
|
)}
|
|
endpoint="/management/territory-options"
|
|
onChange={(path) =>
|
|
search.setData(
|
|
'geo_location_id',
|
|
path[path.length - 1]?.id ?? '',
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<button className={button} disabled={search.processing}>
|
|
Filtrar
|
|
</button>
|
|
<Link
|
|
className="ml-3 underline"
|
|
href="/management/education"
|
|
>
|
|
Limpar
|
|
</Link>
|
|
<Errors errors={search.errors} />
|
|
</div>
|
|
</form>
|
|
<section className="grid gap-3 md:grid-cols-2">
|
|
{[
|
|
['skills', 'Competências'],
|
|
['occupations', 'Ocupações'],
|
|
['providers', 'Instituições'],
|
|
['courses', 'Cursos'],
|
|
].map(([kind, title]) => (
|
|
<Catalog
|
|
key={kind}
|
|
kind={kind}
|
|
title={title}
|
|
providers={props.providers}
|
|
/>
|
|
))}
|
|
</section>
|
|
{props.draftsAvailable && (
|
|
<section className="space-y-3 rounded border p-4">
|
|
<h2 className="text-xl font-semibold">
|
|
Os meus rascunhos e submissões
|
|
</h2>
|
|
<p>
|
|
Estes registos pertencem apenas à sua conta. Rascunhos
|
|
incompletos não entram nos indicadores de trabalhadores
|
|
ou vagas.
|
|
</p>
|
|
<Link className="underline" href="/management/education">
|
|
Iniciar outra declaração
|
|
</Link>
|
|
{ownDrafts.length === 0 && (
|
|
<p>Ainda não guardou rascunhos.</p>
|
|
)}
|
|
<ul>
|
|
{ownDrafts.map((item) => (
|
|
<li key={item.id}>
|
|
<Link
|
|
className="underline"
|
|
href={`/management/education?draft=${encodeURIComponent(item.id)}`}
|
|
preserveState={false}
|
|
>
|
|
{item.kind === 'demands'
|
|
? 'Procura de trabalhadores'
|
|
: 'Capacidade formativa'}{' '}
|
|
—{' '}
|
|
{item.state === 'submitted'
|
|
? 'submetida'
|
|
: 'rascunho'}
|
|
, versão {item.version_no}
|
|
{item.label
|
|
? ` — ${item.label}`
|
|
: ''} —{' '}
|
|
{new Date(item.updated_at).toLocaleString(
|
|
'pt-MZ',
|
|
)}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{props.ownDrafts && <Pagination page={props.ownDrafts} />}
|
|
</section>
|
|
)}
|
|
<Intake
|
|
key={`demands:${props.selectedDraft?.kind === 'demands' ? props.selectedDraft.id : 'new'}`}
|
|
kind="demands"
|
|
props={props}
|
|
onSaved={saved}
|
|
onDirty={markDirty}
|
|
/>
|
|
<Intake
|
|
key={`capacities:${props.selectedDraft?.kind === 'capacities' ? props.selectedDraft.id : 'new'}`}
|
|
kind="capacities"
|
|
props={props}
|
|
onSaved={saved}
|
|
onDirty={markDirty}
|
|
/>
|
|
{(['demands', 'capacities'] as const).map((kind) => (
|
|
<section key={kind} className="space-y-3">
|
|
<h2 className="text-xl font-semibold">
|
|
{kind === 'demands'
|
|
? 'Procura de mão-de-obra'
|
|
: 'Oferta formativa declarada'}
|
|
</h2>
|
|
{props[kind].data.map((row) => (
|
|
<Review
|
|
key={row.id}
|
|
row={row}
|
|
kind={kind}
|
|
today={props.today}
|
|
/>
|
|
))}
|
|
{!props[kind].data.length && <p>Ainda sem declarações.</p>}
|
|
<Pagination page={props[kind]} />
|
|
</section>
|
|
))}
|
|
</Management>
|
|
);
|
|
}
|