ручной рефакторинг
This commit is contained in:
@@ -31,7 +31,7 @@ module.exports = {
|
||||
pathGroups: [
|
||||
{ pattern: 'react', group: 'builtin' },
|
||||
{ pattern: '~shared/**', group: 'internal', position: 'before' },
|
||||
{ pattern: '~entitiy/**', group: 'internal', position: 'before' },
|
||||
{ pattern: '~entity/**', group: 'internal', position: 'before' },
|
||||
{ pattern: '~feature/**', group: 'internal', position: 'before' },
|
||||
{ pattern: '~widget/**', group: 'internal', position: 'before' },
|
||||
{ pattern: '~page/**', group: 'internal', position: 'before' },
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
-- Схема БД для элемента "Условия поверки"
|
||||
|
||||
-- Таблица лабораторий
|
||||
CREATE TABLE laboratories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(50) UNIQUE NOT NULL, -- Уникальный код лаборатории
|
||||
description TEXT,
|
||||
address TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица типов условий поверки (температура, влажность, давление и т.д.)
|
||||
CREATE TABLE condition_types (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL, -- "Температура", "Влажность", "Давление"
|
||||
unit VARCHAR(20), -- "°C", "%", "кПа"
|
||||
data_type VARCHAR(20) DEFAULT 'number', -- number, string, boolean
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Связь лаборатории с типами условий (какие условия отслеживает каждая лаборатория)
|
||||
CREATE TABLE laboratory_condition_types (
|
||||
id SERIAL PRIMARY KEY,
|
||||
laboratory_id INTEGER REFERENCES laboratories(id) ON DELETE CASCADE,
|
||||
condition_type_id INTEGER REFERENCES condition_types(id) ON DELETE CASCADE,
|
||||
is_required BOOLEAN DEFAULT false, -- Обязательно ли это условие для данной лаборатории
|
||||
default_value VARCHAR(100), -- Значение по умолчанию
|
||||
min_value DECIMAL(10,3), -- Минимальное значение (для валидации)
|
||||
max_value DECIMAL(10,3), -- Максимальное значение (для валидации)
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(laboratory_id, condition_type_id)
|
||||
);
|
||||
|
||||
-- Таблица для сохранения условий по дням
|
||||
CREATE TABLE daily_conditions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
laboratory_id INTEGER REFERENCES laboratories(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
conditions JSONB NOT NULL, -- JSON объект с условиями: {"temperature": 23.5, "humidity": 45.2, "pressure": 101.3}
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100), -- Кто создал запись
|
||||
UNIQUE(laboratory_id, date) -- Одна запись условий на день для лаборатории
|
||||
);
|
||||
|
||||
-- Индексы для оптимизации
|
||||
CREATE INDEX idx_daily_conditions_lab_date ON daily_conditions(laboratory_id, date);
|
||||
CREATE INDEX idx_daily_conditions_date ON daily_conditions(date);
|
||||
CREATE INDEX idx_laboratory_condition_types_lab ON laboratory_condition_types(laboratory_id);
|
||||
|
||||
-- Вставка начальных данных
|
||||
|
||||
-- Типы условий
|
||||
INSERT INTO condition_types (name, unit, data_type, description) VALUES
|
||||
('Температура', '°C', 'number', 'Температура окружающей среды'),
|
||||
('Влажность', '%', 'number', 'Относительная влажность воздуха'),
|
||||
('Атмосферное давление', 'кПа', 'number', 'Атмосферное давление'),
|
||||
('Давление среды', 'МПа', 'number', 'Давление рабочей среды'),
|
||||
('Напряжение сети', 'В', 'number', 'Напряжение питающей сети'),
|
||||
('Частота сети', 'Гц', 'number', 'Частота питающей сети');
|
||||
|
||||
-- Примеры лабораторий
|
||||
INSERT INTO laboratories (name, code, description) VALUES
|
||||
('Лаборатория температуры', 'TEMP_LAB', 'Лаборатория поверки температурных приборов'),
|
||||
('Лаборатория давления', 'PRESS_LAB', 'Лаборатория поверки приборов давления'),
|
||||
('Электрическая лаборатория', 'ELEC_LAB', 'Лаборатория поверки электрических приборов');
|
||||
|
||||
-- Настройка условий для лабораторий
|
||||
-- Лаборатория температуры
|
||||
INSERT INTO laboratory_condition_types (laboratory_id, condition_type_id, is_required, default_value, min_value, max_value) VALUES
|
||||
(1, 1, true, '20', 15, 30), -- Температура
|
||||
(1, 2, true, '50', 30, 80), -- Влажность
|
||||
(1, 3, false, '101.3', 80, 120); -- Атмосферное давление
|
||||
|
||||
-- Лаборатория давления
|
||||
INSERT INTO laboratory_condition_types (laboratory_id, condition_type_id, is_required, default_value, min_value, max_value) VALUES
|
||||
(2, 1, true, '23', 18, 28), -- Температура
|
||||
(2, 2, false, '45', 20, 70), -- Влажность
|
||||
(2, 3, true, '101.3', 90, 110), -- Атмосферное давление
|
||||
(2, 4, true, '0.1', 0, 100); -- Давление среды
|
||||
|
||||
-- Электрическая лаборатория
|
||||
INSERT INTO laboratory_condition_types (laboratory_id, condition_type_id, is_required, default_value, min_value, max_value) VALUES
|
||||
(3, 1, true, '22', 20, 25), -- Температура
|
||||
(3, 2, true, '60', 40, 70), -- Влажность
|
||||
(3, 5, true, '220', 198, 242), -- Напряжение сети
|
||||
(3, 6, true, '50', 49.5, 50.5); -- Частота сети
|
||||
@@ -76,6 +76,7 @@
|
||||
"react-redux": "^8.1.1",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typescript": "^4.4.4",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { FC, useEffect } from 'react'
|
||||
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { CategoryProvider } from '@/entitiy/template/model/CategoryContext'
|
||||
import { TemplateProvider } from '@/entitiy/template/model/TemplateContext'
|
||||
import { CategoryProvider } from '@/entity/template/model/CategoryContext'
|
||||
import { TemplateProvider } from '@/entity/template/model/TemplateContext'
|
||||
import {
|
||||
setGlobalToastContext,
|
||||
ToastProvider,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
import { store } from './store'
|
||||
|
||||
import { initializeElementRegistry } from '@/entitiy/element/model/interface'
|
||||
import { initializeElementRegistry } from '@/entity/element/model/interface'
|
||||
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { Label } from '@/component/ui/label'
|
||||
import { useCategoryContext } from '@/entitiy/template/model/CategoryContext'
|
||||
import { useCategoryContext } from '@/entity/template/model/CategoryContext'
|
||||
import { Attribute } from '@/type/template'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
getAvailableElementTypes,
|
||||
getElementDefinition,
|
||||
} from '@/entitiy/element/model/interface'
|
||||
} from '@/entity/element/model/interface'
|
||||
import { useToast } from '@/lib/hooks/useToast'
|
||||
import { generateDefaultLayout } from '@/lib/utils'
|
||||
import {
|
||||
@@ -47,6 +47,14 @@ import { Separator } from '../ui/separator'
|
||||
import { ElementFormulaBar } from './ElementFormulaBar'
|
||||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||
|
||||
export function getDefaultLayoutSettings(): FormLayoutSettings {
|
||||
return {
|
||||
gridSize: 20,
|
||||
showGrid: true,
|
||||
snapToGrid: true,
|
||||
}
|
||||
}
|
||||
|
||||
interface ElementFormProps {
|
||||
formData: Partial<TemplateElement>
|
||||
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
|
||||
import {
|
||||
getFileData,
|
||||
getLatestFileForTemplate,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SidebarHeader,
|
||||
SidebarSeparator,
|
||||
} from '@/component/ui/sidebar'
|
||||
import { useCategoryContext } from '@/entitiy/template/model/CategoryContext'
|
||||
import { useCategoryContext } from '@/entity/template/model/CategoryContext'
|
||||
import { Template } from '@/type/template'
|
||||
import { Filter, X } from 'lucide-react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
|
||||
import { Template } from '@/type/template'
|
||||
import { Circle, CircleCheck, FileText, Plus, Trash2 } from 'lucide-react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Edit, Grid, Move, Trash2 } from 'lucide-react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Rnd } from 'react-rnd'
|
||||
import { getElementDefinition } from '../../entitiy/element/model/interface'
|
||||
import { getElementDefinition } from '../../entity/element/model/interface'
|
||||
import {
|
||||
ElementLayout,
|
||||
FormLayoutSettings,
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Calendar } from '@/component/ui/calendar'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/component/ui/popover'
|
||||
import {
|
||||
AlertCircle,
|
||||
Building2,
|
||||
Calendar as CalendarIcon,
|
||||
CheckCircle,
|
||||
Plus,
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
useDailyConditions,
|
||||
useLaboratories,
|
||||
useLaboratoryConditions,
|
||||
} from './hooks'
|
||||
import { VerificationConditionsConfig } from './types'
|
||||
|
||||
interface VerificationConditionsPreviewProps {
|
||||
config: VerificationConditionsConfig
|
||||
onChange?: (config: VerificationConditionsConfig) => void
|
||||
}
|
||||
|
||||
export function VerificationConditionsPreview({
|
||||
config,
|
||||
onChange,
|
||||
}: VerificationConditionsPreviewProps) {
|
||||
const [selectedDate, setSelectedDate] = useState(
|
||||
config.selectedDate || new Date().toISOString().split('T')[0]
|
||||
)
|
||||
const [calendarOpen, setCalendarOpen] = useState(false)
|
||||
|
||||
const { data: laboratories = [] } = useLaboratories()
|
||||
const { data: labData } = useLaboratoryConditions(config.selectedLaboratoryId)
|
||||
const { data: dailyCondition, isLoading } = useDailyConditions(
|
||||
config.selectedLaboratoryId,
|
||||
selectedDate
|
||||
)
|
||||
|
||||
const selectedLab = laboratories.find(
|
||||
lab => lab.id === config.selectedLaboratoryId
|
||||
)
|
||||
|
||||
const hasConditions =
|
||||
dailyCondition && Object.keys(dailyCondition.conditions).length > 0
|
||||
|
||||
const handleDateChange = (date: string) => {
|
||||
setSelectedDate(date)
|
||||
if (onChange) {
|
||||
onChange({ ...config, selectedDate: date })
|
||||
}
|
||||
}
|
||||
|
||||
const handleCalendarSelect = (date: Date | undefined) => {
|
||||
if (date) {
|
||||
const dateString = date.toISOString().split('T')[0]
|
||||
setSelectedDate(dateString)
|
||||
setCalendarOpen(false)
|
||||
if (onChange) {
|
||||
onChange({ ...config, selectedDate: dateString })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.selectedLaboratoryId) {
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-3">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Building2 className="mx-auto mb-1 h-4 w-4 opacity-50" />
|
||||
<p className="text-xs">Лаборатория не выбрана</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (!labData) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-3">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<div className="mx-auto mb-1 h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<p className="text-xs">Загрузка...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="px-3 pb-1 pt-2">
|
||||
<CardTitle className="flex items-center gap-1.5 text-sm">
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
<span className="truncate">{selectedLab?.name || 'Лаборатория'}</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2 px-3 pb-3">
|
||||
{/* Выбор даты */}
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-6 justify-start border-none bg-muted/50 px-2 text-xs font-normal hover:bg-muted/70"
|
||||
disabled={!onChange}
|
||||
>
|
||||
{new Date(selectedDate).toLocaleDateString('ru-RU')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={new Date(selectedDate)}
|
||||
onSelect={handleCalendarSelect}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Статус и данные */}
|
||||
<div className="border-t pt-2">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<span>Загрузка...</span>
|
||||
</div>
|
||||
) : hasConditions ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-xs text-green-600">
|
||||
<CheckCircle className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">Данные внесены</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{labData.conditionTypes.map(conditionType => {
|
||||
const conditionValue =
|
||||
dailyCondition!.conditions[conditionType.id]
|
||||
return (
|
||||
<div
|
||||
key={conditionType.id}
|
||||
className="flex items-center justify-between rounded bg-muted/30 px-2 py-1 text-xs"
|
||||
>
|
||||
<span className="truncate font-medium">
|
||||
{conditionType.name}
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-muted-foreground">
|
||||
{conditionValue || '—'}
|
||||
{conditionValue && conditionType.unit && (
|
||||
<span className="ml-1">{conditionType.unit}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-xs text-orange-600">
|
||||
<AlertCircle className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">Данные не внесены</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled
|
||||
size="sm"
|
||||
className="h-6 w-full text-xs"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="mr-1.5 h-3 w-3" />
|
||||
Внести данные
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/component/ui/dialog'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { Label } from '@/component/ui/label'
|
||||
import { CellsBadge } from '@/entitiy/element/ui/cellsBadge'
|
||||
import {
|
||||
AlertCircle,
|
||||
Building2,
|
||||
Calendar as CalendarIcon,
|
||||
CheckCircle,
|
||||
Plus,
|
||||
Settings,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
useDailyConditions,
|
||||
useLaboratories,
|
||||
useLaboratoryConditions,
|
||||
} from './hooks'
|
||||
import {
|
||||
VerificationConditionsConfig,
|
||||
VerificationConditionsValue,
|
||||
} from './types'
|
||||
|
||||
// Утилиты для работы с localStorage
|
||||
const getLocalStorageKey = (type: string, id: string) => `${type}_${id}`
|
||||
|
||||
const saveToLocalStorage = (key: string, value: string) => {
|
||||
try {
|
||||
localStorage.setItem(key, value)
|
||||
} catch (error) {
|
||||
console.warn('Failed to save to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getFromLocalStorage = (key: string): string => {
|
||||
try {
|
||||
return localStorage.getItem(key) || ''
|
||||
} catch (error) {
|
||||
console.warn('Failed to get from localStorage:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
interface VerificationConditionsRenderProps {
|
||||
config: VerificationConditionsConfig & { id?: string }
|
||||
value?: VerificationConditionsValue
|
||||
onChange?: (value: VerificationConditionsValue) => void
|
||||
onSave?: (value: VerificationConditionsValue) => Promise<void>
|
||||
}
|
||||
|
||||
export function VerificationConditionsRender({
|
||||
config,
|
||||
value,
|
||||
onChange,
|
||||
onSave,
|
||||
}: VerificationConditionsRenderProps) {
|
||||
// Инициализация даты с учетом localStorage
|
||||
const getInitialDate = () => {
|
||||
if (value?.date) return value.date
|
||||
if (config.selectedDate) return config.selectedDate
|
||||
|
||||
// Попытка загрузить из localStorage
|
||||
if (config.id) {
|
||||
const key = getLocalStorageKey('verification_conditions_date', config.id)
|
||||
const savedDate = getFromLocalStorage(key)
|
||||
if (savedDate) return savedDate
|
||||
}
|
||||
|
||||
return new Date().toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState(getInitialDate)
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
const [editingConditions, setEditingConditions] = useState<
|
||||
Record<string, string>
|
||||
>({})
|
||||
|
||||
const { data: laboratories = [] } = useLaboratories()
|
||||
const { data: labData } = useLaboratoryConditions(config.selectedLaboratoryId)
|
||||
const {
|
||||
data: dailyCondition,
|
||||
saveConditions,
|
||||
isSaving,
|
||||
} = useDailyConditions(config.selectedLaboratoryId, selectedDate)
|
||||
|
||||
const selectedLab = laboratories.find(
|
||||
lab => lab.id === config.selectedLaboratoryId
|
||||
)
|
||||
|
||||
const hasConditions =
|
||||
dailyCondition && Object.keys(dailyCondition.conditions).length > 0
|
||||
|
||||
// Инициализация данных при загрузке из БД, только если данные еще не заполнены
|
||||
useEffect(() => {
|
||||
if (
|
||||
dailyCondition &&
|
||||
hasConditions &&
|
||||
onChange &&
|
||||
(!value?.conditions || Object.keys(value.conditions).length === 0)
|
||||
) {
|
||||
onChange({
|
||||
date: selectedDate,
|
||||
conditions: dailyCondition.conditions,
|
||||
})
|
||||
}
|
||||
}, [dailyCondition, hasConditions, onChange, selectedDate, value?.conditions])
|
||||
|
||||
// Синхронизация selectedDate с внешним value.date
|
||||
useEffect(() => {
|
||||
if (value?.date && value.date !== selectedDate) {
|
||||
setSelectedDate(value.date)
|
||||
}
|
||||
}, [value?.date, selectedDate])
|
||||
|
||||
// Инициализация значения если оно не установлено
|
||||
useEffect(() => {
|
||||
if (!value && onChange) {
|
||||
onChange({
|
||||
date: selectedDate,
|
||||
conditions: {},
|
||||
})
|
||||
}
|
||||
}, [value, onChange, selectedDate])
|
||||
|
||||
const handleDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newDate = event.target.value
|
||||
setSelectedDate(newDate)
|
||||
|
||||
// Сохранение в localStorage
|
||||
if (config.id) {
|
||||
const key = getLocalStorageKey('verification_conditions_date', config.id)
|
||||
saveToLocalStorage(key, newDate)
|
||||
}
|
||||
|
||||
// Обновляем значение даты в value
|
||||
if (onChange) {
|
||||
onChange({
|
||||
date: newDate,
|
||||
conditions: value?.conditions || {},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenEditDialog = (isCreating = false) => {
|
||||
if (isCreating) {
|
||||
const emptyConditions: Record<string, string> = {}
|
||||
labData?.conditionTypes.forEach(conditionType => {
|
||||
emptyConditions[conditionType.id] = ''
|
||||
})
|
||||
setEditingConditions(emptyConditions)
|
||||
} else {
|
||||
setEditingConditions(dailyCondition?.conditions || {})
|
||||
}
|
||||
setEditDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSaveConditions = async () => {
|
||||
if (!config.selectedLaboratoryId || !selectedDate || !labData) return
|
||||
|
||||
try {
|
||||
await saveConditions(editingConditions)
|
||||
|
||||
if (onChange) {
|
||||
onChange({
|
||||
date: selectedDate,
|
||||
conditions: editingConditions,
|
||||
})
|
||||
}
|
||||
|
||||
setEditDialogOpen(false)
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConditionChange = (conditionId: string, value: string) => {
|
||||
setEditingConditions(prev => ({
|
||||
...prev,
|
||||
[conditionId]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
if (!config.selectedLaboratoryId) {
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-3">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<AlertCircle className="mx-auto mb-1 h-4 w-4 opacity-50" />
|
||||
<p className="text-xs">Лаборатория не настроена</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (!labData) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-3">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<div className="mx-auto mb-1 h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<p className="text-xs">Загрузка...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Бейдж ячеек */}
|
||||
<CellsBadge targetCells={config.targetCells || []} />
|
||||
|
||||
<Card>
|
||||
<CardHeader className="px-3 pb-1 pt-2">
|
||||
<CardTitle className="flex items-center gap-1.5 text-sm">
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
<span className="truncate">
|
||||
{selectedLab?.name || 'Лаборатория'}
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-2 px-3 pb-3">
|
||||
{/* Выбор даты */}
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={handleDateChange}
|
||||
lang="ru"
|
||||
className="h-6 border-none bg-muted/50 px-2 text-xs font-normal hover:bg-muted/70 focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Статус и данные */}
|
||||
<div className="border-t pt-2">
|
||||
{hasConditions ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5 text-xs text-green-600">
|
||||
<CheckCircle className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">Данные внесены</span>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={() => handleOpenEditDialog(false)}
|
||||
>
|
||||
<Settings className="mr-1 h-3 w-3" />
|
||||
Настроить
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">
|
||||
Условия поверки
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<div>{selectedLab?.name}</div>
|
||||
<div>
|
||||
{new Date(selectedDate).toLocaleDateString('ru-RU')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{labData.conditionTypes.map(conditionType => (
|
||||
<div key={conditionType.id} className="space-y-1">
|
||||
<Label className="text-xs font-medium">
|
||||
{conditionType.name}
|
||||
{conditionType.unit && (
|
||||
<span className="ml-1 font-normal text-muted-foreground">
|
||||
({conditionType.unit})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
value={
|
||||
editingConditions[conditionType.id] || ''
|
||||
}
|
||||
onChange={e =>
|
||||
handleConditionChange(
|
||||
conditionType.id,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="Значение"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSaveConditions}
|
||||
disabled={isSaving}
|
||||
size="sm"
|
||||
className="h-7 flex-1 text-xs"
|
||||
>
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditDialogOpen(false)}
|
||||
size="sm"
|
||||
className="h-7 flex-1 text-xs"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{labData.conditionTypes.map(conditionType => {
|
||||
const conditionValue =
|
||||
dailyCondition!.conditions[conditionType.id]
|
||||
return (
|
||||
<div
|
||||
key={conditionType.id}
|
||||
className="flex items-center justify-between rounded bg-muted/30 px-2 py-1 text-xs"
|
||||
>
|
||||
<span className="truncate font-medium">
|
||||
{conditionType.name}
|
||||
</span>
|
||||
<span className="ml-2 font-mono text-muted-foreground">
|
||||
{conditionValue || '—'}
|
||||
{conditionValue && conditionType.unit && (
|
||||
<span className="ml-1">{conditionType.unit}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-xs text-orange-600">
|
||||
<AlertCircle className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">Данные не внесены</span>
|
||||
</div>
|
||||
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => handleOpenEditDialog(true)}
|
||||
disabled={isSaving}
|
||||
size="sm"
|
||||
className="h-6 w-full text-xs"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="mr-1.5 h-3 w-3" />
|
||||
Внести данные
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">
|
||||
Условия поверки
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<div>{selectedLab?.name}</div>
|
||||
<div>
|
||||
{new Date(selectedDate).toLocaleDateString('ru-RU')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{labData.conditionTypes.map(conditionType => (
|
||||
<div key={conditionType.id} className="space-y-1">
|
||||
<Label className="text-xs font-medium">
|
||||
{conditionType.name}
|
||||
{conditionType.unit && (
|
||||
<span className="ml-1 font-normal text-muted-foreground">
|
||||
({conditionType.unit})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
value={editingConditions[conditionType.id] || ''}
|
||||
onChange={e =>
|
||||
handleConditionChange(
|
||||
conditionType.id,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="Значение"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSaveConditions}
|
||||
disabled={isSaving}
|
||||
size="sm"
|
||||
className="h-7 flex-1 text-xs"
|
||||
>
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditDialogOpen(false)}
|
||||
size="sm"
|
||||
className="h-7 flex-1 text-xs"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { CellsBadge } from '@/entitiy/element/ui/cellsBadge'
|
||||
import { ElementDefinition } from '@/entity/element/model/interface'
|
||||
import { CellsBadge } from '@/entity/element/ui/cellsBadge'
|
||||
import { ElementOption } from '@/type/template'
|
||||
import { Plus, Square, Trash2 } from 'lucide-react'
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/component/ui/select'
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { CellsBadge } from '@/entitiy/element/ui/cellsBadge'
|
||||
import { ElementDefinition } from '@/entity/element/model/interface'
|
||||
import { CellsBadge } from '@/entity/element/ui/cellsBadge'
|
||||
import { useToast } from '@/lib/hooks/useToast'
|
||||
import { ElementOption } from '@/type/template'
|
||||
import { ChevronDown, Plus, Trash2 } from 'lucide-react'
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Badge } from '@/component/ui/badge'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Label } from '@/component/ui/label'
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { CellsBadge } from '@/entitiy/element/ui/cellsBadge'
|
||||
import { ElementDefinition } from '@/entity/element/model/interface'
|
||||
import { CellsBadge } from '@/entity/element/ui/cellsBadge'
|
||||
import { CellTarget } from '@/type/template'
|
||||
import { FileText, Settings, Weight } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Checkbox } from '@/component/ui/checkbox'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { CellsBadge } from '@/entitiy/element/ui/cellsBadge'
|
||||
import { ElementDefinition } from '@/entity/element/model/interface'
|
||||
import { CellsBadge } from '@/entity/element/ui/cellsBadge'
|
||||
import { Plus, Save, SaveOff, Type } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Card, CardContent } from '@/component/ui/card'
|
||||
import {
|
||||
AlertCircle,
|
||||
Building2,
|
||||
Calendar as CalendarIcon,
|
||||
Droplets,
|
||||
Gauge,
|
||||
Settings,
|
||||
Thermometer,
|
||||
} from 'lucide-react'
|
||||
import { VerificationConditionsConfig } from './types'
|
||||
|
||||
interface VerificationConditionsPreviewProps {
|
||||
config: VerificationConditionsConfig
|
||||
onChange?: (config: VerificationConditionsConfig) => void
|
||||
}
|
||||
|
||||
const ConditionSkeleton = ({
|
||||
index,
|
||||
icon: Icon,
|
||||
name,
|
||||
unit,
|
||||
}: {
|
||||
index: number
|
||||
icon: any
|
||||
name: string
|
||||
unit: string
|
||||
}) => (
|
||||
<div className="flex items-center gap-2 rounded-md bg-muted/30 p-1.5 text-sm">
|
||||
<div className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
||||
<Icon className="h-2.5 w-2.5 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium text-foreground">{name}</div>
|
||||
</div>
|
||||
<div className="h-5 shrink-0 rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
||||
— {unit}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Моковые условия поверки
|
||||
const mockConditions = [
|
||||
{ name: 'Температура', unit: '°C', icon: Thermometer },
|
||||
{ name: 'Влажность', unit: '%', icon: Droplets },
|
||||
{ name: 'Давление', unit: 'кПа', icon: Gauge },
|
||||
{ name: 'Освещенность', unit: 'лк', icon: AlertCircle },
|
||||
{ name: 'Вибрация', unit: 'м/с²', icon: AlertCircle },
|
||||
{ name: 'Магнитное поле', unit: 'А/м', icon: AlertCircle },
|
||||
{ name: 'Электромагнитное поле', unit: 'В/м', icon: AlertCircle },
|
||||
]
|
||||
|
||||
export function VerificationConditionsPreview({
|
||||
config,
|
||||
onChange,
|
||||
}: VerificationConditionsPreviewProps) {
|
||||
const conditionsToShow = mockConditions.slice(0, 7)
|
||||
const selectedDate =
|
||||
config.selectedDate || new Date().toISOString().split('T')[0]
|
||||
|
||||
if (!config.selectedLaboratoryId) {
|
||||
return (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-3">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Building2 className="mx-auto mb-1 h-4 w-4 opacity-50" />
|
||||
<p className="text-xs">Лаборатория не выбрана</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2">
|
||||
{/* Информация о лаборатории и дате */}
|
||||
<div className="flex items-center gap-2 rounded-md bg-muted/20 p-2 text-sm">
|
||||
<Building2 className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="font-medium text-muted-foreground">Лаборатория</span>
|
||||
<div className="ml-auto flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
<span>{new Date(selectedDate).toLocaleDateString('ru-RU')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Общий статус с кнопкой настроить */}
|
||||
<div className="flex items-center justify-between px-2 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-orange-600">
|
||||
<AlertCircle className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">Данные не внесены</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
<Settings className="mr-1 h-3 w-3" />
|
||||
Настроить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Моковые условия */}
|
||||
<div className="space-y-1">
|
||||
{conditionsToShow.map((condition, index) => (
|
||||
<ConditionSkeleton
|
||||
key={index}
|
||||
index={index}
|
||||
icon={condition.icon}
|
||||
name={condition.name}
|
||||
unit={condition.unit}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { Card, CardContent } from '@/component/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/component/ui/dialog'
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { Label } from '@/component/ui/label'
|
||||
import { CellsBadge } from '@/entity/element/ui/cellsBadge'
|
||||
import {
|
||||
AlertCircle,
|
||||
Building2,
|
||||
Calendar as CalendarIcon,
|
||||
CheckCircle,
|
||||
Droplets,
|
||||
Gauge,
|
||||
Settings,
|
||||
Thermometer,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
useDailyConditions,
|
||||
useLaboratories,
|
||||
useLaboratoryConditions,
|
||||
} from './hooks'
|
||||
import {
|
||||
VerificationConditionsConfig,
|
||||
VerificationConditionsValue,
|
||||
} from './types'
|
||||
|
||||
// Утилиты для работы с localStorage
|
||||
const getLocalStorageKey = (type: string, id: string) => `${type}_${id}`
|
||||
|
||||
const saveToLocalStorage = (key: string, value: string) => {
|
||||
try {
|
||||
localStorage.setItem(key, value)
|
||||
} catch (error) {
|
||||
console.warn('Failed to save to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getFromLocalStorage = (key: string): string => {
|
||||
try {
|
||||
return localStorage.getItem(key) || ''
|
||||
} catch (error) {
|
||||
console.warn('Failed to get from localStorage:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Иконки для разных типов условий
|
||||
const getConditionIcon = (name: string) => {
|
||||
const lowerName = name.toLowerCase()
|
||||
if (lowerName.includes('температур')) return Thermometer
|
||||
if (lowerName.includes('влажн')) return Droplets
|
||||
if (lowerName.includes('давлен')) return Gauge
|
||||
return AlertCircle
|
||||
}
|
||||
|
||||
const ConditionItem = ({
|
||||
name,
|
||||
value,
|
||||
unit,
|
||||
}: {
|
||||
name: string
|
||||
value?: string
|
||||
unit?: string
|
||||
}) => {
|
||||
const Icon = getConditionIcon(name)
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md bg-muted/30 p-1.5 text-sm">
|
||||
<div className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-primary/10">
|
||||
<Icon className="h-2.5 w-2.5 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium text-foreground">{name}</div>
|
||||
</div>
|
||||
<div className="h-5 shrink-0 rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
||||
{value || '—'} {unit || ''}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface VerificationConditionsRenderProps {
|
||||
config: VerificationConditionsConfig & { id?: string }
|
||||
value?: VerificationConditionsValue
|
||||
onChange?: (value: VerificationConditionsValue) => void
|
||||
onSave?: (value: VerificationConditionsValue) => Promise<void>
|
||||
}
|
||||
|
||||
export function VerificationConditionsRender({
|
||||
config,
|
||||
value,
|
||||
onChange,
|
||||
onSave,
|
||||
}: VerificationConditionsRenderProps) {
|
||||
// Инициализация даты с учетом localStorage
|
||||
const getInitialDate = () => {
|
||||
if (value?.date) return value.date
|
||||
if (config.selectedDate) return config.selectedDate
|
||||
|
||||
// Попытка загрузить из localStorage
|
||||
if (config.id) {
|
||||
const key = getLocalStorageKey('verification_conditions_date', config.id)
|
||||
const savedDate = getFromLocalStorage(key)
|
||||
if (savedDate) return savedDate
|
||||
}
|
||||
|
||||
return new Date().toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState(getInitialDate)
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
const [editingConditions, setEditingConditions] = useState<
|
||||
Record<string, string>
|
||||
>({})
|
||||
|
||||
const { data: laboratories = [] } = useLaboratories()
|
||||
const { data: labData } = useLaboratoryConditions(config.selectedLaboratoryId)
|
||||
const {
|
||||
data: dailyCondition,
|
||||
saveConditions,
|
||||
isSaving,
|
||||
} = useDailyConditions(config.selectedLaboratoryId, selectedDate)
|
||||
|
||||
const selectedLab = laboratories.find(
|
||||
lab => lab.id === config.selectedLaboratoryId
|
||||
)
|
||||
|
||||
const hasConditions =
|
||||
dailyCondition && Object.keys(dailyCondition.conditions).length > 0
|
||||
|
||||
// Инициализация данных при загрузке из БД, только если данные еще не заполнены
|
||||
useEffect(() => {
|
||||
if (
|
||||
dailyCondition &&
|
||||
hasConditions &&
|
||||
onChange &&
|
||||
(!value?.conditions || Object.keys(value.conditions).length === 0)
|
||||
) {
|
||||
onChange({
|
||||
date: selectedDate,
|
||||
conditions: dailyCondition.conditions,
|
||||
})
|
||||
}
|
||||
}, [dailyCondition, hasConditions, onChange, selectedDate, value?.conditions])
|
||||
|
||||
// Синхронизация selectedDate с внешним value.date
|
||||
useEffect(() => {
|
||||
if (value?.date && value.date !== selectedDate) {
|
||||
setSelectedDate(value.date)
|
||||
}
|
||||
}, [value?.date, selectedDate])
|
||||
|
||||
// Инициализация значения если оно не установлено
|
||||
useEffect(() => {
|
||||
if (!value && onChange) {
|
||||
onChange({
|
||||
date: selectedDate,
|
||||
conditions: {},
|
||||
})
|
||||
}
|
||||
}, [value, onChange, selectedDate])
|
||||
|
||||
const handleDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newDate = event.target.value
|
||||
setSelectedDate(newDate)
|
||||
|
||||
// Сохранение в localStorage
|
||||
if (config.id) {
|
||||
const key = getLocalStorageKey('verification_conditions_date', config.id)
|
||||
saveToLocalStorage(key, newDate)
|
||||
}
|
||||
|
||||
// Обновляем значение даты в value
|
||||
if (onChange) {
|
||||
onChange({
|
||||
date: newDate,
|
||||
conditions: value?.conditions || {},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenEditDialog = (isCreating = false) => {
|
||||
if (isCreating) {
|
||||
const emptyConditions: Record<string, string> = {}
|
||||
labData?.conditionTypes.forEach(conditionType => {
|
||||
emptyConditions[conditionType.id] = ''
|
||||
})
|
||||
setEditingConditions(emptyConditions)
|
||||
} else {
|
||||
setEditingConditions(dailyCondition?.conditions || {})
|
||||
}
|
||||
setEditDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSaveConditions = async () => {
|
||||
if (!config.selectedLaboratoryId || !selectedDate || !labData) return
|
||||
|
||||
try {
|
||||
await saveConditions(editingConditions)
|
||||
|
||||
if (onChange) {
|
||||
onChange({
|
||||
date: selectedDate,
|
||||
conditions: editingConditions,
|
||||
})
|
||||
}
|
||||
|
||||
setEditDialogOpen(false)
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConditionChange = (conditionId: string, value: string) => {
|
||||
setEditingConditions(prev => ({
|
||||
...prev,
|
||||
[conditionId]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
if (!config.selectedLaboratoryId) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<CellsBadge targetCells={config.targetCells || []} />
|
||||
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-3">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Building2 className="mx-auto mb-1 h-4 w-4 opacity-50" />
|
||||
<p className="text-xs">Лаборатория не выбрана</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!labData) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<CellsBadge targetCells={config.targetCells || []} />
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-3">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<div className="mx-auto mb-1 h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<p className="text-xs">Загрузка...</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Бейдж ячеек */}
|
||||
<CellsBadge targetCells={config.targetCells || []} />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2">
|
||||
{/* Информация о лаборатории и дате */}
|
||||
<div className="flex items-center gap-2 rounded-md bg-muted/20 p-2 text-sm">
|
||||
<Building2 className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{selectedLab?.name || 'Лаборатория'}
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={handleDateChange}
|
||||
lang="ru"
|
||||
className="h-5 w-auto border-none bg-transparent p-0 text-xs font-normal focus:bg-background"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Общий статус с кнопкой настроить */}
|
||||
<div className="flex items-center justify-between px-2 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{hasConditions ? (
|
||||
<>
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-600" />
|
||||
<span className="font-medium text-green-600">
|
||||
Данные внесены
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle className="h-3.5 w-3.5 text-orange-600" />
|
||||
<span className="font-medium text-orange-600">
|
||||
Данные не внесены
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={() => handleOpenEditDialog(!hasConditions)}
|
||||
>
|
||||
<Settings className="mr-1 h-3 w-3" />
|
||||
Настроить
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">
|
||||
Условия поверки
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<div>{selectedLab?.name}</div>
|
||||
<div>
|
||||
{new Date(selectedDate).toLocaleDateString('ru-RU')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{labData.conditionTypes.map(conditionType => (
|
||||
<div key={conditionType.id} className="space-y-1">
|
||||
<Label className="text-xs font-medium">
|
||||
{conditionType.name}
|
||||
{conditionType.unit && (
|
||||
<span className="ml-1 font-normal text-muted-foreground">
|
||||
({conditionType.unit})
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
value={editingConditions[conditionType.id] || ''}
|
||||
onChange={e =>
|
||||
handleConditionChange(
|
||||
conditionType.id,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="Значение"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleSaveConditions}
|
||||
disabled={isSaving}
|
||||
size="sm"
|
||||
className="h-7 flex-1 text-xs"
|
||||
>
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditDialogOpen(false)}
|
||||
size="sm"
|
||||
className="h-7 flex-1 text-xs"
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Условия */}
|
||||
<div className="space-y-1">
|
||||
{labData.conditionTypes.map(conditionType => {
|
||||
const conditionValue =
|
||||
dailyCondition?.conditions[conditionType.id]
|
||||
|
||||
return (
|
||||
<ConditionItem
|
||||
key={conditionType.id}
|
||||
name={conditionType.name}
|
||||
value={conditionValue}
|
||||
unit={conditionType.unit}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { ElementDefinition } from '@/entity/element/model/interface'
|
||||
import { CellTarget } from '@/type/template'
|
||||
import { Building2 } from 'lucide-react'
|
||||
import {
|
||||
@@ -1,8 +1,8 @@
|
||||
import { buttonGroupDefinition } from '@/entitiy/element/model/implementations/ButtonGroup'
|
||||
import { selectDefinition } from '@/entitiy/element/model/implementations/SelectElement'
|
||||
import { standardsDefinition } from '@/entitiy/element/model/implementations/StandardsElement/definition'
|
||||
import { textDefinition } from '@/entitiy/element/model/implementations/TextElement'
|
||||
import { verificationConditionsElementDefinition as verificationConditionsDefinition } from '@/entitiy/element/model/implementations/VerificationConditionsElement/definition'
|
||||
import { buttonGroupDefinition } from '@/entity/element/model/implementations/ButtonGroup'
|
||||
import { selectDefinition } from '@/entity/element/model/implementations/SelectElement'
|
||||
import { standardsDefinition } from '@/entity/element/model/implementations/StandardsElement/definition'
|
||||
import { textDefinition } from '@/entity/element/model/implementations/TextElement'
|
||||
import { verificationConditionsElementDefinition as verificationConditionsDefinition } from '@/entity/element/model/implementations/VerificationConditionsElement/definition'
|
||||
import { CellTarget } from '@/type/template'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
patchTemplateApi,
|
||||
templateToCreateInput,
|
||||
templateToPatchInput,
|
||||
} from '@/entitiy/template/api/templateApiService'
|
||||
} from '@/entity/template/api/templateApiService'
|
||||
import { Template } from '@/type/template'
|
||||
import {
|
||||
QueryClient,
|
||||
@@ -14,10 +14,28 @@ const QUALIFIED_RANGE_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // She
|
||||
export type CellValue = string | number | boolean | null | undefined
|
||||
|
||||
// Утилитная функция для устранения машинной ошибки в числах с плавающей точкой
|
||||
// Применяется только при финальном выводе значений
|
||||
function fixFloatingPointError(value: CellValue): CellValue {
|
||||
if (typeof value === 'number' && isFinite(value)) {
|
||||
const factor = Math.pow(10, 14)
|
||||
return Math.round(value * factor) / factor
|
||||
// Проверяем, является ли число "почти целым" или "почти простой дробью"
|
||||
const tolerance = 1e-10
|
||||
|
||||
// Проверка на целое число
|
||||
if (Math.abs(value - Math.round(value)) < tolerance) {
|
||||
return Math.round(value)
|
||||
}
|
||||
|
||||
// Проверка на простые дроби (до 6 знаков после запятой)
|
||||
for (let precision = 1; precision <= 6; precision++) {
|
||||
const factor = Math.pow(10, precision)
|
||||
const rounded = Math.round(value * factor) / factor
|
||||
if (Math.abs(value - rounded) < tolerance) {
|
||||
return rounded
|
||||
}
|
||||
}
|
||||
|
||||
// Для остальных случаев ограничиваем до разумного количества знаков
|
||||
return Math.round(value * 1e10) / 1e10
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -66,6 +84,7 @@ export class Cell {
|
||||
return this.value
|
||||
}
|
||||
|
||||
// Убираем знак равенства
|
||||
const expr = this.raw.substring(1)
|
||||
|
||||
// Проверяем, содержит ли формула волатильные функции
|
||||
@@ -166,13 +185,6 @@ export class Cell {
|
||||
// Заменяем функции Excel
|
||||
const functions = this.sheet.workbook.getFunctions()
|
||||
|
||||
// Отладка
|
||||
if (expr.includes('ТЕСТ')) {
|
||||
// console.log('Processing ТЕСТ function')
|
||||
// console.log('Available functions:', Object.keys(functions))
|
||||
// console.log('Original code:', code)
|
||||
}
|
||||
|
||||
for (const [funcName] of Object.entries(functions)) {
|
||||
// Экранируем специальные символы в названии функции для регулярного выражения
|
||||
const escapedFuncName = funcName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
@@ -193,10 +205,6 @@ export class Cell {
|
||||
}
|
||||
}
|
||||
|
||||
if (expr.includes('ТЕСТ')) {
|
||||
// console.log('Final code:', code)
|
||||
}
|
||||
|
||||
// Создаём контекст для выполнения
|
||||
const context = {
|
||||
__functions__: functions,
|
||||
@@ -249,18 +257,13 @@ export class Cell {
|
||||
}
|
||||
|
||||
private evaluateExpression(expr: string, context: any): CellValue {
|
||||
// Простая реализация eval для математических выражений и функций
|
||||
// В реальном проекте лучше использовать специализированную библиотеку
|
||||
// для парсинга и выполнения формул
|
||||
|
||||
try {
|
||||
// Создаём функцию с контекстом
|
||||
const func = new Function(...Object.keys(context), `return ${expr}`)
|
||||
|
||||
const result = func(...Object.values(context))
|
||||
|
||||
// Исправляем машинную ошибку в 16-м символе после запятой
|
||||
return fixFloatingPointError(result)
|
||||
return result
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid expression: ${expr}`)
|
||||
}
|
||||
@@ -429,12 +432,133 @@ export class Workbook {
|
||||
}
|
||||
}
|
||||
|
||||
private topologicalSort(cells: Set<string>): string[] {
|
||||
const visited = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
const result: string[] = []
|
||||
const hasCycle = new Set<string>()
|
||||
|
||||
const visit = (cellName: string): boolean => {
|
||||
if (visiting.has(cellName)) {
|
||||
// Обнаружена циклическая зависимость
|
||||
hasCycle.add(cellName)
|
||||
return false
|
||||
}
|
||||
|
||||
if (visited.has(cellName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
visiting.add(cellName)
|
||||
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
|
||||
// Посещаем всех предшественников (ячейки, от которых зависит текущая)
|
||||
for (const precedent of cell.precedents) {
|
||||
if (cells.has(precedent)) {
|
||||
if (!visit(precedent)) {
|
||||
hasCycle.add(cellName)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ячейка не существует или другая ошибка - игнорируем
|
||||
}
|
||||
|
||||
visiting.delete(cellName)
|
||||
visited.add(cellName)
|
||||
result.push(cellName)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Посещаем все ячейки
|
||||
for (const cellName of cells) {
|
||||
if (!visited.has(cellName)) {
|
||||
visit(cellName)
|
||||
}
|
||||
}
|
||||
|
||||
// Если обнаружены циклы, выводим предупреждение
|
||||
if (hasCycle.size > 0) {
|
||||
console.warn(
|
||||
'Обнаружены циклические зависимости в ячейках:',
|
||||
Array.from(hasCycle)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private detectCycle(startCell: string): boolean {
|
||||
const visited = new Set<string>()
|
||||
const visiting = new Set<string>()
|
||||
|
||||
const visit = (cellName: string): boolean => {
|
||||
if (visiting.has(cellName)) {
|
||||
return true // Цикл найден
|
||||
}
|
||||
|
||||
if (visited.has(cellName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
visiting.add(cellName)
|
||||
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
for (const precedent of cell.precedents) {
|
||||
if (visit(precedent)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ячейка не существует - игнорируем
|
||||
}
|
||||
|
||||
visiting.delete(cellName)
|
||||
visited.add(cellName)
|
||||
return false
|
||||
}
|
||||
|
||||
return visit(startCell)
|
||||
}
|
||||
|
||||
recalc(): void {
|
||||
// Пересчёт всех "грязных" ячеек
|
||||
while (this._dirty.size > 0) {
|
||||
const current = this._dirty.values().next().value!
|
||||
this._dirty.delete(current)
|
||||
this.getCell(current).evaluate()
|
||||
if (this._dirty.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Выполняем топологическую сортировку для определения правильного порядка пересчета
|
||||
const sortedCells = this.topologicalSort(this._dirty)
|
||||
|
||||
// Очищаем множество "грязных" ячеек
|
||||
this._dirty.clear()
|
||||
|
||||
// Пересчитываем ячейки в правильном порядке
|
||||
for (const cellName of sortedCells) {
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
|
||||
// Проверяем на циклические зависимости перед вычислением
|
||||
if (this.detectCycle(cellName)) {
|
||||
console.error(
|
||||
`Циклическая зависимость обнаружена для ячейки ${cellName}`
|
||||
)
|
||||
cell.value = '#CYCLE!'
|
||||
} else {
|
||||
cell.evaluate()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Ошибка при пересчете ячейки ${cellName}:`, error)
|
||||
try {
|
||||
const cell = this.getCell(cellName)
|
||||
cell.value = '#ERROR!'
|
||||
} catch (getError) {
|
||||
// Ячейка не существует - игнорируем
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +568,6 @@ export class Workbook {
|
||||
}
|
||||
const value = this.getCell(qualifiedName).value
|
||||
|
||||
// Исправляем машинную ошибку в 16-м символе после запятой для числовых значений
|
||||
return fixFloatingPointError(value)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,7 @@ export interface ExcelFunction {
|
||||
}
|
||||
|
||||
// Волатильные функции - пересчитываются при любом изменении
|
||||
export const VOLATILE_FUNCTIONS = new Set([
|
||||
'СЛУЧ',
|
||||
'СЛУЧМЕЖДУ',
|
||||
'СЛУЧДР',
|
||||
'СЕЙЧАС',
|
||||
'СЕГОДНЯ',
|
||||
])
|
||||
export const VOLATILE_FUNCTIONS = new Set(['СЛУЧ', 'СЛУЧМЕЖДУ', 'СЛУЧДР'])
|
||||
|
||||
export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
|
||||
СЛУЧ: () => Math.random(),
|
||||
@@ -124,43 +118,4 @@ export const DEFAULT_FUNCTIONS: Record<string, ExcelFunction> = {
|
||||
if (кратное === 0) return 0
|
||||
return Math.round(число / кратное) * кратное
|
||||
},
|
||||
|
||||
// Функции даты и времени
|
||||
СЕЙЧАС: () => Date.now(),
|
||||
СЕГОДНЯ: () => {
|
||||
const date = new Date()
|
||||
date.setHours(0, 0, 0, 0)
|
||||
return date.getTime()
|
||||
},
|
||||
ДАТАПРОТОКОЛА: (дата: number | Date) => {
|
||||
try {
|
||||
const months = [
|
||||
'',
|
||||
'января', 'февраля', 'марта',
|
||||
'апреля', 'мая', 'июня',
|
||||
'июля', 'августа', 'сентября',
|
||||
'октября', 'ноября', 'декабря',
|
||||
]
|
||||
let dateObj: Date
|
||||
if (typeof дата === 'number') {
|
||||
dateObj = new Date(дата)
|
||||
} else if (дата instanceof Date) {
|
||||
dateObj = дата
|
||||
} else {
|
||||
return 'Неверный формат даты'
|
||||
}
|
||||
const day = dateObj.getDate().toString().padStart(2, '0')
|
||||
const month = months[dateObj.getMonth() + 1]
|
||||
const year = dateObj.getFullYear()
|
||||
return `${day} ${month} ${year}`
|
||||
} catch (e) {
|
||||
return 'Неверный формат даты'
|
||||
}
|
||||
},
|
||||
|
||||
// Отладочная функция
|
||||
ТЕСТ: (значение: any = 'Функция работает!') => {
|
||||
// console.log('ТЕСТ вызван с:', значение)
|
||||
return значение
|
||||
},
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
CellValue,
|
||||
Workbook,
|
||||
} from '../lib/spreadsheet-engine/spreadsheet-engine'
|
||||
import { CellValue, Workbook } from '../feature/excelEngine/excelEngine'
|
||||
interface SheetConfig {
|
||||
name: string
|
||||
rows: number
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CellUtils,
|
||||
CellValue,
|
||||
Workbook,
|
||||
} from '../lib/spreadsheet-engine/spreadsheet-engine'
|
||||
} from '../feature/excelEngine/excelEngine'
|
||||
|
||||
interface CellData {
|
||||
value: string
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* Унифицированные утилиты для работы с адресами ячеек Excel
|
||||
* Заменяет дублированный код в разных частях приложения
|
||||
*/
|
||||
|
||||
export interface CellAddress {
|
||||
column: string
|
||||
row: number
|
||||
@@ -25,23 +20,6 @@ export function columnToIndex(column: string): number {
|
||||
return result - 1 // zero-based
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует числовой индекс в буквенный столбец
|
||||
* 0 -> A, 1 -> B, ..., 25 -> Z, 26 -> AA, 27 -> AB, etc.
|
||||
*/
|
||||
export function indexToColumn(index: number): string {
|
||||
let result = ''
|
||||
let num = index + 1 // Convert to 1-based
|
||||
|
||||
while (num > 0) {
|
||||
num-- // Adjust for 0-based alphabet
|
||||
result = String.fromCharCode(65 + (num % 26)) + result
|
||||
num = Math.floor(num / 26)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит адрес ячейки в компоненты (столбец и строка)
|
||||
* "A1" -> { column: "A", row: 1 }
|
||||
@@ -71,82 +49,3 @@ export function cellAddressToCoordinates(address: string): CellCoordinates {
|
||||
col: columnToIndex(parsed.column),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует координаты в адрес ячейки
|
||||
* { row: 0, col: 0 } -> "A1"
|
||||
* { row: 2, col: 1 } -> "B3"
|
||||
*/
|
||||
export function coordinatesToCellAddress(coords: CellCoordinates): string {
|
||||
const column = indexToColumn(coords.col)
|
||||
const row = coords.row + 1 // Convert to 1-based
|
||||
return `${column}${row}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает адрес ячейки из столбца и строки
|
||||
* createCellAddress("A", 1) -> "A1"
|
||||
* createCellAddress(0, 1) -> "A1" (если передан индекс столбца)
|
||||
*/
|
||||
export function createCellAddress(
|
||||
column: string | number,
|
||||
row: number
|
||||
): string {
|
||||
const columnStr = typeof column === 'number' ? indexToColumn(column) : column
|
||||
return `${columnStr}${row}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидирует адрес ячейки
|
||||
* isValidCellAddress("A1") -> true
|
||||
* isValidCellAddress("ZZZ999999") -> true
|
||||
* isValidCellAddress("A") -> false
|
||||
*/
|
||||
export function isValidCellAddress(cell: string): boolean {
|
||||
return /^[A-Z]+\d+$/i.test(cell)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает метку столбца из индекса
|
||||
* getColumnLabel(0) -> "A"
|
||||
* getColumnLabel(25) -> "Z"
|
||||
* getColumnLabel(26) -> "AA"
|
||||
*/
|
||||
export function getColumnLabel(index: number): string {
|
||||
return indexToColumn(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Вспомогательная функция для совместимости с существующим кодом
|
||||
* Аналог cellAddressToCoordinates, но с другим названием
|
||||
*/
|
||||
export const cellAddressToCoords = cellAddressToCoordinates
|
||||
|
||||
/**
|
||||
* Парсит диапазон ячеек в массив адресов
|
||||
* "A1:C3" -> ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"]
|
||||
*/
|
||||
export function parseRange(range: string): string[] {
|
||||
const match = range.match(/^([A-Z]+\d+):([A-Z]+\d+)$/i)
|
||||
if (!match) return []
|
||||
|
||||
const startCell = parseCellAddress(match[1])
|
||||
const endCell = parseCellAddress(match[2])
|
||||
|
||||
if (!startCell || !endCell) return []
|
||||
|
||||
const startCol = columnToIndex(startCell.column)
|
||||
const endCol = columnToIndex(endCell.column)
|
||||
const startRow = startCell.row
|
||||
const endRow = endCell.row
|
||||
|
||||
const result: string[] = []
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startCol; col <= endCol; col++) {
|
||||
result.push(createCellAddress(col, row))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
FormLayoutSettings,
|
||||
TemplateElement,
|
||||
} from '../type/template'
|
||||
import { parseCellAddress } from './cell-utils'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@@ -73,13 +72,3 @@ export function generateDefaultLayout(order: number): ElementLayout {
|
||||
zIndex: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация адреса ячейки - теперь использует централизованную утилиту
|
||||
export function validateCellAddress(cell: string): boolean {
|
||||
return parseCellAddress(cell) !== null
|
||||
}
|
||||
|
||||
// Генерация уникального ID для элемента
|
||||
export function generateElementId(): string {
|
||||
return `element_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ElementConstructor } from '@/component/TemplateManager/ElementConstructor'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
|
||||
import { useDelayedSave } from '@/hook/useDelayedSave'
|
||||
import { FormLayoutSettings, Template, TemplateElement } from '@/type/template'
|
||||
import { ArrowLeft, FileText, Settings, Wrench } from 'lucide-react'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import DualSpreadsheet from '@/component/DualSpreadsheet/DualSpreadsheet'
|
||||
import { Button } from '@/component/ui/button'
|
||||
import { getElementDefinition } from '@/entitiy/element/model/interface'
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { getElementDefinition } from '@/entity/element/model/interface'
|
||||
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
|
||||
import { cellAddressToCoordinates } from '@/lib/cell-utils'
|
||||
import { useToast } from '@/lib/hooks/useToast'
|
||||
import { getLatestFileForTemplate } from '@/service/fileApiService'
|
||||
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
useDeleteStandard,
|
||||
useStandards,
|
||||
useUpdateStandard,
|
||||
} from '@/entitiy/element/model/implementations/StandardsElement/hooks'
|
||||
} from '@/entity/element/model/implementations/StandardsElement/hooks'
|
||||
import {
|
||||
CreateStandardInput,
|
||||
PatchStandardInput,
|
||||
Standard,
|
||||
} from '@/entitiy/element/model/implementations/StandardsElement/types'
|
||||
} from '@/entity/element/model/implementations/StandardsElement/types'
|
||||
import {
|
||||
Calendar,
|
||||
Edit,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TemplateEditor } from '@/component/TemplateManager/TemplateEditor'
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
export const TemplateEditPage = () => {
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
import {
|
||||
apiTemplateWithAttributesToTemplate,
|
||||
getTemplateWithAttributesApi,
|
||||
} from '@/entitiy/template/api/templateApiService'
|
||||
import { useCategoryContext } from '@/entitiy/template/model/CategoryContext'
|
||||
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
|
||||
} from '@/entity/template/api/templateApiService'
|
||||
import { useCategoryContext } from '@/entity/template/model/CategoryContext'
|
||||
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
|
||||
import { Template, TemplateAttributeDetail } from '@/type/template'
|
||||
import TemplateCard from '@/widget/template/ui/TemplateCard'
|
||||
import clsx from 'clsx'
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Типы для API формул электронных таблиц
|
||||
*/
|
||||
|
||||
export interface FormulaApiConfig {
|
||||
baseUrl: string
|
||||
apiKey?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export interface FormulaApiError {
|
||||
code: string
|
||||
message: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface FormulaApiEvent {
|
||||
type:
|
||||
| 'SAVE_START'
|
||||
| 'SAVE_SUCCESS'
|
||||
| 'SAVE_ERROR'
|
||||
| 'LOAD_START'
|
||||
| 'LOAD_SUCCESS'
|
||||
| 'LOAD_ERROR'
|
||||
templateId: string
|
||||
version?: number
|
||||
error?: FormulaApiError
|
||||
}
|
||||
|
||||
export interface SaveFormulasRequest {
|
||||
templateId: string
|
||||
formulaData: Record<string, Record<string, any>>
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface SaveFormulasResponse {
|
||||
success: boolean
|
||||
templateId: string
|
||||
version: number
|
||||
timestamp: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface LoadFormulasRequest {
|
||||
templateId: string
|
||||
version?: number
|
||||
}
|
||||
|
||||
export interface LoadFormulasResponse {
|
||||
success: boolean
|
||||
notFound?: boolean
|
||||
data?: TemplateFormulaData
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface FormulaHistoryRequest {
|
||||
templateId: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface FormulaHistoryResponse {
|
||||
success: boolean
|
||||
history: Array<{
|
||||
version: number
|
||||
timestamp: string
|
||||
description?: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface SyncStatus {
|
||||
templateId: string
|
||||
lastSync: string
|
||||
isOnline: boolean
|
||||
pendingChanges: number
|
||||
}
|
||||
|
||||
export interface TemplateFormulaData {
|
||||
templateId: string
|
||||
version: number
|
||||
sheets: Record<string, SheetFormulaData>
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface SheetFormulaData {
|
||||
sheetName: string
|
||||
cells: Record<string, any>
|
||||
metadata: {
|
||||
rowCount: number
|
||||
columnCount: number
|
||||
lastCalculated: string
|
||||
}
|
||||
}
|
||||
25
src/type/imports.d.ts
vendored
25
src/type/imports.d.ts
vendored
@@ -1,25 +0,0 @@
|
||||
// Типы для контроля импортов между слоями архитектуры
|
||||
declare module 'app/*' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module 'page/*' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module 'feature/*' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module 'entitiy/*' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module 'shared/*' {
|
||||
const content: any
|
||||
export default content
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Типы элементов интерфейса
|
||||
|
||||
// Импортируем ElementType из interface.ts
|
||||
import type { ElementType } from '@/entitiy/element/model/interface'
|
||||
import type { ElementType } from '@/entity/element/model/interface'
|
||||
|
||||
export interface ElementOption {
|
||||
value: string
|
||||
@@ -80,19 +80,6 @@ export interface TemplateAttributeDetail {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// Для совместимости со старым кодом - алиас
|
||||
export type CategoryField = Attribute
|
||||
export type CategoryFieldType = 'text' | 'select' | 'number' | 'textarea'
|
||||
export interface CategoryFieldOption {
|
||||
id: string
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
export interface CategoryFieldValue {
|
||||
fieldId: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
// Типы шаблонов
|
||||
export interface Template {
|
||||
id: string
|
||||
@@ -106,10 +93,3 @@ export interface Template {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
// Типы для работы с данными
|
||||
export interface TemplateData {
|
||||
templateId: string
|
||||
values: Record<string, any> // elementId -> value
|
||||
calculatedCells?: Record<string, any> // cellName -> value
|
||||
}
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
# API для элемента "Условия поверки"
|
||||
|
||||
## Базовый URL
|
||||
|
||||
```
|
||||
/api/v1/verification-conditions
|
||||
```
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### 1. Лаборатории
|
||||
|
||||
#### GET /laboratories
|
||||
|
||||
Получить список всех лабораторий
|
||||
|
||||
```json
|
||||
Response 200:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Лаборатория температуры",
|
||||
"code": "TEMP_LAB",
|
||||
"description": "Лаборатория поверки температурных приборов",
|
||||
"address": "ул. Примерная, 1",
|
||||
"conditionTypes": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Температура",
|
||||
"unit": "°C",
|
||||
"isRequired": true,
|
||||
"defaultValue": "20",
|
||||
"minValue": 15,
|
||||
"maxValue": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### GET /laboratories/:id
|
||||
|
||||
Получить лабораторию по ID с настройками условий
|
||||
|
||||
#### POST /laboratories
|
||||
|
||||
Создать новую лабораторию
|
||||
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"name": "Новая лаборатория",
|
||||
"code": "NEW_LAB",
|
||||
"description": "Описание",
|
||||
"address": "Адрес"
|
||||
}
|
||||
```
|
||||
|
||||
#### PUT /laboratories/:id
|
||||
|
||||
Обновить лабораторию
|
||||
|
||||
#### DELETE /laboratories/:id
|
||||
|
||||
Удалить лабораторию
|
||||
|
||||
### 2. Типы условий
|
||||
|
||||
#### GET /condition-types
|
||||
|
||||
Получить все доступные типы условий
|
||||
|
||||
```json
|
||||
Response 200:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Температура",
|
||||
"unit": "°C",
|
||||
"dataType": "number",
|
||||
"description": "Температура окружающей среды"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### POST /condition-types
|
||||
|
||||
Создать новый тип условия
|
||||
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"name": "Новое условие",
|
||||
"unit": "единица",
|
||||
"dataType": "number",
|
||||
"description": "Описание"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Настройка условий для лабораторий
|
||||
|
||||
#### GET /laboratories/:labId/condition-types
|
||||
|
||||
Получить настроенные типы условий для лаборатории
|
||||
|
||||
#### POST /laboratories/:labId/condition-types
|
||||
|
||||
Добавить тип условия к лаборатории
|
||||
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"conditionTypeId": 1,
|
||||
"isRequired": true,
|
||||
"defaultValue": "20",
|
||||
"minValue": 15,
|
||||
"maxValue": 30
|
||||
}
|
||||
```
|
||||
|
||||
#### PUT /laboratories/:labId/condition-types/:typeId
|
||||
|
||||
Обновить настройки типа условия для лаборатории
|
||||
|
||||
#### DELETE /laboratories/:labId/condition-types/:typeId
|
||||
|
||||
Удалить тип условия из лаборатории
|
||||
|
||||
### 4. Условия по дням (основная функциональность)
|
||||
|
||||
#### GET /daily-conditions
|
||||
|
||||
Получить условия по дням с фильтрацией
|
||||
|
||||
```
|
||||
Query параметры:
|
||||
- laboratoryId: ID лаборатории
|
||||
- dateFrom: дата начала (YYYY-MM-DD)
|
||||
- dateTo: дата окончания (YYYY-MM-DD)
|
||||
- date: конкретная дата (YYYY-MM-DD)
|
||||
|
||||
Примеры:
|
||||
GET /daily-conditions?laboratoryId=1&dateFrom=2024-01-01&dateTo=2024-01-31
|
||||
GET /daily-conditions?laboratoryId=1&date=2024-01-15
|
||||
```
|
||||
|
||||
```json
|
||||
Response 200:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"laboratoryId": 1,
|
||||
"date": "2024-01-15",
|
||||
"conditions": {
|
||||
"temperature": 23.5,
|
||||
"humidity": 45.2,
|
||||
"atmosphericPressure": 101.3
|
||||
},
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"updatedAt": "2024-01-15T10:30:00Z",
|
||||
"createdBy": "user@example.com"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### GET /daily-conditions/:id
|
||||
|
||||
Получить условия для конкретного дня по ID
|
||||
|
||||
#### POST /daily-conditions
|
||||
|
||||
Создать/обновить условия для дня
|
||||
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"laboratoryId": 1,
|
||||
"date": "2024-01-15",
|
||||
"conditions": {
|
||||
"temperature": 23.5,
|
||||
"humidity": 45.2,
|
||||
"atmosphericPressure": 101.3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### PUT /daily-conditions/:id
|
||||
|
||||
Обновить условия для дня
|
||||
|
||||
#### DELETE /daily-conditions/:id
|
||||
|
||||
Удалить запись условий
|
||||
|
||||
### 5. Специальные эндпоинты для элемента
|
||||
|
||||
#### GET /daily-conditions/for-element
|
||||
|
||||
Получить условия для использования в элементе формы
|
||||
|
||||
```
|
||||
Query параметры:
|
||||
- laboratoryId: ID лаборатории (обязательный)
|
||||
- date: дата (обязательный, YYYY-MM-DD)
|
||||
```
|
||||
|
||||
```json
|
||||
Response 200:
|
||||
{
|
||||
"laboratoryId": 1,
|
||||
"laboratoryName": "Лаборатория температуры",
|
||||
"date": "2024-01-15",
|
||||
"conditions": [
|
||||
{
|
||||
"typeId": 1,
|
||||
"typeName": "Температура",
|
||||
"unit": "°C",
|
||||
"value": 23.5,
|
||||
"isRequired": true,
|
||||
"minValue": 15,
|
||||
"maxValue": 30
|
||||
},
|
||||
{
|
||||
"typeId": 2,
|
||||
"typeName": "Влажность",
|
||||
"unit": "%",
|
||||
"value": 45.2,
|
||||
"isRequired": true,
|
||||
"minValue": 30,
|
||||
"maxValue": 80
|
||||
}
|
||||
],
|
||||
"hasUnsavedChanges": false
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /daily-conditions/save-for-element
|
||||
|
||||
Сохранить условия из элемента формы
|
||||
|
||||
```json
|
||||
Request:
|
||||
{
|
||||
"laboratoryId": 1,
|
||||
"date": "2024-01-15",
|
||||
"conditions": {
|
||||
"temperature": 23.5,
|
||||
"humidity": 45.2,
|
||||
"atmosphericPressure": 101.3
|
||||
}
|
||||
}
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"id": 1,
|
||||
"success": true,
|
||||
"message": "Условия сохранены"
|
||||
}
|
||||
```
|
||||
|
||||
## Типы данных TypeScript
|
||||
|
||||
```typescript
|
||||
interface Laboratory {
|
||||
id: number
|
||||
name: string
|
||||
code: string
|
||||
description?: string
|
||||
address?: string
|
||||
conditionTypes?: LaboratoryConditionType[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
interface ConditionType {
|
||||
id: number
|
||||
name: string
|
||||
unit?: string
|
||||
dataType: 'number' | 'string' | 'boolean'
|
||||
description?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface LaboratoryConditionType {
|
||||
id: number
|
||||
laboratoryId: number
|
||||
conditionTypeId: number
|
||||
conditionType?: ConditionType
|
||||
isRequired: boolean
|
||||
defaultValue?: string
|
||||
minValue?: number
|
||||
maxValue?: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface DailyConditions {
|
||||
id: number
|
||||
laboratoryId: number
|
||||
laboratory?: Laboratory
|
||||
date: string // YYYY-MM-DD
|
||||
conditions: Record<string, any> // JSON объект
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
createdBy?: string
|
||||
}
|
||||
|
||||
interface ConditionValue {
|
||||
typeId: number
|
||||
typeName: string
|
||||
unit?: string
|
||||
value: any
|
||||
isRequired: boolean
|
||||
minValue?: number
|
||||
maxValue?: number
|
||||
}
|
||||
|
||||
interface ElementConditionsData {
|
||||
laboratoryId: number
|
||||
laboratoryName: string
|
||||
date: string
|
||||
conditions: ConditionValue[]
|
||||
hasUnsavedChanges: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
- `400` - Неверные параметры запроса
|
||||
- `404` - Лаборатория/условия не найдены
|
||||
- `409` - Конфликт (условия для этой даты уже существуют)
|
||||
- `422` - Ошибка валидации (значения не в допустимом диапазоне)
|
||||
- `500` - Внутренняя ошибка сервера
|
||||
128
yarn.lock
128
yarn.lock
@@ -7,6 +7,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
|
||||
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
|
||||
|
||||
"@babel/code-frame@^7.0.0":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
|
||||
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.27.1"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
"@babel/helper-validator-identifier@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
|
||||
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
|
||||
version "7.27.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6"
|
||||
@@ -865,6 +879,16 @@
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.83.0"
|
||||
|
||||
"@ts-morph/common@~0.12.3":
|
||||
version "0.12.3"
|
||||
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.3.tgz#a96e250217cd30e480ab22ec6a0ebbe65fd784ff"
|
||||
integrity sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==
|
||||
dependencies:
|
||||
fast-glob "^3.2.7"
|
||||
minimatch "^3.0.4"
|
||||
mkdirp "^1.0.4"
|
||||
path-browserify "^1.0.1"
|
||||
|
||||
"@tybys/wasm-util@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369"
|
||||
@@ -916,6 +940,11 @@
|
||||
dependencies:
|
||||
undici-types "~7.8.0"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
|
||||
integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
||||
@@ -1545,6 +1574,11 @@ clsx@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
code-block-writer@^11.0.0:
|
||||
version "11.0.3"
|
||||
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.3.tgz#9eec2993edfb79bfae845fbc093758c0a0b73b76"
|
||||
integrity sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
@@ -1567,6 +1601,11 @@ commander@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||
|
||||
commander@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||
|
||||
common-tags@^1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
|
||||
@@ -1577,6 +1616,17 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
cosmiconfig@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
|
||||
integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==
|
||||
dependencies:
|
||||
"@types/parse-json" "^4.0.0"
|
||||
import-fresh "^3.2.1"
|
||||
parse-json "^5.0.0"
|
||||
path-type "^4.0.0"
|
||||
yaml "^1.10.0"
|
||||
|
||||
cross-spawn@^7.0.2, cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
@@ -1761,6 +1811,13 @@ emoji-regex@^9.2.2:
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
error-ex@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
||||
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
|
||||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es-abstract@^1.23.2, es-abstract@^1.23.5, es-abstract@^1.23.9, es-abstract@^1.24.0:
|
||||
version "1.24.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.0.tgz#c44732d2beb0acc1ed60df840869e3106e7af328"
|
||||
@@ -2092,7 +2149,7 @@ fast-equals@^4.0.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7"
|
||||
integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==
|
||||
|
||||
fast-glob@^3.2.9, fast-glob@^3.3.2:
|
||||
fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
|
||||
@@ -2473,6 +2530,11 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
|
||||
call-bound "^1.0.3"
|
||||
get-intrinsic "^1.2.6"
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
||||
|
||||
is-async-function@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523"
|
||||
@@ -2694,7 +2756,7 @@ jiti@^1.21.6:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9"
|
||||
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
@@ -2711,6 +2773,11 @@ json-buffer@3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
||||
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
|
||||
|
||||
json-parse-even-better-errors@^2.3.0:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
|
||||
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
||||
|
||||
json-schema-traverse@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
@@ -2728,6 +2795,11 @@ json5@^1.0.2:
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
json5@^2.1.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
keyv@^4.5.3:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
@@ -2835,7 +2907,7 @@ minimatch@9.0.3:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
@@ -2859,6 +2931,11 @@ minimist@^1.2.0, minimist@^1.2.6:
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
motion-dom@^11.18.1:
|
||||
version "11.18.1"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2"
|
||||
@@ -3043,6 +3120,21 @@ parent-module@^1.0.0:
|
||||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parse-json@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
|
||||
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
error-ex "^1.3.1"
|
||||
json-parse-even-better-errors "^2.3.0"
|
||||
lines-and-columns "^1.1.6"
|
||||
|
||||
path-browserify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
@@ -3893,6 +3985,11 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
"true-myth@^4.1.0":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/true-myth/-/true-myth-4.1.1.tgz#ff4ac9d5130276e34aa338757e2416ec19248ba2"
|
||||
integrity sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==
|
||||
|
||||
ts-api-utils@^1.0.1:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064"
|
||||
@@ -3903,6 +4000,26 @@ ts-interface-checker@^0.1.9:
|
||||
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
||||
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
|
||||
|
||||
ts-morph@^13.0.1:
|
||||
version "13.0.3"
|
||||
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.3.tgz#c0c51d1273ae2edb46d76f65161eb9d763444c1d"
|
||||
integrity sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==
|
||||
dependencies:
|
||||
"@ts-morph/common" "~0.12.3"
|
||||
code-block-writer "^11.0.0"
|
||||
|
||||
ts-prune@^0.10.3:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/ts-prune/-/ts-prune-0.10.3.tgz#b6c71a525543b38dcf947a7d3adfb7f9e8b91f38"
|
||||
integrity sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==
|
||||
dependencies:
|
||||
commander "^6.2.1"
|
||||
cosmiconfig "^7.0.1"
|
||||
json5 "^2.1.3"
|
||||
lodash "^4.17.21"
|
||||
"true-myth" "^4.1.0"
|
||||
ts-morph "^13.0.1"
|
||||
|
||||
tsconfig-paths@^3.15.0:
|
||||
version "3.15.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
|
||||
@@ -4213,6 +4330,11 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
yaml@^1.10.0:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yaml@^2.3.4:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.0.tgz#15f8c9866211bdc2d3781a0890e44d4fa1a5fff6"
|
||||
|
||||
Reference in New Issue
Block a user