Ajouter main.html
This commit is contained in:
commit
d73ad532d8
1 changed files with 741 additions and 0 deletions
741
main.html
Normal file
741
main.html
Normal file
|
@ -0,0 +1,741 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Deployment Planner</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
/* Custom scrollbar for a better dark-theme experience */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1f2937; /* gray-800 */
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #374151; /* gray-700 */
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4b5563; /* gray-600 */
|
||||||
|
}
|
||||||
|
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
||||||
|
filter: invert(0.8);
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 text-gray-200 min-h-screen font-sans antialiased">
|
||||||
|
|
||||||
|
<div id="app-container">
|
||||||
|
<!-- App content will be rendered here by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals Container -->
|
||||||
|
<div id="modal-container">
|
||||||
|
<!-- Modals will be rendered here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// --- DATA MODELS & CONSTANTS ---
|
||||||
|
const ActionType = {
|
||||||
|
COMPONENT: "Déploiement d'un composant technique",
|
||||||
|
SECRET: "Changement de secret",
|
||||||
|
DMN: "Changement de DMN",
|
||||||
|
DDL: "DDL",
|
||||||
|
DML: "DML"
|
||||||
|
};
|
||||||
|
|
||||||
|
const ActionMoment = {
|
||||||
|
BEFORE: "Avant la release",
|
||||||
|
DURING: "Pendant la release",
|
||||||
|
AFTER: "Après la release"
|
||||||
|
};
|
||||||
|
|
||||||
|
const ItemStatus = {
|
||||||
|
TODO: "À faire",
|
||||||
|
IN_PROGRESS: "En cours",
|
||||||
|
DONE: "Terminé",
|
||||||
|
FAILED: "Échoué",
|
||||||
|
NA: "N/A"
|
||||||
|
};
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ id: 'actions', name: "Liste d'actions" },
|
||||||
|
{ id: 'quality', name: 'Quality Checks' },
|
||||||
|
{ id: 'overview', name: 'Aperçus (Jira/BPMN)' },
|
||||||
|
{ id: 'detailed', name: 'Plan Détaillé' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- APPLICATION STATE ---
|
||||||
|
let state = {
|
||||||
|
view: 'list', // 'list' or 'detail'
|
||||||
|
deploymentPlans: [],
|
||||||
|
selectedPlan: null,
|
||||||
|
activeTab: 'actions',
|
||||||
|
isActionModalOpen: false,
|
||||||
|
isStatusModalOpen: false,
|
||||||
|
currentAction: null,
|
||||||
|
lastSaveMessage: 'Jamais'
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- UTILITY FUNCTIONS ---
|
||||||
|
function formatDate(isoString) {
|
||||||
|
return new Date(isoString).toLocaleString('fr-FR', { dateStyle: 'medium', timeStyle: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSafeUrl(url) {
|
||||||
|
if (url && (url.startsWith('https://') || url.startsWith('http://'))) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
return 'about:blank';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyStatusGrid() {
|
||||||
|
return {
|
||||||
|
onss: { int: ItemStatus.NA, acc: ItemStatus.NA, prod: ItemStatus.NA },
|
||||||
|
onem: { int: ItemStatus.NA, acc: ItemStatus.NA, prod: ItemStatus.NA },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RENDERING ENGINE ---
|
||||||
|
const appContainer = document.getElementById('app-container');
|
||||||
|
const modalContainer = document.getElementById('modal-container');
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
// Unbind all events before re-rendering to avoid duplicates
|
||||||
|
unbindEvents();
|
||||||
|
|
||||||
|
appContainer.innerHTML = `
|
||||||
|
<header class="bg-gray-800/50 backdrop-blur-sm sticky top-0 z-20 shadow-lg border-b border-sky-500/20">
|
||||||
|
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg class="h-8 w-8 text-sky-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-1.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25A2.25 2.25 0 015.25 3h4.5M15 3.75l3 3m0 0l-3 3m3-3h-9" /></svg>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-100">Deployment Planner</h1>
|
||||||
|
</div>
|
||||||
|
${state.view === 'detail' ? `
|
||||||
|
<button id="back-to-list-btn" class="flex items-center space-x-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg shadow-md transition-colors duration-200">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path fill-rule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clip-rule="evenodd" /></svg>
|
||||||
|
<span>Retour à la liste</span>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="container mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
${state.view === 'list' ? renderListView() : renderDetailView()}
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modalContainer.innerHTML = `
|
||||||
|
${renderActionModal()}
|
||||||
|
${renderStatusModal()}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Re-bind events after rendering
|
||||||
|
bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VIEW TEMPLATES ---
|
||||||
|
function renderListView() {
|
||||||
|
const plansHtml = state.deploymentPlans.map(plan => `
|
||||||
|
<div class="bg-gray-800 rounded-xl shadow-lg overflow-hidden border border-gray-700 hover:border-sky-500 transition-all duration-300 transform hover:-translate-y-1">
|
||||||
|
<div class="p-6">
|
||||||
|
<h3 class="text-xl font-bold text-sky-400 mb-2">${plan.name}</h3>
|
||||||
|
<p class="text-sm text-gray-400">Dernière modification: ${formatDate(plan.lastModified)}</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">Actions: ${plan.actions.length}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-700/50 p-4 flex justify-end space-x-3">
|
||||||
|
<button data-action="open-plan" data-plan-id="${plan.id}" class="px-4 py-2 bg-sky-500 hover:bg-sky-600 text-white rounded-lg text-sm font-semibold transition-colors duration-200">Ouvrir</button>
|
||||||
|
<button data-action="delete-plan" data-plan-id="${plan.id}" class="p-2 bg-red-800/50 hover:bg-red-700/50 text-red-400 hover:text-white rounded-full transition-colors duration-200">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.58.22-2.365.468a.75.75 0 10.23 1.482l.149-.046A12.705 12.705 0 0110 8a12.705 12.705 0 015.986-2.299l.149.046a.75.75 0 10.23-1.482A13.455 13.455 0 0014 4.193v-.443A2.75 2.75 0 0011.25 1h-2.5zM10 10a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 10zM8.75 11.5a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5zM11.25 11.5a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z" clip-rule="evenodd" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-3xl font-semibold">Plans de Déploiement</h2>
|
||||||
|
<button id="new-plan-btn" class="flex items-center space-x-2 bg-sky-500 hover:bg-sky-600 text-white font-bold py-2 px-4 rounded-lg shadow-lg hover:shadow-sky-500/50 transition-all duration-300">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" /></svg>
|
||||||
|
<span>Nouveau Plan</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${state.deploymentPlans.length > 0 ? `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">${plansHtml}</div>` : `
|
||||||
|
<div class="text-center py-16 px-6 bg-gray-800 rounded-lg border-2 border-dashed border-gray-700">
|
||||||
|
<svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path></svg>
|
||||||
|
<h3 class="mt-2 text-lg font-medium text-gray-300">Aucun plan de déploiement</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Commencez par créer votre premier plan.</p>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetailView() {
|
||||||
|
const plan = state.selectedPlan;
|
||||||
|
if (!plan) return '<div>Erreur: Aucun plan sélectionné.</div>';
|
||||||
|
|
||||||
|
const tabsHtml = TABS.map(tab => `
|
||||||
|
<button data-action="change-tab" data-tab-id="${tab.id}"
|
||||||
|
class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200
|
||||||
|
${state.activeTab === tab.id ? 'border-sky-500 text-sky-400' : 'border-transparent text-gray-400 hover:text-gray-200 hover:border-gray-500'}">
|
||||||
|
${tab.name}
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="space-y-8">
|
||||||
|
<!-- Plan Header -->
|
||||||
|
<div class="p-6 bg-gray-800 rounded-xl shadow-lg border border-gray-700 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="plan-name" class="text-sm font-medium text-gray-400">Nom du Plan</label>
|
||||||
|
<input data-action="update-plan-field" data-field="name" id="plan-name" type="text" value="${plan.name}" class="text-3xl font-bold bg-transparent border-0 border-b-2 border-gray-600 focus:border-sky-500 focus:ring-0 transition w-full sm:w-auto text-gray-100 p-1" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<p class="text-sm text-gray-400 whitespace-nowrap">Dernière sauvegarde : ${state.lastSaveMessage}</p>
|
||||||
|
<button id="save-plan-btn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg shadow-lg transition-colors duration-200">Sauvegarder</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div class="border-b border-gray-700">
|
||||||
|
<nav class="-mb-px flex space-x-8" aria-label="Tabs">${tabsHtml}</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div class="bg-gray-800/50 p-6 rounded-lg border border-gray-700">
|
||||||
|
${renderTabContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTabContent() {
|
||||||
|
switch(state.activeTab) {
|
||||||
|
case 'actions': return renderActionsTab();
|
||||||
|
case 'quality': return renderQualityTab();
|
||||||
|
case 'overview': return renderOverviewTab();
|
||||||
|
case 'detailed': return renderDetailedTab();
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TAB CONTENT RENDERERS ---
|
||||||
|
function renderActionsTab() {
|
||||||
|
const actions = state.selectedPlan.actions;
|
||||||
|
const actionsHtml = actions.map((action, index) => `
|
||||||
|
<tr class="hover:bg-gray-700/50">
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-100">${action.sujet}</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-300">${action.type}</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-300">${action.moment}</td>
|
||||||
|
<td class="px-4 py-4 text-center">
|
||||||
|
<button data-action="open-status-modal" data-action-id="${action.id}" class="p-2 bg-gray-700 hover:bg-gray-600 rounded-md">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 text-sky-400"><path fill-rule="evenodd" d="M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z" clip-rule="evenodd" /></svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-sky-400 space-x-3">
|
||||||
|
${action.url ? `<a href="${action.url}" target="_blank" class="hover:underline">URL</a>` : ''}
|
||||||
|
${action.jira ? `<a href="${action.jira}" target="_blank" class="hover:underline">Jira</a>` : ''}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
||||||
|
<button data-action="edit-action" data-action-id="${action.id}" class="text-sky-400 hover:text-sky-300">Modifier</button>
|
||||||
|
<button data-action="remove-action" data-index="${index}" class="text-red-500 hover:text-red-400">Suppr.</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Liste d'actions</h3>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-700">
|
||||||
|
<thead class="bg-gray-800">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Sujet</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Type</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Moment</th>
|
||||||
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-400 uppercase tracking-wider">Statut</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Liens</th>
|
||||||
|
<th class="relative px-4 py-3"><span class="sr-only">Edit</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-gray-800/50 divide-y divide-gray-700">${actionsHtml}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button data-action="add-action" class="text-sky-400 hover:text-sky-300 font-semibold">+ Ajouter une action</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQualityTab() {
|
||||||
|
const plan = state.selectedPlan;
|
||||||
|
const itemStatusOptions = Object.values(ItemStatus).map(s => `<option value="${s}">${s}</option>`).join('');
|
||||||
|
|
||||||
|
const businessChecks = plan.qualityChecks.business.map((check, i) => `
|
||||||
|
<div class="flex items-center gap-3 p-2 bg-gray-900/50 rounded-md">
|
||||||
|
<input type="text" value="${check.sujet}" data-action="update-quality-check" data-type="business" data-index="${i}" data-field="sujet" class="flex-grow bg-transparent focus:ring-0 border-0 border-b border-gray-600 focus:border-sky-500 transition">
|
||||||
|
<select data-action="update-quality-check" data-type="business" data-index="${i}" data-field="statut" class="bg-gray-700 border-gray-600 rounded-md focus:ring-sky-500 focus:border-sky-500 text-sm">
|
||||||
|
${Object.values(ItemStatus).map(s => `<option value="${s}" ${check.statut === s ? 'selected' : ''}>${s}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
<button data-action="remove-quality-check" data-type="business" data-index="${i}" class="text-red-500 hover:text-red-400 p-1"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /></svg></button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const technicalChecks = plan.qualityChecks.technical.map((check, i) => `
|
||||||
|
<div class="flex items-center gap-3 p-2 bg-gray-900/50 rounded-md">
|
||||||
|
<input type="text" value="${check.sujet}" data-action="update-quality-check" data-type="technical" data-index="${i}" data-field="sujet" class="flex-grow bg-transparent focus:ring-0 border-0 border-b border-gray-600 focus:border-sky-500 transition">
|
||||||
|
<select data-action="update-quality-check" data-type="technical" data-index="${i}" data-field="statut" class="bg-gray-700 border-gray-600 rounded-md focus:ring-sky-500 focus:border-sky-500 text-sm">
|
||||||
|
${Object.values(ItemStatus).map(s => `<option value="${s}" ${check.statut === s ? 'selected' : ''}>${s}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
<button data-action="remove-quality-check" data-type="technical" data-index="${i}" class="text-red-500 hover:text-red-400 p-1"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /></svg></button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<!-- Business Validations -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-lg font-semibold mb-3">Validations Business (ACC)</h4>
|
||||||
|
<div class="space-y-3">${businessChecks}</div>
|
||||||
|
<button data-action="add-quality-check" data-type="business" class="text-sky-400 hover:text-sky-300 font-semibold mt-2">+ Ajouter une validation business</button>
|
||||||
|
</div>
|
||||||
|
<!-- Technical Validations -->
|
||||||
|
<div>
|
||||||
|
<h4 class="text-lg font-semibold mb-3">Validations Techniques</h4>
|
||||||
|
<div class="space-y-3">${technicalChecks}</div>
|
||||||
|
<button data-action="add-quality-check" data-type="technical" class="text-sky-400 hover:text-sky-300 font-semibold mt-2">+ Ajouter une validation technique</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOverviewTab() {
|
||||||
|
const plan = state.selectedPlan;
|
||||||
|
return `
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- JIRA -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Aperçu Tickets JIRA</h3>
|
||||||
|
<input type="url" value="${plan.jiraOverviewUrl}" data-action="update-plan-field" data-field="jiraOverviewUrl" placeholder="https://your.jira.com/..." class="w-full bg-gray-900 border-gray-600 rounded-md mb-4">
|
||||||
|
<iframe src="${getSafeUrl(plan.jiraOverviewUrl)}" class="w-full h-96 rounded-lg border border-gray-700"></iframe>
|
||||||
|
</div>
|
||||||
|
<!-- BPMN -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Aperçu Visuel (BPMN)</h3>
|
||||||
|
<input type="url" value="${plan.bpmnUrl}" data-action="update-plan-field" data-field="bpmnUrl" placeholder="https://app.diagrams.net/..." class="w-full bg-gray-900 border-gray-600 rounded-md mb-4">
|
||||||
|
<iframe src="${getSafeUrl(plan.bpmnUrl)}" class="w-full h-96 rounded-lg border border-gray-700"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetailedTab() {
|
||||||
|
const plan = state.selectedPlan;
|
||||||
|
const actionOptions = plan.actions.map(act => `<option value="${act.id}">${act.sujet}</option>`).join('');
|
||||||
|
|
||||||
|
let html = '<div class="space-y-8">';
|
||||||
|
for(const env of ['acc', 'prod']) {
|
||||||
|
html += `<div><h3 class="text-2xl font-bold uppercase mb-4 border-b-2 border-sky-500 pb-2">${env}</h3>`;
|
||||||
|
html += '<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">';
|
||||||
|
for(const tenant of ['onss', 'onem']) {
|
||||||
|
html += `<div class="space-y-4"><h4 class="text-xl font-semibold text-sky-400 uppercase">${tenant}</h4>`;
|
||||||
|
plan.detailedPlan[env][tenant].forEach((section, i) => {
|
||||||
|
const disabled = section.isFinished ? 'disabled' : '';
|
||||||
|
const detailedActionsHtml = section.actions.map((dAction, j) => `
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 p-2 bg-gray-800 rounded">
|
||||||
|
<select data-action="update-detailed-action" data-env="${env}" data-tenant="${tenant}" data-section-index="${i}" data-action-index="${j}" data-field="actionId" class="bg-gray-700 border-gray-600 rounded-md text-sm" ${disabled}>
|
||||||
|
${plan.actions.map(act => `<option value="${act.id}" ${dAction.actionId === act.id ? 'selected':''}>${act.sujet}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
<input type="text" value="${dAction.responsable}" data-action="update-detailed-action" data-env="${env}" data-tenant="${tenant}" data-section-index="${i}" data-action-index="${j}" data-field="responsable" placeholder="Responsable" class="bg-gray-700 border-gray-600 rounded-md text-sm" ${disabled}>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="datetime-local" value="${dAction.moment}" data-action="update-detailed-action" data-env="${env}" data-tenant="${tenant}" data-section-index="${i}" data-action-index="${j}" data-field="moment" class="bg-gray-700 border-gray-600 rounded-md text-sm w-full" ${disabled}>
|
||||||
|
<button data-action="remove-detailed-action" data-env="${env}" data-tenant="${tenant}" data-section-index="${i}" data-action-index="${j}" class="ml-2 text-red-500 hover:text-red-400" ${disabled}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
html += `
|
||||||
|
<div class="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<input type="text" value="${section.title}" data-action="update-detailed-section" data-env="${env}" data-tenant="${tenant}" data-index="${i}" data-field="title" class="text-lg font-bold bg-transparent focus:ring-0 border-0 border-b border-gray-600 focus:border-sky-500 transition" ${disabled}>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button data-action="duplicate-detailed-section" data-env="${env}" data-tenant="${tenant}" data-index="${i}" title="Dupliquer" class="p-1 text-gray-400 hover:text-white" ${disabled}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path d="M7 3.5A1.5 1.5 0 018.5 2h3.879a1.5 1.5 0 011.06.44l3.122 3.121A1.5 1.5 0 0117 6.621V16.5a1.5 1.5 0 01-1.5 1.5h-7A1.5 1.5 0 017 16.5v-13z" /><path d="M4.5 6A1.5 1.5 0 003 7.5v10A1.5 1.5 0 004.5 19h7a1.5 1.5 0 001.5-1.5v-1.336a.75.75 0 00-1.5 0V17.5a.5.5 0 01-.5.5h-7a.5.5 0 01-.5-.5v-10a.5.5 0 01.5-.5H5V6a.5.5 0 01-.5-.5z" /></svg></button>
|
||||||
|
<button data-action="remove-detailed-section" data-env="${env}" data-tenant="${tenant}" data-index="${i}" title="Supprimer" class="p-1 text-red-500 hover:text-red-400"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"><path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.58.22-2.365.468a.75.75 0 10.23 1.482l.149-.046A12.705 12.705 0 0110 8a12.705 12.705 0 015.986-2.299l.149.046a.75.75 0 10.23-1.482A13.455 13.455 0 0014 4.193v-.443A2.75 2.75 0 0011.25 1h-2.5zM10 10a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 10zM8.75 11.5a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5zM11.25 11.5a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z" clip-rule="evenodd" /></svg></button>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" ${section.isFinished ? 'checked':''} data-action="update-detailed-section" data-env="${env}" data-tenant="${tenant}" data-index="${i}" data-field="isFinished" class="h-4 w-4 rounded bg-gray-700 border-gray-600 text-sky-600 focus:ring-sky-500">
|
||||||
|
<label class="ml-2 text-sm text-gray-400">Terminé</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">${detailedActionsHtml}</div>
|
||||||
|
<button data-action="add-detailed-action" data-env="${env}" data-tenant="${tenant}" data-section-index="${i}" class="text-sky-400 hover:text-sky-300 font-semibold mt-3 text-sm" ${disabled}>+ Ajouter une action détaillée</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += `<button data-action="add-detailed-section" data-env="${env}" data-tenant="${tenant}" class="w-full text-center py-2 border-2 border-dashed border-gray-700 hover:bg-gray-700/50 rounded-lg transition">+ Ajouter un plan détaillé</button></div>`;
|
||||||
|
}
|
||||||
|
html += '</div></div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MODAL TEMPLATES ---
|
||||||
|
function renderActionModal() {
|
||||||
|
if (!state.isActionModalOpen) return '';
|
||||||
|
const action = state.currentAction;
|
||||||
|
const actionTypeOptions = Object.values(ActionType).map(t => `<option value="${t}" ${action.type === t ? 'selected':''}>${t}</option>`).join('');
|
||||||
|
const actionMomentOptions = Object.values(ActionMoment).map(m => `<option value="${m}" ${action.moment === m ? 'selected':''}>${m}</option>`).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="action-modal" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-2xl p-8 w-full max-w-2xl border border-gray-700" id="action-modal-content">
|
||||||
|
<h3 class="text-2xl font-bold mb-6">${action.id ? 'Modifier' : 'Ajouter'} une Action</h3>
|
||||||
|
<form id="action-form" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Sujet</label>
|
||||||
|
<input name="sujet" type="text" value="${action.sujet}" required class="mt-1 w-full bg-gray-900 border-gray-600 rounded-md">
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Type</label>
|
||||||
|
<select name="type" class="mt-1 w-full bg-gray-900 border-gray-600 rounded-md">${actionTypeOptions}</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Moment</label>
|
||||||
|
<select name="moment" class="mt-1 w-full bg-gray-900 border-gray-600 rounded-md">${actionMomentOptions}</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">URL</label>
|
||||||
|
<input name="url" type="url" value="${action.url}" class="mt-1 w-full bg-gray-900 border-gray-600 rounded-md">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">JIRA</label>
|
||||||
|
<input name="jira" type="url" value="${action.jira}" class="mt-1 w-full bg-gray-900 border-gray-600 rounded-md">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-400">Remarque</label>
|
||||||
|
<textarea name="remarque" rows="3" class="mt-1 w-full bg-gray-900 border-gray-600 rounded-md">${action.remarque}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-4 pt-4">
|
||||||
|
<button type="button" data-action="close-action-modal" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg">Annuler</button>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-sky-500 hover:bg-sky-600 text-white font-bold rounded-lg">Sauvegarder</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatusModal() {
|
||||||
|
if (!state.isStatusModalOpen) return '';
|
||||||
|
const action = state.currentAction;
|
||||||
|
|
||||||
|
let gridHtml = '';
|
||||||
|
for(const env of ['int', 'acc', 'prod']) {
|
||||||
|
gridHtml += `<div class="font-bold text-right pr-4 uppercase text-gray-400">${env}</div>`;
|
||||||
|
for(const tenant of ['onss', 'onem']) {
|
||||||
|
gridHtml += `<select data-action="update-status" data-tenant="${tenant}" data-env="${env}" class="bg-gray-700 border-gray-600 rounded-md focus:ring-sky-500 focus:border-sky-500 text-sm">`;
|
||||||
|
gridHtml += Object.values(ItemStatus).map(s => `<option value="${s}" ${action.statut[tenant][env] === s ? 'selected' : ''}>${s}</option>`).join('');
|
||||||
|
gridHtml += `</select>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="status-modal" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-2xl p-8 border border-gray-700" id="status-modal-content">
|
||||||
|
<h3 class="text-2xl font-bold mb-2">Statuts pour :</h3>
|
||||||
|
<p class="text-sky-400 mb-6">${action.sujet}</p>
|
||||||
|
<div class="grid grid-cols-3 gap-x-12 gap-y-6">
|
||||||
|
<div></div>
|
||||||
|
<div class="font-bold text-center text-gray-400">ONSS</div>
|
||||||
|
<div class="font-bold text-center text-gray-400">ONEM</div>
|
||||||
|
${gridHtml}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-8">
|
||||||
|
<button data-action="close-status-modal" class="px-4 py-2 bg-sky-500 hover:bg-sky-600 text-white font-bold rounded-lg">Fermer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- EVENT HANDLING ---
|
||||||
|
|
||||||
|
// Centralized event handler
|
||||||
|
function handleEvent(event) {
|
||||||
|
const target = event.target.closest('[data-action]');
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const action = target.dataset.action;
|
||||||
|
const ds = target.dataset; // shortcut for dataset
|
||||||
|
|
||||||
|
// --- PLAN LIST ACTIONS ---
|
||||||
|
if (action === 'open-plan') {
|
||||||
|
const plan = state.deploymentPlans.find(p => p.id === ds.planId);
|
||||||
|
if (plan) {
|
||||||
|
// Deep copy for editing
|
||||||
|
state.selectedPlan = JSON.parse(JSON.stringify(plan));
|
||||||
|
state.view = 'detail';
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action === 'delete-plan') {
|
||||||
|
state.deploymentPlans = state.deploymentPlans.filter(p => p.id !== ds.planId);
|
||||||
|
console.log(`Plan with id ${ds.planId} deleted.`);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HEADER & DETAIL VIEW ACTIONS ---
|
||||||
|
if (action === 'back-to-list') {
|
||||||
|
state.selectedPlan = null;
|
||||||
|
state.view = 'list';
|
||||||
|
state.activeTab = 'actions';
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if (action === 'new-plan') {
|
||||||
|
handleCreateNewPlan();
|
||||||
|
}
|
||||||
|
if (action === 'save-plan') {
|
||||||
|
handleSavePlan();
|
||||||
|
}
|
||||||
|
if (action === 'update-plan-field') {
|
||||||
|
state.selectedPlan[ds.field] = event.target.value;
|
||||||
|
// Re-render only if it's a URL to update iframe
|
||||||
|
if(ds.field === 'jiraOverviewUrl' || ds.field === 'bpmnUrl') render();
|
||||||
|
}
|
||||||
|
if (action === 'change-tab') {
|
||||||
|
state.activeTab = ds.tabId;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ACTIONS TAB ---
|
||||||
|
if (action === 'add-action') {
|
||||||
|
state.currentAction = { id: '', sujet: '', type: ActionType.COMPONENT, moment: ActionMoment.DURING, statut: createEmptyStatusGrid(), remarque: '', url: '', jira: '' };
|
||||||
|
state.isActionModalOpen = true;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if (action === 'edit-action') {
|
||||||
|
const actionToEdit = state.selectedPlan.actions.find(a => a.id === ds.actionId);
|
||||||
|
if (actionToEdit) {
|
||||||
|
state.currentAction = JSON.parse(JSON.stringify(actionToEdit));
|
||||||
|
state.isActionModalOpen = true;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action === 'remove-action') {
|
||||||
|
state.selectedPlan.actions.splice(parseInt(ds.index, 10), 1);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- QUALITY TAB ---
|
||||||
|
if (action === 'add-quality-check') {
|
||||||
|
const newCheck = { id: crypto.randomUUID(), sujet: '', statut: ItemStatus.TODO };
|
||||||
|
state.selectedPlan.qualityChecks[ds.type].push(newCheck);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if (action === 'remove-quality-check') {
|
||||||
|
state.selectedPlan.qualityChecks[ds.type].splice(parseInt(ds.index, 10), 1);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if (action === 'update-quality-check') {
|
||||||
|
state.selectedPlan.qualityChecks[ds.type][ds.index][ds.field] = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DETAILED TAB ---
|
||||||
|
if (action === 'add-detailed-section') {
|
||||||
|
const newSection = { id: crypto.randomUUID(), title: `Nouveau Plan ${ds.env.toUpperCase()} ${ds.tenant.toUpperCase()}`, isFinished: false, actions: [] };
|
||||||
|
state.selectedPlan.detailedPlan[ds.env][ds.tenant].push(newSection);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if (action === 'remove-detailed-section') {
|
||||||
|
state.selectedPlan.detailedPlan[ds.env][ds.tenant].splice(parseInt(ds.index), 1);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if(action === 'duplicate-detailed-section') {
|
||||||
|
const originalSection = state.selectedPlan.detailedPlan[ds.env][ds.tenant][ds.index];
|
||||||
|
const newSection = JSON.parse(JSON.stringify(originalSection));
|
||||||
|
newSection.id = crypto.randomUUID();
|
||||||
|
newSection.title = `${originalSection.title} (Copie)`;
|
||||||
|
newSection.isFinished = false;
|
||||||
|
state.selectedPlan.detailedPlan[ds.env][ds.tenant].splice(parseInt(ds.index) + 1, 0, newSection);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if(action === 'update-detailed-section') {
|
||||||
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||||
|
state.selectedPlan.detailedPlan[ds.env][ds.tenant][ds.index][ds.field] = value;
|
||||||
|
if(ds.field === 'isFinished') render(); // re-render to disable/enable inputs
|
||||||
|
}
|
||||||
|
if(action === 'add-detailed-action') {
|
||||||
|
const newAction = { id: crypto.randomUUID(), actionId: state.selectedPlan?.actions[0]?.id || '', responsable: '', moment: new Date().toISOString().slice(0, 16) };
|
||||||
|
state.selectedPlan.detailedPlan[ds.env][ds.tenant][ds.sectionIndex].actions.push(newAction);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if(action === 'remove-detailed-action') {
|
||||||
|
state.selectedPlan.detailedPlan[ds.env][ds.tenant][ds.sectionIndex].actions.splice(parseInt(ds.actionIndex), 1);
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if(action === 'update-detailed-action') {
|
||||||
|
state.selectedPlan.detailedPlan[ds.env][ds.tenant][ds.sectionIndex].actions[ds.actionIndex][ds.field] = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MODAL ACTIONS ---
|
||||||
|
if (action === 'close-action-modal' || event.target.id === 'action-modal') {
|
||||||
|
state.isActionModalOpen = false;
|
||||||
|
state.currentAction = null;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if (action === 'close-status-modal' || event.target.id === 'status-modal') {
|
||||||
|
const actionToUpdate = state.selectedPlan.actions.find(a => a.id === state.currentAction.id);
|
||||||
|
if(actionToUpdate) actionToUpdate.statut = state.currentAction.statut;
|
||||||
|
state.isStatusModalOpen = false;
|
||||||
|
state.currentAction = null;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
if(action === 'open-status-modal') {
|
||||||
|
const actionToUpdate = state.selectedPlan.actions.find(a => a.id === ds.actionId);
|
||||||
|
if(actionToUpdate) {
|
||||||
|
state.currentAction = JSON.parse(JSON.stringify(actionToUpdate));
|
||||||
|
state.isStatusModalOpen = true;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(action === 'update-status') {
|
||||||
|
state.currentAction.statut[ds.tenant][ds.env] = event.target.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleActionFormSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(event.target);
|
||||||
|
const actionData = state.currentAction;
|
||||||
|
|
||||||
|
actionData.sujet = formData.get('sujet');
|
||||||
|
actionData.type = formData.get('type');
|
||||||
|
actionData.moment = formData.get('moment');
|
||||||
|
actionData.url = formData.get('url');
|
||||||
|
actionData.jira = formData.get('jira');
|
||||||
|
actionData.remarque = formData.get('remarque');
|
||||||
|
|
||||||
|
const index = state.selectedPlan.actions.findIndex(a => a.id === actionData.id);
|
||||||
|
if (index > -1) {
|
||||||
|
state.selectedPlan.actions[index] = actionData;
|
||||||
|
} else {
|
||||||
|
actionData.id = crypto.randomUUID();
|
||||||
|
state.selectedPlan.actions.push(actionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isActionModalOpen = false;
|
||||||
|
state.currentAction = null;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TOP-LEVEL ACTION HANDLERS ---
|
||||||
|
function handleSavePlan() {
|
||||||
|
const planToSave = state.selectedPlan;
|
||||||
|
if (planToSave) {
|
||||||
|
planToSave.lastModified = new Date().toISOString();
|
||||||
|
|
||||||
|
console.log("--- SAVING PLAN TO SERVER ---");
|
||||||
|
console.log(JSON.stringify(planToSave, null, 2));
|
||||||
|
console.log("--- SAVE COMPLETE ---");
|
||||||
|
|
||||||
|
const index = state.deploymentPlans.findIndex(p => p.id === planToSave.id);
|
||||||
|
if (index > -1) {
|
||||||
|
state.deploymentPlans[index] = { ...planToSave };
|
||||||
|
}
|
||||||
|
state.lastSaveMessage = new Date().toLocaleTimeString();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateNewPlan() {
|
||||||
|
const newPlan = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
name: "Nouveau Plan de Déploiement",
|
||||||
|
lastModified: new Date().toISOString(),
|
||||||
|
jiraOverviewUrl: '',
|
||||||
|
bpmnUrl: '',
|
||||||
|
actions: [],
|
||||||
|
qualityChecks: { business: [], technical: [] },
|
||||||
|
detailedPlan: { acc: { onss: [], onem: [] }, prod: { onss: [], onem: [] } },
|
||||||
|
};
|
||||||
|
state.deploymentPlans.push(newPlan);
|
||||||
|
state.selectedPlan = JSON.parse(JSON.stringify(newPlan));
|
||||||
|
state.view = 'detail';
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- EVENT BINDING ---
|
||||||
|
let mainClickListener, mainInputListener, formSubmitListener;
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
// Use event delegation on the root container for performance
|
||||||
|
mainClickListener = handleEvent;
|
||||||
|
document.body.addEventListener('click', mainClickListener);
|
||||||
|
|
||||||
|
// For live input updates that don't need a full re-render
|
||||||
|
mainInputListener = handleEvent;
|
||||||
|
document.body.addEventListener('input', mainInputListener);
|
||||||
|
|
||||||
|
const actionForm = document.getElementById('action-form');
|
||||||
|
if (actionForm) {
|
||||||
|
formSubmitListener = handleActionFormSubmit;
|
||||||
|
actionForm.addEventListener('submit', formSubmitListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindEvents() {
|
||||||
|
if (mainClickListener) document.body.removeEventListener('click', mainClickListener);
|
||||||
|
if (mainInputListener) document.body.removeEventListener('input', mainInputListener);
|
||||||
|
const actionForm = document.getElementById('action-form');
|
||||||
|
if (actionForm && formSubmitListener) {
|
||||||
|
actionForm.removeEventListener('submit', formSubmitListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- INITIALIZATION ---
|
||||||
|
function init() {
|
||||||
|
console.log("Initializing Deployment Planner...");
|
||||||
|
state.deploymentPlans = getMockData();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMockData() {
|
||||||
|
const emptyStatusGrid = createEmptyStatusGrid();
|
||||||
|
return [{
|
||||||
|
id: 'plan-1',
|
||||||
|
name: "Déploiement Q3 2025 - Microservice 'Orion'",
|
||||||
|
lastModified: "2025-09-12T10:00:00Z",
|
||||||
|
jiraOverviewUrl: "",
|
||||||
|
bpmnUrl: "",
|
||||||
|
actions: [
|
||||||
|
{ id: 'act-1', sujet: 'Déployer API Gateway v1.2', type: ActionType.COMPONENT, moment: ActionMoment.DURING, statut: { ...emptyStatusGrid, onss: {...emptyStatusGrid.onss, int: ItemStatus.DONE} }, remarque: 'Nécessite un redémarrage du pod', url: 'http://example.com/docs/api-gateway', jira: 'http://jira.com/OR-123' },
|
||||||
|
{ id: 'act-2', sujet: 'Mettre à jour le secret DB_PASSWORD', type: ActionType.SECRET, moment: ActionMoment.BEFORE, statut: { ...emptyStatusGrid }, remarque: '', url: '', jira: 'http://jira.com/OR-124' }
|
||||||
|
],
|
||||||
|
qualityChecks: {
|
||||||
|
business: [{ id: 'qc-b-1', sujet: 'Valider le flux de commande principal', statut: ItemStatus.TODO }],
|
||||||
|
technical: [{ id: 'qc-t-1', sujet: 'Tests de performance > 100 req/s', statut: ItemStatus.TODO }]
|
||||||
|
},
|
||||||
|
detailedPlan: {
|
||||||
|
acc: {
|
||||||
|
onss: [{ id: 'dp-acc-onss-1', title: 'Déploiement ACC ONSS', isFinished: false, actions: [{id: 'dpa-1', actionId: 'act-1', responsable: 'John Doe', moment: '2025-09-15T09:00'}] }],
|
||||||
|
onem: []
|
||||||
|
},
|
||||||
|
prod: { onss: [], onem: [] }
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue