Skip to content

Commit b1bb2ad

Browse files
committed
Merge branch 'v1.5.00'
2 parents 84da189 + 4edd6ca commit b1bb2ad

File tree

332 files changed

+13673
-907
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

332 files changed

+13673
-907
lines changed

client/README.md

+13
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ The functional areas are in `packages` as noted below. Mostly the packages are s
8686
- host: the wrapper for the application which then hosts individual packages. Components in here include the framework - navigation, footer, drawer, app bar - and pages such as login
8787
- system: packages which are re-used by other packages, such as `item` and `name` but are application-specific, which is why they are in here and not in `common` which is trying to be more application agnostic.
8888

89+
Packages should have the following hierarchy:
90+
91+
```mermaid
92+
graph RL;
93+
common --> config
94+
system --> common
95+
others["invoices, inventory, dashboard, etc"] --> system
96+
host-->others;
97+
```
98+
99+
Packages shouldn't have circular dependencies to other packages, i.e. packages can only import packages on their left (as shown in the diagram).
100+
For example, the `common` packages can be imported by the `system` package but the `common` package can't import the `system` or the `invoices` package.
101+
89102
Code is separated into functional areas, so that we can isolate bundles to these areas. These are not the same as areas in the site; there is a separation between code organisation and UI organisation - for example the site has `Distribution > Outbound Shipments` and `Replenishment > Inbound Shipments` and both of these are found in the `invoices` package in the code, as they are functionally very similar while being logically different.
90103

91104
Within each area you'll see a similar pattern of this for tabular data, which is pretty much everything:

client/packages/android/app/src/main/java/org/openmsupply/client/NativeApi.java

+31-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@
2929
import java.net.HttpURLConnection;
3030
import java.net.InetAddress;
3131
import java.net.MalformedURLException;
32+
import java.net.NetworkInterface;
33+
import java.net.SocketException;
3234
import java.net.URL;
3335
import java.nio.charset.StandardCharsets;
3436
import java.util.ArrayDeque;
3537
import java.util.Deque;
38+
import java.util.Enumeration;
3639

3740
import javax.net.ssl.SSLHandshakeException;
3841

@@ -344,15 +347,34 @@ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
344347
});
345348
}
346349

350+
// Attempt to get a non-loopback address for the local server
351+
// and fallback to loopback if there is an error
352+
private String getLocalAddress(NsdServiceInfo serviceInfo){
353+
try {
354+
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
355+
NetworkInterface intf = en.nextElement();
356+
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
357+
InetAddress inetAddress = enumIpAddr.nextElement();
358+
if (!inetAddress.isLoopbackAddress() && !inetAddress.isLinkLocalAddress() && inetAddress.isSiteLocalAddress()) {
359+
return inetAddress.getHostAddress();
360+
}
361+
}
362+
}
363+
} catch (SocketException ex) {
364+
Log.e(OM_SUPPLY, ex.toString());
365+
}
366+
return serviceInfo.getHost().getHostAddress();
367+
}
347368
private JSObject serviceInfoToObject(NsdServiceInfo serviceInfo) {
348369
String serverHardwareId = parseAttribute(serviceInfo, discoveryConstants.HARDWARE_ID_KEY);
370+
Boolean isLocal = serverHardwareId.equals(discoveryConstants.hardwareId);
349371
return new JSObject()
350372
.put("protocol", parseAttribute(serviceInfo, discoveryConstants.PROTOCOL_KEY))
351373
.put("clientVersion", parseAttribute(serviceInfo, discoveryConstants.CLIENT_VERSION_KEY))
352374
.put("port", serviceInfo.getPort())
353-
.put("ip", serviceInfo.getHost().getHostAddress())
375+
.put("ip", isLocal ? getLocalAddress(serviceInfo) : serviceInfo.getHost().getHostAddress())
354376
.put("hardwareId", serverHardwareId)
355-
.put("isLocal", serverHardwareId.equals(discoveryConstants.hardwareId));
377+
.put("isLocal", isLocal);
356378

357379
}
358380

