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

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

@@ -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 +1 @@
/// <reference types="vite/client" />
/// <reference types="vite/client" />