From 967be9e750221ab2ab783f95df79bb26d290a45e Mon Sep 17 00:00:00 2001
From: Martial Simon <msimon_fr@hotmail.com>
Date: Mon, 15 Sep 2025 01:07:58 +0200
Subject: add: added projects

---
 .../src/routes/dashboard/models/+page.svelte       | 568 +++++++++++++++++++++
 1 file changed, 568 insertions(+)
 create mode 100644 ping/frontend/src/routes/dashboard/models/+page.svelte

(limited to 'ping/frontend/src/routes/dashboard/models/+page.svelte')

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>
-- 
cgit v1.2.3

