Files
CRM_Montzar/apps/web/src/views/CurrencySettingsView.vue
T

324 lines
7.3 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { computed, onMounted, ref } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import { CURRENCY_OPTIONS, formatMoney } from '@/lib/money';
import MoneyInput from '@/components/MoneyInput.vue';
import type { OrganizationDto, UpdateOrgCurrenciesRequest } from '@mento/shared';
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const ok = ref('');
const org = ref<OrganizationDto | null>(null);
const baseCurrency = ref('IRR');
const enabled = ref<string[]>(['IRR', 'USD', 'EUR']);
const rates = ref<Record<string, string>>({});
const rateCurrencies = computed(() =>
enabled.value.filter((c) => c !== baseCurrency.value),
);
function toggleCurrency(code: string) {
if (code === baseCurrency.value) return;
if (enabled.value.includes(code)) {
enabled.value = enabled.value.filter((c) => c !== code);
} else {
enabled.value = [...enabled.value, code];
if (!rates.value[code]) rates.value[code] = '';
}
}
function onBaseChange() {
if (!enabled.value.includes(baseCurrency.value)) {
enabled.value = [baseCurrency.value, ...enabled.value];
}
}
async function load() {
loading.value = true;
error.value = '';
try {
const { data } = await api.get<OrganizationDto>('/organizations/current');
org.value = data;
baseCurrency.value = data.baseCurrency || 'IRR';
enabled.value = data.enabledCurrencies?.length
? [...data.enabledCurrencies]
: ['IRR'];
const next: Record<string, string> = {};
for (const r of data.exchangeRates ?? []) {
next[r.currency] = String(r.rateToBase);
}
rates.value = next;
} catch (e) {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری تنظیمات ارز ناموفق بود.';
}
} finally {
loading.value = false;
}
}
async function save() {
saving.value = true;
error.value = '';
ok.value = '';
try {
const body: UpdateOrgCurrenciesRequest = {
baseCurrency: baseCurrency.value,
enabledCurrencies: enabled.value,
rates: rateCurrencies.value
.filter((c) => rates.value[c] && Number(rates.value[c]) > 0)
.map((c) => ({
currency: c,
rateToBase: Number(rates.value[c]),
})),
};
const { data } = await api.patch<OrganizationDto>(
'/organizations/current/currencies',
body,
);
org.value = data;
ok.value = 'تنظیمات ارز ذخیره شد.';
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg)
? msg.join('، ')
: (msg ?? 'ذخیره ناموفق بود');
} else {
error.value = 'ذخیره ناموفق بود';
}
} finally {
saving.value = false;
}
}
onMounted(load);
</script>
<template>
<section class="page">
<header class="page-head">
<div class="head-copy">
<h1>ارزها و نرخ تبدیل</h1>
<p class="lead">
ارز پایه برای جمع‌ها و پیش‌بینی استفاده می‌شود. نرخ یعنی چند واحد ارز پایه
برابر با ۱ واحد ارز خارجی است.
</p>
</div>
</header>
<p v-if="loading" class="muted">در حال بارگذاری…</p>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="ok" class="ok">{{ ok }}</p>
<form v-if="!loading" class="panel" @submit.prevent="save">
<div class="field">
<label>ارز پایه</label>
<select v-model="baseCurrency" @change="onBaseChange">
<option
v-for="c in CURRENCY_OPTIONS"
:key="c.code"
:value="c.code"
>
{{ c.labelFa }} ({{ c.code }})
</option>
</select>
</div>
<div class="field">
<label>ارزهای فعال</label>
<div class="chips">
<label
v-for="c in CURRENCY_OPTIONS"
:key="c.code"
class="chip"
:class="{ on: enabled.includes(c.code), locked: c.code === baseCurrency }"
>
<input
type="checkbox"
:checked="enabled.includes(c.code)"
:disabled="c.code === baseCurrency"
@change="toggleCurrency(c.code)"
/>
{{ c.labelFa }}
</label>
</div>
</div>
<div v-if="rateCurrencies.length" class="rates">
<h2>نرخ به {{ baseCurrency }}</h2>
<div v-for="code in rateCurrencies" :key="code" class="rate-row">
<span>۱ {{ code }} =</span>
<MoneyInput
v-model="rates[code]"
:decimals="6"
:placeholder="`نرخ به ${baseCurrency}`"
/>
<span>{{ baseCurrency }}</span>
</div>
<p v-if="rates.USD" class="muted example">
مثال: ۱ دلار ≈
{{ formatMoney(Number(rates.USD) || 0, baseCurrency) }}
</p>
</div>
<button class="btn primary" type="submit" :disabled="saving">
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
</form>
</section>
</template>
<style scoped>
.page {
display: grid;
gap: 1rem;
max-width: 640px;
min-width: 0;
overflow-x: clip;
}
.page-head {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
}
.head-copy {
min-width: 0;
}
h1 {
margin: 0 0 0.3rem;
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
font-weight: 800;
}
.lead,
.muted {
margin: 0;
color: #64748b;
}
.panel {
display: grid;
gap: 1rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 18px;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
padding: 1.1rem;
}
.field {
display: grid;
gap: 0.4rem;
}
label {
color: #64748b;
font-size: 0.9rem;
}
select,
input[type='number'] {
border: 1px solid #e2e8f0;
border-radius: var(--radius);
min-height: var(--control-h);
padding: 0 0.85rem;
font: inherit;
background: #fff;
width: 100%;
box-sizing: border-box;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border: 1px solid #e2e8f0;
border-radius: 999px;
padding: 0.4rem 0.75rem;
cursor: pointer;
color: inherit;
font-size: 0.9rem;
}
.chip.on {
border-color: #99f6e4;
background: #f0fdfa;
color: #0f766e;
}
.chip.locked {
opacity: 0.85;
}
.rates {
display: grid;
gap: 0.55rem;
}
.rates h2 {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: #0f172a;
}
.rate-row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.5rem;
align-items: center;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: var(--control-h);
padding: 0 0.95rem;
border-radius: var(--radius);
font-weight: 700;
border: 1px solid transparent;
cursor: pointer;
font: inherit;
}
.btn.primary {
background: #14b8a6;
color: #fff;
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
}
.error {
margin: 0;
color: #b91c1c;
}
.ok {
margin: 0;
color: #0f766e;
font-weight: 600;
}
@media (max-width: 700px) {
.rate-row {
grid-template-columns: 1fr;
}
}
</style>