Files
portal/app/resources/js/views/admin/AdminSupplierPricesView.vue
T

157 lines
5.8 KiB
Vue

<template>
<div class="admin-supplier-prices-view">
<h1 class="text-h4 mb-6">Цены поставщиков (закупка)</h1>
<v-alert
v-if="fetchError"
type="warning"
variant="tonal"
class="mb-4"
density="compact"
data-testid="suppliers-fetch-error"
closable
@click:close="fetchError = null"
>
{{ fetchError }}
</v-alert>
<v-card elevation="1">
<v-data-table :headers="headers" :items="suppliers" density="comfortable" class="numeric-tnum">
<template #[`item.cost_rub`]="{ item }">
<v-text-field
v-model="item.cost_rub"
type="number"
step="0.01"
min="0"
density="compact"
hide-details
variant="plain"
:aria-label="`Cost () для ${item.name}`"
/>
</template>
<template #[`item.quality_score`]="{ item }">
<v-text-field
v-model="item.quality_score"
type="number"
step="0.01"
min="0"
max="9.99"
density="compact"
hide-details
variant="plain"
:aria-label="`Quality для ${item.name}`"
/>
</template>
<template #[`item.is_active`]="{ item }">
<v-switch
v-model="item.is_active"
hide-details
inset
density="compact"
:aria-label="`Active для ${item.name}`"
/>
</template>
<template #[`item.actions`]="{ item }">
<div class="d-flex flex-column align-end ga-1">
<v-btn size="small" color="primary" :loading="!!saving[item.id]" @click="save(item)">
Сохранить
</v-btn>
<v-tooltip v-if="errorMessages[item.id]" location="left">
<template #activator="{ props: tooltipProps }">
<v-icon
v-bind="tooltipProps"
color="error"
size="small"
:data-testid="`supplier-error-${item.id}`"
>
mdi-alert-circle
</v-icon>
</template>
<span>{{ errorMessages[item.id] }}</span>
</v-tooltip>
</div>
</template>
</v-data-table>
</v-card>
<v-snackbar
v-model="successToastOpen"
:timeout="3000"
color="success"
location="bottom right"
data-testid="supplier-success-toast"
>
{{ successToastText }}
</v-snackbar>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import { getAdminSuppliers, updateAdminSupplier, type AdminSupplier } from '../../api/admin';
import { extractErrorMessage } from '../../api/client';
/**
* SaaS-admin → Цены поставщиков (Plan 4 Task 10, Sprint 1 G2 error handling).
*
* Backend: AdminSuppliersController (GET/PATCH).
* Палитра Forest + JetBrains Mono для tnum-цифр.
*
* defineExpose ниже — для Vitest unit-тестов.
*/
const suppliers = ref<AdminSupplier[]>([]);
const saving = reactive<Record<number, boolean>>({});
const errorMessages = reactive<Record<number, string>>({});
const fetchError = ref<string | null>(null);
const successToastOpen = ref(false);
const successToastText = ref('');
const headers = [
{ title: 'Код', key: 'code', sortable: false, width: 80 },
{ title: 'Название', key: 'name', sortable: false },
{ title: 'Цена (₽)', key: 'cost_rub', sortable: false, width: 140 },
{ title: 'Качество', key: 'quality_score', sortable: false, width: 100 },
{ title: 'Активен', key: 'is_active', sortable: false, width: 100 },
{ title: 'Действия', key: 'actions', sortable: false, width: 120 },
];
async function load(): Promise<void> {
fetchError.value = null;
try {
suppliers.value = await getAdminSuppliers();
} catch (err) {
fetchError.value = extractErrorMessage(err, 'Не удалось загрузить список поставщиков.');
}
}
async function save(s: AdminSupplier): Promise<void> {
saving[s.id] = true;
delete errorMessages[s.id]; // очистить предыдущую ошибку перед retry
try {
await updateAdminSupplier(s.id, {
cost_rub: s.cost_rub,
quality_score: s.quality_score,
is_active: s.is_active,
});
successToastText.value = `Сохранено: ${s.name} (${s.code}).`;
successToastOpen.value = true;
} catch (err) {
errorMessages[s.id] = extractErrorMessage(err, 'Не удалось сохранить изменения.');
} finally {
saving[s.id] = false;
}
}
onMounted(load);
defineExpose({ load, save, suppliers, saving, errorMessages, fetchError, successToastOpen, successToastText });
</script>
<style scoped>
.numeric-tnum :deep(td) {
font-feature-settings: 'tnum';
font-family: 'JetBrains Mono', monospace;
}
</style>