Compare commits

...

5 Commits

Author SHA1 Message Date
95cc10a8de assign_dont_work 2025-07-14 19:10:55 +02:00
1439d33584 planning 2025-07-14 18:03:29 +02:00
9fad10e7b7 static planning 2025-07-14 17:44:07 +02:00
62c6bb5fa5 static planning 2025-07-14 17:43:39 +02:00
56cca2b498 duration 2025-07-14 16:53:52 +02:00
12 changed files with 384 additions and 40 deletions

4
ressources.csv Normal file
View File

@ -0,0 +1,4 @@
ressource,profil,7/14/2025,7/15/2025,7/16/2025,7/17/2025,7/18/2025,7/19/2025,7/20/2025,7/21/2025,7/22/2025
Simon BOYER,dev,0,0.5,0.5,0.5,0.5,0,0.5,0.5,0.5
Fatima BROUM,dev,0,1,1,1,1,0,1,1,1
Need Dev,dev,0,0,0,0,0,0,1,1,1
1 ressource profil 7/14/2025 7/15/2025 7/16/2025 7/17/2025 7/18/2025 7/19/2025 7/20/2025 7/21/2025 7/22/2025
2 Simon BOYER dev 0 0.5 0.5 0.5 0.5 0 0.5 0.5 0.5
3 Fatima BROUM dev 0 1 1 1 1 0 1 1 1
4 Need Dev dev 0 0 0 0 0 0 1 1 1

View File

@ -36,3 +36,11 @@ npm run build
You can preview the production build with `npm run preview`. You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
Pour créer la BDD en local :
npm run init-db
Pour lancer l'application
npm run dev

Binary file not shown.

View File

@ -21,7 +21,8 @@ db.exec(`
progress INTEGER, progress INTEGER,
type TEXT, type TEXT,
parent INTEGER, parent INTEGER,
lazy BOOLEAN DEFAULT 0 -- Ajout ici assignedTo TEXT,
lazy_loading BOOLEAN DEFAULT 0
); );
CREATE TABLE links ( CREATE TABLE links (
@ -31,5 +32,16 @@ db.exec(`
type TEXT type TEXT
); );
`); `);
// table pour les ressources
db.exec(`
CREATE TABLE IF NOT EXISTS resource_planning (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ressource TEXT NOT NULL,
profil TEXT NOT NULL,
date TEXT NOT NULL,
disponibilite REAL NOT NULL
);
`);
console.log('✅ Base de données initialisée avec succès.'); console.log('✅ Base de données initialisée avec succès.');

View File

@ -4,4 +4,14 @@ import { resolve } from 'path';
// 📁 chemin absolu vers la BDD SQLite // 📁 chemin absolu vers la BDD SQLite
const db = new Database(resolve('data.db')); const db = new Database(resolve('data.db'));
db.exec(`
CREATE TABLE IF NOT EXISTS resource_planning (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ressource TEXT NOT NULL,
profil TEXT NOT NULL,
date TEXT NOT NULL,
disponibilite REAL NOT NULL
);
`);
export default db; export default db;

View File

@ -25,6 +25,7 @@
<a href="/" class:selected={current === "/"} title="Accueil">🏠</a> <a href="/" class:selected={current === "/"} title="Accueil">🏠</a>
<a href="/gantt" class:selected={current === "/gantt"} title="Gantt">📅</a> <a href="/gantt" class:selected={current === "/gantt"} title="Gantt">📅</a>
<a href="/import" class:selected={current === "/import"} title="Importer">📂</a> <a href="/import" class:selected={current === "/import"} title="Importer">📂</a>
<a href="/planning" class="block px-4 py-2 hover:bg-gray-100">📅</a>
<a href="/config" class:selected={current === "/config"} title="Config">⚙️</a> <a href="/config" class:selected={current === "/config"} title="Config">⚙️</a>
</nav> </nav>
<button class="reset-btn" on:click={resetData} title="Réinitialiser les données"> <button class="reset-btn" on:click={resetData} title="Réinitialiser les données">

View File

