Files
compras/resources/js/pages/candidate/profile.tsx
T
2026-10-30 21:25:19 +02:00

377 lines
16 KiB
TypeScript

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 };
additional_availability?: Data['availability'][];
};
export default function Profile({
companyId,
email,
profile,
emailVerified = false,
emailDeliveryAvailable = false,
accessStatus,
opportunities = [],
opportunityPagination,
selectedOpportunity,
submissions = [],
submissionPagination,
}: {
companyId: number;
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,
phone: profile.data.phone ?? '',
additional_availability: profile.data.additional_availability ?? [],
reason: '',
});
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.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>
{(['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]}
onChange={(e) =>
form.setData('availability', {
...form.data.availability,
[key]: e.target.value,
})
}
/>
</label>
),
)}
</fieldset>
{form.data.additional_availability.map(
(location, index) => (
<fieldset
key={index}
className="grid gap-3 sm:grid-cols-3"
>
<legend>
Outro local de disponibilidade {index + 1}
</legend>
{(
[
'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]}
onChange={(e) =>
form.setData(
'additional_availability',
form.data.additional_availability.map(
(item, position) =>
position === index
? {
...item,
[key]: e
.target
.value,
}
: item,
),
)
}
/>
</label>
))}
<Button
type="button"
variant="outline"
onClick={() =>
form.setData(
'additional_availability',
form.data.additional_availability.filter(
(_, position) =>
position !== index,
),
)
}
>
Retirar este local
</Button>
</fieldset>
),
)}
<Button
type="button"
variant="outline"
disabled={
form.data.additional_availability.length >= 20
}
onClick={() =>
form.setData('additional_availability', [
...form.data.additional_availability,
{ province: '', district: '', locality: '' },
])
}
>
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>
</>
);
}