Repository: island-is/island.is
Pull request #18881: feat(j-s): Statistics
Author: login: unakb
Labels: name: dirty bypass
description: Don't run dirty checks like 'newlint' and 'auto-format'
Created: 2025-04-28T11:39:03Z | Merged: 2025-04-30T12:30:42Z

[Asana](https://app.asana.com/1/203394141643832/project/1199153462262248/task/1209666397625010)

## What

First round of displaying statistics

## Why

So product owners and other users can get a better feel for what´s going on inside the system. 

## Checklist:

- [x] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [x] Formatting passes locally with my changes
- [x] I have rebased against main before asking for a review


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
  - Introduced a comprehensive statistics page for judicial system administrators, accessible from the user administration section.
  - Added interactive filtering for case statistics by date range and institution.
  - Enabled CSV export of aggregated case statistics.
  - Displayed detailed breakdowns for request cases, indictment cases, and subpoenas, including counts, in-progress/completed statuses, and average processing times.
  - Provided tooltips and localized labels for enhanced usability.

- **User Interface**
  - Added a new "Statistics" button to the user admin page for quick access.
  - Implemented new components for displaying statistics, filters, formatted data, and CSV export.

- **API and Backend**
  - Added new endpoints and GraphQL queries to fetch case statistics with optional filtering.
  - Enhanced auditing with a new action type for statistics retrieval.
  - Introduced detailed case and subpoena statistics models and services to support the statistics page.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts b/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts
index 562b51479515..e481f3754417 100644
--- a/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts
+++ b/apps/judicial-system/api/src/app/modules/auth/auth.controller.ts
@@ -51,7 +51,6 @@ import {
   type User,
 } from '@island.is/judicial-system/types'
 
-import { BackendService } from '../backend'
 import { authModuleConfig } from './auth.config'
 import { AuthService } from './auth.service'
 import { Cookie } from './auth.types'
diff --git a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts
index f19fee3c586d..47b2367ce6ff 100644
--- a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts
+++ b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts
@@ -14,7 +14,7 @@ import {
   SendNotificationResponse,
   SignatureConfirmationResponse,
 } from '../case'
-import { CaseListEntry } from '../case-list'
+import { CaseListEntry, CaseStatistics } from '../case-list'
 import {
   CivilClaimant,
   Defendant,
@@ -195,6 +195,19 @@ export class BackendService extends DataSource<{ req: Request }> {
   getCase(id: string): Promise<Case> {
     return this.get<Case>(`case/${id}`, this.caseTransformer)
   }
+  getCaseStatistics(
+    fromDate?: Date,
+    toDate?: Date,
+    institutionId?: string,
+  ): Promise<CaseStatistics> {
+    const params = new URLSearchParams()
+
+    if (fromDate) params.append('fromDate', fromDate.toISOString())
+    if (toDate) params.append('toDate', toDate.toISOString())
+    if (institutionId) params.append('institutionId', institutionId)
+
+    return this.get(`cases/statistics?${params.toString()}`)
+  }
 
   getConnectedCases(id: string): Promise<Case[]> {
     return this.get(`case/${id}/connectedCases`)
diff --git a/apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts b/apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts
index 5d17ef930143..a3f2ad88aa52 100644
--- a/apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts
+++ b/apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts
@@ -16,8 +16,10 @@ import type { User } from '@island.is/judicial-system/types'
 
 import { BackendService } from '../backend'
 import { CaseListQueryInput } from './dto/caseList.input'
+import { CaseStatisticsInput } from './dto/caseStatistics.input'
 import { CaseListInterceptor } from './interceptors/caseList.interceptor'
 import { CaseListEntry } from './models/caseList.model'
+import { CaseStatistics } from './models/caseStatistics.model'
 
 @UseGuards(JwtGraphQlAuthUserGuard)
 @Resolver(() => [CaseListEntry])
@@ -58,4 +60,29 @@ export class CaseListResolver {
 
     return result
   }
+
+  @Query(() => CaseStatistics, { nullable: true })
+  caseStatistics(
+    @Args('input', { type: () => CaseStatisticsInput, nullable: true })
+    input: CaseStatisticsInput,
+    @CurrentGraphQlUser()
+    user: User,
+    @Context('dataSources')
+    { backendService }: { backendService: BackendService },
+  ): Promise<CaseStatistics> {
+    this.logger.debug('Getting case statistics')
+
+    const result = this.auditTrailService.audit(
+      user.id,
+      AuditedAction.GET_CASES_STATISTICS,
+      backendService.getCaseStatistics(
+        input.fromDate,
+        input.toDate,
+        input.institutionId,
+      ),
+      (caseStatistics: CaseStatistics) => caseStatistics.count.toString(),
+    )
+
+    return result
+  }
 }
diff --git a/apps/judicial-system/api/src/app/modules/case-list/dto/caseStatistics.input.ts b/apps/judicial-system/api/src/app/modules/case-list/dto/caseStatistics.input.ts
new file mode 100644
index 000000000000..aa34b3269bbe
--- /dev/null
+++ b/apps/judicial-system/api/src/app/modules/case-list/dto/caseStatistics.input.ts
@@ -0,0 +1,21 @@
+import { Allow, IsOptional } from 'class-validator'
+
+import { Field, InputType } from '@nestjs/graphql'
+
+@InputType()
+export class CaseStatisticsInput {
+  @Allow()
+  @IsOptional()
+  @Field(() => Date, { nullable: true })
+  readonly fromDate?: Date
+
+  @Allow()
+  @IsOptional()
+  @Field(() => Date, { nullable: true })
+  readonly toDate?: Date
+
+  @Allow()
+  @IsOptional()
+  @Field(() => String, { nullable: true })
+  readonly institutionId?: string
+}
diff --git a/apps/judicial-system/api/src/app/modules/case-list/index.ts b/apps/judicial-system/api/src/app/modules/case-list/index.ts
index dc5bb8727ec4..218aff9c431f 100644
--- a/apps/judicial-system/api/src/app/modules/case-list/index.ts
+++ b/apps/judicial-system/api/src/app/modules/case-list/index.ts
@@ -1 +1,2 @@
 export { CaseListEntry } from './models/caseList.model'
+export { CaseStatistics } from './models/caseStatistics.model'
diff --git a/apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts b/apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts
new file mode 100644
index 000000000000..263b9944cb2b
--- /dev/null
+++ b/apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts
@@ -0,0 +1,72 @@
+import { Field, ObjectType } from '@nestjs/graphql'
+
+import { ServiceStatus } from '@island.is/judicial-system/types'
+
+@ObjectType()
+export class IndictmentCaseStatistics {
+  @Field(() => Number)
+  count!: number
+
+  @Field(() => Number)
+  inProgressCount!: number
+
+  @Field(() => Number)
+  rulingCount!: number
+
+  @Field(() => Number)
+  averageRulingTimeMs!: number
+
+  @Field(() => Number)
+  averageRulingTimeDays!: number
+}
+
+@ObjectType()
+export class RequestCaseStatistics {
+  @Field(() => Number)
+  count!: number
+
+  @Field(() => Number)
+  inProgressCount!: number
+
+  @Field(() => Number)
+  completedCount!: number
+}
+
+@ObjectType()
+export class ServiceStatusStatistics {
+  @Field(() => ServiceStatus, { nullable: true })
+  serviceStatus!: ServiceStatus | null
+
+  @Field(() => Number)
+  count!: number
+
+  @Field(() => Number)
+  averageServiceTimeMs!: number
+
+  @Field(() => Number)
+  averageServiceTimeDays!: number
+}
+
+@ObjectType()
+export class SubpoenaStatistics {
+  @Field(() => Number)
+  count!: number
+
+  @Field(() => [ServiceStatusStatistics])
+  serviceStatusStatistics!: ServiceStatusStatistics[]
+}
+
+@ObjectType()
+export class CaseStatistics {
+  @Field(() => Number)
+  count!: number
+
+  @Field(() => RequestCaseStatistics)
+  requestCases!: RequestCaseStatistics
+
+  @Field(() => IndictmentCaseStatistics)
+  indictmentCases!: IndictmentCaseStatistics
+
+  @Field(() => SubpoenaStatistics)
+  subpoenas!: SubpoenaStatistics
+}
diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts
index 5db0417721a5..054be0088f81 100644
--- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts
@@ -17,7 +17,12 @@ import {
   UseGuards,
   UseInterceptors,
 } from '@nestjs/common'
-import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'
+import {
+  ApiCreatedResponse,
+  ApiOkResponse,
+  ApiQuery,
+  ApiTags,
+} from '@nestjs/swagger'
 
 import {
   DokobitError,
@@ -50,6 +55,7 @@ import {
 
 import { nowFactory } from '../../factories'
 import {
+  adminRule,
   courtOfAppealsAssistantRule,
   courtOfAppealsJudgeRule,
   courtOfAppealsRegistrarRule,
@@ -102,6 +108,7 @@ import {
 import { CaseListInterceptor } from './interceptors/caseList.interceptor'
 import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor'
 import { Case } from './models/case.model'
+import { CaseStatistics } from './models/caseStatistics.response'
 import { SignatureConfirmationResponse } from './models/signatureConfirmation.response'
 import { transitionCase } from './state/case.state'
 import { CaseService, UpdateCase } from './case.service'
@@ -893,4 +900,24 @@ export class CaseController {
 
     return this.caseService.createCourtCase(theCase, user)
   }
+
+  @UseGuards(JwtAuthUserGuard, RolesGuard)
+  @RolesRules(adminRule)
+  @Get('cases/statistics')
+  @ApiOkResponse({
+    type: CaseStatistics,
+    description: 'Gets court centered statistics for cases',
+  })
+  @ApiQuery({ name: 'fromDate', required: false, type: String })
+  @ApiQuery({ name: 'toDate', required: false, type: String })
+  @ApiQuery({ name: 'institutionId', required: false, type: String })
+  getStatistics(
+    @Query('fromDate') fromDate?: Date,
+    @Query('toDate') toDate?: Date,
+    @Query('institutionId') institutionId?: string,
+  ): Promise<CaseStatistics> {
+    this.logger.debug('Getting statistics for all cases')
+
+    return this.caseService.getCaseStatistics(fromDate, toDate, institutionId)
+  }
 }
diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts
index 2a8815398974..24e4d7676d57 100644
--- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts
@@ -1,4 +1,4 @@
-import { Op } from 'sequelize'
+import { Op, WhereOptions } from 'sequelize'
 import { Includeable, Transaction } from 'sequelize/types'
 import { Sequelize } from 'sequelize-typescript'
 
@@ -79,8 +79,14 @@ import { User } from '../user'
 import { Victim } from '../victim'
 import { CreateCaseDto } from './dto/createCase.dto'
 import { getCasesQueryFilter } from './filters/cases.filter'
+import { partition } from './filters/filterHelpers'
 import { Case } from './models/case.model'
 import { MinimalCase } from './models/case.types'
+import {
+  CaseStatistics,
+  IndictmentCaseStatistics,
+  RequestCaseStatistics,
+} from './models/caseStatistics.response'
 import { CaseString } from './models/caseString.model'
 import { DateLog } from './models/dateLog.model'
 import { SignatureConfirmationResponse } from './models/signatureConfirmation.response'
@@ -2342,4 +2348,129 @@ export class CaseService {
 
     return updatedCase
   }
+
+  async getCaseStatistics(
+    from?: Date,
+    to?: Date,
+    institutionId?: string,
+  ): Promise<CaseStatistics> {
+    let where: WhereOptions = {
+      state: {
+        [Op.not]: [
+          CaseState.DELETED,
+          CaseState.DRAFT,
+          CaseState.NEW,
+          CaseState.WAITING_FOR_CONFIRMATION,
+        ],
+      },
+    }
+
+    if (from || to) {
+      where.created = {}
+      if (from) {
+        where.created[Op.gte] = from
+      }
+      if (to) {
+        where.created[Op.lte] = to
+      }
+    }
+
+    if (institutionId) {
+      where = {
+        ...where,
+        [Op.or]: [
+          { courtId: institutionId },
+          { prosecutorsOfficeId: institutionId },
+        ],
+      }
+    }
+
+    const cases = await this.caseModel.findAll({
+      where,
+      include: [
+        {
+          model: EventLog,
+          required: false,
+          attributes: ['created', 'eventType'],
+          where: {
+            eventType: EventType.INDICTMENT_CONFIRMED,
+          },
+        },
+      ],
+    })
+
+    const [indictments, requests] = partition(cases, (c) =>
+      isIndictmentCase(c.type),
+    )
+
+    const requestCases = this.getRequestCaseStatistics(requests)
+    const indictmentCases = this.getIndictmentStatistics(indictments)
+    const subpoenas = await this.subpoenaService.getStatistics(
+      from,
+      to,
+      institutionId,
+    )
+
+    const stats: CaseStatistics = {
+      count: cases.length,
+      requestCases,
+      indictmentCases,
+      subpoenas,
+    }
+
+    return stats
+  }
+
+  getIndictmentStatistics(cases: Case[]): IndictmentCaseStatistics {
+    const inProgressCount = cases.filter(
+      (caseItem) => caseItem.state !== CaseState.COMPLETED,
+    ).length
+
+    const rulingCount = cases.filter(
+      (caseItem) => caseItem.rulingDate !== null,
+    ).length
+
+    const totalCount = cases.length
+
+    const caseDurations = cases
+      .map((caseItem) => {
+        const confirmedEvent = caseItem.eventLogs?.[0]
+        if (!confirmedEvent?.created || !caseItem.rulingDate) return null
+
+        const diff =
+          caseItem.rulingDate.getTime() - confirmedEvent.created.getTime()
+        return diff > 0 ? diff : null
+      })
+      .filter((ms): ms is number => ms !== null)
+
+    const averageRulingTimeMs = caseDurations.length
+      ? Math.round(
+          caseDurations.reduce((sum, ms) => sum + ms, 0) / caseDurations.length,
+        )
+      : 0
+
+    return {
+      count: totalCount,
+      inProgressCount,
+      rulingCount,
+      averageRulingTimeMs,
+      averageRulingTimeDays: Math.round(
+        averageRulingTimeMs / (1000 * 60 * 60 * 24),
+      ),
+    }
+  }
+
+  getRequestCaseStatistics(cases: Case[]): RequestCaseStatistics {
+    const inProgressCount = cases.filter(
+      (c) => !isCompletedCase(c.state),
+    ).length
+
+    const completedCount = cases.filter((c) => isCompletedCase(c.state)).length
+
+    return {
+      count: cases.length,
+      inProgressCount,
+      completedCount,
+    }
+  }
 }
diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts b/apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts
new file mode 100644
index 000000000000..07e10b5942cc
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts
@@ -0,0 +1,17 @@
+export const partition = <T>(
+  array: T[],
+  predicate: (item: T) => boolean,
+): [T[], T[]] => {
+  const pass: T[] = []
+  const fail: T[] = []
+
+  for (const item of array) {
+    if (predicate(item)) {
+      pass.push(item)
+    } else {
+      fail.push(item)
+    }
+  }
+
+  return [pass, fail]
+}
diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts
index 172c0bb70d31..7d39a1b0eae2 100644
--- a/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts
@@ -14,7 +14,7 @@ import {
 } from '@island.is/judicial-system/types'
 
 import { Case } from '../../models/case.model'
-import { verifyFullAccess, verifyNoAccess, verifyReadAccess } from './verify'
+import { verifyFullAccess, verifyNoAccess } from './verify'
 
 // TODO: Fix defender indictment tests
 //       Add spokesperson tests
diff --git a/apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts b/apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts
new file mode 100644
index 000000000000..07d9c59ca2ee
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts
@@ -0,0 +1,45 @@
+import { ApiProperty } from '@nestjs/swagger'
+
+import { SubpoenaStatistics } from '../../subpoena/models/subpoenaStatistics.response'
+
+export class IndictmentCaseStatistics {
+  @ApiProperty({ type: Number })
+  count!: number
+
+  @ApiProperty({ type: Number })
+  inProgressCount!: number
+
+  @ApiProperty({ type: Number })
+  rulingCount!: number
+
+  @ApiProperty({ type: Number })
+  averageRulingTimeMs!: number
+
+  @ApiProperty({ type: Number })
+  averageRulingTimeDays!: number
+}
+
+export class RequestCaseStatistics {
+  @ApiProperty({ type: Number })
+  count!: number
+
+  @ApiProperty({ type: Number })
+  inProgressCount!: number
+
+  @ApiProperty({ type: Number})
+  completedCount!: number
+}
+
+export class CaseStatistics {
+  @ApiProperty({ type: Number })
+  count!: number
+
+  @ApiProperty({ type: RequestCaseStatistics })
+  requestCases!: RequestCaseStatistics
+
+  @ApiProperty({ type: IndictmentCaseStatistics })
+  indictmentCases!: IndictmentCaseStatistics
+
+  @ApiProperty({ type: SubpoenaStatistics })
+  subpoenas!: SubpoenaStatistics
+}
diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts b/apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts
new file mode 100644
index 000000000000..699a81b4dc12
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts
@@ -0,0 +1,31 @@
+import { ApiProperty } from '@nestjs/swagger'
+
+import { ServiceStatus } from '@island.is/judicial-system/types'
+
+export class ServiceStatusStatistics {
+  @ApiProperty({ enum: ServiceStatus })
+  serviceStatus!: ServiceStatus | null
+
+  @ApiProperty({ type: Number })
+  count!: number
+
+  @ApiProperty({
+    type: Number,
+    description: 'Average time in milliseconds between created and serviceDate',
+  })
+  averageServiceTimeMs!: number
+
+  @ApiProperty({
+    type: Number,
+    description: 'Average time in days between created and serviceDate',
+  })
+  averageServiceTimeDays!: number
+}
+
+export class SubpoenaStatistics {
+  @ApiProperty({ type: Number })
+  count!: number
+
+  @ApiProperty({ type: [ServiceStatusStatistics] })
+  serviceStatusStatistics!: ServiceStatusStatistics[]
+}
diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts
index f98bff91811a..930f21ea1c68 100644
--- a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts
@@ -1,5 +1,13 @@
 import { Base64 } from 'js-base64'
-import { Includeable, Sequelize } from 'sequelize'
+import {
+  col,
+  fn,
+  Includeable,
+  literal,
+  Op,
+  Sequelize,
+  WhereOptions,
+} from 'sequelize'
 import { Transaction } from 'sequelize/types'
 
 import {
@@ -43,6 +51,10 @@ import { User } from '../user'
 import { UpdateSubpoenaDto } from './dto/updateSubpoena.dto'
 import { DeliverResponse } from './models/deliver.response'
 import { Subpoena } from './models/subpoena.model'
+import {
+  ServiceStatusStatistics,
+  SubpoenaStatistics,
+} from './models/subpoenaStatistics.response'
 
 export const include: Includeable[] = [
   {
@@ -534,4 +546,86 @@ export class SubpoenaService {
 
     return this.update(subpoena, subpoenaInfo)
   }
+
+  async getStatistics(
+    from?: Date,
+    to?: Date,
+    institutionId?: string,
+  ): Promise<SubpoenaStatistics> {
+    const where: WhereOptions = {
+      policeSubpoenaId: {
+        [Op.ne]: null,
+      },
+    }
+
+    if (from || to) {
+      where.created = {}
+      if (from) {
+        where.created[Op.gte] = from
+      }
+      if (to) {
+        where.created[Op.lte] = to
+      }
+    }
+
+    const include: Includeable[] = []
+
+    if (institutionId) {
+      include.push({
+        model: Case,
+        required: true,
+        attributes: [],
+        where: {
+          [Op.or]: [
+            { courtId: institutionId },
+            { prosecutorsOfficeId: institutionId },
+          ],
+        },
+      })
+    }
+
+    const count = await this.subpoenaModel.count({
+      where,
+      include,
+    })
+
+    const grouped = (await this.subpoenaModel.findAll({
+      where,
+      include,
+      attributes: [
+        'serviceStatus',
+        [fn('COUNT', col('Subpoena.id')), 'count'],
+        [
+          literal(
+            'AVG(EXTRACT(EPOCH FROM "Subpoena"."service_date" - "Subpoena"."created") * 1000)',
+          ),
+          'averageServiceTimeMs',
+        ],
+      ],
+      group: ['serviceStatus'],
+      raw: true,
+    })) as unknown as {
+      serviceStatus: ServiceStatus | null
+      count: string
+      averageServiceTimeMs: string | null
+    }[]
+
+    const serviceStatusStatistics: ServiceStatusStatistics[] = grouped.map(
+      (row) => ({
+        serviceStatus: row.serviceStatus,
+        count: Number(row.count),
+        averageServiceTimeMs: Math.round(Number(row.averageServiceTimeMs) || 0),
+        averageServiceTimeDays:
+          Math.round(Number(row.averageServiceTimeMs) / 1000 / 60 / 60 / 24) ||
+          0,
+      }),
+    )
+
+    const stats: SubpoenaStatistics = {
+      count,
+      serviceStatusStatistics,
+    }
+
+    return stats
+  }
 }
diff --git a/apps/judicial-system/web/pages/notendur/tolfraedi.ts b/apps/judicial-system/web/pages/notendur/tolfraedi.ts
new file mode 100644
index 000000000000..a49bd5d55026
--- /dev/null
+++ b/apps/judicial-system/web/pages/notendur/tolfraedi.ts
@@ -0,0 +1,3 @@
+import Statistics from '@island.is/judicial-system-web/src/routes/Admin/Statistics/Statistics'
+
+export default Statistics
diff --git a/apps/judicial-system/web/src/components/LabelValue/LabelValue.tsx b/apps/judicial-system/web/src/components/LabelValue/LabelValue.tsx
new file mode 100644
index 000000000000..fa12e6ddd555
--- /dev/null
+++ b/apps/judicial-system/web/src/components/LabelValue/LabelValue.tsx
@@ -0,0 +1,26 @@
+import { FC, ReactNode } from 'react'
+
+import { Box, ResponsiveProp, Space, Text } from '@island.is/island-ui/core'
+
+interface LabelValueProps {
+  label: string | ReactNode
+  value: string | ReactNode
+  marginBottom?: ResponsiveProp<Space | 'auto'>
+}
+
+export const LabelValue: FC<LabelValueProps> = ({
+  label,
+  value,
+  marginBottom = 0,
+}) => {
+  return (
+    <Box marginBottom={marginBottom}>
+      <Text as="span" fontWeight="semiBold">
+        {label}:{' '}
+      </Text>
+      <Text as="span">{value}</Text>
+    </Box>
+  )
+}
+
+export default LabelValue
diff --git a/apps/judicial-system/web/src/components/index.ts b/apps/judicial-system/web/src/components/index.ts
index 4da86cd4dccf..1719a894d417 100644
--- a/apps/judicial-system/web/src/components/index.ts
+++ b/apps/judicial-system/web/src/components/index.ts
@@ -122,3 +122,4 @@ export { default as RequestAppealRulingNotToBePublishedCheckbox } from './Reques
 export { LawyerRegistryProvider } from './LawyerRegistryProvider/LawyerRegistryProvider'
 export { default as RequiredStar } from './RequiredStar/RequiredStar'
 export { VictimInfo } from './VictimInfo/VictimInfo'
+export { LabelValue } from './LabelValue/LabelValue'
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.css.ts b/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.css.ts
new file mode 100644
index 000000000000..638695b42578
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.css.ts
@@ -0,0 +1,32 @@
+import { style } from '@vanilla-extract/css'
+
+import { theme } from '@island.is/island-ui/theme'
+
+export const statisticsPageContainer = style({
+  minHeight: '100vh',
+  background: theme.color.purple100,
+  display: 'flex',
+  justifyContent: 'center',
+  paddingTop: theme.spacing[6],
+  paddingBottom: theme.spacing[6],
+})
+
+export const statisticsContentBox = style({
+  width: '100%',
+  background: theme.color.white,
+  minHeight: 644,
+  minWidth: 350,
+  '@media': {
+    [`screen and (max-width: ${theme.breakpoints.md - 1}px)`]: {
+      padding: theme.spacing[4],
+    },
+    [`screen and (min-width: ${theme.breakpoints.md}px)`]: {
+      padding: `${theme.spacing[4]}px ${theme.spacing[6]}px`,
+    },
+    [`screen and (min-width: ${theme.breakpoints.lg}px)`]: {
+      padding: `${theme.spacing[8]}px ${theme.spacing[10]}px`,
+      maxWidth: 1100,
+      borderRadius: theme.border.radius.large,
+    },
+  },
+})
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts b/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts
new file mode 100644
index 000000000000..3f1d855b9711
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts
@@ -0,0 +1,27 @@
+import { defineMessages } from 'react-intl'
+
+export const strings = defineMessages({
+  subpoenaStatisticsTooltip: {
+    id: 'judicial.system.core:admin.statistics.subpoena_tooltip',
+    defaultMessage:
+      'Yfirlit yfir fyrirköll, birtingarstöður og meðal afgreiðslutíma þeirra birtingaraðferða sem eiga við',
+    description: 'Tooltip texti fyrir fyrirkalls hluta á tölfræði skjá.',
+  },
+  indictmentStatisticsTooltip: {
+    id: 'judicial.system.core:admin.statistics.indictment_tooltip',
+    defaultMessage:
+      'Heildarfjöldi: Ákærur sem voru stofnaðar á völdu tímabili. Í vinnslu: hjá dómstóli. Lokið með dómi og meðaltími frá því að mál barst dómstólum þar til búið var að dæma í málinu.',
+    description: 'Tooltip texti fyrir ákæru hluta á tölfræði skjá.',
+  },
+  allCasesStatisticsTooltip: {
+    id: 'judicial.system.core:admin.statistics.all_cases_tooltip',
+    defaultMessage: 'Öll mál (R og S) sem voru  á völdu tímabili',
+    description: 'Tooltip texti fyrir ákæru hluta á tölfræði skjá.',
+  },
+  requestCaseStatisticsTooltip: {
+    id: 'judicial.system.core:admin.statistics.request_cases_tooltip',
+    defaultMessage:
+      'Heildarfjöldi: Öll R mál sem voru stofnuð á völdu tímabili',
+    description: 'Tooltip texti fyrir R mála hluta á tölfræði skjá.',
+  },
+})
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx b/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx
new file mode 100644
index 000000000000..baaf8588dfd7
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx
@@ -0,0 +1,311 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useIntl } from 'react-intl'
+import { AnimatePresence } from 'motion/react'
+
+import {
+  Box,
+  GridColumn,
+  GridRow,
+  SkeletonLoader,
+  Text,
+  ToggleSwitchCheckbox,
+  Tooltip,
+} from '@island.is/island-ui/core'
+import {
+  InfoCard,
+  LabelValue,
+  PageHeader,
+} from '@island.is/judicial-system-web/src/components'
+import {
+  CaseStatistics,
+  ServiceStatus,
+} from '@island.is/judicial-system-web/src/graphql/schema'
+import { useInstitution } from '@island.is/judicial-system-web/src/utils/hooks'
+
+import CountAndDays from './components/CountAndDays'
+import { FiltersPanel } from './components/FiltersPanel'
+import { StatisticsCSVButton } from './components/StatisticsCSVButton'
+import { useCaseStatisticsQuery } from './getCaseStatistics.generated'
+import { mapServiceStatusTitle } from './helpers'
+import { strings } from './Statistics.strings'
+import * as styles from './Statistics.css'
+
+const Statistics = () => {
+  const [filter, setFilter] = useState(false)
+  const [stats, setStats] = useState<CaseStatistics | undefined>()
+  const [filters, setFilters] = useState<{
+    fromDate?: Date
+    toDate?: Date
+    institution?: { label: string; value: string }
+  }>({})
+
+  const { formatMessage } = useIntl()
+  const { districtCourts } = useInstitution()
+
+  const queryVariables = useMemo(() => {
+    return filter
+      ? {
+          fromDate: filters.fromDate,
+          toDate: filters.toDate,
+          institutionId: filters.institution?.value,
+        }
+      : {}
+  }, [filter, filters])
+
+  const { data, loading } = useCaseStatisticsQuery({
+    variables: {
+      input: queryVariables,
+    },
+    fetchPolicy: 'cache-and-network',
+  })
+
+  useEffect(() => {
+    if (data?.caseStatistics) {
+      setStats(data.caseStatistics)
+    }
+  }, [data])
+
+  return (
+    <Box className={styles.statisticsPageContainer}>
+      <PageHeader title="Tölfræði" />
+      <Box className={styles.statisticsContentBox}>
+        <Box
+          display="flex"
+          justifyContent="spaceBetween"
+          alignItems="center"
+          marginBottom={[2, 2, 4, 6, 6]}
+        >
+          <Text as="h1" variant="h1">
+            Tölfræði
+          </Text>
+          <ToggleSwitchCheckbox
+            name="toggleFilter"
+            label={<Text>Sía</Text>}
+            checked={filter}
+            onChange={setFilter}
+          />
+        </Box>
+
+        <GridRow
+          rowGap={3}
+          direction={['columnReverse', 'columnReverse', 'columnReverse', 'row']}
+        >
+          <GridColumn
+            span={['12/12', '12/12', '12/12', filter ? '8/12' : '12/12']}
+          >
+            {loading && !stats ? (
+              <SkeletonLoader height={800} />
+            ) : (
+              <AnimatePresence mode="wait">
+                <>
+                  {' '}
+                  <InfoCard
+                    sections={[
+                      {
+                        id: 'all-cases',
+                        items: [
+                          {
+                            id: 'case-item',
+                            title: (
+                              <Text variant="h3" marginBottom={2}>
+                                {'Öll mál '}
+                                <Tooltip
+                                  placement="right"
+                                  text={formatMessage(
+                                    strings.allCasesStatisticsTooltip,
+                                  )}
+                                />
+                              </Text>
+                            ),
+                            values: [
+                              <LabelValue
+                                label="Heildarfjöldi"
+                                value={stats?.count}
+                              />,
+                            ],
+                          },
+                        ],
+                      },
+                      {
+                        id: 'request-cases',
+                        items: [
+                          {
+                            id: 'request-case-item',
+                            title: (
+                              <Text variant="h3" marginBottom={2}>
+                                {'R mál '}
+                                <Tooltip
+                                  placement="right"
+                                  text={formatMessage(
+                                    strings.requestCaseStatisticsTooltip,
+                                  )}
+                                />
+                              </Text>
+                            ),
+                            values: [
+                              <Box
+                                key="request-cases-statistics-values"
+                                display="flex"
+                                flexDirection="column"
+                                rowGap={1}
+                              >
+                                <LabelValue
+                                  label="Heildarfjöldi"
+                                  value={stats?.requestCases.count}
+                                />
+
+                                <LabelValue
+                                  label="Í vinnslu"
+                                  value={stats?.requestCases.inProgressCount}
+                                />
+
+                                <LabelValue
+                                  label="Lokið"
+                                  value={stats?.requestCases.completedCount}
+                                />
+                              </Box>,
+                            ],
+                          },
+                        ],
+                      },
+                      {
+                        id: 'indictment-cases',
+                        items: [
+                          {
+                            id: 'indictment-case-item',
+                            title: (
+                              <Text variant="h3" marginBottom={2}>
+                                {'S mál '}
+                                <Tooltip
+                                  placement="right"
+                                  text={formatMessage(
+                                    strings.indictmentStatisticsTooltip,
+                                  )}
+                                />
+                              </Text>
+                            ),
+                            values: [
+                              <Box
+                                key="indictment-statistics-values"
+                                display="flex"
+                                flexDirection="column"
+                                rowGap={1}
+                              >
+                                <LabelValue
+                                  label="Heildarfjöldi"
+                                  value={stats?.indictmentCases.count}
+                                />
+                                <LabelValue
+                                  label="Í vinnslu"
+                                  value={stats?.indictmentCases.inProgressCount}
+                                />
+                                <CountAndDays
+                                  label="Lokið með dómi"
+                                  count={stats?.indictmentCases.rulingCount}
+                                  days={
+                                    stats?.indictmentCases.averageRulingTimeDays
+                                  }
+                                />
+                              </Box>,
+                            ],
+                          },
+                        ],
+                      },
+                      {
+                        id: 'subpoenas',
+                        items: [
+                          {
+                            id: 'case-stat-item',
+                            title: (
+                              <Text variant="h3" marginBottom={2}>
+                                {'Fyrirköll '}
+                                <Tooltip
+                                  placement="right"
+                                  text={formatMessage(
+                                    strings.subpoenaStatisticsTooltip,
+                                  )}
+                                />
+                              </Text>
+                            ),
+                            values: [
+                              <Box
+                                key="subpoena-statistics-values"
+                                display="flex"
+                                flexDirection="column"
+                                rowGap={1}
+                              >
+                                <LabelValue
+                                  label="Heildarfjöldi"
+                                  value={stats?.subpoenas.count}
+                                />
+                                {[null, ...Object.values(ServiceStatus)].map(
+                                  (status) => {
+                                    const stat =
+                                      stats?.subpoenas.serviceStatusStatistics.find(
+                                        (s) => s.serviceStatus === status,
+                                      )
+                                    return (
+                                      <CountAndDays
+                                        key={status ?? 'unserviced'}
+                                        label={mapServiceStatusTitle(status)}
+                                        count={stat?.count}
+                                        days={
+                                          status
+                                            ? stat?.averageServiceTimeDays
+                                            : undefined
+                                        }
+                                      />
+                                    )
+                                  },
+                                )}
+                              </Box>,
+                            ],
+                          },
+                        ],
+                      },
+                    ]}
+                  />
+                  <Box display="flex" justifyContent="flexEnd" marginTop={2}>
+                    <StatisticsCSVButton
+                      stats={stats}
+                      fromDate={filter ? filters.fromDate : undefined}
+                      toDate={filter ? filters.toDate : undefined}
+                      institutionName={
+                        filter ? filters.institution?.label : undefined
+                      }
+                    />
+                  </Box>
+                </>
+              </AnimatePresence>
+            )}
+          </GridColumn>
+
+          {filter && (
+            <GridColumn span={['12/12', '12/12', '12/12', '4/12']}>
+              <Box display="flex" flexDirection="column" rowGap={3}>
+                <FiltersPanel
+                  districtCourts={districtCourts}
+                  institution={filters.institution}
+                  fromDate={filters.fromDate}
+                  toDate={filters.toDate}
+                  setInstitution={(institution) =>
+                    setFilters((prev) => ({ ...prev, institution }))
+                  }
+                  setFromDate={(fromDate) =>
+                    setFilters((prev) => ({ ...prev, fromDate }))
+                  }
+                  setToDate={(toDate) =>
+                    setFilters((prev) => ({ ...prev, toDate }))
+                  }
+                  onClear={() => setFilters({})}
+                />
+              </Box>
+            </GridColumn>
+          )}
+        </GridRow>
+      </Box>
+    </Box>
+  )
+}
+
+export default Statistics
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx b/apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx
new file mode 100644
index 000000000000..2b142dde6440
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx
@@ -0,0 +1,43 @@
+import { Box, Text } from '@island.is/island-ui/core'
+import { theme } from '@island.is/island-ui/theme'
+import { LabelValue } from '@island.is/judicial-system-web/src/components'
+
+interface Props {
+  label: string
+  count?: number
+  days?: number
+}
+
+const formatDays = (days: number) =>
+  days < 1 ? '< 1 dagur' : `${days} ${days === 1 ? 'dagur' : 'dagar'}`
+
+export const CountAndDays = ({ label, count, days }: Props) => (
+  <Box
+    background="blue100"
+    borderRadius="large"
+    display="flex"
+    justifyContent="spaceBetween"
+    alignItems="center"
+  >
+    <LabelValue label={label} value={count ?? 0} />
+
+    {!!count && days !== undefined && (
+      <>
+        <Box flexGrow={1} marginX={2}>
+          <div
+            style={{
+              height: '1px',
+              borderBottom: `1px dotted ${theme.color.blue200}`,
+              width: '100%',
+            }}
+          />
+        </Box>
+        <Text>
+          <b>{formatDays(days)}</b>
+        </Text>
+      </>
+    )}
+  </Box>
+)
+
+export default CountAndDays
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx b/apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx
new file mode 100644
index 000000000000..93ec4996d625
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx
@@ -0,0 +1,84 @@
+import { FC } from 'react'
+import { AnimatePresence, motion } from 'motion/react'
+
+import { Box, Button, DatePicker, Select } from '@island.is/island-ui/core'
+
+type FiltersPanelProps = {
+  districtCourts: { name?: string | null; id?: string | undefined }[]
+  institution?: { label: string; value: string }
+  fromDate?: Date
+  toDate?: Date
+  setInstitution: (institution?: { label: string; value: string }) => void
+  setFromDate: (date?: Date) => void
+  setToDate: (date?: Date) => void
+  onClear: () => void
+}
+
+export const FiltersPanel: FC<FiltersPanelProps> = ({
+  districtCourts,
+  institution,
+  fromDate,
+  toDate,
+  setInstitution,
+  setFromDate,
+  setToDate,
+  onClear,
+}) => (
+  <AnimatePresence mode="wait">
+    <motion.div
+      key="filters"
+      initial={{ opacity: 0, x: 50 }}
+      animate={{ opacity: 1, x: 0 }}
+      exit={{ opacity: 0, x: 50 }}
+      transition={{ duration: 0.4, ease: 'easeInOut' }}
+    >
+      <Box display="flex" flexDirection="column" rowGap={2}>
+        <Select
+          name="court"
+          label="Veldu dómstól"
+          placeholder="Dómstóll"
+          size="sm"
+          options={districtCourts.map((court) => ({
+            label: court.name ?? '',
+            value: court.id ?? '',
+          }))}
+          onChange={(selectedOption) =>
+            setInstitution(selectedOption ?? undefined)
+          }
+          value={institution ?? null}
+        />
+
+        <DatePicker
+          name="statisticsDateFrom"
+          label="Veldu dagsetningu frá"
+          placeholderText="Frá"
+          size="sm"
+          selected={fromDate}
+          maxDate={new Date()}
+          handleChange={(date: Date | null) => setFromDate(date ?? undefined)}
+        />
+
+        <DatePicker
+          name="statisticsDateTo"
+          label="Veldu dagsetningu til"
+          placeholderText="Til"
+          size="sm"
+          maxDate={new Date()}
+          minDate={fromDate}
+          selected={toDate}
+          handleChange={(date: Date | null) => setToDate(date ?? undefined)}
+        />
+      </Box>
+      <Box display="flex" justifyContent="flexEnd" marginTop={1}>
+        <Button
+          size="small"
+          variant="text"
+          colorScheme="destructive"
+          onClick={onClear}
+        >
+          Hreinsa
+        </Button>
+      </Box>
+    </motion.div>
+  </AnimatePresence>
+)
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx b/apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx
new file mode 100644
index 000000000000..cd84663de70f
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx
@@ -0,0 +1,152 @@
+import { FC } from 'react'
+
+import { Button } from '@island.is/island-ui/core'
+import { formatDate } from '@island.is/judicial-system/formatters'
+import { CaseStatistics } from '@island.is/judicial-system-web/src/graphql/schema'
+
+import { mapServiceStatusTitle } from '../helpers'
+
+const convertToCSV = (rows: Record<string, unknown>[]): string => {
+  if (!rows.length) return ''
+  const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
+  const escapeCSVValue = (val: unknown): string => {
+    if (typeof val === 'string') {
+      return val.includes(',') || val.includes('"')
+        ? `"${val.replace(/"/g, '""')}"`
+        : val
+    }
+    return val != null ? String(val) : ''
+  }
+  const csv = [
+    headers.join(','),
+    ...rows.map((row) =>
+      headers.map((key) => escapeCSVValue(row[key])).join(','),
+    ),
+  ]
+  return csv.join('\n')
+}
+
+const millisecondsToHours = (ms: number): string =>
+  (ms / (1000 * 60 * 60)).toFixed(2)
+
+interface StatisticsCSVButtonProps {
+  stats?: CaseStatistics
+  fromDate?: Date
+  toDate?: Date
+  institutionName?: string
+}
+
+export const StatisticsCSVButton: FC<StatisticsCSVButtonProps> = ({
+  stats,
+  fromDate,
+  toDate,
+  institutionName,
+}) => {
+  const formatDateForFilename = (date: Date) =>
+    formatDate(date)?.replace(/\./g, '_')
+
+  const getFilename = () => {
+    const parts = [
+      'RVG_tölur',
+      institutionName?.replace(/\s+/g, '_').toLowerCase(),
+      fromDate ? formatDateForFilename(fromDate) : '',
+      toDate ? formatDateForFilename(toDate) : '',
+    ]
+    return parts.filter(Boolean).join('_') || 'case_statistics'
+  }
+
+  const buildRequestSection = (s: CaseStatistics) => [
+    { Tegund: 'R mál' },
+    { Titill: 'Heildarfjöldi', Samtals: s.requestCases.count },
+    { Titill: 'Í vinnslu', Samtals: s.requestCases.inProgressCount },
+    { Titill: 'Lokið', Samtals: s.requestCases.completedCount },
+  ]
+
+  const buildIndictmentSection = (s: CaseStatistics) => [
+    { Tegund: 'S mál' },
+    { Titill: 'Heildarfjöldi', Samtals: s.indictmentCases.count },
+    {
+      Titill: 'Í vinnslu',
+      Samtals: s.indictmentCases.inProgressCount,
+    },
+    {
+      Titill: 'Lokið með dómi',
+      Samtals: s.indictmentCases.rulingCount,
+      'Meðaltími (dagar)': s.indictmentCases.averageRulingTimeDays,
+      'Meðaltími (klst)': millisecondsToHours(
+        s.indictmentCases.averageRulingTimeMs,
+      ),
+    },
+  ]
+
+  const buildSubpoenaSection = (s: CaseStatistics) => [
+    { Tegund: 'Fyrirköll' },
+    { Titill: 'Heildarfjöldi', Samtals: s.subpoenas.count },
+    ...s.subpoenas.serviceStatusStatistics.map((status) => ({
+      Titill: mapServiceStatusTitle(status.serviceStatus),
+      Samtals: status.count,
+      'Meðaltími (dagar)': status.serviceStatus
+        ? status.averageServiceTimeDays
+        : '',
+      'Meðaltími (klst)': status.serviceStatus
+        ? millisecondsToHours(status.averageServiceTimeMs)
+        : '',
+    })),
+  ]
+
+  const handleDownload = () => {
+    if (!stats) return
+
+    const periodText =
+      fromDate && toDate
+        ? `${formatDate(fromDate)} - ${formatDate(toDate)}`
+        : fromDate
+        ? `${formatDate(fromDate)} - nú`
+        : toDate
+        ? `Frá upphafi - ${formatDate(toDate)}`
+        : 'Frá upphafi'
+
+    const institutionText = institutionName || 'Allir dómstólar'
+
+    const headerInfo = [
+      {
+        Síur: institutionText,
+      },
+      { Síur: periodText },
+    ]
+
+    const csvData = [
+      { Tegund: 'Öll mál' },
+      { Titill: 'Heildarfjöldi', Samtals: stats.count },
+      ...buildRequestSection(stats),
+      ...buildIndictmentSection(stats),
+      ...buildSubpoenaSection(stats),
+    ]
+
+    const csv = convertToCSV([...headerInfo, ...csvData])
+    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
+    const url = URL.createObjectURL(blob)
+
+    const link = document.createElement('a')
+    link.href = url
+    link.setAttribute('download', `${getFilename()}.csv`)
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+  }
+
+  return (
+    <Button
+      variant="ghost"
+      size="small"
+      icon="download"
+      iconType="outline"
+      onClick={handleDownload}
+      disabled={!stats}
+    >
+      Sækja CSV
+    </Button>
+  )
+}
+
+export default StatisticsCSVButton
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/getCaseStatistics.graphql b/apps/judicial-system/web/src/routes/Admin/Statistics/getCaseStatistics.graphql
new file mode 100644
index 000000000000..350e95610419
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/getCaseStatistics.graphql
@@ -0,0 +1,26 @@
+query CaseStatistics($input: CaseStatisticsInput) {
+  caseStatistics(input: $input) {
+    count
+    requestCases {
+      count
+      inProgressCount
+      completedCount
+    }
+    indictmentCases {
+      count
+      inProgressCount
+      rulingCount
+      averageRulingTimeMs
+      averageRulingTimeDays
+    }
+    subpoenas {
+      count
+      serviceStatusStatistics {
+        serviceStatus
+        count
+        averageServiceTimeMs
+        averageServiceTimeDays
+      }
+    }
+  }
+}
diff --git a/apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts b/apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts
new file mode 100644
index 000000000000..2b1701162cc9
--- /dev/null
+++ b/apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts
@@ -0,0 +1,12 @@
+import { ServiceStatus } from '@island.is/judicial-system/types'
+
+export const serviceStatusLabels: Record<ServiceStatus, string> = {
+  [ServiceStatus.DEFENDER]: 'Birt verjanda',
+  [ServiceStatus.ELECTRONICALLY]: 'Birt rafrænt',
+  [ServiceStatus.IN_PERSON]: 'Birt persónulega',
+  [ServiceStatus.EXPIRED]: 'Rann út á tíma',
+  [ServiceStatus.FAILED]: 'Árangurslaus birting',
+}
+
+export const mapServiceStatusTitle = (status?: ServiceStatus | null): string =>
+  status ? serviceStatusLabels[status] ?? 'Óbirt' : 'Óbirt'
diff --git a/apps/judicial-system/web/src/routes/Admin/UserForm/UserForm.tsx b/apps/judicial-system/web/src/routes/Admin/UserForm/UserForm.tsx
index bd6e55c8c5bc..6e0d95e7a958 100644
--- a/apps/judicial-system/web/src/routes/Admin/UserForm/UserForm.tsx
+++ b/apps/judicial-system/web/src/routes/Admin/UserForm/UserForm.tsx
@@ -12,7 +12,6 @@ import * as constants from '@island.is/judicial-system/consts'
 import {
   isCoreUser,
   isProsecutorsOffice,
-  prosecutorsOfficeTypes,
 } from '@island.is/judicial-system/types'
 import {
   FormContentContainer,
diff --git a/apps/judicial-system/web/src/routes/Admin/Users/Users.tsx b/apps/judicial-system/web/src/routes/Admin/Users/Users.tsx
index fc4a731a530e..5be401895c9e 100644
--- a/apps/judicial-system/web/src/routes/Admin/Users/Users.tsx
+++ b/apps/judicial-system/web/src/routes/Admin/Users/Users.tsx
@@ -30,6 +30,7 @@ import * as styles from './Users.css'
 export const Users = () => {
   const router = useRouter()
   const [selectedInstitution, setSelectedInstitution] = useState<string>()
+
   const { formatMessage } = useIntl()
   const {
     allInstitutions,
@@ -58,7 +59,7 @@ export const Users = () => {
   return (
     <div className={styles.userControlContainer}>
       <PageHeader title={formatMessage(titles.admin.users)} />
-      <Box display="flex" marginBottom={9}>
+      <Box display="flex" marginBottom={9} justifyContent="spaceBetween">
         <Button
           icon="add"
           onClick={() => {
@@ -67,7 +68,17 @@ export const Users = () => {
         >
           Nýr notandi
         </Button>
+        <Button
+          variant="ghost"
+          icon="calculator"
+          onClick={() => {
+            router.push(constants.STATISTICS_ROUTE)
+          }}
+        >
+          Tölfræði
+        </Button>
       </Box>
+
       <Box
         marginBottom={8}
         display="flex"
diff --git a/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts b/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts
index 9ade0598cc0f..66d8f4fecbeb 100644
--- a/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts
+++ b/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts
@@ -13,6 +13,7 @@ import { auditTrailModuleConfig } from './auditTrail.config'
 export enum AuditedAction {
   LOGIN = 'LOGIN',
   GET_CASES = 'GET_CASES',
+  GET_CASES_STATISTICS = 'GET_CASES_STATISTICS',
   GET_CASE = 'GET_CASE',
   GET_CONNECTED_CASES = 'GET_CONNECTED_CASES',
   GET_INDICTMENTS = 'GET_INDICTMENTS',
diff --git a/libs/judicial-system/consts/src/lib/consts.ts b/libs/judicial-system/consts/src/lib/consts.ts
index d75db1de1b52..20ab606e5950 100644
--- a/libs/judicial-system/consts/src/lib/consts.ts
+++ b/libs/judicial-system/consts/src/lib/consts.ts
@@ -209,6 +209,8 @@ export const INDICTMENTS_ADD_FILES_ROUTE = '/akaera/gogn'
 export const USERS_ROUTE = '/notendur'
 export const CREATE_USER_ROUTE = '/notendur/nyr'
 export const CHANGE_USER_ROUTE = '/notendur/breyta'
+export const STATISTICS_ROUTE = '/notendur/tolfraedi'
+
 //#endregion Admin user routes
 
 //#region Shared routes


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>apps/judicial-system/web/src/components/LabelValue/LabelValue.tsx (1)</summary><blockquote>

`1-27`: _🛠️ Refactor suggestion_

**Add missing unit tests and Storybook story for `LabelValue`**  
The component implementation is solid, but test coverage and a visual preview via Storybook would help catch regressions and demonstrate margin responsiveness.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (18)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/case/case.controller.ts (1)</summary><blockquote>

`904-922`: **API endpoint implementation looks good, but consider adding more documentation for the statistics endpoint.**

The new GET endpoint for retrieving case statistics is well-implemented with proper guards, decorators, and query parameters. The functionality is correctly delegated to the service layer.



Consider adding more detailed API documentation using Swagger annotations to describe the expected format of date parameters and what the statistics include. This will help API consumers better understand the expected inputs and outputs.

```diff
  @ApiOkResponse({
    type: CaseStatistics,
    description: 'Gets court centered statistics for cases',
  })
  @ApiQuery({ name: 'fromDate', required: false, type: String })
  @ApiQuery({ name: 'toDate', required: false, type: String })
  @ApiQuery({ name: 'institutionId', required: false, type: String })
+ @ApiOperation({
+   summary: 'Get case statistics',
+   description: 'Retrieves aggregated statistics for cases with optional filtering by date range and institution.'
+ })
```

</blockquote></details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx (2)</summary><blockquote>

`44-55`: **Improve date formatting and filename generation with more robust error handling.**

The date formatting for filenames could be more robust to handle potential null values.


Add error handling to ensure formatDate never returns null and improve the filename generation logic.

```diff
- const formatDateForFilename = (date: Date) =>
-   formatDate(date)?.replace(/\./g, '_')
+ const formatDateForFilename = (date: Date) => {
+   const formatted = formatDate(date)
+   return formatted ? formatted.replace(/\./g, '_') : ''
+ }

  const getFilename = () => {
    const parts = [
      'RVG_tölur',
-     institutionName?.replace(/\s+/g, '_').toLowerCase(),
+     institutionName ? institutionName.replace(/\s+/g, '_').toLowerCase() : '',
      fromDate ? formatDateForFilename(fromDate) : '',
      toDate ? formatDateForFilename(toDate) : '',
    ]
    return parts.filter(Boolean).join('_') || 'case_statistics'
  }
```

---

`97-136`: **Improve CSV download implementation with better cleanup and error handling.**

The current implementation for CSV download works but could be improved for better resource management and error handling.


Enhance the CSV download implementation with proper URL object cleanup and error handling:

```diff
  const handleDownload = () => {
    if (!stats) return
    
    // ...existing code...
    
    const csv = convertToCSV([...headerInfo, ...csvData])
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
    const url = URL.createObjectURL(blob)

    try {
      const link = document.createElement('a')
      link.href = url
      link.setAttribute('download', `${getFilename()}.csv`)
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
    } catch (error) {
      console.error('Failed to download CSV:', error)
    } finally {
+     // Always revoke the object URL to avoid memory leaks
+     URL.revokeObjectURL(url)
    }
  }
```

</blockquote></details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.css.ts (2)</summary><blockquote>

`5-12`: **CSS styling looks good, consider semantic HTML compatibility.**

The styling for the statistics page container follows best practices with flexbox and proper spacing.



Consider adding overflow handling to ensure content remains accessible on smaller screens:

```diff
export const statisticsPageContainer = style({
  minHeight: '100vh',
  background: theme.color.purple100,
  display: 'flex',
  justifyContent: 'center',
  paddingTop: theme.spacing[6],
  paddingBottom: theme.spacing[6],
+ overflow: 'auto',
})
```

---

`14-32`: **Good responsive design approach for content box.**

The content box styling uses responsive design principles effectively with breakpoints for different screen sizes.



Consider adjusting the min-height value to use theme variables for consistency:

```diff
export const statisticsContentBox = style({
  width: '100%',
  background: theme.color.white,
- minHeight: 644,
+ minHeight: theme.spacing[20] * 2, // Use theme spacing multipliers for consistency
  minWidth: 350,
  // ...rest of code remains the same
})
```

</blockquote></details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx (3)</summary><blockquote>

`5-9`: **Props interface looks good, consider adding defaultProps.**

The props interface is well-defined with appropriate types.



Consider adding default props to improve component usage flexibility:

```diff
interface Props {
  label: string
  count?: number
  days?: number
}

+ const defaultProps = {
+   count: undefined,
+   days: undefined,
+ };
```

---

`11-12`: **Good localization handling for days formatting.**

The function correctly handles plural forms for days in Icelandic.



Consider extracting this formatting function to a shared utilities file if it might be reused elsewhere in the application.

---

`14-41`: **Component implementation is clean and follows best practices.**

The component implementation is well-structured with proper conditional rendering and styling.



Consider using a more semantic HTML structure for accessibility:

```diff
export const CountAndDays = ({ label, count, days }: Props) => (
  <Box
    background="blue100"
    borderRadius="large"
    display="flex"
    justifyContent="spaceBetween"
    alignItems="center"
+   role="group"
+   aria-label={`Statistics for ${label}`}
  >
    <LabelValue label={label} value={count ?? 0} />

    {!!count && days !== undefined && (
      <>
        <Box flexGrow={1} marginX={2}>
          <div
            style={{
              height: '1px',
              borderBottom: `1px dotted ${theme.color.blue200}`,
              width: '100%',
            }}
+           aria-hidden="true"
          />
        </Box>
        <Text>
          <b>{formatDays(days)}</b>
        </Text>
      </>
    )}
  </Box>
)
```

</blockquote></details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts (1)</summary><blockquote>

`29-30`: **Minor formatting inconsistency.**

There's a small spacing inconsistency in the ApiProperty decorator.


```diff
-  @ApiProperty({ type: Number})
+  @ApiProperty({ type: Number })
```

</blockquote></details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts (3)</summary><blockquote>

`571-585`: **Shadowing the exported `include` constant may cause confusion**

Inside `getStatistics()` you declare  
```ts
const include: Includeable[] = []
```  
while an exported constant with the same name already exists at the top of the file.  
Although this is legal (block-scoped) it makes the code harder to reason about and can
lead to subtle bugs if future maintainers move code around.

```diff
-    const include: Includeable[] = []
+    const subpoenaInclude: Includeable[] = []
```

…and update subsequent references.

---

`587-591`: **Avoid running two separate queries when one would suffice**

`count()` executes a full table scan and the following `findAll()` repeats almost the
same predicates.  You can compute the total count in the grouped query itself with
`COUNT(*) OVER ()` (PostgreSQL) or a nested sub-query to save a trip to the database.

This is not blocking, but worth optimising before statistics start hitting production
traffic.

---

`613-621`: **Deriving days from the un-rounded value improves accuracy**

`averageServiceTimeDays` is computed from the _string_ that came from the DB instead of
the already rounded millisecond value.  If `averageServiceTimeMs` is `null`, the second
calculation still divides by a value that may be `undefined`.

```diff
- averageServiceTimeMs: Math.round(Number(row.averageServiceTimeMs) || 0),
- averageServiceTimeDays:
-   Math.round(Number(row.averageServiceTimeMs) / 1000 / 60 / 60 / 24) || 0,
+ const avgMs = Number(row.averageServiceTimeMs) || 0
+ averageServiceTimeMs: Math.round(avgMs),
+ averageServiceTimeDays: Math.round(avgMs / 1000 / 60 / 60 / 24),
```

</blockquote></details>
<details>
<summary>apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts (1)</summary><blockquote>

`37-39`: **Nullable GraphQL field but non-nullable TS property**

`serviceStatus` is declared as `!:` yet may legitimately be `null` and is exposed with
`nullable: true` in GraphQL.  Consider marking the TS property as optional to avoid
confusion:

```diff
- serviceStatus!: ServiceStatus | null
+ serviceStatus?: ServiceStatus | null
```

</blockquote></details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx (2)</summary><blockquote>

`119-123`: **Missing React `key` prop in mapped list**

`InfoCard` renders the `values` array with `map`, therefore each direct child should
have a `key`.

```diff
-<LabelValue label="Heildarfjöldi" value={stats?.count} />
+<LabelValue
+  key="all-count"
+  label="Heildarfjöldi"
+  value={stats?.count}
/>
```
Apply the same pattern to other single-element `values` arrays to silence React
warnings.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 119-123: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation. 

(lint/correctness/useJsxKeyInIterable)

</details>

</details>

---

`239-258`: **Ensure stable keys when mapping over enum values**

The key is currently `status ?? 'unserviced'` but `status` is an enum value which is a
string; collisions are unlikely, yet adding the index guarantees uniqueness:

```diff
- {[null, ...Object.values(ServiceStatus)].map((status) => {
+ {[null, ...Object.values(ServiceStatus)].map((status, idx) => {
...
-   key={status ?? 'unserviced'}
+   key={status ?? `unserviced-${idx}`}
```

</blockquote></details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/case/case.service.ts (3)</summary><blockquote>

`2388-2400`: **`findAll` + `include` may duplicate cases**

Including `EventLog` (1-N) without `distinct: true` means a case with multiple
`INDICTMENT_CONFIRMED` events will appear multiple times, inflating statistics.

Add `distinct: true` or switch to a sub-query to guarantee each case is counted once.

---

`2425-2461`: **Average ruling time may be skewed by cases lacking a confirmation event**

If a case is missing the `INDICTMENT_CONFIRMED` event, `confirmedEvent` is `undefined`
and the case is ignored in the duration array, effectively excluding it from the
average.  Verify that this is intentional; otherwise consider falling back to
`case.created` for such cases so statistics are comparable across all indictments.

---

`2352-2422`: **Whole-row loading can become expensive**

`getCaseStatistics()` loads every case into memory and iterates in JS.  
For large datasets (multiple years) this will stress the API node.

A future optimisation would push aggregation into SQL (e.g. `GROUP BY type, state`)
similar to what you did in `getStatistics()` for subpoenas.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ee463fa1b5c065c904718395dd34846ba760462b and 4183d1ad9b42646852392983212d7ac1b1248c14.

</details>

<details>
<summary>📒 Files selected for processing (27)</summary>

* `apps/judicial-system/api/src/app/modules/auth/auth.controller.ts` (0 hunks)
* `apps/judicial-system/api/src/app/modules/backend/backend.service.ts` (2 hunks)
* `apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts` (2 hunks)
* `apps/judicial-system/api/src/app/modules/case-list/dto/caseStatistics.input.ts` (1 hunks)
* `apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts` (1 hunks)
* `apps/judicial-system/backend/src/app/modules/case/case.controller.ts` (4 hunks)
* `apps/judicial-system/backend/src/app/modules/case/case.service.ts` (3 hunks)
* `apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts` (1 hunks)
* `apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts` (1 hunks)
* `apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts` (1 hunks)
* `apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts` (1 hunks)
* `apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts` (3 hunks)
* `apps/judicial-system/web/pages/notendur/tolfraedi.ts` (1 hunks)
* `apps/judicial-system/web/src/components/LabelValue/LabelValue.tsx` (1 hunks)
* `apps/judicial-system/web/src/components/index.ts` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.css.ts` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/getCaseStatistics.graphql` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/UserForm/UserForm.tsx` (0 hunks)
* `apps/judicial-system/web/src/routes/Admin/Users/Users.tsx` (3 hunks)
* `libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts` (1 hunks)
* `libs/judicial-system/consts/src/lib/consts.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* apps/judicial-system/api/src/app/modules/auth/auth.controller.ts
* apps/judicial-system/web/src/routes/Admin/UserForm/UserForm.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (2)</summary>

<details>
<summary>`apps/**/*`: "Confirm that the code adheres to the following:
- NextJS best practices, including file structure, API routes, and static generation methods.
- Efficient state manage...</summary>

> `apps/**/*`: "Confirm that the code adheres to the following:
> - NextJS best practices, including file structure, API routes, and static generation methods.
> - Efficient state management and server-side rendering techniques.
> - Optimal use of TypeScript for component and utility type safety."
> 

- `apps/judicial-system/web/src/components/index.ts`
- `apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts`
- `apps/judicial-system/web/src/routes/Admin/Users/Users.tsx`
- `apps/judicial-system/web/pages/notendur/tolfraedi.ts`
- `apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts`
- `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts`
- `apps/judicial-system/web/src/routes/Admin/Statistics/getCaseStatistics.graphql`
- `apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts`
- `apps/judicial-system/web/src/components/LabelValue/LabelValue.tsx`
- `apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts`
- `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.css.ts`
- `apps/judicial-system/api/src/app/modules/case-list/dto/caseStatistics.input.ts`
- `apps/judicial-system/api/src/app/modules/backend/backend.service.ts`
- `apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx`
- `apps/judicial-system/backend/src/app/modules/case/case.controller.ts`
- `apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts`
- `apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx`
- `apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx`
- `apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts`
- `apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts`
- `apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts`
- `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx`
- `apps/judicial-system/backend/src/app/modules/case/case.service.ts`

</details>
<details>
<summary>`libs/**/*`: "Confirm that the code adheres to the following:
- Reusability of components and hooks across different NextJS apps.
- TypeScript usage for defining props and exportin...</summary>

> `libs/**/*`: "Confirm that the code adheres to the following:
> - Reusability of components and hooks across different NextJS apps.
> - TypeScript usage for defining props and exporting types.
> - Effective tree-shaking and bundling practices."
> 

- `libs/judicial-system/consts/src/lib/consts.ts`
- `libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts`

</details>

</details><details>
<summary>🧬 Code Graph Analysis (10)</summary>

<details>
<summary>apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts (2)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts (1)</summary>

* `CaseStatistics` (33-45)

</details>
<details>
<summary>apps/judicial-system/api/src/app/modules/user/user.resolver.ts (1)</summary>

* `user` (62-77)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts (1)</summary><blockquote>

<details>
<summary>libs/judicial-system/types/src/index.ts (1)</summary>

* `ServiceStatus` (9-9)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/api/src/app/modules/backend/backend.service.ts (1)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts (1)</summary>

* `CaseStatistics` (33-45)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx (1)</summary><blockquote>

<details>
<summary>libs/island-ui/core/src/lib/Text/Text.tsx (1)</summary>

* `Text` (109-186)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts (1)</summary><blockquote>

<details>
<summary>libs/judicial-system/types/src/index.ts (1)</summary>

* `ServiceStatus` (9-9)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx (1)</summary><blockquote>

<details>
<summary>libs/island-ui/core/src/lib/Select/Select.tsx (1)</summary>

* `Select` (28-200)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts (3)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts (3)</summary>

* `IndictmentCaseStatistics` (5-20)
* `RequestCaseStatistics` (22-31)
* `CaseStatistics` (33-45)

</details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts (2)</summary>

* `ServiceStatusStatistics` (5-23)
* `SubpoenaStatistics` (25-31)

</details>
<details>
<summary>libs/judicial-system/types/src/index.ts (1)</summary>

* `ServiceStatus` (9-9)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts (1)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts (1)</summary>

* `SubpoenaStatistics` (25-31)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts (2)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts (2)</summary>

* `SubpoenaStatistics` (25-31)
* `ServiceStatusStatistics` (5-23)

</details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/case/case.service.ts (1)</summary>

* `include` (227-435)

</details>

</blockquote></details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/case/case.service.ts (3)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts (3)</summary>

* `CaseStatistics` (33-45)
* `IndictmentCaseStatistics` (5-20)
* `RequestCaseStatistics` (22-31)

</details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts (1)</summary>

* `partition` (1-17)

</details>
<details>
<summary>libs/judicial-system/types/src/lib/case.ts (3)</summary>

* `isIndictmentCase` (304-306)
* `indictmentCases` (302-302)
* `isCompletedCase` (371-373)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx</summary>

[error] 14-14: Do not shadow the global "escape" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx</summary>

[error] 119-123: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation. 

(lint/correctness/useJsxKeyInIterable)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: build (judicial-system-backend)
* GitHub Check: tests (judicial-system-api,judicial-system-audit-trail,judicial-system-auth,judicial-system-const...
* GitHub Check: build (judicial-system-api,judicial-system-digital-mailbox-api,judicial-system-web,judicial-syste...
* GitHub Check: tests (judicial-system-backend)
* GitHub Check: linting (judicial-system-api,judicial-system-backend,judicial-system-web,judicial-system-audit-tr...

</details>

<details>
<summary>🔇 Additional comments (23)</summary><blockquote>

<details>
<summary>apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts (1)</summary>

`17-17`: **Ensure updated access verification imports**  
The test now imports `verifyFullAccess` and `verifyNoAccess`; confirm these methods replace any previous `verifyReadAccess` behavior and exist in the `verify` module.

</details>
<details>
<summary>libs/judicial-system/consts/src/lib/consts.ts (1)</summary>

`212-212`: **Approve addition of `STATISTICS_ROUTE`**  
The new route constant follows existing naming conventions and supports the admin statistics page.

</details>
<details>
<summary>apps/judicial-system/web/src/components/index.ts (1)</summary>

`125-125`: **Barrel export added for `LabelValue`**  
The new export makes the component available across the app; ensure imports are updated accordingly.

</details>
<details>
<summary>apps/judicial-system/web/pages/notendur/tolfraedi.ts (1)</summary>

`1-3`: **Looks good, follows NextJS best practices.**

The file implements a NextJS page correctly by importing and exporting the `Statistics` component. This follows the standard NextJS pattern for page creation and component reuse.

</details>
<details>
<summary>libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts (1)</summary>

`16-16`: **Appropriate addition to audit trail actions.**

The new `GET_CASES_STATISTICS` audit action is correctly placed after the related `GET_CASES` action, maintaining a logical organization in the enum.

</details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Users/Users.tsx (1)</summary>

`62-80`: **UI enhancement follows component design patterns.**

The Statistics button is correctly implemented with appropriate icon and variant. The `justifyContent="spaceBetween"` property ensures proper spacing between the buttons, following island-ui/core Box component patterns.

</details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts (1)</summary>

`1-27`: **Well-structured internationalization implementation.**

The internationalization setup follows react-intl best practices with properly formatted message IDs, descriptive comments for translators, and consistent naming patterns. The tooltips provide helpful context for users of different statistics sections.

</details>
<details>
<summary>apps/judicial-system/api/src/app/modules/backend/backend.service.ts (2)</summary>

`18-18`: **Well-structured import of the CaseStatistics model.**

The import statement is well placed with related types, maintaining good organization in the imports section.

---

`199-211`: **LGTM! Well-implemented backend integration for statistics.**

The `getCaseStatistics` method is well structured with:
- Properly typed optional parameters
- Clean URL parameter handling
- Correctly formatted ISO date strings
- Reuse of the existing GET infrastructure

This implementation provides a clean interface to the backend's statistics endpoint while maintaining consistency with the existing service methods pattern.

</details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts (1)</summary>

`1-13`: **Well-structured helper utilities for ServiceStatus mapping.**

The helper functions are well organized with:
- Clear, descriptive Icelandic labels for each service status
- A defensive utility function that handles null/undefined values gracefully
- Consistent default value ("Óbirt") when status is missing or invalid

The exported constants and functions follow best practices for reusable utilities.

</details>
<details>
<summary>apps/judicial-system/api/src/app/modules/case-list/dto/caseStatistics.input.ts (1)</summary>

`1-22`: **Well-structured GraphQL input DTO with proper validation.**

The `CaseStatisticsInput` class is well designed with:
- Appropriate decorators for GraphQL schema generation
- Proper validation annotations
- Consistent use of optional properties
- Clear type definitions

This input type properly supports the filtering requirements for the statistics feature.

</details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/getCaseStatistics.graphql (1)</summary>

`1-27`: **Comprehensive GraphQL query structure for statistics.**

The query is well defined with:
- Optional input parameter for filtering
- Detailed nested structure for different case types
- Inclusion of both counts and time-based metrics
- Consistent naming conventions

This query provides all the necessary data points for the statistics feature in a single request, optimizing frontend performance.

</details>
<details>
<summary>apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts (1)</summary>

`19-22`: **Imports look good.**

The new imports for `CaseStatisticsInput` and `CaseStatistics` are appropriately added to support the new functionality.

</details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts (1)</summary>

`1-31`: **Model structure and documentation look good.**

The `ServiceStatusStatistics` and `SubpoenaStatistics` classes are well-structured with:
- Appropriate type definitions
- Clear property naming
- Detailed ApiProperty decorators for Swagger documentation
- Proper handling of nullable types where appropriate

This model will provide a solid foundation for the statistics feature.

</details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx (5)</summary>

`6-15`: **Props interface is well-defined.**

The FiltersPanelProps type properly defines all the necessary properties with appropriate types, making the component interface clear and type-safe.

---

`27-34`: **Good use of animations.**

The implementation of motion animations with AnimatePresence provides a smooth user experience when the filters panel appears and disappears.

---

`36-49`: **Select implementation follows best practices.**

The court selection dropdown correctly:
- Maps data to the expected option format
- Handles selection changes properly
- Maintains controlled component state

---

`51-70`: **Date pickers have appropriate constraints.**

Good implementation of date constraints:
- Maximum date is set to today
- The "to date" has a minimum date set to the "from date" to prevent illogical date ranges
- Both components are properly controlled with value handling

---

`72-81`: **Clear button implementation looks good.**

The clear button is properly positioned and uses appropriate styling with the destructive color scheme to indicate its action.

</details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts (2)</summary>

`1-31`: **Well-structured statistics models.**

The `IndictmentCaseStatistics` and `RequestCaseStatistics` classes provide a clear structure for the different types of case statistics with appropriate:
- Count fields for various states
- Time calculation fields where relevant
- Proper ApiProperty decorators for Swagger documentation

---

`33-45`: **Aggregation model is well-designed.**

The `CaseStatistics` class effectively aggregates all the different statistics types into a cohesive response model, making it easy to query all statistics in a single request.

</details>
<details>
<summary>apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts (1)</summary>

`599-604`: **Expression assumes `service_date` column name**

The literal:

```sql
AVG(EXTRACT(EPOCH FROM "Subpoena"."service_date" - "Subpoena"."created") * 1000)
```

hard-codes `service_date`. If the model maps `serviceDate` → `service_date` this is
fine, otherwise the query will fail at runtime. Please double-check that the column
names align with the database schema.

</details>
<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx (1)</summary>

`45-53`: **Passing native `Date` objects through GraphQL may fail silently**

`queryVariables` forwards `Date` instances; depending on your Apollo scalar setup,
these might be serialised as empty objects.  Confirm that the GraphQL schema expects
ISO strings or a custom `DateTime` scalar and convert accordingly:

```ts
fromDate: filters.fromDate?.toISOString(),
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T14:24:21Z
user: login: unakb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T14:57:26Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T14:57:50Z
user: login: unakb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T15:02:49Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T15:03:16Z
user: login: unakb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-28T15:19:41Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T15:20:27Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx (1)</summary><blockquote>

`3-3`: **Import path for AnimatePresence is appropriate for Framer Motion v11+**

The import path `motion/react` follows the new modular package structure in Framer Motion v11+ that improves performance through better tree-shaking, as noted in the provided learnings.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx (5)</summary><blockquote>

`101-101`: **Remove unnecessary whitespace character**

There's an extra space character in the JSX that should be removed.

```diff
-                  {' '}
+                  
```

---

`99-100`: **Improve AnimatePresence implementation**

The AnimatePresence component is used with `mode="wait"` but wraps multiple elements directly without proper animation configuration. Consider wrapping each animated element with motion components to properly handle enter/exit animations.

```diff
              <AnimatePresence mode="wait">
-                <>
+                <motion.div
+                  initial={{ opacity: 0 }}
+                  animate={{ opacity: 1 }}
+                  exit={{ opacity: 0 }}
+                  key={filter ? 'filtered' : 'unfiltered'}
+                >
```

And close the motion.div tag:

```diff
-                </>
+                </motion.div>
```

Don't forget to add the import:

```diff
- import { AnimatePresence } from 'motion/react'
+ import { AnimatePresence, motion } from 'motion/react'
```

---

`241-260`: **Optimize rendering with useMemo for service status statistics**

The mapping over ServiceStatus values is recalculated on every render. Consider using useMemo to optimize performance, especially when dealing with potentially large datasets.

```diff
-                               {[null, ...Object.values(ServiceStatus)].map(
-                                 (status) => {
-                                   const stat =
-                                     stats?.subpoenas.serviceStatusStatistics.find(
-                                       (s) => s.serviceStatus === status,
-                                     )
-                                   return (
-                                     <CountAndDays
-                                       key={status ?? 'unserviced'}
-                                       label={mapServiceStatusTitle(status)}
-                                       count={stat?.count}
-                                       days={
-                                         status
-                                           ? stat?.averageServiceTimeDays
-                                           : undefined
-                                       }
-                                     />
-                                   )
-                                 },
-                               )}
+                               {useMemo(() => 
+                                 [null, ...Object.values(ServiceStatus)].map(
+                                   (status) => {
+                                     const stat =
+                                       stats?.subpoenas.serviceStatusStatistics.find(
+                                         (s) => s.serviceStatus === status,
+                                       )
+                                     return (
+                                       <CountAndDays
+                                         key={status ?? 'unserviced'}
+                                         label={mapServiceStatusTitle(status)}
+                                         count={stat?.count}
+                                         days={
+                                           status
+                                             ? stat?.averageServiceTimeDays
+                                             : undefined
+                                         }
+                                       />
+                                     )
+                                   },
+                                 ), [stats?.subpoenas.serviceStatusStatistics])}
```

Make sure to import useMemo if it's not already imported:

```diff
-import { useEffect, useMemo, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
```

---

`102-267`: **Enhance accessibility and provide fallback values**

The InfoCard component could benefit from enhanced accessibility and explicit fallback values for statistics that might be undefined.


Consider adding the following improvements:

1. Add explicit fallback values for statistics:
```diff
                                <LabelValue
                                  label="Heildarfjöldi"
-                                 value={stats?.count}
+                                 value={stats?.count ?? 'Engin gögn'}
                                />
```

2. Add appropriate ARIA attributes for screen readers:
```diff
                  <InfoCard
+                    aria-label="Tölfræði yfir mál"
+                    role="region"
                    sections={[
```

3. Add section descriptions for screen readers:
```diff
                      {
                        id: 'all-cases',
+                       aria-label="Tölfræði yfir öll mál",
                        items: [
```

This pattern should be applied to all statistics sections to improve accessibility and provide better handling of undefined values.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 121-125: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation. 

(lint/correctness/useJsxKeyInIterable)

</details>

</details>

---

`268-277`: **Add a loading state to the CSV export button**

The CSV export button doesn't have a loading state, which could lead to confusion if exporting a large dataset takes time.

```diff
                    <StatisticsCSVButton
                      stats={stats}
                      fromDate={filter ? filters.fromDate : undefined}
                      toDate={filter ? filters.toDate : undefined}
                      institutionName={
                        filter ? filters.institution?.label : undefined
                      }
+                     disabled={loading}
                    />
```

This assumes the StatisticsCSVButton component accepts a disabled prop. If it doesn't, consider enhancing the component to support this functionality.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4183d1ad9b42646852392983212d7ac1b1248c14 and ad98d0692a09243f393eb2886c10aa7b971a7773.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts` (2 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx` (1 hunks)
* `apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx
* apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`apps/**/*`: "Confirm that the code adheres to the following:
- NextJS best practices, including file structure, API routes, and static generation methods.
- Efficient state manage...</summary>

> `apps/**/*`: "Confirm that the code adheres to the following:
> - NextJS best practices, including file structure, API routes, and static generation methods.
> - Efficient state management and server-side rendering techniques.
> - Optimal use of TypeScript for component and utility type safety."
> 

- `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx`

</details>

</details><details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx (2)</summary>

```
Learnt from: unakb
PR: island-is/island.is#18881
File: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx:3-3
Timestamp: 2025-04-28T15:20:24.933Z
Learning: For Framer Motion v11 and above, components like AnimatePresence should be imported from 'motion/react' instead of 'framer-motion' due to the library's new modular package structure that improves performance and enables better tree-shaking.
```

```
Learnt from: unakb
PR: island-is/island.is#18881
File: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx:3-3
Timestamp: 2025-04-28T15:20:24.933Z
Learning: For Framer Motion v11 and above, components like AnimatePresence should be imported from 'motion/react' instead of 'framer-motion' due to the library's new modular package structure that improves tree-shaking and reduces bundle sizes.
```

</details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx</summary>

[error] 121-125: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation. 

(lint/correctness/useJsxKeyInIterable)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: build (judicial-system-api,judicial-system-digital-mailbox-api,judicial-system-web,judicial-syste...
* GitHub Check: linting (judicial-system-api,judicial-system-backend,judicial-system-web,judicial-system-audit-tr...
* GitHub Check: linting-workspace
* GitHub Check: formatting
* GitHub Check: build (judicial-system-backend)
* GitHub Check: tests (judicial-system-api,judicial-system-audit-trail,judicial-system-auth,judicial-system-const...
* GitHub Check: tests (judicial-system-backend)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T15:25:29Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T15:28:45Z
user: login: gudjong
body: Some minor comments.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T12:04:00Z
user: login: unakb
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T12:07:17Z

Review comments:
diff_hunk: @@ -0,0 +1,17 @@
+export const partition = <T>(
+  array: T[],
+  predicate: (item: T) => boolean,
+): [T[], T[]] => {
+  const pass: T[] = []
+  const fail: T[] = []
+
+  for (const item of array) {
+    if (predicate(item)) {
+      pass.push(item)
+    } else {
+      fail.push(item)
+    }
+  }
+
+  return [pass, fail]
+}
path: apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add documentation and tests for `partition` utility**  
The `partition` helper is implemented correctly but lacks JSDoc and unit tests for edge cases (empty array, all-pass/all-fail). Please add documentation comments and comprehensive tests to ensure reliability.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts lines 1 to 17, the partition function lacks JSDoc comments and unit tests. Add a JSDoc comment above the function describing its purpose, parameters, and return type. Then create unit tests covering normal cases and edge cases such as empty arrays, all items passing, and all items failing to ensure the function behaves correctly in all scenarios.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-28T14:24:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import { FC } from 'react'
+
+import { Button } from '@island.is/island-ui/core'
+import { formatDate } from '@island.is/judicial-system/formatters'
+import { CaseStatistics } from '@island.is/judicial-system-web/src/graphql/schema'
+
+import { mapServiceStatusTitle } from '../helpers'
+
+const convertToCSV = (rows: Record<string, unknown>[]): string => {
+  if (!rows.length) return ''
+
+  const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
+
+  const escape = (val: unknown): string => {
+    if (typeof val === 'string') {
+      return val.includes(',') || val.includes('"')
+        ? `"${val.replace(/"/g, '""')}`
+        : val
+    }
+    return val != null ? String(val) : ''
+  }
+
+  const csv = [
+    headers.join(','),
+    ...rows.map((row) => headers.map((key) => escape(row[key])).join(',')),
+  ]
+
+  return csv.join('\n')
+}
path: apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix function name shadowing global property and refine type safety.**

The `escape` function name shadows the global `escape` property, which could lead to confusion.


Rename the `escape` function to something more descriptive like `escapeCSVValue` to avoid shadowing the global property.

```diff
- const escape = (val: unknown): string => {
+ const escapeCSVValue = (val: unknown): string => {
    if (typeof val === 'string') {
      return val.includes(',') || val.includes('"')
        ? `"${val.replace(/"/g, '""')}`
        : val
    }
    return val != null ? String(val) : ''
  }

  const csv = [
    headers.join(','),
-   ...rows.map((row) => headers.map((key) => escape(row[key])).join(',')),
+   ...rows.map((row) => headers.map((key) => escapeCSVValue(row[key])).join(',')),
  ]
```

Also, note that there's a bug in the string escaping - if a string includes quotes, the quoted string is not properly closed. The correct code should add closing quotes.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const convertToCSV = (rows: Record<string, unknown>[]): string => {
  if (!rows.length) return ''

  const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))

  const escapeCSVValue = (val: unknown): string => {
    if (typeof val === 'string') {
      return val.includes(',') || val.includes('"')
        ? `"${val.replace(/"/g, '""')}"`
        : val
    }
    return val != null ? String(val) : ''
  }

  const csv = [
    headers.join(','),
    ...rows.map((row) =>
      headers.map((key) => escapeCSVValue(row[key])).join(',')
    ),
  ]

  return csv.join('\n')
}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 14-14: Do not shadow the global "escape" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx between lines 9 and 29, rename the local function 'escape' to 'escapeCSVValue' to avoid shadowing the global 'escape' property. Additionally, fix the string escaping logic by ensuring that if a string contains quotes or commas, it is properly enclosed in double quotes with both opening and closing quotes, and internal quotes are doubled for CSV compliance.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-28T14:24:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import { FC } from 'react'
+
+import { Button } from '@island.is/island-ui/core'
+import { formatDate } from '@island.is/judicial-system/formatters'
+import { CaseStatistics } from '@island.is/judicial-system-web/src/graphql/schema'
+
+import { mapServiceStatusTitle } from '../helpers'
+
+const convertToCSV = (rows: Record<string, unknown>[]): string => {
+  if (!rows.length) return ''
+
+  const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
+
+  const escape = (val: unknown): string => {
+    if (typeof val === 'string') {
+      return val.includes(',') || val.includes('"')
+        ? `"${val.replace(/"/g, '""')}`
+        : val
+    }
+    return val != null ? String(val) : ''
+  }
+
+  const csv = [
+    headers.join(','),
+    ...rows.map((row) => headers.map((key) => escape(row[key])).join(',')),
+  ]
+
+  return csv.join('\n')
+}
+
+interface StatisticsCSVButtonProps {
+  stats?: CaseStatistics
+  fromDate?: Date
+  toDate?: Date
+  institutionName?: string
+}
+
+export const StatisticsCSVButton: FC<StatisticsCSVButtonProps> = ({
+  stats,
+  fromDate,
+  toDate,
+  institutionName,
+}) => {
+  const formatDateForFilename = (date: Date) =>
+    formatDate(date)?.replace(/\./g, '_')
+
+  const getFilename = () => {
+    const parts = [
+      'RVG_tölur',
+      institutionName?.replace(/\s+/g, '_').toLowerCase(),
+      fromDate ? formatDateForFilename(fromDate) : '',
+      toDate ? formatDateForFilename(toDate) : '',
+    ]
+    return parts.filter(Boolean).join('_') || 'case_statistics'
+  }
+
+  const buildRequestSection = (s: CaseStatistics) => [
+    { Tegund: 'R mál' },
+    { Titill: 'Heildarfjöldi', Samtals: s.requestCases.count },
+    { Titill: 'Í vinnslu', Samtals: s.requestCases.inProgressCount },
+    { Titill: 'Lokið', Samtals: s.requestCases.completedCount },
+  ]
+
+  const buildIndictmentSection = (s: CaseStatistics) => [
+    { Tegund: 'S mál' },
+    { Titill: 'Heildarfjöldi', Samtals: s.indictmentCases.count },
+    {
+      Titill: 'Í vinnslu',
+      Samtals: s.indictmentCases.inProgressCount,
+    },
+    {
+      Titill: 'Lokið með dómi',
+      Samtals: s.indictmentCases.rulingCount,
+      'Meðaltími (dagar)': s.indictmentCases.averageRulingTimeDays,
+      'Meðaltími (klst)': (
+        s.indictmentCases.averageRulingTimeMs /
+        (1000 * 60 * 60)
+      ).toFixed(2),
+    },
path: apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Use consistent time formatting approach across the component.**

The calculation for converting milliseconds to hours is repeated in multiple places, which could lead to inconsistencies.


Extract the time conversion logic to a helper function for better maintainability.

```diff
+ const millisecondsToHours = (ms: number): string => 
+   (ms / (1000 * 60 * 60)).toFixed(2)

  // Then use it here
  {
    Titill: 'Lokið með dómi',
    Samtals: s.indictmentCases.rulingCount,
    'Meðaltími (dagar)': s.indictmentCases.averageRulingTimeDays,
-   'Meðaltími (klst)': (
-     s.indictmentCases.averageRulingTimeMs /
-     (1000 * 60 * 60)
-   ).toFixed(2),
+   'Meðaltími (klst)': millisecondsToHours(s.indictmentCases.averageRulingTimeMs),
  }

  // And use the same helper function in the subpoena section as well
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
// Add this helper (e.g. at the top of the file or just above your component)
const millisecondsToHours = (ms: number): string =>
  (ms / (1000 * 60 * 60)).toFixed(2)

// …later, in your CSV data definition…

{
  Titill: 'Lokið með dómi',
  Samtals: s.indictmentCases.rulingCount,
  'Meðaltími (dagar)': s.indictmentCases.averageRulingTimeDays,
  'Meðaltími (klst)': millisecondsToHours(
    s.indictmentCases.averageRulingTimeMs,
  ),
},
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx around lines 74 to 79, the conversion of milliseconds to hours is done inline multiple times, risking inconsistency. Extract this conversion logic into a reusable helper function that takes milliseconds as input and returns the formatted hours string. Replace all inline conversions with calls to this helper function to ensure consistent formatting and easier maintenance.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-28T14:24:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,4 +60,29 @@ export class CaseListResolver {
 
     return result
   }
+
+  @Query(() => CaseStatistics, { nullable: true })
+  caseStatistics(
+    @Args('input', { type: () => CaseStatisticsInput, nullable: true })
+    input: CaseStatisticsInput,
+    @CurrentGraphQlUser()
+    user: User,
+    @Context('dataSources')
+    { backendService }: { backendService: BackendService },
+  ): Promise<CaseStatistics> {
+    this.logger.debug('Getting case statistics')
+
+    const result = this.auditTrailService.audit(
+      user.id,
+      AuditedAction.GET_CASES,
+      backendService.getCaseStatistics(
+        input.fromDate,
+        input.toDate,
+        input.institutionId,
+      ),
+      (caseStatistics: CaseStatistics) => caseStatistics.count.toString(),
+    )
+
+    return result
+  }
path: apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Incorrect audit action for statistics.**

The implementation follows the established pattern for GraphQL resolvers, but the audit action should be more specific. You're using `AuditedAction.GET_CASES` which is the same action used for listing cases, but this is a distinct operation that should have its own audit trail entry.


```diff
-      AuditedAction.GET_CASES,
+      AuditedAction.GET_CASES_STATISTICS,
```

I recommend defining a new audit action constant specifically for this statistics operation, following the same pattern seen in other actions like `GET_USER`.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if GET_CASES_STATISTICS is defined in the audit trail constants
rg -A 5 -B 5 "GET_CASES_STATISTICS" libs/judicial-system/audit-trail/src/
```

Length of output: 1130

---



</details>

**Use Specific Audit Action for Case Statistics**

The `caseStatistics` resolver should use the dedicated `GET_CASES_STATISTICS` audit action (already defined in `AuditedAction`) to keep your audit trail precise.

Affected location:
- File: `apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts`
- Method: `caseStatistics` (around line 64)

Suggested diff:
```diff
   const result = this.auditTrailService.audit(
-    user.id,
-    AuditedAction.GET_CASES,
+    user.id,
+    AuditedAction.GET_CASES_STATISTICS,
    backendService.getCaseStatistics(
      input.fromDate,
      input.toDate,
      input.institutionId,
    ),
    (caseStatistics: CaseStatistics) => caseStatistics.count.toString(),
  )
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  @Query(() => CaseStatistics, { nullable: true })
  caseStatistics(
    @Args('input', { type: () => CaseStatisticsInput, nullable: true })
    input: CaseStatisticsInput,
    @CurrentGraphQlUser()
    user: User,
    @Context('dataSources')
    { backendService }: { backendService: BackendService },
  ): Promise<CaseStatistics> {
    this.logger.debug('Getting case statistics')

    const result = this.auditTrailService.audit(
      user.id,
      AuditedAction.GET_CASES_STATISTICS,
      backendService.getCaseStatistics(
        input.fromDate,
        input.toDate,
        input.institutionId,
      ),
      (caseStatistics: CaseStatistics) => caseStatistics.count.toString(),
    )

    return result
  }
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts around lines 64 to 87, the audit action used in the caseStatistics resolver is incorrectly set to AuditedAction.GET_CASES, which is meant for listing cases. To fix this, replace AuditedAction.GET_CASES with the more specific AuditedAction.GET_CASES_STATISTICS constant to accurately reflect the operation in the audit trail.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-28T14:24:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,308 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useIntl } from 'react-intl'
+import { AnimatePresence } from 'motion/react'
path: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Incorrect import path for AnimatePresence**

`AnimatePresence` is exported from `framer-motion`, not `motion/react`.  
This will throw at runtime and break the statistics page.

```diff
-import { AnimatePresence } from 'motion/react'
+import { AnimatePresence } from 'framer-motion'
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
-import { AnimatePresence } from 'motion/react'
+import { AnimatePresence } from 'framer-motion'
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx at line 3, the import path for AnimatePresence is incorrect; it should be imported from 'framer-motion' instead of 'motion/react'. Update the import statement to import AnimatePresence from 'framer-motion' to prevent runtime errors and ensure the statistics page functions correctly.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-28T14:24:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,152 @@
+import { FC } from 'react'
+
+import { Button } from '@island.is/island-ui/core'
+import { formatDate } from '@island.is/judicial-system/formatters'
+import { CaseStatistics } from '@island.is/judicial-system-web/src/graphql/schema'
+
+import { mapServiceStatusTitle } from '../helpers'
+
+const convertToCSV = (rows: Record<string, unknown>[]): string => {
+  if (!rows.length) return ''
+
+  const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
+
+  const escape = (val: unknown): string => {
+    if (typeof val === 'string') {
+      return val.includes(',') || val.includes('"')
+        ? `"${val.replace(/"/g, '""')}`
+        : val
+    }
+    return val != null ? String(val) : ''
+  }
+
+  const csv = [
+    headers.join(','),
+    ...rows.map((row) => headers.map((key) => escape(row[key])).join(',')),
+  ]
+
+  return csv.join('\n')
+}
path: apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx
user: login: unakb
body: Good point, will fix :D 
created_at: 2025-04-28T14:57:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,152 @@
+import { FC } from 'react'
+
+import { Button } from '@island.is/island-ui/core'
+import { formatDate } from '@island.is/judicial-system/formatters'
+import { CaseStatistics } from '@island.is/judicial-system-web/src/graphql/schema'
+
+import { mapServiceStatusTitle } from '../helpers'
+
+const convertToCSV = (rows: Record<string, unknown>[]): string => {
+  if (!rows.length) return ''
+
+  const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
+
+  const escape = (val: unknown): string => {
+    if (typeof val === 'string') {
+      return val.includes(',') || val.includes('"')
+        ? `"${val.replace(/"/g, '""')}`
+        : val
+    }
+    return val != null ? String(val) : ''
+  }
+
+  const csv = [
+    headers.join(','),
+    ...rows.map((row) => headers.map((key) => escape(row[key])).join(',')),
+  ]
+
+  return csv.join('\n')
+}
path: apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx
user: login: coderabbitai[bot]
body: @unakb Thanks for taking this feedback onboard! The fix will improve both code clarity and ensure CSV data is properly escaped. Let me know if you need any help with this or have other questions! 



(\(\
(^.^)
o(")(")

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20shadowing%20globals!%20Rename%20%60escape%60%20to%20%60escapeCSVValue%60%20for%20clarity%20%26%20fix%20CSV%20string%20escaping%20by%20properly%20closing%20quotes.%20Cleaner%2C%20safer%20CSV%20exports!%20Try%20%40coderabbitai%20for%20smart%20code%20reviews%20%F0%9F%9A%80%E2%9C%A8&url=https%3A%2F%2Fgithub.com%2Fisland-is%2Fisland.is%2Fpull%2F18881%23discussion_r2063777727)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@unakb Thanks for taking this feedback onboard! The fix will improve both code clarity and ensure CSV data is properly escaped. Let me know if you need any help with this or have other questions! 



(\(\
(^.^)
o(")(")

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20shadowing%20globals!%20Rename%20%60escape%60%20to%20%60escapeCSVValue%60%20for%20clarity%20%26%20fix%20CSV%20string%20escaping%20by%20properly%20closing%20quotes.%20Cleaner%2C%20safer%20CSV%20exports!%20Try%20%40coderabbitai%20for%20smart%20code%20reviews%20%F0%9F%9A%80%E2%9C%A8&url=https%3A%2F%2Fgithub.com%2Fisland-is%2Fisland.is%2Fpull%2F18881%23discussion_r2063777727)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-28T14:57:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,4 +60,29 @@ export class CaseListResolver {
 
     return result
   }
+
+  @Query(() => CaseStatistics, { nullable: true })
+  caseStatistics(
+    @Args('input', { type: () => CaseStatisticsInput, nullable: true })
+    input: CaseStatisticsInput,
+    @CurrentGraphQlUser()
+    user: User,
+    @Context('dataSources')
+    { backendService }: { backendService: BackendService },
+  ): Promise<CaseStatistics> {
+    this.logger.debug('Getting case statistics')
+
+    const result = this.auditTrailService.audit(
+      user.id,
+      AuditedAction.GET_CASES,
+      backendService.getCaseStatistics(
+        input.fromDate,
+        input.toDate,
+        input.institutionId,
+      ),
+      (caseStatistics: CaseStatistics) => caseStatistics.count.toString(),
+    )
+
+    return result
+  }
path: apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts
user: login: unakb
body: Yes good catch! I forgot to replace the audited action with my new one
created_at: 2025-04-28T15:02:49Z
author_association: MEMBER
diff_hunk: @@ -58,4 +60,29 @@ export class CaseListResolver {
 
     return result
   }
+
+  @Query(() => CaseStatistics, { nullable: true })
+  caseStatistics(
+    @Args('input', { type: () => CaseStatisticsInput, nullable: true })
+    input: CaseStatisticsInput,
+    @CurrentGraphQlUser()
+    user: User,
+    @Context('dataSources')
+    { backendService }: { backendService: BackendService },
+  ): Promise<CaseStatistics> {
+    this.logger.debug('Getting case statistics')
+
+    const result = this.auditTrailService.audit(
+      user.id,
+      AuditedAction.GET_CASES,
+      backendService.getCaseStatistics(
+        input.fromDate,
+        input.toDate,
+        input.institutionId,
+      ),
+      (caseStatistics: CaseStatistics) => caseStatistics.count.toString(),
+    )
+
+    return result
+  }
path: apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts
user: login: coderabbitai[bot]
body: @unakb Thanks for confirming! Using the specific `AuditedAction.GET_CASES_STATISTICS` constant will make your audit trail more accurate and easier to filter when reviewing system activity. This kind of attention to detail helps maintain a clean audit history.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Precision%20matters%20in%20audit%20trails!%20%F0%9F%9B%A1%EF%B8%8F%20Use%20%60GET_CASES_STATISTICS%60%20instead%20of%20%60GET_CASES%60%20for%20case%20stats%20to%20keep%20logs%20clear%20and%20accurate.%20Try%20%40coderabbitai%20for%20smart%20code%20reviews!&url=https%3A%2F%2Fgithub.com%2Fisland-is%2Fisland.is%2Fpull%2F18881%23discussion_r2063777754)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@unakb Thanks for confirming! Using the specific `AuditedAction.GET_CASES_STATISTICS` constant will make your audit trail more accurate and easier to filter when reviewing system activity. This kind of attention to detail helps maintain a clean audit history.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Precision%20matters%20in%20audit%20trails!%20%F0%9F%9B%A1%EF%B8%8F%20Use%20%60GET_CASES_STATISTICS%60%20instead%20of%20%60GET_CASES%60%20for%20case%20stats%20to%20keep%20logs%20clear%20and%20accurate.%20Try%20%40coderabbitai%20for%20smart%20code%20reviews!&url=https%3A%2F%2Fgithub.com%2Fisland-is%2Fisland.is%2Fpull%2F18881%23discussion_r2063777754)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-28T15:03:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,308 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useIntl } from 'react-intl'
+import { AnimatePresence } from 'motion/react'
path: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx
user: login: unakb
body: No this is the correct way to import because we're using > v11
created_at: 2025-04-28T15:19:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,308 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useIntl } from 'react-intl'
+import { AnimatePresence } from 'motion/react'
path: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@unakb You're absolutely right! Thank you for the clarification. Framer Motion v11+ indeed uses the new modular package structure where `AnimatePresence` should be imported from `motion/react` rather than the older `framer-motion` path. This new structure helps with tree-shaking and reduces bundle sizes.



---

🌐 Web query:

```web
framer-motion v11 import from motion/react instead of framer-motion
```

💡 Result:

Starting with Framer Motion version 11, there are significant changes that impact how you import and use the library. Here's what you need to know to transition from importing from `framer-motion` to `motion/react`:

## Why the Change?
- **Merger**: Version 11.11.12 merged Framer Motion with Motion One, shifting towards the Framer Motion API conventions for a broader user base[4].
- **Performance Improvements**: The new version focuses on performance enhancements, such as moving render scheduling to microtasks, which improves handling of synchronous re-renders[1].

## How to Import in Version 11
You need to change your import statements to use `motion/react` instead of `framer-motion`:

**Old Way (with framer-motion):**
```javascript
import { motion } from "framer-motion";
```

**New Way (with motion/react):**
```javascript
import { motion } from "motion/react";
```

## Key Changes and Considerations

- **Velocity Calculation**: Updates to a `MotionValue` within the same frame no longer contribute to velocity calculations, ensuring that synchronous updates are treated as instantaneous[1].
- **Render Scheduling**: Render updates are now scheduled as microtasks, which helps with performance and ensures that only the final render is applied on-screen[1].
- **Testing with Jest**: Due to the render scheduling change, tests should await an animation frame to ensure that changes have applied before asserting styles[1].

## Incompatible with React 19
Be aware that current versions of Framer Motion may have compatibility issues with React 19[7]. If you are using React 19, you might need to check for updates or workarounds.

By moving to `motion/react`, you benefit from enhancements in performance and compatibility with newer animation APIs. Ensure you review the updated documentation and examples to leverage the full potential of these changes.

Citations:

- 1: https://motion.dev/docs/react-upgrade-guide
- 2: https://github.com/framer/motion/issues/2726
- 3: https://www.npmjs.com/package/framer-motion/v/11.1.1
- 4: https://motion.dev/docs/upgrade-guide
- 5: https://blog.maximeheckel.com/posts/guide-animations-spark-joy-framer-motion/
- 6: https://www.npmjs.com/package/framer-motion
- 7: https://github.com/framer/motion/issues/2668
- 8: https://github.com/chakra-ui/chakra-ui/issues/3618
- 9: https://www.npmjs.com/package/framer-motion/v/11.13.1
- 10: https://motion.dev/docs/react-motion-component

---



</details>

@unakb You're absolutely right! The web search confirms that with Framer Motion v11+, the correct import path is `motion/react` rather than `framer-motion`. This change came with the v11 merger of Framer Motion with Motion One, bringing performance improvements like microtask-based render scheduling. Thank you for the correction.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: unakb
PR: island-is/island.is#18881
File: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx:3-3
Timestamp: 2025-04-28T15:20:24.933Z
Learning: For Framer Motion v11 and above, components like AnimatePresence should be imported from 'motion/react' instead of 'framer-motion' due to the library's new modular package structure that improves tree-shaking and reduces bundle sizes.
```

---

```
Learnt from: unakb
PR: island-is/island.is#18881
File: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx:3-3
Timestamp: 2025-04-28T15:20:24.933Z
Learning: For Framer Motion v11 and above, components like AnimatePresence should be imported from 'motion/react' instead of 'framer-motion' due to the library's new modular package structure that improves performance and enables better tree-shaking.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@unakb You're absolutely right! Thank you for the clarification. Framer Motion v11+ indeed uses the new modular package structure where `AnimatePresence` should be imported from `motion/react` rather than the older `framer-motion` path. This new structure helps with tree-shaking and reduces bundle sizes.



---

🌐 Web query:

```web
framer-motion v11 import from motion/react instead of framer-motion
```

💡 Result:

Starting with Framer Motion version 11, there are significant changes that impact how you import and use the library. Here's what you need to know to transition from importing from `framer-motion` to `motion/react`:

## Why the Change?
- **Merger**: Version 11.11.12 merged Framer Motion with Motion One, shifting towards the Framer Motion API conventions for a broader user base[4].
- **Performance Improvements**: The new version focuses on performance enhancements, such as moving render scheduling to microtasks, which improves handling of synchronous re-renders[1].

## How to Import in Version 11
You need to change your import statements to use `motion/react` instead of `framer-motion`:

**Old Way (with framer-motion):**
```javascript
import { motion } from "framer-motion";
```

**New Way (with motion/react):**
```javascript
import { motion } from "motion/react";
```

## Key Changes and Considerations

- **Velocity Calculation**: Updates to a `MotionValue` within the same frame no longer contribute to velocity calculations, ensuring that synchronous updates are treated as instantaneous[1].
- **Render Scheduling**: Render updates are now scheduled as microtasks, which helps with performance and ensures that only the final render is applied on-screen[1].
- **Testing with Jest**: Due to the render scheduling change, tests should await an animation frame to ensure that changes have applied before asserting styles[1].

## Incompatible with React 19
Be aware that current versions of Framer Motion may have compatibility issues with React 19[7]. If you are using React 19, you might need to check for updates or workarounds.

By moving to `motion/react`, you benefit from enhancements in performance and compatibility with newer animation APIs. Ensure you review the updated documentation and examples to leverage the full potential of these changes.

Citations:

- 1: https://motion.dev/docs/react-upgrade-guide
- 2: https://github.com/framer/motion/issues/2726
- 3: https://www.npmjs.com/package/framer-motion/v/11.1.1
- 4: https://motion.dev/docs/upgrade-guide
- 5: https://blog.maximeheckel.com/posts/guide-animations-spark-joy-framer-motion/
- 6: https://www.npmjs.com/package/framer-motion
- 7: https://github.com/framer/motion/issues/2668
- 8: https://github.com/chakra-ui/chakra-ui/issues/3618
- 9: https://www.npmjs.com/package/framer-motion/v/11.13.1
- 10: https://motion.dev/docs/react-motion-component

---



</details>

@unakb You're absolutely right! The web search confirms that with Framer Motion v11+, the correct import path is `motion/react` rather than `framer-motion`. This change came with the v11 merger of Framer Motion with Motion One, bringing performance improvements like microtask-based render scheduling. Thank you for the correction.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: unakb
PR: island-is/island.is#18881
File: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx:3-3
Timestamp: 2025-04-28T15:20:24.933Z
Learning: For Framer Motion v11 and above, components like AnimatePresence should be imported from 'motion/react' instead of 'framer-motion' due to the library's new modular package structure that improves tree-shaking and reduces bundle sizes.
```

---

```
Learnt from: unakb
PR: island-is/island.is#18881
File: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx:3-3
Timestamp: 2025-04-28T15:20:24.933Z
Learning: For Framer Motion v11 and above, components like AnimatePresence should be imported from 'motion/react' instead of 'framer-motion' due to the library's new modular package structure that improves performance and enables better tree-shaking.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-28T15:20:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,311 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useIntl } from 'react-intl'
+import { AnimatePresence } from 'motion/react'
+
+import {
+  Box,
+  GridColumn,
+  GridRow,
+  SkeletonLoader,
+  Text,
+  ToggleSwitchCheckbox,
+  Tooltip,
+} from '@island.is/island-ui/core'
+import {
+  InfoCard,
+  LabelValue,
+  PageHeader,
+} from '@island.is/judicial-system-web/src/components'
+import {
+  CaseStatistics,
+  ServiceStatus,
+} from '@island.is/judicial-system-web/src/graphql/schema'
+import { useInstitution } from '@island.is/judicial-system-web/src/utils/hooks'
+
+import CountAndDays from './components/CountAndDays'
+import { FiltersPanel } from './components/FiltersPanel'
+import { StatisticsCSVButton } from './components/StatisticsCSVButton'
+import { useCaseStatisticsQuery } from './getCaseStatistics.generated'
+import { mapServiceStatusTitle } from './helpers'
+import { strings } from './Statistics.strings'
+import * as styles from './Statistics.css'
+
+const Statistics = () => {
+  const [filter, setFilter] = useState(false)
+  const [stats, setStats] = useState<CaseStatistics | undefined>()
+  const [filters, setFilters] = useState<{
+    fromDate?: Date
+    toDate?: Date
+    institution?: { label: string; value: string }
+  }>({})
+
+  const { formatMessage } = useIntl()
+  const { districtCourts } = useInstitution()
+
+  const queryVariables = useMemo(() => {
+    return filter
+      ? {
+          fromDate: filters.fromDate,
+          toDate: filters.toDate,
+          institutionId: filters.institution?.value,
+        }
+      : {}
+  }, [filter, filters])
+
+  const { data, loading } = useCaseStatisticsQuery({
+    variables: {
+      input: queryVariables,
+    },
+    fetchPolicy: 'cache-and-network',
+  })
+
+  useEffect(() => {
+    if (data?.caseStatistics) {
+      setStats(data.caseStatistics)
+    }
+  }, [data])
+
+  return (
+    <Box className={styles.statisticsPageContainer}>
+      <PageHeader title="Tölfræði" />
+      <Box className={styles.statisticsContentBox}>
+        <Box
+          display="flex"
+          justifyContent="spaceBetween"
+          alignItems="center"
+          marginBottom={[2, 2, 4, 6, 6]}
+        >
+          <Text as="h1" variant="h1">
+            Tölfræði
+          </Text>
+          <ToggleSwitchCheckbox
+            name="toggleFilter"
+            label={<Text>Sía</Text>}
+            checked={filter}
+            onChange={setFilter}
+          />
+        </Box>
+
+        <GridRow
+          rowGap={3}
+          direction={['columnReverse', 'columnReverse', 'columnReverse', 'row']}
+        >
+          <GridColumn
+            span={['12/12', '12/12', '12/12', filter ? '8/12' : '12/12']}
+          >
+            {loading && !stats ? (
+              <SkeletonLoader height={800} />
+            ) : (
+              <AnimatePresence mode="wait">
+                <>
+                  {' '}
+                  <InfoCard
+                    sections={[
+                      {
+                        id: 'all-cases',
+                        items: [
+                          {
+                            id: 'case-item',
+                            title: (
+                              <Text variant="h3" marginBottom={2}>
+                                {'Öll mál '}
+                                <Tooltip
+                                  placement="right"
+                                  text={formatMessage(
+                                    strings.allCasesStatisticsTooltip,
+                                  )}
+                                />
+                              </Text>
+                            ),
+                            values: [
+                              <LabelValue
+                                label="Heildarfjöldi"
+                                value={stats?.count}
+                              />,
+                            ],
path: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add a key prop to the LabelValue component in the iterable**

React requires a unique key prop for elements in arrays to help with efficient reconciliation and rendering.

```diff
                             <LabelValue
+                              key="all-cases-count"
                               label="Heildarfjöldi"
                               value={stats?.count}
                             />,
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                              <LabelValue
                                key="all-cases-count"
                                label="Heildarfjöldi"
                                value={stats?.count}
                              />,
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 121-125: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation. 

(lint/correctness/useJsxKeyInIterable)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx around lines 121 to 125, the LabelValue component inside an array is missing a unique key prop. Add a key prop with a unique value, such as an identifier from the stats object or the index, to the LabelValue component to satisfy React's requirement for keys in lists and improve rendering performance.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-28T15:25:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,311 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useIntl } from 'react-intl'
+import { AnimatePresence } from 'motion/react'
+
+import {
+  Box,
+  GridColumn,
+  GridRow,
+  SkeletonLoader,
+  Text,
+  ToggleSwitchCheckbox,
+  Tooltip,
+} from '@island.is/island-ui/core'
+import {
+  InfoCard,
+  LabelValue,
+  PageHeader,
+} from '@island.is/judicial-system-web/src/components'
+import {
+  CaseStatistics,
+  ServiceStatus,
+} from '@island.is/judicial-system-web/src/graphql/schema'
+import { useInstitution } from '@island.is/judicial-system-web/src/utils/hooks'
+
+import CountAndDays from './components/CountAndDays'
+import { FiltersPanel } from './components/FiltersPanel'
+import { StatisticsCSVButton } from './components/StatisticsCSVButton'
+import { useCaseStatisticsQuery } from './getCaseStatistics.generated'
+import { mapServiceStatusTitle } from './helpers'
+import { strings } from './Statistics.strings'
+import * as styles from './Statistics.css'
+
+const Statistics = () => {
+  const [filter, setFilter] = useState(false)
+  const [stats, setStats] = useState<CaseStatistics | undefined>()
+  const [filters, setFilters] = useState<{
+    fromDate?: Date
+    toDate?: Date
+    institution?: { label: string; value: string }
+  }>({})
+
+  const { formatMessage } = useIntl()
+  const { districtCourts } = useInstitution()
+
+  const queryVariables = useMemo(() => {
+    return filter
+      ? {
+          fromDate: filters.fromDate,
+          toDate: filters.toDate,
+          institutionId: filters.institution?.value,
+        }
+      : {}
+  }, [filter, filters])
+
+  const { data, loading } = useCaseStatisticsQuery({
+    variables: {
+      input: queryVariables,
+    },
+    fetchPolicy: 'cache-and-network',
+  })
path: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for the GraphQL query**

The query implementation doesn't handle potential errors that could occur during data fetching. Consider capturing and handling the error state from the query hook to provide appropriate feedback to users when data fetching fails.

```diff
- const { data, loading } = useCaseStatisticsQuery({
+ const { data, loading, error } = useCaseStatisticsQuery({
    variables: {
      input: queryVariables,
    },
    fetchPolicy: 'cache-and-network',
  })

+ // Add error handling UI
+ useEffect(() => {
+   if (error) {
+     // Show error message or toast notification
+     console.error('Failed to fetch statistics:', error);
+   }
+ }, [error]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const { data, loading, error } = useCaseStatisticsQuery({
    variables: {
      input: queryVariables,
    },
    fetchPolicy: 'cache-and-network',
  })

  useEffect(() => {
    if (error) {
      console.error('Failed to fetch statistics:', error)
    }
  }, [error])
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx around lines 55 to 60, the GraphQL query using useCaseStatisticsQuery lacks error handling. Modify the query hook to also destructure the error property, then implement logic to check for this error and display an appropriate user-friendly message or UI feedback when an error occurs during data fetching.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-28T15:25:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +15,7 @@ import {
   SignatureConfirmationResponse,
 } from '../case'
 import { CaseListEntry } from '../case-list'
+import { CaseStatistics } from '../case-list/models/caseStatistics.model'
path: apps/judicial-system/api/src/app/modules/backend/backend.service.ts
user: login: gudjong
body: Please consider using a barrel import here.
created_at: 2025-04-29T11:38:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,27 @@
+import { defineMessages } from 'react-intl'
path: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts
user: login: gudjong
body: Please consider not using Contentful for these strings.
created_at: 2025-04-29T11:59:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,27 @@
+import { defineMessages } from 'react-intl'
path: apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts
user: login: unakb
body: I decided to use it just for these ones as I wasn't sure how clear the descriptions were and so we can easily update them if we want to change them. 😄 
created_at: 2025-04-29T12:07:17Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-28T11:39:10Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces a comprehensive case statistics feature across the judicial system's backend and web applications. It adds new API endpoints, data models, and backend logic to aggregate and serve statistics on cases, indictments, requests, and subpoenas, with filtering options by date and institution. The frontend includes a new statistics page with UI components for displaying, filtering, and exporting aggregated data, as well as navigation updates to access the statistics screen. Supporting utilities, types, and audit trail entries are also introduced or updated to support this functionality.

## Changes

| File(s)                                                                                                         | Change Summary |
|-----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `apps/judicial-system/api/src/app/modules/auth/auth.controller.ts`, `apps/judicial-system/backend/src/app/modules/case/filters/test/defenceUserFilter.spec.ts`, `apps/judicial-system/web/src/routes/Admin/UserForm/UserForm.tsx` | Removed unused import statements. No logic changes. |
| `apps/judicial-system/api/src/app/modules/backend/backend.service.ts`                                           | Added `getCaseStatistics` method to fetch case statistics from the backend with optional filtering. Imported `CaseStatistics` type. |
| `apps/judicial-system/api/src/app/modules/case-list/caseList.resolver.ts`                                      | Added `caseStatistics` GraphQL resolver method to retrieve and audit case statistics, with input filtering. Imported relevant types. |
| `apps/judicial-system/api/src/app/modules/case-list/dto/caseStatistics.input.ts`                               | Introduced `CaseStatisticsInput` GraphQL input type for filtering statistics by date and institution. |
| `apps/judicial-system/api/src/app/modules/case-list/models/caseStatistics.model.ts`                            | Added GraphQL object types for various case statistics categories (indictment, request, subpoena, overall). |
| `apps/judicial-system/api/src/app/modules/case-list/index.ts`                                                  | Exported `CaseStatistics` from `caseStatistics.model` to extend module's public API. |
| `apps/judicial-system/backend/src/app/modules/case/case.controller.ts`                                         | Added authenticated and authorized GET endpoint `/cases/statistics` for retrieving case statistics with optional filters. |
| `apps/judicial-system/backend/src/app/modules/case/case.service.ts`                                            | Added methods to compute and return aggregated case statistics, including helpers for indictment and request cases. |
| `apps/judicial-system/backend/src/app/modules/case/filters/filterHelpers.ts`                                   | Introduced a generic `partition` utility function to split arrays by predicate. |
| `apps/judicial-system/backend/src/app/modules/case/models/caseStatistics.response.ts`                          | Added TypeScript classes modeling the structure of case statistics API responses, with Swagger decorators. |
| `apps/judicial-system/backend/src/app/modules/subpoena/models/subpoenaStatistics.response.ts`                  | Added TypeScript classes for subpoena and service status statistics, with Swagger decorators. |
| `apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts`                                    | Added `getStatistics` method to aggregate subpoena statistics by service status and time, with filtering. |
| `apps/judicial-system/web/pages/notendur/tolfraedi.ts`                                                         | Added new page that exports the statistics component as default. |
| `apps/judicial-system/web/src/components/LabelValue/LabelValue.tsx`                                            | Introduced `LabelValue` React component for displaying label-value pairs. |
| `apps/judicial-system/web/src/components/index.ts`                                                             | Exported the new `LabelValue` component. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.css.ts`                                       | Added CSS module for statistics page layout and content box styling. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.strings.ts`                                   | Added localized tooltip messages for statistics sections. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/Statistics.tsx`                                          | Added main React component for statistics page: fetches, filters, and displays case statistics, with CSV export. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/components/CountAndDays.tsx`                             | Added component to display counts and average days, with formatting. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/components/FiltersPanel.tsx`                             | Added filter panel component for selecting date range and institution. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/components/StatisticsCSVButton.tsx`                      | Added button component to export statistics as CSV, with formatting logic. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/getCaseStatistics.graphql`                               | Added GraphQL query for fetching case statistics with optional input filters. |
| `apps/judicial-system/web/src/routes/Admin/Statistics/helpers.ts`                                               | Added helper for mapping service status codes to Icelandic labels. |
| `apps/judicial-system/web/src/routes/Admin/Users/Users.tsx`                                                    | Added "Tölfræði" button to user admin page, linking to statistics route. |
| `libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts`                                               | Added `GET_CASES_STATISTICS` action to the `AuditedAction` enum. |
| `libs/judicial-system/consts/src/lib/consts.ts`                                                                | Added `STATISTICS_ROUTE` constant for the statistics page route. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant WebApp as Web App (Statistics Page)
    participant API as API (GraphQL)
    participant Backend as Backend Service
    participant CaseService
    participant SubpoenaService

    User->>WebApp: Navigate to Statistics Page
    WebApp->>API: Query caseStatistics(input)
    API->>Backend: backendService.getCaseStatistics(fromDate, toDate, institutionId)
    Backend->>CaseService: getCaseStatistics(from, to, institutionId)
    CaseService->>CaseService: Partition cases, compute statistics
    CaseService->>SubpoenaService: getStatistics(from, to, institutionId)
    SubpoenaService-->>CaseService: Subpoena statistics
    CaseService-->>Backend: Aggregated CaseStatistics
    Backend-->>API: CaseStatistics
    API-->>WebApp: CaseStatistics
    WebApp-->>User: Display statistics, allow filtering and CSV export
```

```mermaid
sequenceDiagram
    User->>WebApp: Click "Tölfræði" button
    WebApp->>WebApp: Route to /notendur/tolfraedi
    WebApp->>API: Query caseStatistics (optionally with filters)
    API-->>WebApp: Return statistics data
    WebApp-->>User: Render statistics page with data
```

```mermaid
sequenceDiagram
    User->>WebApp: Use FiltersPanel to select date/institution
    WebApp->>API: Query caseStatistics with updated filters
    API-->>WebApp: Filtered statistics data
    WebApp-->>User: Update displayed statistics
```

```mermaid
sequenceDiagram
    User->>WebApp: Click "CSV Export" button
    WebApp->>WebApp: Generate CSV from displayed statistics
    WebApp-->>User: Trigger CSV file download
```

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ad98d0692a09243f393eb2886c10aa7b971a7773 and 91ceac2717ba82753ca1201f3b8c89398a41d19e.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `apps/judicial-system/api/src/app/modules/backend/backend.service.ts` (2 hunks)
* `apps/judicial-system/api/src/app/modules/case-list/index.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/judicial-system/api/src/app/modules/case-list/index.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* apps/judicial-system/api/src/app/modules/backend/backend.service.ts

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=island-is/island.is&utm_content=18881):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: nx-cloud[bot]
created_at: 2025-04-28T11:41:45Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/6810c883f7d820161adf9fe7?utm_source=pull-request&utm_medium=comment) for commit 834e9645eaf5958e100b9cfd62f46cbbfbb9f875.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx run-many --projects judicial-system-api,judi...` | ✅ Succeeded | 3s | [View ↗](https://cloud.nx.app/runs/M4wAvTvitG?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=build --projects=judicial-...` | ✅ Succeeded | 15s | [View ↗](https://cloud.nx.app/runs/8kr5bTGW4d?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=build --projects=judicial-...` | ✅ Succeeded | 13s | [View ↗](https://cloud.nx.app/runs/OwlsTXoL5D?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --projects judicial-system-backend ...` | ✅ Succeeded | 1s | [View ↗](https://cloud.nx.app/runs/FCaXU8TK57?utm_source=pull-request&utm_medium=comment) |
| `nx run-many -t lint --output-style=static --pro...` | ✅ Succeeded | 4s | [View ↗](https://cloud.nx.app/runs/PVLTtBzCgx?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=codegen/frontend-client --...` | ✅ Succeeded | 13s | [View ↗](https://cloud.nx.app/runs/Fe2Qwd12kM?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=codegen/backend-schema --a...` | ✅ Succeeded | 5s | [View ↗](https://cloud.nx.app/runs/0GAkgHs7az?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=codegen/backend-client --a...` | ✅ Succeeded | 22s | [View ↗](https://cloud.nx.app/runs/So1jmErwgp?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-04-29 12:44:40` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE
user: login: datadog-island-is[bot]
created_at: 2025-04-28T12:14:36Z
body: 
## Datadog Report

Branch report: [`j-s/statistics`](https://app.datadoghq.eu/ci/test-branch/github.com%2Fisland-is%2Fisland.is/judicial-system-api/j-s%2Fstatistics?env=ci&index=citest&_user_click_origin_=github)
Commit report: [`20ddefc`](https://app.datadoghq.eu/ci/test-commit/github.com%2Fisland-is%2Fisland.is/judicial-system-api/j-s%2Fstatistics/20ddefc7284ec7c461e1bf44801ed8d068ce5cdc?env=ci&index=citest&_user_click_origin_=github)
Test service: `judicial-system-api`

:white_check_mark: 0 Failed, 61 Passed, 0 Skipped, 3.47s Total Time




author_association: NONE