fix team page // fix meeting page API

This commit is contained in:
Diyar Akhgar 2025-06-08 02:59:17 +03:30
parent 8f96b6a571
commit 6ca289deb0
9 changed files with 846 additions and 1037 deletions

View File

@ -1,66 +1,40 @@
<template>
<div class="buy-subscription-container">
<!-- Subscription Title -->
<div class="subscription-title">
<h3 style="text-align: center; margin-bottom: 20px;">
لطفا نوع اشتراک خود را انتخاب کنید
</h3>
<span>
ما مدل مجوزدهی انعطافپذیری ارائه میدهد. شما میتوانید بهصورت ماهانه و به ازای هر کاربر پرداخت کنید. تعداد کاربران را میتوان فوراً افزایش داد، اما در صورت کاهش مجوزها، تغییرات در پایان دورهی صورتحساب اعمال خواهند شد.
</span>
<h3 style="text-align: center; margin-bottom: 20px;">لطفا نوع اشتراک خود را انتخاب کنید</h3>
<span>مدل مجوزدهی انعطافپذیر با پرداخت ماهانه به ازای هر کاربر. تعداد کاربران را میتوان فوراً افزایش داد، اما کاهش مجوزها در پایان دورهی صورتحساب اعمال میشود.</span>
</div>
<!-- User Count Selector -->
<div class="user-count" style="text-align: start; margin: 3rem 0 2rem 0;">
<label for="memberCount" style="margin-left: 10px;">تعداد کاربران : </label>
<label for="memberCount" style="margin-left: 10px;">تعداد کاربران:</label>
<select
id="memberCount"
:value="memberCount"
@change="updateMemberCount($event)">
@change="updateMemberCount($event)"
>
<option v-for="count in availableMemberOptions" :key="count" :value="count">
{{ count }} کاربر
</option>
</select>
</div>
<!-- Plan Cards -->
<div style="display: flex; justify-content: space-between; flex-wrap: wrap;">
<div class="plan-card">
<div v-for="(plan, key) in plans" :key="key" class="plan-card">
<div class="card-inner">
<h4>هفتگی</h4>
<h4>{{ plan.name }}</h4>
<div class="card-price-title">
<p>{{ (plans.weekly.price * memberCount).toLocaleString() }} تومان</p>
<small>برای {{ memberCount }} کاربر، در هفته</small>
<p>{{ (plan.price * memberCount).toLocaleString() }} تومان</p>
<small>برای {{ memberCount }} کاربر، در {{ plan.name.toLowerCase() }}</small>
</div>
<button class="primary-button" @click="selectPlan('weekly')">
انتخاب طرح اشتراک
</button>
</div>
</div>
<div class="plan-card">
<div class="card-inner">
<h4>ماهانه</h4>
<div class="card-price-title">
<p>{{ (plans.monthly.price * memberCount).toLocaleString() }} تومان</p>
<small>برای {{ memberCount }} کاربر، در ماه</small>
</div>
<button class="primary-button" @click="selectPlan('monthly')">
انتخاب طرح اشتراک
</button>
</div>
</div>
<div class="plan-card">
<div class="card-inner">
<h4>سالانه</h4>
<div class="card-price-title">
<p>{{ (plans.yearly.price * memberCount).toLocaleString() }} تومان</p>
<small>برای {{ memberCount }} کاربر، در سال</small>
</div>
<button class="primary-button" @click="selectPlan('yearly')">
انتخاب طرح اشتراک
</button>
<button class="primary-button" @click="selectPlan(key)">انتخاب طرح اشتراک</button>
</div>
</div>
</div>
<!-- فاکتور -->
<!-- Invoice -->
<div
v-if="selectedPlan"
class="invoice-box"
@ -75,15 +49,11 @@
<span>مالیات (۹٪):</span>
<span>{{ selectedPlan.tax.toLocaleString() }} تومان</span>
</div>
<div
style="display: flex; justify-content: space-between; font-weight: bold; font-size: 16px; margin-bottom: 20px;"
>
<div style="display: flex; justify-content: space-between; font-weight: bold; font-size: 16px; margin-bottom: 20px;">
<span>مبلغ قابل پرداخت:</span>
<span>{{ selectedPlan.total.toLocaleString() }} تومان</span>
</div>
<button class="primary-button" style="width: 100%;" @click="pay">
پرداخت
</button>
<button class="primary-button" style="width: 100%;" @click="pay">پرداخت</button>
</div>
</div>
</template>
@ -94,18 +64,11 @@ import axios from 'axios';
export default {
name: 'BuySubscription',
props: {
memberCount: {
type: Number,
default: 5,
},
availableMemberOptions: {
type: Array,
default: () => [5, 10, 20, 100],
},
baseUrl: {
type: String,
required: true,
},
memberCount: { type: Number, default: 5 },
availableMemberOptions: { type: Array, default: () => [5, 10, 20, 100] },
baseUrl: { type: String, required: true },
hasActiveSubscription: { type: Boolean, default: false },
hasExpiredSubscription: { type: Boolean, default: false }, // جدید
},
data() {
return {
@ -121,67 +84,60 @@ export default {
updateMemberCount(event) {
const newCount = Number(event.target.value);
this.$emit('update:memberCount', newCount);
if (this.selectedPlan) {
this.selectPlan(
this.selectedPlan.name === 'هفتگی' ? 'weekly' : this.selectedPlan.name === 'ماهانه' ? 'monthly' : 'yearly'
);
}
if (this.selectedPlan) this.selectPlan(this.selectedPlan.name.toLowerCase());
},
selectPlan(planKey) {
const plan = this.plans[planKey];
if (!plan) return;
const base = plan.price * this.memberCount;
const tax = Math.round(base * 0.09);
this.selectedPlan = {
...plan,
basePrice: base,
tax,
total: base + tax,
};
this.$emit('plan-selected', this.selectedPlan);
this.selectedPlan = { ...plan, basePrice: base, tax, total: base + tax };
},
async pay() {
if (!this.selectedPlan) {
alert('لطفاً ابتدا یک طرح اشتراک انتخاب کنید.');
if (this.hasActiveSubscription) {
alert('شما اشتراک فعالی دارید و نمی‌توانید اشتراک دیگری خریداری کنید.');
return;
}
if (this.hasExpiredSubscription) {
alert('شما یکبار اشتراک تهیه کردید و نمی‌توانید دوباره اشتراک تهیه کنید.');
return;
}
if (!this.selectedPlan) {
alert('لطفاً یک طرح اشتراک انتخاب کنید.');
return;
}
try {
const startTime = new Date().toISOString();
let endTime;
if (this.selectedPlan.name === 'هفتگی') {
endTime = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
} else if (this.selectedPlan.name === 'ماهانه') {
endTime = new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000).toISOString();
} else if (this.selectedPlan.name === 'سالانه') {
endTime = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000).toISOString();
}
const endTime = this.calculateEndTime(this.selectedPlan.name);
const subscriptionData = {
user_count: this.memberCount,
license_number: `ABC-${Math.random().toString(36).substr(2, 6).toUpperCase()}-XYZ`,
startTime: startTime,
endTime: endTime,
startTime,
endTime,
price: this.selectedPlan.total,
};
const token = localStorage.getItem('token');
await axios.post(`${this.baseUrl}/add_subscription/`, subscriptionData, {
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'application/json',
},
if (!token) throw new Error('توکن احراز هویت یافت نشد.');
const response = await axios.post(`${this.baseUrl}/add_subscription/`, subscriptionData, {
headers: { Authorization: `Token ${token}`, 'Content-Type': 'application/json' },
});
alert(`پرداخت با موفقیت انجام شد برای ${this.memberCount} کاربر`);
this.$emit('payment-success', { subscriptionId: response.data.subscription_id });
this.selectedPlan = null;
this.$emit('payment-success');
} catch (error) {
console.error('Error registering subscription:', error);
alert('خطا در ثبت اشتراک. لطفاً دوباره تلاش کنید.');
}
},
calculateEndTime(planName) {
const now = new Date();
if (planName === 'هفتگی') {
return new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString();
} else if (planName === 'ماهانه') {
return new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString();
} else {
return new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000).toISOString();
}
},
},
};
</script>

View File

@ -1,9 +1,9 @@
<template>
<div v-if="isOpen && !isRoomSelectionOpen" class="modal-overlay" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-content" ref="modalContent" @click.stop>
<div class="popUp-header">
<h2>ایجاد جلسه جدید</h2>
<button @click="closeModal">
<button @click="closeModalByButton">
<svg
xmlns="http://www.w3.org/2000/svg"
width="35"
@ -38,25 +38,16 @@
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="meetingTitle">نام جلسه</label>
<input
type="text"
id="meetingTitle"
v-model="form.title"
required
/>
<input type="text" id="meetingTitle" v-model="form.title" required />
</div>
<div class="form-group">
<label for="meet-description">شرح جلسه</label>
<textarea
name="meet-description"
id="meet-description"
v-model="form.description"
></textarea>
<textarea name="meet-description" id="meet-description" v-model="form.description"></textarea>
</div>
<div class="form-group">
<label for="meetingDate">روز</label>
<div class="input-group">
<span style="position: absolute; z-index: 1; top: 10px; right: 32%;">
<span class="calendar-icon" style="position: absolute; z-index: 99; top: 10px; left: 65%;">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
@ -95,8 +86,8 @@
:auto-submit="true"
input-class="form-control"
id="meetingDate"
required
style="border-radius: 0 8px 8px 0; text-align: center; position: relative;"
required
/>
</div>
</div>
@ -286,31 +277,31 @@
<div class="form-group">
<label style="font-size: 19px; font-weight: 600;">اتاق جلسه</label>
<div class="rooms-selecter">
<span>{{ form.selectedRoom ? '0 اتاق انتخاب شده' : '0 اتاق انتخاب شده' }}</span>
<button type="button" @click="openRoomSelection" style="cursor: pointer;">انتخاب اتاق جلسه</button>
<span>{{ form.selectedRoom ? '1 اتاق انتخاب شده' : '0 اتاق انتخاب شده' }}</span>
<button type="button" @click="openRoomSelection">انتخاب اتاق جلسه</button>
</div>
</div>
<div class="participants-objects">
<h2>شرکت کنندگان</h2>
<p><span style="color: #101010;font-weight: 600;">کاربران</span> یا <span style="color: #101010;font-weight: 600;">مهمانان تیم</span> را با پر کردن شماره تلفن آنها دعوت کنید.</p>
<p><span style="color: #101010; font-weight: 600;">کاربران</span> یا <span style="color: #101010; font-weight: 600;">مهمانان تیم</span> را از لیست زیر انتخاب کنید.</p>
<span class="participants-guide">
میتوانید به مجری اجازه بدهید تا ابزارهایی برای مدیریت این جلسه و همچنین ابزارهایی برای مدیریت مجوزها در طول جلسه به او بدهد.
</span>
</div>
<div class="presenter">
<div class="presenter">
<div style="display: flex; align-items: center; height: 100%;">
<div class="avatar-wrapper">
<img class="user-avatar" :src="profileIcon" />
</div>
<div class="user-info">
<p class="user-name">{{ fullName }}</p>
<span>{{ userPhone || 'شماره تلفن موجود نیست' }}</span>
<span>{{ userPhone }}</span>
</div>
</div>
<p class="presenter-role">{{ userRole }}</p>
</div>
<div class="presenter" v-for="participant in participants" :key="participant.phone">
<div style="display: flex;align-items: center;height: 100%;">
<div class="presenter" v-for="participant in participants" :key="participant.id">
<div style="display: flex; align-items: center; height: 100%;">
<div class="avatar-wrapper">
<img class="user-avatar" :src="participant.profile_img || defaultProfileIcon" />
</div>
@ -320,7 +311,7 @@
</div>
</div>
<p class="presenter-role">{{ participant.role }}</p>
<button @click="removeParticipant(participant.phone)">
<button @click="removeParticipant(participant.id)">
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 32 32" fill="none">
<rect x="0.5" y="0.5" width="31" height="31" rx="7.5" fill="white"/>
<rect x="0.5" y="0.5" width="31" height="31" rx="7.5" stroke="#E2DEE9"/>
@ -330,21 +321,55 @@
</button>
</div>
<div class="form-group">
<label for="participantPhone">اضافه کردن شرکت کننده</label>
<label for="participantInput">اضافه کردن شرکت کننده</label>
<div class="participant-input">
<input
type="tel"
id="participantPhone"
v-model="newParticipantPhone"
placeholder="لطفا شماره تلفن شرکت کننده را وارد کنید"
@keyup.enter="addParticipant"
/>
<button type="button" @click="addParticipant">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 16 16" fill="none">
<path d="M3.33203 8H12.6654" stroke="#3A57E8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 3.33325V12.6666" stroke="#3A57E8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<div class="custom-input" @click="toggleDropdown" ref="customInput">
<span v-if="!selectedParticipantId">یک عضو تیم انتخاب کنید</span>
<span v-else>{{ getParticipantName(selectedParticipantId) }}</span>
<svg
class="dropdown-icon"
:class="{ active: isDropdownOpen }"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M5 7.5L10 12.5L15 7.5"
stroke="#3A57E8"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<div v-if="isDropdownOpen" class="dropdown-menu" ref="dropdownMenu">
<div
v-for="member in teamMembers"
:key="member.user.id"
class="dropdown-item"
@click="selectParticipant(member.user.id)"
>
<div class="avatar-wrapper">
<img
:src="member.profile_img || defaultProfileIcon"
class="user-avatar"
alt="Profile"
/>
</div>
<div class="user-info">
<div style="display: flex; flex-direction: column; gap: 1rem;">
<p class="user-name">{{ member.user.first_name }} {{ member.user.last_name }}</p>
<span>{{ member.mobile_number }}</span>
</div>
<span class="user-role">{{ member.semat || 'بدون سمت' }}</span>
</div>
</div>
<div v-if="!teamMembers.length" class="dropdown-item no-members">
هیچ عضوی یافت نشد
</div>
</div>
</div>
</div>
</form>
@ -353,7 +378,7 @@
</span>
</div>
<div class="form-actions">
<button type="button" class="cancel-button" @click="closeModal">بازگشت</button>
<button type="button" class="cancel-button" @click="closeModalByButton">بازگشت</button>
<button type="button" class="submit-button" @click="handleSubmit">ایجاد جلسه</button>
</div>
</div>
@ -369,21 +394,19 @@
import VuePersianDatetimePicker from 'vue3-persian-datetime-picker';
import moment from 'moment-jalaali';
import RoomSelectionModal from './RoomSelectionModal.vue';
import axios from 'axios';
const API_BASE_URL = 'http://my.xroomapp.com:8000';
export default {
name: 'MeetingModal',
components: {
VuePersianDatetimePicker,
RoomSelectionModal,
},
name: 'CreateMeetingModal',
components: { VuePersianDatetimePicker, RoomSelectionModal },
props: {
isOpen: {
type: Boolean,
default: false,
},
isOpen: { type: Boolean, default: false },
},
data() {
return {
defaultProfileIcon: 'https://models.readyplayer.me/681f59760bc631a87ad25172.png',
form: {
title: '',
description: '',
@ -393,27 +416,26 @@ export default {
endHour: 18,
endMinute: 0,
selectedRoom: null,
use_space: false,
},
participants: [],
newParticipantPhone: '',
defaultProfileIcon: 'https://c.animaapp.com/m9nvumalUMfQbN/img/frame.svg',
error: null,
teamMembers: [],
selectedParticipantId: '',
isDropdownOpen: false,
isRoomSelectionOpen: false,
error: null,
};
},
computed: {
customer() {
// دریافت اطلاعات کاربر از localStorage با کلید customer
return JSON.parse(localStorage.getItem('customer') || '{}');
},
fullName() {
const user = JSON.parse(localStorage.getItem('user') || '{}');
return user.first_name && user.last_name
? `${user.first_name} ${user.last_name}`
: 'کاربر مهمان';
return user.first_name && user.last_name ? `${user.first_name} ${user.last_name}` : 'کاربر مهمان';
},
userPhone() {
return 3;
return this.customer.mobile_number || 'شماره تلفن موجود نیست';
},
userRole() {
return this.customer.semat || 'مجری';
@ -422,65 +444,122 @@ export default {
return this.customer.profile_img || this.defaultProfileIcon;
},
userId() {
const customer = JSON.parse(localStorage.getItem('customer') || '{}');
return customer.id || null;
}
return this.customer.id || null;
},
},
watch: {
isOpen(newVal) {
document.body.style.overflow = newVal && !this.isRoomSelectionOpen ? 'hidden' : '';
if (newVal) {
document.body.style.overflow = 'hidden';
} else if (!this.isRoomSelectionOpen) {
document.body.style.overflow = '';
this.$nextTick(() => {
if (this.$refs.modalContent) {
this.$refs.modalContent.addEventListener('click', this.closeDropdownOnClick);
}
});
} else {
if (this.$refs.modalContent) {
this.$refs.modalContent.removeEventListener('click', this.closeDropdownOnClick);
}
}
},
isRoomSelectionOpen(newVal) {
if (newVal) {
document.body.style.overflow = 'hidden';
} else if (!this.isOpen) {
document.body.style.overflow = '';
}
document.body.style.overflow = newVal ? 'hidden' : '';
},
},
mounted() {
if (this.$refs.modalContent) {
this.$refs.modalContent.addEventListener('click', this.closeDropdownOnClick);
}
},
beforeUnmount() {
if (this.$refs.modalContent) {
this.$refs.modalContent.removeEventListener('click', this.closeDropdownOnClick);
}
},
methods: {
beforeDestroy() {
document.body.style.overflow = '';
async fetchTeamMembers() {
try {
const token = localStorage.getItem('token');
if (!token) throw new Error('توکن احراز هویت پیدا نشد');
const response = await axios.get(`${API_BASE_URL}/get_all_team_members`, {
headers: { Authorization: `Token ${token.trim()}` },
});
this.teamMembers = response.data.members.filter(
(member) => member.user?.id && member.user.first_name && member.user.last_name
);
} catch (error) {
if (error.response?.status === 403) {
alert('لطفاً دوباره وارد شوید');
window.location.href = '/login';
}
this.error = 'خطا در بارگذاری لیست اعضای تیم';
}
},
toggleDropdown(event) {
this.isDropdownOpen = !this.isDropdownOpen;
event.stopPropagation();
},
selectParticipant(id) {
this.selectedParticipantId = id;
this.isDropdownOpen = false;
this.addParticipant();
},
closeDropdownOnClick(event) {
if (this.isDropdownOpen && this.$refs.customInput && !this.$refs.customInput.contains(event.target)) {
this.isDropdownOpen = false;
}
},
openRoomSelection() {
this.isRoomSelectionOpen = true;
},
handleRoomSelection(room) {
this.form.selectedRoom = room;
this.form.use_space = room.use_space;
this.isRoomSelectionOpen = false;
},
getParticipantName(id) {
const member = this.teamMembers.find((m) => m.user.id === id);
return member ? `${member.user.first_name} ${member.user.last_name}` : '';
},
addParticipant() {
if (!this.newParticipantPhone || !this.validatePhone(this.newParticipantPhone)) {
this.error = 'لطفاً شماره تلفن معتبر وارد کنید (مثال: 09123456789)';
if (!this.selectedParticipantId) {
this.error = 'لطفاً یک عضو تیم انتخاب کنید.';
return;
}
if (
this.participants.some((p) => p.phone === this.newParticipantPhone) ||
this.newParticipantPhone === this.userPhone
) {
this.error = 'این شماره تلفن قبلاً اضافه شده است';
const selectedMember = this.teamMembers.find((member) => member.user.id === this.selectedParticipantId);
if (!selectedMember) {
this.error = 'کاربر انتخاب‌شده یافت نشد.';
return;
}
if (this.participants.some((p) => p.id === this.selectedParticipantId)) {
this.error = 'این کاربر قبلاً اضافه شده است.';
return;
}
if (this.selectedParticipantId === this.userId) {
this.error = 'نمی‌توانید خودتان را به‌عنوان شرکت‌کننده اضافه کنید.';
return;
}
this.participants.push({
phone: this.newParticipantPhone,
name: 'کاربر مهمان',
role: 'شرکت‌کننده',
profile_img: this.defaultProfileIcon,
id: selectedMember.user.id,
phone: selectedMember.mobile_number,
name: `${selectedMember.user.first_name} ${selectedMember.user.last_name}`,
role: selectedMember.semat || 'بدون سمت',
profile_img: selectedMember.profile_img || this.defaultProfileIcon,
});
this.newParticipantPhone = '';
this.selectedParticipantId = '';
this.isDropdownOpen = false;
this.error = null;
},
removeParticipant(phone) {
this.participants = this.participants.filter((p) => p.phone !== phone);
removeParticipant(id) {
this.participants = this.participants.filter((p) => p.id !== id);
},
validatePhone(phone) {
return /^09[0-9]{9}$/.test(phone);
closeModal(event) {
if (event && event.target.classList.contains('modal-overlay')) {
this.$emit('close');
this.resetForm();
}
},
closeModal() {
closeModalByButton() {
this.$emit('close');
this.resetForm();
},
@ -494,39 +573,30 @@ export default {
endHour: 18,
endMinute: 0,
selectedRoom: null,
use_space: false,
};
this.participants = [];
this.newParticipantPhone = '';
this.selectedParticipantId = '';
this.isDropdownOpen = false;
this.error = null;
this.isRoomSelectionOpen = false;
},
incrementTime(field) {
if (field === 'startHour' && this.form.startHour < 23) {
this.form.startHour++;
} else if (field === 'startMinute' && this.form.startMinute < 59) {
this.form.startMinute++;
} else if (field === 'endHour' && this.form.endHour < 23) {
this.form.endHour++;
} else if (field === 'endMinute' && this.form.endMinute < 59) {
this.form.endMinute++;
}
const limits = { startHour: 23, startMinute: 59, endHour: 23, endMinute: 59 };
if (this.form[field] < limits[field]) this.form[field]++;
},
decrementTime(field) {
if (field === 'startHour' && this.form.startHour > 0) {
this.form.startHour--;
} else if (field === 'startMinute' && this.form.startMinute > 0) {
this.form.startMinute--;
} else if (field === 'endHour' && this.form.endHour > 0) {
this.form.endHour--;
} else if (field === 'endMinute' && this.form.endMinute > 0) {
this.form.endMinute--;
}
if (this.form[field] > 0) this.form[field]--;
},
async handleSubmit() {
if (!this.form.title || !this.form.date) {
this.error = 'لطفاً نام جلسه و تاریخ را وارد کنید.';
return;
}
if (!this.form.selectedRoom) {
this.error = 'لطفاً یک اتاق برای جلسه انتخاب کنید.';
return;
}
const momentDate = moment(this.form.date, 'jYYYY/jMM/jDD');
if (!momentDate.isValid()) {
this.error = 'تاریخ وارد شده معتبر نیست.';
@ -540,43 +610,162 @@ export default {
}
const startDateTime = momentDate
.clone()
.set({
hour: this.form.startHour,
minute: this.form.startMinute,
second: 0,
})
.set({ hour: this.form.startHour, minute: this.form.startMinute, second: 0 })
.toISOString();
try {
const userIds = [
...(this.userPhone ? [this.userPhone] : []),
...this.participants.map((p) => p.phone),
];
const meetingData = {
name: this.form.title,
description: this.form.description,
date_time: startDateTime,
space: this.form.selectedRoom ? this.form.selectedRoom.id : null,
space: this.form.selectedRoom.id,
asset_bundle: 1,
use_space: !!this.form.selectedRoom,
user_ids: userIds,
use_space: this.form.use_space,
user_ids: [this.userId, ...this.participants.map((p) => p.id)],
};
console.log('داده‌های ارسالی به API:', JSON.stringify(meetingData, null, 2));
this.$emit('create-meeting', meetingData);
this.closeModal();
this.closeModalByButton();
} catch (error) {
this.error = `خطا در آماده‌سازی داده‌ها: ${error.message}`;
console.error('خطا در handleSubmit:', error);
}
},
},
created() {
this.fetchTeamMembers();
},
};
</script>
<style scoped>
.participant-input {
position: relative;
display: flex;
align-items: center;
}
.custom-input {
width: 100%;
padding: 10px;
border: 1px solid #E2DEE9;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
transition: border-color 0.3s ease;
user-select: none;
}
.custom-input:hover {
border-color: #3A57E8;
}
.dropdown-icon {
transition: transform 0.3s ease;
}
.custom-input.active .dropdown-icon {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 1px solid #E2DEE9;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-height: 300px;
overflow-y: auto;
z-index: 1000;
margin-top: 5px;
}
.dropdown-item {
display: flex;
align-items: center;
padding: 0px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #f5f7fa;
}
.dropdown-item .avatar-wrapper {
width: 90px;
height: 90px;
margin-right: 0px;
}
.dropdown-item .user-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.dropdown-item .user-info {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
padding-left: 2.5rem;
margin-top: 15px;
}
.dropdown-item .user-name {
font-size: 16px;
font-weight: 600;
color: #101010;
margin: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: clip;
width: 100px;
}
.dropdown-item .user-info span {
font-size: 14px;
color: #718096;
width: 75px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: clip;
}
.dropdown-item .user-role {
font-size: 12px;
color: #3A57E8;
font-weight: 500;
}
.no-members {
padding: 10px;
text-align: center;
color: #718096;
}
.participant-input button {
margin-left: 10px;
padding: 10px;
background: #3A57E8;
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
/* */
.modal-overlay {
position: fixed;
top: 0;
@ -876,6 +1065,10 @@ export default {
font-size: 18px;
color: #101010;
font-weight: 600;
width: 160px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: clip;
}
.presenter button {

View File

@ -1,7 +1,7 @@
<template>
<div class="tab-content">
<!-- Access Container -->
<div class="access-container">
<!-- Header Section -->
<div class="access-header">
<div class="header-content">
<img :src="require('@/assets/img/lock Icon.png')" alt="lock" class="lock-icon" />
@ -16,90 +16,83 @@
</button>
</div>
<!-- Info Cards Section -->
<div class="info-cards" v-if="isLoading">
<div class="loading">در حال بارگذاری...</div>
</div>
<div class="info-cards" v-else-if="error">
<div class="error">{{ error }} <button @click="retryFetch">تلاش مجدد</button></div>
</div>
<div class="info-cards" v-else>
<!-- Billing Info Card -->
<div class="info-card">
<div class="card-content">
<h4>{{ translations.billing.title }}</h4>
<div class="billing-info">
<p class="billing-address">{{ billingInfo.address }}</p>
<p class="billing-phoneNum">
{{ translations.billing.phone }}: <span>{{ billingInfo.phone }}</span>
<!-- Info Cards -->
<div class="info-cards">
<div v-if="isLoading" class="loading">در حال بارگذاری...</div>
<div v-else-if="error" class="error">
{{ error }} <button @click="retryFetch">تلاش مجدد</button>
</div>
<template v-else>
<div class="info-card">
<div class="card-content">
<h4>{{ translations.billing.title }}</h4>
<div class="billing-info">
<p class="billing-address">{{ billingInfo.address }}</p>
<p class="billing-phoneNum">
{{ translations.billing.phone }}: <span>{{ billingInfo.phone }}</span>
</p>
<p class="billing-email">
{{ translations.billing.email }}: <span>{{ billingInfo.email }}</span>
</p>
</div>
</div>
<button class="secondary-button" @click="openBillingModal">
{{ translations.billing.editButton }}
</button>
</div>
<div class="info-card">
<div class="card-content">
<h4>{{ translations.memberships.title }}</h4>
<p class="memberships">
{{ hasActiveSubscription ? translations.memberships.active : translations.memberships.inactive }}
</p>
<p class="billing-email">
{{ translations.billing.email }}: <span>{{ billingInfo.email }}</span>
</div>
<button class="secondary-button" @click="navigateToSubscription">
{{ translations.memberships.manageButton }}
</button>
</div>
<div class="info-card">
<div class="card-content">
<h4>{{ translations.payment.title }}</h4>
<p class="payment-method">
{{ paymentMethod || translations.payment.noMethod }}
</p>
</div>
</div>
<button class="secondary-button" @click="openBillingModal">
{{ translations.billing.editButton }}
</button>
</div>
<!-- Memberships Card -->
<div class="info-card">
<div class="card-content">
<h4>{{ translations.memberships.title }}</h4>
<p class="memberships">
{{ hasActiveSubscription ? translations.memberships.active : translations.memberships.inactive }}
</p>
</div>
<button class="secondary-button" @click="manageMemberships">
{{ translations.memberships.manageButton }}
</button>
</div>
<!-- Payment Method Card -->
<div class="info-card">
<div class="card-content">
<h4>{{ translations.payment.title }}</h4>
<p class="payment-method">
{{ paymentMethod || translations.payment.noMethod }}
</p>
</div>
</div>
<!-- Team Subscription Card -->
<div class="info-card">
<div class="card-content">
<h4>{{ translations.subscription.title }}</h4>
<div class="subscription-info" v-if="hasRemainingCapacity">
<p class="subscription-all">
{{ translations.subscription.total }}: <span>{{ subscriptionCount }} {{ translations.subscription.users }}</span>
</p>
<p class="subscription-remainder">
{{ translations.subscription.remaining }}: <span>{{ remainingCapacity }} {{ translations.subscription.users }}</span>
</p>
<p class="subscription-added">
{{ translations.subscription.added }}: <span>{{ teamMemberCapacity }} {{ translations.subscription.users }}</span>
<div class="info-card">
<div class="card-content">
<h4>{{ translations.subscription.title }}</h4>
<div v-if="hasRemainingCapacity" class="subscription-info">
<p class="subscription-all">
{{ translations.subscription.total }}: <span>{{ subscriptionCount }} {{ translations.subscription.users }}</span>
</p>
<p class="subscription-remainder">
{{ translations.subscription.remaining }}: <span>{{ remainingCapacity }} {{ translations.subscription.users }}</span>
</p>
<p class="subscription-added">
{{ translations.subscription.added }}: <span>{{ teamMemberCapacity }} {{ translations.subscription.users }}</span>
</p>
</div>
<p v-else class="invalid-subscription">
{{ translations.subscription.noActive }}
</p>
</div>
<p class="invalid-subscription" v-else>
{{ translations.subscription.noActive }}
</p>
<button
:class="hasRemainingCapacity ? 'disable-button' : 'secondary-button'"
:disabled="hasRemainingCapacity"
@click="navigateToSubscription"
>
{{ hasRemainingCapacity ? translations.subscription.activeButton : translations.subscription.buyButton }}
</button>
</div>
<button
:class="hasRemainingCapacity ? 'disable-button' : 'secondary-button'"
:disabled="hasRemainingCapacity"
@click="navigateToSubscription"
>
{{ hasRemainingCapacity ? translations.subscription.activeButton : translations.subscription.buyButton }}
</button>
</div>
</template>
</div>
<!-- Billing Modal -->
<!-- Edit Billing Modal -->
<EditBillingModal
:isVisible="isBillingModalVisible"
:is-visible="isBillingModalVisible"
@close="closeBillingModal"
@update:billingInfo="updateBillingInfo"
@update:billing-info="updateBillingInfo"
/>
</div>
</div>
@ -110,24 +103,11 @@ import EditBillingModal from '@/components/EditBillingModal.vue';
export default {
name: 'Membership',
components: {
EditBillingModal,
},
components: { EditBillingModal },
props: {
subscriptionCount: {
type: Number,
required: true,
validator: (value) => value >= 0,
},
teamMemberCapacity: {
type: Number,
required: true,
validator: (value) => value >= 0,
},
isBillingModalVisible: {
type: Boolean,
default: false,
},
subscriptionCount: { type: Number, required: true, validator: value => value >= 0 },
teamMemberCapacity: { type: Number, required: true, validator: value => value >= 0 },
isBillingModalVisible: { type: Boolean, default: false },
},
data() {
return {
@ -141,7 +121,7 @@ export default {
memberships: {
title: 'عضویت‌ها',
active: 'اشتراک فعال است',
inactive: 'هنوز مجوزی فعال نیست. کاربران شما نمی‌توانند از XRoom با واترمارک استفاده کنند.',
inactive: 'هنوز مجوزی فعال نیست. کاربران شما نمی‌توانند از XRoom بدون واترمارک استفاده کنند.',
manageButton: 'مدیریت عضویت‌ها',
},
payment: {
@ -152,15 +132,13 @@ export default {
title: 'وضعیت اشتراک تیم',
total: 'ظرفیت کل تیم',
remaining: 'ظرفیت باقی‌مانده',
added: 'کاربران اضافه کرده',
added: 'کاربران اضافه شده',
users: 'کاربر',
noActive: 'شما اشتراک فعالی ندارین، لطفا اشتراک جدیدی خریداری نمایید.',
noActive: 'شما اشتراک فعالی ندارید، لطفاً اشتراک جدیدی خریداری کنید.',
activeButton: 'اشتراک فعال دارید',
buyButton: 'خرید اشتراک جدید',
},
error: {
fetchFailed: 'خطا در دریافت اطلاعات. لطفاً دوباره تلاش کنید.',
},
error: { fetchFailed: 'خطا در دریافت اطلاعات. لطفاً دوباره تلاش کنید.' },
},
billingInfo: {
address: 'اصفهان، خیابان وحید، نبش خیابان حسین آباد، مجتمع عسگری ۳، واحد ۳ ۸۱۷۵۹۴۹۹۹۱',
@ -182,19 +160,23 @@ export default {
},
},
created() {
this.simulateFetch();
this.fetchData();
},
methods: {
simulateFetch() {
// شبیهسازی دریافت دادهها
async fetchData() {
this.isLoading = true;
setTimeout(() => {
try {
await new Promise(resolve => setTimeout(resolve, 1000));
this.hasActiveSubscription = this.subscriptionCount > 0;
} catch {
this.error = this.translations.error.fetchFailed;
} finally {
this.isLoading = false;
}, 1000);
}
},
retryFetch() {
this.error = null;
this.simulateFetch();
this.fetchData();
},
navigateToSubscription() {
this.$emit('change-tab', 'buy-subscription');
@ -212,6 +194,7 @@ export default {
};
</script>
<style scoped>
.access-container {

View File

@ -34,7 +34,7 @@
<h2>فضاها</h2>
<span>یک اتاق برای این جلسه انتخاب کنید</span>
</div>
<div class="rooms-list" v-if="rooms.length > 0">
<div class="rooms-list" v-if="rooms.length">
<div
v-for="room in rooms"
:key="room.id"
@ -46,8 +46,8 @@
:src="room.image"
alt="Room Image"
class="room-image"
width="120px"
height="120px"
width="120"
height="120"
@error="room.image = 'https://via.placeholder.com/150'"
/>
<div class="room-details" style="margin-right: 10px;">
@ -168,12 +168,12 @@
<div v-else>
<span>هیچ فضایی یافت نشد.</span>
</div>
<div v-if="temporaryRooms.length > 0">
<div v-if="temporaryRooms.length">
<div class="popUp-title">
<h2>اتاقهای موقت</h2>
<span>اتاقهای موقت ایجادشده برای این جلسه</span>
</div>
<div class="temporary-rooms-list">
<div class="rooms-list">
<div
v-for="room in temporaryRooms"
:key="room.id"
@ -185,8 +185,8 @@
:src="room.image"
alt="Room Image"
class="room-image"
width="120px"
height="120px"
width="120"
height="120"
@error="room.image = 'https://via.placeholder.com/150'"
/>
<div class="room-details" style="margin-right: 10px;">
@ -305,35 +305,8 @@
</div>
</div>
</div>
<div class="popUp-title">
<h2>ایجاد اتاق موقت</h2>
<span>اتاق موقت را فقط برای این جلسه ایجاد کنید</span>
</div>
<div class="temporary-room-form">
<form @submit.prevent="createTemporaryRoom">
<div class="form-group">
<label for="tempRoomName">نام اتاق</label>
<input
type="text"
id="tempRoomName"
v-model="newTempRoom.name"
required
/>
</div>
<div class="form-group">
<label for="tempRoomImage">تصویر اتاق</label>
<input type="file" id="tempRoomImage" accept="image/*" @change="handleImageUpload" />
</div>
<div class="form-group">
<label for="tempRoomCapacity">ظرفیت</label>
<input v-model.number="newTempRoom.capacity" type="number" id="tempRoomCapacity" required />
</div>
<div class="form-group">
<label for="tempRoomType">نوع</label>
<input v-model="newTempRoom.type" id="tempRoomType" required />
</div>
<button type="submit" class="create-room">ایجاد اتاق</button>
</form>
<div v-else>
<span>هیچ اتاق موقتی یافت نشد.</span>
</div>
<div class="form-actions">
<button type="button" class="cancel-button" @click="cancel">لغو</button>
@ -346,72 +319,81 @@
<script>
import axios from 'axios';
const API_BASE_URL = 'http://my.xroomapp.com:8000';
const DEFAULT_IMAGE = 'https://via.placeholder.com/150';
export default {
name: 'RoomSelectionModal',
props: {
isOpen: {
type: Boolean,
default: false,
},
isOpen: { type: Boolean, default: false },
},
data() {
return {
rooms: [],
temporaryRooms: [],
selectedRoom: null,
newTempRoom: {
name: '',
capacity: 0,
type: '',
image: null,
},
error: null,
};
},
created() {
this.fetchSpaces();
this.fetchTemporaryRooms();
},
methods: {
async fetchSpaces() {
try {
const token = localStorage.getItem('token');
if (!token) {
this.error = 'توکن احراز هویت پیدا نشد';
console.error('توکن احراز هویت پیدا نشد');
return;
}
if (!token) throw new Error('توکن احراز هویت پیدا نشد');
const response = await axios.get('http://my.xroomapp.com:8000/get_space', {
headers: {
Authorization: `Token ${token.trim()}`,
},
const response = await axios.get(`${API_BASE_URL}/get_space`, {
headers: { Authorization: `Token ${token.trim()}` },
});
console.log('فضاها:', response.data.spaces);
this.rooms = response.data.spaces.map((space) => {
const imageUrl = space.assetBundleRoomId?.img
? `http://my.xroomapp.com:8000${space.assetBundleRoomId.img}`
: 'https://via.placeholder.com/150';
return {
id: space.id,
image: imageUrl,
name: space.name,
capacity: space.capacity,
type: space.description || 'فضا',
isTemporary: false,
};
});
this.rooms = response.data.spaces.map((space) => ({
id: space.id,
image: space.assetBundleRoomId?.img
? `${API_BASE_URL}${space.assetBundleRoomId.img}`
: DEFAULT_IMAGE,
name: space.name,
capacity: space.capacity,
type: space.description || 'فضا',
isTemporary: false,
}));
} catch (error) {
console.error('خطا در دریافت فضاها:', error);
this.error = 'خطا در بارگذاری لیست اتاق‌ها';
if (error.response && error.response.status === 403) {
if (error.response?.status === 403) {
alert('لطفاً دوباره وارد شوید');
window.location.href = '/login';
}
this.error = 'خطا در بارگذاری لیست اتاق‌ها';
}
},
async fetchTemporaryRooms() {
try {
const token = localStorage.getItem('token');
if (!token) throw new Error('توکن احراز هویت پیدا نشد');
const response = await axios.get(`${API_BASE_URL}/get_assigned_assetbundle_rooms`, {
headers: { Authorization: `Token ${token.trim()}` },
});
this.temporaryRooms = response.data.assetbundle_rooms.map((room) => ({
id: room.id,
image: room.img ? `${API_BASE_URL}${room.img}` : DEFAULT_IMAGE,
name: room.name,
capacity: room.maxPerson,
type: room.description || 'اتاق موقت',
isTemporary: true,
}));
} catch (error) {
if (error.response?.status === 403) {
alert('لطفاً دوباره وارد شوید');
window.location.href = '/login';
}
this.error = 'خطا در بارگذاری لیست اتاق‌های موقت';
}
},
selectRoom(roomId) {
this.selectedRoom = roomId;
this.selectedRoom = this.selectedRoom === roomId ? null : roomId;
},
cancel() {
this.$emit('close');
@ -425,27 +407,13 @@ export default {
const selectedRoomDetails = [...this.rooms, ...this.temporaryRooms].find(
(room) => room.id === this.selectedRoom
);
this.$emit('submit-room', selectedRoomDetails);
this.$emit('submit-room', {
...selectedRoomDetails,
id: selectedRoomDetails.isTemporary ? 12 : selectedRoomDetails.id,
use_space: !selectedRoomDetails.isTemporary,
});
this.selectedRoom = null;
},
handleImageUpload(event) {
const file = event.target.files[0];
if (file) {
this.newTempRoom.image = URL.createObjectURL(file);
}
},
createTemporaryRoom() {
const newRoom = {
id: this.rooms.length + this.temporaryRooms.length + 1,
image: this.newTempRoom.image || 'https://via.placeholder.com/150',
name: this.newTempRoom.name,
capacity: this.newTempRoom.capacity,
type: this.newTempRoom.type,
isTemporary: true,
};
this.temporaryRooms.push(newRoom);
this.newTempRoom = { name: '', capacity: 0, type: '', image: null };
},
},
};
</script>

View File

@ -1,7 +1,7 @@
<template>
<div>
<!-- Overlay for mobile when sidebar is open -->
<div class="overlay" v-if="isOpen && isMobile" @click="$emit('close')"></div>
<div class="overlay" :class="{ active: isOpen && isMobile }" v-if="isOpen && isMobile" @click="$emit('close')"></div>
<div class="sidebar" :class="{ 'open': isOpen }">
@ -62,16 +62,14 @@
<div class="text-wrapper">پشتیبانی</div>
</router-link>
</div>
</div>
<!-- Close Button for mobile -->
<button class="close-button" v-if="isMobile" @click="$emit('close')">
<button class="close-button" v-show="isOpen" v-if="isMobile" @click="$emit('close')">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</template>
@ -134,6 +132,8 @@ export default {
overflow-x: hidden;
overflow-y: auto;
z-index: 1000;
transform: transform translateX(100%);
transition: transform 0.3s ease-in-out;
}
.sidebar::-webkit-scrollbar {
@ -146,6 +146,7 @@ export default {
}
.sidebar.open {
transform: translateX(0);
display: flex;
flex-direction: column;
gap: 3rem;
@ -159,6 +160,14 @@ export default {
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
}
.overlay.active {
opacity: 1;
pointer-events: auto;
}
.close-button {
@ -171,6 +180,7 @@ export default {
background-color: #fff;
border-radius: 5px;
padding-top: 7px;
z-index: 10000;
}
.close-button svg {
@ -337,8 +347,12 @@ export default {
.sidebar {
width: 250px;
padding: 1rem 1rem 1rem 0.5rem;
display: none;
transition: transform 0.3s ease-in-out;
transform: translateX(100%);
}
.sidebar.open {
transform: translateX(0);
}
.nav-button {
@ -366,8 +380,11 @@ export default {
.sidebar {
width: 22rem;
padding: 1rem 1rem 1rem 0.5rem;
display: none;
transition: transform 0.3s ease-in-out;
transform: translateX(100%);
}
.sidebar.open {
transform: translateX(0);
}
.nav-button {
@ -397,6 +414,8 @@ export default {
padding: 30px 50px;
display: flex;
flex-direction: column;
transform: translateX(0);
}
.close-button {
@ -440,8 +459,7 @@ export default {
.sidebar {
width: 360px;
padding: 30px 50px;
display: flex;
flex-direction: column;
transform: translateX(0);
}
.close-button {

View File

@ -1,9 +1,10 @@
<template>
<div class="tab-content">
<!-- Team Logo Section -->
<div class="team-logo">
<div class="card-title">
<h2>لوگوی تیم</h2>
<p>این لوگو در اتاقهای شما استفاده خواهد شد. توصیه میکنیم از یک تصویر شفاف با نسبت تصویر 2:1 استفاده کنید.</p>
<p>این لوگو در اتاقهای شما نمایش داده میشود. توصیه میشود از تصویر شفاف با نسبت 2:1 استفاده کنید.</p>
</div>
<div class="logo-info">
<img :src="teamLogo || require('@/assets/img/team-logo.jpg')" alt="team logo" />
@ -16,34 +17,27 @@
viewBox="0 0 16 16"
fill="none"
>
<g clip-path="url(#clip0_312_7133)">
<path
d="M2.66602 11.3333V12.6666C2.66602 13.0202 2.80649 13.3593 3.05654 13.6094C3.30659 13.8594 3.64573 13.9999 3.99935 13.9999H11.9993C12.353 13.9999 12.6921 13.8594 12.9422 13.6094C13.1922 13.3593 13.3327 13.0202 13.3327 12.6666V11.3333"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4.66602 6.00008L7.99935 2.66675L11.3327 6.00008"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8 2.66675V10.6667"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_312_7133">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
<path
d="M2.66602 11.3333V12.6666C2.66602 13.0202 2.80649 13.3593 3.05654 13.6094C3.30659 13.8594 3.64573 13.9999 3.99935 13.9999H11.9993C12.353 13.9999 12.6921 13.8594 12.9422 13.6094C13.1922 13.3593 13.3327 13.0202 13.3327 12.6666V11.3333"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4.66602 6.00008L7.99935 2.66675L11.3327 6.00008"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8 2.66675V10.6667"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span>آپلود</span>
@ -60,7 +54,7 @@
<div class="logo-sample">
<div class="logo-sample-title">
<h2>نمونه</h2>
<span>به این ترتیب لوگوی تیم شما در اتاقهای شما به نظر میرسد.</span>
<span>نمایش لوگوی تیم شما در اتاقها به این شکل خواهد بود.</span>
</div>
<div class="sample-logos">
<img
@ -72,6 +66,8 @@
</div>
</div>
</div>
<!-- Team Info Section -->
<div class="team-info">
<div class="card-title">
<h2>جزئیات تیم</h2>
@ -85,15 +81,7 @@
/>
</div>
<div class="form-group">
<label for="company_name">نام شرکت</label>
<input
id="company_name"
type="text"
v-model="form.companyName"
/>
</div>
<div class="form-group">
<label for="type_activity">نوع فعالیت شرکت</label>
<label for="type_activity">نوع فعالیت</label>
<input
id="type_activity"
type="text"
@ -116,7 +104,6 @@ export default {
return {
form: {
teamName: '',
companyName: '',
activityType: '',
teamId: null,
},
@ -131,93 +118,75 @@ export default {
};
},
created() {
// دریافت اطلاعات تیم در زمان ایجاد کامپوننت
this.fetchTeamData();
},
methods: {
/* دریافت اطلاعات تیم از API */
async fetchTeamData() {
try {
const token = localStorage.getItem('token');
if (!token) throw new Error('توکن احراز هویت یافت نشد.');
const response = await axios.get(`${this.baseUrl}/get_team`, {
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'application/json',
},
headers: { Authorization: `Token ${token}`, 'Content-Type': 'application/json' },
});
const team = response.data.teams[0];
if (team) {
this.form.teamName = team.name || '';
this.form.activityType = team.description || '';
this.form.teamId = team.id;
this.teamLogo = team.logo ? `${this.baseUrl}${team.logo}` : null;
} else {
alert('هیچ اطلاعاتی برای تیم یافت نشد.');
}
} catch (error) {
alert('خطا در بارگذاری اطلاعات تیم. لطفاً دوباره تلاش کنید.');
} catch {
alert('خطا در بارگذاری اطلاعات تیم.');
}
},
handleLogoUpload(event) {
const file = event.target.files[0];
if (file) {
this.teamLogo = URL.createObjectURL(file);
this.uploadedLogoFile = file;
this.uploadedLogoFile = event.target.files[0];
if (this.uploadedLogoFile) {
this.teamLogo = URL.createObjectURL(this.uploadedLogoFile);
}
},
/* ارسال فرم برای به‌روزرسانی اطلاعات تیم */
async submitForm() {
const hasFormData = this.form.teamName || this.form.companyName || this.form.activityType;
const hasLogo = !!this.uploadedLogoFile;
if (!hasFormData && !hasLogo) {
alert('لطفاً حداقل یک فیلد یا لوگو را وارد کنید.');
if (!this.form.teamName && !this.form.activityType && !this.uploadedLogoFile) {
alert('لطفاً حداقل یک فیلد یا لوگو وارد کنید.');
return;
}
const formData = new FormData();
if (this.form.teamName) formData.append('name', this.form.teamName);
if (this.form.companyName) formData.append('company_name', this.form.companyName);
if (this.form.activityType) formData.append('description', this.form.activityType);
if (this.uploadedLogoFile) formData.append('logo', this.uploadedLogoFile);
try {
const formData = new FormData();
if (this.form.teamName) formData.append('name', this.form.teamName);
if (this.form.activityType) formData.append('description', this.form.activityType);
if (this.uploadedLogoFile) formData.append('logo', this.uploadedLogoFile);
const token = localStorage.getItem('token');
if (!token) throw new Error('توکن احراز هویت یافت نشد.');
await axios.patch(`${this.baseUrl}/update_team/${this.form.teamId}/`, formData, {
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'multipart/form-data',
},
headers: { Authorization: `Token ${token}`, 'Content-Type': 'multipart/form-data' },
});
this.$emit('update:teamData', {
this.$emit('update:team-data', {
teamName: this.form.teamName,
companyName: this.form.companyName,
activityType: this.form.activityType,
teamLogo: this.uploadedLogoFile,
});
alert('اطلاعات تیم با موفقیت به‌روزرسانی شد');
// ریست فرم و لوگو
this.form.teamName = '';
this.form.companyName = '';
this.form.activityType = '';
this.teamLogo = null;
this.uploadedLogoFile = null;
const fileInput = this.$refs.fileUpload;
if (fileInput) {
fileInput.value = '';
}
alert('اطلاعات تیم با موفقیت به‌روزرسانی شد.');
this.resetForm();
await this.fetchTeamData();
} catch (error) {
alert('خطا در به‌روزرسانی اطلاعات تیم. لطفاً دوباره تلاش کنید.');
} catch {
alert('خطا در به‌روزرسانی اطلاعات تیم.');
}
},
resetForm() {
this.form.teamName = '';
this.form.activityType = '';
this.teamLogo = null;
this.uploadedLogoFile = null;
if (this.$refs.fileUpload) {
this.$refs.fileUpload.value = '';
}
},
},
};
</script>
<style scoped>
.tab-content {
display: flex;

View File

@ -1,9 +1,14 @@
<template>
<div class="card license-card">
<span>لایسنسهای قابل استفاده: {{ remainingCapacity }}</span>
<div class="buy-subscription" @click="goToBuySubscription">
<p>خرید اشتراک</p>
<span>
<div>
<!-- License Info -->
<div class="card license-card">
<span>لایسنسهای قابل استفاده: {{ remainingCapacity }}</span>
<div
v-if="!hasActiveSubscription"
class="buy-subscription"
@click="goToBuySubscription"
>
<p>خرید اشتراک</p>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@ -26,30 +31,29 @@
stroke-linejoin="round"
/>
</svg>
</span>
</div>
</div>
</div>
<div class="user-cards">
<div class="user-card" v-for="(user, index) in userList" :key="index">
<div class="user-card-header">
<img :src="user.avatar" class="user-avatar" alt="avatar" />
<div class="user-info-box">
<div class="user-info-tags">
<div class="user-name">{{ user.name }}</div>
<div class="user-email">{{ user.email }}</div>
</div>
<div class="user-activity">
<div class="user-role">{{ user.role }}</div>
<div class="user-version">{{ user.version }}</div>
<!-- User List -->
<div class="user-cards">
<div v-for="(user, index) in userList" :key="index" class="user-card">
<div class="user-card-header">
<img :src="user.avatar" class="user-avatar" alt="avatar" />
<div class="user-info-box">
<div class="user-info-tags">
<div class="user-name">{{ user.name }}</div>
<div class="user-email">{{ user.email }}</div>
</div>
<div class="user-activity">
<div class="user-role">{{ user.role }}</div>
<div class="user-version">{{ user.version }}</div>
</div>
</div>
</div>
</div>
<div class="user-footer">
<span>اکانت XRoom</span>
<div class="user-actions">
<button>
<i class="icon">
<div class="user-footer">
<span>اکانت XRoom</span>
<div class="user-actions">
<button>
<svg
xmlns="http://www.w3.org/2000/svg"
width="25"
@ -86,10 +90,8 @@
stroke-linejoin="round"
/>
</svg>
</i>
</button>
<button>
<i class="icon">
</button>
<button>
<svg
xmlns="http://www.w3.org/2000/svg"
width="25"
@ -133,59 +135,55 @@
stroke-linejoin="round"
/>
</svg>
</i>
</button>
</button>
</div>
</div>
</div>
<!-- Add User Card -->
<div class="user-card add-card" @click="openAddUserModal">
<span class="add-text">
<span style="font-size: 23px; margin-left: 0.5rem;">+</span>
اضافه کردن کاربر جدید
</span>
</div>
</div>
<div class="user-card add-card" @click="openAddUserModal">
<span class="add-text">
<span style="font-size: 23px; margin-left: 0.5rem;">+</span> اضافه کردن کاربر جدید
</span>
</div>
</div>
<!-- Modal -->
<AddUserModal
:isVisible="isAddUserModalVisible"
@close="closeAddUserModal"
@add-user="submitNewUser"
/>
<!-- Add User Modal -->
<AddUserModal
:is-visible="isAddUserModalVisible"
@close="closeAddUserModal"
@add-user="submitNewUser"
/>
</div>
</template>
<script>
import AddUserModal from '@/components/AddUserModal.vue';
export default {
name: 'UsersTab',
components: {
AddUserModal,
},
name: 'TeamUser',
components: { AddUserModal },
props: {
userList: {
type: Array,
default: () => [],
},
teamMemberCapacity: {
type: Number,
default: 0,
},
subscriptionCount: {
type: Number,
default: 0,
},
userList: { type: Array, default: () => [] },
teamMemberCapacity: { type: Number, default: 0 },
subscriptionCount: { type: Number, default: 0 },
hasActiveSubscription: { type: Boolean, default: false },
},
data() {
return {
isAddUserModalVisible: false,
};
},
computed: {
remainingCapacity() {
const capacity = this.subscriptionCount - this.teamMemberCapacity;
return capacity;
return this.subscriptionCount - this.teamMemberCapacity;
},
},
methods: {
openAddUserModal() {
if (this.remainingCapacity <= 0) {
alert('ظرفیت تیم پر شده است. لطفاً اشتراک جدیدی خریداری کنید.');
this.goToBuySubscription();
this.$emit('change-tab', 'buy-subscription');
alert('اشتراک فعالی ندارید , لطفا اشتراک تهیه نمایید.');
return;
}
this.isAddUserModalVisible = true;
@ -201,15 +199,9 @@ export default {
this.$emit('change-tab', 'buy-subscription');
},
},
data() {
return {
isAddUserModalVisible: false,
};
},
};
</script>
<style scoped>
/* User Info Section */
.user-info {
@ -283,6 +275,10 @@ export default {
color: #3a57e8;
font-weight: 600;
font-size: 17px;
white-space: nowrap;
text-overflow: ellipsis;
width: 110px;
overflow-x: clip;
}
.buy-subscription {

View File

@ -1,6 +1,5 @@
<template>
<div>
<!-- Description -->
<div class="section-description">
<div class="section-title">مدیریت جلسات</div>
<p class="title-description">
@ -8,7 +7,6 @@
</p>
</div>
<!-- Meeting Section -->
<div class="meeting-section">
<div class="meeting-filters">
<div class="search-section">
@ -65,14 +63,13 @@
</div>
</div>
<!-- Meet Discover -->
<div :class="filteredMeetings.length === 0 ? 'meet-discover' : 'meetings-container'">
<span class="discover-result" v-if="filteredMeetings.length === 0">
هیچ جلسهای یافت نشد. با کلیک کردن، یک جلسه جدید ایجاد کنید
</span>
<div v-else class="meetings-list">
<div v-for="meeting in filteredMeetings" :key="meeting.id" class="meeting-item">
<img :src="meeting.image" alt="Meeting Image" class="meeting-image" width="120px" height="120px" />
<img :src="meeting.image" alt="Meeting Image" class="meeting-image" width="120" height="120" />
<div class="meeting-details" style="margin-right: 10px;">
<h3 class="meet-title">{{ meeting.title }}</h3>
<p class="meet-capacity">
@ -120,7 +117,6 @@
</div>
</div>
<!-- Create Meeting Modal -->
<CreateMeetingModal
:is-open="showModal"
@create-meeting="createNewMeeting"
@ -133,11 +129,11 @@
import CreateMeetingModal from '@/components/CreateMeetingModal.vue';
import axios from 'axios';
const API_BASE_URL = 'http://my.xroomapp.com:8000';
export default {
name: 'DashboardPage',
components: {
CreateMeetingModal,
},
name: 'Meetings',
components: { CreateMeetingModal },
data() {
return {
searchQuery: '',
@ -179,15 +175,14 @@ export default {
filterMeetings() {
let filtered = this.meetings;
if (this.searchQuery) {
filtered = filtered.filter(
(meeting) =>
meeting.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
meeting.type.toLowerCase().includes(this.searchQuery.toLowerCase())
filtered = filtered.filter((meeting) =>
[meeting.title, meeting.type].some((field) =>
field.toLowerCase().includes(this.searchQuery.toLowerCase())
)
);
}
if (this.activeFilter === 'future') {
const now = new Date();
filtered = filtered.filter((meeting) => new Date(meeting.date) > now);
filtered = filtered.filter((meeting) => new Date(meeting.date) > new Date());
}
this.filteredMeetings = filtered;
},
@ -197,59 +192,40 @@ export default {
},
async refreshToken() {
try {
const response = await axios.post('http://my.xroomapp.com:8000/refresh_token', {
const response = await axios.post(`${API_BASE_URL}/refresh_token`, {
refresh_token: localStorage.getItem('refresh_token'),
});
const newToken = response.data.access_token;
localStorage.setItem('token', newToken);
return newToken;
} catch (error) {
console.error('خطا در refresh توکن:', error);
alert('لطفاً دوباره وارد شوید');
window.location.href = '/login';
return null;
throw error;
}
},
async createNewMeeting(meetingData) {
try {
console.log('داده‌های ارسالی به API:', JSON.stringify(meetingData, null, 2));
let token = localStorage.getItem('token');
console.log('توکن اولیه:', token);
if (!token) {
throw new Error('توکن احراز هویت پیدا نشد');
}
if (!token) throw new Error('توکن احراز هویت پیدا نشد');
let response = await axios.post(
'http://my.xroomapp.com:8000/add_meeting',
meetingData,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token.trim()}`,
},
}
).catch(async (error) => {
if (error.response && error.response.status === 403) {
console.log('تلاش برای refresh توکن...');
const config = {
headers: {
'Content-Type': 'application/json',
Authorization: `Token ${token.trim()}`,
},
};
let response = await axios.post(`${API_BASE_URL}/add_meeting`, meetingData, config).catch(async (error) => {
if (error.response?.status === 403) {
token = await this.refreshToken();
if (token) {
return await axios.post(
'http://my.xroomapp.com:8000/add_meeting',
meetingData,
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Token ${token.trim()}`,
},
}
);
}
return axios.post(`${API_BASE_URL}/add_meeting`, meetingData, {
headers: { ...config.headers, Authorization: `Token ${token.trim()}` },
});
}
throw error;
});
console.log('پاسخ API:', response.data);
const newMeeting = {
id: response.data.meeting.id,
title: response.data.meeting.name,
@ -264,37 +240,14 @@ export default {
this.showModal = false;
alert('جلسه با موفقیت ایجاد شد!');
} catch (error) {
console.error('خطا در ارسال درخواست به API:', error);
if (error.response) {
console.error('جزئیات پاسخ سرور:', error.response.data);
alert(`خطایی در ایجاد جلسه رخ داد: ${error.response.data.message || error.message}`);
} else {
alert(`خطایی در ایجاد جلسه رخ داد: ${error.message}`);
}
alert(`خطایی در ایجاد جلسه رخ داد: ${error.response?.data?.message || error.message}`);
}
},
},
};
</script>
<style scoped>
/* .dashboard-page {
margin-right: 360px;
padding: 20px;
direction: rtl;
font-family: IRANSansXFaNum, sans-serif;
}
.content {
background-color: #f8f9fa;
border-radius: 20px;
padding: 35px 80px;
display: flex;
flex-direction: column;
gap: 32px;
}
*/
.section-title {
font-size: 20px;

View File

@ -1,72 +1,66 @@
<template>
<div>
<!-- Description -->
<div class="section-description">
<div class="section-title">مدیریت اعضا</div>
<p>
در این بخش به شما امکان میدهد تا اتاقها، فایلها و جلسات را با همکاران خود به اشتراک بگذارید. در این بخش میتوانید تیم خود را مدیریت کنید.
</p>
</div>
<!-- Tab Buttons -->
<div class="tab-buttons">
<button
:class="['tab-btn', activeTab === 'users' ? 'active' : '']"
@click="activeTab = 'users'"
>
کاربران
</button>
<button
:class="['tab-btn', activeTab === 'buy-subscription' ? 'active' : '']"
@click="activeTab = 'buy-subscription'"
>
خرید اشتراک
</button>
<button
:class="['tab-btn', activeTab === 'membership' ? 'active' : '']"
@click="activeTab = 'membership'"
>
اشتراک ها
</button>
<button
:class="['tab-btn', activeTab === 'details' ? 'active' : '']"
@click="activeTab = 'details'"
>
جزئیات
</button>
</div>
<!-- Tab Content -->
<div v-if="activeTab === 'users'">
<TeamUser
:userList="userList"
:teamMemberCapacity="teamMemberCapacity"
:subscriptionCount="subscriptionCount"
@add-user="submitNewUser"
@change-tab="changeTab"
/>
</div>
<div v-if="activeTab === 'membership'">
<Membership
:subscriptionCount="subscriptionCount"
:teamMemberCapacity="teamMemberCapacity"
:isBillingModalVisible="isBillingModalVisible"
@change-tab="changeTab"
@update:isBillingModalVisible="isBillingModalVisible = $event"
/>
</div>
<div v-if="activeTab === 'details'">
<TeamDetails @update:teamData="handleTeamData" />
</div>
<div v-if="activeTab === 'buy-subscription'">
<BuySubscription
:memberCount="memberCount"
:availableMemberOptions="availableMemberOptions"
:baseUrl="baseUrl"
@update:memberCount="memberCount = $event"
@plan-selected="selectedPlan = $event"
@payment-success="handlePaymentSuccess"
/>
</div>
<div>
<!-- Section Description -->
<div class="section-description">
<div class="section-title">مدیریت اعضا</div>
<p>در این بخش میتوانید اتاقها، فایلها و جلسات را با همکاران خود به اشتراک بگذارید و تیم خود را مدیریت کنید.</p>
</div>
<!-- Tab Buttons -->
<div class="tab-buttons">
<button
:class="['tab-btn', { active: activeTab === 'users' }]"
@click="activeTab = 'users'"
>کاربران</button>
<button
:class="['tab-btn', { active: activeTab === 'buy-subscription' }]"
@click="activeTab = 'buy-subscription'"
>خرید اشتراک</button>
<button
:class="['tab-btn', { active: activeTab === 'membership' }]"
@click="activeTab = 'membership'"
>اشتراکها</button>
<button
:class="['tab-btn', { active: activeTab === 'details' }]"
@click="activeTab = 'details'"
>جزئیات</button>
</div>
<!-- Tab Content -->
<div v-if="activeTab === 'users'">
<TeamUser
:user-list="userList"
:team-member-capacity="teamMemberCapacity"
:subscription-count="subscriptionCount"
:has-active-subscription="hasActiveSubscription"
@add-user="submitNewUser"
@change-tab="changeTab"
/>
</div>
<div v-if="activeTab === 'membership'">
<Membership
:subscription-count="subscriptionCount"
:team-member-capacity="teamMemberCapacity"
:is-billing-modal-visible="isBillingModalVisible"
@change-tab="changeTab"
@update:is-billing-modal-visible="isBillingModalVisible = $event"
/>
</div>
<div v-if="activeTab === 'details'">
<TeamDetails @update:team-data="handleTeamData" />
</div>
<div v-if="activeTab === 'buy-subscription'">
<BuySubscription
:member-count="memberCount"
:available-member-options="availableMemberOptions"
:base-url="baseUrl"
:has-active-subscription="hasActiveSubscription"
:has-expired-subscription="hasExpiredSubscription"
@update:member-count="memberCount = $event"
@payment-success="handlePaymentSuccess"
/>
</div>
</div>
</template>
<script>
@ -77,160 +71,127 @@ import TeamDetails from '@/components/TeamDetails.vue';
import axios from 'axios';
export default {
name: 'DashboardPage',
components: {
TeamUser,
BuySubscription,
Membership,
TeamDetails,
},
name: 'Team',
components: { TeamUser, BuySubscription, Membership, TeamDetails },
data() {
return {
isBillingModalVisible: false,
activeTab: 'users',
userList: [],
memberCount: 5,
availableMemberOptions: [5, 10, 20, 100],
selectedPlan: null,
userList: [],
activeTab: 'users',
previewUrl: '',
currentPreviewIndex: null,
currentPreviewType: null,
videoOptions: {
autoplay: false,
controls: true,
sources: [
{
type: 'video/mp4',
src: '',
},
],
},
userData: {
customer: {},
user: {
first_name: '',
last_name: '',
},
images: [],
pdfs: [],
videos: [],
glbs: [],
subscription: null,
},
newFileName: '',
selectedFile: null,
uploading: false,
baseUrl: 'http://194.62.43.230:8000',
currentUploadType: 'image',
dialogTitle: 'آپلود فایل جدید',
fileAccept: '*/*',
teamMemberCapacity: 0,
subscriptionCount: 0,
hasActiveSubscription: false,
hasExpiredSubscription: false, // جدید: بررسی اشتراک منقضیشده
subscriptionEndTime: null, // جدید: ذخیره تاریخ انقضای اشتراک
teamId: null,
subscriptionId: null,
isBillingModalVisible: false,
baseUrl: 'http://my.xroomapp.com:8000',
};
},
created() {
this.fetchUserData();
this.fetchTeamMemberInfo();
this.fetchTeamData();
const tab = this.$route.query.tab;
if (tab) {
this.activeTab = tab;
}
this.initializeData();
},
methods: {
async initializeData() {
const tab = this.$route.query.tab;
if (tab) this.activeTab = tab;
await Promise.all([
this.fetchUserData(),
this.fetchTeamMemberInfo(),
this.fetchTeamData(),
]);
},
changeTab(tabName) {
this.activeTab = tabName;
},
async fetchTeamData() {
try {
const token = localStorage.getItem('token');
const response = await axios.get('http://my.xroomapp.com:8000/get_team', {
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'application/json',
},
});
const response = await this.axiosGet('/get_team');
const team = response.data.teams[0];
if (team) {
this.teamId = team.id;
} else {
this.teamId = null;
}
this.teamId = team?.id || null;
} catch (error) {
alert('خطا در بارگذاری اطلاعات تیم. لطفاً دوباره تلاش کنید.');
console.error('Error fetching team data:', error);
alert('خطا در بارگذاری اطلاعات تیم.');
}
},
async handlePaymentSuccess() {
await this.fetchTeamMemberInfo();
await this.fetchUserData();
this.activeTab = 'membership';
},
async fetchTeamMemberInfo() {
try {
const token = localStorage.getItem('token');
const response = await axios.get(`${this.baseUrl}/get_all_team_members`, {
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'application/json',
},
});
this.userList = response.data.members.map((member) => ({
name: `${member.first_name} ${member.last_name}`,
email: member.username,
role: 'کاربر',
const response = await this.axiosGet('/get_all_team_members');
this.userList = response.data.members.map(member => ({
name: `${member.user.first_name} ${member.user.last_name}`,
email: member.user.username,
role: member.semat || 'کاربر',
version: 'نسخه آزمایشی',
avatar: 'https://models.readyplayer.me/681f59760bc631a87ad25172.png',
avatar: member.profile_img || 'https://models.readyplayer.me/681f59760bc631a87ad25172.png',
}));
this.teamMemberCapacity = response.data.members.length;
} catch (error) {
alert('خطا در بارگذاری اطلاعات اعضای تیم. لطفاً دوباره تلاش کنید.');
console.error('Error fetching team members:', error);
alert('خطا در بارگذاری اطلاعات اعضای تیم.');
}
},
async fetchUserData() {
try {
const token = localStorage.getItem('token');
const response = await axios.get(`${this.baseUrl}/getInfo`, {
headers: {
Authorization: `Token ${token}`,
},
});
this.userData = response.data;
if (this.userData.data.subscription) {
this.subscriptionCount = this.userData.data.subscription || 0;
} else {
this.subscriptionCount = 0;
}
const response = await this.axiosGet('/get_user_subscriptions');
const subscriptions = response.data.subscriptions || [];
this.subscriptionCount = subscriptions.reduce((total, sub) => total + sub.user_count, 0);
this.subscriptionId = subscriptions[0]?.id || null;
this.subscriptionEndTime = subscriptions[0]?.endTime || null; // جدید: تاریخ انقضا
const now = new Date();
const isExpiredByTime = this.subscriptionEndTime && new Date(this.subscriptionEndTime) < now;
const isExpiredByCapacity = this.subscriptionCount <= this.teamMemberCapacity;
this.hasActiveSubscription = subscriptions.length > 0 && !isExpiredByTime && !isExpiredByCapacity;
this.hasExpiredSubscription = subscriptions.length > 0 && (isExpiredByTime || isExpiredByCapacity);
} catch (error) {
alert('خطا در بارگذاری اطلاعات کاربر. لطفاً دوباره تلاش کنید.');
console.error('Error fetching user data:', error);
alert('خطا در بارگذاری اطلاعات اشتراک.');
}
},
async handlePaymentSuccess({ subscriptionId }) {
try {
this.subscriptionId = subscriptionId;
await Promise.all([
this.fetchUserData(),
this.fetchTeamMemberInfo(),
this.fetchTeamData(),
]);
if (!this.teamId && this.subscriptionId) {
await this.createTeam();
alert('اشتراک و تیم به درستی ساخته شد.');
} else if (this.teamId) {
alert('تیم از قبل وجود دارد.');
}
this.activeTab = 'membership';
} catch (error) {
console.error('Error handling payment success:', error);
alert('خطا در پردازش پرداخت یا ساخت تیم.');
}
},
async createTeam() {
const teamData = {
name: 'تیم 1',
description: 'فعالیت',
max_persons: this.subscriptionCount.toString(),
subscriptionId: this.subscriptionId,
};
await this.axiosPost('/add_team', teamData);
await this.fetchTeamData();
},
async submitNewUser(newUser) {
const remainingCapacity = this.subscriptionCount - this.teamMemberCapacity;
if (remainingCapacity <= 0) {
alert('ظرفیت تیم پر شده است. لطفاً اشتراک جدیدی خریداری کنید.');
if (this.subscriptionCount - this.teamMemberCapacity <= 0) {
alert('اشتراک فعالی ندارید , اشتراک تهیه نمایید.');
this.activeTab = 'buy-subscription';
return;
}
if (!this.teamId) {
alert('خطا: اطلاعات تیم یافت نشد. لطفاً دوباره تلاش کنید.');
alert('خطا: اطلاعات تیم یافت نشد.');
return;
}
try {
const token = localStorage.getItem('token');
await axios.post(
'http://my.xroomapp.com:8000/add_teamMember/',
{
...newUser,
teamId: this.teamId,
},
{
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'application/json',
},
}
);
await this.axiosPost('/add_teamMember/', { ...newUser, teamId: this.teamId });
this.userList.push({
...newUser,
avatar: 'https://models.readyplayer.me/681f59760bc631a87ad25172.png',
@ -239,227 +200,39 @@ export default {
});
this.teamMemberCapacity++;
await this.fetchTeamMemberInfo();
alert('کاربر با موفقیت اضافه شد');
alert('کاربر با موفقیت اضافه شد.');
} catch (error) {
alert('خطا در اضافه کردن کاربر. لطفاً دوباره تلاش کنید.');
console.error('Error adding user:', error);
alert('خطا در اضافه کردن کاربر.');
}
},
handleBackdropClick(event) {
if (event.target === this.$refs.filePreviewDialog) {
this.closePreviewDialog();
}
handleTeamData(data) {
console.log('Team data updated:', data);
},
openPreviewDialog(type, index, url) {
if (type === 'video') {
this.videoOptions.sources[0].src = url;
this.$nextTick(() => {
this.$refs.filePreviewDialog?.showModal();
});
}
if (!this.$refs.filePreviewDialog) {
return;
}
this.currentPreviewType = type;
this.currentPreviewIndex = index;
this.previewUrl = url;
if (type === 'video') {
this.videoOptions.sources[0].src = url;
this.videoOptions.poster = this.getVideoThumbnail();
this.previewUrl = url;
} else {
this.previewUrl = url;
}
this.$nextTick(() => {
this.$refs.filePreviewDialog?.showModal();
async axiosGet(endpoint) {
const token = localStorage.getItem('token');
if (!token) throw new Error('توکن احراز هویت یافت نشد.');
return await axios.get(`${this.baseUrl}${endpoint}`, {
headers: { Authorization: `Token ${token}`, 'Content-Type': 'application/json' },
});
if (type === 'image') {
this.previewImageUrl = url;
this.previewPdfUrl = '';
} else if (type === 'pdf') {
this.previewPdfUrl = url;
this.previewImageUrl = '';
}
this.$refs.filePreviewDialog.showModal();
},
getVideoThumbnail() {
return 'https://cdn-icons-png.flaticon.com/512/2839/2839038.png';
},
closePreviewDialog() {
const dialog = this.$refs.filePreviewDialog;
if (dialog && typeof dialog.close === 'function') {
dialog.close();
}
this.previewUrl = '';
this.currentPreviewIndex = null;
this.currentPreviewType = null;
},
async downloadFile() {
const url =
this.currentPreviewType === 'image' ? this.previewImageUrl : this.previewPdfUrl;
if (!url) return;
try {
const response = await fetch(url);
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
if (this.currentPreviewType === 'image') {
a.download = `image-${new Date().getTime()}.${url.split('.').pop()}`;
} else if (this.currentPreviewType === 'pdf') {
a.download = `document-${new Date().getTime()}.pdf`;
}
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
} catch (error) {
alert('خطا در دانلود فایل');
}
},
async deleteFile() {
if (this.currentPreviewIndex === null || !this.currentPreviewType) return;
try {
const token = localStorage.getItem('token');
let deleteUrl = '';
let itemId = '';
let fileArray = [];
switch (this.currentPreviewType) {
case 'image':
fileArray = this.userData.images;
itemId = fileArray[this.currentPreviewIndex].id;
deleteUrl = `${this.baseUrl}/deleteImage/${itemId}/`;
break;
case 'pdf':
fileArray = this.userData.pdfs;
itemId = fileArray[this.currentPreviewIndex].id;
deleteUrl = `${this.baseUrl}/deletePdf/${itemId}/`;
break;
case 'video':
fileArray = this.userData.videos;
itemId = fileArray[this.currentPreviewIndex].id;
deleteUrl = `${this.baseUrl}/deleteVideo/${itemId}/`;
break;
case 'glb':
fileArray = this.userData.glbs;
itemId = fileArray[this.currentPreviewIndex].id;
deleteUrl = `${this.baseUrl}/deleteGlb/${itemId}/`;
break;
}
await axios.delete(deleteUrl, {
headers: {
Authorization: `Token ${token}`,
},
});
this.closePreviewDialog();
await this.fetchUserData();
alert('فایل با موفقیت حذف شد');
} catch (error) {
alert('خطا در حذف فایل');
}
},
getFullImageUrl(relativePath) {
if (!relativePath) return '';
return `${this.baseUrl}${relativePath}`;
},
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR');
},
openDialog(type) {
this.currentUploadType = type;
switch (type) {
case 'image':
this.dialogTitle = 'آپلود تصویر جدید';
this.fileAccept = 'image/*';
break;
case 'pdf':
this.dialogTitle = 'آپلود فایل PDF';
this.fileAccept = '.pdf';
break;
case 'video':
this.dialogTitle = 'آپلود ویدیو';
this.fileAccept = 'video/*';
break;
case 'glb':
this.dialogTitle = 'آپلود مدل 3D';
this.fileAccept = '.glb';
break;
}
this.$refs.newFileDialog.showModal();
},
closeDialog() {
this.newFileName = '';
this.selectedFile = null;
this.$refs.newFileDialog.close();
},
handleFileChange(event) {
this.selectedFile = event.target.files[0];
},
async uploadFile() {
if (!this.selectedFile) {
return;
}
this.uploading = true;
const formData = new FormData();
formData.append('name', this.newFileName || this.selectedFile.name);
switch (this.currentUploadType) {
case 'image':
formData.append('image', this.selectedFile);
break;
case 'pdf':
formData.append('pdf', this.selectedFile);
break;
case 'video':
formData.append('video', this.selectedFile);
break;
case 'glb':
formData.append('glb', this.selectedFile);
break;
}
try {
const token = localStorage.getItem('token');
let uploadUrl = '';
switch (this.currentUploadType) {
case 'image':
uploadUrl = `${this.baseUrl}/uploadImage/`;
break;
case 'pdf':
uploadUrl = `${this.baseUrl}/uploadPdf/`;
break;
case 'video':
uploadUrl = `${this.baseUrl}/uploadVideo/`;
break;
case 'glb':
uploadUrl = `${this.baseUrl}/uploadGlb/`;
break;
}
await axios.post(uploadUrl, formData, {
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'multipart/form-data',
},
});
this.closeDialog();
await this.fetchUserData();
alert('فایل با موفقیت آپلود شد');
} catch (error) {
alert('خطا در آپلود فایل');
} finally {
this.uploading = false;
}
async axiosPost(endpoint, data) {
const token = localStorage.getItem('token');
if (!token) throw new Error('توکن احراز هویت یافت نشد.');
return await axios.post(`${this.baseUrl}${endpoint}`, data, {
headers: { Authorization: `Token ${token}`, 'Content-Type': 'application/json' },
});
},
},
watch: {
'$route.query.tab'(newTab) {
if (newTab) {
this.activeTab = newTab;
}
if (newTab) this.activeTab = newTab;
},
},
};
</script>
<style scoped>
.section-title {