summaryrefslogtreecommitdiff
path: root/ping/frontend/src/routes/dashboard/models/+page.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'ping/frontend/src/routes/dashboard/models/+page.svelte')
-rw-r--r--ping/frontend/src/routes/dashboard/models/+page.svelte568
1 files changed, 568 insertions, 0 deletions
diff --git a/ping/frontend/src/routes/dashboard/models/+page.svelte b/ping/frontend/src/routes/dashboard/models/+page.svelte
new file mode 100644
index 0000000..3be6f00
--- /dev/null
+++ b/ping/frontend/src/routes/dashboard/models/+page.svelte
@@ -0,0 +1,568 @@
+<script lang="ts">
+ import Avatar from '$lib/components/Avatar.svelte';
+ import UserSelector from '$lib/components/input/UserSelector.svelte';
+ import { authFetch, getUser, type IUser } from '$lib/stores/auth';
+ import { addToast } from '$lib/stores/toast';
+ import { onMount } from 'svelte';
+ import { get } from 'svelte/store';
+
+ interface IModel {
+ id: string;
+ members: IUser[];
+ name: string;
+ owner: IUser;
+ }
+
+ let modelName = $state('');
+ let sources = $state([]);
+ let models = $state<IModel[]>([]);
+ let modelToEdit = $state<number | null>(null);
+ let modelOwner = $state<IUser | null>(null);
+
+ let users = $state<IUser[]>([]);
+
+ function addSource() {
+ sources = [...sources, ''];
+ }
+
+ function removeSource(index: number) {
+ sources = sources.filter((_, i) => i !== index);
+ }
+
+ function handleSubmit(e: SubmitEvent) {
+ e.preventDefault();
+ if (modelToEdit === null) {
+ createModel();
+ } else {
+ updateModel();
+ }
+ }
+
+ async function createModel() {
+ try {
+ const response = await authFetch('/api/projects', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ accept: 'application/json'
+ },
+ body: JSON.stringify({
+ name: modelName
+ })
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ models = [...models, data];
+ await Promise.all(
+ users.map((user) =>
+ authFetch(`/api/projects/${data.id}/add-user`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ accept: '*/*'
+ },
+ body: JSON.stringify({ userId: user.id })
+ })
+ .then((res) => {
+ if (!res.ok) {
+ addToast({
+ color: 'red',
+ title: 'Erreur',
+ message: `Impossible d'ajouter l'utilisateur ${user.displayName} au modèle "${modelName}".`
+ });
+ console.error(`HTTP error! status: ${res.status}`);
+ }
+ return res.json().catch(() => null);
+ })
+ .then((memberData) => {
+ console.log('Member added:', memberData);
+ })
+ .catch((err) => {
+ console.error('Error adding member:', err);
+ })
+ )
+ );
+ addToast({
+ color: 'green',
+ title: 'Modèle créé',
+ message: `Le modèle "${modelName}" a été créé avec succès.`
+ });
+ modelName = '';
+ return data;
+ } catch (error) {
+ console.error('Error creating model:', error);
+ addToast({
+ color: 'red',
+ title: 'Erreur',
+ message: `Une erreur est survenue lors de la création du modèle "${modelName}".`
+ });
+ throw error;
+ }
+ }
+
+ async function updateModel() {
+ try {
+ if (modelToEdit === null || models.length <= modelToEdit) {
+ addToast({
+ color: 'red',
+ title: 'Erreur',
+ message: 'Aucun modèle sélectionné pour la mise à jour.'
+ });
+ return;
+ }
+ const model = models[modelToEdit];
+ console.log('Updating model:', model);
+ const response = await authFetch(`/api/projects/${model.id}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ accept: 'application/json'
+ },
+ body: JSON.stringify({
+ name: modelName
+ })
+ });
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const updatedModel = await response.json();
+ models = models.map((m, i) => (i === modelToEdit ? updatedModel : m));
+ modelToEdit = null;
+ modelName = '';
+ addToast({
+ color: 'green',
+ title: 'Modèle mis à jour',
+ message: `Le modèle "${updatedModel.name}" a été mis à jour avec succès.`
+ });
+ } catch (error) {
+ console.error('Error updating model:', error);
+ addToast({
+ color: 'red',
+ title: 'Erreur',
+ message: `Une erreur est survenue lors de la mise à jour du modèle "${modelName}".`
+ });
+ throw error;
+ }
+ }
+
+ async function getMyModels() {
+ try {
+ const response = await authFetch('/api/projects', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ accept: 'application/json'
+ }
+ });
+ const data = await response.json();
+ console.log('My models:', data);
+ return data;
+ } catch (error) {
+ console.error('Error fetching models:', error);
+ addToast({
+ color: 'red',
+ title: 'Erreur',
+ message: 'Impossible de charger les modèles.'
+ });
+ throw error;
+ }
+ }
+
+ function deleteModel(i: number) {
+ if (models.length <= i || i < 0) {
+ addToast({
+ color: 'red',
+ title: 'Erreur',
+ message: 'Modèle introuvable.'
+ });
+ return;
+ }
+
+ modelToEdit = null;
+
+ const modelToDelete = models[i];
+ authFetch(`/api/projects/${modelToDelete.id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ accept: 'application/json'
+ }
+ })
+ .then(() => {
+ models = models.filter((_, index) => index !== i);
+ addToast({
+ color: 'green',
+ title: 'Modèle supprimé',
+ message: `Le modèle "${modelToDelete.name}" a été supprimé avec succès.`
+ });
+ })
+ .catch((err) => {
+ console.error('Error deleting model:', err);
+ addToast({
+ color: 'red',
+ title: 'Erreur',
+ message: `Impossible de supprimer le modèle "${modelToDelete.name}".`
+ });
+ });
+ }
+
+ function onEditButtonPressed(i: number) {
+ if (modelToEdit === i) {
+ modelToEdit = null;
+ modelName = '';
+ users = [];
+ modelOwner = null;
+ } else {
+ modelToEdit = i;
+ modelName = models[i].name;
+ users = models[i].members || [];
+ modelOwner = models[i].owner || null;
+ }
+ }
+
+ let currentUser = $state<IUser | null>(null);
+
+ onMount(() => {
+ getMyModels()
+ .then((lsModels) => {
+ models = lsModels;
+ console.log('Models loaded:', models);
+ })
+ .catch((err) => {
+ console.error('Error loading models:', err);
+ });
+ getUser()
+ .then((user) => {
+ currentUser = user;
+ console.log('Current user:', currentUser);
+ })
+ .catch((err) => {
+ console.error('Error fetching current user:', err);
+ });
+ });
+</script>
+
+<section id="models">
+ <div class="models-container">
+ <div class="active-models">
+ <h2>Modèles actifs</h2>
+ <div class="models-list">
+ {#each models as m, i}
+ <div class="model-item" class:selected={modelToEdit === i}>
+ <div class="model-name">
+ <span>{m.name}</span><br />
+ <code style="font-size:8px">{m.id}</code>
+ </div>
+ <div class="model-metrics">
+ <div class="metric">
+ <span class="metric-label">Eco score</span>
+ <span class="metric-value green">{69} %</span>
+ </div>
+ <div class="metric">
+ <span class="metric-label">Efficacité</span>
+ <span class="metric-value green">{69} %</span>
+ </div>
+ </div>
+ <button class="modify-btn" onclick={() => onEditButtonPressed(i)}>✏️</button>
+ <button class="delete-btn btn" onclick={() => deleteModel(i)}>🗑️</button>
+ </div>
+ {/each}
+ </div>
+ </div>
+
+ <div class="new-model">
+ <form onsubmit={handleSubmit}>
+ <div class="new-model-header">
+ {#if modelToEdit !== null}
+ <h2>Modifier un modèle</h2>
+ {:else}
+ <h2>Nouveau modèle</h2>
+ {/if}
+ <button class="register-btn" type="submit">📁 Enregistrer</button>
+ </div>
+ <div class="form-group">
+ <label for="name">Nom du modèle</label>
+ <input type="text" id="name" bind:value={modelName} placeholder="Modèle X" />
+ <div class="flex gap-2 p-2">
+ {#if modelToEdit !== null}
+ <Avatar username={modelOwner?.displayName} url={modelOwner?.avatar} />
+ {:else if currentUser}
+ <Avatar username={currentUser.displayName} url={currentUser.avatar} />
+ {:else}
+ Chargement en cours...
+ {/if}
+ <UserSelector bind:users />
+ </div>
+ </div>
+
+ <div class="sources-section">
+ <div class="sources-header">
+ <span>Sources de données</span>
+ <button type="button" class="add-source-btn" onclick={addSource}>
+ ➕ Ajouter une source de données
+ </button>
+ </div>
+
+ {#each sources as source, index}
+ <div class="source-item">
+ <!-- svelte-ignore a11y_label_has_associated_control -->
+ <label>Source {index + 1}</label>
+ <div class="source-input-group">
+ <input type="text" bind:value={sources[index]} placeholder="Source" />
+ <button type="button" class="remove-source-btn" onclick={() => removeSource(index)}>
+ 🗑️
+ </button>
+ </div>
+ </div>
+ {/each}
+ </div>
+ </form>
+ </div>
+ </div>
+</section>
+
+<style>
+ #models {
+ padding: 16px;
+ background-color: var(--bg-secondary);
+ color: white;
+ min-height: calc(100vh - 72px);
+ }
+
+ .models-container {
+ display: flex;
+ gap: 16px;
+ max-width: 1400px;
+ }
+
+ h2 {
+ color: var(--text-lime);
+ font-size: 24px;
+ margin: 0 0 16px 0;
+ font-weight: 600;
+ }
+
+ /* Modèles actifs */
+ .active-models {
+ flex: 1;
+ background-color: var(--bg-primary);
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ padding: 16px;
+ margin: 16px;
+ color: white;
+ }
+
+ .models-list {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .model-item {
+ background-color: var(--bg-secondary);
+ padding: 16px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border: 2px solid transparent;
+ }
+
+ .model-item.selected {
+ border: 2px solid var(--text-lime);
+ }
+
+ .model-name {
+ font-weight: 500;
+ font-size: 16px;
+ color: white;
+ }
+
+ .model-metrics {
+ display: flex;
+ gap: 30px;
+ }
+
+ .metric {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+ }
+
+ .metric-label {
+ font-size: 12px;
+ color: #888888;
+ }
+
+ .metric-value {
+ font-size: 18px;
+ font-weight: bold;
+ }
+
+ .metric-value.green {
+ color: var(--text-lime);
+ }
+
+ .modify-btn {
+ background-color: var(--btn-primary);
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ transition-duration: 0.2s;
+ }
+
+ .modify-btn:hover {
+ background-color: var(--btn-primary-hover);
+ transform: scale(1.025);
+ }
+
+ /* Nouveau modèle */
+ .new-model {
+ flex: 1;
+ background-color: var(--bg-primary);
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ padding: 16px;
+ margin: 16px;
+ color: white;
+ }
+
+ .new-model-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ }
+
+ .register-btn {
+ background-color: var(--btn-primary);
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 600;
+ font-size: 14px;
+ transition-duration: 0.2s;
+ }
+
+ .register-btn:hover {
+ background-color: var(--btn-primary-hover);
+ transform: scale(1.025);
+ }
+
+ .form-group {
+ margin-bottom: 16px;
+ }
+
+ label {
+ display: block;
+ margin-bottom: 8px;
+ font-size: 14px;
+ color: white;
+ font-weight: 500;
+ }
+
+ input[type='text'] {
+ width: 100%;
+ background-color: var(--bg-secondary);
+ border: none;
+ border-bottom: 2px solid var(--text-lime);
+ padding: 8px;
+ border-radius: 8px 8px 0 0;
+ color: white;
+ font-size: 14px;
+ box-sizing: border-box;
+ }
+
+ input[type='text']:focus {
+ outline: none;
+ border-bottom-color: var(--text-lime);
+ }
+
+ input[type='text']::placeholder {
+ color: #888888;
+ }
+
+ /* Sources section */
+ .sources-section {
+ margin-top: 16px;
+ }
+
+ .sources-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+
+ .sources-header span {
+ font-size: 14px;
+ font-weight: 500;
+ color: white;
+ }
+
+ .add-source-btn {
+ background-color: var(--btn-secondary);
+ color: var(--text-lime);
+ border: 1px solid var(--text-lime);
+ padding: 8px 16px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 600;
+ transition-duration: 0.2s;
+ }
+
+ .add-source-btn:hover {
+ background-color: var(--text-lime);
+ color: var(--bg-primary);
+ transform: scale(1.025);
+ }
+
+ .source-item {
+ margin-bottom: 15px;
+ }
+
+ .source-item label {
+ margin-bottom: 6px;
+ font-size: 12px;
+ color: #888888;
+ }
+
+ .source-input-group {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ }
+
+ .source-input-group input {
+ flex: 1;
+ }
+
+ .remove-source-btn {
+ background-color: var(--btn-secondary);
+ color: #888888;
+ border: none;
+ padding: 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 16px;
+ transition-duration: 0.2s;
+ min-width: 40px;
+ }
+
+ .remove-source-btn:hover {
+ color: #ef4444;
+ background-color: var(--btn-primary);
+ transform: scale(1.025);
+ }
+</style>