Enhance admin maintenance functionality with new endpoints and response types
CI / Backend (build + test) (push) Successful in 1m17s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s

- Added new DELETE endpoints for managing audit logs and disabled apps in the admin maintenance section.
- Updated existing endpoint for closed tickets to use a unified response type, `MaintenanceCleanupResponseDto`.
- Enhanced API documentation to reflect the new operations and their expected request/response formats.
- Improved frontend integration with new functions for deleting old audit logs and disabled apps, including user confirmation prompts.
- Added localization support for new maintenance actions in both Russian and English.
This commit is contained in:
Leonid Pershin
2026-07-14 18:37:22 +03:00
parent 8dfeb05912
commit 94ba514b8e
15 changed files with 449 additions and 15 deletions
+12 -2
View File
@@ -1,6 +1,16 @@
import { apiRequest } from '@/shared/api/client'
import type { DeleteClosedTicketsResponseDto } from '@/shared/api/types'
import type { MaintenanceCleanupResponseDto } from '@/shared/api/types'
export function deleteClosedTickets() {
return apiRequest<DeleteClosedTicketsResponseDto>('/admin/maintenance/tickets/closed', { method: 'DELETE' })
return apiRequest<MaintenanceCleanupResponseDto>('/admin/maintenance/tickets/closed', { method: 'DELETE' })
}
export function deleteOldAuditLogs(olderThanDays: number) {
return apiRequest<MaintenanceCleanupResponseDto>(`/admin/maintenance/audit-logs?olderThanDays=${olderThanDays}`, {
method: 'DELETE',
})
}
export function deleteDisabledApps() {
return apiRequest<MaintenanceCleanupResponseDto>('/admin/maintenance/apps/disabled', { method: 'DELETE' })
}
+77 -1
View File
@@ -1,16 +1,21 @@
import { useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { toast } from '@/shared/ui/toast-store'
import { Button } from '@/shared/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui/card'
import { deleteClosedTickets } from '@/features/admin/maintenance/api'
import { Input } from '@/shared/ui/input'
import { deleteClosedTickets, deleteDisabledApps, deleteOldAuditLogs } from '@/features/admin/maintenance/api'
export const Route = createFileRoute('/admin/maintenance')({ component: AdminMaintenancePage })
const DEFAULT_AUDIT_RETENTION_DAYS = 90
function AdminMaintenancePage() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [auditRetentionDays, setAuditRetentionDays] = useState(DEFAULT_AUDIT_RETENTION_DAYS)
const deleteClosedTicketsMutation = useMutation({
mutationFn: deleteClosedTickets,
@@ -21,6 +26,24 @@ function AdminMaintenancePage() {
onError: () => toast.error(t('auth.genericError')),
})
const deleteOldAuditLogsMutation = useMutation({
mutationFn: () => deleteOldAuditLogs(auditRetentionDays),
onSuccess: async (data) => {
toast.success(t('admin.maintenance.audit.deleted', { count: data.deletedCount }))
await queryClient.invalidateQueries({ queryKey: ['admin-audit'] })
},
onError: () => toast.error(t('auth.genericError')),
})
const deleteDisabledAppsMutation = useMutation({
mutationFn: deleteDisabledApps,
onSuccess: async (data) => {
toast.success(t('admin.maintenance.apps.deleted', { count: data.deletedCount }))
await queryClient.invalidateQueries({ queryKey: ['admin-apps'] })
},
onError: () => toast.error(t('auth.genericError')),
})
return (
<div className="flex flex-col gap-4">
<Card>
@@ -42,6 +65,59 @@ function AdminMaintenancePage() {
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">{t('admin.maintenance.audit.title')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">{t('admin.maintenance.audit.description')}</p>
<div className="flex flex-wrap items-center gap-2">
<label className="flex items-center gap-2 text-sm text-muted-foreground">
{t('admin.maintenance.audit.olderThan')}
<Input
type="number"
min={1}
max={3650}
value={auditRetentionDays}
onChange={(e) => setAuditRetentionDays(Number(e.target.value))}
className="w-20"
/>
{t('admin.maintenance.audit.days')}
</label>
<Button
variant="outline"
disabled={deleteOldAuditLogsMutation.isPending || auditRetentionDays < 1}
onClick={() => {
if (confirm(t('admin.maintenance.audit.confirm', { days: auditRetentionDays })))
deleteOldAuditLogsMutation.mutate()
}}
>
{t('admin.maintenance.audit.action')}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">{t('admin.maintenance.apps.title')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">{t('admin.maintenance.apps.description')}</p>
<div>
<Button
variant="outline"
disabled={deleteDisabledAppsMutation.isPending}
onClick={() => {
if (confirm(t('admin.maintenance.apps.confirm'))) deleteDisabledAppsMutation.mutate()
}}
>
{t('admin.maintenance.apps.action')}
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
+1 -1
View File
@@ -270,7 +270,7 @@ export type TicketSummaryDto = {
messagePreview: string | null
}
export type DeleteClosedTicketsResponseDto = {
export type MaintenanceCleanupResponseDto = {
deletedCount: number
}
+32
View File
@@ -354,6 +354,22 @@ const resources = {
confirm: 'Удалить все закрытые обращения и файлы в них? Действие необратимо.',
deleted: 'Удалено обращений: {{count}}.',
},
audit: {
title: 'Журнал аудита',
description: 'Удалить записи журнала аудита старше указанного числа дней. Действие необратимо.',
olderThan: 'Старше',
days: 'дней',
action: 'Удалить старые записи',
confirm: 'Удалить записи аудита старше {{days}} дней? Действие необратимо.',
deleted: 'Удалено записей аудита: {{count}}.',
},
apps: {
title: 'Отключённые приложения',
description: 'Удалить все отключённые приложения из каталога. Действие необратимо.',
action: 'Удалить отключённые приложения',
confirm: 'Удалить все отключённые приложения? Действие необратимо.',
deleted: 'Удалено приложений: {{count}}.',
},
},
stats: {
totalUsers: 'Всего пользователей',
@@ -720,6 +736,22 @@ const resources = {
confirm: 'Delete all closed tickets and their files? This cannot be undone.',
deleted: 'Deleted tickets: {{count}}.',
},
audit: {
title: 'Audit log',
description: 'Delete audit log entries older than the given number of days. This cannot be undone.',
olderThan: 'Older than',
days: 'days',
action: 'Delete old entries',
confirm: 'Delete audit entries older than {{days}} days? This cannot be undone.',
deleted: 'Deleted audit entries: {{count}}.',
},
apps: {
title: 'Disabled apps',
description: 'Delete all disabled apps from the catalog. This cannot be undone.',
action: 'Delete disabled apps',
confirm: 'Delete all disabled apps? This cannot be undone.',
deleted: 'Deleted apps: {{count}}.',
},
},
stats: {
totalUsers: 'Total users',