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

View File

@ -1,9 +1,9 @@
<template> <template>
<div v-if="isOpen && !isRoomSelectionOpen" class="modal-overlay" @click="closeModal"> <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"> <div class="popUp-header">
<h2>ایجاد جلسه جدید</h2> <h2>ایجاد جلسه جدید</h2>
<button @click="closeModal"> <button @click="closeModalByButton">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="35" width="35"
@ -38,25 +38,16 @@
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<div class="form-group"> <div class="form-group">
<label for="meetingTitle">نام جلسه</label> <label for="meetingTitle">نام جلسه</label>
<input <input type="text" id="meetingTitle" v-model="form.title" required />
type="text"
id="meetingTitle"
v-model="form.title"
required
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="meet-description">شرح جلسه</label> <label for="meet-description">شرح جلسه</label>
<textarea <textarea name="meet-description" id="meet-description" v-model="form.description"></textarea>
name="meet-description"
id="meet-description"
v-model="form.description"
></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="meetingDate">روز</label> <label for="meetingDate">روز</label>
<div class="input-group"> <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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="18" width="18"
@ -95,8 +86,8 @@
:auto-submit="true" :auto-submit="true"
input-class="form-control" input-class="form-control"
id="meetingDate" id="meetingDate"
required
style="border-radius: 0 8px 8px 0; text-align: center; position: relative;" style="border-radius: 0 8px 8px 0; text-align: center; position: relative;"
required
/> />
</div> </div>
</div> </div>
@ -286,13 +277,13 @@
<div class="form-group"> <div class="form-group">
<label style="font-size: 19px; font-weight: 600;">اتاق جلسه</label> <label style="font-size: 19px; font-weight: 600;">اتاق جلسه</label>
<div class="rooms-selecter"> <div class="rooms-selecter">
<span>{{ form.selectedRoom ? '0 اتاق انتخاب شده' : '0 اتاق انتخاب شده' }}</span> <span>{{ form.selectedRoom ? '1 اتاق انتخاب شده' : '0 اتاق انتخاب شده' }}</span>
<button type="button" @click="openRoomSelection" style="cursor: pointer;">انتخاب اتاق جلسه</button> <button type="button" @click="openRoomSelection">انتخاب اتاق جلسه</button>
</div> </div>
</div> </div>
<div class="participants-objects"> <div class="participants-objects">
<h2>شرکت کنندگان</h2> <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 class="participants-guide">
میتوانید به مجری اجازه بدهید تا ابزارهایی برای مدیریت این جلسه و همچنین ابزارهایی برای مدیریت مجوزها در طول جلسه به او بدهد. میتوانید به مجری اجازه بدهید تا ابزارهایی برای مدیریت این جلسه و همچنین ابزارهایی برای مدیریت مجوزها در طول جلسه به او بدهد.
</span> </span>
@ -304,13 +295,13 @@
</div> </div>
<div class="user-info"> <div class="user-info">
<p class="user-name">{{ fullName }}</p> <p class="user-name">{{ fullName }}</p>
<span>{{ userPhone || 'شماره تلفن موجود نیست' }}</span> <span>{{ userPhone }}</span>
</div> </div>
</div> </div>
<p class="presenter-role">{{ userRole }}</p> <p class="presenter-role">{{ userRole }}</p>
</div> </div>
<div class="presenter" v-for="participant in participants" :key="participant.phone"> <div class="presenter" v-for="participant in participants" :key="participant.id">
<div style="display: flex;align-items: center;height: 100%;"> <div style="display: flex; align-items: center; height: 100%;">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img class="user-avatar" :src="participant.profile_img || defaultProfileIcon" /> <img class="user-avatar" :src="participant.profile_img || defaultProfileIcon" />
</div> </div>
@ -320,7 +311,7 @@
</div> </div>
</div> </div>
<p class="presenter-role">{{ participant.role }}</p> <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"> <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" fill="white"/>
<rect x="0.5" y="0.5" width="31" height="31" rx="7.5" stroke="#E2DEE9"/> <rect x="0.5" y="0.5" width="31" height="31" rx="7.5" stroke="#E2DEE9"/>
@ -330,21 +321,55 @@
</button> </button>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="participantPhone">اضافه کردن شرکت کننده</label> <label for="participantInput">اضافه کردن شرکت کننده</label>
<div class="participant-input"> <div class="participant-input">
<input <div class="custom-input" @click="toggleDropdown" ref="customInput">
type="tel" <span v-if="!selectedParticipantId">یک عضو تیم انتخاب کنید</span>
id="participantPhone" <span v-else>{{ getParticipantName(selectedParticipantId) }}</span>
v-model="newParticipantPhone" <svg
placeholder="لطفا شماره تلفن شرکت کننده را وارد کنید" class="dropdown-icon"
@keyup.enter="addParticipant" :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"
/> />
<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"/>
</svg> </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>
</div> </div>
</form> </form>
@ -353,7 +378,7 @@
</span> </span>
</div> </div>
<div class="form-actions"> <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> <button type="button" class="submit-button" @click="handleSubmit">ایجاد جلسه</button>
</div> </div>
</div> </div>
@ -369,21 +394,19 @@
import VuePersianDatetimePicker from 'vue3-persian-datetime-picker'; import VuePersianDatetimePicker from 'vue3-persian-datetime-picker';
import moment from 'moment-jalaali'; import moment from 'moment-jalaali';
import RoomSelectionModal from './RoomSelectionModal.vue'; import RoomSelectionModal from './RoomSelectionModal.vue';
import axios from 'axios';
const API_BASE_URL = 'http://my.xroomapp.com:8000';
export default { export default {
name: 'MeetingModal', name: 'CreateMeetingModal',
components: { components: { VuePersianDatetimePicker, RoomSelectionModal },
VuePersianDatetimePicker,
RoomSelectionModal,
},
props: { props: {
isOpen: { isOpen: { type: Boolean, default: false },
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
defaultProfileIcon: 'https://models.readyplayer.me/681f59760bc631a87ad25172.png',
form: { form: {
title: '', title: '',
description: '', description: '',
@ -393,27 +416,26 @@ export default {
endHour: 18, endHour: 18,
endMinute: 0, endMinute: 0,
selectedRoom: null, selectedRoom: null,
use_space: false,
}, },
participants: [], participants: [],
newParticipantPhone: '', teamMembers: [],
defaultProfileIcon: 'https://c.animaapp.com/m9nvumalUMfQbN/img/frame.svg', selectedParticipantId: '',
error: null, isDropdownOpen: false,
isRoomSelectionOpen: false, isRoomSelectionOpen: false,
error: null,
}; };
}, },
computed: { computed: {
customer() { customer() {
// دریافت اطلاعات کاربر از localStorage با کلید customer
return JSON.parse(localStorage.getItem('customer') || '{}'); return JSON.parse(localStorage.getItem('customer') || '{}');
}, },
fullName() { fullName() {
const user = JSON.parse(localStorage.getItem('user') || '{}'); const user = JSON.parse(localStorage.getItem('user') || '{}');
return user.first_name && user.last_name return user.first_name && user.last_name ? `${user.first_name} ${user.last_name}` : 'کاربر مهمان';
? `${user.first_name} ${user.last_name}`
: 'کاربر مهمان';
}, },
userPhone() { userPhone() {
return 3; return this.customer.mobile_number || 'شماره تلفن موجود نیست';
}, },
userRole() { userRole() {
return this.customer.semat || 'مجری'; return this.customer.semat || 'مجری';
@ -422,65 +444,122 @@ export default {
return this.customer.profile_img || this.defaultProfileIcon; return this.customer.profile_img || this.defaultProfileIcon;
}, },
userId() { userId() {
const customer = JSON.parse(localStorage.getItem('customer') || '{}'); return this.customer.id || null;
return customer.id || null; },
}
}, },
watch: { watch: {
isOpen(newVal) { isOpen(newVal) {
document.body.style.overflow = newVal && !this.isRoomSelectionOpen ? 'hidden' : '';
if (newVal) { if (newVal) {
document.body.style.overflow = 'hidden'; this.$nextTick(() => {
} else if (!this.isRoomSelectionOpen) { if (this.$refs.modalContent) {
document.body.style.overflow = ''; this.$refs.modalContent.addEventListener('click', this.closeDropdownOnClick);
}
});
} else {
if (this.$refs.modalContent) {
this.$refs.modalContent.removeEventListener('click', this.closeDropdownOnClick);
}
} }
}, },
isRoomSelectionOpen(newVal) { isRoomSelectionOpen(newVal) {
if (newVal) { document.body.style.overflow = newVal ? 'hidden' : '';
document.body.style.overflow = 'hidden'; },
} else if (!this.isOpen) { },
document.body.style.overflow = ''; 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: { methods: {
beforeDestroy() { async fetchTeamMembers() {
document.body.style.overflow = ''; 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() { openRoomSelection() {
this.isRoomSelectionOpen = true; this.isRoomSelectionOpen = true;
}, },
handleRoomSelection(room) { handleRoomSelection(room) {
this.form.selectedRoom = room; this.form.selectedRoom = room;
this.form.use_space = room.use_space;
this.isRoomSelectionOpen = false; 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() { addParticipant() {
if (!this.newParticipantPhone || !this.validatePhone(this.newParticipantPhone)) { if (!this.selectedParticipantId) {
this.error = 'لطفاً شماره تلفن معتبر وارد کنید (مثال: 09123456789)'; this.error = 'لطفاً یک عضو تیم انتخاب کنید.';
return; return;
} }
if ( const selectedMember = this.teamMembers.find((member) => member.user.id === this.selectedParticipantId);
this.participants.some((p) => p.phone === this.newParticipantPhone) || if (!selectedMember) {
this.newParticipantPhone === this.userPhone this.error = 'کاربر انتخاب‌شده یافت نشد.';
) { return;
this.error = 'این شماره تلفن قبلاً اضافه شده است'; }
if (this.participants.some((p) => p.id === this.selectedParticipantId)) {
this.error = 'این کاربر قبلاً اضافه شده است.';
return;
}
if (this.selectedParticipantId === this.userId) {
this.error = 'نمی‌توانید خودتان را به‌عنوان شرکت‌کننده اضافه کنید.';
return; return;
} }
this.participants.push({ this.participants.push({
phone: this.newParticipantPhone, id: selectedMember.user.id,
name: 'کاربر مهمان', phone: selectedMember.mobile_number,
role: 'شرکت‌کننده', name: `${selectedMember.user.first_name} ${selectedMember.user.last_name}`,
profile_img: this.defaultProfileIcon, role: selectedMember.semat || 'بدون سمت',
profile_img: selectedMember.profile_img || this.defaultProfileIcon,
}); });
this.newParticipantPhone = ''; this.selectedParticipantId = '';
this.isDropdownOpen = false;
this.error = null; this.error = null;
}, },
removeParticipant(phone) { removeParticipant(id) {
this.participants = this.participants.filter((p) => p.phone !== phone); this.participants = this.participants.filter((p) => p.id !== id);
}, },
validatePhone(phone) { closeModal(event) {
return /^09[0-9]{9}$/.test(phone); if (event && event.target.classList.contains('modal-overlay')) {
this.$emit('close');
this.resetForm();
}
}, },
closeModal() { closeModalByButton() {
this.$emit('close'); this.$emit('close');
this.resetForm(); this.resetForm();
}, },
@ -494,39 +573,30 @@ export default {
endHour: 18, endHour: 18,
endMinute: 0, endMinute: 0,
selectedRoom: null, selectedRoom: null,
use_space: false,
}; };
this.participants = []; this.participants = [];
this.newParticipantPhone = ''; this.selectedParticipantId = '';
this.isDropdownOpen = false;
this.error = null; this.error = null;
this.isRoomSelectionOpen = false; this.isRoomSelectionOpen = false;
}, },
incrementTime(field) { incrementTime(field) {
if (field === 'startHour' && this.form.startHour < 23) { const limits = { startHour: 23, startMinute: 59, endHour: 23, endMinute: 59 };
this.form.startHour++; if (this.form[field] < limits[field]) this.form[field]++;
} 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++;
}
}, },
decrementTime(field) { decrementTime(field) {
if (field === 'startHour' && this.form.startHour > 0) { if (this.form[field] > 0) this.form[field]--;
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--;
}
}, },
async handleSubmit() { async handleSubmit() {
if (!this.form.title || !this.form.date) { if (!this.form.title || !this.form.date) {
this.error = 'لطفاً نام جلسه و تاریخ را وارد کنید.'; this.error = 'لطفاً نام جلسه و تاریخ را وارد کنید.';
return; return;
} }
if (!this.form.selectedRoom) {
this.error = 'لطفاً یک اتاق برای جلسه انتخاب کنید.';
return;
}
const momentDate = moment(this.form.date, 'jYYYY/jMM/jDD'); const momentDate = moment(this.form.date, 'jYYYY/jMM/jDD');
if (!momentDate.isValid()) { if (!momentDate.isValid()) {
this.error = 'تاریخ وارد شده معتبر نیست.'; this.error = 'تاریخ وارد شده معتبر نیست.';
@ -540,43 +610,162 @@ export default {
} }
const startDateTime = momentDate const startDateTime = momentDate
.clone() .clone()
.set({ .set({ hour: this.form.startHour, minute: this.form.startMinute, second: 0 })
hour: this.form.startHour,
minute: this.form.startMinute,
second: 0,
})
.toISOString(); .toISOString();
try { try {
const userIds = [
...(this.userPhone ? [this.userPhone] : []),
...this.participants.map((p) => p.phone),
];
const meetingData = { const meetingData = {
name: this.form.title, name: this.form.title,
description: this.form.description, description: this.form.description,
date_time: startDateTime, date_time: startDateTime,
space: this.form.selectedRoom ? this.form.selectedRoom.id : null, space: this.form.selectedRoom.id,
asset_bundle: 1, asset_bundle: 1,
use_space: !!this.form.selectedRoom, use_space: this.form.use_space,
user_ids: userIds, user_ids: [this.userId, ...this.participants.map((p) => p.id)],
}; };
console.log('داده‌های ارسالی به API:', JSON.stringify(meetingData, null, 2));
this.$emit('create-meeting', meetingData); this.$emit('create-meeting', meetingData);
this.closeModal(); this.closeModalByButton();
} catch (error) { } catch (error) {
this.error = `خطا در آماده‌سازی داده‌ها: ${error.message}`; this.error = `خطا در آماده‌سازی داده‌ها: ${error.message}`;
console.error('خطا در handleSubmit:', error);
} }
}, },
}, },
created() {
this.fetchTeamMembers();
},
}; };
</script> </script>
<style scoped> <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 { .modal-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -876,6 +1065,10 @@ export default {
font-size: 18px; font-size: 18px;
color: #101010; color: #101010;
font-weight: 600; font-weight: 600;
width: 160px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: clip;
} }
.presenter button { .presenter button {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,68 +1,62 @@
<template> <template>
<div> <div>
<!-- Description --> <!-- Section Description -->
<div class="section-description"> <div class="section-description">
<div class="section-title">مدیریت اعضا</div> <div class="section-title">مدیریت اعضا</div>
<p> <p>در این بخش میتوانید اتاقها، فایلها و جلسات را با همکاران خود به اشتراک بگذارید و تیم خود را مدیریت کنید.</p>
در این بخش به شما امکان میدهد تا اتاقها، فایلها و جلسات را با همکاران خود به اشتراک بگذارید. در این بخش میتوانید تیم خود را مدیریت کنید.
</p>
</div> </div>
<!-- Tab Buttons --> <!-- Tab Buttons -->
<div class="tab-buttons"> <div class="tab-buttons">
<button <button
:class="['tab-btn', activeTab === 'users' ? 'active' : '']" :class="['tab-btn', { active: activeTab === 'users' }]"
@click="activeTab = 'users'" @click="activeTab = 'users'"
> >کاربران</button>
کاربران
</button>
<button <button
:class="['tab-btn', activeTab === 'buy-subscription' ? 'active' : '']" :class="['tab-btn', { active: activeTab === 'buy-subscription' }]"
@click="activeTab = 'buy-subscription'" @click="activeTab = 'buy-subscription'"
> >خرید اشتراک</button>
خرید اشتراک
</button>
<button <button
:class="['tab-btn', activeTab === 'membership' ? 'active' : '']" :class="['tab-btn', { active: activeTab === 'membership' }]"
@click="activeTab = 'membership'" @click="activeTab = 'membership'"
> >اشتراکها</button>
اشتراک ها
</button>
<button <button
:class="['tab-btn', activeTab === 'details' ? 'active' : '']" :class="['tab-btn', { active: activeTab === 'details' }]"
@click="activeTab = 'details'" @click="activeTab = 'details'"
> >جزئیات</button>
جزئیات
</button>
</div> </div>
<!-- Tab Content --> <!-- Tab Content -->
<div v-if="activeTab === 'users'"> <div v-if="activeTab === 'users'">
<TeamUser <TeamUser
:userList="userList" :user-list="userList"
:teamMemberCapacity="teamMemberCapacity" :team-member-capacity="teamMemberCapacity"
:subscriptionCount="subscriptionCount" :subscription-count="subscriptionCount"
:has-active-subscription="hasActiveSubscription"
@add-user="submitNewUser" @add-user="submitNewUser"
@change-tab="changeTab" @change-tab="changeTab"
/> />
</div> </div>
<div v-if="activeTab === 'membership'"> <div v-if="activeTab === 'membership'">
<Membership <Membership
:subscriptionCount="subscriptionCount" :subscription-count="subscriptionCount"
:teamMemberCapacity="teamMemberCapacity" :team-member-capacity="teamMemberCapacity"
:isBillingModalVisible="isBillingModalVisible" :is-billing-modal-visible="isBillingModalVisible"
@change-tab="changeTab" @change-tab="changeTab"
@update:isBillingModalVisible="isBillingModalVisible = $event" @update:is-billing-modal-visible="isBillingModalVisible = $event"
/> />
</div> </div>
<div v-if="activeTab === 'details'"> <div v-if="activeTab === 'details'">
<TeamDetails @update:teamData="handleTeamData" /> <TeamDetails @update:team-data="handleTeamData" />
</div> </div>
<div v-if="activeTab === 'buy-subscription'"> <div v-if="activeTab === 'buy-subscription'">
<BuySubscription <BuySubscription
:memberCount="memberCount" :member-count="memberCount"
:availableMemberOptions="availableMemberOptions" :available-member-options="availableMemberOptions"
:baseUrl="baseUrl" :base-url="baseUrl"
@update:memberCount="memberCount = $event" :has-active-subscription="hasActiveSubscription"
@plan-selected="selectedPlan = $event" :has-expired-subscription="hasExpiredSubscription"
@update:member-count="memberCount = $event"
@payment-success="handlePaymentSuccess" @payment-success="handlePaymentSuccess"
/> />
</div> </div>
@ -77,160 +71,127 @@ import TeamDetails from '@/components/TeamDetails.vue';
import axios from 'axios'; import axios from 'axios';
export default { export default {
name: 'DashboardPage', name: 'Team',
components: { components: { TeamUser, BuySubscription, Membership, TeamDetails },
TeamUser,
BuySubscription,
Membership,
TeamDetails,
},
data() { data() {
return { return {
isBillingModalVisible: false, activeTab: 'users',
userList: [],
memberCount: 5, memberCount: 5,
availableMemberOptions: [5, 10, 20, 100], 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, teamMemberCapacity: 0,
subscriptionCount: 0, subscriptionCount: 0,
hasActiveSubscription: false,
hasExpiredSubscription: false, // جدید: بررسی اشتراک منقضیشده
subscriptionEndTime: null, // جدید: ذخیره تاریخ انقضای اشتراک
teamId: null, teamId: null,
subscriptionId: null,
isBillingModalVisible: false,
baseUrl: 'http://my.xroomapp.com:8000',
}; };
}, },
created() { created() {
this.fetchUserData(); this.initializeData();
this.fetchTeamMemberInfo();
this.fetchTeamData();
const tab = this.$route.query.tab;
if (tab) {
this.activeTab = tab;
}
}, },
methods: { 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) { changeTab(tabName) {
this.activeTab = tabName; this.activeTab = tabName;
}, },
async fetchTeamData() { async fetchTeamData() {
try { try {
const token = localStorage.getItem('token'); const response = await this.axiosGet('/get_team');
const response = await axios.get('http://my.xroomapp.com:8000/get_team', {
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'application/json',
},
});
const team = response.data.teams[0]; const team = response.data.teams[0];
if (team) { this.teamId = team?.id || null;
this.teamId = team.id;
} else {
this.teamId = null;
}
} catch (error) { } catch (error) {
alert('خطا در بارگذاری اطلاعات تیم. لطفاً دوباره تلاش کنید.'); console.error('Error fetching team data:', error);
alert('خطا در بارگذاری اطلاعات تیم.');
} }
}, },
async handlePaymentSuccess() {
await this.fetchTeamMemberInfo();
await this.fetchUserData();
this.activeTab = 'membership';
},
async fetchTeamMemberInfo() { async fetchTeamMemberInfo() {
try { try {
const token = localStorage.getItem('token'); const response = await this.axiosGet('/get_all_team_members');
const response = await axios.get(`${this.baseUrl}/get_all_team_members`, { this.userList = response.data.members.map(member => ({
headers: { name: `${member.user.first_name} ${member.user.last_name}`,
Authorization: `Token ${token}`, email: member.user.username,
'Content-Type': 'application/json', role: member.semat || 'کاربر',
},
});
this.userList = response.data.members.map((member) => ({
name: `${member.first_name} ${member.last_name}`,
email: member.username,
role: 'کاربر',
version: 'نسخه آزمایشی', version: 'نسخه آزمایشی',
avatar: 'https://models.readyplayer.me/681f59760bc631a87ad25172.png', avatar: member.profile_img || 'https://models.readyplayer.me/681f59760bc631a87ad25172.png',
})); }));
this.teamMemberCapacity = response.data.members.length; this.teamMemberCapacity = response.data.members.length;
} catch (error) { } catch (error) {
alert('خطا در بارگذاری اطلاعات اعضای تیم. لطفاً دوباره تلاش کنید.'); console.error('Error fetching team members:', error);
alert('خطا در بارگذاری اطلاعات اعضای تیم.');
} }
}, },
async fetchUserData() { async fetchUserData() {
try { try {
const token = localStorage.getItem('token'); const response = await this.axiosGet('/get_user_subscriptions');
const response = await axios.get(`${this.baseUrl}/getInfo`, { const subscriptions = response.data.subscriptions || [];
headers: { this.subscriptionCount = subscriptions.reduce((total, sub) => total + sub.user_count, 0);
Authorization: `Token ${token}`, this.subscriptionId = subscriptions[0]?.id || null;
}, this.subscriptionEndTime = subscriptions[0]?.endTime || null; // جدید: تاریخ انقضا
}); const now = new Date();
this.userData = response.data; const isExpiredByTime = this.subscriptionEndTime && new Date(this.subscriptionEndTime) < now;
if (this.userData.data.subscription) { const isExpiredByCapacity = this.subscriptionCount <= this.teamMemberCapacity;
this.subscriptionCount = this.userData.data.subscription || 0; this.hasActiveSubscription = subscriptions.length > 0 && !isExpiredByTime && !isExpiredByCapacity;
} else { this.hasExpiredSubscription = subscriptions.length > 0 && (isExpiredByTime || isExpiredByCapacity);
this.subscriptionCount = 0;
}
} catch (error) { } 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) { async submitNewUser(newUser) {
const remainingCapacity = this.subscriptionCount - this.teamMemberCapacity; if (this.subscriptionCount - this.teamMemberCapacity <= 0) {
if (remainingCapacity <= 0) { alert('اشتراک فعالی ندارید , اشتراک تهیه نمایید.');
alert('ظرفیت تیم پر شده است. لطفاً اشتراک جدیدی خریداری کنید.');
this.activeTab = 'buy-subscription'; this.activeTab = 'buy-subscription';
return; return;
} }
if (!this.teamId) { if (!this.teamId) {
alert('خطا: اطلاعات تیم یافت نشد. لطفاً دوباره تلاش کنید.'); alert('خطا: اطلاعات تیم یافت نشد.');
return; return;
} }
try { try {
const token = localStorage.getItem('token'); await this.axiosPost('/add_teamMember/', { ...newUser, teamId: this.teamId });
await axios.post(
'http://my.xroomapp.com:8000/add_teamMember/',
{
...newUser,
teamId: this.teamId,
},
{
headers: {
Authorization: `Token ${token}`,
'Content-Type': 'application/json',
},
}
);
this.userList.push({ this.userList.push({
...newUser, ...newUser,
avatar: 'https://models.readyplayer.me/681f59760bc631a87ad25172.png', avatar: 'https://models.readyplayer.me/681f59760bc631a87ad25172.png',
@ -239,227 +200,39 @@ export default {
}); });
this.teamMemberCapacity++; this.teamMemberCapacity++;
await this.fetchTeamMemberInfo(); await this.fetchTeamMemberInfo();
alert('کاربر با موفقیت اضافه شد'); alert('کاربر با موفقیت اضافه شد.');
} catch (error) { } catch (error) {
alert('خطا در اضافه کردن کاربر. لطفاً دوباره تلاش کنید.'); console.error('Error adding user:', error);
alert('خطا در اضافه کردن کاربر.');
} }
}, },
handleBackdropClick(event) { handleTeamData(data) {
if (event.target === this.$refs.filePreviewDialog) { console.log('Team data updated:', data);
this.closePreviewDialog();
}
}, },
openPreviewDialog(type, index, url) { async axiosGet(endpoint) {
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();
});
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'); const token = localStorage.getItem('token');
let deleteUrl = ''; if (!token) throw new Error('توکن احراز هویت یافت نشد.');
let itemId = ''; return await axios.get(`${this.baseUrl}${endpoint}`, {
let fileArray = []; headers: { Authorization: `Token ${token}`, 'Content-Type': 'application/json' },
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) { async axiosPost(endpoint, data) {
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'); const token = localStorage.getItem('token');
let uploadUrl = ''; if (!token) throw new Error('توکن احراز هویت یافت نشد.');
switch (this.currentUploadType) { return await axios.post(`${this.baseUrl}${endpoint}`, data, {
case 'image': headers: { Authorization: `Token ${token}`, 'Content-Type': 'application/json' },
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;
}
}, },
}, },
watch: { watch: {
'$route.query.tab'(newTab) { '$route.query.tab'(newTab) {
if (newTab) { if (newTab) this.activeTab = newTab;
this.activeTab = newTab;
}
}, },
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.section-title { .section-title {