Skip to content

Commit 39ea20f

Browse files
authored
feat(case): adds resolved by user to cases (#6166)
1 parent 4877caa commit 39ea20f

File tree

6 files changed

+140
-45
lines changed

6 files changed

+140
-45
lines changed

src/dispatch/case/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ class Case(Base, TimeStampMixin, ProjectMixin):
176176

177177
ticket = relationship("Ticket", uselist=False, backref="case", cascade="all, delete-orphan")
178178

179+
# Foreign key to individual who resolved
180+
resolved_by_id = Column(Integer, ForeignKey("individual_contact.id"))
181+
resolved_by = relationship("IndividualContact", foreign_keys=[resolved_by_id])
182+
179183
# resources
180184
case_costs = relationship(
181185
"CaseCost",
@@ -325,6 +329,7 @@ class CaseBase(DispatchBase):
325329
description: str | None = None
326330
resolution: str | None = None
327331
resolution_reason: CaseResolutionReason | None = None
332+
resolved_by: IndividualContactRead | None = None
328333
status: CaseStatus | None = None
329334
visibility: Visibility | None = None
330335

src/dispatch/case/service.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,14 @@ def update(*, db_session, case: Case, case_in: CaseUpdate, current_user: Dispatc
388388
case_id=case.id,
389389
)
390390

391+
if case.status == CaseStatus.closed:
392+
individual = individual_service.get_or_create(
393+
db_session=db_session,
394+
email=current_user.email,
395+
project=case.project,
396+
)
397+
case.resolved_by = individual
398+
391399
if case.visibility != case_in.visibility:
392400
case.visibility = case_in.visibility
393401

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Add resolved_by to case table
2+
3+
Revision ID: 4649b11b683f
4+
Revises: 408118048599
5+
Create Date: 2025-08-01 14:11:04.276577
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "4649b11b683f"
15+
down_revision = "408118048599"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.add_column("case", sa.Column("resolved_by_id", sa.Integer(), nullable=True))
23+
op.create_foreign_key(
24+
"fk_case_resolved_by_id", "case", "individual_contact", ["resolved_by_id"], ["id"]
25+
)
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
op.drop_constraint("fk_case_resolved_by_id", "case", type_="foreignkey")
32+
op.drop_column("case", "resolved_by_id")
33+
# ### end Alembic commands ###

src/dispatch/plugins/dispatch_slack/case/messages.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,22 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]:
147147
Section(
148148
text=f"*Resolution description* \n {case.resolution}"[:MAX_SECTION_TEXT_LENGTH]
149149
),
150-
Actions(
151-
elements=[
152-
Button(
153-
text="Re-open",
154-
action_id=CaseNotificationActions.reopen,
155-
style="primary",
156-
value=button_metadata,
157-
)
158-
]
159-
),
160150
]
161151
)
152+
if case.resolved_by:
153+
blocks.append(Section(text=f"*Resolved by* \n {case.resolved_by.individual.email}"))
154+
blocks.append(
155+
Actions(
156+
elements=[
157+
Button(
158+
text="Re-open",
159+
action_id=CaseNotificationActions.reopen,
160+
style="primary",
161+
value=button_metadata,
162+
)
163+
]
164+
),
165+
)
162166
else:
163167
action_buttons = [
164168
Button(

src/dispatch/static/dispatch/src/case/CaseAttributesDrawer.vue

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,27 @@ const handleResolutionUpdate = (newResolution) => {
104104
</v-col>
105105
</v-row>
106106

107+
<v-row no-gutters align="center" class="pt-6">
108+
<v-col cols="1">
109+
<div class="dispatch-font">Resolved By</div>
110+
</v-col>
111+
<v-col cols="10">
112+
<div class="pl-8 d-flex align-center ml-3">
113+
<v-icon size="14px" class="mr-2">mdi-account-check</v-icon>
114+
<span
115+
style="
116+
font-weight: 500;
117+
color: rgb(60, 65, 73);
118+
font-size: 0.8125rem;
119+
margin-left: 2px;
120+
"
121+
>
122+
{{ modelValue.resolved_by?.name || "Not specified" }}
123+
</span>
124+
</div>
125+
</v-col>
126+
</v-row>
127+
107128
<v-row no-gutters align="center" class="pt-6">
108129
<v-col cols="1">
109130
<div class="dispatch-font">Priority</div>

src/dispatch/static/dispatch/src/case/DetailsTab.vue

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,42 +24,63 @@
2424
/>
2525
</v-col>
2626
<v-col cols="12">
27-
<v-select
28-
v-model="resolution_reason"
29-
label="Resolution Reason"
30-
:items="$store.state.case_management.resolutionReasons"
31-
hint="The general reason why a given case was resolved."
32-
:menu-props="{ contentClass: 'resolution-menu' }"
33-
>
34-
<template #item="{ item, props }">
35-
<v-list-item v-bind="props">
36-
<template #title>
37-
<div class="d-flex align-center justify-space-between">
38-
{{ item.title }}
39-
<v-tooltip location="right">
40-
<template #activator="{ props: tooltipProps }">
41-
<v-icon
42-
v-bind="tooltipProps"
43-
icon="mdi-information"
44-
size="small"
45-
class="ml-2"
46-
/>
27+
<v-card variant="outlined" class="pa-4 mt-n5">
28+
<v-card-title class="text-h6 pa-0 mb-4">Resolution Details</v-card-title>
29+
<v-row>
30+
<v-col cols="12">
31+
<div class="d-flex align-center">
32+
<span class="text-body-2 text-medium-emphasis mr-2">Resolved By:</span>
33+
<v-chip v-if="resolved_by" pill size="small">
34+
<v-avatar color="teal" start>
35+
<span class="text-white">{{ initials(resolved_by.name) }}</span>
36+
</v-avatar>
37+
{{ resolved_by.name }}
38+
</v-chip>
39+
<span v-else class="text-body-2 text-disabled">Not specified</span>
40+
</div>
41+
</v-col>
42+
<v-col cols="12">
43+
<v-select
44+
v-model="resolution_reason"
45+
label="Resolution Reason"
46+
:items="$store.state.case_management.resolutionReasons"
47+
hint="The general reason why a given case was resolved."
48+
:menu-props="{ contentClass: 'resolution-menu' }"
49+
>
50+
<template #item="{ item, props }">
51+
<v-list-item v-bind="props">
52+
<template #title>
53+
<div class="d-flex align-center justify-space-between">
54+
{{ item.title }}
55+
<v-tooltip location="right">
56+
<template #activator="{ props: tooltipProps }">
57+
<v-icon
58+
v-bind="tooltipProps"
59+
icon="mdi-information"
60+
size="small"
61+
class="ml-2"
62+
/>
63+
</template>
64+
<span>{{
65+
$store.state.case_management.resolutionTooltips[item.title]
66+
}}</span>
67+
</v-tooltip>
68+
</div>
4769
</template>
48-
<span>{{ $store.state.case_management.resolutionTooltips[item.title] }}</span>
49-
</v-tooltip>
50-
</div>
51-
</template>
52-
</v-list-item>
53-
</template>
54-
</v-select>
55-
</v-col>
56-
<v-col cols="12">
57-
<v-textarea
58-
v-model="resolution"
59-
label="Resolution"
60-
hint="Description of the actions taken to resolve the case."
61-
clearable
62-
/>
70+
</v-list-item>
71+
</template>
72+
</v-select>
73+
</v-col>
74+
<v-col cols="12">
75+
<v-textarea
76+
v-model="resolution"
77+
label="Resolution"
78+
hint="Description of the actions taken to resolve the case."
79+
clearable
80+
/>
81+
</v-col>
82+
</v-row>
83+
</v-card>
6384
</v-col>
6485
<v-col cols="12">
6586
<participant-select
@@ -195,6 +216,7 @@
195216
<script>
196217
import { required } from "@/util/form"
197218
import { mapFields } from "vuex-map-fields"
219+
import { initials } from "@/filters"
198220
199221
import CaseFilterCombobox from "@/case/CaseFilterCombobox.vue"
200222
import CasePrioritySelect from "@/case/priority/CasePrioritySelect.vue"
@@ -210,6 +232,7 @@ export default {
210232
setup() {
211233
return {
212234
rules: { required },
235+
initials,
213236
}
214237
},
215238
name: "CaseDetailsTab",
@@ -264,6 +287,7 @@ export default {
264287
"selected.reported_at",
265288
"selected.resolution_reason",
266289
"selected.resolution",
290+
"selected.resolved_by",
267291
"selected.signals",
268292
"selected.stable_at",
269293
"selected.status",

0 commit comments

Comments
 (0)