Skip to content

Commit c528c45

Browse files
committed
feat: add auto-generate asset barcode preference and related functionality
1 parent eb80fe7 commit c528c45

29 files changed

Lines changed: 218 additions & 50 deletions

File tree

api/src/main/java/com/grash/dto/GeneralPreferencesPatchDTO.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,49 @@ public class GeneralPreferencesPatchDTO {
2222

2323
@Schema(description = "Language preference")
2424
private Language language;
25-
25+
2626
@Schema(description = "Currency", implementation = IdDTO.class)
2727
private Currency currency;
28-
28+
2929
@Schema(description = "Business type")
3030
private BusinessType businessType;
31-
31+
3232
@Schema(description = "Date format")
3333
private DateFormat dateFormat;
34-
34+
3535
@Schema(description = "Time zone")
3636
private String timeZone;
37-
37+
3838
@Schema(description = "Auto-assign work orders flag")
3939
private boolean autoAssignWorkOrders;
40-
40+
4141
@Schema(description = "Auto-assign requests flag")
4242
private boolean autoAssignRequests;
43-
43+
4444
@Schema(description = "Disable notifications for closed work orders")
4545
private boolean disableClosedWorkOrdersNotif;
46-
46+
4747
@Schema(description = "Ask for feedback on work order closed")
4848
private boolean askFeedBackOnWOClosed;
49-
49+
5050
@Schema(description = "Include labor cost in total cost")
5151
private boolean laborCostInTotalCost;
52-
52+
5353
@Schema(description = "Allow work order updates for requesters")
5454
private boolean woUpdateForRequesters;
55-
55+
5656
@Schema(description = "Simplified work order mode")
5757
private boolean simplifiedWorkOrder;
58-
58+
5959
@Schema(description = "Days before preventive maintenance notification")
6060
private int daysBeforePrevMaintNotification;
61-
61+
6262
@Schema(description = "CSV separator character")
6363
private String csvSeparator;
6464

65+
@Schema(description = "Automatically generate asset barcode")
66+
private boolean autoGenerateAssetBarcode;
67+
6568
public void setDaysBeforePrevMaintNotification(int daysBeforePrevMaintNotification) {
6669
if (daysBeforePrevMaintNotification < 0)
6770
throw new CustomException("Invalid daysBeforePrevMaintNotification", HttpStatus.BAD_REQUEST);

api/src/main/java/com/grash/model/GeneralPreferences.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ public class GeneralPreferences {
6666
@Schema(description = "Days before preventive maintenance notification")
6767
private int daysBeforePrevMaintNotification = 1;
6868

69+
@Schema(description = "Automatically generate asset barcode")
70+
private boolean autoGenerateAssetBarcode;
71+
6972
@NotBlank
7073
@Schema(description = "CSV separator character", requiredMode = Schema.RequiredMode.REQUIRED)
7174
private String csvSeparator = ",";

api/src/main/java/com/grash/service/AssetService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ public Asset create(Asset asset, User user) {
8989
throw new CustomException("You need a license to add a child asset to another asset.",
9090
HttpStatus.FORBIDDEN);
9191
asset.setCustomId(getAssetNumber(company));
92-
92+
if ((asset.getBarCode() == null || asset.getBarCode().isBlank()) && user.getCompany().getCompanySettings().getGeneralPreferences().isAutoGenerateAssetBarcode()) {
93+
asset.setBarCode(UUID.randomUUID().toString());
94+
}
9395
Asset savedAsset = assetRepository.saveAndFlush(asset);
9496
em.refresh(savedAsset);
9597
Map<String, Object> webhookPayload = new HashMap<>();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<databaseChangeLog
3+
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
6+
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
7+
8+
<changeSet id="1779804618-1" author="Ibrahima G. Coulibaly">
9+
<addColumn tableName="general_preferences">
10+
<column name="auto_generate_asset_barcode" type="boolean" defaultValue="false">
11+
<constraints nullable="false" />
12+
</column>
13+
</addColumn>
14+
</changeSet>
15+
</databaseChangeLog>

api/src/main/resources/db/master.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,6 @@
209209
relativeToChangelogFile="true"/>
210210
<include file="changelog/2026_05_05_00000000001_create_keygen_request_tracker.xml"
211211
relativeToChangelogFile="true"/>
212+
<include file="changelog/2026_05_26_1779804618_autoGenerateAssetBarcode.xml"
213+
relativeToChangelogFile="true"/>
212214
</databaseChangeLog>

frontend/src/content/own/Assets/Show/AssetDetails.tsx

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,40 @@ import * as React from 'react';
3434
import useAuth from '../../../../hooks/useAuth';
3535
import { useNavigate } from 'react-router-dom';
3636
import { getCustomFieldValuesForDetails } from '../../type';
37+
import { QRCodeSVG } from 'qrcode.react';
3738

3839
interface PropsType {
3940
asset: AssetDTO;
4041
loading: boolean;
4142
}
43+
const downloadQRCode = (value: string) => {
44+
const svgElement = document.getElementById(`qr-code-${value}`);
45+
if (!svgElement) return;
4246

43-
const LabelWrapper = styled(Box)(
44-
({ theme }) => `
45-
font-size: ${theme.typography.pxToRem(10)};
46-
font-weight: bold;
47-
text-transform: uppercase;
48-
border-radius: ${theme.general.borderRadiusSm};
49-
padding: ${theme.spacing(0.9, 1.5, 0.7)};
50-
line-height: 1;
51-
`
52-
);
47+
const svgData = new XMLSerializer().serializeToString(svgElement);
48+
const canvas = document.createElement('canvas');
49+
const ctx = canvas.getContext('2d');
50+
const img = new Image();
51+
52+
canvas.width = 120;
53+
canvas.height = 120;
54+
55+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
56+
const url = URL.createObjectURL(svgBlob);
57+
58+
img.onload = () => {
59+
ctx?.drawImage(img, 0, 0);
60+
URL.revokeObjectURL(url);
61+
62+
const pngUrl = canvas.toDataURL('image/png');
63+
const link = document.createElement('a');
64+
link.href = pngUrl;
65+
link.download = `qr-code-${value}.png`;
66+
link.click();
67+
};
68+
69+
img.src = url;
70+
};
5371
const AssetDetails = ({ asset, loading }: PropsType) => {
5472
const { t }: { t: any } = useTranslation();
5573
const theme = useTheme();
@@ -73,7 +91,7 @@ const AssetDetails = ({ asset, loading }: PropsType) => {
7391
: null
7492
},
7593
{ label: t('area'), value: asset?.area },
76-
{ label: t('barcode'), value: asset?.barCode }
94+
{ label: t('barcode'), value: asset?.barCode, barcode: true }
7795
];
7896
const moreInfosFields = [
7997
{
@@ -95,18 +113,34 @@ const AssetDetails = ({ asset, loading }: PropsType) => {
95113
];
96114
const BasicField = ({
97115
label,
98-
value
116+
value,
117+
barcode
99118
}: {
100119
label: string | number;
101120
value: string | number;
121+
barcode?: boolean;
102122
}) => {
103123
return value ? (
104124
<Grid item xs={12}>
105125
<Stack spacing={5} direction="row">
106126
<Typography variant="h6" fontWeight="bold">
107127
{label}
108128
</Typography>
109-
<Typography variant="h6">{value}</Typography>
129+
<Box>
130+
<Typography variant="h6">{value}</Typography>
131+
{barcode && value && (
132+
<QRCodeSVG
133+
id={`qr-code-${value}`}
134+
value={value.toString()}
135+
size={120}
136+
level="H"
137+
onClick={() => {
138+
downloadQRCode(value.toString());
139+
}}
140+
style={{ marginTop: theme.spacing(1), cursor: 'pointer' }}
141+
/>
142+
)}
143+
</Box>
110144
</Stack>
111145
<Divider sx={{ mt: 1 }} />
112146
</Grid>

frontend/src/content/own/Settings/Features/Asset/index.tsx

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,93 @@
11
import { Box, Button, Grid } from '@mui/material';
22
import { useTranslation } from 'react-i18next';
33
import { useNavigate } from 'react-router-dom';
4-
import { ChevronRight } from '@mui/icons-material';
4+
import { Formik } from 'formik';
5+
import CustomSwitch from '../../../components/form/CustomSwitch';
6+
import useAuth from '../../../../../hooks/useAuth';
7+
import { GeneralPreferences } from '../../../../../models/owns/generalPreferences';
58
import SettingsSection from '../../components/SettingsSection';
9+
import { ChevronRight } from '@mui/icons-material';
610

711
function AssetSettings() {
812
const { t }: { t: any } = useTranslation();
913
const navigate = useNavigate();
14+
const { patchGeneralPreferences, companySettings } = useAuth();
15+
const { generalPreferences } = companySettings;
16+
17+
const switches: {
18+
title: string;
19+
description: string;
20+
name: keyof GeneralPreferences;
21+
}[] = [
22+
{
23+
title: t('auto_generate_asset_barcode'),
24+
description: t('auto_generate_asset_barcode_description'),
25+
name: 'autoGenerateAssetBarcode'
26+
}
27+
];
28+
29+
const onSubmit = async (
30+
_values,
31+
{ resetForm, setErrors, setStatus, setSubmitting }
32+
) => {};
1033

1134
return (
1235
<Grid item xs={12}>
1336
<Box p={4}>
14-
<SettingsSection title={t('customize_form')}>
15-
<Box display="flex" flexDirection="column" gap={2}>
16-
<Button
17-
variant="text"
18-
endIcon={<ChevronRight />}
19-
onClick={() =>
20-
navigate('/app/settings/features/asset/custom-fields')
21-
}
22-
sx={{
23-
justifyContent: 'space-between',
24-
textTransform: 'none'
25-
}}
26-
>
27-
{t('configure_fields')}
28-
</Button>
29-
</Box>
30-
</SettingsSection>
37+
<Formik
38+
initialValues={generalPreferences}
39+
validationSchema={undefined}
40+
onSubmit={onSubmit}
41+
>
42+
{({
43+
errors,
44+
handleBlur,
45+
handleChange,
46+
handleSubmit,
47+
isSubmitting,
48+
touched,
49+
values
50+
}) => (
51+
<form onSubmit={handleSubmit}>
52+
<SettingsSection title={t('preferences')}>
53+
<Grid container spacing={2}>
54+
{switches.map((element) => (
55+
<CustomSwitch
56+
key={element.name}
57+
title={element.title}
58+
description={element.description}
59+
checked={values[element.name]}
60+
name={element.name}
61+
handleChange={(event) => {
62+
handleChange(event);
63+
patchGeneralPreferences({
64+
[element.name]: event.target.checked
65+
});
66+
}}
67+
/>
68+
))}
69+
</Grid>
70+
</SettingsSection>
71+
<SettingsSection title={t('customize_form')}>
72+
<Box display="flex" flexDirection="column" gap={2}>
73+
<Button
74+
variant="text"
75+
endIcon={<ChevronRight />}
76+
onClick={() =>
77+
navigate('/app/settings/features/asset/custom-fields')
78+
}
79+
sx={{
80+
justifyContent: 'space-between',
81+
textTransform: 'none'
82+
}}
83+
>
84+
{t('custom_fields')}
85+
</Button>
86+
</Box>
87+
</SettingsSection>
88+
</form>
89+
)}
90+
</Formik>
3191
</Box>
3292
</Grid>
3393
);

frontend/src/content/own/Settings/Features/Contractors/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function ContractorsSettings() {
2424
textTransform: 'none'
2525
}}
2626
>
27-
{t('configure_fields')}
27+
{t('custom_fields')}
2828
</Button>
2929
</Box>
3030
</SettingsSection>

frontend/src/content/own/Settings/Features/Location/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function LocationSettings() {
2424
textTransform: 'none'
2525
}}
2626
>
27-
{t('configure_fields')}
27+
{t('custom_fields')}
2828
</Button>
2929
</Box>
3030
</SettingsSection>

frontend/src/content/own/Settings/Features/Meters/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function MetersSettings() {
2424
textTransform: 'none'
2525
}}
2626
>
27-
{t('configure_fields')}
27+
{t('custom_fields')}
2828
</Button>
2929
</Box>
3030
</SettingsSection>

0 commit comments

Comments
 (0)