Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public final class EventConstants {
public static final String CASEWORKER_EDIT_PANEL_COMPOSITION = "caseworker-edit-panel-composition";
public static final String CASEWORKER_UPDATE_CASE_DATA = "update-case-data";
public static final String SUPERUSER_REINDEX_CASES = "superuser-reindex-cases";
public static final String SUPERUSER_VIEW_OPEN_CASES_STATISTICS = "superuser-view-open-cases-statistics";
public static final String CHANGE_SECURITY_CLASS = "change-security-class";
public static final String CASEWORKER_REFER_TO_JUDGE = "refer-to-judge";
public static final String CREATE_BUNDLE = "createBundle";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,15 @@ public class CaseData {
)
private String deleteFieldName;

@CCD(
label = "Case statistics (copy/paste)",
hint = "Copy and paste this into Excel (tab-separated values).",
typeOverride = TextArea,
access = {SuperUserOnlyAccess.class}
)
@External
private String openCasesStatisticsTsv;

@CCD(
label = "Reindex cases modified since",
access = {SuperUserOnlyAccess.class}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package uk.gov.hmcts.sptribs.common.event;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.postgresql.PGConnection;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import uk.gov.hmcts.ccd.sdk.api.CCDConfig;
import uk.gov.hmcts.ccd.sdk.api.CaseDetails;
import uk.gov.hmcts.ccd.sdk.api.ConfigBuilder;
import uk.gov.hmcts.ccd.sdk.api.callback.AboutToStartOrSubmitResponse;
import uk.gov.hmcts.sptribs.ciccase.model.CaseData;
import uk.gov.hmcts.sptribs.ciccase.model.State;
import uk.gov.hmcts.sptribs.ciccase.model.UserRole;
import uk.gov.hmcts.sptribs.common.ccd.PageBuilder;

import java.io.StringWriter;
import java.sql.Connection;

import static uk.gov.hmcts.sptribs.caseworker.util.EventConstants.SUPERUSER_VIEW_OPEN_CASES_STATISTICS;
import static uk.gov.hmcts.sptribs.ciccase.model.UserRole.SUPER_USER;
import static uk.gov.hmcts.sptribs.ciccase.model.access.Permissions.CREATE_READ_UPDATE_DELETE;

@Component
@RequiredArgsConstructor
public class ViewOpenCasesStatisticsEvent implements CCDConfig<CaseData, State, UserRole> {

private final JdbcTemplate jdbcTemplate;

@Override
public void configure(final ConfigBuilder<CaseData, State, UserRole> configBuilder) {
PageBuilder pageBuilder = new PageBuilder(configBuilder
.event(SUPERUSER_VIEW_OPEN_CASES_STATISTICS)
.forAllStates()
.name("View case statistics")
.description("Shows a tab-separated report for copy/paste into Excel (includes closure reasons for CaseClosed)")
.aboutToStartCallback(this::aboutToStart)
.grant(CREATE_READ_UPDATE_DELETE, SUPER_USER));

pageBuilder.page("viewOpenCasesStatistics")
.pageLabel("View case statistics")
.label(
"viewOpenCasesStatisticsLabel",
"Copy the report below into Excel (Ctrl/Cmd+A then Ctrl/Cmd+C). Closed cases include closure columns."
)
.optional(CaseData::getOpenCasesStatisticsTsv)
.done();
}

public AboutToStartOrSubmitResponse<CaseData, State> aboutToStart(
CaseDetails<CaseData, State> details
) {
CaseData data = details.getData();
data.setOpenCasesStatisticsTsv(generateCasesStatisticsTsv());

return AboutToStartOrSubmitResponse.<CaseData, State>builder()
.data(data)
.build();
}

@SneakyThrows
private String generateCasesStatisticsTsv() {
String copySql = """
COPY (
select
reference::text as "Case reference",
to_char(created_date, 'YYYY-MM-DD HH24:MI') as "Created (UTC)",
state as "State",
to_char(coalesce(last_state_modified_date, created_date), 'YYYY-MM-DD HH24:MI')
as "State since (UTC)",
coalesce(data ->> 'closeCloseCaseReason', '') as "Closure reason",
concat_ws(' - ',
nullif(data ->> 'closeRejectionReason', ''),
nullif(data ->> 'closeStrikeOutReason', ''),
nullif(data ->> 'caseIssueFinalDecisionDecisionTemplate', '')
) as "Closure details"
from ccd.case_data
where case_type_id = 'CriminalInjuriesCompensation'
limit 50000
) TO STDOUT WITH (FORMAT csv, DELIMITER E'\\t', HEADER TRUE, NULL '');
""";

try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
StringWriter writer = new StringWriter(256 * 1024);
PGConnection pgConnection = connection.unwrap(PGConnection.class);
pgConnection.getCopyAPI().copyOut(copySql, writer);
return writer.toString();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package uk.gov.hmcts.sptribs.common.event;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.postgresql.PGConnection;
import org.postgresql.copy.CopyManager;
import org.springframework.jdbc.core.JdbcTemplate;
import uk.gov.hmcts.ccd.sdk.ConfigBuilderImpl;
import uk.gov.hmcts.ccd.sdk.api.CaseDetails;
import uk.gov.hmcts.ccd.sdk.api.Event;
import uk.gov.hmcts.ccd.sdk.api.callback.AboutToStartOrSubmitResponse;
import uk.gov.hmcts.sptribs.ciccase.model.CaseData;
import uk.gov.hmcts.sptribs.ciccase.model.State;
import uk.gov.hmcts.sptribs.ciccase.model.UserRole;

import java.io.Writer;
import java.sql.Connection;
import javax.sql.DataSource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
import static uk.gov.hmcts.sptribs.caseworker.util.EventConstants.SUPERUSER_VIEW_OPEN_CASES_STATISTICS;
import static uk.gov.hmcts.sptribs.testutil.ConfigTestUtil.createCaseDataConfigBuilder;
import static uk.gov.hmcts.sptribs.testutil.ConfigTestUtil.getEventsFrom;

@ExtendWith(MockitoExtension.class)
class ViewOpenCasesStatisticsEventTest {

@Mock
private JdbcTemplate jdbcTemplate;

@InjectMocks
private ViewOpenCasesStatisticsEvent event;

@Test
void shouldAddConfigurationToConfigBuilder() {
final ConfigBuilderImpl<CaseData, State, UserRole> configBuilder = createCaseDataConfigBuilder();

event.configure(configBuilder);

assertThat(getEventsFrom(configBuilder).values())
.extracting(Event::getId)
.contains(SUPERUSER_VIEW_OPEN_CASES_STATISTICS);
}

@Test
void shouldPopulateTsvOnAboutToStart() throws Exception {
final CaseDetails<CaseData, State> caseDetails = new CaseDetails<>();
final CaseData caseData = new CaseData();
caseDetails.setData(caseData);

DataSource dataSource = org.mockito.Mockito.mock(DataSource.class);
Connection connection = org.mockito.Mockito.mock(Connection.class);
PGConnection pgConnection = org.mockito.Mockito.mock(PGConnection.class);
CopyManager copyManager = org.mockito.Mockito.mock(CopyManager.class);

when(jdbcTemplate.getDataSource()).thenReturn(dataSource);
when(dataSource.getConnection()).thenReturn(connection);
when(connection.unwrap(PGConnection.class)).thenReturn(pgConnection);
when(pgConnection.getCopyAPI()).thenReturn(copyManager);

doAnswer(invocation -> {
Writer writer = invocation.getArgument(1);
writer.write(
"Case reference\tCreated (UTC)\tState\tState since (UTC)\tClosure reason\tClosure details\n"
+ "123\t2025-12-18 10:00\tCaseClosed\t2025-12-17 10:00\tcaseRejected\t"
+ "deadlineMissed\n"
);
return 1L;
}).when(copyManager).copyOut(anyString(), any(Writer.class));

AboutToStartOrSubmitResponse<CaseData, State> response = event.aboutToStart(caseDetails);

String tsv = response.getData().getOpenCasesStatisticsTsv();
assertThat(tsv).contains("Case reference\tCreated (UTC)\tState\tState since (UTC)");
assertThat(tsv).contains("123\t");
assertThat(tsv).contains("\tcaseRejected\t");
assertThat(tsv).contains("deadlineMissed");
}
}