Compare commits

...
2 Commits
27 changed files with 355 additions and 305 deletions
+10
View File
@@ -4,5 +4,15 @@
"rules": { "rules": {
"react/rules-of-hooks": "error", "react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }] "react/only-export-components": ["warn", { "allowConstantExport": true }]
},
"overrides": [
{
// File-based routing TanStack Router: файл роута обязан экспортировать `Route` рядом с
// компонентом страницы — правило тут неисполнимо в принципе.
"files": ["src/routes/**"],
"rules": {
"react/only-export-components": "off"
} }
}
]
} }
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { qk } from '@/shared/api/query-keys' import { qk } from '@/shared/api/query-keys'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { EntryTraceDto } from '@/shared/api/types'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -42,52 +43,11 @@ export function EntryTraceDialog({
{data && ( {data && (
<dl className="grid grid-cols-[110px_1fr] gap-x-3 gap-y-1.5 text-sm"> <dl className="grid grid-cols-[110px_1fr] gap-x-3 gap-y-1.5 text-sm">
<Row label={t('admin.channels.traceLayer')}> <Row label={t('admin.channels.traceLayer')}>{layerSummary(data, t)}</Row>
{data.layerName <Row label={t('admin.channels.traceSlot')}>{slotSummary(data, t)}</Row>
? `${data.layerName}${data.layerPriority !== null ? ` (${t('admin.channels.priority')} ${data.layerPriority})` : ''}` <Row label={t('admin.channels.traceGroup')}>{groupSummary(data)}</Row>
: null}
</Row>
<Row label={t('admin.channels.traceSlot')}>
{data.slotTitle
? [
data.slotTitle,
data.slotWeekday === null
? t('admin.channels.everyDay')
: t(`admin.channels.weekdays.${data.slotWeekday}`),
data.slotTargetStart?.slice(0, 5),
data.slotDurationMinutes ? `${data.slotDurationMinutes} мин` : null,
data.driftMinutes !== 0
? t('admin.channels.traceDrift', { minutes: data.driftMinutes })
: null,
data.snapped ? t('admin.channels.traceSnapped') : null,
]
.filter(Boolean)
.join(' · ')
: null}
</Row>
<Row label={t('admin.channels.traceGroup')}>
{data.groupName
? `${data.groupName}${data.groupItemCount !== null ? ` (${data.groupItemCount})` : ''}`
: null}
</Row>
<Row label={t('admin.channels.traceCollection')}>{data.collectionName}</Row> <Row label={t('admin.channels.traceCollection')}>{data.collectionName}</Row>
<Row label={t('admin.channels.traceStrategy')}> <Row label={t('admin.channels.traceStrategy')}>{strategySummary(data, t)}</Row>
{data.strategy
? [
t(`admin.channels.strategies.${data.strategy}`),
data.cooldownDays
? t('admin.channels.traceCooldown', { days: data.cooldownDays })
: null,
data.candidatesAfterCooldown !== null
? t('admin.channels.traceCandidates', {
count: data.candidatesAfterCooldown,
})
: null,
]
.filter(Boolean)
.join(' · ')
: null}
</Row>
<Row label={t('admin.channels.traceJunction')}>{data.junctionName}</Row> <Row label={t('admin.channels.traceJunction')}>{data.junctionName}</Row>
</dl> </dl>
)} )}
@@ -96,6 +56,49 @@ export function EntryTraceDialog({
) )
} }
type Translate = ReturnType<typeof useTranslation>['t']
/** Склейка непустых частей строки трейса; пусто — значит строка не заполнена (покажем «—»). */
const joinParts = (parts: (string | null | undefined)[]) => parts.filter(Boolean).join(' · ') || null
function layerSummary(data: EntryTraceDto, t: Translate) {
if (!data.layerName) return null
const priority =
data.layerPriority !== null ? ` (${t('admin.channels.priority')} ${data.layerPriority})` : ''
return `${data.layerName}${priority}`
}
function slotSummary(data: EntryTraceDto, t: Translate) {
if (!data.slotTitle) return null
return joinParts([
data.slotTitle,
data.slotWeekday === null
? t('admin.channels.everyDay')
: t(`admin.channels.weekdays.${data.slotWeekday}`),
data.slotTargetStart?.slice(0, 5),
data.slotDurationMinutes ? `${data.slotDurationMinutes} мин` : null,
data.driftMinutes !== 0 ? t('admin.channels.traceDrift', { minutes: data.driftMinutes }) : null,
data.snapped ? t('admin.channels.traceSnapped') : null,
])
}
function groupSummary(data: EntryTraceDto) {
if (!data.groupName) return null
const count = data.groupItemCount !== null ? ` (${data.groupItemCount})` : ''
return `${data.groupName}${count}`
}
function strategySummary(data: EntryTraceDto, t: Translate) {
if (!data.strategy) return null
return joinParts([
t(`admin.channels.strategies.${data.strategy}`),
data.cooldownDays ? t('admin.channels.traceCooldown', { days: data.cooldownDays }) : null,
data.candidatesAfterCooldown !== null
? t('admin.channels.traceCandidates', { count: data.candidatesAfterCooldown })
: null,
])
}
function Row({ label, children }: { label: string; children: React.ReactNode }) { function Row({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<> <>
@@ -55,6 +55,9 @@ export function GridTab({
const [copyTargets, setCopyTargets] = useState<number[]>([]) const [copyTargets, setCopyTargets] = useState<number[]>([])
const [copyFromChannel, setCopyFromChannel] = useState('') const [copyFromChannel, setCopyFromChannel] = useState('')
const toggleCopyTarget = (day: number, checked: boolean) =>
setCopyTargets((current) => (checked ? [...current, day] : current.filter((d) => d !== day)))
const { data: channels } = useQuery({ queryKey: qk.channels.all, queryFn: listChannels }) const { data: channels } = useQuery({ queryKey: qk.channels.all, queryFn: listChannels })
const addLayerMutation = useMutation({ const addLayerMutation = useMutation({
@@ -318,13 +321,7 @@ export function GridTab({
<input <input
type="checkbox" type="checkbox"
checked={copyTargets.includes(day)} checked={copyTargets.includes(day)}
onChange={(e) => onChange={(e) => toggleCopyTarget(day, e.target.checked)}
setCopyTargets((current) =>
e.target.checked
? [...current, day]
: current.filter((d) => d !== day),
)
}
/> />
{t(`admin.channels.weekdays.${day}`)} {t(`admin.channels.weekdays.${day}`)}
</label> </label>
@@ -87,6 +87,9 @@ export function RulesCard({
onError, onError,
}) })
const removeWindow