navigate(`/devices/${device.id}`)}>
| {device.id} |
diff --git a/mdm-front/src/pages/DevicesPage/components/DevicesDateRangePicker/DevicesDateRangePicker.scss b/mdm-front/src/pages/DevicesPage/components/DevicesDateRangePicker/DevicesDateRangePicker.scss
new file mode 100644
index 0000000..d5620c3
--- /dev/null
+++ b/mdm-front/src/pages/DevicesPage/components/DevicesDateRangePicker/DevicesDateRangePicker.scss
@@ -0,0 +1,246 @@
+@use '../../../../shared/styles/variables' as *;
+
+.devices-date-range {
+ position: relative;
+ width: 100%;
+}
+
+.devices-date-range__trigger {
+ width: 100%;
+ min-height: 34px;
+ padding: 0 10px;
+
+ border: none;
+ border-radius: 999px;
+ background: #eef1f6;
+
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ color: #4f5b73;
+ font-size: 14px;
+ font-weight: 500;
+ text-align: left;
+ cursor: pointer;
+
+ span {
+ min-width: 0;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &.is-open {
+ box-shadow: 0 0 0 2px rgba(3, 29, 154, 0.15);
+ }
+}
+
+.devices-date-range__clear,
+.devices-date-range__chevron {
+ flex: 0 0 auto;
+ color: $blue;
+}
+
+.devices-date-range__popover {
+ position: absolute;
+ z-index: 60;
+ top: calc(100% + 8px);
+ left: -13px;
+
+ width: 100%;
+ padding: 12px;
+
+ border-radius: 18px;
+ background: #ffffff;
+ border: 1.5px solid $gray20;
+}
+
+.devices-date-range__calendar {
+ --rdp-accent-color: $blue;
+ --rdp-accent-background-color: #e8edff;
+ --rdp-day_button-border-radius: 12px;
+ --rdp-day_button-height: 34px;
+ --rdp-day_button-width: 34px;
+ --rdp-day-height: 36px;
+ --rdp-day-width: 36px;
+
+ margin: 0;
+ font-family: inherit;
+
+ .rdp-months {
+ max-width: 100%;
+ }
+
+ .rdp-month {
+ width: 100%;
+ }
+
+ .rdp-caption_label {
+ font-size: 14px;
+ font-weight: 700;
+ color: #151a24;
+ }
+
+ .rdp-nav {
+ gap: 4px;
+ }
+
+ .rdp-button_previous,
+ .rdp-button_next {
+ width: 28px;
+ height: 28px;
+ color: $blue;
+ border-radius: 8px;
+
+ &:hover {
+ background: #eef1f6;
+ }
+ }
+
+ .rdp-weekday {
+ color: #8c96aa;
+ font-size: 12px;
+ font-weight: 600;
+ }
+
+ .rdp-day_button {
+ color: #30394b;
+ font-size: 12px;
+ font-weight: 600;
+ }
+
+ .rdp-day_button:hover {
+ background: #eef1f6;
+ }
+
+ .rdp-selected .rdp-day_button {
+ background: $blue;
+ color: #ffffff;
+ }
+
+ .rdp-range_middle .rdp-day_button {
+ background: #e8edff;
+ color: $blue;
+ border-radius: 0;
+ }
+
+ .rdp-range_start .rdp-day_button,
+ .rdp-range_end .rdp-day_button {
+ background: $blue;
+ color: #ffffff;
+ border-radius: 12px;
+ }
+
+ .rdp-outside {
+ opacity: 0.35;
+ }
+}
+
+.devices-date-range__time {
+ margin-top: 12px;
+
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+
+ label {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+
+ span {
+ color: #738098;
+ font-size: 11px;
+ font-weight: 600;
+ }
+
+ input {
+ height: 32px;
+ padding: 0 10px;
+
+ border: none;
+ outline: none;
+ border-radius: 10px;
+ background: #eef1f6;
+
+ color: #30394b;
+ font-size: 13px;
+ font-weight: 600;
+ font-family: inherit;
+ }
+ }
+}
+
+.devices-date-range__presets {
+ margin-top: 10px;
+
+ display: flex;
+ gap: 6px;
+
+ button {
+ height: 28px;
+ padding: 0 10px;
+
+ border: none;
+ border-radius: 999px;
+ background: #eef1f6;
+
+ color: #4f5b73;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+
+ &:hover {
+ background: #e8edff;
+ color: $blue;
+ }
+ }
+}
+
+.devices-date-range__footer {
+ margin-top: 12px;
+ padding-top: 10px;
+ border-top: 1px solid #e3e8f0;
+
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ p {
+ margin: 0;
+ color: #738098;
+ font-size: 12px;
+ font-weight: 500;
+ }
+}
+
+.devices-date-range__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.devices-date-range__reset,
+.devices-date-range__apply {
+ height: 32px;
+ padding: 0 12px;
+
+ border: none;
+ border-radius: 10px;
+
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.devices-date-range__reset {
+ background: #eef1f6;
+ color: #4f5b73;
+}
+
+.devices-date-range__apply {
+ background: $blue;
+ color: #ffffff;
+}
\ No newline at end of file
diff --git a/mdm-front/src/pages/DevicesPage/components/DevicesDateRangePicker/DevicesDateRangePicker.tsx b/mdm-front/src/pages/DevicesPage/components/DevicesDateRangePicker/DevicesDateRangePicker.tsx
new file mode 100644
index 0000000..ca97b44
--- /dev/null
+++ b/mdm-front/src/pages/DevicesPage/components/DevicesDateRangePicker/DevicesDateRangePicker.tsx
@@ -0,0 +1,223 @@
+import { useEffect, useRef, useState } from 'react'
+import { DayPicker } from 'react-day-picker'
+import type { DateRange } from 'react-day-picker'
+import { ru } from 'date-fns/locale'
+import { format } from 'date-fns'
+import { CalendarDays, ChevronDown, X } from 'lucide-react'
+
+import 'react-day-picker/style.css'
+import './DevicesDateRangePicker.scss'
+
+export type DevicesDateRangePickerValue = {
+ from: Date | null
+ to: Date | null
+ fromTime: string
+ toTime: string
+}
+
+type DevicesDateRangePickerProps = {
+ value: DevicesDateRangePickerValue
+ onChange: (value: DevicesDateRangePickerValue) => void
+}
+
+function formatDate(date: Date | null) {
+ if (!date) return ''
+
+ return format(date, 'dd.MM.yyyy', {
+ locale: ru,
+ })
+}
+
+function getLabel(value: DevicesDateRangePickerValue) {
+ if (!value.from && !value.to) {
+ return 'Выберите период'
+ }
+
+ if (value.from && !value.to) {
+ return `${formatDate(value.from)}, ${value.fromTime} — ...`
+ }
+
+ return `${formatDate(value.from)}, ${value.fromTime} — ${formatDate(value.to)}, ${value.toTime}`
+}
+
+export function DevicesDateRangePicker({
+ value,
+ onChange,
+}: DevicesDateRangePickerProps) {
+ const [isOpen, setIsOpen] = useState(false)
+ const rootRef = useRef(null)
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (!rootRef.current) return
+
+ if (!rootRef.current.contains(event.target as Node)) {
+ setIsOpen(false)
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside)
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ }
+ }, [])
+
+ const selectedRange: DateRange | undefined =
+ value.from || value.to
+ ? {
+ from: value.from ?? undefined,
+ to: value.to ?? undefined,
+ }
+ : undefined
+
+ function handleSelect(range: DateRange | undefined) {
+ onChange({
+ ...value,
+ from: range?.from ?? null,
+ to: range?.to ?? null,
+ })
+ }
+
+ function handleReset() {
+ onChange({
+ from: null,
+ to: null,
+ fromTime: '07:00',
+ toTime: '16:00',
+ })
+ }
+
+ function handleToday() {
+ const today = new Date()
+
+ onChange({
+ from: today,
+ to: today,
+ fromTime: '00:00',
+ toTime: '23:59',
+ })
+ }
+
+ function handleWeek() {
+ const today = new Date()
+ const start = new Date()
+
+ start.setDate(today.getDate() - 7)
+
+ onChange({
+ from: start,
+ to: today,
+ fromTime: '00:00',
+ toTime: '23:59',
+ })
+ }
+
+ return (
+
+
+
+ {isOpen && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {value.from && value.to
+ ? `Выбрано: ${formatDate(value.from)} — ${formatDate(value.to)}`
+ : 'Период не выбран'}
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.scss b/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.scss
index 75b44b1..4cdbce2 100644
--- a/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.scss
+++ b/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.scss
@@ -35,7 +35,7 @@
.devices-filter-item {
border-radius: 14px;
background: #ffffff;
- overflow: hidden;
+ overflow: visible;
}
.devices-filter-item__header {
@@ -43,9 +43,9 @@
}
.devices-filter-item__trigger {
- width: 100%;
+ width: calc(100% - 32px);
min-height: 44px;
- padding: 0 14px;
+ margin: 0 16px;
border: none;
border-bottom: 1px solid transparent;
@@ -56,14 +56,14 @@
justify-content: space-between;
gap: 12px;
- color: #30394b;
- font-size: 15px;
+ color: black;
+ font-size: 18px;
font-weight: 500;
cursor: pointer;
&[data-state='open'] {
- color: #031d9a;
- border-bottom-color: #e3e8f0;
+ //color: $blue;
+ border-bottom-color: $gray20;
}
}
@@ -77,7 +77,7 @@
}
.devices-filter-item__content {
- overflow: hidden;
+ //overflow: hidden;
&[data-state='open'] {
animation: filterSlideDown 0.2s ease;
@@ -89,7 +89,7 @@
}
.devices-filter-item__inner {
- padding: 12px 14px 10px;
+ padding: 12px 16px 16px;
}
.devices-period {
@@ -101,7 +101,7 @@
.devices-period__divider {
width: 12px;
height: 1px;
- background: #8c96aa;
+ background: $gray50;
flex: 0 0 12px;
}
@@ -115,14 +115,14 @@
border-radius: 999px;
background: #eef1f6;
- color: #4f5b73;
- font-size: 12px;
+ color: $gray50;
+ font-size: 16px;
font-weight: 400;
cursor: pointer;
}
.devices-filter-reset {
- margin: 8px 0 0 auto;
+ margin: 12px 0 0 auto;
padding: 0;
display: block;
@@ -130,8 +130,8 @@
border: none;
background: transparent;
- color: #031d9a;
- font-size: 12px;
+ color: $blue;
+ font-size: 14px;
font-weight: 400;
text-decoration: underline;
cursor: pointer;
@@ -145,18 +145,24 @@
min-height: 28px;
- color: #30394b;
+ color: $gray50;
font-size: 13px;
& + & {
- margin-top: 8px;
+ margin-top: 12px;
}
}
.devices-filter-row__label {
- color: #30394b;
- font-size: 13px;
- font-weight: 400;
+ color: $gray50;
+ font-size: 16px;
+ font-weight: 500;
+ text-align: left;
+}
+
+.devices-checkbox-list{
+ display: flex;
+ flex-direction: column;
}
.devices-radio,
@@ -166,7 +172,8 @@
gap: 6px;
color: #30394b;
- font-size: 13px;
+ font-size: 16px;
+ font-weight: 500;
cursor: pointer;
user-select: none;
diff --git a/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.tsx b/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.tsx
index 2bf267e..ce76032 100644
--- a/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.tsx
+++ b/mdm-front/src/pages/DevicesPage/components/DevicesFiltersPanel/DevicesFiltersPanel.tsx
@@ -1,13 +1,11 @@
import { useState } from 'react'
-import DatePicker, { registerLocale } from 'react-datepicker'
-import { ru } from 'date-fns/locale/ru'
import * as Accordion from '@radix-ui/react-accordion'
import { ChevronDown } from 'lucide-react'
import './Datepicker.scss'
+import { DevicesDateRangePicker, type DevicesDateRangePickerValue } from '../DevicesDateRangePicker/DevicesDateRangePicker'
import './DevicesFiltersPanel.scss'
-registerLocale('ru', ru)
type DevicesFiltersPanelProps = {
isOpen: boolean
@@ -15,13 +13,12 @@ type DevicesFiltersPanelProps = {
export function DevicesFiltersPanel({ isOpen }: DevicesFiltersPanelProps) {
- const [startDate, setStartDate] = useState(
- new Date(2026, 3, 20, 7, 0),
- )
-
- const [endDate, setEndDate] = useState(
- new Date(2026, 3, 22, 16, 0),
- )
+ const [workPeriod, setWorkPeriod] = useState({
+ from: new Date(2026, 3, 20),
+ to: new Date(2026, 3, 22),
+ fromTime: '07:00',
+ toTime: '16:00',
+ })
return (
|