476 lines
16 KiB
JavaScript
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
|
|
};
|
|
}
|