Files
compras/resources/js/pages/candidate/opportunity.tsx
T
2026-10-30 20:52:10 +02:00

324 lines
16 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import { FormErrors, controlClass } from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Field = { key: string; label: string; type: string; required: boolean };
type Location = { province: string; district: string; locality: string };
const answerText = (value: string | Location) =>
typeof value === 'string' ? value : '';
export default function Opportunity({
companyId,
opportunity,
profile,
email,
emailVerified,
}: {
companyId: number;
opportunity: {
id: number;
title: string;
description: string;
closes_on: string;
publication_version: number;
form: {
version: number;
definition: {
fields: Field[];
documents: {
key: string;
label: string;
required: boolean;
formats: string[];
max_kb: number;
}[];
};
};
};
profile: {
version: number;
data: {
name: string;
phone?: string;
summary: string;
availability: Location;
additional_availability?: Location[];
};
};
email: string;
emailVerified: boolean;
}) {
const locations = [
profile.data.availability,
...(profile.data.additional_availability ?? []),
];
const source: Record<string, string | Location> = {
name: profile.data.name,
full_name: profile.data.name,
email,
phone: profile.data.phone ?? '',
summary: profile.data.summary,
availability_location: profile.data.availability,
};
const form = useForm({
operation_key: crypto.randomUUID(),
profile_version: profile.version,
publication_version: opportunity.publication_version,
answers: Object.fromEntries(
opportunity.form.definition.fields.map((field) => [
field.key,
field.type === 'location'
? typeof source[field.key] === 'object'
? source[field.key]
: { province: '', district: '', locality: '' }
: typeof source[field.key] === 'string'
? source[field.key]
: '',
]),
) as Record<string, string | Location>,
documents: {} as Record<string, File>,
});
return (
<>
<Head title={opportunity.title} />
<main className="mx-auto max-w-3xl space-y-5 p-6">
<Link
href={`/candidaturas/${companyId}/perfil`}
className="underline"
>
O meu perfil
</Link>
<h1 className="text-3xl font-semibold">{opportunity.title}</h1>
<p className="whitespace-pre-wrap">{opportunity.description}</p>
<p>
Fecho: {opportunity.closes_on} · formulário v
{opportunity.form.version} · perfil v{profile.version}
</p>
<p>
Dados existentes foram preenchidos a partir do perfil. As
respostas específicas não alteram o perfil. A recepção não
constitui aceitação, selecção ou contratação.
</p>
{!emailVerified && (
<p>Confirme o email na sua área antes de submeter.</p>
)}
{profile.version === 0 && <p>Guarde primeiro o seu perfil.</p>}
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
form.post(
`/candidaturas/${companyId}/oportunidades/${opportunity.id}`,
);
}}
>
<FormErrors errors={form.errors} />
{opportunity.form.definition.fields.map((field) =>
field.type === 'location' ? (
<fieldset
key={field.key}
className="grid gap-3 sm:grid-cols-3"
>
<legend>
{field.label}
{field.required ? ' *' : ''}
</legend>
{field.key === 'availability_location' &&
locations.length > 1 && (
<label className="grid gap-2 sm:col-span-3">
Usar local guardado no perfil
<select
className={controlClass}
value={locations.findIndex(
(location) => {
const answer =
form.data.answers[
field.key
];
return (
typeof answer ===
'object' &&
(
[
'province',
'district',
'locality',
] as const
).every(
(part) =>
answer[
part
] ===
location[
part
],
)
);
},
)}
onChange={(e) => {
const location =
locations[
Number(
e.target.value,
)
];
if (location)
form.setData(
'answers',
{
...form.data
.answers,
[field.key]: {
...location,
},
},
);
}}
>
<option value={-1}>
Local preenchido nesta
candidatura
</option>
{locations.map(
(location, index) => (
<option
value={index}
key={index}
>
{location.province}{' '}
·{' '}
{location.district}{' '}
·{' '}
{location.locality}
</option>
),
)}
</select>
</label>
)}
{(
[
'province',
'district',
'locality',
] as const
).map((part) => (
<label key={part}>
{
{
province: 'Província',
district: 'Distrito',
locality: 'Localidade',
}[part]
}
<input
className={controlClass}
required={field.required}
value={
(typeof form.data.answers[
field.key
] === 'object'
? (form.data.answers[
field.key
] as Location)
: {
province: '',
district: '',
locality: '',
})[part]
}
onChange={(e) =>
form.setData('answers', {
...form.data.answers,
[field.key]: {
...(form.data.answers[
field.key
] as Location),
[part]: e.target.value,
},
})
}
/>
</label>
))}
</fieldset>
) : (
<label className="grid gap-2" key={field.key}>
{field.label}
{field.required ? ' *' : ''}
{field.type === 'textarea' ? (
<textarea
className={controlClass}
required={field.required}
value={answerText(
form.data.answers[field.key],
)}
onChange={(e) =>
form.setData('answers', {
...form.data.answers,
[field.key]: e.target.value,
})
}
/>
) : (
<input
className={controlClass}
type={field.type}
required={field.required}
value={answerText(
form.data.answers[field.key],
)}
onChange={(e) =>
form.setData('answers', {
...form.data.answers,
[field.key]: e.target.value,
})
}
/>
)}
</label>
),
)}
{opportunity.form.definition.documents.map((document) => (
<label key={document.key} className="grid gap-2">
{document.label}
{document.required ? ' *' : ''}
<span className="text-sm">
{document.formats.join(', ')} · até{' '}
{document.max_kb} KB
</span>
<input
type="file"
className={controlClass}
required={document.required}
accept={document.formats
.map((format) => `.${format}`)
.join(',')}
onChange={(e) => {
const next = { ...form.data.documents };
const file = e.target.files?.[0];
if (file) next[document.key] = file;
else delete next[document.key];
form.setData('documents', next);
}}
/>
</label>
))}
{form.progress && (
<p>Envio dos documentos: {form.progress.percentage}%</p>
)}
<Button
disabled={
form.processing ||
!emailVerified ||
profile.version === 0
}
>
Submeter candidatura e obter comprovativo
</Button>
</form>
</main>
</>
);
}