diff --git a/.gitignore b/.gitignore index 5fe44dfc..130782bd 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ taxonomy/fixtures/test_tax_* *.csv *.pgdump compose.yaml +*.bacpac diff --git a/Dockerfile b/Dockerfile index ddf2d16f..96d317ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ + # syntax=docker/dockerfile:1 # Prepare the base environment. FROM python:3.11.8-slim as builder_base_wastd diff --git a/kustomize/overlays/prod/kustomization.yaml b/kustomize/overlays/prod/kustomization.yaml index 81f8c201..ac3d84cd 100644 --- a/kustomize/overlays/prod/kustomization.yaml +++ b/kustomize/overlays/prod/kustomization.yaml @@ -25,5 +25,5 @@ patches: - path: service_patch.yaml images: - name: ghcr.io/dbca-wa/wastd - newTag: 2.1.3 + newTag: 2.1.4 diff --git a/pyproject.toml b/pyproject.toml index a4fb1655..19bebe69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [tool.poetry] name = "wastd" -version = "2.1.3" +version = "2.1.4" + description = "Western Australian Sea Turtles Database" authors = ["Florian Mayer ", "Ashley Felton ","Evan Hallein ", "Rick Wang "] diff --git a/wamtram2/models.py b/wamtram2/models.py index 1d2090b5..dd955eae 100644 --- a/wamtram2/models.py +++ b/wamtram2/models.py @@ -42,6 +42,9 @@ class TrtBeachPositions(models.Model): class Meta: managed = False db_table = "TRT_BEACH_POSITIONS" + + def __str__(self): + return f"{self.description}" class TrtBodyParts(models.Model): @@ -2058,8 +2061,9 @@ class TrtRecordedTags(models.Model): side = models.CharField( db_column="SIDE", max_length=1, blank=True, null=True ) # Field name made lowercase. - tag_state = models.CharField( - db_column="TAG_STATE", max_length=10, blank=True, null=True + tag_state = models.ForeignKey( + 'TrtTagStates', models.CASCADE, db_column="TAG_STATE", max_length=10, + blank=True, null=True ) # Field name made lowercase. comments = models.CharField( db_column="COMMENTS", max_length=255, blank=True, null=True @@ -2222,7 +2226,6 @@ def __str__(self): return f"{self.common_name}" - class TrtTags(models.Model): tag_id = models.CharField( db_column="TAG_ID", primary_key=True, max_length=10 @@ -2490,6 +2493,7 @@ class Meta: def __str__(self): return f"{self.description}" + SEX_CHOICES = [ ("F", "Female"), ("M", "Male"), diff --git a/wamtram2/static/css/observation_management.css b/wamtram2/static/css/observation_management.css new file mode 100644 index 00000000..7ed74431 --- /dev/null +++ b/wamtram2/static/css/observation_management.css @@ -0,0 +1,90 @@ +.container-fluid { + padding: 20px; +} + + +.section-card { + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 5px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); +} + +.nav-tabs { + margin-bottom: 20px; +} + +.tab-content { + padding: 20px 0; +} + +.form-row { + margin-bottom: 15px; +} + +#searchResults { + margin-top: 20px; +} + +#searchResults th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; +} + +#searchResults td { + vertical-align: middle; +} + +.modified { + background-color: #fff3cd; +} + +.readonly-field { + background-color: #e9ecef; + cursor: not-allowed; +} + +.table-responsive { + overflow-x: auto; +} + +#searchResults { + margin-bottom: 0; +} + +#searchResults th { + background-color: #f8f9fa; + position: sticky; + top: 0; + z-index: 1; +} + +#searchResults td { + vertical-align: middle; +} + +.tag-info { + font-size: 0.85em; + color: #666; + margin-bottom: 5px; +} + +.action-buttons { + white-space: nowrap; +} + +@media (max-width: 768px) { + .container-fluid { + padding: 10px; + } + + .section-card { + padding: 15px; + } + + .form-row { + margin-bottom: 10px; + } +} \ No newline at end of file diff --git a/wamtram2/static/css/search_styles.css b/wamtram2/static/css/search_styles.css index 2e456ed4..0d2cc174 100644 --- a/wamtram2/static/css/search_styles.css +++ b/wamtram2/static/css/search_styles.css @@ -1,8 +1,48 @@ /* search_styles.css */ -/* Search input field style */ +/* Select2 customization */ +.select2-container .select2-selection--single { + height: 38px; + padding: 6px 12px; + font-size: 15px; + line-height: 1.42857143; + color: #495057; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} + +.select2-container--default .select2-selection--single .select2-selection__rendered { + line-height: 24px; + padding-left: 0; + color: #495057; +} + +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 36px; +} + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #f5f5f5; + color: #444; +} + +.select2-results__option { + padding: 6px 12px; + color: #444; + line-height: 1.42857143; +} + +.select2-dropdown { + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f8f9fa; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} + +/* Keep existing styles for other search elements */ .search-field { - /* Styles are similar to '.form-control' */ display: block; width: 100%; height: 38px; @@ -15,14 +55,10 @@ background-image: none; border: 1px solid #ccc; border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; } -/* Search results container style */ .search-results { border: 1px solid #ccc; border-radius: 5px; @@ -33,11 +69,10 @@ position: absolute; z-index: 1000; width: 100%; - max-height: 200px; /* Max height */ - overflow-y: auto; /* Scroll if too tall */ + max-height: 200px; + overflow-y: auto; } -/* Search result item style */ .search-result { padding: 6px 12px; cursor: pointer; @@ -45,12 +80,10 @@ line-height: 1.42857143; } -/* Style for the result item when hovered */ .search-result:hover { background-color: #f5f5f5; } -/* Style for the result item when it is selected */ .search-result.selected { background-color: #e9e9e9; } diff --git a/wamtram2/static/css/turtle_management.css b/wamtram2/static/css/turtle_management.css new file mode 100644 index 00000000..3516af89 --- /dev/null +++ b/wamtram2/static/css/turtle_management.css @@ -0,0 +1,143 @@ + +.form-label { + color: #495057; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.table-container { + overflow-y: auto; + position: relative; + height: 40vh; + margin: 1rem 0; + border: 1px solid #dee2e6; + border-radius: 0.25rem; +} + + +.table { + width: 100%; + margin-bottom: 0; + color: #212529; +} + + +.table thead th { + position: sticky; + top: 0; + background: #f8f9fa; + z-index: 1; + font-weight: 500; + border-bottom: 2px solid #dee2e6; +} + + +.table tbody tr:hover { + background-color: rgba(0,0,0,.075); + cursor: pointer; +} + + +.section-card { + background-color: #fff; + border: 1px solid #dee2e6; + border-radius: 5px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); +} + + +.nav-tabs { + margin-bottom: 20px; + border-bottom: 1px solid #dee2e6; +} + +.nav-tabs .nav-link { + color: #495057; + border: 1px solid transparent; + border-top-left-radius: .25rem; + border-top-right-radius: .25rem; +} + +.nav-tabs .nav-link.active { + color: #495057; + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} + + +.form-row { + margin-bottom: 15px; +} + +.form-control.readonly-field { + background-color: #e9ecef; + cursor: not-allowed; +} + + +.modified { + background-color: #fff3cd; +} + +@media (max-width: 768px) { + .container-fluid { + padding: 10px; + } + + .section-card { + padding: 15px; + } + + .table { + font-size: 0.875rem; + } + + .table th, + .table td { + padding: 0.5rem; + } +} + +.search-area { + margin-bottom: 1.5rem; +} + +.search-area .input-group { + margin-bottom: 0.5rem; +} + + +.tag-info-area { + margin-top: 1.5rem; +} + +.observation-area { + margin-top: 1.5rem; +} + +.section-card { + padding: 20px; + background-color: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.12); + margin-bottom: 20px; +} + +.card { + margin-bottom: 15px; +} + +.card-body { + padding: 15px; +} + +.btn-success { + margin-bottom: 15px; +} + +.tab-content { + padding-top: 20px; +} \ No newline at end of file diff --git a/wamtram2/static/js/expandFields.js b/wamtram2/static/js/expandFields.js index eb758ce1..07a7ccf3 100644 --- a/wamtram2/static/js/expandFields.js +++ b/wamtram2/static/js/expandFields.js @@ -2,14 +2,10 @@ document.addEventListener('DOMContentLoaded', function() { const entryId = document.body.dataset.entryId; if (entryId) { - console.log("Entry ID found:", entryId); expandAllFields(); - } else { - console.log("No Entry ID found"); } function expandAllFields() { - console.log("Expanding all fields"); const fieldsToExpand = [ 'injuryAdditionalFields', 'additionalRecaptureLeftTag', @@ -31,10 +27,7 @@ document.addEventListener('DOMContentLoaded', function() { const element = document.getElementById(id); if (element) { const displayStyle = element.tagName === 'TR' ? 'table-row' : 'block'; - console.log(`Expanding ${id} as ${displayStyle}`); element.style.display = displayStyle; - } else { - console.log(`Element not found: ${id}`); } }); @@ -42,7 +35,6 @@ document.addEventListener('DOMContentLoaded', function() { } function removeToggleButtons() { - console.log("Removing toggle buttons"); const buttonsToRemove = [ 'toggleInjuryFieldsButton', 'toggleRecaptureTagsBtn', @@ -52,15 +44,12 @@ document.addEventListener('DOMContentLoaded', function() { 'toggleNewPITTagsBtn', 'advancedDataButton' ]; - + buttonsToRemove.forEach(id => { const button = document.getElementById(id); if (button) { - console.log(`Removing ${id}`); button.style.display = 'none'; - } else { - console.log(`Button not found: ${id}`); - } + } }); } }); \ No newline at end of file diff --git a/wamtram2/static/js/observation_management.js b/wamtram2/static/js/observation_management.js new file mode 100644 index 00000000..fc72237a --- /dev/null +++ b/wamtram2/static/js/observation_management.js @@ -0,0 +1,519 @@ +$(document).ready(function() { + initializeBasicSelects(); + initializeSearchSelects(); + // If initialData is defined, set initial form values + if (typeof initialData !== 'undefined' && initialData) { + setInitialFormValues(); + } +}); + +// Initialize basic select elements +function initializeBasicSelects() { + const basicSelects = [ + 'alive', + 'nesting', + 'activity_code', + 'beach_position_code', + 'condition_code', + 'egg_count_method', + 'datum_code' + ]; + + basicSelects.forEach(selectName => { + $(`select[name="${selectName}"]`).select2({ + placeholder: 'Select...', + allowClear: true + }); + }); +} + +// Initialize search select elements +function initializeSearchSelects() { + // Initialize person searches + initializePersonSearch('measurer_person', 'Search measurer...'); + initializePersonSearch('measurer_reporter_person', 'Search measurer reporter...'); + initializePersonSearch('tagger_person', 'Search tagger...'); + initializePersonSearch('reporter_person', 'Search reporter...'); + + // Initialize place search + initializePlaceSearch(); +} + +// Initialize person search select with AJAX +function initializePersonSearch(fieldName, placeholder) { + $(`select[name="${fieldName}"]`).select2({ + ajax: { + url: searchPersonsUrl, + dataType: 'json', + delay: 250, + data: function(params) { + return { + q: params.term || '' + }; + }, + processResults: function(data) { + return { + results: data.map(function(item) { + return { + id: item.person_id, + text: `${item.first_name} ${item.surname}` + }; + }) + }; + }, + cache: true + }, + minimumInputLength: 2, + placeholder: placeholder, + allowClear: true + }); +} + +// Initialize place search select with AJAX +function initializePlaceSearch() { + $('select[name="place_code"]').select2({ + ajax: { + url: searchPlacesUrl, + dataType: 'json', + delay: 250, + data: function(params) { + return { + q: params.term || '' + }; + }, + processResults: function(data) { + return { + results: data.map(function(item) { + return { + id: item.place_code, + text: item.full_name + }; + }) + }; + }, + cache: true + }, + minimumInputLength: 2, + placeholder: 'Search place...', + allowClear: true + }); +} + +// Set all initial form values +function setInitialFormValues() { + setBasicFields(); + setTagInfo(); + setMeasurements(); + setDamageRecords(); +} + +// Set basic form fields +function setBasicFields() { + const basicInfo = initialData.basic_info; + if (!basicInfo) return; + + // Set date/time + if (basicInfo.observation_date) { + $('[name="observation_date"]').val(basicInfo.observation_date); + } + + // Set person fields + const personFields = ['measurer_person', 'measurer_reporter_person', 'tagger_person', 'reporter_person']; + personFields.forEach(field => { + if (basicInfo[field] && basicInfo[field].id) { + const $select = $(`select[name="${field}"]`); + const option = new Option(basicInfo[field].text, basicInfo[field].id, true, true); + $select.append(option).trigger('change'); + } + }); + + // Set place select + if (basicInfo.place_code) { + const $placeSelect = $('select[name="place_code"]'); + const option = new Option(basicInfo.place_code.text, basicInfo.place_code.id, true, true); + $placeSelect.append(option).trigger('change'); + } + + // Set other basic fields + const basicFields = [ + 'observation_id', 'turtle_id', 'alive', 'nesting', + 'activity_code', 'beach_position_code', 'condition_code', + 'egg_count_method', 'status', 'comments', + 'datum_code', 'latitude', 'longitude' + ]; + + basicFields.forEach(fieldName => { + if (basicInfo[fieldName] !== undefined) { + const $field = $(`[name="${fieldName}"]`); + if ($field.length) { + if ($field.is('select')) { + $field.val(basicInfo[fieldName]).trigger('change'); + } else { + $field.val(basicInfo[fieldName]); + } + } + } + }); +} + +// Set measurements data and render measurement cards +function setMeasurements() { + const measurementContainer = document.getElementById('measurementContainer'); + if (!measurementContainer || !initialData.measurements) return; + + measurementContainer.innerHTML = ''; + + if (initialData.measurements.length === 0) { + measurementContainer.innerHTML = '

