ручной рефакторинг
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
2
src/type/vite-env.d.ts
vendored
2
src/type/vite-env.d.ts
vendored
@@ -1 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
Reference in New Issue
Block a user