Files
onboard/resources/js/Pages/Metriks/Components/MetrikaForm.vue
2026-01-04 23:15:06 +09:00

603 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, computed, reactive, onMounted, watch } from 'vue'
import { Head, router, useForm } from '@inertiajs/vue3'
import {
NForm, NFormItem, NInput, NInputNumber, NSelect,
NSwitch, NButton, NSpace, NCard, NH1, NH3,
NText, NTag, NProgress, NDivider, NGrid, NGi,
NAlert, NStatistic, NIcon, NTooltip, NRadioGroup,
NRadio, NCheckbox, NDatePicker, NTimePicker
} from 'naive-ui'
import {useNotification} from "../../../Composables/useNotification.js";
const props = defineProps({
groupId: {
type: [String, Number],
required: true
},
initialData: {
type: Object,
default: () => ({})
}
})
const { errorApi } = useNotification()
// Состояние формы
const formRef = ref(null)
const form = ref({
group_id: props.groupId,
comment: '',
// Динамические поля будут добавлены позже
})
const formProcessing = ref(false)
const formSections = ref([])
const formRules = ref({})
const formTitle = ref('')
const formDescription = ref('')
const isSubmitting = ref(false)
const isSavingDraft = ref(false)
const lastSavedTime = ref(null)
// Используем useForm из Inertia
// const { data: form, defaults, post, processing, errors: formErrors, reset } = useForm()
// Загрузка формы
onMounted(async () => {
await loadFormData()
await loadExistingData()
})
// Загрузка структуры формы
const loadFormData = async () => {
try {
const response = await axios.get(`/api/metric-forms/${props.groupId}`)
const formData = response.data
console.log(formData)
formTitle.value = formData.group.name
formDescription.value = formData.group.description
formSections.value = formData.form.sections
// Инициализируем значения формы
const initialValues = { group_id: props.groupId, comment: '' }
for (let fieldName in formData.form.default) {
initialValues[fieldName] = formData.form.default[fieldName]
}
form.value = {
...form.value,
...initialValues
}
// Настраиваем правила валидации
formRules.value = formData.form.validation
} catch (error) {
console.error('Ошибка загрузки формы:', error)
// errorApi()
}
}
// Загрузка существующих данных
const loadExistingData = async () => {
try {
const response = await axios.get(`/api/metric-forms/${props.groupId}/existing`)
if (response.data.existing_data) {
const existing = response.data.existing_data
// Заполняем форму существующими данными
Object.keys(existing.data).forEach(key => {
form[key] = existing.data[key]
})
form.comment = existing.comment || ''
lastSavedTime.value = new Date(existing.submitted_at).toLocaleTimeString('ru-RU')
}
} catch (error) {
console.error('Ошибка загрузки существующих данных:', error)
}
}
// Вычисляемые свойства
const currentDate = computed(() => {
return new Date().toLocaleDateString('ru-RU', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
})
const totalFieldsCount = computed(() => {
return formSections.value.reduce((total, section) => total + section.fields.length, 0)
})
const filledFieldsCount = computed(() => {
return Object.keys(form.value).filter(key =>
key.startsWith('metrika_item_') &&
form.value[key] !== null &&
form.value[key] !== '' &&
form.value[key] !== undefined
).length
})
const progressPercentage = computed(() => {
if (totalFieldsCount.value === 0) return 0
return Math.round((filledFieldsCount.value / totalFieldsCount.value) * 100)
})
const requiredFieldsCount = computed(() => {
return formSections.value.reduce((total, section) => {
return total + section.fields.filter(f => f.is_required).length
}, 0)
})
const filledRequiredFieldsCount = computed(() => {
let count = 0
formSections.value.forEach(section => {
section.fields.forEach(field => {
if (field.is_required) {
const fieldName = `metrika_item_${field.metrika_item_id}`
if (form.value[fieldName] !== null && form.value[fieldName] !== '' && form.value[fieldName] !== undefined) {
count++
}
}
})
})
return count
})
const requiredFieldsStatus = computed(() => {
const filled = filledRequiredFieldsCount.value
const total = requiredFieldsCount.value
if (filled === 0) return { type: 'error', text: '0%' }
if (filled === total) return { type: 'success', text: '100%' }
const percent = Math.round((filled / total) * 100)
return {
type: percent > 50 ? 'warning' : 'error',
text: `${percent}%`
}
})
const formStatus = computed(() => {
if (filledFieldsCount.value === 0) return 'empty'
if (filledRequiredFieldsCount.value < requiredFieldsCount.value) return 'incomplete'
// if (Object.keys(formErrors).length > 0) return 'invalid'
return 'ready'
})
const formStatusText = computed(() => {
const texts = {
'empty': 'Не заполнено',
'incomplete': 'Не все обязательные поля',
'invalid': 'Есть ошибки',
'ready': 'Готово к отправке'
}
return texts[formStatus.value] || 'Неизвестно'
})
const formStatusType = computed(() => {
const types = {
'empty': 'default',
'incomplete': 'warning',
'invalid': 'error',
'ready': 'success'
}
return types[formStatus.value] || 'default'
})
const canSubmit = computed(() => {
return formStatus.value === 'ready' && !formProcessing.value
})
const hasData = computed(() => {
return filledFieldsCount.value > 0
})
const showStatistics = computed(() => {
return totalFieldsCount.value > 0
})
// Методы
const getFieldComponent = (dataType) => {
const components = {
'integer': NInputNumber,
'float': NInputNumber,
'string': NInput,
'text': NInput,
'textarea': NInput,
'boolean': NSwitch,
'select': NSelect,
'radio': NRadioGroup,
'checkbox': NCheckbox,
'date': NDatePicker,
'time': NTimePicker
}
return components[dataType] || NInput
}
const getFieldOptions = (field) => {
if (field.options && Array.isArray(field.options)) {
return field.options.map(opt => ({
label: opt.label || opt,
value: opt.value || opt
}))
}
return []
}
const getFieldName = (fieldKey) => {
const itemId = fieldKey.replace('metrika_item_', '')
for (const section of formSections.value) {
for (const field of section.fields) {
if (field.metrika_item_id.toString() === itemId) {
return field.metrika_item_name
}
}
}
return fieldKey
}
const getStatusTagType = (status) => {
const types = {
'empty': 'default',
'incomplete': 'warning',
'invalid': 'error',
'ready': 'success'
}
return types[status] || 'default'
}
const submitForm = async () => {
if (!formRef.value) return
// Валидация формы
await formRef.value.validate()
// if (Object.keys(formErrors).length > 0) {
// // notification.warning({
// // title: 'Ошибки в форме',
// // content: 'Исправьте ошибки перед отправкой',
// // duration: 4000
// // })
// return
// }
isSubmitting.value = true
try {
// const response = await post(route('metric-forms.save', { group: props.groupId }), {
// onSuccess: () => {
// notification.success({
// title: 'Успешно!',
// content: 'Отчет сохранен',
// duration: 5000
// })
// lastSavedTime.value = new Date().toLocaleTimeString('ru-RU')
// },
// onError: (errors) => {
// notification.error({
// title: 'Ошибка сохранения',
// content: 'Проверьте правильность заполнения данных',
// duration: 5000
// })
// }
// })
const response = await axios.post(`/api/metric-forms/${props.groupId}/save`, formRef.value)
.then(res => {
// notification.success({
// title: 'Успешно!',
// content: 'Отчет сохранен',
// duration: 5000
// })
router.visit('/')
lastSavedTime.value = new Date().toLocaleTimeString('ru-RU')
})
.catch(e => {
// notification.error({
// title: 'Ошибка сохранения',
// content: 'Проверьте правильность заполнения данных',
// duration: 5000
// })
})
} catch (error) {
console.error('Ошибка сохранения:', error)
// notification.error({
// title: 'Ошибка',
// content: error.message || 'Произошла ошибка при сохранении',
// duration: 5000
// })
} finally {
isSubmitting.value = false
}
}
const saveDraft = async () => {
isSavingDraft.value = true
try {
// Сохраняем в localStorage
const draftData = {
form: form.value,
timestamp: new Date().toISOString(),
groupId: props.groupId
}
localStorage.setItem(`metric_form_draft_${props.groupId}`, JSON.stringify(draftData))
lastSavedTime.value = new Date().toLocaleTimeString('ru-RU')
// message.success('Черновик сохранен')
} catch (error) {
console.error('Ошибка сохранения черновика:', error)
// message.error('Не удалось сохранить черновик')
} finally {
isSavingDraft.value = false
}
}
const resetForm = () => {
if (confirm('Вы уверены, что хотите сбросить форму? Все несохраненные данные будут потеряны.')) {
reset()
loadFormData()
// message.info('Форма сброшена')
}
}
const exportFormData = () => {
const exportData = {
group: formTitle.value,
date: currentDate.value,
data: {},
comment: form.comment
}
// Собираем данные полей
formSections.value.forEach(section => {
section.fields.forEach(field => {
const fieldName = `metrika_item_${field.metrika_item_id}`
exportData.data[field.metrika_item_name] = form.value[fieldName]
})
})
// Создаем и скачиваем файл
const dataStr = JSON.stringify(exportData, null, 2)
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
const link = document.createElement('a')
link.setAttribute('href', dataUri)
link.setAttribute('download', `metric_report_${formTitle.value}_${new Date().toISOString().split('T')[0]}.json`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
</script>
<template>
<div>
<Head :title="`Ввод данных: ${formTitle}`" />
<!-- Заголовок формы -->
<div class="max-w-6xl mx-auto mt-4 mb-4">
<div class="flex items-center justify-between">
<div>
<n-h1 class="!mb-2">{{ formTitle }}</n-h1>
<n-text depth="3" v-if="formDescription">
{{ formDescription }}
</n-text>
<div class="flex items-center gap-2 mt-2">
<n-tag :type="getStatusTagType(formStatus)" size="small" round>
{{ formStatusText }}
</n-tag>
<n-text depth="3" class="text-sm">
{{ currentDate }}
</n-text>
</div>
</div>
<!-- <n-space>-->
<!-- <n-button @click="$inertia.visit(route('dashboard'))" secondary>-->
<!-- Назад-->
<!-- </n-button>-->
<!-- <n-button @click="exportFormData" :disabled="!hasData">-->
<!-- Экспорт-->
<!-- </n-button>-->
<!-- </n-space>-->
</div>
</div>
<!-- Прогресс заполнения -->
<div class="max-w-6xl mx-auto mb-2">
<n-card size="small">
<n-space justify="space-between" align="center">
<n-text>Прогресс заполнения</n-text>
<n-text strong>{{ progressPercentage }}%</n-text>
</n-space>
<n-progress
:percentage="progressPercentage"
:height="6"
:border-radius="0"
:show-indicator="false"
class="mt-2"
/>
</n-card>
</div>
<!-- Основная форма -->
<div class="max-w-6xl mx-auto">
<n-card>
<n-form
ref="formRef"
:model="form"
:rules="formRules"
@submit.prevent="submitForm"
>
<!-- Секции формы -->
<div v-for="(section, sectionIndex) in formSections" :key="sectionIndex">
<n-divider v-if="sectionIndex > 0" />
<div class="mb-6">
<n-h3>{{ section.name }}</n-h3>
<n-text depth="3" class="text-sm">
Заполните данные по секции
</n-text>
</div>
<n-grid :cols="section.fields.length > 5 ? 2 : 1" :x-gap="24" :y-gap="16">
<n-gi
v-for="field in section.fields"
:key="field.metrika_item_id"
:span="field.span || 1"
>
<n-form-item
:label="field.metrika_item_name"
:path="`metrika_item_${field.metrika_item_id}`"
:required="field.is_required"
>
<!-- <template #label>-->
<!-- <div class="flex items-center gap-2">-->
<!-- <span>{{ field.metrika_item_name }}</span>-->
<!-- <n-tooltip v-if="field.metrika_item_description" trigger="hover">-->
<!-- <template #trigger>-->
<!-- <n-icon size="14" class="cursor-help">-->
<!-- <InfoCircle />-->
<!-- </n-icon>-->
<!-- </template>-->
<!-- {{ field.metrika_item_description }}-->
<!-- </n-tooltip>-->
<!-- </div>-->
<!-- </template>-->
<!-- Динамические поля ввода -->
<component
:is="getFieldComponent(field.metrika_item_data_type)"
v-model:value="form[`metrika_item_${field.metrika_item_id}`]"
:placeholder="field.placeholder || `Введите ${field.metrika_item_name.toLowerCase()}`"
:options="getFieldOptions(field)"
:min="field.validation_rules?.min"
:max="field.validation_rules?.max"
:step="field.metrika_item_data_type === 'float' ? 0.1 : 1"
:rows="3"
clearable
class="w-full"
/>
<!-- <template #feedback>-->
<!-- <div v-if="formErrors[`metric_${field.metrika_item_id}`]" class="text-red-500 text-xs mt-1">-->
<!-- {{ formErrors[`metric_${field.metrika_item_id}`][0] }}-->
<!-- </div>-->
<!-- </template>-->
</n-form-item>
</n-gi>
</n-grid>
</div>
<!-- Комментарий -->
<n-form-item label="Комментарий" path="comment">
<n-input
v-model:value="form.comment"
type="textarea"
placeholder="Дополнительные заметки и комментарии..."
:autosize="{ minRows: 3, maxRows: 6 }"
/>
</n-form-item>
<!-- Валидационные ошибки -->
<!-- <n-alert-->
<!-- v-if="Object.keys(formErrors).length > 0"-->
<!-- title="Ошибки заполнения"-->
<!-- type="error"-->
<!-- class="mb-4"-->
<!-- >-->
<!-- <ul class="list-disc pl-5">-->
<!-- <li v-for="(errors, field) in formErrors" :key="field">-->
<!-- {{ getFieldName(field) }}: {{ errors[0] }}-->
<!-- </li>-->
<!-- </ul>-->
<!-- </n-alert>-->
<!-- Статистика -->
<!-- <n-card v-if="showStatistics" title="Статистика" size="small" class="mb-4">-->
<!-- <n-grid :cols="4" responsive="screen">-->
<!-- <n-gi>-->
<!-- <n-statistic label="Заполнено полей">-->
<!-- {{ filledFieldsCount }} / {{ totalFieldsCount }}-->
<!-- </n-statistic>-->
<!-- </n-gi>-->
<!-- <n-gi>-->
<!-- <n-statistic label="Обязательные поля">-->
<!-- <n-tag :type="requiredFieldsStatus.type" size="small">-->
<!-- {{ requiredFieldsStatus.text }}-->
<!-- </n-tag>-->
<!-- </n-statistic>-->
<!-- </n-gi>-->
<!-- <n-gi>-->
<!-- <n-statistic label="Последнее сохранение">-->
<!-- {{ lastSavedTime || 'Не сохранено' }}-->
<!-- </n-statistic>-->
<!-- </n-gi>-->
<!-- <n-gi>-->
<!-- <n-statistic label="Статус">-->
<!-- <n-tag :type="formStatusType" size="small">-->
<!-- {{ formStatusText }}-->
<!-- </n-tag>-->
<!-- </n-statistic>-->
<!-- </n-gi>-->
<!-- </n-grid>-->
<!-- </n-card>-->
<!-- Кнопки действий -->
<n-space justify="end">
<!-- <n-button @click="resetForm" :disabled="isSubmitting">-->
<!-- Сбросить-->
<!-- </n-button>-->
<!-- <n-button-->
<!-- @click="saveDraft"-->
<!-- :loading="isSavingDraft"-->
<!-- secondary-->
<!-- >-->
<!-- Сохранить черновик-->
<!-- </n-button>-->
<n-button
type="primary"
@click="submitForm"
:loading="isSubmitting"
:disabled="!canSubmit"
>
Сохранить отчет
</n-button>
</n-space>
</n-form>
</n-card>
</div>
</div>
</template>
<style scoped>
.n-form-item :deep(.n-form-item-label__text) {
display: flex;
align-items: center;
gap: 4px;
}
.n-divider {
margin: 32px 0;
}
.n-statistic :deep(.n-statistic-label) {
font-size: 0.875rem;
color: #6b7280;
}
.n-statistic :deep(.n-statistic-value) {
font-size: 1.125rem;
font-weight: 600;
}
</style>