Files
brusnitsyn fb2e6c58e3
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
first commit
2026-04-06 00:06:00 +09:00

375 lines
17 KiB
Vue
Raw Permalink 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 } 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>