@@ -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 -->
0 commit comments