mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Imlement visits deletion API
This commit is contained in:
parent
6e773b6b51
commit
550d20c555
8 changed files with 151 additions and 27 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
|
||||
- `POST /api/v1/visits` endpoint.
|
||||
- User now can create visits manually on the map.
|
||||
- User can now delete a visit by clicking on the delete button in the visit popup.
|
||||
- Import failure now throws an internal server error.
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -75,6 +75,21 @@ class Api::V1::VisitsController < ApiController
|
|||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
visit = current_api_user.visits.find(params[:id])
|
||||
|
||||
if visit.destroy
|
||||
head :no_content
|
||||
else
|
||||
render json: {
|
||||
error: 'Failed to delete visit',
|
||||
errors: visit.errors.full_messages
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { error: 'Visit not found' }, status: :not_found
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visit_params
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
|
||||
|
||||
import "@rails/ujs"
|
||||
import "@rails/actioncable"
|
||||
import "controllers"
|
||||
import "@hotwired/turbo-rails"
|
||||
|
|
|
|||
|
|
@ -72,22 +72,21 @@ export default class extends Controller {
|
|||
// Create the Add Visit control
|
||||
const AddVisitControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'add-visit-button');
|
||||
button.innerHTML = '📍 Add a Visit';
|
||||
button.title = 'Click to add a visit to the map';
|
||||
const button = L.DomUtil.create('button', 'leaflet-control-button add-visit-button');
|
||||
button.innerHTML = '➕';
|
||||
button.title = 'Add a visit';
|
||||
|
||||
// Style the button
|
||||
button.style.backgroundColor = 'white';
|
||||
button.style.padding = '8px 12px';
|
||||
button.style.border = '2px solid #ccc';
|
||||
button.style.borderRadius = '4px';
|
||||
// Style the button to match other map controls
|
||||
button.style.width = '48px';
|
||||
button.style.height = '48px';
|
||||
button.style.border = 'none';
|
||||
button.style.cursor = 'pointer';
|
||||
button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
||||
button.style.fontSize = '14px';
|
||||
button.style.fontWeight = 'bold';
|
||||
button.style.marginBottom = '5px';
|
||||
button.style.display = 'block';
|
||||
button.style.width = '100%';
|
||||
button.style.backgroundColor = 'white';
|
||||
button.style.borderRadius = '4px';
|
||||
button.style.padding = '0';
|
||||
button.style.lineHeight = '48px';
|
||||
button.style.fontSize = '18px';
|
||||
button.style.textAlign = 'center';
|
||||
button.style.transition = 'all 0.2s ease';
|
||||
|
||||
|
|
@ -135,10 +134,9 @@ export default class extends Controller {
|
|||
this.isAddingVisit = true;
|
||||
|
||||
// Update button style to show active state
|
||||
button.style.backgroundColor = '#007bff';
|
||||
button.style.backgroundColor = '#dc3545';
|
||||
button.style.color = 'white';
|
||||
button.style.borderColor = '#0056b3';
|
||||
button.innerHTML = '✕ Cancel';
|
||||
button.innerHTML = '✕';
|
||||
|
||||
// Change cursor to crosshair
|
||||
this.map.getContainer().style.cursor = 'crosshair';
|
||||
|
|
@ -155,8 +153,7 @@ export default class extends Controller {
|
|||
// Reset button style
|
||||
button.style.backgroundColor = 'white';
|
||||
button.style.color = 'black';
|
||||
button.style.borderColor = '#ccc';
|
||||
button.innerHTML = '📍 Add Visit';
|
||||
button.innerHTML = '➕';
|
||||
|
||||
// Reset cursor
|
||||
this.map.getContainer().style.cursor = '';
|
||||
|
|
@ -370,7 +367,7 @@ export default class extends Controller {
|
|||
|
||||
if (stimulusController && stimulusController.visitsManager) {
|
||||
console.log('Found maps controller with visits manager');
|
||||
|
||||
|
||||
// Clear existing visits and fetch fresh data
|
||||
if (stimulusController.visitsManager.visitCircles) {
|
||||
stimulusController.visitsManager.visitCircles.clearLayers();
|
||||
|
|
@ -386,7 +383,7 @@ export default class extends Controller {
|
|||
}
|
||||
} else {
|
||||
console.log('Could not find maps controller or visits manager');
|
||||
|
||||
|
||||
// Fallback: Try to dispatch a custom event
|
||||
const refreshEvent = new CustomEvent('visits:refresh', { bubbles: true });
|
||||
mapsController.dispatchEvent(refreshEvent);
|
||||
|
|
@ -414,7 +411,7 @@ export default class extends Controller {
|
|||
if (confirmedVisitsLayer && !map.hasLayer(confirmedVisitsLayer)) {
|
||||
console.log('Adding confirmed visits layer to map');
|
||||
map.addLayer(confirmedVisitsLayer);
|
||||
|
||||
|
||||
// Update the layer control checkbox to reflect the layer is now active
|
||||
this.updateLayerControlCheckbox('Confirmed Visits', true);
|
||||
}
|
||||
|
|
@ -442,7 +439,7 @@ export default class extends Controller {
|
|||
if (label && label.textContent.trim() === layerName) {
|
||||
console.log(`Updating ${layerName} checkbox to ${isEnabled}`);
|
||||
input.checked = isEnabled;
|
||||
|
||||
|
||||
// Trigger change event to ensure proper state management
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1357,7 +1357,7 @@ export class VisitsManager {
|
|||
<label class="label">
|
||||
<span class="label-text text-sm font-medium">Location</span>
|
||||
</label>
|
||||
<select class="select select-bordered select-sm w-full bg-base-200 text-base-content" name="place">
|
||||
<select class="select select-bordered select-sm text-xs w-full bg-base-200 text-base-content" name="place">
|
||||
${possiblePlaces.length > 0 ? possiblePlaces.map(place => `
|
||||
<option value="${place.id}" ${place.id === visit.place.id ? 'selected' : ''}>
|
||||
${place.name}
|
||||
|
|
@ -1391,6 +1391,14 @@ export class VisitsManager {
|
|||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-sm btn-outline btn-error w-full delete-visit" data-id="${visit.id}">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
Delete Visit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1507,9 +1515,11 @@ export class VisitsManager {
|
|||
// Add event listeners for confirm and decline buttons
|
||||
const confirmBtn = form.querySelector('.confirm-visit');
|
||||
const declineBtn = form.querySelector('.decline-visit');
|
||||
const deleteBtn = form.querySelector('.delete-visit');
|
||||
|
||||
confirmBtn?.addEventListener('click', (event) => this.handleStatusChange(event, visit.id, 'confirmed'));
|
||||
declineBtn?.addEventListener('click', (event) => this.handleStatusChange(event, visit.id, 'declined'));
|
||||
deleteBtn?.addEventListener('click', (event) => this.handleDeleteVisit(event, visit.id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1551,6 +1561,51 @@ export class VisitsManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deletion of a visit with confirmation
|
||||
* @param {Event} event - The click event
|
||||
* @param {string} visitId - The visit ID to delete
|
||||
*/
|
||||
async handleDeleteVisit(event, visitId) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Show confirmation dialog
|
||||
const confirmDelete = confirm('Are you sure you want to delete this visit? This action cannot be undone.');
|
||||
|
||||
if (!confirmDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/visits/${visitId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Close the popup
|
||||
if (this.currentPopup) {
|
||||
this.map.closePopup(this.currentPopup);
|
||||
this.currentPopup = null;
|
||||
}
|
||||
|
||||
// Refresh the visits list
|
||||
this.fetchAndDisplayVisits();
|
||||
showFlashMessage('notice', 'Visit deleted successfully');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = errorData.error || 'Failed to delete visit';
|
||||
showFlashMessage('error', errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting visit:', error);
|
||||
showFlashMessage('error', 'Failed to delete visit');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates text to a specified length and adds ellipsis if needed
|
||||
* @param {string} text - The text to truncate
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :areas, only: %i[index create update destroy]
|
||||
resources :points, only: %i[index create update destroy]
|
||||
resources :visits, only: %i[index create update] do
|
||||
resources :visits, only: %i[index create update destroy] do
|
||||
get 'possible_places', to: 'visits/possible_places#index', on: :member
|
||||
collection do
|
||||
post 'merge', to: 'visits#merge'
|
||||
|
|
|
|||
|
|
@ -322,4 +322,61 @@ RSpec.describe 'Api::V1::Visits', type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/visits/:id' do
|
||||
let!(:visit) { create(:visit, user: user, place: place) }
|
||||
let!(:other_user_visit) { create(:visit, user: other_user, place: place) }
|
||||
|
||||
context 'when visit exists and belongs to current user' do
|
||||
it 'deletes the visit' do
|
||||
expect {
|
||||
delete "/api/v1/visits/#{visit.id}", headers: auth_headers
|
||||
}.to change { user.visits.count }.by(-1)
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
|
||||
it 'removes the visit from the database' do
|
||||
delete "/api/v1/visits/#{visit.id}", headers: auth_headers
|
||||
|
||||
expect { visit.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when visit does not exist' do
|
||||
it 'returns not found status' do
|
||||
delete '/api/v1/visits/999999', headers: auth_headers
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['error']).to eq('Visit not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when visit belongs to another user' do
|
||||
it 'returns not found status' do
|
||||
delete "/api/v1/visits/#{other_user_visit.id}", headers: auth_headers
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['error']).to eq('Visit not found')
|
||||
end
|
||||
|
||||
it 'does not delete the visit' do
|
||||
expect {
|
||||
delete "/api/v1/visits/#{other_user_visit.id}", headers: auth_headers
|
||||
}.not_to change { Visit.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid API key' do
|
||||
let(:invalid_auth_headers) { { 'Authorization' => 'Bearer invalid-key' } }
|
||||
|
||||
it 'returns unauthorized status' do
|
||||
delete "/api/v1/visits/#{visit.id}", headers: invalid_auth_headers
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue