From 3222c05c2cf7f93641f0ee94d9d08236d492710e Mon Sep 17 00:00:00 2001 From: Christopher Seven Phiri Date: Tue, 16 Jul 2024 09:37:24 +0200 Subject: [PATCH] Add stats (#1) * add stats page * Update actions.ts --- package.json | 4 +- pnpm-lock.yaml | 448 ++++++++++++++++++ src/app/(dashboard)/page.tsx | 5 + src/app/(dashboard)/patients/content.tsx | 66 +++ src/app/(dashboard)/patients/filter-card.tsx | 69 --- src/app/(dashboard)/patients/page.tsx | 16 +- src/app/(dashboard)/patients/toolbar.tsx | 137 ------ src/app/(dashboard)/stats/actions.ts | 98 ++++ src/app/(dashboard)/stats/content.tsx | 28 ++ src/app/(dashboard)/stats/page.tsx | 25 + src/components/filters/context.tsx | 49 ++ src/components/filters/filter-card.tsx | 174 +++++++ .../filters}/input-filter.tsx | 0 src/components/filters/toolbar.tsx | 123 +++++ src/components/ui/calendar.tsx | 66 +++ src/components/ui/select.tsx | 160 +++++++ src/lib/fhir/bundle.ts | 24 +- src/lib/models/types.ts | 10 + src/model/filters.ts | 32 ++ 19 files changed, 1320 insertions(+), 214 deletions(-) create mode 100644 src/app/(dashboard)/patients/content.tsx delete mode 100644 src/app/(dashboard)/patients/filter-card.tsx delete mode 100644 src/app/(dashboard)/patients/toolbar.tsx create mode 100644 src/app/(dashboard)/stats/actions.ts create mode 100644 src/app/(dashboard)/stats/content.tsx create mode 100644 src/app/(dashboard)/stats/page.tsx create mode 100644 src/components/filters/context.tsx create mode 100644 src/components/filters/filter-card.tsx rename src/{app/(dashboard)/patients => components/filters}/input-filter.tsx (100%) create mode 100644 src/components/filters/toolbar.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/select.tsx diff --git a/package.json b/package.json index 1cb0aab..5a45963 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@smile-cdr/fhirts": "^2.0.7", @@ -28,6 +29,7 @@ "clsx": "^2.0.0", "cmdk": "^0.2.0", "cryptr": "^6.2.0", + "date-fns": "^3.6.0", "eslint": "8.48.0", "eslint-config-next": "13.4.19", "jsonwebtoken": "^9.0.2", @@ -39,6 +41,7 @@ "postcss": "8.4.29", "react": "18.2.0", "react-daisyui": "^5.0.0", + "react-day-picker": "^8.10.1", "react-dom": "18.2.0", "react-hook-form": "^7.45.4", "react-icons": "^5.2.1", @@ -54,4 +57,3 @@ "daisyui": "^3.6.4" } } - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 540ce06..bc4aa68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: '@radix-ui/react-popover': specifier: ^1.0.6 version: 1.0.6(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-separator': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) @@ -62,6 +65,9 @@ dependencies: cryptr: specifier: ^6.2.0 version: 6.2.0 + date-fns: + specifier: ^3.6.0 + version: 3.6.0 eslint: specifier: 8.48.0 version: 8.48.0 @@ -95,6 +101,9 @@ dependencies: react-daisyui: specifier: ^5.0.0 version: 5.0.0(daisyui@3.6.4)(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.3.3) + react-day-picker: + specifier: ^8.10.1 + version: 8.10.1(date-fns@3.6.0)(react@18.2.0) react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) @@ -661,6 +670,10 @@ packages: resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==} dev: false + /@radix-ui/number@1.1.0: + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + dev: false + /@radix-ui/primitive@1.0.0: resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} dependencies: @@ -673,6 +686,10 @@ packages: '@babel/runtime': 7.22.11 dev: false + /@radix-ui/primitive@1.1.0: + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + dev: false + /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} peerDependencies: @@ -694,6 +711,49 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-arrow@1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-compose-refs@1.0.0(react@18.2.0): resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} peerDependencies: @@ -717,6 +777,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-context@1.0.0(react@18.2.0): resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} peerDependencies: @@ -740,6 +813,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-context@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-dialog@1.0.0(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} peerDependencies: @@ -801,6 +887,19 @@ packages: react-remove-scroll: 2.5.5(@types/react@18.2.21)(react@18.2.0) dev: false + /@radix-ui/react-direction@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==} peerDependencies: @@ -842,6 +941,30 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.0(react@18.2.0): resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==} peerDependencies: @@ -865,6 +988,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-focus-guards@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-focus-scope@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==} peerDependencies: @@ -902,6 +1038,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-icons@1.3.0(react@18.2.0): resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -935,6 +1093,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-id@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-label@2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==} peerDependencies: @@ -1021,6 +1193,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-popper@1.2.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/rect': 1.1.0 + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==} peerDependencies: @@ -1054,6 +1255,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-portal@1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} peerDependencies: @@ -1122,6 +1344,66 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@radix-ui/react-select@2.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-direction': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.7(@types/react@18.2.21)(react@18.2.0) + dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} peerDependencies: @@ -1168,6 +1450,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-slot@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0): resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} peerDependencies: @@ -1191,6 +1487,19 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-use-controllable-state@1.0.0(react@18.2.0): resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==} peerDependencies: @@ -1216,6 +1525,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-use-escape-keydown@1.0.0(react@18.2.0): resolution: {integrity: sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==} peerDependencies: @@ -1241,6 +1564,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0): resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} peerDependencies: @@ -1264,6 +1601,32 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + + /@radix-ui/react-use-previous@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.21)(react@18.2.0): resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -1279,6 +1642,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-rect@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/rect': 1.1.0 + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + /@radix-ui/react-use-size@1.0.1(@types/react@18.2.21)(react@18.2.0): resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} peerDependencies: @@ -1294,12 +1671,50 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-size@1.1.0(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.2.0) + '@types/react': 18.2.21 + react: 18.2.0 + dev: false + + /@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: '@babel/runtime': 7.22.11 dev: false + /@radix-ui/rect@1.1.0: + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + dev: false + /@react-aria/focus@3.17.1(react@18.2.0): resolution: {integrity: sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==} peerDependencies: @@ -2043,6 +2458,10 @@ packages: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: false + /date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dev: false + /dayjs@1.11.9: resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} dev: false @@ -3946,6 +4365,16 @@ packages: tailwindcss: 3.3.3 dev: false + /react-day-picker@8.10.1(date-fns@3.6.0)(react@18.2.0): + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + date-fns: 3.6.0 + react: 18.2.0 + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -4031,6 +4460,25 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.21)(react@18.2.0) dev: false + /react-remove-scroll@2.5.7(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.21 + react: 18.2.0 + react-remove-scroll-bar: 2.3.4(@types/react@18.2.21)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.21)(react@18.2.0) + tslib: 2.6.2 + use-callback-ref: 1.3.0(@types/react@18.2.21)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.21)(react@18.2.0) + dev: false + /react-style-singleton@2.2.1(@types/react@18.2.21)(react@18.2.0): resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx index 6b2fdbc..9ffdb80 100644 --- a/src/app/(dashboard)/page.tsx +++ b/src/app/(dashboard)/page.tsx @@ -10,6 +10,10 @@ const actions = [ name: "Facilities", link: "/facilities", }, + { + name: "Stats", + link: "/stats", + }, ]; export default async function Page() { @@ -40,3 +44,4 @@ export default async function Page() { ); } + diff --git a/src/app/(dashboard)/patients/content.tsx b/src/app/(dashboard)/patients/content.tsx new file mode 100644 index 0000000..554ba33 --- /dev/null +++ b/src/app/(dashboard)/patients/content.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useGenericContext } from "@/components/filters/context"; +import { Patient } from "@/lib/fhir/types"; +import Link from "next/link"; +import React from "react"; + +type Props = {}; + +const Content = (props: Props) => { + const { data } = useGenericContext(); + const patients = data as Patient[]; + return ( +
+ {patients.map((patient) => ( +
+
+

+ {patient.name} + + View{" "} + +

+
+ + ART/HCC Number: {patient.identifier} + + Gender: {patient.gender} + BirthDate: {patient.birthDate} + {patient.phoneNumbers.map((value, index) => ( +
+ + Phone {index + 1}: {value.number} + + Owner: {value.owner} +
+ ))} + Active: {patient.active} + {patient.address.map((address, index) => ( +
+ Facility: {address.facility} + Physical: {address.physical} +
+ ))} + + Registration Date: {patient.registrationDate} + + + Registered By: {patient.registratedBy} + +
+
+
+ ))} + {patients.length == 0 && ( +
+
+

No Patients Found

+
+
+ )} +
+ ); +}; + +export default Content; diff --git a/src/app/(dashboard)/patients/filter-card.tsx b/src/app/(dashboard)/patients/filter-card.tsx deleted file mode 100644 index 3c70254..0000000 --- a/src/app/(dashboard)/patients/filter-card.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Filter, FilterFormData, FilterParamType } from "@/model/filters"; -import { useFieldArray, useFormContext } from "react-hook-form"; -import { Input } from "@/components/ui/input"; - -const GetInput = ({ type, name }: { type: FilterParamType; name: string }) => { - const { control } = useFormContext(); - if (type == FilterParamType.string) { - return ( - } - /> - ); - } - - return
Input
; -}; - -const FilterCard = ({ - filter, - arrayIndex, -}: { - filter: Filter; - arrayIndex: number; -}) => { - const { control } = useFormContext(); - const { fields } = useFieldArray( - { - control, - name: `filters.${arrayIndex}.params`, - } - ); - - return ( - - {filter.name} - - {fields.map((raw, index) => { - const param = filter.params.find((p) => p.name == raw.name)!; - return ( -
- - {param.title} - - - - - -
- ); - })} -
-
- ); -}; - -export default FilterCard; diff --git a/src/app/(dashboard)/patients/page.tsx b/src/app/(dashboard)/patients/page.tsx index 4982f7f..9c59c35 100644 --- a/src/app/(dashboard)/patients/page.tsx +++ b/src/app/(dashboard)/patients/page.tsx @@ -1,5 +1,7 @@ import { fetchData } from "./actions"; -import Toolbar from "./toolbar"; +import FilterToolbar from "../../../components/filters/toolbar"; +import { patientFilters } from "@/model/filters"; +import Content from "./content"; export default async function Page({ searchParams, @@ -8,11 +10,13 @@ export default async function Page({ }) { return (
-
{searchParams.q}
- {/* - */} - + + +
); } - diff --git a/src/app/(dashboard)/patients/toolbar.tsx b/src/app/(dashboard)/patients/toolbar.tsx deleted file mode 100644 index f90c8a7..0000000 --- a/src/app/(dashboard)/patients/toolbar.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import React from "react"; -import { DataFacetedFilter } from "./input-filter"; -import { FilterFormData, patientFilters } from "@/model/filters"; -import { Separator } from "@/components/ui/separator"; -import FilterCard from "./filter-card"; -import { useForm, FormProvider, Control, useFieldArray } from "react-hook-form"; -import { Button } from "@/components/ui/button"; -import { Patient } from "../../../lib/fhir/types"; -import Link from "next/link"; - -type Props = { - action: (data: FormData) => Promise; -}; - -const Toolbar = ({ action }: Props) => { - const methods = useForm({ - defaultValues: { - filters: [], - }, - }); - console.log(methods.formState.isDirty); - const [patients, setPatients] = React.useState([]); - const [loading, setLoading] = React.useState(false); - - const onSubmit = async (data: FilterFormData) => { - setLoading(true); - const form = new FormData(); - form.append("data", JSON.stringify(data)); - const responses = await action(form); - setPatients(responses); - setLoading(false); - }; - - return ( -
- -
- - -
-
- {loading && ( -
-

Fetching...

- -
- )} - {patients.map((patient) => ( -
-
-

- {patient.name} - - View{" "} - -

-
- - ART/HCC Number: {patient.identifier} - - Gender: {patient.gender} - BirthDate: {patient.birthDate} - {patient.phoneNumbers.map((value, index) => ( -
- - Phone {index + 1}: {value.number} - - Owner: {value.owner} -
- ))} - Active: {patient.active} - {patient.address.map((address, index) => ( -
- Facility: {address.facility} - Physical: {address.physical} -
- ))} - - Registration Date: {patient.registrationDate} - - - Registered By: {patient.registratedBy} - -
-
-
- ))} - {!loading && patients.length == 0 && ( -
-
-

No Patients Found

-
-
- )} -
-
- ); -}; - -const FormContainer = ({ - control, -}: { - control: Control; -}) => { - const { fields, append, prepend, remove, swap, move, insert } = useFieldArray( - { - control, - name: "filters", - } - ); - return ( -
- append(filter)} - deleteFilter={(filter) => - remove(fields.findIndex((f) => f.filterId == filter.filterId)) - } - clear={() => { - remove(); - }} - /> - - {fields.map((filter, index) => { - const data = patientFilters.find((f) => f.id == filter.filterId); - - return ; - })} - -
- ); -}; - -export default Toolbar; diff --git a/src/app/(dashboard)/stats/actions.ts b/src/app/(dashboard)/stats/actions.ts new file mode 100644 index 0000000..4eebca6 --- /dev/null +++ b/src/app/(dashboard)/stats/actions.ts @@ -0,0 +1,98 @@ +"use server"; + +import { fetchBundle } from "@/lib/fhir/bundle"; +import { LocationData, SummaryItem } from "@/lib/models/types"; +import { FilterFormData } from "@/model/filters"; +import { fhirR4 } from "@smile-cdr/fhirts"; +import { format } from "date-fns"; + +export async function fetchRequiredData() { + const locationQuery = paramGenerator("/Location", { + _count: 100, + type: "https://d-tree.org/fhir/location-type|facility", + }); + var bundle = await fetchBundle([locationQuery]); + const locations = getLocationData( + bundle.entry?.[0]?.resource as fhirR4.Bundle + ); + return { + locations, + }; +} + +export async function fetchData(formData: FormData) { + const data = JSON.parse( + formData.getAll("data")[0] as string + ) as FilterFormData; + + console.log(JSON.stringify(data)); + const baseFilter = data.filters.map((filter) => { + const temp: Record = {}; + if (filter.template == "_tag_location") { + const template = `http://smartregister.org/fhir/location-tag|${ + filter.params[0].value ?? "" + }`; + temp["_tag"] = template; + } else { + temp[filter.template] = filter.params[0].value ?? ""; + } + return temp; + }); + const query: Record = { + _summary: "count", + questionnaire: "patient-finish-visit", + }; + baseFilter.forEach((filter) => { + Object.assign(query, filter); + }); + if (query["date"]) { + query["authored"] = format(query["date"], "yyyy-MM-dd"); + delete query["date"]; + } + const allVisits = paramGenerator("/QuestionnaireResponse", query); + const bundle = await fetchBundle([allVisits]); + const summary: string[] = ["Total visits"]; + console.log(JSON.stringify(bundle)); + + return getResults(bundle, summary); +} + +const getLocationData = (bundle: fhirR4.Bundle | undefined): LocationData[] => { + if (bundle == undefined) { + return []; + } + return ( + bundle.entry?.map((entry) => { + return { + id: entry.resource?.id ?? "", + name: (entry.resource as fhirR4.Location)?.name ?? "", + }; + }) ?? [] + ); +}; + +const getResults = ( + bundle: fhirR4.Bundle | undefined, + summary: string[] +): SummaryItem[] => { + if (bundle == undefined) { + return []; + } + return ( + bundle.entry?.map((entry, idx) => { + return { + name: summary[idx], + value: (entry.resource as fhirR4.Bundle)?.total ?? 0, + }; + }) ?? [] + ); +}; + +const paramGenerator = ( + resources: string, + params: Record +) => { + return `${resources}?${Object.keys(params) + .map((key) => `${key}=${params[key]}`) + .join("&")}`; +}; diff --git a/src/app/(dashboard)/stats/content.tsx b/src/app/(dashboard)/stats/content.tsx new file mode 100644 index 0000000..eae2e28 --- /dev/null +++ b/src/app/(dashboard)/stats/content.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useGenericContext } from "@/components/filters/context"; +import { SummaryItem } from "@/lib/models/types"; +import React from "react"; + +type Props = {}; + +const Content = (props: Props) => { + const { data } = useGenericContext(); + const summaries = data as SummaryItem[]; + return ( +
+ {summaries.map((summary) => ( +
+
+

+ {summary.name} + {summary.value} +

+
+
+ ))} +
+ ); +}; + +export default Content; diff --git a/src/app/(dashboard)/stats/page.tsx b/src/app/(dashboard)/stats/page.tsx new file mode 100644 index 0000000..b0c3c62 --- /dev/null +++ b/src/app/(dashboard)/stats/page.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { fetchData, fetchRequiredData } from "./actions"; +import FilterToolbar from "@/components/filters/toolbar"; +import { statsFilters } from "@/model/filters"; +import Content from "./content"; + +type Props = {}; + +const Page = async (props: Props) => { + const data = await fetchRequiredData(); + return ( +
+ + + +
+ ); +}; + +export default Page; diff --git a/src/components/filters/context.tsx b/src/components/filters/context.tsx new file mode 100644 index 0000000..9531305 --- /dev/null +++ b/src/components/filters/context.tsx @@ -0,0 +1,49 @@ +import React, { + createContext, + useContext, + useState, + ReactNode, + Dispatch, + SetStateAction, +} from "react"; + +interface ContextProps { + data: T; + setData: Dispatch>; +} + +const createGenericContext = () => { + const GenericContext = createContext | undefined>(undefined); + + const useGenericContext = () => { + const context = useContext(GenericContext); + if (!context) { + throw new Error( + "useGenericContext must be used within a GenericContextProvider" + ); + } + return context; + }; + + const GenericContextProvider = ({ + children, + initialData, + }: { + children: ReactNode; + initialData: T; + }) => { + const [data, setData] = useState(initialData); + return ( + + {children} + + ); + }; + + return { useGenericContext, GenericContextProvider }; +}; + +export const { useGenericContext, GenericContextProvider } = + createGenericContext(); + +export default createGenericContext; diff --git a/src/components/filters/filter-card.tsx b/src/components/filters/filter-card.tsx new file mode 100644 index 0000000..2dfea4a --- /dev/null +++ b/src/components/filters/filter-card.tsx @@ -0,0 +1,174 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { IoIosCalendar as CalendarIcon } from "react-icons/io"; +import { Calendar } from "@/components/ui/calendar"; +import { Filter, FilterFormData, FilterParamType } from "@/model/filters"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "../ui/button"; +import { cn } from "@/lib/utils"; +import { format } from "date-fns"; + +const GetInput = ({ + type, + name, + prefileValue, +}: { + type: FilterParamType; + name: string; + prefileValue?: any; +}) => { + const { control } = useFormContext(); + console.log(type); + + if (type === FilterParamType.string) { + return ( + } + /> + ); + } else if (type === FilterParamType.select) { + return ( + ( + + )} + /> + ); + } else if (type === FilterParamType.date) { + return ( + ( + + + + + + + + + + // date > new Date() || date < new Date("1900-01-01") + // } + initialFocus + /> + + + + + )} + /> + ); + } + + return
Input
; +}; + +const FilterCard = ({ + filter, + arrayIndex, + prefillData, +}: { + filter: Filter; + arrayIndex: number; + prefillData?: any; +}) => { + const { control } = useFormContext(); + const { fields } = useFieldArray({ + control, + name: `filters.${arrayIndex}.params`, + }); + + return ( + + {filter.name} + + {fields.map((raw, index) => { + const param = filter.params.find((p) => p.name == raw.name)!; + return ( +
+ + {param.title} + + + + + +
+ ); + })} +
+
+ ); +}; + +export default FilterCard; diff --git a/src/app/(dashboard)/patients/input-filter.tsx b/src/components/filters/input-filter.tsx similarity index 100% rename from src/app/(dashboard)/patients/input-filter.tsx rename to src/components/filters/input-filter.tsx diff --git a/src/components/filters/toolbar.tsx b/src/components/filters/toolbar.tsx new file mode 100644 index 0000000..3f62d13 --- /dev/null +++ b/src/components/filters/toolbar.tsx @@ -0,0 +1,123 @@ +"use client"; + +import React from "react"; +import { DataFacetedFilter } from "./input-filter"; +import { Filter, FilterFormData, patientFilters } from "@/model/filters"; +import { Separator } from "@/components/ui/separator"; +import FilterCard from "./filter-card"; +import { useForm, FormProvider, Control, useFieldArray } from "react-hook-form"; +import { Button } from "@/components/ui/button"; +import { GenericContextProvider, useGenericContext } from "./context"; + +type Props = { + action: (data: FormData) => Promise; + defaultItem: T; + filters: Filter[]; + prefillData?: any; + children?: React.ReactElement; +}; + +const FilterToolbar = (props: Props) => { + return ( + + + + ); +}; + +const FilterToolbarContainer = ({ + action, + filters, + children, + prefillData, +}: Props) => { + const methods = useForm({ + defaultValues: { + filters: [], + }, + }); + console.log(methods.formState.isDirty); + const { setData } = useGenericContext(); + const [loading, setLoading] = React.useState(false); + + const onSubmit = async (data: FilterFormData) => { + setLoading(true); + const form = new FormData(); + form.append("data", JSON.stringify(data)); + const responses = await action(form); + setData(responses); + setLoading(false); + }; + + return ( +
+ +
+ + +
+
+ {loading && ( +
+

Fetching...

+ +
+ )} + {children} +
+
+ ); +}; + +const FormContainer = ({ + control, + filters, + prefillData, +}: { + control: Control; + filters: Filter[]; + prefillData?: any; +}) => { + const { fields, append, prepend, remove, swap, move, insert } = useFieldArray( + { + control, + name: "filters", + } + ); + return ( +
+ append(filter)} + deleteFilter={(filter) => + remove(fields.findIndex((f) => f.filterId == filter.filterId)) + } + clear={() => { + remove(); + }} + /> + + {fields.map((filter, index) => { + const data = filters.find((f) => f.id == filter.filterId); + console.log(JSON.stringify(filter)); + + return ( + + ); + })} + +
+ ); +}; + +export default FilterToolbar; diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx new file mode 100644 index 0000000..2f02434 --- /dev/null +++ b/src/components/ui/calendar.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { DayPicker } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +export type CalendarProps = React.ComponentProps + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..cbe5a36 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/lib/fhir/bundle.ts b/src/lib/fhir/bundle.ts index bc49103..0d8f4ca 100644 --- a/src/lib/fhir/bundle.ts +++ b/src/lib/fhir/bundle.ts @@ -1,6 +1,7 @@ -import { fhirR4 } from "@smile-cdr/fhirts"; +import { fhirR4, BundleUtilities } from "@smile-cdr/fhirts"; import { fhirServer } from "../api/axios"; import { AxiosError } from "axios"; +import { IBundle } from "@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IBundle"; export const pushResourceBundle = async (resources: fhirR4.Resource[]) => { try { @@ -30,3 +31,24 @@ export const pushResourceBundle = async (resources: fhirR4.Resource[]) => { } } }; + +export const fetchBundle = async ( + requests: string[] +): Promise => { + const bundle: IBundle = { + resourceType: "Bundle", + type: "transaction", + }; + bundle.entry = requests.map((request) => { + return { + request: { + method: "GET", + url: request, + }, + }; + }); + console.log(JSON.stringify(bundle)); + + const response = await fhirServer.post("/", bundle); + return response.data; +}; diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index 60fb5dc..9a8ccb4 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -21,3 +21,13 @@ export type CarePlanDataActivity = { taskExists: boolean; taskType: "normal" | "scheduled"; }; + +export type LocationData = { + id: string; + name: string; +} + +export type SummaryItem = { + name: string; + value: number +} diff --git a/src/model/filters.ts b/src/model/filters.ts index 4f32ca1..a647061 100644 --- a/src/model/filters.ts +++ b/src/model/filters.ts @@ -2,6 +2,7 @@ export interface Filter { id: string; name: string; template: string; + isObject?: boolean; params: FilterParams[]; } @@ -17,6 +18,7 @@ export interface FilterParams { name: string; type: FilterParamType; title: any; + prefillKey?: string; } export const patientFilters: Filter[] = [ @@ -75,6 +77,36 @@ export const patientFilters: Filter[] = [ }, ]; +export const statsFilters: Filter[] = [ + { + id: "filter-by-location", + name: "Search by Location", + template: "_tag_location", + isObject: true, + params: [ + { + name: "location", + title: "Enter facility", + type: FilterParamType.select, + prefillKey: "locations", + }, + ], + }, + { + id: "filter-by-date", + name: "Search by Date", + template: "date", + isObject: true, + params: [ + { + name: "date", + title: "Enter date", + type: FilterParamType.date, + }, + ], + }, +]; + export const filters = { patient: patientFilters, };