fix: share the dashboard assistant and count leads from data
The hero ask box now uses the same confirm-before-create path as the assistant, count questions use real filters, and lead mobile, Jalali follow-ups, and rial labels stay consistent. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,6 +22,8 @@ const emit = defineEmits<{
|
||||
'update:askQuery': [value: string];
|
||||
ask: [preset?: string];
|
||||
clear: [];
|
||||
busy: [value: boolean];
|
||||
notice: [value: { error: string; done: string }];
|
||||
}>();
|
||||
|
||||
const shortcuts = [
|
||||
@@ -69,7 +71,9 @@ function onVoiceTranscript(text: string) {
|
||||
}
|
||||
|
||||
function looksLikeAction(text: string) {
|
||||
return /بساز|بسازید|ثبت کن|ثبت کنید|اضافه کن|ایجاد کن|یک مشتری|مشتری جدید|یک فعالیت|فعالیت جدید/.test(text);
|
||||
return /بساز|بسازید|ثبت کن|ثبت کنید|اضافه کن|ایجاد کن|یک مشتری|مشتری جدید|یک فعالیت|فعالیت جدید|یک لید|لید جدید/.test(
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
function refreshDraft() {
|
||||
@@ -140,11 +144,13 @@ async function parseCommand(text: string, fallbackToAsk: boolean) {
|
||||
try {
|
||||
const { data } = await api.post<VoiceCommandDraft>('/ai/voice-command', { text });
|
||||
if (data.intent === 'none') {
|
||||
if (fallbackToAsk) {
|
||||
if (fallbackToAsk && !looksLikeAction(text)) {
|
||||
emit('ask', text);
|
||||
return;
|
||||
}
|
||||
voiceError.value = data.summary;
|
||||
voiceError.value = looksLikeAction(text)
|
||||
? 'دستور را کامل نفهمیدم. نام و مشخصات را واضحتر بگویید تا قبل از ساخت، تأیید کنید.'
|
||||
: data.summary;
|
||||
return;
|
||||
}
|
||||
draft.value = data;
|
||||
@@ -177,6 +183,9 @@ async function onTypedSubmit() {
|
||||
await parseCommand(text, true);
|
||||
return;
|
||||
}
|
||||
voiceError.value = '';
|
||||
voiceDone.value = '';
|
||||
draft.value = null;
|
||||
emit('ask');
|
||||
}
|
||||
|
||||
@@ -241,6 +250,16 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch([voiceBusy, confirming], () => {
|
||||
emit('busy', voiceBusy.value || confirming.value);
|
||||
});
|
||||
|
||||
watch([voiceError, voiceDone], ([error, done]) => {
|
||||
emit('notice', { error, done });
|
||||
});
|
||||
|
||||
defineExpose({ submitFromInput: onTypedSubmit });
|
||||
|
||||
watch(
|
||||
() => props.answer,
|
||||
(value) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ const props = defineProps<{
|
||||
asking?: boolean;
|
||||
askAnswer?: string;
|
||||
askError?: string;
|
||||
statusText?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -113,7 +114,7 @@ const shortcutLabel = computed(() =>
|
||||
<UiIcon name="send" :size="15" />
|
||||
</button>
|
||||
</form>
|
||||
<p v-if="asking" class="hero-ask-status">در حال پاسخ…</p>
|
||||
<p v-if="asking" class="hero-ask-status">{{ statusText || 'در حال پاسخ…' }}</p>
|
||||
<p v-else-if="askError" class="hero-ask-status err">{{ askError }}</p>
|
||||
<p v-else-if="askAnswer" class="hero-ask-status">{{ askAnswer }}</p>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ const showAmount = computed(
|
||||
() => !props.copy.hideAmount && props.deal.amount != null,
|
||||
);
|
||||
const money = computed(() =>
|
||||
formatCurrencyParts(props.deal.amount ?? 0, props.deal.currencyUnit ?? 'تومان'),
|
||||
formatCurrencyParts(props.deal.amount ?? 0, props.deal.currencyUnit ?? 'ریال'),
|
||||
);
|
||||
const colors = computed(() => PIPELINE_ACCENT[props.accent]);
|
||||
const assigneeTitle = computed(() =>
|
||||
|
||||
@@ -37,7 +37,7 @@ const rows = computed(() =>
|
||||
);
|
||||
|
||||
function money(deal: PipelineDeal) {
|
||||
return formatCurrencyParts(deal.amount ?? 0, deal.currencyUnit ?? 'تومان');
|
||||
return formatCurrencyParts(deal.amount ?? 0, deal.currencyUnit ?? 'ریال');
|
||||
}
|
||||
|
||||
const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
||||
|
||||
@@ -17,8 +17,13 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
entity: RoutineEntityRefDto;
|
||||
title?: string;
|
||||
emptyTitle?: string;
|
||||
emptyHint?: string;
|
||||
}>(),
|
||||
{ title: 'کارهای مرتبط' },
|
||||
{
|
||||
title: 'کارهای مرتبط',
|
||||
emptyTitle: 'هنوز کاری برای این پرونده ثبت نشده.',
|
||||
},
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
@@ -83,7 +88,7 @@ function onCreated(t: RoutineTaskDto) {
|
||||
<section v-if="enabled" class="related">
|
||||
<header>
|
||||
<h3>
|
||||
<UiIcon name="check" :size="16" />
|
||||
<span class="mark"><UiIcon name="check" :size="15" /></span>
|
||||
{{ title }}
|
||||
<em v-if="open.length">{{ toFaDigits(open.length) }} باز</em>
|
||||
</h3>
|
||||
@@ -96,7 +101,21 @@ function onCreated(t: RoutineTaskDto) {
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="actions.error.value" class="error">{{ actions.error.value }}</p>
|
||||
<div v-if="loading && !items.length" class="muted">در حال بارگذاری…</div>
|
||||
<p v-else-if="!items.length" class="muted">هنوز کاری برای این پرونده ثبت نشده.</p>
|
||||
<div v-else-if="!items.length" class="empty">
|
||||
<div class="empty-art" aria-hidden="true">
|
||||
<svg viewBox="0 0 72 72" width="72" height="72">
|
||||
<rect x="18" y="16" width="36" height="44" rx="8" fill="#f4fbf8" stroke="#b7e4d4" stroke-width="1.6" />
|
||||
<rect x="26" y="10" width="20" height="12" rx="4" fill="#e7f6f1" stroke="#7dcebb" stroke-width="1.6" />
|
||||
<path d="M30 40l5 5 9-11" fill="none" stroke="#12856a" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="muted">{{ emptyTitle }}</p>
|
||||
<p v-if="emptyHint" class="hint">{{ emptyHint }}</p>
|
||||
<button class="empty-cta" type="button" @click="quickOpen = true">
|
||||
ایجاد Task
|
||||
<UiIcon name="plus" :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="rows">
|
||||
<RoutineTaskRow
|
||||
@@ -197,4 +216,92 @@ h3 em {
|
||||
.error {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.empty-art,
|
||||
.hint,
|
||||
.empty-cta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.fancy {
|
||||
gap: 0.85rem;
|
||||
padding: 0.95rem 0.85rem 1rem;
|
||||
border: 1px solid #e7eef2;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.fancy .mark {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.fancy header :deep(.ui-btn),
|
||||
.fancy header :deep(button) {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
box-shadow: none;
|
||||
height: auto;
|
||||
min-height: 34px;
|
||||
padding: 0.3rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
flex-direction: row-reverse;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.fancy .empty {
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.4rem 0.5rem 0.2rem;
|
||||
}
|
||||
|
||||
.fancy .empty-art {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
margin-bottom: 0.2rem;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, #f7fcfa, #eef8f4);
|
||||
}
|
||||
|
||||
.fancy .hint {
|
||||
display: block;
|
||||
margin: 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.fancy .empty-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.35rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
padding: 0.45rem 0.9rem;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@ export const CURRENCY_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
export function currencySuffix(code: string): string {
|
||||
if (code === 'IRT') return 'ریال';
|
||||
return CURRENCY_OPTIONS.find((c) => c.code === code)?.suffix ?? code;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { currencySuffix } from '@/lib/money';
|
||||
import { matchesSearch } from '@/lib/search';
|
||||
|
||||
export type PipelineAccent = 'green' | 'orange' | 'purple' | 'blue' | 'rose' | 'slate';
|
||||
@@ -118,8 +119,8 @@ export const PIPELINE_ACCENT: Record<
|
||||
|
||||
const ACCENT_CYCLE: PipelineAccent[] = ['green', 'orange', 'purple'];
|
||||
|
||||
/** خروجی: ۲۵۰,۰۰۰,۰۰۰ تومان */
|
||||
export function formatCurrency(amount: number, unit = 'تومان'): string {
|
||||
/** خروجی: ۲۵۰,۰۰۰,۰۰۰ ریال */
|
||||
export function formatCurrency(amount: number, unit = 'ریال'): string {
|
||||
try {
|
||||
return `${new Intl.NumberFormat('fa-IR').format(amount)} ${unit}`;
|
||||
} catch {
|
||||
@@ -129,7 +130,7 @@ export function formatCurrency(amount: number, unit = 'تومان'): string {
|
||||
|
||||
export function formatCurrencyParts(
|
||||
amount: number,
|
||||
unit = 'تومان',
|
||||
unit = 'ریال',
|
||||
): { number: string; unit: string } {
|
||||
try {
|
||||
return {
|
||||
@@ -155,8 +156,7 @@ export function initialsFromTitle(title: string): string {
|
||||
}
|
||||
|
||||
export function currencyUnitLabel(currency: string): string {
|
||||
if (currency === 'IRR' || currency === 'IRT') return 'تومان';
|
||||
return currency;
|
||||
return currencySuffix(currency || 'IRR');
|
||||
}
|
||||
|
||||
/** نگاشت برد API به مدل UI مسیر فروش */
|
||||
|
||||
@@ -30,6 +30,8 @@ const activities = ref<ActivityDto[]>([]);
|
||||
const board = ref<PipelineBoardDto | null>(null);
|
||||
const askQuery = ref('');
|
||||
const asking = ref(false);
|
||||
const assistantBusy = ref(false);
|
||||
const assistantRef = ref<{ submitFromInput: () => Promise<void> } | null>(null);
|
||||
const askAnswer = ref('');
|
||||
const askError = ref('');
|
||||
const askSources = ref<AskMentoSourceDto[]>([]);
|
||||
@@ -83,6 +85,28 @@ function clearAsk() {
|
||||
askSources.value = [];
|
||||
}
|
||||
|
||||
function onHeroAsk() {
|
||||
if (!assistantRef.value) {
|
||||
askError.value = 'داشبورد هنوز آماده نیست. چند لحظه بعد دوباره بفرستید.';
|
||||
return;
|
||||
}
|
||||
void assistantRef.value.submitFromInput();
|
||||
}
|
||||
|
||||
function onAssistantNotice(payload: { error: string; done: string }) {
|
||||
if (payload.done) {
|
||||
askAnswer.value = payload.done;
|
||||
askError.value = '';
|
||||
return;
|
||||
}
|
||||
if (payload.error) {
|
||||
askError.value = payload.error;
|
||||
askAnswer.value = '';
|
||||
return;
|
||||
}
|
||||
if (!asking.value) askError.value = '';
|
||||
}
|
||||
|
||||
async function submitAsk(preset?: string) {
|
||||
const q = (preset || askQuery.value).trim();
|
||||
if (!q || asking.value) return;
|
||||
@@ -154,11 +178,12 @@ onUnmounted(() => {
|
||||
:first-name="firstName"
|
||||
:user-initial="userInitial"
|
||||
:alerts="home?.alerts ?? []"
|
||||
:asking="asking"
|
||||
:asking="asking || assistantBusy"
|
||||
:ask-answer="askAnswer"
|
||||
:ask-error="askError"
|
||||
:status-text="assistantBusy && !asking ? 'در حال فهمیدن دستور…' : ''"
|
||||
v-model:ask-query="askQuery"
|
||||
@ask="submitAsk"
|
||||
@ask="onHeroAsk"
|
||||
/>
|
||||
|
||||
<p v-if="loading" class="muted center">در حال بارگذاری داشبورد…</p>
|
||||
@@ -181,6 +206,7 @@ onUnmounted(() => {
|
||||
<HomeSourcesChart :sources="home?.sources ?? []" />
|
||||
<HomeAiAssistant
|
||||
id="home-ai-assistant"
|
||||
ref="assistantRef"
|
||||
v-model:ask-query="askQuery"
|
||||
:asking="asking"
|
||||
:answer="askAnswer"
|
||||
@@ -188,6 +214,8 @@ onUnmounted(() => {
|
||||
:sources="askSources"
|
||||
@ask="submitAsk"
|
||||
@clear="clearAsk"
|
||||
@busy="assistantBusy = $event"
|
||||
@notice="onAssistantNotice"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||
import { formatClockFa, formatJalaliNumeric, parseLocalInput, toFaDigits } from '@/lib/jalali';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { canManageSettings } from '@/lib/roles';
|
||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import {
|
||||
UiButton,
|
||||
UiCard,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
UiInput,
|
||||
UiPageHeader,
|
||||
UiSelect,
|
||||
UiTextarea,
|
||||
} from '@/components/ui';
|
||||
import RelatedTasksPanel from '@/components/routines/RelatedTasksPanel.vue';
|
||||
import type {
|
||||
@@ -53,6 +56,10 @@ const noteBody = ref('');
|
||||
const followUpAt = ref('');
|
||||
const followUpBody = ref('');
|
||||
const editing = ref(false);
|
||||
const moreOpen = ref(false);
|
||||
const copied = ref(false);
|
||||
const narrow = ref(typeof window !== 'undefined' && window.matchMedia('(max-width: 900px)').matches);
|
||||
let narrowMq: MediaQueryList | null = null;
|
||||
const editForm = ref({
|
||||
title: '',
|
||||
contactName: '',
|
||||
@@ -192,6 +199,12 @@ async function assignLead() {
|
||||
}
|
||||
}
|
||||
|
||||
function localToIso(value: string): string | undefined {
|
||||
const parsed = parseLocalInput(value);
|
||||
if (!parsed || Number.isNaN(parsed.getTime())) return undefined;
|
||||
return parsed.toISOString();
|
||||
}
|
||||
|
||||
async function logCall() {
|
||||
busy.value = true;
|
||||
error.value = '';
|
||||
@@ -199,7 +212,7 @@ async function logCall() {
|
||||
await api.post(`/leads/${id.value}/call`, {
|
||||
outcome: callOutcome.value || undefined,
|
||||
body: callBody.value || undefined,
|
||||
nextFollowUpAt: callFollowUp.value || undefined,
|
||||
nextFollowUpAt: callFollowUp.value ? localToIso(callFollowUp.value) : undefined,
|
||||
});
|
||||
callOutcome.value = '';
|
||||
callBody.value = '';
|
||||
@@ -230,12 +243,13 @@ async function addNote() {
|
||||
}
|
||||
|
||||
async function setFollowUp() {
|
||||
if (!followUpAt.value) return;
|
||||
const nextFollowUpAt = localToIso(followUpAt.value);
|
||||
if (!nextFollowUpAt) return;
|
||||
busy.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await api.post(`/leads/${id.value}/follow-up`, {
|
||||
nextFollowUpAt: new Date(followUpAt.value).toISOString(),
|
||||
nextFollowUpAt,
|
||||
body: followUpBody.value || undefined,
|
||||
});
|
||||
followUpAt.value = '';
|
||||
@@ -286,11 +300,66 @@ async function archiveLead() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
const telHref = computed(() => (lead.value?.mobile ? `tel:${lead.value.mobile}` : undefined));
|
||||
|
||||
const feed = computed(() => {
|
||||
const rows: Array<{ key: string; title: string; by: string; at: string }> = [];
|
||||
for (const ev of lead.value?.events ?? []) {
|
||||
rows.push({
|
||||
key: `ev-${ev.id}`,
|
||||
title: ev.title,
|
||||
by: ev.body?.trim() || 'توسط سیستم',
|
||||
at: ev.createdAt,
|
||||
});
|
||||
}
|
||||
for (const a of activities.value) {
|
||||
const who = a.createdBy?.fullName || a.assignee?.fullName;
|
||||
rows.push({
|
||||
key: `act-${a.id}`,
|
||||
title: a.title,
|
||||
by: who ? `توسط ${who}` : 'توسط سیستم',
|
||||
at: a.createdAt,
|
||||
});
|
||||
}
|
||||
return rows.sort((a, b) => +new Date(b.at) - +new Date(a.at)).slice(0, 4);
|
||||
});
|
||||
|
||||
function feedParts(iso: string) {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return { day: '—', time: '' };
|
||||
return { day: formatJalaliNumeric(d), time: formatClockFa(d) };
|
||||
}
|
||||
|
||||
async function copyMobile() {
|
||||
const value = lead.value?.mobile;
|
||||
if (!value) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
copied.value = true;
|
||||
window.setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 1400);
|
||||
} catch {
|
||||
copied.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function syncNarrow() {
|
||||
narrow.value = window.matchMedia('(max-width: 900px)').matches;
|
||||
if (!narrow.value) moreOpen.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
narrowMq = window.matchMedia('(max-width: 900px)');
|
||||
narrowMq.addEventListener('change', syncNarrow);
|
||||
void load();
|
||||
});
|
||||
onBeforeUnmount(() => narrowMq?.removeEventListener('change', syncNarrow));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<section class="page" :class="{ editing, 'more-open': moreOpen }">
|
||||
<div class="desk-only">
|
||||
<UiPageHeader
|
||||
:title="lead?.title || 'جزئیات لید'"
|
||||
:lead="lead ? `موبایل ${lead.mobile}` : 'در حال بارگذاری…'"
|
||||
@@ -326,15 +395,196 @@ onMounted(load);
|
||||
</UiButton>
|
||||
</template>
|
||||
</UiPageHeader>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="moreOpen"
|
||||
class="sheet-back"
|
||||
type="button"
|
||||
aria-label="بستن"
|
||||
@click="moreOpen = false"
|
||||
/>
|
||||
|
||||
<p v-if="okMsg" class="ok">{{ okMsg }}</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
||||
|
||||
<template v-else-if="lead">
|
||||
<div v-if="narrow" class="mob">
|
||||
<article class="m-hero">
|
||||
<div class="m-top">
|
||||
<span class="m-mark" aria-hidden="true">
|
||||
<UiIcon name="bag" :size="22" />
|
||||
</span>
|
||||
<div class="m-id">
|
||||
<div class="m-name-row">
|
||||
<h2>{{ lead.title }}</h2>
|
||||
<span class="m-pill"><i />{{ lead.stageName }}</span>
|
||||
</div>
|
||||
<button class="m-phone" type="button" @click="copyMobile">
|
||||
<span dir="ltr">{{ toFaDigits(lead.mobile) }}</span>
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<rect x="8" y="8" width="11" height="11" rx="2" />
|
||||
<path d="M5 15V6a2 2 0 0 1 2-2h9" />
|
||||
</svg>
|
||||
<em v-if="copied">کپی شد</em>
|
||||
</button>
|
||||
</div>
|
||||
<RouterLink class="m-back" :to="{ name: 'leads' }" aria-label="بازگشت">
|
||||
<UiIcon name="chevron-left" :size="20" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="m-actions" :class="{ converted: !!lead.convertedPersonId }">
|
||||
<a class="m-btn mint" :href="telHref">
|
||||
<UiIcon name="phone" :size="16" />
|
||||
تماس
|
||||
</a>
|
||||
<button class="m-btn" type="button" @click="startEdit">
|
||||
<UiIcon name="pencil" :size="16" />
|
||||
ویرایش
|
||||
</button>
|
||||
<button
|
||||
v-if="!lead.convertedPersonId"
|
||||
class="m-btn solid"
|
||||
type="button"
|
||||
:disabled="busy"
|
||||
@click="convertLead"
|
||||
>
|
||||
<UiIcon name="user-plus" :size="16" />
|
||||
تبدیل به مشتری
|
||||
</button>
|
||||
<button class="m-btn icon" type="button" aria-label="بیشتر" @click="moreOpen = true">
|
||||
<UiIcon name="dots" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article v-if="!editing" class="m-card">
|
||||
<header class="m-card-h">
|
||||
<h3>اطلاعات لید</h3>
|
||||
<button class="m-ico" type="button" aria-label="ویرایش اطلاعات" @click="startEdit">
|
||||
<UiIcon name="document" :size="16" />
|
||||
</button>
|
||||
</header>
|
||||
<dl class="m-grid">
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="user" :size="14" /> نام</dt>
|
||||
<dd>{{ lead.title }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="users" :size="14" /> مالک / مخاطب</dt>
|
||||
<dd>{{ lead.contactName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell hot">
|
||||
<dt><UiIcon name="phone" :size="14" /> موبایل</dt>
|
||||
<dd dir="ltr">{{ toFaDigits(lead.mobile) }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="phone" :size="14" /> تلفن</dt>
|
||||
<dd dir="ltr">{{ lead.phone ? toFaDigits(lead.phone) : '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="pin" :size="14" /> شهر</dt>
|
||||
<dd>{{ lead.city || '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="grid" :size="14" /> منطقه</dt>
|
||||
<dd>{{ lead.region || '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="bag" :size="14" /> صنف</dt>
|
||||
<dd>{{ lead.trade || '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="camera" :size="14" /> اینستاگرام</dt>
|
||||
<dd dir="ltr">{{ lead.instagram || '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="target" :size="14" /> منبع</dt>
|
||||
<dd>{{ lead.source || '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="flag" :size="14" /> وضعیت</dt>
|
||||
<dd><span class="m-pill"><i />{{ lead.stageName }}</span></dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="user" :size="14" /> کارشناس</dt>
|
||||
<dd>{{ lead.assigneeName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="calendar" :size="14" /> تاریخ ثبت</dt>
|
||||
<dd>{{ formatDate(lead.createdAt) }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="chart" :size="14" /> روز در مرحله</dt>
|
||||
<dd>{{ toFaDigits(lead.daysInStage) }}</dd>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<dt><UiIcon name="clock" :size="14" /> پیگیری بعدی</dt>
|
||||
<dd>{{ formatDate(lead.nextFollowUpAt) }}</dd>
|
||||
</div>
|
||||
<div v-if="lead.addresses?.length" class="cell wide">
|
||||
<dt><UiIcon name="pin" :size="14" /> آدرسها</dt>
|
||||
<dd>
|
||||
<p v-for="(addr, i) in lead.addresses" :key="i" class="addr-line">{{ addr }}</p>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article v-if="!editing" class="m-card">
|
||||
<header class="m-card-h">
|
||||
<h3>یادداشت</h3>
|
||||
<button class="m-ico" type="button" aria-label="ویرایش یادداشت" @click="startEdit">
|
||||
<UiIcon name="pencil" :size="16" />
|
||||
</button>
|
||||
</header>
|
||||
<p class="m-note">{{ lead.notes?.trim() || 'یادداشتی ثبت نشده است.' }}</p>
|
||||
</article>
|
||||
|
||||
<RelatedTasksPanel
|
||||
v-if="!editing"
|
||||
class="fancy"
|
||||
:entity="{ type: 'lead', id: lead.id, label: lead.title }"
|
||||
empty-title="هنوز کاری برای این لید ثبت نشده."
|
||||
empty-hint="برای پیگیری بهتر، یک تسک ایجاد کنید."
|
||||
/>
|
||||
|
||||
<article v-if="!editing" class="m-card">
|
||||
<header class="m-card-h">
|
||||
<h3>
|
||||
<span class="m-ico static"><UiIcon name="clock" :size="16" /></span>
|
||||
فعالیتها
|
||||
</h3>
|
||||
<RouterLink class="m-add" :to="{ name: 'activity-new' }">
|
||||
ایجاد فعالیت
|
||||
<UiIcon name="plus" :size="14" />
|
||||
</RouterLink>
|
||||
</header>
|
||||
<ol v-if="feed.length" class="m-feed">
|
||||
<li v-for="row in feed" :key="row.key">
|
||||
<span class="m-feed-ic"><UiIcon name="document" :size="16" /></span>
|
||||
<div class="m-feed-body">
|
||||
<strong>{{ row.title }}</strong>
|
||||
<small>{{ row.by }}</small>
|
||||
</div>
|
||||
<time>
|
||||
<b>{{ feedParts(row.at).day }}</b>
|
||||
<span>{{ feedParts(row.at).time }}</span>
|
||||
</time>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="m-note">هنوز فعالیتی ثبت نشده است.</p>
|
||||
<RouterLink class="m-all" :to="{ name: 'activities' }">
|
||||
مشاهده همه فعالیتها
|
||||
<UiIcon name="arrow-left" :size="14" />
|
||||
</RouterLink>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="main">
|
||||
<UiCard title="اطلاعات لید">
|
||||
<UiCard class="info-card" title="اطلاعات لید">
|
||||
<div v-if="editing" class="edit-grid">
|
||||
<UiField label="عنوان" required>
|
||||
<UiInput v-model="editForm.title" />
|
||||
@@ -402,9 +652,12 @@ onMounted(load);
|
||||
</dl>
|
||||
</UiCard>
|
||||
|
||||
<RelatedTasksPanel :entity="{ type: 'lead', id: lead.id, label: lead.title }" />
|
||||
<RelatedTasksPanel
|
||||
v-if="!narrow"
|
||||
:entity="{ type: 'lead', id: lead.id, label: lead.title }"
|
||||
/>
|
||||
|
||||
<UiCard v-if="activities.length" title="فعالیتهای مرتبط">
|
||||
<UiCard v-if="activities.length" class="desk-only" title="فعالیتهای مرتبط">
|
||||
<ul class="lead-acts">
|
||||
<li v-for="a in activities" :key="a.id">
|
||||
<RouterLink :to="{ name: 'activity-detail', params: { id: a.id } }">
|
||||
@@ -420,7 +673,7 @@ onMounted(load);
|
||||
</ul>
|
||||
</UiCard>
|
||||
|
||||
<UiCard title="جدول زمانی">
|
||||
<UiCard class="desk-only" title="جدول زمانی">
|
||||
<ul v-if="lead.events.length" class="timeline">
|
||||
<li v-for="ev in lead.events" :key="ev.id">
|
||||
<strong>{{ ev.title }}</strong>
|
||||
@@ -437,6 +690,7 @@ onMounted(load);
|
||||
</div>
|
||||
|
||||
<aside class="side">
|
||||
<button class="sheet-close" type="button" @click="moreOpen = false">بستن</button>
|
||||
<UiCard title="تغییر وضعیت">
|
||||
<UiField label="وضعیت">
|
||||
<UiSelect v-model="stageId">
|
||||
@@ -467,10 +721,10 @@ onMounted(load);
|
||||
<UiInput v-model="callOutcome" placeholder="مثلاً پاسخ داد / اشغال" />
|
||||
</UiField>
|
||||
<UiField label="توضیح">
|
||||
<UiInput v-model="callBody" placeholder="خلاصه مکالمه" />
|
||||
<UiTextarea v-model="callBody" :rows="4" placeholder="خلاصه مکالمه" />
|
||||
</UiField>
|
||||
<UiField label="پیگیری بعدی">
|
||||
<input v-model="callFollowUp" class="native" type="datetime-local" />
|
||||
<JalaliDateInput v-model="callFollowUp" mode="datetime" />
|
||||
</UiField>
|
||||
<UiButton size="sm" :disabled="busy" @click="logCall">ثبت تماس</UiButton>
|
||||
</UiCard>
|
||||
@@ -486,7 +740,7 @@ onMounted(load);
|
||||
|
||||
<UiCard title="پیگیری بعدی">
|
||||
<UiField label="زمان">
|
||||
<input v-model="followUpAt" class="native" type="datetime-local" />
|
||||
<JalaliDateInput v-model="followUpAt" mode="datetime" />
|
||||
</UiField>
|
||||
<UiField label="توضیح">
|
||||
<UiInput v-model="followUpBody" placeholder="اختیاری" />
|
||||
@@ -653,13 +907,438 @@ onMounted(load);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sheet-back,
|
||||
.sheet-close,
|
||||
.mob {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.desk-only,
|
||||
:deep(.desk-only) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: none;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.meta {
|
||||
.editing .layout,
|
||||
.more-open .layout {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.more-open:not(.editing) .main {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.info-card) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editing :deep(.info-card) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.meta,
|
||||
.edit-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mob {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m-hero,
|
||||
.m-card,
|
||||
.m-top,
|
||||
.m-id,
|
||||
.m-actions,
|
||||
.m-grid,
|
||||
.m-name-row,
|
||||
.mob :deep(.related) {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.m-hero,
|
||||
.m-card,
|
||||
.mob :deep(.related) {
|
||||
background: #fff;
|
||||
border: 1px solid #e7eef2;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.m-hero {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
padding: 0.95rem 0.9rem 0.85rem;
|
||||
}
|
||||
|
||||
.m-top {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 0.7rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.m-mark {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.m-id {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
padding-top: 0.1rem;
|
||||
}
|
||||
|
||||
.m-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.m-name-row h2 {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.m-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.12rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.m-pill i {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #14b8a6;
|
||||
}
|
||||
|
||||
.m-phone {
|
||||
justify-self: start;
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
border: 0;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #64748b;
|
||||
font: inherit;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m-phone em {
|
||||
font-style: normal;
|
||||
color: #0f766e;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.m-back {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.m-actions {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto minmax(0, 1fr) 36px;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.m-actions.converted {
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
}
|
||||
|
||||
.m-btn {
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: #f3f5f7;
|
||||
color: #334155;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0 0.45rem;
|
||||
font: inherit;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m-btn.mint {
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.m-btn.solid {
|
||||
background: #12856a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.m-btn.solid:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.m-btn.icon {
|
||||
width: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.m-card {
|
||||
padding: 0.9rem 0.85rem 0.85rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.m-card-h {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m-card-h h3 {
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.m-ico {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m-ico.static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.m-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.45rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cell {
|
||||
min-width: 0;
|
||||
border: 1px solid #eef2f5;
|
||||
border-radius: 12px;
|
||||
padding: 0.55rem 0.6rem 0.6rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cell.wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.cell.hot {
|
||||
background: #e8f7f1;
|
||||
border-color: #d7f0e6;
|
||||
}
|
||||
|
||||
.cell.hot dt,
|
||||
.cell.hot :deep(.ico) {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.cell dt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
margin: 0 0 0.28rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cell dd {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.cell dd[dir='ltr'] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.m-note {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
color: #334155;
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.7;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.m-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
border-radius: 999px;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
text-decoration: none;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.35rem 0.7rem;
|
||||
}
|
||||
|
||||
.m-feed {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.m-feed li {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.m-feed-ic {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
background: #e7f6f1;
|
||||
color: #0f766e;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.m-feed-body {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.m-feed-body strong {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.m-feed-body small {
|
||||
color: #94a3b8;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.m-feed time {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 0.1rem;
|
||||
color: #64748b;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.m-feed time b {
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.m-all {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.side {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sheet-back {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 60;
|
||||
border: 0;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.more-open .side {
|
||||
display: grid;
|
||||
position: fixed;
|
||||
z-index: 70;
|
||||
inset-inline: 0;
|
||||
bottom: 0;
|
||||
max-height: min(78vh, 640px);
|
||||
overflow: auto;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
padding: 0.85rem 0.85rem calc(0.9rem + env(safe-area-inset-bottom));
|
||||
background: #f8fafc;
|
||||
border-radius: 18px 18px 0 0;
|
||||
box-shadow: 0 -12px 40px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
|
||||
.sheet-close {
|
||||
display: inline-flex;
|
||||
justify-self: start;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem 0.8rem;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -814,15 +814,18 @@ async function onContactVcfFile(ev: Event) {
|
||||
|
||||
async function submitCreate() {
|
||||
createError.value = '';
|
||||
if (!createForm.value.title.trim() || !createForm.value.mobile.trim()) {
|
||||
createError.value = 'نام/شرکت و موبایل الزامی است.';
|
||||
const mobile = createForm.value.mobile.trim();
|
||||
const company = createForm.value.title.trim();
|
||||
const person = createForm.value.contactName.trim();
|
||||
if (!mobile) {
|
||||
createError.value = 'موبایل الزامی است.';
|
||||
return;
|
||||
}
|
||||
createBusy.value = true;
|
||||
const payload = {
|
||||
title: createForm.value.title.trim(),
|
||||
mobile: createForm.value.mobile.trim(),
|
||||
contactName: createForm.value.contactName.trim() || null,
|
||||
title: company || person || mobile,
|
||||
mobile,
|
||||
contactName: person || null,
|
||||
city: createForm.value.city.trim() || null,
|
||||
region: createForm.value.region.trim() || null,
|
||||
source: createForm.value.source.trim() || null,
|
||||
@@ -1633,7 +1636,7 @@ onBeforeUnmount(() => {
|
||||
<form class="create-form" @submit.prevent="submitCreate">
|
||||
<label>
|
||||
نام / شرکت
|
||||
<input v-model="createForm.title" required placeholder="مثلاً فروشگاه آریا" />
|
||||
<input v-model="createForm.title" placeholder="مثلاً فروشگاه آریا" />
|
||||
</label>
|
||||
<label>
|
||||
نام مخاطب
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import { currencySuffix } from '@/lib/money';
|
||||
import MoneyInput from '@/components/MoneyInput.vue';
|
||||
import {
|
||||
UiBadge,
|
||||
@@ -280,7 +281,7 @@ onMounted(async () => {
|
||||
<div class="price-input-wrapper">
|
||||
<MoneyInput v-model="unitPrice" :decimals="0" placeholder="مبلغ واحد" />
|
||||
<span class="currency-unit-label">
|
||||
{{ currency === 'IRR' || currency === 'IRT' ? 'تومان' : currency }}
|
||||
{{ currencySuffix(currency) }}
|
||||
</span>
|
||||
</div>
|
||||
</UiField>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatClockFa, formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||
import { formatMoney } from '@/lib/money';
|
||||
import { currencySuffix, formatMoney } from '@/lib/money';
|
||||
import {
|
||||
UiBadge,
|
||||
UiBusyOverlay,
|
||||
@@ -141,8 +141,7 @@ function formatTime(iso: string) {
|
||||
}
|
||||
|
||||
function currencyLabel(code: string) {
|
||||
if (code === 'IRR' || code === 'IRT') return 'تومان';
|
||||
return code;
|
||||
return currencySuffix(code);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
|
||||
Reference in New Issue
Block a user