Files
CRM_Montzar/apps/web/src/components/TeamsSettingsPanel.vue
T

419 lines
12 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.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import type {
OrgUserDto,
TeamDto,
TeamMemberRole,
} from '@mento/shared';
import {
DATA_VISIBILITY_LABELS,
DATA_VISIBILITIES,
type DataVisibility,
} from '@mento/shared';
import { UiButton, UiEmptyState, UiField, UiInput } from '@/components/ui';
import { toFaDigits } from '@/lib/jalali';
const props = defineProps<{
users: OrgUserDto[];
canEdit: boolean;
}>();
const emit = defineEmits<{
refreshUsers: [];
}>();
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const ok = ref('');
const teams = ref<TeamDto[]>([]);
const newName = ref('');
const newDesc = ref('');
const editingId = ref<string | null>(null);
const editingMetaId = ref<string | null>(null);
const editName = ref('');
const editDesc = ref('');
const memberDraft = ref<Record<string, TeamMemberRole>>({});
const editableUsers = computed(() =>
props.users.filter((u) => u.isActive !== false && u.role !== 'owner' && u.role !== 'admin'),
);
async function load() {
loading.value = true;
error.value = '';
try {
const { data } = await api.get<TeamDto[]>('/teams');
teams.value = data;
} catch (e) {
if (axios.isAxiosError(e) && e.response?.status === 403) {
error.value = 'ماژول تیم‌ها و سطح دسترسی به پرونده‌ها برای این محیط فعال نیست.';
} else {
error.value = 'بارگذاری تیم‌ها ناموفق بود.';
}
} finally {
loading.value = false;
}
}
async function createTeam() {
if (!newName.value.trim()) {
error.value = 'نام تیم را وارد کنید.';
return;
}
saving.value = true;
error.value = '';
try {
await api.post('/teams', {
name: newName.value.trim(),
description: newDesc.value.trim(),
});
newName.value = '';
newDesc.value = '';
ok.value = 'تیم ساخته شد.';
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'ساخت تیم ناموفق بود.')
: 'ساخت تیم ناموفق بود.';
} finally {
saving.value = false;
}
}
function startEdit(team: TeamDto) {
editingId.value = team.id;
editingMetaId.value = null;
const draft: Record<string, TeamMemberRole> = {};
for (const m of team.members) {
draft[m.userId] = m.role;
}
memberDraft.value = draft;
}
function startMetaEdit(team: TeamDto) {
editingMetaId.value = team.id;
editingId.value = null;
editName.value = team.name;
editDesc.value = team.description || '';
}
async function saveMeta() {
if (!editingMetaId.value) return;
if (!editName.value.trim()) {
error.value = 'نام تیم را وارد کنید.';
return;
}
saving.value = true;
error.value = '';
try {
await api.patch(`/teams/${editingMetaId.value}`, {
name: editName.value.trim(),
description: editDesc.value.trim(),
});
ok.value = 'اطلاعات تیم ذخیره شد.';
editingMetaId.value = null;
await load();
} catch (e) {
error.value = axios.isAxiosError(e)
? String((e.response?.data as { message?: string })?.message ?? 'ذخیره تیم ناموفق بود.')
: 'ذخیره تیم ناموفق بود.';
} finally {
saving.value = false;
}
}
function toggleMember(userId: string) {
if (memberDraft.value[userId]) {
const next = { ...memberDraft.value };
delete next[userId];
memberDraft.value = next;
} else {
memberDraft.value = { ...memberDraft.value, [userId]: 'member' };
}
}
function setRole(userId: string, role: TeamMemberRole) {
if (!memberDraft.value[userId]) return;
memberDraft.value = { ...memberDraft.value, [userId]: role };
}
async function saveMembers() {
if (!editingId.value) return;
saving.value = true;
error.value = '';
try {
await api.put(`/teams/${editingId.value}/members`, {
members: Object.entries(memberDraft.value).map(([userId, role]) => ({
userId,
role,
})),
});
ok.value = 'اعضای تیم ذخیره شد.';
editingId.value = null;
await load();
} catch {
error.value = 'ذخیره اعضا ناموفق بود.';
} finally {
saving.value = false;
}
}
async function removeTeam(id: string) {
if (!confirm('این تیم حذف شود؟')) return;
try {
await api.delete(`/teams/${id}`);
if (editingId.value === id) editingId.value = null;
await load();
} catch {
error.value = 'حذف تیم ناموفق بود.';
}
}
async function setVisibility(userId: string, dataVisibility: DataVisibility) {
try {
await api.patch(`/users/${userId}/visibility`, { dataVisibility });
ok.value =
'سطح دسترسی به پرونده‌ها به‌روز شد. کاربر باید دوباره وارد شود تا اعمال شود.';
emit('refreshUsers');
} catch (e) {
error.value = axios.isAxiosError(e)
? String(
(e.response?.data as { message?: string })?.message ??
'تغییر سطح دسترسی به پرونده‌ها ناموفق بود.',
)
: 'تغییر سطح دسترسی به پرونده‌ها ناموفق بود.';
}
}
onMounted(() => {
void load();
});
</script>
<template>
<div class="teams-panel">
<p v-if="error" class="error">{{ error }}</p>
<p v-if="ok" class="ok">{{ ok }}</p>
<section v-if="canEdit" class="card">
<h3>تیم جدید</h3>
<div class="row">
<UiField label="نام تیم">
<UiInput v-model="newName" placeholder="مثلاً فروش تهران" />
</UiField>
<UiField label="توضیح">
<UiInput v-model="newDesc" placeholder="اختیاری" />
</UiField>
<UiButton :loading="saving" @click="createTeam">افزودن</UiButton>
</div>
</section>
<section class="card">
<h3>تیم‌ها</h3>
<UiEmptyState
v-if="!loading && !teams.length"
title="تیمی تعریف نشده"
description="تیم بسازید تا اعضای فروش پرونده‌های هم‌تیمی را ببینند."
/>
<div v-else class="team-list">
<article v-for="t in teams" :key="t.id" class="team">
<header>
<div>
<strong>{{ t.name }}</strong>
<p class="muted">
{{ t.description || '—' }}
· {{ toFaDigits(t.memberCount) }} عضو
</p>
</div>
<div v-if="canEdit" class="actions">
<UiButton variant="secondary" @click="startMetaEdit(t)">ویرایش</UiButton>
<UiButton variant="secondary" @click="startEdit(t)">اعضا</UiButton>
<UiButton variant="secondary" @click="removeTeam(t.id)">حذف</UiButton>
</div>
</header>
<p class="members">
{{
t.members.length
? t.members
.map((m) => `${m.fullName}${m.role === 'manager' ? ' (سرپرست)' : ''}`)
.join('، ')
: 'بدون عضو'
}}
</p>
<div v-if="editingMetaId === t.id" class="editor">
<div class="row">
<UiField label="نام تیم">
<UiInput v-model="editName" />
</UiField>
<UiField label="توضیح">
<UiInput v-model="editDesc" />
</UiField>
</div>
<div class="actions">
<UiButton :loading="saving" @click="saveMeta">ذخیره</UiButton>
<UiButton variant="secondary" @click="editingMetaId = null">انصراف</UiButton>
</div>
</div>
<div v-if="editingId === t.id" class="editor">
<p class="hint">اعضا را انتخاب کنید؛ سرپرست همان عضو تیم است.</p>
<label v-for="u in users.filter((x) => x.isActive !== false)" :key="u.id" class="person">
<input
type="checkbox"
:checked="Boolean(memberDraft[u.id])"
@change="toggleMember(u.id)"
/>
<span>{{ u.fullName }}</span>
<select
v-if="memberDraft[u.id]"
:value="memberDraft[u.id]"
@change="setRole(u.id, ($event.target as HTMLSelectElement).value as TeamMemberRole)"
>
<option value="member">عضو</option>
<option value="manager">سرپرست</option>
</select>
</label>
<div class="actions">
<UiButton :loading="saving" @click="saveMembers">ذخیره اعضا</UiButton>
<UiButton variant="secondary" @click="editingId = null">انصراف</UiButton>
</div>
</div>
</article>
</div>
</section>
<section class="card">
<h3>سطح دسترسی به پرونده‌ها</h3>
<p class="hint">
مالک و مدیر همیشه همه پرونده‌ها را می‌بینند. برای بقیه: فقط خود، تیم‌های عضو، یا کل سازمان.
</p>
<UiEmptyState
v-if="!editableUsers.length"
title="کاربر قابل تنظیم نیست"
description="فقط نقش‌های فروش و پشتیبانی سطح دسترسی جدا به پرونده‌ها دارند."
/>
<div v-else class="vis-table">
<div v-for="u in editableUsers" :key="u.id" class="vis-row">
<span>{{ u.fullName }}</span>
<select
:value="u.dataVisibility ?? 'own'"
:disabled="!canEdit"
@change="
setVisibility(
u.id,
($event.target as HTMLSelectElement).value as DataVisibility,
)
"
>
<option v-for="v in DATA_VISIBILITIES" :key="v" :value="v">
{{ DATA_VISIBILITY_LABELS[v] }}
</option>
</select>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.teams-panel {
display: grid;
gap: 1rem;
}
.card {
background: #fff;
border: 1px solid #eef2f7;
border-radius: var(--radius, 12px);
padding: 1rem;
}
.card h3 {
margin: 0 0 0.75rem;
font-size: 1rem;
}
.row {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 0.75rem;
align-items: end;
}
.team-list {
display: grid;
gap: 0.75rem;
}
.team {
border: 1px solid #eef2f7;
border-radius: var(--radius, 12px);
padding: 0.75rem;
}
.team header {
display: flex;
justify-content: space-between;
gap: 0.75rem;
align-items: start;
}
.actions {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.muted,
.hint,
.members {
color: #64748b;
font-size: 0.85rem;
margin: 0.25rem 0 0;
}
.editor {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid #eef2f7;
display: grid;
gap: 0.45rem;
}
.person {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.5rem;
align-items: center;
padding: 0.35rem 0;
}
.vis-table {
display: grid;
gap: 0.5rem;
}
.vis-row {
display: grid;
grid-template-columns: 1fr 220px;
gap: 0.75rem;
align-items: center;
padding: 0.45rem 0;
border-bottom: 1px solid #f1f5f9;
}
select {
font: inherit;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.4rem 0.55rem;
background: #fff;
}
.error {
color: #b91c1c;
font-weight: 600;
}
.ok {
color: #0f766e;
font-weight: 600;
}
@media (max-width: 800px) {
.row,
.vis-row {
grid-template-columns: 1fr;
}
}
</style>