Files
econom-calculator/resources/js/pages/medical-reports/Show.vue
brusnitsyn 3edc8e667e
Some checks failed
tests / ci (8.5) (push) Has been cancelled
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
first commit
2026-04-03 17:20:05 +09:00

649 lines
33 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 lang="ts">
import { Form, Head, Link } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import MedicalReportController from '@/actions/App/Http/Controllers/MedicalReportController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type {
MedicalReport,
MedicalReportDepartment,
MedicalReportSheet,
MedicalReportSheetField,
MedicalReportStructuredEntry,
MedicalReportStructuredSection,
MedicalReportStructuredTemplate,
MedicalReportSummary,
} from '@/types';
type Props = {
report: MedicalReport;
departments: MedicalReportDepartment[];
selectedDepartment?: string | null;
selectedSheet?: string | null;
sheets: MedicalReportSheet[];
structuredTemplate?: MedicalReportStructuredTemplate | null;
currentSheet?: {
key: string;
name: string;
fields: MedicalReportSheetField[];
} | null;
summary: MedicalReportSummary;
};
const props = defineProps<Props>();
const groupedFields = computed(() => {
return (props.currentSheet?.fields ?? []).reduce(
(groups, field) => {
const group = groups[field.row_label] ?? [];
group.push(field);
groups[field.row_label] = group;
return groups;
},
{} as Record<string, MedicalReportSheetField[]>,
);
});
const structuredEntries = ref<MedicalReportStructuredEntry[]>([]);
const structuredSectionEntries = ref<Record<string, MedicalReportStructuredEntry[]>>({});
watch(
() => props.structuredTemplate?.entries ?? [],
(entries) => {
structuredEntries.value = entries.map((entry) => ({ ...entry }));
},
{ immediate: true },
);
watch(
() => props.structuredTemplate?.sections ?? [],
(sections) => {
structuredSectionEntries.value = sections.reduce(
(entries, section) => {
entries[section.key] = section.entries.map((entry) => ({ ...entry }));
return entries;
},
{} as Record<string, MedicalReportStructuredEntry[]>,
);
},
{ immediate: true },
);
const addStructuredEntry = (): void => {
structuredEntries.value.push({ ...(props.structuredTemplate?.empty_entry ?? {}) });
};
const removeStructuredEntry = (index: number): void => {
structuredEntries.value.splice(index, 1);
};
const addStructuredSectionEntry = (section: MedicalReportStructuredSection): void => {
const entries = structuredSectionEntries.value[section.key] ?? [];
entries.push({ ...section.empty_entry });
structuredSectionEntries.value[section.key] = entries;
};
const removeStructuredSectionEntry = (sectionKey: string, index: number): void => {
structuredSectionEntries.value[sectionKey]?.splice(index, 1);
};
const structuredGridClass = (count: number): string => {
if (count >= 4) {
return 'md:grid-cols-2 xl:grid-cols-4';
}
if (count === 3) {
return 'md:grid-cols-3';
}
if (count === 2) {
return 'md:grid-cols-2';
}
return 'md:grid-cols-1';
};
</script>
<template>
<Head :title="report.name" />
<div class="flex flex-col gap-6 p-4 md:p-6">
<section
class="rounded-3xl border border-slate-200 bg-linear-to-br from-white via-slate-50 to-emerald-50 p-6 shadow-sm"
>
<div class="grid gap-6 xl:grid-cols-[1.3fr_1fr]">
<div class="space-y-3">
<div class="inline-flex w-fit rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-emerald-700 uppercase">
Отчет по отделениям
</div>
<Heading
title="Каждое отделение заполняет только свои показатели"
description="Отделения создаются только из реальных отчетов. Здесь они вводят свои показатели, а для экономистов формируется отдельный свод."
/>
<div class="flex flex-wrap gap-2">
<div class="rounded-full border border-slate-950 bg-slate-950 px-4 py-2 text-sm text-white">
Ввод отделений
</div>
<Link
:href="MedicalReportController.economists({ medicalReport: report.id })"
class="rounded-full border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 transition-colors hover:border-slate-300"
>
Свод экономистов
</Link>
<Link
:href="MedicalReportController.builder({ medicalReport: report.id })"
class="rounded-full border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 transition-colors hover:border-slate-300"
>
Конструктор форм
</Link>
</div>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-white/80 p-4">
<div class="text-xs uppercase tracking-[0.16em] text-slate-500">
Отделений
</div>
<div class="mt-2 text-2xl font-semibold text-slate-900">
{{ summary.department_count }}
</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white/80 p-4">
<div class="text-xs uppercase tracking-[0.16em] text-slate-500">
Блоков ввода
</div>
<div class="mt-2 text-2xl font-semibold text-slate-900">
{{ summary.sheet_count }}
</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white/80 p-4">
<div class="text-xs uppercase tracking-[0.16em] text-slate-500">
Полей отделения
</div>
<div class="mt-2 text-2xl font-semibold text-slate-900">
{{ summary.editable_count }}
</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-white/80 p-4">
<div class="text-xs uppercase tracking-[0.16em] text-slate-500">
Уже заполнено
</div>
<div class="mt-2 text-2xl font-semibold text-slate-900">
{{ summary.filled_count }}
</div>
</div>
</div>
</div>
</section>
<section class="grid gap-6 xl:grid-cols-[320px_1fr]">
<aside class="rounded-3xl border border-slate-200 bg-white p-4 shadow-sm">
<div class="mb-4 text-sm font-semibold text-slate-900">
Отделения
</div>
<div class="space-y-2">
<Link
v-for="department in departments"
:key="department.key"
:href="
MedicalReportController.show(
{ medicalReport: report.id },
{ query: { department: department.key } },
)
"
class="block rounded-2xl border px-4 py-3 transition-colors"
:class="
selectedDepartment === department.key
? 'border-emerald-300 bg-emerald-50 text-emerald-900'
: 'border-slate-200 bg-slate-50 text-slate-700 hover:border-slate-300 hover:bg-white'
"
>
<div class="text-[11px] uppercase tracking-[0.16em] text-slate-400">
{{ department.profile_name ?? 'Без профиля' }}
</div>
<div class="font-medium">{{ department.name }}</div>
<div class="mt-1 text-xs text-slate-500">
{{ department.report_input_type_label ?? 'Тип ввода не задан' }}
</div>
<div class="mt-1 text-xs text-slate-500">
Заполнено полей: {{ department.filled_count }}
</div>
<div class="mt-1 text-xs">
<span
:class="
department.has_template
? 'text-emerald-600'
: 'text-amber-600'
"
>
{{
department.has_template
? 'Шаблон настроен'
: 'Шаблон в работе'
}}
</span>
</div>
</Link>
</div>
</aside>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm">
<div
class="mb-6 flex flex-col gap-4 border-b border-slate-100 pb-5 md:flex-row md:items-end md:justify-between"
>
<div>
<div class="text-xs uppercase tracking-[0.16em] text-slate-500">
Отделение
</div>
<h1 class="mt-2 text-2xl font-semibold text-slate-950">
{{
departments.find(
(department) =>
department.key === selectedDepartment,
)?.name ?? 'Отделение не выбрано'
}}
</h1>
<div
v-if="departments.find((department) => department.key === selectedDepartment)?.report_input_type_label"
class="mt-2 inline-flex rounded-full bg-sky-100 px-3 py-1 text-xs font-semibold text-sky-700"
>
{{
departments.find(
(department) => department.key === selectedDepartment,
)?.report_input_type_label
}}
</div>
<p class="mt-1 text-sm text-slate-500">
Доступны только те блоки первой таблицы Excel, в которых у этого отделения есть свои показатели.
</p>
</div>
<div class="text-sm text-slate-500">
Обновлено:
{{
report.updated_at
? new Date(report.updated_at).toLocaleString('ru-RU')
: 'не сохранялось'
}}
</div>
</div>
<div
v-if="!structuredTemplate && sheets.length === 0"
class="rounded-2xl bg-slate-50 p-6 text-sm text-slate-500"
>
<div>
Для выбранного отделения пока не найдено полей в первой таблице Excel.
</div>
<div class="mt-3">
Источники:
</div>
<ul class="mt-2 space-y-1 text-xs text-slate-600">
<li
v-for="source in departments.find((department) => department.key === selectedDepartment)?.sources ?? []"
:key="source.path"
>
{{ source.name }}
</li>
</ul>
</div>
<template v-else-if="structuredTemplate">
<div class="mb-6 rounded-3xl border border-emerald-200 bg-emerald-50/70 p-4">
<div class="text-sm font-semibold text-emerald-900">
{{ structuredTemplate.title }}
</div>
<div class="mt-1 text-sm text-emerald-700">
{{ structuredTemplate.description }}
</div>
</div>
<Form
v-bind="MedicalReportController.updateStructured.form({ medicalReport: report.id })"
class="space-y-6"
v-slot="{ errors, processing, recentlySuccessful }"
>
<input type="hidden" name="department" :value="selectedDepartment ?? ''" />
<input type="hidden" name="template_key" :value="structuredTemplate.key" />
<template v-if="structuredTemplate.sections?.length">
<div
v-for="section in structuredTemplate.sections"
:key="section.key"
class="space-y-4 rounded-3xl border border-slate-200 bg-slate-50/80 p-4"
>
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<div class="text-lg font-semibold text-slate-900">
{{ section.title }}
</div>
<div class="mt-1 text-sm text-slate-500">
Экономистам уйдет только блок "Итого" по этой секции.
</div>
</div>
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
<div
v-for="total in section.totals"
:key="`${section.key}-${total.key}`"
class="rounded-2xl border border-white bg-white px-4 py-3"
>
<div class="text-xs uppercase tracking-[0.16em] text-slate-500">
{{ total.label }}
</div>
<div class="mt-2 text-lg font-semibold text-slate-900">
{{ total.value || '0' }}
</div>
</div>
</div>
</div>
<div class="space-y-4">
<div
v-for="(entry, index) in structuredSectionEntries[section.key] ?? []"
:key="`${section.key}-${index}`"
class="rounded-3xl border border-slate-200 bg-white p-4"
>
<div class="mb-4 flex items-center justify-between gap-4">
<div class="text-sm font-semibold text-slate-900">
Строка {{ index + 1 }}
</div>
<Button
type="button"
variant="outline"
size="sm"
:disabled="(structuredSectionEntries[section.key] ?? []).length === 1"
@click="removeStructuredSectionEntry(section.key, index)"
>
Удалить
</Button>
</div>
<div
class="grid gap-4"
:class="structuredGridClass(section.fields.length)"
>
<div
v-for="field in section.fields"
:key="`${section.key}-${field.key}`"
class="rounded-2xl border border-slate-100 bg-slate-50 p-4"
>
<Label
v-if="field.type === 'department-select'"
class="text-sm leading-5 text-slate-800"
>
{{ field.label }}
</Label>
<Label
v-else
:for="`${section.key}-${field.key}-${index}`"
class="text-sm leading-5 text-slate-800"
>
{{ field.label }}
</Label>
<input
v-if="field.type === 'department-select'"
type="hidden"
:name="`sections[${section.key}][entries][${index}][${field.key}]`"
:value="entry[field.key] ?? ''"
/>
<Select
v-if="field.type === 'department-select'"
v-model="entry[field.key]"
>
<SelectTrigger class="mt-3 w-full">
<SelectValue :placeholder="`Выберите: ${field.label}`" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="option in structuredTemplate.department_options"
:key="option.id"
:value="String(option.id)"
>
{{ option.name }}
</SelectItem>
</SelectContent>
</Select>
<Input
v-else
:id="`${section.key}-${field.key}-${index}`"
v-model="entry[field.key]"
:name="`sections[${section.key}][entries][${index}][${field.key}]`"
class="mt-3"
/>
<InputError
class="mt-2"
:message="errors[`sections.${section.key}.entries.${index}.${field.key}`]"
/>
</div>
</div>
</div>
</div>
<Button type="button" variant="outline" @click="addStructuredSectionEntry(section)">
Добавить строку в секцию
</Button>
</div>
</template>
<div v-else class="space-y-4">
<div
v-for="(entry, index) in structuredEntries"
:key="index"
class="rounded-3xl border border-slate-200 bg-slate-50/80 p-4"
>
<div class="mb-4 flex items-center justify-between gap-4">
<div class="text-sm font-semibold text-slate-900">
Строка {{ index + 1 }}
</div>
<Button
type="button"
variant="outline"
size="sm"
:disabled="structuredEntries.length === 1"
@click="removeStructuredEntry(index)"
>
Удалить
</Button>
</div>
<div
class="grid gap-4"
:class="structuredGridClass(structuredTemplate.fields?.length ?? 1)"
>
<div
v-for="field in structuredTemplate.fields ?? []"
:key="field.key"
class="rounded-2xl border border-white bg-white p-4"
>
<Label
v-if="field.type === 'department-select'"
class="text-sm leading-5 text-slate-800"
>
{{ field.label }}
</Label>
<Label
v-else
:for="`${field.key}-${index}`"
class="text-sm leading-5 text-slate-800"
>
{{ field.label }}
</Label>
<input
v-if="field.type === 'department-select'"
type="hidden"
:name="`entries[${index}][${field.key}]`"
:value="entry[field.key] ?? ''"
/>
<Select
v-if="field.type === 'department-select'"
v-model="entry[field.key]"
>
<SelectTrigger class="mt-3 w-full">
<SelectValue :placeholder="`Выберите: ${field.label}`" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="option in structuredTemplate.department_options"
:key="option.id"
:value="String(option.id)"
>
{{ option.name }}
</SelectItem>
</SelectContent>
</Select>
<Input
v-else
:id="`${field.key}-${index}`"
v-model="entry[field.key]"
:name="`entries[${index}][${field.key}]`"
class="mt-3"
/>
<InputError
class="mt-2"
:message="errors[`entries.${index}.${field.key}`]"
/>
</div>
</div>
</div>
<div v-if="structuredTemplate.totals?.length" class="grid gap-2 sm:grid-cols-2 xl:grid-cols-4">
<div
v-for="total in structuredTemplate.totals"
:key="total.key"
class="rounded-2xl border border-slate-200 bg-white px-4 py-3"
>
<div class="text-xs uppercase tracking-[0.16em] text-slate-500">
{{ total.label }}
</div>
<div class="mt-2 text-lg font-semibold text-slate-900">
{{ total.value || '0' }}
</div>
</div>
</div>
<Button type="button" variant="outline" @click="addStructuredEntry">
Добавить строку
</Button>
</div>
<div class="flex flex-wrap items-center gap-4">
<Button :disabled="processing">
Сохранить данные отделения
</Button>
<p v-if="recentlySuccessful" class="text-sm text-emerald-600">
Изменения сохранены.
</p>
</div>
</Form>
</template>
<template v-else>
<div class="mb-6 flex flex-wrap gap-2">
<Link
v-for="sheet in sheets"
:key="sheet.key"
:href="
MedicalReportController.show(
{ medicalReport: report.id },
{
query: {
department: selectedDepartment,
sheet: sheet.key,
},
},
)
"
class="rounded-full border px-4 py-2 text-sm transition-colors"
:class="
selectedSheet === sheet.key
? 'border-slate-950 bg-slate-950 text-white'
: 'border-slate-200 bg-slate-50 text-slate-700 hover:border-slate-300'
"
>
{{ sheet.name }}
<span class="ml-2 text-xs opacity-70">
{{ sheet.filled_count }}/{{ sheet.editable_count }}
</span>
</Link>
</div>
<Form
v-if="currentSheet"
v-bind="MedicalReportController.update.form({ medicalReport: report.id })"
class="space-y-6"
v-slot="{ errors, processing, recentlySuccessful }"
>
<input
type="hidden"
name="department"
:value="selectedDepartment ?? ''"
/>
<input type="hidden" name="sheet" :value="currentSheet.key" />
<div
v-for="(fields, groupTitle) in groupedFields"
:key="groupTitle"
class="rounded-3xl border border-slate-200 bg-slate-50/80 p-4"
>
<div class="mb-4 text-sm font-semibold text-slate-900">
{{ groupTitle }}
</div>
<div class="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
<div
v-for="field in fields"
:key="field.coordinate"
class="rounded-2xl border border-white bg-white p-4"
>
<Label :for="field.coordinate" class="text-sm leading-5 text-slate-800">
{{ field.column_label }}
</Label>
<div class="mt-1 text-xs text-slate-500">
{{ field.description }}
</div>
<Input
:id="field.coordinate"
:name="`values[${field.coordinate}]`"
:default-value="field.value"
class="mt-3"
/>
<InputError
class="mt-2"
:message="errors[`values.${field.coordinate}`]"
/>
</div>
</div>
</div>
<div class="flex items-center gap-4">
<Button :disabled="processing">
Сохранить данные отделения
</Button>
<p v-if="recentlySuccessful" class="text-sm text-emerald-600">
Изменения сохранены.
</p>
</div>
</Form>
</template>
</div>
</section>
</div>
</template>