@ -0,0 +1,72 @@
import { json } from '@sveltejs/kit';
import db from '$lib/server/db';
export async function POST({ request }) {
const body = await request.json();
try {
if (Array.isArray(body)) {
if (body.length > 1) {
// Cas dun import CSV → remplacement total
const insert = db.prepare(`
INSERT INTO resource_planning (ressource, profil, date, disponibilite)
VALUES (@ressource, @profil, @date, @disponibilite)
`);
const clear = db.prepare(`DELETE FROM resource_planning`);
const transaction = db.transaction((data: any[]) => {
clear.run();
for (const entry of data) {
insert.run(entry);
}
});
transaction(body);
} else if (body.length === 1) {
// Cas d'une modif manuelle → update ou insert
const { ressource, profil, date, disponibilite } = body[0];
const existing = db.prepare(`
SELECT id FROM resource_planning
WHERE ressource = ? AND profil = ? AND date = ?
`).get(ressource, profil, date);
if (existing) {
db.prepare(`
UPDATE resource_planning
SET disponibilite = ?
WHERE id = ?
`).run(disponibilite, existing.id);
} else {
db.prepare(`
INSERT INTO resource_planning (ressource, profil, date, disponibilite)
VALUES (?, ?, ?, ?)
`).run(ressource, profil, date, disponibilite);
}
}
}
return json({ success: true });
} catch (e) {
console.error('Erreur POST /api/planning', e);
return json({ success: false, error: e.message }, { status: 500 });
}
}
export async function GET() {
const rows = db.prepare(`SELECT * FROM resource_planning`).all();
const grouped = new Map();
for (const row of rows) {
const key = `${row.ressource}|${row.profil}`;
if (!grouped.has(key)) {
grouped.set(key, {
ressource: row.ressource,
profil: row.profil,
disponibilites: {}
});
}
grouped.get(key).disponibilites[row.date] = row.disponibilite;
}
return json(Array.from(grouped.values()));
}

View File

@ -12,8 +12,30 @@ export async function POST({ request }) {
const { tasks, links } = await request.json(); const { tasks, links } = await request.json();
const insertTask = db.prepare(` const insertTask = db.prepare(`
INSERT INTO tasks (id, text, start, end, duration, progress, type, parent, lazy) INSERT INTO tasks (
VALUES (@id, @text, @start, @end, @duration, @progress, @type, @parent, @lazy) id,
text,
start,
end,
duration,
progress,
type,
parent,
assignedTo,
lazy_loading
)
VALUES (
@id,
@text,
@start,
@end,
@duration,
@progress,
@type,
@parent,
@assignedTo,
@lazy_loading
)
`); `);
const insertLink = db.prepare(` const insertLink = db.prepare(`
@ -21,21 +43,20 @@ export async function POST({ request }) {
VALUES (@id, @source, @target, @type) VALUES (@id, @source, @target, @type)
`); `);
const taskTx = db.transaction((all) => { const taskTx = db.transaction(({ tasks, links }) => {
db.prepare('DELETE FROM tasks').run(); db.prepare('DELETE FROM tasks').run();
db.prepare('DELETE FROM links').run(); db.prepare('DELETE FROM links').run();
for (const task of all.tasks) { for (const task of tasks) {
insertTask.run({ insertTask.run({
...task, ...task,
start: typeof task.start === 'object' ? new Date(task.start).toISOString() : task.start, start: typeof task.start === 'object' ? new Date(task.start).toISOString() : task.start,
end: typeof task.end === 'object' ? new Date(task.end).toISOString() : task.end, end: typeof task.end === 'object' ? new Date(task.end).toISOString() : task.end,
lazy: task.lazy ? 1 : 0, // ⚠️ SQLite ne supporte pas le type boolean natif lazy_loading: task.lazy_loading ? 1 : 0
}); });
} }
for (const link of links) {
for (const link of all.links) {
insertLink.run(link); insertLink.run(link);
} }
}); });

View File

@ -4,18 +4,31 @@
let tasks = []; let tasks = [];
let links = []; let links = [];
// 🗓️ Affichage : mois + jours
const scales = [ const scales = [
{ unit: 'month', step: 1, format: 'MMMM yyyy' }, { unit: 'month', step: 1, format: 'MMMM yyyy' },
{ unit: 'day', step: 1, format: 'd' } { unit: 'day', step: 1, format: 'd' }
]; ];
// 📋 Options : colonnes personnalisées
const options = {
taskList: {
visible: true, // ✅ Important
columns: [
{ id: 'text', label: 'Nom', value: 'text', width: 200 },
{ id: 'assignedTo', label: 'Assignée à', value: 'assignedTo', width: 150 }
]
}
};
onMount(async () => { onMount(async () => {
const res = await fetch('/api/tasks'); const res = await fetch('/api/tasks');
if (res.ok) { if (res.ok) {
const { tasks: loadedTasks, links: loadedLinks } = await res.json(); const { tasks: loadedTasks, links: loadedLinks } = await res.json();
tasks = loadedTasks.map(task => { tasks = loadedTasks.map(task => {
// Assure que start est une date ISO
const start = new Date(task.start); const start = new Date(task.start);
const duration = task.duration || task.estimation || 1; const duration = task.duration || task.estimation || 1;
@ -24,8 +37,9 @@
return { return {
...task, ...task,
start: start.toISOString().slice(0, 10), // "YYYY-MM-DD" start: start.toISOString().slice(0, 10),
end: end.toISOString().slice(0, 10) end: end.toISOString().slice(0, 10),
duration: duration
}; };
}); });
@ -34,9 +48,8 @@
console.error('Erreur de chargement des données depuis la base.'); console.error('Erreur de chargement des données depuis la base.');
} }
}); });
</script> </script>
<Willow> <Willow>
<Gantt {tasks} {links} {scales} /> <Gantt {tasks} {links} {scales} {options} />
</Willow> </Willow>

View File

@ -25,13 +25,14 @@
return { return {
id: index + 1, id: index + 1,
text: entry['Nom Ticket'], text: entry['Nom Ticket'],
start: start.toISOString().split('T')[0], // ⬅️ Converti en string start: start.toISOString().split('T')[0],
end: end.toISOString().split('T')[0], // ⬅️ Converti en string end: end.toISOString().split('T')[0],
duration: Number(entry['Estimation (j)']) || 1, duration: Number(entry['Estimation (j)']) || 1,
progress: Number(entry['RAF']) || 0, progress: Number(entry['RAF']) || 0,
type: 'task', type: 'task',
parent: null, parent: null,
lazy: false assignedTo: entry['assignedTo'] || '', // ✅ Ajout ici
lazy_loading: false // ✅ Correspond à la colonne renommée
}; };
}); });
@ -53,6 +54,7 @@
} }
</script> </script>
<h2>Importer un fichier CSV</h2> <h2>Importer un fichier CSV</h2>
<input type="file" accept=".csv" on:change={handleFile} /> <input type="file" accept=".csv" on:change={handleFile} />

