import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { audioRecordingRepository } from '@/services/repositories' /** * Audio Recording Store * Manages global audio recording state and operations */ export const useAudioRecordingStore = defineStore('audioRecording', () => { // State const recordings = ref(new Map()) // workOrderId -> recordings array const activeRecordingSessions = ref(new Map()) // userId -> session data const transcriptionJobs = ref(new Map()) // recordingId -> transcription status const audioFormats = ref([]) const transcriptionConfig = ref({}) const loading = ref(false) const error = ref(null) // Recording state const globalRecordingActive = ref(false) const activeRecordingCount = ref(0) // Audio analysis cache const waveformData = ref(new Map()) // recordingId -> waveform data const analysisData = ref(new Map()) // recordingId -> analysis data // Getters const getRecordingsForWorkOrder = computed(() => { return (workOrderId) => recordings.value.get(workOrderId) || [] }) const getActiveRecordingSession = computed(() => { return (userId) => activeRecordingSessions.value.get(userId) }) const hasActiveRecording = computed(() => { return (userId) => activeRecordingSessions.value.has(userId) }) const getTranscriptionStatus = computed(() => { return (recordingId) => transcriptionJobs.value.get(recordingId) }) const getTotalDurationForWorkOrder = computed(() => { return (workOrderId) => { const workOrderRecordings = recordings.value.get(workOrderId) || [] return workOrderRecordings.reduce((total, recording) => { return total + (recording.duration || 0) }, 0) } }) const getTotalSizeForWorkOrder = computed(() => { return (workOrderId) => { const workOrderRecordings = recordings.value.get(workOrderId) || [] return workOrderRecordings.reduce((total, recording) => { return total + (recording.size || 0) }, 0) } }) const getTranscribedCount = computed(() => { return (workOrderId) => { const workOrderRecordings = recordings.value.get(workOrderId) || [] return workOrderRecordings.filter(recording => recording.transcription).length } }) const getWaveformData = computed(() => { return (recordingId) => waveformData.value.get(recordingId) }) const getAnalysisData = computed(() => { return (recordingId) => analysisData.value.get(recordingId) }) // Actions const loadRecordings = async (workOrderId) => { try { loading.value = true error.value = null const recordingsList = await audioRecordingRepository.getByWorkOrderId(workOrderId) recordings.value.set(workOrderId, recordingsList) return recordingsList } catch (err) { error.value = err.message || 'Failed to load recordings' throw err } finally { loading.value = false } } const uploadRecording = async (workOrderId, recordingData) => { try { loading.value = true error.value = null const uploadedRecording = await audioRecordingRepository.upload({ ...recordingData, workOrderId }) // Update local state const currentRecordings = recordings.value.get(workOrderId) || [] recordings.value.set(workOrderId, [uploadedRecording, ...currentRecordings]) return uploadedRecording } catch (err) { error.value = err.message || 'Failed to upload recording' throw err } finally { loading.value = false } } const updateRecording = async (workOrderId, recordingId, updateData) => { try { loading.value = true error.value = null const updatedRecording = await audioRecordingRepository.update(recordingId, updateData) // Update local state const currentRecordings = recordings.value.get(workOrderId) || [] const index = currentRecordings.findIndex(recording => recording.id === recordingId) if (index !== -1) { currentRecordings[index] = updatedRecording recordings.value.set(workOrderId, [...currentRecordings]) } return updatedRecording } catch (err) { error.value = err.message || 'Failed to update recording' throw err } finally { loading.value = false } } const deleteRecording = async (workOrderId, recordingId) => { try { loading.value = true error.value = null await audioRecordingRepository.delete(recordingId) // Update local state const currentRecordings = recordings.value.get(workOrderId) || [] const filteredRecordings = currentRecordings.filter(recording => recording.id !== recordingId) recordings.value.set(workOrderId, filteredRecordings) // Clear related data waveformData.value.delete(recordingId) analysisData.value.delete(recordingId) transcriptionJobs.value.delete(recordingId) return true } catch (err) { error.value = err.message || 'Failed to delete recording' throw err } finally { loading.value = false } } const startRecordingSession = async (userId, workOrderId, sessionData = {}) => { try { loading.value = true error.value = null // Check if user already has an active session if (activeRecordingSessions.value.has(userId)) { throw new Error('User already has an active recording session') } const session = await audioRecordingRepository.startRecordingSession({ userId, workOrderId, ...sessionData }) // Update local state activeRecordingSessions.value.set(userId, { ...session, workOrderId, startTime: new Date(), localDuration: 0 }) activeRecordingCount.value = activeRecordingSessions.value.size globalRecordingActive.value = activeRecordingCount.value > 0 return session } catch (err) { error.value = err.message || 'Failed to start recording session' throw err } finally { loading.value = false } } const stopRecordingSession = async (userId, recordingData) => { try { loading.value = true error.value = null const activeSession = activeRecordingSessions.value.get(userId) if (!activeSession) { throw new Error('No active recording session found for user') } const recording = await audioRecordingRepository.stopRecordingSession(activeSession.id, recordingData) // Update recordings for the work order const workOrderId = activeSession.workOrderId const currentRecordings = recordings.value.get(workOrderId) || [] recordings.value.set(workOrderId, [recording, ...currentRecordings]) // Remove from active sessions activeRecordingSessions.value.delete(userId) activeRecordingCount.value = activeRecordingSessions.value.size globalRecordingActive.value = activeRecordingCount.value > 0 return recording } catch (err) { error.value = err.message || 'Failed to stop recording session' throw err } finally { loading.value = false } } const updateSessionDuration = (userId, duration) => { const session = activeRecordingSessions.value.get(userId) if (session) { session.localDuration = duration activeRecordingSessions.value.set(userId, { ...session }) } } const transcribeRecording = async (recordingId, options = {}) => { try { loading.value = true error.value = null const transcriptionJob = await audioRecordingRepository.transcribe(recordingId, options) // Track transcription status transcriptionJobs.value.set(recordingId, { ...transcriptionJob, status: 'pending', startedAt: new Date().toISOString() }) // Start polling for completion pollTranscriptionStatus(recordingId) return transcriptionJob } catch (err) { error.value = err.message || 'Failed to start transcription' throw err } finally { loading.value = false } } const pollTranscriptionStatus = async (recordingId) => { try { const status = await audioRecordingRepository.getTranscriptionStatus(recordingId) transcriptionJobs.value.set(recordingId, status) if (status.status === 'completed') { // Update the recording with transcription data updateRecordingTranscription(recordingId, status.transcription) } else if (status.status === 'processing') { // Continue polling setTimeout(() => pollTranscriptionStatus(recordingId), 5000) } else if (status.status === 'failed') { console.error('Transcription failed:', status.error) } } catch (err) { console.error('Failed to poll transcription status:', err) } } const updateRecordingTranscription = (recordingId, transcriptionData) => { // Find and update the recording in all work orders for (const [workOrderId, recordingsList] of recordings.value.entries()) { const recordingIndex = recordingsList.findIndex(r => r.id === recordingId) if (recordingIndex !== -1) { recordingsList[recordingIndex] = { ...recordingsList[recordingIndex], transcription: transcriptionData.text, transcriptionConfidence: transcriptionData.confidence, transcriptionLanguage: transcriptionData.language, transcriptionUpdatedAt: new Date().toISOString() } recordings.value.set(workOrderId, [...recordingsList]) break } } } const generateWaveform = async (recordingId) => { try { const waveform = await audioRecordingRepository.generateWaveform(recordingId) waveformData.value.set(recordingId, waveform) return waveform } catch (err) { console.error('Failed to generate waveform:', err) return null } } const getAudioAnalysis = async (recordingId) => { try { const analysis = await audioRecordingRepository.getAudioAnalysis(recordingId) analysisData.value.set(recordingId, analysis) return analysis } catch (err) { console.error('Failed to get audio analysis:', err) return null } } const convertAudioFormat = async (recordingId, format) => { try { loading.value = true error.value = null const convertedBlob = await audioRecordingRepository.convertFormat(recordingId, format) // Create download const url = URL.createObjectURL(convertedBlob) const link = document.createElement('a') link.href = url link.download = `recording-${recordingId}.${format}` document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) return true } catch (err) { error.value = err.message || 'Failed to convert audio format' throw err } finally { loading.value = false } } const searchRecordings = async (query, filters = {}) => { try { loading.value = true error.value = null const searchResults = await audioRecordingRepository.searchByContent(query, filters) return searchResults } catch (err) { error.value = err.message || 'Failed to search recordings' throw err } finally { loading.value = false } } const loadActiveSessions = async (userId) => { try { const activeSessions = await audioRecordingRepository.getActiveSessions(userId) // Restore active sessions to local state activeSessions.forEach(session => { activeRecordingSessions.value.set(session.userId, { ...session, startTime: new Date(session.startTime), localDuration: 0 }) }) activeRecordingCount.value = activeRecordingSessions.value.size globalRecordingActive.value = activeRecordingCount.value > 0 return activeSessions } catch (err) { console.error('Failed to load active recording sessions:', err) return [] } } const loadSupportedFormats = async () => { try { const formats = await audioRecordingRepository.getSupportedFormats() audioFormats.value = formats return formats } catch (err) { console.error('Failed to load supported audio formats:', err) // Provide default supported formats if API call fails const defaultFormats = [ { mimeType: 'audio/webm', extension: 'webm', maxSize: 10485760 }, // 10MB { mimeType: 'audio/mp4', extension: 'mp4', maxSize: 10485760 }, { mimeType: 'audio/ogg', extension: 'ogg', maxSize: 10485760 } ] audioFormats.value = defaultFormats return defaultFormats } } const loadTranscriptionConfig = async () => { try { const config = await audioRecordingRepository.getTranscriptionConfig() transcriptionConfig.value = config return config } catch (err) { console.error('Failed to load transcription config:', err) // Provide default transcription config if API call fails const defaultConfig = { enabled: false, supportedLanguages: ['en', 'es', 'fr'], maxFileSize: 10485760, // 10MB timeout: 30000 // 30 seconds } transcriptionConfig.value = defaultConfig return defaultConfig } } const getStatistics = async (workOrderId) => { try { return await audioRecordingRepository.getStatistics(workOrderId) } catch (err) { error.value = err.message || 'Failed to get recording statistics' throw err } } const validateAudioFile = async (audioFile) => { try { return await audioRecordingRepository.validateAudioFile(audioFile) } catch (err) { error.value = err.message || 'Failed to validate audio file' throw err } } const bulkOperation = async (recordingIds, operation, options = {}) => { try { loading.value = true error.value = null const result = await audioRecordingRepository.bulkOperation(recordingIds, operation, options) // Update local state based on operation if (operation === 'delete') { // Remove deleted recordings from local state recordingIds.forEach(recordingId => { for (const [workOrderId, recordingsList] of recordings.value.entries()) { const filteredRecordings = recordingsList.filter(r => r.id !== recordingId) if (filteredRecordings.length !== recordingsList.length) { recordings.value.set(workOrderId, filteredRecordings) } } }) } return result } catch (err) { error.value = err.message || 'Failed to perform bulk operation' throw err } finally { loading.value = false } } const clearWorkOrderData = (workOrderId) => { recordings.value.delete(workOrderId) } const clearUserSession = (userId) => { activeRecordingSessions.value.delete(userId) activeRecordingCount.value = activeRecordingSessions.value.size globalRecordingActive.value = activeRecordingCount.value > 0 } const clearError = () => { error.value = null } return { // State recordings, activeRecordingSessions, transcriptionJobs, audioFormats, transcriptionConfig, loading, error, globalRecordingActive, activeRecordingCount, waveformData, analysisData, // Getters getRecordingsForWorkOrder, getActiveRecordingSession, hasActiveRecording, getTranscriptionStatus, getTotalDurationForWorkOrder, getTotalSizeForWorkOrder, getTranscribedCount, getWaveformData, getAnalysisData, // Actions loadRecordings, uploadRecording, updateRecording, deleteRecording, startRecordingSession, stopRecordingSession, updateSessionDuration, transcribeRecording, generateWaveform, getAudioAnalysis, convertAudioFormat, searchRecordings, loadActiveSessions, loadSupportedFormats, loadTranscriptionConfig, getStatistics, validateAudioFile, bulkOperation, clearWorkOrderData, clearUserSession, clearError } })