ultimatepos/Modules/Communication/Resources/js/components/MeetingsApp.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>&nbsp;</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>