@@ -472,6 +494,13 @@ public class FrontEndHost {
472494
JSObject data;
473495

474496
public FrontEndHost(JSObject data) {
497+
String ip = data.getString("ip");
498+
// attempt to translate loopback addresses to an actual IP address
499+
// so that we can display the local server IP for users to connect to the API
500+
if (data.getBool("isLocal") && (ip.equals("127.0.0.1") || ip.equals("localhost"))) {
501+
NsdServiceInfo serviceInfo = createLocalServiceInfo();
502+
data.put("ip", getLocalAddress(serviceInfo));
503+
}
475504
this.data = data;
476505
}
477506

client/packages/coldchain/README.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## Cold chain
2+
3+
### Overview
4+
5+
Cold chain Domain
6+
7+
### Intentions
8+
9+
The intention is that this package is responsible for cold chain domain features:
10+
11+
- Temperature logs
12+
- Monitoring
13+
- CCE
14+
15+
### Tips & Things to keep in mind
16+
17+
### Future considerations
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@openmsupply-client/coldchain",
3+
"version": "0.0.0",
4+
"sideEffects": false,
5+
"private": true,
6+
"main": "./src/index.ts",
7+
"devDependencies": {},
8+
"scripts": {
9+
"tsc": "tsc",
10+
"eslint": "eslint ./src"
11+
},
12+
"dependencies": {
13+
"@openmsupply-client/common": "^0.0.1",
14+
"@openmsupply-client/config": "^0.0.0",
15+
"@openmsupply-client/system": "^0.0.0",
16+
"react": "^18.0.0",
17+
"react-dom": "^18.0.0"
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import React, { PropsWithChildren } from 'react';
2+
import {
3+
BaseButton,
4+
Box,
5+
RouteBuilder,
6+
TemperatureBreachSortFieldInput,
7+
Typography,
8+
useMatch,
9+
useNavigate,
10+
useUrlQuery,
11+
} from '@openmsupply-client/common';
12+
import { useFormatDateTime, useTranslation } from '@common/intl';
13+
import { CircleAlertIcon } from '@common/icons';
14+
import { alpha, useTheme } from '@common/styles';
15+
import { AppRoute } from '@openmsupply-client/config';
16+
import {
17+
TemperatureBreachFragment,
18+
useTemperatureBreach,
19+
} from './Monitoring/api/TemperatureBreach';
20+
21+
const Text: React.FC<PropsWithChildren> = ({ children }) => (
22+
<Typography
23+
component="div"
24+
sx={{
25+
fontSize: '14px',
26+
display: 'flex',
27+
alignContent: 'center',
28+
flexWrap: 'wrap',
29+
}}
30+
>
31+
{children}
32+
</Typography>
33+
);
34+
35+
const Separator = () => (
36+
<Text>
37+
<Typography paddingX={0.5}>|</Typography>
38+
</Text>
39+
);
40+
41+
const DetailButton = ({ breach }: { breach: TemperatureBreachFragment }) => {
42+
const t = useTranslation('coldchain');
43+
const navigate = useNavigate();
44+
const { urlQuery } = useUrlQuery();
45+
const currentTab = (urlQuery['tab'] as string) ?? '';
46+
const isColdchain = useMatch(
47+
RouteBuilder.create(AppRoute.Coldchain).addWildCard().build()
48+
);
49+
50+
if (isColdchain && currentTab === t('label.breaches')) return null;
51+
52+
return (
53+
<BaseButton
54+
variant="contained"
55+
style={{ height: 32 }}
56+
onClick={() =>
57+
navigate(
58+
RouteBuilder.create(AppRoute.Coldchain)
59+
.addPart(AppRoute.Monitoring)
60+
.addQuery({ tab: t('label.breaches') })
61+
.addQuery({
62+
sort: TemperatureBreachSortFieldInput.StartDatetime,
63+
})
64+
.addQuery({ acknowledged: false })
65+
.addQuery({ 'sensor.name': breach.sensor?.name ?? '' })
66+
.build()
67+
)
68+
}
69+
>
70+
{t('button.view-details')}
71+
</BaseButton>
72+
);
73+
};
74+
const Location = ({ breach }: { breach: TemperatureBreachFragment }) => {
75+
const t = useTranslation('coldchain');
76+
77+
if (!breach?.location?.name) return null;
78+
return (
79+
<>
80+
<Separator />
81+
{!!breach?.location?.name && (
82+
<Text>
83+
{t('message.location')}
84+
<b style={{ paddingLeft: 4 }}>{breach.location.name}</b>
85+
</Text>
86+
)}
87+
</>
88+
);
89+
};
90+
91+
export const ColdchainNotification = () => {
92+
const theme = useTheme();
93+
const t = useTranslation('coldchain');
94+
const { data: breaches } = useTemperatureBreach.document.notifications({
95+
first: 1,
96+
offset: 0,
97+
sortBy: { key: 'startDatetime', direction: 'desc', isDesc: true },
98+
filterBy: { acknowledged: false },
99+
});
100+
const { localisedDistanceToNow } = useFormatDateTime();
101+
const breach = breaches?.nodes?.[0];
102+
103+
if (!breach) return null;
104+
105+
return (
106+
<Box
107+
sx={{
108+
borderBottom: '1px solid',
109+
borderBottomColor: 'primary.main',
110+
backgroundColor: alpha(theme.palette.primary.main, 0.075),
111+
flex: '0 0 54px',
112+
display: 'flex',
113+
paddingLeft: 2,
114+
alignContent: 'center',
115+
flexWrap: 'wrap',
116+
}}
117+
>
118+
<Box
119+
sx={{
120+
display: 'flex',
121+
alignContent: 'center',
122+
flexWrap: 'wrap',
123+
marginRight: 1,
124+
}}
125+
>
126+
<CircleAlertIcon
127+
fill={theme.palette.error.main}
128+
sx={{
129+
color: 'background.white',
130+
}}
131+
width={27}
132+
height={27}
133+
/>
134+
</Box>
135+
<Text>
136+
<b>
137+
{t('message.notification-breach-detected', {
138+
time: localisedDistanceToNow(breach.startDatetime),
139+
})}
140+
</b>
141+
</Text>
142+
<Separator />
143+
<Text>
144+
{t('message.last-temperature', {
145+
temperature: breach.maxOrMinTemperature,
146+
})}
147+
</Text>
148+
<Separator />
149+
<Text>
150+
{t('message.device')}
151+
<b style={{ paddingLeft: 4 }}>{breach.sensor?.name}</b>
152+
</Text>
153+
<Location breach={breach} />
154+
<Box
155+
sx={{
156+
justifyContent: 'flex-end',
157+
display: 'flex',
158+
flex: 1,
159+
marginRight: 2,
160+
height: '32px',
161+
}}
162+
>
163+
<DetailButton breach={breach} />
164+
</Box>
165+
</Box>
166+
);
167+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React, { FC } from 'react';
2+
import { RouteBuilder, Routes, Route } from '@openmsupply-client/common';
3+
import { AppRoute } from '@openmsupply-client/config';
4+
import { ListView } from './Sensor/ListView';
5+
import { ListView as MonitoringListView } from './Monitoring/ListView';
6+
7+
export const ColdchainService: FC = () => {
8+
const monitoringRoute = RouteBuilder.create(AppRoute.Monitoring).build();
9+
const sensorRoute = RouteBuilder.create(AppRoute.Sensors).build();
10+
11+
return (
12+
<Routes>
13+
<Route path={monitoringRoute} element={<MonitoringListView />} />
14+
<Route path={sensorRoute} element={<ListView />} />
15+
</Routes>
16+
);
17+
};
18+
19+
export default ColdchainService;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { FC } from 'react';
2+
import { DetailTabs } from '@common/components';
3+
import { TemperatureLogList } from './TemperatureLog';
4+
import { useTranslation } from '@common/intl';
5+
import {
6+
TemperatureBreachSortFieldInput,
7+
TemperatureLogSortFieldInput,
8+
} from '@openmsupply-client/common';
9+
import { TemperatureBreachList } from './TemperatureBreach';
10+
import { TemperatureChart } from './TemperatureChart';
11+
12+
export const ListView: FC = () => {
13+
const t = useTranslation('coldchain');
14+
15+
const tabs = [
16+
{
17+
Component: <TemperatureChart />,
18+
value: t('label.chart'),
19+
sort: {
20+
key: TemperatureLogSortFieldInput.Datetime,
21+
dir: 'desc' as 'desc' | 'asc',
22+
},
23+
},
24+
{
25+
Component: <TemperatureBreachList />,
26+
value: t('label.breaches'),
27+
sort: {
28+
key: TemperatureBreachSortFieldInput.StartDatetime,
29+
dir: 'desc' as 'desc' | 'asc',
30+
},
31+
},
32+
{
33+
Component: <TemperatureLogList />,
34+
value: t('label.log'),
35+
sort: {
36+
key: TemperatureLogSortFieldInput.Datetime,
37+
dir: 'desc' as 'desc' | 'asc',
38+
},
39+
},
40+
];
41+
42+
return <DetailTabs tabs={tabs} />;
43+
};

0 commit comments

Comments
 (0)