191 lines
7.1 KiB
Vue
191 lines
7.1 KiB
Vue
<script setup>
|
||
import { ref, h, computed } from 'vue'
|
||
import { router, Link } from '@inertiajs/vue3'
|
||
import {
|
||
NButton, NText, NDataTable, NEmpty, NDropdown, NIcon, NScrollbar,
|
||
NTag, useDialog, useMessage,
|
||
NEl,
|
||
} from 'naive-ui'
|
||
import { TbReportMedical, TbPlus, TbDotsVertical, TbLayoutGrid } from 'vue-icons-plus/tb'
|
||
import AppLayout from '../../Layouts/AppLayout.vue'
|
||
import AppContainer from '../../Components/AppContainer.vue'
|
||
import SectionCard from '../../Components/SectionCard.vue'
|
||
import PageBanner from '../../Components/PageBanner.vue'
|
||
import PresetCard from './Components/PresetCard.vue'
|
||
import TemplatePickerModal from './Components/TemplatePickerModal.vue'
|
||
|
||
const props = defineProps({
|
||
documents: { type: Array, default: () => [] },
|
||
presets: { type: Array, default: () => [] },
|
||
categories: { type: Array, default: () => [] },
|
||
canManage: { type: Boolean, default: false },
|
||
})
|
||
|
||
const dialog = useDialog()
|
||
const message = useMessage()
|
||
const showPicker = ref(false)
|
||
|
||
// «Отчёт с нуля» + первые шаблоны в ленте создания.
|
||
const blankPreset = computed(() => props.presets.find((p) => p.key === 'blank'))
|
||
const featured = computed(() => props.presets.filter((p) => p.key !== 'blank').slice(0, 5))
|
||
|
||
const openPreset = (preset) => {
|
||
router.get(preset.key === 'blank' ? '/reports/new' : `/reports/new?preset=${preset.key}`)
|
||
}
|
||
|
||
const openDocument = (id) => router.get(`/reports/${id}`)
|
||
|
||
const rowProps = (row) => ({ style: 'cursor: pointer;', onClick: () => openDocument(row.id) })
|
||
|
||
const duplicate = (row) => router.post(`/reports/${row.id}/duplicate`)
|
||
|
||
const remove = (row) => {
|
||
dialog.warning({
|
||
title: 'Удалить отчёт',
|
||
content: `Удалить «${row.name}»? Действие необратимо.`,
|
||
positiveText: 'Удалить',
|
||
negativeText: 'Отмена',
|
||
onPositiveClick: () => router.delete(`/reports/${row.id}`, {
|
||
preserveScroll: true,
|
||
onSuccess: () => message.success('Отчёт удалён'),
|
||
}),
|
||
})
|
||
}
|
||
|
||
const rowMenu = (row) => [
|
||
{ key: 'open', label: 'Открыть' },
|
||
{ key: 'duplicate', label: 'Дублировать' },
|
||
{ key: 'delete', label: 'Удалить' },
|
||
]
|
||
|
||
const onMenuSelect = (key, row) => {
|
||
if (key === 'open') openDocument(row.id)
|
||
if (key === 'duplicate') duplicate(row)
|
||
if (key === 'delete') remove(row)
|
||
}
|
||
|
||
const columns = computed(() => [
|
||
{
|
||
title: 'Название',
|
||
key: 'name',
|
||
render: (row) => h('div', {}, [
|
||
h('div', { style: 'font-weight: 600;' }, row.name),
|
||
row.description ? h(NText, { depth: 3, style: 'font-size: 12px;' }, () => row.description) : null,
|
||
]),
|
||
},
|
||
{ title: 'Источник', key: 'datasetLabel', width: 200, render: (row) => h(NTag, { size: 'small', bordered: false, round: true }, () => row.datasetLabel) },
|
||
{ title: 'Автор', key: 'author', width: 180, render: (row) => row.author ?? '—' },
|
||
{ title: 'Изменено', key: 'updatedAt', width: 130, render: (row) => row.updatedAt ?? '—' },
|
||
{
|
||
title: '',
|
||
key: 'actions',
|
||
width: 56,
|
||
render: (row) => props.canManage
|
||
? h(NDropdown, {
|
||
trigger: 'click',
|
||
options: rowMenu(row),
|
||
onSelect: (key) => onMenuSelect(key, row),
|
||
}, () => h(NButton, {
|
||
round: true,
|
||
size: 'small',
|
||
onClick: (e) => e.stopPropagation(),
|
||
}, () => h(NIcon, null, () => h(TbDotsVertical)))) : null,
|
||
},
|
||
])
|
||
</script>
|
||
|
||
<template>
|
||
<AppLayout>
|
||
<AppContainer>
|
||
<PageBanner
|
||
title="Отчёты"
|
||
:icon="TbReportMedical"
|
||
:breadcrumbs="[{ label: 'Главная', href: '/', tag: Link }]"
|
||
>
|
||
<template #meta>
|
||
<NText depth="3" style="font-size: 13px;">Создавайте отчёты с нуля или по готовым шаблонам</NText>
|
||
</template>
|
||
</PageBanner>
|
||
|
||
<SectionCard v-if="canManage" title="Создание отчёта">
|
||
<template #header-extra>
|
||
<NButton text type="primary" @click="showPicker = true">
|
||
<template #icon><TbLayoutGrid /></template>
|
||
Открыть все шаблоны
|
||
</NButton>
|
||
</template>
|
||
<NScrollbar x-scrollable class="pb-3">
|
||
<div class="create-row">
|
||
<NEl class="create-card" @click="openPreset({ key: 'blank' })">
|
||
<div class="blank-tile"><NIcon :size="28"><TbPlus /></NIcon></div>
|
||
<NText class="blank-title">Отчёт с нуля</NText>
|
||
</NEl>
|
||
<div v-for="preset in featured" :key="preset.key" class="create-card" @click="openPreset(preset)">
|
||
<PresetCard :preset="preset" />
|
||
</div>
|
||
</div>
|
||
</NScrollbar>
|
||
</SectionCard>
|
||
|
||
<SectionCard title="Отчёты" no-padding>
|
||
<NEmpty v-if="!documents.length" description="Пока нет сохранённых отчётов" style="padding: 32px;" />
|
||
<NDataTable
|
||
v-else
|
||
min-height="calc(100vh - 570px)"
|
||
:columns="columns"
|
||
:data="documents"
|
||
:row-props="rowProps"
|
||
:bordered="false"
|
||
size="small"
|
||
/>
|
||
</SectionCard>
|
||
|
||
<TemplatePickerModal
|
||
v-model:show="showPicker"
|
||
:presets="presets"
|
||
:categories="categories"
|
||
@select="openPreset"
|
||
/>
|
||
</AppContainer>
|
||
</AppLayout>
|
||
</template>
|
||
|
||
<style scoped>
|
||
:deep(.n-data-table-th) { background: transparent !important; }
|
||
:deep(.n-data-table) { background: transparent; }
|
||
:deep(.n-data-table-wrapper) { border-radius: 0; }
|
||
:deep(.n-data-table-th .n-data-table-th__title) { font-size: 12px; }
|
||
:deep(.n-data-table-td) { font-size: 13px; }
|
||
|
||
.create-row {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding-top: 2px;
|
||
}
|
||
.create-card {
|
||
width: 220px;
|
||
flex-shrink: 0;
|
||
}
|
||
.blank-tile {
|
||
height: 84px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--primary-color);
|
||
border-radius: 8px;
|
||
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
|
||
margin-bottom: 10px;
|
||
}
|
||
.create-card .blank-tile { border: 1px dashed color-mix(in srgb, var(--primary-color) 40%, transparent); }
|
||
.create-card:first-child {
|
||
border: 1px solid var(--n-border-color, rgba(255,255,255,.1));
|
||
border-radius: 12px;
|
||
padding: 14px;
|
||
cursor: pointer;
|
||
transition: border-color .15s, transform .15s;
|
||
background: var(--n-card-color);
|
||
}
|
||
.create-card:first-child:hover { border-color: var(--primary-color); transform: translateY(-2px); }
|
||
.blank-title { display: block; font-weight: 600; font-size: 14px; }
|
||
</style>
|