No measurements found

'; + return; + } + + initialData.measurements.forEach(measurement => { + const measurementHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + measurementContainer.insertAdjacentHTML('beforeend', measurementHtml); + }); +} + +// Set damage records data and render damage cards +function setDamageRecords() { + const damageContainer = document.getElementById('damageContainer'); + if (!damageContainer || !initialData.damage_records) return; + + damageContainer.innerHTML = ''; + + if (initialData.damage_records.length === 0) { + damageContainer.innerHTML = '

No damage records found

'; + return; + } + + initialData.damage_records.forEach(damage => { + const damageHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + damageContainer.insertAdjacentHTML('beforeend', damageHtml); + }); +} + +// Set tag info +function setTagInfo() { + // Regular tags + const tagContainer = document.getElementById('tagContainer'); + if (tagContainer && initialData.tag_info?.recorded_tags) { + tagContainer.innerHTML = ''; + initialData.tag_info.recorded_tags.forEach(tag => { + const tagHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + tagContainer.insertAdjacentHTML('beforeend', tagHtml); + }); + } + + // PIT tags + const pitTagContainer = document.getElementById('pitTagContainer'); + if (pitTagContainer && initialData.tag_info?.recorded_pit_tags) { + pitTagContainer.innerHTML = ''; + initialData.tag_info.recorded_pit_tags.forEach(pitTag => { + const pitTagHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + pitTagContainer.insertAdjacentHTML('beforeend', pitTagHtml); + }); + } +} + +// Handle form submission +function handleFormSubmit() { + const formData = { + basic_info: getBasicInfo(), + tag_info: getTagInfo(), + measurements: getMeasurements(), + damage_records: getDamageRecords(), + location: getLocationInfo() + }; + + $.ajax({ + url: submitUrl, + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(formData), + headers: { + 'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val() + }, + success: function(response) { + if (response.status === 'success') { + showSuccessMessage('Observation saved successfully'); + } else { + showErrorMessage(response.message || 'Error saving observation'); + } + }, + error: function(xhr) { + showErrorMessage('Error saving observation'); + } + }); +} + +// Get basic information from form +function getBasicInfo() { + const observationDateTime = $('[name="observation_date"]').val(); + return { + observation_id: $('[name="observation_id"]').val(), + observation_date: observationDateTime, + alive: $('[name="alive"]').val(), + nesting: $('[name="nesting"]').val(), + activity_code: $('[name="activity_code"]').val(), + beach_position_code: $('[name="beach_position_code"]').val(), + condition_code: $('[name="condition_code"]').val(), + egg_count_method: $('[name="egg_count_method"]').val(), + status: $('[name="status"]').val(), + comments: $('[name="comments"]').val() + }; +} + +// Get measurements from form +function getMeasurements() { + const measurements = []; + $('.measurement-card').each(function() { + measurements.push({ + measurement_type: $(this).find('[name="measurement_type"]').val(), + measurement_value: $(this).find('[name="measurement_value"]').val() + }); + }); + return measurements; +} + +// Get damage records from form +function getDamageRecords() { + const damageRecords = []; + $('.damage-card').each(function() { + damageRecords.push({ + body_part: $(this).find('[name="body_part"]').val(), + damage_code: $(this).find('[name="damage_code"]').val(), + damage_cause_code: $(this).find('[name="damage_cause_code"]').val(), + comments: $(this).find('[name="damage_comments"]').val() + }); + }); + return damageRecords; +} + +// Get location information from form +function getLocationInfo() { + return { + place_code: $('[name="place_code"]').val(), + datum_code: $('[name="datum_code"]').val(), + latitude: $('[name="latitude"]').val(), + longitude: $('[name="longitude"]').val() + }; +} + +// Add getTagInfo function +function getTagInfo() { + const tagInfo = { + recorded_tags: [], + recorded_pit_tags: [] + }; + + // Get regular tags + $('.tag-card').each(function() { + tagInfo.recorded_tags.push({ + tag_id: $(this).find('[name="tag_id"]').val(), + tag_side: $(this).find('[name="tag_side"]').val(), + tag_position: $(this).find('[name="tag_position"]').val(), + tag_state: $(this).find('[name="tag_state"]').val() + }); + }); + + // Get PIT tags + $('.pit-tag-card').each(function() { + tagInfo.recorded_pit_tags.push({ + tag_id: $(this).find('[name="pittag_id"]').val(), + tag_position: $(this).find('[name="pit_tag_position"]').val(), + tag_state: $(this).find('[name="pit_tag_state"]').val() + }); + }); + + return tagInfo; +} + +// Show success message +function showSuccessMessage(message) { + const alertHtml = ` + + `; + $('#messageContainer').html(alertHtml); + setTimeout(() => { + $('.alert').alert('close'); + }, 3000); +} + +// Show error message +function showErrorMessage(message) { + const alertHtml = ` + + `; + $('#messageContainer').html(alertHtml); + setTimeout(() => { + $('.alert').alert('close'); + }, 3000); +} diff --git a/wamtram2/static/js/turtle_management.js b/wamtram2/static/js/turtle_management.js new file mode 100644 index 00000000..96a6500a --- /dev/null +++ b/wamtram2/static/js/turtle_management.js @@ -0,0 +1,577 @@ +document.addEventListener('DOMContentLoaded', function() { + + const searchButtons = document.querySelectorAll('[id$="SearchBtn"]'); + const searchResultForm = document.getElementById('searchResultForm'); + const noResultsDiv = document.getElementById('noResults'); + const loadingSpinner = document.querySelector('.loading-spinner'); + const loadingOverlay = document.querySelector('.loading-overlay'); + const saveButton = document.getElementById('saveTurtleBtn'); + + const searchTypeMapping = { + 'turtleid': 'turtle_id', + 'flippertag': 'tag_id', + 'pittag': 'pit_tag_id', + 'otheridentification': 'other_id' + }; + + const saveConfirmationModalElement = document.getElementById('saveConfirmationModal'); + const unsavedChangesModalElement = document.getElementById('unsavedChangesModal'); + + const saveConfirmationModal = saveConfirmationModalElement ? + new bootstrap.Modal(saveConfirmationModalElement) : null; + const unsavedChangesModal = unsavedChangesModalElement ? + new bootstrap.Modal(unsavedChangesModalElement) : null; + + let originalFormData = {}; + let hasUnsavedChanges = false; + let pendingSearchData = null; + + function clearForm() { + if (searchResultForm) { + const formElements = searchResultForm.querySelectorAll('input, select'); + formElements.forEach(element => { + element.value = ''; + }); + + const containers = [ + 'tagContainer', + 'pitTagContainer', + 'identificationContainer', + 'observationContainer', + 'sampleContainer', + 'documentContainer' + ]; + + containers.forEach(containerId => { + const container = document.getElementById(containerId); + if (container) { + container.innerHTML = ''; + } + }); + + searchResultForm.style.display = 'none'; + if (noResultsDiv) { + noResultsDiv.style.display = 'none'; + } + } + } + + function saveOriginalFormData() { + originalFormData = {}; + const formElements = searchResultForm.querySelectorAll('input, select'); + formElements.forEach(element => { + originalFormData[element.name] = element.value; + }); + } + + function getFormChanges() { + const changes = {}; + const currentFormData = {}; + const formElements = searchResultForm.querySelectorAll('input, select'); + + formElements.forEach(element => { + const value = element.value; + if (value !== originalFormData[element.name]) { + const label = element.previousElementSibling?.textContent || element.name; + changes[label] = { + old: originalFormData[element.name] || '(empty)', + new: value || '(empty)' + }; + } + }); + + return changes; + } + + function showSaveConfirmation(changes) { + if (!saveConfirmationModal) { + console.error('Save confirmation modal not found'); + return false; + } + + const changesContent = document.getElementById('changesContent'); + if (!changesContent) { + console.error('Changes content element not found'); + return false; + } + + if (Object.keys(changes).length === 0) { + changesContent.innerHTML = '

