Skip to content

Commit a950879

Browse files
committed
Update
1 parent d9c9b62 commit a950879

6 files changed

Lines changed: 341 additions & 45 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 5.1.6 on 2025-08-17 12:58
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("inventory", "0008_auto_20250720_2343"),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name="attachment",
15+
options={
16+
"ordering": ["order", "upload_date"],
17+
"verbose_name": "attachment",
18+
"verbose_name_plural": "attachments",
19+
},
20+
),
21+
migrations.AddField(
22+
model_name="attachment",
23+
name="order",
24+
field=models.PositiveIntegerField(
25+
default=0,
26+
help_text="Order in which attachments are displayed",
27+
verbose_name="order",
28+
),
29+
),
30+
]

landolfio/inventory/models/attachment.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,19 @@ class Attachment(models.Model):
2525
upload_date = models.DateField(
2626
auto_now_add=True, verbose_name=_("upload date"), max_length=255
2727
)
28+
order = models.PositiveIntegerField(
29+
default=0,
30+
verbose_name=_("order"),
31+
help_text=_("Order in which attachments are displayed"),
32+
)
2833

2934
def __str__(self):
3035
return f"{self.attachment} {_('from')} {self.asset}"
3136

3237
class Meta:
3338
verbose_name = _("attachment")
3439
verbose_name_plural = _("attachments")
35-
ordering = ["upload_date"]
40+
ordering = ["order", "upload_date"]
3641

3742
def __init__(self, *args, **kwargs):
3843
super().__init__(*args, **kwargs)

landolfio/inventory_frontend/templates/detail.html

Lines changed: 210 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,16 +411,27 @@ <h5 class="card-title mb-0">{% translate "Attachments" %}</h5>
411411
</div>
412412
<div class="card-body">
413413
{% if asset.attachments.exists %}
414-
<div class="row g-3 mb-4">
414+
<div class="row g-3 mb-4" id="attachments-gallery">
415415
{% for photo in asset.attachments.all %}
416-
<div class="col-6 col-md-4 col-lg-3">
417-
<div class="position-relative" style="aspect-ratio: 1;">
416+
<div class="col-6 col-md-4 col-lg-3" data-attachment-id="{{ photo.pk }}">
417+
<div class="position-relative attachment-item" style="aspect-ratio: 1;">
418418
<img src="{{ photo.attachment.url }}"
419419
data-fancybox="gallery"
420420
data-caption="{{ photo.upload_date}} · {{ photo.attachment.name }}"
421421
alt="{{ photo.attachment.name }}"
422422
class="img-fluid rounded w-100 h-100"
423423
style="object-fit: cover; cursor: pointer;"/>
424+
<div class="attachment-controls position-absolute top-0 end-0 p-2">
425+
<button class="btn btn-sm btn-danger delete-attachment-btn"
426+
data-attachment-id="{{ photo.pk }}"
427+
title="{% translate 'Delete attachment' %}">
428+
<i class="fas fa-trash"></i>
429+
</button>
430+
</div>
431+
<div class="drag-handle position-absolute top-0 start-0 p-2"
432+
title="{% translate 'Drag to reorder' %}">
433+
<i class="fas fa-grip-vertical text-white" style="text-shadow: 1px 1px 2px rgba(0,0,0,0.8);"></i>
434+
</div>
424435
</div>
425436
</div>
426437
{% endfor %}
@@ -668,6 +679,7 @@ <h5 class="modal-title">{% translate "Delete Asset" %}</h5>
668679
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui/dist/fancybox.css"/>
669680

670681
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
682+
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
671683
{% endblock %}
672684

