timgremore commited on
Commit
8ba1194
1 Parent(s): 1e8f5e5

feat: Mark codes as final for reporting purposes

Browse files
lib/medicode/transcriptions.ex CHANGED
@@ -14,6 +14,8 @@ defmodule Medicode.Transcriptions do
14
  TranscriptionChunkKeyword
15
  }
16
 
 
 
17
  @doc """
18
  Create transcription record and begin transcribing
19
 
@@ -177,6 +179,31 @@ defmodule Medicode.Transcriptions do
177
  end
178
  end
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  @doc """
181
  List transcription chunks by transcription ID.
182
 
@@ -236,6 +263,33 @@ defmodule Medicode.Transcriptions do
236
  Repo.all(query)
237
  end
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  @doc """
240
  Creates a transcription.
241
 
 
14
  TranscriptionChunkKeyword
15
  }
16
 
17
+ alias Medicode.Coding.CodeVector
18
+
19
  @doc """
20
  Create transcription record and begin transcribing
21
 
 
179
  end
180
  end
181
 
182
+ def finalize_code_vector_by_chunk_id_and_by_user_id(
183
+ chunk_id,
184
+ code_vector_id,
185
+ user_id
186
+ ) do
187
+ query =
188
+ TranscriptionChunkCodeVector
189
+ |> where(
190
+ [v],
191
+ v.transcription_chunk_id == ^chunk_id and v.code_vector_id == ^code_vector_id
192
+ )
193
+
194
+ with %TranscriptionChunkCodeVector{} = chunk_vector <- Medicode.Repo.one(query),
195
+ changeset <-
196
+ TranscriptionChunkCodeVector.changeset(chunk_vector, %{
197
+ finalized_at: DateTime.utc_now(),
198
+ finalized_by_user_id: user_id
199
+ }),
200
+ {:ok, chunk_vector} <- Medicode.Repo.update(changeset) do
201
+ {:ok, chunk_vector}
202
+ else
203
+ res -> res
204
+ end
205
+ end
206
+
207
  @doc """
208
  List transcription chunks by transcription ID.
209
 
 
263
  Repo.all(query)
264
  end
265
 
266
+ @doc """
267
+ List transcription chunk vector codes by transcription ID.
268
+ """
269
+ def list_transcription_finalized_codes(transcription_id) do
270
+ code_vector_query =
271
+ from(
272
+ cv in CodeVector,
273
+ join: tc in TranscriptionChunkCodeVector,
274
+ on: tc.code_vector_id == cv.id,
275
+ join: chunk in TranscriptionChunk,
276
+ on: tc.transcription_chunk_id == chunk.id,
277
+ where: chunk.transcription_id == ^transcription_id and not is_nil(tc.finalized_at),
278
+ select: [:id, :code]
279
+ )
280
+
281
+ query =
282
+ from(
283
+ tc in TranscriptionChunkCodeVector,
284
+ join: chunk in TranscriptionChunk,
285
+ on: tc.transcription_chunk_id == chunk.id,
286
+ where: chunk.transcription_id == ^transcription_id and not is_nil(tc.finalized_at),
287
+ preload: [code_vector: ^code_vector_query]
288
+ )
289
+
290
+ Repo.all(query)
291
+ end
292
+
293
  @doc """
294
  Creates a transcription.
295
 
lib/medicode/transcriptions/transcription_chunk_code_vector.ex CHANGED
@@ -11,10 +11,12 @@ defmodule Medicode.Transcriptions.TranscriptionChunkCodeVector do
11
  schema "transcription_chunk_code_vectors" do
12
  field :cosine_similarity, :float
13
  field :weighting, {:array, :string}
 
14
 
15
  belongs_to :transcription_chunk, Medicode.Transcriptions.TranscriptionChunk
16
  belongs_to :code_vector, Medicode.Coding.CodeVector
17
  belongs_to :assigned_by_user, Medicode.Accounts.User
 
18
 
