diff --git a/.reek.yml b/.reek.yml index cd12f9cc576..bb725320a37 100644 --- a/.reek.yml +++ b/.reek.yml @@ -55,6 +55,7 @@ detectors: exclude: - Address - Api::V3::DecisionReviews::ContestableIssueFinder#initialize + - ExternalApi::WebexService::RecordingsListResponse::Recording#initialize DataClump: exclude: - ForeignKeyPolymorphicAssociationJob @@ -135,6 +136,11 @@ detectors: - ExternalApi::PexipService#send_pexip_request - ControllerSchema#remove_unknown_keys - BusinessLineReporter#as_csv + - Hearings::DownloadTranscriptionFileJob#build_error_explanation + - Hearings::RefreshWebexAccessTokenJob#perform + - TranscriptionFile#update_status! + - ExternalApi::WebexService#create_conference + - TranscriptionTransformer InstanceVariableAssumption: exclude: - Appeal @@ -143,6 +149,7 @@ detectors: - Api::V3::DecisionReviews::HigherLevelReviewIntakeProcessor - ETL::Syncer - User + - Hearings::DownloadTranscriptionFileJob IrresponsibleModule: enabled: false LongParameterList: @@ -163,6 +170,8 @@ detectors: - LegacyDocket#distribute_priority_appeals - LegacyDocket#distribute_nonpriority_appeals - ExternalApi::MPIService#search_people_info + - Hearings::FetchWebexRecordingsDetailsJob#send_file + - ExternalApi::WebexService#initialize ManualDispatch: exclude: - Api::V3::DecisionReviews::IntakeError#potential_error_code @@ -180,6 +189,7 @@ detectors: max_allowed_nesting: 2 exclude: - AsyncableJobsReporter + - TranscriptionTransformer#create_transcription_pages NilCheck: enabled: false RepeatedConditional: @@ -199,6 +209,8 @@ detectors: - Veteran - LegacyDocket - Test::UsersController + - Fakes::WebexService + - Hearings::DownloadTranscriptionFileJob TooManyConstants: exclude: - Fakes::BGSServicePOA @@ -216,6 +228,8 @@ detectors: - Api::V3::DecisionReviews::IntakeError - ControllerSchema::Field - HearingEmailStatusMailer + - ExternalApi::WebexService + - TranscriptionFileIssuesMailer TooManyMethods: enabled: false TooManyStatements: @@ -255,10 +269,14 @@ detectors: - UpdatePOAConcern - VBMSCaseflowLogger#log - LegacyDocket + - ExternalApi::PexipService#not_found_response + - WebexConcern UnusedParameters: exclude: - Docket#distribute_appeals - HearingRequestDocket#distribute_appeals + - Fakes::WebexService#fetch_recording_details + - Fakes::WebexService#fetch_room_details ### Directory specific configuration diff --git a/Gemfile b/Gemfile index a6e0728b60e..d60777fe2c2 100644 --- a/Gemfile +++ b/Gemfile @@ -74,6 +74,7 @@ gem "rack", "~> 2.2.6.2" gem "rails", "6.1.7.7" # Used to colorize output for rake tasks gem "rainbow" +gem "rcredstash", "~> 1.1.0" # React gem "react_on_rails", "11.3.0" gem "redis-mutex" @@ -83,6 +84,7 @@ gem "request_store" gem "roo", "~> 2.7" gem "rswag-api" gem "rswag-ui" +gem "rtf" gem "ruby_claim_evidence_api", git: "https://github.com/department-of-veterans-affairs/ruby_claim_evidence_api.git", ref: "fed623802afe7303f4b8b5fe27cff0e903699873" # Use SCSS for stylesheets gem "sass-rails", "~> 5.0" @@ -99,6 +101,7 @@ gem "tzinfo", "~> 2.0" # Use Uglifier as compressor for JavaScript assets gem "uglifier", ">= 1.3.0" gem "validates_email_format_of" +gem "webvtt-ruby" gem "ziptz" group :production, :staging, :ssh_forwarding, :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 2632deaf2e2..100076cd374 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1500,7 +1500,7 @@ GEM crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.6) - d3-rails (7.0.0) + d3-rails (7.8.5) railties (>= 3.1) danger (6.2.2) claide (~> 1.0) @@ -1651,8 +1651,8 @@ GEM immigrant (0.3.6) activerecord (>= 3.0) jaro_winkler (1.5.6) - jmespath (1.3.1) - jquery-rails (4.5.1) + jmespath (1.6.2) + jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -1898,6 +1898,10 @@ GEM ffi (~> 1.0) rb-readline (0.5.5) rchardet (1.8.0) + rcredstash (1.1.0) + aws-sdk-dynamodb + aws-sdk-kms + thor react_on_rails (11.3.0) addressable connection_pool @@ -1974,6 +1978,7 @@ GEM rswag-ui (2.13.0) actionpack (>= 3.1, < 7.2) railties (>= 3.1, < 7.2) + rtf (0.3.3) rubocop (0.83.0) parallel (~> 1.10) parser (>= 2.7.0.1) @@ -2111,6 +2116,7 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + webvtt-ruby (0.4.0) xmldsig (0.3.2) nokogiri xmlenc (0.8.0) @@ -2211,6 +2217,7 @@ DEPENDENCIES rails-erd rainbow rb-readline + rcredstash (~> 1.1.0) react_on_rails (= 11.3.0) redis-mutex redis-namespace (~> 1.11.0) @@ -2225,6 +2232,7 @@ DEPENDENCIES rswag-api rswag-specs rswag-ui + rtf rubocop (= 0.83) rubocop-performance rubocop-rails @@ -2254,6 +2262,7 @@ DEPENDENCIES validates_email_format_of webdrivers webmock + webvtt-ruby ziptz BUNDLED WITH diff --git a/app/controllers/hearings/hearing_day_controller.rb b/app/controllers/hearings/hearing_day_controller.rb index 274e9f2387b..401f194051c 100644 --- a/app/controllers/hearings/hearing_day_controller.rb +++ b/app/controllers/hearings/hearing_day_controller.rb @@ -44,9 +44,9 @@ def show hearings: hearing_day.hearings_for_user(current_user).map { |hearing| hearing.quick_to_hash(current_user.id) } ) } - rescue VirtualHearings::LinkService::PINKeyMissingError, - VirtualHearings::LinkService::URLHostMissingError, - VirtualHearings::LinkService::URLPathMissingError => error + rescue VirtualHearings::PexipLinkService::PINKeyMissingError, + VirtualHearings::PexipLinkService::URLHostMissingError, + VirtualHearings::PexipLinkService::URLPathMissingError => error log_error(error) render json: { hearing_day: hearing_day.to_hash(include_conference_link: false).merge( @@ -89,7 +89,9 @@ def create def update hearing_day.update!(update_params) - render json: hearing_day.to_hash + render json: hearing_day.to_hash.merge( + conference_link: ::HearingDaySerializer.serialize_conference_link(hearing_day.conference_link) + ) end def destroy diff --git a/app/controllers/hearings/transcription_files_controller.rb b/app/controllers/hearings/transcription_files_controller.rb new file mode 100644 index 00000000000..9701580f50e --- /dev/null +++ b/app/controllers/hearings/transcription_files_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Hearings::TranscriptionFilesController < ApplicationController + include HearingsConcerns::VerifyAccess + + rescue_from ActiveRecord::RecordNotFound, with: :render_page_not_found + before_action :verify_access_to_hearings, only: [:download_transcription_file] + + # Downloads file and sends to user's local computer + def download_transcription_file + tmp_location = file.fetch_file_from_s3! + File.open(tmp_location, "r") { |stream| send_data stream.read, filename: file.file_name } + file.clean_up_tmp_location + end + + def render_page_not_found + redirect_to "/404" + end + + private + + def file + @file ||= TranscriptionFile.find(params[:file_id]) + end +end diff --git a/app/controllers/hearings_controller.rb b/app/controllers/hearings_controller.rb index 59095f24635..bfa4089daa3 100644 --- a/app/controllers/hearings_controller.rb +++ b/app/controllers/hearings_controller.rb @@ -82,6 +82,7 @@ def virtual_hearing_job_status alias_with_host: hearing.virtual_hearing&.formatted_alias_or_alias_with_host, guest_link: hearing.virtual_hearing&.guest_link, host_link: hearing.virtual_hearing&.host_link, + co_host_link: hearing.virtual_hearing&.co_host_hearing_link, guest_pin: hearing.virtual_hearing&.guest_pin, host_pin: hearing.virtual_hearing&.host_pin } diff --git a/app/controllers/organizations/users_controller.rb b/app/controllers/organizations/users_controller.rb index 6a125576a84..f5b7650bcf2 100644 --- a/app/controllers/organizations/users_controller.rb +++ b/app/controllers/organizations/users_controller.rb @@ -32,6 +32,7 @@ def update adjust_admin_rights end + update_user_conference_provider render json: { users: json_administered_users([user_to_modify]) }, status: :ok end @@ -67,6 +68,14 @@ def adjust_admin_rights end end + def update_user_conference_provider + new_conference_provider = params.dig(:attributes, :conference_provider) + + if organization["url"] == HearingsManagement.singleton.url && new_conference_provider + OrganizationsUser.update_user_conference_provider(user_to_modify, new_conference_provider) + end + end + def organization_url params[:organization_url] end diff --git a/app/jobs/hearings/create_non_virtual_conference_job.rb b/app/jobs/hearings/create_non_virtual_conference_job.rb new file mode 100644 index 00000000000..78b1ca678e3 --- /dev/null +++ b/app/jobs/hearings/create_non_virtual_conference_job.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# This job creates a Webex conference & link for a non virtual hearing + +class Hearings::CreateNonVirtualConferenceJob < CaseflowJob + # We are not using ensure_current_user_is_set because of some + # potential for rollbacks if the set user is not the system user + + queue_with_priority :high_priority + application_attr :hearing_schedule + + attr_reader :hearing + + # Retry if Webex returns an invalid response. + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + job.log_error(exception) + end + + def perform(hearing:) + RequestStore.store[:current_user] = User.system_user + WebexConferenceLink.find_or_create_by!( + hearing: hearing, + created_by: hearing.created_by, + updated_by: hearing.created_by + ) + end +end diff --git a/app/jobs/hearings/download_transcription_file_job.rb b/app/jobs/hearings/download_transcription_file_job.rb new file mode 100644 index 00000000000..1b2ad774e4f --- /dev/null +++ b/app/jobs/hearings/download_transcription_file_job.rb @@ -0,0 +1,240 @@ +# frozen_string_literal: true + +require "open-uri" + +# Downloads transcription file from Webex using temporary download link and uploads to S3 +# - Download link passed to this job from FetchRecordingDetailsJob +# - File type either audio (mp3), video (mp4), or vtt (transcript) + +class Hearings::DownloadTranscriptionFileJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + + queue_with_priority :low_priority + application_attr :hearing_schedule + + attr_reader :file_name, :transcription_file + + class FileNameError < StandardError; end + class FileDownloadError < StandardError; end + class HearingAssociationError < StandardError; end + + retry_on(FileDownloadError, wait: 5.minutes) do |job, exception| + details_hash = { + temporary_download_link: { link: job.arguments.first[:download_link] }, + error: { type: "download" }, + provider: "webex" + } + error_details = job.build_error_details(exception, details_hash) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + retry_on(TranscriptionFileUpload::FileUploadError, wait: :exponentially_longer) do |job, exception| + details_hash = { error: { type: "upload" }, provider: "S3" } + error_details = job.build_error_details(exception, details_hash) + job.transcription_file.clean_up_tmp_location + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + retry_on(TranscriptionTransformer::FileConversionError, wait: 10.seconds) do |job, exception| + job.transcription_file.clean_up_tmp_location + details_hash = { error: { type: "conversion" }, conversion_type: "rtf" } + error_details = job.build_error_details(exception, details_hash) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + discard_on(FileNameError) do |job, exception| + details_hash = { + error: { type: "download" }, + provider: "webex", + reason: "Unable to parse hearing information from file name: #{job.file_name}", + expected_file_name_format: "[docket_number]_[internal_id]_[hearing_type].[file_type]" + } + error_details = job.build_error_details(exception, details_hash) + job.log_download_error(exception) + job.send_transcription_issues_email(error_details) + end + + # Purpose: Downloads audio (mp3), video (mp4), or transcript (vtt) file from Webex temporary download link and + # uploads the file to corresponding S3 location. If file is vtt, kicks off conversion of vtt to rtf + # and uploads rtf file to S3. + def perform(download_link:, file_name:) + ensure_current_user_is_set + @file_name = file_name + @transcription_file ||= find_or_create_transcription_file + ensure_hearing_held + + download_file_to_tmp!(download_link) + @transcription_file.upload_to_s3! if @transcription_file.date_upload_aws.nil? + convert_to_rtf_and_upload_to_s3! if @transcription_file.file_type == "vtt" + @transcription_file.clean_up_tmp_location + end + + # Purpose: Builds hash of values to be listed in mail template + # + # Note: Public method to provide access during job retry + # + # Params: error - Instance of error + # details_hash - hash of attributes and values to be listed in mail template + # + # Returns: The hash for details on the error + def build_error_details(error, details_hash) + details_hash.merge( + docket_number: !file_name_error?(error) ? hearing.docket_number : nil, + appeal_id: !file_name_error?(error) ? hearing.appeal.external_id : nil, + error: details_hash[:error].merge( + explanation: build_error_explanation(details_hash) + ) + ) + end + + # Purpose: Logs error and captures exception + # + # Note: Public method to provide access during job retry + # + # Params: error - Error object + def log_download_error(error) + extra = { + application: self.class.name, + hearing_id: !file_name_error?(error) ? hearing.id : nil, + file_name: file_name, + job_id: job_id + } + log_error(error, extra: extra) + end + + private + + # Purpose: Downloads file from temporary download link provided by + # FetchRecordingDetailsJob. Update file status of transcription file depending on download success/failure. + # + # Params: download_link - string, URI for temporary download link + # file_name - string, to be parsed for hearing identifiers + # + # Returns: Updated @transcription_file + def download_file_to_tmp!(link) + transcription_file = @transcription_file + return if File.exist?(transcription_file.tmp_location) + + URI(link).open do |download| + IO.copy_stream(download, transcription_file.tmp_location) + end + transcription_file.update_status!(process: :retrieval, status: :success) + log_info("File #{file_name} successfully downloaded from Webex. Uploading to S3...") + rescue OpenURI::HTTPError => error + transcription_file.update_status!(process: :retrieval, status: :failure) + transcription_file.clean_up_tmp_location + raise FileDownloadError, "Webex temporary download link responded with error: #{error}" + end + + # Purpose: Hearing for which the Webex conference was held + # + # Note: Public method to provide access during job retry + # + # Returns: Hearing object + def hearing + @hearing ||= parse_hearing + end + + # Purpose: Parses hearing details from identifiers present in file name + # + # Returns: Hearing object or error if hearing not able to be parsed or found + def parse_hearing + identifiers = file_name.split(".").first + hearing_id = identifiers.split("_")[1] + hearing_type = identifiers.split("_").last.split("-").first + hearing_type.constantize.find(hearing_id) + rescue StandardError => error + raise FileNameError, "Encountered error #{error} when attempting to parse hearing from file name '#{file_name}'" + end + + # Purpose: Docket number associated with the hearing for which the transcription was created + # + # Returns: string or error + def docket_number + hearing.docket_number + end + + # Purpose: Finds existing transcription file record if job previously failed and retry initiated. Otherewise, creates + # new record. + # + # Params: file_name_arg - string, optional parameter with default value of file name attribute. Allows for method to + # be reused when converting vtt file to rtf. + # + # Returns: TranscriptionFile object + def find_or_create_transcription_file(file_name_arg = file_name) + TranscriptionFile.find_or_create_by( + file_name: file_name_arg, + hearing_id: hearing.id, + hearing_type: hearing.class.name, + docket_number: docket_number + ) do |file| + file.file_type = file_name_arg.split(".").last + file.created_by_id = RequestStore[:current_user].id + file.save! + end + rescue ActiveRecord::RecordInvalid => error + raise FileNameError, error + end + + # Purpose: Converts vtt to rtf, creates new record for converted transcription file, and uploads + # converted file to S3 + # + # Returns: integer value of 1 if tmp file deleted after successful upload + def convert_to_rtf_and_upload_to_s3! + log_info("Converting file #{file_name} to rtf...") + transcription_file = @transcription_file + file_paths = transcription_file.convert_to_rtf! + file_paths.each do |file_path| + output_file_name = file_path.split("/").last + output_file = find_or_create_transcription_file(output_file_name) + log_info("Successfully converted #{file_name} to rtf. Uploading #{output_file.file_type} to S3...") + output_file.upload_to_s3! + output_file.clean_up_tmp_location + end + end + + # Purpose: If disposition of associated hearing is not marked as held, sends email to VA Operations Team and + # continues with download job + def ensure_hearing_held + return if hearing.held? + + msg = "Download of Webex transcription files initiated for hearing (docket ##{docket_number}) successful, " \ + "but hearing's disposition not set to held." + Rails.logger.warn(HearingAssociationError.new(msg)) + # TO IMPLEMENT: SEND EMAIL TO VA OPS TEAM + end + + # Purpose: Logs message + # + # Params: message - string + def log_info(message) + Rails.logger.info(message) + end + + JOB_ACTIONS = { + download: { verb: "download", direction: "from" }, + upload: { verb: "upload", direction: "to" }, + conversion: { verb: "convert", direction: "to" } + }.freeze + + # Purpose: Builds error message to be printed in email notifications + # + # Params: details_hash - hash of attributes and values to be listed in mail template + # + # Returns: String message + def build_error_explanation(details_hash) + action = JOB_ACTIONS[details_hash[:error][:type].to_sym] + action_recipient = details_hash[:provider]&.titlecase || details_hash.delete(:conversion_type) + file_type = @transcription_file ? "#{@transcription_file.file_type} " : "" + + "#{action[:verb]} a #{file_type}file #{action[:direction]} #{action_recipient}" + end + + def file_name_error?(error) + error.is_a?(FileNameError) + end +end diff --git a/app/jobs/hearings/fetch_webex_recordings_details_job.rb b/app/jobs/hearings/fetch_webex_recordings_details_job.rb new file mode 100644 index 00000000000..d6bb8bc596c --- /dev/null +++ b/app/jobs/hearings/fetch_webex_recordings_details_job.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# This job will retrieve a list of webex hearing recording detail links +# and download the information from the links + +class Hearings::FetchWebexRecordingsDetailsJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearing_schedule + attr_reader :recording_id, :host_email, :meeting_title + + # rubocop:disable Layout/LineLength + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + recording_id = job.arguments&.first&.[](:recording_id) + host_email = job.arguments&.first&.[](:host_email) + meeting_title = job.arguments&.first&.[](:meeting_title) + query = "?hostEmail=#{host_email}" + error_details = { + error: { type: "retrieval", explanation: "retrieve recording details from Webex" }, + provider: "webex", + api_call: + "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}recordings/#{recording_id}#{query}", + response: { status: exception.code, message: exception.message }.to_json, + recording_id: recording_id, + host_email: host_email, + meeting_title: meeting_title + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + # rubocop:enable Layout/LineLength + + def perform(recording_id:, host_email:, meeting_title:) + ensure_current_user_is_set + data = fetch_recording_details(recording_id, host_email) + topic = data.topic + + mp4_link = data.mp4_link + send_file(topic, "mp4", mp4_link, meeting_title) + + vtt_link = data.vtt_link + send_file(topic, "vtt", vtt_link, meeting_title) + + mp3_link = data.mp3_link + send_file(topic, "mp3", mp3_link, meeting_title) + end + + def log_error(error) + extra = { + application: self.class.name, + job_id: job_id + } + super(error, extra: extra) + end + + private + + def fetch_recording_details(id, email) + query = { "hostEmail": email } + WebexService.new(recordings_config(query)).fetch_recording_details(id) + end + + def create_file_name(topic, extension, meeting_title) + counter = topic.split("-").last + "#{meeting_title}-#{counter}.#{extension}" + end + + def send_file(topic, extension, link, meeting_title) + file_name = create_file_name(topic, extension, meeting_title) + Hearings::DownloadTranscriptionFileJob.perform_later(download_link: link, file_name: file_name) + end +end diff --git a/app/jobs/hearings/fetch_webex_recordings_list_job.rb b/app/jobs/hearings/fetch_webex_recordings_list_job.rb new file mode 100644 index 00000000000..73fc4634278 --- /dev/null +++ b/app/jobs/hearings/fetch_webex_recordings_list_job.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# This job will retrieve a list of webex hearing recordings and details every hour + +class Hearings::FetchWebexRecordingsListJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearing_schedule + + attr_reader :meeting_id, :meeting_title + + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + max = 100 + id = job.arguments&.first&.[](:meeting_id) + meeting_title = job.arguments&.first&.[](:meeting_title) + query = "?max=#{max}&meetingId=#{id}" + error_details = { + error: { type: "retrieval", explanation: "retrieve a list of recordings from Webex" }, + provider: "webex", + api_call: + "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}admin/recordings/#{query}", + response: { status: exception.code, message: exception.message }.to_json, + meeting_id: id, + meeting_title: meeting_title + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + + def perform(meeting_id:, meeting_title:) + ensure_current_user_is_set + fetch_recordings_list(meeting_id).recordings.each do |recording| + Hearings::FetchWebexRecordingsDetailsJob.perform_later( + recording_id: recording.id, host_email: recording.host_email, meeting_title: meeting_title + ) + end + end + + def log_error(error) + super(error, extra: { application: self.class.name, job_id: job_id }) + end + + private + + def fetch_recordings_list(id) + max = 100 + meeting_id = id + query = { "max": max, "meetingId": meeting_id } + WebexService.new(recordings_config(query)).fetch_recordings_list + end +end diff --git a/app/jobs/hearings/fetch_webex_room_meeting_details_job.rb b/app/jobs/hearings/fetch_webex_room_meeting_details_job.rb new file mode 100644 index 00000000000..fe42544707c --- /dev/null +++ b/app/jobs/hearings/fetch_webex_room_meeting_details_job.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# This job retrieves details about a specific meeting room from Webex using their API. +class Hearings::FetchWebexRoomMeetingDetailsJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearing_schedule + + attr_reader :room_id, :meeting_title + + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + room_id = job.arguments&.first&.[](:room_id) + meeting_title = job.arguments&.first&.[](:meeting_title) + error_details = { + error: { type: "retrieval", explanation: "retrieve details of room from Webex" }, + provider: "webex", + api_call: + "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}rooms/#{room_id}/meetingInfo", + response: { status: exception.code, message: exception.message }.to_json, + room_id: room_id, + meeting_title: meeting_title + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + + def perform(room_id:, meeting_title:) + ensure_current_user_is_set + room_meeting_details = fetch_room_details(room_id) + Hearings::FetchWebexRecordingsListJob.perform_later( + meeting_id: room_meeting_details.meeting_id, + meeting_title: meeting_title + ) + end + + private + + # This constructs the headers and calls on the webex endpoint + # to retreive the meeting details from the specified room using ID + # Params: id - The unique ID of the webex room + # Return: The response object created from the response from the API + def fetch_room_details(id) + WebexService.new(rooms_config).fetch_room_details(id) + end +end diff --git a/app/jobs/hearings/fetch_webex_rooms_list_job.rb b/app/jobs/hearings/fetch_webex_rooms_list_job.rb new file mode 100644 index 00000000000..f7253dd1ba6 --- /dev/null +++ b/app/jobs/hearings/fetch_webex_rooms_list_job.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# This job will retrieve a list of all the webex meeting rooms used by the VA to hold hearings + +class Hearings::FetchWebexRoomsListJob < CaseflowJob + include Hearings::EnsureCurrentUserIsSet + include Hearings::SendTranscriptionIssuesEmail + include WebexConcern + + queue_with_priority :low_priority + application_attr :hearings_schedule + + retry_on(Caseflow::Error::WebexApiError, wait: :exponentially_longer) do |job, exception| + sort_by = "created" + max = 1000 + query = "?sortBy=#{sort_by}&max=#{max}" + error_details = { + error: { type: "retrieval", explanation: "retrieve a list of rooms from Webex" }, + provider: "webex", + api_call: "GET #{ENV['WEBEX_HOST_MAIN']}#{ENV['WEBEX_DOMAIN_MAIN']}#{ENV['WEBEX_API_MAIN']}rooms#{query}", + response: { status: exception.code, message: exception.message }.to_json + } + job.log_error(exception) + job.send_transcription_issues_email(error_details) + end + + def perform + ensure_current_user_is_set + fetch_rooms_list.rooms.each do |room| + title = filter_title(room.title).first + next if title.blank? + + Hearings::FetchWebexRoomMeetingDetailsJob.perform_later(room_id: room.id, meeting_title: title) + end + end + + def log_error(error) + super(error, extra: { application: self.class.name, job_id: job_id }) + end + + private + + def filter_title(title) + title.scan(/\d*-*\d+_\d+_[A-Za-z]*Hearing/) + end + + def fetch_rooms_list + sort_by = "created" + max = 1000 + query = { "sortBy": sort_by, "max": max } + + WebexService.new(rooms_config(query)).fetch_rooms_list + end +end diff --git a/app/jobs/hearings/refresh_webex_access_token_job.rb b/app/jobs/hearings/refresh_webex_access_token_job.rb new file mode 100644 index 00000000000..c2721771675 --- /dev/null +++ b/app/jobs/hearings/refresh_webex_access_token_job.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# This file defines the RefreshWebexAccessTokenJob class, a job that refreshes the access token +# used for Webex API calls. This job is part of the VirtualHearings module. +# +# The job has the following key behaviors: +# 1. The perform method creates a new instance of WebexService and calls its +# refresh_access_token method to get a new access token from the Webex API. +# 2. If the response from the Webex API is successful, the new access and refresh tokens are +# stored in CredStash with the keys webex_#{Rails.deploy_env}_access_token and +# webex_#{Rails.deploy_env}_refresh_token respectively. +# 3. If an error occurs during the process, it is caught and logged using the log_error method. +# +# This job is queued with low priority, indicating that it does not need to be run immediately +# and can wait until the system is less busy. + +class Hearings::RefreshWebexAccessTokenJob < CaseflowJob + queue_with_priority :low_priority + + def perform + webex_service = WebexService.new( + host: ENV["WEBEX_HOST_MAIN"], + port: nil, + aud: nil, + apikey: nil, + domain: ENV["WEBEX_DOMAIN_MAIN"], + api_endpoint: ENV["WEBEX_API_MAIN"], + query: nil + ) + response = webex_service.refresh_access_token + + if response.success? + new_access_token = response.access_token + new_refresh_token = response.refresh_token + + CredStash.put("webex_#{Rails.deploy_env}_access_token", new_access_token) + CredStash.put("webex_#{Rails.deploy_env}_refresh_token", new_refresh_token) + end + rescue StandardError => error + log_error(error) + end +end diff --git a/app/jobs/hearings/send_transcription_issues_email.rb b/app/jobs/hearings/send_transcription_issues_email.rb new file mode 100644 index 00000000000..af1eb627dbe --- /dev/null +++ b/app/jobs/hearings/send_transcription_issues_email.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Hearings::SendTranscriptionIssuesEmail + def send_transcription_issues_email(error_details) + TranscriptionFileIssuesMailer.issue_notification(error_details).deliver_now! + rescue StandardError, Savon::Error, BGS::ShareError => error + # Savon::Error and BGS::ShareError are sometimes thrown when making requests to BGS endpoints + log_error(error) + end +end diff --git a/app/jobs/virtual_hearings/conference_client.rb b/app/jobs/virtual_hearings/conference_client.rb new file mode 100644 index 00000000000..ed1fc3a8941 --- /dev/null +++ b/app/jobs/virtual_hearings/conference_client.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module VirtualHearings::ConferenceClient + include WebexConcern + + def client(virtual_hearing) + @client ||= case virtual_hearing.conference_provider + when "pexip" then create_pexip_client + when "webex" then create_webex_client + when nil + virtual_hearing.set_default_meeting_type + + return create_pexip_client if virtual_hearing.conference_provider == "pexip" + + return create_webex_client if virtual_hearing.conference_provider == "webex" + + raise_not_found_error + else + raise_not_found_error + end + end + + private + + def raise_not_found_error + msg = "Conference Provider for the Virtual Hearing Not Found" + + fail Caseflow::Error::MeetingTypeNotFoundError, message: msg + end + + def create_webex_client + WebexService.new(instant_connect_config) + end + + def create_pexip_client + PexipService.new( + host: ENV["PEXIP_MANAGEMENT_NODE_HOST"], + port: ENV["PEXIP_MANAGEMENT_NODE_PORT"], + user_name: ENV["PEXIP_USERNAME"], + password: ENV["PEXIP_PASSWORD"], + client_host: ENV["PEXIP_CLIENT_HOST"] + ) + end +end diff --git a/app/jobs/virtual_hearings/conference_job.rb b/app/jobs/virtual_hearings/conference_job.rb index 4dfa3ad9e3c..a9389c88024 100644 --- a/app/jobs/virtual_hearings/conference_job.rb +++ b/app/jobs/virtual_hearings/conference_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -class VirtualHearings::ConferenceJob < ApplicationJob - include VirtualHearings::PexipClient +class VirtualHearings::ConferenceJob < CaseflowJob + include VirtualHearings::ConferenceClient private diff --git a/app/jobs/virtual_hearings/create_conference_job.rb b/app/jobs/virtual_hearings/create_conference_job.rb index 0c390164315..5fad8ab5bae 100644 --- a/app/jobs/virtual_hearings/create_conference_job.rb +++ b/app/jobs/virtual_hearings/create_conference_job.rb @@ -50,7 +50,21 @@ class VirtualHearingLinkGenerationFailed < StandardError; end hearing_type: kwargs[:hearing_type] } - Raven.capture_exception(exception, extra: extra) + job.log_error(exception, extra: extra) + end + + # Retry if Webex returns an invalid response. + retry_on(Caseflow::Error::WebexApiError, attempts: 5, wait: :exponentially_longer) do |job, exception| + Rails.logger.error("#{job.class.name} (#{job.job_id}) failed with error: #{exception}") + + kwargs = job.arguments.first + extra = { + application: job.class.app_name.to_s, + hearing_id: kwargs[:hearing_id], + hearing_type: kwargs[:hearing_type] + } + + job.log_error(exception, extra: extra) end # Log the timezone of the job. This is primarily used for debugging context around times @@ -123,41 +137,47 @@ def log_virtual_hearing_state(virtual_hearing) Rails.logger.info("Establishment Updated At: (#{virtual_hearing.establishment.updated_at})") end - def create_conference_tags + def create_conference_metrics_tags custom_metric_info.merge(attrs: { hearing_id: virtual_hearing.hearing_id }) end def create_conference - if FeatureToggle.enabled?(:virtual_hearings_use_new_links, user: virtual_hearing.updated_by) - generate_links_and_pins - else - assign_virtual_hearing_alias_and_pins if should_initialize_alias_and_pins? + return generate_links_and_pins if virtual_hearing.conference_provider == "pexip" - Rails.logger.info( - "Trying to create conference for hearing (#{virtual_hearing.hearing_type} " \ - "[#{virtual_hearing.hearing_id}])..." - ) + create_webex_conference + end - pexip_response = create_pexip_conference + def create_webex_conference + Rails.logger.info( + "Trying to create Webex conference for hearing (#{virtual_hearing.hearing_type} " \ + "[#{virtual_hearing.hearing_id}])..." + ) - Rails.logger.info("Pexip response: #{pexip_response.inspect}") + create_webex_conference_response = create_new_conference - if pexip_response.error - error_display = pexip_error_display(pexip_response) + Rails.logger.info("Create Webex Conference Response: #{create_webex_conference_response.inspect}") - Rails.logger.error("CreateConferenceJob failed: #{error_display}") + conference_creation_error(create_webex_conference_response) if create_webex_conference_response.error - virtual_hearing.establishment.update_error!(error_display) + MetricsService.increment_counter(metric_name: "created_conference.successful", **create_conference_metrics_tags) - MetricsService.increment_counter(metric_name: "created_conference.failed", **create_conference_tags) + virtual_hearing.update( + host_hearing_link: create_webex_conference_response.host_link, + co_host_hearing_link: create_webex_conference_response.co_host_link, + guest_hearing_link: create_webex_conference_response.guest_link + ) - fail pexip_response.error - end + MetricsService.increment_counter(metric_name: "created_conference.successful", **create_conference_tags) + end - MetricsService.increment_counter(metric_name: "created_conference.successful", **create_conference_tags) + def conference_creation_error(create_conference_response) + error_display = error_display(create_conference_response) - virtual_hearing.update(conference_id: pexip_response.data[:conference_id]) - end + MetricsService.increment_counter(metric_name: "created_conference.failed", **create_conference_metrics_tags) + + virtual_hearing.establishment.update_error!(error_display) + + fail create_conference_response.error end def send_emails(email_type) @@ -168,20 +188,16 @@ def send_emails(email_type) ).call rescue StandardError => error extra = { application: "hearings", email_type: email_type, virtual_hearing_id: virtual_hearing.id } - Raven.capture_exception(error, extra: extra) + log_error(error, extra: extra) end end - def pexip_error_display(response) + def error_display(response) "(#{response.error.code}) #{response.error.message}" end - def create_pexip_conference - client.create_conference( - host_pin: virtual_hearing.host_pin, - guest_pin: virtual_hearing.guest_pin, - name: virtual_hearing.alias - ) + def create_new_conference + client(virtual_hearing).create_conference(virtual_hearing) end def should_initialize_alias_and_pins? @@ -207,7 +223,7 @@ def generate_links_and_pins "[#{virtual_hearing.hearing_id}])..." ) begin - link_service = VirtualHearings::LinkService.new + link_service = VirtualHearings::PexipLinkService.new virtual_hearing.update!( host_hearing_link: link_service.host_link, guest_hearing_link: link_service.guest_link, @@ -216,7 +232,7 @@ def generate_links_and_pins alias_with_host: link_service.alias_with_host ) rescue StandardError => error - Raven.capture_exception(error: error) + log_error(error) raise VirtualHearingLinkGenerationFailed end end diff --git a/app/jobs/virtual_hearings/delete_conference_link_job.rb b/app/jobs/virtual_hearings/delete_conference_link_job.rb new file mode 100644 index 00000000000..10889d0ed97 --- /dev/null +++ b/app/jobs/virtual_hearings/delete_conference_link_job.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# The DeleteConferenceLinkJob is a job thats collects conference_links from past hearing days. +# It then iterates through that collection and adjusts attribute values for each link. +# Afterwards each link then has `.destroy` called on it to issue a [soft delete]. + +class VirtualHearings::DeleteConferenceLinkJob < CaseflowJob + queue_with_priority :low_priority + + def perform + begin + RequestStore[:current_user] = User.system_user + retreive_stale_conference_links.each(&:soft_removal_of_link) + rescue StandardError => error + log_error(error) + end + end + + private + + # Purpose: Queries the DB table of conference_links that are associated with a hearing_day that has already passed. + # + # Params: None + # + # Return: A collection of links for hearing days that have passed. + def retreive_stale_conference_links + ConferenceLink.joins(:hearing_day).where("scheduled_for < ?", Time.zone.today) + end +end diff --git a/app/jobs/virtual_hearings/delete_conferences_job.rb b/app/jobs/virtual_hearings/delete_conferences_job.rb index 9f713c64cf5..1a44843f2f8 100644 --- a/app/jobs/virtual_hearings/delete_conferences_job.rb +++ b/app/jobs/virtual_hearings/delete_conferences_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true ## -# Job that deletes the pexip conference resource if the hearing was held or +# Job that deletes the pexip/webex conference resource if the hearing was held or # if the hearing type is switched from virtual to original hearing type. # It also sends cancellation emails to hearing participants if latter is case. @@ -18,7 +18,7 @@ class EmailsFailedToSend < StandardError; end before_perform do Rails.logger.info( - "#{self.class.name} for deleting Pexip conferences and sending cancellation emails" + "#{self.class.name} for deleting Pexip or Webex conferences and sending cancellation emails" ) end @@ -47,14 +47,14 @@ def perform count_deleted_and_log(VirtualHearingRepository.ready_for_deletion) do |virtual_hearing| log_virtual_hearing_state(virtual_hearing) - Rails.logger.info("Deleting Pexip conference for hearing (#{virtual_hearing.hearing_id})") + Rails.logger.info("Deleting Pexip or Webex conference for hearing (#{virtual_hearing.hearing_id})") process_virtual_hearing(virtual_hearing) end log_failed_virtual_hearings if exception_list.present? - # raise DeleteConferencesJobFailure if EmailsFailedToSend and/or PexipApiErrors were raised + # raise DeleteConferencesJobFailure if EmailsFailedToSend and/or Pexip/Webex ApiErrors were raised fail DeleteConferencesJobFailure if exception_list.present? end @@ -66,9 +66,13 @@ def exception_list def log_failed_virtual_hearings vh_with_pexip_errors = exception_list[Caseflow::Error::PexipApiError] + vh_with_webex_errors = exception_list[Caseflow::Error::WebexApiError] if vh_with_pexip_errors - Rails.logger.info("Failed to delete conferences for the following hearings: " \ + Rails.logger.info("Failed to delete pexip conferences for the following hearings: " \ "#{vh_with_pexip_errors.map(&:hearing_id)}") + elsif vh_with_webex_errors + Rails.logger.info("Failed to delete webex conferences for the following hearings: " \ + "#{vh_with_webex_errors.map(&:hearing_id)}") end vh_with_email_errors = exception_list[EmailsFailedToSend] @@ -82,7 +86,8 @@ def log_virtual_hearing_state(virtual_hearing) super Rails.logger.info("Cancelled?: (#{virtual_hearing.cancelled?})") - Rails.logger.info("Pexip conference id: (#{virtual_hearing.conference_id?})") + Rails.logger.info("Conference id: (#{virtual_hearing.conference_id})") + Rails.logger.info("Meeting Type: (#{virtual_hearing.conference_provider})") end def send_cancellation_emails(virtual_hearing) @@ -138,16 +143,19 @@ def process_virtual_hearing(virtual_hearing) true end - # Returns whether or not the conference was deleted from Pexip + # Returns whether or not the conference was deleted from Pexip or Webex + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def delete_conference(virtual_hearing) - response = client.delete_conference(conference_id: virtual_hearing.conference_id) - Rails.logger.info("Pexip response: #{response}") + response = client(virtual_hearing).delete_conference(virtual_hearing) + Rails.logger.info("#{virtual_hearing.conference_provider.capitalize} response: #{response}") fail response.error unless response.success? true rescue Caseflow::Error::PexipNotFoundError Rails.logger.info("Conference for hearing (#{virtual_hearing.hearing_id}) was already deleted") + rescue Caseflow::Error::WebexNotFoundError + Rails.logger.info("Conference for hearing (#{virtual_hearing.hearing_id}) was already deleted") # Assume the conference was already deleted if it's no longer in Pexip. true @@ -167,6 +175,23 @@ def delete_conference(virtual_hearing) } ) + false + rescue Caseflow::Error::WebexApiError => error + Rails.logger.error("Failed to delete conference from Webex for hearing (#{virtual_hearing.hearing_id})" \ + " with error: (#{error.code}) #{error.message}") + + (exception_list[Caseflow::Error::WebexApiError] ||= []) << virtual_hearing + + capture_exception( + error: error, + extra: { + hearing_id: virtual_hearing.hearing_id, + virtual_hearing_id: virtual_hearing.id, + webex_conference_Id: virtual_hearing.conference_id + } + ) + false end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength end diff --git a/app/jobs/virtual_hearings/pexip_client.rb b/app/jobs/virtual_hearings/pexip_client.rb deleted file mode 100644 index 70e5023f662..00000000000 --- a/app/jobs/virtual_hearings/pexip_client.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module VirtualHearings::PexipClient - def client - @client ||= PexipService.new( - host: ENV["PEXIP_MANAGEMENT_NODE_HOST"], - port: ENV["PEXIP_MANAGEMENT_NODE_PORT"], - user_name: ENV["PEXIP_USERNAME"], - password: ENV["PEXIP_PASSWORD"], - client_host: ENV["PEXIP_CLIENT_HOST"] - ) - end -end diff --git a/app/mailers/hearing_mailer.rb b/app/mailers/hearing_mailer.rb index bec1385e9ee..73863c4ca65 100644 --- a/app/mailers/hearing_mailer.rb +++ b/app/mailers/hearing_mailer.rb @@ -165,7 +165,7 @@ def link end # Raise an error if the link contains the old virtual hearing link 2021-11-10 - if hearing_link.include?(BAD_VIRTUAL_LINK_TEXT) + if hearing_link.nil? || hearing_link.include?(BAD_VIRTUAL_LINK_TEXT) fail BadVirtualLinkError, virtual_hearing_id: virtual_hearing&.id end diff --git a/app/mailers/transcription_file_issues_mailer.rb b/app/mailers/transcription_file_issues_mailer.rb new file mode 100644 index 00000000000..324515b6646 --- /dev/null +++ b/app/mailers/transcription_file_issues_mailer.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +# rubocop:disable Rails/ApplicationMailer +## +# TranscriptionFileIssuesMailer: +# - Generate emails from the templates in app/views/transcription_file_issues +## +class TranscriptionFileIssuesMailer < ActionMailer::Base + default from: "Board of Veterans' Appeals " + layout "transcription_file_issues_mailer" + + # Purpose: Builds email from view in app/views/transcription_file_issues_mailer/issue_notification + # + # Params: details - Hash of key-value pairs required to populate email template: + # - error: { type: string, explanation: string } + # - type: to render subject in #build_subject + # - explanation: "Caseflow attempted to #{explanation} and received a fatal error." + # - provider: string, to build subject and closing statement in #build_outro + # - docket_number: string, optional, but if present renders in subject + # - appeal_id: string, optional, but if present renders Case Details link + # + # - Optionally, any additional key-value pairs are iterated over and included in body as bullets + # according to following formats: + # - key: value =>
  • key.to_s: value
  • + # - key: { link: value } =>
  • key.to_s
  • + # - key: { nested_key_1: value_1, nested_key_2: value_2 } => + #
  • key: + # + #
  • + # + def issue_notification(details) + @details = details + build_mailer_params + + mail(subject: build_subject, **mailer_config[:emails]) + end + + private + + def build_mailer_params + @provider = @details.delete(:provider) + @details[:case_details] = build_case_details_link(@details.delete(:appeal_id)) + @error_type = @details[:error][:type] + @explanation = @details.delete(:error)&.dig(:explanation) + @outro = build_outro + end + + def build_case_details_link(appeal_id) + return unless appeal_id + + { link: mailer_config[:base_url] + "/queue/appeals/#{appeal_id}" } + end + + def build_subject + provider = @provider ? " #{@provider.titlecase}" : "" + docket_number = @details[:docket_number] ? " #{@details[:docket_number].titlecase}" : "" + "File #{@error_type.titlecase} Error -" + provider + docket_number + end + + def mailer_config + case Rails.deploy_env + when :development, :test + { base_url: non_external_link, + emails: { to: "Caseflow@test.com" } } + when :uat + { base_url: "https://appeals.cf.uat.ds.va.gov", + emails: { to: "BID_Appeals_UAT@bah.com" } } + when :prodtest + { base_url: "https://appeals.cf.prodtest.ds.va.gov", + emails: { to: "VHACHABID_Appeals_ProdTest@va.gov" } } + when :preprod + { base_url: "https://appeals.cf.preprod.ds.va.gov" } + when :prod + { base_url: "https://appeals.cf.ds.va.gov", + emails: { to: "BVAHearingTeam@VA.gov", + cc: "OITAppealsHelpDesk@va.gov" } } + end + end + + # The link for the case details page when not in prod or uat + def non_external_link + # Rails.deploy_env returns :development for both development and demo envs, use ENV["DEPLOY_ENV"] + return "https://demo.appeals.va.gov" if ENV["DEPLOY_ENV"] == "demo" + + "localhost:3000" + end + + def build_outro + return "continued communication between Caseflow and #{@provider.titlecase}" if issue_with_conference_provider? + + "Caseflow has been supplied with the necessary files" + end + + def issue_with_conference_provider? + %w[webex].include?(@provider) + end +end +# rubocop:enable Rails/ApplicationMailer diff --git a/app/models/concerns/conferenceable_concern.rb b/app/models/concerns/conferenceable_concern.rb new file mode 100644 index 00000000000..0d37f1aec9d --- /dev/null +++ b/app/models/concerns/conferenceable_concern.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +## +# Any model that includes this concern will be able to be assigned a conference provider +# for use in creating virtual conference links. + +module ConferenceableConcern + extend ActiveSupport::Concern + + DEFAULT_SERVICE = ENV["DEFAULT_CONFERENCE_SERVICE"] || "pexip" + + included do + has_one :meeting_type, as: :conferenceable + + after_create :set_default_meeting_type + + delegate :conference_provider, to: :meeting_type, allow_nil: true + end + + # Determines which associated entity this new item should inherit its conference + # provider from. + # + # Virtual hearings will inherit their conference providers from the hearing they've + # been created for. + # + # Other items will inherit from the conference provider assigned to the user who is + # creating them. + # + # @return [String] the conference provider/service name to assign to the new object ("webex" or "pexip") + def determine_service_name + return hearing&.conference_provider if is_a? VirtualHearing + + try(:created_by).try(:conference_provider) + end + + # Creates an associated MeetingType record for the newly created object. + # Which conference provider will be configured within this record is determined + # by #determine_service_name + # + # @return [MeetingType] the new MeetingType object after it has been reloaded. + def set_default_meeting_type + unless meeting_type + MeetingType.create!( + service_name: determine_service_name || DEFAULT_SERVICE, + conferenceable: self + ) + + reload_meeting_type + end + end +end diff --git a/app/models/concerns/hearing_concern.rb b/app/models/concerns/hearing_concern.rb index 74d5331a1c9..59c6af1d4f0 100644 --- a/app/models/concerns/hearing_concern.rb +++ b/app/models/concerns/hearing_concern.rb @@ -5,6 +5,7 @@ ## module HearingConcern extend ActiveSupport::Concern + include RunAsyncable CLOSED_HEARING_DISPOSITIONS = [ Constants.HEARING_DISPOSITION_TYPES.postponed, @@ -102,4 +103,51 @@ def calculate_submission_window end_date end + + def subject_for_conference + "#{docket_number}_#{id}_#{self.class}" + end + + def nbf + scheduled_for.beginning_of_day.to_i + end + + def exp + scheduled_for.end_of_day.to_i + end + + # Returns the new 1:1 conference link object for legacy and ama hearings + # that are non virtual and have a webex meeting type + def non_virtual_conference_link + ConferenceLink.find_by(hearing: self) + end + + # Associate hearing with transcription files across multiple dockets and order accordingly + def transcription_files_by_docket_number + # Remove hyphen in case of counter at end of file name to allow for alphabetical sort + transcription_files.sort_by { |file| file.file_name.split("-").join }.group_by(&:docket_number).values + end + + # Group transcription files by docket number before mapping through nested array and serializing + def serialized_transcription_files + transcription_files_by_docket_number.map do |file_groups| + file_groups.map do |file| + TranscriptionFileSerializer.new(file).serializable_hash[:data][:attributes] + end + end + end + + def start_non_virtual_hearing_job? + disposition.nil? && conference_provider == "webex" && + virtual_hearing.nil? && ConferenceLink.find_by(hearing: self).nil? + end + + def start_non_virtual_hearing_job + perform_later_or_now(Hearings::CreateNonVirtualConferenceJob, hearing: self) + end + + # Complexity of create schedule hearing task was too large - had to break out + def maybe_create_non_virtual_conference + start_non_virtual_hearing_job if start_non_virtual_hearing_job? + end end diff --git a/app/models/concerns/webex_concern.rb b/app/models/concerns/webex_concern.rb new file mode 100644 index 00000000000..ad1fd700a1f --- /dev/null +++ b/app/models/concerns/webex_concern.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Shared Webex methods used for Webex related functions +module WebexConcern + extend ActiveSupport::Concern + + # Purpose: Set up the configuration for calling the recordings endpoint + # + # Params: query - additional details for how returned data should be displayed + # + # Return: Object with header information for the endpoin + def recordings_config(query) + { + host: ENV["WEBEX_HOST_MAIN"], + port: ENV["WEBEX_PORT"], + aud: ENV["WEBEX_ORGANIZATION"], + apikey: WebexService.access_token, + domain: ENV["WEBEX_DOMAIN_MAIN"], + api_endpoint: ENV["WEBEX_API_MAIN"], + query: query + } + end + + # Purpose: Set up the configuration for calling the rooms endpoint + # + # Params: query - additional details for how returned data should be displayed + # + # Return: Object with header information for the endpoint + def rooms_config(query = nil) + { + host: ENV["WEBEX_HOST_MAIN"], + port: ENV["WEBEX_PORT"], + aud: ENV["WEBEX_ORGANIZATION"], + apikey: ENV["WEBEX_BOTTOKEN"], + domain: ENV["WEBEX_DOMAIN_MAIN"], + api_endpoint: ENV["WEBEX_API_MAIN"], + query: query + } + end + + # Purpose: Set up the configuration for calling the instant connect endpoint + # + # Params: query - additional details for how returned data should be displayed + # + # Return: Object with header information for the endpoint + def instant_connect_config(query = nil) + { + host: ENV["WEBEX_HOST_IC"], + port: ENV["WEBEX_PORT"], + aud: ENV["WEBEX_ORGANIZATION"], + apikey: ENV["WEBEX_BOTTOKEN"], + domain: ENV["WEBEX_DOMAIN_IC"], + api_endpoint: ENV["WEBEX_API_IC"], + query: query + } + end +end diff --git a/app/models/hearing.rb b/app/models/hearing.rb index 6008a49d157..ae4cf65c02b 100644 --- a/app/models/hearing.rb +++ b/app/models/hearing.rb @@ -31,6 +31,7 @@ class Hearing < CaseflowRecord include UpdatedByUserConcern include HearingConcern include HasHearingEmailRecipientsConcern + include ConferenceableConcern # VA Notify Hooks prepend HearingScheduled @@ -49,6 +50,7 @@ class Hearing < CaseflowRecord has_many :hearing_issue_notes has_many :email_events, class_name: "SentHearingEmailEvent" has_many :email_recipients, class_name: "HearingEmailRecipient" + has_many :transcription_files, as: :hearing class HearingDayFull < StandardError; end @@ -187,6 +189,10 @@ def advance_on_docket_motion .first end + def daily_docket_conference_link + hearing_day.conference_link + end + # returns scheduled datetime object considering the timezones # @return [nil] if hearing_day is nil # @return [Time] in scheduled_in_timezone timezone - if scheduled_datetime and scheduled_in_timezone are present diff --git a/app/models/hearing_day.rb b/app/models/hearing_day.rb index 9d559d2695c..76df6df34b1 100644 --- a/app/models/hearing_day.rb +++ b/app/models/hearing_day.rb @@ -52,6 +52,7 @@ class HearingDayHasChildrenRecords < StandardError; end before_create :assign_created_by_user after_update :update_children_records after_create :generate_link_on_create + before_destroy :soft_link_removal # Validates if the judge id maps to an actual record. validates :judge, presence: true, if: -> { judge_id.present? } @@ -136,6 +137,7 @@ def hearings_for_user(current_user) caseflow_and_vacols_hearings end + # :reek:BooleanParameter def to_hash(include_conference_link = false) judge_names = HearingDayJudgeNameQuery.new([self]).call video_hearing_days_request_types = if VirtualHearing::VALID_REQUEST_TYPES.include? request_type @@ -215,13 +217,22 @@ def half_day? total_slots ? total_slots <= 5 : false end - # over write of the .conference_link method from belongs_to :conference_link to add logic to create of not there + def scheduled_date_passed? + scheduled_for < Date.current + end + + # over write of the .conference_link method from belongs_to :conference_link to add logic to create if not there def conference_link - @conference_link ||= find_or_create_conference_link! + @conference_link ||= scheduled_date_passed? ? nil : find_or_create_conference_link! end private + # called through the 'before_destroy' callback on the hearing_day object. + def soft_link_removal + ConferenceLink.where(hearing_day: self).find_each(&:soft_removal_of_link) + end + def assign_created_by_user self.created_by ||= RequestStore[:current_user] end @@ -233,7 +244,7 @@ def log_error(error) def generate_link_on_create begin - this.conference_link + conference_link rescue StandardError => error log_error(error) end @@ -279,13 +290,11 @@ def combine_time_and_date(time, timezone, date) formatted_datetime_string end - # Method to get the associated conference link record if exists and if not create new one + # Method to get the associated conference link records if they exist and if not create new ones def find_or_create_conference_link! - conference_link = ConferenceLink.find_by_hearing_day_id(id) - if conference_link.nil? - conference_link = ConferenceLink.create(hearing_day_id: id) + if FeatureToggle.enabled?(:pexip_conference_service) + PexipConferenceLink.find_or_create_by!(hearing_day: self, created_by: created_by) end - conference_link end class << self diff --git a/app/models/hearings/conference_link.rb b/app/models/hearings/conference_link.rb index c6e3ea56e4e..65724685fdf 100644 --- a/app/models/hearings/conference_link.rb +++ b/app/models/hearings/conference_link.rb @@ -4,84 +4,47 @@ class ConferenceLink < CaseflowRecord class NoAliasWithHostPresentError < StandardError; end class LinkMismatchError < StandardError; end + acts_as_paranoid + include UpdatedByUserConcern include CreatedByUserConcern + include ConferenceableConcern - after_create :generate_links_and_pins - - class << self - def client_host_or_default - ENV["VIRTUAL_HEARING_URL_HOST"] || "care.evn.va.gov" - end + belongs_to :hearing, polymorphic: true - def formatted_alias(alias_name) - "BVA#{alias_name}@#{client_host_or_default}" - end - - def base_url - "https://#{client_host_or_default}/bva-app/" - end - end - - alias_attribute :alias_name, :alias + after_create :generate_conference_information belongs_to :hearing_day + belongs_to :created_by, class_name: "User" - # Override the host pin - def host_pin - host_pin_long || self[:host_pin] - end - - # rubocop:disable Naming/MemoizedInstanceVariableName - def host_link - @full_host_link ||= "#{ConferenceLink.base_url}?join=1&media=&escalate=1&" \ - "conference=#{alias_with_host}&" \ - "pin=#{host_pin}&role=host" - end - # rubocop:enable Naming/MemoizedInstanceVariableName - - def guest_pin - return guest_pin_long if !guest_pin_long.nil? + alias_attribute :alias_name, :alias - link_service = VirtualHearings::LinkService.new - update!(guest_pin_long: link_service.guest_pin) - guest_pin_long + # Purpose: updates the conf_link and then soft_deletes them. + # + # Params: None + # + # Return: None + def soft_removal_of_link + update!(update_conf_links) + destroy end - def guest_link - return guest_hearing_link if !guest_hearing_link.to_s.empty? + private - if !alias_name.nil? - link_service = VirtualHearings::LinkService.new(alias_name) - update!(guest_hearing_link: link_service.guest_link) - elsif !alias_with_host.nil? - link_service = VirtualHearings::LinkService.new(alias_with_host.split("@")[0].split("A")[1]) - update!(guest_hearing_link: link_service.guest_link, alias: link_service.get_conference_id) - end - guest_hearing_link + # Purpose: Updates conference_link attributes when passed into the 'update!' method. + # + # Params: None + # + # Return: Hash that will update the conference_link + def update_conf_links + { + conference_deleted: true, + updated_by_id: RequestStore[:current_user] = User.system_user, + updated_at: Time.zone.now + } end - private - - def generate_links_and_pins - Rails.logger.info( - "Trying to create conference links for Hearing Day Id: #{hearing_day_id}." - ) - begin - link_service = VirtualHearings::LinkService.new - update!( - alias: link_service.get_conference_id, - host_link: link_service.host_link, - host_pin_long: link_service.host_pin, - alias_with_host: link_service.alias_with_host, - guest_hearing_link: link_service.guest_link, - guest_pin_long: link_service.guest_pin - ) - rescue VirtualHearings::LinkService::PINKeyMissingError, - VirtualHearings::LinkService::URLHostMissingError, - VirtualHearings::LinkService::URLPathMissingError => error - Raven.capture_exception(error: error) - raise error - end + def generate_conference_information + fail NotImplementedError end end diff --git a/app/models/hearings/forms/base_hearing_update_form.rb b/app/models/hearings/forms/base_hearing_update_form.rb index 259b4acf547..83887aa39ed 100644 --- a/app/models/hearings/forms/base_hearing_update_form.rb +++ b/app/models/hearings/forms/base_hearing_update_form.rb @@ -128,14 +128,24 @@ def start_async_job? end def start_async_job + # If converting hearing from virtual to non-virtual if start_async_job? && virtual_hearing_cancelled? perform_later_or_now(VirtualHearings::DeleteConferencesJob) + maybe_start_activate_non_virtual_job + # If converting hearing from non-virtual to virtual elsif start_async_job? - start_activate_job + start_activate_virtual_job end end - def start_activate_job + # If a Webex hearing, activate new Webex conference links when converting from virtual to non-virtual + def maybe_start_activate_non_virtual_job + return unless hearing.conference_provider == "webex" + + perform_later_or_now(Hearings::CreateNonVirtualConferenceJob, hearing: hearing) + end + + def start_activate_virtual_job hearing.virtual_hearing.establishment.submit_for_processing! job_args = { diff --git a/app/models/hearings/pexip_conference_link.rb b/app/models/hearings/pexip_conference_link.rb new file mode 100644 index 00000000000..47b4b6455d0 --- /dev/null +++ b/app/models/hearings/pexip_conference_link.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class PexipConferenceLink < ConferenceLink + class << self + def client_host_or_default + ENV["VIRTUAL_HEARING_URL_HOST"] || "care.evn.va.gov" + end + + def formatted_alias(alias_name) + "BVA#{alias_name}@#{client_host_or_default}" + end + + def base_url + "https://#{client_host_or_default}/bva-app/" + end + end + + # Override the host pin + def host_pin + host_pin_long || self[:host_pin] + end + + def host_link + "#{self.class.base_url}?join=1&media=&escalate=1&" \ + "conference=#{alias_with_host}&" \ + "pin=#{host_pin}&role=host" + end + + def guest_pin + return guest_pin_long if !guest_pin_long.nil? + + link_service = VirtualHearings::PexipLinkService.new + update!(guest_pin_long: link_service.guest_pin) + guest_pin_long + end + + def guest_link + return guest_hearing_link if !guest_hearing_link.to_s.empty? + + if !alias_name.nil? + link_service = VirtualHearings::PexipLinkService.new(alias_name) + update!(guest_hearing_link: link_service.guest_link) + elsif !alias_with_host.nil? + link_service = VirtualHearings::PexipLinkService.new(alias_with_host.split("@")[0].split("A")[1]) + update!(guest_hearing_link: link_service.guest_link, alias: link_service.get_conference_id) + end + guest_hearing_link + end + + private + + # :reek:FeatureEnvy + def generate_conference_information + Rails.logger.info( + "Trying to create a Pexip conference link for Hearing Day Id: #{hearing_day_id}." + ) + begin + link_service = VirtualHearings::PexipLinkService.new + update!( + alias: link_service.get_conference_id, + host_link: link_service.host_link, + host_pin_long: link_service.host_pin, + alias_with_host: link_service.alias_with_host, + guest_hearing_link: link_service.guest_link, + guest_pin_long: link_service.guest_pin + ) + rescue VirtualHearings::PexipLinkService::PINKeyMissingError, + VirtualHearings::PexipLinkService::URLHostMissingError, + VirtualHearings::PexipLinkService::URLPathMissingError => error + Raven.capture_exception(error: error) + raise error + end + end +end diff --git a/app/models/hearings/transcription_file.rb b/app/models/hearings/transcription_file.rb new file mode 100644 index 00000000000..6342e0f8fc6 --- /dev/null +++ b/app/models/hearings/transcription_file.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +class TranscriptionFile < CaseflowRecord + belongs_to :hearing, polymorphic: true + + belongs_to :transcription + belongs_to :docket + + VALID_FILE_TYPES = %w[mp3 mp4 vtt rtf xls csv].freeze + + validates :file_type, inclusion: { in: VALID_FILE_TYPES, message: "'%s' is not valid" } + + # Purpose: Fetches file from S3 + # Return: The temporary save location of the file + def fetch_file_from_s3! + S3Service.fetch_file(aws_link, tmp_location) + tmp_location + end + + # Purpose: Uploads transcription file to its corresponding location in S3 + def upload_to_s3! + TranscriptionFileUpload.new(self).call + end + + # Purpose: Converts transcription file from vtt to rtf + # + # Returns: string, tmp location of rtf (or xls/csv file if error) + def convert_to_rtf! + return unless file_type == "vtt" + + hearing_info = { + judge: hearing.judge&.full_name, + appeal_id: hearing.appeal&.veteran_file_number, + date: hearing.scheduled_for + } + file_paths = TranscriptionTransformer.new(tmp_location, hearing_info).call + update_status!(process: :conversion, status: :success) + file_paths + rescue TranscriptionTransformer::FileConversionError => error + update_status!(process: :conversion, status: :failure) + raise error, error.message + end + + # Purpose: Maps file handling process with associated field to update + DATE_FIELDS = { + retrieval: :date_receipt_webex, + upload: :date_upload_aws, + conversion: :date_converted + }.freeze + + # Purpose: Updates statue of transcription file after completion of process. If process was success, updates + # associated date field on record. + # + # Params: process - symbol, used to map process with associated file status and date field + # status - symbol, either :success or :failure + # aws_link - string, optional argument of AWS S3 location + # + # Returns: Updated transcription file record + def update_status!(process:, status:, upload_link: nil) + params = { + file_status: Constants.TRANSCRIPTION_FILE_STATUSES.send(process).send(status), + updated_by_id: RequestStore[:current_user].id + } + params[:aws_link] = upload_link if upload_link + params[DATE_FIELDS[process]] = Time.zone.now if status == :success + update!(params) + end + + # Purpose: Location of temporary file in tmp/transcription_files/ folder + # + # Returns: string, folder path + def tmp_location + File.join(Rails.root, "tmp", "transcription_files", file_type, file_name) + end + + # Purpose: Removes temporary file (if it exists) from corresponding tmp folder + # + # Returns: integer value of 1 if file deleted, nil if file not found + def clean_up_tmp_location + File.delete(tmp_location) if File.exist?(tmp_location) + end +end diff --git a/app/models/hearings/virtual_hearing.rb b/app/models/hearings/virtual_hearing.rb index 378f3bdfd36..d58d2346284 100644 --- a/app/models/hearings/virtual_hearing.rb +++ b/app/models/hearings/virtual_hearing.rb @@ -37,6 +37,7 @@ class NoAliasWithHostPresentError < StandardError; end class LinkMismatchError < StandardError; end include UpdatedByUserConcern + include ConferenceableConcern class << self def client_host_or_default @@ -67,8 +68,7 @@ def base_url lambda { joins(:establishment) .where(" - conference_deleted = false AND - conference_id IS NOT NULL AND ( + conference_deleted = false AND ( request_cancelled = true OR virtual_hearing_establishments.processed_at IS NOT NULL )") @@ -165,26 +165,36 @@ def host_pin def guest_link return guest_hearing_link if guest_hearing_link.present? - "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ - "conference=#{formatted_alias_or_alias_with_host}&" \ - "pin=#{guest_pin}&role=guest" + if conference_provider == "pexip" + "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ + "conference=#{formatted_alias_or_alias_with_host}&" \ + "pin=#{guest_pin}&role=guest" + end + end + + def co_host_hearing_link + self[:co_host_hearing_link] end def host_link return host_hearing_link if host_hearing_link.present? - "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ - "conference=#{formatted_alias_or_alias_with_host}&" \ - "pin=#{host_pin}&role=host" + if conference_provider == "pexip" + "#{VirtualHearing.base_url}?join=1&media=&escalate=1&" \ + "conference=#{formatted_alias_or_alias_with_host}&" \ + "pin=#{host_pin}&role=host" + end end def test_link(title) + return "https://instant-usgov.webex.com/mediatest" if conference_provider == "webex" + if use_vc_test_link? if ENV["VIRTUAL_HEARING_URL_HOST"].blank? - fail(VirtualHearings::LinkService::URLHostMissingError, message: COPY::URL_HOST_MISSING_ERROR_MESSAGE) + fail(VirtualHearings::PexipLinkService::URLHostMissingError, message: COPY::URL_HOST_MISSING_ERROR_MESSAGE) end if ENV["VIRTUAL_HEARING_URL_PATH"].blank? - fail(VirtualHearings::LinkService::URLPathMissingError, message: COPY::URL_PATH_MISSING_ERROR_MESSAGE) + fail(VirtualHearings::PexipLinkService::URLPathMissingError, message: COPY::URL_PATH_MISSING_ERROR_MESSAGE) end host_and_path = "#{ENV['VIRTUAL_HEARING_URL_HOST']}#{ENV['VIRTUAL_HEARING_URL_PATH']}" @@ -215,7 +225,7 @@ def pending? # Determines if the hearing conference has been created def active? # the conference has been created the virtual hearing is active - conference_id.present? || (guest_hearing_link.present? && host_hearing_link.present?) + guest_hearing_link.present? && host_hearing_link.present? end # Determines if the conference was deleted @@ -225,7 +235,7 @@ def active? # require us to delete the conference but not set `request_cancelled`. def closed? # the conference has been created the virtual hearing was deleted - conference_id.present? && conference_deleted? + conference_deleted? end # Determines the status of the Virtual Hearing based on the establishment @@ -264,7 +274,7 @@ def rebuild_and_save_links fail NoAliasWithHostPresentError if alias_with_host.blank? conference_id = alias_with_host[/BVA(\d+)@/, 1] - link_service = VirtualHearings::LinkService.new(conference_id) + link_service = VirtualHearings::PexipLinkService.new(conference_id) # confirm that we extracted the conference ID correctly, # and that the original link was generated with the link service @@ -277,6 +287,19 @@ def rebuild_and_save_links update!(host_hearing_link: link_service.host_link, guest_hearing_link: link_service.guest_link) end + # :reek:FeatureEnvy + def subject_for_conference + "#{hearing.docket_number}_#{hearing.id}_#{hearing.class}" + end + + def nbf + hearing.scheduled_for.beginning_of_day.to_i + end + + def exp + hearing.scheduled_for.end_of_day.to_i + end + private def assign_created_by_user diff --git a/app/models/hearings/webex_conference_link.rb b/app/models/hearings/webex_conference_link.rb new file mode 100644 index 00000000000..c4ab9c82447 --- /dev/null +++ b/app/models/hearings/webex_conference_link.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class WebexConferenceLink < ConferenceLink + include WebexConcern + + def guest_pin + nil + end + + def guest_link + guest_hearing_link + end + + private + + def generate_conference_information + meeting_type.update!(service_name: "webex") + + conference_response = WebexService.new(instant_connect_config).create_conference(hearing) + + update!( + host_link: conference_response.host_link, + co_host_link: conference_response.co_host_link, + guest_hearing_link: conference_response.guest_link + ) + end +end diff --git a/app/models/legacy_hearing.rb b/app/models/legacy_hearing.rb index ab137d2fa8b..ab86f4c0914 100644 --- a/app/models/legacy_hearing.rb +++ b/app/models/legacy_hearing.rb @@ -35,6 +35,7 @@ class LegacyHearing < CaseflowRecord include UpdatedByUserConcern include HearingConcern include HasHearingEmailRecipientsConcern + include ConferenceableConcern # VA Notify Hooks prepend HearingScheduled @@ -75,6 +76,7 @@ class LegacyHearing < CaseflowRecord has_one :hearing_location, as: :hearing has_many :email_events, class_name: "SentHearingEmailEvent", foreign_key: :hearing_id has_many :email_recipients, class_name: "HearingEmailRecipient", foreign_key: :hearing_id + has_many :transcription_files, as: :hearing alias_attribute :location, :hearing_location accepts_nested_attributes_for :hearing_location, reject_if: proc { |attributes| attributes.blank? } @@ -368,6 +370,10 @@ def vacols_hearing_exists? end end + def daily_docket_conference_link + hearing_day.conference_link + end + # The scheduled time for a legacy hearing after it have been retrieved from VACOLS and processed for time zone. # # @return [Time] a Time object in the calculated time zone and DST offset diff --git a/app/models/meeting_type.rb b/app/models/meeting_type.rb new file mode 100644 index 00000000000..be1518480c6 --- /dev/null +++ b/app/models/meeting_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +## +# A class to repreesnt a polymorphic join table that the allow for the use of the +# conferenceable association. +# +# Any model that includes a conferenceable assoication with end up with record in this table. +# +# The service_name pertains to which video conferencing service an entity is assigned to use. + +class MeetingType < CaseflowRecord + belongs_to :conferenceable, polymorphic: true + + enum service_name: { pexip: 0, webex: 1 } + + scope :pexip, -> { where(service_name: "pexip") } + scope :webex, -> { where(service_name: "webex") } + + alias_attribute :conference_provider, :service_name +end diff --git a/app/models/organizations_user.rb b/app/models/organizations_user.rb index 189d98de647..caf892a125a 100644 --- a/app/models/organizations_user.rb +++ b/app/models/organizations_user.rb @@ -28,6 +28,15 @@ def remove_admin_rights_from_user(user, organization) existing_record(user, organization)&.update!(admin: false) end + def update_user_conference_provider(user, new_service_name) + # This could be an upsert once we get to Rails 6 + if user.meeting_type + user.meeting_type.update!(service_name: new_service_name) + else + MeetingType.create!(service_name: new_service_name, conferenceable: user) + end + end + def remove_user_from_organization(user, organization) if user_is_judge_of_team?(user, organization) fail Caseflow::Error::ActionForbiddenError, message: COPY::JUDGE_TEAM_REMOVE_JUDGE_ERROR diff --git a/app/models/serializers/work_queue/administered_user_serializer.rb b/app/models/serializers/work_queue/administered_user_serializer.rb index 61b86097292..dffbb0f75a4 100644 --- a/app/models/serializers/work_queue/administered_user_serializer.rb +++ b/app/models/serializers/work_queue/administered_user_serializer.rb @@ -11,4 +11,5 @@ class WorkQueue::AdministeredUserSerializer < WorkQueue::UserSerializer params[:organization].dvc&.eql?(object) end end + attribute :conference_provider end diff --git a/app/models/tasks/assign_hearing_disposition_task.rb b/app/models/tasks/assign_hearing_disposition_task.rb index a501e32e7d9..d0f218535ee 100644 --- a/app/models/tasks/assign_hearing_disposition_task.rb +++ b/app/models/tasks/assign_hearing_disposition_task.rb @@ -207,7 +207,7 @@ def reschedule( elsif email_recipients_attributes.present? create_or_update_email_recipients(new_hearing, email_recipients_attributes) end - + new_hearing.maybe_create_non_virtual_conference [new_hearing_task, self.class.create_assign_hearing_disposition_task!(appeal, new_hearing_task, new_hearing)] end end diff --git a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb index 1d898787a6c..4815fe61deb 100644 --- a/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb +++ b/app/models/tasks/hearing_mail_tasks/hearing_postponement_request_mail_task.rb @@ -142,9 +142,8 @@ def reschedule( disposition_task = AssignHearingDispositionTask .create_assign_hearing_disposition_task!(appeal, new_hearing_task, new_hearing) - + new_hearing.maybe_create_non_virtual_conference AppellantNotification.notify_appellant(appeal, Constants.EVENT_TYPE_FILTERS.hearing_scheduled) - [new_hearing_task, disposition_task] end end diff --git a/app/models/tasks/schedule_hearing_task.rb b/app/models/tasks/schedule_hearing_task.rb index da3962cba04..e8589018c98 100644 --- a/app/models/tasks/schedule_hearing_task.rb +++ b/app/models/tasks/schedule_hearing_task.rb @@ -202,6 +202,7 @@ def create_schedule_hearing_tasks(params) # Create and assign the hearing now that it has been scheduled created_tasks << AssignHearingDispositionTask.create_assign_hearing_disposition_task!(appeal, parent, hearing) + hearing.maybe_create_non_virtual_conference # The only other option is to cancel the schedule hearing task elsif params[:status] == Constants.TASK_STATUSES.cancelled # If we are cancelling the schedule hearing task, we need to withdraw the request diff --git a/app/models/user.rb b/app/models/user.rb index 43d9eab9662..d218be4b0fc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,6 +2,7 @@ class User < CaseflowRecord # rubocop:disable Metrics/ClassLength include BgsService + include ConferenceableConcern include EventConcern has_many :dispatch_tasks, class_name: "Dispatch::Task" diff --git a/app/serializers/hearings/conference_link_serializer.rb b/app/serializers/hearings/conference_link_serializer.rb index 3797eae384d..5cc550e1a20 100644 --- a/app/serializers/hearings/conference_link_serializer.rb +++ b/app/serializers/hearings/conference_link_serializer.rb @@ -7,4 +7,7 @@ class ConferenceLinkSerializer attribute :alias, &:alias_with_host attribute :guest_pin, &:guest_pin attribute :guest_link, &:guest_link + attribute :co_host_link, &:co_host_link + attribute :type + attribute :conference_provider end diff --git a/app/serializers/hearings/hearing_day_serializer.rb b/app/serializers/hearings/hearing_day_serializer.rb index 905578e02fd..478b672f653 100644 --- a/app/serializers/hearings/hearing_day_serializer.rb +++ b/app/serializers/hearings/hearing_day_serializer.rb @@ -95,7 +95,8 @@ def self.serialize_collection(hearing_days) filled_slots_count_for_days: filled_slots_count_for_days, judge_names: judge_names } - ).serializable_hash[:data].map { |hearing_day| hearing_day[:attributes] } + ).serializable_hash[:data] + .pluck(:attributes) end def self.serialize_conference_link(conference_link) diff --git a/app/serializers/hearings/hearing_serializer.rb b/app/serializers/hearings/hearing_serializer.rb index d6e3f808694..5bb7dcd5652 100644 --- a/app/serializers/hearings/hearing_serializer.rb +++ b/app/serializers/hearings/hearing_serializer.rb @@ -4,6 +4,9 @@ class HearingSerializer include FastJsonapi::ObjectSerializer include HearingSerializerBase + attribute :daily_docket_conference_link do |hearing| + HearingDaySerializer.serialize_conference_link(hearing.daily_docket_conference_link) + end attribute :aod, &:aod? attribute :advance_on_docket_motion do |hearing| if hearing.aod? @@ -44,6 +47,7 @@ class HearingSerializer attribute :contested_claim do |hearing| hearing.appeal.contested_claim? end + attribute :conference_provider attribute :mst do |hearing| hearing.appeal.mst? end @@ -63,6 +67,11 @@ class HearingSerializer attribute :judge_id attribute :location attribute :military_service, if: for_full + attribute :non_virtual_conference_link do |object| + if !object.non_virtual_conference_link.nil? + ConferenceLinkSerializer.new(object.non_virtual_conference_link).serializable_hash[:data][:attributes] + end + end attribute :notes attribute :paper_case do false @@ -92,6 +101,11 @@ class HearingSerializer attribute :transcript_requested attribute :transcript_sent_date attribute :transcription + attribute :transcription_files, if: for_worksheet do |hearing| + if hearing.conference_provider == "webex" + hearing.serialized_transcription_files + end + end attribute :uuid attribute :veteran_age, if: for_full attribute :veteran_file_number diff --git a/app/serializers/hearings/legacy_hearing_serializer.rb b/app/serializers/hearings/legacy_hearing_serializer.rb index 8abb72a7d22..536abeb5579 100644 --- a/app/serializers/hearings/legacy_hearing_serializer.rb +++ b/app/serializers/hearings/legacy_hearing_serializer.rb @@ -37,7 +37,11 @@ class LegacyHearingSerializer attribute :contested_claim do |hearing| hearing.appeal.contested_claim end + attribute :conference_provider attribute :current_issue_count + attribute :daily_docket_conference_link do |hearing| + HearingDaySerializer.serialize_conference_link(hearing.daily_docket_conference_link) + end attribute :disposition attribute :disposition_editable attribute :docket_name @@ -58,6 +62,11 @@ class LegacyHearingSerializer attribute :judge_id attribute :location attribute :military_service, if: for_worksheet + attribute :non_virtual_conference_link do |object| + if !object.non_virtual_conference_link.nil? + ConferenceLinkSerializer.new(object.non_virtual_conference_link).serializable_hash[:data][:attributes] + end + end attribute :notes attribute :paper_case do |object| object.appeal.paper_case? @@ -84,6 +93,11 @@ class LegacyHearingSerializer attribute :submission_window_end, if: for_worksheet, &:calculate_submission_window attribute :summary attribute :transcript_requested + attribute :transcription_files, if: for_worksheet do |hearing| + if hearing.conference_provider == "webex" + hearing.serialized_transcription_files + end + end attribute :user_id attribute :vacols_id, if: for_worksheet attribute :vbms_id diff --git a/app/serializers/hearings/transcription_file_serializer.rb b/app/serializers/hearings/transcription_file_serializer.rb new file mode 100644 index 00000000000..2f13cd94492 --- /dev/null +++ b/app/serializers/hearings/transcription_file_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class TranscriptionFileSerializer + include FastJsonapi::ObjectSerializer + + attribute :id + attribute :docket_number + attribute :hearing_type + attribute :date_upload_aws + attribute :file_name + attribute :file_status + attribute :file_type +end diff --git a/app/serializers/hearings/virtual_hearing_serializer.rb b/app/serializers/hearings/virtual_hearing_serializer.rb index a3803336ccd..e4c55ba66f9 100644 --- a/app/serializers/hearings/virtual_hearing_serializer.rb +++ b/app/serializers/hearings/virtual_hearing_serializer.rb @@ -14,5 +14,7 @@ class VirtualHearingSerializer attribute :guest_pin attribute :host_link, &:host_link attribute :guest_link, &:guest_link + attribute :co_host_link, &:co_host_hearing_link attribute :job_completed, &:job_completed? + attribute :conference_provider end diff --git a/app/services/external_api/pexip_service.rb b/app/services/external_api/pexip_service.rb index 6cb26d61704..5f47d40d5fd 100644 --- a/app/services/external_api/pexip_service.rb +++ b/app/services/external_api/pexip_service.rb @@ -13,7 +13,12 @@ def initialize(host:, port: 443, user_name:, password:, client_host:) @client_host = client_host end - def create_conference(host_pin:, guest_pin:, name:) + # :reek:FeatureEnvy + def create_conference(virtual_hearing) + host_pin = virtual_hearing.host_pin + guest_pin = virtual_hearing.guest_pin + name = virtual_hearing.alias + body = { "aliases": [{ "alias": "BVA#{name}" }, { "alias": VirtualHearing.formatted_alias(name) }, { "alias": name }], "allow_guests": true, @@ -35,18 +40,28 @@ def create_conference(host_pin:, guest_pin:, name:) ExternalApi::PexipService::CreateResponse.new(resp) end - def delete_conference(conference_id:) - return if conference_id.nil? + def delete_conference(virtual_hearing) + if virtual_hearing.conference_id.nil? + return ExternalApi::PexipService::DeleteResponse.new(not_found_response) + end - delete_endpoint = "#{CONFERENCES_ENDPOINT}#{conference_id}/" + delete_endpoint = "#{CONFERENCES_ENDPOINT}#{virtual_hearing.conference_id}/" resp = send_pexip_request(delete_endpoint, :delete) - return if resp.nil? + return lack_of_connectivity_response if resp.nil? ExternalApi::PexipService::DeleteResponse.new(resp) end + def not_found_response + HTTPI::Response.new(404, {}, {}) + end + private + def lack_of_connectivity_response + HTTPI::Response.new(503, {}, {}) + end + attr_reader :host, :port, :user_name, :password, :client_host # :nocov: diff --git a/app/services/external_api/pexip_service/response.rb b/app/services/external_api/pexip_service/response.rb index bcb91028f96..bb987aecf97 100644 --- a/app/services/external_api/pexip_service/response.rb +++ b/app/services/external_api/pexip_service/response.rb @@ -21,6 +21,7 @@ def success? private # :nocov: + # rubocop:disable Metrics/CyclomaticComplexity def check_for_error return if success? @@ -34,10 +35,15 @@ def check_for_error Caseflow::Error::PexipNotFoundError.new(code: code, message: msg) when 405 Caseflow::Error::PexipMethodNotAllowedError.new(code: code, message: msg) + when 503 + Caseflow::Error::PexipServiceNotReachableError.new(code: code, message: "Pexip Service is currently not + available") else + Caseflow::Error::PexipApiError.new(code: code, message: msg) end end + # rubocop:enable Metrics/CyclomaticComplexity def error_message return "No error message from Pexip" if resp.raw_body.empty? diff --git a/app/services/external_api/webex_service.rb b/app/services/external_api/webex_service.rb new file mode 100644 index 00000000000..5d44dea3406 --- /dev/null +++ b/app/services/external_api/webex_service.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require "json" + +# This file defines the ExternalApi::WebexService class, which is responsible for interacting +# with the Webex API. This service is used for creating and deleting conferences, refreshing +# access tokens, and fetching recording details. +# +# Key behaviors include: +# 1. The initialize method sets up the service with necessary parameters like host, port, aud, +# apikey, domain, api_endpoint, and query. +# 2. The create_conference method sends a POST request to the Webex API to create a new conference. +# 3. The delete_conference method sends a POST request to the Webex API to delete a conference. +# 4. The refresh_access_token method sends a POST request to the Webex API to refresh the access token. +# 5. The fetch_recordings_list method sends a GET request to the Webex API to fetch a list of recordings. +# 6. The fetch_recording_details method sends a GET request to the Webex API to fetch details of a recording. +# 7. The send_webex_request method is a private method used to send requests to the Webex API +# with the specified body and method. +# +# All requests to the Webex API are recorded using the MetricsService. +class ExternalApi::WebexService + # rubocop:disable Metrics/ParameterLists + def initialize(host:, port:, aud:, apikey:, domain:, api_endpoint:, query: nil) + @host = host + @port = port + @aud = aud + @apikey = apikey + @domain = domain + @api_endpoint = api_endpoint + @query = query + end + # rubocop:enable Metrics/ParameterLists + + def self.access_token + CredStash.get("webex_#{Rails.deploy_env}_access_token") + end + + def create_conference(conferenced_item) + body = { + "jwt": { + "sub": conferenced_item.subject_for_conference, + "nbf": conferenced_item.nbf, + "exp": conferenced_item.exp + }, + "aud": @aud, + "numHost": 2, + "provideShortUrls": true, + "verticalType": "gen" + } + method = "POST" + ExternalApi::WebexService::CreateResponse.new(send_webex_request(body, method)) + end + + def delete_conference(conferenced_item) + body = { + "jwt": { + "sub": conferenced_item.subject_for_conference, + "nbf": 0, + "exp": 0 + }, + "aud": @aud, + "numHost": 2, + "provideShortUrls": true, + "verticalType": "gen" + } + method = "POST" + ExternalApi::WebexService::DeleteResponse.new(send_webex_request(body, method)) + end + + # Purpose: Refreshing the access token to access the API + # Return: The response body + def refresh_access_token + url = URI::DEFAULT_PARSER.escape("https://#{@host}#{@domain}#{@api_endpoint}access_token") + + body = { + grant_type: "refresh_token", + client_id: ENV["WEBEX_CLIENT_ID"], + client_secret: ENV["WEBEX_CLIENT_SECRET"], + refresh_token: CredStash.get("webex_#{Rails.deploy_env}_refresh_token") + } + + headers = { + "Content-Type" => "application/x-www-form-urlencoded", + "Accept" => "application/json", + "Authorization" => CredStash.get("webex_#{Rails.deploy_env}_access_token") + } + + request = HTTPI::Request.new + request.url = url + request.body = URI.encode_www_form(body) + request.headers = headers + + response = HTTPI.post(request) + + ExternalApi::WebexService::AccessTokenRefreshResponse.new(response) + end + + def fetch_recordings_list + body = nil + method = "GET" + @api_endpoint += "admin/recordings" + ExternalApi::WebexService::RecordingsListResponse.new(send_webex_request(body, method)) + end + + def fetch_recording_details(recording_id) + body = nil + method = "GET" + @api_endpoint += "recordings/#{recording_id}" + ExternalApi::WebexService::RecordingDetailsResponse.new(send_webex_request(body, method)) + end + + def fetch_rooms_list + body = nil + method = "GET" + @api_endpoint += "rooms" + ExternalApi::WebexService::RoomsListResponse.new(send_webex_request(body, method)) + end + + def fetch_room_details(room_id) + body = nil + method = "GET" + @api_endpoint += "rooms/#{room_id}/meetingInfo" + ExternalApi::WebexService::RoomDetailsResponse.new(send_webex_request(body, method)) + end + + private + + # :nocov: + # rubocop:disable Metrics/MethodLength + def send_webex_request(body, method) + url = "https://#{@host}#{@domain}#{@api_endpoint}" + request = HTTPI::Request.new(url) + request.open_timeout = 300 + request.read_timeout = 300 + request.body = body.to_json unless body.nil? + request.query = @query + request.headers = { "Authorization": "Bearer #{@apikey}", "Content-Type": "application/json" } + + MetricsService.record( + "#{@host} #{method} request to #{url}", + service: :webex, + name: @api_endpoint + ) do + case method + when "POST" + response = HTTPI.post(request) + fail ExternalApi::WebexService::Response.new(response).error if response.error? + + response + when "GET" + response = HTTPI.get(request) + fail ExternalApi::WebexService::Response.new(response).error if response.error? + + response + else + fail NotImplementedError + end + end + end + # rubocop:enable Metrics/MethodLength + # :nocov: +end diff --git a/app/services/external_api/webex_service/access_token_refresh_response.rb b/app/services/external_api/webex_service/access_token_refresh_response.rb new file mode 100644 index 00000000000..f82c1ddcc26 --- /dev/null +++ b/app/services/external_api/webex_service/access_token_refresh_response.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::AccessTokenRefreshResponse < ExternalApi::WebexService::Response + def access_token + data["access_token"] + end + + def refresh_token + data["refresh_token"] + end +end diff --git a/app/services/external_api/webex_service/create_response.rb b/app/services/external_api/webex_service/create_response.rb new file mode 100644 index 00000000000..2e45b8152a5 --- /dev/null +++ b/app/services/external_api/webex_service/create_response.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::CreateResponse < ExternalApi::WebexService::Response + def base_url + data["baseUrl"] + end + + def host_link + "#{base_url}#{data.dig('host', 0, 'short')}" + end + + def co_host_link + "#{base_url}#{data.dig('host', 1, 'short')}" + end + + def guest_link + "#{base_url}#{data.dig('guest', 0, 'short')}" + end +end diff --git a/app/services/external_api/webex_service/delete_response.rb b/app/services/external_api/webex_service/delete_response.rb new file mode 100644 index 00000000000..5c8be1f2f1c --- /dev/null +++ b/app/services/external_api/webex_service/delete_response.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::DeleteResponse < ExternalApi::WebexService::Response; end diff --git a/app/services/external_api/webex_service/recording_details_response.rb b/app/services/external_api/webex_service/recording_details_response.rb new file mode 100644 index 00000000000..e8ab5afb393 --- /dev/null +++ b/app/services/external_api/webex_service/recording_details_response.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RecordingDetailsResponse < ExternalApi::WebexService::Response + def mp4_link + data["temporaryDirectDownloadLinks"]["recordingDownloadLink"] + end + + def vtt_link + data["temporaryDirectDownloadLinks"]["transcriptDownloadLink"] + end + + def mp3_link + data["temporaryDirectDownloadLinks"]["audioDownloadLink"] + end + + def topic + data["topic"] + end +end diff --git a/app/services/external_api/webex_service/recordings_list_response.rb b/app/services/external_api/webex_service/recordings_list_response.rb new file mode 100644 index 00000000000..709480fa488 --- /dev/null +++ b/app/services/external_api/webex_service/recordings_list_response.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RecordingsListResponse < ExternalApi::WebexService::Response + def recordings + return [] if data["items"].blank? + + data["items"].map { |item| Recording.new(item["id"], item["hostEmail"]) } + end + + class Recording + attr_reader :id, :host_email + + # rubocop:disable Naming/MethodParameterName, Naming/VariableName + def initialize(id, hostEmail) + @id = id + @host_email = hostEmail + end + # rubocop:enable Naming/MethodParameterName, Naming/VariableName + end +end diff --git a/app/services/external_api/webex_service/response.rb b/app/services/external_api/webex_service/response.rb new file mode 100644 index 00000000000..5bd77e23247 --- /dev/null +++ b/app/services/external_api/webex_service/response.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::Response + attr_reader :resp, :code + + def initialize(resp) + @resp = resp + @code = @resp.code + end + + def data + JSON.parse(resp.raw_body).with_indifferent_access + end + + def error + check_for_errors + end + + def success? + !resp.error? + end + + private + + def check_for_errors + return false if success? + + msg = error_message + case code + when 400 + Caseflow::Error::WebexBadRequestError.new(code: code, message: msg) + when 501 + Caseflow::Error::WebexApiError.new(code: code, message: msg) + when 404 + Caseflow::Error::WebexNotFoundError.new(code: code, message: msg) + when 405 + Caseflow::Error::WebexMethodNotAllowedError.new(code: code, message: msg) + else + Caseflow::Error::WebexApiError.new(code: code, message: msg) + end + end + + def error_message + return "No error message from Webex" if resp.raw_body.empty? + + begin + if !invalid_token + { + message: data.dig(:message), + descriptions: data.dig(:errors)&.pluck(:description)&.compact + } + end + data["message"] + rescue JSON::ParserError + "No error message from Webex" + end + end + + def invalid_token + if data["error_description"] == "The access token expired" + fail Caseflow::Error::WebexInvalidTokenError.new( + code: @code, + message: data["error"], + descriptions: data["error_description"] + ) + end + + nil + end +end diff --git a/app/services/external_api/webex_service/room_details_response.rb b/app/services/external_api/webex_service/room_details_response.rb new file mode 100644 index 00000000000..6211420efbf --- /dev/null +++ b/app/services/external_api/webex_service/room_details_response.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RoomDetailsResponse < ExternalApi::WebexService::Response + def meeting_id + data["meetingId"] + end +end diff --git a/app/services/external_api/webex_service/rooms_list_response.rb b/app/services/external_api/webex_service/rooms_list_response.rb new file mode 100644 index 00000000000..b88da04a133 --- /dev/null +++ b/app/services/external_api/webex_service/rooms_list_response.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class ExternalApi::WebexService::RoomsListResponse < ExternalApi::WebexService::Response + def rooms + return [] if data["items"].blank? + + data["items"].map { |item| Room.new(item["id"], item["title"]) } + end + + class Room + attr_reader :id, :title + + def initialize(id, title) + @id = id + @title = title + end + end +end diff --git a/app/services/virtual_hearings/link_service.rb b/app/services/virtual_hearings/pexip_link_service.rb similarity index 97% rename from app/services/virtual_hearings/link_service.rb rename to app/services/virtual_hearings/pexip_link_service.rb index 21439be6865..4ecace901a3 100644 --- a/app/services/virtual_hearings/link_service.rb +++ b/app/services/virtual_hearings/pexip_link_service.rb @@ -5,7 +5,7 @@ ## # Service for generating new guest and host virtual hearings links ## -class VirtualHearings::LinkService +class VirtualHearings::PexipLinkService class PINKeyMissingError < StandardError; end class URLHostMissingError < StandardError; end class URLPathMissingError < StandardError; end diff --git a/app/views/hearing_email_status_mailer/notification.html.erb b/app/views/hearing_email_status_mailer/notification.html.erb index 5b2388c1a53..d71290485c8 100644 --- a/app/views/hearing_email_status_mailer/notification.html.erb +++ b/app/views/hearing_email_status_mailer/notification.html.erb @@ -4,4 +4,4 @@ <%= content_for :instructions do %>

    Please check the email address and <%= external_link hearing_details_url(@hearing), display_text: "update it on the Hearing Details page" %> if it contains a typo or you have an alternate address. If you update the email address and click Save at the bottom of the screen, email will be sent to the updated address.

    -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/hearing_mailer/sections/_how_to_join.html.erb b/app/views/hearing_mailer/sections/_how_to_join.html.erb index 396a58ebd5f..f8fca697590 100644 --- a/app/views/hearing_mailer/sections/_how_to_join.html.erb +++ b/app/views/hearing_mailer/sections/_how_to_join.html.erb @@ -6,4 +6,4 @@ Click on the link below, or copy and paste the link into the address field of your web browser:
    <%= external_link @link %>

    -

    IMPORTANT: You must click on the green video button (icon) to enter the virtual hearing room.

    +

    IMPORTANT: You must click on the video button (icon) to enter the virtual hearing room.

    diff --git a/app/views/layouts/transcription_file_issues_mailer.html.erb b/app/views/layouts/transcription_file_issues_mailer.html.erb new file mode 100644 index 00000000000..c83fd898a0b --- /dev/null +++ b/app/views/layouts/transcription_file_issues_mailer.html.erb @@ -0,0 +1,16 @@ + + + + + + + + + <%= yield %> + <%= yield :intro %> + <%= yield :content %> + <%= yield :signature %> + + diff --git a/app/views/queue/index.html.erb b/app/views/queue/index.html.erb index 8832fea20d7..ef7a6762643 100644 --- a/app/views/queue/index.html.erb +++ b/app/views/queue/index.html.erb @@ -25,6 +25,7 @@ canEditCavcDashboards: current_user.can_edit_cavc_dashboards?, canViewCavcDashboards: current_user.can_view_cavc_dashboards?, userIsCobAdmin: ClerkOfTheBoard.singleton.admins.include?(current_user), + conferenceProvider: current_user.conference_provider, featureToggles: { collect_video_and_central_emails: FeatureToggle.enabled?(:collect_video_and_central_emails, user: current_user), enable_hearing_time_slots: FeatureToggle.enabled?(:enable_hearing_time_slots, user: current_user), @@ -60,6 +61,8 @@ cc_appeal_workflow: FeatureToggle.enabled?(:cc_appeal_workflow, user: current_user), metricsBrowserError: FeatureToggle.enabled?(:metrics_browser_error, user: current_user), cc_vacatur_visibility: FeatureToggle.enabled?(:cc_vacatur_visibility, user: current_user), + conference_selection_visibility: (FeatureToggle.enabled?(:pexip_conference_service) && + FeatureToggle.enabled?(:webex_conference_service)), additional_remand_reasons: FeatureToggle.enabled?(:additional_remand_reasons, user: current_user), acd_cases_tied_to_judges_no_longer_with_board: FeatureToggle.enabled?(:acd_cases_tied_to_judges_no_longer_with_board, user: current_user), admin_case_distribution: FeatureToggle.enabled?(:admin_case_distribution, user: current_user), diff --git a/app/views/transcription_file_issues_mailer/issue_notification.html.erb b/app/views/transcription_file_issues_mailer/issue_notification.html.erb new file mode 100644 index 00000000000..886b42e03cd --- /dev/null +++ b/app/views/transcription_file_issues_mailer/issue_notification.html.erb @@ -0,0 +1,41 @@ +<%= content_for :intro do %> +

    + Attn: +

    +

    + Caseflow attempted to <%= @explanation %> and received a fatal error. +

    +<% end %> + +<%= content_for :content do %> +
      + <% @details.each do |key, value|%> +
    • + <% if value.respond_to?(:has_key?) %> + <% if value[:link] %> + ><%= key.to_s.titlecase %> + <% else %> + <%= key.to_s.titlecase %>: +
        + <% value.each do |k, v| %> +
      • <%= k.to_s.titlecase %>: <%= v %>
      • + <% end%> +
      + <% end %> + <% else %> + <%= key.to_s.titlecase %>: <%= value || "N/A" %> + <% end %> +
    • + <% end %> +
    +<% end %> + +<%= content_for :signature do %> +

    + Please investigate this issue further to ensure <%= @outro %>. Direct any questions to OITAppealsHelpDesk@va.gov. +

    +

    + Sincerely,
    + The Board of Veterans' Appeals +

    +<% end %> diff --git a/app/workflows/transcription_file_upload.rb b/app/workflows/transcription_file_upload.rb new file mode 100644 index 00000000000..19db1c44742 --- /dev/null +++ b/app/workflows/transcription_file_upload.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class TranscriptionFileUpload + attr_reader :file_name, :file_type + + S3_SUB_BUCKET = "vaec-appeals-caseflow" + + S3_SUB_FOLDERS = { + mp3: "transcript_audio", + mp4: "transcript_audio", + vtt: "transcript_raw", + rtf: "transcript_text", + xls: "transcript_text", + csv: "transcript_text" + }.freeze + + class FileUploadError < StandardError; end + + # Params: transcription_file - TranscriptionFile object + def initialize(transcription_file) + @transcription_file = transcription_file + @file_name = @transcription_file.file_name + @file_type = @transcription_file.file_type + @folder_name = (Rails.deploy_env == :prod) ? S3_SUB_BUCKET : "#{S3_SUB_BUCKET}-#{Rails.deploy_env}" + end + + # Purpose: Uploads transcription file to its corresponding location in S3 + def call + S3Service.store_file(s3_location, @transcription_file.tmp_location, :filepath) + @transcription_file.update_status!(process: :upload, status: :success, upload_link: s3_location) + Rails.logger.info("File #{file_name} successfully uploaded to S3 location: #{s3_location}") + rescue StandardError => error + @transcription_file.update_status!(process: :upload, status: :failure) + raise FileUploadError "Amazon S3 service responded with error: #{error}" + end + + private + + # Purpose: Location of uploaded file in s3 + # + # Returns: string, s3 filepath + def s3_location + @folder_name + "/" + S3_SUB_FOLDERS[file_type.to_sym] + "/" + file_name + end +end diff --git a/app/workflows/transcription_transformer.rb b/app/workflows/transcription_transformer.rb new file mode 100644 index 00000000000..999edaba214 --- /dev/null +++ b/app/workflows/transcription_transformer.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require "webvtt" +require "rtf" +require "csv" + +# Workflow for converting VTT transcription files to RTF +class TranscriptionTransformer + class FileConversionError < StandardError; end + + def initialize(vtt_path, hearing_info) + @vtt_path = vtt_path + @error_count = 0 + @hearing_info = hearing_info + @length = 0 + end + + def call + paths = [convert_to_rtf(@vtt_path)] + csv_path = @vtt_path.gsub("vtt", "csv") + if File.exist?(csv_path) + paths.push(csv_path) + elsif @error_count > 0 + error_hash = { + error_count: @error_count, + length: @length, + hearing_info: @hearing_info + } + paths.push(build_csv(csv_path, error_hash)) + end + paths + end + + private + + # Convert vtt file to rtf or csv if there is an error + # Params: path - the file path of the vtt file + # Returns the file path of the newly converted file + def convert_to_rtf(path) + rtf_path = path.gsub("vtt", "rtf") + return rtf_path if File.exist?(rtf_path) + + begin + converted_file = File.open(path, "r") { |io| io.read.encode("UTF-8", invalid: :replace, replace: "...") } + File.open(path, "w") { |file| file.write(converted_file) } + vtt = WebVTT.read(path) + @length = vtt.actual_total_length + doc = RTF::Document.new(RTF::Font.new(RTF::Font::ROMAN, "Times New Roman")) + doc.footer = RTF::FooterNode.new(doc, RTF::FooterNode::UNIVERSAL) + doc.style.left_margin = 1300 + doc.style.right_margin = 1300 + create_cover_page(doc) + doc.page_break + create_transcription_pages(vtt, doc) + raw_doc = create_footer_and_spacing(doc) + File.open(rtf_path, "w") { |file| file.write(raw_doc) } + rtf_path + rescue StandardError + raise FileConversionError + end + end + + # Create cover page + # Params: document - the document object + # Returns the document with the cover page + def create_cover_page(document) + border_width = 40 + document.table(2, 1, 9200) do |table| + table.cell_margin = 30 + header_row = table[0] + header_row.border_width = border_width + header_row.shading_colour = RTF::Colour.new(0, 0, 0) + table[1].border_width = border_width + header_row[0] << " Department of Veterans Affairs" + generate_cover_info(table[1][0]) + end + end + + # Create the text pages on the file + # Params: + # transcript - the original vtt file + # document - the document object + # Returns the document with the transcription pages + def create_transcription_pages(transcript, document) + styles = {} + styles["PS_CODE"] = RTF::ParagraphStyle.new + styles["CS_CODE"] = RTF::CharacterStyle.new + styles["PS_CODE"].line_spacing = -1 + styles["CS_CODE"].underline = true + format_transcript(transcript).each do |cue| + document.paragraph(styles["PS_CODE"]) do |paragraph_style| + paragraph_style.apply(styles["CS_CODE"]) do |char_style| + char_style << cue[:identifier].upcase + end + paragraph_style.paragraph << ": #{cue[:text]}" + paragraph_style.paragraph + end + end + end + + # Format the transcript by consolidating speakers who talk multiple times in a row + # Params: transcript - the original vtt file + # Returns the compressed transcript + def format_transcript(transcript) + compressed_transcript = [] + prev_id = " This is not anyones name." + prev_index = -1 + transcript.cues.each do |cue| + identifier = cue.identifier&.strip&.scan(/[a-zA-Z]+/)&.join(" ") || "" + name = (identifier == "") ? "Unknown" : identifier + if name.match?(/#{prev_id}/) + compressed_transcript[prev_index][:text] += " " + cue.text + else + original_text = cue.text + @error_count += original_text.scan("[...]").size + prev_id = name + compressed_transcript.push(identifier: name, text: original_text) + prev_index += 1 + end + end + + compressed_transcript + end + + # create the footer + # Params: document - the document object + # returns the document with the footer + def create_footer_and_spacing(document) + document.footer << "Insert Veteran's Last Name, First Name, MI, Claim No" + rtf_footer = + "\\footer\\pard\\" + (" " * 47) + "\\chpgn" + (" " * 18) + "Veteran's Last, First, Claim No\\par" + rtf_spacing = "sl120\\slmult1" + raw_rtf = document.to_rtf.sub(document.footer.to_rtf, "{#{rtf_footer}}") + raw_rtf.gsub("sl-1", rtf_spacing) + end + + # streamlines adding line breaks + # Params: + # row - the current row in the document + # count - amount of line breaks to add + def insert_line_breaks(row, count) + breaks = 0 + while breaks < count + row.line_break + breaks += 1 + end + end + + # Params: path - the path to save the csv + # details - hash that has details pertaining to the error + # Returns the created csv + def build_csv(path, details) + filename = path.split("/").last.sub(".csv", "") + header = %w[length appeal_id hearing_date judge issues filename] + length = details[:length] + count = details[:count] + hearing_info = details[:hearing_info] + length_string = "#{(length / 3600).floor}:#{(length / 60 % 60).floor}:#{(length % 60).floor}" + CSV.open(path, "w") do |writer| + writer << header + writer << [length_string, hearing_info[:appeal_id], hearing_info[:date]&.strftime("%m/%d/%Y"), + hearing_info[:judge]&.upcase, "#{count} inaudible", filename] + end + path + end + + # rubocop:disable Metrics/MethodLength + # Generates the template info for the cover page + # Params: row - the table row that the info will be occupying in the doc + # return the modified cover doc + def generate_cover_info(row) + insert_line_breaks(row, 1) + row << " TRANSCRIPT OF HEARING" + insert_line_breaks(row, 2) + row << " BEFORE" + insert_line_breaks(row, 2) + row << " BOARD OF VETERANS' APPEALS" + insert_line_breaks(row, 2) + row << " WASHINGTON, D.C. 20420" + insert_line_breaks(row, 4) + row << " Video Conference at Insert City, State" + insert_line_breaks(row, 4) + row << " IN THE APPEAL OF : Insert Veterans Last Name, First Name, MI" + insert_line_breaks(row, 1) + row << "  Insert Veteran's Claim No" + insert_line_breaks(row, 5) + row << " DATE : Insert Date" + insert_line_breaks(row, 5) + row << " REPRESENTED BY : Insert Name of Representative" + insert_line_breaks(row, 1) + row << " Insert Representative's Organization" + insert_line_breaks(row, 5) + row << " MEMBER OF BOARD : Insert Veterans Law Judge's name, Judge" + insert_line_breaks(row, 5) + row << " WITNESSES :  Insert Full Name of witness, Appellant" + insert_line_breaks(row, 1) + row << "     Insert Full Name of other witnesses, Witness" + insert_line_breaks(row, 5) + end + # rubocop:enable Metrics/MethodLength +end diff --git a/client/COPY.json b/client/COPY.json index bbc5dc80ad9..4c9265d8c23 100644 --- a/client/COPY.json +++ b/client/COPY.json @@ -786,6 +786,7 @@ "USER_MANAGEMENT_GIVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Add admin rights", "USER_MANAGEMENT_REMOVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Remove admin rights", "USER_MANAGEMENT_REMOVE_USER_FROM_ORG_BUTTON_TEXT": "Remove from team", + "USER_MANAGEMENT_SELECT_HEARINGS_CONFERENCE_TYPE": "Schedule hearings using:", "MEMBERSHIP_REQUEST_ACTION_SUCCESS_TITLE": "You successfully %s %s's request", "MEMBERSHIP_REQUEST_ACTION_SUCCESS_MESSAGE": "The user was %s regular member access to %s.", "VHA_MEMBERSHIP_REQUEST_AUTOMATIC_VHA_ACCESS_NOTE": "Note: If you are requesting specialized access and are not a member of the general VHA group, you will automatically be given access to the general VHA group if your request is approved.", @@ -1216,8 +1217,9 @@ "VLJ_VIRTUAL_HEARING_LINK_LABEL": "VLJ Link", "GUEST_VIRTUAL_HEARING_LINK_LABEL": "Guest Link", "REPRESENTATIVE_VIRTUAL_HEARING_LINK_LABEL": "Virtual Hearing Link", - "VLJ_VIRTUAL_HEARINGS_LINK_TEXT": "Start Virtual Hearing", - "GUEST_VIRTUAL_HEARINGS_LINK_TEXT": "Join Virtual Hearing", + "HC_VIRTUAL_HEARING_LINK_LABEL": "Hearing Coordinator Link", + "VLJ_VIRTUAL_HEARINGS_LINK_TEXT": "Start Hearing", + "GUEST_VIRTUAL_HEARINGS_LINK_TEXT": "Join Hearing", "PIN_KEY_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid PIN key", "URL_HOST_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid URL host", "URL_PATH_MISSING_ERROR_MESSAGE": "Cannot generate a virtual hearing URL without a valid URL path", diff --git a/client/app/components/CopyTextButton.jsx b/client/app/components/CopyTextButton.jsx index 77e49f0c3da..85b687ec6d2 100644 --- a/client/app/components/CopyTextButton.jsx +++ b/client/app/components/CopyTextButton.jsx @@ -14,27 +14,32 @@ export const clipboardButtonStyling = (defaults) => padding: '0.75rem', // Offset the additional padding so when this component appears in an unordered list of items its baseline matches. margin: '-0.75rem 0', - overflowWrap: 'break-word' + overflowWrap: 'break-word', }); export default class CopyTextButton extends React.PureComponent { render = () => { const { text, textToCopy, label, styling, ariaLabel } = this.props; - const buttonStyles = isEmpty(styling) ? - { - borderColor: COLORS.GREY_LIGHT, - borderWidth: '1px', + + const buttonStyles = isEmpty(styling) ? { + borderColor: COLORS.GREY_LIGHT, + borderWidth: '1px', + color: COLORS.GREY_DARK, + ':hover': { + backgroundColor: 'transparent', color: COLORS.GREY_DARK, - ':hover': { - backgroundColor: 'transparent', - color: COLORS.GREY_DARK, - borderColor: COLORS.PRIMARY, - borderBottomWidth: '1px' - }, - '& > svg path': { fill: COLORS.GREY_LIGHT }, - '&:hover > svg path': { fill: COLORS.PRIMARY } - } : - styling; + borderColor: COLORS.PRIMARY, + borderBottomWidth: '1px', + }, + ':disabled': { + backgroundColor: COLORS.GREY_BACKGROUND, + borderColor: COLORS.GREY_LIGHT, + color: COLORS.GREY_LIGHT, + borderBottomWidth: '1px', + }, + '& > svg path': { fill: COLORS.GREY_LIGHT }, + '&:hover > svg path': { fill: COLORS.PRIMARY }, + } : styling; return ( @@ -43,6 +48,7 @@ export default class CopyTextButton extends React.PureComponent { type="submit" className="cf-apppeal-id" aria-label={ariaLabel || `Copy ${label} ${text}`} + disabled={textToCopy === null} {...clipboardButtonStyling(buttonStyles)} > {text}  @@ -57,7 +63,7 @@ export default class CopyTextButton extends React.PureComponent { CopyTextButton.defaultProps = { styling: {}, label: '', - textToCopy: null + textToCopy: null, }; CopyTextButton.propTypes = { @@ -77,5 +83,6 @@ CopyTextButton.propTypes = { * If ariaLabel not set, populates the aria-label as `Copy ${label} ${text}` */ label: PropTypes.string, - styling: PropTypes.object + styling: PropTypes.object, + disabled: PropTypes.bool, }; diff --git a/client/app/components/Table.jsx b/client/app/components/Table.jsx index 6bad532a713..7e0c3ea200e 100644 --- a/client/app/components/Table.jsx +++ b/client/app/components/Table.jsx @@ -312,7 +312,7 @@ BodyRows.propTypes = { tbodyRef: PropTypes.func, id: PropTypes.string, getKeyForRow: PropTypes.func, - bodyStyling: PropTypes.array + bodyStyling: PropTypes.object }; FooterRow.propTypes = { @@ -342,5 +342,5 @@ Table.propTypes = { sortAscending: PropTypes.bool }), bodyClassName: PropTypes.string, - bodyStyling: PropTypes.array + bodyStyling: PropTypes.object }; diff --git a/client/app/hearings/components/VirtualHearingLink.jsx b/client/app/hearings/components/VirtualHearingLink.jsx index ee8e6e94697..b0d8f901a91 100644 --- a/client/app/hearings/components/VirtualHearingLink.jsx +++ b/client/app/hearings/components/VirtualHearingLink.jsx @@ -9,22 +9,20 @@ import { ExternalLinkIcon } from '../../components/icons/ExternalLinkIcon'; const ICON_POSITION_FIX = css({ position: 'relative', top: 1 }); const VirtualHearingLink = ({ - isVirtual, newWindow, link, - virtualHearing, - label + label, + hearing }) => { - if (!isVirtual) { - return null; - } - return ( - + {label}   - + ); @@ -47,7 +45,12 @@ VirtualHearingLink.propTypes = { aliasWithHost: PropTypes.string, jobCompleted: PropTypes.bool }).isRequired, - label: PropTypes.string + label: PropTypes.string, + hearing: PropTypes.shape({ + dailyDocketConferenceLink: PropTypes.shape({ + coHostLink: PropTypes.string + }) + }) }; VirtualHearingLink.defaultProps = { diff --git a/client/app/hearings/components/dailyDocket/DailyDocket.jsx b/client/app/hearings/components/dailyDocket/DailyDocket.jsx index a1427027638..e7038ec4078 100644 --- a/client/app/hearings/components/dailyDocket/DailyDocket.jsx +++ b/client/app/hearings/components/dailyDocket/DailyDocket.jsx @@ -22,7 +22,6 @@ import COPY from '../../../../COPY'; import UserAlerts from '../../../components/UserAlerts'; import HEARING_DISPOSITION_TYPES from '../../../../constants/HEARING_DISPOSITION_TYPES'; import { ScheduledInErrorModal } from '../ScheduledInErrorModal'; -import { DailyDocketGuestLinkSection } from './DailyDocketGuestLinkSection'; const alertStyling = css({ marginBottom: '30px', @@ -301,9 +300,6 @@ export default class DailyDocket extends React.Component { )} - {(user.userIsHearingManagement || user.userIsHearingAdmin) && ( - - )} { - - // Conference Link Information - const { alias, guestLink, guestPin } = linkInfo || {}; - - const containerStyle = { - display: 'grid', - gridTemplateColumns: '1fr 1.8fr', - backgroundColor: '#f1f1f1', - padding: '1em 0 0 1em', - marginLeft: '-40px', - marginRight: '-40px' - }; - - const roomInfoContainerStyle = { - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'space-around' - }; - - // Props needed for the copy text button component - const CopyTextButtonProps = { - text: GUEST_LINK_LABELS.COPY_GUEST_LINK, - label: GUEST_LINK_LABELS.COPY_GUEST_LINK, - textToCopy: guestLink - }; - - // Takes pin from guestLink - const usePinFromLink = () => guestLink?.match(/pin=\d+/)[0]?.split('=')[1]; - - // Takes alias from guestLink - const useAliasFromLink = () => guestLink?.split('&')[0]?.match(/conference=.+/)[0]?.split('=')[1]; - - /** - * Render information about the guest link - * @param {conferenceRoom} - The conference link alias - * @param {pin} - The guest pin - * @param {roleAccess} - Boolean for if the current user has access to the guest link - * @returns The room information - */ - const renderRoomInfo = () => { - return ( -
    -

    {GUEST_LINK_LABELS.GUEST_CONFERENCE_ROOM}:{alias || useAliasFromLink()}

    -

    {GUEST_LINK_LABELS.GUEST_PIN}:{usePinFromLink()}#

    -

    -
    - ); - }; - - return ( -
    -

    {GUEST_LINK_LABELS.GUEST_LINK_SECTION_LABEL}

    - {renderRoomInfo(alias, guestPin)} -
    - ); -}; - -DailyDocketGuestLinkSection.propTypes = { - linkInfo: PropTypes.shape({ - guestLink: PropTypes.string, - guestPin: PropTypes.string, - alias: PropTypes.string, - }), -}; diff --git a/client/app/hearings/components/dailyDocket/DailyDocketRow.jsx b/client/app/hearings/components/dailyDocket/DailyDocketRow.jsx index df50c8f68cd..553c19fed6e 100644 --- a/client/app/hearings/components/dailyDocket/DailyDocketRow.jsx +++ b/client/app/hearings/components/dailyDocket/DailyDocketRow.jsx @@ -5,6 +5,7 @@ import { css } from 'glamor'; import PropTypes from 'prop-types'; import React from 'react'; import { isUndefined, isNil, isEmpty, omitBy, get } from 'lodash'; +import StringUtil from 'app/util/StringUtil'; import HEARING_DISPOSITION_TYPES from '../../../../constants/HEARING_DISPOSITION_TYPES'; @@ -273,9 +274,11 @@ class DailyDocketRow extends React.Component { isLegacyHearing = () => this.props.hearing?.docketName === 'legacy'; conferenceLinkOnClick = () => { - const { conferenceLink } = this.props; + const { conferenceLink, hearing } = this.props; - window.open(conferenceLink?.hostLink, 'Recording Session').focus(); + const link = hearing.conferenceProvider === 'webex' ? hearing.nonVirtualConferenceLink : conferenceLink; + + window.open(link.hostLink, 'Recording Session').focus(); } getInputProps = () => { @@ -375,11 +378,18 @@ class DailyDocketRow extends React.Component { return (
    {hearing?.isVirtual && } - {hearing?.isVirtual !== true && userJudgeOrCoordinator(user, hearing) && } + onClick={this.conferenceLinkOnClick} > Connect to Recording System } + {hearing?.isVirtual !== true && hearing?.scheduledForIsPast && userJudgeOrCoordinator(user, hearing) &&
    + + Host Link: N/A +
    } + {
    + {StringUtil.capitalizeFirst(hearing?.conferenceProvider || 'Pexip')} hearing +
    } (
    - Edit Hearing Details + Hearing Details and links
    @@ -350,18 +351,22 @@ PreppedCheckbox.propTypes = { export const StaticVirtualHearing = ({ hearing, user }) => (
    - + { hearing?.scheduledForIsPast ? ( + {virtualHearingScheduledDatePassedLabelFull(virtualHearingRoleForUser(user, hearing))} + ) : ( + + )} {hearing?.virtualHearing?.status === 'pending' && (
    {COPY.VIRTUAL_HEARING_SCHEDULING_IN_PROGRESS} diff --git a/client/app/hearings/components/details/DetailsForm.jsx b/client/app/hearings/components/details/DetailsForm.jsx index c528201c72b..075e28c20ac 100644 --- a/client/app/hearings/components/details/DetailsForm.jsx +++ b/client/app/hearings/components/details/DetailsForm.jsx @@ -117,12 +117,14 @@ const DetailsForm = (props) => { initialRepresentativeTz={initialHearing?.representativeTz} /> - {!isLegacy && ( + {/* Don't render Transcription Details section if Legacy Hearing AND Pexip conference provider*/} + {!(isLegacy && hearing.conferenceProvider === 'pexip') && ( )} @@ -140,7 +142,8 @@ DetailsForm.propTypes = { wasVirtual: PropTypes.bool, isVirtual: PropTypes.bool, scheduledTimeString: PropTypes.string, - readableRequestType: PropTypes.string + readableRequestType: PropTypes.string, + conferenceProvider: PropTypes.string }), initialHearing: PropTypes.shape({ virtualHearing: PropTypes.object diff --git a/client/app/hearings/components/details/HearingLinks.jsx b/client/app/hearings/components/details/HearingLinks.jsx index 4e7d1edd6b9..10f34bc059e 100644 --- a/client/app/hearings/components/details/HearingLinks.jsx +++ b/client/app/hearings/components/details/HearingLinks.jsx @@ -2,7 +2,6 @@ import { css } from 'glamor'; import PropTypes from 'prop-types'; import React from 'react'; -import { COLORS } from '../../../constants/AppConstants'; import { VIRTUAL_HEARING_HOST, virtualHearingRoleForUser } from '../../utils'; import { rowThirds, @@ -20,16 +19,13 @@ export const VirtualHearingLinkDetails = ({ role, link, hearing, - wasVirtual, isVirtual, user, label, virtualHearing }) => ( - {hearing?.scheduledForIsPast || wasVirtual ? ( - Expired - ) : ( + {link ? ( + ) : ( + N/A )} -
    - Conference Room: - {`${aliasWithHost}`} -
    -
    - PIN: - {pin} -
    - {!hearing?.scheduledForIsPast && !wasVirtual && ( - + {hearing.conferenceProvider === 'pexip' ? ( + <> +
    + Conference Room: + {aliasWithHost || 'N/A'} +
    +
    + PIN: + {pin || 'N/A'} +
    + + ) : ( +
    + {link} +
    )} +
    ); @@ -70,13 +78,18 @@ VirtualHearingLinkDetails.propTypes = { }; export const LinkContainer = ( - { link, linkText, user, hearing, isVirtual, wasVirtual, virtualHearing, role, label } -) => ( -
    - {label}: - {!virtualHearing || virtualHearing?.status === 'pending' ? ( - {COPY.VIRTUAL_HEARING_SCHEDULING_IN_PROGRESS} - ) : ( + { link, linkText, user, hearing, isVirtual, wasVirtual, virtualHearing, role, label, links } +) => { + // The pin used depends on the role and link used depends on virtual or not + const getPin = () => { + const isPexipHearingCoordinator = (hearing.conferenceProvider === 'pexip' && role === 'HC'); + + return (role === 'VLJ' || isPexipHearingCoordinator) ? links?.hostPin : links?.guestPin; + }; + + return ( +
    + {label}: - )} -
    -); +
    + ); +}; LinkContainer.propTypes = { hearing: PropTypes.object, @@ -102,41 +115,75 @@ LinkContainer.propTypes = { role: PropTypes.string, user: PropTypes.object, virtualHearing: PropTypes.object, - wasVirtual: PropTypes.bool + wasVirtual: PropTypes.bool, + links: PropTypes.object }; -export const HearingLinks = ({ hearing, virtualHearing, isVirtual, wasVirtual, user }) => { - if (!isVirtual && !wasVirtual) { - return null; - } - +export const HearingLinks = ({ hearing, virtualHearing, isVirtual, wasVirtual, user, isCancelled }) => { + const { + scheduledForIsPast, + conferenceProvider, + dailyDocketConferenceLink, + nonVirtualConferenceLink + } = hearing; const showHostLink = virtualHearingRoleForUser(user, hearing) === VIRTUAL_HEARING_HOST; + const getLinks = () => { + if (scheduledForIsPast || isCancelled) { + return null; + } else if (isVirtual) { + return virtualHearing; + } else if (conferenceProvider === 'pexip') { + return dailyDocketConferenceLink; + } else if (conferenceProvider === 'webex') { + return nonVirtualConferenceLink; + } + }; + + const links = getLinks(); + return (
    - {showHostLink && } + {showHostLink && ( + <> + + + + )} -
    ); }; @@ -146,5 +193,6 @@ HearingLinks.propTypes = { hearing: PropTypes.object, isVirtual: PropTypes.bool, wasVirtual: PropTypes.bool, - virtualHearing: PropTypes.object + virtualHearing: PropTypes.object, + isCancelled: PropTypes.bool }; diff --git a/client/app/hearings/components/details/TranscriptionFilesTable.jsx b/client/app/hearings/components/details/TranscriptionFilesTable.jsx new file mode 100644 index 00000000000..2757b61ecd5 --- /dev/null +++ b/client/app/hearings/components/details/TranscriptionFilesTable.jsx @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { useState, useEffect } from 'react'; +import _ from 'lodash'; +import moment from 'moment-timezone'; + +import Link from '../../../components/Link'; +import Table from '../../../components/Table'; +import { genericRow } from './style'; +import DocketTypeBadge from '../../../components/DocketTypeBadge'; +import { COLORS } from '../../../constants/AppConstants'; +import { DownloadIcon } from '../../../components/icons/DownloadIcon'; + +const transcriptionFileColumns = [ + { + align: 'left', + valueFunction: (rowObject) => rowObject.docketName && ( + + + {rowObject.docketNumber} + + ), + header: 'Docket(s)' + }, + { + align: 'left', + valueName: 'dateUploadAws', + valueFunction: (rowObject) => moment(rowObject.dateUploadAws).format('MM/DD/YYYY'), + header: 'Uploaded', + }, + { + align: 'left', + valueFunction: (rowObject) => ( + + {rowObject.fileName} + + + ), + header: 'File Link' + }, + { + align: 'left', + valueName: 'fileStatus', + header: 'Status' + } +]; + +const rowClassNames = (rowObject) => `${rowObject.isEvenGroup ? 'even' : 'odd'}-row-group`; + +const TranscriptionFilesTable = ({ hearing }) => { + const [rows, setRows] = useState([]); + + // Format table to group files by docket number and style accordingly + const buildRowsFromFileGroups = () => { + // Flatten nested objects into nested arrays + const fileGroups = _.values(hearing.transcriptionFiles).map((rec) => _.values(rec)); + + return fileGroups.map((fileGroup, groupIndex) => { + return fileGroup.map((file, fileIndex) => { + return { + ...file, + isEvenGroup: groupIndex % 2 === 0, + docketName: fileIndex === 0 ? file.hearingType : null, + docketNumber: fileIndex === 0 ? file.docketNumber : null + }; + }); + }).flat(); + }; + + useEffect(() => { + setRows(buildRowsFromFileGroups()); + }, []); + + return ( +
    + index} + rowObjects={rows} + rowClassNames={rowClassNames} + /> + + ); +}; + +TranscriptionFilesTable.propTypes = { + hearing: PropTypes.shape({ + transcriptionFiles: PropTypes.object, + docketName: PropTypes.string, + docketNumber: PropTypes.string + }) +}; + +export default TranscriptionFilesTable; diff --git a/client/app/hearings/components/details/TranscriptionFormSection.jsx b/client/app/hearings/components/details/TranscriptionFormSection.jsx index 1e3169dfcb2..81e73f794b3 100644 --- a/client/app/hearings/components/details/TranscriptionFormSection.jsx +++ b/client/app/hearings/components/details/TranscriptionFormSection.jsx @@ -5,33 +5,51 @@ import { ContentSection } from '../../../components/ContentSection'; import TranscriptionDetailsInputs from './TranscriptionDetailsInputs'; import TranscriptionProblemInputs from './TranscriptionProblemInputs'; import TranscriptionRequestInputs from './TranscriptionRequestInputs'; +import TranscriptionFilesTable from './TranscriptionFilesTable'; +import { genericRow } from './style'; export const TranscriptionFormSection = ( - { hearing, transcription, readOnly, update } + { hearing, transcription, readOnly, update, isLegacy } ) => ( - update('transcription', values)} - readOnly={readOnly} - /> -
    + {/* If Legacy Hearing and conference provider Webex, only render Transcription Files table */} + {!isLegacy && ( + <> + update('transcription', values)} + readOnly={readOnly} + /> +
    -

    Transcription Problem

    - update('transcription', values)} - readOnly={readOnly} - /> -
    +

    Transcription Problem

    + update('transcription', values)} + readOnly={readOnly} + /> +
    -

    Transcription Request

    - update('hearing', values)} - readOnly={readOnly} - /> +

    Transcription Request

    + update('hearing', values)} + readOnly={readOnly} + /> + {hearing.conferenceProvider === 'webex' &&
    } + + )} + + {/* If conference provider not Webex, do not render Transcriptoin Files table */} + {hearing.conferenceProvider === 'webex' && ( + <> +

    Transcription Files

    + + + )} ); @@ -39,5 +57,6 @@ TranscriptionFormSection.propTypes = { update: PropTypes.func, hearing: PropTypes.object, readOnly: PropTypes.bool, - transcription: PropTypes.object + transcription: PropTypes.object, + isLegacy: PropTypes.bool }; diff --git a/client/app/hearings/components/details/VirtualHearingFields.jsx b/client/app/hearings/components/details/VirtualHearingFields.jsx index 08c807de10d..fd6a3c70ec8 100644 --- a/client/app/hearings/components/details/VirtualHearingFields.jsx +++ b/client/app/hearings/components/details/VirtualHearingFields.jsx @@ -1,27 +1,36 @@ import PropTypes from 'prop-types'; import React, { useContext } from 'react'; +import { css } from 'glamor'; import { ContentSection } from '../../../components/ContentSection'; import { HearingLinks } from './HearingLinks'; import { HearingsUserContext } from '../../contexts/HearingsUserContext'; +import StringUtil from '../../../util/StringUtil'; export const VirtualHearingFields = ({ hearing, virtualHearing }) => { - if (!hearing?.isVirtual && !hearing?.wasVirtual) { - return null; - } - const user = useContext(HearingsUserContext); + const checkCancelled = () => { + const disposition = ['postponed', 'cancelled', 'scheduled_in_error']; + + return disposition.includes(hearing?.disposition); + }; + return ( +
    + {StringUtil.capitalizeFirst(hearing.conferenceProvider)} Hearing +
    ); @@ -35,7 +44,8 @@ VirtualHearingFields.propTypes = { appellantIsNotVeteran: PropTypes.bool, scheduledForIsPast: PropTypes.bool, wasVirtual: PropTypes.bool, - isVirtual: PropTypes.bool + isVirtual: PropTypes.bool, + conferenceProvider: PropTypes.string }), initialHearing: PropTypes.shape({ virtualHearing: PropTypes.object @@ -44,7 +54,7 @@ VirtualHearingFields.propTypes = { virtualHearing: PropTypes.shape({ appellantEmail: PropTypes.string, representativeEmail: PropTypes.string, - jobCompleted: PropTypes.bool + jobCompleted: PropTypes.bool, }), errors: PropTypes.shape({ appellantEmail: PropTypes.string, diff --git a/client/app/hearings/constants.js b/client/app/hearings/constants.js index c4407d2267b..c430e7a7c49 100644 --- a/client/app/hearings/constants.js +++ b/client/app/hearings/constants.js @@ -91,14 +91,6 @@ export const ACTIONS = { HANDLE_CONFERENCE_LINK_ERROR: 'HANDLE_CONFERENCE_LINK_ERROR' }; -// Labels for guest link -export const GUEST_LINK_LABELS = { - COPY_GUEST_LINK: 'Copy Guest Link', - GUEST_LINK_SECTION_LABEL: 'Guest links for non-virtual hearings', - GUEST_CONFERENCE_ROOM: 'Conference Room', - GUEST_PIN: 'PIN', -} - export const SPREADSHEET_TYPES = { RoSchedulePeriod: { value: 'RoSchedulePeriod', diff --git a/client/app/hearings/utils.js b/client/app/hearings/utils.js index b4a38677e5a..72994842d10 100644 --- a/client/app/hearings/utils.js +++ b/client/app/hearings/utils.js @@ -219,6 +219,11 @@ export const virtualHearingLinkLabelFull = (role) => COPY.VLJ_VIRTUAL_HEARING_LINK_LABEL_FULL : COPY.REPRESENTATIVE_VIRTUAL_HEARING_LINK_LABEL; +export const virtualHearingScheduledDatePassedLabelFull = (role) => +role === VIRTUAL_HEARING_HOST ? +`${COPY.VLJ_VIRTUAL_HEARING_LINK_LABEL_FULL}: N/A` : +`${COPY.REPRESENTATIVE_VIRTUAL_HEARING_PASSED_LABEL}: N/A`; + export const pollVirtualHearingData = (hearingId, onSuccess) => ( // Did not specify retryCount so if api call fails, it'll stop polling. // If need to retry on failure, pass in retryCount diff --git a/client/app/queue/OrganizationUsers.jsx b/client/app/queue/OrganizationUsers.jsx index 886850ccba6..de900bf9009 100644 --- a/client/app/queue/OrganizationUsers.jsx +++ b/client/app/queue/OrganizationUsers.jsx @@ -1,8 +1,9 @@ +/* eslint-disable max-lines */ /* eslint-disable no-nested-ternary */ /* eslint-disable max-len */ + import React from 'react'; import PropTypes from 'prop-types'; -import { css } from 'glamor'; import { sprintf } from 'sprintf-js'; import AppSegment from '@department-of-veterans-affairs/caseflow-frontend-toolkit/components/AppSegment'; @@ -16,33 +17,7 @@ import { LOGO_COLORS } from '../constants/AppConstants'; import COPY from '../../COPY'; import LoadingDataDisplay from '../components/LoadingDataDisplay'; import MembershipRequestTable from './MembershipRequestTable'; - -const userStyle = css({ - margin: '.5rem 0 .5rem', - padding: '.5rem 0 .5rem', - listStyle: 'none' -}); -const topUserStyle = css({ - borderTop: '.1rem solid gray', - margin: '.5rem 0 .5rem', - padding: '1rem 0 .5rem', - listStyle: 'none' -}); -const topUserBorder = css({ - borderBottom: '.1rem solid gray', -}); -const buttonStyle = css({ - paddingRight: '1rem', - display: 'inline-block' -}); -const buttonContainerStyle = css({ - borderBottom: '1rem solid gray', - borderWidth: '1px', - padding: '.5rem 0 2rem', -}); -const listStyle = css({ - listStyle: 'none' -}); +import SelectConferenceTypeRadioField from './SelectConferenceTypeRadioField'; export default class OrganizationUsers extends React.PureComponent { constructor(props) { @@ -226,7 +201,7 @@ export default class OrganizationUsers extends React.PureComponent { } adminButton = (user, admin) => -
    removeUserButton = (user) => -

    {COPY.USER_MANAGEMENT_EDIT_USER_IN_ORG_LABEL}

    -
      +
        { (judgeTeam || dvcTeam) ? '' :
      • {COPY.USER_MANAGEMENT_ADMIN_RIGHTS_HEADING}{COPY.USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION}
      • }
      • {COPY.USER_MANAGEMENT_REMOVE_USER_HEADING}{ judgeTeam ? COPY.USER_MANAGEMENT_JUDGE_TEAM_REMOVE_USER_DESCRIPTION : @@ -334,7 +336,7 @@ getFilteredUsers = () => {
    { listOfUsers.length > 0 ? ( -
      {listOfUsers}
    +
      {listOfUsers}
    ) : ( <>

    No results found

    @@ -342,7 +344,6 @@ getFilteredUsers = () => { ) } -
    ; } @@ -414,5 +415,6 @@ getFilteredUsers = () => { } OrganizationUsers.propTypes = { - organization: PropTypes.string + organization: PropTypes.string, + conferenceSelectionVisibility: PropTypes.bool }; diff --git a/client/app/queue/QueueApp.jsx b/client/app/queue/QueueApp.jsx index a0c0a7c447d..06d50aa6530 100644 --- a/client/app/queue/QueueApp.jsx +++ b/client/app/queue/QueueApp.jsx @@ -17,6 +17,7 @@ import { setCanEditCavcDashboards, setCanViewCavcDashboards, setFeatureToggles, + setMeetingType, setUserId, setUserRole, setUserCssId, @@ -115,6 +116,7 @@ class QueueApp extends React.PureComponent { this.props.setCanEditAod(this.props.canEditAod); this.props.setCanEditNodDate(this.props.userCanViewEditNodDate); this.props.setUserIsCobAdmin(this.props.userIsCobAdmin); + this.props.setMeetingType(this.props.conferenceProvider); this.props.setCanEditCavcRemands(this.props.canEditCavcRemands); this.props.setCanEditCavcDashboards(this.props.canEditCavcDashboards); this.props.setCanViewCavcDashboards(this.props.canViewCavcDashboards); @@ -602,7 +604,9 @@ class QueueApp extends React.PureComponent { }; routedOrganizationUsers = (props) => ( - + ); routedTeamManagement = (props) => ; @@ -948,7 +952,8 @@ class QueueApp extends React.PureComponent { render={this.routedAssignToUser} /> @@ -1161,7 +1166,8 @@ class QueueApp extends React.PureComponent { /> ({ @@ -1496,6 +1504,7 @@ const mapDispatchToProps = (dispatch) => setCanEditAod, setCanEditNodDate, setUserIsCobAdmin, + setMeetingType, setCanEditCavcRemands, setCanEditCavcDashboards, setCanViewCavcDashboards, diff --git a/client/app/queue/SelectConferenceTypeRadioField.jsx b/client/app/queue/SelectConferenceTypeRadioField.jsx new file mode 100644 index 00000000000..52c3417e2a5 --- /dev/null +++ b/client/app/queue/SelectConferenceTypeRadioField.jsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import ApiUtil from '../util/ApiUtil'; + +import RadioField from '../components/RadioField'; +import COPY from '../../COPY'; + +const radioOptions = [ + { displayText: 'Pexip', value: 'pexip' }, + { displayText: 'Webex', value: 'webex' }, +]; + +const SelectConferenceTypeRadioField = ({ + name, + conferenceProvider, + organization, + user, +}) => { + const [value, setValue] = useState(conferenceProvider); + + const modifyConferenceType = (newConferenceProvider) => { + const payload = { + data: { + ...user, + attributes: { + ...user.attributes, + conference_provider: newConferenceProvider, + }, + }, + }; + + ApiUtil.patch(`/organizations/${organization}/users/${user.id}`, payload); + }; + + return ( + <> + + setValue(newValue) || modifyConferenceType(newValue) + } + vertical + /> + + ); +}; + +SelectConferenceTypeRadioField.propTypes = { + name: PropTypes.string, + onClick: PropTypes.func, + conferenceProvider: PropTypes.string, + organization: PropTypes.string, + user: PropTypes.shape({ + id: PropTypes.string, + attributes: PropTypes.object, + }), +}; + +export default SelectConferenceTypeRadioField; diff --git a/client/app/queue/uiReducer/uiActions.js b/client/app/queue/uiReducer/uiActions.js index b479848b394..47fbe37eb02 100644 --- a/client/app/queue/uiReducer/uiActions.js +++ b/client/app/queue/uiReducer/uiActions.js @@ -48,6 +48,13 @@ export const setUserIsCobAdmin = (userIsCobAdmin) => ({ } }); +export const setMeetingType = (conferenceProvider) => ({ + type: ACTIONS.SET_CONFERENCE_PROVIDER, + payload: { + conferenceProvider + } +}); + export const setCanViewOvertimeStatus = (canViewOvertimeStatus) => ({ type: ACTIONS.SET_CAN_VIEW_OVERTIME_STATUS, payload: { diff --git a/client/app/queue/uiReducer/uiConstants.js b/client/app/queue/uiReducer/uiConstants.js index 15723d7f431..d8dab0638ab 100644 --- a/client/app/queue/uiReducer/uiConstants.js +++ b/client/app/queue/uiReducer/uiConstants.js @@ -8,6 +8,7 @@ export const ACTIONS = { SET_FEEDBACK_URL: 'SET_FEEDBACK_URL', SET_TARGET_USER: 'SET_TARGET_USER', + SET_CONFERENCE_PROVIDER: 'SET_CONFERENCE_PROVIDER', HIGHLIGHT_INVALID_FORM_ITEMS: 'HIGHLIGHT_INVALID_FORM_ITEMS', RESET_ERROR_MESSAGES: 'RESET_ERROR_MESSAGES', diff --git a/client/app/queue/uiReducer/uiReducer.js b/client/app/queue/uiReducer/uiReducer.js index 96a66077b1c..94dc63eaf7e 100644 --- a/client/app/queue/uiReducer/uiReducer.js +++ b/client/app/queue/uiReducer/uiReducer.js @@ -100,6 +100,10 @@ const workQueueUiReducer = (state = initialState, action = {}) => { return update(state, { userIsCobAdmin: { $set: action.payload.userIsCobAdmin } }); + case ACTIONS.SET_CONFERENCE_PROVIDER: + return update(state, { + conferenceProvider: { $set: action.payload.conferenceProvider } + }); case ACTIONS.SET_CAN_EDIT_CAVC_REMANDS: return update(state, { canEditCavcRemands: { $set: action.payload.canEditCavcRemands } diff --git a/client/app/styles/_commons.scss b/client/app/styles/_commons.scss index c5ff8c2e62e..f010f599ded 100644 --- a/client/app/styles/_commons.scss +++ b/client/app/styles/_commons.scss @@ -1174,7 +1174,7 @@ ul { &::before { position: absolute; left: 3px; - top: 2px; + top: 6px; } } } diff --git a/client/app/styles/_team_management.scss b/client/app/styles/_team_management.scss index d5b01198ced..ef9dc0dc8ea 100644 --- a/client/app/styles/_team_management.scss +++ b/client/app/styles/_team_management.scss @@ -69,7 +69,6 @@ .search-bar-styling-for-filter { padding-left: 80px; - margin-top: 20px; margin-bottom: 15px; justify-self: right; display: block; @@ -79,7 +78,7 @@ padding-left: 80px; justify-self: flex-end; display: flex; - margin-bottom: 5px; + margin-bottom: 10px; } .no-results-found-styling { @@ -101,3 +100,39 @@ padding-left: 55vw; margin-right: 15vw; } + +.add-dropdown { + padding: 3rem 0 4rem; +} + +.instruction-list { + list-style: none; + margin: 0 0 0 3rem; + padding: 1.5rem 0 2rem; + font-size: 19px; +} + +.user-list { + margin: 0; +} + +.user-list-item { + display: flex; + flex-wrap: wrap; + border-top: .1rem solid $color-black; + padding: 4rem 0 2rem; + margin: 0; + + &:first-child { + margin-top: 30px; + } +} + +.title-buttons { + width: 60rem; +} + +.button-style { + padding: 1rem 2.5rem 2rem 0; + display: inline-block; +} diff --git a/client/app/styles/hearings/_hearings.scss b/client/app/styles/hearings/_hearings.scss index 5abb0a3d001..c3bce185a8c 100644 --- a/client/app/styles/hearings/_hearings.scss +++ b/client/app/styles/hearings/_hearings.scss @@ -709,3 +709,58 @@ $form-width: 575px; .hearing-time-scheduled-in-timezone { margin-top: 0; } + +// Transcription Files Table +.transcription-files-table { + margin-top: 0; + + *:focus { + outline: none; + } + + th:first-child { + padding-left: 2.5rem; + } + + tbody { + + tr { + &:first-child td { + padding-top: 3rem; + } + + &.even-row-group td { + background-color: $cf-background; + } + + td { + border: 0; + padding-top: 0; + padding-bottom: 3rem; + + &:first-child { + padding-left: 2.5rem; + } + + &:last-child { + font-style: italic; + } + + a { + display: inline-flex; + align-items: center; + } + + svg { + margin-left: 1rem; + } + } + } + + .even-row-group + .odd-row-group td, + .odd-row-group + .even-row-group td { + border-top: 1px solid $color-gray-lighter; + padding-top: 3rem; + } + } +} diff --git a/client/constants/TRANSCRIPTION_FILE_STATUSES.json b/client/constants/TRANSCRIPTION_FILE_STATUSES.json new file mode 100644 index 00000000000..945540147d9 --- /dev/null +++ b/client/constants/TRANSCRIPTION_FILE_STATUSES.json @@ -0,0 +1,14 @@ +{ + "retrieval": { + "success": "Successful retrieval (Webex)", + "failure": "Failed retrieval (Webex)" + }, + "upload": { + "success": "Successful upload (AWS)", + "failure": "Failed upload (AWS)" + }, + "conversion": { + "success": "Successful conversion", + "failure": "Failed conversion" + } +} diff --git a/client/test/app/hearings/components/Details.test.js b/client/test/app/hearings/components/Details.test.js index 11d7f55ccdf..913f4c9a8ec 100644 --- a/client/test/app/hearings/components/Details.test.js +++ b/client/test/app/hearings/components/Details.test.js @@ -12,7 +12,8 @@ import { amaHearing, defaultHearing, virtualHearing, - vsoUser + amaWebexHearing, + legacyWebexHearing } from 'test/data'; import Button from 'app/components/Button'; import DateSelector from 'app/components/DateSelector'; @@ -20,11 +21,12 @@ import Details from 'app/hearings/components/Details'; import DetailsForm from 'app/hearings/components/details/DetailsForm'; import HearingTypeDropdown from 'app/hearings/components/details/HearingTypeDropdown'; import SearchableDropdown from 'app/components/SearchableDropdown'; -import TranscriptionRequestInputs from - 'app/hearings/components/details/TranscriptionRequestInputs'; +import TranscriptionDetailsInputs from 'app/hearings/components/details/TranscriptionDetailsInputs'; +import TranscriptionProblemInputs from 'app/hearings/components/details/TranscriptionProblemInputs'; +import TranscriptionRequestInputs from 'app/hearings/components/details/TranscriptionRequestInputs'; +import TranscriptionFilesTable from 'app/hearings/components/details/TranscriptionFilesTable'; import EmailConfirmationModal from 'app/hearings/components/EmailConfirmationModal'; import toJson from 'enzyme-to-json'; -import { node } from 'prop-types'; // Define the function spies const saveHearingSpy = jest.fn(); @@ -74,7 +76,9 @@ describe('Details', () => { expect(details.find(VirtualHearingFields).prop('virtualHearing')).toEqual( null ); - expect(details.find(VirtualHearingFields).children()).toHaveLength(0); + // VirtualHearingFields will always show for any virtual or non virtual hearing + // as we move forward with Webex integration + expect(details.find(VirtualHearingFields).children()).toHaveLength(1); // Ensure the transcription section is displayed by default for ama hearings expect(details.find(TranscriptionFormSection)).toHaveLength(1); @@ -230,43 +234,133 @@ describe('Details', () => { expect(toJson(details, { noKey: true })).toMatchSnapshot(); }); - test('Does not display transcription section for legacy hearings', () => { - const details = mount( -
    , - { - wrappingComponent: hearingDetailsWrapper( - anyUser, - legacyHearing - ), - wrappingComponentProps: { store: detailsStore }, - } - ); - - // Assertions - expect(details.find(DetailsHeader)).toHaveLength(1); - expect(details.find(DetailsForm)).toHaveLength(1); - - // Ensure that the virtualHearing form is not displayed by default - expect(details.find(VirtualHearingFields).prop('virtualHearing')).toEqual( - null - ); - expect(details.find(VirtualHearingFields).children()).toHaveLength(0); - - // Ensure the transcription form is not displayed for legacy hearings - expect(details.find(TranscriptionFormSection)).toHaveLength(0); - - // Ensure the save and cancel buttons are present - detailButtonsTest(details); - - expect(toJson(details, { noKey: true })).toMatchSnapshot(); + describe('TranscriptiomFormSection', () => { + describe('pexip hearing', () => { + test('Displays transcription section but not transcription files table for AMA hearings', () => { + const details = mount( +
    , + { + wrappingComponent: hearingDetailsWrapper( + anyUser, + amaHearing + ), + wrappingComponentProps: { store: detailsStore }, + } + ); + + expect(details.find(TranscriptionFormSection)).toHaveLength(1); + expect(details.find(TranscriptionDetailsInputs)).toHaveLength(1); + expect(details.find(TranscriptionProblemInputs)).toHaveLength(1); + expect(details.find(TranscriptionRequestInputs)).toHaveLength(1); + expect(details.find(TranscriptionFilesTable)).toHaveLength(0); + }); + + test('Does not display transcription section for legacy hearings', () => { + const details = mount( +
    , + { + wrappingComponent: hearingDetailsWrapper( + anyUser, + legacyHearing + ), + wrappingComponentProps: { store: detailsStore }, + } + ); + + // Assertions + expect(details.find(DetailsHeader)).toHaveLength(1); + expect(details.find(DetailsForm)).toHaveLength(1); + + // Ensure that the virtualHearing form is not displayed by default + expect(details.find(VirtualHearingFields).prop('virtualHearing')).toEqual( + null + ); + // VirtualHearingFields will always show for any virtual or non virtual hearing + // as we move forward with Webex integration + expect(details.find(VirtualHearingFields).children()).toHaveLength(1); + + // Ensure the transcription form is not displayed for legacy hearings + expect(details.find(TranscriptionFormSection)).toHaveLength(0); + // expect(details.find(TranscriptionFilesTable)).toHaveLength(0); + + // Ensure the save and cancel buttons are present + detailButtonsTest(details); + + expect(toJson(details, { noKey: true })).toMatchSnapshot(); + }); + }); + + describe('webex hearing', () => { + test('Displays transcription section, including transcription files table, for AMA hearings', () => { + const details = mount( +
    , + { + wrappingComponent: hearingDetailsWrapper( + anyUser, + amaWebexHearing + ), + wrappingComponentProps: { store: detailsStore }, + } + ); + + expect(details.find(TranscriptionFormSection)).toHaveLength(1); + expect(details.find(TranscriptionDetailsInputs)).toHaveLength(1); + expect(details.find(TranscriptionProblemInputs)).toHaveLength(1); + expect(details.find(TranscriptionRequestInputs)).toHaveLength(1); + expect(details.find(TranscriptionFilesTable)).toHaveLength(1); + }); + + test('Only displays transcription files table, and not other transcription form inputs, for legacy hearings', () => { + const details = mount( +
    , + { + wrappingComponent: hearingDetailsWrapper( + anyUser, + legacyWebexHearing + ), + wrappingComponentProps: { store: detailsStore }, + } + ); + + expect(details.find(TranscriptionFormSection)).toHaveLength(1); + expect(details.find(TranscriptionDetailsInputs)).toHaveLength(0); + expect(details.find(TranscriptionProblemInputs)).toHaveLength(0); + expect(details.find(TranscriptionRequestInputs)).toHaveLength(0); + expect(details.find(TranscriptionFilesTable)).toHaveLength(1); + }); + }); }); test('Displays VirtualHearing details when there is a virtual hearing', () => { diff --git a/client/test/app/hearings/components/VirtualHearings/__snapshots__/AppellantSection.test.js.snap b/client/test/app/hearings/components/VirtualHearings/__snapshots__/AppellantSection.test.js.snap index fe29ce17fcb..c8a844dc1c4 100644 --- a/client/test/app/hearings/components/VirtualHearings/__snapshots__/AppellantSection.test.js.snap +++ b/client/test/app/hearings/components/VirtualHearings/__snapshots__/AppellantSection.test.js.snap @@ -25,6 +25,7 @@ exports[`Appellant Displays appellant information when appellant is not veteran "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -105,6 +106,7 @@ exports[`Appellant Displays appellant information when appellant is not veteran "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -301,6 +303,7 @@ exports[`Appellant Displays email alert when email is null 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -525,6 +528,7 @@ exports[`Appellant Displays email alert when email is undefined 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -744,6 +748,7 @@ exports[`Appellant Displays timezone when showTimezoneField is passed as prop 1` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -825,6 +830,7 @@ exports[`Appellant Displays timezone when showTimezoneField is passed as prop 1` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -17116,6 +17122,7 @@ exports[`Appellant Does not allow editing emails when read-only 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -17197,6 +17204,7 @@ exports[`Appellant Does not allow editing emails when read-only 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -17331,6 +17339,7 @@ exports[`Appellant Does not display address when formFieldsOnly = true 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -17411,6 +17420,7 @@ exports[`Appellant Does not display address when formFieldsOnly = true 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -17550,6 +17560,7 @@ exports[`Appellant Matches snapshot with default props 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -17630,6 +17641,7 @@ exports[`Appellant Matches snapshot with default props 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", diff --git a/client/test/app/hearings/components/VirtualHearings/__snapshots__/Fields.test.js.snap b/client/test/app/hearings/components/VirtualHearings/__snapshots__/Fields.test.js.snap index f0cef36fe1f..67ae786380c 100644 --- a/client/test/app/hearings/components/VirtualHearings/__snapshots__/Fields.test.js.snap +++ b/client/test/app/hearings/components/VirtualHearings/__snapshots__/Fields.test.js.snap @@ -25,6 +25,7 @@ exports[`Fields Display timezone and divider for Central 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -32531,6 +32532,7 @@ exports[`Fields Display timezone and divider for Video 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, diff --git a/client/test/app/hearings/components/VirtualHearings/__snapshots__/RepresentativeSection.test.js.snap b/client/test/app/hearings/components/VirtualHearings/__snapshots__/RepresentativeSection.test.js.snap index b69b7c34ca4..0553382bf88 100644 --- a/client/test/app/hearings/components/VirtualHearings/__snapshots__/RepresentativeSection.test.js.snap +++ b/client/test/app/hearings/components/VirtualHearings/__snapshots__/RepresentativeSection.test.js.snap @@ -26,6 +26,7 @@ exports[`RepresentativeSection Displays timezone when showTimezoneField is passe "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -107,6 +108,7 @@ exports[`RepresentativeSection Displays timezone when showTimezoneField is passe "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -16377,6 +16379,7 @@ exports[`RepresentativeSection Does not allow editing emails when read-only 1`] "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -16458,6 +16461,7 @@ exports[`RepresentativeSection Does not allow editing emails when read-only 1`] "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -16570,6 +16574,7 @@ exports[`RepresentativeSection Does not display address when formFieldsOnly = tr "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -16650,6 +16655,7 @@ exports[`RepresentativeSection Does not display address when formFieldsOnly = tr "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -16799,6 +16805,7 @@ exports[`RepresentativeSection Matches snapshot with default props 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -16879,6 +16886,7 @@ exports[`RepresentativeSection Matches snapshot with default props 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -17038,6 +17046,7 @@ exports[`RepresentativeSection Shows Representative name when representative add "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -17073,6 +17082,7 @@ exports[`RepresentativeSection Shows Representative name when representative add "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -17117,6 +17127,7 @@ exports[`RepresentativeSection Shows Representative name when representative add "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -17144,6 +17155,7 @@ exports[`RepresentativeSection Shows Representative name when representative add "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -17297,6 +17309,7 @@ exports[`RepresentativeSection Shows Representative not present message when no "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -17332,6 +17345,7 @@ exports[`RepresentativeSection Shows Representative not present message when no "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -17375,6 +17389,7 @@ exports[`RepresentativeSection Shows Representative not present message when no "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -17402,6 +17417,7 @@ exports[`RepresentativeSection Shows Representative not present message when no "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", diff --git a/client/test/app/hearings/components/__snapshots__/Details.test.js.snap b/client/test/app/hearings/components/__snapshots__/Details.test.js.snap index 9ae6339857e..b99d8414ffb 100644 --- a/client/test/app/hearings/components/__snapshots__/Details.test.js.snap +++ b/client/test/app/hearings/components/__snapshots__/Details.test.js.snap @@ -24,6 +24,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -59,6 +60,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -102,6 +104,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -149,6 +152,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -184,6 +188,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -227,6 +232,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -279,6 +285,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -314,6 +321,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -363,6 +371,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -9366,6 +9375,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -9401,6 +9411,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -9464,6 +9475,7 @@ exports[`Details Displays HearingConversion when converting from central 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -25760,6 +25772,7 @@ SAN FRANCISCO, CA 94103 "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -25795,6 +25808,7 @@ SAN FRANCISCO, CA 94103 "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -25858,6 +25872,7 @@ SAN FRANCISCO, CA 94103 "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -42202,6 +42217,7 @@ exports[`Details Displays HearingConversion when converting from video 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -42303,6 +42319,7 @@ exports[`Details Displays HearingConversion when converting from video 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -42409,6 +42426,7 @@ exports[`Details Displays HearingConversion when converting from video 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -51470,6 +51488,7 @@ exports[`Details Displays HearingConversion when converting from video 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -67843,6 +67862,7 @@ SAN FRANCISCO, CA 94103 "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -85788,6 +85808,7 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -85818,6 +85839,7 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = ` "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -85868,6 +85890,7 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -94925,6 +94948,7 @@ exports[`Details Displays HearingConversion when converting from virtual 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -111298,6 +111322,7 @@ SAN FRANCISCO, CA 94103 "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -129257,6 +129282,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -129292,6 +129318,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -129335,6 +129362,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -129403,6 +129431,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -129438,6 +129467,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -129481,6 +129511,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -129566,6 +129597,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -129601,6 +129633,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -129650,6 +129683,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -129713,11 +129747,12 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing aria-describedby="tooltip-500000003" aria-label="" className="cf-apppeal-id" - data-css-cjlnsd="" + data-css-p6dv0b="" data-event="focus mouseenter" data-event-off="mouseleave keydown" data-for="tooltip-500000003" data-tip={true} + disabled={true} onClick={[Function]} tabIndex={0} type="submit" @@ -129883,6 +129918,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -129918,6 +129954,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -129967,6 +130004,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -130007,6 +130045,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -130042,6 +130081,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -130091,6 +130131,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -130437,6 +130478,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -130472,6 +130514,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -130521,6 +130564,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -130574,6 +130618,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -130609,6 +130654,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -130658,6 +130704,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -139210,6 +139257,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -139245,6 +139293,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -139294,6 +139343,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -139332,6 +139382,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -139367,6 +139418,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -139416,6 +139468,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -139440,6 +139493,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -139455,7 +139509,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing } >
    - Virtual Hearing Links + Hearing Links
    +
    + + Pexip + Hearing + +
    - Join Virtual Hearing + Join Hearing
    -
    @@ -140382,6 +140482,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -140417,6 +140518,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -140466,6 +140568,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -140504,6 +140607,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -140539,6 +140643,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -140588,6 +140693,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -173416,6 +173522,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -173451,6 +173558,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -173500,6 +173608,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -173517,6 +173626,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "worksheetIssues": Object {}, } } + isLegacy={false} transcription={ Object { "expectedReturnDate": undefined, @@ -177701,6 +177811,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -177736,6 +177847,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -177785,6 +177897,7 @@ exports[`Details Displays VirtualHearing details when there is a virtual hearing "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -178017,6 +178130,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -178052,6 +178166,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -178095,6 +178210,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -178163,6 +178279,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -178198,6 +178315,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -178241,6 +178359,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -178326,6 +178445,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -178361,6 +178481,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -178410,6 +178531,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -178473,11 +178595,12 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip aria-describedby="tooltip-500000003" aria-label="" className="cf-apppeal-id" - data-css-cjlnsd="" + data-css-p6dv0b="" data-event="focus mouseenter" data-event-off="mouseleave keydown" data-for="tooltip-500000003" data-tip={true} + disabled={true} onClick={[Function]} tabIndex={0} type="submit" @@ -178643,6 +178766,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -178678,6 +178802,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -178727,6 +178852,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -178767,6 +178893,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -178802,6 +178929,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -178851,6 +178979,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -179197,6 +179326,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -179232,6 +179362,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -179281,6 +179412,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -179334,6 +179466,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -179369,6 +179502,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -179418,6 +179552,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -187970,6 +188105,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -188005,6 +188141,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -188054,6 +188191,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -188092,6 +188230,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -188127,6 +188266,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -188176,6 +188316,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -188200,6 +188341,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -188215,7 +188357,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip } >
    - Virtual Hearing Links + Hearing Links
    +
    + + Pexip + Hearing + +
    - Join Virtual Hearing + Join Hearing
    -
    @@ -189142,6 +189330,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -189177,6 +189366,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -189226,6 +189416,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -189264,6 +189455,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -189299,6 +189491,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -189348,6 +189541,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -222176,6 +222370,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -222211,6 +222406,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -222260,6 +222456,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -222277,6 +222474,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "worksheetIssues": Object {}, } } + isLegacy={false} transcription={ Object { "expectedReturnDate": undefined, @@ -226461,6 +226659,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "centralOfficeTimeString": "03:30", "claimantId": 4, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 1, "disposition": null, "dispositionEditable": true, @@ -226496,6 +226695,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "fullName": "Aaron Judge_HearingsAndCases Abshire", "id": 3, "lastLoginAt": null, + "meetingType": "pexip", "roles": Object {}, "selectedRegionalOffice": null, "stationId": "101", @@ -226545,6 +226745,7 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip "appellantEmail": "Bob.Smith@test.com", "appellantTz": "America/Denver", "clientHost": "care.evn.va.gov", + "conferenceProvider": "pexip", "guestLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=2684353125#&role=guest", "guestPin": "2684353125#", "hostLink": "https://care.evn.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0000009@care.evn.va.gov&pin=8600030#&role=host", @@ -226785,22 +226986,24 @@ exports[`Details Does not display EmailConfirmationModal when updating transcrip `; -exports[`Details Does not display transcription section for legacy hearings 1`] = ` +exports[`Details Matches snapshot with default props 1`] = ` @@ -226903,15 +227095,17 @@ exports[`Details Does not display transcription section for legacy hearings 1`] Object { "advanceOnDocketMotion": null, "aod": false, - "appealExternalId": "0bf0263c-d863-4405-9b2e-f55cff77c6c3", + "appealExternalId": "0bf0263c-d863-4405-9b2e-f55cff77c6c4", "appealId": 613, "appellantAddressLine1": "9999 MISSION ST", "appellantCity": "SAN FRANCISCO", "appellantEmailAddress": "tom.brady@caseflow.gov", "appellantFirstName": "Tom", - "appellantIsNotVeteran": true, + "appellantIsNotVeteran": false, "appellantLastName": "Brady", + "appellantRelationship": "Spouse", "appellantState": "CA", + "appellantTz": "America/Los_Angeles", "appellantZip": "94103", "availableHearingLocations": Object {}, "bvaPoc": null, @@ -226919,11 +227113,12 @@ exports[`Details Does not display transcription section for legacy hearings 1`] "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, - "currentIssueCount": 1, + "conferenceProvider": "pexip", + "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, - "docketName": "legacy", - "docketNumber": "200624-613", + "docketName": "hearing", + "docketNumber": "200624-614", "emailEvents": Array [], "evidenceWindowWaived": false, "externalId": "61e7af7a-586c-446d-b8ee-a65be467e9e0", @@ -226959,21 +227154,30 @@ exports[`Details Does not display transcription section for legacy hearings 1`] "regionalOfficeName": "St. Petersburg regional office", "regionalOfficeTimezone": "America/New_York", "representative": "PARALYZED VETERANS OF AMERICA, INC.", + "representativeAddress": Object { + "addressLine1": "9999 MISSION ST", + "city": "SAN FRANCISCO", + "state": "CA", + "zip": "94103", + }, "representativeEmailAddress": "tom.brady@caseflow.gov", "representativeName": null, + "representativeTz": "America/Los_Angeles", "room": "1", - "scheduledFor": "2020-07-06T04:00:00.000-04:00", + "scheduledFor": "2020-07-06T06:00:00.000-04:00", "scheduledForIsPast": false, - "scheduledTime": "2000-01-01T04:00:00.000-05:00", - "scheduledTimeString": "04:00", + "scheduledTime": "2000-01-01T06:00:00.000-05:00", + "scheduledTimeString": "06:00", "summary": null, + "transcriptRequested": null, + "transcription": Object {}, "uuid": "61e7af7a-586c-446d-b8ee-a65be467e9e0", "veteranAge": 85, - "veteranEmailAddress": "Brian.Hodkiewicz@test.com", + "veteranEmailAddress": "John.Smith@test.com", "veteranFileNumber": "100000005", - "veteranFirstName": "Brian", + "veteranFirstName": "John", "veteranGender": "M", - "veteranLastName": "Hodkiewicz", + "veteranLastName": "Smith", "virtualHearing": null, "wasVirtual": false, "witness": null, @@ -226982,28 +227186,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] } onReceiveAlerts={[Function]} onReceiveTransitioningAlert={[Function]} - saveHearing={ - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hearing": Object { - "email_recipients_attributes": Object {}, - "transcriptSentDate": "07/25/2020", - "transcription_attributes": Object {}, - "virtual_hearing_attributes": Object {}, - }, - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - } - } + saveHearing={[MockFunction]} setHearing={[MockFunction]} transitionAlert={[Function]} > @@ -227025,21 +227208,23 @@ exports[`Details Does not display transcription section for legacy hearings 1`]
    - Brian Hodkiewicz's Hearing Details + John Smith's Hearing Details
    Veteran ID: @@ -227161,11 +227355,12 @@ exports[`Details Does not display transcription section for legacy hearings 1`] aria-describedby="tooltip-100000005" aria-label="" className="cf-apppeal-id" - data-css-cjlnsd="" + data-css-p6dv0b="" data-event="focus mouseenter" data-event-off="mouseleave keydown" data-for="tooltip-100000005" data-tip={true} + disabled={true} onClick={[Function]} tabIndex={0} type="submit" @@ -227314,15 +227509,17 @@ exports[`Details Does not display transcription section for legacy hearings 1`] Object { "advanceOnDocketMotion": null, "aod": false, - "appealExternalId": "0bf0263c-d863-4405-9b2e-f55cff77c6c3", + "appealExternalId": "0bf0263c-d863-4405-9b2e-f55cff77c6c4", "appealId": 613, "appellantAddressLine1": "9999 MISSION ST", "appellantCity": "SAN FRANCISCO", "appellantEmailAddress": "tom.brady@caseflow.gov", "appellantFirstName": "Tom", - "appellantIsNotVeteran": true, + "appellantIsNotVeteran": false, "appellantLastName": "Brady", + "appellantRelationship": "Spouse", "appellantState": "CA", + "appellantTz": "America/Los_Angeles", "appellantZip": "94103", "availableHearingLocations": Object {}, "bvaPoc": null, @@ -227330,11 +227527,12 @@ exports[`Details Does not display transcription section for legacy hearings 1`] "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, - "currentIssueCount": 1, + "conferenceProvider": "pexip", + "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, - "docketName": "legacy", - "docketNumber": "200624-613", + "docketName": "hearing", + "docketNumber": "200624-614", "emailEvents": Array [], "evidenceWindowWaived": false, "externalId": "61e7af7a-586c-446d-b8ee-a65be467e9e0", @@ -227370,14 +227568,22 @@ exports[`Details Does not display transcription section for legacy hearings 1`] "regionalOfficeName": "St. Petersburg regional office", "regionalOfficeTimezone": "America/New_York", "representative": "PARALYZED VETERANS OF AMERICA, INC.", + "representativeAddress": Object { + "addressLine1": "9999 MISSION ST", + "city": "SAN FRANCISCO", + "state": "CA", + "zip": "94103", + }, "representativeEmailAddress": "tom.brady@caseflow.gov", "representativeName": null, + "representativeTz": "America/Los_Angeles", "room": "1", - "scheduledFor": "2020-07-06T04:00:00.000-04:00", + "scheduledFor": "2020-07-06T06:00:00.000-04:00", "scheduledForIsPast": false, - "scheduledTime": "2000-01-01T04:00:00.000-05:00", - "scheduledTimeString": "04:00", + "scheduledTime": "2000-01-01T06:00:00.000-05:00", + "scheduledTimeString": "06:00", "summary": null, + "transcriptRequested": null, "transcriptSentDate": undefined, "transcription": Object { "expectedReturnDate": undefined, @@ -227387,11 +227593,11 @@ exports[`Details Does not display transcription section for legacy hearings 1`] }, "uuid": "61e7af7a-586c-446d-b8ee-a65be467e9e0", "veteranAge": 85, - "veteranEmailAddress": "Brian.Hodkiewicz@test.com", + "veteranEmailAddress": "John.Smith@test.com", "veteranFileNumber": "100000005", - "veteranFirstName": "Brian", + "veteranFirstName": "John", "veteranGender": "M", - "veteranLastName": "Hodkiewicz", + "veteranLastName": "Smith", "virtualHearing": null, "wasVirtual": false, "witness": null, @@ -227404,15 +227610,17 @@ exports[`Details Does not display transcription section for legacy hearings 1`] Object { "advanceOnDocketMotion": null, "aod": false, - "appealExternalId": "0bf0263c-d863-4405-9b2e-f55cff77c6c3", + "appealExternalId": "0bf0263c-d863-4405-9b2e-f55cff77c6c4", "appealId": 613, "appellantAddressLine1": "9999 MISSION ST", "appellantCity": "SAN FRANCISCO", "appellantEmailAddress": "tom.brady@caseflow.gov", "appellantFirstName": "Tom", - "appellantIsNotVeteran": true, + "appellantIsNotVeteran": false, "appellantLastName": "Brady", + "appellantRelationship": "Spouse", "appellantState": "CA", + "appellantTz": "America/Los_Angeles", "appellantZip": "94103", "availableHearingLocations": Object {}, "bvaPoc": null, @@ -227420,11 +227628,12 @@ exports[`Details Does not display transcription section for legacy hearings 1`] "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, - "currentIssueCount": 1, + "conferenceProvider": "pexip", + "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, - "docketName": "legacy", - "docketNumber": "200624-613", + "docketName": "hearing", + "docketNumber": "200624-614", "emailEvents": Array [], "evidenceWindowWaived": false, "externalId": "61e7af7a-586c-446d-b8ee-a65be467e9e0", @@ -227460,14 +227669,22 @@ exports[`Details Does not display transcription section for legacy hearings 1`] "regionalOfficeName": "St. Petersburg regional office", "regionalOfficeTimezone": "America/New_York", "representative": "PARALYZED VETERANS OF AMERICA, INC.", + "representativeAddress": Object { + "addressLine1": "9999 MISSION ST", + "city": "SAN FRANCISCO", + "state": "CA", + "zip": "94103", + }, "representativeEmailAddress": "tom.brady@caseflow.gov", "representativeName": null, + "representativeTz": "America/Los_Angeles", "room": "1", - "scheduledFor": "2020-07-06T04:00:00.000-04:00", + "scheduledFor": "2020-07-06T06:00:00.000-04:00", "scheduledForIsPast": false, - "scheduledTime": "2000-01-01T04:00:00.000-05:00", - "scheduledTimeString": "04:00", + "scheduledTime": "2000-01-01T06:00:00.000-05:00", + "scheduledTimeString": "06:00", "summary": null, + "transcriptRequested": null, "transcriptSentDate": undefined, "transcription": Object { "expectedReturnDate": undefined, @@ -227477,11 +227694,11 @@ exports[`Details Does not display transcription section for legacy hearings 1`] }, "uuid": "61e7af7a-586c-446d-b8ee-a65be467e9e0", "veteranAge": 85, - "veteranEmailAddress": "Brian.Hodkiewicz@test.com", + "veteranEmailAddress": "John.Smith@test.com", "veteranFileNumber": "100000005", - "veteranFirstName": "Brian", + "veteranFirstName": "John", "veteranGender": "M", - "veteranLastName": "Hodkiewicz", + "veteranLastName": "Smith", "virtualHearing": null, "wasVirtual": false, "witness": null, @@ -227508,10 +227725,10 @@ exports[`Details Does not display transcription section for legacy hearings 1`] "label": "Docket Number", "value": - 200624-613 + 200624-614 , }, Object { @@ -227545,6 +227762,7 @@ exports[`Details Does not display transcription section for legacy hearings 1`] data-css-1atuorp="" >
    - L + H - Click to copy -
    - - -
    -
    -
    -`; - -exports[`DailyDocketGuestLinkSection renders correctly for non hearing admins and hearing management users 1`] = ` -
    -
    -

    - Guest links for non-virtual hearings -

    -
    -

    - Conference Room - : - - BVA0000001@caseflow.va.gov - -

    -

    - PIN - : - - 3998472 - # - -

    -

    - - - - -

    -
    -
    -
    -`; diff --git a/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap b/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap index 595030c9ceb..38258f85557 100644 --- a/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap +++ b/client/test/app/hearings/components/dailyDocket/__snapshots__/DailyDocketRow.test.js.snap @@ -262,6 +262,12 @@ exports[`DailyDocketRow renders correctly for non virtual, DVC 1`] = ` Connect to Recording System +
    + + Pexip + hearing + +
    +
    + + Webex + hearing + +
    - Edit Hearing Details + Hearing Details and links @@ -1203,14 +1215,13 @@ exports[`DailyDocketRow renders correctly for non virtual, Transcriber 1`] = ` class="cf-form-radio-option" >
    @@ -1506,6 +1540,12 @@ exports[`DailyDocketRow renders correctly for non virtual, VSO 1`] = `
    +
    + + Pexip + hearing + +
    @@ -2148,6 +2210,12 @@ exports[`DailyDocketRow renders correctly for non virtual, attorney 1`] = ` Connect to Recording System +
    + + Pexip + hearing + +
    +
    + + Webex + hearing + +
    - Edit Hearing Details + Hearing Details and links @@ -3089,14 +3163,13 @@ exports[`DailyDocketRow renders correctly for non virtual, hearing cooridnator 1 class="cf-form-radio-option" >
    @@ -3431,6 +3527,12 @@ exports[`DailyDocketRow renders correctly for non virtual, judge 1`] = ` Connect to Recording System +
    + + Webex + hearing + +
    +
    + + Pexip + hearing +
    - Edit Hearing Details + Hearing Details and links diff --git a/client/test/app/hearings/components/dailyDocket/__snapshots__/StaticVirtualHearing.test.js.snap b/client/test/app/hearings/components/dailyDocket/__snapshots__/StaticVirtualHearing.test.js.snap index b6e14b9ce82..9ce34859cd3 100644 --- a/client/test/app/hearings/components/dailyDocket/__snapshots__/StaticVirtualHearing.test.js.snap +++ b/client/test/app/hearings/components/dailyDocket/__snapshots__/StaticVirtualHearing.test.js.snap @@ -31,7 +31,7 @@ Object { stroke-width="1" >
    `; + +exports[`StaticVirtualHearing renders past hearing correctly 1`] = ` +
    +
    + + undefined: N/A + +
    +
    +`; diff --git a/client/test/app/hearings/components/details/HearingLinks.test.js b/client/test/app/hearings/components/details/HearingLinks.test.js index a40b5f32600..3cb5bef358c 100644 --- a/client/test/app/hearings/components/details/HearingLinks.test.js +++ b/client/test/app/hearings/components/details/HearingLinks.test.js @@ -1,62 +1,162 @@ import React from 'react'; import { HearingLinks } from 'app/hearings/components/details/HearingLinks'; -import { anyUser, vsoUser } from 'test/data/user'; +import { anyUser, vsoUser, hearingUser } from 'test/data/user'; import { inProgressvirtualHearing } from 'test/data/virtualHearings'; -import { virtualHearing, amaHearing } from 'test/data/hearings'; +import { virtualHearing, amaHearing, virtualWebexHearing } from 'test/data/hearings'; import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import VirtualHearingLink from 'app/hearings/components/VirtualHearingLink'; -const hearing = { - scheduledForIsPast: false -}; - describe('HearingLinks', () => { - test('Matches snapshot with default props when passed in', () => { + test('Matches snapshot when hearing is virtual, pexip, and in progress', () => { + const hearing = { + scheduledForIsPast: false, + conferenceProvider: 'pexip', + isVirtual: true + }; + const form = mount( - + + ); + + expect(form).toMatchSnapshot(); + expect(form.find('LinkContainer')).toHaveLength(3); + expect( + form.find('LinkContainer').exists({ role: 'VLJ' }) + ).toBe(true); + expect( + form.find('VirtualHearingLinkDetails').exists({ label: 'Join Hearing' }) + ).toBe(true); + expect( + form.find('VirtualHearingLinkDetails').exists({ label: 'Start Hearing' }) + ).toBe(true); + expect(form.contains(Conference Room: )).toBe(true); + expect(form.contains(PIN: )).toBe(true); + }); + + test('Matches snapshot when hearing was virtual and occurred', () => { + const hearing = { + scheduledForIsPast: false, + wasVirtual: true, + conferenceProvider: 'pexip' + }; + + const form = mount( + ); expect(form).toMatchSnapshot(); expect(form.find(VirtualHearingLink)).toHaveLength(0); + expect( + form.find('span').filterWhere((node) => node.text() === 'N/A') + ).toHaveLength(3); }); - test('Matches snapshot when hearing is virtual and in progress', () => { + test('Matches snapshot when hearing is virtual, webex, and in progress', () => { + const hearing = { + scheduledForIsPast: false, + isVirtual: true, + conferenceProvider: 'webex' + }; + const form = mount( ); expect(form).toMatchSnapshot(); - expect(form.find(VirtualHearingLink)).toHaveLength(2); + expect(form.find('VirtualHearingLinkDetails')).toHaveLength(3); expect( - form.find(VirtualHearingLink).exists({ label: 'Join Virtual Hearing' }) + form.find('VirtualHearingLinkDetails').exists({ label: 'Join Hearing' }) ).toBe(true); expect( - form.find(VirtualHearingLink).exists({ label: 'Start Virtual Hearing' }) + form.find('VirtualHearingLinkDetails').exists({ label: 'Start Hearing' }) + ).toBe(true); + expect( + form.find('LinkContainer').exists({ link: virtualWebexHearing.virtualHearing.hostLink }) ).toBe(true); }); - test('Matches snapshot when hearing was virtual and occurred', () => { + test('Matches snapshot when hearing is non-virtual, webex, and in progress', () => { + const hearing = { + scheduledForIsPast: false, + conferenceProvider: 'webex', + nonVirtualConferenceLink: { + alias: null, + coHostLink: 'https://instant-usgov.webex.com/visit/yqju5qi', + conferenceProvider: 'webex', + guestLink: 'https://instant-usgov.webex.com/visit/m9p1k56', + guestPin: null, + hostLink: 'https://instant-usgov.webex.com/visit/owhuy7m', + hostPin: null, + type: 'WebexConferenceLink' + } + }; const form = mount( ); expect(form).toMatchSnapshot(); - expect(form.find(VirtualHearingLink)).toHaveLength(0); + expect(form.find('LinkContainer')).toHaveLength(3); + expect(form.find('VirtualHearingLinkDetails')).toHaveLength(3); + expect( + form.find('LinkContainer').exists({ link: hearing.nonVirtualConferenceLink.hostLink }) + ).toBe(true); + }); + + test('Matches snapshot when hearing is non-virtual, pexip, and in progress', () => { + const hearing = { + scheduledForIsPast: false, + wasVirtual: false, + conferenceProvider: 'pexip', + readableRequestType: 'Video', + dailyDocketConferenceLink: { + alias: 'BVA0001094@example.va.gov', + coHostLink: null, + conferenceProvider: 'pexip', + guestLink: 'https://example.va.gov/sample/?conference=BVA0001094@example.va.gov&pin=1342380867&callType=video', + guestPin: '1342380867', + hearingDayId: 151, + hostLink: 'https://example.va.gov/bva-app/?join=1&media=&escalate=1&conference=BVA0001094@example.va.gov&pin=1073526&role=host', + hostPin: '1073526', + type: 'PexipConferenceLink' + } + }; + + const form = mount( + + ); + + expect(form).toMatchSnapshot(); + expect(form.find('LinkContainer')).toHaveLength(3); + expect(form.find('VirtualHearingLinkDetails')).toHaveLength(3); expect( - form.find('span').filterWhere((node) => node.text() === 'Expired') - ).toHaveLength(2); + form.find('LinkContainer').exists({ link: hearing.dailyDocketConferenceLink.hostLink }) + ).toBe(true); }); test('Only displays Guest Link when user is not a host', () => { @@ -72,6 +172,20 @@ describe('HearingLinks', () => { expect(form).toMatchSnapshot(); expect(form.find(VirtualHearingLink)).toHaveLength(1); // Ensure it's the guest link - expect(form.find(VirtualHearingLink).prop('link')).toEqual(amaHearing.virtualHearing.guestLink) - }) + expect(form.find(VirtualHearingLink).prop('link')).toEqual(amaHearing.virtualHearing.guestLink); + }); + + test('Display NA for links when hearing is cancelled', () => { + render( + + ); + + expect(screen.getAllByText('N/A').length).toBe(9); + }); }); diff --git a/client/test/app/hearings/components/details/VirtualHearingFields.test.js b/client/test/app/hearings/components/details/VirtualHearingFields.test.js index b93d3c3fb82..c6183213284 100644 --- a/client/test/app/hearings/components/details/VirtualHearingFields.test.js +++ b/client/test/app/hearings/components/details/VirtualHearingFields.test.js @@ -31,9 +31,8 @@ describe('VirtualHearingFields', () => { ); // Assertions - expect(virtualHearingForm.children()).toHaveLength(0); + expect(virtualHearingForm.children()).toHaveLength(1); expect(virtualHearingForm).toMatchSnapshot(); - }); test('Shows only hearing links with no virtualHearing', () => { @@ -42,6 +41,7 @@ describe('VirtualHearingFields', () => { , { @@ -72,11 +72,72 @@ describe('VirtualHearingFields', () => { } ); + const hearingMeetingType = amaHearing.judge.meetingType; + // Assertions expect(virtualHearingForm.find(ContentSection)).toHaveLength(1); expect(virtualHearingForm.find(HearingLinks)).toHaveLength(1); + expect(hearingMeetingType).toBeTruthy(); + expect(hearingMeetingType).toStrictEqual('pexip' || 'webex'); + + expect(virtualHearingForm).toMatchSnapshot(); + }); + + test('Renders webex conference when conference provider is webex', () => { + const webexHearing = { + ...amaHearing, + conferenceProvider: 'webex' + }; + + // Run the test + const virtualHearingForm = mount( + , + + { + wrappingComponent: hearingDetailsWrapper(anyUser, webexHearing), + wrappingComponentProps: { store: detailsStore } + } + ); + + // Assertions + expect(virtualHearingForm.text().includes('Webex Hearing')).toBeTruthy(); + + expect(virtualHearingForm).toMatchSnapshot(); + }); + + test('Renders pexip conference when conference provider is pexip', () => { + const webexHearing = { + ...amaHearing, + conferenceProvider: 'pexip' + }; + + // Run the test + const virtualHearingForm = mount( + , + + { + wrappingComponent: hearingDetailsWrapper(anyUser, webexHearing), + wrappingComponentProps: { store: detailsStore } + } + ); + + // Assertions + expect(virtualHearingForm.text().includes('Pexip Hearing')).toBeTruthy(); expect(virtualHearingForm).toMatchSnapshot(); }); -}) -; +}); diff --git a/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap b/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap index 13ef713c952..22670476ceb 100644 --- a/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap +++ b/client/test/app/hearings/components/details/__snapshots__/DetailsForm.test.js.snap @@ -24,6 +24,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -8442,6 +8443,7 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` "centralOfficeTimeString": "04:00", "claimantId": 604, "closestRegionalOffice": null, + "conferenceProvider": "pexip", "currentIssueCount": 0, "disposition": null, "dispositionEditable": true, @@ -8513,7 +8515,564 @@ exports[`DetailsForm Matches snapshot with default props when passed in 1`] = ` } } virtualHearing={null} - /> + > + +
    +

    + Hearing Links +

    +
    +
    + + Pexip + Hearing + +
    + +
    + +