View File

@ -0,0 +1,201 @@
<script lang="ts">
import Papa from 'papaparse';
import { onMount } from 'svelte';
import { writable, get } from 'svelte/store';
const planning = writable<any[]>([]);
const dates = writable<string[]>([]);
let file: File | null = null;
const saveMessage = writable('');
let timeout: ReturnType<typeof setTimeout>;
const joursFeries = [
'2025-01-01', '2025-04-21', '2025-05-01', '2025-05-08', '2025-05-29',
'2025-07-14', '2025-08-15', '2025-11-01', '2025-11-11', '2025-12-25'
];
function isWeekend(date: string) {
const d = new Date(date);
return d.getDay() === 0 || d.getDay() === 6;
}
function isJourFerie(date: string) {
return joursFeries.includes(date);
}
function formatDateFr(dateStr: string) {
const d = new Date(dateStr);
return d.toLocaleDateString('fr-FR', {
weekday: 'short',
day: '2-digit',
month: 'short'
});
}
function showSaveMessage(message: string = '✅ Sauvegarde enregistrée') {
saveMessage.set(message);
clearTimeout(timeout);
timeout = setTimeout(() => saveMessage.set(''), 2000);
}
onMount(async () => {
const res = await fetch('/api/planning');
if (res.ok) {
const data = await res.json();
if (data.length) {
const allDates = new Set<string>();
data.forEach(row => {
Object.keys(row.disponibilites).forEach(d => allDates.add(d));
});
const sortedDates = Array.from(allDates).sort();
dates.set(sortedDates);
planning.set(data);
}
}
});
async function handleFileUpload() {
if (!file) return;
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: async (result) => {
const rows = result.data as any[];
const colonnesDates = Object.keys(rows[0]).filter(k => /^\d/.test(k));
dates.set(colonnesDates);
const structured = rows.map(row => ({
ressource: row.ressource,
profil: row.profil,
disponibilites: colonnesDates.reduce((acc, date) => {
acc[date] = parseFloat(row[date] || 0);
return acc;
}, {} as Record<string, number>)
}));
planning.set(structured);
const payload = [];
for (const row of structured) {
for (const date of colonnesDates) {
payload.push({
ressource: row.ressource,
profil: row.profil,
date,
disponibilite: row.disponibilites[date]
});
}
}
await fetch('/api/planning', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
showSaveMessage();
}
});
}
async function updateCell(rowIndex: number, date: string, value: string) {
const val = parseFloat(value);
if (![0, 0.5, 1].includes(val)) return;
planning.update(current => {
current[rowIndex].disponibilites[date] = val;
return current;
});
const row = get(planning)[rowIndex];
await fetch('/api/planning', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{
ressource: row.ressource,
profil: row.profil,
date,
disponibilite: val
}
])
});
showSaveMessage();
}
function totalByDate(date: string) {
const rows = get(planning);
return rows.reduce((sum, r) => sum + (r.disponibilites[date] || 0), 0);
}
</script>
<h1 class="text-xl font-bold mb-4">📅 Planning Ressources</h1>
<input type="file" accept=".csv" on:change={(e) => file = e.target.files?.[0]} />
<button on:click={handleFileUpload} class="ml-2 bg-blue-600 text-white px-3 py-1 rounded">Charger</button>
{#if $planning.length > 0}
<table class="mt-6 border-collapse border w-full text-sm text-center">
<thead>
<tr>
<th class="border px-2 py-1 bg-white text-left">Ressource</th>
<th class="border px-2 py-1 bg-white text-left">Profil</th>
{#each $dates as date}
<th
class="border px-2 py-1"
class:bg-red-100={isJourFerie(date)}
class:bg-gray-100={!isJourFerie(date) && isWeekend(date)}
>
{formatDateFr(date)}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each $planning as row, i}
<tr>
<td class="border px-2 py-1 text-left">{row.ressource}</td>
<td class="border px-2 py-1 text-left">{row.profil}</td>
{#each $dates as date}
<td class="border p-0">
<input
type="number"
min="0"
max="1"
step="0.5"
value={row.disponibilites[date]}
class="w-full text-center py-1 bg-transparent"
on:change={(e) => updateCell(i, date, e.target.value)}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
<tfoot>
<tr>
<td class="border font-semibold text-right px-2 py-1" colspan="2">Total</td>
{#each $dates as date}
<td class="border px-2 py-1 font-semibold">
{totalByDate(date)}
</td>
{/each}
</tr>
</tfoot>
</table>
{/if}
{#if $saveMessage}
<div
class="fixed bottom-4 right-4 bg-green-600 text-white px-4 py-2 rounded shadow-lg transition-opacity duration-300"
>
{$saveMessage}
</div>
{/if}

View File

@ -1,7 +1,7 @@
ID Ticket,Nom Ticket,Estimation (j),Sprint,Statut,RAF ID Ticket,Nom Ticket,Estimation (j),Sprint,Statut,RAF,assignedTo
T-001,Authentification,5,Sprint 1,Terminé,0 T-001,Authentification,5,Sprint 1,Terminé,0,Simon BOYER
T-002,Page daccueil,3,Sprint 1,En cours,2 T-002,Page daccueil,3,Sprint 1,En cours,2,Simon BOYER
T-003,Système de recherche,4,Sprint 2,À faire,4 T-003,Système de recherche,4,Sprint 2,À faire,4,Fatima BROUM
T-005,Sprint 3,3,Sprint 1,En cours,2 T-005,Sprint 3,3,Sprint 1,En cours,2,Fatima BROUM
T-004,Configuration BO,4,Sprint 2,À faire,4 T-004,Configuration BO,4,Sprint 2,À faire,4,Fatima BROUM
T-006,Endpoint à configurer sur api,1,Sprint 2,À faire,1 T-006,Endpoint à configurer sur api,1,Sprint 2,À faire,1,Fatima BROUM
1 ID Ticket Nom Ticket Estimation (j) Sprint Statut RAF assignedTo
2 T-001 Authentification 5 Sprint 1 Terminé 0 Simon BOYER
3 T-002 Page d’accueil 3 Sprint 1 En cours 2 Simon BOYER
4 T-003 Système de recherche 4 Sprint 2 À faire 4 Fatima BROUM
5 T-005 Sprint 3 3 Sprint 1 En cours 2 Fatima BROUM
6 T-004 Configuration BO 4 Sprint 2 À faire 4 Fatima BROUM
7 T-006 Endpoint à configurer sur api 1 Sprint 2 À faire 1 Fatima BROUM