19
  timestamps(type: :utc_datetime)
20
  end
@@ -27,7 +29,9 @@ defmodule Medicode.Transcriptions.TranscriptionChunkCodeVector do
27
  :code_vector_id,
28
  :cosine_similarity,
29
  :weighting,
30
- :assigned_by_user_id
 
 
31
  ])
32
  |> validate_required([
33
  :transcription_chunk_id,
 
11
  schema "transcription_chunk_code_vectors" do
12
  field :cosine_similarity, :float
13
  field :weighting, {:array, :string}
14
+ field :finalized_at, :utc_datetime
15
 
16
  belongs_to :transcription_chunk, Medicode.Transcriptions.TranscriptionChunk
17
  belongs_to :code_vector, Medicode.Coding.CodeVector
18
  belongs_to :assigned_by_user, Medicode.Accounts.User
19
+ belongs_to :finalized_by_user, Medicode.Accounts.User
20
 
21
  timestamps(type: :utc_datetime)
22
  end
 
29
  :code_vector_id,
30
  :cosine_similarity,
31
  :weighting,
32
+ :assigned_by_user_id,
33
+ :finalized_by_user_id,
34
+ :finalized_at
35
  ])
36
  |> validate_required([
37
  :transcription_chunk_id,
lib/medicode_web/components/components.ex CHANGED
@@ -72,6 +72,7 @@ defmodule MedicodeWeb.Components do
72
  attr(:target, :any, required: true)
73
  attr(:transcription, Medicode.Transcriptions.Transcription, required: true)
74
  attr(:summary_keywords, :list, default: [])
 
75
 
76
  @doc """
77
  Shows the status and keywords for the current session.
@@ -132,6 +133,18 @@ defmodule MedicodeWeb.Components do
132
  <% end %>
133
  </div>
134
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
135
  </div>
136
  """
137
  end
 
72
  attr(:target, :any, required: true)
73
  attr(:transcription, Medicode.Transcriptions.Transcription, required: true)
74
  attr(:summary_keywords, :list, default: [])
75
+ attr(:finalized_codes, :list, default: [])
76
 
77
  @doc """
78
  Shows the status and keywords for the current session.
 
133
  <% end %>
134
  </div>
135
  </div>
136
+ <div :if={!Enum.empty?(@finalized_codes)} class="px-4 pt-2 pb-10 flex flex-col gap-2">
137
+ <p class="leading-normal font-bold text-type-black-primary uppercase">Finalized Codes</p>
138
+
139
+ <div
140
+ class="flex flex-row items-center divide-x divide-black/15 text-sm leading-normal text-type-black-tertiary"
141
+ id="finalized_vector_code_list"
142
+ >
143
+ <span :for={chunk_code <- @finalized_codes} class="px-2" title={chunk_code.code_vector.code}>
144
+ <%= chunk_code.code_vector.code %>
145
+ </span>
146
+ </div>
147
+ </div>
148
  </div>
149
  """
150
  end
lib/medicode_web/components/transcription_chunk_codings_component.ex CHANGED
@@ -18,8 +18,9 @@ defmodule MedicodeWeb.Components.TranscriptionTextCodingsComponent do
18
  {%TranscriptionChunkCodeVector{
19
  code_vector: code_vector,
20
  assigned_by_user: assigned_by_user
21
- }, feedback} <- @code_vectors_with_feedback
22
  }
 
23
  chunk_id={@chunk_id}
24
  code_vector={code_vector}
25
  score={1.0}
@@ -38,7 +39,7 @@ defmodule MedicodeWeb.Components.TranscriptionTextCodingsComponent do
38
  ~H"""
39
  <div
40
  phx-mounted={JS.transition({"ease-out", "opacity-0", "opacity-100"})}
41
- class="ease-out duration-300 opacity-0 transition-all flex items-center gap-4 px-[14px] py-3 text-sm"
42
  >
43
  <div class="flex gap-3">
44
  <.feedback_button
@@ -62,10 +63,29 @@ defmodule MedicodeWeb.Components.TranscriptionTextCodingsComponent do
62
  <div class="w-1 h-full border-l border-[#444444]/20"></div>
63
 
64
  <div
 
65
  class={"w-full flex flex-col gap-1 font-secondary text-type-black-primary ml-4 px-2 py-1 rounded #{code_color(@weighting)}"}
66
  title={code_title(@score, @weighting)}
67
  >
68
- <p class="text-lg font-bold leading-[22.97px]"><%= @code_vector.code %></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  <p class="text-base leading-[20.42px]"><%= @code_vector.description %></p>
70
  <div class="flex flex-row items-center w-full gap-4">
71
  <p :if={!is_nil(@assigned_by_user)} class="text-xs text-base leading-[20.42px]">
@@ -133,6 +153,11 @@ defmodule MedicodeWeb.Components.TranscriptionTextCodingsComponent do
133
  {:noreply, socket}
134
  end
135
 
 
 
 
 
 
136
  # Supporting function for determining background color for code in `tag_result/1`
137
  defp code_color(weighting) do
138
  cond do
 
18
  {%TranscriptionChunkCodeVector{
19
  code_vector: code_vector,
20
  assigned_by_user: assigned_by_user
21
+ } = transcription_chunk_code_vector, feedback} <- @code_vectors_with_feedback
22
  }
23
+ transcription_chunk_code_vector={transcription_chunk_code_vector}
24
  chunk_id={@chunk_id}
25
  code_vector={code_vector}
26
  score={1.0}
 
39
  ~H"""
40
  <div
41
  phx-mounted={JS.transition({"ease-out", "opacity-0", "opacity-100"})}
42
+ class="group ease-out duration-300 opacity-0 transition-all flex items-center gap-4 px-[14px] py-3 text-sm"
43
  >
44
  <div class="flex gap-3">
45
  <.feedback_button
 
63
  <div class="w-1 h-full border-l border-[#444444]/20"></div>
64
 
65
  <div
66
+ id={"chunk-#{@chunk_id}-coding-#{@code_vector.id}"}
67
  class={"w-full flex flex-col gap-1 font-secondary text-type-black-primary ml-4 px-2 py-1 rounded #{code_color(@weighting)}"}
68
  title={code_title(@score, @weighting)}
69
  >
70
+ <div class="flex flex-row justify-between">
71
+ <p class="text-lg font-bold leading-[22.97px]"><%= @code_vector.code %></p>
72
+ <button
73
+ :if={is_nil(@transcription_chunk_code_vector.finalized_at)}
74
+ type="button"
75
+ class="transition-all duration-300 opacity-0 group-hover:opacity-100 hover:bg-slate-200 px-2 border border-1 border-iron-mountain rounded-md"
76
+ phx-click="finalize_code"
77
+ phx-value-chunk_id={@chunk_id}
78
+ phx-value-code_vector_id={@code_vector.id}
79
+ phx-target={@myself}
80
+ >
81
+ Finalize
82
+ </button>
83
+ <.icon
84
+ :if={!is_nil(@transcription_chunk_code_vector.finalized_at)}
85
+ name="hero-check-badge"
86
+ class="opacity-40 group-hover:opacity-70"
87
+ />
88
+ </div>
89
  <p class="text-base leading-[20.42px]"><%= @code_vector.description %></p>
90
  <div class="flex flex-row items-center w-full gap-4">
91
  <p :if={!is_nil(@assigned_by_user)} class="text-xs text-base leading-[20.42px]">
 
153
  {:noreply, socket}
154
  end
155
 
156
+ def handle_event("finalize_code", params, socket) do
157
+ socket.assigns.on_finalize_code.(params)
158
+ {:noreply, socket}
159
+ end
160
+
161
  # Supporting function for determining background color for code in `tag_result/1`
162
  defp code_color(weighting) do
163
  cond do
lib/medicode_web/components/transcription_text_component.ex CHANGED
@@ -48,6 +48,7 @@ defmodule MedicodeWeb.Components.TranscriptionTextComponent do
48
  |> assign(:code_vectors_with_feedback, code_vectors_with_feedback)
49
  |> assign(:on_feedback, assigns.on_feedback)
50
  |> assign(:on_remove_code, assigns.on_remove_code)
 
51
  end)
