{
+ if (windowWidth > theme.grid.breakpoints.lg) {
+ return (
+ (workqueueConfig &&
+ workqueueConfig.columns.map((column) => ({
+ label: intl.formatMessage(column.label),
+ width: 35,
+ key: column.id,
+ sortFunction: onColumnClick,
+ isSorted: sortedCol === column.id
+ }))) ??
+ []
+ )
+ } else {
+ return (
+ (workqueueConfig &&
+ workqueueConfig.columns
+ .map((column) => ({
+ label: intl.formatMessage(column.label),
+ width: 35,
+ key: column.id,
+ sortFunction: onColumnClick,
+ isSorted: sortedCol === column.id
+ }))
+ .slice(0, 2)) ??
+ []
+ )
+ }
+ }
+
+ let content
+ let noResultText = intl.formatMessage(messages.noResult)
+ if (isLoading) {
+ content = (
+
+
+
+ )
+ } else if (queryError || fieldValueErrors.length > 0) {
+ noResultText = ''
+ content = (
+
+ {intl.formatMessage(messages.queryError)}
+
+ )
+ } else if (queryData && total > 0) {
+ content = (
+
+ )
+ }
+ return (
+
+
+
+ }
+ title={`${intl.formatMessage(messages.searchResult)} ${
+ isLoading ? '' : ' (' + total + ')'
+ }`}
+ >
+ {content}
+
+
+
+ )
+}
+
+function SearchModifierComponent({
+ searchParams
+}: {
+ searchParams: Record
+}) {
+ const navigate = useNavigate()
+ const intl = useIntl()
+
+ return (
+ <>
+
+
+ navigate(ROUTES.V2.ADVANCED_SEARCH.path, { state: searchParams })
+ }
+ >
+ {intl.formatMessage(messages.edit)}
+
+
+ >
+ )
+}
diff --git a/packages/client/src/v2-events/features/events/AdvancedSearch/TabSearch.tsx b/packages/client/src/v2-events/features/events/AdvancedSearch/TabSearch.tsx
index da353942e6..ac8ce76313 100644
--- a/packages/client/src/v2-events/features/events/AdvancedSearch/TabSearch.tsx
+++ b/packages/client/src/v2-events/features/events/AdvancedSearch/TabSearch.tsx
@@ -11,6 +11,7 @@
import * as React from 'react'
import styled from 'styled-components'
import { useIntl, defineMessages, IntlShape } from 'react-intl'
+import { useNavigate } from 'react-router-dom'
import { Accordion } from '@opencrvs/components'
import { FieldValue } from '@opencrvs/commons/client'
import { Icon } from '@opencrvs/components/lib/Icon'
@@ -18,6 +19,8 @@ import { Button } from '@opencrvs/components/lib/Button'
import { EventConfig } from '@opencrvs/commons'
import { FormFieldGenerator } from '@client/v2-events/components/forms/FormFieldGenerator'
import { getAllUniqueFields } from '@client/v2-events/utils'
+import { ROUTES } from '@client/v2-events/routes'
+import { getValidationErrorsForForm } from '@client/v2-events/components/forms/validation'
const SearchButton = styled(Button)`
margin-top: 32px;
@@ -26,7 +29,7 @@ const SearchButton = styled(Button)`
const messagesToDefine = {
search: {
defaultMessage: 'Search',
- description: 'The title of search input submit button',
+ description: 'Label for search button',
id: 'v2.buttons.search'
},
hide: {
@@ -47,7 +50,8 @@ function getSectionFields(
event: EventConfig,
formValues: Record,
handleFieldChange: (fieldId: string, value: FieldValue) => void,
- intl: IntlShape
+ intl: IntlShape,
+ fieldValues?: Record
) {
const advancedSearchSections = event.advancedSearch
const allUniqueFields = getAllUniqueFields(event)
@@ -55,11 +59,11 @@ function getSectionFields(
const advancedSearchFieldId = section.fields.map(
(f: { fieldId: string }) => f.fieldId
)
- const fields = allUniqueFields.filter((field) =>
+ const advancedSearchFields = allUniqueFields.filter((field) =>
advancedSearchFieldId.includes(field.id)
)
- const modifiedFields = fields.map((f) => ({
+ const modifiedFields = advancedSearchFields.map((f) => ({
...f,
required: false // advanced search fields need not be required
}))
@@ -77,6 +81,7 @@ function getSectionFields(
fields={modifiedFields}
formData={formValues}
id={section.id}
+ initialValues={fieldValues}
setAllFieldsDirty={false}
onChange={(updatedValues) => {
Object.entries(updatedValues).forEach(([fieldId, value]) => {
@@ -89,16 +94,28 @@ function getSectionFields(
})
}
-export function TabSearch({ currentEvent }: { currentEvent: EventConfig }) {
+export function TabSearch({
+ currentEvent,
+ fieldValues
+}: {
+ currentEvent: EventConfig
+ fieldValues?: Record
+}) {
+ const [hasEnoughParams, setHasEnoughParams] = React.useState(false)
const intl = useIntl()
const [formValues, setFormValues] = React.useState<
Record
>({})
+ const navigate = useNavigate()
React.useEffect(() => {
setFormValues({})
}, [currentEvent])
+ React.useEffect(() => {
+ setHasEnoughParams(Object.entries(formValues).length > 0)
+ }, [formValues])
+
const handleFieldChange = (fieldId: string, value: FieldValue) => {
setFormValues((prev) => ({
...prev,
@@ -106,25 +123,70 @@ export function TabSearch({ currentEvent }: { currentEvent: EventConfig }) {
}))
}
+ const advancedSearchSections = currentEvent.advancedSearch
+ const allUniqueFields = getAllUniqueFields(currentEvent)
+ const fieldErrors = advancedSearchSections.reduce(
+ (errorsOnFields, currentSection) => {
+ const advancedSearchFieldIds = currentSection.fields.map(
+ (f: { fieldId: string }) => f.fieldId
+ )
+ const advancedSearchFields = allUniqueFields.filter((field) =>
+ advancedSearchFieldIds.includes(field.id)
+ )
+
+ const modifiedFields = advancedSearchFields.map((f) => ({
+ ...f,
+ required: false // advanced search fields need not be required
+ }))
+
+ const err = getValidationErrorsForForm(modifiedFields, formValues)
+
+ return {
+ ...errorsOnFields,
+ ...err
+ }
+ },
+ {}
+ )
+
+ const allErrors = Object.values(fieldErrors).flatMap(
+ // @ts-ignore
+ (errObj) => errObj.errors
+ )
+
+ const handleSearch = () => {
+ const searchParams = new URLSearchParams()
+
+ Object.entries(formValues).forEach(([key, value]) => {
+ if (value) {
+ searchParams.append(key, String(value))
+ } // Convert all values to strings
+ })
+
+ const navigateTo = ROUTES.V2.SEARCH_RESULT.buildPath({
+ eventType: currentEvent.id
+ })
+
+ navigate(`${navigateTo}?${searchParams.toString()}`)
+ }
+
const SectionFields = getSectionFields(
currentEvent,
formValues,
handleFieldChange,
- intl
+ intl,
+ fieldValues
)
- const hasEnoughParams = Object.entries(formValues).length > 0
const Search = (
0}
id="search"
size="large"
type="primary"
- onClick={() => {
- alert(JSON.stringify(formValues)) // @todo replace with actual search
- }}
+ onClick={handleSearch}
>
{intl.formatMessage(messages.search)}
diff --git a/packages/client/src/v2-events/routes/config.tsx b/packages/client/src/v2-events/routes/config.tsx
index 1bc3640207..f01fdcefbc 100644
--- a/packages/client/src/v2-events/routes/config.tsx
+++ b/packages/client/src/v2-events/routes/config.tsx
@@ -26,6 +26,7 @@ import { router as workqueueRouter } from '@client/v2-events/features/workqueues
import { EventOverviewLayout } from '@client/v2-events/layouts'
import { TRPCErrorBoundary } from '@client/v2-events/routes/TRPCErrorBoundary'
import { TRPCProvider } from '@client/v2-events/trpc'
+import { SearchResult } from '@client/v2-events/features/events/AdvancedSearch/SearchResult'
import { ROUTES } from './routes'
/**
@@ -124,6 +125,10 @@ export const routesConfig = {
{
path: ROUTES.V2.ADVANCED_SEARCH.path,
element:
+ },
+ {
+ path: ROUTES.V2.SEARCH_RESULT.path,
+ element:
}
]
} satisfies RouteObject
diff --git a/packages/client/src/v2-events/routes/routes.ts b/packages/client/src/v2-events/routes/routes.ts
index 3f4282c1d5..74a7944903 100644
--- a/packages/client/src/v2-events/routes/routes.ts
+++ b/packages/client/src/v2-events/routes/routes.ts
@@ -85,7 +85,10 @@ export const ROUTES = {
}
),
WORKQUEUES: workqueueRoutes,
- ADVANCED_SEARCH: route('advanced-search')
+ ADVANCED_SEARCH: route('advanced-search'),
+ SEARCH_RESULT: route('search-result/:eventType', {
+ params: { eventType: string().defined() }
+ })
}
)
}
diff --git a/packages/commons/src/conditionals/validate.ts b/packages/commons/src/conditionals/validate.ts
index cd262bacf4..88b0de061b 100644
--- a/packages/commons/src/conditionals/validate.ts
+++ b/packages/commons/src/conditionals/validate.ts
@@ -230,7 +230,7 @@ function runCustomFieldValidations({
* e.g. email is proper format, date is a valid date, etc.
* for custom validations @see runCustomFieldValidations
*/
-function validateFieldInput({
+export function validateFieldInput({
field,
value
}: {
diff --git a/packages/commons/src/events/AdvancedSearchConfig.ts b/packages/commons/src/events/AdvancedSearchConfig.ts
index 37979e9b78..a37b3e0093 100644
--- a/packages/commons/src/events/AdvancedSearchConfig.ts
+++ b/packages/commons/src/events/AdvancedSearchConfig.ts
@@ -12,7 +12,6 @@ import { z } from 'zod'
import { TranslationConfig } from './TranslationConfig'
export const AdvancedSearchConfig = z.object({
- id: z.string().describe('Advanced search section id'),
title: TranslationConfig.describe('Advanced search tab title'),
fields: z
.array(
diff --git a/packages/commons/src/events/EventIndex.ts b/packages/commons/src/events/EventIndex.ts
index fef6d0db34..a478fbbc85 100644
--- a/packages/commons/src/events/EventIndex.ts
+++ b/packages/commons/src/events/EventIndex.ts
@@ -16,4 +16,11 @@ export const EventIndex = EventMetadata.extend({
data: z.record(z.string(), z.any())
})
+export const EventSearchIndex = z.record(z.string(), z.any()).and(
+ z.object({
+ type: z.string() // Ensures "type" (event-id) exists and is a string
+ })
+)
+
+export type EventSearchIndex = z.infer
export type EventIndex = z.infer
diff --git a/packages/commons/src/fixtures/tennis-club-membership-event.ts b/packages/commons/src/fixtures/tennis-club-membership-event.ts
index dab986be1a..6fdd472f96 100644
--- a/packages/commons/src/fixtures/tennis-club-membership-event.ts
+++ b/packages/commons/src/fixtures/tennis-club-membership-event.ts
@@ -1243,7 +1243,6 @@ export const tennisClubMembershipEvent = defineConfig({
],
advancedSearch: [
{
- id: 'RANDOM',
title: {
defaultMessage: 'Tennis club registration search',
description: 'This is what this event is referred as in the system',
diff --git a/packages/events/src/router/event/index.ts b/packages/events/src/router/event/index.ts
index 08977c6b91..9cabddc325 100644
--- a/packages/events/src/router/event/index.ts
+++ b/packages/events/src/router/event/index.ts
@@ -20,7 +20,7 @@ import {
getEventById
} from '@events/service/events/events'
import { presignFilesInEvent } from '@events/service/files'
-import { getIndexedEvents } from '@events/service/indexing/indexing'
+import { getIndex, getIndexedEvents } from '@events/service/indexing/indexing'
import {
EventConfig,
getUUID,
@@ -39,7 +39,8 @@ import {
NotifyActionInput,
RegisterActionInput,
ValidateActionInput,
- FieldValue
+ FieldValue,
+ EventSearchIndex
} from '@opencrvs/commons/events'
import { router, publicProcedure } from '@events/router/trpc'
import { approveCorrection } from '@events/service/events/actions/approve-correction'
@@ -273,5 +274,10 @@ export const eventRouter = router({
logger.info(input.data)
return getEventById(input.eventId)
})
- })
+ }),
+ search: publicProcedure
+ .input(EventSearchIndex)
+ .query(async ({ input, ctx }) => {
+ return getIndex(input)
+ })
})
diff --git a/packages/events/src/service/indexing/indexing.ts b/packages/events/src/service/indexing/indexing.ts
index d8c6f8991f..cf6b891fcf 100644
--- a/packages/events/src/service/indexing/indexing.ts
+++ b/packages/events/src/service/indexing/indexing.ts
@@ -14,6 +14,7 @@ import {
EventConfig,
EventDocument,
EventIndex,
+ EventSearchIndex,
FieldConfig,
FieldType,
getCurrentEventState
@@ -28,6 +29,8 @@ import {
import { getAllFields, logger } from '@opencrvs/commons'
import { Transform } from 'stream'
import { z } from 'zod'
+import { DEFAULT_SIZE, generateQuery } from './utils'
+
function eventToEventIndex(event: EventDocument): EventIndex {
return encodeEventIndex(getCurrentEventState(event))
}
@@ -302,3 +305,30 @@ export async function getIndexedEvents() {
return events
}
+
+export async function getIndex(eventParams: EventSearchIndex) {
+ const esClient = getOrCreateClient()
+ const { type, ...queryParams } = eventParams
+
+ if (Object.values(queryParams).length === 0) {
+ throw new Error('No search params provided')
+ }
+
+ const query = generateQuery(queryParams)
+
+ const response = await esClient.search({
+ index: getEventIndexName(type),
+ size: DEFAULT_SIZE,
+ request_cache: false,
+ query
+ })
+
+ const events = z.array(EventIndex).parse(
+ response.hits.hits
+ .map((hit) => hit._source)
+ .filter((event): event is EncodedEventIndex => event !== undefined)
+ .map((event) => decodeEventIndex(event))
+ )
+
+ return events
+}
diff --git a/packages/events/src/service/indexing/utils.ts b/packages/events/src/service/indexing/utils.ts
new file mode 100644
index 0000000000..d1828ee027
--- /dev/null
+++ b/packages/events/src/service/indexing/utils.ts
@@ -0,0 +1,34 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * OpenCRVS is also distributed under the terms of the Civil Registration
+ * & Healthcare Disclaimer located at http://opencrvs.org/license.
+ *
+ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
+ */
+import { EventSearchIndex } from '@opencrvs/commons/events'
+import {
+ QueryDslQueryContainer,
+ SearchRequest
+} from '@elastic/elasticsearch/lib/api/types'
+
+const FIELD_SEPARATOR = '____'
+export const DEFAULT_SIZE = 10
+
+export function generateQuery(event: Omit) {
+ const must: QueryDslQueryContainer[] = Object.entries(event).map(
+ ([key, value]) => ({
+ match: {
+ [`data.${key.replaceAll('.', FIELD_SEPARATOR)}`]: value
+ }
+ })
+ )
+
+ return {
+ bool: {
+ must
+ }
+ } as SearchRequest['query']
+}