430 lines
20 KiB
TypeScript
430 lines
20 KiB
TypeScript
import { useForm } from '@inertiajs/react';
|
|
import { modalityLabels } from '@/components/opportunity-search-filters';
|
|
import { button, card, input } from '@/pages/investment/shared';
|
|
|
|
export type AlertPreferences = {
|
|
version: number;
|
|
enabled: boolean;
|
|
frequency: string;
|
|
channel: string;
|
|
next_run_at: string | null;
|
|
criteria_available?: boolean;
|
|
sources?: string[];
|
|
criteria: {
|
|
sector?: string;
|
|
province_id?: string;
|
|
category?: string;
|
|
tender_regime?: string;
|
|
funding_certainty?: string;
|
|
funding_state?: string;
|
|
};
|
|
};
|
|
export type AlertOptions = {
|
|
frequencies: Record<string, string>;
|
|
sectors: { slug: string; name: string }[];
|
|
provinces: { id: string; name: string }[];
|
|
categories: string[];
|
|
tendersAvailable?: boolean;
|
|
fundingAvailable?: boolean;
|
|
tenderRegimes?: Record<string, string>;
|
|
tenderActs?: Record<string, string>;
|
|
};
|
|
|
|
export default function OpportunityAlertPreferences({
|
|
preferences,
|
|
options,
|
|
}: {
|
|
preferences: AlertPreferences;
|
|
options: AlertOptions;
|
|
}) {
|
|
const form = useForm({
|
|
version: preferences.version,
|
|
enabled: true,
|
|
frequency: preferences.frequency,
|
|
channel: 'in_app',
|
|
sources: preferences.sources ?? ['opportunities'],
|
|
criteria: {
|
|
sector: preferences.criteria.sector ?? '',
|
|
province_id: preferences.criteria.province_id ?? '',
|
|
category: preferences.criteria.category ?? '',
|
|
tender_regime: preferences.criteria.tender_regime ?? '',
|
|
funding_certainty:
|
|
preferences.criteria.funding_certainty ?? 'documented',
|
|
funding_state: preferences.criteria.funding_state ?? '',
|
|
},
|
|
consent: false,
|
|
});
|
|
const send = (enabled: boolean) => {
|
|
form.transform((data) => ({
|
|
...data,
|
|
enabled,
|
|
consent: enabled && data.consent,
|
|
}));
|
|
form.put('/investidor/alert-preferences', { preserveScroll: true });
|
|
};
|
|
return (
|
|
<section
|
|
className={card + ' space-y-3'}
|
|
aria-label="Preferências de alertas"
|
|
>
|
|
<h2 className="text-xl font-bold">
|
|
Novidades dos catálogos públicos
|
|
</h2>
|
|
<p role="status">
|
|
{preferences.enabled
|
|
? 'Acompanhamento activo.'
|
|
: 'Acompanhamento desactivado.'}
|
|
</p>
|
|
{preferences.enabled &&
|
|
preferences.criteria_available === false && (
|
|
<p role="alert">
|
|
Uma selecção anterior deixou de estar disponível no
|
|
catálogo. O acompanhamento das selecções indisponíveis
|
|
fica suspenso; os restantes catálogos seleccionados
|
|
continuam a ser verificados. Pode escolher outra opção
|
|
ou cancelar os alertas a qualquer momento.
|
|
</p>
|
|
)}
|
|
<p>
|
|
Escolha os catálogos, critérios e frequência. As novas
|
|
disponibilizações compatíveis aparecem nesta área. Não são
|
|
enviados emails ou SMS. O acompanhamento de concursos considera
|
|
anúncios e actos publicados no catálogo do portal. Fundos e
|
|
apoios consideram novas versões do catálogo do portal; não há
|
|
recolha automática de fontes externas.
|
|
</p>
|
|
<p>
|
|
Ao activar, reactivar ou alterar os critérios ou catálogos, os
|
|
registos já disponíveis estabelecem o ponto de partida e não
|
|
geram alertas retroactivos. Uma oportunidade ou versão de acto
|
|
não gera alertas repetidos.
|
|
</p>
|
|
<form
|
|
className="space-y-3"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
send(true);
|
|
}}
|
|
>
|
|
<fieldset disabled={form.processing} className="space-y-3">
|
|
<legend className="font-semibold">
|
|
O que pretende acompanhar
|
|
</legend>
|
|
<fieldset className="space-y-2">
|
|
<legend className="font-semibold">Catálogos</legend>
|
|
{(['opportunities', 'tenders', 'funding'] as const).map(
|
|
(source) => (
|
|
<label
|
|
key={source}
|
|
className="flex items-start gap-2"
|
|
>
|
|
<input
|
|
name={`source-${source}`}
|
|
type="checkbox"
|
|
checked={form.data.sources.includes(
|
|
source,
|
|
)}
|
|
disabled={
|
|
((source === 'tenders' &&
|
|
!options.tendersAvailable) ||
|
|
(source === 'funding' &&
|
|
!options.fundingAvailable)) &&
|
|
!form.data.sources.includes(source)
|
|
}
|
|
onChange={(event) => {
|
|
form.setData(
|
|
'sources',
|
|
event.target.checked
|
|
? [
|
|
...form.data.sources,
|
|
source,
|
|
]
|
|
: form.data.sources.filter(
|
|
(value) =>
|
|
value !== source,
|
|
),
|
|
);
|
|
form.setData('consent', false);
|
|
}}
|
|
/>
|
|
{source === 'opportunities'
|
|
? 'Oportunidades de investimento'
|
|
: source === 'tenders'
|
|
? 'Concursos e actos publicados'
|
|
: 'Fundos, programas e incentivos publicados'}
|
|
</label>
|
|
),
|
|
)}
|
|
{!options.tendersAvailable && (
|
|
<p className="text-sm">
|
|
O catálogo de concursos não está disponível
|
|
neste momento.
|
|
</p>
|
|
)}
|
|
{!options.fundingAvailable && (
|
|
<p className="text-sm">
|
|
O catálogo de fundos não está disponível neste
|
|
momento.
|
|
</p>
|
|
)}
|
|
</fieldset>
|
|
<label className="block">
|
|
Sector
|
|
<select
|
|
className={input}
|
|
value={form.data.criteria.sector}
|
|
onChange={(event) =>
|
|
form.setData('criteria', {
|
|
...form.data.criteria,
|
|
sector: event.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">Todos os sectores</option>
|
|
{form.data.criteria.sector &&
|
|
!options.sectors.some(
|
|
(sector) =>
|
|
sector.slug ===
|
|
form.data.criteria.sector,
|
|
) && (
|
|
<option value={form.data.criteria.sector}>
|
|
Sector anterior indisponível
|
|
</option>
|
|
)}
|
|
{options.sectors.map((sector) => (
|
|
<option key={sector.slug} value={sector.slug}>
|
|
{sector.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="block">
|
|
Província
|
|
<select
|
|
className={input}
|
|
value={form.data.criteria.province_id}
|
|
onChange={(event) =>
|
|
form.setData('criteria', {
|
|
...form.data.criteria,
|
|
province_id: event.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">Todo o território</option>
|
|
{form.data.criteria.province_id &&
|
|
!options.provinces.some(
|
|
(province) =>
|
|
province.id ===
|
|
form.data.criteria.province_id,
|
|
) && (
|
|
<option
|
|
value={form.data.criteria.province_id}
|
|
>
|
|
Província anterior indisponível
|
|
</option>
|
|
)}
|
|
{options.provinces.map((province) => (
|
|
<option key={province.id} value={province.id}>
|
|
{province.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<p className="text-sm">
|
|
A selecção provincial usa o mesmo âmbito territorial do
|
|
catálogo, incluindo locais dos níveis inferiores.
|
|
</p>
|
|
{form.data.sources.includes('opportunities') && (
|
|
<label className="block">
|
|
Modalidade
|
|
<select
|
|
className={input}
|
|
value={form.data.criteria.category}
|
|
onChange={(event) =>
|
|
form.setData('criteria', {
|
|
...form.data.criteria,
|
|
category: event.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">Todas as modalidades</option>
|
|
{form.data.criteria.category &&
|
|
!options.categories.includes(
|
|
form.data.criteria.category,
|
|
) && (
|
|
<option
|
|
value={form.data.criteria.category}
|
|
>
|
|
Modalidade anterior indisponível
|
|
</option>
|
|
)}
|
|
{options.categories.map((category) => (
|
|
<option key={category} value={category}>
|
|
{modalityLabels[category] ?? category}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
{form.data.sources.includes('tenders') && (
|
|
<label className="block">
|
|
Regime do concurso
|
|
<select
|
|
className={input}
|
|
value={form.data.criteria.tender_regime}
|
|
onChange={(event) =>
|
|
form.setData('criteria', {
|
|
...form.data.criteria,
|
|
tender_regime: event.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">Todos os regimes</option>
|
|
{form.data.criteria.tender_regime &&
|
|
!Object.hasOwn(
|
|
options.tenderRegimes ?? {},
|
|
form.data.criteria.tender_regime,
|
|
) && (
|
|
<option
|
|
value={
|
|
form.data.criteria.tender_regime
|
|
}
|
|
>
|
|
Regime anterior indisponível
|
|
</option>
|
|
)}
|
|
{Object.entries(
|
|
options.tenderRegimes ?? {},
|
|
).map(([key, label]) => (
|
|
<option key={key} value={key}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
{form.data.sources.includes('funding') && (
|
|
<fieldset className="space-y-3">
|
|
<legend>Condições para fundos e apoios</legend>
|
|
<label className="block">
|
|
Âmbitos por confirmar
|
|
<select
|
|
className={input}
|
|
value={form.data.criteria.funding_certainty}
|
|
onChange={(event) =>
|
|
form.setData('criteria', {
|
|
...form.data.criteria,
|
|
funding_certainty:
|
|
event.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="documented">
|
|
Só âmbito documentado para o sector e
|
|
província escolhidos
|
|
</option>
|
|
<option value="include_unknown">
|
|
Incluir e assinalar informação por
|
|
confirmar
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label className="block">
|
|
Estado de uma janela
|
|
<select
|
|
className={input}
|
|
value={form.data.criteria.funding_state}
|
|
onChange={(event) =>
|
|
form.setData('criteria', {
|
|
...form.data.criteria,
|
|
funding_state: event.target.value,
|
|
})
|
|
}
|
|
>
|
|
<option value="">Qualquer estado</option>
|
|
<option value="open">
|
|
Aberta segundo as datas documentadas
|
|
</option>
|
|
<option value="upcoming">
|
|
Abertura futura
|
|
</option>
|
|
<option value="closed">Encerrada</option>
|
|
<option value="unknown">
|
|
Por confirmar
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<p className="text-sm">
|
|
Sector e província referem-se ao programa. Uma
|
|
chamada pode ter cobertura mais restrita. O
|
|
aviso acompanha novas versões publicadas; uma
|
|
mudança de data sem nova publicação não gera
|
|
outro aviso.
|
|
</p>
|
|
</fieldset>
|
|
)}
|
|
<label className="block">
|
|
Frequência
|
|
<select
|
|
className={input}
|
|
value={form.data.frequency}
|
|
onChange={(event) =>
|
|
form.setData('frequency', event.target.value)
|
|
}
|
|
>
|
|
{Object.entries(options.frequencies).map(
|
|
([key, label]) => (
|
|
<option key={key} value={key}>
|
|
{label}
|
|
</option>
|
|
),
|
|
)}
|
|
</select>
|
|
</label>
|
|
<p>
|
|
Canal: notificações no portal. «Na próxima verificação»
|
|
depende da execução periódica do acompanhamento.
|
|
</p>
|
|
<label className="flex items-start gap-2">
|
|
<input
|
|
type="checkbox"
|
|
name="alert-consent"
|
|
checked={form.data.consent}
|
|
onChange={(event) =>
|
|
form.setData('consent', event.target.checked)
|
|
}
|
|
/>
|
|
Quero receber estes alertas no portal e confirmo os
|
|
critérios escolhidos.
|
|
</label>
|
|
{Object.entries(form.errors).map(([key, error]) => (
|
|
<p key={key} role="alert">
|
|
{error}
|
|
</p>
|
|
))}
|
|
<div className="flex flex-wrap gap-3">
|
|
<button type="submit" className={button}>
|
|
{preferences.enabled
|
|
? 'Guardar preferências'
|
|
: 'Activar alertas'}
|
|
</button>
|
|
{preferences.enabled && (
|
|
<button
|
|
type="button"
|
|
className="underline"
|
|
onClick={() => send(false)}
|
|
>
|
|
Cancelar estes alertas
|
|
</button>
|
|
)}
|
|
</div>
|
|
</fieldset>
|
|
</form>
|
|
<p className="text-sm">
|
|
O cancelamento conserva as notificações anteriores e não altera
|
|
o acompanhamento das manifestações já apresentadas.
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|