375 lines
17 KiB
Vue
375 lines
17 KiB
Vue
<script setup lang="ts">
|
||
import { Form, Head } from '@inertiajs/vue3';
|
||
import Heading from '@/components/Heading.vue';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from '@/components/ui/card';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { index, store } from '@/routes/reports/operations';
|
||
import { store as storeService } from '@/routes/reports/operations/services';
|
||
import type {
|
||
ReportPeriodSummary,
|
||
ServiceCatalogItem,
|
||
ServiceEntryMap,
|
||
Team,
|
||
} from '@/types';
|
||
|
||
type Props = {
|
||
currentTeam?: Team | null;
|
||
periods: ReportPeriodSummary[];
|
||
departments: Array<{ id: number; name: string }>;
|
||
serviceCatalogs: ServiceCatalogItem[];
|
||
recipientDepartments: Array<{ id: number; name: string }>;
|
||
selectedPeriodId?: number | null;
|
||
selectedProviderDepartmentId?: number | null;
|
||
selectedServiceCatalogId?: number | null;
|
||
entries: ServiceEntryMap;
|
||
canEdit: boolean;
|
||
};
|
||
|
||
const props = defineProps<Props>();
|
||
|
||
defineOptions({
|
||
layout: (props: { currentTeam?: Team | null }) => ({
|
||
breadcrumbs: [
|
||
{
|
||
title: 'Отчеты',
|
||
href: props.currentTeam ? index(props.currentTeam.slug) : '/',
|
||
},
|
||
{
|
||
title: 'Объемные показатели',
|
||
href: props.currentTeam ? index(props.currentTeam.slug) : '/',
|
||
},
|
||
],
|
||
}),
|
||
});
|
||
|
||
const formatAmount = (value: number) =>
|
||
new Intl.NumberFormat('ru-RU', {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
}).format(value);
|
||
</script>
|
||
|
||
<template>
|
||
<Head title="Объемные показатели" />
|
||
|
||
<div class="flex flex-col gap-6">
|
||
<Heading
|
||
title="Объемные показатели"
|
||
description="Здесь хранится журнал оказанных услуг: какое отделение оказало какую услугу другим отделениям, в каком количестве и по какой цене."
|
||
/>
|
||
|
||
<div class="grid gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||
<Card class="border-sidebar-border/70">
|
||
<CardHeader>
|
||
<CardTitle>Контекст ввода</CardTitle>
|
||
<CardDescription>
|
||
Выберите период, отделение-исполнитель и услугу, чтобы
|
||
заполнить распределение по получателям.
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Form
|
||
v-if="props.currentTeam"
|
||
v-bind="index.form(props.currentTeam.slug)"
|
||
class="space-y-4"
|
||
>
|
||
<div class="grid gap-2">
|
||
<Label for="operations-period">Период</Label>
|
||
<select
|
||
id="operations-period"
|
||
name="period"
|
||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||
>
|
||
<option
|
||
v-for="period in periods"
|
||
:key="period.id"
|
||
:value="period.id"
|
||
:selected="period.id === selectedPeriodId"
|
||
>
|
||
{{ period.label }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="grid gap-2">
|
||
<Label for="operations-provider">
|
||
Отделение-исполнитель
|
||
</Label>
|
||
<select
|
||
id="operations-provider"
|
||
name="provider_department"
|
||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||
>
|
||
<option
|
||
v-for="department in departments"
|
||
:key="department.id"
|
||
:value="department.id"
|
||
:selected="
|
||
department.id ===
|
||
selectedProviderDepartmentId
|
||
"
|
||
>
|
||
{{ department.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="grid gap-2">
|
||
<Label for="operations-service">Услуга</Label>
|
||
<select
|
||
id="operations-service"
|
||
name="service"
|
||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||
>
|
||
<option
|
||
v-for="serviceCatalog in serviceCatalogs"
|
||
:key="serviceCatalog.id"
|
||
:value="serviceCatalog.id"
|
||
:selected="
|
||
serviceCatalog.id ===
|
||
selectedServiceCatalogId
|
||
"
|
||
>
|
||
{{ serviceCatalog.name }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<Button type="submit">Открыть ведомость</Button>
|
||
</Form>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card class="border-sidebar-border/70">
|
||
<CardHeader>
|
||
<CardTitle>Новая услуга</CardTitle>
|
||
<CardDescription>
|
||
Каталог услуг задает единицу измерения и базовую цену,
|
||
от которой дальше считаются затраты.
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Form
|
||
v-if="props.currentTeam"
|
||
v-bind="storeService.form(props.currentTeam.slug)"
|
||
class="space-y-4"
|
||
v-slot="{ errors, processing }"
|
||
>
|
||
<div class="grid gap-2 md:grid-cols-2">
|
||
<div class="grid gap-2">
|
||
<Label for="service-code">Код</Label>
|
||
<Input
|
||
id="service-code"
|
||
name="code"
|
||
placeholder="kdl_total"
|
||
required
|
||
/>
|
||
<p
|
||
v-if="errors.code"
|
||
class="text-sm text-destructive"
|
||
>
|
||
{{ errors.code }}
|
||
</p>
|
||
</div>
|
||
<div class="grid gap-2">
|
||
<Label for="service-name">Название</Label>
|
||
<Input
|
||
id="service-name"
|
||
name="name"
|
||
placeholder="КДЛ плановая служба"
|
||
required
|
||
/>
|
||
<p
|
||
v-if="errors.name"
|
||
class="text-sm text-destructive"
|
||
>
|
||
{{ errors.name }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid gap-2 md:grid-cols-3">
|
||
<div class="grid gap-2">
|
||
<Label for="service-unit">Единица</Label>
|
||
<Input
|
||
id="service-unit"
|
||
name="unit"
|
||
placeholder="усл."
|
||
/>
|
||
</div>
|
||
<div class="grid gap-2">
|
||
<Label for="service-default-price">
|
||
Цена за единицу
|
||
</Label>
|
||
<Input
|
||
id="service-default-price"
|
||
type="number"
|
||
name="default_price"
|
||
min="0"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="grid gap-2">
|
||
<Label for="service-sort-order">Порядок</Label>
|
||
<Input
|
||
id="service-sort-order"
|
||
type="number"
|
||
name="sort_order"
|
||
min="0"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<input type="hidden" name="is_active" value="1" />
|
||
|
||
<Button type="submit" :disabled="processing">
|
||
Добавить услугу
|
||
</Button>
|
||
</Form>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Card class="border-sidebar-border/70">
|
||
<CardHeader>
|
||
<div
|
||
class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
|
||
>
|
||
<div>
|
||
<CardTitle>Распределение услуг</CardTitle>
|
||
<CardDescription>
|
||
Заполняется по отделениям-получателям. Сумма затрат
|
||
считается как количество × цена.
|
||
</CardDescription>
|
||
</div>
|
||
|
||
<Badge :variant="canEdit ? 'default' : 'secondary'">
|
||
{{ canEdit ? 'Доступно редактирование' : 'Период утвержден' }}
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Form
|
||
v-if="
|
||
props.currentTeam &&
|
||
selectedPeriodId &&
|
||
selectedProviderDepartmentId &&
|
||
selectedServiceCatalogId
|
||
"
|
||
v-bind="store.form(props.currentTeam.slug)"
|
||
class="space-y-6"
|
||
v-slot="{ processing }"
|
||
>
|
||
<input
|
||
type="hidden"
|
||
name="report_period_id"
|
||
:value="selectedPeriodId"
|
||
/>
|
||
<input
|
||
type="hidden"
|
||
name="provider_department_id"
|
||
:value="selectedProviderDepartmentId"
|
||
/>
|
||
<input
|
||
type="hidden"
|
||
name="service_catalog_id"
|
||
:value="selectedServiceCatalogId"
|
||
/>
|
||
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full border-collapse text-sm">
|
||
<thead>
|
||
<tr class="border-b">
|
||
<th class="px-3 py-2 text-left">
|
||
Отделение-получатель
|
||
</th>
|
||
<th class="px-3 py-2 text-left">
|
||
Количество
|
||
</th>
|
||
<th class="px-3 py-2 text-left">
|
||
Цена
|
||
</th>
|
||
<th class="px-3 py-2 text-left">
|
||
Сумма
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="department in recipientDepartments"
|
||
:key="department.id"
|
||
class="border-b"
|
||
>
|
||
<td class="px-3 py-3 font-medium">
|
||
{{ department.name }}
|
||
</td>
|
||
<td class="px-3 py-3">
|
||
<Input
|
||
:name="`entries[${department.id}][quantity]`"
|
||
type="number"
|
||
min="0"
|
||
step="0.01"
|
||
:default-value="
|
||
String(
|
||
entries[department.id]
|
||
?.quantity ?? 0,
|
||
)
|
||
"
|
||
:disabled="!canEdit"
|
||
/>
|
||
</td>
|
||
<td class="px-3 py-3">
|
||
<Input
|
||
:name="`entries[${department.id}][unit_price]`"
|
||
type="number"
|
||
min="0"
|
||
step="0.01"
|
||
:default-value="
|
||
String(
|
||
entries[department.id]
|
||
?.unitPrice ?? 0,
|
||
)
|
||
"
|
||
:disabled="!canEdit"
|
||
/>
|
||
</td>
|
||
<td class="px-3 py-3 text-muted-foreground">
|
||
{{
|
||
formatAmount(
|
||
entries[department.id]
|
||
?.totalAmount ?? 0,
|
||
)
|
||
}}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<Button type="submit" :disabled="processing || !canEdit">
|
||
Сохранить распределение
|
||
</Button>
|
||
</Form>
|
||
|
||
<p
|
||
v-else
|
||
class="py-6 text-center text-sm text-muted-foreground"
|
||
>
|
||
Создайте период, услугу и выберите отделение-исполнитель.
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</template>
|