52
  end
53
 
@@ -111,6 +112,7 @@ defmodule MedicodeWeb.Components.TranscriptionTextComponent do
111
  code_vectors_with_feedback={@code_vectors_with_feedback}
112
  on_feedback={@on_feedback}
113
  on_remove_code={@on_remove_code}
 
114
  />
115
  <.live_component
116
  :if={!@classification_loading}
 
48
  |> assign(:code_vectors_with_feedback, code_vectors_with_feedback)
49
  |> assign(:on_feedback, assigns.on_feedback)
50
  |> assign(:on_remove_code, assigns.on_remove_code)
51
+ |> assign(:on_finalize_code, assigns.on_finalize_code)
52
  end)
53
  end
54
 
 
112
  code_vectors_with_feedback={@code_vectors_with_feedback}
113
  on_feedback={@on_feedback}
114
  on_remove_code={@on_remove_code}
115
+ on_finalize_code={@on_finalize_code}
116
  />
117
  <.live_component
118
  :if={!@classification_loading}
lib/medicode_web/live/transcriptions_live/show.ex CHANGED
@@ -20,6 +20,7 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
20
  transcription_chunk_ids = Enum.map(transcription.chunks, &%{id: &1.id})
21
 
22
  summary_keywords = Transcriptions.list_transcription_summary_keywords(transcription.id)
 
