(update) : gestion des sessions et prévisualisation des médias
- Ajout bouton création de session avec dialog - Ajout bouton suppression de session avec confirmation - Ajout preview plein écran pour images, vidéos et audio - Remplacement v-file-input par v-btn stylé pour upload - Preview vidéo à 1/3 de la durée pour éviter écran noir - Améliorations de style: dialogs arrondis, champs alignés
This commit is contained in:
610
VApp/src/views/SessionEditor.vue
Normal file
610
VApp/src/views/SessionEditor.vue
Normal file
@@ -0,0 +1,610 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row class="mb-4 align-center">
|
||||||
|
<v-col cols="12" md="4" class="d-flex align-center gap-2">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedSessionId"
|
||||||
|
:items="availableSessions"
|
||||||
|
item-title="title"
|
||||||
|
item-value="id"
|
||||||
|
label="Choisir une session"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="loadSession"
|
||||||
|
rounded="xl"
|
||||||
|
class="flex-grow-1"
|
||||||
|
></v-select>
|
||||||
|
<v-btn icon="mdi-plus" color="green" variant="tonal" @click="openCreateDialog" title="Nouvelle Session"></v-btn>
|
||||||
|
<v-btn icon="mdi-delete" color="red" variant="tonal" @click="deleteDialogVisible = true" :disabled="!selectedSessionId" title="Supprimer Session"></v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<h1 class="text-h4 white--text">Éditeur</h1>
|
||||||
|
</v-col>
|
||||||
|
<v-col class="text-right">
|
||||||
|
<v-btn rounded="xl" color="primary" @click="saveSession" :loading="saving" :disabled="!selectedSessionId">
|
||||||
|
<v-icon start>mdi-content-save</v-icon> Sauvegarder
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<div v-if="!selectedSessionId" class="text-center mt-10 text-h5 grey--text">
|
||||||
|
Veuillez sélectionner une session pour commencer.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<v-alert v-if="error" type="error" closable class="mb-4">{{ error }}</v-alert>
|
||||||
|
<v-alert rounded="xl" v-if="success" type="success" closable class="mb-4">{{ success }}</v-alert>
|
||||||
|
|
||||||
|
<v-card class="mb-6" rounded="xl">
|
||||||
|
<v-card-title>Configuration Générale</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field color="primary" v-model="sessionConfig.PackId" label="ID du Pack" readonly variant="outlined" rounded="xl"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field color="primary" v-model="sessionConfig.PackTitle" label="Titre du Pack" variant="outlined" rounded="xl"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex justify-space-between align-center">
|
||||||
|
Questions ({{ sessionConfig.Questions.length }})
|
||||||
|
<v-btn color="secondary" size="small" prepend-icon="mdi-plus" @click="openQuestionDialog()">Ajouter</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list lines="two" bg-color="transparent">
|
||||||
|
<v-list-item v-if="sessionConfig.Questions.length === 0">
|
||||||
|
<v-list-item-title class="text-center">Aucune question définie.</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<template v-for="(question, index) in sessionConfig.Questions" :key="question.QuestionId">
|
||||||
|
<v-card variant="outlined" class="mb-2">
|
||||||
|
<v-card-text class="d-flex align-center py-2">
|
||||||
|
<div class="mr-4 font-weight-bold text-h6">{{ index + 1 }}</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="text-subtitle-1 font-weight-bold">
|
||||||
|
{{ question.QuestionId }} - {{ question.Type }} ({{ question.Points }} pts)
|
||||||
|
</div>
|
||||||
|
<div class="text-body-2">{{ question.QuestionText }}</div>
|
||||||
|
<div class="text-caption grey--text">Réponse: {{ question.MasterData.CorrectAnswer }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column gap-1">
|
||||||
|
<v-btn icon="mdi-arrow-up" size="x-small" variant="text" :disabled="index === 0" @click="moveQuestion(index, -1)"></v-btn>
|
||||||
|
<v-btn icon="mdi-arrow-down" size="x-small" variant="text" :disabled="index === sessionConfig.Questions.length - 1" @click="moveQuestion(index, 1)"></v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex ml-2">
|
||||||
|
<v-btn icon="mdi-pencil" size="small" variant="text" color="blue" @click="openQuestionDialog(question, index)"></v-btn>
|
||||||
|
<v-btn icon="mdi-delete" size="small" variant="text" color="red" @click="deleteQuestion(index)"></v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dialog d'édition de question -->
|
||||||
|
<v-dialog v-model="dialogVisible" persistent max-width="800px">
|
||||||
|
<v-card rounded="xl" class="pa-3">
|
||||||
|
<v-card-title class="text-title-style">
|
||||||
|
{{ editingIndex === -1 ? 'Nouvelle Question' : 'Éditer la Question' }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field color="primary" density="compact" variant="outlined" rounded="lg" v-model="editedQuestion.QuestionId" label="ID Question" hint="Unique ID, ex: Q-005"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-select color="primary" density="compact" variant="outlined" rounded="lg" v-model="editedQuestion.Type" :items="['video', 'audio', 'picture']" label="Type"></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field color="primary" density="compact" variant="outlined" rounded="lg" v-model.number="editedQuestion.Points" type="number" label="Points"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea color="primary" variant="outlined" rounded="lg" v-model="editedQuestion.QuestionText" label="Texte de la question" rows="2"></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<!-- Media Section -->
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex align-end">
|
||||||
|
<!-- Preview Section -->
|
||||||
|
<div v-if="editedQuestion.MediaUrl" class="mr-4 text-center" style="width: 120px; height: 120px; background: #333; display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 8px; position: relative;">
|
||||||
|
<img v-if="editedQuestion.Type === 'picture'" :src="getPreviewUrl(editedQuestion.MediaUrl)" style="width: 100%; height: 100%; object-fit: cover; cursor: pointer;" @click="showFullPreview = true">
|
||||||
|
<video
|
||||||
|
v-else-if="editedQuestion.Type === 'video'"
|
||||||
|
:src="getPreviewUrl(editedQuestion.MediaUrl)"
|
||||||
|
style="width: 100%; height: 100%; object-fit: cover; cursor: pointer;"
|
||||||
|
@click="showVideoPreview = true"
|
||||||
|
@loadedmetadata="(e) => e.target.currentTime = e.target.duration / 3"
|
||||||
|
muted
|
||||||
|
></video>
|
||||||
|
<div v-else-if="editedQuestion.Type === 'audio'" class="text-caption" style="cursor: pointer;" @click="showAudioPreview = true">
|
||||||
|
<v-icon size="64" color="primary">mdi-music</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-text-field color="primary" density="compact" variant="outlined" rounded="lg" v-model="editedQuestion.MediaUrl" label="URL du Média" class="flex-grow-2 mr-2" hide-details></v-text-field>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref="fileInputRef"
|
||||||
|
style="display: none;"
|
||||||
|
@change="onFileSelected"
|
||||||
|
accept="image/*,video/*,audio/*"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
rounded="lg"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
:loading="uploading"
|
||||||
|
:disabled="!editedQuestion.Type"
|
||||||
|
@click="$refs.fileInputRef.click()"
|
||||||
|
>
|
||||||
|
<v-icon start>mdi-upload</v-icon>
|
||||||
|
Upload
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div v-if="uploadError" class="text-caption text-red">{{ uploadError }}</div>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12"><v-divider></v-divider></v-col>
|
||||||
|
<v-col cols="12" class="text-h6">Paramètres</v-col>
|
||||||
|
|
||||||
|
<v-col cols="6" md="4">
|
||||||
|
<v-switch inset v-model="editedQuestion.Settings.AutoPlay" label="AutoPlay" density="compact" color="primary"></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="4">
|
||||||
|
<v-switch inset v-model="editedQuestion.Settings.Loop" label="Loop" density="compact" color="primary"></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="4">
|
||||||
|
<v-text-field color="primary" variant="outlined" rounded="lg" v-model.number="editedQuestion.Settings.PlayTime" type="number" label="Durée (sec)" density="compact"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12"><v-divider></v-divider></v-col>
|
||||||
|
<v-label class="text-title-style">
|
||||||
|
Réponse (Maître du jeu)
|
||||||
|
</v-label>
|
||||||
|
<v-col cols="12" class="text-h6"></v-col>
|
||||||
|
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field color="primary" variant="outlined" rounded="lg" v-model="editedQuestion.MasterData.CorrectAnswer" label="Réponse Correcte"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea color="primary" variant="outlined" rounded="lg" v-model="editedQuestion.MasterData.MasterNotes" label="Notes pour le MJ" rows="2"></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea color="primary" variant="outlined" rounded="lg" v-model="editedQuestion.MasterData.Help" label="Indice / Aide" rows="2"></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" rounded="xl" variant="outlined" @click="closeDialog">Annuler</v-btn>
|
||||||
|
<v-btn color="success" rounded="xl" variant="outlined" @click="saveQuestion">Ok</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Dialog Preview Image -->
|
||||||
|
<v-dialog v-model="showFullPreview" max-width="90vw">
|
||||||
|
<v-card rounded="xl" class="pa-2">
|
||||||
|
<v-card-text class="d-flex justify-center" style="padding: 16px;">
|
||||||
|
<v-img
|
||||||
|
:src="getPreviewUrl(editedQuestion.MediaUrl)"
|
||||||
|
max-height="80vh"
|
||||||
|
rounded="lg"
|
||||||
|
@click="showFullPreview = false"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
></v-img>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" variant="text" @click="showFullPreview = false">Fermer</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Dialog Preview Video -->
|
||||||
|
<v-dialog v-model="showVideoPreview" max-width="90vw">
|
||||||
|
<v-card rounded="xl" class="pa-2">
|
||||||
|
<v-card-text class="d-flex justify-center" style="padding: 16px;">
|
||||||
|
<video
|
||||||
|
:src="getPreviewUrl(editedQuestion.MediaUrl)"
|
||||||
|
controls
|
||||||
|
autoplay
|
||||||
|
disablepictureinpicture
|
||||||
|
disableremoteplayback
|
||||||
|
controlslist="nodownload"
|
||||||
|
style="max-width: 100%; max-height: 80vh; border-radius: 12px;"
|
||||||
|
></video>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" variant="text" @click="showVideoPreview = false">Fermer</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Dialog Preview Audio -->
|
||||||
|
<v-dialog v-model="showAudioPreview" max-width="500px">
|
||||||
|
<v-card rounded="xl" class="pa-4">
|
||||||
|
<v-card-title class="text-center">
|
||||||
|
<v-icon size="64" color="primary">mdi-music-circle</v-icon>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="d-flex justify-center" style="padding: 16px;">
|
||||||
|
<audio
|
||||||
|
:src="getPreviewUrl(editedQuestion.MediaUrl)"
|
||||||
|
controls
|
||||||
|
controlslist="nodownload"
|
||||||
|
disablepictureinpicture
|
||||||
|
disableremoteplayback
|
||||||
|
autoplay
|
||||||
|
style="width: 100%;"
|
||||||
|
></audio>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" variant="text" @click="showAudioPreview = false">Fermer</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Dialog Création de session -->
|
||||||
|
<v-dialog v-model="createDialogVisible" persistent max-width="500px">
|
||||||
|
<v-card rounded="xl">
|
||||||
|
<v-card-title class="text-h5">Nouvelle Session</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="newSessionName"
|
||||||
|
label="Nom de la session"
|
||||||
|
variant="outlined"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="createSession"
|
||||||
|
></v-text-field>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="grey" variant="text" @click="createDialogVisible = false">Annuler</v-btn>
|
||||||
|
<v-btn color="green" variant="elevated" @click="createSession" :disabled="!newSessionName">Créer</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Dialog Suppression de session -->
|
||||||
|
<v-dialog v-model="deleteDialogVisible" max-width="400px">
|
||||||
|
<v-card rounded="xl">
|
||||||
|
<v-card-title class="text-h5 delete-dialog-title-style">Supprimer la session ?</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Cette action est irréversible ! Tous les fichiers de cette session seront supprimés.
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn rounded="xl" class="mb-3" color="grey" variant="text" @click="deleteDialogVisible = false">Annuler</v-btn>
|
||||||
|
<v-btn rounded="xl" class="mb-3 mr-3" color="red" variant="elevated" @click="confirmDeleteSession">Supprimer</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
import config from '@/config.js';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const availableSessions = ref([]);
|
||||||
|
const selectedSessionId = ref(null);
|
||||||
|
|
||||||
|
const sessionConfig = reactive({
|
||||||
|
PackId: '',
|
||||||
|
PackTitle: '',
|
||||||
|
Questions: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultQuestion = {
|
||||||
|
QuestionId: '',
|
||||||
|
Type: '',
|
||||||
|
Points: 1,
|
||||||
|
QuestionText: '',
|
||||||
|
MediaUrl: '',
|
||||||
|
Settings: {
|
||||||
|
StartAt: null,
|
||||||
|
StopAt: null,
|
||||||
|
AutoPlay: false,
|
||||||
|
Loop: false,
|
||||||
|
PlayTime: null,
|
||||||
|
DisplayMode: 'Cover',
|
||||||
|
BlurEffect: false
|
||||||
|
},
|
||||||
|
MasterData: {
|
||||||
|
CorrectAnswer: '',
|
||||||
|
MasterNotes: '',
|
||||||
|
Help: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const editingIndex = ref(-1);
|
||||||
|
const editedQuestion = reactive(JSON.parse(JSON.stringify(defaultQuestion)));
|
||||||
|
|
||||||
|
// Creation Session
|
||||||
|
const createDialogVisible = ref(false);
|
||||||
|
const newSessionName = ref('');
|
||||||
|
const deleteDialogVisible = ref(false);
|
||||||
|
const showFullPreview = ref(false);
|
||||||
|
const showVideoPreview = ref(false);
|
||||||
|
const showAudioPreview = ref(false);
|
||||||
|
|
||||||
|
const saving = ref(false);
|
||||||
|
const uploading = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
const success = ref('');
|
||||||
|
const uploadError = ref('');
|
||||||
|
|
||||||
|
// MQTT
|
||||||
|
const mqttBrokerUrl = config.mqttBrokerUrl;
|
||||||
|
// Guess HTTP URL from MQTT URL (assuming typical setup or localhost fallback)
|
||||||
|
// Ideally this should be in config too
|
||||||
|
const httpBaseUrl = mqttBrokerUrl.replace('ws://', 'http://').replace('wss://', 'https://').split(':')[0] + ':' + (config.mqttBrokerUrl.includes('localhost') ? '3000' : '3000');
|
||||||
|
// Warning: This URL construction is brittle. Better to put api URL in config. But for now we assume port 3000 next to MQTT.
|
||||||
|
// Correct logic: take protocol://hostname from mqtt, add :3000.
|
||||||
|
function getApiUrl() {
|
||||||
|
// Port 3001 to avoid conflict with Vite (3000)
|
||||||
|
// Use the same hostname as the current page to avoid "Private Network Access" blocks
|
||||||
|
return `http://192.168.1.178:3001`;
|
||||||
|
}
|
||||||
|
const API_URL = getApiUrl();
|
||||||
|
|
||||||
|
function getPreviewUrl(relativePath) {
|
||||||
|
if (!relativePath || relativePath.startsWith('http')) return relativePath;
|
||||||
|
// Construct full URL: API_URL + /quizz/ + sessionId + relativePath
|
||||||
|
// relativePath starts with /assets/...
|
||||||
|
return `${API_URL}/quizz/${selectedSessionId.value}${relativePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let client = null;
|
||||||
|
const topics = {
|
||||||
|
requestList: 'game/session/list/request',
|
||||||
|
responseList: 'game/session/list/response',
|
||||||
|
requestConfig: 'game/session/config/request',
|
||||||
|
getConfig: 'game/session/config/get',
|
||||||
|
updateConfig: 'game/session/config/update',
|
||||||
|
createSession: 'game/session/create',
|
||||||
|
deleteSession: 'game/session/delete'
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
client = mqtt.connect(mqttBrokerUrl);
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
console.log('SessionEditor: Connected to MQTT');
|
||||||
|
client.subscribe(topics.responseList);
|
||||||
|
client.subscribe(topics.getConfig);
|
||||||
|
|
||||||
|
// Request list of sessions
|
||||||
|
client.publish(topics.requestList, '{}');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', (topic, message) => {
|
||||||
|
if (topic === topics.responseList) {
|
||||||
|
try {
|
||||||
|
availableSessions.value = JSON.parse(message.toString());
|
||||||
|
console.log("Sessions received:", availableSessions.value);
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
else if (topic === topics.getConfig) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message.toString());
|
||||||
|
// Reset config
|
||||||
|
sessionConfig.Questions = [];
|
||||||
|
Object.assign(sessionConfig, data);
|
||||||
|
console.log('Session config loaded');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing session config', e);
|
||||||
|
error.value = "Erreur lors du chargement de la configuration.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (client) client.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadSession() {
|
||||||
|
if (!selectedSessionId.value) return;
|
||||||
|
console.log("Loading session:", selectedSessionId.value);
|
||||||
|
client.publish(topics.requestConfig, JSON.stringify({ SessionId: selectedSessionId.value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openQuestionDialog(item, index) {
|
||||||
|
if (item) {
|
||||||
|
editingIndex.value = index;
|
||||||
|
Object.assign(editedQuestion, JSON.parse(JSON.stringify(item)));
|
||||||
|
if(!editedQuestion.Settings) editedQuestion.Settings = {...defaultQuestion.Settings};
|
||||||
|
if(!editedQuestion.MasterData) editedQuestion.MasterData = {...defaultQuestion.MasterData};
|
||||||
|
} else {
|
||||||
|
editingIndex.value = -1;
|
||||||
|
Object.assign(editedQuestion, JSON.parse(JSON.stringify(defaultQuestion)));
|
||||||
|
editedQuestion.QuestionId = "Q-NEW";
|
||||||
|
}
|
||||||
|
dialogVisible.value = true;
|
||||||
|
uploadError.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
Object.assign(editedQuestion, JSON.parse(JSON.stringify(defaultQuestion)));
|
||||||
|
editingIndex.value = -1;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveQuestion() {
|
||||||
|
if (editingIndex.value > -1) {
|
||||||
|
Object.assign(sessionConfig.Questions[editingIndex.value], editedQuestion);
|
||||||
|
} else {
|
||||||
|
sessionConfig.Questions.push(JSON.parse(JSON.stringify(editedQuestion)));
|
||||||
|
}
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteQuestion(index) {
|
||||||
|
if (confirm('Êtes-vous sûr de vouloir supprimer cette question ?')) {
|
||||||
|
sessionConfig.Questions.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveQuestion(index, direction) {
|
||||||
|
const newIndex = index + direction;
|
||||||
|
if (newIndex >= 0 && newIndex < sessionConfig.Questions.length) {
|
||||||
|
const item = sessionConfig.Questions.splice(index, 1)[0];
|
||||||
|
sessionConfig.Questions.splice(newIndex, 0, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFileSelected(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
handleFileUpload(file);
|
||||||
|
}
|
||||||
|
// Reset input so same file can be selected again
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileUpload(files) {
|
||||||
|
if (!files) return;
|
||||||
|
// Vuetify v-file-input can return a single object or an array depending on version/props
|
||||||
|
const file = Array.isArray(files) ? files[0] : files;
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
uploading.value = true;
|
||||||
|
uploadError.value = '';
|
||||||
|
|
||||||
|
// Validation: Type is mandatory for folder sorting
|
||||||
|
if (!editedQuestion.Type) {
|
||||||
|
uploadError.value = "Veuillez sélectionner un Type avant d'uploader un fichier.";
|
||||||
|
uploading.value = false;
|
||||||
|
// Reset input (optional but cleanest)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
// Append metadata BEFORE file so multer can read them first for destination/filename
|
||||||
|
formData.append('questionId', editedQuestion.QuestionId);
|
||||||
|
formData.append('type', editedQuestion.Type);
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
// Upload to backend
|
||||||
|
const response = await axios.post(`${API_URL}/upload/${selectedSessionId.value}`, formData);
|
||||||
|
|
||||||
|
if (response.data && response.data.path) {
|
||||||
|
editedQuestion.MediaUrl = response.data.path;
|
||||||
|
console.log("File uploaded successfully:", response.data.path);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Upload error:", e);
|
||||||
|
uploadError.value = "Erreur lors de l'upload du fichier.";
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
newSessionName.value = '';
|
||||||
|
createDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSession() {
|
||||||
|
if (!newSessionName.value) return;
|
||||||
|
|
||||||
|
console.log("Creating session:", newSessionName.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.stringify({ SessionName: newSessionName.value });
|
||||||
|
client.publish(topics.createSession, payload);
|
||||||
|
|
||||||
|
success.value = "Demande de création envoyée...";
|
||||||
|
setTimeout(() => success.value = '', 3000);
|
||||||
|
|
||||||
|
createDialogVisible.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error creating session", e);
|
||||||
|
error.value = "Erreur lors de la création.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDeleteSession() {
|
||||||
|
if (!selectedSessionId.value) return;
|
||||||
|
|
||||||
|
console.log("Deleting session:", selectedSessionId.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.stringify({ SessionId: selectedSessionId.value });
|
||||||
|
client.publish(topics.deleteSession, payload);
|
||||||
|
|
||||||
|
success.value = "Demande de suppression envoyée...";
|
||||||
|
setTimeout(() => success.value = '', 3000);
|
||||||
|
|
||||||
|
deleteDialogVisible.value = false;
|
||||||
|
selectedSessionId.value = null;
|
||||||
|
sessionConfig.Questions = [];
|
||||||
|
sessionConfig.PackId = '';
|
||||||
|
sessionConfig.PackTitle = '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error deleting session", e);
|
||||||
|
error.value = "Erreur lors de la suppression.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSession() {
|
||||||
|
if (!selectedSessionId.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
success.value = '';
|
||||||
|
error.value = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
SessionId: selectedSessionId.value,
|
||||||
|
Config: sessionConfig
|
||||||
|
});
|
||||||
|
client.publish(topics.updateConfig, payload);
|
||||||
|
success.value = "Sauvegarde envoyée.";
|
||||||
|
setTimeout(() => success.value = '', 3000);
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Erreur lors de la sauvegarde : " + e.message;
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gap-1 {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.gap-2 {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.delete-dialog-title-style {
|
||||||
|
color: rgb(var(--v-theme-primary)) !important;
|
||||||
|
padding-left: 6%;
|
||||||
|
padding-top: 3%;
|
||||||
|
}
|
||||||
|
.text-title-style {
|
||||||
|
color: rgb(var(--v-theme-primary), 1) !important;
|
||||||
|
opacity: 1;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user