mvp
This commit is contained in:
@@ -72,6 +72,12 @@
|
||||
{{ form.currency }}
|
||||
{% if form.currency.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.currency.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.responsible_person.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Responsable Solicitante</label>
|
||||
{{ form.responsible_person }}
|
||||
{% if form.responsible_person.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.responsible_person.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
@@ -94,15 +100,29 @@
|
||||
{% if form.description.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1.5rem; background: #f8fafc; padding: 1rem; border-radius: 6px; border: 1px dashed var(--efintel-blue);">
|
||||
<label for="{{ form.receipt.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-petroleum); display:flex; align-items:center; gap:0.3rem;"><i data-lucide="paperclip" style="width:14px;"></i> Adjuntar Evidencia (Boleta / Factura)</label>
|
||||
<div class="form-group" style="margin-bottom: 1.5rem; background: #f8fafc; padding: 1.5rem; border-radius: 6px; border: 1px dashed var(--efintel-blue);">
|
||||
<label for="{{ form.receipt.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-petroleum); display:flex; align-items:center; gap:0.3rem;"><i data-lucide="paperclip" style="width:14px;"></i> Adjuntar Evidencia(s) (Boletas / Facturas)</label>
|
||||
{{ form.receipt }}
|
||||
<small style="color: var(--efintel-gray); font-size: 0.7rem; display:block; margin-top:0.3rem;">Formatos aceptados: Imágenes (.png, .jpg) o Documentos PDF.</small>
|
||||
<div id="new-files-list" style="margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.4rem;"></div>
|
||||
<small style="color: var(--efintel-gray); font-size: 0.7rem; display:block; margin-top:0.3rem; margin-bottom: 0.75rem;">Puedes seleccionar múltiples archivos (imágenes o PDF).</small>
|
||||
{% if form.receipt.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.receipt.errors }}</div>{% endif %}
|
||||
|
||||
{% if form.instance.pk and form.instance.documents.first %}
|
||||
<div style="margin-top:0.8rem; font-size:0.8rem;">
|
||||
<strong>Evidencia Actual:</strong> <a href="{{ form.instance.documents.first.file.url }}" target="_blank" style="color:var(--efintel-blue);">Ver Archivo Adjunto</a>
|
||||
{% if form.instance.pk and form.instance.documents.all %}
|
||||
<div style="margin-top: 1rem; border-top: 1px solid #e2e8f0; padding-top: 0.75rem;">
|
||||
<strong style="font-size: 0.8rem; color: var(--efintel-petroleum); display: block; margin-bottom: 0.5rem;">Documentos Adjuntos Actuales:</strong>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
{% for doc in form.instance.documents.all %}
|
||||
<div class="document-row" id="doc-row-{{ doc.id }}" style="display: flex; justify-content: space-between; align-items: center; background: white; padding: 0.5rem 0.75rem; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 0.8rem;">
|
||||
<span style="font-weight: 500; color: var(--efintel-dark); display: flex; align-items: center; gap: 0.3rem;">
|
||||
<i data-lucide="file-text" style="width: 14px; color: var(--efintel-blue);"></i>
|
||||
<a href="{{ doc.file.url }}" target="_blank" style="color: var(--efintel-blue); text-decoration: none;">{{ doc.title|default:doc.file.name }}</a>
|
||||
</span>
|
||||
<button type="button" onclick="deleteDocument(event, {{ doc.id }})" style="background: none; border: none; color: var(--efintel-danger); cursor: pointer; display: flex; align-items: center; padding: 0.2rem;" title="Eliminar evidencia">
|
||||
<i data-lucide="trash-2" style="width: 14px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -134,8 +154,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chilean regions JSON data -->
|
||||
<script id="regions-communes-data" type="application/json">
|
||||
{{ regions_communes_json|safe }}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
document.querySelectorAll('input:not([type="checkbox"]), select, textarea').forEach(el => {
|
||||
el.style.width = '100%';
|
||||
el.style.padding = '0.65rem';
|
||||
el.style.border = '1px solid var(--efintel-border)';
|
||||
@@ -177,10 +202,8 @@
|
||||
const dropdownList = document.getElementById('category-dropdown-list');
|
||||
|
||||
if (categorySelect && searchInput && dropdownList) {
|
||||
// Hide original select (make it display none so form data is still submitted)
|
||||
categorySelect.style.display = 'none';
|
||||
|
||||
// Extract options from the django-generated select
|
||||
const options = Array.from(categorySelect.options)
|
||||
.filter(opt => opt.value !== '')
|
||||
.map(opt => ({
|
||||
@@ -188,7 +211,6 @@
|
||||
text: opt.textContent
|
||||
}));
|
||||
|
||||
// Render option list dynamically
|
||||
function renderList(filteredOptions) {
|
||||
dropdownList.innerHTML = '';
|
||||
if (filteredOptions.length === 0) {
|
||||
@@ -200,66 +222,68 @@
|
||||
const item = document.createElement('div');
|
||||
item.className = 'dropdown-item';
|
||||
item.style.padding = '0.65rem 1rem';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.fontSize = '0.85rem';
|
||||
item.style.borderBottom = '1px solid #f1f5f9';
|
||||
item.textContent = opt.text;
|
||||
|
||||
// Mark parent categories differently (they usually have a badge in representation or just end with standard code)
|
||||
// Note: we can detect if it's a main partida (ends in '000: ' or contains 'Partida')
|
||||
// Check if this is a parent category (1000, 2000, 3000, 4000)
|
||||
const isParent = opt.text.includes('1000: ') || opt.text.includes('2000: ') || opt.text.includes('3000: ') || opt.text.includes('4000: ');
|
||||
if (isParent) {
|
||||
item.style.fontWeight = '700';
|
||||
item.style.background = '#f8fafc';
|
||||
item.style.color = 'var(--efintel-petroleum)';
|
||||
item.style.background = '#f1f5f9';
|
||||
item.style.color = 'var(--efintel-gray)';
|
||||
item.style.cursor = 'not-allowed';
|
||||
item.style.opacity = '0.8';
|
||||
|
||||
// Disable in the original select to prevent browser submit/autofill
|
||||
const origOpt = categorySelect.querySelector(`option[value="${opt.value}"]`);
|
||||
if (origOpt) origOpt.disabled = true;
|
||||
} else {
|
||||
item.style.cursor = 'pointer';
|
||||
item.addEventListener('click', () => {
|
||||
categorySelect.value = opt.value;
|
||||
searchInput.value = opt.text;
|
||||
dropdownList.style.display = 'none';
|
||||
categorySelect.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = 'var(--efintel-light)';
|
||||
item.style.color = 'var(--efintel-blue)';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.background = 'white';
|
||||
item.style.color = 'inherit';
|
||||
});
|
||||
}
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
categorySelect.value = opt.value;
|
||||
searchInput.value = opt.text;
|
||||
dropdownList.style.display = 'none';
|
||||
categorySelect.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = 'var(--efintel-light)';
|
||||
item.style.color = 'var(--efintel-blue)';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.background = isParent ? '#f8fafc' : 'white';
|
||||
item.style.color = isParent ? 'var(--efintel-petroleum)' : 'inherit';
|
||||
});
|
||||
|
||||
dropdownList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize input value if edit mode
|
||||
const selectedOption = categorySelect.options[categorySelect.selectedIndex];
|
||||
if (selectedOption && selectedOption.value !== '') {
|
||||
searchInput.value = selectedOption.textContent;
|
||||
}
|
||||
|
||||
// Show dropdown list on focus
|
||||
searchInput.addEventListener('focus', () => {
|
||||
renderList(options);
|
||||
dropdownList.style.display = 'block';
|
||||
});
|
||||
|
||||
// Dynamic filtering
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = options.filter(opt => opt.text.toLowerCase().includes(query));
|
||||
renderList(filtered);
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!searchInput.contains(e.target) && !dropdownList.contains(e.target)) {
|
||||
dropdownList.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Custom Searchable Dropdown for Project
|
||||
const projectSelect = document.getElementById('id_project');
|
||||
const projectSearchInput = document.getElementById('project-search-input');
|
||||
@@ -286,34 +310,41 @@
|
||||
const item = document.createElement('div');
|
||||
item.className = 'dropdown-item';
|
||||
item.style.padding = '0.65rem 1rem';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.fontSize = '0.85rem';
|
||||
item.style.borderBottom = '1px solid #f1f5f9';
|
||||
item.textContent = opt.text;
|
||||
|
||||
// Check if this is a Portfolio (starts with PORT-). Cost Centers (CC-) are selectable.
|
||||
const isPortfolio = opt.text.startsWith('PORT-');
|
||||
if (isPortfolio) {
|
||||
item.style.fontWeight = '700';
|
||||
item.style.background = '#f8fafc';
|
||||
item.style.color = 'var(--efintel-petroleum)';
|
||||
item.style.background = '#f1f5f9';
|
||||
item.style.color = 'var(--efintel-gray)';
|
||||
item.style.cursor = 'not-allowed';
|
||||
item.style.opacity = '0.8';
|
||||
|
||||
// Disable in the original select
|
||||
const origOpt = projectSelect.querySelector(`option[value="${opt.value}"]`);
|
||||
if (origOpt) origOpt.disabled = true;
|
||||
} else {
|
||||
item.style.cursor = 'pointer';
|
||||
item.addEventListener('click', () => {
|
||||
projectSelect.value = opt.value;
|
||||
projectSearchInput.value = opt.text;
|
||||
projectDropdownList.style.display = 'none';
|
||||
projectSelect.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = 'var(--efintel-light)';
|
||||
item.style.color = 'var(--efintel-blue)';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.background = 'white';
|
||||
item.style.color = 'inherit';
|
||||
});
|
||||
}
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
projectSelect.value = opt.value;
|
||||
projectSearchInput.value = opt.text;
|
||||
projectDropdownList.style.display = 'none';
|
||||
projectSelect.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = 'var(--efintel-light)';
|
||||
item.style.color = 'var(--efintel-blue)';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.background = isPortfolio ? '#f8fafc' : 'white';
|
||||
item.style.color = isPortfolio ? 'var(--efintel-petroleum)' : 'inherit';
|
||||
});
|
||||
|
||||
projectDropdownList.appendChild(item);
|
||||
});
|
||||
}
|
||||
@@ -340,5 +371,148 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dynamic Region & Commune Dropdowns
|
||||
const regionSelect = document.getElementById('id_region');
|
||||
const communeSelect = document.getElementById('id_comuna');
|
||||
const dataEl = document.getElementById('regions-communes-data');
|
||||
|
||||
if (regionSelect && communeSelect && dataEl) {
|
||||
const regionsCommunes = JSON.parse(dataEl.textContent);
|
||||
|
||||
function updateCommunes(selectedRegion, selectedCommune = '') {
|
||||
communeSelect.innerHTML = '<option value="">--- Seleccione Comuna ---</option>';
|
||||
if (selectedRegion && regionsCommunes[selectedRegion]) {
|
||||
regionsCommunes[selectedRegion].forEach(comm => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = comm;
|
||||
opt.textContent = comm;
|
||||
if (comm === selectedCommune) {
|
||||
opt.selected = true;
|
||||
}
|
||||
communeSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
regionSelect.addEventListener('change', (e) => {
|
||||
updateCommunes(e.target.value);
|
||||
});
|
||||
|
||||
// Initialize region/commune values on load
|
||||
const initialRegion = regionSelect.value;
|
||||
const initialCommune = "{{ form.instance.comuna|default:'' }}";
|
||||
if (initialRegion) {
|
||||
updateCommunes(initialRegion, initialCommune);
|
||||
}
|
||||
}
|
||||
|
||||
// AJAX Document Evidences Deletion (Saved in DB)
|
||||
function deleteDocument(e, docId) {
|
||||
e.preventDefault();
|
||||
if (!confirm('¿Estás seguro de que deseas eliminar este documento de evidencia?')) return;
|
||||
|
||||
fetch(`/gastos/documento/${docId}/eliminar/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token }}',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const row = document.getElementById(`doc-row-${docId}`);
|
||||
if (row) {
|
||||
row.style.transition = 'all 0.3s';
|
||||
row.style.opacity = '0';
|
||||
setTimeout(() => row.remove(), 300);
|
||||
}
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'No se pudo eliminar el archivo.'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
alert('Error al conectar con el servidor.');
|
||||
});
|
||||
}
|
||||
|
||||
// Client-side Upload File Deletion Queue using DataTransfer
|
||||
const fileInput = document.getElementById('id_receipt');
|
||||
const newFilesList = document.getElementById('new-files-list');
|
||||
|
||||
if (fileInput && newFilesList) {
|
||||
let dt = new DataTransfer();
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
// Append newly selected files to our ongoing list
|
||||
for (let i = 0; i < fileInput.files.length; i++) {
|
||||
dt.items.add(fileInput.files[i]);
|
||||
}
|
||||
// Update input files list
|
||||
fileInput.files = dt.files;
|
||||
renderNewFiles();
|
||||
});
|
||||
|
||||
function renderNewFiles() {
|
||||
newFilesList.innerHTML = '';
|
||||
|
||||
if (dt.files.length > 0) {
|
||||
const header = document.createElement('strong');
|
||||
header.style.fontSize = '0.75rem';
|
||||
header.style.color = 'var(--efintel-blue)';
|
||||
header.style.display = 'block';
|
||||
header.style.marginBottom = '0.35rem';
|
||||
header.textContent = 'Archivos en cola para subir:';
|
||||
newFilesList.appendChild(header);
|
||||
}
|
||||
|
||||
Array.from(dt.files).forEach((file, index) => {
|
||||
const row = document.createElement('div');
|
||||
row.style.display = 'flex';
|
||||
row.style.justifyContent = 'space-between';
|
||||
row.style.alignItems = 'center';
|
||||
row.style.background = 'var(--efintel-light)';
|
||||
row.style.padding = '0.45rem 0.75rem';
|
||||
row.style.borderRadius = '4px';
|
||||
row.style.fontSize = '0.75rem';
|
||||
row.style.border = '1px solid var(--efintel-border)';
|
||||
row.style.marginBottom = '0.3rem';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.style.color = 'var(--efintel-petroleum)';
|
||||
nameSpan.style.fontWeight = '600';
|
||||
nameSpan.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
|
||||
row.appendChild(nameSpan);
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.type = 'button';
|
||||
deleteBtn.style.background = 'none';
|
||||
deleteBtn.style.border = 'none';
|
||||
deleteBtn.style.color = 'var(--efintel-danger)';
|
||||
deleteBtn.style.cursor = 'pointer';
|
||||
deleteBtn.style.padding = '0.2rem';
|
||||
deleteBtn.style.display = 'flex';
|
||||
deleteBtn.style.alignItems = 'center';
|
||||
deleteBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;
|
||||
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const newDt = new DataTransfer();
|
||||
for (let i = 0; i < dt.files.length; i++) {
|
||||
if (i !== index) {
|
||||
newDt.items.add(dt.files[i]);
|
||||
}
|
||||
}
|
||||
dt = newDt;
|
||||
fileInput.files = dt.files;
|
||||
renderNewFiles();
|
||||
});
|
||||
row.appendChild(deleteBtn);
|
||||
|
||||
newFilesList.appendChild(row);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user