Implement ticket panel synchronization and configuration updates
- Added `panelChannelId` and `panelMessageId` fields to the `TicketConfig` model for managing ticket panel settings. - Introduced `syncTicketPanel` function to handle posting and updating the ticket panel in Discord channels via BullMQ. - Enhanced error handling for ticket panel synchronization, including specific error messages for missing categories and posting failures. - Updated WebUI components to support panel channel selection and improved error messaging for synchronization issues. - Added localization entries for new error messages related to ticket panel synchronization in both English and German. - Refactored ticket category management to trigger panel synchronization upon category creation, update, or deletion.
This commit is contained in:
@@ -8,6 +8,7 @@ import { FieldAnchor } from '@/components/layout/field-anchor';
|
||||
import { useTranslations } from '@/components/locale-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { DiscordEmojiSelect } from '@/components/ui/discord-emoji-select';
|
||||
import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -24,6 +25,23 @@ interface TicketCategoriesManagerProps {
|
||||
initialCategories: TicketCategoryDashboard[];
|
||||
}
|
||||
|
||||
function panelSyncErrorMessage(
|
||||
code: string | undefined,
|
||||
fallback: string | undefined,
|
||||
t: (key: string) => string
|
||||
): string {
|
||||
if (code === 'timeout') {
|
||||
return t('modulePages.tickets.errors.panelTimeout');
|
||||
}
|
||||
if (code === 'no_categories') {
|
||||
return t('modulePages.tickets.errors.noCategories');
|
||||
}
|
||||
if (code === 'not_posted') {
|
||||
return t('modulePages.tickets.errors.panelNotPosted');
|
||||
}
|
||||
return fallback ?? t('common.saveError');
|
||||
}
|
||||
|
||||
export function TicketCategoriesManager({ guildId, initialCategories }: TicketCategoriesManagerProps) {
|
||||
const t = useTranslations();
|
||||
const [categories, setCategories] = useState<LocalCategory[]>(
|
||||
@@ -49,9 +67,11 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
supportRoleIds: draft.supportRoleIds
|
||||
})
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as (TicketCategoryDashboard & { error?: string }) | null;
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| (TicketCategoryDashboard & { error?: string; code?: string })
|
||||
| null;
|
||||
if (!response.ok || !body) {
|
||||
toast.error(body?.error ?? t('common.saveError'));
|
||||
toast.error(panelSyncErrorMessage(body?.code, body?.error, t));
|
||||
return;
|
||||
}
|
||||
setCategories((prev) => [...prev, { ...body, clientKey: body.id ?? `new-${Date.now()}` }]);
|
||||
@@ -79,9 +99,11 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
supportRoleIds: category.supportRoleIds
|
||||
})
|
||||
});
|
||||
const body = (await response.json().catch(() => null)) as (TicketCategoryDashboard & { error?: string }) | null;
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| (TicketCategoryDashboard & { error?: string; code?: string })
|
||||
| null;
|
||||
if (!response.ok || !body) {
|
||||
toast.error(body?.error ?? t('common.saveError'));
|
||||
toast.error(panelSyncErrorMessage(body?.code, body?.error, t));
|
||||
return;
|
||||
}
|
||||
toast.success(t('common.saveSuccess'));
|
||||
@@ -100,7 +122,8 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) {
|
||||
toast.error(t('common.saveError'));
|
||||
const body = (await response.json().catch(() => null)) as { error?: string; code?: string } | null;
|
||||
toast.error(panelSyncErrorMessage(body?.code, body?.error, t));
|
||||
return;
|
||||
}
|
||||
setCategories((prev) => prev.filter((entry) => entry.clientKey !== category.clientKey));
|
||||
@@ -139,12 +162,15 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tickets.categoryEmoji')}</Label>
|
||||
<Input
|
||||
value={category.emoji ?? ''}
|
||||
onChange={(event) =>
|
||||
<DiscordEmojiSelect
|
||||
guildId={guildId}
|
||||
value={category.emoji ?? undefined}
|
||||
onChange={(emoji) =>
|
||||
setCategories((prev) =>
|
||||
prev.map((entry) =>
|
||||
entry.clientKey === category.clientKey ? { ...entry, emoji: event.target.value } : entry
|
||||
entry.clientKey === category.clientKey
|
||||
? { ...entry, emoji: emoji ?? null }
|
||||
: entry
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -200,7 +226,11 @@ export function TicketCategoriesManager({ guildId, initialCategories }: TicketCa
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tickets.categoryEmoji')}</Label>
|
||||
<Input value={draft.emoji} onChange={(event) => setDraft((prev) => ({ ...prev, emoji: event.target.value }))} />
|
||||
<DiscordEmojiSelect
|
||||
guildId={guildId}
|
||||
value={draft.emoji || undefined}
|
||||
onChange={(emoji) => setDraft((prev) => ({ ...prev, emoji: emoji ?? '' }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-2">
|
||||
<Label>{t('modulePages.tickets.categoryDescription')}</Label>
|
||||
|
||||
@@ -13,7 +13,16 @@ import { Switch } from '@/components/ui/switch';
|
||||
import type { SettingsSaveResult } from '@/components/settings/settings-form';
|
||||
import { SettingsForm } from '@/components/settings/settings-form';
|
||||
|
||||
async function saveTicketConfig(guildId: string, value: TicketConfigDashboard): Promise<SettingsSaveResult> {
|
||||
async function saveTicketConfig(
|
||||
guildId: string,
|
||||
value: TicketConfigDashboard,
|
||||
errorMessages: {
|
||||
timeout: string;
|
||||
notPosted: string;
|
||||
noCategories: string;
|
||||
network: string;
|
||||
}
|
||||
): Promise<SettingsSaveResult> {
|
||||
try {
|
||||
const response = await fetch(`/api/guilds/${guildId}/tickets`, {
|
||||
method: 'PATCH',
|
||||
@@ -21,12 +30,24 @@ async function saveTicketConfig(guildId: string, value: TicketConfigDashboard):
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = (await response.json().catch(() => null)) as { error?: string } | null;
|
||||
const body = (await response.json().catch(() => null)) as {
|
||||
error?: string;
|
||||
code?: string;
|
||||
} | null;
|
||||
if (body?.code === 'timeout') {
|
||||
return { ok: false, error: errorMessages.timeout };
|
||||
}
|
||||
if (body?.code === 'no_categories') {
|
||||
return { ok: false, error: errorMessages.noCategories };
|
||||
}
|
||||
if (body?.code === 'not_posted') {
|
||||
return { ok: false, error: errorMessages.notPosted };
|
||||
}
|
||||
return { ok: false, error: body?.error ?? `Request failed with status ${response.status}` };
|
||||
}
|
||||
return { ok: true };
|
||||
} catch {
|
||||
return { ok: false, error: 'Network error' };
|
||||
return { ok: false, error: errorMessages.network };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +63,14 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
|
||||
return (
|
||||
<SettingsForm<TicketConfigDashboard>
|
||||
initialValue={initialValue}
|
||||
onSave={(value) => saveTicketConfig(guildId, value)}
|
||||
onSave={(value) =>
|
||||
saveTicketConfig(guildId, value, {
|
||||
timeout: t('modulePages.tickets.errors.panelTimeout'),
|
||||
notPosted: t('modulePages.tickets.errors.panelNotPosted'),
|
||||
noCategories: t('modulePages.tickets.errors.noCategories'),
|
||||
network: t('common.networkError')
|
||||
})
|
||||
}
|
||||
>
|
||||
{({ value, setValue }) => (
|
||||
<Card>
|
||||
@@ -77,6 +105,18 @@ export function TicketConfigForm({ guildId, initialValue }: TicketConfigFormProp
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
<FieldAnchor id="field-tickets-panel-channel">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tickets.panelChannelId')}</Label>
|
||||
<DiscordChannelSelect
|
||||
guildId={guildId}
|
||||
value={value.panelChannelId}
|
||||
onChange={(panelChannelId) => setValue((prev) => ({ ...prev, panelChannelId }))}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('modulePages.tickets.panelChannelHint')}</p>
|
||||
</div>
|
||||
</FieldAnchor>
|
||||
|
||||
<FieldAnchor id="field-tickets-category">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('modulePages.tickets.categoryId')}</Label>
|
||||
|
||||
Reference in New Issue
Block a user