first commit
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

This commit is contained in:
brusnitsyn
2026-04-06 00:06:00 +09:00
commit fb2e6c58e3
409 changed files with 42953 additions and 0 deletions

View File

@@ -0,0 +1,374 @@
<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>