Initial commit: fabric-trade-admin panel with responsive layouts and UI refinements.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import BaseModal from '@/components/ui/BaseModal.vue'
|
||||
import BaseSwitch from '@/components/ui/BaseSwitch.vue'
|
||||
import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
|
||||
import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import { adminUsers as initialUsers } from '@/data/mockData'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { USER_ROLE_LABELS } from '@/utils/adminLabels'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import type { AdminUser, UserRole } from '@/types'
|
||||
|
||||
const userList = ref<AdminUser[]>([...initialUsers])
|
||||
const searchQuery = ref('')
|
||||
const modalMode = ref<'add' | 'edit' | null>(null)
|
||||
const editingId = ref<string | null>(null)
|
||||
const formError = ref('')
|
||||
const saving = ref(false)
|
||||
const toast = useToast()
|
||||
|
||||
const emptyForm = () => ({
|
||||
fullName: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
role: 'sales' as UserRole,
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
const isModalOpen = computed(() => modalMode.value !== null)
|
||||
const modalTitle = computed(() => (modalMode.value === 'edit' ? 'ویرایش کاربر' : 'افزودن کاربر'))
|
||||
const isFormValid = computed(
|
||||
() => form.fullName.trim() && form.phone.trim() && form.email.trim(),
|
||||
)
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
const q = searchQuery.value.trim()
|
||||
if (!q) return userList.value
|
||||
return userList.value.filter(
|
||||
(u) =>
|
||||
u.fullName.includes(q) ||
|
||||
u.phone.includes(q) ||
|
||||
u.email.includes(q) ||
|
||||
USER_ROLE_LABELS[u.role].includes(q),
|
||||
)
|
||||
})
|
||||
|
||||
function openAddModal() {
|
||||
modalMode.value = 'add'
|
||||
editingId.value = null
|
||||
Object.assign(form, emptyForm())
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
function openEditModal(user: AdminUser) {
|
||||
modalMode.value = 'edit'
|
||||
editingId.value = user.id
|
||||
form.fullName = user.fullName
|
||||
form.phone = user.phone
|
||||
form.email = user.email
|
||||
form.role = user.role
|
||||
form.isActive = user.isActive
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalMode.value = null
|
||||
editingId.value = null
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
function submitUser() {
|
||||
if (!isFormValid.value) {
|
||||
formError.value = 'نام، تلفن و ایمیل الزامی هستند.'
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
const payload = {
|
||||
fullName: form.fullName.trim(),
|
||||
phone: form.phone.trim(),
|
||||
email: form.email.trim(),
|
||||
role: form.role,
|
||||
isActive: form.isActive,
|
||||
}
|
||||
|
||||
if (modalMode.value === 'edit' && editingId.value) {
|
||||
const index = userList.value.findIndex((u) => u.id === editingId.value)
|
||||
if (index !== -1) {
|
||||
userList.value[index] = { ...userList.value[index]!, ...payload }
|
||||
}
|
||||
} else {
|
||||
userList.value.push({
|
||||
id: `usr-${crypto.randomUUID().slice(0, 8)}`,
|
||||
...payload,
|
||||
lastLoginAt: null,
|
||||
})
|
||||
}
|
||||
|
||||
toast.success(modalMode.value === 'edit' ? 'اطلاعات کاربر بهروزرسانی شد.' : 'کاربر جدید ثبت شد.')
|
||||
saving.value = false
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchQuery.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="users-page">
|
||||
<div class="admin-page-header">
|
||||
<div>
|
||||
<h1 class="admin-page-header__title">کاربران و دسترسیها</h1>
|
||||
<p class="admin-page-header__breadcrumb">سیستم / کاربران</p>
|
||||
</div>
|
||||
<button type="button" class="admin-btn admin-btn--primary" @click="openAddModal">
|
||||
افزودن کاربر
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="users-page__search admin-card">
|
||||
<AdminIcon name="search" :size="18" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
class="users-page__search-input"
|
||||
placeholder="جستجو در نام، تلفن، ایمیل..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BaseEmptyState
|
||||
v-if="!filteredUsers.length"
|
||||
class="admin-card"
|
||||
icon="users"
|
||||
:title="searchQuery.trim() ? 'نتیجهای یافت نشد' : 'کاربری ثبت نشده'"
|
||||
:description="
|
||||
searchQuery.trim()
|
||||
? 'عبارت جستجو را تغییر دهید یا فیلترها را پاک کنید.'
|
||||
: 'برای شروع، اولین کاربر را ثبت کنید.'
|
||||
"
|
||||
>
|
||||
<button v-if="searchQuery.trim()" type="button" class="admin-btn admin-btn--outline admin-btn--sm" @click="clearSearch">
|
||||
پاک کردن جستجو
|
||||
</button>
|
||||
<button v-else type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="openAddModal">
|
||||
افزودن کاربر
|
||||
</button>
|
||||
</BaseEmptyState>
|
||||
|
||||
<div v-else class="users-page__table admin-card desktop-only">
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>نام</th>
|
||||
<th>تلفن</th>
|
||||
<th>ایمیل</th>
|
||||
<th>نقش</th>
|
||||
<th class="status">وضعیت</th>
|
||||
<th>آخرین ورود</th>
|
||||
<th class="actions">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in filteredUsers" :key="user.id">
|
||||
<td>{{ user.fullName }}</td>
|
||||
<td>{{ user.phone }}</td>
|
||||
<td dir="ltr">{{ user.email }}</td>
|
||||
<td>{{ USER_ROLE_LABELS[user.role] }}</td>
|
||||
<td class="status">
|
||||
<ActiveSwitchCell
|
||||
:id="user.id"
|
||||
v-model="user.isActive"
|
||||
:entity-name="user.fullName"
|
||||
entity-type="کاربر"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ user.lastLoginAt ? formatDateTime(user.lastLoginAt) : '—' }}</td>
|
||||
<td class="actions">
|
||||
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(user)">
|
||||
ویرایش
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredUsers.length" class="compact-only admin-card-list">
|
||||
<article v-for="user in filteredUsers" :key="user.id" class="admin-card-item">
|
||||
<div class="admin-card-item__header">
|
||||
<p class="admin-card-item__title">{{ user.fullName }}</p>
|
||||
<ActiveSwitchCell
|
||||
:id="user.id"
|
||||
v-model="user.isActive"
|
||||
:entity-name="user.fullName"
|
||||
entity-type="کاربر"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin-card-item__meta">
|
||||
<span>{{ USER_ROLE_LABELS[user.role] }}</span>
|
||||
<span>{{ user.phone }}</span>
|
||||
<span dir="ltr">{{ user.email }}</span>
|
||||
</div>
|
||||
<div class="admin-card-item__actions">
|
||||
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEditModal(user)">
|
||||
ویرایش
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<BaseModal :open="isModalOpen" :title="modalTitle" size="lg" @close="closeModal">
|
||||
<form class="admin-form" @submit.prevent="submitUser">
|
||||
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="user-name">نام کامل <span class="admin-form__required">*</span></label>
|
||||
<input id="user-name" v-model="form.fullName" type="text" class="admin-input" />
|
||||
</div>
|
||||
|
||||
<div class="admin-form__row">
|
||||
<div class="admin-form__field">
|
||||
<label for="user-phone">تلفن <span class="admin-form__required">*</span></label>
|
||||
<input id="user-phone" v-model="form.phone" type="text" class="admin-input" />
|
||||
</div>
|
||||
<div class="admin-form__field">
|
||||
<label for="user-email">ایمیل <span class="admin-form__required">*</span></label>
|
||||
<input id="user-email" v-model="form.email" type="email" class="admin-input" dir="ltr" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<label for="user-role">نقش</label>
|
||||
<select id="user-role" v-model="form.role" class="admin-input">
|
||||
<option value="admin">مدیر سیستم</option>
|
||||
<option value="sales">واحد فروش</option>
|
||||
<option value="finance">واحد مالی</option>
|
||||
<option value="warehouse">انبار</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="admin-form__field">
|
||||
<BaseSwitch v-model="form.isActive" label="وضعیت" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button type="button" class="admin-btn admin-btn--outline" @click="closeModal">انصراف</button>
|
||||
<button type="button" class="admin-btn admin-btn--primary" :disabled="!isFormValid || saving" @click="submitUser">
|
||||
<span v-if="saving" class="admin-btn__spinner" aria-hidden="true" />
|
||||
{{ modalMode === 'edit' ? 'ذخیره تغییرات' : 'ثبت کاربر' }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.users-page__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.users-page__search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 0.875rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.users-page__table {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user