Email de test et ajout manuel sans champ obligatoire
Étape 67 : adresse + bouton dans l'onglet « Messages » pour envoyer tout de suite un email de test, résultat réel affiché (envoyé, désactivé, connexion ou message refusé, réponse du serveur SMTP) — POST .../notifications/test. Étape 66 : prénom et nom facultatifs à l'ajout manuel d'un bénévole ; un bénévole sans nom s'affiche « Sans nom » / « ? » partout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5c2cc0a6c3
commit
461742eb78
@@ -1,9 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useSendPlanningNotification } from '@/api/hooks';
|
||||
import { useSendPlanningNotification, useSendTestEmail } from '@/api/hooks';
|
||||
import type { TestEmailResult } from '@/api/schema';
|
||||
import { ErrorNote } from '@/components/state';
|
||||
import { Btn, FieldLabel, HelpTip, Panel, PanelHeader } from '@/components/ui';
|
||||
import { t } from '@/i18n';
|
||||
import { cx } from '@/lib/cx';
|
||||
|
||||
const MAIL_VARIABLES = [
|
||||
'{prenom}',
|
||||
@@ -100,6 +102,8 @@ export function ConfigMail() {
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<TestEmailPanel />
|
||||
|
||||
<Panel shadow="lg">
|
||||
<PanelHeader hatched>{t('config.mail.previewTitle')}</PanelHeader>
|
||||
<div className="p-[13px]">
|
||||
@@ -116,3 +120,63 @@ export function ConfigMail() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const RESULT_TONE: Record<TestEmailResult['status'], string> = {
|
||||
sent: 'bg-green text-white',
|
||||
disabled: 'bg-yellow-soft',
|
||||
connection_failed: 'bg-orange',
|
||||
rejected: 'bg-orange',
|
||||
};
|
||||
|
||||
/** Envoie un email de test à l'adresse saisie et affiche le résultat réel
|
||||
* du serveur SMTP (envoyé, envoi désactivé, connexion ou message refusé). */
|
||||
function TestEmailPanel() {
|
||||
const [email, setEmail] = useState('');
|
||||
const test = useSendTestEmail();
|
||||
const result = test.data;
|
||||
return (
|
||||
<Panel shadow="lg">
|
||||
<PanelHeader>{t('config.mail.test.title')}</PanelHeader>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (email.trim()) test.mutate(email.trim());
|
||||
}}
|
||||
className="flex flex-col gap-3 p-[13px]"
|
||||
>
|
||||
<p className="font-mono text-[10.5px] leading-relaxed opacity-70">{t('config.mail.test.help')}</p>
|
||||
<label className="block">
|
||||
<FieldLabel>{t('config.mail.test.email')}</FieldLabel>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={t('config.mail.test.placeholder')}
|
||||
className="w-full border-2 border-ink bg-cream p-[9px] font-mono text-[12px]"
|
||||
/>
|
||||
</label>
|
||||
<Btn type="submit" disabled={!email.trim() || test.isPending}>
|
||||
{test.isPending ? t('config.mail.test.sending') : t('config.mail.test.send')}
|
||||
</Btn>
|
||||
{result && !test.isPending ? (
|
||||
<div className={cx('border-2 border-ink p-[9px]', RESULT_TONE[result.status])}>
|
||||
<div className="font-display text-[12px] font-extrabold leading-snug">
|
||||
{t(`config.mail.test.result.${result.status}`)}
|
||||
</div>
|
||||
{result.detail ? (
|
||||
<div className="mt-1 break-words font-mono text-[10.5px] leading-snug">
|
||||
{t('config.mail.test.serverSaid', { detail: result.detail })}
|
||||
</div>
|
||||
) : null}
|
||||
{result.status !== 'sent' && result.status !== 'disabled' ? (
|
||||
<div className="mt-1 font-mono text-[10px] leading-snug opacity-80">
|
||||
{t('config.mail.test.config', { host: result.smtp_host, from: result.smtp_from })}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{test.isError ? <ErrorNote error={test.error} /> : null}
|
||||
</form>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import { foldAccents } from '@/lib/planning';
|
||||
import { t } from '@/i18n';
|
||||
import { datePart, shortDayLabel, timePart } from '@/lib/time';
|
||||
import { cx } from '@/lib/cx';
|
||||
import { volunteerInitials, volunteerName } from '@/lib/volunteer';
|
||||
|
||||
const MAP_FIELDS: Array<{ key: ImportFieldKey; required: boolean }> = [
|
||||
{ key: 'first_name', required: true },
|
||||
@@ -888,11 +889,10 @@ export function ConfigVolunteers() {
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-[7px]">
|
||||
<span className="flex h-6 w-6 flex-none items-center justify-center border-2 border-ink bg-paper font-display text-[9.5px] font-extrabold">
|
||||
{volunteer.first_name[0]}
|
||||
{volunteer.last_name[0]}
|
||||
{volunteerInitials(volunteer)}
|
||||
</span>
|
||||
<span className="truncate font-display text-[12.5px] font-bold">
|
||||
{volunteer.first_name} {volunteer.last_name}
|
||||
{volunteerName(volunteer)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="hidden truncate font-mono text-[11px] opacity-80 md:block">
|
||||
@@ -950,11 +950,10 @@ export function ConfigVolunteers() {
|
||||
</div>
|
||||
<div className="mt-[7px] flex items-center gap-[9px]">
|
||||
<span className="flex h-[34px] w-[34px] flex-none items-center justify-center border-2 border-cream bg-yellow font-display text-[13px] font-extrabold text-ink">
|
||||
{selected.first_name[0]}
|
||||
{selected.last_name[0]}
|
||||
{volunteerInitials(selected)}
|
||||
</span>
|
||||
<span className="font-display text-[18px] font-black tracking-tight">
|
||||
{selected.first_name} {selected.last_name}
|
||||
{volunteerName(selected)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1082,10 +1081,7 @@ function AddVolunteerForm({ onClose }: { onClose: () => void }) {
|
||||
|
||||
const submit = async () => {
|
||||
setLocalError(null);
|
||||
if (!form.first_name.trim() || !form.last_name.trim()) {
|
||||
setLocalError(t('config.vol.requiredNames'));
|
||||
return;
|
||||
}
|
||||
// Aucun champ obligatoire : la fiche peut être complétée plus tard.
|
||||
try {
|
||||
await createVolunteer.mutateAsync({
|
||||
first_name: form.first_name.trim(),
|
||||
@@ -1127,11 +1123,11 @@ function AddVolunteerForm({ onClose }: { onClose: () => void }) {
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="block">
|
||||
<FieldLabel>{t('config.vol.field.firstName')} *</FieldLabel>
|
||||
<FieldLabel>{t('config.vol.field.firstName')}</FieldLabel>
|
||||
<TextInput value={form.first_name} onChange={(v) => set('first_name', v)} />
|
||||
</label>
|
||||
<label className="block">
|
||||
<FieldLabel>{t('config.vol.field.lastName')} *</FieldLabel>
|
||||
<FieldLabel>{t('config.vol.field.lastName')}</FieldLabel>
|
||||
<TextInput value={form.last_name} onChange={(v) => set('last_name', v)} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user