23
 
24
  initial_state = %{
25
  current_recording_id: 0,
@@ -29,7 +30,7 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
29
  summary_keywords: summary_keywords,
30
  transcription: transcription,
31
  transcriptions: list_transcriptions(session["current_user"]),
32
- finalized_codes: []
33
  }
34
 
35
  socket =
@@ -57,6 +58,7 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
57
  target={self()}
58
  transcription={@transcription}
59
  summary_keywords={@summary_keywords}
 
60
  />
61
 
62
  <img
@@ -76,6 +78,7 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
76
  finalized_codes={@finalized_codes}
77
  on_feedback={&on_feedback/1}
78
  on_remove_code={&on_remove_code/1}
 
79
  />
80
  </div>
81
  </div>
@@ -92,6 +95,10 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
92
  send(self(), {"remove_code", params})
93
  end
94
 
 
 
 
 
95
  @impl Phoenix.LiveView
96
  def handle_event("reset_to_pending", _params, socket) do
97
  {:noreply, assign(socket, :status, :pending)}
@@ -260,6 +267,28 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
260
  {:noreply, socket}
261
  end
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  def handle_info({ref, _result}, socket) do
264
  # See this Fly article for the usage of Task.async to start `transcribe_and_tag_audio/2` and handle the end of the
265
  # task here: https://fly.io/phoenix-files/liveview-async-task/
@@ -275,7 +304,9 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
275
  classification_loading: classification_loading,
276
  current_user: socket.assigns.current_user,
277
  on_feedback: &on_feedback/1,
278
- on_remove_code: &on_remove_code/1
 
 
279
  )
280
 
281
  chunk = Transcriptions.get_transcription_chunk!(chunk_id)
 
20
  transcription_chunk_ids = Enum.map(transcription.chunks, &%{id: &1.id})
21
 
22
  summary_keywords = Transcriptions.list_transcription_summary_keywords(transcription.id)
23
+ finalized_codes = Transcriptions.list_transcription_finalized_codes(transcription.id)
24
 
25
  initial_state = %{
26
  current_recording_id: 0,
 
30
  summary_keywords: summary_keywords,
31
  transcription: transcription,
32
  transcriptions: list_transcriptions(session["current_user"]),
33
+ finalized_codes: finalized_codes
34
  }