No changes detected.

'; + return false; + } + + let html = '
The following changes will be saved:
    '; + for (const [field, values] of Object.entries(changes)) { + html += `
  • ${field}:
    + From: ${values.old}
    + To: ${values.new}
  • `; + } + html += '
'; + changesContent.innerHTML = html; + saveConfirmationModal.show(); + return true; + } + + async function handleSearch(searchType, searchValue) { + if (hasUnsavedChanges && unsavedChangesModal) { + pendingSearchData = { searchType, searchValue }; + unsavedChangesModal.show(); + return; + } + + try { + loadingSpinner.style.display = 'block'; + loadingOverlay.style.display = 'block'; + + const formElements = searchResultForm.querySelectorAll('input, select'); + formElements.forEach(element => element.value = ''); + noResultsDiv.style.display = 'none'; + + const params = new URLSearchParams(); + params.append(searchType, searchValue); + + const response = await fetch(`/wamtram2/api/turtle-search/?${params.toString()}`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + const data = await response.json(); + + if (data.status === 'success' && data.data.length > 0) { + const turtle = data.data[0]; + + searchResultForm.style.display = 'block'; + + populateBasicInfo(turtle); + + populateTagInfo(turtle); + + populatePitTagInfo(turtle); + + populateIdentificationInfo(turtle); + + populateObservationInfo(turtle); + + populateSampleInfo(turtle); + + populateDocumentInfo(turtle); + + saveOriginalFormData(); + hasUnsavedChanges = false; + } else { + noResultsDiv.style.display = 'block'; + searchResultForm.style.display = 'none'; + } + } catch (error) { + console.error('Search error:', error); + showAlert('An error occurred while searching. Please try again.', 'danger'); + } finally { + loadingSpinner.style.display = 'none'; + loadingOverlay.style.display = 'none'; + } + } + + function populateBasicInfo(turtle) { + const basicInfoFields = { + 'turtle_id': turtle.turtle_id, + 'species': turtle.species, + 'turtle_name': turtle.turtle_name, + 'sex': turtle.sex, + 'cause_of_death': turtle.cause_of_death, + 'turtle_status': turtle.turtle_status, + 'date_entered': turtle.date_entered, + 'comments': turtle.comments, + 'location': turtle.location + }; + + Object.entries(basicInfoFields).forEach(([field, value]) => { + const element = searchResultForm.querySelector(`[name="${field}"]`); + if (element) { + element.value = value || ''; + } + }); + } + + function populateTagInfo(turtle) { + const tagContainer = document.getElementById('tagContainer'); + tagContainer.innerHTML = ''; + + if (!turtle.tags || turtle.tags.length === 0) { + tagContainer.innerHTML = '

No tags found

'; + return; + } + + turtle.tags.forEach(tag => { + const tagHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + tagContainer.insertAdjacentHTML('beforeend', tagHtml); + }); + } + + function populatePitTagInfo(turtle) { + const pitTagContainer = document.getElementById('pitTagContainer'); + pitTagContainer.innerHTML = ''; + + if (!turtle.pit_tags || turtle.pit_tags.length === 0) { + pitTagContainer.innerHTML = '

No PIT tags found

'; + return; + } + + turtle.pit_tags.forEach(tag => { + const pitTagHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + pitTagContainer.insertAdjacentHTML('beforeend', pitTagHtml); + }); + } + + function populateIdentificationInfo(turtle) { + const identificationContainer = document.getElementById('identificationContainer'); + identificationContainer.innerHTML = ''; + + if (!turtle.identifications || turtle.identifications.length === 0) { + identificationContainer.innerHTML = '

No other identifications found

'; + return; + } + + turtle.identifications.forEach(ident => { + const identHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + identificationContainer.insertAdjacentHTML('beforeend', identHtml); + }); + } + + function populateObservationInfo(turtle) { + const observationContainer = document.getElementById('observationContainer'); + observationContainer.innerHTML = ''; + + if (!turtle.observations || turtle.observations.length === 0) { + observationContainer.innerHTML = '

No observations found

'; + return; + } + + turtle.observations.forEach(obs => { + const obsHtml = ` +
+
+
+
+
+ + ${obs.observation_id} +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + observationContainer.insertAdjacentHTML('beforeend', obsHtml); + }); + } + + function populateSampleInfo(turtle) { + const sampleContainer = document.getElementById('sampleContainer'); + sampleContainer.innerHTML = ''; + + if (!turtle.samples || turtle.samples.length === 0) { + sampleContainer.innerHTML = '

No samples found

'; + return; + } + + turtle.samples.forEach(sample => { + const sampleHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + sampleContainer.insertAdjacentHTML('beforeend', sampleHtml); + }); + } + + function populateDocumentInfo(turtle) { + const documentContainer = document.getElementById('documentContainer'); + documentContainer.innerHTML = ''; + + if (!turtle.documents || turtle.documents.length === 0) { + documentContainer.innerHTML = '

No documents found

'; + return; + } + + turtle.documents.forEach(doc => { + const docHtml = ` +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ `; + documentContainer.insertAdjacentHTML('beforeend', docHtml); + }); + } + + async function handleSave() { + try { + loadingSpinner.style.display = 'block'; + loadingOverlay.style.display = 'block'; + + const formData = {}; + const formElements = searchResultForm.querySelectorAll('input, select'); + formElements.forEach(element => { + formData[element.name] = element.value; + }); + + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + if (!csrfToken) { + throw new Error('CSRF token not found'); + } + + + const response = await fetch('/wamtram2/api/turtle-update/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify(formData) + }); + + const data = await response.json(); + + if (data.status === 'success') { + showAlert('Changes saved successfully!', 'success'); + } else { + showAlert(data.message || 'Error saving changes', 'danger'); + } + } catch (error) { + console.error('Save error:', error); + showAlert('An error occurred while saving. Please try again.', 'danger'); + } finally { + loadingSpinner.style.display = 'none'; + loadingOverlay.style.display = 'none'; + } + } + + function showAlert(message, type) { + const alertDiv = document.createElement('div'); + alertDiv.className = `alert alert-${type} mt-3`; + alertDiv.textContent = message; + searchResultForm.parentNode.insertBefore(alertDiv, searchResultForm); + setTimeout(() => alertDiv.remove(), 3000); + } + + searchButtons.forEach(button => { + button.addEventListener('click', function() { + const searchInput = this.parentElement.previousElementSibling; + let searchType = searchInput.id.replace('Search', '').toLowerCase(); + searchType = searchTypeMapping[searchType] || searchType; + const searchValue = searchInput.value.trim(); + + if (searchValue) { + handleSearch(searchType, searchValue); + } + }); + }); + + if (saveButton) { + saveButton.addEventListener('click', function() { + const changes = getFormChanges(); + if (showSaveConfirmation(changes)) { + document.getElementById('confirmSaveBtn')?.addEventListener('click', function() { + saveConfirmationModal?.hide(); + handleSave(); + }); + } + }); + } + + if (searchResultForm) { + searchResultForm.addEventListener('change', function() { + hasUnsavedChanges = true; + }); + } + + document.getElementById('discardChangesBtn')?.addEventListener('click', function() { + unsavedChangesModal?.hide(); + hasUnsavedChanges = false; + if (pendingSearchData) { + const { searchType, searchValue } = pendingSearchData; + pendingSearchData = null; + handleSearch(searchType, searchValue); + } + }); + + clearForm(); +}); diff --git a/wamtram2/templates/wamtram2/add_person.html b/wamtram2/templates/wamtram2/add_person.html index a5932acc..8bdd6c20 100644 --- a/wamtram2/templates/wamtram2/add_person.html +++ b/wamtram2/templates/wamtram2/add_person.html @@ -56,8 +56,7 @@ - - {% include "includes/messages.html" %} + diff --git a/wamtram2/templates/wamtram2/admin_tools.html b/wamtram2/templates/wamtram2/admin_tools.html index 8f6047a8..81e8ca8b 100644 --- a/wamtram2/templates/wamtram2/admin_tools.html +++ b/wamtram2/templates/wamtram2/admin_tools.html @@ -1,52 +1,54 @@ {% extends "base_wastd.html" %} -{% load static %} +{% load static bootstrap4 %} {% block extra_style %} - +{{ block.super }} + {{ form.media.css }} + {% endblock %} {% block page_content_inner %} @@ -54,6 +56,22 @@

