Référent aussi bénévole : onglets Mon équipe / Mon planning
Un référent rattaché à une fiche bénévole voit aussi ses propres créneaux (vue bénévole) dans un second onglet de son écran, ouvert d'office quand il tient un créneau de bénévole hors de ses postes ; « TOI » dans l'équipe pointée. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
16d8fb44a7
commit
a37f6c2c41
@@ -174,6 +174,9 @@
|
||||
"mobileLead.message.audience": "{count} bénévole(s) le verront",
|
||||
"mobileLead.message.help": "Consignes du poste (tenue, matériel, point de rendez-vous…), affichées sur l'écran de chaque bénévole affecté au poste.",
|
||||
"mobileLead.message.placeholder": "Ex. Gilet jaune fourni à la base bénévoles. Rendez-vous 10 min avant le début.",
|
||||
"mobileLead.view.team": "Mon équipe",
|
||||
"mobileLead.view.planning": "Mon planning · {count}",
|
||||
"mobileLead.you": "TOI",
|
||||
"mobileLead.noPosition": "Vous n'êtes actuellement référent d'aucun poste.",
|
||||
"mobileLead.noShift": "Aucun créneau sur ce poste.",
|
||||
"mobileLead.badgePresent": "✓ PRÉSENT",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useShifts,
|
||||
useUpdatePosition,
|
||||
useVolunteer,
|
||||
useVolunteerPlanning,
|
||||
useVolunteers,
|
||||
} from '@/api/hooks';
|
||||
import { useAuth } from '@/auth/auth-context';
|
||||
@@ -35,10 +36,18 @@ import { VolunteerHomeView } from '@/pages/VolunteerHomeView';
|
||||
// courant : « Donner un accès de connexion » promu en référent), il voit
|
||||
// alors sa vue de bénévole classique (`VolunteerHomeView`).
|
||||
//
|
||||
// Un référent peut aussi être affecté comme bénévole, y compris la même
|
||||
// soirée : s'il est rattaché à une fiche bénévole, deux onglets — « Mon
|
||||
// équipe » (cet écran) et « Mon planning » (`VolunteerHomeView`, ses
|
||||
// propres créneaux). On ouvre sur « Mon planning » quand il tient en ce
|
||||
// moment un créneau de bénévole hors de ses postes et qu'aucun de ses
|
||||
// postes n'a de créneau en cours ; sinon sur « Mon équipe ».
|
||||
//
|
||||
// Le pointage présent/absent reste local à l'écran (état React, jamais
|
||||
// envoyé au serveur) : le pointage sur site est hors périmètre (CLAUDE.md §5).
|
||||
|
||||
type Presence = Record<string, 'present' | 'absent'>;
|
||||
type View = 'team' | 'planning';
|
||||
|
||||
const STATUS_KEY: Record<SlotStatus, string> = {
|
||||
done: 'mobileLead.status.done',
|
||||
@@ -54,6 +63,8 @@ export function MobileLeadPage() {
|
||||
const assignments = useAssignments();
|
||||
const dayParts = useDayParts();
|
||||
const me = useVolunteer(account?.volunteer_id);
|
||||
const myPlanning = useVolunteerPlanning(account?.volunteer_id);
|
||||
const [view, setView] = useState<View | null>(null);
|
||||
const updatePosition = useUpdatePosition();
|
||||
const now = useNowMinutes();
|
||||
const [presence, setPresence] = useState<Presence>({});
|
||||
@@ -158,214 +169,251 @@ export function MobileLeadPage() {
|
||||
: null;
|
||||
const myName = me.data ? `${me.data.first_name} ${me.data.last_name}` : null;
|
||||
|
||||
const inProgress = (start: string, end: string) =>
|
||||
slotStatus(naiveMinutes(start), naiveMinutes(end), now) === 'now';
|
||||
const ledShiftIds = new Set(myPositions.flatMap((p) => (shiftsByPosition.get(p.id) ?? []).map((s) => s.id)));
|
||||
const leadingNow = (shifts.data ?? []).some((s) => ledShiftIds.has(s.id) && inProgress(s.starts_at, s.ends_at));
|
||||
const myEntries = myPlanning.data ?? [];
|
||||
const volunteeringNow = myEntries.some(
|
||||
(e) => !ledShiftIds.has(e.shift_id) && inProgress(e.starts_at, e.ends_at),
|
||||
);
|
||||
const currentView: View = view ?? (volunteeringNow && !leadingNow ? 'planning' : 'team');
|
||||
const tabs = account?.volunteer_id ? (
|
||||
<div className="mx-auto grid w-full max-w-[640px] grid-cols-2 border-b-[3px] border-ink">
|
||||
<ViewTab active={currentView === 'team'} live={leadingNow} onClick={() => setView('team')}>
|
||||
{t('mobileLead.view.team')}
|
||||
</ViewTab>
|
||||
<ViewTab active={currentView === 'planning'} live={volunteeringNow} onClick={() => setView('planning')}>
|
||||
{t('mobileLead.view.planning', { count: myEntries.length })}
|
||||
</ViewTab>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (currentView === 'planning' && account?.volunteer_id) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
{tabs}
|
||||
<VolunteerHomeView volunteerId={account.volunteer_id} fallbackName={account.email.split('@')[0]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[640px] flex-1 flex-col">
|
||||
<div className="bg-ink px-4 pb-4 pt-[15px] text-cream sm:px-6">
|
||||
{myPositions.length > 1 ? (
|
||||
<div className="mb-3 flex flex-wrap gap-[6px]">
|
||||
{myPositions.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPositionId(p.id);
|
||||
setShiftId(null);
|
||||
}}
|
||||
className={cx(
|
||||
'min-h-[36px] cursor-pointer border-2 border-cream px-[10px] font-display text-[11.5px] font-extrabold uppercase',
|
||||
p.id === position.id ? 'bg-yellow text-ink' : 'bg-transparent text-cream',
|
||||
)}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center gap-3">
|
||||
{me.data ? (
|
||||
<VolunteerAvatar
|
||||
volunteerId={me.data.id}
|
||||
hasPhoto={me.data.has_photo}
|
||||
initials={`${me.data.first_name[0]}${me.data.last_name[0]}`}
|
||||
className="h-[60px] w-[60px] !border-blue text-[19px]"
|
||||
/>
|
||||
) : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-mono text-[10.5px] font-medium uppercase leading-none text-blue">
|
||||
{myName ? t('mobileLead.kickerNamed', { name: myName }) : t('mobileLead.kicker')}
|
||||
</div>
|
||||
<div className="mt-[5px] break-words font-display text-[27px] font-black leading-[1.05] tracking-tight">
|
||||
{position.name.toUpperCase()}
|
||||
</div>
|
||||
{position.location ? (
|
||||
<div className="mt-[3px] font-display text-[13px] font-semibold leading-snug text-sand-dark">
|
||||
{position.location}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{positionShifts.length ? (
|
||||
<div className="mt-[13px] grid grid-cols-2 gap-[6px]">
|
||||
{positionShifts.map((s) => {
|
||||
const on = s.id === shift?.id;
|
||||
const status = slotStatus(naiveMinutes(s.starts_at), naiveMinutes(s.ends_at), now);
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
{tabs}
|
||||
<div className="mx-auto flex w-full max-w-[640px] flex-1 flex-col">
|
||||
<div className="bg-ink px-4 pb-4 pt-[15px] text-cream sm:px-6">
|
||||
{myPositions.length > 1 ? (
|
||||
<div className="mb-3 flex flex-wrap gap-[6px]">
|
||||
{myPositions.map((p) => (
|
||||
<button
|
||||
key={s.id}
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setShiftId(s.id)}
|
||||
aria-pressed={on}
|
||||
onClick={() => {
|
||||
setPositionId(p.id);
|
||||
setShiftId(null);
|
||||
}}
|
||||
className={cx(
|
||||
'min-h-[50px] cursor-pointer border-2 border-cream px-2 py-[6px] text-left',
|
||||
on ? 'bg-blue text-ink' : 'bg-transparent text-cream',
|
||||
'min-h-[36px] cursor-pointer border-2 border-cream px-[10px] font-display text-[11.5px] font-extrabold uppercase',
|
||||
p.id === position.id ? 'bg-yellow text-ink' : 'bg-transparent text-cream',
|
||||
)}
|
||||
>
|
||||
<span className="block font-display text-[12px] font-black leading-tight">
|
||||
{longDayLabel(`${festivalDay(s.starts_at, boundary)}T12:00`)}
|
||||
</span>
|
||||
<span className="block font-mono text-[11px] font-semibold leading-snug">
|
||||
{clockLabel(naiveMinutes(s.starts_at))}–{clockLabel(naiveMinutes(s.ends_at))} ·{' '}
|
||||
{t(STATUS_KEY[status])}
|
||||
</span>
|
||||
{p.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 font-mono text-[11px] leading-relaxed opacity-70">{t('mobileLead.noShift')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shift ? (
|
||||
<>
|
||||
<div className="grid grid-cols-3 border-b-[3px] border-ink">
|
||||
<Counter value={presentCount} label={t('mobileLead.count.present')} className="border-r-2 bg-mint" />
|
||||
<Counter
|
||||
value={team.length - presentCount - absentCount}
|
||||
label={t('mobileLead.count.waiting')}
|
||||
className="border-r-2 bg-paper"
|
||||
/>
|
||||
<Counter
|
||||
value={absentCount}
|
||||
label={t('mobileLead.count.absent')}
|
||||
className={absentCount ? 'bg-orange' : 'bg-paper'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{absentCount ? (
|
||||
<div className="border-b-[3px] border-ink bg-[repeating-linear-gradient(45deg,#FF5C39_0_9px,#E0421F_9px_18px)] px-[14px] py-[11px] font-display text-[15px] font-black leading-snug">
|
||||
{t(absentCount > 1 ? 'mobileLead.absentAlert.other' : 'mobileLead.absentAlert.one', {
|
||||
count: absentCount,
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-[11px] px-[14px] pb-7 pt-[14px] sm:px-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-display text-[12px] font-black tracking-wider">
|
||||
{t('mobileLead.roll', { count: team.length })}
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] font-medium uppercase opacity-65">
|
||||
{shiftStatus === 'now'
|
||||
? t('mobileLead.when.now')
|
||||
: shiftStatus === 'next'
|
||||
? t('mobileLead.when.next', { time: clockLabel(naiveMinutes(shift.starts_at)) })
|
||||
: t('mobileLead.when.done')}
|
||||
</span>
|
||||
</div>
|
||||
{team.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={cx(
|
||||
'border-[3px] border-ink shadow-brutal-md',
|
||||
member.state === 'absent' ? 'bg-orange-blush' : member.state === 'present' ? 'bg-mint-pale' : 'bg-paper',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-[10px] px-[10px] py-[9px]">
|
||||
<VolunteerAvatar
|
||||
volunteerId={member.volunteerId}
|
||||
hasPhoto={member.hasPhoto}
|
||||
initials={member.initials}
|
||||
className="h-12 w-12 !border-2 text-[15px]"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block break-words font-display text-[16px] font-extrabold leading-tight">
|
||||
{member.name}
|
||||
</span>
|
||||
{member.phone ? (
|
||||
<span className="block font-mono text-[11px] font-medium leading-snug opacity-70">
|
||||
{member.phone}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{member.phone ? (
|
||||
<a
|
||||
href={`tel:${member.phone.replace(/\s+/g, '')}`}
|
||||
aria-label={t('mobileLead.callLabel', { name: member.name })}
|
||||
className="flex h-[46px] w-[46px] flex-none items-center justify-center border-2 border-ink bg-cream font-display text-[10px] font-black"
|
||||
>
|
||||
{t('mobileLead.call')}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 border-t-[3px] border-ink">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => mark(member.id, 'present')}
|
||||
aria-pressed={member.state === 'present'}
|
||||
className={cx(
|
||||
'min-h-[52px] cursor-pointer border-r-[3px] border-ink font-display text-[15px] font-black',
|
||||
member.state === 'present' ? 'bg-green text-white' : 'bg-cream',
|
||||
)}
|
||||
>
|
||||
{t(member.state === 'present' ? 'mobileLead.badgePresent' : 'mobileLead.markPresent')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => mark(member.id, 'absent')}
|
||||
aria-pressed={member.state === 'absent'}
|
||||
className={cx(
|
||||
'min-h-[52px] cursor-pointer font-display text-[15px] font-black',
|
||||
member.state === 'absent' ? 'bg-orange' : 'bg-cream',
|
||||
)}
|
||||
>
|
||||
{t(member.state === 'absent' ? 'mobileLead.badgeAbsent' : 'mobileLead.markAbsent')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{me.data ? (
|
||||
<VolunteerAvatar
|
||||
volunteerId={me.data.id}
|
||||
hasPhoto={me.data.has_photo}
|
||||
initials={`${me.data.first_name[0]}${me.data.last_name[0]}`}
|
||||
className="h-[60px] w-[60px] !border-blue text-[19px]"
|
||||
/>
|
||||
) : null}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-mono text-[10.5px] font-medium uppercase leading-none text-blue">
|
||||
{myName ? t('mobileLead.kickerNamed', { name: myName }) : t('mobileLead.kicker')}
|
||||
</div>
|
||||
))}
|
||||
{team.length === 0 ? (
|
||||
<div className="border-[3px] border-dashed border-ink p-4 font-mono text-[11px] leading-relaxed opacity-70">
|
||||
{t('mobileLead.emptyTeam')}
|
||||
<div className="mt-[5px] break-words font-display text-[27px] font-black leading-[1.05] tracking-tight">
|
||||
{position.name.toUpperCase()}
|
||||
</div>
|
||||
{position.location ? (
|
||||
<div className="mt-[3px] font-display text-[13px] font-semibold leading-snug text-sand-dark">
|
||||
{position.location}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{positionShifts.length ? (
|
||||
<div className="mt-[13px] grid grid-cols-2 gap-[6px]">
|
||||
{positionShifts.map((s) => {
|
||||
const on = s.id === shift?.id;
|
||||
const status = slotStatus(naiveMinutes(s.starts_at), naiveMinutes(s.ends_at), now);
|
||||
return (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
onClick={() => setShiftId(s.id)}
|
||||
aria-pressed={on}
|
||||
className={cx(
|
||||
'min-h-[50px] cursor-pointer border-2 border-cream px-2 py-[6px] text-left',
|
||||
on ? 'bg-blue text-ink' : 'bg-transparent text-cream',
|
||||
)}
|
||||
>
|
||||
<span className="block font-display text-[12px] font-black leading-tight">
|
||||
{longDayLabel(`${festivalDay(s.starts_at, boundary)}T12:00`)}
|
||||
</span>
|
||||
<span className="block font-mono text-[11px] font-semibold leading-snug">
|
||||
{clockLabel(naiveMinutes(s.starts_at))}–{clockLabel(naiveMinutes(s.ends_at))} ·{' '}
|
||||
{t(STATUS_KEY[status])}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 font-mono text-[11px] leading-relaxed opacity-70">{t('mobileLead.noShift')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shift ? (
|
||||
<>
|
||||
<div className="grid grid-cols-3 border-b-[3px] border-ink">
|
||||
<Counter value={presentCount} label={t('mobileLead.count.present')} className="border-r-2 bg-mint" />
|
||||
<Counter
|
||||
value={team.length - presentCount - absentCount}
|
||||
label={t('mobileLead.count.waiting')}
|
||||
className="border-r-2 bg-paper"
|
||||
/>
|
||||
<Counter
|
||||
value={absentCount}
|
||||
label={t('mobileLead.count.absent')}
|
||||
className={absentCount ? 'bg-orange' : 'bg-paper'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{absentCount ? (
|
||||
<div className="border-b-[3px] border-ink bg-[repeating-linear-gradient(45deg,#FF5C39_0_9px,#E0421F_9px_18px)] px-[14px] py-[11px] font-display text-[15px] font-black leading-snug">
|
||||
{t(absentCount > 1 ? 'mobileLead.absentAlert.other' : 'mobileLead.absentAlert.one', {
|
||||
count: absentCount,
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Message du poste : montré à chaque bénévole affecté, sur son
|
||||
écran (créneau mis en avant et liste des créneaux). */}
|
||||
<div className="border-t-[3px] border-ink bg-sand px-[14px] pb-7 pt-[14px] sm:px-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-display text-[12px] font-black tracking-wider">
|
||||
{t('mobileLead.message.title')}
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] font-medium opacity-65">
|
||||
{t('mobileLead.message.audience', { count: teamSize })}
|
||||
</span>
|
||||
<div className="flex flex-col gap-[11px] px-[14px] pb-7 pt-[14px] sm:px-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-display text-[12px] font-black tracking-wider">
|
||||
{t('mobileLead.roll', { count: team.length })}
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] font-medium uppercase opacity-65">
|
||||
{shiftStatus === 'now'
|
||||
? t('mobileLead.when.now')
|
||||
: shiftStatus === 'next'
|
||||
? t('mobileLead.when.next', { time: clockLabel(naiveMinutes(shift.starts_at)) })
|
||||
: t('mobileLead.when.done')}
|
||||
</span>
|
||||
</div>
|
||||
{team.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={cx(
|
||||
'border-[3px] border-ink shadow-brutal-md',
|
||||
member.state === 'absent' ? 'bg-orange-blush' : member.state === 'present' ? 'bg-mint-pale' : 'bg-paper',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-[10px] px-[10px] py-[9px]">
|
||||
<VolunteerAvatar
|
||||
volunteerId={member.volunteerId}
|
||||
hasPhoto={member.hasPhoto}
|
||||
initials={member.initials}
|
||||
className="h-12 w-12 !border-2 text-[15px]"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block break-words font-display text-[16px] font-extrabold leading-tight">
|
||||
{member.name}
|
||||
{member.volunteerId === account?.volunteer_id ? (
|
||||
<span className="ml-[6px] inline-block border-2 border-ink bg-blue px-[5px] py-[2px] align-middle font-display text-[9.5px] font-black leading-none">
|
||||
{t('mobileLead.you')}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{member.phone ? (
|
||||
<span className="block font-mono text-[11px] font-medium leading-snug opacity-70">
|
||||
{member.phone}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{member.phone ? (
|
||||
<a
|
||||
href={`tel:${member.phone.replace(/\s+/g, '')}`}
|
||||
aria-label={t('mobileLead.callLabel', { name: member.name })}
|
||||
className="flex h-[46px] w-[46px] flex-none items-center justify-center border-2 border-ink bg-cream font-display text-[10px] font-black"
|
||||
>
|
||||
{t('mobileLead.call')}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 border-t-[3px] border-ink">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => mark(member.id, 'present')}
|
||||
aria-pressed={member.state === 'present'}
|
||||
className={cx(
|
||||
'min-h-[52px] cursor-pointer border-r-[3px] border-ink font-display text-[15px] font-black',
|
||||
member.state === 'present' ? 'bg-green text-white' : 'bg-cream',
|
||||
)}
|
||||
>
|
||||
{t(member.state === 'present' ? 'mobileLead.badgePresent' : 'mobileLead.markPresent')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => mark(member.id, 'absent')}
|
||||
aria-pressed={member.state === 'absent'}
|
||||
className={cx(
|
||||
'min-h-[52px] cursor-pointer font-display text-[15px] font-black',
|
||||
member.state === 'absent' ? 'bg-orange' : 'bg-cream',
|
||||
)}
|
||||
>
|
||||
{t(member.state === 'absent' ? 'mobileLead.badgeAbsent' : 'mobileLead.markAbsent')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{team.length === 0 ? (
|
||||
<div className="border-[3px] border-dashed border-ink p-4 font-mono text-[11px] leading-relaxed opacity-70">
|
||||
{t('mobileLead.emptyTeam')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Message du poste : montré à chaque bénévole affecté, sur son
|
||||
écran (créneau mis en avant et liste des créneaux). */}
|
||||
<div className="border-t-[3px] border-ink bg-sand px-[14px] pb-7 pt-[14px] sm:px-6">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-display text-[12px] font-black tracking-wider">
|
||||
{t('mobileLead.message.title')}
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] font-medium opacity-65">
|
||||
{t('mobileLead.message.audience', { count: teamSize })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mb-[9px] mt-1 font-mono text-[10.5px] leading-relaxed opacity-70">
|
||||
{t('mobileLead.message.help')}
|
||||
</p>
|
||||
<MessageEditor
|
||||
key={position.id}
|
||||
value={position.message}
|
||||
updatedAt={position.message_updated_at}
|
||||
placeholder={t('mobileLead.message.placeholder')}
|
||||
saving={updatePosition.isPending}
|
||||
error={updatePosition.error}
|
||||
onSave={(text) => updatePosition.mutateAsync({ id: position.id, body: { message: text } })}
|
||||
/>
|
||||
</div>
|
||||
<p className="mb-[9px] mt-1 font-mono text-[10.5px] leading-relaxed opacity-70">
|
||||
{t('mobileLead.message.help')}
|
||||
</p>
|
||||
<MessageEditor
|
||||
key={position.id}
|
||||
value={position.message}
|
||||
updatedAt={position.message_updated_at}
|
||||
placeholder={t('mobileLead.message.placeholder')}
|
||||
saving={updatePosition.isPending}
|
||||
error={updatePosition.error}
|
||||
onSave={(text) => updatePosition.mutateAsync({ id: position.id, body: { message: text } })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -379,3 +427,31 @@ function Counter({ value, label, className }: { value: number; label: string; cl
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewTab({
|
||||
active,
|
||||
live,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
/** Un créneau de cette vue est en cours : pastille orange. */
|
||||
live: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
className={cx(
|
||||
'flex min-h-[46px] cursor-pointer items-center justify-center gap-[7px] border-r-[3px] border-ink px-2 font-display text-[12px] font-black uppercase tracking-wide last:border-r-0',
|
||||
active ? 'bg-ink text-yellow' : 'bg-paper text-ink hover:bg-yellow-soft',
|
||||
)}
|
||||
>
|
||||
{live ? <span className="h-[9px] w-[9px] flex-none border-2 border-ink bg-orange" aria-hidden="true" /> : null}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user