Files
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

564 lines
27 KiB
TypeScript

import { useState } from 'react';
import { CandidateLocationPicker } from '@/components/candidate-location-picker';
import type {
GeographyReference,
GeographyDescription,
} from '@/components/administrative-location';
import { Head, Link, router, useForm } from '@inertiajs/react';
import { FormErrors, controlClass } from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Data = {
name: string;
phone: string | null;
summary: string;
availability: { province: string; district: string; locality: string };
availability_reference?: GeographyReference | null;
additional_availability?: Data['availability'][];
additional_availability_references?: (GeographyReference | null)[];
};
export default function Profile({
companyId,
geographyEnabled = false,
availabilityGeography = null,
email,
profile,
emailVerified = false,
emailDeliveryAvailable = false,
accessStatus,
opportunities = [],
opportunityPagination,
selectedOpportunity,
submissions = [],
submissionPagination,
}: {
companyId: number;
geographyEnabled?: boolean;
availabilityGeography?: GeographyDescription;
email: string;
profile: { version: number; data: Data };
emailVerified?: boolean;
emailDeliveryAvailable?: boolean;
accessStatus?: string | null;
opportunities?: { id: number; title: string; closes_on: string }[];
opportunityPagination?: { prev: string | null; next: string | null };
selectedOpportunity?: { id: number; title: string } | null;
submissions?: {
id: number;
reference: string;
created_at: string;
title?: string | null;
}[];
submissionPagination?: {
prev: string | null;
next: string | null;
total: number;
};
}) {
const form = useForm({
version: profile.version,
...profile.data,
availability_reference: profile.data.availability_reference ?? null,
phone: profile.data.phone ?? '',
additional_availability: profile.data.additional_availability ?? [],
additional_availability_references:
profile.data.additional_availability_references ??
(profile.data.additional_availability ?? []).map(
() => null as GeographyReference | null,
),
reason: '',
});
const [locationKeys, setLocationKeys] = useState(() =>
(profile.data.additional_availability ?? []).map(() =>
crypto.randomUUID(),
),
);
return (
<>
<Head title="O meu perfil" />
<section className="mx-auto max-w-3xl space-y-3 p-6">
{selectedOpportunity && (
<aside className="space-y-2 rounded-lg border p-4">
<h2 className="font-semibold">
Oportunidade escolhida: {selectedOpportunity.title}
</h2>
<p>
Prepare o perfil e confirme o email. A candidatura
só será enviada quando confirmar a submissão no
formulário.
</p>
<Link
className="underline"
href={`/candidaturas/${companyId}/oportunidades/${selectedOpportunity.id}`}
>
Continuar a preparar esta candidatura
</Link>
</aside>
)}
<h2 className="text-xl font-semibold">
Oportunidades publicadas
</h2>
{opportunities.map((item) => (
<p key={item.id}>
<Link
className="underline"
href={`/candidaturas/${companyId}/oportunidades/${item.id}`}
>
{item.title} · {item.closes_on}
</Link>
</p>
))}
<nav
aria-label="Páginas de oportunidades"
className="flex gap-4"
>
{opportunityPagination?.prev && (
<Link href={opportunityPagination.prev} preserveScroll>
Anterior
</Link>
)}
{opportunityPagination?.next && (
<Link href={opportunityPagination.next} preserveScroll>
Seguinte
</Link>
)}
</nav>
<h2 className="text-xl font-semibold">
As minhas candidaturas
</h2>
{submissions.map((item) => (
<p key={item.id}>
<Link
className="underline"
href={`/candidaturas/${companyId}/submissoes/${item.id}`}
>
{item.title ? `${item.title} · ` : ''}
{item.reference} · {item.created_at}
</Link>
</p>
))}
{submissionPagination && (
<p>
{submissionPagination.total} candidatura(s) entregue(s).
</p>
)}
<nav
aria-label="Páginas dos meus comprovativos"
className="flex gap-4"
>
{submissionPagination?.prev && (
<Link href={submissionPagination.prev} preserveScroll>
Comprovativos anteriores
</Link>
)}
{submissionPagination?.next && (
<Link href={submissionPagination.next} preserveScroll>
Mais comprovativos
</Link>
)}
</nav>
</section>
<main className="mx-auto max-w-3xl space-y-6 p-6">
<p>SGC · Área do candidato</p>
<h1 className="text-3xl font-semibold">O meu perfil</h1>
<p>
{email} · versão {profile.version}
</p>
<p>
{emailVerified
? 'Email confirmado. A identidade ainda depende de verificação própria.'
: 'Email ainda não confirmado. A entrada comprova apenas o acesso à conta.'}{' '}
Pode candidatar-se às oportunidades publicadas. O envio de
documentos segue os requisitos de cada oportunidade.
</p>
{!emailVerified && (
<Button
variant="outline"
disabled={!emailDeliveryAvailable}
onClick={() =>
router.post(
`/candidaturas/${companyId}/pedir-confirmacao`,
)
}
>
Pedir confirmação do email
</Button>
)}
{!emailVerified && !emailDeliveryAvailable && (
<p>
O envio de email ainda não está configurado; pode
continuar a preparar o perfil.
</p>
)}
{accessStatus === 'requested' && (
<p>
Se o envio estiver disponível, receberá uma ligação
temporária de confirmação.
</p>
)}
{accessStatus === 'opportunity_unavailable' && (
<p role="status">
Entrou na sua conta, mas a oportunidade escolhida já não
está disponível. Consulte as oportunidades publicadas
acima.
</p>
)}
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
form.transform((data) => {
const {
availability_reference,
additional_availability_references,
...rest
} = data;
return geographyEnabled
? {
...rest,
availability_reference,
additional_availability_references,
}
: rest;
});
form.post(`/candidaturas/${companyId}/perfil`, {
preserveScroll: true,
preserveState: false,
});
}}
>
<FormErrors errors={form.errors} />
<label className="grid gap-2">
Nome completo
<input
className={controlClass}
required
minLength={3}
maxLength={200}
value={form.data.name}
onChange={(e) =>
form.setData('name', e.target.value)
}
/>
</label>
<label className="grid gap-2">
Telefone
<input
className={controlClass}
maxLength={50}
value={form.data.phone}
onChange={(e) =>
form.setData('phone', e.target.value)
}
/>
</label>
<label className="grid gap-2">
Resumo profissional
<textarea
className={controlClass}
required
minLength={10}
maxLength={5000}
value={form.data.summary}
onChange={(e) =>
form.setData('summary', e.target.value)
}
/>
</label>
<fieldset className="grid gap-3 sm:grid-cols-3">
<legend>
Local de disponibilidade para trabalhar ou prestar
serviço
</legend>
{geographyEnabled &&
!form.data.availability_reference && (
<CandidateLocationPicker
companyId={companyId}
apply={(reference, location) => {
form.setData('availability', location);
form.setData(
'availability_reference',
reference,
);
}}
/>
)}
{geographyEnabled &&
form.data.availability_reference && (
<div className="space-y-2 rounded-lg border p-3 text-sm">
<p>
Disponibilidade principal associada ao
catálogo.
</p>
{availabilityGeography?.requires_review && (
<p>
A edição da referência precisa de
revisão. As designações registadas
foram preservadas.
</p>
)}
<Button
type="button"
variant="outline"
onClick={() =>
form.setData(
'availability_reference',
null,
)
}
>
Alterar selecção ou preencher
manualmente
</Button>
</div>
)}
{(['province', 'district', 'locality'] as const).map(
(key) => (
<label className="grid gap-2" key={key}>
{
{
province: 'Província',
district: 'Distrito',
locality: 'Localidade',
}[key]
}
<input
className={controlClass}
required
maxLength={150}
value={form.data.availability[key]}
readOnly={
geographyEnabled &&
!!form.data.availability_reference
}
onChange={(e) =>
form.setData('availability', {
...form.data.availability,
[key]: e.target.value,
})
}
/>
</label>
),
)}
</fieldset>
{form.data.additional_availability.map(
(location, index) => (
<fieldset
key={locationKeys[index]}
className="grid gap-3 rounded-lg border p-4 sm:grid-cols-3"
>
<legend>
Outro local de disponibilidade {index + 1}
</legend>
{geographyEnabled &&
!form.data
.additional_availability_references[
index
] && (
<div className="sm:col-span-3">
<CandidateLocationPicker
companyId={companyId}
label="Escolher este local no catálogo"
apply={(
reference,
selected,
) => {
form.setData((data) => ({
...data,
additional_availability:
data.additional_availability.map(
(
item,
position,
) =>
position ===
index
? selected
: item,
),
additional_availability_references:
data.additional_availability_references.map(
(
item,
position,
) =>
position ===
index
? reference
: item,
),
}));
}}
/>
</div>
)}
{geographyEnabled &&
form.data
.additional_availability_references[
index
] && (
<div className="space-y-2 text-sm sm:col-span-3">
<p>Local associado ao catálogo.</p>
<Button
type="button"
variant="outline"
onClick={() =>
form.setData(
'additional_availability_references',
form.data.additional_availability_references.map(
(item, position) =>
position ===
index
? null
: item,
),
)
}
>
Alterar selecção ou preencher
manualmente
</Button>
</div>
)}
{(
[
'province',
'district',
'locality',
] as const
).map((key) => (
<label className="grid gap-2" key={key}>
{
{
province: 'Província',
district: 'Distrito',
locality: 'Localidade',
}[key]
}
<input
className={controlClass}
required
maxLength={150}
value={location[key]}
readOnly={
geographyEnabled &&
!!form.data
.additional_availability_references[
index
]
}
onChange={(event) =>
form.setData(
'additional_availability',
form.data.additional_availability.map(
(item, position) =>
position === index
? {
...item,
[key]: event
.target
.value,
}
: item,
),
)
}
/>
</label>
))}
<Button
type="button"
variant="outline"
onClick={() => {
form.setData((data) => ({
...data,
additional_availability:
data.additional_availability.filter(
(_, position) =>
position !== index,
),
additional_availability_references:
data.additional_availability_references.filter(
(_, position) =>
position !== index,
),
}));
setLocationKeys((keys) =>
keys.filter(
(_, position) =>
position !== index,
),
);
}}
>
Retirar este local
</Button>
</fieldset>
),
)}
<Button
type="button"
variant="outline"
disabled={
form.data.additional_availability.length >= 20
}
onClick={() => {
form.setData((data) => ({
...data,
additional_availability: [
...data.additional_availability,
{
province: '',
district: '',
locality: '',
},
],
additional_availability_references: [
...data.additional_availability_references,
null,
],
}));
setLocationKeys((keys) => [
...keys,
crypto.randomUUID(),
]);
}}
>
Adicionar local de disponibilidade
</Button>
<p className="text-sm">
Disponibilidade é distinta da morada. Actualizar o
perfil cria uma versão para usos futuros, preservando as
anteriores.
</p>
<label className="grid gap-2">
Motivo da actualização
<textarea
className={controlClass}
required
minLength={10}
maxLength={4000}
value={form.data.reason}
onChange={(e) =>
form.setData('reason', e.target.value)
}
/>
</label>
<Button disabled={form.processing}>
Guardar nova versão do perfil
</Button>
</form>
<Button
variant="outline"
onClick={() =>
router.post(`/candidaturas/${companyId}/sair`)
}
>
Sair da área do candidato
</Button>
</main>
</>
);
}