Administration Tools

+ + + - +
- +
- +
-

Batch Management

-

Manage and review data entry batches.

+

Batch Curation

+

Review and manage data entry batches for curation.

diff --git a/wamtram2/templates/wamtram2/batch_curation_list.html b/wamtram2/templates/wamtram2/batch_curation_list.html index c8953ce8..9f727b8c 100644 --- a/wamtram2/templates/wamtram2/batch_curation_list.html +++ b/wamtram2/templates/wamtram2/batch_curation_list.html @@ -156,7 +156,7 @@ function viewBatch() { if (selectedBatchId) { - window.location.href = "{% url 'wamtram2:batch_entries' 0 %}".replace('0', selectedBatchId); + window.location.href = "{% url 'wamtram2:entries_curation' 0 %}".replace('0', selectedBatchId); } } @@ -189,7 +189,7 @@ $('.grid-row').click(function() { const batchId = $(this).data('batch-id'); - window.location.href = "{% url 'wamtram2:batch_entries' 0 %}".replace('0', batchId); + window.location.href = "{% url 'wamtram2:entries_curation' 0 %}".replace('0', batchId); }); $('#saveColumnSettings').click(function() { diff --git a/wamtram2/templates/wamtram2/entry_curation_list.html b/wamtram2/templates/wamtram2/entry_curation_list.html index fb5389d3..a49004ec 100644 --- a/wamtram2/templates/wamtram2/entry_curation_list.html +++ b/wamtram2/templates/wamtram2/entry_curation_list.html @@ -102,11 +102,11 @@

Entries for Batch {{ batch_id }}

@@ -190,13 +190,15 @@

Entries for Batch {{ batch_id }}

{{ entry.get_sex_display|default:'-' }}
-
{% elif column.field == 'place_code' %} diff --git a/wamtram2/templates/wamtram2/export_form.html b/wamtram2/templates/wamtram2/export_form.html index 89cf8353..36698d9f 100644 --- a/wamtram2/templates/wamtram2/export_form.html +++ b/wamtram2/templates/wamtram2/export_form.html @@ -37,7 +37,6 @@

Export Data

- {% include "includes/messages.html" %} diff --git a/wamtram2/templates/wamtram2/observation_management.html b/wamtram2/templates/wamtram2/observation_management.html new file mode 100644 index 00000000..277a9a8f --- /dev/null +++ b/wamtram2/templates/wamtram2/observation_management.html @@ -0,0 +1,354 @@ +{% extends "base_wastd.html" %} +{% load static bootstrap4 %} + +{% block extra_style %} + {{ block.super }} + {{ form.media.css }} + + + + + + + +{% endblock %} + +{% block breadcrumbs %} + + +{% endblock %} + +{% block page_content_inner %} +{% csrf_token %} +
+
+
+ +
+
+ +
+

Basic Information

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ + + + + +
+
+
+

Flipper Tags

+
+
+
+
+
+

PIT Tags

+
+
+
+
+
+

Other Identification

+
+
+
+
+
+

Measurements

+
+
+
+
+
+

Damage

+
+
+
+
+
+

Scars

+
+
+
+
+
+

Other Information

+
+
+
+
+ + +
+
+ + +
+
+ Loading... +
+
+ + +
+{% endblock %} + +{% block extra_js %} +{{ block.super }} + + + +{% endblock %} \ No newline at end of file diff --git a/wamtram2/templates/wamtram2/tag_register.html b/wamtram2/templates/wamtram2/tag_register.html index 20ff3c38..3449fa90 100644 --- a/wamtram2/templates/wamtram2/tag_register.html +++ b/wamtram2/templates/wamtram2/tag_register.html @@ -25,7 +25,6 @@

Register Tags

- {% include "includes/messages.html" %} diff --git a/wamtram2/templates/wamtram2/template_manage.html b/wamtram2/templates/wamtram2/template_manage.html index 7fadfd3e..f9e53e35 100644 --- a/wamtram2/templates/wamtram2/template_manage.html +++ b/wamtram2/templates/wamtram2/template_manage.html @@ -35,9 +35,8 @@

Template Management

- {% include "includes/messages.html" %} - +
Create New Template
diff --git a/wamtram2/templates/wamtram2/transfer_observation.html b/wamtram2/templates/wamtram2/transfer_observation.html index 7ed9ca39..f156de47 100644 --- a/wamtram2/templates/wamtram2/transfer_observation.html +++ b/wamtram2/templates/wamtram2/transfer_observation.html @@ -23,8 +23,7 @@ - - {% include "includes/messages.html" %} + diff --git a/wamtram2/templates/wamtram2/trtdataentry_form.html b/wamtram2/templates/wamtram2/trtdataentry_form.html index 0f5da454..56b98856 100644 --- a/wamtram2/templates/wamtram2/trtdataentry_form.html +++ b/wamtram2/templates/wamtram2/trtdataentry_form.html @@ -980,21 +980,38 @@
NEW PIT Tag(s)
-