@clinicemr/esm-post-registration-redirect-app 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/802.js +1 -1
- package/dist/918.js +1 -1
- package/dist/openmrs-esm-post-registration-redirect-app.js.buildmanifest.json +6 -6
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/my-registered-patients.component.tsx +113 -103
- package/src/post-registration-redirect.component.tsx +8 -0
- package/src/registered-patients-store.ts +61 -0
package/dist/802.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app=globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app||[]).push([[802],{7237(e,t,n){n.d(t,{A:()=>a});var r=n(935),i=n.n(r)()((function(e){return e[1]}));i.push([e.id,".-esm-post-registration-redirect__my-registered-patients__container___qhgFl{padding:1.5rem 2rem;max-width:1200px;margin:0 auto}.-esm-post-registration-redirect__my-registered-patients__title___qy2WX{margin-bottom:1rem;font-weight:600}.-esm-post-registration-redirect__my-registered-patients__emptyTile___yyuEf{padding:2rem;text-align:center}",""]),i.locals={container:"-esm-post-registration-redirect__my-registered-patients__container___qhgFl",title:"-esm-post-registration-redirect__my-registered-patients__title___qy2WX",emptyTile:"-esm-post-registration-redirect__my-registered-patients__emptyTile___yyuEf"};const a=i},5802(e,t,n){n.r(t),n.d(t,{default:()=>
|
|
1
|
+
"use strict";(globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app=globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app||[]).push([[802],{7237(e,t,n){n.d(t,{A:()=>a});var r=n(935),i=n.n(r)()((function(e){return e[1]}));i.push([e.id,".-esm-post-registration-redirect__my-registered-patients__container___qhgFl{padding:1.5rem 2rem;max-width:1200px;margin:0 auto}.-esm-post-registration-redirect__my-registered-patients__title___qy2WX{margin-bottom:1rem;font-weight:600}.-esm-post-registration-redirect__my-registered-patients__emptyTile___yyuEf{padding:2rem;text-align:center}",""]),i.locals={container:"-esm-post-registration-redirect__my-registered-patients__container___qhgFl",title:"-esm-post-registration-redirect__my-registered-patients__title___qy2WX",emptyTile:"-esm-post-registration-redirect__my-registered-patients__emptyTile___yyuEf"};const a=i},5802(e,t,n){n.r(t),n.d(t,{default:()=>R});var r=n(1343),i=n(2339),a=n(1123),o=n(5803),l=n(5848),s=n(5940),c=n(8767),u=n(2591),d=n.n(u),p=n(1740),f=n.n(p),m=n(8128),g=n.n(m),y=n(855),v=n.n(y),h=n(3051),b=n.n(h),_=n(3656),w=n.n(_),O=n(7237),E={};E.styleTagTransform=w(),E.setAttributes=v(),E.insert=g().bind(null,"head"),E.domAPI=f(),E.insertStyleElement=b(),d()(O.A,E);const P=O.A&&O.A.locals?O.A.locals:void 0;function k(e,t,n,r,i,a,o){try{var l=e[a](o),s=l.value}catch(e){return void n(e)}l.done?t(s):Promise.resolve(s).then(r,i)}function S(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function j(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{},r=Object.keys(n);"function"==typeof Object.getOwnPropertySymbols&&(r=r.concat(Object.getOwnPropertySymbols(n).filter((function(e){return Object.getOwnPropertyDescriptor(n,e).enumerable})))),r.forEach((function(t){S(e,t,n[t])}))}return e}function A(e,t){return t=null!=t?t:{},Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(t)):function(e){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t.push.apply(t,n)}return t}(Object(t)).forEach((function(n){Object.defineProperty(e,n,Object.getOwnPropertyDescriptor(t,n))})),e}function N(e){return(t=function(){return function(e,t){var n,r,i,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),l=Object.defineProperty;return l(o,"next",{value:s(0)}),l(o,"throw",{value:s(1)}),l(o,"return",{value:s(2)}),"function"==typeof Symbol&&l(o,Symbol.iterator,{value:function(){return this}}),o;function s(l){return function(s){return function(l){if(n)throw new TypeError("Generator is already executing.");for(;o&&(o=0,l[0]&&(a=0)),a;)try{if(n=1,r&&(i=2&l[0]?r.return:l[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,l[1])).done)return i;switch(r=0,i&&(l=[2&l[0],i.value]),l[0]){case 0:case 1:i=l;break;case 4:return a.label++,{value:l[1],done:!1};case 5:a.label++,r=l[1],l=[0];continue;case 7:l=a.ops.pop(),a.trys.pop();continue;default:if(!((i=(i=a.trys).length>0&&i[i.length-1])||6!==l[0]&&2!==l[0])){a=0;continue}if(3===l[0]&&(!i||l[1]>i[0]&&l[1]<i[3])){a.label=l[1];break}if(6===l[0]&&a.label<i[1]){a.label=i[1],i=l;break}if(i&&a.label<i[2]){a.label=i[2],a.ops.push(l);break}i[2]&&a.ops.pop(),a.trys.pop();continue}l=t.call(e,a)}catch(e){l=[6,e],r=0}finally{n=i=0}if(5&l[0])throw l[1];return{value:l[0]?l[1]:void 0,done:!0}}([l,s])}}}(this,(function(t){switch(t.label){case 0:return 0===e.length?[2,[]]:[4,Promise.allSettled(e.map((function(e){return(0,s.openmrsFetch)("".concat(s.restBaseUrl,"/patient/").concat(e,"?v=").concat(encodeURIComponent("custom:(uuid,display,identifiers:(identifier,preferred),person:(gender,age))"))).then((function(e){return e.data}))})))];case 1:return[2,t.sent().filter((function(e){return"fulfilled"===e.status})).map((function(e){return e.value}))]}}))},function(){var e=this,n=arguments;return new Promise((function(r,i){var a=t.apply(e,n);function o(e){k(a,r,i,o,l,"next",e)}function l(e){k(a,r,i,o,l,"throw",e)}o(void 0)}))})();var t}const R=function(){var e,t,n=(0,i.useTranslation)().t,u=(0,s.useSession)(),d=(0,s.useConfig)(),p=null==u||null===(e=u.user)||void 0===e?void 0:e.uuid,f=(0,r.useMemo)((function(){return p?(0,c.f)(p).slice(0,d.pageSize):[]}),[p,d.pageSize]),m=p&&f.length>0?["my-registered-patients",p,f.map((function(e){return e.patientUuid})).join(",")]:null,g=(0,a.Ay)(m,(function(){return N(f.map((function(e){return e.patientUuid})))})),y=g.data,v=g.error,h=g.isLoading,b=(0,r.useMemo)((function(){var e=new Map((null!=y?y:[]).map((function(e){return[e.uuid,e]})));return f.map((function(t){var n,r,i,a,o,l,c,u=e.get(t.patientUuid);if(!u)return null;var d=null!==(n=null===(a=u.identifiers)||void 0===a?void 0:a.find((function(e){return e.preferred})))&&void 0!==n?n:null===(o=u.identifiers)||void 0===o?void 0:o[0];return{id:u.uuid,name:u.display,identifier:null!==(r=null==d?void 0:d.identifier)&&void 0!==r?r:"—",gender:null!==(i=null===(l=u.person)||void 0===l?void 0:l.gender)&&void 0!==i?i:"—",age:null!=(null===(c=u.person)||void 0===c?void 0:c.age)?String(u.person.age):"—",registeredOn:t.registeredAt?(0,s.formatDate)(new Date(t.registeredAt)):"—"}})).filter(Boolean)}),[y,f]),_=[{key:"name",header:n("patientName","Name")},{key:"identifier",header:n("identifier","Identifier")},{key:"gender",header:n("gender","Gender")},{key:"age",header:n("age","Age")},{key:"registeredOn",header:n("registeredOn","Registered on")},{key:"actions",header:""}];return 0===f.length?r.createElement("div",{className:P.container},r.createElement("h2",{className:P.title},n("myRegisteredPatients","My Registered Patients")),r.createElement(o.FAs,{className:P.emptyTile},r.createElement("p",null,n("noPatientsRegistered","You haven't registered any patients yet.")))):h?r.createElement("div",{className:P.container},r.createElement("h2",{className:P.title},n("myRegisteredPatients","My Registered Patients")),r.createElement(o.OOb,{headers:_,rowCount:Math.min(f.length,5)})):v?r.createElement("div",{className:P.container},r.createElement("h2",{className:P.title},n("myRegisteredPatients","My Registered Patients")),r.createElement(o.jeF,{kind:"error",title:n("errorLoading","Could not load patients you've registered."),subtitle:String(null!==(t=null==v?void 0:v.message)&&void 0!==t?t:""),lowContrast:!0,hideCloseButton:!0})):r.createElement("div",{className:P.container},r.createElement("h2",{className:P.title},n("myRegisteredPatients","My Registered Patients")),r.createElement(o.bQt,{rows:b,headers:_,useZebraStyles:!0},(function(e){var t=e.rows,i=e.headers,a=e.getHeaderProps,c=e.getRowProps,u=e.getTableProps;return r.createElement(o.K3K,null,r.createElement(o.XIK,u(),r.createElement(o.ndF,null,r.createElement(o.Hjg,null,i.map((function(e){return r.createElement(o.A0N,A(j({},a({header:e})),{key:e.key}),e.header)})))),r.createElement(o.BFY,null,t.map((function(e){return r.createElement(o.Hjg,A(j({},c({row:e})),{key:e.id}),e.cells.map((function(t){return"actions"===t.info.header?r.createElement(o.nA6,{key:t.id},r.createElement(o.$nd,{kind:"ghost",size:"sm",renderIcon:l.Qp,onClick:function(){return(0,s.navigate)({to:"${openmrsSpaBase}/patient/".concat(e.id,"/chart")})}},n("openChart","Open chart"))):r.createElement(o.nA6,{key:t.id},t.value)})))})))))})))}},8767(e,t,n){n.d(t,{D:()=>a,f:()=>o});var r="clinicemr.registeredPatients";function i(){try{var e=window.localStorage.getItem(r);if(!e)return{};var t=JSON.parse(e);return t&&"object"==(void 0===t?"undefined":(n=t)&&"undefined"!=typeof Symbol&&n.constructor===Symbol?"symbol":typeof n)?t:{}}catch(e){return{}}var n}function a(e,t){var n;if(e&&t){var a=i(),o=(null!==(n=a[e])&&void 0!==n?n:[]).filter((function(e){return e.patientUuid!==t}));o.unshift({patientUuid:t,registeredAt:(new Date).toISOString()}),a[e]=o.slice(0,200),function(e){try{window.localStorage.setItem(r,JSON.stringify(e))}catch(e){}}(a)}}function o(e){var t;return e&&null!==(t=i()[e])&&void 0!==t?t:[]}}}]);
|
package/dist/918.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app=globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app||[]).push([[918],{2918(e,t,
|
|
1
|
+
"use strict";(globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app=globalThis.webpackChunk_clinicemr_esm_post_registration_redirect_app||[]).push([[918],{2918(e,t,r){r.r(t),r.d(t,{default:()=>u});var i=r(1343),n=r(2339),a=r(5803),o=r(5940),s=r(8767);const u=function(){var e=(0,n.useTranslation)().t,t=(0,o.useSession)(),r=(0,o.useConfig)();return(0,i.useEffect)((function(){var e;if(null==t?void 0:t.user){var i,n,a,u=-1===(a=(n=window.location.pathname.replace(/\/$/,"").split("/")).findIndex((function(e){return"post-registration"===e})))||a+1>=n.length?"":null!==(i=n[a+1])&&void 0!==i?i:"",c=(null!==(e=t.user.roles)&&void 0!==e?e:[]).map((function(e){var t,r;return String(null!==(t=null!==(r=e.display)&&void 0!==r?r:e.name)&&void 0!==t?t:"")})).some((function(e){return e.trim().toLowerCase()===r.registrationClerkRoleName.trim().toLowerCase()}));u&&t.user.uuid&&(0,s.D)(t.user.uuid,u),c?(0,o.navigate)({to:"${openmrsSpaBase}/".concat(r.registeredPatientsListPath)}):u?(0,o.navigate)({to:"${openmrsSpaBase}/patient/".concat(u,"/chart")}):(0,o.navigate)({to:"${openmrsSpaBase}/home"})}}),[t,r.registrationClerkRoleName,r.registeredPatientsListPath]),i.createElement("div",{style:{padding:"2rem"}},i.createElement(a.OuH,{description:e("redirecting","Redirecting…")}))}},8767(e,t,r){r.d(t,{D:()=>a,f:()=>o});var i="clinicemr.registeredPatients";function n(){try{var e=window.localStorage.getItem(i);if(!e)return{};var t=JSON.parse(e);return t&&"object"==(void 0===t?"undefined":(r=t)&&"undefined"!=typeof Symbol&&r.constructor===Symbol?"symbol":typeof r)?t:{}}catch(e){return{}}var r}function a(e,t){var r;if(e&&t){var a=n(),o=(null!==(r=a[e])&&void 0!==r?r:[]).filter((function(e){return e.patientUuid!==t}));o.unshift({patientUuid:t,registeredAt:(new Date).toISOString()}),a[e]=o.slice(0,200),function(e){try{window.localStorage.setItem(i,JSON.stringify(e))}catch(e){}}(a)}}function o(e){var t;return e&&null!==(t=n()[e])&&void 0!==t?t:[]}}}]);
|
|
@@ -328,9 +328,9 @@
|
|
|
328
328
|
"initial": false,
|
|
329
329
|
"entry": false,
|
|
330
330
|
"recorded": false,
|
|
331
|
-
"size":
|
|
331
|
+
"size": 19326,
|
|
332
332
|
"sizes": {
|
|
333
|
-
"javascript":
|
|
333
|
+
"javascript": 19326
|
|
334
334
|
},
|
|
335
335
|
"names": [],
|
|
336
336
|
"idHints": [],
|
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
"802.js"
|
|
343
343
|
],
|
|
344
344
|
"auxiliaryFiles": [],
|
|
345
|
-
"hash": "
|
|
345
|
+
"hash": "8f90e2f442b22ddf",
|
|
346
346
|
"childrenByOrder": {}
|
|
347
347
|
},
|
|
348
348
|
{
|
|
@@ -400,9 +400,9 @@
|
|
|
400
400
|
"initial": false,
|
|
401
401
|
"entry": false,
|
|
402
402
|
"recorded": false,
|
|
403
|
-
"size":
|
|
403
|
+
"size": 5679,
|
|
404
404
|
"sizes": {
|
|
405
|
-
"javascript":
|
|
405
|
+
"javascript": 5679
|
|
406
406
|
},
|
|
407
407
|
"names": [],
|
|
408
408
|
"idHints": [],
|
|
@@ -414,7 +414,7 @@
|
|
|
414
414
|
"918.js"
|
|
415
415
|
],
|
|
416
416
|
"auxiliaryFiles": [],
|
|
417
|
-
"hash": "
|
|
417
|
+
"hash": "442cd1b78639a318",
|
|
418
418
|
"childrenByOrder": {}
|
|
419
419
|
}
|
|
420
420
|
]
|
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.2.0"},"pages":[{"component":"postRegistrationRedirect","route":"post-registration"},{"component":"myRegisteredPatients","route":"my-registered-patients"}],"extensions":[{"name":"registration-clerk-lockdown","component":"registrationClerkLockdown","slot":"top-nav-actions-slot"}],"version":"1.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.2.0"},"pages":[{"component":"postRegistrationRedirect","route":"post-registration"},{"component":"myRegisteredPatients","route":"my-registered-patients"}],"extensions":[{"name":"registration-clerk-lockdown","component":"registrationClerkLockdown","slot":"top-nav-actions-slot"}],"version":"1.2.0"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clinicemr/esm-post-registration-redirect-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Role-aware post-registration redirect for OpenMRS SSUUBO distro",
|
|
6
6
|
"browser": "dist/openmrs-esm-post-registration-redirect-app.js",
|
|
@@ -25,36 +25,37 @@ import {
|
|
|
25
25
|
formatDate,
|
|
26
26
|
} from '@openmrs/esm-framework';
|
|
27
27
|
import type { PostRegistrationRedirectConfig } from './config-schema';
|
|
28
|
+
import { getRegisteredPatients } from './registered-patients-store';
|
|
28
29
|
import styles from './my-registered-patients.scss';
|
|
29
30
|
|
|
30
|
-
interface
|
|
31
|
+
interface PatientResource {
|
|
31
32
|
uuid: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
display: string;
|
|
36
|
-
identifiers?: Array<{ identifier: string; preferred?: boolean }>;
|
|
37
|
-
person?: { gender?: string; age?: number };
|
|
38
|
-
};
|
|
39
|
-
auditInfo?: {
|
|
40
|
-
creator?: { uuid: string; display?: string };
|
|
41
|
-
};
|
|
33
|
+
display: string;
|
|
34
|
+
identifiers?: Array<{ identifier: string; preferred?: boolean }>;
|
|
35
|
+
person?: { gender?: string; age?: number };
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Use the built-in `full` representation. Some OpenMRS REST builds return
|
|
49
|
-
// 500 when asked to resolve deeply-nested custom reps (particularly the
|
|
50
|
-
// `auditInfo:(creator:(...))` shape) on the encounter search endpoint.
|
|
51
|
-
// `full` always works and gives us everything we need; we filter client-side.
|
|
52
|
-
const FROM_DATE_DAYS_BACK = 365;
|
|
38
|
+
const patientRep =
|
|
39
|
+
'custom:(uuid,display,identifiers:(identifier,preferred),person:(gender,age))';
|
|
53
40
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Fetch a list of patients by UUID via the OpenMRS REST patient endpoint.
|
|
43
|
+
* The single-patient endpoint (`/ws/rest/v1/patient/{uuid}`) is reliable on
|
|
44
|
+
* every OpenMRS REST build, unlike the encounter search endpoint which
|
|
45
|
+
* requires `patient` as a parameter and 500s otherwise.
|
|
46
|
+
*/
|
|
47
|
+
async function fetchPatients(uuids: string[]): Promise<PatientResource[]> {
|
|
48
|
+
if (uuids.length === 0) return [];
|
|
49
|
+
const results = await Promise.allSettled(
|
|
50
|
+
uuids.map((uuid) =>
|
|
51
|
+
openmrsFetch<PatientResource>(
|
|
52
|
+
`${restBaseUrl}/patient/${uuid}?v=${encodeURIComponent(patientRep)}`,
|
|
53
|
+
).then((res) => res.data),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
return results
|
|
57
|
+
.filter((r): r is PromiseFulfilledResult<PatientResource> => r.status === 'fulfilled')
|
|
58
|
+
.map((r) => r.value);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
const MyRegisteredPatients: React.FC = () => {
|
|
@@ -63,41 +64,44 @@ const MyRegisteredPatients: React.FC = () => {
|
|
|
63
64
|
const config = useConfig<PostRegistrationRedirectConfig>();
|
|
64
65
|
const currentUserUuid = session?.user?.uuid;
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
// Read the per-user list of registered patient UUIDs (most recent first).
|
|
68
|
+
const entries = useMemo(
|
|
69
|
+
() => (currentUserUuid ? getRegisteredPatients(currentUserUuid).slice(0, config.pageSize) : []),
|
|
70
|
+
[currentUserUuid, config.pageSize],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const swrKey = currentUserUuid && entries.length > 0
|
|
74
|
+
? ['my-registered-patients', currentUserUuid, entries.map((e) => e.patientUuid).join(',')]
|
|
75
|
+
: null;
|
|
72
76
|
|
|
73
|
-
const { data, error, isLoading } = useSWR
|
|
77
|
+
const { data, error, isLoading } = useSWR(swrKey, () => fetchPatients(entries.map((e) => e.patientUuid)));
|
|
74
78
|
|
|
75
79
|
const rows = useMemo(() => {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}, [data,
|
|
80
|
+
const patients = data ?? [];
|
|
81
|
+
const byUuid = new Map(patients.map((p) => [p.uuid, p]));
|
|
82
|
+
return entries
|
|
83
|
+
.map((entry) => {
|
|
84
|
+
const p = byUuid.get(entry.patientUuid);
|
|
85
|
+
if (!p) return null;
|
|
86
|
+
const preferredId = p.identifiers?.find((i) => i.preferred) ?? p.identifiers?.[0];
|
|
87
|
+
return {
|
|
88
|
+
id: p.uuid,
|
|
89
|
+
name: p.display,
|
|
90
|
+
identifier: preferredId?.identifier ?? '—',
|
|
91
|
+
gender: p.person?.gender ?? '—',
|
|
92
|
+
age: p.person?.age != null ? String(p.person.age) : '—',
|
|
93
|
+
registeredOn: entry.registeredAt ? formatDate(new Date(entry.registeredAt)) : '—',
|
|
94
|
+
};
|
|
95
|
+
})
|
|
96
|
+
.filter(Boolean) as Array<{
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
identifier: string;
|
|
100
|
+
gender: string;
|
|
101
|
+
age: string;
|
|
102
|
+
registeredOn: string;
|
|
103
|
+
}>;
|
|
104
|
+
}, [data, entries]);
|
|
101
105
|
|
|
102
106
|
const headers = [
|
|
103
107
|
{ key: 'name', header: t('patientName', 'Name') },
|
|
@@ -108,11 +112,22 @@ const MyRegisteredPatients: React.FC = () => {
|
|
|
108
112
|
{ key: 'actions', header: '' },
|
|
109
113
|
];
|
|
110
114
|
|
|
115
|
+
if (entries.length === 0) {
|
|
116
|
+
return (
|
|
117
|
+
<div className={styles.container}>
|
|
118
|
+
<h2 className={styles.title}>{t('myRegisteredPatients', 'My Registered Patients')}</h2>
|
|
119
|
+
<Tile className={styles.emptyTile}>
|
|
120
|
+
<p>{t('noPatientsRegistered', "You haven't registered any patients yet.")}</p>
|
|
121
|
+
</Tile>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
if (isLoading) {
|
|
112
127
|
return (
|
|
113
128
|
<div className={styles.container}>
|
|
114
129
|
<h2 className={styles.title}>{t('myRegisteredPatients', 'My Registered Patients')}</h2>
|
|
115
|
-
<DataTableSkeleton headers={headers} rowCount={5} />
|
|
130
|
+
<DataTableSkeleton headers={headers} rowCount={Math.min(entries.length, 5)} />
|
|
116
131
|
</div>
|
|
117
132
|
);
|
|
118
133
|
}
|
|
@@ -120,6 +135,7 @@ const MyRegisteredPatients: React.FC = () => {
|
|
|
120
135
|
if (error) {
|
|
121
136
|
return (
|
|
122
137
|
<div className={styles.container}>
|
|
138
|
+
<h2 className={styles.title}>{t('myRegisteredPatients', 'My Registered Patients')}</h2>
|
|
123
139
|
<InlineNotification
|
|
124
140
|
kind="error"
|
|
125
141
|
title={t('errorLoading', "Could not load patients you've registered.")}
|
|
@@ -134,53 +150,47 @@ const MyRegisteredPatients: React.FC = () => {
|
|
|
134
150
|
return (
|
|
135
151
|
<div className={styles.container}>
|
|
136
152
|
<h2 className={styles.title}>{t('myRegisteredPatients', 'My Registered Patients')}</h2>
|
|
137
|
-
{rows
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<TableRow>
|
|
148
|
-
{dataHeaders.map((h) => (
|
|
149
|
-
<TableHeader {...getHeaderProps({ header: h })} key={h.key}>
|
|
150
|
-
{h.header}
|
|
151
|
-
</TableHeader>
|
|
152
|
-
))}
|
|
153
|
-
</TableRow>
|
|
154
|
-
</TableHead>
|
|
155
|
-
<TableBody>
|
|
156
|
-
{dataRows.map((r) => (
|
|
157
|
-
<TableRow {...getRowProps({ row: r })} key={r.id}>
|
|
158
|
-
{r.cells.map((cell) =>
|
|
159
|
-
cell.info.header === 'actions' ? (
|
|
160
|
-
<TableCell key={cell.id}>
|
|
161
|
-
<Button
|
|
162
|
-
kind="ghost"
|
|
163
|
-
size="sm"
|
|
164
|
-
renderIcon={ArrowRight}
|
|
165
|
-
onClick={() =>
|
|
166
|
-
navigate({ to: `\${openmrsSpaBase}/patient/${r.id}/chart` })
|
|
167
|
-
}
|
|
168
|
-
>
|
|
169
|
-
{t('openChart', 'Open chart')}
|
|
170
|
-
</Button>
|
|
171
|
-
</TableCell>
|
|
172
|
-
) : (
|
|
173
|
-
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
174
|
-
),
|
|
175
|
-
)}
|
|
176
|
-
</TableRow>
|
|
153
|
+
<DataTable rows={rows} headers={headers} useZebraStyles>
|
|
154
|
+
{({ rows: dataRows, headers: dataHeaders, getHeaderProps, getRowProps, getTableProps }) => (
|
|
155
|
+
<TableContainer>
|
|
156
|
+
<Table {...getTableProps()}>
|
|
157
|
+
<TableHead>
|
|
158
|
+
<TableRow>
|
|
159
|
+
{dataHeaders.map((h) => (
|
|
160
|
+
<TableHeader {...getHeaderProps({ header: h })} key={h.key}>
|
|
161
|
+
{h.header}
|
|
162
|
+
</TableHeader>
|
|
177
163
|
))}
|
|
178
|
-
</
|
|
179
|
-
</
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
164
|
+
</TableRow>
|
|
165
|
+
</TableHead>
|
|
166
|
+
<TableBody>
|
|
167
|
+
{dataRows.map((r) => (
|
|
168
|
+
<TableRow {...getRowProps({ row: r })} key={r.id}>
|
|
169
|
+
{r.cells.map((cell) =>
|
|
170
|
+
cell.info.header === 'actions' ? (
|
|
171
|
+
<TableCell key={cell.id}>
|
|
172
|
+
<Button
|
|
173
|
+
kind="ghost"
|
|
174
|
+
size="sm"
|
|
175
|
+
renderIcon={ArrowRight}
|
|
176
|
+
onClick={() =>
|
|
177
|
+
navigate({ to: `\${openmrsSpaBase}/patient/${r.id}/chart` })
|
|
178
|
+
}
|
|
179
|
+
>
|
|
180
|
+
{t('openChart', 'Open chart')}
|
|
181
|
+
</Button>
|
|
182
|
+
</TableCell>
|
|
183
|
+
) : (
|
|
184
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
185
|
+
),
|
|
186
|
+
)}
|
|
187
|
+
</TableRow>
|
|
188
|
+
))}
|
|
189
|
+
</TableBody>
|
|
190
|
+
</Table>
|
|
191
|
+
</TableContainer>
|
|
192
|
+
)}
|
|
193
|
+
</DataTable>
|
|
184
194
|
</div>
|
|
185
195
|
);
|
|
186
196
|
};
|
|
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
3
3
|
import { InlineLoading } from '@carbon/react';
|
|
4
4
|
import { navigate, useConfig, useSession } from '@openmrs/esm-framework';
|
|
5
5
|
import type { PostRegistrationRedirectConfig } from './config-schema';
|
|
6
|
+
import { addRegisteredPatient } from './registered-patients-store';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Acts as a one-shot redirect target for the patient registration app.
|
|
@@ -30,6 +31,13 @@ const PostRegistrationRedirect: React.FC = () => {
|
|
|
30
31
|
(role) => role.trim().toLowerCase() === config.registrationClerkRoleName.trim().toLowerCase(),
|
|
31
32
|
);
|
|
32
33
|
|
|
34
|
+
// Record the freshly-registered patient against the current user so the
|
|
35
|
+
// My Registered Patients list can show them (server-side filtering by
|
|
36
|
+
// creator is not supported on the encounter REST endpoint).
|
|
37
|
+
if (patientUuid && session.user.uuid) {
|
|
38
|
+
addRegisteredPatient(session.user.uuid, patientUuid);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
if (isRegistrationClerk) {
|
|
34
42
|
navigate({ to: `\${openmrsSpaBase}/${config.registeredPatientsListPath}` });
|
|
35
43
|
} else if (patientUuid) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local persistent record of patients each user has registered.
|
|
3
|
+
*
|
|
4
|
+
* The OpenMRS REST encounter search endpoint requires a `patient` parameter,
|
|
5
|
+
* so we cannot list "all encounters of type X created by user Y" server-side.
|
|
6
|
+
* As a pragmatic MVP we keep a per-user list of registered patient UUIDs in
|
|
7
|
+
* localStorage and fetch each patient by UUID when rendering the list.
|
|
8
|
+
*
|
|
9
|
+
* Limitations: list is per-browser and per-device. Clearing site data wipes
|
|
10
|
+
* it. Switching browsers shows different lists. Acceptable for a single-clerk
|
|
11
|
+
* registration kiosk.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const STORAGE_KEY = 'clinicemr.registeredPatients';
|
|
15
|
+
const MAX_ENTRIES_PER_USER = 200;
|
|
16
|
+
|
|
17
|
+
interface RegisteredPatientEntry {
|
|
18
|
+
patientUuid: string;
|
|
19
|
+
registeredAt: string; // ISO timestamp
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface StoreShape {
|
|
23
|
+
// map: userUuid -> entries (most recent first)
|
|
24
|
+
[userUuid: string]: RegisteredPatientEntry[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readStore(): StoreShape {
|
|
28
|
+
try {
|
|
29
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
30
|
+
if (!raw) return {};
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
33
|
+
} catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeStore(store: StoreShape): void {
|
|
39
|
+
try {
|
|
40
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore quota errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function addRegisteredPatient(userUuid: string, patientUuid: string): void {
|
|
47
|
+
if (!userUuid || !patientUuid) return;
|
|
48
|
+
const store = readStore();
|
|
49
|
+
const existing = store[userUuid] ?? [];
|
|
50
|
+
// Move/insert this patient to the front, dedup
|
|
51
|
+
const filtered = existing.filter((e) => e.patientUuid !== patientUuid);
|
|
52
|
+
filtered.unshift({ patientUuid, registeredAt: new Date().toISOString() });
|
|
53
|
+
store[userUuid] = filtered.slice(0, MAX_ENTRIES_PER_USER);
|
|
54
|
+
writeStore(store);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getRegisteredPatients(userUuid: string): RegisteredPatientEntry[] {
|
|
58
|
+
if (!userUuid) return [];
|
|
59
|
+
const store = readStore();
|
|
60
|
+
return store[userUuid] ?? [];
|
|
61
|
+
}
|