ручной рефакторинг

This commit is contained in:
2025-07-28 17:03:01 +03:00
parent d8e6dcd7d5
commit b98dfeef20
55 changed files with 844 additions and 1416 deletions

View File

@@ -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' },

View File

@@ -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); -- Частота сети

View File

@@ -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"
}

View File

@@ -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,

View File

@@ -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'

View File

@@ -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'

View File

@@ -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>>>

View File

@@ -1,4 +1,4 @@
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
import {
getFileData,
getLatestFileForTemplate,

View File

@@ -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'

View File

@@ -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'

View File

@@ -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,

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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 {

View File

@@ -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'

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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 значение
},
}

View File

@@ -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

View File

@@ -3,7 +3,7 @@ import {
CellUtils,
CellValue,
Workbook,
} from '../lib/spreadsheet-engine/spreadsheet-engine'
} from '../feature/excelEngine/excelEngine'
interface CellData {
value: string

View File

@@ -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
}

View File

@@ -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)}`
}

View File

@@ -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'

View File

@@ -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'

View File

@@ -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,

View File

@@ -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 = () => {

View File

@@ -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'

View File

@@ -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
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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"