loko/streetup/stock/templates/stock/preparation_order_add.html
2026-07-22 14:48:40 +02:00

877 lines
40 KiB
HTML

{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load stock_custom_filters %}
{% block content %}
<div class="container mt-3">
<div class="row justify-content-center">
<div class="col-lg-8 col-md-10 col-sm-12">
<h3 style="text-align: center;">{% translate "Nouveau Bon de Préparation" %}</h3>
<form method="post" enctype="multipart/form-data" id="preparation-order-form">
{% csrf_token %}
<div class="mb-3">
{{ form.non_field_errors }}
</div>
{% if from_intervention and intervention %}
<!-- Bloc regroupant l'origine de la demande -->
<div class="card mb-3 shadow">
<div class="card-header bg-info text-white">
<i class="bi bi-info-circle"></i> {% translate "Origine de la demande" %}
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<strong>{% translate "Intervention" %}:</strong>
<a href="{% url 'interventions:intervention_detail' intervention.id %}" target="_blank">
#{{ intervention.id }} - {{ intervention.title|default:intervention.description|truncatewords:10 }}
</a>
</div>
<div class="col-md-6">
<strong>{% translate "Priorité" %}:</strong>
{% if intervention.priority == '1' %}
<span class="badge bg-danger">{% translate "Urgent" %}</span>
{% elif intervention.priority == '2' %}
<span class="badge bg-warning">{% translate "Elevé" %}</span>
{% elif intervention.priority == '3' %}
<span class="badge bg-info">{% translate "Normal" %}</span>
{% else %}
<span class="badge bg-secondary">{% translate "Bas" %}</span>
{% endif %}
</div>
</div>
<div class="row mt-2">
<div class="col-md-6">
<strong>{% translate "Statut" %}:</strong>
{{ intervention.get_status_display }}
</div>
<div class="col-md-6">
<strong>{% translate "Date création" %}:</strong>
{{ intervention.created_date|date:"d/m/Y H:i" }}
</div>
</div>
</div>
</div>
{% endif %}
<!-- Bloc numéro et statut du bon -->
<div class="card mb-3 shadow">
<div class="card-header">
{% translate "Informations du bon" %}
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
{{ form.order_number.label_tag }}
{{ form.order_number|add_class:"form-control" }}
{{ form.order_number.errors }}
</div>
<div class="col-md-4 mb-3">
{{ form.status.label_tag }}
{{ form.status|add_class:"form-control" }}
{{ form.status.errors }}
</div>
<div class="col-md-4 mb-3">
{{ form.priority.label_tag }}
{{ form.priority|add_class:"form-select" }}
{{ form.priority.errors }}
</div>
</div>
{% if not from_intervention %}
<div class="row">
<div class="col-12 mb-3">
{{ form.requester.label_tag }}
{{ form.requester }}
<div class="form-text">{% translate "Optionnel — personne ou service à l'origine de la demande." %}</div>
{{ form.requester.errors }}
</div>
</div>
{% endif %}
<div class="row mt-2">
<div class="col-md-6 mb-3 d-flex align-items-center">
<div class="form-check form-switch mt-3">
{{ form.is_loan }}
<label class="form-check-label fw-bold text-primary" for="{{ form.is_loan.auto_id }}">
{{ form.is_loan.label }}
</label>
</div>
{{ form.is_loan.errors }}
</div>
<div class="col-md-6 mb-3" id="estimated_return_date_wrapper" style="display: none;">
{{ form.estimated_return_date.label_tag }}
{{ form.estimated_return_date }}
{{ form.estimated_return_date.errors }}
</div>
</div>
</div>
</div>
<!-- Bloc regroupant les produits à préparer -->
<div class="card mb-3 shadow">
<div class="card-header">
{% translate "Articles en stock" %}
</div>
<div class="card-body">
<!-- Zone de recherche et ajout -->
<div class="row mb-4">
<div class="col-md-6">
<label for="product-search">{% translate "Rechercher un produit" %}</label>
<input type="text"
id="product-search"
class="form-control"
placeholder="{% translate "Tapez pour rechercher..." %}"
autocomplete="off">
<!-- Dropdown pour les résultats -->
<div id="search-results" class="list-group position-absolute" style="display: none; z-index: 1000; max-height: 300px; overflow-y: auto; width: calc(50% - 30px);"></div>
</div>
<div class="col-md-3">
<label for="product-quantity" id="product-quantity-label">{% translate "Quantité" %}</label>
<input type="number"
id="product-quantity"
class="form-control"
value="1"
min="1"
placeholder="1">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" id="add-product-btn" class="btn btn-primary w-100">
<i class="bi bi-plus-circle"></i> {% translate "Ajouter" %}
</button>
</div>
</div>
<!-- Liste des produits ajoutés -->
<div id="products-list">
<h6 class="mb-3">{% translate "Produits sélectionnés" %}:</h6>
<div id="products-container">
<p class="text-muted text-center" id="empty-message">
<i class="bi bi-inbox"></i> {% translate "Aucun produit ajouté" %}
</p>
</div>
</div>
<!-- Formset caché pour la soumission -->
<div style="display: none;">
{{ formset.management_form }}
<div id="formset-container"></div>
</div>
</div>
</div>
<!-- Bloc "Autres produits" -->
<div class="card mb-3 shadow">
<div class="card-header">
<i class="bi bi-pencil-square"></i> {% translate "Autres produits" %}
</div>
<div class="card-body">
<p class="text-muted small">
<i class="bi bi-info-circle"></i>
{% translate "Ajoutez des lignes pour des produits non référencés : décrivez librement, choisissez la catégorie et la quantité." %}
</p>
<!-- Formulaire d'ajout -->
<div class="row g-2 mb-3 align-items-start">
<div class="col-md-6">
<label for="other-description">{% translate "Description" %}</label>
<textarea
id="other-description"
class="form-control"
placeholder="{% translate 'Description du produit...' %}"
autocomplete="off"
rows="2"
></textarea>
</div>
<div class="col-md-2">
<label for="other-quantity">{% translate "Quantité" %}</label>
<input type="number"
id="other-quantity"
class="form-control"
value="1"
min="1">
</div>
<div class="col-md-2">
<label for="other-category">{% translate "Catégorie" %}</label>
<select id="other-category" class="form-select">
<option value="">--- {% translate "Catégorie" %} ---</option>
{% for cat in categories %}
<option value="{{ cat.id }}" data-code="{{ cat.code }}">{{ cat.name_fr }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2" id="other-signtype-wrapper" style="display:none;">
<label for="other-signtype">{% translate "Type" %}</label>
<input type="text"
id="other-signtype"
class="form-control"
placeholder="{% translate 'Code ou nom...' %}"
autocomplete="off">
<div id="other-signtype-results" class="list-group position-absolute" style="display:none;z-index:1002;max-height:250px;overflow-y:auto;"></div>
<input type="hidden" id="other-signtype-id" value="">
</div>
</div>
<div class="row mb-3">
<div class="col-auto">
<button type="button" id="add-other-btn" class="btn btn-light">
<i class="bi bi-plus-circle"></i>{% translate "Ajouter la ligne" %}
</button>
</div>
</div>
<!-- Liste des lignes ajoutées -->
<div id="other-items-list">
<p class="text-muted text-center" id="other-empty-message">
<i class="bi bi-inbox"></i> {% translate "Aucune ligne ajoutée" %}
</p>
</div>
<!-- Champs cachés pour soumission -->
<div style="display:none;">
<input type="hidden" id="other-TOTAL_FORMS" name="other-TOTAL_FORMS" value="0">
<div id="other-formset-container"></div>
</div>
</div>
</div>
<!-- Documents joints -->
<div class="card mb-3 shadow">
<div class="card-header">
<i class="bi bi-paperclip me-1"></i>{% translate "Documents joints" %}
</div>
<div class="card-body">
<div id="prep-drop-zone" class="drop-zone">
<i class="bi bi-cloud-upload fs-2 text-muted"></i>
<p class="mt-2 mb-1 fw-semibold">{% translate "Glissez-déposez vos fichiers ici" %}</p>
<p class="text-muted small">{% translate "ou" %}</p>
<label for="prep_documents_input" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-folder2-open me-1"></i>{% translate "Parcourir..." %}
</label>
<input type="file" id="prep_documents_input" name="documents" multiple class="d-none"
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.odt,.ods,.txt">
</div>
<div id="prep-file-list" class="mt-2"></div>
<div class="form-text">{% translate "Formats acceptés : images, PDF, Word, Excel, ODT, ODS, texte. Optionnel." %}</div>
</div>
</div>
<!-- Envoi du formulaire -->
<div class="d-flex justify-content-between mt-4">
{% if from_intervention %}
<a href="{% url 'interventions:intervention_detail' intervention.id %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> {% translate "Retour à l'intervention" %}
</a>
{% else %}
<a href="{% url 'stock:stock_movements' %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> {% translate "Retour" %}
</a>
{% endif %}
<button type="submit" class="btn btn-success">
<i class="bi bi-check-circle"></i> {% translate "Créer le bon de préparation" %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}
{% block script %}
<style>
#search-results .list-group-item {
cursor: pointer;
border-left: 3px solid transparent;
}
#search-results .list-group-item:hover {
background-color: #f8f9fa;
border-left-color: #0d6efd;
}
.product-card {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
background: #f8f9fa;
}
.location-badge {
background-color: #e9ecef;
padding: 2px 8px;
border-radius: 4px;
margin-right: 5px;
font-size: 0.85rem;
}
.product-info {
font-size: 0.9rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Gestion du prêt de matériel
const isLoanCheckbox = document.getElementById('{{ form.is_loan.auto_id }}');
const returnDateWrapper = document.getElementById('estimated_return_date_wrapper');
const returnDateInput = document.getElementById('{{ form.estimated_return_date.auto_id }}');
function toggleReturnDateField() {
if (isLoanCheckbox && isLoanCheckbox.checked) {
returnDateWrapper.style.display = 'block';
if (returnDateInput) returnDateInput.required = true;
} else {
returnDateWrapper.style.display = 'none';
if (returnDateInput) {
returnDateInput.required = false;
returnDateInput.value = '';
}
}
}
if (isLoanCheckbox) {
isLoanCheckbox.addEventListener('change', toggleReturnDateField);
toggleReturnDateField(); // Etat initial
}
const productSearch = document.getElementById('product-search');
const searchResults = document.getElementById('search-results');
const productQuantity = document.getElementById('product-quantity');
const addProductBtn = document.getElementById('add-product-btn');
const productsContainer = document.getElementById('products-container');
const emptyMessage = document.getElementById('empty-message');
const formsetContainer = document.getElementById('formset-container');
const productsApiUrl = "{% url 'stock:products_autocomplete_api' %}";
// Détecter le préfixe du formset depuis le management form
const managementForm = formsetContainer.parentElement;
const totalFormsInput = managementForm.querySelector('[id$="-TOTAL_FORMS"]');
const formPrefix = totalFormsInput ? totalFormsInput.id.replace('-TOTAL_FORMS', '').replace('id_', '') : 'form';
console.log('Formset prefix detected:', formPrefix);
let selectedProduct = null;
let productsAdded = [];
let searchTimeout = null;
// Charger les produits existants du formset (en cas d'erreur de validation)
function loadExistingProducts() {
const productIds = [];
const productQuantities = {};
{% for form in formset %}
{% if form.product.value and form.quantity.value %}
productIds.push({{ form.product.value }});
productQuantities[{{ form.product.value }}] = {{ form.quantity.value }};
{% endif %}
{% endfor %}
if (productIds.length > 0) {
// Récupérer les détails de tous les produits en une seule requête
fetch(`${productsApiUrl}?ids=${productIds.join(',')}`)
.then(response => response.json())
.then(data => {
data.results.forEach(product => {
productsAdded.push({
id: product.id,
text: product.text,
code: product.code,
category: product.category,
total_stock: product.total_stock,
reserved_quantity: product.reserved_quantity || 0,
available_stock: product.available_stock || product.total_stock,
threshold: product.threshold,
locations: product.locations,
unit: product.unit || 'pc',
unit_display: product.unit_display || 'Pièce(s)',
quantity: productQuantities[product.id]
});
});
renderProductsList();
})
.catch(error => console.error('Erreur de chargement des produits:', error));
}
}
// Charger les produits existants au démarrage
loadExistingProducts();
// Recherche de produits avec debounce
productSearch.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
searchResults.style.display = 'none';
return;
}
searchTimeout = setTimeout(() => {
fetch(`${productsApiUrl}?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
displaySearchResults(data.results);
})
.catch(error => {
console.error('Erreur de recherche:', error);
});
}, 300);
});
// Afficher les résultats de recherche
function displaySearchResults(results) {
searchResults.innerHTML = '';
if (results.length === 0) {
searchResults.innerHTML = '<div class="list-group-item">{% translate "Aucun produit trouvé" %}</div>';
searchResults.style.display = 'block';
return;
}
results.forEach(product => {
const item = document.createElement('div');
item.className = 'list-group-item list-group-item-action';
const stockBadge = product.total_stock > 0
? `<span class="badge bg-success ms-2">${product.total_stock} ${product.unit_display || 'en stock'}</span>`
: `<span class="badge bg-secondary ms-2">Stock vide</span>`;
item.innerHTML = `
<strong>${product.text}</strong>
<small class="text-muted">(${product.code})</small>
${stockBadge}
${product.category ? `<br><small class="text-muted">${product.category}</small>` : ''}
`;
item.addEventListener('click', () => {
selectProduct(product);
});
searchResults.appendChild(item);
});
searchResults.style.display = 'block';
}
// Sélectionner un produit
function selectProduct(product) {
selectedProduct = product;
productSearch.value = `${product.text} (${product.code})`;
searchResults.style.display = 'none';
// Update quantity label with unit
const qtyLabel = document.getElementById('product-quantity-label');
if (qtyLabel) {
qtyLabel.textContent = product.unit_display
? `{% translate "Quantité" %} (${product.unit_display})`
: `{% translate "Quantité" %}`;
}
productQuantity.focus();
}
// Fermer les résultats si on clique ailleurs
document.addEventListener('click', function(e) {
if (!productSearch.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
// Ajouter un produit à la liste
addProductBtn.addEventListener('click', function() {
if (!selectedProduct) {
alert('{% translate "Veuillez sélectionner un produit" %}');
return;
}
const quantity = parseInt(productQuantity.value);
if (!quantity || quantity < 1) {
alert('{% translate "Veuillez entrer une quantité valide" %}');
return;
}
// Vérifier si le produit existe déjà
const existingIndex = productsAdded.findIndex(p => p.id === selectedProduct.id);
if (existingIndex !== -1) {
// Mettre à jour la quantité
productsAdded[existingIndex].quantity += quantity;
} else {
// Ajouter le nouveau produit
productsAdded.push({
id: selectedProduct.id,
text: selectedProduct.text,
code: selectedProduct.code,
category: selectedProduct.category,
total_stock: selectedProduct.total_stock,
reserved_quantity: selectedProduct.reserved_quantity || 0,
available_stock: selectedProduct.available_stock || selectedProduct.total_stock,
threshold: selectedProduct.threshold,
locations: selectedProduct.locations,
unit: selectedProduct.unit || 'pc',
unit_display: selectedProduct.unit_display || 'Pièce(s)',
quantity: quantity
});
}
// Réinitialiser les champs
productSearch.value = '';
productQuantity.value = '1';
selectedProduct = null;
// Mettre à jour l'affichage
renderProductsList();
});
// Afficher la liste des produits ajoutés
function renderProductsList() {
productsContainer.innerHTML = '';
if (productsAdded.length === 0) {
emptyMessage.style.display = 'block';
updateFormset();
return;
}
emptyMessage.style.display = 'none';
productsAdded.forEach((product, index) => {
const card = document.createElement('div');
card.className = 'product-card';
// Construire les emplacements
let locationsHtml = '';
if (product.locations && product.locations.length > 0) {
locationsHtml = '<div class="mt-2"><small class="text-muted"><i class="bi bi-geo-alt"></i> Emplacements:</small><br>';
product.locations.forEach(loc => {
const stockBadge = loc.quantity > 0
? `<span class="badge bg-success">${loc.quantity}</span>`
: `<span class="badge bg-secondary">0</span>`;
locationsHtml += `<span class="location-badge">${loc.warehouse} - ${loc.name} ${stockBadge}</span>`;
});
locationsHtml += '</div>';
}
const stockClass = product.total_stock < product.threshold ? 'text-danger' : 'text-success';
card.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">${product.text}</h6>
<div class="product-info">
<span class="text-muted">Code: ${product.code}</span>
${product.category ? ` | <span class="text-muted">${product.category}</span>` : ''}
<br>
<span class="${stockClass}">
<i class="bi bi-box-seam"></i> Stock total: ${product.total_stock}
</span>
${product.reserved_quantity > 0 ? ` | <span class="text-warning"><i class="bi bi-lock"></i> Réservé: ${product.reserved_quantity}</span>` : ''}
<br>
<span class="text-${product.available_stock >= product.quantity ? 'success' : 'danger'}">
<i class="bi bi-check-circle"></i> Disponible: ${product.available_stock}
</span>
${product.total_stock < product.threshold ? ' <i class="bi bi-exclamation-triangle text-warning"></i>' : ''}
</div>
${locationsHtml}
</div>
<div class="text-end ms-3">
<div class="mb-2">
<strong class="fs-5">${product.quantity}</strong>
<small class="text-muted">${product.unit_display || 'unités'}</small>
</div>
<button type="button" class="btn btn-danger btn-sm" onclick="removeProduct(${index})">
<i class="bi bi-trash"></i> {% translate "Retirer" %}
</button>
</div>
</div>
`;
productsContainer.appendChild(card);
});
// Mettre à jour le formset caché
updateFormset();
}
// Supprimer un produit
window.removeProduct = function(index) {
productsAdded.splice(index, 1);
renderProductsList();
};
// Mettre à jour le formset caché pour la soumission
function updateFormset() {
formsetContainer.innerHTML = '';
const totalFormsInput = managementForm.querySelector('[id$="-TOTAL_FORMS"]');
totalFormsInput.value = productsAdded.length;
productsAdded.forEach((product, index) => {
const productInput = document.createElement('input');
productInput.type = 'hidden';
productInput.name = `${formPrefix}-${index}-product`;
productInput.value = product.id;
const quantityInput = document.createElement('input');
quantityInput.type = 'hidden';
quantityInput.name = `${formPrefix}-${index}-quantity`;
quantityInput.value = product.quantity;
const idInput = document.createElement('input');
idInput.type = 'hidden';
idInput.name = `${formPrefix}-${index}-id`;
idInput.value = '';
const deleteInput = document.createElement('input');
deleteInput.type = 'hidden';
deleteInput.name = `${formPrefix}-${index}-DELETE`;
deleteInput.value = '';
formsetContainer.appendChild(productInput);
formsetContainer.appendChild(quantityInput);
formsetContainer.appendChild(idInput);
formsetContainer.appendChild(deleteInput);
});
}
// Soumettre le formulaire - valider qu'il y a au moins un produit ou une autre ligne
document.getElementById('preparation-order-form').addEventListener('submit', function(e) {
if (productsAdded.length === 0 && otherItemsAdded.length === 0) {
e.preventDefault();
alert('{% translate "Veuillez ajouter au moins un produit ou une autre ligne" %}');
return false;
}
});
// Permettre d'ajouter avec Enter dans le champ quantité
productQuantity.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addProductBtn.click();
}
});
// ================================
// Section "Autres produits"
// ================================
const otherDescription = document.getElementById('other-description');
const otherQuantity = document.getElementById('other-quantity');
const otherCategory = document.getElementById('other-category');
const otherSigntypeWrap = document.getElementById('other-signtype-wrapper');
const otherSigntype = document.getElementById('other-signtype');
const otherSigntypeRes = document.getElementById('other-signtype-results');
const otherSigntypeId = document.getElementById('other-signtype-id');
const addOtherBtn = document.getElementById('add-other-btn');
const otherItemsList = document.getElementById('other-items-list');
const otherEmptyMsg = document.getElementById('other-empty-message');
const otherTotalInput = document.getElementById('other-TOTAL_FORMS');
const otherFormset = document.getElementById('other-formset-container');
const signPanelTypesUrl = "{% url 'stock:sign_panel_types_api' %}";
// IDs des catégories "Signalisation"
const signalisationIds = new Set({{ signalisation_category_ids|default:"[]"|safe }});
let otherItemsAdded = [];
let signTypeTimeout = null;
let selectedSignType = null;
// Afficher/cacher le champ "type signalisation" selon la catégorie
otherCategory.addEventListener('change', function() {
const catId = parseInt(this.value) || 0;
if (signalisationIds.has(catId)) {
otherSigntypeWrap.style.display = '';
} else {
otherSigntypeWrap.style.display = 'none';
otherSigntype.value = '';
otherSigntypeId.value = '';
selectedSignType = null;
}
});
// Autocomplete pour les types de panneaux
otherSigntype.addEventListener('input', function() {
clearTimeout(signTypeTimeout);
const q = this.value.trim();
otherSigntypeId.value = '';
selectedSignType = null;
if (q.length < 1) { otherSigntypeRes.style.display = 'none'; return; }
signTypeTimeout = setTimeout(() => {
fetch(`${signPanelTypesUrl}?q=${encodeURIComponent(q)}`)
.then(r => r.json())
.then(data => showSignTypeResults(data.results))
.catch(() => {});
}, 250);
});
function showSignTypeResults(results) {
otherSigntypeRes.innerHTML = '';
if (!results || results.length === 0) {
otherSigntypeRes.innerHTML = '<div class="list-group-item text-muted small">{% translate "Aucun type trouvé" %}</div>';
} else {
results.forEach(function(pt) {
const item = document.createElement('div');
item.className = 'list-group-item list-group-item-action';
item.innerHTML = `<strong>${pt.code}</strong> — <span class="text-muted">${pt.name}</span>`;
item.addEventListener('click', () => selectSignType(pt));
otherSigntypeRes.appendChild(item);
});
}
otherSigntypeRes.style.display = 'block';
}
function selectSignType(pt) {
selectedSignType = pt;
otherSigntype.value = `${pt.code} — ${pt.name}`;
otherSigntypeId.value = pt.id;
otherSigntypeRes.style.display = 'none';
}
document.addEventListener('click', function(e) {
if (!otherSigntype.contains(e.target) && !otherSigntypeRes.contains(e.target)) {
otherSigntypeRes.style.display = 'none';
}
});
// Ajouter une ligne "autre produit"
addOtherBtn.addEventListener('click', function() {
const desc = otherDescription.value.trim();
if (!desc) {
alert('{% translate "Veuillez saisir une description" %}');
otherDescription.focus();
return;
}
const qty = parseInt(otherQuantity.value) || 1;
const catId = parseInt(otherCategory.value) || null;
const catText = catId ? otherCategory.options[otherCategory.selectedIndex].text : '';
const isSignalisation = catId && signalisationIds.has(catId);
let signTypeId = null;
let signTypeText = '';
if (isSignalisation && otherSigntypeId.value) {
signTypeId = parseInt(otherSigntypeId.value);
signTypeText = otherSigntype.value;
}
otherItemsAdded.push({ desc, qty, catId, catText, signTypeId, signTypeText });
// Réinitialiser
otherDescription.value = '';
otherQuantity.value = '1';
otherCategory.value = '';
otherSigntypeWrap.style.display = 'none';
otherSigntype.value = '';
otherSigntypeId.value = '';
selectedSignType = null;
renderOtherItems();
});
otherDescription.addEventListener('keypress', function(e) {
if (e.key === 'Enter') { e.preventDefault(); addOtherBtn.click(); }
});
function renderOtherItems() {
otherItemsList.querySelectorAll('.other-item-card').forEach(el => el.remove());
otherEmptyMsg.style.display = otherItemsAdded.length === 0 ? 'block' : 'none';
otherItemsAdded.forEach(function(item, index) {
const card = document.createElement('div');
card.className = 'other-item-card product-card d-flex justify-content-between align-items-start';
card.innerHTML = `
<div class="flex-grow-1">
<h6 class="mb-1">${item.desc}</h6>
<small class="text-muted">
${item.catText ? `<i class="bi bi-tag"></i> ${item.catText}` : ''}
${item.signTypeText ? ` &bull; <i class="bi bi-sign-stop"></i> ${item.signTypeText}` : ''}
</small>
</div>
<div class="text-end ms-3">
<div class="mb-2"><strong class="fs-5">${item.qty}</strong> <small class="text-muted">{% translate "unités" %}</small></div>
<button type="button" class="btn btn-danger btn-sm" onclick="removeOtherItem(${index})">
<i class="bi bi-trash"></i> {% translate "Retirer" %}
</button>
</div>`;
otherItemsList.appendChild(card);
});
updateOtherFormset();
}
window.removeOtherItem = function(index) {
otherItemsAdded.splice(index, 1);
renderOtherItems();
};
function updateOtherFormset() {
otherFormset.innerHTML = '';
otherTotalInput.value = otherItemsAdded.length;
otherItemsAdded.forEach(function(item, i) {
const f = (name, val) => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = `other-${i}-${name}`;
inp.value = val !== null && val !== undefined ? val : '';
otherFormset.appendChild(inp);
};
f('description', item.desc);
f('quantity', item.qty);
f('category_id', item.catId || '');
f('sign_panel_type_id', item.signTypeId || '');
});
}
// ================================
// Zone de dépôt documents
// ================================
(function() {
const dropZone = document.getElementById('prep-drop-zone');
const fileInput = document.getElementById('prep_documents_input');
const fileList = document.getElementById('prep-file-list');
if (!dropZone || !fileInput) return;
let selectedFiles = new DataTransfer();
function renderFileList() {
fileList.innerHTML = '';
Array.from(selectedFiles.files).forEach(function(file, idx) {
const item = document.createElement('div');
item.className = 'd-flex align-items-center gap-2 bg-white border rounded p-2 mt-1';
item.innerHTML = `
<i class="bi bi-file-earmark text-secondary"></i>
<span class="flex-grow-1 text-truncate small">${file.name}</span>
<small class="text-muted">${(file.size/1024).toFixed(0)} Ko</small>
<button type="button" class="btn btn-sm btn-outline-danger py-0 px-1 remove-file-btn" data-idx="${idx}">
<i class="bi bi-x"></i>
</button>`;
fileList.appendChild(item);
});
fileInput.files = selectedFiles.files;
}
function addFiles(files) {
Array.from(files).forEach(function(f) { selectedFiles.items.add(f); });
renderFileList();
}
dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.classList.add('dragover'); });
dropZone.addEventListener('dragleave', function() { dropZone.classList.remove('dragover'); });
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.classList.remove('dragover');
addFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', function() {
addFiles(this.files);
this.value = '';
});
fileList.addEventListener('click', function(e) {
const btn = e.target.closest('.remove-file-btn');
if (!btn) return;
const idx = parseInt(btn.dataset.idx);
const dt = new DataTransfer();
Array.from(selectedFiles.files).forEach(function(f, i) { if (i !== idx) dt.items.add(f); });
selectedFiles = dt;
renderFileList();
});
})();
});
</script>
<style>
.drop-zone { border: 2px dashed #6c757d; border-radius: 0.5rem; padding: 2rem; text-align: center; cursor: pointer; transition: background .2s, border-color .2s; }
.drop-zone.dragover, .drop-zone:hover { border-color: #0d6efd; background: #e8f0fe; }
</style>
{% endblock script %}