From 95a2e05e5f100dd8bb89f0aa1d69af26b395163d Mon Sep 17 00:00:00 2001 From: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:58:01 -0400 Subject: [PATCH] feat: Service Edits (#1097) * fix ts errors * go to service edit page in edit mode * group with other basic components * tweaks from convo with Josh * update service edit drawer to use RHF * convert AttributeModal to RHF * attribute supplement schemas * schema updates * dynamic fields * update i18n generation * update schema & migrations * update api * Attribute modal * update handler cache * fix linting errors * alter view * supplement handling * uncomment criteria * update mock data * update msw worker, clean up common schema files * update handler * add submit handler, update schema * use Section.* components * add sections * patch for z.never() * schema update * break in to pieces * add i18next as peerdep * enable new turbo ui * sonarcloud workspace binding * service attribute processing * update criteria * extract processing to separate fns * unify service attrib select criteria, update api & mock data * allow passed data, widen param type * display data * implement Add Attribute modal * fix useRouter import * launch service drawer * remove data viewer * temp disable gtag addition * `slug` isn't being passed to page props?? * temp remove service multi select popover * revert paths * fix PageContent props * fix group spacing * Add Coverage modal * remove placeholder * styling * api routes & mock data * service edit drawer work * cleanup console.log * update attribute schemas * remove devtool * misc cleanup * sonarlint config * cleanup * fix schema * fix sonarlint issues * add sonarlint recommendation * fix sonar/deepsource issues --------- Signed-off-by: Joe Karow <58997957+JoeKarow@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .github/renovate.json | 2 +- .vscode/settings.json | 6 - InReach.code-workspace | 7 + apps/app/.vscode/settings.json | 6 - apps/app/src/pages/_app.tsx | 10 +- .../{edit/index.tsx => edit.tsx} | 29 +- .../[orgLocationId]/edit/[orgServiceId].tsx | 298 ---- apps/app/src/pages/org/[slug]/edit.tsx | 2 - apps/app/src/pages/org/[slug]/index.tsx | 9 +- apps/app/src/types/nextjs-routes.d.ts | 1 - apps/web/.vscode/settings.json | 6 - lambdas/.vscode/settings.json | 6 - package.json | 3 +- packages/analytics/.vscode/settings.json | 6 - packages/api/.vscode/settings.json | 6 - packages/api/formatters/attributes.ts | 116 +- packages/api/package.json | 2 + packages/api/router/fieldOpt/index.ts | 4 + .../query.attributesByCategory.handler.ts | 22 +- .../router/fieldOpt/query.ccaMap.handler.ts | 28 + .../router/fieldOpt/query.ccaMap.schema.ts | 4 + .../router/fieldOpt/query.govDists.handler.ts | 1 + packages/api/router/fieldOpt/schemas.ts | 1 + .../mutation.attachAttribute.handler.ts | 46 +- .../mutation.attachAttribute.schema.ts | 72 +- .../service/formatters/modalAndEditDrawer.ts | 382 +++++ .../query.forServiceEditDrawer.handler.ts | 9 +- .../service/query.forServiceModal.handler.ts | 132 +- packages/api/router/serviceArea/index.ts | 18 + .../serviceArea/mutation.addToArea.handler.ts | 50 + .../serviceArea/mutation.addToArea.schema.ts | 26 + .../mutation.delFromArea.handler.ts | 46 + .../mutation.delFromArea.schema.ts | 19 + packages/api/router/serviceArea/schemas.ts | 2 + packages/auth/.vscode/settings.json | 6 - packages/config/.vscode/settings.json | 6 - packages/crowdin/.vscode/settings.json | 6 - packages/db/.vscode/settings.json | 6 - .../db/generated/attributeSupplementSchema.ts | 90 ++ packages/db/lib/generateData.ts | 1 + .../generators/attributeSuppDataSchemas.ts | 35 + packages/db/lib/generators/index.ts | 1 + .../index.ts | 150 ++ .../data.json | 1396 +++++++++++++++++ .../2024-02-15_attribute-attachments/index.ts | 64 + ...2024-03-21_attribute-supplement-schemas.ts | 95 ++ .../2024-04-03_access-instruction-schemas.ts | 297 ++++ packages/db/prisma/data-migrations/index.ts | 4 + .../migration.sql | 48 + .../migration.sql | 11 + .../migration.sql | 33 + .../20240216161351_alter_view/migration.sql | 36 + packages/db/prisma/schema.prisma | 11 + packages/db/zod_util/attributeSupplement.ts | 7 +- packages/env/.vscode/settings.json | 6 - packages/eslint-config/.vscode/settings.json | 6 - packages/ui/.storybook/main.ts | 1 - packages/ui/.vscode/settings.json | 4 - packages/ui/components/core/UserAvatar.tsx | 3 +- .../data-display/ContactInfo/index.tsx | 3 +- packages/ui/components/data-display/Hours.tsx | 8 +- .../PhoneNumberEntry/withHookForm.tsx | 7 +- .../data-portal/ServiceEditDrawer.tsx | 309 ---- .../index.stories.tsx} | 15 +- .../data-portal/ServiceEditDrawer/index.tsx | 361 +++++ .../data-portal/ServiceEditDrawer/schemas.ts | 55 + .../data-portal/ServiceEditDrawer/styles.ts | 28 + .../data-portal/ServiceSelect/index.tsx | 36 +- packages/ui/components/sections/Navbar.tsx | 16 +- .../ui/components/sections/ServicesInfo.tsx | 45 +- packages/ui/hooks/useEditMode.ts | 6 +- packages/ui/hooks/useFreeText.ts | 17 +- packages/ui/mockData/fieldOpt.ts | 43 +- .../json/fieldOpt.attributesByCategory.json | 2 +- .../ui/mockData/json/fieldOpt.ccaMap.json | 1 + .../json/location.forLocationCard.json | 2 +- .../json/service.forServiceEditDrawer.json | 2 +- .../json/service.forServiceInfoCard.json | 2 +- .../json/service.forServiceModal.json | 2 +- packages/ui/mockData/organization.ts | 7 + packages/ui/mockData/serviceArea.ts | 10 + packages/ui/modals/CoverageArea/hooks.ts | 7 +- .../ui/modals/CoverageArea/index.stories.tsx | 1 + packages/ui/modals/CoverageArea/index.tsx | 386 ++--- packages/ui/modals/CoverageArea/schema.ts | 27 - packages/ui/modals/LoginSignUp/index.tsx | 12 +- packages/ui/modals/Service/ModalText.tsx | 16 + packages/ui/modals/Service/index.stories.tsx | 1 - packages/ui/modals/Service/index.tsx | 409 +---- packages/ui/modals/Service/processor.tsx | 263 ++++ packages/ui/modals/Service/styles.ts | 17 + .../dataPortal/Attributes/SelectionItem.tsx | 41 + .../modals/dataPortal/Attributes/fields.tsx | 251 ++- .../dataPortal/Attributes/index.stories.tsx | 16 +- .../ui/modals/dataPortal/Attributes/index.tsx | 479 ++---- .../ui/modals/dataPortal/Attributes/schema.ts | 20 + .../ui/modals/dataPortal/Attributes/types.ts | 44 + packages/util/.vscode/settings.json | 6 - patches/json-schema-to-zod@2.0.14.patch | 28 + pnpm-lock.yaml | 13 +- turbo.json | 1 + 101 files changed, 4598 insertions(+), 2117 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 apps/app/.vscode/settings.json rename apps/app/src/pages/org/[slug]/[orgLocationId]/{edit/index.tsx => edit.tsx} (96%) delete mode 100644 apps/app/src/pages/org/[slug]/[orgLocationId]/edit/[orgServiceId].tsx delete mode 100644 apps/web/.vscode/settings.json delete mode 100644 lambdas/.vscode/settings.json delete mode 100644 packages/analytics/.vscode/settings.json delete mode 100644 packages/api/.vscode/settings.json create mode 100644 packages/api/router/fieldOpt/query.ccaMap.handler.ts create mode 100644 packages/api/router/fieldOpt/query.ccaMap.schema.ts create mode 100644 packages/api/router/service/formatters/modalAndEditDrawer.ts create mode 100644 packages/api/router/serviceArea/mutation.addToArea.handler.ts create mode 100644 packages/api/router/serviceArea/mutation.addToArea.schema.ts create mode 100644 packages/api/router/serviceArea/mutation.delFromArea.handler.ts create mode 100644 packages/api/router/serviceArea/mutation.delFromArea.schema.ts delete mode 100644 packages/auth/.vscode/settings.json delete mode 100644 packages/config/.vscode/settings.json delete mode 100644 packages/crowdin/.vscode/settings.json delete mode 100644 packages/db/.vscode/settings.json create mode 100644 packages/db/generated/attributeSupplementSchema.ts create mode 100644 packages/db/lib/generators/attributeSuppDataSchemas.ts create mode 100644 packages/db/prisma/data-migrations/2024-02-14_attribute-supplement-schemas/index.ts create mode 100644 packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/data.json create mode 100644 packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/index.ts create mode 100644 packages/db/prisma/data-migrations/2024-03-21_attribute-supplement-schemas.ts create mode 100644 packages/db/prisma/data-migrations/2024-04-03_access-instruction-schemas.ts create mode 100644 packages/db/prisma/migrations/20240214173007_attribute_supplement_schemas/migration.sql create mode 100644 packages/db/prisma/migrations/20240215164734_attribute_attachment/migration.sql create mode 100644 packages/db/prisma/migrations/20240215172645_update_attrib_by_cat_view/migration.sql create mode 100644 packages/db/prisma/migrations/20240216161351_alter_view/migration.sql delete mode 100644 packages/env/.vscode/settings.json delete mode 100644 packages/eslint-config/.vscode/settings.json delete mode 100644 packages/ui/components/data-portal/ServiceEditDrawer.tsx rename packages/ui/components/data-portal/{ServiceEditDrawer.stories.tsx => ServiceEditDrawer/index.stories.tsx} (68%) create mode 100644 packages/ui/components/data-portal/ServiceEditDrawer/index.tsx create mode 100644 packages/ui/components/data-portal/ServiceEditDrawer/schemas.ts create mode 100644 packages/ui/components/data-portal/ServiceEditDrawer/styles.ts create mode 100644 packages/ui/mockData/json/fieldOpt.ccaMap.json delete mode 100644 packages/ui/modals/CoverageArea/schema.ts create mode 100644 packages/ui/modals/Service/ModalText.tsx create mode 100644 packages/ui/modals/Service/processor.tsx create mode 100644 packages/ui/modals/Service/styles.ts create mode 100644 packages/ui/modals/dataPortal/Attributes/SelectionItem.tsx create mode 100644 packages/ui/modals/dataPortal/Attributes/schema.ts create mode 100644 packages/ui/modals/dataPortal/Attributes/types.ts delete mode 100644 packages/util/.vscode/settings.json create mode 100644 patches/json-schema-to-zod@2.0.14.patch diff --git a/.github/renovate.json b/.github/renovate.json index 9366e4aed10..0f9dc84ca00 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -6,7 +6,7 @@ "packageRules": [ { "groupName": "patched packages", - "matchPackageNames": ["@crowdin/ota-client", "trpc-panel", "msw-storybook-addon"], + "matchPackageNames": ["@crowdin/ota-client", "trpc-panel", "msw-storybook-addon", "json-schema-to-zod"], "matchUpdateTypes": ["major", "minor", "patch"] }, { diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/InReach.code-workspace b/InReach.code-workspace index 7cc0b6def4c..e2abad320e8 100644 --- a/InReach.code-workspace +++ b/InReach.code-workspace @@ -19,6 +19,7 @@ "figma.figma-vscode-extension", "yoavbls.pretty-ts-errors", "quick-lint.quick-lint-js", + "sonarsource.sonarlint-vscode", ], }, "folders": [ @@ -224,6 +225,12 @@ "typescript.tsdk": "✨ InReach (root)/node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "typescript.workspaceSymbols.scope": "allOpenProjects", + "sonarlint.connectedMode.project": { + "connectionId": "inreach", + "projectKey": "weareinreach_InReach", + }, + "sonarlint.output.showAnalyzerLogs": true, + "sonarlint.output.showVerboseLogs": false, }, "launch": { "configurations": [ diff --git a/apps/app/.vscode/settings.json b/apps/app/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/apps/app/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/apps/app/src/pages/_app.tsx b/apps/app/src/pages/_app.tsx index b66767f3792..db029c6449d 100644 --- a/apps/app/src/pages/_app.tsx +++ b/apps/app/src/pages/_app.tsx @@ -9,7 +9,7 @@ import { type AppProps, type NextWebVitalsMetric } from 'next/app' import dynamic from 'next/dynamic' import Head from 'next/head' import { useRouter } from 'next/router' -import Script from 'next/script' +// import Script from 'next/script' import { type Session } from 'next-auth' import { appWithTranslation } from 'next-i18next' import { DefaultSeo, type DefaultSeoProps } from 'next-seo' @@ -52,7 +52,7 @@ export function reportWebVitals(stats: NextWebVitalsMetric) { appEvent.webVitals(stats) } -const PageContent = ({ Component, ...pageProps }: AppPropsWithGridSwitch) => { +const PageContent = ({ Component, pageProps }: AppPropsWithGridSwitch) => { const router = useRouter() const autoResetState = Component.autoResetState ? { key: router.asPath } : {} return Component.omitGrid ? ( @@ -78,11 +78,13 @@ const MyApp = (appProps: AppPropsWithGridSwitch) => { - + */} diff --git a/apps/app/src/pages/org/[slug]/[orgLocationId]/edit/index.tsx b/apps/app/src/pages/org/[slug]/[orgLocationId]/edit.tsx similarity index 96% rename from apps/app/src/pages/org/[slug]/[orgLocationId]/edit/index.tsx rename to apps/app/src/pages/org/[slug]/[orgLocationId]/edit.tsx index 0f8a0619725..55e0d4e6fcf 100644 --- a/apps/app/src/pages/org/[slug]/[orgLocationId]/edit/index.tsx +++ b/apps/app/src/pages/org/[slug]/[orgLocationId]/edit.tsx @@ -1,4 +1,3 @@ -import { DevTool } from '@hookform/devtools' import { createStyles, Grid, Stack, Tabs, Title } from '@mantine/core' import { compareArrayVals } from 'crud-object-diff' import { type InferGetServerSidePropsType, type NextPage } from 'next' @@ -131,11 +130,12 @@ const OrgLocationPage: NextPage + onClick: async () => { router.push({ pathname: '/org/[slug]/edit', query: { slug: data.organization.slug }, - }), + }) + }, }} organizationId={data.organization.id} saved={Boolean(isSaved)} @@ -193,19 +193,19 @@ const OrgLocationPage: NextPage + + {'Associated services'} + + {'Associate service(s) to this location'} - - - - {'Associated services'} - + />*/}
@@ -224,7 +224,6 @@ const OrgLocationPage: NextPage - ) @@ -247,14 +246,14 @@ export const getServerSideProps = async ({ if (!session) { return { redirect: { - destination: '/', + destination: `/org/${slug}/${id}`, permanent: false, }, } } const ssg = await trpcServerClient({ session }) const { id: orgId } = await ssg.organization.getIdFromSlug.fetch({ slug }) - const [i18n] = await Promise.all([ + const [i18n] = await Promise.allSettled([ getServerSideTranslations(locale, [ 'common', 'services', @@ -262,15 +261,19 @@ export const getServerSideProps = async ({ 'phone-type', 'country', 'gov-dist', + orgId, ]), ssg.location.forLocationPageEdits.prefetch({ id }), ssg.location.getAlerts.prefetch({ id }), ]) + + const translations = i18n.status === 'fulfilled' ? i18n.value : {} + const props = { organizationId: orgId, session, trpcState: ssg.dehydrate(), - ...i18n, + ...translations, } return { diff --git a/apps/app/src/pages/org/[slug]/[orgLocationId]/edit/[orgServiceId].tsx b/apps/app/src/pages/org/[slug]/[orgLocationId]/edit/[orgServiceId].tsx deleted file mode 100644 index ae142e0d69d..00000000000 --- a/apps/app/src/pages/org/[slug]/[orgLocationId]/edit/[orgServiceId].tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { Grid, Stack } from '@mantine/core' -import dynamic from 'next/dynamic' -import { useRouter } from 'next/router' -import { useTranslation } from 'next-i18next' -import { type GetServerSideProps } from 'nextjs-routes' -import { type ReactNode, Suspense, useEffect, useState } from 'react' -import { /*type Path,*/ useFieldArray, useForm } from 'react-hook-form' -import { Textarea, TextInput } from 'react-hook-form-mantine' -// import { type Merge } from 'type-fest' -import { z } from 'zod' - -import { prefixedId } from '@weareinreach/api/schemas/idPrefix' -import { trpcServerClient } from '@weareinreach/api/trpc' -import { checkServerPermissions } from '@weareinreach/auth' -import { generateId } from '@weareinreach/db/lib/idGen' -import { Badge } from '@weareinreach/ui/components/core/Badge' -import { Section } from '@weareinreach/ui/components/core/Section' -import { InlineTextInput } from '@weareinreach/ui/components/data-portal/InlineTextInput' -import { ServiceSelect } from '@weareinreach/ui/components/data-portal/ServiceSelect' -import { api } from '~app/utils/api' -import { getServerSideTranslations } from '~app/utils/i18n' -import { Button } from '~ui/components/core/Button' - -const DevTool = dynamic(() => import('@hookform/devtools').then((mod) => mod.DevTool), { ssr: false }) - -const FreetextObject = z - .object({ - text: z.string().nullable(), - key: z.string().nullish(), - ns: z.string().nullish(), - }) - .nullish() - -// type MapValue = A extends Map ? V : never - -const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]) -type Literal = z.infer -type Json = Literal | { [key: string]: Json } | Json[] -const JsonSchema: z.ZodType = z.lazy(() => - z.union([literalSchema, z.array(JsonSchema), z.record(JsonSchema)]) -) - -const FormSchema = z.object({ - name: FreetextObject, - description: FreetextObject, - services: prefixedId('serviceTag').array(), - attributes: z - .object({ - text: z - .object({ - key: z.string(), - text: z.string(), - ns: z.string(), - }) - .nullable(), - boolean: z.boolean().nullable(), - data: z.any(), - active: z.boolean(), - countryId: z.string().nullable(), - govDistId: z.string().nullable(), - languageId: z.string().nullable(), - category: z.string(), - attributeId: z.string(), - supplementId: z.string(), - }) - .array(), - published: z.boolean(), - deleted: z.boolean(), -}) -const isObject = (x: unknown): x is object => typeof x === 'object' - -type FormSchemaType = z.infer -const EditServicePage = () => { - const { t } = useTranslation() - const router = useRouter<'/org/[slug]/[orgLocationId]/edit/[orgServiceId]'>() - const { data: attributeMap } = api.attribute.map.useQuery() - const { data } = api.page.serviceEdit.useQuery({ id: router.query.orgServiceId ?? '' }) - const { data: allServices } = api.service.getOptions.useQuery() - const form = useForm({ - values: data ? { ...data, services: data.services.map(({ id }) => id) } : undefined, - }) - const attribFields = useFieldArray({ control: form.control, name: 'attributes', keyName: '_rhfId' }) - - console.log(`🚀 ~ EditServicePage ~ attribFields:`, attribFields.fields) - - const dirtyFields = { - name: isObject(form.formState.dirtyFields.name) ? form.formState.dirtyFields.name.text : false, - description: isObject(form.formState.dirtyFields.description) - ? form.formState.dirtyFields.description.text - : false, - services: form.formState.dirtyFields.services ?? false, - } - const dataAttributes = form.watch('attributes') ?? [] - const activeServices = form.watch('services') ?? [] - - type AttrSectionKeys = 'clientsServed' | 'cost' | 'eligibility' | 'languages' | 'additionalInfo' - // type AttrSectionVals = Merge< - // FormSchemaType['attributes'][number], - // { _rhfName: Path; _rhfLabel: string } - // > - - const attributeBase: { - [key in AttrSectionKeys]: ReactNode[] - } = { - clientsServed: [], - cost: [], - eligibility: [], - languages: [], - additionalInfo: [], - } - const [attributes, setAttributes] = useState(attributeBase) - - console.log(`🚀 ~ EditServicePage ~ attributes:`, attributes) - - useEffect(() => { - if (!attributeMap) return - const attrToSet = attributeBase - - for (const [i, item] of dataAttributes.entries()) { - const attribDef = attributeMap.byId.get(item.attributeId) - - console.log(`🚀 ~ useEffect ~ attribDef:`, attribDef) - - if (!attribDef) continue - - const attribNs = attribDef.tsKey.split('.').length - ? (attribDef.tsKey.split('.').shift() as string) - : attribDef.tsKey - console.log(`🚀 ~ useEffect ~ attribNs:`, attribNs) - - switch (attribNs) { - case 'tpop': { - // attrToSet.clientsServed.push({ - // ...item, - // _rhfName: `attributes.${i}.text.text`, - // _rhfLabel: 'Target Population', - // }) - attrToSet.clientsServed.push( - } - name={`attributes.${i}.text.text`} - control={form.control} - label='Target Population' - data-isDirty={form.getFieldState(`attributes.${i}.text.text`).isDirty} - autosize - /> - ) - break - } - case 'cost': { - if (attribDef.tag === 'cost-free') - // attrToSet.cost.push({ - // ...item, - // _rhfName: `attributes.${i}`, - // _rhfLabel: 'Cost', - // }) - break - } - } - } - setAttributes(attrToSet) - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataAttributes, attributeMap]) - - return ( - <> - - - - - } - name='name.text' - control={form.control} - fontSize='h2' - data-isDirty={dirtyFields.name} - /> - - - } - name='description.text' - control={form.control} - data-isDirty={dirtyFields.description} - autosize - /> - - - - - - {activeServices.map((serviceId) => { - const service = allServices?.find((s) => s.id === serviceId) - if (!service) return null - return ( - {t(service.tsKey, { ns: service.tsNs })} - ) - })} - - - - {t('service.get-help')} - - {attributes.clientsServed.length ? ( - // attributes.clientsServed.map(({ _rhfName, _rhfLabel, ...item }) => ( - // } - // name={_rhfName} - // control={form.control} - // label={_rhfLabel} - // data-isDirty={form.getFieldState(_rhfName).isDirty} - // autosize - // /> - // )) - attributes.clientsServed - ) : ( - - )} - - {t('service.cost')} - {t('service.eligibility')} - {t('service.languages')} - {t('service.extra-info')} - - - {/* @ts-expect-error Hush, devtool. */} - - - ) -} - -export default EditServicePage - -export const getServerSideProps: GetServerSideProps = async ({ locale, params, req, res }) => { - const urlParams = z - .object({ - slug: z.string(), - orgLocationId: prefixedId('orgLocation'), - orgServiceId: prefixedId('orgService'), - }) - .safeParse(params) - if (!urlParams.success) return { notFound: true } - const { slug, orgLocationId: _, orgServiceId } = urlParams.data - const session = await checkServerPermissions({ - ctx: { req, res }, - permissions: ['dataPortalBasic'], - has: 'some', - }) - if (!session) { - return { - redirect: { - destination: '/', - permanent: false, - }, - } - } - const ssg = await trpcServerClient({ session }) - const { id: orgId } = await ssg.organization.getIdFromSlug.fetch({ slug }) - const [i18n] = await Promise.all([ - getServerSideTranslations(locale, ['common', 'services', 'attribute']), - ssg.page.serviceEdit.prefetch({ id: orgServiceId }), - ssg.component.ServiceSelect.prefetch(), - ssg.service.getOptions.prefetch(), - ]) - const props = { - organizationId: orgId, - session, - trpcState: ssg.dehydrate(), - ...i18n, - } - - return { - props, - } -} diff --git a/apps/app/src/pages/org/[slug]/edit.tsx b/apps/app/src/pages/org/[slug]/edit.tsx index d25cca9aeae..24238f6fb3b 100644 --- a/apps/app/src/pages/org/[slug]/edit.tsx +++ b/apps/app/src/pages/org/[slug]/edit.tsx @@ -1,4 +1,3 @@ -import { DevTool } from '@hookform/devtools' import { Grid, Stack } from '@mantine/core' import { useElementSize } from '@mantine/hooks' import { t } from 'i18next' @@ -127,7 +126,6 @@ const OrganizationPage: NextPageWithOptions - diff --git a/apps/app/src/pages/org/[slug]/index.tsx b/apps/app/src/pages/org/[slug]/index.tsx index 29897c1f7b7..d8f6b247fbe 100644 --- a/apps/app/src/pages/org/[slug]/index.tsx +++ b/apps/app/src/pages/org/[slug]/index.tsx @@ -10,7 +10,6 @@ import { useEffect, useRef, useState } from 'react' import { trpcServerClient } from '@weareinreach/api/trpc' import { AlertMessage } from '@weareinreach/ui/components/core/AlertMessage' -// import { GoogleMap } from '@weareinreach/ui/components/core/GoogleMap' import { Toolbar } from '@weareinreach/ui/components/core/Toolbar' import { ContactSection } from '@weareinreach/ui/components/sections/ContactSection' import { ListingBasicInfo } from '@weareinreach/ui/components/sections/ListingBasicInfo' @@ -40,11 +39,12 @@ const useStyles = createStyles((theme) => ({ })) const OrganizationPage = ({ - slug, + slug: passedSlug, organizationId: orgId, }: InferGetStaticPropsType) => { const router = useRouter<'/org/[slug]'>() - const { data, status } = api.organization.forOrgPage.useQuery({ slug }, { enabled: !!slug }) + const slug = passedSlug ?? router.query.slug + const { data, status } = api.organization.forOrgPage.useQuery({ slug }) // const { query } = router const { t } = useTranslation(formatNS(orgId)) const [activeTab, setActiveTab] = useState('services') @@ -155,7 +155,7 @@ const OrganizationPage = ({ <> {isTablet && } - {width && ( + {Boolean(width) && ( id)} width={width} @@ -244,7 +244,6 @@ export const getStaticProps = async ({ }: GetStaticPropsContext>) => { if (!params) return { notFound: true } const { slug } = params - const ssg = await trpcServerClient({ session: null }) try { const redirect = await ssg.organization.slugRedirect.fetch(slug) diff --git a/apps/app/src/types/nextjs-routes.d.ts b/apps/app/src/types/nextjs-routes.d.ts index 8ff841f0fb4..96bc95ed54e 100644 --- a/apps/app/src/types/nextjs-routes.d.ts +++ b/apps/app/src/types/nextjs-routes.d.ts @@ -30,7 +30,6 @@ declare module "nextjs-routes" { | DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }> | StaticRoute<"/api/trpc-playground"> | StaticRoute<"/"> - | DynamicRoute<"/org/[slug]/[orgLocationId]/edit/[orgServiceId]", { "slug": string; "orgLocationId": string; "orgServiceId": string }> | DynamicRoute<"/org/[slug]/[orgLocationId]/edit", { "slug": string; "orgLocationId": string }> | DynamicRoute<"/org/[slug]/[orgLocationId]", { "slug": string; "orgLocationId": string }> | DynamicRoute<"/org/[slug]/edit", { "slug": string }> diff --git a/apps/web/.vscode/settings.json b/apps/web/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/apps/web/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/lambdas/.vscode/settings.json b/lambdas/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/lambdas/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/package.json b/package.json index 76193f78bec..45688b86616 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,8 @@ "patchedDependencies": { "@crowdin/ota-client@1.0.0": "patches/@crowdin__ota-client@1.0.0.patch", "social-links@1.14.0": "patches/social-links@1.14.0.patch", - "trpc-panel@1.3.4": "patches/trpc-panel@1.3.4.patch" + "trpc-panel@1.3.4": "patches/trpc-panel@1.3.4.patch", + "json-schema-to-zod@2.0.14": "patches/json-schema-to-zod@2.0.14.patch" }, "peerDependencyRules": { "allowedVersions": { diff --git a/packages/analytics/.vscode/settings.json b/packages/analytics/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/analytics/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/api/.vscode/settings.json b/packages/api/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/api/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/api/formatters/attributes.ts b/packages/api/formatters/attributes.ts index ce8fc579b9f..fd27db61d76 100644 --- a/packages/api/formatters/attributes.ts +++ b/packages/api/formatters/attributes.ts @@ -1,3 +1,5 @@ +import { type Simplify } from 'type-fest' + import { type Prisma } from '@weareinreach/db' export const formatAttributes = { @@ -16,19 +18,35 @@ export const formatAttributes = { select: { id: true, tag: true, - // tsKey: true, - // tsNs: true, - // icon: true, - // iconBg: true, - categories: { select: { category: { select: { tag: true, ns: true } } } }, + tsKey: true, + tsNs: true, + icon: true, + iconBg: true, + showOnLocation: true, + categories: { select: { category: { select: { tag: true, icon: true, ns: true } } } }, + _count: { select: { parents: true, children: true } }, }, }, active: true, countryId: true, + country: { + select: { + cca2: true, + id: true, + name: true, + }, + }, data: true, govDistId: true, + govDist: { select: { tsKey: true, tsNs: true, abbrev: true, id: true } }, id: true, languageId: true, + language: { + select: { + languageName: true, + nativeName: true, + }, + }, text: { select: { tsKey: { select: { key: true, text: true, ns: true } } } }, boolean: true, }, @@ -82,54 +100,102 @@ export const formatAttributes = { } type ReturnedData = { + boolean: boolean | null attribute: { id: string + _count: { + children: number + parents: number + } tag: string - // tsKey: string - // tsNs: string + tsKey: string + tsNs: string categories: { category: { tag: string ns: string + icon: string | null } }[] - // icon: string | null - // iconBg: string | null + icon: string | null + iconBg: string | null + showOnLocation: boolean | null } - boolean: boolean | null - id: string + country: { + id: string + name: string + cca2: string + } | null + govDist: { + id: string + tsKey: string + tsNs: string + abbrev: string | null + } | null + language: { + languageName: string + nativeName: string + } | null data: Prisma.JsonValue + id: string active: boolean text: { tsKey: { key: string - text: string ns: string + text: string } } | null - countryId: string | null govDistId: string | null + countryId: string | null languageId: string | null }[] type DataOutput = { - text: { - key: string - text: string - ns: string - } | null + // ids + attributeId: string + supplementId: string + // attribute + category: string + _count: { + children: number + parents: number + } + tag: string + tsKey: string + tsNs: string + icon: string | null + iconBg: string | null + showOnLocation: boolean | null + // supplement boolean: boolean | null + country: { + id: string + name: string + cca2: string + } | null + govDist: { + id: string + tsKey: string + tsNs: string + abbrev: string | null + } | null + language: { + languageName: string + nativeName: string + } | null data: Prisma.JsonValue active: boolean - countryId: string | null govDistId: string | null + countryId: string | null languageId: string | null - category: string - tag: string - attributeId: string - supplementId: string + text: { + key: string + ns: string + text: string + } | null } type ReturnSegmented = { - attributes: DataOutput[] - accessDetails: DataOutput[] + attributes: Simplify[] + accessDetails: Simplify[] } diff --git a/packages/api/package.json b/packages/api/package.json index 6c1606a6d3c..d5628c69046 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -61,6 +61,7 @@ "@weareinreach/eslint-config": "0.100.0", "dotenv-cli": "7.4.1", "eslint": "8.57.0", + "i18next": "23.10.1", "inquirer-search-list": "1.2.6", "just-pascal-case": "3.2.0", "next": "14.1.4", @@ -71,6 +72,7 @@ "typescript": "5.4.3" }, "peerDependencies": { + "i18next": "23.10.1", "next": ">=13" } } diff --git a/packages/api/router/fieldOpt/index.ts b/packages/api/router/fieldOpt/index.ts index 9a16c851ab7..a6c367195d4 100644 --- a/packages/api/router/fieldOpt/index.ts +++ b/packages/api/router/fieldOpt/index.ts @@ -84,4 +84,8 @@ export const fieldOptRouter = defineRouter({ const handler = await importHandler(namespaced('orgBadges'), () => import('./query.orgBadges.handler')) return handler(opts) }), + ccaMap: publicProcedure.input(schema.ZCcaMapSchema).query(async (opts) => { + const handler = await importHandler(namespaced('ccaMap'), () => import('./query.ccaMap.handler')) + return handler(opts) + }), }) diff --git a/packages/api/router/fieldOpt/query.attributesByCategory.handler.ts b/packages/api/router/fieldOpt/query.attributesByCategory.handler.ts index a2a50f0a535..771c577cff7 100644 --- a/packages/api/router/fieldOpt/query.attributesByCategory.handler.ts +++ b/packages/api/router/fieldOpt/query.attributesByCategory.handler.ts @@ -1,29 +1,41 @@ +import Ajv, { type JSONSchemaType } from 'ajv' + import { prisma } from '@weareinreach/db' import { type FieldAttributes } from '@weareinreach/db/zod_util/attributeSupplement' import { type TRPCHandlerParams } from '~api/types/handler' import { fieldAttributesSchema, type TAttributesByCategorySchema } from './query.attributesByCategory.schema' +const ajv = new Ajv() +const validateJsonSchema = (schema: unknown): schema is JSONSchemaType => { + if (schema && typeof schema === 'object' && ajv.validateSchema(schema)) { + return true + } + return false +} + export const attributesByCategory = async ({ input }: TRPCHandlerParams) => { console.log(input) const result = await prisma.attributesByCategory.findMany({ where: { categoryName: Array.isArray(input?.categoryName) ? { in: input.categoryName } : input?.categoryName, - // canAttachTo: input?.canAttachTo?.length ? { hasSome: input.canAttachTo } : undefined, + canAttachTo: input?.canAttachTo?.length ? { hasSome: input.canAttachTo } : undefined, }, orderBy: [{ categoryName: 'asc' }, { attributeName: 'asc' }], }) const flushedResults = result.map((item) => { - const { dataSchema, ...rest } = item + const { formSchema, dataSchema, ...rest } = item - const parsedDataSchema = fieldAttributesSchema.safeParse(dataSchema) + const parsedFormSchema = fieldAttributesSchema.safeParse(formSchema) + const parsedDataSchema = validateJsonSchema(dataSchema) ? dataSchema : null return { ...rest, - dataSchema: parsedDataSchema.success - ? (parsedDataSchema.data as FieldAttributes[] | FieldAttributes[][]) + formSchema: parsedFormSchema.success + ? (parsedFormSchema.data as FieldAttributes[] | FieldAttributes[][]) : null, + dataSchema: parsedDataSchema, } }) return flushedResults diff --git a/packages/api/router/fieldOpt/query.ccaMap.handler.ts b/packages/api/router/fieldOpt/query.ccaMap.handler.ts new file mode 100644 index 00000000000..dc21f59d493 --- /dev/null +++ b/packages/api/router/fieldOpt/query.ccaMap.handler.ts @@ -0,0 +1,28 @@ +import { prisma } from '@weareinreach/db' +import { handleError } from '~api/lib/errorHandler' +import { type TRPCHandlerParams } from '~api/types/handler' + +import { type TCcaMapSchema } from './query.ccaMap.schema' + +export const ccaMap = async ({ input }: TRPCHandlerParams) => { + try { + const data = await prisma.country.findMany({ + where: input.activeForOrgs + ? { + activeForOrgs: input.activeForOrgs ?? true, + } + : {}, + select: { + id: true, + cca2: true, + }, + }) + const byId = new Map(data.map(({ id, cca2 }) => [id, cca2])) + const byCCA = new Map(data.map(({ id, cca2 }) => [cca2, id])) + + return { byId, byCCA } + } catch (error) { + handleError(error) + } +} +export default ccaMap diff --git a/packages/api/router/fieldOpt/query.ccaMap.schema.ts b/packages/api/router/fieldOpt/query.ccaMap.schema.ts new file mode 100644 index 00000000000..94e2b924fba --- /dev/null +++ b/packages/api/router/fieldOpt/query.ccaMap.schema.ts @@ -0,0 +1,4 @@ +import { z } from 'zod' + +export const ZCcaMapSchema = z.object({ activeForOrgs: z.boolean().optional() }) +export type TCcaMapSchema = z.infer diff --git a/packages/api/router/fieldOpt/query.govDists.handler.ts b/packages/api/router/fieldOpt/query.govDists.handler.ts index 501af8271df..16233d696bc 100644 --- a/packages/api/router/fieldOpt/query.govDists.handler.ts +++ b/packages/api/router/fieldOpt/query.govDists.handler.ts @@ -16,6 +16,7 @@ export const govDists = async ({ input }: TRPCHandlerParams) => country: { select: { cca2: true } }, govDistType: { select: { tsKey: true, tsNs: true } }, }, + orderBy: { tsKey: 'asc' }, }) return results } catch (error) { diff --git a/packages/api/router/fieldOpt/schemas.ts b/packages/api/router/fieldOpt/schemas.ts index 7b506a54dda..54901ae681e 100644 --- a/packages/api/router/fieldOpt/schemas.ts +++ b/packages/api/router/fieldOpt/schemas.ts @@ -1,6 +1,7 @@ // codegen:start {preset: barrel, include: ./*.schema.ts} export * from './query.attributeCategories.schema' export * from './query.attributesByCategory.schema' +export * from './query.ccaMap.schema' export * from './query.countries.schema' export * from './query.getSubDistricts.schema' export * from './query.govDists.schema' diff --git a/packages/api/router/organization/mutation.attachAttribute.handler.ts b/packages/api/router/organization/mutation.attachAttribute.handler.ts index 14b4836d793..9282eee9574 100644 --- a/packages/api/router/organization/mutation.attachAttribute.handler.ts +++ b/packages/api/router/organization/mutation.attachAttribute.handler.ts @@ -1,4 +1,5 @@ -import { getAuditedClient } from '@weareinreach/db' +import { generateNestedFreeText, getAuditedClient } from '@weareinreach/db' +import { connectOneId, connectOneIdRequired } from '~api/schemas/nestedOps' import { type TRPCHandlerParams } from '~api/types/handler' import { type TAttachAttributeSchema } from './mutation.attachAttribute.schema' @@ -8,17 +9,40 @@ export const attachAttribute = async ({ input, }: TRPCHandlerParams) => { const prisma = getAuditedClient(ctx.actorId) - const { translationKey, freeText, attributeSupplement } = input + const { locationId, organizationId, serviceId } = input - const result = await prisma.$transaction(async (tx) => { - const tKey = translationKey ? await tx.translationKey.create(translationKey) : undefined - const fText = freeText ? await tx.freeText.create(freeText) : undefined - const aSupp = attributeSupplement ? await tx.attributeSupplement.create(attributeSupplement) : undefined - return { - translationKey: tKey, - freeText: fText, - attributeSupplement: aSupp, - } + const { id: orgId } = organizationId + ? { id: organizationId } + : await prisma.organization.findFirstOrThrow({ + where: { + OR: [{ locations: { some: { id: locationId } } }, { services: { some: { id: serviceId } } }], + }, + select: { + id: true, + }, + }) + + const freeText = input.text + ? generateNestedFreeText({ orgId, text: input.text, type: 'attSupp', itemId: input.id }) + : undefined + + const result = await prisma.attributeSupplement.create({ + data: { + id: input.id, + attribute: connectOneIdRequired(input.attributeId), + organization: connectOneId(organizationId), + country: connectOneId(input.countryId), + govDist: connectOneId(input.govDistId), + language: connectOneId(input.languageId), + service: connectOneId(serviceId), + location: connectOneId(locationId), + boolean: input.boolean, + data: input.data, + text: freeText, + }, + select: { + id: true, + }, }) return result } diff --git a/packages/api/router/organization/mutation.attachAttribute.schema.ts b/packages/api/router/organization/mutation.attachAttribute.schema.ts index 6cfcf27a17c..7d1ead360d3 100644 --- a/packages/api/router/organization/mutation.attachAttribute.schema.ts +++ b/packages/api/router/organization/mutation.attachAttribute.schema.ts @@ -1,64 +1,20 @@ import { z } from 'zod' -import { generateFreeText, generateId, InputJsonValue, Prisma } from '@weareinreach/db' +import { JsonInputOrNull } from '@weareinreach/db' import { prefixedId } from '~api/schemas/idPrefix' -export const ZAttachAttributeSchema = z - .object({ - organizationId: prefixedId('organization'), - attributeId: prefixedId('attribute'), - supplement: z - .object({ - data: InputJsonValue.optional(), - boolean: z.boolean().optional(), - countryId: z.string().optional(), - govDistId: z.string().optional(), - languageId: z.string().optional(), - text: z.string().optional(), - }) - .optional(), - }) - .transform((parsedData) => { - const { organizationId, attributeId, supplement: supplementInput } = parsedData +export const ZAttachAttributeSchema = z.object({ + id: prefixedId('attributeSupplement'), + attributeId: prefixedId('attribute'), + organizationId: prefixedId('organization').optional(), + serviceId: prefixedId('orgService').optional(), + locationId: prefixedId('orgLocation').optional(), + countryId: z.string().optional(), + govDistId: z.string().optional(), + languageId: z.string().optional(), + text: z.string().optional(), + boolean: z.coerce.boolean().optional(), + data: JsonInputOrNull.optional(), +}) - const supplementId = supplementInput ? generateId('attributeSupplement') : undefined - - const { freeText, translationKey } = - supplementId && supplementInput?.text - ? generateFreeText({ - orgId: organizationId, - text: supplementInput.text, - type: 'attSupp', - itemId: supplementId, - }) - : { freeText: undefined, translationKey: undefined } - - const { boolean, countryId, data, govDistId, languageId } = supplementInput ?? {} - - const supplementData = supplementInput - ? { - id: supplementId, - countryId, - boolean, - data, - govDistId, - languageId, - textId: freeText?.id, - attributeId, - organizationId, - } - : undefined - - return { - freeText: freeText ? Prisma.validator()({ data: freeText }) : undefined, - translationKey: translationKey - ? Prisma.validator()({ data: translationKey }) - : undefined, - attributeSupplement: supplementData - ? Prisma.validator()({ - data: supplementData, - }) - : undefined, - } - }) export type TAttachAttributeSchema = z.infer diff --git a/packages/api/router/service/formatters/modalAndEditDrawer.ts b/packages/api/router/service/formatters/modalAndEditDrawer.ts new file mode 100644 index 00000000000..807a55dae11 --- /dev/null +++ b/packages/api/router/service/formatters/modalAndEditDrawer.ts @@ -0,0 +1,382 @@ +import { type TOptions } from 'i18next' +import { type z } from 'zod' + +import { type Prisma, prisma } from '@weareinreach/db' +import { attributeSupplementSchema } from '@weareinreach/db/generated/attributeSupplementSchema' +import { accessInstructions } from '@weareinreach/db/zod_util/attributeSupplement' +import { globalWhere } from '~api/selects/global' + +const getFreeText = (freeTextRecord: NonNullable) => { + const { tsKey } = freeTextRecord + const { key: dbKey } = tsKey + const deconstructedKey = dbKey.split('.') + const ns = deconstructedKey[0] + if (!deconstructedKey.length || !ns) throw new Error('Invalid key') + const key = deconstructedKey.join('.') + const options = { ns, defaultValue: tsKey.text } satisfies TOptions + return { key, options } +} +export const attributeSelect = (showAll?: boolean) => + ({ + ...(showAll + ? {} + : ({ + where: { + active: true, + attribute: { active: true }, + }, + } as const)), + select: { + attribute: { + select: { + id: true, + tag: true, + tsKey: true, + tsNs: true, + icon: true, + iconBg: true, + showOnLocation: true, + categories: { select: { category: { select: { tag: true, ns: true } } } }, + _count: { + select: { + parents: true, + children: true, + }, + }, + }, + }, + active: true, + countryId: true, + data: true, + govDistId: true, + id: true, + language: { select: { id: true, languageName: true } }, + languageId: true, + text: { select: { tsKey: { select: { key: true, text: true, ns: true } } } }, + boolean: true, + }, + }) as const +export const locationSelect = (showAll?: boolean) => + ({ + ...(showAll + ? {} + : ({ + where: { + location: globalWhere.isPublic(), + }, + } as const)), + select: { location: { select: { country: { select: { cca2: true } } } } }, + }) as const + +export const transformToProps = (data: ReturnedData): TransformOutput => { + const { attributes, locations } = data + const output: TransformOutput = { + accessInstructions: { + publicTransit: undefined, + email: [], + phone: [], + website: [], + }, + attributeSections: { + clientsServed: { + focused: [], + targetPopulation: [], + }, + eligibility: { + age: undefined, + }, + cost: { + description: [], + badged: [], + }, + language: [], + atCapacity: false, + misc: [], + miscWithIcons: [], + }, + } + type AttributeToProcess = (typeof attributes)[number]['attribute'] + type SupplementToProcess = Omit<(typeof attributes)[number], 'attribute'> + const processAccessInstruction = ( + data: z.infer>, + supplement: SupplementToProcess + ) => { + const { access_type, access_value } = data + switch (access_type) { + case 'publicTransit': { + if (!supplement.text) break + output.accessInstructions.publicTransit = { + key: supplement.id, + children: getFreeText(supplement.text), + } + break + } + case 'email': { + if (access_value) + output.accessInstructions.email.push({ + id: supplement.id, + title: null, + description: null, + email: access_value, + primary: false, + locationOnly: false, + serviceOnly: false, + }) + break + } + case 'phone': { + const country = locations.find(({ location }) => Boolean(location.country))?.location?.country?.cca2 + if (!country) break + if (access_value) + output.accessInstructions.phone.push({ + id: supplement.id, + number: access_value, + phoneType: null, + country, + primary: false, + locationOnly: false, + ext: null, + description: null, + }) + break + } + case 'link': + case 'file': { + if (access_value) + output.accessInstructions.website.push({ + id: supplement.id, + description: null, + isPrimary: false, + orgLocationOnly: false, + url: access_value, + }) + } + } + } + + const handleSrvFocus = (attribute: AttributeToProcess, supplement: SupplementToProcess) => { + if (typeof attribute.icon === 'string' && attribute._count.parents === 0) { + output.attributeSections.clientsServed.focused.push({ + key: supplement.id, + icon: attribute.icon, + children: { + tsKey: attribute.tsKey, + tOptions: { ns: attribute.tsNs }, + }, + }) + } + } + const handleEligibility = (attribute: AttributeToProcess, supplement: SupplementToProcess) => { + const type = attribute.tsKey.split('.').pop() as string + switch (type) { + case 'elig-age': { + const parsed = attributeSupplementSchema.numMinMaxOrRange.safeParse(supplement.data) + if (!parsed.success) break + const { min, max } = parsed.data + const context = (function () { + switch (true) { + case Boolean(min) && Boolean(max): { + return 'range' + } + case Boolean(min): { + return 'min' + } + default: { + return 'max' + } + } + })() + + output.attributeSections.eligibility.age = { + key: supplement.id, + children: { + key: 'service.elig-age', + options: { ns: 'common', context, min, max }, + }, + } + + break + } + case 'other-describe': { + if (!supplement.text) break + output.attributeSections.clientsServed.targetPopulation.push({ + key: supplement.id, + children: getFreeText(supplement.text), + }) + break + } + } + } + const handleCost = (attribute: AttributeToProcess, supplement: SupplementToProcess) => { + if (!attribute.icon) return + if (supplement.text) { + output.attributeSections.cost.description.push({ + key: supplement.id, + children: getFreeText(supplement.text), + }) + } + const parsed = attributeSupplementSchema.currency.safeParse(supplement.data) + if (parsed.success) { + output.attributeSections.cost.badged.push({ + key: supplement.id, + icon: attribute.icon, + style: { justifyContent: 'start' }, + children: { + tsKey: attribute.tsKey, + tOptions: { ns: attribute.tsNs }, + miscInterpolation: { + data: parsed.data.cost, + currency: parsed.data.currency ?? undefined, + style: 'currency', + }, + }, + }) + } + } + const handleLanguage = (attribute: AttributeToProcess, supplement: SupplementToProcess) => { + if (!supplement.language) return + output.attributeSections.language.push(supplement.language.languageName) + } + const handleAdditional = (attribute: AttributeToProcess, supplement: SupplementToProcess) => { + if (attribute.tsKey.includes('at-capacity')) output.attributeSections.atCapacity = true + else { + typeof attribute.icon === 'string' + ? output.attributeSections.miscWithIcons.push({ + key: supplement.id, + icon: attribute.icon, + children: { + tsKey: attribute.tsKey, + tOptions: { ns: attribute.tsNs }, + }, + }) + : output.attributeSections.misc.push({ + tsKey: attribute.tsKey, + tOptions: { ns: attribute.tsNs }, + }) + } + } + const processAttribute = (attribute: AttributeToProcess, supplement: SupplementToProcess) => { + const namespace = attribute.tsKey.split('.').shift() as string + + switch (namespace) { + /** Clients served */ + case 'srvfocus': { + handleSrvFocus(attribute, supplement) + break + } + /** Target Population & Eligibility Requirements */ + case 'eligibility': { + handleEligibility(attribute, supplement) + break + } + case 'cost': { + handleCost(attribute, supplement) + break + } + + case 'lang': { + handleLanguage(attribute, supplement) + break + } + case 'additional': { + handleAdditional(attribute, supplement) + break + } + default: { + break + } + } + } + + for (const { attribute, ...supplement } of attributes) { + const flatAttribs = attribute.categories.map(({ category }) => category.tag) + if (flatAttribs.includes('service-access-instructions')) { + // process access instruction + const parsed = accessInstructions.getAll().safeParse(supplement.data) + if (parsed.success) { + processAccessInstruction(parsed.data, supplement) + } + } else { + // process attribute + processAttribute(attribute, supplement) + } + } + return output +} + +const testTxn = async () => + await prisma.orgService.findFirstOrThrow({ + where: { id: '' }, + select: { attributes: attributeSelect(), locations: locationSelect() }, + }) +type ReturnedData = Prisma.PromiseReturnType +type TransformOutput = { + accessInstructions: { + publicTransit?: ModalTextProps + email: EmailProps[] + phone: PhoneProps[] + website: WebsiteProps[] + } + attributeSections: { + clientsServed: { + focused: AttributeBadgeProps[] + targetPopulation: ModalTextProps[] + } + eligibility: { + age?: ModalTextProps + } + cost: { + badged: AttributeBadgeProps[] + description: ModalTextProps[] + } + language: string[] + atCapacity: boolean + miscWithIcons: AttributeBadgeProps[] + misc: ChildrenT[] + } +} +type ModalTextProps = { + key: string + children: { + key: string + options: TOptions + } +} +type EmailProps = { + id: string + title: null + description: null + email: string + primary: boolean + locationOnly: boolean + serviceOnly: boolean +} +type PhoneProps = { + id: string + number: string + phoneType: null + country: string + primary: boolean + locationOnly: boolean + ext: null + description: null +} +type WebsiteProps = { + id: string + description: null + isPrimary: false + orgLocationOnly: boolean + url: string +} +type AttributeBadgeProps = { + key: string + icon: string + children: ChildrenT + style?: Record +} +type ChildrenT = { + tsKey: string + tOptions: TOptions + miscInterpolation?: InterpolateNumber +} +type InterpolateNumber = Intl.NumberFormatOptions & { data: number } diff --git a/packages/api/router/service/query.forServiceEditDrawer.handler.ts b/packages/api/router/service/query.forServiceEditDrawer.handler.ts index 6921fbe8fbe..a5b6effdfc5 100644 --- a/packages/api/router/service/query.forServiceEditDrawer.handler.ts +++ b/packages/api/router/service/query.forServiceEditDrawer.handler.ts @@ -1,7 +1,6 @@ import { prisma } from '@weareinreach/db' import { formatAttributes } from '~api/formatters/attributes' import { formatHours } from '~api/formatters/hours' -import { globalSelect } from '~api/selects/global' import { type TRPCHandlerParams } from '~api/types/handler' import { type TForServiceEditDrawerSchema } from './query.forServiceEditDrawer.schema' @@ -20,7 +19,9 @@ export const forServiceEditDrawer = async ({ input }: TRPCHandlerParams phone.id), emails: emails.map(({ email }) => email.id), - locations: locations.map(({ orgLocationId }) => orgLocationId), + // locations: locations.map(({ orgLocationId }) => orgLocationId), services: services.map(({ tag }) => tag.id), hours: formatHours.process(hours), serviceAreas: serviceAreas diff --git a/packages/api/router/service/query.forServiceModal.handler.ts b/packages/api/router/service/query.forServiceModal.handler.ts index c2b13386f6a..8b46dbab5fe 100644 --- a/packages/api/router/service/query.forServiceModal.handler.ts +++ b/packages/api/router/service/query.forServiceModal.handler.ts @@ -1,9 +1,9 @@ import { prisma } from '@weareinreach/db' +import { formatAttributes } from '~api/formatters/attributes' import { globalWhere } from '~api/selects/global' import { type TRPCHandlerParams } from '~api/types/handler' import { type TForServiceModalSchema } from './query.forServiceModal.schema' -import { select } from './selects' export const forServiceModal = async ({ input }: TRPCHandlerParams) => { const result = await prisma.orgService.findUniqueOrThrow({ @@ -26,70 +26,52 @@ export const forServiceModal = async ({ input }: TRPCHandlerParams - attribute.categories.every(({ category }) => category.tag !== 'service-access-instructions') - ) - .map(({ attribute, ...supplement }) => ({ - attribute, - supplement, - })), - accessDetails: result.attributes - .filter(({ attribute }) => - attribute.categories.some(({ category }) => category.tag === 'service-access-instructions') - ) - .map(({ attribute, ...supplement }) => ({ - attribute, - supplement, - })), + attributes, + accessDetails, } return transformed } diff --git a/packages/api/router/serviceArea/index.ts b/packages/api/router/serviceArea/index.ts index eeb18295039..8f13203bf37 100644 --- a/packages/api/router/serviceArea/index.ts +++ b/packages/api/router/serviceArea/index.ts @@ -19,4 +19,22 @@ export const serviceAreaRouter = defineRouter({ const handler = await importHandler(namespaced('update'), () => import('./mutation.update.handler')) return handler(opts) }), + addToArea: permissionedProcedure('updateOrgService') + .input(schema.ZAddToAreaSchema) + .mutation(async (opts) => { + const handler = await importHandler( + namespaced('addToArea'), + () => import('./mutation.addToArea.handler') + ) + return handler(opts) + }), + delFromArea: permissionedProcedure('updateOrgService') + .input(schema.ZDelFromAreaSchema) + .mutation(async (opts) => { + const handler = await importHandler( + namespaced('delFromArea'), + () => import('./mutation.delFromArea.handler') + ) + return handler(opts) + }), }) diff --git a/packages/api/router/serviceArea/mutation.addToArea.handler.ts b/packages/api/router/serviceArea/mutation.addToArea.handler.ts new file mode 100644 index 00000000000..90fed8378ee --- /dev/null +++ b/packages/api/router/serviceArea/mutation.addToArea.handler.ts @@ -0,0 +1,50 @@ +import { TRPCError } from '@trpc/server' + +import { generateId, getAuditedClient } from '@weareinreach/db' +import { handleError } from '~api/lib/errorHandler' +import { type TRPCHandlerParams } from '~api/types/handler' + +import { type TAddToAreaSchema } from './mutation.addToArea.schema' + +export const addToArea = async ({ ctx, input }: TRPCHandlerParams) => { + try { + const prisma = getAuditedClient(ctx.actorId) + + const { id: serviceAreaId } = + typeof input.serviceArea === 'string' + ? { id: input.serviceArea as string } + : await prisma.serviceArea.create({ + data: { + id: generateId('serviceArea'), + ...input.serviceArea, + }, + select: { id: true }, + }) + if (input.countryId) { + const result = await prisma.serviceAreaCountry.create({ + data: { + serviceAreaId, + countryId: input.countryId, + }, + }) + if (result) { + return { result: 'added' } + } + } else if (input.govDistId) { + const result = await prisma.serviceAreaDist.create({ + data: { + serviceAreaId, + govDistId: input.govDistId, + }, + }) + if (result) { + return { result: 'added' } + } + } else { + throw new TRPCError({ code: 'BAD_REQUEST' }) + } + } catch (error) { + handleError(error) + } +} +export default addToArea diff --git a/packages/api/router/serviceArea/mutation.addToArea.schema.ts b/packages/api/router/serviceArea/mutation.addToArea.schema.ts new file mode 100644 index 00000000000..b789dc412a3 --- /dev/null +++ b/packages/api/router/serviceArea/mutation.addToArea.schema.ts @@ -0,0 +1,26 @@ +import { type Simplify } from 'type-fest' +import { z } from 'zod' + +import { prefixedId } from '~api/schemas/idPrefix' + +const organization = z.object({ organizationId: prefixedId('organization') }) +const orgLocation = z.object({ orgLocationId: prefixedId('orgLocation') }) +const orgService = z.object({ orgServiceId: prefixedId('orgService') }) + +const serviceArea = z.union([prefixedId('serviceArea'), organization, orgLocation, orgService]) + +export const ZAddToAreaSchema = z + .object({ + serviceArea, + countryId: prefixedId('country').optional(), + govDistId: prefixedId('govDist').optional(), + }) + .refine( + (data) => + (typeof data.countryId === 'string' || typeof data.govDistId === 'string') && + !(typeof data.countryId === 'string' && typeof data.govDistId === 'string'), + { + message: 'Only one of countryId or govDistId must be provided', + } + ) +export type TAddToAreaSchema = Simplify> diff --git a/packages/api/router/serviceArea/mutation.delFromArea.handler.ts b/packages/api/router/serviceArea/mutation.delFromArea.handler.ts new file mode 100644 index 00000000000..3d390001480 --- /dev/null +++ b/packages/api/router/serviceArea/mutation.delFromArea.handler.ts @@ -0,0 +1,46 @@ +import { TRPCError } from '@trpc/server' + +import { getAuditedClient } from '@weareinreach/db' +import { handleError } from '~api/lib/errorHandler' +import { type TRPCHandlerParams } from '~api/types/handler' + +import { type TDelFromAreaSchema } from './mutation.delFromArea.schema' + +export const delFromArea = async ({ ctx, input }: TRPCHandlerParams) => { + try { + const prisma = getAuditedClient(ctx.actorId) + + if (input.countryId) { + const { serviceAreaId, countryId } = input + const delCountry = await prisma.serviceAreaCountry.delete({ + where: { + serviceAreaId_countryId: { + serviceAreaId, + countryId, + }, + }, + }) + if (delCountry) { + return { result: 'deleted' } + } + } else if (input.govDistId) { + const { serviceAreaId, govDistId } = input + const delGovDist = await prisma.serviceAreaDist.delete({ + where: { + serviceAreaId_govDistId: { + serviceAreaId, + govDistId, + }, + }, + }) + if (delGovDist) { + return { result: 'deleted' } + } + } + + throw new TRPCError({ code: 'BAD_REQUEST' }) + } catch (error) { + handleError(error) + } +} +export default delFromArea diff --git a/packages/api/router/serviceArea/mutation.delFromArea.schema.ts b/packages/api/router/serviceArea/mutation.delFromArea.schema.ts new file mode 100644 index 00000000000..708c359b6f0 --- /dev/null +++ b/packages/api/router/serviceArea/mutation.delFromArea.schema.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' + +import { prefixedId } from '~api/schemas/idPrefix' + +export const ZDelFromAreaSchema = z + .object({ + serviceAreaId: prefixedId('serviceArea'), + countryId: z.string().optional(), + govDistId: z.string().optional(), + }) + .refine( + (data) => + (typeof data.countryId === 'string' || typeof data.govDistId === 'string') && + !(typeof data.countryId === 'string' && typeof data.govDistId === 'string'), + { + message: 'Only one of countryId or govDistId must be provided', + } + ) +export type TDelFromAreaSchema = z.infer diff --git a/packages/api/router/serviceArea/schemas.ts b/packages/api/router/serviceArea/schemas.ts index 917e0a58a5a..5d18aba68c0 100644 --- a/packages/api/router/serviceArea/schemas.ts +++ b/packages/api/router/serviceArea/schemas.ts @@ -1,4 +1,6 @@ // codegen:start {preset: barrel, include: ./*.schema.ts} +export * from './mutation.addToArea.schema' +export * from './mutation.delFromArea.schema' export * from './mutation.update.schema' export * from './query.getServiceArea.schema' // codegen:end diff --git a/packages/auth/.vscode/settings.json b/packages/auth/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/auth/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/config/.vscode/settings.json b/packages/config/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/config/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/crowdin/.vscode/settings.json b/packages/crowdin/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/crowdin/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/db/.vscode/settings.json b/packages/db/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/db/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/db/generated/attributeSupplementSchema.ts b/packages/db/generated/attributeSupplementSchema.ts new file mode 100644 index 00000000000..bc714e9bc46 --- /dev/null +++ b/packages/db/generated/attributeSupplementSchema.ts @@ -0,0 +1,90 @@ +import { z } from 'zod' + +export const attributeSupplementSchema = { + 'access-instruction-email': z + .object({ + access_type: z.literal('email').default('email'), + access_value: z.union([z.string().email(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + 'access-instruction-file': z + .object({ + access_type: z.literal('file').default('file'), + access_value: z.union([z.string().url(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + 'access-instruction-link': z + .object({ + access_type: z.literal('link').default('link'), + access_value: z.union([z.string().url(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + 'access-instruction-location': z + .object({ + access_type: z.literal('location').default('location'), + access_value: z.union([z.string(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + 'access-instruction-other': z + .object({ + access_type: z.literal('other').default('other'), + access_value: z.union([z.string(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + 'access-instruction-phone': z + .object({ + access_type: z.literal('phone').default('phone'), + access_value: z.union([z.string(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + 'access-instruction-publicTransport': z + .object({ + access_type: z.literal('publicTransit').default('publicTransit'), + access_value: z.union([z.string(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + accessInstructions: z.object({ + access_type: z.enum(['email', 'file', 'link', 'location', 'other', 'phone']), + access_value: z.union([z.string(), z.null()]).optional(), + instructions: z.string(), + }), + 'access-instruction-sms': z + .object({ + sms_body: z.string().optional(), + access_type: z.literal('sms').default('sms'), + access_value: z.union([z.string(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + 'access-instruction-whatsapp': z + .object({ + access_type: z.literal('whatsapp').default('whatsapp'), + access_value: z.union([z.string(), z.null()]).optional(), + instructions: z.string().optional(), + }) + .strict(), + currency: z.object({ cost: z.number(), currency: z.union([z.string(), z.null()]).optional() }).strict(), + incompatibleData: z.array(z.record(z.any())), + number: z.object({ num: z.number() }), + numMax: z.object({ max: z.number() }), + numMin: z.object({ min: z.number() }), + numMinMaxOrRange: z.union([ + z.object({ max: z.never().optional(), min: z.number() }).strict(), + z.object({ max: z.number(), min: z.never().optional() }).strict(), + z.object({ max: z.number(), min: z.number() }).strict(), + ]), + numRange: z.object({ max: z.number(), min: z.number() }), + otherDescribe: z.object({ other: z.string() }), +} + +export const isAttributeSupplementSchema = (schema: string): schema is AttributeSupplementSchemas => + Object.keys(attributeSupplementSchema).includes(schema) + +export type AttributeSupplementSchemas = keyof typeof attributeSupplementSchema diff --git a/packages/db/lib/generateData.ts b/packages/db/lib/generateData.ts index af5986c3647..486a1474974 100644 --- a/packages/db/lib/generateData.ts +++ b/packages/db/lib/generateData.ts @@ -39,6 +39,7 @@ const tasks = new Listr( defineJob('Service Categories', job.generateServiceCategories), defineJob('Language lists', job.generateLanguageFiles), defineJob('Translation Namespaces', job.generateNamespaces), + defineJob('Attribute Supplement Data Schemas', job.generateDataSchemas), ], { concurrent: true } ), diff --git a/packages/db/lib/generators/attributeSuppDataSchemas.ts b/packages/db/lib/generators/attributeSuppDataSchemas.ts new file mode 100644 index 00000000000..7d557a76c0e --- /dev/null +++ b/packages/db/lib/generators/attributeSuppDataSchemas.ts @@ -0,0 +1,35 @@ +import { type JsonSchemaObject, jsonSchemaToZod } from 'json-schema-to-zod' + +import { prisma } from '~db/client' +import { type ListrTask } from '~db/lib/generateData' + +import { writeOutput } from './common' + +export const generateDataSchemas = async (task: ListrTask) => { + const data = await prisma.attributeSupplementDataSchema.findMany({ + where: { + active: true, + }, + select: { + tag: true, + schema: true, + }, + orderBy: { tag: 'asc' }, + }) + const schemas = data.map(({ tag, schema }) => { + return `"${tag}": ${jsonSchemaToZod(schema as JsonSchemaObject)},` + }) + + const out = ` + import { z } from 'zod'; + export const attributeSupplementSchema = { + ${schemas.join('\n')} + } + + export const isAttributeSupplementSchema = (schema: string): schema is AttributeSupplementSchemas => Object.keys(attributeSupplementSchema).includes(schema) + + export type AttributeSupplementSchemas = keyof typeof attributeSupplementSchema + ` + await writeOutput('attributeSupplementSchema', out) + task.title = `${task.title} (${data.length} items)` +} diff --git a/packages/db/lib/generators/index.ts b/packages/db/lib/generators/index.ts index 17e4078d0f6..91661da73ac 100644 --- a/packages/db/lib/generators/index.ts +++ b/packages/db/lib/generators/index.ts @@ -2,6 +2,7 @@ export * from './allAttributes' export * from './attributeCategory' export * from './attributesByCategory' +export * from './attributeSuppDataSchemas' export * from './langs' export * from './namespaces' export * from './permission' diff --git a/packages/db/prisma/data-migrations/2024-02-14_attribute-supplement-schemas/index.ts b/packages/db/prisma/data-migrations/2024-02-14_attribute-supplement-schemas/index.ts new file mode 100644 index 00000000000..ca89dd481c6 --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-14_attribute-supplement-schemas/index.ts @@ -0,0 +1,150 @@ +import { prisma, type Prisma } from '~db/client' +import { formatMessage } from '~db/prisma/common' +import { type MigrationJob } from '~db/prisma/dataMigrationRunner' +import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun' +import { type FieldAttributes, FieldType } from '~db/zod_util/attributeSupplement' + +/** Define the job metadata here. */ +const jobDef: JobDef = { + jobId: '2024-02-14_attribute-supplement-schemas', + title: 'attribute supplement schemas', + createdBy: 'Joe Karow', + /** Optional: Longer description for the job */ + description: undefined, +} +/** + * Job export - this variable MUST be UNIQUE + */ +export const job20240214_attribute_supplement_schemas = { + title: `[${jobDef.jobId}] ${jobDef.title}`, + task: async (_ctx, task) => { + /** Create logging instance */ + createLogger(task, jobDef.jobId) + const log = (...args: Parameters) => (task.output = formatMessage(...args)) + /** + * Start defining your data migration from here. + * + * To log output, use `task.output = 'Message to log'` + * + * This will be written to `stdout` and to a log file in `/prisma/migration-logs/` + */ + + // Do stuff + const deleted = await prisma.attributeSupplementDataSchema.deleteMany({ + where: { + id: { + in: [ + 'asds_01GW2HHH9NFEXHG9RHBTM9NRFR', + 'asds_01GW2HHH9PKJ6H9WFSNZSVK2G4', + 'asds_01GW2HHH9P7J5A1CBGN6B5QCG7', + 'asds_01GW2HHH9PN6MJ4ZS7D17G1YTK', + 'asds_01GW2HHH9PSSYV7TKFA6DY68P4', + ], + }, + }, + }) + + log(`Deleted ${deleted.count} records.`) + + const updateData: SchemaUpdate[] = [ + { + data: { + definition: [ + { key: 'min', label: 'Min', name: 'min', type: FieldType.number }, + { key: 'max', label: 'Max', name: 'max', type: FieldType.number }, + ], + // tag: 'numMinMaxOrRange', + }, + where: { + id: 'asds_01GYX872BWWCGTZREHDT2AFF9D', + }, + }, + { + data: { + definition: [ + { key: 'min', label: 'Min', name: 'min', type: FieldType.number }, + { key: 'max', label: 'Max', name: 'max', type: FieldType.number }, + ], + // tag: 'numRange', + }, + where: { + id: 'asds_01GYX872BYZQ6CC344S1SWTJ97', + }, + }, + { + data: { + definition: [{ key: 'min', label: 'Min', name: 'min', type: FieldType.number }], + // tag: 'numMin', + }, + where: { + id: 'asds_01GYX872BZE4TN1MJHMTGVAYZ0', + }, + }, + { + data: { + definition: [{ key: 'max', label: 'Max', name: 'max', type: FieldType.number }], + // tag: 'numMax', + }, + where: { + id: 'asds_01GYX872BZNT0F6WH50XJQWM9G', + }, + }, + { + data: { + definition: [{ key: 'num', label: 'Number', name: 'num', type: FieldType.number }], + // tag: 'number', + }, + where: { + id: 'asds_01GYX872BZKJPVH6VHC0ABFH8A', + }, + }, + { + data: { + definition: [ + { + key: 'incompatible', + label: 'Incompatible', + name: 'incompatible', + type: FieldType.text, + }, + ], + // tag: 'incompatibleData', + }, + where: { + id: 'asds_01GYX872BZSMTHYM4HYYTCENZM', + }, + }, + { + data: { + definition: [{ key: 'other', label: 'Other', name: 'other', type: FieldType.text }], + // tag: 'otherDescribe', + }, + where: { + id: 'asds_01GYX872BZ7V6VQ3NE6KSVVRKH', + }, + }, + ] + const updates = await prisma.$transaction( + updateData.map((args) => + prisma.attributeSupplementDataSchema.update( + args as unknown as Prisma.AttributeSupplementDataSchemaUpdateArgs + ) + ) + ) + log(`Updated ${updates.length} records.`) + /** + * DO NOT REMOVE BELOW + * + * This writes a record to the DB to register that this migration has run successfully. + */ + await jobPostRunner(jobDef) + }, + def: jobDef, +} satisfies MigrationJob + +type SchemaUpdate = { + where: { id: string } + data: { + definition: FieldAttributes | FieldAttributes[] + } +} diff --git a/packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/data.json b/packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/data.json new file mode 100644 index 00000000000..74a3a2fdf2b --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/data.json @@ -0,0 +1,1396 @@ +[ + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "at-capacity" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "geo-near-public-transit" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "geo-public-transit-description" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "has-confidentiality-policy" + } + }, + { + "data": { + "canAttachTo": [ + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "offers-remote-services" + } + }, + { + "data": { + "canAttachTo": [ + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "private-practice" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "religiously-affiliated" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "time-walk-in" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "wheelchair-accessible" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "info" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "warn" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "adults" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "africa-immigrant" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "african-american" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "api" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "asexual" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "asia-immigrant" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "asylee" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "asylum-seeker" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "bipoc" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "bisexual" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "black" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "citizens" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "conversion-therapy-survivors" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "daca-recipient-seeker" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "detained-immigrant" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "disabled" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "gay" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "gender-nonconforming" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "hiv-aids" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "homeless" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "human-trafficking-survivor" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "intersex" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "language-speakers" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "latin-america-immigrant" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "latinx" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "lesbian" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "lgbtq-youth" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "lgbtq-youth-caregivers" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "middle-east-immigrant" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "muslim" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "native-american-two-spirit" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "nonbinary" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "queer" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "refugee" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "residents-green-card-holders" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "seniors" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "sex-workers" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "teens" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "transfeminine" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "transgender" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "transmasculine" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "trans-youth" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "trans-youth-caregivers" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "unaccompanied-minors" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "undocumented" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "cost-fees" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "cost-free" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "elders" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "general-lgbtq" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "elig-age" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "other-describe" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "req-medical-insurance" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "req-photo-id" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "req-proof-of-age" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "req-proof-of-income" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "req-proof-of-residence" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "req-referral" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "time-appointment-required" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "all-languages-by-interpreter" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "american-sign-language" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE", + "ORGANIZATION", + "LOCATION" + ] + }, + "where": { + "tag": "lang-offered" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION" + ] + }, + "where": { + "tag": "bipoc-led" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION" + ] + }, + "where": { + "tag": "black-led" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION" + ] + }, + "where": { + "tag": "immigrant-led" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION" + ] + }, + "where": { + "tag": "trans-led" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION" + ] + }, + "where": { + "tag": "women-led" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accessemail" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accessfile" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accesslink" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accesslocation" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accessphone" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accesspublictransit" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accesssms" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accesstext" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "accesswhatsapp" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "asylum-seekers" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "bipoc-comm" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "caregivers-focus" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "disabled-focus" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "elder-focus" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "gender-nc" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "hiv-comm" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "immigrant-comm" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "incarcerated-focus" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "lgbtq-youth-focus" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "resettled-refugees" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "spanish-speakers" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "trans-comm" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "trans-fem" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "trans-masc" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "trans-youth-focus" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE" + ] + }, + "where": { + "tag": "women-focus" + } + }, + { + "data": { + "canAttachTo": [ + "LOCATION", + "ORGANIZATION", + "SERVICE", + "USER" + ] + }, + "where": { + "tag": "incompatible-info" + } + }, + { + "data": { + "canAttachTo": [ + "SERVICE" + ] + }, + "where": { + "tag": "tpop-other" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "corp-law-firm" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "law-other" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "law-school-clinic" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "legal-nonprofit" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.case-mananger" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.community-org" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.friend-family" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.govt-agency" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.grassroots-direct" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.healthcare" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.lawyer" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.other" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.paralegal" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.social-worker" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.student-club" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.teacher" + } + }, + { + "data": { + "canAttachTo": [ + "USER" + ] + }, + "where": { + "tag": "userserviceprovider.therapist-counselor" + } + } +] \ No newline at end of file diff --git a/packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/index.ts b/packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/index.ts new file mode 100644 index 00000000000..b39733c37e8 --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-15_attribute-attachments/index.ts @@ -0,0 +1,64 @@ +import { z } from 'zod' + +import { prisma, Prisma } from '~db/client' +import { formatMessage } from '~db/prisma/common' +import { type MigrationJob } from '~db/prisma/dataMigrationRunner' +import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun' + +import data from './data.json' + +const Schema = z + .object({ + where: z.object({ tag: z.string() }), + data: z.object({ + canAttachTo: z + .enum(['SERVICE', 'ORGANIZATION', 'LOCATION', 'USER']) + .array() + .transform((x) => ({ set: x })), + }), + }) + .array() + +/** Define the job metadata here. */ +const jobDef: JobDef = { + jobId: '2024-02-15_attribute-attachments', + title: 'attribute attachments', + createdBy: 'Joe Karow', + /** Optional: Longer description for the job */ + description: undefined, +} +/** + * Job export - this variable MUST be UNIQUE + */ +export const job20240215_attribute_attachments = { + title: `[${jobDef.jobId}] ${jobDef.title}`, + task: async (_ctx, task) => { + /** Create logging instance */ + createLogger(task, jobDef.jobId) + const log = (...args: Parameters) => (task.output = formatMessage(...args)) + /** + * Start defining your data migration from here. + * + * To log output, use `task.output = 'Message to log'` + * + * This will be written to `stdout` and to a log file in `/prisma/migration-logs/` + */ + const parsed = Schema.parse(data) + + const updates = await prisma.$transaction( + parsed.map((args) => { + return prisma.attribute.update(args) + }) + ) + + log(`Updated ${updates.length} records.`) + + /** + * DO NOT REMOVE BELOW + * + * This writes a record to the DB to register that this migration has run successfully. + */ + await jobPostRunner(jobDef) + }, + def: jobDef, +} satisfies MigrationJob diff --git a/packages/db/prisma/data-migrations/2024-03-21_attribute-supplement-schemas.ts b/packages/db/prisma/data-migrations/2024-03-21_attribute-supplement-schemas.ts new file mode 100644 index 00000000000..b2c65b4c3c0 --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-03-21_attribute-supplement-schemas.ts @@ -0,0 +1,95 @@ +import { z } from 'zod' +import { zodToJsonSchema } from 'zod-to-json-schema' + +import { prisma } from '~db/client' +import { formatMessage } from '~db/prisma/common' +import { type MigrationJob } from '~db/prisma/dataMigrationRunner' +import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun' +import { JsonInputOrNull } from '~db/zod_util' +import { FieldType } from '~db/zod_util/attributeSupplement' + +/** Define the job metadata here. */ +const jobDef: JobDef = { + jobId: '2024-03-21_attribute-supplement-schemas', + title: 'attribute supplement schemas', + createdBy: 'Joe Karow', + /** Optional: Longer description for the job */ + description: undefined, +} + +const schemas = { + currency: z.object({ + cost: z.number(), + currency: z.string().nullish(), + }), + numMinMaxOrRange: z + .union([ + z.object({ min: z.number(), max: z.never() }), + z.object({ min: z.never(), max: z.number() }), + z.object({ min: z.number(), max: z.number() }), + ]) + .refine(({ min, max }) => (min && max ? min < max : true), { + message: 'min must be less than max', + }), +} +/** + * Job export - this variable MUST be UNIQUE + */ +export const job20240321_attribute_supplement_schemas = { + title: `[${jobDef.jobId}] ${jobDef.title}`, + task: async (_ctx, task) => { + /** Create logging instance */ + createLogger(task, jobDef.jobId) + const log = (...args: Parameters) => (task.output = formatMessage(...args)) + /** + * Start defining your data migration from here. + * + * To log output, use `task.output = 'Message to log'` + * + * This will be written to `stdout` and to a log file in `/prisma/migration-logs/` + */ + + // Do stuff + + const newSchemas = await prisma.attributeSupplementDataSchema.createMany({ + data: [ + { + id: 'asds_01HSGTSP6SKA5NZS9J42Z8S5BT', + tag: 'currency', + name: 'Currency', + definition: [ + { + key: 'cost', + name: 'cost', + type: FieldType.number, + label: 'Cost', + }, + { + key: 'currency', + name: 'currency', + type: FieldType.text, + label: 'Currency', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.currency)), + }, + ], + skipDuplicates: true, + }) + log(`Created ${newSchemas.count} Attribute Supplement Schema records.`) + const updateMinMax = await prisma.attributeSupplementDataSchema.update({ + where: { id: 'asds_01GYX872BWWCGTZREHDT2AFF9D' }, + data: { + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.numMinMaxOrRange)), + }, + }) + log(`Updated Attribute Supplement Schema: ${updateMinMax.name}.`) + /** + * DO NOT REMOVE BELOW + * + * This writes a record to the DB to register that this migration has run successfully. + */ + await jobPostRunner(jobDef) + }, + def: jobDef, +} satisfies MigrationJob diff --git a/packages/db/prisma/data-migrations/2024-04-03_access-instruction-schemas.ts b/packages/db/prisma/data-migrations/2024-04-03_access-instruction-schemas.ts new file mode 100644 index 00000000000..c0abc9f4bcf --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-04-03_access-instruction-schemas.ts @@ -0,0 +1,297 @@ +import { z } from 'zod' +import { zodToJsonSchema } from 'zod-to-json-schema' + +import { prisma } from '~db/client' +import { formatMessage } from '~db/prisma/common' +import { type MigrationJob } from '~db/prisma/dataMigrationRunner' +import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun' +import { JsonInputOrNull } from '~db/zod_util' +import { FieldType } from '~db/zod_util/attributeSupplement' + +/** Define the job metadata here. */ +const jobDef: JobDef = { + jobId: '2024-04-03_access-instruction-schemas', + title: 'access instruction schemas', + createdBy: 'Joe Karow', + /** Optional: Longer description for the job */ + description: undefined, +} + +const schemas = { + email: z.object({ + access_type: z.literal('email').optional().default('email'), + access_value: z.string().email().nullish(), + instructions: z.string().optional(), + }), + file: z.object({ + access_type: z.literal('file').optional().default('file'), + access_value: z.string().url().nullish(), + instructions: z.string().optional(), + }), + link: z.object({ + access_type: z.literal('link').optional().default('link'), + access_value: z.string().url().nullish(), + instructions: z.string().optional(), + }), + location: z.object({ + access_type: z.literal('location').optional().default('location'), + access_value: z.string().nullish(), + instructions: z.string().optional(), + }), + other: z.object({ + access_type: z.literal('other').optional().default('other'), + access_value: z.string().nullish(), + instructions: z.string().optional(), + }), + phone: z.object({ + access_type: z.literal('phone').optional().default('phone'), + access_value: z.string().nullish(), + instructions: z.string().optional(), + }), + sms: z.object({ + access_type: z.literal('sms').optional().default('sms'), + sms_body: z.string().optional(), + access_value: z.string().nullish(), + instructions: z.string().optional(), + }), + whatsapp: z.object({ + access_type: z.literal('whatsapp').optional().default('whatsapp'), + access_value: z.string().nullish(), + instructions: z.string().optional(), + }), + publicTransport: z.object({ + access_type: z.literal('publicTransit').optional().default('publicTransit'), + access_value: z.string().nullish(), + instructions: z.string().optional(), + }), +} +const numMinMaxOrRange = z + .union([ + z.object({ min: z.number(), max: z.never().optional() }), + z.object({ min: z.never().optional(), max: z.number() }), + z.object({ min: z.number(), max: z.number() }), + ]) + .refine(({ min, max }) => (min && max ? min < max : true), { + message: 'min must be less than max', + }) + +/** + * Job export - this variable MUST be UNIQUE + */ +export const job20240403_access_instruction_schemas = { + title: `[${jobDef.jobId}] ${jobDef.title}`, + task: async (_ctx, task) => { + /** Create logging instance */ + createLogger(task, jobDef.jobId) + const log = (...args: Parameters) => (task.output = formatMessage(...args)) + /** + * Start defining your data migration from here. + * + * To log output, use `task.output = 'Message to log'` + * + * This will be written to `stdout` and to a log file in `/prisma/migration-logs/` + */ + + // Do stuff + + const newSchemas = await prisma.attributeSupplementDataSchema.createMany({ + data: [ + { + id: 'asds_01HTJ6EZ419CVQCY4N8KAYYCMB', + tag: 'access-instruction-email', + name: 'Access Instruction - Email', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'Email Address', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.email)), + }, + { + id: 'asds_01HTJ6EZ42H4YZ68RM1WDSEK89', + tag: 'access-instruction-file', + name: 'Access Instruction - File', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'File URL', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.file)), + }, + { + id: 'asds_01HTJ6EZ42PTQZG4SPQDBHM8BN', + tag: 'access-instruction-link', + name: 'Access Instruction - Link', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'Link URL', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.link)), + }, + { + id: 'asds_01HTJ6EZ42YHJT3CY7SK8N2WW6', + tag: 'access-instruction-location', + name: 'Access Instruction - Location', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'Location', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.location)), + }, + { + id: 'asds_01HTJ6EZ42HTHRFAH0JDC2ZXG1', + tag: 'access-instruction-other', + name: 'Access Instruction - Other', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'Other', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.other)), + }, + { + id: 'asds_01HTJ6EZ42TRHK12DVNDK8ZT02', + tag: 'access-instruction-phone', + name: 'Access Instruction - Phone', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'Phone Number', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.phone)), + }, + { + id: 'asds_01HTJ6EZ42GGZJ0R4S73T5KCNK', + tag: 'access-instruction-sms', + name: 'Access Instruction - SMS', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'SMS Details', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.sms)), + }, + { + id: 'asds_01HTJ6EZ42V93736GW3DPM34V8', + tag: 'access-instruction-whatsapp', + name: 'Access Instruction - WhatsApp', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'WhatsApp Number', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.whatsapp)), + }, + { + id: 'asds_01HTJ6EZ42KNM6A4BC02PXZFJH', + tag: 'access-instruction-publicTransport', + name: 'Access Instruction - Public Transport', + definition: [ + { + key: 'access_value', + name: 'access_value', + type: FieldType.text, + label: 'Public Transport Details', + }, + ], + schema: JsonInputOrNull.parse(zodToJsonSchema(schemas.publicTransport)), + }, + ], + skipDuplicates: true, + }) + + log(`Created ${newSchemas.count} Access Instruction Schema records.`) + + const updateData: UpdateData[] = [ + { + where: 'attr_01GW2HHFVKFM4TDY4QRK4AR2ZW', + schemaId: 'asds_01HTJ6EZ419CVQCY4N8KAYYCMB', + }, + { + where: 'attr_01GW2HHFVKMRHFD8SMDAZM3SSM', + schemaId: 'asds_01HTJ6EZ42H4YZ68RM1WDSEK89', + }, + { + where: 'attr_01GW2HHFVMYXMS8ARA3GE7HZFD', + schemaId: 'asds_01HTJ6EZ42PTQZG4SPQDBHM8BN', + }, + { + where: 'attr_01GW2HHFVMH6AE94EXN7T5A87C', + schemaId: 'asds_01HTJ6EZ42YHJT3CY7SK8N2WW6', + }, + { + where: 'attr_01GW2HHFVMKTFWCKBVVFJ5GMY0', + schemaId: 'asds_01HTJ6EZ42TRHK12DVNDK8ZT02', + }, + { + where: 'attr_01GW2HHFVMSX7T1WDNZ5QEHKWT', + schemaId: 'asds_01HTJ6EZ42KNM6A4BC02PXZFJH', + }, + { + where: 'attr_01H6PRPT32KX1JPGJSHAF2D89C', + schemaId: 'asds_01HTJ6EZ42GGZJ0R4S73T5KCNK', + }, + { + where: 'attr_01GW2HHFVMMF19AX2KPBTMV6P3', + schemaId: 'asds_01HTJ6EZ42HTHRFAH0JDC2ZXG1', + }, + { + where: 'attr_01H6PRPTWRS80XFM77EMHKZ787', + schemaId: 'asds_01HTJ6EZ42V93736GW3DPM34V8', + }, + ] + + const updateDefinitions = await prisma.$transaction([ + ...updateData.map(({ where, schemaId }) => + prisma.attribute.update({ + where: { id: where }, + data: { requiredSchemaId: schemaId }, + }) + ), + prisma.attributeSupplementDataSchema.update({ + where: { id: 'asds_01GYX872BWWCGTZREHDT2AFF9D' }, + data: { schema: JsonInputOrNull.parse(zodToJsonSchema(numMinMaxOrRange)) }, + }), + ]) + log(`Updated ${updateDefinitions.length} Attribute records.`) + + /** + * DO NOT REMOVE BELOW + * + * This writes a record to the DB to register that this migration has run successfully. + */ + await jobPostRunner(jobDef) + }, + def: jobDef, +} satisfies MigrationJob + +type UpdateData = { + where: string + schemaId: string +} diff --git a/packages/db/prisma/data-migrations/index.ts b/packages/db/prisma/data-migrations/index.ts index d935976869b..f49dd92b67a 100644 --- a/packages/db/prisma/data-migrations/index.ts +++ b/packages/db/prisma/data-migrations/index.ts @@ -3,10 +3,14 @@ export * from './2024-01-31_fix-attr-supp-json/index' export * from './2024-01-31_target-population-attrib' export * from './2024-02-01_add-missing-attributes/index' export * from './2024-02-02_deactivate-incompatible-attribs' +export * from './2024-02-14_attribute-supplement-schemas/index' +export * from './2024-02-15_attribute-attachments/index' export * from './2024-02-19_attach-orphan-text' export * from './2024-02-20_appsheet-load/index' export * from './2024-02-23_add-missing-website' export * from './2024-03-08_update-alerts-and-org-urls/index' export * from './2024-03-11_hide-locations' export * from './2024-03-15_update-dead-links/index' +export * from './2024-03-21_attribute-supplement-schemas' +export * from './2024-04-03_access-instruction-schemas' // codegen:end diff --git a/packages/db/prisma/migrations/20240214173007_attribute_supplement_schemas/migration.sql b/packages/db/prisma/migrations/20240214173007_attribute_supplement_schemas/migration.sql new file mode 100644 index 00000000000..3ccd720ef84 --- /dev/null +++ b/packages/db/prisma/migrations/20240214173007_attribute_supplement_schemas/migration.sql @@ -0,0 +1,48 @@ +/* + Warnings: + + - Added the required column `schema` to the `AttributeSupplementDataSchema` table without a default value. This is not possible if the table is not empty. + */ +-- AlterTable +ALTER TABLE "AttributeSupplementDataSchema" + ADD COLUMN "schema" JSONB; + +UPDATE + "AttributeSupplementDataSchema" +SET + "schema" = "definition"; + +ALTER TABLE "AttributeSupplementDataSchema" + ALTER COLUMN "schema" SET NOT NULL; + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "AttributeSupplement_active_attributeId_idx" ON + "AttributeSupplement"("active", "attributeId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "OrgLocationService_active_serviceId_idx" ON + "OrgLocationService"("active", "serviceId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "OrgService_organizationId_published_deleted_idx" ON + "OrgService"("organizationId", "published" DESC, "deleted"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "ServiceArea_active_organizationId_idx" ON + "ServiceArea"("active", "organizationId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "ServiceArea_active_orgLocationId_idx" ON + "ServiceArea"("active", "orgLocationId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "ServiceArea_active_orgServiceId_idx" ON + "ServiceArea"("active", "orgServiceId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "ServiceAreaCountry_active_serviceAreaId_idx" ON + "ServiceAreaCountry"("active", "serviceAreaId"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "ServiceAreaDist_active_serviceAreaId_idx" ON + "ServiceAreaDist"("active", "serviceAreaId"); diff --git a/packages/db/prisma/migrations/20240215164734_attribute_attachment/migration.sql b/packages/db/prisma/migrations/20240215164734_attribute_attachment/migration.sql new file mode 100644 index 00000000000..3b72a68a831 --- /dev/null +++ b/packages/db/prisma/migrations/20240215164734_attribute_attachment/migration.sql @@ -0,0 +1,11 @@ +-- CreateEnum +CREATE TYPE "AttributeAttachment" AS ENUM( + 'ORGANIZATION', + 'LOCATION', + 'SERVICE', + 'USER' +); + +-- AlterTable +ALTER TABLE "Attribute" + ADD COLUMN "canAttachTo" "AttributeAttachment"[]; diff --git a/packages/db/prisma/migrations/20240215172645_update_attrib_by_cat_view/migration.sql b/packages/db/prisma/migrations/20240215172645_update_attrib_by_cat_view/migration.sql new file mode 100644 index 00000000000..704492a5382 --- /dev/null +++ b/packages/db/prisma/migrations/20240215172645_update_attrib_by_cat_view/migration.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE VIEW public.attributes_by_category AS +SELECT + ac.id AS "categoryId", + ac.tag AS "categoryName", + ac.name AS "categoryDisplay", + a.id AS "attributeId", + a.tag AS "attributeName", + a."tsKey" AS "attributeKey", + a."tsNs" AS "attributeNs", + a.icon, + a."iconBg", + ac."renderVariant" AS "badgeRender", + a."requireText", + a."requireLanguage", + a."requireGeo", + a."requireBoolean", + a."requireData", + asds.definition AS "dataSchema", + tkey."interpolationValues", + asds.tag AS "dataSchemaName", + a."canAttachTo" +FROM + "AttributeCategory" ac + JOIN "AttributeToCategory" atc ON atc."categoryId" = ac.id + JOIN "Attribute" a ON a.id = atc."attributeId" + LEFT JOIN "AttributeSupplementDataSchema" asds ON asds.id = a."requiredSchemaId" + LEFT JOIN "TranslationKey" tkey ON tkey.key = a."tsKey" +WHERE + a.active = TRUE + AND ac.active = TRUE +ORDER BY + ac.tag, + a.tag; diff --git a/packages/db/prisma/migrations/20240216161351_alter_view/migration.sql b/packages/db/prisma/migrations/20240216161351_alter_view/migration.sql new file mode 100644 index 00000000000..10b021aa1f6 --- /dev/null +++ b/packages/db/prisma/migrations/20240216161351_alter_view/migration.sql @@ -0,0 +1,36 @@ +ALTER VIEW public.attributes_by_category RENAME COLUMN "dataSchema" TO "formSchema"; + +CREATE OR REPLACE VIEW public.attributes_by_category AS +SELECT + ac.id AS "categoryId", + ac.tag AS "categoryName", + ac.name AS "categoryDisplay", + a.id AS "attributeId", + a.tag AS "attributeName", + a."tsKey" AS "attributeKey", + a."tsNs" AS "attributeNs", + a.icon, + a."iconBg", + ac."renderVariant" AS "badgeRender", + a."requireText", + a."requireLanguage", + a."requireGeo", + a."requireBoolean", + a."requireData", + asds.definition AS "formSchema", + tkey."interpolationValues", + asds.tag AS "dataSchemaName", + a."canAttachTo", + asds.SCHEMA AS "dataSchema" +FROM + "AttributeCategory" ac + JOIN "AttributeToCategory" atc ON atc."categoryId" = ac.id + JOIN "Attribute" a ON a.id = atc."attributeId" + LEFT JOIN "AttributeSupplementDataSchema" asds ON asds.id = a."requiredSchemaId" + LEFT JOIN "TranslationKey" tkey ON tkey.key = a."tsKey" +WHERE + a.active = TRUE + AND ac.active = TRUE +ORDER BY + ac.tag, + a.tag; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index d3f68836eeb..82229b3adab 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -997,6 +997,7 @@ model Attribute { requireData Boolean @default(false) requireDataSchema AttributeSupplementDataSchema? @relation(fields: [requiredSchemaId], references: [id]) requiredSchemaId String? + canAttachTo AttributeAttachment[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -1007,6 +1008,13 @@ model Attribute { @@unique([tsKey, tsNs]) } +enum AttributeAttachment { + ORGANIZATION + LOCATION + SERVICE + USER +} + enum FilterType { INCLUDE EXCLUDE @@ -1062,6 +1070,7 @@ model AttributeSupplementDataSchema { name String active Boolean @default(true) definition Json + schema Json // entryComponent String? createdAt DateTime @default(now()) @@ -2297,6 +2306,8 @@ view AttributesByCategory { requireBoolean Boolean requireData Boolean dataSchemaName String? + formSchema Json? + canAttachTo AttributeAttachment[] dataSchema Json? @@unique([categoryId, attributeId]) diff --git a/packages/db/zod_util/attributeSupplement.ts b/packages/db/zod_util/attributeSupplement.ts index b0d120251a4..e869bfffe35 100644 --- a/packages/db/zod_util/attributeSupplement.ts +++ b/packages/db/zod_util/attributeSupplement.ts @@ -79,7 +79,10 @@ export const accessInstructions = { access_type: z.literal(''), ...commonAccessInstructions, }), - + publicTransport: z.object({ + access_type: z.literal('publicTransit'), + ...commonAccessInstructions, + }), getAll: function () { return z.discriminatedUnion('access_type', [ this.email, @@ -91,6 +94,7 @@ export const accessInstructions = { this.sms, this.whatsapp, this.blank, + this.publicTransport, ]) }, } @@ -104,6 +108,7 @@ export type AccessInstructions = { sms: z.infer whatsapp: z.infer blank: z.infer + publicTransport: z.infer getAll: () => z.infer> } diff --git a/packages/env/.vscode/settings.json b/packages/env/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/env/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/eslint-config/.vscode/settings.json b/packages/eslint-config/.vscode/settings.json deleted file mode 100644 index 7c77f0a9b1b..00000000000 --- a/packages/eslint-config/.vscode/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" - } -} diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts index d44faf6a5dd..d8fb696a3d6 100644 --- a/packages/ui/.storybook/main.ts +++ b/packages/ui/.storybook/main.ts @@ -113,7 +113,6 @@ const config: StorybookConfig = { const plugin = new I18NextHMRPlugin({ localesDir: path.resolve(__dirname, '../../../apps/app/public/locales'), }) - // @ts-expect-error It doesn't like the i18nHMRPlugin for some reason... Array.isArray(config.plugins) ? config.plugins.push(plugin) : (config.plugins = [plugin]) } diff --git a/packages/ui/.vscode/settings.json b/packages/ui/.vscode/settings.json index 331acd2d095..a2c3de751c3 100644 --- a/packages/ui/.vscode/settings.json +++ b/packages/ui/.vscode/settings.json @@ -2,9 +2,5 @@ "i18n-ally.localesPaths": "../../apps/app/public/locales", "[json]": { "editor.codeActionsOnSave": { "source.fixAll": "never" } - }, - "sonarlint.connectedMode.project": { - "connectionId": "inreach", - "projectKey": "weareinreach_InReach" } } diff --git a/packages/ui/components/core/UserAvatar.tsx b/packages/ui/components/core/UserAvatar.tsx index 887d5cbd30b..ca810416172 100644 --- a/packages/ui/components/core/UserAvatar.tsx +++ b/packages/ui/components/core/UserAvatar.tsx @@ -1,6 +1,6 @@ import { Avatar, createStyles, Group, rem, Skeleton, Stack, Text, useMantineTheme } from '@mantine/core' import { DateTime } from 'luxon' -import router from 'next/router' +import { useRouter } from 'next/router' import { type User } from 'next-auth' import { useSession } from 'next-auth/react' import { useTranslation } from 'next-i18next' @@ -35,6 +35,7 @@ export const UserAvatar = ({ const { t, i18n } = useTranslation() const { data: session, status } = useSession() const theme = useMantineTheme() + const router = useRouter() const subText = () => { if (!user && useLoggedIn && subheading !== undefined) { diff --git a/packages/ui/components/data-display/ContactInfo/index.tsx b/packages/ui/components/data-display/ContactInfo/index.tsx index d5d22b9c09c..0c809072bc7 100644 --- a/packages/ui/components/data-display/ContactInfo/index.tsx +++ b/packages/ui/components/data-display/ContactInfo/index.tsx @@ -47,7 +47,8 @@ export const ContactInfo = ({ return {items} } -export const hasContactInfo = (data: PassedDataObject) => { +export const hasContactInfo = (data: PassedDataObject | null | undefined): data is PassedDataObject => { + if (!data) return false const { websites, phones, emails, socialMedia } = data return Boolean(websites.length || phones.length || emails.length || socialMedia.length) } diff --git a/packages/ui/components/data-display/Hours.tsx b/packages/ui/components/data-display/Hours.tsx index 914dc8db250..4ce5434dea4 100644 --- a/packages/ui/components/data-display/Hours.tsx +++ b/packages/ui/components/data-display/Hours.tsx @@ -2,6 +2,7 @@ import { createStyles, List, rem, Skeleton, Stack, Table, Text, Title } from '@m import { Interval } from 'luxon' import { useTranslation } from 'next-i18next' +import { type ApiOutput } from '@weareinreach/api' import { HoursDrawer } from '~ui/components/data-portal/HoursDrawer' import { useCustomVariant } from '~ui/hooks/useCustomVariant' import { useLocalizedDays } from '~ui/hooks/useLocalizedDays' @@ -39,11 +40,13 @@ const nullObj = { 6: [], } -export const Hours = ({ parentId, label = 'regular', edit }: HoursProps) => { +export const Hours = ({ parentId, label = 'regular', edit, data: passedData }: HoursProps) => { const { t, i18n } = useTranslation('common') const variants = useCustomVariant() const { classes } = useStyles() - const { data, isLoading } = api.orgHours.forHoursDisplay.useQuery(parentId) + const { data, isLoading } = passedData + ? { data: passedData, isLoading: false } + : api.orgHours.forHoursDisplay.useQuery(parentId) const dayMap = useLocalizedDays(i18n.resolvedLanguage) if (!data && !isLoading) return null @@ -106,4 +109,5 @@ export interface HoursProps { parentId: string label?: keyof typeof labelKeys edit?: boolean + data?: ApiOutput['orgHours']['forHoursDisplay'] } diff --git a/packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx b/packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx index 274eff68f0d..73c0074a7ec 100644 --- a/packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx +++ b/packages/ui/components/data-portal/PhoneNumberEntry/withHookForm.tsx @@ -29,13 +29,16 @@ export const PhoneNumberEntry = ({ label = 'Phone Number', required, }: PhoneNumberEntryProps) => { - const { data: countryList } = api.fieldOpt.countries.useQuery( + const { data: countryData } = api.fieldOpt.countries.useQuery( { activeForOrgs: true }, { - initialData: [], select: (data) => transformCountryList(data), } ) + const countryList = useMemo(() => { + if (!countryData) return [] + return countryData + }, [countryData]) const validCountries = countryList.map(({ data }) => data.cca2) const { diff --git a/packages/ui/components/data-portal/ServiceEditDrawer.tsx b/packages/ui/components/data-portal/ServiceEditDrawer.tsx deleted file mode 100644 index 422140daf67..00000000000 --- a/packages/ui/components/data-portal/ServiceEditDrawer.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import { - Box, - type ButtonProps, - createPolymorphicComponent, - createStyles, - Drawer, - List, - Modal, - rem, - Stack, - Text, - Textarea, - Title, -} from '@mantine/core' -import { useForm } from '@mantine/form' -import { useDisclosure } from '@mantine/hooks' -import compact from 'just-compact' -import { useTranslation } from 'next-i18next' -import { forwardRef, type ReactNode, useEffect, useMemo } from 'react' - -import { Badge } from '~ui/components/core/Badge' -import { Breadcrumb } from '~ui/components/core/Breadcrumb' -import { useCustomVariant } from '~ui/hooks' -import { Icon } from '~ui/icon' -import { trpc as api } from '~ui/lib/trpcClient' -import { DataViewer } from '~ui/other/DataViewer' - -import { InlineTextInput } from './InlineTextInput' - -const useStyles = createStyles((theme) => ({ - drawerContent: { - borderRadius: `${rem(32)} 0 0 0`, - minWidth: '40vw', - }, - drawerBody: { - padding: `${rem(40)} ${rem(32)}`, - '&:not(:only-child)': { - paddingTop: rem(40), - }, - }, - badgeGroup: { - width: '100%', - cursor: 'pointer', - backgroundColor: theme.fn.lighten(theme.other.colors.secondary.teal, 0.9), - borderRadius: rem(8), - padding: rem(4), - }, - tealText: { - color: theme.other.colors.secondary.teal, - }, - dottedCard: { - border: `${rem(1)} dashed ${theme.other.colors.secondary.teal}`, - borderRadius: rem(16), - padding: rem(20), - }, -})) -const _ServiceEditDrawer = forwardRef( - ({ serviceId, ...props }, ref) => { - const [drawerOpened, drawerHandler] = useDisclosure(true) - const [serviceModalOpened, serviceModalHandler] = useDisclosure(false) - const { classes } = useStyles() - const form = useForm() - const variants = useCustomVariant() - const { t } = useTranslation(['country', 'gov-dist']) - // #region Get existing data/populate form - const { data, isLoading } = api.service.forServiceEditDrawer.useQuery(serviceId, { - refetchOnWindowFocus: false, - }) - - useEffect(() => { - if (data && !isLoading) { - form.setValues(data) - form.resetDirty() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, isLoading]) - - // #endregion - - // #region Get all available service options & filter selected - const { data: allServices } = api.service.getOptions.useQuery(undefined, { refetchOnWindowFocus: false }) - - const serviceBadges: ReactNode[] = - !form.values.services?.length || !allServices - ? [] - : form.values.services.map(({ id }) => { - const service = allServices.find((item) => item.id === id) - if (service) { - return {t(service.tsKey, { ns: service.tsNs })} - } - }) - - // #endregion - - // #region Get service area options - const { data: geoMap } = api.fieldOpt.countryGovDistMap.useQuery(undefined, { - refetchOnWindowFocus: false, - }) - const serviceAreas = () => { - const serviceAreaObj: Record = {} - const { countries, districts } = form.values.serviceAreas ?? {} - if (!geoMap) return null - const countryIdRegex = /^ctry_.*/ - const distIdRegex = /^gdst_.*/ - - if (countries?.length) { - for (const country of countries) { - const array = serviceAreaObj[country] - const countryDetails = geoMap.get(country) - if (!countryDetails) continue - if (!Array.isArray(array)) { - serviceAreaObj[country] = [ - - - All of {t(countryDetails.tsKey, { ns: countryDetails.tsNs })} - - , - ] - } else { - array.push( - - - All of {t(countryDetails.tsKey, { ns: countryDetails.tsNs })} - - - ) - } - } - } - if (districts?.length) { - for (const district of districts) { - const govDist = geoMap.get(district) - if (!govDist) continue - const country = govDist.parent?.parent?.id ?? govDist.parent?.id ?? '' - if (!countryIdRegex.test(country)) continue - const array = serviceAreaObj[country] - const parent = govDist.parent?.id ?? '' - const parentDist = geoMap.get(parent) - if (!distIdRegex.test(parent) || !parentDist) { - Array.isArray(array) - ? array.push( - - {t(govDist.tsKey, { ns: govDist.tsNs })} - - ) - : (serviceAreaObj[country] = [ - - {t(govDist.tsKey, { ns: govDist.tsNs })} - , - ]) - continue - } - Array.isArray(array) - ? array.push( - - - {t(parentDist.tsKey, { ns: parentDist.tsNs })} - {t(govDist.tsKey, { ns: govDist.tsNs })} - - - ) - : (serviceAreaObj[country] = [ - - - {t(parentDist.tsKey, { ns: parentDist.tsNs })} - {t(govDist.tsKey, { ns: govDist.tsNs })} - - , - ]) - continue - } - } - return Object.entries(serviceAreaObj).map(([key, value]) => { - const country = geoMap.get(key) - if (!country) return null - return ( - - {t(country.tsKey, { ns: country.tsNs })} - }> - {value} - - - ) - }) - } - - // #endregion - - return ( - <> - - - - - - - - - - - {Boolean(serviceBadges.length) && ( - <> - - {serviceBadges} - - - Tag edit screen - - - )} - {/* */} - - - Coverage Area - - {serviceAreas()} - {/* {Boolean(geoMap?.size) && } */} - - {/* */} - - - - - - - - - - ) - } -) -_ServiceEditDrawer.displayName = 'ServiceEditDrawer' - -export const ServiceEditDrawer = createPolymorphicComponent<'button', ServiceEditDrawerProps>( - _ServiceEditDrawer -) - -interface ServiceEditDrawerProps extends ButtonProps { - serviceId: string -} - -interface FreeText { - id?: string - key: string - ns: string - tsKey: { - text: string | null - crowdinId: number | null - } -} -interface Attribute { - attribute: { - categories?: string[] - id: string - tsKey: string - tsNs: string - icon?: string | null - } - supplement: { - id: string - active?: boolean - data: unknown - boolean?: boolean | null - countryId?: string | null - govDistId?: string | null - languageId?: string | null - text: FreeText | null - } -} -interface FormData { - id: string - published: boolean - deleted: boolean - serviceName: FreeText | null - description: FreeText | null - hours: { - id: string - dayIndex: number - start: Date - end: Date - closed: boolean - tz: string | null - }[] - phones: string[] - emails: string[] - locations: string[] - services: { - id: string - primaryCategoryId: string - }[] - serviceAreas: { - id: string - countries: string[] - districts: string[] - } | null - attributes: Attribute[] - accessDetails: { - id?: string - attribute: { id: string; tsKey: string; tsNs: string } - supplement: { - id: string - data: unknown - text: { id?: string; key: string; ns: string; tsKey: { text: string; crowdinId: number | null } } | null - } - }[] -} diff --git a/packages/ui/components/data-portal/ServiceEditDrawer.stories.tsx b/packages/ui/components/data-portal/ServiceEditDrawer/index.stories.tsx similarity index 68% rename from packages/ui/components/data-portal/ServiceEditDrawer.stories.tsx rename to packages/ui/components/data-portal/ServiceEditDrawer/index.stories.tsx index 8206fb7722f..e2c04861a7c 100644 --- a/packages/ui/components/data-portal/ServiceEditDrawer.stories.tsx +++ b/packages/ui/components/data-portal/ServiceEditDrawer/index.stories.tsx @@ -1,11 +1,14 @@ import { type Meta, type StoryObj } from '@storybook/react' import { Button } from '~ui/components/core/Button' -import { fieldOpt } from '~ui/mockData/fieldOpt' +import { component } from '~ui/mockData/component' +import { allFieldOptHandlers } from '~ui/mockData/fieldOpt' import { organization } from '~ui/mockData/organization' +import { orgHours } from '~ui/mockData/orgHours' import { service } from '~ui/mockData/service' +import { serviceArea } from '~ui/mockData/serviceArea' -import { ServiceEditDrawer } from './ServiceEditDrawer' +import { ServiceEditDrawer } from './index' export default { title: 'Data Portal/Drawers/Service Edit', @@ -27,14 +30,18 @@ export default { service.getNames, service.forServiceEditDrawer, service.getOptions, - fieldOpt.govDistsByCountry, - fieldOpt.countryGovDistMap, + component.ServiceSelect, + orgHours.forHoursDisplay, + serviceArea.addToArea, + serviceArea.delFromArea, + ...allFieldOptHandlers, ], }, args: { component: Button, children: 'Open Drawer', variant: 'inlineInvertedUtil1', + serviceId: 'osvc_123456789000000', }, } satisfies Meta diff --git a/packages/ui/components/data-portal/ServiceEditDrawer/index.tsx b/packages/ui/components/data-portal/ServiceEditDrawer/index.tsx new file mode 100644 index 00000000000..3c8c0f214e0 --- /dev/null +++ b/packages/ui/components/data-portal/ServiceEditDrawer/index.tsx @@ -0,0 +1,361 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { + ActionIcon, + Box, + type ButtonProps, + createPolymorphicComponent, + Drawer, + Group, + List, + Stack, + Text, + Title, + Tooltip, +} from '@mantine/core' +import { useDisclosure } from '@mantine/hooks' +import { useTranslation } from 'next-i18next' +import { forwardRef, type ReactNode } from 'react' +import { useForm } from 'react-hook-form' +import { Textarea, TextInput } from 'react-hook-form-mantine' +import invariant from 'tiny-invariant' + +import { Badge } from '~ui/components/core/Badge' +import { Breadcrumb } from '~ui/components/core/Breadcrumb' +import { Button } from '~ui/components/core/Button' +import { Section } from '~ui/components/core/Section' +import { ContactInfo, hasContactInfo } from '~ui/components/data-display/ContactInfo' +import { Hours } from '~ui/components/data-display/Hours' +import { ServiceSelect } from '~ui/components/data-portal/ServiceSelect' +import { useCustomVariant } from '~ui/hooks' +import { Icon } from '~ui/icon' +import { trpc as api } from '~ui/lib/trpcClient' +import { CoverageArea } from '~ui/modals/CoverageArea' +import { AttributeModal } from '~ui/modals/dataPortal/Attributes' +import { processAccessInstructions, processAttributes } from '~ui/modals/Service/processor' + +import { FormSchema, type TFormSchema } from './schemas' +import { useStyles } from './styles' +import { InlineTextInput } from '../InlineTextInput' + +const isObject = (x: unknown): x is object => typeof x === 'object' + +const ServiceAreaItem = ({ + serviceId, + serviceAreaId, + countryId, + govDistId, + children, +}: ServiceAreaItemProps) => { + const apiUtils = api.useUtils() + const removeServiceArea = api.serviceArea.delFromArea.useMutation({ + onSuccess: () => apiUtils.service.forServiceEditDrawer.invalidate(serviceId), + }) + if (!serviceAreaId || !(countryId || govDistId)) { + return children + } + + const actionHandler = () => { + removeServiceArea.mutate({ serviceAreaId, countryId, govDistId }) + } + + return ( + + + + + + + {children} + + ) +} + +const _ServiceEditDrawer = forwardRef( + ({ serviceId, ...props }, ref) => { + const [drawerOpened, drawerHandler] = useDisclosure(false) + const { classes } = useStyles() + const variants = useCustomVariant() + const { t, i18n } = useTranslation(['common', 'gov-dist']) + const apiUtils = api.useUtils() + // #region Get existing data/populate form + const { data } = api.service.forServiceEditDrawer.useQuery(serviceId, { + refetchOnWindowFocus: false, + }) + const form = useForm({ + resolver: zodResolver(FormSchema), + values: data, + }) + const dirtyFields = { + name: isObject(form.formState.dirtyFields.name) ? form.formState.dirtyFields.name.text : false, + description: isObject(form.formState.dirtyFields.description) + ? form.formState.dirtyFields.description.text + : false, + services: form.formState.dirtyFields.services ?? false, + } + + // #endregion + + // #region Get all available service options & filter selected + const { data: allServices } = api.service.getOptions.useQuery(undefined, { refetchOnWindowFocus: false }) + + const activeServices = form.watch('services') ?? [] + + // #endregion + + // #region Get service area options + const { data: geoMap } = api.fieldOpt.countryGovDistMap.useQuery(undefined, { + refetchOnWindowFocus: false, + }) + const { data: countryMap } = api.fieldOpt.ccaMap.useQuery( + { activeForOrgs: true }, + { refetchOnWindowFocus: false } + ) + const serviceAreas = () => { + const countryTranslation = new Intl.DisplayNames(i18n.language, { type: 'region' }) + const serviceAreaObj: Record = {} + const { countries, districts } = form.watch('serviceAreas') ?? {} + if (!geoMap) return null + const countryIdRegex = /^ctry_.*/ + const distIdRegex = /^gdst_.*/ + + const processCountry = (countryId: string) => { + serviceAreaObj[countryId] ??= [] + const array = serviceAreaObj[countryId] + invariant(array) + const cca2 = countryMap?.byId.get(countryId) + if (!cca2) return + const serviceAreaId = data?.serviceAreas?.id + const item = ( + + + All of {countryTranslation.of(cca2)} + + + ) + array.push(item) + } + const processDistrict = (govDistId: string) => { + const govDist = geoMap.get(govDistId) + const country = govDist?.parent?.parent?.id ?? govDist?.parent?.id ?? '' + if (!countryIdRegex.test(country) || !govDist) return + serviceAreaObj[country] ??= [] + const array = serviceAreaObj[country] + invariant(array) + const parent = govDist.parent?.id ?? '' + const parentDist = geoMap.get(parent) + const serviceAreaId = data?.serviceAreas?.id + const item = ( + + + + {!distIdRegex.test(parent) || !parentDist + ? t(govDist.tsKey, { ns: govDist.tsNs }) + : `${t(parentDist.tsKey, { ns: parentDist.tsNs })} - ${t(govDist.tsKey, { ns: govDist.tsNs })}`} + + + + ) + + array.push(item) + } + + if (countries?.length) { + for (const country of countries) { + processCountry(country) + } + } + if (districts?.length) { + for (const district of districts) { + processDistrict(district) + } + } + return Object.entries(serviceAreaObj)?.map(([key, value]) => { + const country = countryMap?.byId.get(key) + if (!country) return null + return ( + + {countryTranslation.of(country)} + + {value} + + + ) + }) + } + + // #endregion + + if (!data) return null + + const { getHelp, publicTransit } = data + ? processAccessInstructions({ + accessDetails: data?.accessDetails, + locations: data?.locations, + t, + }) + : { getHelp: null, publicTransit: null } + + const attributes = processAttributes({ + attributes: data.attributes, + locale: i18n.resolvedLanguage ?? 'en', + t, + }) + const coverageModalServiceArea = data.serviceAreas?.id ?? { orgServiceId: serviceId } + + return ( + <> + + + + + + + + } + parentRecord={{ serviceId: data.id }} + attachesTo={['SERVICE']} + > + Add Attribute + + + + + + + + } + label='Service Name' + name='name.text' + control={form.control} + fontSize='h2' + data-isDirty={dirtyFields.name} + /> + } + label='Description' + name='description.text' + control={form.control} + data-isDirty={dirtyFields.description} + autosize + /> + + Services + + + {activeServices.map((serviceId) => { + const service = allServices?.find((s) => s.id === serviceId) + if (!service) return null + return ( + + {t(service.tsKey, { ns: service.tsNs })} + + ) + })} + + + + {/* */} + + Coverage Area + + {serviceAreas()} + { + apiUtils.service.forServiceEditDrawer.invalidate(serviceId) + apiUtils.service.forServiceModal.invalidate(serviceId) + }} + component={Button} + variant={variants.Button.secondarySm} + > + Add new service area + + {/* {Boolean(geoMap?.size) && } */} + + + {hasContactInfo(getHelp) && ( + + )} + {publicTransit} + {Boolean(Object.values(data.hours).length) && ( + + )} + + + + {attributes.clientsServed.srvfocus} + + + {attributes.clientsServed.targetPop} + + + {attributes.cost} + + {attributes.eligibility.age} + + + {attributes.eligibility.requirements.map((text, i) => ( + {text} + ))} + + + + {attributes.eligibility.freeText} + + + + + + {attributes.lang.map((lang, i) => ( + {lang} + ))} + + + + + + {attributes.miscWithIcons} + + + + {attributes.misc.map((text, i) => ( + {text} + ))} + + + + + + + + + + + + ) + } +) +_ServiceEditDrawer.displayName = 'ServiceEditDrawer' + +export const ServiceEditDrawer = createPolymorphicComponent<'button', ServiceEditDrawerProps>( + _ServiceEditDrawer +) + +interface ServiceEditDrawerProps extends ButtonProps { + serviceId: string +} + +interface ServiceAreaItemProps { + serviceId: string + serviceAreaId?: string + countryId?: string + govDistId?: string + children: ReactNode +} diff --git a/packages/ui/components/data-portal/ServiceEditDrawer/schemas.ts b/packages/ui/components/data-portal/ServiceEditDrawer/schemas.ts new file mode 100644 index 00000000000..2e318f6edc8 --- /dev/null +++ b/packages/ui/components/data-portal/ServiceEditDrawer/schemas.ts @@ -0,0 +1,55 @@ +import { z } from 'zod' + +import { prefixedId } from '@weareinreach/api/schemas/idPrefix' + +const FreetextObject = z + .object({ + text: z.string().nullable(), + key: z.string().nullish(), + ns: z.string().nullish(), + crowdinId: z.number().nullish(), + }) + .nullish() + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]) +type Literal = z.infer +type Json = Literal | { [key: string]: Json } | Json[] +const JsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(JsonSchema), z.record(JsonSchema)]) +) + +export const FormSchema = z.object({ + name: FreetextObject, + description: FreetextObject, + services: prefixedId('serviceTag').array(), + attributes: z + .object({ + text: z + .object({ + key: z.string(), + text: z.string(), + ns: z.string(), + }) + .nullable(), + boolean: z.boolean().nullable(), + data: z.any(), + active: z.boolean(), + countryId: z.string().nullable(), + govDistId: z.string().nullable(), + languageId: z.string().nullable(), + category: z.string(), + attributeId: z.string(), + supplementId: z.string(), + }) + .array(), + serviceAreas: z + .object({ + id: prefixedId('serviceArea'), + countries: prefixedId('country').array(), + districts: prefixedId('govDist').array(), + }) + .nullable(), + published: z.boolean(), + deleted: z.boolean(), +}) +export type TFormSchema = z.infer diff --git a/packages/ui/components/data-portal/ServiceEditDrawer/styles.ts b/packages/ui/components/data-portal/ServiceEditDrawer/styles.ts new file mode 100644 index 00000000000..fd0aa692948 --- /dev/null +++ b/packages/ui/components/data-portal/ServiceEditDrawer/styles.ts @@ -0,0 +1,28 @@ +import { createStyles, rem } from '@mantine/core' + +export const useStyles = createStyles((theme) => ({ + drawerContent: { + borderRadius: `${rem(32)} 0 0 0`, + minWidth: '40vw', + }, + drawerBody: { + padding: `${rem(40)} ${rem(32)}`, + '&:not(:only-child)': { + paddingTop: rem(40), + }, + }, + badgeGroup: { + width: '100%', + backgroundColor: theme.fn.lighten(theme.other.colors.secondary.teal, 0.9), + borderRadius: rem(8), + padding: rem(4), + }, + tealText: { + color: theme.other.colors.secondary.teal, + }, + dottedCard: { + border: `${rem(1)} dashed ${theme.other.colors.secondary.teal}`, + borderRadius: rem(16), + padding: rem(20), + }, +})) diff --git a/packages/ui/components/data-portal/ServiceSelect/index.tsx b/packages/ui/components/data-portal/ServiceSelect/index.tsx index 93fe71e8dc3..6a434fa7ff3 100644 --- a/packages/ui/components/data-portal/ServiceSelect/index.tsx +++ b/packages/ui/components/data-portal/ServiceSelect/index.tsx @@ -1,11 +1,10 @@ -import { Box, type BoxProps, createStyles, Drawer, Group, rem, Stack, Text, Title } from '@mantine/core' +import { Box, type BoxProps, createStyles, Drawer, Group, rem, Stack, Title } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import { useTranslation } from 'next-i18next' import { type FieldValues, type UseControllerProps, useFormState } from 'react-hook-form' import { Checkbox } from 'react-hook-form-mantine' import { Breadcrumb } from '~ui/components/core/Breadcrumb' -import { useCustomVariant } from '~ui/hooks/useCustomVariant' import { trpc as api } from '~ui/lib/trpcClient' const useStyles = createStyles((theme) => ({ @@ -38,27 +37,28 @@ export const ServiceSelect = ({ const { data } = api.component.ServiceSelect.useQuery() const { classes } = useStyles() const { t } = useTranslation('services') - const variants = useCustomVariant() const form = useFormState({ control, name }) const serviceGroups = data ? ( - + {data.map((category) => ( - - {t(category.tsKey)} - {category.services.map((service) => ( - - ))} + + {t(category.tsKey)} + + {category.services.map((service) => ( + + ))} + ))} diff --git a/packages/ui/components/sections/Navbar.tsx b/packages/ui/components/sections/Navbar.tsx index a622f3a3245..6734c8faa17 100644 --- a/packages/ui/components/sections/Navbar.tsx +++ b/packages/ui/components/sections/Navbar.tsx @@ -63,11 +63,7 @@ const EditModeBar = () => { const apiUtils = api.useUtils() const { unsaved, saveEvent } = useEditMode() const { t } = useTranslation('common') - const router = useRouter< - | '/org/[slug]/edit' - | '/org/[slug]/[orgLocationId]/edit' - | '/org/[slug]/[orgLocationId]/edit/[orgServiceId]' - >() + const router = useRouter<'/org/[slug]/edit' | '/org/[slug]/[orgLocationId]/edit'>() const { orgLocationId, slug, orgServiceId } = router.query const apiQuery = (() => { @@ -128,9 +124,6 @@ const EditModeBar = () => { case '/org/[slug]/[orgLocationId]/edit': { return '/org/[slug]/[orgLocationId]' } - case '/org/[slug]/[orgLocationId]/edit/[orgServiceId]': { - return '/org/[slug]/[orgLocationId]' - } default: { return router.pathname } @@ -234,10 +227,3 @@ export const Navbar = () => { ) } - -type NavbarProps = { - editMode?: boolean - editModeRef?: { - handleEditSubmit: (handler: () => void) => void - } -} diff --git a/packages/ui/components/sections/ServicesInfo.tsx b/packages/ui/components/sections/ServicesInfo.tsx index 8ac851584b1..8df0044cf27 100644 --- a/packages/ui/components/sections/ServicesInfo.tsx +++ b/packages/ui/components/sections/ServicesInfo.tsx @@ -3,8 +3,12 @@ import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' import { transformer } from '@weareinreach/util/transformer' +import { Link } from '~ui/components/core' import { Badge } from '~ui/components/core/Badge' -import { useCustomVariant, useScreenSize } from '~ui/hooks' +import { ServiceEditDrawer } from '~ui/components/data-portal/ServiceEditDrawer' +import { useCustomVariant } from '~ui/hooks/useCustomVariant' +import { useEditMode } from '~ui/hooks/useEditMode' +import { useScreenSize } from '~ui/hooks/useScreenSize' import { Icon } from '~ui/icon' import { trpc as api } from '~ui/lib/trpcClient' import { ServiceModal } from '~ui/modals/Service' @@ -33,6 +37,7 @@ const useServiceSectionStyles = createStyles((theme) => ({ const ServiceSection = ({ category, services, hideRemoteBadges }: ServiceSectionProps) => { const router = useRouter<'/org/[slug]' | '/org/[slug]/[orgLocationId]'>() + const { isEditMode } = useEditMode() const { slug } = router.isReady ? router.query : { slug: '' } const { data: orgId } = api.organization.getIdFromSlug.useQuery({ slug }, { enabled: router.isReady }) const { t } = useTranslation(orgId?.id ? ['common', 'services', orgId.id] : ['common', 'services']) @@ -54,16 +59,8 @@ const ServiceSection = ({ category, services, hideRemoteBadges }: ServiceSection {services.map((service) => { const serviceName = t(service.tsKey, { ns: orgId?.id, defaultValue: service.defaultText }) - return ( - apiUtils.service.forServiceModal.prefetch(service.id)} - > + const children = ( + <> {service.offersRemote && !hideRemoteBadges ? ( {serviceName} @@ -72,8 +69,32 @@ const ServiceSection = ({ category, services, hideRemoteBadges }: ServiceSection ) : ( {serviceName} )} - + + ) + + return isEditMode ? ( + + + {children} + + + ) : ( + apiUtils.service.forServiceModal.prefetch(service.id)} + > + {children} ) })} diff --git a/packages/ui/hooks/useEditMode.ts b/packages/ui/hooks/useEditMode.ts index 411bd671199..efff401d836 100644 --- a/packages/ui/hooks/useEditMode.ts +++ b/packages/ui/hooks/useEditMode.ts @@ -9,11 +9,7 @@ export const useEditMode = () => { if (!ctx) { throw new Error('useEditMode must be used within a EditModeProvider') } - const editPaths: (typeof router.pathname)[] = [ - '/org/[slug]/edit', - '/org/[slug]/[orgLocationId]/edit', - '/org/[slug]/[orgLocationId]/edit/[orgServiceId]', - ] + const editPaths: (typeof router.pathname)[] = ['/org/[slug]/edit', '/org/[slug]/[orgLocationId]/edit'] const isEditMode = editPaths.includes(router.pathname) diff --git a/packages/ui/hooks/useFreeText.ts b/packages/ui/hooks/useFreeText.ts index 4b1f8468935..b40277143a1 100644 --- a/packages/ui/hooks/useFreeText.ts +++ b/packages/ui/hooks/useFreeText.ts @@ -3,8 +3,16 @@ import { useTranslation } from 'next-i18next' import { type DB } from '@weareinreach/api/prisma/types' +const isNestedFreeText = (item: unknown): item is NestedFreeText => { + if (!item || typeof item !== 'object') return false + if ('tsKey' in item) return true + return false +} + export const getFreeText: GetFreeText = (freeTextRecord, tOptions) => { - const { key: dbKey, tsKey } = freeTextRecord + const { key: dbKey, tsKey } = isNestedFreeText(freeTextRecord) + ? freeTextRecord + : { key: freeTextRecord.key, tsKey: { text: freeTextRecord.text } } const deconstructedKey = dbKey.split('.') const ns = deconstructedKey[0] if (!deconstructedKey.length || !ns) throw new Error('Invalid key') @@ -20,13 +28,14 @@ export const useFreeText: UseFreeText = (freeTextRecord, tOptions) => { return t(key, options) } -export interface UseFreeTextProps extends Pick, Partial> { +export interface NestedFreeText extends Pick, Partial> { tsKey: Pick & Partial> } +export type TranslationKeyRecord = Pick export type GetFreeText = ( - freeTextRecord: UseFreeTextProps, + freeTextRecord: NestedFreeText | TranslationKeyRecord, tOptions?: TOptions ) => { key: string; options: TOptions } -export type UseFreeText = (freeTextRecord: UseFreeTextProps, tOptions?: TOptions) => string +export type UseFreeText = (freeTextRecord: NestedFreeText, tOptions?: TOptions) => string diff --git a/packages/ui/mockData/fieldOpt.ts b/packages/ui/mockData/fieldOpt.ts index ae4c279f48f..7ac50baa08e 100644 --- a/packages/ui/mockData/fieldOpt.ts +++ b/packages/ui/mockData/fieldOpt.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { type ApiOutput } from '@weareinreach/api' +import { type $Enums } from '@weareinreach/db' import { getTRPCMock, type MockAPIHandler, type MockHandlerObject } from '~ui/lib/getTrpcMock' const queryAttributeCategories: MockAPIHandler<'fieldOpt', 'attributeCategories'> = async (query) => { @@ -12,13 +13,32 @@ const queryAttributeCategories: MockAPIHandler<'fieldOpt', 'attributeCategories' } const queryAttributesByCategory: MockAPIHandler<'fieldOpt', 'attributesByCategory'> = async (query) => { - const attributesByCategory = (await import('./json/fieldOpt.attributesByCategory.json')).default - if (typeof query === 'string' || Array.isArray(query)) { - return attributesByCategory.filter(({ categoryName }) => - Array.isArray(query) ? query.includes(categoryName) : query === categoryName - ) as ApiOutput['fieldOpt']['attributesByCategory'] + const attributesByCategory = (await import('./json/fieldOpt.attributesByCategory.json')) + .default as ApiOutput['fieldOpt']['attributesByCategory'] + + if (query?.categoryName || query?.canAttachTo?.length) { + const canAttachSet = new Set(query.canAttachTo) + const catNameSet = new Set(Array.isArray(query.categoryName) ? query.categoryName : [query.categoryName]) + return attributesByCategory.filter(({ canAttachTo, categoryName }) => { + let match = false + + if (query.canAttachTo?.length) { + for (const item of canAttachTo) { + if (canAttachSet.has(item as $Enums.AttributeAttachment)) { + match = true + break + } + } + } + if (query.categoryName) { + match = catNameSet.has(categoryName) + } + + return match + }) } - return attributesByCategory as ApiOutput['fieldOpt']['attributesByCategory'] + + return attributesByCategory } const queryLanguages: MockAPIHandler<'fieldOpt', 'languages'> = async (query) => { @@ -214,6 +234,17 @@ export const fieldOpt = { } }, }), + ccaMap: getTRPCMock({ + path: ['fieldOpt', 'ccaMap'], + response: async ({ activeForOrgs }) => { + const { default: data } = await import('./json/fieldOpt.ccaMap.json') + const dataToUse = activeForOrgs ? data.true : data.false + return { + byId: new Map(Object.entries(dataToUse.byId)), + byCCA: new Map(Object.entries(dataToUse.byCCA)), + } + }, + }), } satisfies MockHandlerObject<'fieldOpt'> export const allFieldOptHandlers = Object.values(fieldOpt) diff --git a/packages/ui/mockData/json/fieldOpt.attributesByCategory.json b/packages/ui/mockData/json/fieldOpt.attributesByCategory.json index e15c5fabd68..b77f210d665 100644 --- a/packages/ui/mockData/json/fieldOpt.attributesByCategory.json +++ b/packages/ui/mockData/json/fieldOpt.attributesByCategory.json @@ -1 +1 @@ -[{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV3YJ2AWADHVKG79BQ0","attributeName":"at-capacity","attributeKey":"additional.at-capacity","attributeNs":"attribute","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV4D5ZHFMAE7852GB4P","attributeName":"geo-near-public-transit","attributeKey":"additional.geo-near-public-transit","attributeNs":"attribute","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV48VQJBMFA05QCBBV9","attributeName":"geo-public-transit-description","attributeKey":"additional.geo-public-transit-description","attributeNs":"attribute","badgeRender":"ATTRIBUTE","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV3BADK80TG0DXXFPMM","attributeName":"has-confidentiality-policy","attributeKey":"additional.has-confidentiality-policy","attributeNs":"attribute","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV5Q7XN2ZNTYFR1AD3M","attributeName":"offers-remote-services","attributeKey":"additional.offers-remote-services","attributeNs":"attribute","icon":"carbon:globe","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV4TM7H5V6FHWA7S9JK","attributeName":"time-walk-in","attributeKey":"additional.time-walk-in","attributeNs":"attribute","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV5FYXQNGTPAQB7G2TF","attributeName":"wheelchair-accessible","attributeKey":"additional.wheelchair-accessible","attributeNs":"attribute","interpolationValues":{"true":"Accessible","false":"Not Accessible"},"icon":"carbon:accessibility","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":true,"requireData":false},{"categoryId":"attc_01GYSVX1N9T91BJYSHRDPCHJBS","categoryName":"alerts","categoryDisplay":"Alerts","attributeId":"attr_01GYSVX1NAMR6RDV6M69H4KN3T","attributeName":"info","attributeKey":"alerts.info","attributeNs":"attribute","icon":"carbon:information-filled","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GYSVX1N9T91BJYSHRDPCHJBS","categoryName":"alerts","categoryDisplay":"Alerts","attributeId":"attr_01GYSVX1NAKP7C6JKJ342ZM35M","attributeName":"warn","attributeKey":"alerts.warn","attributeNs":"attribute","icon":"carbon:warning-filled","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVFKNMYPN8F86M0H576","categoryName":"cost","categoryDisplay":"Cost","attributeId":"attr_01GW2HHFVGWKWB53HWAAHQ9AAZ","attributeName":"cost-fees","attributeKey":"cost.cost-fees","attributeNs":"attribute","icon":"carbon:piggy-bank","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"numMinMaxOrRange","dataSchema":{"anyOf":[{"type":"object","required":["min"],"properties":{"min":{"type":"number"}}},{"type":"object","required":["max"],"properties":{"max":{"type":"number"}}},{"type":"object","required":["min","max"],"properties":{"max":{"type":"number"},"min":{"type":"number"}}}],"$schema":"http://json-schema.org/draft-07/schema#"}},{"categoryId":"attc_01GW2HHFVFKNMYPN8F86M0H576","categoryName":"cost","categoryDisplay":"Cost","attributeId":"attr_01GW2HHFVGDTNW9PDQNXK6TF1T","attributeName":"cost-free","attributeKey":"cost.cost-free","attributeNs":"attribute","icon":"carbon:piggy-bank","badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01GW2HHFVN72D7XEBZZJXCJQXQ","attributeName":"bipoc-comm","attributeKey":"srvfocus.bipoc-comm","attributeNs":"attribute","icon":"️‍️‍✊🏿","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01H6P951P0V3CR807P8KRH82S1","attributeName":"elders","attributeKey":"crisis-support-community.elders","attributeNs":"attribute","icon":"🌳","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01H6P8T277D0C8HFQA6N09FJWD","attributeName":"general-lgbtq","attributeKey":"crisis-support-community.general-lgbtq","attributeNs":"attribute","icon":"🏳️‍🌈","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01GW2HHFVQCZPA3Z5GW6J3MQHW","attributeName":"lgbtq-youth-focus","attributeKey":"srvfocus.lgbtq-youth-focus","attributeNs":"attribute","icon":"🌱","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01GW2HHFVPSYBCYF37B44WP6CZ","attributeName":"trans-comm","attributeKey":"srvfocus.trans-comm","attributeNs":"attribute","icon":"🏳️‍⚧️","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVGSAZXGR4JAVHEK6ZC","attributeName":"elig-age","attributeKey":"eligibility.elig-age","attributeNs":"attribute","interpolationValues":{"max":"Under{{max}}","min":"{{min}} and older","range":"{{min}} -{{max}}"},"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"numMinMaxOrRange","dataSchema":{"anyOf":[{"type":"object","required":["min"],"properties":{"min":{"type":"number"}}},{"type":"object","required":["max"],"properties":{"max":{"type":"number"}}},{"type":"object","required":["min","max"],"properties":{"max":{"type":"number"},"min":{"type":"number"}}}],"$schema":"http://json-schema.org/draft-07/schema#"}},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVJDKVF1HV7559CNZCY","attributeName":"other-describe","attributeKey":"eligibility.other-describe","attributeNs":"attribute","badgeRender":"LIST","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVH9DPBZ968VXGE50E7","attributeName":"req-medical-insurance","attributeKey":"eligibility.req-medical-insurance","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVHZ599M48CMSPGDCSC","attributeName":"req-photo-id","attributeKey":"eligibility.req-photo-id","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVH0GQK0GAJR5D952V3","attributeName":"req-proof-of-age","attributeKey":"eligibility.req-proof-of-age","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVHEVX4PMNN077ASQMG","attributeName":"req-proof-of-income","attributeKey":"eligibility.req-proof-of-income","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVHGMVCAY1G5BWF1PFB","attributeName":"req-proof-of-residence","attributeKey":"eligibility.req-proof-of-residence","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVJH8MADHYTHBV54CER","attributeName":"req-referral","attributeKey":"eligibility.req-referral","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVGJ5GD2WHNJDPSFNRW","attributeName":"time-appointment-required","attributeKey":"eligibility.time-appointment-required","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVJQQ68XGSBXM976BDF","categoryName":"languages","categoryDisplay":"Languages","attributeId":"attr_01GW2HHFVJGDDWTR5D0C8BY357","attributeName":"all-languages-by-interpreter","attributeKey":"lang.all-languages-by-interpreter","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVJQQ68XGSBXM976BDF","categoryName":"languages","categoryDisplay":"Languages","attributeId":"attr_01GW2HHFVJF09GXY5N5CKMSANJ","attributeName":"american-sign-language","attributeKey":"lang.american-sign-language","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVJQQ68XGSBXM976BDF","categoryName":"languages","categoryDisplay":"Languages","attributeId":"attr_01GW2HHFVJ8K180CNX339BTXM2","attributeName":"lang-offered","attributeKey":"lang.lang-offered","attributeNs":"attribute","badgeRender":"LIST","requireText":false,"requireLanguage":true,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVRH531R2HAV8DMDZSC","attributeName":"corp-law-firm","attributeKey":"userlawpractice.corp-law-firm","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVSE2074QZJ4SKEW74J","attributeName":"law-other","attributeKey":"userlawpractice.law-other","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"otherDescribe","dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["other"],"properties":{"other":{"type":"string"}}}},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVRS8XEJ3TJBBEQJ707","attributeName":"law-school-clinic","attributeKey":"userlawpractice.law-school-clinic","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVRFPRQCQHNJA6BM3XP","attributeName":"legal-nonprofit","attributeKey":"userlawpractice.legal-nonprofit","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVNPKMHYK12DDRVC1VJ","attributeName":"bipoc-led","attributeKey":"orgleader.bipoc-led","attributeNs":"attribute","icon":"🤎","iconBg":"#F1DD7F","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVN3JX2J7REFFT5NAMS","attributeName":"black-led","attributeKey":"orgleader.black-led","attributeNs":"attribute","icon":"️‍️‍✊🏿","iconBg":"#C77E54","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVNHMF72WHVKRF6W4TA","attributeName":"immigrant-led","attributeKey":"orgleader.immigrant-led","attributeNs":"attribute","icon":"️‍️‍🌎","iconBg":"#79ADD7","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVN3RYX9JMXDZSQZM70","attributeName":"trans-led","attributeKey":"orgleader.trans-led","attributeNs":"attribute","icon":"️‍🏳️‍⚧️","iconBg":"#705890","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVKFM4TDY4QRK4AR2ZW","attributeName":"accessemail","attributeKey":"serviceaccess.accessemail","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"accessInstructions","dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["access_type","instructions"],"properties":{"access_type":{"enum":["email","file","link","location","other","phone"],"type":"string"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}}}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVKMRHFD8SMDAZM3SSM","attributeName":"accessfile","attributeKey":"serviceaccess.accessfile","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"accessInstructions","dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["access_type","instructions"],"properties":{"access_type":{"enum":["email","file","link","location","other","phone"],"type":"string"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}}}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMYXMS8ARA3GE7HZFD","attributeName":"accesslink","attributeKey":"serviceaccess.accesslink","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"accessInstructions","dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["access_type","instructions"],"properties":{"access_type":{"enum":["email","file","link","location","other","phone"],"type":"string"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}}}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMH6AE94EXN7T5A87C","attributeName":"accesslocation","attributeKey":"serviceaccess.accesslocation","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"accessInstructions","dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["access_type","instructions"],"properties":{"access_type":{"enum":["email","file","link","location","other","phone"],"type":"string"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}}}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMKTFWCKBVVFJ5GMY0","attributeName":"accessphone","attributeKey":"serviceaccess.accessphone","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"accessInstructions","dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["access_type","instructions"],"properties":{"access_type":{"enum":["email","file","link","location","other","phone"],"type":"string"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}}}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMSX7T1WDNZ5QEHKWT","attributeName":"accesspublictransit","attributeKey":"serviceaccess.accesspublictransit","attributeNs":"attribute","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMMF19AX2KPBTMV6P3","attributeName":"accesstext","attributeKey":"serviceaccess.accesstext","attributeNs":"attribute","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPCVX8F3B7M30ZJEHW","attributeName":"asylum-seekers","attributeKey":"srvfocus.asylum-seekers","attributeNs":"attribute","icon":"️‍️‍🌎","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVN72D7XEBZZJXCJQXQ","attributeName":"bipoc-comm","attributeKey":"srvfocus.bipoc-comm","attributeNs":"attribute","icon":"️‍️‍✊🏿","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQ7SYGD3KM8WP9X50B","attributeName":"gender-nc","attributeKey":"srvfocus.gender-nc","attributeNs":"attribute","icon":"🏳️‍⚧️","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVRMQFJ9AMA633SQQGV","attributeName":"hiv-comm","attributeKey":"srvfocus.hiv-comm","attributeNs":"attribute","icon":"💛","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPTK9555WHJHDBDA2J","attributeName":"immigrant-comm","attributeKey":"srvfocus.immigrant-comm","attributeNs":"attribute","icon":"️‍️‍🌎","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQCZPA3Z5GW6J3MQHW","attributeName":"lgbtq-youth-focus","attributeKey":"srvfocus.lgbtq-youth-focus","attributeNs":"attribute","icon":"🌱","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPJERY0GS9D7F56A23","attributeName":"resettled-refugees","attributeKey":"srvfocus.resettled-refugees","attributeNs":"attribute","icon":"️‍️‍🌎","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQ8AGBKBBZJWTHNP2F","attributeName":"spanish-speakers","attributeKey":"srvfocus.spanish-speakers","attributeNs":"attribute","icon":"🗣️","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPSYBCYF37B44WP6CZ","attributeName":"trans-comm","attributeKey":"srvfocus.trans-comm","attributeNs":"attribute","icon":"🏳️‍⚧️","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQX4M8DY1FSAYSJSSK","attributeName":"trans-fem","attributeKey":"srvfocus.trans-fem","attributeNs":"attribute","icon":"🏳️‍⚧️","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQEFWW42MBAD64BWXZ","attributeName":"trans-masc","attributeKey":"srvfocus.trans-masc","attributeNs":"attribute","icon":"🏳️‍⚧️","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQVEGH6W3A2ANH1QZE","attributeName":"trans-youth-focus","attributeKey":"srvfocus.trans-youth-focus","attributeNs":"attribute","icon":"🌱","badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TK83N5E52PPP828SD88KP8","attributeName":"userserviceprovider.case-mananger","attributeKey":"userserviceprovider.case-mananger","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01GW2HHFVTTZ83PZR61M37R8R7","attributeName":"userserviceprovider.community-org","attributeKey":"userserviceprovider.community-org","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01GW2HHFVSPXWJJPFG9DKXESEK","attributeName":"userserviceprovider.healthcare","attributeKey":"userserviceprovider.healthcare","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM092CFVG6H0MR148AVAP7","attributeName":"userserviceprovider.lawyer","attributeKey":"userserviceprovider.lawyer","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM0AJHVK8TSR8JNFANFNZ7","attributeName":"userserviceprovider.other","attributeKey":"userserviceprovider.other","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM09EG0G84NXH40G5TESB5","attributeName":"userserviceprovider.paralegal","attributeKey":"userserviceprovider.paralegal","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM09RAK024ZDZQ6FSY0TXB","attributeName":"userserviceprovider.social-worker","attributeKey":"userserviceprovider.social-worker","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01GW2HHFVTN6MSCMBW740Y7HN1","attributeName":"userserviceprovider.student-club","attributeKey":"userserviceprovider.student-club","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM0A19DD6S97DNH76ZVP40","attributeName":"userserviceprovider.teacher","attributeKey":"userserviceprovider.teacher","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM0AA4CZXJJHMXHE1PHMVV","attributeName":"userserviceprovider.therapist-counselor","attributeKey":"userserviceprovider.therapist-counselor","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false},{"categoryId":"attc_01GW2HHFVKM2PSHFWVFM0TWX1P","categoryName":"system","categoryDisplay":"System","attributeId":"attr_01GW2HHFVK8KPRGKYFSSM5ECPQ","attributeName":"incompatible-info","attributeKey":"sys.incompatible-info","attributeNs":"attribute","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"incompatibleData","dataSchema":{"type":"array","items":{"type":"object","additionalProperties":{}},"$schema":"http://json-schema.org/draft-07/schema#"}}] \ No newline at end of file +[{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV3YJ2AWADHVKG79BQ0","attributeName":"at-capacity","attributeKey":"additional.at-capacity","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV4D5ZHFMAE7852GB4P","attributeName":"geo-near-public-transit","attributeKey":"additional.geo-near-public-transit","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV48VQJBMFA05QCBBV9","attributeName":"geo-public-transit-description","attributeKey":"additional.geo-public-transit-description","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"ATTRIBUTE","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV3BADK80TG0DXXFPMM","attributeName":"has-confidentiality-policy","attributeKey":"additional.has-confidentiality-policy","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV5Q7XN2ZNTYFR1AD3M","attributeName":"offers-remote-services","attributeKey":"additional.offers-remote-services","attributeNs":"attribute","interpolationValues":null,"icon":"carbon:globe","iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV4TM7H5V6FHWA7S9JK","attributeName":"time-walk-in","attributeKey":"additional.time-walk-in","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFV3DJ380F351SKB0B74","categoryName":"additional-information","categoryDisplay":"Additional Information","attributeId":"attr_01GW2HHFV5FYXQNGTPAQB7G2TF","attributeName":"wheelchair-accessible","attributeKey":"additional.wheelchair-accessible","attributeNs":"attribute","interpolationValues":{"true":"Accessible","false":"Not Accessible"},"icon":"carbon:accessibility","iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":true,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GYSVX1N9T91BJYSHRDPCHJBS","categoryName":"alerts","categoryDisplay":"Alerts","attributeId":"attr_01GYSVX1NAMR6RDV6M69H4KN3T","attributeName":"info","attributeKey":"alerts.info","attributeNs":"attribute","interpolationValues":null,"icon":"carbon:information-filled","iconBg":null,"badgeRender":null,"requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GYSVX1N9T91BJYSHRDPCHJBS","categoryName":"alerts","categoryDisplay":"Alerts","attributeId":"attr_01GYSVX1NAKP7C6JKJ342ZM35M","attributeName":"warn","attributeKey":"alerts.warn","attributeNs":"attribute","interpolationValues":null,"icon":"carbon:warning-filled","iconBg":null,"badgeRender":null,"requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVFKNMYPN8F86M0H576","categoryName":"cost","categoryDisplay":"Cost","attributeId":"attr_01GW2HHFVGWKWB53HWAAHQ9AAZ","attributeName":"cost-fees","attributeKey":"cost.cost-fees","attributeNs":"attribute","interpolationValues":null,"icon":"carbon:piggy-bank","iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"numMinMaxOrRange","canAttachTo":["SERVICE"],"formSchema":[{"key":"min","label":"Min","name":"min","type":"number"},{"key":"max","label":"Max","name":"max","type":"number"}],"dataSchema":{"anyOf":[{"type":"object","required":["min","max"],"properties":{"max":{"not":{}},"min":{"type":"number"}},"additionalProperties":false},{"type":"object","required":["min","max"],"properties":{"max":{"type":"number"},"min":{"not":{}}},"additionalProperties":false},{"type":"object","required":["min","max"],"properties":{"max":{"type":"number"},"min":{"type":"number"}},"additionalProperties":false}],"$schema":"http://json-schema.org/draft-07/schema#"}},{"categoryId":"attc_01GW2HHFVFKNMYPN8F86M0H576","categoryName":"cost","categoryDisplay":"Cost","attributeId":"attr_01GW2HHFVGDTNW9PDQNXK6TF1T","attributeName":"cost-free","attributeKey":"cost.cost-free","attributeNs":"attribute","interpolationValues":null,"icon":"carbon:piggy-bank","iconBg":null,"badgeRender":"ATTRIBUTE","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01GW2HHFVN72D7XEBZZJXCJQXQ","attributeName":"bipoc-comm","attributeKey":"srvfocus.bipoc-comm","attributeNs":"attribute","interpolationValues":null,"icon":"️‍️‍✊🏿","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01H6P951P0V3CR807P8KRH82S1","attributeName":"elders","attributeKey":"crisis-support-community.elders","attributeNs":"attribute","interpolationValues":null,"icon":"🌳","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01H6P8T277D0C8HFQA6N09FJWD","attributeName":"general-lgbtq","attributeKey":"crisis-support-community.general-lgbtq","attributeNs":"attribute","interpolationValues":null,"icon":"🏳️‍🌈","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01GW2HHFVQCZPA3Z5GW6J3MQHW","attributeName":"lgbtq-youth-focus","attributeKey":"srvfocus.lgbtq-youth-focus","attributeNs":"attribute","interpolationValues":null,"icon":"🌱","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01H6P8SSY4C141YH7BAC1RW7KJ","categoryName":"crisis-support-community","categoryDisplay":"Crisis Support Community","attributeId":"attr_01GW2HHFVPSYBCYF37B44WP6CZ","attributeName":"trans-comm","attributeKey":"srvfocus.trans-comm","attributeNs":"attribute","interpolationValues":null,"icon":"🏳️‍⚧️","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVGSAZXGR4JAVHEK6ZC","attributeName":"elig-age","attributeKey":"eligibility.elig-age","attributeNs":"attribute","interpolationValues":{"max":"Under{{max}}","min":"{{min}} and older","range":"{{min}} -{{max}}"},"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"numMinMaxOrRange","canAttachTo":["SERVICE"],"formSchema":[{"key":"min","label":"Min","name":"min","type":"number"},{"key":"max","label":"Max","name":"max","type":"number"}],"dataSchema":{"anyOf":[{"type":"object","required":["min","max"],"properties":{"max":{"not":{}},"min":{"type":"number"}},"additionalProperties":false},{"type":"object","required":["min","max"],"properties":{"max":{"type":"number"},"min":{"not":{}}},"additionalProperties":false},{"type":"object","required":["min","max"],"properties":{"max":{"type":"number"},"min":{"type":"number"}},"additionalProperties":false}],"$schema":"http://json-schema.org/draft-07/schema#"}},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVJDKVF1HV7559CNZCY","attributeName":"other-describe","attributeKey":"eligibility.other-describe","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVH9DPBZ968VXGE50E7","attributeName":"req-medical-insurance","attributeKey":"eligibility.req-medical-insurance","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVHZ599M48CMSPGDCSC","attributeName":"req-photo-id","attributeKey":"eligibility.req-photo-id","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVH0GQK0GAJR5D952V3","attributeName":"req-proof-of-age","attributeKey":"eligibility.req-proof-of-age","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVHEVX4PMNN077ASQMG","attributeName":"req-proof-of-income","attributeKey":"eligibility.req-proof-of-income","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVHGMVCAY1G5BWF1PFB","attributeName":"req-proof-of-residence","attributeKey":"eligibility.req-proof-of-residence","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVJH8MADHYTHBV54CER","attributeName":"req-referral","attributeKey":"eligibility.req-referral","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVGHPW1Y72SA8377623","categoryName":"eligibility-requirements","categoryDisplay":"Eligibility Requirements","attributeId":"attr_01GW2HHFVGJ5GD2WHNJDPSFNRW","attributeName":"time-appointment-required","attributeKey":"eligibility.time-appointment-required","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVJQQ68XGSBXM976BDF","categoryName":"languages","categoryDisplay":"Languages","attributeId":"attr_01GW2HHFVJGDDWTR5D0C8BY357","attributeName":"all-languages-by-interpreter","attributeKey":"lang.all-languages-by-interpreter","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVJQQ68XGSBXM976BDF","categoryName":"languages","categoryDisplay":"Languages","attributeId":"attr_01GW2HHFVJF09GXY5N5CKMSANJ","attributeName":"american-sign-language","attributeKey":"lang.american-sign-language","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVJQQ68XGSBXM976BDF","categoryName":"languages","categoryDisplay":"Languages","attributeId":"attr_01GW2HHFVJ8K180CNX339BTXM2","attributeName":"lang-offered","attributeKey":"lang.lang-offered","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":"LIST","requireText":false,"requireLanguage":true,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE","ORGANIZATION","LOCATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVRH531R2HAV8DMDZSC","attributeName":"corp-law-firm","attributeKey":"userlawpractice.corp-law-firm","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVSE2074QZJ4SKEW74J","attributeName":"law-other","attributeKey":"userlawpractice.law-other","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"otherDescribe","canAttachTo":["USER"],"formSchema":[{"key":"other","label":"Other","name":"other","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","required":["other"],"properties":{"other":{"type":"string"}}}},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVRS8XEJ3TJBBEQJ707","attributeName":"law-school-clinic","attributeKey":"userlawpractice.law-school-clinic","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVRSN3W3GYZZ43WCW24","categoryName":"law-practice-options","categoryDisplay":"Law Practice Options","attributeId":"attr_01GW2HHFVRFPRQCQHNJA6BM3XP","attributeName":"legal-nonprofit","attributeKey":"userlawpractice.legal-nonprofit","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVNPKMHYK12DDRVC1VJ","attributeName":"bipoc-led","attributeKey":"orgleader.bipoc-led","attributeNs":"attribute","interpolationValues":null,"icon":"🤎","iconBg":"#F1DD7F","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVN3JX2J7REFFT5NAMS","attributeName":"black-led","attributeKey":"orgleader.black-led","attributeNs":"attribute","interpolationValues":null,"icon":"️‍️‍✊🏿","iconBg":"#C77E54","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVNHMF72WHVKRF6W4TA","attributeName":"immigrant-led","attributeKey":"orgleader.immigrant-led","attributeNs":"attribute","interpolationValues":null,"icon":"️‍️‍🌎","iconBg":"#79ADD7","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVMNHV2ZS5875JWCRJ7","categoryName":"organization-leadership","categoryDisplay":"Organization Leadership","attributeId":"attr_01GW2HHFVN3RYX9JMXDZSQZM70","attributeName":"trans-led","attributeKey":"orgleader.trans-led","attributeNs":"attribute","interpolationValues":null,"icon":"️‍🏳️‍⚧️","iconBg":"#705890","badgeRender":"LEADER","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVKFM4TDY4QRK4AR2ZW","attributeName":"accessemail","attributeKey":"serviceaccess.accessemail","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"access-instruction-email","canAttachTo":["SERVICE"],"formSchema":[{"key":"access_value","label":"Email Address","name":"access_value","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"access_type":{"type":"string","const":"email","default":"email"},"access_value":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}]},"instructions":{"type":"string"}},"additionalProperties":false}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVKMRHFD8SMDAZM3SSM","attributeName":"accessfile","attributeKey":"serviceaccess.accessfile","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"access-instruction-file","canAttachTo":["SERVICE"],"formSchema":[{"key":"access_value","label":"File URL","name":"access_value","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"access_type":{"type":"string","const":"file","default":"file"},"access_value":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"instructions":{"type":"string"}},"additionalProperties":false}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMYXMS8ARA3GE7HZFD","attributeName":"accesslink","attributeKey":"serviceaccess.accesslink","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"access-instruction-link","canAttachTo":["SERVICE"],"formSchema":[{"key":"access_value","label":"Link URL","name":"access_value","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"access_type":{"type":"string","const":"link","default":"link"},"access_value":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"instructions":{"type":"string"}},"additionalProperties":false}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMH6AE94EXN7T5A87C","attributeName":"accesslocation","attributeKey":"serviceaccess.accesslocation","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"access-instruction-location","canAttachTo":["SERVICE"],"formSchema":[{"key":"access_value","label":"Location","name":"access_value","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"access_type":{"type":"string","const":"location","default":"location"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}},"additionalProperties":false}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMKTFWCKBVVFJ5GMY0","attributeName":"accessphone","attributeKey":"serviceaccess.accessphone","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"access-instruction-phone","canAttachTo":["SERVICE"],"formSchema":[{"key":"access_value","label":"Phone Number","name":"access_value","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"access_type":{"type":"string","const":"phone","default":"phone"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}},"additionalProperties":false}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMSX7T1WDNZ5QEHKWT","attributeName":"accesspublictransit","attributeKey":"serviceaccess.accesspublictransit","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":"access-instruction-publicTransport","canAttachTo":["SERVICE"],"formSchema":[{"key":"access_value","label":"Public Transport Details","name":"access_value","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"access_type":{"type":"string","const":"publicTransit","default":"publicTransit"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}},"additionalProperties":false}},{"categoryId":"attc_01GW2HHFVKAMMGPD71H90XRJ38","categoryName":"service-access-instructions","categoryDisplay":"Service Access Instructions","attributeId":"attr_01GW2HHFVMMF19AX2KPBTMV6P3","attributeName":"accesstext","attributeKey":"serviceaccess.accesstext","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":"access-instruction-other","canAttachTo":["SERVICE"],"formSchema":[{"key":"access_value","label":"Other","name":"access_value","type":"text"}],"dataSchema":{"type":"object","$schema":"http://json-schema.org/draft-07/schema#","properties":{"access_type":{"type":"string","const":"other","default":"other"},"access_value":{"type":["string","null"]},"instructions":{"type":"string"}},"additionalProperties":false}},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPCVX8F3B7M30ZJEHW","attributeName":"asylum-seekers","attributeKey":"srvfocus.asylum-seekers","attributeNs":"attribute","interpolationValues":null,"icon":"️‍️‍🌎","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVN72D7XEBZZJXCJQXQ","attributeName":"bipoc-comm","attributeKey":"srvfocus.bipoc-comm","attributeNs":"attribute","interpolationValues":null,"icon":"️‍️‍✊🏿","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQ7SYGD3KM8WP9X50B","attributeName":"gender-nc","attributeKey":"srvfocus.gender-nc","attributeNs":"attribute","interpolationValues":null,"icon":"🏳️‍⚧️","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVRMQFJ9AMA633SQQGV","attributeName":"hiv-comm","attributeKey":"srvfocus.hiv-comm","attributeNs":"attribute","interpolationValues":null,"icon":"💛","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPTK9555WHJHDBDA2J","attributeName":"immigrant-comm","attributeKey":"srvfocus.immigrant-comm","attributeNs":"attribute","interpolationValues":null,"icon":"️‍️‍🌎","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQCZPA3Z5GW6J3MQHW","attributeName":"lgbtq-youth-focus","attributeKey":"srvfocus.lgbtq-youth-focus","attributeNs":"attribute","interpolationValues":null,"icon":"🌱","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPJERY0GS9D7F56A23","attributeName":"resettled-refugees","attributeKey":"srvfocus.resettled-refugees","attributeNs":"attribute","interpolationValues":null,"icon":"️‍️‍🌎","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQ8AGBKBBZJWTHNP2F","attributeName":"spanish-speakers","attributeKey":"srvfocus.spanish-speakers","attributeNs":"attribute","interpolationValues":null,"icon":"🗣️","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVPSYBCYF37B44WP6CZ","attributeName":"trans-comm","attributeKey":"srvfocus.trans-comm","attributeNs":"attribute","interpolationValues":null,"icon":"🏳️‍⚧️","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQX4M8DY1FSAYSJSSK","attributeName":"trans-fem","attributeKey":"srvfocus.trans-fem","attributeNs":"attribute","interpolationValues":null,"icon":"🏳️‍⚧️","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQEFWW42MBAD64BWXZ","attributeName":"trans-masc","attributeKey":"srvfocus.trans-masc","attributeNs":"attribute","interpolationValues":null,"icon":"🏳️‍⚧️","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVNXMNJNV47BF2BPM1R","categoryName":"service-focus","categoryDisplay":"Service Focus","attributeId":"attr_01GW2HHFVQVEGH6W3A2ANH1QZE","attributeName":"trans-youth-focus","attributeKey":"srvfocus.trans-youth-focus","attributeNs":"attribute","interpolationValues":null,"icon":"🌱","iconBg":null,"badgeRender":"COMMUNITY","requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["LOCATION","ORGANIZATION","SERVICE"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TK83N5E52PPP828SD88KP8","attributeName":"userserviceprovider.case-mananger","attributeKey":"userserviceprovider.case-mananger","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01GW2HHFVTTZ83PZR61M37R8R7","attributeName":"userserviceprovider.community-org","attributeKey":"userserviceprovider.community-org","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01GW2HHFVSPXWJJPFG9DKXESEK","attributeName":"userserviceprovider.healthcare","attributeKey":"userserviceprovider.healthcare","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM092CFVG6H0MR148AVAP7","attributeName":"userserviceprovider.lawyer","attributeKey":"userserviceprovider.lawyer","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM0AJHVK8TSR8JNFANFNZ7","attributeName":"userserviceprovider.other","attributeKey":"userserviceprovider.other","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM09EG0G84NXH40G5TESB5","attributeName":"userserviceprovider.paralegal","attributeKey":"userserviceprovider.paralegal","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM09RAK024ZDZQ6FSY0TXB","attributeName":"userserviceprovider.social-worker","attributeKey":"userserviceprovider.social-worker","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01GW2HHFVTN6MSCMBW740Y7HN1","attributeName":"userserviceprovider.student-club","attributeKey":"userserviceprovider.student-club","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM0A19DD6S97DNH76ZVP40","attributeName":"userserviceprovider.teacher","attributeKey":"userserviceprovider.teacher","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVSQWE2Y2RF3DT2VEYX","categoryName":"service-provider-options","categoryDisplay":"Service Provider Options","attributeId":"attr_01H2TM0AA4CZXJJHMXHE1PHMVV","attributeName":"userserviceprovider.therapist-counselor","attributeKey":"userserviceprovider.therapist-counselor","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["USER"],"formSchema":null,"dataSchema":null},{"categoryId":"attc_01GW2HHFVKM2PSHFWVFM0TWX1P","categoryName":"system","categoryDisplay":"System","attributeId":"attr_01GW2HHFVK8KPRGKYFSSM5ECPQ","attributeName":"incompatible-info","attributeKey":"sys.incompatible-info","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":false,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":true,"dataSchemaName":"incompatibleData","canAttachTo":["LOCATION","ORGANIZATION","SERVICE","USER"],"formSchema":[{"key":"incompatible","label":"Incompatible","name":"incompatible","type":"text"}],"dataSchema":{"type":"array","items":{"type":"object","additionalProperties":{}},"$schema":"http://json-schema.org/draft-07/schema#"}},{"categoryId":"attc_01HNG5BPYJADWX4YFVNENS3TRD","categoryName":"target-population","categoryDisplay":"Target Population","attributeId":"attr_01HNG5GDC5MXW30F32FWJNJ98C","attributeName":"tpop-other","attributeKey":"tpop.other","attributeNs":"attribute","interpolationValues":null,"icon":null,"iconBg":null,"badgeRender":null,"requireText":true,"requireLanguage":false,"requireGeo":false,"requireBoolean":false,"requireData":false,"dataSchemaName":null,"canAttachTo":["SERVICE"],"formSchema":null,"dataSchema":null}] \ No newline at end of file diff --git a/packages/ui/mockData/json/fieldOpt.ccaMap.json b/packages/ui/mockData/json/fieldOpt.ccaMap.json new file mode 100644 index 00000000000..91daec8f6de --- /dev/null +++ b/packages/ui/mockData/json/fieldOpt.ccaMap.json @@ -0,0 +1 @@ +{"true":{"byId":{"ctry_01GW2HHDKBRDF1DMR5DA9DAT7K":"PW","ctry_01GW2HHDK67GZQVGA3NZ8PE5SS":"AS","ctry_01GW2HHDKCRS9KW4FG2WR2GG06":"UM","ctry_01GW2HHDKFJ4Q7PBTTN4GSMPV0":"MP","ctry_01GW2HHDK8HTCM0MWQXBJRXEYB":"MH","ctry_01GW2HHDK9M26M80SG63T21SVH":"US","ctry_01GW2HHDKB9DG2T2YZM5MFFVX9":"MX","ctry_01GW2HHDKAWXWYHAAESAA5HH94":"CA","ctry_01GW2HHDK9DG12Y7RQMVEE5XSQ":"VI","ctry_01GW2HHDKGZ2XQ8Q9D8GX564MJ":"GU","ctry_01GW2HHDK7PACTC9GJ2XBMVPKY":"PR"},"byCCA":{"PW":"ctry_01GW2HHDKBRDF1DMR5DA9DAT7K","AS":"ctry_01GW2HHDK67GZQVGA3NZ8PE5SS","UM":"ctry_01GW2HHDKCRS9KW4FG2WR2GG06","MP":"ctry_01GW2HHDKFJ4Q7PBTTN4GSMPV0","MH":"ctry_01GW2HHDK8HTCM0MWQXBJRXEYB","US":"ctry_01GW2HHDK9M26M80SG63T21SVH","MX":"ctry_01GW2HHDKB9DG2T2YZM5MFFVX9","CA":"ctry_01GW2HHDKAWXWYHAAESAA5HH94","VI":"ctry_01GW2HHDK9DG12Y7RQMVEE5XSQ","GU":"ctry_01GW2HHDKGZ2XQ8Q9D8GX564MJ","PR":"ctry_01GW2HHDK7PACTC9GJ2XBMVPKY"}},"false":{"byId":{"ctry_01GW2HHDK76HGWY2MNZTFSTT61":"NL","ctry_01GW2HHDK88EQB8BSKDWEPG76N":"VG","ctry_01GW2HHDKBRDF1DMR5DA9DAT7K":"PW","ctry_01GW2HHDK6BPY9VBW9WR5HDVA5":"GT","ctry_01GW2HHDK67CD2NGY24P7G22SF":"BD","ctry_01GW2HHDK7JEDGYF9BQBZVDW0X":"PA","ctry_01GW2HHDK7372NTNTPP3V9BPGV":"NI","ctry_01GW2HHDK7HX3TFSB0HX6YDCRR":"RE","ctry_01GW2HHDK70QBRG8S7ZXZP89W9":"SL","ctry_01GW2HHDK8SPF3A2D6NW2XPTHM":"BB","ctry_01GW2HHDK8JM827C3VY37CM7TF":"IL","ctry_01GW2HHDK87660QN220M6T4S2K":"EH","ctry_01GW2HHDK87VQBC3WXQDJJZ6XM":"AI","ctry_01GW2HHDK8ZYZHYB9CAVW6C4ER":"RW","ctry_01GW2HHDK8NAY2T3GHNV1HVD3M":"TR","ctry_01GW2HHDK8VJ7Z7R0D99K5E4AN":"MZ","ctry_01GW2HHDK8AXT584ZTWBN739SN":"NG","ctry_01GW2HHDK8A3W54F0AW64VV2GD":"RU","ctry_01GW2HHDK8K7BJH8JW34Q7JK12":"CU","ctry_01GW2HHDK8NAN0FT6100XB79PX":"JE","ctry_01GW2HHDK8WHEVKX1JE6V2E5JJ":"CR","ctry_01GW2HHDK8MZVCAHNP3EBTHDGH":"PM","ctry_01GW2HHDK8HE6ZXBJEN5GGJYBA":"SV","ctry_01GW2HHDK96FCPGE9HBHC01VDE":"HN","ctry_01GW2HHDK9114P6V7VFFNFTMNP":"MO","ctry_01GW2HHDKAKGTGCFFE8HQGZXF4":"MK","ctry_01GW2HHDKATDZGNR55QRS6Y5NX":"AF","ctry_01GW2HHDKA6G8FACKBG654B237":"BW","ctry_01GW2HHDKAFPAWV4NQHE9VT4BA":"AQ","ctry_01GW2HHDKAR1506PSRF3PB1HYX":"MR","ctry_01GW2HHDKA77DT55ZPJ8XKM8P3":"QA","ctry_01GW2HHDKAMRVA2E0TZBFPX7VF":"ES","ctry_01GW2HHDKA60427HJJGXNGC3EV":"YE","ctry_01GW2HHDKBHVDM3WCVS2THFMHT":"SO","ctry_01GW2HHDKBCAGQ1RJGC3ZP6Z5S":"SE","ctry_01GW2HHDKBKDT96T91NCRFZE8A":"LI","ctry_01GW2HHDKBM1E4N9900Q4N942Y":"GS","ctry_01GW2HHDKBJ9NNQ06K5V64NAB8":"MA","ctry_01GW2HHDKBN3B46NQY3YEACJ0K":"MG","ctry_01GW2HHDKBX833SWMMRHFEXGDG":"UA","ctry_01GW2HHDKBEYBGKWE7BCHXZHJ6":"IR","ctry_01GW2HHDKBRG2CC67WN648YK7S":"PL","ctry_01GW2HHDK65BRH9H4P9YZH3P3F":"IT","ctry_01GW2HHDK6ENG0N2YD2ZXD043N":"DZ","ctry_01GW2HHDKBDJH4JA3QZRZCDB1C":"TD","ctry_01GW2HHDKBARW56WYXTBMBX9Z8":"VU","ctry_01GW2HHDKBRRZ84X368SH12W7Q":"PE","ctry_01GW2HHDKBT6AMAW7JQ18MAEGF":"SZ","ctry_01GW2HHDKB96KWQYA4T0D8VXTV":"MQ","ctry_01GW2HHDKBTPBSDQE1XHBMBXJE":"LU","ctry_01GW2HHDKBS9SYM4Z85HKVCW7S":"DJ","ctry_01GW2HHDKBP9EB7HW41MH04MCA":"HT","ctry_01GW2HHDKBCYJXSMFM5N5VW3Q5":"SY","ctry_01GW2HHDKBW4RFHHBG7R71M5B5":"GI","ctry_01GW2HHDKB1FFT4ZEBATSQXWRB":"IS","ctry_01GW2HHDKC0MEFFZTGK0RWCRQA":"FJ","ctry_01GW2HHDKCQX4EJEVCEME3EEBS":"KP","ctry_01GW2HHDKCK11551EN1JD1AMT6":"GN","ctry_01GW2HHDKC0N6MV90J9VWJ6CQN":"KN","ctry_01GW2HHDKCBYW7V5T1DWEBPFRV":"BH","ctry_01GW2HHDKC6BNE39T2DV3QC4NJ":"LT","ctry_01GW2HHDKCJV96MQND976G21HR":"GE","ctry_01GW2HHDK67GZQVGA3NZ8PE5SS":"AS","ctry_01GW2HHDKCGEP7FXQHWTQWB904":"RS","ctry_01GW2HHDKCFG7G5S12F71S6QG5":"ML","ctry_01GW2HHDKC2Q96XZGFSTMMQPNB":"MN","ctry_01GW2HHDKCVDAHZVSVC2YD68XN":"DM","ctry_01GW2HHDKCBG5RYMSXJ7NE0EQH":"GL","ctry_01GW2HHDKC2S12HZKRBMRC51WB":"ET","ctry_01GW2HHDKC77VK7AC2YXTJFP9H":"LY","ctry_01GW2HHDKCGYGA9KDYNQ3CK7H7":"NA","ctry_01GW2HHDKCZNJN6F1J1JTQRZ25":"MD","ctry_01GW2HHDKCS87MJP31FAC9S4NB":"BO","ctry_01GW2HHDKDEHGJVDQC1VDXFM0P":"CW","ctry_01GW2HHDKDHB62D7NFGE7RRNDN":"ID","ctry_01GW2HHDKD14G6PFAPNEMTW5F5":"BY","ctry_01GW2HHDKC5TFN4HP9KQ7QCQR9":"SB","ctry_01GW2HHDKCZ5TVB7QPAQNRS362":"TF","ctry_01GW2HHDKDHEQGH45498FKFDM8":"LR","ctry_01GW2HHDKDD06KYX7CY9JJR1SQ":"MT","ctry_01GW2HHDKD311V08MQJWG7CK03":"MF","ctry_01GW2HHDKDNEGQ31JPB4V9BBMK":"ZA","ctry_01GW2HHDKDMWST2K7SVAAEP7K5":"TG","ctry_01GW2HHDKD3SFPCG8ED0EP3MX3":"AM","ctry_01GW2HHDKD9T5SF5CAMVPA1M4F":"SH","ctry_01GW2HHDKDSS3YW7W5994KW3ZT":"AL","ctry_01GW2HHDKDE152HYH5H1TD8CK6":"AD","ctry_01GW2HHDKD48PB3V9R7ZWXZ3KP":"SS","ctry_01GW2HHDKDXS2FKHD8YPRKBZPY":"GG","ctry_01GW2HHDKDWZR3XW85F2G82SME":"CZ","ctry_01GW2HHDKDT4XD73Q57W6631FN":"GP","ctry_01GW2HHDKDK9BZQS106Q7H8XJR":"GD","ctry_01GW2HHDKDDHRPPGSEFG2P69F0":"PK","ctry_01GW2HHDKD09MHFTWE9SKVMMJ0":"VA","ctry_01GW2HHDKEYSJWN1W0AYDS0NH4":"WF","ctry_01GW2HHDKEZMTF03P4JYAFZRN1":"CM","ctry_01GW2HHDKEVZK4MDJTHQ025AQG":"JM","ctry_01GW2HHDKEFVF6N0VM03AGGZF7":"TM","ctry_01GW2HHDKEGN1AYW58P63XQPZ0":"BN","ctry_01GW2HHDKEW7A1ZRA4EFNWN6FB":"GH","ctry_01GW2HHDKE2127XE2CXSCB0SWG":"KW","ctry_01GW2HHDKE6ZTA1B0KNFYXA7XZ":"ER","ctry_01GW2HHDKENXBKBZTFJ4PA6Z2C":"ZW","ctry_01GW2HHDKE97QHYGJXTAGWENKK":"TZ","ctry_01GW2HHDKEM266QFKJYJVHC53T":"EE","ctry_01GW2HHDKEZN2DQG8Y7XVGHA0G":"BZ","ctry_01GW2HHDKE6SRWQT7YKMEV7N5Y":"SA","ctry_01GW2HHDKEKF565F1E5ZPTXFHE":"NZ","ctry_01GW2HHDKE747AN7G49Z1R39X7":"CO","ctry_01GW2HHDKEZED648DCY8RGH5NA":"MY","ctry_01GW2HHDKE7QG3QATZ72P7YT7V":"AG","ctry_01GW2HHDKE6FEQT1R83S60KVPT":"BM","ctry_01GW2HHDKEH1KYC4T8236ZSY3C":"AT","ctry_01GW2HHDKFTYX6T84QB1WZYYAY":"WS","ctry_01GW2HHDKF91Z0RAW6TC6P5TAP":"UG","ctry_01GW2HHDKFT9BZY0XPP6DESZX8":"NP","ctry_01GW2HHDKF47X5KGDW8RWF841A":"PY","ctry_01GW2HHDKFCKFTFJP7MARGNHRZ":"AU","ctry_01GW2HHDKF8Y6KNW8X28APN60W":"FO","ctry_01GW2HHDKFGDMKVKEAVVCVHXG4":"KG","ctry_01GW2HHDKFK3ME6G5ZNBVT26XS":"PF","ctry_01GW2HHDKFWZA5DF33HK20AMQK":"EC","ctry_01GW2HHDKF1ZKQGXHGKH67QXYA":"HK","ctry_01GW2HHDKF1NR34T2G6FMKQCYD":"TL","ctry_01GW2HHDKFYB0C2FBC8KQ50M36":"VC","ctry_01GW2HHDKFX9C6AWNQ3F32P95P":"VN","ctry_01GW2HHDKF939YJGVTR7H30KPX":"IE","ctry_01GW2HHDKFPPCXV7F8BNVZZ81G":"FI","ctry_01GW2HHDKFEWN5X3FJ1QFQ55NW":"JP","ctry_01GW2HHDKGHKX9XHGFYN7FAF95":"NF","ctry_01GW2HHDKGTHZA8H042BMMGF3J":"BR","ctry_01GW2HHDKGA35BCNN91RB2DFX7":"ME","ctry_01GW2HHDKGRJCTDX2GK4ZZDKAS":"GY","ctry_01GW2HHDKG21ZM449SKC9HB5YK":"SK","ctry_01GW2HHDKGB3V0H2DKQA84VA5K":"MC","ctry_01GW2HHDKGEYB1AM0P2DVYBS7A":"CN","ctry_01GW2HHDKGHT708N7RM8QDDEBK":"AW","ctry_01GW2HHDKG8SKA53C146FV5E0G":"LA","ctry_01GW2HHDKGJG43HR2M2T7JQ5DC":"VE","ctry_01GW2HHDKGRRZM6X62GJ3M62Z4":"PS","ctry_01GW2HHDKGQMAVVKJ3SPPCS689":"SX","ctry_01GW2HHDKGGQ8SZPJH74RXNCP6":"HU","ctry_01GW2HHDKGGTVBR39ZYJVVDGNY":"DE","ctry_01GW2HHDKG1TWFSYE4QQ6CRFX7":"TO","ctry_01GW2HHDKGM9N3RGKASW4677KV":"IQ","ctry_01GW2HHDKGVAGE7SGTXWD5V2EP":"TH","ctry_01GW2HHDKHHXXT7NMR9TQ0TY97":"FM","ctry_01GW2HHDKHCQVPDAC0H3PQV53N":"BE","ctry_01GW2HHDKH6NG85CKMSESSPHQK":"KR","ctry_01GW2HHDKH7Z155XNFE328RJK5":"DK","ctry_01GW2HHDKH3SMD6TVK8MPGE6DD":"OM","ctry_01GW2HHDK9B08AAMK0WGCVR313":"NU","ctry_01GW2HHDK95AW3T41X8R552F6R":"SI","ctry_01GW2HHDK9HZ6M9Q2PJEX8T6HA":"PG","ctry_01GW2HHDKH5GV00V6MM2F7CZ3P":"GF","ctry_01GW2HHDKJ4620D1AB421E7JYX":"SN","ctry_01GW2HHDKHCN2Q3JMYRQ7HJ5VH":"MS","ctry_01GW2HHDKCRS9KW4FG2WR2GG06":"UM","ctry_01GW2HHDK6TWQ3BN3PG06DQ3HM":"FR","ctry_01GW2HHDK7QA15RA6W4YR0YTPQ":"BT","ctry_01GW2HHDK73TNXH3DFC5BV244P":"ST","ctry_01GW2HHDK79QNCV8EVYND8E6MY":"NR","ctry_01GW2HHDK7Z5N9Q2JPZNXP8VPE":"GA","ctry_01GW2HHDK7HMC32S6PHPMFACZX":"NC","ctry_01GW2HHDKCW140K2TYTEE750YT":"CF","ctry_01GW2HHDK74HTQCYV1EVM79B1G":"NO","ctry_01GW2HHDKCGAXYVWHRWSD40516":"DO","ctry_01GW2HHDK7BMH7NEWWCXXE4XTG":"TV","ctry_01GW2HHDK7H0GWV1MRQ6EF6E8N":"IN","ctry_01GW2HHDKD20967VJAPNPBARXM":"CG","ctry_01GW2HHDKDXBNPG01NBMPKYFZ2":"AE","ctry_01GW2HHDKE2EJHZ22HF2GWSB4H":"KM","ctry_01GW2HHDKE1JW9G222WXWADT5M":"FK","ctry_01GW2HHDKEE1FJ2XTH7MA2AZP1":"TC","ctry_01GW2HHDKFTPR1W2P2M9E081J5":"GB","ctry_01GW2HHDKHMX2CYG3GWYA23W4B":"TW","ctry_01GW2HHDKFVJ4QNETXPR3PGZES":"HM","ctry_01GW2HHDKG19TTZ3XA7NKMZ3TJ":"IO","ctry_01GW2HHDKHF02X3NEPGZ12ZFZV":"SC","ctry_01GW2HHDKFJ4Q7PBTTN4GSMPV0":"MP","ctry_01GW2HHDK8FK98CXW90F5HFRJH":"CK","ctry_01GW2HHDKJRNN53ENTSS4Z001K":"PN","ctry_01GW2HHDK8HTCM0MWQXBJRXEYB":"MH","ctry_01GW2HHDK7AMX8NEKVMBA64YJS":"CD","ctry_01GW2HHDK7T16K1ZJ1PZX1E1ZV":"KH","ctry_01GW2HHDK7DF62BBZYJCBFGVYY":"MU","ctry_01GW2HHDK796T1HRBRQMRV2ZGH":"SM","ctry_01GW2HHDK99MQM7GZ8EN4WZ9FK":"MM","ctry_01GW2HHDK9ZK9PGCD8MGCD8YN9":"EG","ctry_01GW2HHDK9NG4F38ZGH49JE311":"LB","ctry_01GW2HHDK9D4RQ30ZEV336H36F":"KE","ctry_01GW2HHDK95TZ43CS71K1P62JD":"GQ","ctry_01GW2HHDK9SZZ6W0GPYA0STVYG":"HR","ctry_01GW2HHDKAE2X1VJCYX05SQ15T":"KZ","ctry_01GW2HHDKA32KTYFEBFF1FRX6G":"XK","ctry_01GW2HHDK90V1XFYZ6AWB05J58":"GM","ctry_01GW2HHDKAQTGEVED1E27QW61N":"PH","ctry_01GW2HHDK9JPEM2C72VSS71G4H":"BQ","ctry_01GW2HHDKH7K0Z1YPRYG3CMEMW":"BS","ctry_01GW2HHDK9M26M80SG63T21SVH":"US","ctry_01GW2HHDKAYQQC5ZYAY81QFF4G":"AR","ctry_01GW2HHDK9W45HXETV96K8CZAZ":"PT","ctry_01GW2HHDK94QVSXT8Q12EDB4XP":"TT","ctry_01GW2HHDKAHQVZWP2NHE3A02B5":"TJ","ctry_01GW2HHDKA3CZ3WF381DJZ5D0Z":"AZ","ctry_01GW2HHDKAGCFGK8W1ZFE62QKQ":"RO","ctry_01GW2HHDKAMPFS09WBC3XZHQKX":"CL","ctry_01GW2HHDKAZ27CDARTX8QS2JMN":"CY","ctry_01GW2HHDKAJ64YAST64W2BJQSV":"CV","ctry_01GW2HHDKAWJN8K8E3EYVZDENE":"BG","ctry_01GW2HHDKHRXXQZHPZYDV3SNVZ":"MW","ctry_01GW2HHDKHXN8ZSHAGKGENMQ4N":"GW","ctry_01GW2HHDKB9DG2T2YZM5MFFVX9":"MX","ctry_01GW2HHDKAWXWYHAAESAA5HH94":"CA","ctry_01GW2HHDKAZYVYFHDZNZDE4HPB":"UY","ctry_01GW2HHDKDSY01QJC6KX2BRXH8":"BV","ctry_01GW2HHDKEG4RY89ACRQMNT8SB":"SJ","ctry_01GW2HHDKFRPGM3P3HD5747R23":"TK","ctry_01GW2HHDKFFE67B6CGZGV25R1C":"IM","ctry_01GW2HHDKHV33E0R9ZQSE302T3":"CX","ctry_01GW2HHDKFPXSJ18WSJ8GEZKJ0":"ZM","ctry_01GW2HHDKFPY9T4YYDFWGBZP5P":"LC","ctry_01GW2HHDKGQRTDPFBTD9GJT3BN":"AX","ctry_01GW2HHDKHRGA9ME9MF56RDYYC":"CI","ctry_01GW2HHDKHA0DCHV26S9FDT5P4":"KY","ctry_01GW2HHDK6FGT7BES1NPX66JTQ":"SG","ctry_01GW2HHDKGG7JZ2RN968FJNWJ8":"TN","ctry_01GW2HHDKGB5RY2JAQVVF4RC4X":"BI","ctry_01GW2HHDKGFE1AY05PY62XEWPM":"CH","ctry_01GW2HHDKGTGG62NESH7TDS364":"BJ","ctry_01GW2HHDKGR9X8QJBDSK84PAG1":"KI","ctry_01GW2HHDKH7NVGT1JBPB5B9SP3":"SD","ctry_01GW2HHDKH6KGE8D69GPF7SSAJ":"UZ","ctry_01GW2HHDKHD6PPNGQWWGD59BQT":"BF","ctry_01GW2HHDKHZNVZP299TK5QC9X6":"LK","ctry_01GW2HHDKHMY5MATBT7VET2W95":"NE","ctry_01GW2HHDKHZP2AGK419VGS2YQ0":"SR","ctry_01GW2HHDKH6H61ZD7Q4D1EVHP5":"AO","ctry_01GW2HHDKH74J20AXR1GWGX6ZQ":"LS","ctry_01GW2HHDK66TC7PJG0EVPFMBFP":"CC","ctry_01GW2HHDK6FR8HMD3W523Q3WRA":"MV","ctry_01GW2HHDK9DG12Y7RQMVEE5XSQ":"VI","ctry_01GW2HHDK6V0QGJ9GRNQCW6A29":"BA","ctry_01GW2HHDK69KVF1HPHBRBTSSBE":"LV","ctry_01GW2HHDK6GRHQQEYGGJX4CQ1Z":"GR","ctry_01GW2HHDK6BG4CBH38VKSZ9M4X":"YT","ctry_01GW2HHDK6Q6BB2C5DWKAYKVP9":"JO","ctry_01GW2HHDKH0MMEJM9R74Z359R6":"BL","ctry_01GW2HHDKGZ2XQ8Q9D8GX564MJ":"GU","ctry_01GW2HHDK7PACTC9GJ2XBMVPKY":"PR"},"byCCA":{"NL":"ctry_01GW2HHDK76HGWY2MNZTFSTT61","VG":"ctry_01GW2HHDK88EQB8BSKDWEPG76N","PW":"ctry_01GW2HHDKBRDF1DMR5DA9DAT7K","GT":"ctry_01GW2HHDK6BPY9VBW9WR5HDVA5","BD":"ctry_01GW2HHDK67CD2NGY24P7G22SF","PA":"ctry_01GW2HHDK7JEDGYF9BQBZVDW0X","NI":"ctry_01GW2HHDK7372NTNTPP3V9BPGV","RE":"ctry_01GW2HHDK7HX3TFSB0HX6YDCRR","SL":"ctry_01GW2HHDK70QBRG8S7ZXZP89W9","BB":"ctry_01GW2HHDK8SPF3A2D6NW2XPTHM","IL":"ctry_01GW2HHDK8JM827C3VY37CM7TF","EH":"ctry_01GW2HHDK87660QN220M6T4S2K","AI":"ctry_01GW2HHDK87VQBC3WXQDJJZ6XM","RW":"ctry_01GW2HHDK8ZYZHYB9CAVW6C4ER","TR":"ctry_01GW2HHDK8NAY2T3GHNV1HVD3M","MZ":"ctry_01GW2HHDK8VJ7Z7R0D99K5E4AN","NG":"ctry_01GW2HHDK8AXT584ZTWBN739SN","RU":"ctry_01GW2HHDK8A3W54F0AW64VV2GD","CU":"ctry_01GW2HHDK8K7BJH8JW34Q7JK12","JE":"ctry_01GW2HHDK8NAN0FT6100XB79PX","CR":"ctry_01GW2HHDK8WHEVKX1JE6V2E5JJ","PM":"ctry_01GW2HHDK8MZVCAHNP3EBTHDGH","SV":"ctry_01GW2HHDK8HE6ZXBJEN5GGJYBA","HN":"ctry_01GW2HHDK96FCPGE9HBHC01VDE","MO":"ctry_01GW2HHDK9114P6V7VFFNFTMNP","MK":"ctry_01GW2HHDKAKGTGCFFE8HQGZXF4","AF":"ctry_01GW2HHDKATDZGNR55QRS6Y5NX","BW":"ctry_01GW2HHDKA6G8FACKBG654B237","AQ":"ctry_01GW2HHDKAFPAWV4NQHE9VT4BA","MR":"ctry_01GW2HHDKAR1506PSRF3PB1HYX","QA":"ctry_01GW2HHDKA77DT55ZPJ8XKM8P3","ES":"ctry_01GW2HHDKAMRVA2E0TZBFPX7VF","YE":"ctry_01GW2HHDKA60427HJJGXNGC3EV","SO":"ctry_01GW2HHDKBHVDM3WCVS2THFMHT","SE":"ctry_01GW2HHDKBCAGQ1RJGC3ZP6Z5S","LI":"ctry_01GW2HHDKBKDT96T91NCRFZE8A","GS":"ctry_01GW2HHDKBM1E4N9900Q4N942Y","MA":"ctry_01GW2HHDKBJ9NNQ06K5V64NAB8","MG":"ctry_01GW2HHDKBN3B46NQY3YEACJ0K","UA":"ctry_01GW2HHDKBX833SWMMRHFEXGDG","IR":"ctry_01GW2HHDKBEYBGKWE7BCHXZHJ6","PL":"ctry_01GW2HHDKBRG2CC67WN648YK7S","IT":"ctry_01GW2HHDK65BRH9H4P9YZH3P3F","DZ":"ctry_01GW2HHDK6ENG0N2YD2ZXD043N","TD":"ctry_01GW2HHDKBDJH4JA3QZRZCDB1C","VU":"ctry_01GW2HHDKBARW56WYXTBMBX9Z8","PE":"ctry_01GW2HHDKBRRZ84X368SH12W7Q","SZ":"ctry_01GW2HHDKBT6AMAW7JQ18MAEGF","MQ":"ctry_01GW2HHDKB96KWQYA4T0D8VXTV","LU":"ctry_01GW2HHDKBTPBSDQE1XHBMBXJE","DJ":"ctry_01GW2HHDKBS9SYM4Z85HKVCW7S","HT":"ctry_01GW2HHDKBP9EB7HW41MH04MCA","SY":"ctry_01GW2HHDKBCYJXSMFM5N5VW3Q5","GI":"ctry_01GW2HHDKBW4RFHHBG7R71M5B5","IS":"ctry_01GW2HHDKB1FFT4ZEBATSQXWRB","FJ":"ctry_01GW2HHDKC0MEFFZTGK0RWCRQA","KP":"ctry_01GW2HHDKCQX4EJEVCEME3EEBS","GN":"ctry_01GW2HHDKCK11551EN1JD1AMT6","KN":"ctry_01GW2HHDKC0N6MV90J9VWJ6CQN","BH":"ctry_01GW2HHDKCBYW7V5T1DWEBPFRV","LT":"ctry_01GW2HHDKC6BNE39T2DV3QC4NJ","GE":"ctry_01GW2HHDKCJV96MQND976G21HR","AS":"ctry_01GW2HHDK67GZQVGA3NZ8PE5SS","RS":"ctry_01GW2HHDKCGEP7FXQHWTQWB904","ML":"ctry_01GW2HHDKCFG7G5S12F71S6QG5","MN":"ctry_01GW2HHDKC2Q96XZGFSTMMQPNB","DM":"ctry_01GW2HHDKCVDAHZVSVC2YD68XN","GL":"ctry_01GW2HHDKCBG5RYMSXJ7NE0EQH","ET":"ctry_01GW2HHDKC2S12HZKRBMRC51WB","LY":"ctry_01GW2HHDKC77VK7AC2YXTJFP9H","NA":"ctry_01GW2HHDKCGYGA9KDYNQ3CK7H7","MD":"ctry_01GW2HHDKCZNJN6F1J1JTQRZ25","BO":"ctry_01GW2HHDKCS87MJP31FAC9S4NB","CW":"ctry_01GW2HHDKDEHGJVDQC1VDXFM0P","ID":"ctry_01GW2HHDKDHB62D7NFGE7RRNDN","BY":"ctry_01GW2HHDKD14G6PFAPNEMTW5F5","SB":"ctry_01GW2HHDKC5TFN4HP9KQ7QCQR9","TF":"ctry_01GW2HHDKCZ5TVB7QPAQNRS362","LR":"ctry_01GW2HHDKDHEQGH45498FKFDM8","MT":"ctry_01GW2HHDKDD06KYX7CY9JJR1SQ","MF":"ctry_01GW2HHDKD311V08MQJWG7CK03","ZA":"ctry_01GW2HHDKDNEGQ31JPB4V9BBMK","TG":"ctry_01GW2HHDKDMWST2K7SVAAEP7K5","AM":"ctry_01GW2HHDKD3SFPCG8ED0EP3MX3","SH":"ctry_01GW2HHDKD9T5SF5CAMVPA1M4F","AL":"ctry_01GW2HHDKDSS3YW7W5994KW3ZT","AD":"ctry_01GW2HHDKDE152HYH5H1TD8CK6","SS":"ctry_01GW2HHDKD48PB3V9R7ZWXZ3KP","GG":"ctry_01GW2HHDKDXS2FKHD8YPRKBZPY","CZ":"ctry_01GW2HHDKDWZR3XW85F2G82SME","GP":"ctry_01GW2HHDKDT4XD73Q57W6631FN","GD":"ctry_01GW2HHDKDK9BZQS106Q7H8XJR","PK":"ctry_01GW2HHDKDDHRPPGSEFG2P69F0","VA":"ctry_01GW2HHDKD09MHFTWE9SKVMMJ0","WF":"ctry_01GW2HHDKEYSJWN1W0AYDS0NH4","CM":"ctry_01GW2HHDKEZMTF03P4JYAFZRN1","JM":"ctry_01GW2HHDKEVZK4MDJTHQ025AQG","TM":"ctry_01GW2HHDKEFVF6N0VM03AGGZF7","BN":"ctry_01GW2HHDKEGN1AYW58P63XQPZ0","GH":"ctry_01GW2HHDKEW7A1ZRA4EFNWN6FB","KW":"ctry_01GW2HHDKE2127XE2CXSCB0SWG","ER":"ctry_01GW2HHDKE6ZTA1B0KNFYXA7XZ","ZW":"ctry_01GW2HHDKENXBKBZTFJ4PA6Z2C","TZ":"ctry_01GW2HHDKE97QHYGJXTAGWENKK","EE":"ctry_01GW2HHDKEM266QFKJYJVHC53T","BZ":"ctry_01GW2HHDKEZN2DQG8Y7XVGHA0G","SA":"ctry_01GW2HHDKE6SRWQT7YKMEV7N5Y","NZ":"ctry_01GW2HHDKEKF565F1E5ZPTXFHE","CO":"ctry_01GW2HHDKE747AN7G49Z1R39X7","MY":"ctry_01GW2HHDKEZED648DCY8RGH5NA","AG":"ctry_01GW2HHDKE7QG3QATZ72P7YT7V","BM":"ctry_01GW2HHDKE6FEQT1R83S60KVPT","AT":"ctry_01GW2HHDKEH1KYC4T8236ZSY3C","WS":"ctry_01GW2HHDKFTYX6T84QB1WZYYAY","UG":"ctry_01GW2HHDKF91Z0RAW6TC6P5TAP","NP":"ctry_01GW2HHDKFT9BZY0XPP6DESZX8","PY":"ctry_01GW2HHDKF47X5KGDW8RWF841A","AU":"ctry_01GW2HHDKFCKFTFJP7MARGNHRZ","FO":"ctry_01GW2HHDKF8Y6KNW8X28APN60W","KG":"ctry_01GW2HHDKFGDMKVKEAVVCVHXG4","PF":"ctry_01GW2HHDKFK3ME6G5ZNBVT26XS","EC":"ctry_01GW2HHDKFWZA5DF33HK20AMQK","HK":"ctry_01GW2HHDKF1ZKQGXHGKH67QXYA","TL":"ctry_01GW2HHDKF1NR34T2G6FMKQCYD","VC":"ctry_01GW2HHDKFYB0C2FBC8KQ50M36","VN":"ctry_01GW2HHDKFX9C6AWNQ3F32P95P","IE":"ctry_01GW2HHDKF939YJGVTR7H30KPX","FI":"ctry_01GW2HHDKFPPCXV7F8BNVZZ81G","JP":"ctry_01GW2HHDKFEWN5X3FJ1QFQ55NW","NF":"ctry_01GW2HHDKGHKX9XHGFYN7FAF95","BR":"ctry_01GW2HHDKGTHZA8H042BMMGF3J","ME":"ctry_01GW2HHDKGA35BCNN91RB2DFX7","GY":"ctry_01GW2HHDKGRJCTDX2GK4ZZDKAS","SK":"ctry_01GW2HHDKG21ZM449SKC9HB5YK","MC":"ctry_01GW2HHDKGB3V0H2DKQA84VA5K","CN":"ctry_01GW2HHDKGEYB1AM0P2DVYBS7A","AW":"ctry_01GW2HHDKGHT708N7RM8QDDEBK","LA":"ctry_01GW2HHDKG8SKA53C146FV5E0G","VE":"ctry_01GW2HHDKGJG43HR2M2T7JQ5DC","PS":"ctry_01GW2HHDKGRRZM6X62GJ3M62Z4","SX":"ctry_01GW2HHDKGQMAVVKJ3SPPCS689","HU":"ctry_01GW2HHDKGGQ8SZPJH74RXNCP6","DE":"ctry_01GW2HHDKGGTVBR39ZYJVVDGNY","TO":"ctry_01GW2HHDKG1TWFSYE4QQ6CRFX7","IQ":"ctry_01GW2HHDKGM9N3RGKASW4677KV","TH":"ctry_01GW2HHDKGVAGE7SGTXWD5V2EP","FM":"ctry_01GW2HHDKHHXXT7NMR9TQ0TY97","BE":"ctry_01GW2HHDKHCQVPDAC0H3PQV53N","KR":"ctry_01GW2HHDKH6NG85CKMSESSPHQK","DK":"ctry_01GW2HHDKH7Z155XNFE328RJK5","OM":"ctry_01GW2HHDKH3SMD6TVK8MPGE6DD","NU":"ctry_01GW2HHDK9B08AAMK0WGCVR313","SI":"ctry_01GW2HHDK95AW3T41X8R552F6R","PG":"ctry_01GW2HHDK9HZ6M9Q2PJEX8T6HA","GF":"ctry_01GW2HHDKH5GV00V6MM2F7CZ3P","SN":"ctry_01GW2HHDKJ4620D1AB421E7JYX","MS":"ctry_01GW2HHDKHCN2Q3JMYRQ7HJ5VH","UM":"ctry_01GW2HHDKCRS9KW4FG2WR2GG06","FR":"ctry_01GW2HHDK6TWQ3BN3PG06DQ3HM","BT":"ctry_01GW2HHDK7QA15RA6W4YR0YTPQ","ST":"ctry_01GW2HHDK73TNXH3DFC5BV244P","NR":"ctry_01GW2HHDK79QNCV8EVYND8E6MY","GA":"ctry_01GW2HHDK7Z5N9Q2JPZNXP8VPE","NC":"ctry_01GW2HHDK7HMC32S6PHPMFACZX","CF":"ctry_01GW2HHDKCW140K2TYTEE750YT","NO":"ctry_01GW2HHDK74HTQCYV1EVM79B1G","DO":"ctry_01GW2HHDKCGAXYVWHRWSD40516","TV":"ctry_01GW2HHDK7BMH7NEWWCXXE4XTG","IN":"ctry_01GW2HHDK7H0GWV1MRQ6EF6E8N","CG":"ctry_01GW2HHDKD20967VJAPNPBARXM","AE":"ctry_01GW2HHDKDXBNPG01NBMPKYFZ2","KM":"ctry_01GW2HHDKE2EJHZ22HF2GWSB4H","FK":"ctry_01GW2HHDKE1JW9G222WXWADT5M","TC":"ctry_01GW2HHDKEE1FJ2XTH7MA2AZP1","GB":"ctry_01GW2HHDKFTPR1W2P2M9E081J5","TW":"ctry_01GW2HHDKHMX2CYG3GWYA23W4B","HM":"ctry_01GW2HHDKFVJ4QNETXPR3PGZES","IO":"ctry_01GW2HHDKG19TTZ3XA7NKMZ3TJ","SC":"ctry_01GW2HHDKHF02X3NEPGZ12ZFZV","MP":"ctry_01GW2HHDKFJ4Q7PBTTN4GSMPV0","CK":"ctry_01GW2HHDK8FK98CXW90F5HFRJH","PN":"ctry_01GW2HHDKJRNN53ENTSS4Z001K","MH":"ctry_01GW2HHDK8HTCM0MWQXBJRXEYB","CD":"ctry_01GW2HHDK7AMX8NEKVMBA64YJS","KH":"ctry_01GW2HHDK7T16K1ZJ1PZX1E1ZV","MU":"ctry_01GW2HHDK7DF62BBZYJCBFGVYY","SM":"ctry_01GW2HHDK796T1HRBRQMRV2ZGH","MM":"ctry_01GW2HHDK99MQM7GZ8EN4WZ9FK","EG":"ctry_01GW2HHDK9ZK9PGCD8MGCD8YN9","LB":"ctry_01GW2HHDK9NG4F38ZGH49JE311","KE":"ctry_01GW2HHDK9D4RQ30ZEV336H36F","GQ":"ctry_01GW2HHDK95TZ43CS71K1P62JD","HR":"ctry_01GW2HHDK9SZZ6W0GPYA0STVYG","KZ":"ctry_01GW2HHDKAE2X1VJCYX05SQ15T","XK":"ctry_01GW2HHDKA32KTYFEBFF1FRX6G","GM":"ctry_01GW2HHDK90V1XFYZ6AWB05J58","PH":"ctry_01GW2HHDKAQTGEVED1E27QW61N","BQ":"ctry_01GW2HHDK9JPEM2C72VSS71G4H","BS":"ctry_01GW2HHDKH7K0Z1YPRYG3CMEMW","US":"ctry_01GW2HHDK9M26M80SG63T21SVH","AR":"ctry_01GW2HHDKAYQQC5ZYAY81QFF4G","PT":"ctry_01GW2HHDK9W45HXETV96K8CZAZ","TT":"ctry_01GW2HHDK94QVSXT8Q12EDB4XP","TJ":"ctry_01GW2HHDKAHQVZWP2NHE3A02B5","AZ":"ctry_01GW2HHDKA3CZ3WF381DJZ5D0Z","RO":"ctry_01GW2HHDKAGCFGK8W1ZFE62QKQ","CL":"ctry_01GW2HHDKAMPFS09WBC3XZHQKX","CY":"ctry_01GW2HHDKAZ27CDARTX8QS2JMN","CV":"ctry_01GW2HHDKAJ64YAST64W2BJQSV","BG":"ctry_01GW2HHDKAWJN8K8E3EYVZDENE","MW":"ctry_01GW2HHDKHRXXQZHPZYDV3SNVZ","GW":"ctry_01GW2HHDKHXN8ZSHAGKGENMQ4N","MX":"ctry_01GW2HHDKB9DG2T2YZM5MFFVX9","CA":"ctry_01GW2HHDKAWXWYHAAESAA5HH94","UY":"ctry_01GW2HHDKAZYVYFHDZNZDE4HPB","BV":"ctry_01GW2HHDKDSY01QJC6KX2BRXH8","SJ":"ctry_01GW2HHDKEG4RY89ACRQMNT8SB","TK":"ctry_01GW2HHDKFRPGM3P3HD5747R23","IM":"ctry_01GW2HHDKFFE67B6CGZGV25R1C","CX":"ctry_01GW2HHDKHV33E0R9ZQSE302T3","ZM":"ctry_01GW2HHDKFPXSJ18WSJ8GEZKJ0","LC":"ctry_01GW2HHDKFPY9T4YYDFWGBZP5P","AX":"ctry_01GW2HHDKGQRTDPFBTD9GJT3BN","CI":"ctry_01GW2HHDKHRGA9ME9MF56RDYYC","KY":"ctry_01GW2HHDKHA0DCHV26S9FDT5P4","SG":"ctry_01GW2HHDK6FGT7BES1NPX66JTQ","TN":"ctry_01GW2HHDKGG7JZ2RN968FJNWJ8","BI":"ctry_01GW2HHDKGB5RY2JAQVVF4RC4X","CH":"ctry_01GW2HHDKGFE1AY05PY62XEWPM","BJ":"ctry_01GW2HHDKGTGG62NESH7TDS364","KI":"ctry_01GW2HHDKGR9X8QJBDSK84PAG1","SD":"ctry_01GW2HHDKH7NVGT1JBPB5B9SP3","UZ":"ctry_01GW2HHDKH6KGE8D69GPF7SSAJ","BF":"ctry_01GW2HHDKHD6PPNGQWWGD59BQT","LK":"ctry_01GW2HHDKHZNVZP299TK5QC9X6","NE":"ctry_01GW2HHDKHMY5MATBT7VET2W95","SR":"ctry_01GW2HHDKHZP2AGK419VGS2YQ0","AO":"ctry_01GW2HHDKH6H61ZD7Q4D1EVHP5","LS":"ctry_01GW2HHDKH74J20AXR1GWGX6ZQ","CC":"ctry_01GW2HHDK66TC7PJG0EVPFMBFP","MV":"ctry_01GW2HHDK6FR8HMD3W523Q3WRA","VI":"ctry_01GW2HHDK9DG12Y7RQMVEE5XSQ","BA":"ctry_01GW2HHDK6V0QGJ9GRNQCW6A29","LV":"ctry_01GW2HHDK69KVF1HPHBRBTSSBE","GR":"ctry_01GW2HHDK6GRHQQEYGGJX4CQ1Z","YT":"ctry_01GW2HHDK6BG4CBH38VKSZ9M4X","JO":"ctry_01GW2HHDK6Q6BB2C5DWKAYKVP9","BL":"ctry_01GW2HHDKH0MMEJM9R74Z359R6","GU":"ctry_01GW2HHDKGZ2XQ8Q9D8GX564MJ","PR":"ctry_01GW2HHDK7PACTC9GJ2XBMVPKY"}}} \ No newline at end of file diff --git a/packages/ui/mockData/json/location.forLocationCard.json b/packages/ui/mockData/json/location.forLocationCard.json index de102344321..8d843aa5e1a 100644 --- a/packages/ui/mockData/json/location.forLocationCard.json +++ b/packages/ui/mockData/json/location.forLocationCard.json @@ -1 +1 @@ -{"id":"oloc_01GVH3VEVBERFNA9PHHJYEBGA3","name":"Whitman-Walker 1525","street1":"1525 14th St. NW","street2":null,"city":"Washington","postCode":"20005","latitude":38.91,"longitude":-77.032,"country":"US","govDist":{"abbrev":"DC","tsKey":"us-district-of-columbia","tsNs":"gov-dist"},"phones":[],"attributes":[],"services":["medical.CATEGORYNAME","mental-health.CATEGORYNAME"]} \ No newline at end of file +{"id":"oloc_01GVH3VEVBERFNA9PHHJYEBGA3","name":"Whitman-Walker 1525","street1":"1525 14th St. NW","street2":null,"city":"Washington","postCode":"20005","latitude":38.91,"longitude":-77.032,"notVisitable":false,"country":"US","govDist":{"abbrev":"DC","tsKey":"us-district-of-columbia","tsNs":"gov-dist"},"phones":[],"attributes":[],"services":["medical.CATEGORYNAME","mental-health.CATEGORYNAME"]} \ No newline at end of file diff --git a/packages/ui/mockData/json/service.forServiceEditDrawer.json b/packages/ui/mockData/json/service.forServiceEditDrawer.json index dfda96b34aa..f06e8749140 100644 --- a/packages/ui/mockData/json/service.forServiceEditDrawer.json +++ b/packages/ui/mockData/json/service.forServiceEditDrawer.json @@ -1 +1 @@ -{"id":"osvc_01GVH3VEVPF1KEKBTRVTV70WGV","description":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.osvc_01GVH3VEVPF1KEKBTRVTV70WGV.description","ns":"org-data","tsKey":{"text":"Whitman-Walker provides walk-in HIV testing at multiple locations in DC. Walk-in HIV testing includes a confidential, rapid HIV test and risk-reduction counseling. The counseling provides clients with education on their options for having safer sex. Whitman-Walker uses the INSTI® HIV-1/HIV-2 Rapid Antibody Test and results take one minute.","crowdinId":773222}},"hours":[],"published":true,"deleted":false,"serviceName":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.osvc_01GVH3VEVPF1KEKBTRVTV70WGV.name","ns":"org-data","tsKey":{"text":"Get rapid HIV testing","crowdinId":773224}},"phones":["ophn_01GVH3VEVC36PW0Z9GDV0ZERV1","ophn_01GVH3VEVCFKT3NWQ79STYVDKR"],"emails":[],"locations":["oloc_01GVH3VEVBRCFA2AHNTWCXQA2B","oloc_01GVH3VEVBSA85T6VR2C38BJPT"],"services":[{"id":"svtg_01GW2HHFBRPBXSYN12DWNEAJJ7","primaryCategoryId":"svct_01GW2HHEVKVHTWSBY7PVWC5390"}],"serviceAreas":{"id":"svar_01GW2HT9F1JKT1MCAJ3P7XBDHP","countries":[],"districts":["gdst_01GW2HJ5A278S2G84AB3N9FCW0"]},"attributes":[{"attribute":{"id":"attr_01GW2HHFVA06WHRSM241ZF0FY0","tsKey":"community.hiv-aids","tsNs":"attribute","icon":null,"categories":["community"]},"supplement":{"id":"atts_01E4ENGMG266R5BH78D7B2MB7M","active":true,"data":null,"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":null}},{"attribute":{"id":"attr_01GW2HHFVGDTNW9PDQNXK6TF1T","tsKey":"cost.cost-free","tsNs":"attribute","icon":"carbon:piggy-bank","categories":["cost"]},"supplement":{"id":"atts_01E4ENGMG2XWR5JQ1JMBN2SQVM","active":true,"data":null,"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":null}},{"attribute":{"id":"attr_01GW2HHFV3BADK80TG0DXXFPMM","tsKey":"additional.has-confidentiality-policy","tsNs":"attribute","icon":null,"categories":["additional-information"]},"supplement":{"id":"atts_01E4ENGMG2J94M4S9DQTE57GWN","active":true,"data":null,"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":null}},{"attribute":{"id":"attr_01GW2HHFV4TM7H5V6FHWA7S9JK","tsKey":"additional.time-walk-in","tsNs":"attribute","icon":null,"categories":["additional-information"]},"supplement":{"id":"atts_01E4ENGMG20KXGB20JYGZ4X938","active":true,"data":null,"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":null}},{"attribute":{"id":"attr_01GW2HHFVJ8K180CNX339BTXM2","tsKey":"lang.lang-offered","tsNs":"attribute","icon":null,"categories":["languages"]},"supplement":{"id":"atts_01GW2HT9F15B2HJK144B3NZHQK","active":true,"data":null,"boolean":null,"countryId":null,"govDistId":null,"languageId":"lang_0000000000N3K70GZXE29Z03A4","text":null}},{"attribute":{"id":"attr_01GW2HHFVK8KPRGKYFSSM5ECPQ","tsKey":"sys.incompatible-info","tsNs":"attribute","icon":null,"categories":["system"]},"supplement":{"id":"atts_01GW2HT9F13VVJCJ8W2WE86R6N","active":true,"data":{"json":[{"community-lgbt":"true"},{"lang-all-languages-by-interpreter":"Language access services are available, including ASL interpreting."}]},"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":null}}],"accessDetails":[{"attribute":{"id":"attr_01GW2HHFVMYXMS8ARA3GE7HZFD","tsKey":"serviceaccess.accesslink","tsNs":"attribute","icon":null,"categories":[{"category":{"tag":"service-access-instructions"}}]},"supplement":{"id":"atts_01GW2HT9F01W2M7FBSKSXAQ9R4","active":true,"data":{"json":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4234"},"access_type":"link","access_value":"https://www.whitman-walker.org/hiv-sti-testing","instructions":"Visit the website to learn more about Whitman-Walker's testing hours and locations.","access_value_ES":"https://www.whitman-walker.org/hiv-sti-testing","instructions_ES":"Visita el sitio web para obtener más información sobre los horarios y lugares de prueba de Whitman-Walker."}}},"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F01W2M7FBSKSXAQ9R4","ns":"org-data","tsKey":{"text":"Visit the website to learn more about Whitman-Walker's testing hours and locations.","crowdinId":1535739}}}},{"attribute":{"id":"attr_01GW2HHFVMKTFWCKBVVFJ5GMY0","tsKey":"serviceaccess.accessphone","tsNs":"attribute","icon":null,"categories":[{"category":{"tag":"service-access-instructions"}}]},"supplement":{"id":"atts_01GW2HT9F09GFRWM3JK2A43AWG","active":true,"data":{"json":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4235"},"access_type":"phone","access_value":"202-745-7000","instructions":"Contact the Main Office about services offered in multiple languages upon request.","access_value_ES":"202-745-7000","instructions_ES":"Comunícate con la oficina principal sobre los servicios que se ofrecen en varios idiomas si lo solicitas."}}},"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F09GFRWM3JK2A43AWG","ns":"org-data","tsKey":{"text":"Contact the Main Office about services offered in multiple languages upon request.","crowdinId":1535743}}}},{"attribute":{"id":"attr_01GW2HHFVMH6AE94EXN7T5A87C","tsKey":"serviceaccess.accesslocation","tsNs":"attribute","icon":null,"categories":[{"category":{"tag":"service-access-instructions"}}]},"supplement":{"id":"atts_01GW2HT9F0SPS3EBCQ710RCNTA","active":true,"data":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4231"},"access_type":"location","access_value":"2301 M. Luther King Jr., Washington DC 20020","instructions":"Max Robinson Center - NO walk-in testing is available. Monday:08:30-12:30, 13:30-17:30; Tuesday:08:30 - 12:30, 13:30 - 17:30; Wednesday:08:30 - 12:30, 13:30 - 17:30; Thursday:08:30 - 12:30, 13:30 - 17:30; Friday:08:30 - 12:30, 14:15 - 17:30.","access_value_ES":"2301 M. Luther King Jr., Washington DC 20020","instructions_ES":"Centro Max Robinson:NO hay pruebas disponibles sin cita previa. Lunes:08:30-12:30, 13:30-17:30; Martes:08:30 - 12:30, 13:30 - 17:30; Miércoles:08:30 - 12:30, 13:30 - 17:30; Jueves:08:30 - 12:30, 13:30 - 17:30; Viernes:08:30 - 12:30, 14:15 - 17:30."}},"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F0SPS3EBCQ710RCNTA","ns":"org-data","tsKey":{"text":"Max Robinson Center - NO walk-in testing is available. Monday:08:30-12:30, 13:30-17:30; Tuesday:08:30 - 12:30, 13:30 - 17:30; Wednesday:08:30 - 12:30, 13:30 - 17:30; Thursday:08:30 - 12:30, 13:30 - 17:30; Friday:08:30 - 12:30, 14:15 - 17:30.","crowdinId":1535745}}}},{"attribute":{"id":"attr_01GW2HHFVMH6AE94EXN7T5A87C","tsKey":"serviceaccess.accesslocation","tsNs":"attribute","icon":null,"categories":[{"category":{"tag":"service-access-instructions"}}]},"supplement":{"id":"atts_01GW2HT9F0638MD74PJ3SCWNXC","active":true,"data":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4233"},"access_type":"location","access_value":"1525 14th St, NW Washington, DC 20005","instructions":"Whitman-Walker at 1525 - NO walk-in testing is available. Monday-Thursday:08:30-12:30 & 13:30-17:30; Friday:08:30- 12:30 & 14:30 -17:30.","access_value_ES":"1525 14th St, NW Washington, DC 20005","instructions_ES":"Whitman-Walker en 1525:NO hay pruebas disponibles. Lunes-Jueves:08:30-12:30 y 13:30-17:30; Viernes:08:30- 12:30 y 14:30 -17:30."}},"boolean":null,"countryId":null,"govDistId":null,"languageId":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F0638MD74PJ3SCWNXC","ns":"org-data","tsKey":{"text":"Whitman-Walker at 1525 - NO walk-in testing is available. Monday-Thursday:08:30-12:30 & 13:30-17:30; Friday:08:30- 12:30 & 14:30 -17:30.","crowdinId":1535741}}}}]} +{"id":"osvc_01GVH3VEVPF1KEKBTRVTV70WGV","published":true,"deleted":false,"locations":[{"orgLocationId":"oloc_01GVH3VEVBRCFA2AHNTWCXQA2B","location":{"country":{"cca2":"US"}}},{"orgLocationId":"oloc_01GVH3VEVBSA85T6VR2C38BJPT","location":{"country":{"cca2":"US"}}}],"name":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.osvc_01GVH3VEVPF1KEKBTRVTV70WGV.name","text":"Get rapid HIV testing","ns":"org-data","crowdinId":773224},"description":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.osvc_01GVH3VEVPF1KEKBTRVTV70WGV.description","text":"Whitman-Walker provides walk-in HIV testing at multiple locations in DC. Walk-in HIV testing includes a confidential, rapid HIV test and risk-reduction counseling. The counseling provides clients with education on their options for having safer sex. Whitman-Walker uses the INSTI® HIV-1/HIV-2 Rapid Antibody Test and results take one minute.","ns":"org-data","crowdinId":773222},"phones":["ophn_01GVH3VEVC36PW0Z9GDV0ZERV1","ophn_01GVH3VEVCFKT3NWQ79STYVDKR"],"emails":[],"services":["svtg_01GW2HHFBRPBXSYN12DWNEAJJ7"],"hours":{},"serviceAreas":{"id":"svar_01GW2HT9F1JKT1MCAJ3P7XBDHP","countries":["ctry_01GW2HHDK7PACTC9GJ2XBMVPKY"],"districts":["gdst_01GW2HJ5A278S2G84AB3N9FCW0"]},"attributes":[{"attributeId":"attr_01GW2HHFVA06WHRSM241ZF0FY0","supplementId":"atts_01E4ENGMG266R5BH78D7B2MB7M","tag":"hiv-aids","tsKey":"community.hiv-aids","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"community","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVGDTNW9PDQNXK6TF1T","supplementId":"atts_01E4ENGMG2XWR5JQ1JMBN2SQVM","tag":"cost-free","tsKey":"cost.cost-free","tsNs":"attribute","icon":"carbon:piggy-bank","iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"cost","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFV3BADK80TG0DXXFPMM","supplementId":"atts_01E4ENGMG2J94M4S9DQTE57GWN","tag":"has-confidentiality-policy","tsKey":"additional.has-confidentiality-policy","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"additional-information","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFV4TM7H5V6FHWA7S9JK","supplementId":"atts_01E4ENGMG20KXGB20JYGZ4X938","tag":"time-walk-in","tsKey":"additional.time-walk-in","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"additional-information","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVK8KPRGKYFSSM5ECPQ","supplementId":"atts_01GW2HT9F13VVJCJ8W2WE86R6N","tag":"incompatible-info","tsKey":"sys.incompatible-info","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"system","active":false,"countryId":null,"country":null,"data":{"json":[{"community-lgbt":"true"},{"lang-all-languages-by-interpreter":"Language access services are available, including ASL interpreting."}]},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVJ8K180CNX339BTXM2","supplementId":"atts_01GW2HT9F15B2HJK144B3NZHQK","tag":"lang-offered","tsKey":"lang.lang-offered","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"languages","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":"lang_0000000000N3K70GZXE29Z03A4","language":{"languageName":"English","nativeName":"English"},"boolean":null,"text":null}],"accessDetails":[{"attributeId":"attr_01GW2HHFVMYXMS8ARA3GE7HZFD","supplementId":"atts_01GW2HT9F01W2M7FBSKSXAQ9R4","tag":"accesslink","tsKey":"serviceaccess.accesslink","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"service-access-instructions","active":true,"countryId":null,"country":null,"data":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4234"},"access_type":"link","access_value":"https://www.whitman-walker.org/hiv-sti-testing","instructions":"Visit the website to learn more about Whitman-Walker's testing hours and locations.","access_value_ES":"https://www.whitman-walker.org/hiv-sti-testing","instructions_ES":"Visita el sitio web para obtener más información sobre los horarios y lugares de prueba de Whitman-Walker."}},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F01W2M7FBSKSXAQ9R4","text":"Visit the website to learn more about Whitman-Walker's testing hours and locations.","ns":"org-data"}},{"attributeId":"attr_01GW2HHFVMKTFWCKBVVFJ5GMY0","supplementId":"atts_01GW2HT9F09GFRWM3JK2A43AWG","tag":"accessphone","tsKey":"serviceaccess.accessphone","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"service-access-instructions","active":true,"countryId":null,"country":null,"data":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4235"},"access_type":"phone","access_value":"202-745-7000","instructions":"Contact the Main Office about services offered in multiple languages upon request.","access_value_ES":"202-745-7000","instructions_ES":"Comunícate con la oficina principal sobre los servicios que se ofrecen en varios idiomas si lo solicitas."}},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F09GFRWM3JK2A43AWG","text":"Contact the Main Office about services offered in multiple languages upon request.","ns":"org-data"}},{"attributeId":"attr_01GW2HHFVMH6AE94EXN7T5A87C","supplementId":"atts_01GW2HT9F0SPS3EBCQ710RCNTA","tag":"accesslocation","tsKey":"serviceaccess.accesslocation","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"service-access-instructions","active":true,"countryId":null,"country":null,"data":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4231"},"access_type":"location","access_value":"2301 M. Luther King Jr., Washington DC 20020","instructions":"Max Robinson Center - NO walk-in testing is available. Monday:08:30-12:30, 13:30-17:30; Tuesday:08:30 - 12:30, 13:30 - 17:30; Wednesday:08:30 - 12:30, 13:30 - 17:30; Thursday:08:30 - 12:30, 13:30 - 17:30; Friday:08:30 - 12:30, 14:15 - 17:30.","access_value_ES":"2301 M. Luther King Jr., Washington DC 20020","instructions_ES":"Centro Max Robinson:NO hay pruebas disponibles sin cita previa. Lunes:08:30-12:30, 13:30-17:30; Martes:08:30 - 12:30, 13:30 - 17:30; Miércoles:08:30 - 12:30, 13:30 - 17:30; Jueves:08:30 - 12:30, 13:30 - 17:30; Viernes:08:30 - 12:30, 14:15 - 17:30."}},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F0SPS3EBCQ710RCNTA","text":"Max Robinson Center - NO walk-in testing is available. Monday:08:30-12:30, 13:30-17:30; Tuesday:08:30 - 12:30, 13:30 - 17:30; Wednesday:08:30 - 12:30, 13:30 - 17:30; Thursday:08:30 - 12:30, 13:30 - 17:30; Friday:08:30 - 12:30, 14:15 - 17:30.","ns":"org-data"}},{"attributeId":"attr_01GW2HHFVMH6AE94EXN7T5A87C","supplementId":"atts_01GW2HT9F0638MD74PJ3SCWNXC","tag":"accesslocation","tsKey":"serviceaccess.accesslocation","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"service-access-instructions","active":true,"countryId":null,"country":null,"data":{"json":{"_id":{"$oid":"5e7e4bdbd54f1760921a4233"},"access_type":"location","access_value":"1525 14th St, NW Washington, DC 20005","instructions":"Whitman-Walker at 1525 - NO walk-in testing is available. Monday-Thursday:08:30-12:30 & 13:30-17:30; Friday:08:30- 12:30 & 14:30 -17:30.","access_value_ES":"1525 14th St, NW Washington, DC 20005","instructions_ES":"Whitman-Walker en 1525:NO hay pruebas disponibles. Lunes-Jueves:08:30-12:30 y 13:30-17:30; Viernes:08:30- 12:30 y 14:30 -17:30."}},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":{"key":"orgn_01GVH3V408N0YS7CDYAH3F2BMH.attribute.atts_01GW2HT9F0638MD74PJ3SCWNXC","text":"Whitman-Walker at 1525 - NO walk-in testing is available. Monday-Thursday:08:30-12:30 & 13:30-17:30; Friday:08:30- 12:30 & 14:30 -17:30.","ns":"org-data"}}]} \ No newline at end of file diff --git a/packages/ui/mockData/json/service.forServiceInfoCard.json b/packages/ui/mockData/json/service.forServiceInfoCard.json index f9999113690..546dbec06ce 100644 --- a/packages/ui/mockData/json/service.forServiceInfoCard.json +++ b/packages/ui/mockData/json/service.forServiceInfoCard.json @@ -1 +1 @@ -[{"id":"osvc_01GVH3VEWK33YAKZMQ2W3GT4QK","serviceName":{"tsKey":{"text":"Access PEP and PrEP"},"tsNs":"org-data","defaultText":"Access PEP and PrEP"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEW3CZ8P9VS6A5MA0R7Z","serviceName":{"tsKey":{"text":"Receive behavioral health services"},"tsNs":"org-data","defaultText":"Receive behavioral health services"},"serviceCategories":["mental-health.CATEGORYNAME"],"offersRemote":true},{"id":"osvc_01GVH3VEWFZ5FHZ6S7BXQY1W55","serviceName":{"tsKey":{"text":"Get the COVID-19 vaccine"},"tsNs":"org-data","defaultText":"Get the COVID-19 vaccine"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEWD5ZQY1JZM16Y5M9NG","serviceName":{"tsKey":{"text":"Get legal help for transgender people to replace and update name/gender marker on immigration documents"},"tsNs":"org-data","defaultText":"Get legal help for transgender people to replace and update name/gender marker on immigration documents"},"serviceCategories":["legal.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEVY24KAYTWY2ZSFZNBX","serviceName":{"tsKey":{"text":"Get free individual and group psychotherapy for LGBTQ young people (ages 13-24)"},"tsNs":"org-data","defaultText":"Get free individual and group psychotherapy for LGBTQ young people (ages 13-24)"},"serviceCategories":["community-support.CATEGORYNAME","mental-health.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEVVHBRF1FFXZGMMYG7D","serviceName":{"tsKey":{"text":"Access youth and family support services"},"tsNs":"org-data","defaultText":"Access youth and family support services"},"serviceCategories":["community-support.CATEGORYNAME","medical.CATEGORYNAME","mental-health.CATEGORYNAME"],"offersRemote":true},{"id":"osvc_01GVH3VEWHDC6F5FCQHB0H5GD6","serviceName":{"tsKey":{"text":"Get gender affirming hormone therapy"},"tsNs":"org-data","defaultText":"Get gender affirming hormone therapy"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEVR4SRPFQD2SJF1MCJJ","serviceName":{"tsKey":{"text":"Receive gender affirming care and services"},"tsNs":"org-data","defaultText":"Receive gender affirming care and services"},"serviceCategories":["legal.CATEGORYNAME","medical.CATEGORYNAME","mental-health.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEW2ND36DB0XWAH1PQY0","serviceName":{"tsKey":{"text":"Get dental health services for HIV-positive individuals"},"tsNs":"org-data","defaultText":"Get dental health services for HIV-positive individuals"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEVSNF9NH79R7HC9FHY6","serviceName":{"tsKey":{"text":"Get HIV care for newly diagnosed patients"},"tsNs":"org-data","defaultText":"Get HIV care for newly diagnosed patients"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEWM65579T29F19QXP8E","serviceName":{"tsKey":{"text":"Get help with navigating health insurance options"},"tsNs":"org-data","defaultText":"Get help with navigating health insurance options"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":true},{"id":"osvc_01GVH3VEVZY7K2TYY1ZE7WXRRC","serviceName":{"tsKey":{"text":"Get legal help with immigration services"},"tsNs":"org-data","defaultText":"Get legal help with immigration services"},"serviceCategories":["legal.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VEVPF1KEKBTRVTV70WGV","serviceName":{"tsKey":{"text":"Get rapid HIV testing"},"tsNs":"org-data","defaultText":"Get rapid HIV testing"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":false}] \ No newline at end of file +[{"id":"osvc_01GVH3VDMNH6PJFW50BVWN0N9R","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDMNH6PJFW50BVWN0N9R.name","tsNs":"org-data","defaultText":"Get emergency shelter for youth ages 18-24"},"serviceCategories":["housing.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VDMSN34BACQDMY6S5GPM","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDMSN34BACQDMY6S5GPM.name","tsNs":"org-data","defaultText":"Get education and employment services for youth ages 24 and under"},"serviceCategories":["education-and-employment.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VDMZYAPMQWQ5F3YWM8FW","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDMZYAPMQWQ5F3YWM8FW.name","tsNs":"org-data","defaultText":"Get housing and support services for youth ages 18-24 with HIV"},"serviceCategories":["housing.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VDN19JS30RV26PH04ZA8","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDN19JS30RV26PH04ZA8.name","tsNs":"org-data","defaultText":"Get supportive housing for LGBTQ youth ages 18-24"},"serviceCategories":["housing.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VDN4M572FCVMDZTCNYT0","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDN4M572FCVMDZTCNYT0.name","tsNs":"org-data","defaultText":"Get homeless support services at a drop-in center for ages 24 and under"},"serviceCategories":["housing.CATEGORYNAME","hygiene-and-clothing.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VDN73VP7ZAFMPC67HSWN","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDN73VP7ZAFMPC67HSWN.name","tsNs":"org-data","defaultText":"Get free medical care for youth ages 25 and under"},"serviceCategories":["medical.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VDN9470A0E49NNYP9JX6","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDN9470A0E49NNYP9JX6.name","tsNs":"org-data","defaultText":"Get emergency shelter for children ages 17 and younger"},"serviceCategories":["housing.CATEGORYNAME"],"offersRemote":false},{"id":"osvc_01GVH3VDNCMFMKMGSA8EGA8NPB","serviceName":{"tsKey":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDNCMFMKMGSA8EGA8NPB.name","tsNs":"org-data","defaultText":"Call a crisis help line for youth"},"serviceCategories":["housing.CATEGORYNAME","mental-health.CATEGORYNAME"],"offersRemote":true}] \ No newline at end of file diff --git a/packages/ui/mockData/json/service.forServiceModal.json b/packages/ui/mockData/json/service.forServiceModal.json index 66d55b3b28d..3c8c715ecd2 100644 --- a/packages/ui/mockData/json/service.forServiceModal.json +++ b/packages/ui/mockData/json/service.forServiceModal.json @@ -1 +1 @@ -{"id":"osvc_01GVH3VDMSN34BACQDMY6S5GPM","services":[{"tag":{"tsKey":"education-and-employment.career-counseling"}}],"serviceName":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDMSN34BACQDMY6S5GPM.name","ns":"org-data","tsKey":{"text":"Get education and employment services for youth ages 24 and under"}},"locations":[{"location":{"country":{"cca2":"US"}}}],"attributes":[{"attribute":{"id":"attr_01GW2HHFVGDTNW9PDQNXK6TF1T","tsKey":"cost.cost-free","tsNs":"attribute","icon":"carbon:piggy-bank","iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"cost","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01E4ENGJDYSQVYQQG5K7ZHPRGS","country":null,"language":null,"text":null,"govDist":null,"boolean":null,"data":null}},{"attribute":{"id":"attr_01GW2HHFVE9NE0NMDPK4X8WBNB","tsKey":"community.teens","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"community","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01E4ENGJDYXPSMZJYMYTSRQSBG","country":null,"language":null,"text":null,"govDist":null,"boolean":null,"data":null}},{"attribute":{"id":"attr_01GW2HHFVAKWSPFVAN9CYQE982","tsKey":"community.homeless","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"community","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01E4ENGJDYGA7WJPW6TE0XECN3","country":null,"language":null,"text":null,"govDist":null,"boolean":null,"data":null}},{"attribute":{"id":"attr_01GW2HHFVCKH2AQ2E1CKA1A8HP","tsKey":"community.lgbtq-youth","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"community","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01E4ENGJDY1GX5QJYCSVJ98HKM","country":null,"language":null,"text":null,"govDist":null,"boolean":null,"data":null}},{"attribute":{"id":"attr_01GW2HHFV3BADK80TG0DXXFPMM","tsKey":"additional.has-confidentiality-policy","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"additional-information","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01E4ENGJDY7F991XESQ0GGGZTR","country":null,"language":null,"text":null,"govDist":null,"boolean":null,"data":null}},{"attribute":{"id":"attr_01GW2HHFVGJ5GD2WHNJDPSFNRW","tsKey":"eligibility.time-appointment-required","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"eligibility-requirements","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01E4ENGJDYZTZ70RZDSX8X24WX","country":null,"language":null,"text":null,"govDist":null,"boolean":null,"data":null}},{"attribute":{"id":"attr_01GW2HHFVJ8K180CNX339BTXM2","tsKey":"lang.lang-offered","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"languages","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01GW2HT8C1N900BKNRTY39R58H","country":null,"language":{"languageName":"English","nativeName":"English"},"text":null,"govDist":null,"boolean":null,"data":null}},{"attribute":{"id":"attr_01GW2HHFVGSAZXGR4JAVHEK6ZC","tsKey":"eligibility.elig-age","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"eligibility-requirements","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01GW2HT8C1J8AQAEHVGANCYRPB","country":null,"language":null,"text":null,"govDist":null,"boolean":null,"data":{"json":{"json":{"max":24}}}}}],"hours":[],"description":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDMSN34BACQDMY6S5GPM.description","ns":"org-data","tsKey":{"text":"Larkin Street Academy Services offers job readiness, college readiness, computer classes, job placement and retention, internships, tutoring, GED tutoring and classes, secondary and post-secondary school enrollment and support, mindfulness, visual and performing arts. Offices are open Monday through Thursday, 9:00 AM - 16:00 PM, appointments only."}},"accessDetails":[{"attribute":{"id":"attr_01GW2HHFVMH6AE94EXN7T5A87C","tsKey":"serviceaccess.accesslocation","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"service-access-instructions","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01GW2HT8BWQ0WZ804A34QV7P0J","country":null,"language":null,"text":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.attribute.atts_01GW2HT8BWQ0WZ804A34QV7P0J","ns":"org-data","tsKey":{"text":"The above are drop-in service hours for education. Drop-in hours for employment services are Monday, Tuesday:10 a.m. to noon, and 2:30 to 4:30 p.m. Wednesday:10 a.m. to noon, and 1 to 2 p.m. Thursday:10 a.m. to noon, and 1 to 3 p.m. Friday:10 a.m. to 1 p.m."}},"govDist":null,"boolean":null,"data":{"json":{"_id":{"$oid":"5e7e4bd9d54f1760921a3aff"},"access_type":"location","access_value":"134 Golden Gate Ave, San Francisco, CA 94102","instructions":"The above are drop-in service hours for education. Drop-in hours for employment services are Monday, Tuesday:10 a.m. to noon, and 2:30 to 4:30 p.m. Wednesday:10 a.m. to noon, and 1 to 2 p.m. Thursday:10 a.m. to noon, and 1 to 3 p.m. Friday:10 a.m. to 1 p.m.","access_value_ES":"134 Golden Gate Ave, San Francisco, CA 94102","instructions_ES":"Visita para más información."}}}},{"attribute":{"id":"attr_01GW2HHFVMKTFWCKBVVFJ5GMY0","tsKey":"serviceaccess.accessphone","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"categories":[{"category":{"tag":"service-access-instructions","icon":null}}],"_count":{"parents":0,"children":0}},"supplement":{"id":"atts_01GW2HT8BWZG5BTQ57DAQHJZ5Z","country":null,"language":null,"text":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.attribute.atts_01GW2HT8BWZG5BTQ57DAQHJZ5Z","ns":"org-data","tsKey":{"text":"Call for more information."}},"govDist":null,"boolean":null,"data":{"json":{"_id":{"$oid":"5e7e4bd9d54f1760921a3b00"},"access_type":"phone","access_value":"415-673-0911","instructions":"Call for more information.","access_value_ES":"415-673-0911","instructions_ES":"Llama para más información."}}}}]} +{"id":"osvc_01GVH3VDMSN34BACQDMY6S5GPM","services":[{"tag":{"tsKey":"education-and-employment.career-counseling"}}],"serviceName":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDMSN34BACQDMY6S5GPM.name","ns":"org-data","tsKey":{"text":"Get education and employment services for youth ages 24 and under"}},"locations":[{"location":{"country":{"cca2":"US"}}}],"attributes":[{"attributeId":"attr_01GW2HHFVGSAZXGR4JAVHEK6ZC","supplementId":"atts_01GW2HT8C1J8AQAEHVGANCYRPB","tag":"elig-age","tsKey":"eligibility.elig-age","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"eligibility-requirements","active":true,"countryId":null,"country":null,"data":{"json":{"max":24}},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVGDTNW9PDQNXK6TF1T","supplementId":"atts_01E4ENGJDYSQVYQQG5K7ZHPRGS","tag":"cost-free","tsKey":"cost.cost-free","tsNs":"attribute","icon":"carbon:piggy-bank","iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"cost","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVE9NE0NMDPK4X8WBNB","supplementId":"atts_01E4ENGJDYXPSMZJYMYTSRQSBG","tag":"teens","tsKey":"community.teens","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"community","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVAKWSPFVAN9CYQE982","supplementId":"atts_01E4ENGJDYGA7WJPW6TE0XECN3","tag":"homeless","tsKey":"community.homeless","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"community","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVCKH2AQ2E1CKA1A8HP","supplementId":"atts_01E4ENGJDY1GX5QJYCSVJ98HKM","tag":"lgbtq-youth","tsKey":"community.lgbtq-youth","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"community","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFV3BADK80TG0DXXFPMM","supplementId":"atts_01E4ENGJDY7F991XESQ0GGGZTR","tag":"has-confidentiality-policy","tsKey":"additional.has-confidentiality-policy","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"additional-information","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVGJ5GD2WHNJDPSFNRW","supplementId":"atts_01E4ENGJDYZTZ70RZDSX8X24WX","tag":"time-appointment-required","tsKey":"eligibility.time-appointment-required","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"eligibility-requirements","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":null},{"attributeId":"attr_01GW2HHFVJ8K180CNX339BTXM2","supplementId":"atts_01GW2HT8C1N900BKNRTY39R58H","tag":"lang-offered","tsKey":"lang.lang-offered","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"languages","active":true,"countryId":null,"country":null,"data":null,"govDistId":null,"govDist":null,"languageId":"lang_0000000000N3K70GZXE29Z03A4","language":{"languageName":"English","nativeName":"English"},"boolean":null,"text":null}],"hours":[],"description":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.osvc_01GVH3VDMSN34BACQDMY6S5GPM.description","ns":"org-data","tsKey":{"text":"Larkin Street Academy Services offers job readiness, college readiness, computer classes, job placement and retention, internships, tutoring, GED tutoring and classes, secondary and post-secondary school enrollment and support, mindfulness, visual and performing arts. Offices are open Monday through Thursday, 9:00 AM - 16:00 PM, appointments only."}},"accessDetails":[{"attributeId":"attr_01GW2HHFVMH6AE94EXN7T5A87C","supplementId":"atts_01GW2HT8BWQ0WZ804A34QV7P0J","tag":"accesslocation","tsKey":"serviceaccess.accesslocation","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"service-access-instructions","active":true,"countryId":null,"country":null,"data":{"json":{"_id":{"$oid":"5e7e4bd9d54f1760921a3aff"},"access_type":"location","access_value":"134 Golden Gate Ave, San Francisco, CA 94102","instructions":"The above are drop-in service hours for education. Drop-in hours for employment services are Monday, Tuesday:10 a.m. to noon, and 2:30 to 4:30 p.m. Wednesday:10 a.m. to noon, and 1 to 2 p.m. Thursday:10 a.m. to noon, and 1 to 3 p.m. Friday:10 a.m. to 1 p.m.","access_value_ES":"134 Golden Gate Ave, San Francisco, CA 94102","instructions_ES":"Visita para más información."}},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.attribute.atts_01GW2HT8BWQ0WZ804A34QV7P0J","text":"The above are drop-in service hours for education. Drop-in hours for employment services are Monday, Tuesday:10 a.m. to noon, and 2:30 to 4:30 p.m. Wednesday:10 a.m. to noon, and 1 to 2 p.m. Thursday:10 a.m. to noon, and 1 to 3 p.m. Friday:10 a.m. to 1 p.m.","ns":"org-data"}},{"attributeId":"attr_01GW2HHFVMKTFWCKBVVFJ5GMY0","supplementId":"atts_01GW2HT8BWZG5BTQ57DAQHJZ5Z","tag":"accessphone","tsKey":"serviceaccess.accessphone","tsNs":"attribute","icon":null,"iconBg":null,"showOnLocation":null,"_count":{"parents":0,"children":0},"category":"service-access-instructions","active":true,"countryId":null,"country":null,"data":{"json":{"_id":{"$oid":"5e7e4bd9d54f1760921a3b00"},"access_type":"phone","access_value":"415-673-0911","instructions":"Call for more information.","access_value_ES":"415-673-0911","instructions_ES":"Llama para más información."}},"govDistId":null,"govDist":null,"languageId":null,"language":null,"boolean":null,"text":{"key":"orgn_01GVH3V3RCCBMFD55PWHR8AEC0.attribute.atts_01GW2HT8BWZG5BTQ57DAQHJZ5Z","text":"Call for more information.","ns":"org-data"}}]} \ No newline at end of file diff --git a/packages/ui/mockData/organization.ts b/packages/ui/mockData/organization.ts index 9e2ac025ada..df7703377e9 100644 --- a/packages/ui/mockData/organization.ts +++ b/packages/ui/mockData/organization.ts @@ -146,4 +146,11 @@ export const organization = { removed: input.deletedVals?.length ?? 0, }), }), + attachAttribute: getTRPCMock({ + path: ['organization', 'attachAttribute'], + type: 'mutation', + response: () => ({ + id: 'atts_NEW0ID', + }), + }), } satisfies MockHandlerObject<'organization'> & { searchDistanceLongTitle: HttpHandler } diff --git a/packages/ui/mockData/serviceArea.ts b/packages/ui/mockData/serviceArea.ts index 1503c2c6e48..9641c16b261 100644 --- a/packages/ui/mockData/serviceArea.ts +++ b/packages/ui/mockData/serviceArea.ts @@ -22,4 +22,14 @@ export const serviceArea = { }, }), }), + addToArea: getTRPCMock({ + path: ['serviceArea', 'addToArea'], + type: 'mutation', + response: () => ({ result: 'added' }), + }), + delFromArea: getTRPCMock({ + path: ['serviceArea', 'delFromArea'], + type: 'mutation', + response: () => ({ result: 'deleted' }), + }), } satisfies MockHandlerObject<'serviceArea'> diff --git a/packages/ui/modals/CoverageArea/hooks.ts b/packages/ui/modals/CoverageArea/hooks.ts index 8280909c3f0..83663eda9a5 100644 --- a/packages/ui/modals/CoverageArea/hooks.ts +++ b/packages/ui/modals/CoverageArea/hooks.ts @@ -1,7 +1,12 @@ import { useState } from 'react' +import { type Simplify } from 'type-fest' export const useServiceAreaSelections = () => { - const [selected, setSelected] = useState({ country: null, govDist: null, subDist: null }) + const [selected, setSelected] = useState>({ + country: null, + govDist: null, + subDist: null, + }) const setVal = { country: (value: string) => setSelected({ country: value, govDist: null, subDist: null }), govDist: (value: string) => setSelected((prev) => ({ ...prev, govDist: value, subDist: null })), diff --git a/packages/ui/modals/CoverageArea/index.stories.tsx b/packages/ui/modals/CoverageArea/index.stories.tsx index baa37946816..8f24396705e 100644 --- a/packages/ui/modals/CoverageArea/index.stories.tsx +++ b/packages/ui/modals/CoverageArea/index.stories.tsx @@ -23,6 +23,7 @@ export default { fieldOpt.govDists, serviceArea.getServiceArea, serviceArea.update, + serviceArea.addToArea, ], rqDevtools: true, whyDidYouRender: { collapseGroups: true }, diff --git a/packages/ui/modals/CoverageArea/index.tsx b/packages/ui/modals/CoverageArea/index.tsx index 6a0cacec76a..d2f511a053a 100644 --- a/packages/ui/modals/CoverageArea/index.tsx +++ b/packages/ui/modals/CoverageArea/index.tsx @@ -1,30 +1,20 @@ -import { zodResolver } from '@hookform/resolvers/zod' import { - Badge, Box, Button, type ButtonProps, - CloseButton, createPolymorphicComponent, - Grid, - Group, Modal, Select, Stack, - Text, Title, } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' -import { compareArrayVals } from 'crud-object-diff' -import compact from 'just-compact' import { type TFunction, useTranslation } from 'next-i18next' -import { forwardRef } from 'react' -import { useForm } from 'react-hook-form' +import { forwardRef, useEffect } from 'react' import { trpc as api } from '~ui/lib/trpcClient' import { useServiceAreaSelections } from './hooks' -import { ServiceAreaForm, type ZServiceAreaForm } from './schema' import { useStyles } from './styles' import { ModalTitle } from '../ModalTitle' @@ -35,270 +25,166 @@ const reduceDistType = (data: { tsNs: string; tsKey: string }[] | undefined, t: prev.add(translated) return prev }, new Set()) - return [...valueSet].sort().join('/') + return [...valueSet].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })) } -const CoverageAreaModal = forwardRef(({ id, ...props }, ref) => { - const { classes } = useStyles() - const { t, i18n } = useTranslation(['common', 'gov-dist']) - const countryTranslation = new Intl.DisplayNames(i18n.language, { type: 'region' }) - const [opened, { open, close }] = useDisclosure(true) //TODO: remove `true` when done with dev +const CoverageAreaModal = forwardRef( + ({ serviceArea, onSuccessAction, ...props }, ref) => { + const { classes } = useStyles() + const { t, i18n } = useTranslation(['common', 'gov-dist']) + const countryTranslation = new Intl.DisplayNames(i18n.language, { type: 'region' }) + const [modalOpened, modalHandler] = useDisclosure(false) - const [selected, setVal] = useServiceAreaSelections() + const [selected, setVal] = useServiceAreaSelections() - const { data: dataCountry } = api.fieldOpt.countries.useQuery( - { activeForOrgs: true }, - { - select: (data) => - data.map(({ id, cca2 }) => ({ value: id, label: countryTranslation.of(cca2), cca2 })) ?? [], - placeholderData: [], - } - ) - const { data: dataDistrict } = api.fieldOpt.govDists.useQuery( - { countryId: selected.country ?? '', parentsOnly: true }, - { - enabled: selected.country !== null, + useEffect(() => { + if (modalOpened === true) { + setVal.blank() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [modalOpened]) + + const { data: dataCountry } = api.fieldOpt.countries.useQuery( + { activeForOrgs: true }, + { + select: (data) => + data.map(({ id, cca2 }) => ({ value: id, label: countryTranslation.of(cca2), cca2 })) ?? [], + } + ) + const { data: dataDistrict } = api.fieldOpt.govDists.useQuery( + { countryId: selected.country ?? '', parentsOnly: true }, + { + enabled: selected.country !== null, + select: (data) => + data?.map(({ id, tsKey, tsNs, ...rest }) => ({ + value: id, + label: t(tsKey, { ns: tsNs }), + tsKey, + tsNs, + parent: null, + ...rest, + })) ?? [], + placeholderData: [], + } + ) + const { data: dataSubDist } = api.fieldOpt.getSubDistricts.useQuery(selected.govDist ?? '', { + enabled: selected.govDist !== null, select: (data) => data?.map(({ id, tsKey, tsNs, ...rest }) => ({ value: id, label: t(tsKey, { ns: tsNs }), tsKey, tsNs, - parent: null, ...rest, })) ?? [], placeholderData: [], + }) + + const placeHolders = { + first: t('select.base', { item: 'Country' }), + second: t('select.base', { + item: reduceDistType( + dataDistrict?.map(({ govDistType }) => govDistType), + t + ), + }), + third: t('select.base', { + item: reduceDistType( + dataSubDist?.map(({ govDistType }) => govDistType), + t + ), + }), } - ) - const { data: dataSubDist } = api.fieldOpt.getSubDistricts.useQuery(selected.govDist ?? '', { - enabled: selected.govDist !== null, - select: (data) => - data?.map(({ id, tsKey, tsNs, ...rest }) => ({ - value: id, - label: t(tsKey, { ns: tsNs }), - tsKey, - tsNs, - ...rest, - })) ?? [], - placeholderData: [], - }) - const apiUtils = api.useUtils() - const updateServiceArea = api.serviceArea.update.useMutation() - - const form = useForm({ - resolver: zodResolver(ServiceAreaForm), - defaultValues: async () => { - const data = await apiUtils.serviceArea.getServiceArea.fetch(id) - const formatted = { - id: data?.id ?? id, - countries: data?.countries ?? [], - districts: data?.districts ?? [], - } - return formatted - }, - }) - - const serviceAreaCountries = form.watch('countries') - const serviceAreaDistricts = form.watch('districts') - - const placeHolders = { - first: t('select.base', { item: 'Country' }), - second: t('select.base', { - item: reduceDistType( - dataDistrict?.map(({ govDistType }) => govDistType), - t - ), - }), - third: t('select.base', { - item: reduceDistType( - dataSubDist?.map(({ govDistType }) => govDistType), - t - ), - }), - } - - const handleAdd = () => { - switch (true) { - case !!selected.subDist: - case !!selected.govDist: { - const itemId = selected.subDist ?? selected.govDist - const valToAdd = selected.subDist - ? dataSubDist?.find(({ value }) => value === itemId) - : dataDistrict?.find(({ value }) => value === itemId) - if (!valToAdd) return - form.setValue( - 'districts', - [ - ...serviceAreaDistricts, - { - id: valToAdd.value, - tsKey: valToAdd.tsKey, - tsNs: valToAdd.tsNs, - parent: valToAdd.parent, - country: valToAdd.country, - }, - ], - { - shouldValidate: true, - } - ) - setVal.blank() - break - } - case !!selected.country: { - const valToAdd = dataCountry?.find(({ value }) => value === selected.country) - if (!valToAdd) return - form.setValue('countries', [...serviceAreaCountries, { id: valToAdd?.value, cca2: valToAdd?.cca2 }], { - shouldValidate: true, + const addServiceArea = api.serviceArea.addToArea.useMutation({ + onSuccess: (data) => { + if (onSuccessAction instanceof Function) { + onSuccessAction() + } + if (data?.result) { + modalHandler.close() + } + }, + }) + + const canAdd = !!selected.country + const handleAdd = () => { + if (selected.govDist || selected.subDist) { + const distToAdd = selected.subDist ?? selected.govDist + if (!distToAdd) { + throw new Error('Missing district') + } + addServiceArea.mutate({ + serviceArea, + govDistId: distToAdd, }) - setVal.blank() - break + } else if (selected.country) { + addServiceArea.mutate({ serviceArea, countryId: selected.country }) } } - } - - const activeAreas = compact( - [ - serviceAreaCountries?.map((country) => ( - - - {countryTranslation.of(country.cca2)} - - form.setValue( - 'countries', - serviceAreaCountries?.filter(({ id }) => id !== country.id) - ) - } - /> - - - )), - - // Display -> Country / District / Sub-District - serviceAreaDistricts?.map((govDist) => { - const { id, tsKey, tsNs, country, parent } = govDist - - const displayName = compact([ - country.cca2, - parent ? t(parent.tsKey, { ns: parent.tsNs }) : null, - t(tsKey, { ns: tsNs }), - ]).join(' → ') - - return ( - - - {displayName} - - form.setValue( - 'districts', - serviceAreaDistricts?.filter(({ id }) => id !== govDist.id) - ) - } - /> - - - ) - }), - ].flat() - ) - - const handleSave = () => { - const initialData = { - id: form.formState.defaultValues?.id, - countries: compact(form.formState.defaultValues?.countries?.map((country) => country?.id) ?? []), - districts: compact(form.formState.defaultValues?.districts?.map((district) => district?.id) ?? []), - } - const data = form.getValues() - const currentData = { - id: data.id, - countries: data.countries.map((country) => country.id), - districts: data.districts.map((district) => district.id), - } - const changes = { - id: data.id, - countries: compareArrayVals([initialData.countries, currentData.countries]), - districts: compareArrayVals([initialData.districts, currentData.districts]), - } - updateServiceArea.mutate(changes) - } - - return ( - <> - } - onClose={close} - opened={opened} - > - - - {t('portal-module.service-area')} - ({ ...theme.other.utilityFonts.utility4, color: 'black' })}> - {`${t('organization')}: `} - - - - {activeAreas} - - - theme.other.utilityFonts.utility1}> - {t('add', { - item: '$t(portal-module.service-area)', - })} - - - - + return ( + <> + } + onClose={modalHandler.close} + opened={modalOpened} + > + + + + {t('add', { + item: '$t(portal-module.service-area)', + })} + + + + + - {selected.country && !!dataDistrict?.length && ( - - )} - - - - - - + )} + {selected.govDist && !!dataSubDist?.length && ( + + } + case FieldType.number: { + return } - case 'numRange': - case 'numMinMaxOrRange': { - return ( - - - - - ) + case FieldType.currency: { + return } } - })() + } return ( - - {body} - - + + {schema.flatMap((schema) => { + if (Array.isArray(schema)) { + return {schema.map(renderField)} + } else { + return renderField(schema) + } + })} + ) } interface SuppDataProps { - handler: (data?: object) => void //MouseEventHandler - schema: LiteralUnion + // schema: LiteralUnion + schema: FieldAttributes[] | FieldAttributes[][] } -const SuppLang = ({ handler }: SuppLangProps) => { - const form = useFormContext() - const { t } = useTranslation('common') - const [listOptions, setListOptions] = useState() - api.fieldOpt.languages.useQuery(undefined, { - onSuccess: (data) => - setListOptions(data.map(({ id, languageName }) => ({ value: id, label: languageName }))), +const SuppLang = () => { + const { control } = useFormContext() + const { data: listOptions } = api.fieldOpt.languages.useQuery(undefined, { + select: (data) => data.map(({ id, languageName }) => ({ value: id, label: languageName })), }) return ( - {listOptions && ( - } + {/* + */} ) } -interface SuppLangProps { - handler: (value?: string) => void -} -interface LangList { - value: string - label: string -} - const GeoItem = forwardRef(({ flag, label, ...props }, ref) => { return (
@@ -139,107 +104,86 @@ const GeoItem = forwardRef(({ flag, label, ...prop }) GeoItem.displayName = 'GeoItem' -const SuppGeo = ({ handler, countryOnly }: SuppGeoProps) => { - const form = useFormContext() +const SuppGeo = ({ countryOnly }: SuppGeoProps) => { + // const { control } = useFormContext() const { t } = useTranslation(['country', 'gov-dist']) - const [primaryList, setPrimaryList] = useState() + // const [primaryList, setPrimaryList] = useState() const [secondaryList, setSecondaryList] = useState() const [tertiaryList, setTertiaryList] = useState< NonNullable[number]['subDistricts'] | undefined >() - const [primarySearch, onPrimarySearch] = useState() - const [secondarySearch, onSecondarySearch] = useState() - const [tertiarySearch, onTertiarySearch] = useState() - const countries = api.fieldOpt.countries.useQuery(undefined, { - enabled: Boolean(countryOnly), - onSuccess: (data) => - setPrimaryList(data.map(({ id, name, flag }) => ({ value: id, label: name, flag: flag ?? undefined }))), + const [primarySearch, setPrimarySearch] = useState(null) + const [secondarySearch, setSecondarySearch] = useState(null) + const [tertiarySearch, setTertiarySearch] = useState(null) + + // const [finalValue, setFinalValue] = useState(null) + // const [fieldName, setFieldName] = useState | undefined>( + // countryOnly ? 'countryId' : undefined + // ) + + const { data: countryList, ...countries } = api.fieldOpt.countries.useQuery(undefined, { + enabled: countryOnly ?? false, + select: (data) => data.map(({ id, name, flag }) => ({ value: id, label: name, flag: flag ?? undefined })), }) - api.fieldOpt.govDistsByCountry.useQuery(undefined, { + const { data: distByCountryList } = api.fieldOpt.govDistsByCountry.useQuery(undefined, { enabled: !countryOnly, - onSuccess: (data) => { - setPrimaryList( - data.map(({ id, tsKey, tsNs, flag, govDist }) => ({ - value: id, - label: t(tsKey, { ns: tsNs }), - flag: flag ?? undefined, - districts: govDist, - })) - ) - }, + select: (data) => + data.map(({ id, tsKey, tsNs, flag, govDist }) => ({ + value: id, + label: t(tsKey, { ns: tsNs }), + flag: flag ?? undefined, + districts: govDist, + })), }) - - useEffect(() => { - if (form.values.supplement?.govDistId && secondaryList) { - const secondarySelected = secondaryList.find(({ id }) => id === form.values.supplement?.govDistId) - if (secondarySelected && secondarySelected.subDistricts.length) { - form.setFieldValue('supplement.subDistId', undefined) - onTertiarySearch('') - setTertiaryList(secondarySelected.subDistricts) - } else if (secondarySelected && !secondarySelected.subDistricts.length) { - setTertiaryList(undefined) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form.values.supplement?.govDistId]) - - useEffect(() => { - if (form.values.supplement?.countryId && !countryOnly && primaryList) { - const primarySelected = primaryList.find(({ value }) => value === form.values.supplement?.countryId) - if (primarySelected && primarySelected.districts?.length) { - setSecondaryList(primarySelected.districts) - } else if (primarySelected && !primarySelected.districts?.length) { - onSecondarySearch('') - setSecondaryList(undefined) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form.values.supplement?.countryId, countryOnly]) + const primaryList = countryOnly ? countryList : distByCountryList if (!primaryList && !countries.isSuccess) return <>Loading... return ( {primaryList && ( - ({ value: id, label: t(tsKey, { ns: tsNs }) satisfies string, }))} searchable - searchValue={secondarySearch} - onSearchChange={onSecondarySearch} + searchValue={secondarySearch ?? undefined} + onSearchChange={setSecondarySearch} itemComponent={GeoItem} - {...form.getInputProps('supplement.govDistId')} + // control={control} + name='govDistId' + // {...form.getInputProps('supplement.govDistId')} /> )} {tertiaryList && ( - { - if (!e) return - setAttrCat(e) - }} - withinPortal - searchable - /> - )} -