246 lines
11 KiB
Vue
246 lines
11 KiB
Vue
<script setup>
|
|
import { inject, onMounted, onUnmounted, ref } from 'vue';
|
|
import { getConfig, getLang } from '../utils/config';
|
|
import { fullName, formatDateTime } from '../utils/format';
|
|
import * as meetingsApi from '../api/meetings';
|
|
import { REALTIME_KEY } from '../composables/useRealtime';
|
|
|
|
const config = getConfig();
|
|
const colleagues = config.colleagues || [];
|
|
const currentUserId = config.userId;
|
|
const realtime = inject(REALTIME_KEY);
|
|
let unregisterRealtime = null;
|
|
|
|
const meetings = ref([]);
|
|
const counts = ref({ all: 0, scheduled: 0, active: 0, ended: 0 });
|
|
const currentStatus = ref('');
|
|
const loading = ref(false);
|
|
const creating = ref(false);
|
|
|
|
const form = ref({
|
|
title: '',
|
|
scheduled_at: '',
|
|
participant_ids: [],
|
|
start_now: true,
|
|
});
|
|
|
|
function handleMeetingInvite(payload) {
|
|
const meeting = payload?.meeting;
|
|
const title = meeting?.title || '';
|
|
const inviter = payload?.inviter_name || payload?.inviterName || '';
|
|
const msg = getLang('meeting_invite_notification', ':inviter invited you')
|
|
.replace(':inviter', inviter)
|
|
.replace(':title', title);
|
|
if (typeof window.toastr !== 'undefined') {
|
|
window.toastr.info(msg);
|
|
}
|
|
loadMeetings();
|
|
}
|
|
|
|
function statusLabel(status) {
|
|
const map = {
|
|
scheduled: getLang('status_scheduled'),
|
|
active: getLang('status_active'),
|
|
ended: getLang('status_ended'),
|
|
};
|
|
return map[status] || status;
|
|
}
|
|
|
|
function statusClass(status) {
|
|
const map = {
|
|
scheduled: 'comm-chip-scheduled',
|
|
active: 'comm-chip-active',
|
|
ended: 'comm-chip-ended',
|
|
};
|
|
return map[status] || 'comm-chip-ended';
|
|
}
|
|
|
|
function notifyError(message) {
|
|
if (typeof window.toastr !== 'undefined') {
|
|
window.toastr.error(message);
|
|
} else {
|
|
alert(message);
|
|
}
|
|
}
|
|
|
|
function notifySuccess(message) {
|
|
if (typeof window.toastr !== 'undefined') {
|
|
window.toastr.success(message);
|
|
}
|
|
}
|
|
|
|
async function loadMeetings() {
|
|
loading.value = true;
|
|
try {
|
|
const data = await meetingsApi.fetchMeetings(currentStatus.value);
|
|
meetings.value = data.meetings?.data || [];
|
|
counts.value = data.counts || counts.value;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function submitMeeting() {
|
|
if (!form.value.title.trim()) return;
|
|
creating.value = true;
|
|
try {
|
|
await meetingsApi.createMeeting({
|
|
title: form.value.title.trim(),
|
|
scheduled_at: form.value.scheduled_at || null,
|
|
participant_ids: form.value.participant_ids,
|
|
start_now: form.value.start_now ? 1 : 0,
|
|
});
|
|
form.value = { title: '', scheduled_at: '', participant_ids: [], start_now: true };
|
|
if (window.CommDate?.clearDateTimePicker) {
|
|
window.CommDate.clearDateTimePicker('#meeting-scheduled-at');
|
|
}
|
|
notifySuccess(getLang('meeting_created'));
|
|
await loadMeetings();
|
|
} catch (err) {
|
|
notifyError(err.response?.data?.message || getLang('error', 'Error'));
|
|
} finally {
|
|
creating.value = false;
|
|
}
|
|
}
|
|
|
|
function joinMeeting(id) {
|
|
const url = `${config.routes?.meetings?.roomBase || '/communication/meetings'}/${id}/room`;
|
|
window.open(url, '_blank');
|
|
notifySuccess(getLang('join_opened'));
|
|
}
|
|
|
|
async function endMeeting(id) {
|
|
if (!confirm(getLang('confirm_end_meeting'))) return;
|
|
try {
|
|
await meetingsApi.endMeeting(id);
|
|
notifySuccess(getLang('meeting_ended_success'));
|
|
await loadMeetings();
|
|
} catch {
|
|
notifyError(getLang('error', 'Error'));
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
unregisterRealtime = realtime?.register?.({
|
|
onMeetingInvite: handleMeetingInvite,
|
|
});
|
|
|
|
if (window.CommDate?.initDateTimePicker) {
|
|
window.CommDate.initDateTimePicker('.comm-datetimepicker');
|
|
}
|
|
loadMeetings();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
unregisterRealtime?.();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="comm-card" style="margin-bottom:18px;">
|
|
<div class="comm-card-header">
|
|
<h3 class="comm-card-title"><i class="fa fa-video-camera"></i> {{ getLang('create_meeting') }}</h3>
|
|
</div>
|
|
<div style="padding:18px;">
|
|
<form @submit.prevent="submitMeeting">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="form-group">
|
|
<label>{{ getLang('meeting_title') }}</label>
|
|
<input v-model="form.title" type="text" class="comm-input" :placeholder="getLang('meeting_title')" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label>{{ getLang('scheduled_at') }}</label>
|
|
<input v-model="form.scheduled_at" type="text" id="meeting-scheduled-at" class="comm-input comm-datetimepicker" autocomplete="off" readonly>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label>{{ getLang('participants') }}</label>
|
|
<select v-model="form.participant_ids" class="form-control" multiple style="width:100%;">
|
|
<option v-for="c in colleagues" :key="c.id" :value="c.id">{{ fullName(c) }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="form-group">
|
|
<label> </label>
|
|
<label style="display:block;margin-top:8px;">
|
|
<input v-model="form.start_now" type="checkbox" :true-value="true" :false-value="false"> {{ getLang('start_now') }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="comm-btn comm-btn-primary" :disabled="creating">
|
|
<i :class="creating ? 'fa fa-spinner fa-spin' : 'fa fa-plus'"></i> {{ getLang('create_meeting') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comm-card">
|
|
<div class="comm-card-header">
|
|
<h3 class="comm-card-title">{{ getLang('meetings') }}</h3>
|
|
<div class="comm-tabs">
|
|
<button type="button" class="comm-tab" :class="{ active: currentStatus === '' }" @click="currentStatus = ''; loadMeetings()">
|
|
{{ getLang('filter_all') }} <span class="badge">{{ counts.all }}</span>
|
|
</button>
|
|
<button type="button" class="comm-tab" :class="{ active: currentStatus === 'scheduled' }" @click="currentStatus = 'scheduled'; loadMeetings()">
|
|
{{ getLang('status_scheduled') }} <span class="badge">{{ counts.scheduled }}</span>
|
|
</button>
|
|
<button type="button" class="comm-tab" :class="{ active: currentStatus === 'active' }" @click="currentStatus = 'active'; loadMeetings()">
|
|
{{ getLang('status_active') }} <span class="badge">{{ counts.active }}</span>
|
|
</button>
|
|
<button type="button" class="comm-tab" :class="{ active: currentStatus === 'ended' }" @click="currentStatus = 'ended'; loadMeetings()">
|
|
{{ getLang('status_ended') }} <span class="badge">{{ counts.ended }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div style="padding:16px 18px 20px;">
|
|
<div v-if="loading" class="text-center" style="padding:30px;"><i class="fa fa-spinner fa-spin fa-2x"></i></div>
|
|
<div v-else-if="!meetings.length" class="comm-empty">
|
|
<i class="fa fa-video-camera"></i>
|
|
<p>{{ getLang('no_meetings') }}</p>
|
|
</div>
|
|
<div v-else class="row">
|
|
<div v-for="m in meetings" :key="m.id" class="col-md-6" style="margin-bottom:14px;">
|
|
<div style="border:1px solid #e7ecf3;border-radius:14px;padding:16px;background:#fff;height:100%;">
|
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;">
|
|
<div>
|
|
<h4 style="margin:0 0 6px;font-weight:600;">{{ m.title }}</h4>
|
|
<span class="comm-chip" :class="statusClass(m.status)">{{ statusLabel(m.status) }}</span>
|
|
</div>
|
|
<div style="display:flex;gap:6px;flex-shrink:0;">
|
|
<button v-if="m.status !== 'ended'" type="button" class="comm-btn comm-btn-success btn-sm" @click="joinMeeting(m.id)">
|
|
<i class="fa fa-sign-in"></i> {{ getLang('join_meeting') }}
|
|
</button>
|
|
<button v-if="m.created_by === currentUserId && m.status !== 'ended'" type="button" class="comm-btn comm-btn-danger btn-sm" @click="endMeeting(m.id)">
|
|
<i class="fa fa-stop"></i> {{ getLang('end_meeting') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top:12px;font-size:13px;color:#6b7a8c;">
|
|
<div><strong>{{ getLang('created_by') }}:</strong> {{ fullName(m.creator) }}</div>
|
|
<div><strong>{{ getLang('scheduled_at') }}:</strong> {{ formatDateTime(m.scheduled_at || m.started_at || m.created_at) }}</div>
|
|
<div style="margin-top:6px;">
|
|
<strong>{{ getLang('participants') }}:</strong>
|
|
<span v-for="(p, idx) in (m.participants || [])" :key="p.id">
|
|
<template v-if="idx">, </template>
|
|
{{ fullName(p.user) }}
|
|
<small class="text-muted">({{ p.role === 'host' ? getLang('host') : getLang('participant') }})</small>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|