1263 lines
46 KiB
JavaScript
Executable File
1263 lines
46 KiB
JavaScript
Executable File
/**
|
|
* Linux BenchTools - Peripherals Management
|
|
*/
|
|
|
|
// Global state
|
|
let currentPage = 1;
|
|
let pageSize = 10; // Items per page
|
|
let totalPages = 1;
|
|
let sortBy = 'date_creation';
|
|
let sortOrder = 'desc';
|
|
let filterType = '';
|
|
let filterLocation = '';
|
|
let filterEtat = '';
|
|
let searchQuery = '';
|
|
let peripheralTypes = [];
|
|
let locations = [];
|
|
let devices = [];
|
|
let hosts = [];
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadStatistics();
|
|
loadPeripheralTypes();
|
|
loadLocations();
|
|
loadStockageLocations();
|
|
loadHosts();
|
|
loadBoutiques();
|
|
loadDevices();
|
|
loadPeripherals();
|
|
initStarRating();
|
|
|
|
const utilisationSelect = document.getElementById('utilisation');
|
|
if (utilisationSelect) {
|
|
utilisationSelect.addEventListener('change', updateUtilisationFields);
|
|
updateUtilisationFields();
|
|
}
|
|
|
|
const photoUrlInput = document.getElementById('photo-url-add');
|
|
if (photoUrlInput) {
|
|
photoUrlInput.addEventListener('input', updatePhotoUrlAddUI);
|
|
}
|
|
const photoFileInput = document.getElementById('photo-file-add');
|
|
if (photoFileInput) {
|
|
photoFileInput.addEventListener('change', onPhotoFileAddChange);
|
|
}
|
|
|
|
// Auto-refresh every 30 seconds
|
|
setInterval(() => {
|
|
loadStatistics();
|
|
loadPeripherals();
|
|
}, 30000);
|
|
});
|
|
|
|
// Initialize star rating system
|
|
function initStarRating() {
|
|
const starContainer = document.querySelector('.star-rating');
|
|
if (!starContainer) return;
|
|
|
|
const stars = starContainer.querySelectorAll('i[data-rating]');
|
|
const ratingInput = document.getElementById('rating');
|
|
|
|
// Click handler
|
|
stars.forEach(star => {
|
|
star.addEventListener('click', () => {
|
|
const rating = parseInt(star.dataset.rating);
|
|
ratingInput.value = rating;
|
|
updateStarDisplay(rating);
|
|
});
|
|
|
|
// Hover effect
|
|
star.addEventListener('mouseenter', () => {
|
|
const rating = parseInt(star.dataset.rating);
|
|
updateStarDisplay(rating, true);
|
|
});
|
|
});
|
|
|
|
// Reset to actual rating on mouse leave
|
|
starContainer.addEventListener('mouseleave', () => {
|
|
const currentRating = parseInt(ratingInput.value) || 0;
|
|
updateStarDisplay(currentRating);
|
|
});
|
|
}
|
|
|
|
// Update star display
|
|
function updateStarDisplay(rating, isHover = false) {
|
|
const stars = document.querySelectorAll('.star-rating i[data-rating]');
|
|
stars.forEach(star => {
|
|
const starRating = parseInt(star.dataset.rating);
|
|
if (starRating <= rating) {
|
|
star.classList.add('active');
|
|
} else {
|
|
star.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Set rating value (used when loading peripheral data)
|
|
function setRating(value) {
|
|
const ratingInput = document.getElementById('rating');
|
|
if (ratingInput) {
|
|
ratingInput.value = value || 0;
|
|
updateStarDisplay(parseInt(value) || 0);
|
|
}
|
|
}
|
|
|
|
// Fill USB detailed information fields
|
|
function fillUSBDetails(caracteristiques) {
|
|
if (!caracteristiques) {
|
|
// Hide USB details section if no data
|
|
const section = document.getElementById('usb-details-section');
|
|
if (section) section.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Check if we have USB-specific fields
|
|
const hasUSBData = caracteristiques.vendor_id || caracteristiques.product_id ||
|
|
caracteristiques.usb_type || caracteristiques.interface_classes;
|
|
|
|
const section = document.getElementById('usb-details-section');
|
|
if (!hasUSBData) {
|
|
if (section) section.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Show section and fill fields
|
|
if (section) section.style.display = 'block';
|
|
|
|
// Helper function to set field value
|
|
const setField = (id, value) => {
|
|
const field = document.getElementById(id);
|
|
if (field && value !== null && value !== undefined) {
|
|
field.value = value;
|
|
}
|
|
};
|
|
|
|
// Fill all USB fields
|
|
setField('usb_vendor_id', caracteristiques.vendor_id);
|
|
setField('usb_product_id', caracteristiques.product_id);
|
|
setField('usb_fabricant', caracteristiques.fabricant);
|
|
setField('usb_type', caracteristiques.usb_type);
|
|
setField('usb_version_declared', caracteristiques.usb_version_declared);
|
|
setField('usb_negotiated_speed', caracteristiques.negotiated_speed);
|
|
setField('usb_max_power', caracteristiques.max_power_ma ? `${caracteristiques.max_power_ma} mA` : '');
|
|
|
|
// Power mode
|
|
let powerMode = '';
|
|
if (caracteristiques.is_bus_powered && caracteristiques.is_self_powered) {
|
|
powerMode = 'Bus Powered + Self Powered';
|
|
} else if (caracteristiques.is_bus_powered) {
|
|
powerMode = 'Bus Powered';
|
|
} else if (caracteristiques.is_self_powered) {
|
|
powerMode = 'Self Powered';
|
|
}
|
|
setField('usb_power_mode', powerMode);
|
|
|
|
// Power sufficient (Yes/No with color indicator)
|
|
const powerSufficient = caracteristiques.power_sufficient === true ? '✅ Oui' :
|
|
caracteristiques.power_sufficient === false ? '⚠️ Non' : 'N/A';
|
|
setField('usb_power_sufficient', powerSufficient);
|
|
|
|
// Firmware required
|
|
const firmwareReq = caracteristiques.requires_firmware === true ? '⚠️ Oui (classe 255)' :
|
|
caracteristiques.requires_firmware === false ? '✅ Non' : 'N/A';
|
|
setField('usb_requires_firmware', firmwareReq);
|
|
|
|
// Device class
|
|
const deviceClass = caracteristiques.device_class_nom ?
|
|
`${caracteristiques.device_class} (${caracteristiques.device_class_nom})` :
|
|
caracteristiques.device_class || 'N/A';
|
|
setField('usb_device_class', deviceClass);
|
|
|
|
// Interface classes (normative)
|
|
if (caracteristiques.interface_classes && caracteristiques.interface_classes.length > 0) {
|
|
const interfaceClassesStr = caracteristiques.interface_classes
|
|
.map(ic => `${ic.code} (${ic.name})`)
|
|
.join(', ');
|
|
setField('usb_interface_classes', interfaceClassesStr);
|
|
} else {
|
|
setField('usb_interface_classes', 'N/A');
|
|
}
|
|
}
|
|
|
|
// Load statistics
|
|
async function loadStatistics() {
|
|
try {
|
|
const stats = await apiRequest('/peripherals/statistics/summary');
|
|
|
|
document.getElementById('stat-total').textContent = stats.total_peripherals || 0;
|
|
document.getElementById('stat-disponible').textContent = stats.disponible || 0;
|
|
document.getElementById('stat-pret').textContent = stats.en_pret || 0;
|
|
document.getElementById('stat-low-stock').textContent = stats.low_stock_count || 0;
|
|
} catch (error) {
|
|
console.error('Error loading statistics:', error);
|
|
}
|
|
}
|
|
|
|
// Load peripheral types from YAML config
|
|
async function loadPeripheralTypes() {
|
|
try {
|
|
// For now, use hardcoded types (later can be loaded from API)
|
|
peripheralTypes = [
|
|
'USB', 'Bluetooth', 'Réseau', 'Stockage', 'Video', 'Audio',
|
|
'Câble', 'Quincaillerie', 'Console', 'Microcontrôleur'
|
|
];
|
|
|
|
const typeSelect = document.getElementById('filter-type');
|
|
const typeFormSelect = document.getElementById('type_principal');
|
|
|
|
peripheralTypes.forEach(type => {
|
|
const option1 = document.createElement('option');
|
|
option1.value = type;
|
|
option1.textContent = type;
|
|
typeSelect.appendChild(option1);
|
|
|
|
const option2 = document.createElement('option');
|
|
option2.value = type;
|
|
option2.textContent = type;
|
|
typeFormSelect.appendChild(option2);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading peripheral types:', error);
|
|
}
|
|
}
|
|
|
|
// Load locations
|
|
async function loadLocations() {
|
|
try {
|
|
const response = await apiRequest('/locations');
|
|
locations = response;
|
|
|
|
const locationSelects = [
|
|
document.getElementById('filter-location'),
|
|
document.getElementById('location_id')
|
|
];
|
|
|
|
locationSelects.forEach(select => {
|
|
if (!select) return;
|
|
|
|
// Clear existing options except first
|
|
while (select.options.length > 1) {
|
|
select.remove(1);
|
|
}
|
|
|
|
locations.forEach(loc => {
|
|
const option = document.createElement('option');
|
|
option.value = loc.id;
|
|
option.textContent = loc.nom;
|
|
select.appendChild(option);
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading locations:', error);
|
|
}
|
|
}
|
|
|
|
// Load storage locations from YAML
|
|
async function loadStockageLocations() {
|
|
try {
|
|
const result = await apiRequest('/peripherals/config/stockage-locations');
|
|
if (!result.success) return;
|
|
|
|
const select = document.getElementById('stockage_location');
|
|
if (!select) return;
|
|
|
|
// Clear existing options except first
|
|
while (select.options.length > 1) {
|
|
select.remove(1);
|
|
}
|
|
|
|
result.locations.forEach(name => {
|
|
const option = document.createElement('option');
|
|
option.value = name;
|
|
option.textContent = name;
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading storage locations:', error);
|
|
}
|
|
}
|
|
|
|
// Load hosts from YAML
|
|
async function loadHosts() {
|
|
try {
|
|
const result = await apiRequest('/peripherals/config/hosts');
|
|
if (!result.success) return;
|
|
|
|
hosts = result.hosts || [];
|
|
|
|
const select = document.getElementById('connecte_a');
|
|
if (!select) return;
|
|
|
|
// Clear existing options except first
|
|
while (select.options.length > 1) {
|
|
select.remove(1);
|
|
}
|
|
|
|
hosts.forEach(host => {
|
|
const option = document.createElement('option');
|
|
option.value = host.nom;
|
|
option.textContent = host.localisation ? `${host.nom} (${host.localisation})` : host.nom;
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading hosts:', error);
|
|
}
|
|
}
|
|
|
|
function updateUtilisationFields() {
|
|
const utilisation = document.getElementById('utilisation')?.value || 'non_utilise';
|
|
const hostGroup = document.getElementById('group-connecte_a');
|
|
const locationGroup = document.getElementById('group-location_id');
|
|
const stockageGroup = document.getElementById('group-stockage_location');
|
|
|
|
if (utilisation === 'utilise') {
|
|
if (hostGroup) hostGroup.style.display = 'block';
|
|
if (locationGroup) locationGroup.style.display = 'none';
|
|
if (stockageGroup) stockageGroup.style.display = 'none';
|
|
} else {
|
|
if (hostGroup) hostGroup.style.display = 'none';
|
|
if (locationGroup) locationGroup.style.display = 'none';
|
|
if (stockageGroup) stockageGroup.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Load devices (hosts) for dropdown
|
|
async function loadDevices() {
|
|
try {
|
|
const response = await apiRequest('/peripherals/config/devices');
|
|
if (response.success) {
|
|
devices = response.devices;
|
|
|
|
const deviceSelect = document.getElementById('device_id');
|
|
if (!deviceSelect) return;
|
|
|
|
// Clear existing options except first (En stock)
|
|
while (deviceSelect.options.length > 1) {
|
|
deviceSelect.remove(1);
|
|
}
|
|
|
|
// Add devices to dropdown
|
|
devices.forEach(device => {
|
|
const option = document.createElement('option');
|
|
option.value = device.id;
|
|
// Display format: "hostname (location)" or just "hostname" if no location
|
|
const displayText = device.location ?
|
|
`${device.hostname} (${device.location})` :
|
|
device.hostname;
|
|
option.textContent = displayText;
|
|
deviceSelect.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading devices:', error);
|
|
}
|
|
}
|
|
|
|
// Load boutiques
|
|
async function loadBoutiques() {
|
|
try {
|
|
const result = await apiRequest('/peripherals/config/boutiques');
|
|
if (!result.success) return;
|
|
|
|
const select = document.getElementById('boutique');
|
|
if (!select) return;
|
|
|
|
// Clear existing options except first
|
|
while (select.options.length > 1) {
|
|
select.remove(1);
|
|
}
|
|
|
|
result.boutiques.forEach(name => {
|
|
const option = document.createElement('option');
|
|
option.value = name;
|
|
option.textContent = name;
|
|
select.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading boutiques:', error);
|
|
}
|
|
}
|
|
|
|
// Get device display text by ID (for showing assignment status)
|
|
function getDeviceDisplayText(deviceId) {
|
|
if (!deviceId) {
|
|
return 'En stock';
|
|
}
|
|
|
|
const device = devices.find(d => d.id === deviceId);
|
|
if (!device) {
|
|
return 'En stock';
|
|
}
|
|
|
|
// Return "hostname + location" if location exists, otherwise just hostname
|
|
return device.location ?
|
|
`${device.hostname} → ${device.location}` :
|
|
device.hostname;
|
|
}
|
|
|
|
// Load peripherals
|
|
async function loadPeripherals() {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: currentPage,
|
|
page_size: pageSize,
|
|
sort_by: sortBy,
|
|
sort_order: sortOrder
|
|
});
|
|
|
|
if (filterType) params.append('type_filter', filterType);
|
|
if (filterLocation) params.append('location_id', filterLocation);
|
|
if (filterEtat) params.append('etat', filterEtat);
|
|
if (searchQuery) params.append('search', searchQuery);
|
|
|
|
const data = await apiRequest(`/peripherals?${params}`);
|
|
|
|
totalPages = data.total_pages;
|
|
displayPeripherals(data.items);
|
|
updatePagination(data);
|
|
} catch (error) {
|
|
console.error('Error loading peripherals:', error);
|
|
showError('Erreur lors du chargement des périphériques');
|
|
}
|
|
}
|
|
|
|
// Display peripherals in table
|
|
function displayPeripherals(peripherals) {
|
|
const tbody = document.getElementById('peripherals-tbody');
|
|
|
|
if (peripherals.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="10" class="no-data">Aucun périphérique trouvé</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = peripherals.map(p => `
|
|
<tr onclick="viewPeripheral(${p.id})">
|
|
<td>
|
|
<div class="peripheral-photo">
|
|
${p.thumbnail_url
|
|
? `<img src="${escapeHtml(p.thumbnail_url)}" alt="${escapeHtml(p.nom)}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
|
<i class="fas ${getTypeIcon(p.type_principal)}" style="display:none;"></i>`
|
|
: `<i class="fas ${getTypeIcon(p.type_principal)}"></i>`
|
|
}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<strong>${escapeHtml(p.nom)}</strong>
|
|
${p.en_pret ? '<span class="badge badge-warning">En prêt</span>' : ''}
|
|
${p.is_complete_device ? '<span class="badge badge-info">Appareil complet</span>' : ''}
|
|
</td>
|
|
<td>${escapeHtml(p.type_principal)}</td>
|
|
<td>${escapeHtml(p.marque || '-')}</td>
|
|
<td>${escapeHtml(p.modele || '-')}</td>
|
|
<td><span class="badge badge-${getEtatClass(p.etat)}">${escapeHtml(p.etat)}</span></td>
|
|
<td>${renderStars(p.rating)}</td>
|
|
<td>
|
|
${p.quantite_disponible}
|
|
${p.quantite_disponible === 0 ? '<i class="fas fa-exclamation-triangle text-danger"></i>' : ''}
|
|
</td>
|
|
<td>${p.prix ? `${p.prix.toFixed(2)} €` : '-'}</td>
|
|
<td class="actions">
|
|
<button class="btn-icon" onclick="event.stopPropagation(); editPeripheral(${p.id})" title="Modifier">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn-icon" onclick="event.stopPropagation(); deletePeripheral(${p.id})" title="Supprimer">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// Update pagination
|
|
function updatePagination(data) {
|
|
document.getElementById('pagination-info').textContent =
|
|
`Page ${data.page} / ${data.total_pages} (${data.total} périphériques)`;
|
|
|
|
document.getElementById('btn-prev').disabled = data.page <= 1;
|
|
document.getElementById('btn-next').disabled = data.page >= data.total_pages;
|
|
}
|
|
|
|
// Pagination functions
|
|
function previousPage() {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
loadPeripherals();
|
|
}
|
|
}
|
|
|
|
function nextPage() {
|
|
if (currentPage < totalPages) {
|
|
currentPage++;
|
|
loadPeripherals();
|
|
}
|
|
}
|
|
|
|
// Sort table
|
|
function sortTable(column) {
|
|
if (sortBy === column) {
|
|
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortBy = column;
|
|
sortOrder = 'asc';
|
|
}
|
|
loadPeripherals();
|
|
}
|
|
|
|
// Filter and search
|
|
function filterPeripherals() {
|
|
filterType = document.getElementById('filter-type').value;
|
|
filterLocation = document.getElementById('filter-location').value;
|
|
filterEtat = document.getElementById('filter-etat').value;
|
|
currentPage = 1;
|
|
loadPeripherals();
|
|
}
|
|
|
|
let searchTimeout;
|
|
function searchPeripherals() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
searchQuery = document.getElementById('search').value;
|
|
currentPage = 1;
|
|
loadPeripherals();
|
|
}, 300);
|
|
}
|
|
|
|
// View peripheral details
|
|
function viewPeripheral(id) {
|
|
window.location.href = `peripheral-detail.html?id=${id}`;
|
|
}
|
|
|
|
// Show add modal
|
|
function showAddModal() {
|
|
document.getElementById('form-add-peripheral').reset();
|
|
document.getElementById('modal-add').style.display = 'block';
|
|
updateUtilisationFields();
|
|
updatePhotoUrlAddUI();
|
|
}
|
|
|
|
// Add peripheral
|
|
async function addPeripheral(event) {
|
|
event.preventDefault();
|
|
|
|
const formData = new FormData(event.target);
|
|
const data = {};
|
|
const photoFile = document.getElementById('photo-file-add')?.files?.[0] || null;
|
|
const photoUrl = (document.getElementById('photo-url-add')?.value || '').trim();
|
|
const photoDescription = document.getElementById('photo-description-add')?.value || '';
|
|
const photoPrimary = document.getElementById('photo-primary-add')?.checked || false;
|
|
|
|
for (let [key, value] of formData.entries()) {
|
|
if (value === '') continue;
|
|
|
|
// Convert numeric fields
|
|
if (['photo_file', 'photo_url', 'photo_description', 'photo_primary'].includes(key)) {
|
|
continue;
|
|
} else if (key === 'utilisation') {
|
|
continue;
|
|
} else if (['prix', 'rating', 'quantite_totale', 'quantite_disponible', 'garantie_duree_mois'].includes(key)) {
|
|
data[key] = parseFloat(value);
|
|
} else if (['location_id', 'device_id'].includes(key)) {
|
|
data[key] = parseInt(value);
|
|
} else {
|
|
data[key] = value;
|
|
}
|
|
}
|
|
|
|
const utilisation = document.getElementById('utilisation')?.value || 'non_utilise';
|
|
if (utilisation === 'utilise') {
|
|
const selectedHost = document.getElementById('connecte_a')?.value || null;
|
|
data.connecte_a = selectedHost;
|
|
data.location_id = null;
|
|
const host = hosts.find(h => h.nom === selectedHost);
|
|
if (host && host.localisation) {
|
|
data.location_details = host.localisation;
|
|
}
|
|
} else {
|
|
data.connecte_a = null;
|
|
data.location_id = null;
|
|
const stockage = document.getElementById('stockage_location')?.value || null;
|
|
data.location_details = stockage;
|
|
}
|
|
|
|
try {
|
|
const created = await apiRequest('/peripherals', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (created && (photoFile || photoUrl)) {
|
|
if (photoFile) {
|
|
const photoData = new FormData();
|
|
photoData.append('file', photoFile);
|
|
photoData.append('description', photoDescription);
|
|
photoData.append('is_primary', photoPrimary ? 'true' : 'false');
|
|
await apiRequest(`/peripherals/${created.id}/photos`, {
|
|
method: 'POST',
|
|
body: photoData
|
|
});
|
|
} else if (/^https?:\/\//i.test(photoUrl)) {
|
|
const urlData = new FormData();
|
|
urlData.append('image_url', photoUrl);
|
|
urlData.append('description', photoDescription);
|
|
urlData.append('is_primary', photoPrimary ? 'true' : 'false');
|
|
await apiRequest(`/peripherals/${created.id}/photos/from-url`, {
|
|
method: 'POST',
|
|
body: urlData
|
|
});
|
|
} else if (photoUrl) {
|
|
showError('URL invalide (http/https requis)');
|
|
}
|
|
}
|
|
|
|
closeModal('modal-add');
|
|
showSuccess('Périphérique ajouté avec succès');
|
|
loadPeripherals();
|
|
loadStatistics();
|
|
} catch (error) {
|
|
console.error('Error adding peripheral:', error);
|
|
showError('Erreur lors de l\'ajout du périphérique');
|
|
}
|
|
}
|
|
|
|
function updatePhotoUrlAddUI() {
|
|
const url = (document.getElementById('photo-url-add')?.value || '').trim();
|
|
const button = document.getElementById('btn-photo-url-add');
|
|
const previewWrap = document.getElementById('photo-url-preview-wrap');
|
|
const previewImg = document.getElementById('photo-url-preview');
|
|
const isValid = /^https?:\/\//i.test(url);
|
|
|
|
if (button) button.disabled = !isValid;
|
|
if (!previewWrap || !previewImg) return;
|
|
|
|
if (isValid) {
|
|
previewImg.src = url;
|
|
previewWrap.style.display = 'flex';
|
|
} else {
|
|
previewImg.removeAttribute('src');
|
|
previewWrap.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function onPhotoFileAddChange() {
|
|
const photoFile = document.getElementById('photo-file-add')?.files?.[0] || null;
|
|
if (photoFile) {
|
|
const urlInput = document.getElementById('photo-url-add');
|
|
if (urlInput) urlInput.value = '';
|
|
updatePhotoUrlAddUI();
|
|
}
|
|
}
|
|
|
|
function submitPhotoUrlAdd() {
|
|
const url = (document.getElementById('photo-url-add')?.value || '').trim();
|
|
if (!/^https?:\/\//i.test(url)) {
|
|
showError('URL invalide (http/https requis)');
|
|
return;
|
|
}
|
|
const form = document.getElementById('form-add-peripheral');
|
|
if (form) {
|
|
form.requestSubmit();
|
|
}
|
|
}
|
|
|
|
// Edit peripheral
|
|
async function editPeripheral(id) {
|
|
// Redirect to detail page in edit mode
|
|
window.location.href = `peripheral-detail.html?id=${id}&edit=true`;
|
|
}
|
|
|
|
// Delete peripheral
|
|
async function deletePeripheral(id) {
|
|
if (!confirm('Êtes-vous sûr de vouloir supprimer ce périphérique ?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await apiRequest(`/peripherals/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
showSuccess('Périphérique supprimé avec succès');
|
|
loadPeripherals();
|
|
loadStatistics();
|
|
} catch (error) {
|
|
console.error('Error deleting peripheral:', error);
|
|
showError('Erreur lors de la suppression du périphérique');
|
|
}
|
|
}
|
|
|
|
// Show import USB modal
|
|
function showImportUSBModal() {
|
|
// Reset form and steps
|
|
document.getElementById('form-detect-usb').reset();
|
|
document.getElementById('usb-import-step-1').style.display = 'block';
|
|
document.getElementById('usb-import-step-2').style.display = 'none';
|
|
document.getElementById('modal-import-usb').style.display = 'block';
|
|
}
|
|
|
|
// Show import USB structured modal
|
|
function showImportUSBStructuredModal() {
|
|
document.getElementById('form-import-usb-structured').reset();
|
|
document.getElementById('modal-import-usb-structured').style.display = 'block';
|
|
}
|
|
|
|
// Copy USB command to clipboard
|
|
function copyUSBCommand() {
|
|
const command = document.getElementById('usb-command').textContent;
|
|
const button = document.getElementById('btn-copy-usb');
|
|
const tooltip = button.querySelector('.tooltip-copied');
|
|
|
|
navigator.clipboard.writeText(command).then(() => {
|
|
// Show tooltip
|
|
tooltip.classList.add('show');
|
|
|
|
// Hide tooltip after 2 seconds
|
|
setTimeout(() => {
|
|
tooltip.classList.remove('show');
|
|
}, 2000);
|
|
}).catch((error) => {
|
|
console.error('Erreur lors de la copie:', error);
|
|
showError('Erreur lors de la copie de la commande');
|
|
});
|
|
}
|
|
|
|
// Import USB - Step 1: Detect devices
|
|
let detectedUSBDevices = [];
|
|
let fullUSBOutput = '';
|
|
|
|
async function detectUSBDevices(event) {
|
|
event.preventDefault();
|
|
|
|
const formData = new FormData(event.target);
|
|
fullUSBOutput = formData.get('lsusb_output');
|
|
|
|
try {
|
|
const result = await apiRequest('/peripherals/import/usb-cli/detect', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (result.success && result.devices && result.devices.length > 0) {
|
|
detectedUSBDevices = result.devices;
|
|
|
|
// Build device list HTML
|
|
const deviceListHTML = result.devices.map((device, index) => `
|
|
<div class="usb-device-item" onclick="selectUSBDevice(${index})">
|
|
<input type="radio" name="selected_usb_device" value="${index}" id="usb-device-${index}">
|
|
<div class="usb-device-info">
|
|
<div class="usb-device-bus">${device.bus_line}</div>
|
|
<div class="usb-device-id">ID: ${device.id} • Bus ${device.bus} Device ${device.device}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
document.getElementById('usb-devices-list').innerHTML = deviceListHTML;
|
|
|
|
// Show step 2, hide step 1
|
|
document.getElementById('usb-import-step-1').style.display = 'none';
|
|
document.getElementById('usb-import-step-2').style.display = 'block';
|
|
|
|
showSuccess(`${result.total_devices} périphérique(s) USB détecté(s)`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error detecting USB devices:', error);
|
|
showError(error.message || 'Erreur lors de la détection des périphériques USB');
|
|
}
|
|
}
|
|
|
|
// Select USB device (radio button click)
|
|
function selectUSBDevice(index) {
|
|
const radio = document.getElementById(`usb-device-${index}`);
|
|
if (radio) {
|
|
radio.checked = true;
|
|
// Enable Finaliser button when a device is selected
|
|
document.getElementById('btn-finalize-usb').disabled = false;
|
|
}
|
|
}
|
|
|
|
// Go back to step 1
|
|
function backToUSBStep1() {
|
|
document.getElementById('usb-import-step-2').style.display = 'none';
|
|
document.getElementById('usb-import-step-1').style.display = 'block';
|
|
document.getElementById('btn-finalize-usb').disabled = true;
|
|
detectedUSBDevices = [];
|
|
}
|
|
|
|
// Import selected USB device
|
|
async function importSelectedUSBDevice() {
|
|
const selectedRadio = document.querySelector('input[name="selected_usb_device"]:checked');
|
|
|
|
if (!selectedRadio) {
|
|
showError('Veuillez sélectionner un périphérique');
|
|
return;
|
|
}
|
|
|
|
const selectedIndex = parseInt(selectedRadio.value);
|
|
const selectedDevice = detectedUSBDevices[selectedIndex];
|
|
|
|
if (!selectedDevice) {
|
|
showError('Périphérique sélectionné invalide');
|
|
return;
|
|
}
|
|
|
|
// Extract the device section
|
|
const formData = new FormData();
|
|
formData.append('lsusb_output', fullUSBOutput);
|
|
formData.append('bus', selectedDevice.bus);
|
|
formData.append('device', selectedDevice.device);
|
|
|
|
try {
|
|
const result = await apiRequest('/peripherals/import/usb-cli/extract', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (result.success) {
|
|
if (result.already_exists) {
|
|
// Device already exists
|
|
const existing = result.existing_peripheral;
|
|
const message = `Ce périphérique existe déjà dans la base de données:\n\n` +
|
|
`Nom: ${existing.nom}\n` +
|
|
`Marque: ${existing.marque || 'N/A'}\n` +
|
|
`Modèle: ${existing.modele || 'N/A'}\n` +
|
|
`Fabricant: ${existing.fabricant || 'N/A'}\n` +
|
|
`Produit: ${existing.produit || 'N/A'}\n` +
|
|
`Quantité disponible: ${existing.quantite_disponible}\n\n` +
|
|
`Voulez-vous voir sa fiche ?`;
|
|
|
|
if (confirm(message)) {
|
|
window.location.href = `peripheral-detail.html?id=${existing.id}`;
|
|
}
|
|
} else {
|
|
// New device - pre-fill form
|
|
closeModal('modal-import-usb');
|
|
showAddModal();
|
|
|
|
const suggested = result.suggested_peripheral;
|
|
|
|
// Fill form fields
|
|
if (suggested.nom) document.getElementById('nom').value = suggested.nom;
|
|
if (suggested.marque) document.getElementById('marque').value = suggested.marque;
|
|
if (suggested.modele) document.getElementById('modele').value = suggested.modele;
|
|
if (suggested.fabricant) document.getElementById('fabricant').value = suggested.fabricant;
|
|
if (suggested.produit) document.getElementById('produit').value = suggested.produit;
|
|
if (suggested.numero_serie) document.getElementById('numero_serie').value = suggested.numero_serie;
|
|
|
|
// Fill documentation fields
|
|
if (suggested.cli_yaml) {
|
|
const cliYamlField = document.getElementById('cli_yaml');
|
|
if (cliYamlField) {
|
|
cliYamlField.value = suggested.cli_yaml;
|
|
}
|
|
}
|
|
if (suggested.cli_raw) {
|
|
const cliRawField = document.getElementById('cli_raw');
|
|
if (cliRawField) {
|
|
cliRawField.value = suggested.cli_raw;
|
|
}
|
|
}
|
|
if (suggested.synthese) {
|
|
const syntheseField = document.getElementById('synthese');
|
|
if (syntheseField) {
|
|
syntheseField.value = suggested.synthese;
|
|
}
|
|
}
|
|
|
|
// Fill USB detailed information
|
|
fillUSBDetails(suggested.caracteristiques_specifiques);
|
|
|
|
// Set type_principal and wait for subtypes to load before setting sous_type
|
|
if (suggested.type_principal) {
|
|
document.getElementById('type_principal').value = suggested.type_principal;
|
|
|
|
// Trigger the change event to load subtypes
|
|
const typePrincipalSelect = document.getElementById('type_principal');
|
|
const changeEvent = new Event('change');
|
|
typePrincipalSelect.dispatchEvent(changeEvent);
|
|
|
|
// Wait for subtypes to load, then set sous_type
|
|
if (suggested.sous_type) {
|
|
// Use a Promise-based approach with retry logic
|
|
const setSousType = async () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
const sousTypeSelect = document.getElementById('sous_type');
|
|
if (sousTypeSelect && sousTypeSelect.options.length > 1) {
|
|
// Options are loaded
|
|
sousTypeSelect.value = suggested.sous_type;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
setSousType();
|
|
}
|
|
}
|
|
|
|
showSuccess('Données USB importées avec succès. Vérifiez et complétez les informations.');
|
|
|
|
// Reset USB import state
|
|
detectedUSBDevices = [];
|
|
fullUSBOutput = '';
|
|
document.getElementById('usb-import-step-2').style.display = 'none';
|
|
document.getElementById('usb-import-step-1').style.display = 'block';
|
|
document.getElementById('form-detect-usb').reset();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error extracting USB device:', error);
|
|
showError(error.message || 'Erreur lors de l\'extraction du périphérique USB');
|
|
}
|
|
}
|
|
|
|
// Import USB structured information
|
|
async function importUSBStructured(event) {
|
|
event.preventDefault();
|
|
|
|
const formData = new FormData(event.target);
|
|
|
|
try {
|
|
const result = await apiRequest('/peripherals/import/usb-structured', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (result.success) {
|
|
if (result.already_exists) {
|
|
// Device already exists
|
|
const existing = result.existing_peripheral;
|
|
const message = `Ce périphérique existe déjà dans la base de données:\n\n` +
|
|
`Nom: ${existing.nom}\n` +
|
|
`Marque: ${existing.marque || 'N/A'}\n` +
|
|
`Modèle: ${existing.modele || 'N/A'}\n` +
|
|
`Fabricant: ${existing.fabricant || 'N/A'}\n` +
|
|
`Produit: ${existing.produit || 'N/A'}\n` +
|
|
`Quantité disponible: ${existing.quantite_disponible}\n\n` +
|
|
`Voulez-vous voir sa fiche ?`;
|
|
|
|
if (confirm(message)) {
|
|
window.location.href = `peripheral-detail.html?id=${existing.id}`;
|
|
}
|
|
} else {
|
|
// New device - pre-fill form
|
|
closeModal('modal-import-usb-structured');
|
|
showAddModal();
|
|
|
|
const suggested = result.suggested_peripheral;
|
|
|
|
// Fill form fields
|
|
if (suggested.nom) document.getElementById('nom').value = suggested.nom;
|
|
if (suggested.marque) document.getElementById('marque').value = suggested.marque;
|
|
if (suggested.modele) document.getElementById('modele').value = suggested.modele;
|
|
if (suggested.fabricant) document.getElementById('fabricant').value = suggested.fabricant;
|
|
if (suggested.produit) document.getElementById('produit').value = suggested.produit;
|
|
if (suggested.numero_serie) document.getElementById('numero_serie').value = suggested.numero_serie;
|
|
|
|
// Fill documentation fields
|
|
if (suggested.cli_yaml) {
|
|
const cliYamlField = document.getElementById('cli_yaml');
|
|
if (cliYamlField) {
|
|
cliYamlField.value = suggested.cli_yaml;
|
|
}
|
|
}
|
|
if (suggested.cli_raw) {
|
|
const cliRawField = document.getElementById('cli_raw');
|
|
if (cliRawField) {
|
|
cliRawField.value = suggested.cli_raw;
|
|
}
|
|
}
|
|
|
|
// Fill USB detailed information
|
|
fillUSBDetails(suggested.caracteristiques_specifiques);
|
|
|
|
// Set type_principal and wait for subtypes to load before setting sous_type
|
|
if (suggested.type_principal) {
|
|
document.getElementById('type_principal').value = suggested.type_principal;
|
|
|
|
// Trigger the change event to load subtypes
|
|
const typePrincipalSelect = document.getElementById('type_principal');
|
|
const changeEvent = new Event('change');
|
|
typePrincipalSelect.dispatchEvent(changeEvent);
|
|
|
|
// Wait for subtypes to load, then set sous_type
|
|
if (suggested.sous_type) {
|
|
const setSousType = async () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
const sousTypeSelect = document.getElementById('sous_type');
|
|
if (sousTypeSelect && sousTypeSelect.options.length > 1) {
|
|
sousTypeSelect.value = suggested.sous_type;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
setSousType();
|
|
}
|
|
}
|
|
|
|
showSuccess('Informations USB importées avec succès. Vérifiez et complétez les informations.');
|
|
|
|
// Reset form
|
|
document.getElementById('form-import-usb-structured').reset();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error importing USB structured info:', error);
|
|
showError(error.message || 'Erreur lors de l\'import des informations USB');
|
|
}
|
|
}
|
|
|
|
// Show import markdown modal
|
|
function showImportMDModal() {
|
|
document.getElementById('form-import-md').reset();
|
|
document.getElementById('md-file-preview').innerHTML = '';
|
|
document.getElementById('modal-import-md').style.display = 'block';
|
|
|
|
// Add file change listener for preview
|
|
const fileInput = document.getElementById('md-file');
|
|
fileInput.addEventListener('change', function() {
|
|
const file = fileInput.files[0];
|
|
if (file) {
|
|
document.getElementById('md-file-preview').innerHTML = `
|
|
<div class="file-info">
|
|
<i class="fas fa-file-alt"></i>
|
|
<span>${file.name}</span>
|
|
<span class="file-size">(${(file.size / 1024).toFixed(2)} KB)</span>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Import Markdown
|
|
async function importMarkdown(event) {
|
|
event.preventDefault();
|
|
|
|
const formData = new FormData(event.target);
|
|
const file = document.getElementById('md-file').files[0];
|
|
|
|
if (!file) {
|
|
showError('Veuillez sélectionner un fichier .md');
|
|
return;
|
|
}
|
|
|
|
if (!file.name.endsWith('.md')) {
|
|
showError('Seuls les fichiers .md sont acceptés');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await apiRequest('/peripherals/import/markdown', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (result.success) {
|
|
// Check if peripheral already exists
|
|
if (result.already_exists) {
|
|
closeModal('modal-import-md');
|
|
|
|
const existing = result.existing_peripheral;
|
|
const message = `Ce périphérique existe déjà dans la base de données:\n\n` +
|
|
`Nom: ${existing.nom}\n` +
|
|
`Marque: ${existing.marque || 'N/A'}\n` +
|
|
`Modèle: ${existing.modele || 'N/A'}\n` +
|
|
`Fabricant: ${existing.fabricant || 'N/A'}\n` +
|
|
`Produit: ${existing.produit || 'N/A'}\n` +
|
|
`Quantité: ${existing.quantite_totale}\n\n` +
|
|
`Voulez-vous voir ce périphérique?`;
|
|
|
|
if (confirm(message)) {
|
|
// Redirect to detail page
|
|
window.location.href = `peripheral-detail.html?id=${existing.id}`;
|
|
} else {
|
|
showInfo(`Import annulé - le périphérique "${existing.nom}" existe déjà.`);
|
|
}
|
|
} else if (result.suggested_peripheral) {
|
|
// New peripheral - pre-fill form
|
|
closeModal('modal-import-md');
|
|
showAddModal();
|
|
|
|
const suggested = result.suggested_peripheral;
|
|
|
|
// Fill basic form fields
|
|
if (suggested.nom) document.getElementById('nom').value = suggested.nom;
|
|
if (suggested.type_principal) {
|
|
document.getElementById('type_principal').value = suggested.type_principal;
|
|
loadPeripheralSubtypes(); // Reload subtypes
|
|
}
|
|
|
|
// Wait for subtypes to load before setting sous_type
|
|
setTimeout(() => {
|
|
if (suggested.sous_type) document.getElementById('sous_type').value = suggested.sous_type;
|
|
}, 100);
|
|
|
|
if (suggested.marque) document.getElementById('marque').value = suggested.marque;
|
|
if (suggested.modele) document.getElementById('modele').value = suggested.modele;
|
|
if (suggested.fabricant) document.getElementById('fabricant').value = suggested.fabricant;
|
|
if (suggested.produit) document.getElementById('produit').value = suggested.produit;
|
|
if (suggested.numero_serie) document.getElementById('numero_serie').value = suggested.numero_serie;
|
|
if (suggested.description) document.getElementById('description').value = suggested.description;
|
|
if (suggested.notes) document.getElementById('notes').value = suggested.notes;
|
|
if (suggested.etat) document.getElementById('etat').value = suggested.etat;
|
|
if (suggested.quantite_totale) document.getElementById('quantite_totale').value = suggested.quantite_totale;
|
|
if (suggested.quantite_disponible) document.getElementById('quantite_disponible').value = suggested.quantite_disponible;
|
|
|
|
// Show success message with filename
|
|
showSuccess(`Fichier ${result.filename} importé avec succès. Vérifiez et complétez les informations.`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error importing markdown:', error);
|
|
showError('Erreur lors de l\'import du fichier .md. Vérifiez le format du fichier.');
|
|
}
|
|
}
|
|
|
|
// Cache for peripheral types from API
|
|
let peripheralTypesCache = null;
|
|
|
|
// Load peripheral types from API
|
|
async function loadPeripheralTypesFromAPI() {
|
|
if (peripheralTypesCache) {
|
|
return peripheralTypesCache;
|
|
}
|
|
|
|
try {
|
|
const result = await apiRequest('/peripherals/config/types');
|
|
if (result.success && result.types) {
|
|
peripheralTypesCache = result.types;
|
|
return result.types;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load peripheral types from API:', error);
|
|
}
|
|
|
|
// Fallback to hardcoded types if API fails
|
|
return {
|
|
'USB': ['Clavier', 'Souris', 'Hub', 'Clé USB', 'Webcam', 'Adaptateur WiFi', 'Autre'],
|
|
'Bluetooth': ['Clavier', 'Souris', 'Audio', 'Autre'],
|
|
'Réseau': ['Wi-Fi', 'Ethernet', 'Autre'],
|
|
'Stockage': ['SSD', 'HDD', 'Clé USB', 'Carte SD', 'Autre'],
|
|
'Video': ['GPU', 'Écran', 'Webcam', 'Autre'],
|
|
'Audio': ['Haut-parleur', 'Microphone', 'Casque', 'Carte son', 'Autre'],
|
|
'Câble': ['USB', 'HDMI', 'DisplayPort', 'Ethernet', 'Audio', 'Alimentation', 'Autre'],
|
|
'Quincaillerie': ['Vis', 'Écrou', 'Entretoise', 'Autre'],
|
|
'Console': ['PlayStation', 'Xbox', 'Nintendo', 'Autre'],
|
|
'Microcontrôleur': ['Raspberry Pi', 'Arduino', 'ESP32', 'Autre']
|
|
};
|
|
}
|
|
|
|
// Load peripheral subtypes based on type
|
|
async function loadPeripheralSubtypes() {
|
|
const type = document.getElementById('type_principal').value;
|
|
const subtypeSelect = document.getElementById('sous_type');
|
|
|
|
// Clear current options
|
|
subtypeSelect.innerHTML = '<option value="">Sélectionner...</option>';
|
|
|
|
// Load types from API
|
|
const subtypes = await loadPeripheralTypesFromAPI();
|
|
|
|
if (subtypes[type]) {
|
|
// Add "Autre" at the end if not present
|
|
const typeSubtypes = [...subtypes[type]];
|
|
if (!typeSubtypes.includes('Autre')) {
|
|
typeSubtypes.push('Autre');
|
|
}
|
|
|
|
typeSubtypes.forEach(subtype => {
|
|
const option = document.createElement('option');
|
|
option.value = subtype;
|
|
option.textContent = subtype;
|
|
subtypeSelect.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Helper: Get CSS class for état
|
|
function getEtatClass(etat) {
|
|
const classes = {
|
|
'Neuf': 'success',
|
|
'Bon': 'info',
|
|
'Usagé': 'warning',
|
|
'Défectueux': 'danger',
|
|
'Retiré': 'secondary'
|
|
};
|
|
return classes[etat] || 'secondary';
|
|
}
|
|
|
|
// Get icon based on peripheral type
|
|
function getTypeIcon(type) {
|
|
if (!type) return 'fa-microchip';
|
|
|
|
const typeUpper = type.toUpperCase();
|
|
|
|
// USB devices
|
|
if (typeUpper.includes('USB')) return 'fa-plug';
|
|
|
|
// Storage devices
|
|
if (typeUpper.includes('STOCKAGE') || typeUpper.includes('DISK') ||
|
|
typeUpper.includes('SSD') || typeUpper.includes('HDD') ||
|
|
typeUpper.includes('FLASH')) return 'fa-hard-drive';
|
|
|
|
// Network devices
|
|
if (typeUpper.includes('RÉSEAU') || typeUpper.includes('RESEAU') ||
|
|
typeUpper.includes('NETWORK') || typeUpper.includes('WIFI') ||
|
|
typeUpper.includes('ETHERNET')) return 'fa-network-wired';
|
|
|
|
// Audio devices
|
|
if (typeUpper.includes('AUDIO') || typeUpper.includes('SOUND') ||
|
|
typeUpper.includes('SPEAKER') || typeUpper.includes('HEADPHONE')) return 'fa-volume-up';
|
|
|
|
// Video devices
|
|
if (typeUpper.includes('VIDEO') || typeUpper.includes('VIDÉO') ||
|
|
typeUpper.includes('WEBCAM') || typeUpper.includes('CAMERA')) return 'fa-video';
|
|
|
|
// Input devices
|
|
if (typeUpper.includes('CLAVIER') || typeUpper.includes('KEYBOARD')) return 'fa-keyboard';
|
|
if (typeUpper.includes('SOURIS') || typeUpper.includes('MOUSE')) return 'fa-mouse';
|
|
|
|
// Other devices
|
|
if (typeUpper.includes('BLUETOOTH')) return 'fa-bluetooth';
|
|
if (typeUpper.includes('HUB')) return 'fa-sitemap';
|
|
if (typeUpper.includes('ADAPTATEUR') || typeUpper.includes('ADAPTER')) return 'fa-plug';
|
|
if (typeUpper.includes('CÂBLE') || typeUpper.includes('CABLE')) return 'fa-link';
|
|
|
|
// Default
|
|
return 'fa-microchip';
|
|
}
|
|
|
|
// Helper: Render star rating
|
|
function renderStars(rating) {
|
|
const fullStars = Math.floor(rating);
|
|
const hasHalfStar = rating % 1 >= 0.5;
|
|
const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
|
|
|
|
let html = '';
|
|
|
|
for (let i = 0; i < fullStars; i++) {
|
|
html += '<i class="fas fa-star text-warning"></i>';
|
|
}
|
|
|
|
if (hasHalfStar) {
|
|
html += '<i class="fas fa-star-half-alt text-warning"></i>';
|
|
}
|
|
|
|
for (let i = 0; i < emptyStars; i++) {
|
|
html += '<i class="far fa-star text-muted"></i>';
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// Close modal
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).style.display = 'none';
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
window.onclick = function(event) {
|
|
if (event.target.classList.contains('modal')) {
|
|
event.target.style.display = 'none';
|
|
}
|
|
}
|