feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,220 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import { format } from "date-fns"
|
||||
import { ru } from "date-fns/locale"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import {
|
||||
DateSelector,
|
||||
type DateSelectorValue,
|
||||
} from "@/components/reui/date-selector"
|
||||
import { isCalendarDate, isStatisticsDate } from "@/lib/statistics-range"
|
||||
import { DATE_SELECTOR_RU } from "@/components/statistics/date-selector-i18n"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
|
||||
/**
|
||||
* Период отчёта — DateSelector в Popover (c-date-selector-2).
|
||||
* @see https://reui.io/preview/base/components/c-date-selector-2
|
||||
* @see https://reui.io/docs/components/base/date-selector
|
||||
*/
|
||||
|
||||
export type PeriodPreset = "today" | "24h" | "7d" | "30d" | "month"
|
||||
|
||||
export interface DateRangeYmd {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
const PRESETS: { id: PeriodPreset; label: string }[] = [
|
||||
{ id: "today", label: "Сегодня" },
|
||||
{ id: "24h", label: "24 ч" },
|
||||
{ id: "7d", label: "7 д" },
|
||||
{ id: "30d", label: "30 д" },
|
||||
{ id: "month", label: "Месяц" },
|
||||
]
|
||||
|
||||
function pad2(n: number): string {
|
||||
return String(n).padStart(2, "0")
|
||||
}
|
||||
|
||||
export function formatYmd(d: Date): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`
|
||||
}
|
||||
|
||||
export function parseYmd(s: string): Date | null {
|
||||
if (!isCalendarDate(s)) return null
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s)
|
||||
if (!m) return null
|
||||
const d = new Date(0)
|
||||
d.setFullYear(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
|
||||
d.setHours(0, 0, 0, 0)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
/** Значение периода — дата (YYYY-MM-DD) или полный ISO-таймстамп (пресет «24 ч»). */
|
||||
export function parseRangeDate(s: string): Date | null {
|
||||
if (!isStatisticsDate(s)) return null
|
||||
if (s.includes("T")) {
|
||||
const d = new Date(s)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
return parseYmd(s)
|
||||
}
|
||||
|
||||
export function rangeForPreset(preset: PeriodPreset, now = new Date()): DateRangeYmd {
|
||||
const to = formatYmd(now)
|
||||
if (preset === "today") return { from: to, to }
|
||||
if (preset === "24h") {
|
||||
// Последние 24 часа от сейчас: полный ISO, иначе date-only «вчера…сегодня» даёт до ~48 ч.
|
||||
return {
|
||||
from: new Date(now.getTime() - 24 * 3600_000).toISOString(),
|
||||
to: now.toISOString(),
|
||||
}
|
||||
}
|
||||
if (preset === "7d") {
|
||||
const from = new Date(now)
|
||||
from.setDate(from.getDate() - 6)
|
||||
return { from: formatYmd(from), to }
|
||||
}
|
||||
if (preset === "30d") {
|
||||
const from = new Date(now)
|
||||
from.setDate(from.getDate() - 29)
|
||||
return { from: formatYmd(from), to }
|
||||
}
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
return { from: formatYmd(start), to }
|
||||
}
|
||||
|
||||
export function dateSelectorToRange(value: DateSelectorValue): DateRangeYmd | null {
|
||||
if (value.period === "day") {
|
||||
if (value.operator === "between") {
|
||||
if (!value.startDate || !value.endDate) return null
|
||||
const a = formatYmd(value.startDate)
|
||||
const b = formatYmd(value.endDate)
|
||||
return a <= b ? { from: a, to: b } : { from: b, to: a }
|
||||
}
|
||||
if (value.startDate) {
|
||||
const d = formatYmd(value.startDate)
|
||||
return { from: d, to: d }
|
||||
}
|
||||
}
|
||||
if (value.period === "month" && value.year != null && value.month != null) {
|
||||
const start = new Date(value.year, value.month, 1)
|
||||
const end = new Date(value.year, value.month + 1, 0)
|
||||
return { from: formatYmd(start), to: formatYmd(end) }
|
||||
}
|
||||
if (value.period === "year" && value.year != null) {
|
||||
return { from: `${value.year}-01-01`, to: `${value.year}-12-31` }
|
||||
}
|
||||
if (value.period === "quarter" && value.year != null && value.quarter != null) {
|
||||
const startMonth = value.quarter * 3
|
||||
const start = new Date(value.year, startMonth, 1)
|
||||
const end = new Date(value.year, startMonth + 3, 0)
|
||||
return { from: formatYmd(start), to: formatYmd(end) }
|
||||
}
|
||||
if (value.period === "half-year" && value.year != null && value.halfYear != null) {
|
||||
const startMonth = value.halfYear * 6
|
||||
const start = new Date(value.year, startMonth, 1)
|
||||
const end = new Date(value.year, startMonth + 6, 0)
|
||||
return { from: formatYmd(start), to: formatYmd(end) }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function rangeToSelector(range: DateRangeYmd): DateSelectorValue {
|
||||
return {
|
||||
period: "day",
|
||||
operator: "between",
|
||||
startDate: parseRangeDate(range.from) ?? undefined,
|
||||
endDate: parseRangeDate(range.to) ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function formatRangeLabel(range: DateRangeYmd): string {
|
||||
const from = parseRangeDate(range.from)
|
||||
const to = parseRangeDate(range.to)
|
||||
if (!from || !to) return "Период"
|
||||
if (range.from.includes("T") || range.to.includes("T")) {
|
||||
return formatYmd(from) === formatYmd(to)
|
||||
? `${format(from, "d MMM yyyy HH:mm", { locale: ru })} – ${format(to, "HH:mm", { locale: ru })}`
|
||||
: `${format(from, "d MMM HH:mm", { locale: ru })} – ${format(to, "d MMM HH:mm", { locale: ru })}`
|
||||
}
|
||||
if (range.from === range.to) return format(from, "d MMM yyyy", { locale: ru })
|
||||
return `${format(from, "d MMM", { locale: ru })} – ${format(to, "d MMM yyyy", { locale: ru })}`
|
||||
}
|
||||
|
||||
export function PeriodSelector({
|
||||
range,
|
||||
onChange,
|
||||
}: {
|
||||
range: DateRangeYmd
|
||||
onChange: (next: DateRangeYmd) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const selectorValue = useMemo(() => rangeToSelector(range), [range])
|
||||
|
||||
const handleSelectorChange = useCallback(
|
||||
(value: DateSelectorValue) => {
|
||||
const next = dateSelectorToRange(value)
|
||||
if (!next) return
|
||||
if (next.from === range.from && next.to === range.to) return
|
||||
onChange(next)
|
||||
setOpen(false)
|
||||
},
|
||||
[onChange, range.from, range.to],
|
||||
)
|
||||
|
||||
const activePreset = PRESETS.find((p) => {
|
||||
// «24 ч» хранится ISO-таймстампами: сверяем разбором (длительность окна), а не строками.
|
||||
if (p.id === "24h") {
|
||||
const f = parseRangeDate(range.from)
|
||||
const t = parseRangeDate(range.to)
|
||||
return Boolean(f && t && Math.abs((t.getTime() - f.getTime()) - 24 * 3600_000) < 60_000)
|
||||
}
|
||||
const r = rangeForPreset(p.id)
|
||||
return r.from === range.from && r.to === range.to
|
||||
})?.id
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{PRESETS.map((p) => (
|
||||
<Button
|
||||
key={p.id}
|
||||
type="button"
|
||||
variant={activePreset === p.id ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => onChange(rangeForPreset(p.id))}
|
||||
>
|
||||
{p.label}
|
||||
</Button>
|
||||
))}
|
||||
<Popover open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button type="button" variant="outline" size="sm" className="min-w-40 justify-between" />
|
||||
}
|
||||
>
|
||||
<CalendarIcon className="size-3.5" />
|
||||
<span className="tabular-nums">{formatRangeLabel(range)}</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-auto min-w-[32rem] max-w-[min(100vw-2rem,42rem)] p-3">
|
||||
<DateSelector
|
||||
value={selectorValue}
|
||||
onChange={handleSelectorChange}
|
||||
allowRange
|
||||
defaultPeriodType="day"
|
||||
defaultFilterType="between"
|
||||
periodTypes={["day", "month", "year"]}
|
||||
showTwoMonths
|
||||
weekStartsOn={1}
|
||||
maxYear={2035}
|
||||
dayDateFormat="dd.MM.yyyy"
|
||||
i18n={DATE_SELECTOR_RU}
|
||||
className="sm:w-[470px]"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user