35
 
36
  socket =
 
58
  target={self()}
59
  transcription={@transcription}
60
  summary_keywords={@summary_keywords}
61
+ finalized_codes={@finalized_codes}
62
  />
63
 
64
  <img
 
78
  finalized_codes={@finalized_codes}
79
  on_feedback={&on_feedback/1}
80
  on_remove_code={&on_remove_code/1}
81
+ on_finalize_code={&on_finalize_code/1}
82
  />
83
  </div>
84
  </div>
 
95
  send(self(), {"remove_code", params})
96
  end
97
 
98
+ def on_finalize_code(params) do
99
+ send(self(), {"finalize_code", params})
100
+ end
101
+
102
  @impl Phoenix.LiveView
103
  def handle_event("reset_to_pending", _params, socket) do
104
  {:noreply, assign(socket, :status, :pending)}
 
267
  {:noreply, socket}
268
  end
269
 
270
+ def handle_info(
271
+ {"finalize_code", %{"chunk_id" => chunk_id, "code_vector_id" => code_vector_id}},
272
+ socket
273
+ ) do
274
+ Transcriptions.finalize_code_vector_by_chunk_id_and_by_user_id(
275
+ chunk_id,
276
+ code_vector_id,
277
+ socket.assigns.current_user.id
278
+ )
279
+
280
+ send_transcription_text_update(chunk_id, false, socket)
281
+
282
+ socket =
283
+ socket
284
+ |> assign(
285
+ :finalized_codes,
286
+ Transcriptions.list_transcription_finalized_codes(socket.assigns.transcription.id)
287
+ )
288
+
289
+ {:noreply, socket}
290
+ end
291
+
292
  def handle_info({ref, _result}, socket) do
293
  # See this Fly article for the usage of Task.async to start `transcribe_and_tag_audio/2` and handle the end of the
294
  # task here: https://fly.io/phoenix-files/liveview-async-task/
 
304
  classification_loading: classification_loading,
305
  current_user: socket.assigns.current_user,
306
  on_feedback: &on_feedback/1,
307
+ on_remove_code: &on_remove_code/1,
308
+ on_finalize_code: &on_finalize_code/1,
309
+ finalized_codes: socket.assigns.finalized_codes
310
  )
311
 
312
  chunk = Transcriptions.get_transcription_chunk!(chunk_id)
priv/repo/migrations/20240301170622_add_finalized_at_and_finalized_by_id_to_transcription_chunk_code_vectors.exs ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule Medicode.Repo.Migrations.AddFinalizedAtAndFinalizedByIdToTranscriptionChunkCodeVectors do
2
+ use Ecto.Migration
3
+
4
+ def change do
5
+ alter table(:transcription_chunk_code_vectors) do
6
+ add :finalized_at, :utc_datetime, null: true
7
+
8
+ add :finalized_by_user_id, references(:users, type: :binary_id, on_delete: :nilify_all),
9
+ null: true
10
+ end
11
+ end
12
+ end
test/medicode_web/live/transcriptions_live_show_test.exs CHANGED
@@ -6,7 +6,9 @@ defmodule MedicodeWeb.TranscriptionsLive.ShowTest do
6
  import Medicode.{
7
  AccountsFixtures,
8
  TranscriptionsFixtures,
9
- TranscriptionChunksFixtures
 
 
10
  }
11
 
12
  setup %{conn: conn} do
@@ -14,10 +16,37 @@ defmodule MedicodeWeb.TranscriptionsLive.ShowTest do
14
  user = user_fixture(%{password: password})
15
  transcription = transcription_fixture(%{filename: "my-audio.mp3"})
16
 
17
- transcription_chunk_fixture(%{transcription_id: transcription.id, text: "Foo"})
18
- transcription_chunk_fixture(%{transcription_id: transcription.id, text: "Bar"})
19
 