673685
{% block extrastyle %}
@@ -717,6 +729,45 @@ <h5 class="modal-title">{% translate "Delete Asset" %}</h5>
717729
.filepond--credits {
718730
display: none;
719731
}
732+
733+
.attachment-item {
734+
transition: transform 0.2s ease;
735+
}
736+
737+
.attachment-item:hover {
738+
transform: scale(1.02);
739+
}
740+
741+
.attachment-controls {
742+
opacity: 0;
743+
transition: opacity 0.2s ease;
744+
}
745+
746+
.attachment-item:hover .attachment-controls {
747+
opacity: 1;
748+
}
749+
750+
.drag-handle {
751+
opacity: 0.7;
752+
cursor: grab;
753+
transition: opacity 0.2s ease;
754+
}
755+
756+
.drag-handle:active {
757+
cursor: grabbing;
758+
}
759+
760+
.attachment-item:hover .drag-handle {
761+
opacity: 1;
762+
}
763+
764+
.sortable-ghost {
765+
opacity: 0.5;
766+
}
767+
768+
.sortable-drag {
769+
transform: rotate(5deg);
770+
}
720771
</style>
721772
{% endblock %}
722773

@@ -861,6 +912,162 @@ <h5 class="modal-title">{% translate "Delete Asset" %}</h5>
861912
});
862913
}
863914
});
915+
916+
// Attachment Management Functions
917+
function initAttachmentFeatures() {
918+
// Initialize drag-and-drop reordering
919+
const galleryElement = document.getElementById('attachments-gallery');
920+
if (galleryElement) {
921+
new Sortable(galleryElement, {
922+
animation: 150,
923+
ghostClass: 'sortable-ghost',
924+
dragClass: 'sortable-drag',
925+
handle: '.drag-handle',
926+
onEnd: function(evt) {
927+
const attachmentIds = Array.from(galleryElement.children).map(item =>
928+
item.getAttribute('data-attachment-id')
929+
);
930+
updateAttachmentOrder(attachmentIds);
931+
}
932+
});
933+
}
934+
935+
// Initialize delete buttons
936+
const deleteButtons = document.querySelectorAll('.delete-attachment-btn');
937+
deleteButtons.forEach(button => {
938+
button.removeEventListener('click', handleDeleteClick);
939+
button.addEventListener('click', handleDeleteClick);
940+
});
941+
}
942+
943+
function handleDeleteClick(e) {
944+
e.preventDefault();
945+
e.stopPropagation();
946+
947+
const attachmentId = this.getAttribute('data-attachment-id');
948+
if (confirm('{% translate "Are you sure you want to delete this attachment?" %}')) {
949+
deleteAttachment(attachmentId);
950+
}
951+
}
952+
953+
function updateAttachmentOrder(attachmentIds) {
954+
const csrfToken = getCsrfToken();
955+
if (!csrfToken) return;
956+
957+
fetch('{% url "inventory_frontend:attachment_reorder" asset.pk %}', {
958+
method: 'POST',
959+
headers: {
960+
'Content-Type': 'application/json',
961+
'X-CSRFToken': csrfToken,
962+
},
963+
body: JSON.stringify({
964+
attachment_ids: attachmentIds
965+
})
966+
})
967+
.then(response => response.json())
968+
.then(data => {
969+
if (!data.success) {
970+
location.reload();
971+
}
972+
})
973+
.catch(() => location.reload());
974+
}
975+
976+
function deleteAttachment(attachmentId) {
977+
const csrfToken = getCsrfToken();
978+
if (!csrfToken) return;
979+
980+
const deleteUrl = `{% url "inventory_frontend:attachment_delete" asset.pk 0 %}`.replace('0', attachmentId);
981+
982+
fetch(deleteUrl, {
983+
method: 'POST',
984+
headers: {
985+
'X-CSRFToken': csrfToken,
986+
}
987+
})
988+
.then(response => response.json())
989+
.then(data => {
990+
if (data.success) {
991+
const attachmentElement = document.querySelector(`[data-attachment-id="${attachmentId}"]`);
992+
if (attachmentElement) {
993+
attachmentElement.remove();
994+
}
995+
996+
const gallery = document.getElementById('attachments-gallery');
997+
if (gallery && gallery.children.length === 0) {
998+
location.reload();
999+
}
1000+
} else {
1001+
alert('{% translate "Error deleting attachment. Please try again." %}');
1002+
}
1003+
})
1004+
.catch(() => {
1005+
alert('{% translate "Error deleting attachment. Please try again." %}');
1006+
});
1007+
}
1008+
1009+
function getCsrfToken() {
1010+
const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]');
1011+
if (csrfInput) return csrfInput.value;
1012+
1013+
const cookies = document.cookie.split(';');
1014+
for (let cookie of cookies) {
1015+
const [name, value] = cookie.trim().split('=');
1016+
if (name === 'csrftoken') return value;
1017+
}
1018+
1019+
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
1020+
if (csrfMeta) return csrfMeta.getAttribute('content');
1021+
1022+
return null;
1023+
}
1024+
1025+
// Tab Memory Management
1026+
function initTabMemory() {
1027+
const tabs = document.querySelectorAll('#assetTabs .nav-link');
1028+
const tabContents = document.querySelectorAll('.tab-pane');
1029+
1030+
// Get saved tab from localStorage, default to 'overview'
1031+
const savedTab = localStorage.getItem('assetDetailActiveTab') || 'overview';
1032+
1033+
// Set active tab based on saved preference
1034+
tabs.forEach(tab => {
1035+
const targetId = tab.getAttribute('href').substring(1);
1036+
if (targetId === savedTab) {
1037+
// Remove active class from all tabs and contents
1038+
tabs.forEach(t => t.classList.remove('active'));
1039+
tabContents.forEach(tc => tc.classList.remove('show', 'active'));
1040+
1041+
// Add active class to current tab and content
1042+
tab.classList.add('active');
1043+
const targetContent = document.getElementById(targetId);
1044+
if (targetContent) {
1045+
targetContent.classList.add('show', 'active');
1046+
}
1047+
}
1048+
});
1049+
1050+
// Save tab selection when clicked
1051+
tabs.forEach(tab => {
1052+
tab.addEventListener('shown.bs.tab', function() {
1053+
const targetId = this.getAttribute('href').substring(1);
1054+
localStorage.setItem('assetDetailActiveTab', targetId);
1055+
1056+
// Reinitialize attachment features when attachments tab is shown
1057+
if (targetId === 'attachments') {
1058+
setTimeout(initAttachmentFeatures, 100);
1059+
}
1060+
});
1061+
});
1062+
}
1063+
1064+
// Initialize attachment functionality
1065+
setTimeout(initAttachmentFeatures, 500);
1066+
1067+
// Initialize tab memory and attachment features
1068+
document.addEventListener('DOMContentLoaded', function() {
1069+
initTabMemory();
1070+
});
8641071
</script>
8651072

8661073
<!-- Property Autocomplete Script -->

landolfio/inventory_frontend/urls.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
AssetUpdateView,
1010
AssetAutocompleteView,
1111
PropertyValueAutocompleteView,
12+
AttachmentDeleteView,
13+
AttachmentReorderView,
1214
)
1315

1416
app_name = "inventory_frontend"
@@ -27,4 +29,14 @@
2729
path("create/", AssetCreateView.as_view(), name="create"),
2830
path("asset/<uuid:pk>/update/", AssetUpdateView.as_view(), name="update"),
2931
path("asset/<uuid:pk>/delete/", AssetDeleteView.as_view(), name="delete"),
32+
path(
33+
"<uuid:asset_pk>/attachment/<int:attachment_pk>/delete/",
34+
AttachmentDeleteView.as_view(),
35+
name="attachment_delete",
36+
),
37+
path(
38+
"<uuid:asset_pk>/attachments/reorder/",
39+
AttachmentReorderView.as_view(),
40+
name="attachment_reorder",
41+
),
3042
]

0 commit comments

Comments
 (0)