324 lines
16 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|