20
- %{conn: log_in_user(conn, user), current_user: user, transcription: transcription}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  end
22
 
23
  describe "/" do
@@ -35,5 +64,38 @@ defmodule MedicodeWeb.TranscriptionsLive.ShowTest do
35
  invalid_id = Ecto.UUID.generate()
36
  assert_raise Medicode.Fallback, fn -> get(conn, "/transcriptions/#{invalid_id}") end
37
  end
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  end
39
  end
 
6
  import Medicode.{
7
  AccountsFixtures,
8
  TranscriptionsFixtures,
9
+ TranscriptionChunksFixtures,
10
+ TranscriptionChunkCodeVectorsFixtures,
11
+ CodeVectorsFixtures
12
  }
13
 
14
  setup %{conn: conn} do
 
16
  user = user_fixture(%{password: password})
17
  transcription = transcription_fixture(%{filename: "my-audio.mp3"})
18
 
19
+ transcription_chunk1 =
20
+ transcription_chunk_fixture(%{transcription_id: transcription.id, text: "Bar"})
21
 
22
+ transcription_chunk2 =
23
+ transcription_chunk_fixture(%{transcription_id: transcription.id, text: "Foo"})
24
+
25
+ code_vector1 = code_vector_fixture(%{code: "001"})
26
+ code_vector2 = code_vector_fixture(%{code: "002"})
27
+
28
+ transcription_chunk_code_vector_fixture(%{
29
+ transcription_chunk_id: transcription_chunk1.id,
30
+ code_vector_id: code_vector1.id
31
+ })
32
+
33
+ transcription_chunk_code_vector_fixture(%{
34
+ transcription_chunk_id: transcription_chunk1.id,
35
+ code_vector_id: code_vector2.id
36
+ })
37
+
38
+ transcription_chunk_code_vector_fixture(%{
39
+ transcription_chunk_id: transcription_chunk2.id,
40
+ code_vector_id: code_vector2.id
41
+ })
42
+
43
+ %{
44
+ conn: log_in_user(conn, user),
45
+ current_user: user,
46
+ transcription: transcription,
47
+ transcription_chunk1: transcription_chunk1,
48
+ code_vector1: code_vector1
49
+ }
50
  end
51
 
52
  describe "/" do
 
64
  invalid_id = Ecto.UUID.generate()
65
  assert_raise Medicode.Fallback, fn -> get(conn, "/transcriptions/#{invalid_id}") end
66
  end
67
+
68
+ test "renders codes as finalized", %{
69
+ conn: conn,
70
+ transcription: transcription,
71
+ transcription_chunk1: transcription_chunk1,
72
+ code_vector1: code_vector1
73
+ } do
74
+ conn = get(conn, "/transcriptions/#{transcription.id}")
75
+ assert html_response(conn, 200) =~ "MediCode"
76
+
77
+ {:ok, view, _html} = live(conn)
78
+
79
+ # NOTE: Since MedicodeWeb.TranscriptionsLive.Show.send_transcription_text_update/3 relies on Phoenix.LiveView.send_update/3,
80
+ # we need to call `render(view)` to get the async update rendered before asserting.
81
+ #
82
+ # Additionally, we are using PubSub to update child components. The call to `:sys.get_state(view.pid)` ensures the mailbox is empty
83
+ # and it's safe to assert results.
84
+ # See: https://elixirforum.com/t/testing-liveviews-that-rely-on-pubsub-for-updates/40938/5
85
+ view
86
+ |> element(
87
+ "#chunk-#{transcription_chunk1.id}-coding-#{code_vector1.id} button",
88
+ "Finalize"
89
+ )
90
+ |> render_click()
91
+
92
+ _ = :sys.get_state(view.pid)
93
+
94
+ rendered = render(view)
95
+
96
+ assert rendered =~ "<span class=\"hero-check-badge"
97
+ assert rendered =~ "Finalized Codes"
98
+ assert rendered =~ "001"
99
+ end
100
  end
101
  end