148 lines
5.2 KiB
Vue
148 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* Диалог установки точного ₽-баланса тенанта (SaaS-admin).
|
|
* Используется из карточки тенанта (TenantDetailHeader) и из строки таблицы
|
|
* списка (TenantsTable). Семантика «установить точную сумму» — сервер сам
|
|
* считает знаковую дельту и пишет manual_adjustment + audit.
|
|
*/
|
|
import { computed, ref, watch } from 'vue';
|
|
import { updateTenantBalance } from '../../api/admin';
|
|
import { extractErrorMessage } from '../../api/client';
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean;
|
|
tenantId: number;
|
|
tenantName: string;
|
|
currentBalanceRub: number;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean];
|
|
saved: [payload: { balance_rub: string; delta: string; transaction_id: number }];
|
|
}>();
|
|
|
|
const newBalance = ref('');
|
|
const reason = ref('');
|
|
const submitting = ref(false);
|
|
const errorMsg = ref<string | null>(null);
|
|
|
|
const targetNormalized = computed(() => {
|
|
const raw = newBalance.value.trim().replace(',', '.');
|
|
if (!/^-?\d+(\.\d{1,2})?$/.test(raw)) return '';
|
|
return Number(raw).toFixed(2);
|
|
});
|
|
|
|
const delta = computed(() => {
|
|
if (targetNormalized.value === '') return '';
|
|
return (Number(targetNormalized.value) - props.currentBalanceRub).toFixed(2);
|
|
});
|
|
|
|
const canSave = computed(
|
|
() => !submitting.value && targetNormalized.value !== '' && delta.value !== '' && Number(delta.value) !== 0,
|
|
);
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(open) => {
|
|
if (open) {
|
|
newBalance.value = '';
|
|
reason.value = '';
|
|
errorMsg.value = null;
|
|
}
|
|
},
|
|
);
|
|
|
|
async function submit(): Promise<void> {
|
|
if (!canSave.value) return;
|
|
submitting.value = true;
|
|
errorMsg.value = null;
|
|
try {
|
|
const payload: { balance_rub: string; reason?: string } = { balance_rub: targetNormalized.value };
|
|
if (reason.value.trim() !== '') payload.reason = reason.value.trim();
|
|
const result = await updateTenantBalance(props.tenantId, payload);
|
|
emit('saved', { balance_rub: result.balance_rub, delta: result.delta, transaction_id: result.transaction_id });
|
|
emit('update:modelValue', false);
|
|
} catch (e) {
|
|
errorMsg.value = extractErrorMessage(e, 'Не удалось изменить баланс.');
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
}
|
|
|
|
function close(): void {
|
|
emit('update:modelValue', false);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<v-dialog
|
|
:model-value="modelValue"
|
|
max-width="460"
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
>
|
|
<v-card>
|
|
<v-card-title class="text-h6">Изменить баланс</v-card-title>
|
|
<v-card-subtitle>{{ tenantName }}</v-card-subtitle>
|
|
<v-card-text>
|
|
<div class="text-body-2 text-medium-emphasis mb-3">
|
|
Текущий баланс: <strong class="num">{{ currentBalanceRub.toFixed(2) }} ₽</strong>
|
|
</div>
|
|
|
|
<v-text-field
|
|
v-model="newBalance"
|
|
label="Новый баланс, ₽"
|
|
type="text"
|
|
inputmode="decimal"
|
|
density="comfortable"
|
|
data-testid="balance-input"
|
|
:hint="targetNormalized === '' && newBalance !== '' ? 'Формат: 1234.56' : ''"
|
|
persistent-hint
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="reason"
|
|
label="Причина (необязательно)"
|
|
type="text"
|
|
density="comfortable"
|
|
maxlength="500"
|
|
class="mt-2"
|
|
data-testid="reason-input"
|
|
/>
|
|
|
|
<div v-if="delta !== ''" class="preview mt-3 text-body-2">
|
|
было <span class="num">{{ currentBalanceRub.toFixed(2) }} ₽</span>
|
|
→ станет <span class="num">{{ targetNormalized }} ₽</span>
|
|
(<span class="num" :class="Number(delta) < 0 ? 'text-error' : 'text-success'">
|
|
{{ Number(delta) > 0 ? '+' : '' }}{{ delta }} ₽
|
|
</span>)
|
|
</div>
|
|
|
|
<v-alert v-if="errorMsg" type="error" variant="tonal" density="compact" class="mt-3">
|
|
{{ errorMsg }}
|
|
</v-alert>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn variant="text" :disabled="submitting" @click="close">Отмена</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="flat"
|
|
:loading="submitting"
|
|
:disabled="!canSave"
|
|
data-testid="balance-save"
|
|
@click="submit"
|
|
>
|
|
Сохранить
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.num {
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-feature-settings: 'tnum';
|
|
}
|
|
</style>
|