Files
dictia-public/static/js/modules/composables/bulk-operations.js

476 lines
16 KiB
JavaScript

/**
* Bulk Operations Composable
* Handles bulk API operations for multiple recordings
*/
const { ref, computed } = Vue;
export function useBulkOperations({
selectedRecordingIds,
selectedRecordings,
recordings,
selectedRecording,
bulkActionInProgress,
availableTags,
availableFolders,
showToast,
setGlobalError,
startReprocessingPoll
}) {
// Modal state
const showBulkDeleteModal = ref(false);
const showBulkTagModal = ref(false);
const showBulkReprocessModal = ref(false);
const showBulkFolderModal = ref(false);
const bulkTagAction = ref('add'); // 'add' or 'remove'
const bulkTagSelectedId = ref('');
const bulkReprocessType = ref('summary'); // 'transcription' or 'summary'
// Get CSRF token
const getCsrfToken = () => {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
};
// Helper to get selected IDs as array
const getSelectedIds = () => {
return Array.from(selectedRecordingIds.value);
};
// =========================================
// Bulk Delete
// =========================================
const openBulkDeleteModal = () => {
showBulkDeleteModal.value = true;
};
const closeBulkDeleteModal = () => {
showBulkDeleteModal.value = false;
};
const executeBulkDelete = async () => {
const ids = getSelectedIds();
if (ids.length === 0) return;
bulkActionInProgress.value = true;
closeBulkDeleteModal();
try {
const response = await fetch('/api/recordings/bulk', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({ recording_ids: ids })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to delete recordings');
}
// Remove deleted recordings from local state
const deletedIds = new Set(data.deleted_ids || ids);
recordings.value = recordings.value.filter(r => !deletedIds.has(r.id));
// Clear selected recording if it was deleted
if (selectedRecording.value && deletedIds.has(selectedRecording.value.id)) {
selectedRecording.value = null;
}
// Remove deleted IDs from selection
deletedIds.forEach(id => selectedRecordingIds.value.delete(id));
const count = deletedIds.size;
showToast(`${count} recording${count !== 1 ? 's' : ''} deleted`, 'fa-trash', 3000, 'success');
} catch (error) {
console.error('Bulk delete error:', error);
setGlobalError(`Failed to delete recordings: ${error.message}`);
} finally {
bulkActionInProgress.value = false;
}
};
// =========================================
// Bulk Tag Operations
// =========================================
const openBulkTagModal = (action = 'add') => {
bulkTagAction.value = action;
bulkTagSelectedId.value = '';
showBulkTagModal.value = true;
};
const closeBulkTagModal = () => {
showBulkTagModal.value = false;
bulkTagSelectedId.value = '';
};
const executeBulkTag = async () => {
const ids = getSelectedIds();
const tagId = bulkTagSelectedId.value;
const action = bulkTagAction.value;
// Validate before making API call
if (ids.length === 0) {
console.warn('No recordings selected for bulk tag operation');
return;
}
if (!tagId && tagId !== 0) {
console.warn('No tag selected for bulk tag operation');
return;
}
bulkActionInProgress.value = true;
closeBulkTagModal();
try {
const response = await fetch('/api/recordings/bulk-tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
recording_ids: ids,
tag_id: parseInt(tagId),
action: action
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `Failed to ${action} tag`);
}
// Update local state
const tag = availableTags.value.find(t => t.id == tagId);
if (tag) {
const affectedIds = new Set(data.affected_ids || ids);
recordings.value.forEach(recording => {
if (affectedIds.has(recording.id)) {
if (!recording.tags) recording.tags = [];
if (action === 'add') {
// Add tag if not already present
if (!recording.tags.find(t => t.id === tag.id)) {
recording.tags.push(tag);
}
} else {
// Remove tag
recording.tags = recording.tags.filter(t => t.id !== tag.id);
}
}
});
// Update selected recording if affected
if (selectedRecording.value && affectedIds.has(selectedRecording.value.id)) {
if (!selectedRecording.value.tags) selectedRecording.value.tags = [];
if (action === 'add') {
if (!selectedRecording.value.tags.find(t => t.id === tag.id)) {
selectedRecording.value.tags.push(tag);
}
} else {
selectedRecording.value.tags = selectedRecording.value.tags.filter(t => t.id !== tag.id);
}
}
}
const count = data.affected_ids?.length || ids.length;
const actionText = action === 'add' ? 'added to' : 'removed from';
showToast(`Tag ${actionText} ${count} recording${count !== 1 ? 's' : ''}`, 'fa-tags', 3000, 'success');
} catch (error) {
console.error('Bulk tag error:', error);
setGlobalError(`Failed to ${action} tag: ${error.message}`);
} finally {
bulkActionInProgress.value = false;
}
};
// =========================================
// Bulk Reprocess
// =========================================
const openBulkReprocessModal = () => {
bulkReprocessType.value = 'summary';
showBulkReprocessModal.value = true;
};
const closeBulkReprocessModal = () => {
showBulkReprocessModal.value = false;
};
const executeBulkReprocess = async () => {
const ids = getSelectedIds();
if (ids.length === 0) return;
bulkActionInProgress.value = true;
closeBulkReprocessModal();
try {
const response = await fetch('/api/recordings/bulk-reprocess', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
recording_ids: ids,
type: bulkReprocessType.value
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to queue reprocessing');
}
// Update status for queued recordings
const queuedIds = new Set(data.queued_ids || ids);
const newStatus = bulkReprocessType.value === 'transcription' ? 'PROCESSING' : 'SUMMARIZING';
recordings.value.forEach(recording => {
if (queuedIds.has(recording.id)) {
recording.status = newStatus;
// Start polling for each
if (startReprocessingPoll) {
startReprocessingPoll(recording.id);
}
}
});
if (selectedRecording.value && queuedIds.has(selectedRecording.value.id)) {
selectedRecording.value.status = newStatus;
}
const count = queuedIds.size;
const typeText = bulkReprocessType.value === 'transcription' ? 'Transcription' : 'Summary';
showToast(`${typeText} reprocessing queued for ${count} recording${count !== 1 ? 's' : ''}`, 'fa-sync-alt', 3000, 'success');
} catch (error) {
console.error('Bulk reprocess error:', error);
setGlobalError(`Failed to queue reprocessing: ${error.message}`);
} finally {
bulkActionInProgress.value = false;
}
};
// =========================================
// Bulk Toggle (Inbox/Highlight)
// =========================================
const bulkToggleInbox = async (value = null) => {
const ids = getSelectedIds();
if (ids.length === 0) return;
// If no value specified, toggle based on majority
if (value === null) {
const inboxCount = selectedRecordings.value.filter(r => r.is_inbox).length;
value = inboxCount < ids.length / 2;
}
bulkActionInProgress.value = true;
try {
const response = await fetch('/api/recordings/bulk-toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
recording_ids: ids,
field: 'inbox',
value: value
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to update inbox status');
}
// Update local state
const affectedIds = new Set(data.affected_ids || ids);
recordings.value.forEach(recording => {
if (affectedIds.has(recording.id)) {
recording.is_inbox = value;
}
});
if (selectedRecording.value && affectedIds.has(selectedRecording.value.id)) {
selectedRecording.value.is_inbox = value;
}
const count = affectedIds.size;
const actionText = value ? 'added to' : 'removed from';
showToast(`${count} recording${count !== 1 ? 's' : ''} ${actionText} inbox`, 'fa-inbox', 3000, 'success');
} catch (error) {
console.error('Bulk toggle inbox error:', error);
setGlobalError(`Failed to update inbox status: ${error.message}`);
} finally {
bulkActionInProgress.value = false;
}
};
const bulkToggleHighlight = async (value = null) => {
const ids = getSelectedIds();
if (ids.length === 0) return;
// If no value specified, toggle based on majority
if (value === null) {
const highlightCount = selectedRecordings.value.filter(r => r.is_highlighted).length;
value = highlightCount < ids.length / 2;
}
bulkActionInProgress.value = true;
try {
const response = await fetch('/api/recordings/bulk-toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
recording_ids: ids,
field: 'highlight',
value: value
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to update highlight status');
}
// Update local state
const affectedIds = new Set(data.affected_ids || ids);
recordings.value.forEach(recording => {
if (affectedIds.has(recording.id)) {
recording.is_highlighted = value;
}
});
if (selectedRecording.value && affectedIds.has(selectedRecording.value.id)) {
selectedRecording.value.is_highlighted = value;
}
const count = affectedIds.size;
const actionText = value ? 'highlighted' : 'unhighlighted';
showToast(`${count} recording${count !== 1 ? 's' : ''} ${actionText}`, 'fa-star', 3000, 'success');
} catch (error) {
console.error('Bulk toggle highlight error:', error);
setGlobalError(`Failed to update highlight status: ${error.message}`);
} finally {
bulkActionInProgress.value = false;
}
};
// =========================================
// Bulk Folder Assignment
// =========================================
const bulkAssignFolder = async (folderId) => {
const ids = getSelectedIds();
if (ids.length === 0) return;
bulkActionInProgress.value = true;
showBulkFolderModal.value = false;
try {
const response = await fetch('/api/recordings/bulk/folder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
recording_ids: ids,
folder_id: folderId
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to update folders');
}
// Update local state
const folder = folderId ? availableFolders.value.find(f => f.id === folderId) : null;
recordings.value.forEach(recording => {
if (ids.includes(recording.id)) {
recording.folder_id = folderId;
recording.folder = folder;
}
});
// Update selected recording if affected
if (selectedRecording.value && ids.includes(selectedRecording.value.id)) {
selectedRecording.value.folder_id = folderId;
selectedRecording.value.folder = folder;
}
// Update folder recording counts
if (availableFolders.value) {
availableFolders.value.forEach(f => {
const count = recordings.value.filter(r => r.folder_id === f.id).length;
f.recording_count = count;
});
}
const count = data.updated_count || ids.length;
if (folderId) {
showToast(`${count} recording${count !== 1 ? 's' : ''} moved to "${folder?.name || 'folder'}"`, 'fa-folder', 3000, 'success');
} else {
showToast(`${count} recording${count !== 1 ? 's' : ''} removed from folder`, 'fa-folder-minus', 3000, 'success');
}
} catch (error) {
console.error('Bulk folder assignment error:', error);
setGlobalError(`Failed to update folders: ${error.message}`);
} finally {
bulkActionInProgress.value = false;
}
};
return {
// Modal state
showBulkDeleteModal,
showBulkTagModal,
showBulkReprocessModal,
showBulkFolderModal,
bulkTagAction,
bulkTagSelectedId,
bulkReprocessType,
// Bulk Delete
openBulkDeleteModal,
closeBulkDeleteModal,
executeBulkDelete,
// Bulk Tag
openBulkTagModal,
closeBulkTagModal,
executeBulkTag,
// Bulk Reprocess
openBulkReprocessModal,
closeBulkReprocessModal,
executeBulkReprocess,
// Bulk Toggle
bulkToggleInbox,
bulkToggleHighlight,
// Bulk Folder
bulkAssignFolder
};
}