Refining the speech-to-text conversion structure

This commit is contained in:
2026-09-22 11:02:08 +03:30
parent 17fb33b3e7
commit 4b335aa7ff
16 changed files with 634 additions and 333 deletions
+38 -49
View File
@@ -12,6 +12,7 @@ import {
} from '@/lib/jalali';
import {
UiButton,
UiDropdownMenu,
UiModal,
UiPageHeader,
UiPagination,
@@ -129,11 +130,6 @@ const filterLabel = computed(() => TABS.find((t) => t.key === filter.value)?.lab
const trulyEmpty = computed(() => !loading.value && counts.value.all === 0 && !q.value.trim());
const listEmpty = computed(() => !loading.value && !items.value.length);
function isLastItem(id: string) {
const rows = items.value;
return rows.length > 0 && rows[rows.length - 1]?.id === id;
}
function isOverdue(item: ActivityDto) {
if (item.status !== 'open' || !item.dueAt) return false;
return startOfLocalDay(new Date(item.dueAt)).getTime() < startOfLocalDay().getTime();
@@ -480,7 +476,6 @@ onMounted(() => {
v-for="item in items"
:key="item.id"
class="trow"
:class="{ 'menu-open': menuId === item.id }"
role="row"
>
<div class="title-cell">
@@ -516,29 +511,31 @@ onMounted(() => {
</span>
</div>
<div class="row-more" @click.stop>
<button
class="icon-more"
type="button"
aria-label="عملیات"
@click="menuId = menuId === item.id ? null : item.id"
<UiDropdownMenu
:open="menuId === item.id"
align="end"
@update:open="(v) => (menuId = v ? item.id : null)"
>
<UiIcon name="dots-v" :size="16" />
</button>
<div
v-if="menuId === item.id"
class="popover"
:class="{ 'drop-up': isLastItem(item.id) }"
>
<RouterLink :to="{ name: 'activity-detail', params: { id: item.id } }" @click="menuId = null">
<template #trigger="{ toggle }">
<button
class="icon-more"
type="button"
aria-label="عملیات"
@click.stop="toggle"
>
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<RouterLink :to="{ name: 'activity-detail', params: { id: item.id } }">
مشاهده
</RouterLink>
<RouterLink :to="{ name: 'activity-edit', params: { id: item.id } }" @click="menuId = null">
<RouterLink :to="{ name: 'activity-edit', params: { id: item.id } }">
ویرایش
</RouterLink>
<button v-if="item.status === 'open'" type="button" @click="complete(item.id)">
انجام شد
</button>
</div>
</UiDropdownMenu>
</div>
</article>
</div>
@@ -565,31 +562,32 @@ onMounted(() => {
</p>
</div>
<div class="card-side">
<button
class="icon-more"
type="button"
aria-label="عملیات"
@click.stop="menuId = menuId === item.id ? null : item.id"
<UiDropdownMenu
:open="menuId === item.id"
align="end"
@update:open="(v) => (menuId = v ? item.id : null)"
>
<UiIcon name="dots-v" :size="16" />
</button>
<span class="type-ico" :class="item.type">
<UiIcon :name="typeIcon[item.type]" :size="18" />
</span>
<span class="type-pill" :class="item.type">{{ typeLabel[item.type] }}</span>
<div
v-if="menuId === item.id"
class="popover"
:class="{ 'drop-up': isLastItem(item.id) }"
@click.stop
>
<RouterLink :to="{ name: 'activity-detail', params: { id: item.id } }" @click="menuId = null">
<template #trigger="{ toggle }">
<button
class="icon-more"
type="button"
aria-label="عملیات"
@click.stop="toggle"
>
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<RouterLink :to="{ name: 'activity-detail', params: { id: item.id } }">
مشاهده
</RouterLink>
<button v-if="item.status === 'open'" type="button" @click="complete(item.id)">
انجام شد
</button>
</div>
</UiDropdownMenu>
<span class="type-ico" :class="item.type">
<UiIcon :name="typeIcon[item.type]" :size="18" />
</span>
<span class="type-pill" :class="item.type">{{ typeLabel[item.type] }}</span>
</div>
</div>
<footer class="card-foot">
@@ -849,11 +847,6 @@ h1 {
z-index: 20;
}
.popover.drop-up {
top: auto;
bottom: calc(100% + 6px);
}
.popover a,
.popover button {
display: block;
@@ -968,10 +961,6 @@ h1 {
border-radius: 0 0 var(--radius) var(--radius);
}
.trow.menu-open {
z-index: 12;
}
.title-cell {
display: flex;
gap: 0.55rem;
+44 -35
View File
@@ -9,6 +9,7 @@ import ExcelImportFlow from '@/components/ExcelImportFlow.vue';
import PhoneContactsImport from '@/components/PhoneContactsImport.vue';
import {
UiButton,
UiDropdownMenu,
UiModal,
UiPageHeader,
UiPagination,
@@ -372,9 +373,27 @@ onMounted(load);
<RouterLink class="name" :to="{ name: 'person-detail', params: { id: p.id } }">
{{ p.fullName }}
</RouterLink>
<button class="more" type="button" @click.stop="menuId = menuId === p.id ? null : p.id">
<UiIcon name="dots-v" :size="16" />
</button>
<UiDropdownMenu
:open="menuId === p.id"
align="end"
@update:open="(v) => (menuId = v ? p.id : null)"
>
<template #trigger="{ toggle }">
<button class="more" type="button" @click.stop="toggle">
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink>
<RouterLink :to="{ name: 'person-edit', params: { id: p.id } }">ویرایش</RouterLink>
<button
v-for="mood in PERSON_MOODS"
:key="mood"
type="button"
@click.stop="setMoodFor(p, mood)"
>
{{ moodLabel(mood) }}
</button>
</UiDropdownMenu>
</div>
<p class="role">
{{ [p.position, p.company?.name].filter(Boolean).join(' • ') || 'بدون سمت' }}
@@ -389,17 +408,6 @@ onMounted(load);
</p>
<span class="badge" :class="moodOf(p)">{{ moodLabel(moodOf(p)) }}</span>
</div>
<ul v-if="menuId === p.id" class="menu row-menu">
<li>
<RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink>
</li>
<li>
<RouterLink :to="{ name: 'person-edit', params: { id: p.id } }">ویرایش</RouterLink>
</li>
<li v-for="mood in PERSON_MOODS" :key="mood">
<button type="button" @click.stop="setMoodFor(p, mood)">{{ moodLabel(mood) }}</button>
</li>
</ul>
</article>
</div>
@@ -449,21 +457,28 @@ onMounted(load);
</span>
</td>
<td>{{ formatDate(p.createdAt) }}</td>
<td class="ops">
<button type="button" class="more" @click.stop="menuId = menuId === p.id ? null : p.id">
<UiIcon name="dots-v" :size="16" />
</button>
<ul v-if="menuId === p.id" class="menu row-menu">
<li>
<RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink>
</li>
<li>
<RouterLink :to="{ name: 'person-edit', params: { id: p.id } }">ویرایش</RouterLink>
</li>
<li v-for="mood in PERSON_MOODS" :key="mood">
<button type="button" @click.stop="setMoodFor(p, mood)">{{ moodLabel(mood) }}</button>
</li>
</ul>
<td class="ops" @click.stop>
<UiDropdownMenu
:open="menuId === p.id"
align="end"
@update:open="(v) => (menuId = v ? p.id : null)"
>
<template #trigger="{ toggle }">
<button type="button" class="more" @click.stop="toggle">
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<RouterLink :to="{ name: 'person-detail', params: { id: p.id } }">پرونده</RouterLink>
<RouterLink :to="{ name: 'person-edit', params: { id: p.id } }">ویرایش</RouterLink>
<button
v-for="mood in PERSON_MOODS"
:key="mood"
type="button"
@click.stop="setMoodFor(p, mood)"
>
{{ moodLabel(mood) }}
</button>
</UiDropdownMenu>
</td>
</tr>
</tbody>
@@ -1005,12 +1020,6 @@ strong.newcomer {
flex-shrink: 0;
}
.row-menu {
inset-inline-start: auto;
inset-inline-end: 0;
top: 2.4rem;
}
.empty {
margin: 0;
padding: 1.4rem;
+24 -12
View File
@@ -11,6 +11,7 @@ import {
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import {
UiButton,
UiDropdownMenu,
UiModal,
UiPageHeader,
UiPagination,
@@ -522,12 +523,18 @@ onMounted(load);
<RouterLink class="icon-btn sm" :to="r.href" title="ویرایش">
<UiIcon name="pencil" :size="15" />
</RouterLink>
<button type="button" class="icon-btn sm" @click="menuId = menuId === r.id ? null : r.id">
<UiIcon name="dots-v" :size="15" />
</button>
<div v-if="menuId === r.id" class="menu left">
<UiDropdownMenu
:open="menuId === r.id"
align="end"
@update:open="(v) => (menuId = v ? r.id : null)"
>
<template #trigger="{ toggle }">
<button type="button" class="icon-btn sm" @click.stop="toggle">
<UiIcon name="dots-v" :size="15" />
</button>
</template>
<RouterLink :to="r.href">باز کردن</RouterLink>
</div>
</UiDropdownMenu>
</td>
</tr>
</tbody>
@@ -548,14 +555,20 @@ onMounted(load);
{{ r.statusLabel }}
</span>
<div class="mob-aside">
<button type="button" class="icon-btn sm ghost" @click="menuId = menuId === r.id ? null : r.id">
<UiIcon name="dots-v" :size="16" />
</button>
<UiDropdownMenu
:open="menuId === r.id"
align="end"
@update:open="(v) => (menuId = v ? r.id : null)"
>
<template #trigger="{ toggle }">
<button type="button" class="icon-btn sm ghost" @click.stop="toggle">
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<RouterLink :to="r.href">باز کردن</RouterLink>
</UiDropdownMenu>
<span class="avatar">{{ r.customerName.slice(0, 1) }}</span>
</div>
<div v-if="menuId === r.id" class="menu left mob-menu">
<RouterLink :to="r.href">باز کردن</RouterLink>
</div>
</div>
<div class="mob-card-bottom">
<em :class="{ neg: r.amount < 0 }">{{ money(r.amount, r.currency) }}</em>
@@ -835,7 +848,6 @@ tbody tr:nth-child(even) { background: #fcfdff; }
.mob-info { text-decoration: none; color: inherit; display: grid; gap: 0.12rem; min-width: 0; }
.mob-info .mono, .mob-info .muted { font-size: 0.8rem; color: var(--muted); }
.mob-aside { display: grid; justify-items: center; gap: 0.35rem; }
.mob-menu { top: 2.2rem; }
.mob-card-bottom { display: flex; justify-content: flex-start; }
.mob-card-bottom em { font-style: normal; font-weight: 600; font-size: 0.92rem; }
+26 -24
View File
@@ -6,6 +6,7 @@ import { api } from '@/lib/api';
import { useAuthStore } from '@/stores/auth';
import { canManageSettings } from '@/lib/roles';
import UiIcon from '@/components/UiIcon.vue';
import { UiDropdownMenu } from '@/components/ui';
import { formatRelativeContact, toFaDigits } from '@/lib/jalali';
import type {
CreateLoyaltyRewardRequest,
@@ -785,14 +786,20 @@ onBeforeUnmount(() => {
<button class="act minus" type="button" title="کسر امتیاز" @click="openAdjust(m, -1)">
<UiIcon name="minus" :size="14" />
</button>
<button class="act more" type="button" @click="menuId = menuId === m.id ? null : m.id">
<UiIcon name="dots" :size="14" />
</button>
<div v-if="menuId === m.id" class="menu">
<UiDropdownMenu
:open="menuId === m.id"
align="end"
@update:open="(v) => (menuId = v ? m.id : null)"
>
<template #trigger="{ toggle }">
<button class="act more" type="button" @click.stop="toggle">
<UiIcon name="dots" :size="14" />
</button>
</template>
<RouterLink :to="`/contacts/people/${m.id}`">پروفایل</RouterLink>
<button type="button" @click="openLedger(m)">تاریخچه امتیاز</button>
<button type="button" @click="openRedeem(undefined, m)">ثبت پاداش</button>
</div>
</UiDropdownMenu>
</td>
</tr>
</tbody>
@@ -815,9 +822,20 @@ onBeforeUnmount(() => {
<b class="mob-pts"><span>امتیاز</span><em>{{ faNum(m.loyaltyPoints) }}</em></b>
<div class="mob-side">
<div class="ops">
<button class="act more" type="button" @click.stop="menuId = menuId === m.id ? null : m.id">
<UiIcon name="dots" :size="14" />
</button>
<UiDropdownMenu
:open="menuId === m.id"
align="end"
@update:open="(v) => (menuId = v ? m.id : null)"
>
<template #trigger="{ toggle }">
<button class="act more" type="button" @click.stop="toggle">
<UiIcon name="dots" :size="14" />
</button>
</template>
<RouterLink :to="`/contacts/people/${m.id}`">پروفایل</RouterLink>
<button type="button" @click="openLedger(m)">تاریخچه امتیاز</button>
<button type="button" @click="openRedeem(undefined, m)">ثبت پاداش</button>
</UiDropdownMenu>
<button class="act plus" type="button" @click="openAdjust(m, 1)">
<UiIcon name="plus" :size="14" />
</button>
@@ -825,11 +843,6 @@ onBeforeUnmount(() => {
<UiIcon name="minus" :size="14" />
</button>
</div>
<div v-if="menuId === m.id" class="menu" @click.stop>
<RouterLink :to="`/contacts/people/${m.id}`">پروفایل</RouterLink>
<button type="button" @click="openLedger(m)">تاریخچه امتیاز</button>
<button type="button" @click="openRedeem(undefined, m)">ثبت پاداش</button>
</div>
</div>
</li>
</ul>
@@ -1287,17 +1300,6 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
.act.plus { background: #dcfce7; color: #15803d; }
.act.minus { background: #fee2e2; color: #dc2626; }
.act.more { background: #f1f5f9; color: #64748b; }
.menu {
position: absolute; inset-inline-end: 0; top: 34px; z-index: 5;
background: #fff; border: 1px solid #e2e8f0; border-radius: var(--radius);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
display: grid; min-width: 140px; overflow: hidden;
}
.menu a, .menu button {
background: none; border: 0; text-align: right; padding: 0.55rem 0.75rem;
font: inherit; cursor: pointer; color: var(--ink); text-decoration: none;
}
.menu a:hover, .menu button:hover { background: #f8fafc; }
.mob-list { display: none; list-style: none; margin: 0; padding: 0; gap: 0.65rem; }
.mob-list li {
+16 -35
View File
@@ -3,6 +3,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import UiIcon from '@/components/UiIcon.vue';
import { UiDropdownMenu } from '@/components/ui';
import { formatClockFa, formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
import type {
CreateMessageAutomationRuleRequest,
@@ -414,17 +415,23 @@ onUnmounted(() => {
>
<header>
<div class="m-menu">
<button
type="button"
aria-label="عملیات"
@click="menuId = menuId === row.id ? null : row.id"
<UiDropdownMenu
:open="menuId === row.id"
align="end"
@update:open="(v) => (menuId = v ? row.id : null)"
>
<UiIcon name="dots-v" :size="18" />
</button>
<div v-if="menuId === row.id" class="pop">
<template #trigger="{ toggle }">
<button
type="button"
aria-label="عملیات"
@click.stop="toggle"
>
<UiIcon name="dots-v" :size="18" />
</button>
</template>
<button type="button" @click="openMobileEdit(row)">ویرایش</button>
<button type="button" class="danger" @click="askRemove(row)">حذف</button>
</div>
</UiDropdownMenu>
</div>
<div class="m-copy">
<strong>{{ row.name }}</strong>
@@ -2144,7 +2151,7 @@ textarea {
position: relative;
}
.m-menu > button,
.m-menu :deep(button),
.m-back {
width: 2rem;
height: 2rem;
@@ -2157,32 +2164,6 @@ textarea {
padding: 0;
}
.m-menu .pop {
position: absolute;
inset-inline-start: 0;
top: 2.1rem;
z-index: 4;
min-width: 7.5rem;
display: grid;
padding: 0.35rem;
border-radius: 12px;
background: #fff;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
}
.m-menu .pop button {
border: 0;
background: transparent;
text-align: start;
padding: 0.5rem 0.6rem;
font: inherit;
cursor: pointer;
}
.m-menu .pop .danger {
color: #b91c1c;
}
.m-card footer {
display: flex;
align-items: center;
+74 -65
View File
@@ -8,6 +8,7 @@ import { formatMoney } from '@/lib/money';
import {
UiBadge,
UiButton,
UiDropdownMenu,
UiEmptyState,
UiInput,
UiModal,
@@ -272,22 +273,22 @@ onMounted(load);
<UiIcon name="plus" :size="16" />
محصول جدید
</UiButton>
<div class="more-wrap" @click.stop>
<UiButton
variant="ghost"
size="md"
aria-label="عملیات بیشتر"
@click="moreOpen = !moreOpen"
>
<UiIcon name="dots" :size="18" />
</UiButton>
<div v-if="moreOpen" class="menu">
<button type="button" :disabled="!selected.size" @click="removeSelected">
حذف انتخاب‌شده‌ها
</button>
<button type="button" @click="load">بروزرسانی لیست</button>
</div>
</div>
<UiDropdownMenu v-model:open="moreOpen" align="end">
<template #trigger="{ toggle }">
<UiButton
variant="ghost"
size="md"
aria-label="عملیات بیشتر"
@click.stop="toggle"
>
<UiIcon name="dots" :size="18" />
</UiButton>
</template>
<button type="button" :disabled="!selected.size" @click="removeSelected">
حذف انتخاب‌شده‌ها
</button>
<button type="button" @click="load">بروزرسانی لیست</button>
</UiDropdownMenu>
</template>
</UiPageHeader>
@@ -312,23 +313,23 @@ onMounted(load);
<option value="active">فعال</option>
<option value="inactive">غیرفعال</option>
</UiSelect>
<div class="sort-wrap">
<UiButton variant="outline" @click="sortOpen = !sortOpen">
<UiIcon name="filter" :size="16" />
مرتب‌سازی
</UiButton>
<div v-if="sortOpen" class="menu">
<button type="button" :class="{ on: sort === 'name' }" @click="sort = 'name'; sortOpen = false">
نام
</button>
<button type="button" :class="{ on: sort === 'newest' }" @click="sort = 'newest'; sortOpen = false">
جدیدترین
</button>
<button type="button" :class="{ on: sort === 'price' }" @click="sort = 'price'; sortOpen = false">
قیمت
</button>
</div>
</div>
<UiDropdownMenu v-model:open="sortOpen" align="end">
<template #trigger="{ toggle }">
<UiButton variant="outline" @click.stop="toggle">
<UiIcon name="filter" :size="16" />
مرتب‌سازی
</UiButton>
</template>
<button type="button" :class="{ on: sort === 'name' }" @click="sort = 'name'">
نام
</button>
<button type="button" :class="{ on: sort === 'newest' }" @click="sort = 'newest'">
جدیدترین
</button>
<button type="button" :class="{ on: sort === 'price' }" @click="sort = 'price'">
قیمت
</button>
</UiDropdownMenu>
</div>
<p v-if="error" class="error">{{ error }}</p>
@@ -398,23 +399,27 @@ onMounted(load);
<small>{{ formatTime(p.createdAt) }}</small>
</td>
<td class="ops" @click.stop>
<div class="row-more">
<button
type="button"
class="icon-btn"
aria-label="عملیات"
@click="menuId = menuId === p.id ? null : p.id"
>
<UiIcon name="dots" :size="16" />
</button>
<div v-if="menuId === p.id" class="menu">
<button type="button" @click="goEdit(p.id)">ویرایش</button>
<button type="button" @click="toggleActive(p)">
{{ p.isActive ? 'غیرفعال کردن' : 'فعال کردن' }}
<UiDropdownMenu
:open="menuId === p.id"
align="end"
@update:open="(v) => (menuId = v ? p.id : null)"
>
<template #trigger="{ toggle }">
<button
type="button"
class="icon-btn"
aria-label="عملیات"
@click.stop="toggle"
>
<UiIcon name="dots" :size="16" />
</button>
<button type="button" class="danger" @click="removeProduct(p)">حذف</button>
</div>
</div>
</template>
<button type="button" @click="goEdit(p.id)">ویرایش</button>
<button type="button" @click="toggleActive(p)">
{{ p.isActive ? 'غیرفعال کردن' : 'فعال کردن' }}
</button>
<button type="button" class="danger" @click="removeProduct(p)">حذف</button>
</UiDropdownMenu>
</td>
</tr>
</tbody>
@@ -443,23 +448,27 @@ onMounted(load);
</span>
</div>
<div class="mob-side">
<div class="row-more">
<button
type="button"
class="icon-btn"
aria-label="عملیات"
@click="menuId = menuId === p.id ? null : p.id"
>
<UiIcon name="dots-v" :size="16" />
</button>
<div v-if="menuId === p.id" class="menu left">
<button type="button" @click="goEdit(p.id)">ویرایش</button>
<button type="button" @click="toggleActive(p)">
{{ p.isActive ? 'غیرفعال کردن' : 'فعال کردن' }}
<UiDropdownMenu
:open="menuId === p.id"
align="end"
@update:open="(v) => (menuId = v ? p.id : null)"
>
<template #trigger="{ toggle }">
<button
type="button"
class="icon-btn"
aria-label="عملیات"
@click.stop="toggle"
>
<UiIcon name="dots-v" :size="16" />
</button>
<button type="button" class="danger" @click="removeProduct(p)">حذف</button>
</div>
</div>
</template>
<button type="button" @click="goEdit(p.id)">ویرایش</button>
<button type="button" @click="toggleActive(p)">
{{ p.isActive ? 'غیرفعال کردن' : 'فعال کردن' }}
</button>
<button type="button" class="danger" @click="removeProduct(p)">حذف</button>
</UiDropdownMenu>
<UiBadge :variant="p.isActive ? 'success' : 'neutral'" size="sm" dot>
{{ p.isActive ? 'فعال' : 'غیرفعال' }}
</UiBadge>
+27 -23
View File
@@ -15,6 +15,7 @@ import JalaliDateInput from '@/components/JalaliDateInput.vue';
import MoneyInput from '@/components/MoneyInput.vue';
import UiIcon from '@/components/UiIcon.vue';
import {
UiDropdownMenu,
UiPageHeader,
UiPagination,
UiChart,
@@ -649,13 +650,19 @@ onMounted(() => {
<button type="button" class="icon-btn sm" title="ویرایش" @click="openEdit(g)">
<UiIcon name="pencil" :size="15" />
</button>
<button type="button" class="icon-btn sm" @click="menuId = menuId === g.id ? null : g.id">
<UiIcon name="dots-v" :size="15" />
</button>
<div v-if="menuId === g.id" class="menu">
<UiDropdownMenu
:open="menuId === g.id"
align="end"
@update:open="(v) => (menuId = v ? g.id : null)"
>
<template #trigger="{ toggle }">
<button type="button" class="icon-btn sm" @click.stop="toggle">
<UiIcon name="dots-v" :size="15" />
</button>
</template>
<button type="button" @click="openEdit(g)">ویرایش</button>
<button type="button" class="danger" @click="removeGoal(g.id)">حذف</button>
</div>
</UiDropdownMenu>
</td>
</tr>
</tbody>
@@ -672,13 +679,19 @@ onMounted(() => {
</button>
</div>
<article v-for="g in filteredGoals" :key="g.id" class="goal-card" @click.stop>
<button type="button" class="dots" @click="menuId = menuId === g.id ? null : g.id">
<UiIcon name="dots-v" :size="16" />
</button>
<div v-if="menuId === g.id" class="menu left">
<UiDropdownMenu
:open="menuId === g.id"
align="end"
@update:open="(v) => (menuId = v ? g.id : null)"
>
<template #trigger="{ toggle }">
<button type="button" class="dots" @click.stop="toggle">
<UiIcon name="dots-v" :size="16" />
</button>
</template>
<button type="button" @click="openEdit(g)">ویرایش</button>
<button type="button" class="danger" @click="removeGoal(g.id)">حذف</button>
</div>
</UiDropdownMenu>
<div class="goal-body">
<div>
<strong>{{ g.title }}</strong>
@@ -872,18 +885,6 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; white-space: nowrap;
display: inline-flex; align-items: center; justify-content: center; cursor: pointer; color: #64748b;
}
.icon-btn.sm { width: 30px; height: 30px; }
.menu {
position: absolute; top: calc(100% + 4px); left: 0; z-index: 20; min-width: 120px;
background: #fff; border: 1px solid #e2e8f0; border-radius: var(--radius); box-shadow: 0 10px 24px rgba(15,23,42,.12);
padding: 0.3rem; display: grid;
}
.menu.left { right: 0; left: auto; }
.menu button {
border: 0; background: transparent; text-align: right; padding: 0.5rem 0.65rem; border-radius: var(--radius);
cursor: pointer; font: inherit;
}
.menu button:hover { background: var(--teal-soft); color: var(--teal); }
.menu button.danger { color: #b91c1c; }
.pager { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem; align-items: center; }
.pager-meta { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
@@ -907,8 +908,11 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; white-space: nowrap;
display: grid; place-items: center; cursor: pointer;
}
.goal-card { padding: 0.9rem; display: grid; gap: 0.65rem; position: relative; }
.goal-card :deep(.ui-dd-anchor) {
position: absolute; top: 0.6rem; left: 0.5rem;
}
.goal-card .dots {
position: absolute; top: 0.6rem; left: 0.5rem; border: 0; background: transparent; color: #94a3b8; cursor: pointer;
border: 0; background: transparent; color: #94a3b8; cursor: pointer;
}
.goal-body { display: grid; grid-template-columns: 1fr auto; gap: 0.65rem; align-items: center; }
.goal-body strong { display: block; }
+14 -43
View File
@@ -5,7 +5,7 @@ import axios from 'axios';
import { api } from '@/lib/api';
import { useAuthStore } from '@/stores/auth';
import UiIcon from '@/components/UiIcon.vue';
import UiModal from '@/components/ui/UiModal.vue';
import { UiDropdownMenu, UiModal } from '@/components/ui';
import {
ROLE_PERMISSION_ITEMS,
type InviteUserRequest,
@@ -754,16 +754,20 @@ onUnmounted(() => {
</td>
<td class="muted">{{ formatLastLogin(user.lastLoginAt) }}</td>
<td class="ops" @click.stop>
<div class="kebab">
<button type="button" class="ghost-ico" aria-label="عملیات" @click.stop="menuUserId = menuUserId === user.id ? null : user.id">
<UiIcon name="dots-v" :size="16" />
</button>
<div v-if="menuUserId === user.id" class="menu" @click.stop>
<button type="button" @click="toggleStatus(user)">
{{ user.isActive === false ? 'فعال کردن' : 'غیرفعال کردن' }}
<UiDropdownMenu
:open="menuUserId === user.id"
align="end"
@update:open="(v) => (menuUserId = v ? user.id : null)"
>
<template #trigger="{ toggle }">
<button type="button" class="ghost-ico" aria-label="عملیات" @click.stop="toggle">
<UiIcon name="dots-v" :size="16" />
</button>
</div>
</div>
</template>
<button type="button" @click="toggleStatus(user)">
{{ user.isActive === false ? 'فعال کردن' : 'غیرفعال کردن' }}
</button>
</UiDropdownMenu>
</td>
</tr>
<tr v-if="!loading && !pagedUsers.length">
@@ -1218,39 +1222,6 @@ tbody tr + tr td {
color: #94a3b8;
}
.kebab {
position: relative;
}
.menu {
position: absolute;
inset-inline-start: 0;
top: 100%;
z-index: 5;
min-width: 9rem;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
padding: 0.35rem;
}
.menu button {
width: 100%;
appearance: none;
background: none;
border: 0;
text-align: right;
padding: 0.45rem 0.55rem;
border-radius: 8px;
font: inherit;
cursor: pointer;
}
.menu button:hover {
background: #f8fafc;
}
.ghost-ico {
width: 32px;
height: 32px;