157 lines
5.8 KiB
Vue
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>
|