Initial commit
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { client, errorMessage } from '../../api/client'
|
||||
import type { NotificationChannel, ReminderRule } from '../../api/types'
|
||||
import { useToasts } from '../../components/Toasts'
|
||||
import { Badge, Button, Card, ErrorState, Input, Select, Spinner } from '../../components/ui'
|
||||
import { formatWithZone } from '../../lib/dates'
|
||||
import { describeOffset, REMINDER_PRESETS as PRESETS } from '../../lib/reminders'
|
||||
|
||||
export function ReminderSection({ webinarId, timezone }: { webinarId: number; timezone: string }) {
|
||||
const queryClient = useQueryClient()
|
||||
const toasts = useToasts()
|
||||
const [offset, setOffset] = useState('1440')
|
||||
const [channel, setChannel] = useState<NotificationChannel>('EMAIL')
|
||||
|
||||
const rules = useQuery({
|
||||
queryKey: ['reminders', webinarId],
|
||||
queryFn: async () => {
|
||||
const response = await client.get<ReminderRule[]>(`/admin/webinars/${webinarId}/reminders`)
|
||||
return response.data
|
||||
},
|
||||
})
|
||||
|
||||
function invalidate() {
|
||||
queryClient.invalidateQueries({ queryKey: ['reminders', webinarId] })
|
||||
}
|
||||
|
||||
const add = useMutation({
|
||||
mutationFn: () =>
|
||||
client.post(`/admin/webinars/${webinarId}/reminders`, {
|
||||
offsetMinutes: Number(offset),
|
||||
channel,
|
||||
template: 'reminder',
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toasts.success('Reguła została dodana')
|
||||
invalidate()
|
||||
},
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się dodać reguły')),
|
||||
})
|
||||
|
||||
const toggle = useMutation({
|
||||
mutationFn: ({ id, value }: { id: number; value: boolean }) =>
|
||||
client.post(`/admin/webinars/${webinarId}/reminders/${id}/enabled?value=${value}`),
|
||||
onSuccess: () => invalidate(),
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się zmienić reguły')),
|
||||
})
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (id: number) => client.delete(`/admin/webinars/${webinarId}/reminders/${id}`),
|
||||
onSuccess: () => {
|
||||
toasts.success('Reguła została usunięta')
|
||||
invalidate()
|
||||
},
|
||||
onError: (error) => toasts.error(errorMessage(error, 'Nie udało się usunąć reguły')),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="Przypomnienia"
|
||||
description="Wiadomości wysyłane automatycznie przed startem webinaru"
|
||||
>
|
||||
{rules.isPending && <Spinner />}
|
||||
{rules.isError && (
|
||||
<ErrorState message={errorMessage(rules.error, 'Nie udało się wczytać reguł')} />
|
||||
)}
|
||||
|
||||
{rules.data && (
|
||||
<div className="space-y-4">
|
||||
{rules.data.length === 0 ? (
|
||||
<p className="rounded-md border border-slate-200 bg-slate-50 p-3 text-sm text-slate-500">
|
||||
Brak reguł. Ten webinar nie wyśle żadnych przypomnień.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-slate-100 rounded-md border border-slate-200">
|
||||
{rules.data.map((rule) => (
|
||||
<li key={rule.id} className="flex flex-wrap items-center gap-3 p-3">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rule.enabled}
|
||||
onChange={(e) => toggle.mutate({ id: rule.id, value: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-slate-200 text-blue-700 focus:ring-blue-600"
|
||||
aria-label={rule.enabled ? 'Wyłącz regułę' : 'Włącz regułę'}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={`text-sm font-medium ${rule.enabled ? 'text-slate-900' : 'text-slate-400'}`}>
|
||||
{describeOffset(rule.offsetMinutes)}
|
||||
<span className="ml-2 text-xs font-normal text-slate-500">
|
||||
{rule.channel === 'SMS' ? 'SMS' : 'e-mail'}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{formatWithZone(rule.scheduledAt, timezone)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* liczniki pokazuja, co juz poszlo z tej reguly */}
|
||||
<div className="flex shrink-0 gap-1.5">
|
||||
{rule.sentCount > 0 && (
|
||||
<Badge className="bg-ok-50 text-ok-600 ring-ok-500/20">
|
||||
wysłano {rule.sentCount}
|
||||
</Badge>
|
||||
)}
|
||||
{rule.queuedCount > 0 && (
|
||||
<Badge className="bg-warn-50 text-warn-600 ring-warn-500/25">
|
||||
w kolejce {rule.queuedCount}
|
||||
</Badge>
|
||||
)}
|
||||
{rule.failedCount > 0 && (
|
||||
<Badge className="bg-alarm-50 text-alarm-600 ring-alarm-500/20">
|
||||
błędy {rule.failedCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
icon="trash"
|
||||
onClick={() => {
|
||||
const extra = rule.alreadySent
|
||||
? '\n\nZ tej reguły wysłano już wiadomości. Historia wysyłek zostanie usunięta.'
|
||||
: ''
|
||||
if (window.confirm(`Usunąć regułę "${describeOffset(rule.offsetMinutes)}"?${extra}`)) {
|
||||
remove.mutate(rule.id)
|
||||
}
|
||||
}}
|
||||
className="shrink-0 text-alarm-600 hover:bg-alarm-50"
|
||||
title="Usuń regułę"
|
||||
>
|
||||
<span className="sr-only">Usuń</span>
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-end gap-2 border-t border-slate-100 pt-4">
|
||||
<div className="w-44">
|
||||
<label className="mb-1 block text-xs font-semibold text-slate-700">Kiedy wysłać</label>
|
||||
<Select value={offset} onChange={(e) => setOffset(e.target.value)}>
|
||||
{PRESETS.map((preset) => (
|
||||
<option key={preset.minutes} value={preset.minutes}>
|
||||
{preset.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-28">
|
||||
<label className="mb-1 block text-xs font-semibold text-slate-700">Kanał</label>
|
||||
<Select
|
||||
value={channel}
|
||||
onChange={(e) => setChannel(e.target.value as NotificationChannel)}
|
||||
>
|
||||
<option value="EMAIL">E-mail</option>
|
||||
<option value="SMS">SMS</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<label className="mb-1 block text-xs font-semibold text-slate-700">Własny odstęp</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={offset}
|
||||
onChange={(e) => setOffset(e.target.value)}
|
||||
placeholder="minuty"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
onClick={() => add.mutate()}
|
||||
disabled={add.isPending || !offset || Number(offset) < 1}
|
||||
>
|
||||
Dodaj
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{rules.data.some((rule) => rule.channel === 'SMS') && (
|
||||
<p className="rounded-md border border-blue-200 bg-blue-50 p-3 text-xs leading-relaxed text-blue-900">
|
||||
SMS trafi wyłącznie do uczestników, którzy podali numer telefonu
|
||||
i wyrazili zgodę marketingową na SMS.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user