Initial commit edera-web edera-web origin/edera-web
authorRoberto Stomeo <Roberto Stomeo@dyrectalab.local>
Tue, 1 Jul 2025 15:04:35 +0000 (17:04 +0200)
committerRoberto Stomeo <Roberto Stomeo@dyrectalab.local>
Tue, 1 Jul 2025 15:04:35 +0000 (17:04 +0200)
193 files changed:
edera-web/.gitignore [new file with mode: 0644]
edera-web/.nvmrc [new file with mode: 0644]
edera-web/.prettierrc [new file with mode: 0644]
edera-web/.vscode/settings.json [new file with mode: 0644]
edera-web/README.md [new file with mode: 0644]
edera-web/bitbucket-pipelines.yml [new file with mode: 0644]
edera-web/config-overrides.js [new file with mode: 0644]
edera-web/jsconfig.json [new file with mode: 0644]
edera-web/package-dev.json [new file with mode: 0644]
edera-web/package.json [new file with mode: 0644]
edera-web/public/favicon-dark.png [new file with mode: 0644]
edera-web/public/favicon-light.png [new file with mode: 0644]
edera-web/public/index.html [new file with mode: 0644]
edera-web/scripts/docker/Dockerfile [new file with mode: 0644]
edera-web/scripts/kube/ingress.yml [new file with mode: 0644]
edera-web/scripts/kube/web.yml [new file with mode: 0644]
edera-web/scripts/service-account.json.base64 [new file with mode: 0644]
edera-web/scripts/set-env.sh [new file with mode: 0644]
edera-web/src/App.js [new file with mode: 0644]
edera-web/src/build.json [new file with mode: 0644]
edera-web/src/components/index.js [new file with mode: 0644]
edera-web/src/components/layout/FixedMainCard.js [new file with mode: 0644]
edera-web/src/components/layout/index.js [new file with mode: 0644]
edera-web/src/components/pages/CustomPage.jsx [new file with mode: 0644]
edera-web/src/components/pages/RFIDDeviceTrackPage.js [new file with mode: 0644]
edera-web/src/components/pages/RedirectPage.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/BaseChart.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/BaseChartCard.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/DataMainCard.js [new file with mode: 0644]
edera-web/src/components/pages/charts/EmptyAlert.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/FilterForm.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/RFIDDeviceTrackChart.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/UsageChart.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/UsageMLBreakChart.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/UsageMLHourlyChart.jsx [new file with mode: 0644]
edera-web/src/components/pages/charts/index.js [new file with mode: 0644]
edera-web/src/components/pages/charts/sections/RFIDDeviceTrackSection.js [new file with mode: 0644]
edera-web/src/components/pages/charts/sections/index.js [new file with mode: 0644]
edera-web/src/components/pages/charts/useReportData.js [new file with mode: 0644]
edera-web/src/components/pages/index.jsx [new file with mode: 0644]
edera-web/src/components/pdf/SupplyPdf.js [new file with mode: 0644]
edera-web/src/components/pdf/index.js [new file with mode: 0644]
edera-web/src/components/ra-alerts/SupplyLastTrackAlert.js [new file with mode: 0644]
edera-web/src/components/ra-alerts/index.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/EntityBackButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/EquipmentBackButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/EquipmentTypeBackButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/GenerateQRCodePdfButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/PriceListBackButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/PrintDocxButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/RegisterMaintenanceButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/RequestAssistanceButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/StartAndStopUsageButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/SupplierBackButton.js [new file with mode: 0644]
edera-web/src/components/ra-buttons/index.js [new file with mode: 0644]
edera-web/src/components/ra-details/SupplyShow.js [new file with mode: 0644]
edera-web/src/components/ra-details/index.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/ActivityTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/DetailsTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/EquipmentAttachmentTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/ProtocolTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/RFIDDeviceTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/SupplyTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/UsageRecordTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/UsageStatisticsTab.js [new file with mode: 0644]
edera-web/src/components/ra-details/supply/index.js [new file with mode: 0644]
edera-web/src/components/ra-fields/ActivityStatusField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/AuditLogMessageField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/CityField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/DirtyFormPrintAlertField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/MoneyField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/RFIDDeviceTrackManyField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/SupplyStatusField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/UserPictureField.js [new file with mode: 0644]
edera-web/src/components/ra-fields/index.js [new file with mode: 0644]
edera-web/src/components/ra-forms/ActivityForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/ActivityTypeForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/CustomerAreaForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/CustomerCreateForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/CustomerEditForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/CustomerOfficeForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/CustomerReferentForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/DeviceForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/DocxTemplateForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/EquipmentAttachmentForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/EquipmentCreateForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/EquipmentCreateInDialogForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/EquipmentEditForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/EquipmentTypeAttachmentForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/EquipmentTypeCreateForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/EquipmentTypeEditForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/I18nMessageForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/MaintenanceCreateForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/MaintenanceEditForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/PriceListForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/PriceListItemEditForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/PriceListItemForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/ProtocolAttachmentForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/ProtocolDialogForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/ProtocolForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/RFIDDeviceForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/SupplierCreateForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/SupplierForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/SupplierReferentForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/SupplyDialogForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/SupplyForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/UserForm.js [new file with mode: 0644]
edera-web/src/components/ra-forms/index.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/ActivitySelectInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/ActivityTypeAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CityAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CustomerAreaAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CustomerAreaManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CustomerAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CustomerOfficeAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CustomerOfficeManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CustomerReadOnlyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/CustomerReferentManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/DocxTemplateTypeSelectInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/EquipmentAttachmentManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/EquipmentAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/EquipmentListAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/EquipmentManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/EquipmentTypeAttachmentManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/EquipmentTypeAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/EquipmentTypeManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/InterventionArrayInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/LangSelectInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/MaintenanceManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/MoneyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/NationAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/PriceListItemManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/PriceListManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/ProtocolAttachmentManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/ProtocolAutoComplete.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/ProtocolServiceArrayInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/ProvinceAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/RFIDDeviceAutocomplete.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/RFIDDeviceTypeSelectInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/RegionAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/SupplierAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/SupplierReferentManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/SupplyActivityAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/SupplyAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/SupplyManyInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/UserAutocompleteInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/YesOrNoSelectInput.js [new file with mode: 0644]
edera-web/src/components/ra-inputs/index.js [new file with mode: 0644]
edera-web/src/components/ra-lists/ActivityList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/ActivityTypeList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/AuditLogList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/CustomerList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/DeviceList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/DocxTemplateList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/EquipmentList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/EquipmentTypeList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/I18nMessageList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/MaintenanceList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/ProtocolList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/RFIDDeviceList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/RFIDDeviceTrackList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/SupplierList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/SupplyList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/UserList.js [new file with mode: 0644]
edera-web/src/components/ra-lists/index.js [new file with mode: 0644]
edera-web/src/config.js [new file with mode: 0644]
edera-web/src/contexts/index.js [new file with mode: 0644]
edera-web/src/entities/activity-type.js [new file with mode: 0644]
edera-web/src/entities/activity.js [new file with mode: 0644]
edera-web/src/entities/audit-log.js [new file with mode: 0644]
edera-web/src/entities/customer.js [new file with mode: 0644]
edera-web/src/entities/device.js [new file with mode: 0644]
edera-web/src/entities/docx-template.js [new file with mode: 0644]
edera-web/src/entities/equipment-type.js [new file with mode: 0644]
edera-web/src/entities/equipment.js [new file with mode: 0644]
edera-web/src/entities/i18n-message.js [new file with mode: 0644]
edera-web/src/entities/index.js [new file with mode: 0644]
edera-web/src/entities/maintenance.js [new file with mode: 0644]
edera-web/src/entities/notification.js [new file with mode: 0644]
edera-web/src/entities/price-list.js [new file with mode: 0644]
edera-web/src/entities/protocol.js [new file with mode: 0644]
edera-web/src/entities/rfid-device-track.js [new file with mode: 0644]
edera-web/src/entities/rfid-device.js [new file with mode: 0644]
edera-web/src/entities/supplier.js [new file with mode: 0644]
edera-web/src/entities/supply.js [new file with mode: 0644]
edera-web/src/entities/user.js [new file with mode: 0644]
edera-web/src/hooks/index.js [new file with mode: 0644]
edera-web/src/hooks/useRedirect.js [new file with mode: 0644]
edera-web/src/index.js [new file with mode: 0644]
edera-web/src/menu.js [new file with mode: 0644]
edera-web/src/react-app-env.d.js [new file with mode: 0644]
edera-web/src/reportWebVitals.js [new file with mode: 0644]
edera-web/src/theme.js [new file with mode: 0644]

diff --git a/edera-web/.gitignore b/edera-web/.gitignore
new file mode 100644 (file)
index 0000000..a021272
--- /dev/null
@@ -0,0 +1,54 @@
+# These are some examples of commonly ignored file patterns.
+# You should customize this list as applicable to your project.
+# Learn more about .gitignore:
+#     https://www.atlassian.com/git/tutorials/saving-changes/gitignore
+
+# Node artifact files
+node_modules/
+dist/
+build/
+
+# Compiled Java class files
+*.class
+
+# Compiled Python bytecode
+*.py[cod]
+
+# Log files
+*.log
+
+# Package files
+*.jar
+
+# Maven
+target/
+dist/
+
+# JetBrains IDE
+.idea/
+
+# Unit test reports
+TEST*.xml
+
+# Generated by MacOS
+.DS_Store
+
+# Generated by Windows
+Thumbs.db
+
+# Applications
+*.app
+*.exe
+*.war
+
+# Large media files
+*.mp4
+*.tiff
+*.avi
+*.flv
+*.mov
+*.wmv
+
+html/resources/css/
+html/resources/js/
+package-lock.json
diff --git a/edera-web/.nvmrc b/edera-web/.nvmrc
new file mode 100644 (file)
index 0000000..3c03207
--- /dev/null
@@ -0,0 +1 @@
+18
diff --git a/edera-web/.prettierrc b/edera-web/.prettierrc
new file mode 100644 (file)
index 0000000..d5fba07
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "bracketSpacing": true,
+  "printWidth": 140,
+  "singleQuote": true,
+  "trailingComma": "none",
+  "tabWidth": 2,
+  "useTabs": false
+}
diff --git a/edera-web/.vscode/settings.json b/edera-web/.vscode/settings.json
new file mode 100644 (file)
index 0000000..7a73a41
--- /dev/null
@@ -0,0 +1,2 @@
+{
+}
\ No newline at end of file
diff --git a/edera-web/README.md b/edera-web/README.md
new file mode 100644 (file)
index 0000000..74c7065
--- /dev/null
@@ -0,0 +1,9 @@
+# Come funziona
+
+Per utilizzare questo archetipo come base per i tuoi progetti segui i seguenti passaggi:
+
+- crea un fork del progetto
+- esegui `nvm use` per impostare la versione di node.js corretta
+- esegui `npm install` per installare le dipendenze
+- esegui `npm start` per avviare il server di sviluppo
+- **divertiti**
diff --git a/edera-web/bitbucket-pipelines.yml b/edera-web/bitbucket-pipelines.yml
new file mode 100644 (file)
index 0000000..4546536
--- /dev/null
@@ -0,0 +1,57 @@
+image: google/cloud-sdk:412.0.0-slim
+
+definitions:
+  steps:
+    - step: &build
+        image: node:latest
+        name: Build
+        caches:
+          - node
+        script:
+          - source scripts/set-env.sh
+          - export VERSION="${REVISION}.${BITBUCKET_BUILD_NUMBER}"
+          - 'echo "{ \"version\": \"${VERSION}\" }" > ./src/build.json'
+          - npm install
+          - npm run build
+        artifacts:
+          - build/**
+    - step: &publish
+        name: Publish
+        trigger: automatic
+        services:
+          - docker
+        script:
+          - source scripts/set-env.sh
+          - export MODULE=web
+          - export VERSION="${REVISION}.${BITBUCKET_BUILD_NUMBER}"
+          - export IMAGE_BASE="eu.gcr.io/${PROJECT_ID}/${NAMESPACE}-${MODULE}"
+          - export IMAGE="${IMAGE_BASE}:${VERSION}"
+          - cat scripts/service-account.json.base64 | base64 --decode >> build/service-account.json
+          - gcloud auth activate-service-account --key-file build/service-account.json
+          - gcloud config set project ${PROJECT_ID}
+          - gcloud config set compute/zone europe-west1-b
+          - gcloud auth configure-docker
+          - docker build -t ${IMAGE} -f ./scripts/docker/Dockerfile .
+          - docker push ${IMAGE}
+          - docker tag $IMAGE "${IMAGE_BASE}:latest"
+          - docker push "${IMAGE_BASE}:latest"
+          - git tag -a "${VERSION}" -m "version ${VERSION}"
+          - git push origin "${VERSION}"
+        artifacts:
+          - build/**
+    - step: &deploy
+        name: Deploy
+        script:
+          - source scripts/set-env.sh
+          - gcloud auth activate-service-account --key-file build/service-account.json
+          - rm build/service-account.json
+          - gsutil -m cp -r build/* ${BUCKET}
+pipelines:
+  branches:
+    develop:
+      - step: *build
+      - step: *publish
+      - step: *deploy
+    master:
+      - step: *build
+      - step: *publish
\ No newline at end of file
diff --git a/edera-web/config-overrides.js b/edera-web/config-overrides.js
new file mode 100644 (file)
index 0000000..5c15969
--- /dev/null
@@ -0,0 +1,31 @@
+const webpack = require('webpack');
+const WorkBoxPlugin = require('workbox-webpack-plugin');
+
+module.exports = function override(config) {
+  config.resolve.fallback = {
+    process: require.resolve('process/browser'),
+    // zlib: require.resolve('browserify-zlib'),
+    stream: require.resolve('stream-browserify'),
+    crypto: require.resolve('crypto-browserify'),
+    util: require.resolve('util'),
+    buffer: require.resolve('buffer')
+    // asset: require.resolve('assert')
+  };
+
+  // https://stackoverflow.com/questions/69135310/workaround-for-cache-size-limit-in-create-react-app-pwa-service-worker
+  config.plugins.forEach((plugin) => {
+    if (plugin instanceof WorkBoxPlugin.InjectManifest) {
+      plugin.config.maximumFileSizeToCacheInBytes = 50 * 1024 * 1024;
+    }
+  });
+
+  config.plugins = [
+    ...config.plugins,
+    new webpack.ProvidePlugin({
+      process: 'process/browser.js',
+      Buffer: ['buffer', 'Buffer']
+    })
+  ];
+
+  return config;
+};
diff --git a/edera-web/jsconfig.json b/edera-web/jsconfig.json
new file mode 100644 (file)
index 0000000..35332c7
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "commonjs",
+    "baseUrl": "src"
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules"]
+}
diff --git a/edera-web/package-dev.json b/edera-web/package-dev.json
new file mode 100644 (file)
index 0000000..5584e78
--- /dev/null
@@ -0,0 +1,84 @@
+{
+  "name": "edera",
+  "description": "Manufacturing Execution System - Applica Software Guru",
+  "version": "1.0.0",
+  "private": true,
+  "dependencies": {
+    "@ant-design/icons": "file:../../modules/react-admin/node_modules/@ant-design/icons",
+    "@applica-software-guru/crud-client": "^1.1",
+    "@applica-software-guru/iam-client": "^1.1",
+    "@applica-software-guru/react-admin": "file:../../modules/react-admin",
+    "highcharts": "^11.2.0",
+    "highcharts-react-official": "^3.2.1",
+    "@mui/icons-material": "file:../../modules/react-admin/node_modules/@mui/icons-material",
+    "@mui/material": "file:../../modules/react-admin/node_modules/@mui/material",
+    "@react-pdf/renderer": "^3.1.12",
+    "dayjs": "^1.11.10",
+    "react": "file:../../modules/react-admin/node_modules/react",
+    "react-admin": "file:../../modules/react-admin/node_modules/react-admin",
+    "react-app-rewired": "^2.2.1",
+    "react-dom": "file:../../modules/react-admin/node_modules/react-dom",
+    "react-hook-form": "file:../../modules/react-admin/node_modules/react-hook-form",
+    "react-router": "file:../../modules/react-admin/node_modules/react-router",
+    "react-router-dom": "file:../../modules/react-admin/node_modules/react-router-dom",
+    "react-scripts": "^5.0.1",
+    "react-sticky-box": "file:../../modules/react-admin/node_modules/react-sticky-box",
+    "react-query": "file:../../modules/react-admin/node_modules/react-query",
+    "react-apexcharts": "^1.4.1",
+    "web-vitals": "^3.0.3",
+    "apexcharts": "^3.44.0",
+    "qrcode": "^1.5.3"
+  },
+  "scripts": {
+    "start": "react-app-rewired start",
+    "build": "react-app-rewired build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "babel": {
+    "presets": [
+      "@babel/preset-react"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "resolutions": {
+    "@svgr/webpack": "6.4.0"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.19.3",
+    "@babel/eslint-parser": "^7.19.1",
+    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
+    "buffer": "^6.0.3",
+    "crypto-browserify": "^3.12.0",
+    "eslint": "^8.25.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-config-react-app": "7.0.1",
+    "eslint-plugin-flowtype": "^8.0.3",
+    "eslint-plugin-import": "^2.26.0",
+    "eslint-plugin-jsx-a11y": "^6.6.1",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-react": "^7.31.10",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "prettier": "^2.7.1",
+    "process": "^0.11.10",
+    "react-error-overlay": "6.0.9",
+    "stream-browserify": "^3.0.0"
+  }
+}
diff --git a/edera-web/package.json b/edera-web/package.json
new file mode 100644 (file)
index 0000000..2e0ba63
--- /dev/null
@@ -0,0 +1,61 @@
+{
+  "name": "web",
+  "description": "Web Template - Applica Software Guru",
+  "version": "1.0.0",
+  "private": true,
+  "dependencies": {
+    "@applica-software-guru/crud-client": "^1.1",
+    "@applica-software-guru/iam-client": "^1.1",
+    "@applica-software-guru/react-admin": "^1.4",
+    "@react-pdf/renderer": "^3.1.14",
+    "apexcharts": "^3.44.0",
+    "highcharts": "^11.2.0",
+    "highcharts-react-official": "^3.2.1",
+    "qrcode": "^1.5.3",
+    "react": "^18.2.0",
+    "react-apexcharts": "^1.4.1",
+    "react-app-rewired": "^2.2.1",
+    "react-dom": "^18.2.0",
+    "react-scripts": "^5.0.1",
+    "web-vitals": "^3.0.3"
+  },
+  "scripts": {
+    "start": "react-app-rewired start",
+    "build": "react-app-rewired build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": ["react-app", "react-app/jest"]
+  },
+  "babel": {
+    "presets": ["@babel/preset-react"]
+  },
+  "browserslist": {
+    "production": [">0.2%", "not dead", "not op_mini all"],
+    "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
+  },
+  "resolutions": {
+    "@svgr/webpack": "6.4.0"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.19.3",
+    "@babel/eslint-parser": "^7.19.1",
+    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
+    "buffer": "^6.0.3",
+    "crypto-browserify": "^3.12.0",
+    "eslint": "^8.25.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-config-react-app": "7.0.1",
+    "eslint-plugin-flowtype": "^8.0.3",
+    "eslint-plugin-import": "^2.26.0",
+    "eslint-plugin-jsx-a11y": "^6.6.1",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-react": "^7.31.10",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "prettier": "^2.7.1",
+    "process": "^0.11.10",
+    "react-error-overlay": "6.0.9",
+    "stream-browserify": "^3.0.0"
+  }
+}
diff --git a/edera-web/public/favicon-dark.png b/edera-web/public/favicon-dark.png
new file mode 100644 (file)
index 0000000..85288ca
Binary files /dev/null and b/edera-web/public/favicon-dark.png differ
diff --git a/edera-web/public/favicon-light.png b/edera-web/public/favicon-light.png
new file mode 100644 (file)
index 0000000..7fd5d0c
Binary files /dev/null and b/edera-web/public/favicon-light.png differ
diff --git a/edera-web/public/index.html b/edera-web/public/index.html
new file mode 100644 (file)
index 0000000..72a97c1
--- /dev/null
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="theme-color" content="#ffffff" />
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
+    <meta name="description" content="Edera" />
+    <link href="%PUBLIC_URL%/favicon-light.png" rel="apple-touch-icon" media="(prefers-color-scheme: light)" />
+    <link href="%PUBLIC_URL%/favicon-dark.png" rel="apple-touch-icon" media="(prefers-color-scheme: dark)" />
+    <link href="%PUBLIC_URL%/favicon-light.png" rel="icon" media="(prefers-color-scheme: light)" />
+    <link href="%PUBLIC_URL%/favicon-dark.png" rel="icon" media="(prefers-color-scheme: dark)" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>Edera</title>
+
+    <link rel="preconnect" href="https://fonts.gstatic.com" />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap&family=Public+Sans:wght@400;500;600;700"
+      rel="stylesheet"
+    />
+
+    <!-- this is to resolve issue in old safari browser in tablet -->
+    <script src="https://cdn.jsdelivr.net/npm/resize-observer-polyfill@1.5.1/dist/ResizeObserver.min.js"></script>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>
diff --git a/edera-web/scripts/docker/Dockerfile b/edera-web/scripts/docker/Dockerfile
new file mode 100644 (file)
index 0000000..1334730
--- /dev/null
@@ -0,0 +1,6 @@
+FROM httpd:2
+ARG PORT=80
+
+EXPOSE $PORT
+
+ADD ./build/ /usr/local/apache2/htdocs/
\ No newline at end of file
diff --git a/edera-web/scripts/kube/ingress.yml b/edera-web/scripts/kube/ingress.yml
new file mode 100644 (file)
index 0000000..a0ce7b5
--- /dev/null
@@ -0,0 +1,33 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: app-ingress-web
+  annotations:
+    cert-manager.io/issuer: letsencrypt
+    nginx.ingress.kubernetes.io/use-regex: 'true'
+    nginx.ingress.kubernetes.io/rewrite-target: /edera-web/$1
+    nginx.ingress.kubernetes.io/backend-protocol: 'HTTPS'
+    nginx.ingress.kubernetes.io/upstream-vhost: 'storage.googleapis.com'
+    kubernetes.io/ingress.class: 'nginx'
+    kubernetes.io/ingress.allow-http: 'true'
+    nginx.ingress.kubernetes.io/server-snippet: |
+      location ~ ^/$ {
+        rewrite  ^.*$  /index.html  last;
+      }
+spec:
+  ingressClassName: nginx
+  tls:
+    - hosts:
+        - edera.applica.guru
+      secretName: letsencrypt
+  rules:
+    - host: edera.applica.guru
+      http:
+        paths:
+          - path: /(?!api)(.*)
+            pathType: ImplementationSpecific
+            backend:
+              service:
+                name: edera-bucket
+                port:
+                  number: 443
\ No newline at end of file
diff --git a/edera-web/scripts/kube/web.yml b/edera-web/scripts/kube/web.yml
new file mode 100644 (file)
index 0000000..466cfd7
--- /dev/null
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: edera-bucket
+spec:
+  type: ExternalName
+  externalName: storage.googleapis.com
diff --git a/edera-web/scripts/service-account.json.base64 b/edera-web/scripts/service-account.json.base64
new file mode 100644 (file)
index 0000000..6841642
--- /dev/null
@@ -0,0 +1,42 @@
+ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAiYXBwbGljYS1n
+ZW5lcmFsIiwKICAicHJpdmF0ZV9rZXlfaWQiOiAiNDk3YzZmNTU5N2Y0NTI3NWY3ZTlmZTQwMzFj
+NGI4MjllMjY4NzEyMSIsCiAgInByaXZhdGVfa2V5IjogIi0tLS0tQkVHSU4gUFJJVkFURSBLRVkt
+LS0tLVxuTUlJRXZnSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2d3Z2dTa0FnRUFBb0lCQVFD
+dDRrY1B3UVJ6bUQ3b1xucHlVQVBlTnBnS01TWGxTM25CYmVtSUFlcmk3dm5GeHJIMFV4RlJndmRN
+VTlzRTJ6L0h5WXNTMGNnR1R3Y01Fd1xuQlVVYVF3UWpUMFVxdlBrN3ByY2lOWURSTXVNQXY1MW5Y
+WkpRZ0RYeitMSG15VldhaE1VRTlMczM4YzJxeUFpalxuVTBjakhqTmxsMmp3S2J5OWJVNUVUcWlT
+eEtxdE1FRVlwSUFzV2kvWG1iekx6NlVtSnladm56ZCtkRFNRQTU3aFxuY0NsOEFTdlBpQTdIeXVz
+b1lzVWJ2S2xRS0tmcndtV3NBUllEVjdRc0t6bEpsR0ViWE1sWlozdzB1VCszVkxXOVxuTWMyS1Fo
+SGpvaWpoTW9yRjdVdjVWMjJrTWkyL2V4RkI3L3ErbllCejU5K1MxUXZlRmpCd3E5L0gwQTRyTXpU
+a1xubWZDYzhjMWZBZ01CQUFFQ2dnRUFOR1dsdlRrUUl0Y2pTYzhvSnJEL2lLaXpPeE5DMnd0Rmx2
+RUVWbnB0ZVZXNFxuUWExc0Y3VEFFM2pRQU4xU0pPVDJGTHI3R1lZVkpLRU5qZTlnbWQvTTdPanpz
+a084cEwyQm5PVGJldTZuR2ZBalxudWVTbjlPc1ZsdjEvZWtoOEs3Skxma2xTNnpKSm8rZGdOdnNl
+eWhYTkxoVllrVm82WGlpRWQ2L3VPei9aSUpPVVxuTFlHN1hPVHExNWhKcmZaM2trdDBBRldJc2NK
+SXVFbFBlUTJOaDVWY3VSeVJwUVQwaFA0NUlRUTZFdElBWXp4OVxuUnRGUWFhd2tpWVFqZ0RkelFO
+Qm1tUHN2ZWc4Y2JkQXlWZVlxcitaVkFiNk1pWjJ2N01RR0hyeEFBcnhSeXRMZlxuMmd2VUl0K3pv
+dmJBemo3Q3h0YjUvS3JDdHUwMC9aWTcwNzRxTExnR1NRS0JnUUR2NXRkNmxWbUg5cXl3TThTc1xu
+NnA5NE5lTVFrbU9vME41SjBUUisxTGFITnBkdjBaY3B0bEJDS0lCTXorVTBBc0p0cG11dlpGTGdH
+cmp6U3E2cFxuVFlGUis0Mlc1NjJGMkoxWmlKZmdnWDdubWp3TmhUNzdrdWUwbjVWbE1iTHBUN0Iz
+ajB6V3Ftc0hqZ09lZWhDblxueEIwOXpKcXJEYm96MkV3UGVvRkhDVjB5dHdLQmdRQzVqVmpJd21F
+d01PU2g5UHF3QzJ5SVBYeS81UkpGTG4yL1xuNWxzUmhRbkkrTkxTZUJjWnRIV1BiOWcyNVF6SFVV
+dWRRNGhiYWpHaDM1R2t5SGtIcjJ5MGVjdTFUd2pVTitGMFxubzdhejRzekk4a3kzRk9sRUpvdXM4
+enFNQWsrYnhFR1hrUUNkWmFiL0ltS0svR1dRK21RcXlDV0RtUkFvR3NVcVxuTEJNdUhKOXltUUtC
+Z0YrRnJBRGNYT1RkWEk5Z1hZeDRjM3piQUFtR01IWjBqRDRhTmV2V2FNTllBbDU4dHRMZVxuREFE
+N3ZYSllTU3czZVJGTjlZekZ4cFlETGVkNXNpZ3BlemVZa1Iwb0xKaWgwcTFtelFxUXBXWTBySHE1
+dG9WWFxuVGpsR1hhY0liZk9tVGw2Y3lYeWtLSysrWlVTQjJBWGsrYnUwcjFVeXh4U0RxRzExV3Vw
+ZEdTWHJBb0dCQUo2YVxucm9CMGZvU2wxbGlGd2Q3RzlRK0RsMldqMWJraTQwUXNFRDNxZlJHM2R1
+V0cxeUFXdThKT3RQOC9UR3YzRm00blxuc3ArSkowR1ppN0hSMW5wMlBiSUt4ZENGN1NNUlhQckpr
+YnN6cXg0ODFzeEw2SlJqYWxMOFdWZ2lCWkE4OG1BdlxuQnRxRGNIcDNGc3A4c2doNXJ6Tk9mNXA4
+Tkc1RGE3TC9sNmw3dCtOSkFvR0JBT2d6MHM1WGErRzZLTmlVN0IySFxuc0l2MmRNMzZvNlZ3a21P
+T3FjdCtkVS8ycG12ZGM4aFpDbFZGYXBQeWZ4djFqVnNjSDUrZG5FaFJIaTEzYXdIU1xuVzc3MnQ5
+WUdBNWpGb2dnakVPRU5QbVR2QUwxd01NL0ExRCtmanVaWVArOEVDaEU0QmFmbVN3WWZGVzJRQmxE
+eFxuZlhoOHNmT0FnMjhuMEc1QWNGcXZVSFJNXG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXG4i
+LAogICJjbGllbnRfZW1haWwiOiAiYml0YnVja2V0LXBpcGVsaW5lc0BhcHBsaWNhLWdlbmVyYWwu
+aWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJjbGllbnRfaWQiOiAiMTA4Mzk0ODE1Njc3NDgx
+NDI2MjYwIiwKICAiYXV0aF91cmkiOiAiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1
+dGgyL2F1dGgiLAogICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20v
+dG9rZW4iLAogICJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29v
+Z2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwKICAiY2xpZW50X3g1MDlfY2VydF91cmwiOiAi
+aHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9iaXRidWNr
+ZXQtcGlwZWxpbmVzJTQwYXBwbGljYS1nZW5lcmFsLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwK
+ICAidW5pdmVyc2VfZG9tYWluIjogImdvb2dsZWFwaXMuY29tIgp9Cg==
\ No newline at end of file
diff --git a/edera-web/scripts/set-env.sh b/edera-web/scripts/set-env.sh
new file mode 100644 (file)
index 0000000..f3d2c21
--- /dev/null
@@ -0,0 +1,6 @@
+export PROJECT_ID=applica-general
+export PROJECT_NAME=applica-general
+export PROFILE=production
+export NAMESPACE=edera
+export REVISION=1.0
+export BUCKET=gs://edera-web
\ No newline at end of file
diff --git a/edera-web/src/App.js b/edera-web/src/App.js
new file mode 100644 (file)
index 0000000..66f158c
--- /dev/null
@@ -0,0 +1,79 @@
+import '@applica-software-guru/react-admin/style.css';
+
+import * as entities from './entities';
+
+import { API_URL, APP_NAME, COPY } from './config';
+import { ActivatePage, ApplicaAdmin, HttpError, RecoverPage, RegisterPage, Resource } from '@applica-software-guru/react-admin';
+import { CustomPage, RFIDDeviceTrackPage, RedirectPage } from 'components/pages';
+import { createAttachmentsParser, createDataProvider } from '@applica-software-guru/crud-client';
+
+import { CustomRoutes } from 'ra-core';
+import { Route } from 'react-router-dom';
+import build from './build.json';
+import { createAuthProvider } from '@applica-software-guru/iam-client';
+import dayjs from 'dayjs';
+import duration from 'dayjs/plugin/duration';
+import menu from './menu';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import theme from './theme';
+
+dayjs.extend(duration);
+dayjs.extend(relativeTime);
+
+const authProvider = createAuthProvider({ apiUrl: API_URL });
+const dataProvider = createDataProvider({
+  apiUrl: API_URL,
+  getHeaders: () => authProvider.getHeaders(),
+  getToken: () => authProvider.getToken(),
+  attachmentsParser: createAttachmentsParser(),
+  HttpErrorClass: HttpError
+});
+const App = () => {
+  return (
+    <ApplicaAdmin
+      theme={theme}
+      apiUrl={API_URL}
+      authProvider={authProvider}
+      dataProvider={dataProvider}
+      defaultLocale="it"
+      menu={menu}
+      name={APP_NAME}
+      copy={COPY}
+      dashboard={RedirectPage}
+      version={build.version}
+      enablePasswordRecover
+      enableNotification
+    >
+      <Resource name="entities/notification" {...entities.notification} />
+      <Resource name="entities/user" {...entities.user} />
+      <Resource name="entities/i18n-message" {...entities.i18nMessage} />
+      <Resource name="entities/device" {...entities.device} />
+      <Resource name="entities/audit-log" {...entities.auditLog} />
+      <Resource name="entities/rfid-device" {...entities.rfidDevice} />
+      <Resource name="entities/customer" {...entities.customer} />
+      <Resource name="entities/activity-type" {...entities.activityType} />
+      <Resource name="entities/supplier" {...entities.supplier} />
+      <Resource name="entities/equipment-type" {...entities.equipmentType} />
+      <Resource name="entities/price-list" {...entities.priceList} />
+      <Resource name="entities/protocol" {...entities.protocol} />
+      <Resource name="entities/equipment" {...entities.equipment} />
+      <Resource name="entities/supply" {...entities.supply} />
+      <Resource name="entities/activity" {...entities.activity} />
+      <Resource name="entities/maintenance" {...entities.maintenance} />
+      <Resource name="entities/rfid-device-track" {...entities.rfidDeviceTrack} />
+      <Resource name="entities/docx-template" {...entities.docxTemplate} />
+      <CustomRoutes>
+        <Route path="/custom-page" element={<CustomPage />} />
+        <Route path="/position" element={<RFIDDeviceTrackPage />} />
+      </CustomRoutes>
+      <CustomRoutes noLayout>
+        <Route path="/register" element={<RegisterPage name={APP_NAME} copy={COPY} version={build.version} />} />
+        <Route path="/recover" element={<RecoverPage name={APP_NAME} copy={COPY} version={build.version} />} />
+        <Route path="/activate/:token" element={<ActivatePage name={APP_NAME} copy={COPY} version={build.version} />} />
+      </CustomRoutes>
+      <Resource name="entities/city" />
+    </ApplicaAdmin>
+  );
+};
+
+export default App;
diff --git a/edera-web/src/build.json b/edera-web/src/build.json
new file mode 100644 (file)
index 0000000..40d58d6
--- /dev/null
@@ -0,0 +1 @@
+{ "version": "0.0.0" }
diff --git a/edera-web/src/components/index.js b/edera-web/src/components/index.js
new file mode 100644 (file)
index 0000000..37ad9ca
--- /dev/null
@@ -0,0 +1,6 @@
+export * from './ra-lists';
+export * from './ra-inputs';
+export * from './ra-fields';
+export * from './ra-forms';
+export * from './ra-buttons';
+export * from './ra-details';
diff --git a/edera-web/src/components/layout/FixedMainCard.js b/edera-web/src/components/layout/FixedMainCard.js
new file mode 100644 (file)
index 0000000..6b6f010
--- /dev/null
@@ -0,0 +1,17 @@
+import { MainCard } from '@applica-software-guru/react-admin';
+
+const FixedMainCard = ({ children, ...props }) => (
+  <MainCard
+    {...props}
+    sx={{
+      '&.MuiToolbar-root': {
+        border: 0
+      }
+    }}
+    content={true}
+  >
+    {children}
+  </MainCard>
+);
+
+export default FixedMainCard;
diff --git a/edera-web/src/components/layout/index.js b/edera-web/src/components/layout/index.js
new file mode 100644 (file)
index 0000000..68e3daf
--- /dev/null
@@ -0,0 +1 @@
+export { default as FixedMainCard } from './FixedMainCard';
diff --git a/edera-web/src/components/pages/CustomPage.jsx b/edera-web/src/components/pages/CustomPage.jsx
new file mode 100644 (file)
index 0000000..91c077d
--- /dev/null
@@ -0,0 +1,15 @@
+import { Box, Typography } from '@mui/material';
+
+import { useTranslate } from '@applica-software-guru/react-admin';
+
+const CustomPage = () => {
+  const translate = useTranslate();
+  return (
+    <Box>
+      <Typography variant="h1">{translate('ra.custom_pages.welcome.title')}</Typography>
+      <Typography variant="body1">{translate('ra.custom_pages.welcome.subtitle')}</Typography>
+    </Box>
+  );
+};
+
+export default CustomPage;
diff --git a/edera-web/src/components/pages/RFIDDeviceTrackPage.js b/edera-web/src/components/pages/RFIDDeviceTrackPage.js
new file mode 100644 (file)
index 0000000..f59ad2c
--- /dev/null
@@ -0,0 +1,14 @@
+import { Stack } from '@mui/material';
+import { useThemeConfig } from '@applica-software-guru/react-admin';
+import RFIDDeviceTrackSection from './charts/sections';
+
+const RFIDDeviceTrackPage = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <Stack direction="column" spacing={spacing}>
+      <RFIDDeviceTrackSection />
+    </Stack>
+  );
+};
+
+export default RFIDDeviceTrackPage;
diff --git a/edera-web/src/components/pages/RedirectPage.jsx b/edera-web/src/components/pages/RedirectPage.jsx
new file mode 100644 (file)
index 0000000..f35a6da
--- /dev/null
@@ -0,0 +1,16 @@
+import { Navigate } from 'react-router';
+import { useEffect } from 'react';
+import { useRedirect } from '../../hooks';
+const RedirectPage = () => {
+  const { get: getRedirect } = useRedirect();
+  const redirect = getRedirect();
+  useEffect(() => {
+    if (redirect) {
+      document.location.href = redirect;
+    }
+  }, [redirect]);
+
+  return <Navigate to="/entities/notification" />;
+};
+
+export default RedirectPage;
diff --git a/edera-web/src/components/pages/charts/BaseChart.jsx b/edera-web/src/components/pages/charts/BaseChart.jsx
new file mode 100644 (file)
index 0000000..f9f38fc
--- /dev/null
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ReactApexChart from 'react-apexcharts';
+const BaseChart = ({ series, type = 'bar', sparkline = true, height = 150, options }) => {
+  const chartOptions = options || {
+    chart: {
+      sparkline: {
+        enabled: sparkline
+      },
+      height: 'auto',
+      type,
+      toolbar: {
+        show: false
+      }
+    },
+    stroke: {
+      curve: 'straight'
+    },
+
+    dataLabels: {
+      enabled: false
+    },
+
+    plotOptions: {
+      [type]: {
+        columnWidth: '80%'
+      }
+    },
+    xaxis: {
+      labels: {
+        show: false
+      }
+    },
+    yaxis: {
+      labels: {
+        show: false
+      }
+    },
+
+    tooltip: {
+      fixed: {
+        enabled: false
+      },
+      x: {
+        show: false
+      },
+      marker: {
+        show: false
+      }
+    }
+  };
+
+  return <ReactApexChart options={chartOptions} series={series} type={type} height={height} />;
+};
+
+BaseChart.propTypes = {
+  type: PropTypes.string.isRequired,
+  sparkline: PropTypes.bool,
+  series: PropTypes.arrayOf(
+    PropTypes.shape({
+      name: PropTypes.string,
+      data: PropTypes.any
+    })
+  )
+};
+
+export default BaseChart;
diff --git a/edera-web/src/components/pages/charts/BaseChartCard.jsx b/edera-web/src/components/pages/charts/BaseChartCard.jsx
new file mode 100644 (file)
index 0000000..e865627
--- /dev/null
@@ -0,0 +1,45 @@
+import { Chip, Stack, Typography } from '@mui/material';
+import { FallOutlined, RiseOutlined } from '@ant-design/icons';
+
+import { MainCard } from '@applica-software-guru/react-admin';
+import React from 'react';
+
+const percentageFormatter = new Intl.NumberFormat('it-IT', {
+  style: 'percent',
+  maximumFractionDigits: 2
+});
+
+const formatPercentage = (v) => percentageFormatter.format(v / 100);
+
+const BaseChartCard = ({ title, total, trend, children }) => {
+  return (
+    <MainCard>
+      <Stack>
+        <Typography>{title}</Typography>
+        <Stack spacing={1.5} direction="row" alignItems="center">
+          <Typography variant="h4" color="inherit">
+            {total}
+          </Typography>
+          {trend && (
+            <Chip
+              color={trend > 0 ? 'primary' : 'error'}
+              variant="combined"
+              icon={
+                trend > 0 ? (
+                  <RiseOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />
+                ) : (
+                  <FallOutlined style={{ fontSize: '0.75rem', color: 'inherit' }} />
+                )
+              }
+              label={formatPercentage(trend)}
+              size="small"
+            />
+          )}
+        </Stack>
+        {children}
+      </Stack>
+    </MainCard>
+  );
+};
+
+export default BaseChartCard;
diff --git a/edera-web/src/components/pages/charts/DataMainCard.js b/edera-web/src/components/pages/charts/DataMainCard.js
new file mode 100644 (file)
index 0000000..794b694
--- /dev/null
@@ -0,0 +1,20 @@
+import { LoadingIndicator, MainCard } from '@applica-software-guru/react-admin';
+import { Stack, Typography } from '@mui/material';
+
+const DataMainCard = (props) => {
+  const { isFetching, title, data, unit } = props;
+  return (
+    <MainCard>
+      <Stack>
+        <Typography>{title}</Typography>
+        <Stack spacing={1.5} direction="row" alignItems="center">
+          <Typography variant="h4" color="inherit">
+            {isFetching ? <LoadingIndicator /> : `${data} ${unit}`}
+          </Typography>
+        </Stack>
+      </Stack>
+    </MainCard>
+  );
+};
+
+export default DataMainCard;
diff --git a/edera-web/src/components/pages/charts/EmptyAlert.jsx b/edera-web/src/components/pages/charts/EmptyAlert.jsx
new file mode 100644 (file)
index 0000000..bc9185d
--- /dev/null
@@ -0,0 +1,27 @@
+import { useTranslate } from '@applica-software-guru/react-admin';
+import { Box, Typography } from '@mui/material';
+import WarningIcon from '@mui/icons-material/Warning';
+
+const WarningIconBig = () => {
+  return <WarningIcon sx={{ fontSize: 100, textAlign: 'center' }} />;
+};
+
+const EmptyAlert = ({ visible = true }) => {
+  const translate = useTranslate();
+  if (!visible) return null;
+  return (
+    <Box
+      sx={{ mt: 4, mb: 4 }}
+      justifyContent={'center'}
+      display={'flex'}
+      flexDirection={'column'}
+      alignItems={'center'}
+      textAlign={'center'}
+    >
+      <WarningIconBig />
+      <Typography>{translate('resources.entities/usage.tabs.chart.no-data-description')}</Typography>
+    </Box>
+  );
+};
+
+export default EmptyAlert;
diff --git a/edera-web/src/components/pages/charts/FilterForm.jsx b/edera-web/src/components/pages/charts/FilterForm.jsx
new file mode 100644 (file)
index 0000000..2971431
--- /dev/null
@@ -0,0 +1,45 @@
+import { Form, useThemeConfig } from '@applica-software-guru/react-admin';
+import React, { useEffect, useRef } from 'react';
+
+import { Grid } from '@mui/material';
+import PropTypes from 'prop-types';
+import { useWatch } from 'react-hook-form';
+
+const FilterFormContent = ({ children, onChange }) => {
+  const { spacing } = useThemeConfig();
+  const values = useWatch();
+  const didMount = useRef(false);
+  useEffect(() => {
+    if (!didMount.current) {
+      didMount.current = true;
+      return;
+    }
+    onChange(values);
+  }, [values, onChange]);
+
+  return (
+    <Grid container spacing={spacing} direction="row-reverse">
+      {React.Children.map(children, (child) => (
+        <Grid item lg={3} md={3} sm={12} xs={12}>
+          {React.cloneElement(child, { fullWidth: true, display: 'legend' })}
+        </Grid>
+      ))}
+    </Grid>
+  );
+};
+
+const FilterForm = ({ children, onChange }) => {
+  const handleSubmit = (values) => console.log(values);
+  return (
+    <Form onSubmit={handleSubmit}>
+      <FilterFormContent children={children} onChange={onChange} />
+    </Form>
+  );
+};
+
+FilterForm.propTypes = {
+  children: PropTypes.node,
+  onChange: PropTypes.func.isRequired
+};
+
+export default FilterForm;
diff --git a/edera-web/src/components/pages/charts/RFIDDeviceTrackChart.jsx b/edera-web/src/components/pages/charts/RFIDDeviceTrackChart.jsx
new file mode 100644 (file)
index 0000000..9c0b66f
--- /dev/null
@@ -0,0 +1,78 @@
+import React, { useMemo } from 'react';
+import BaseChartCard from './BaseChartCard';
+import { useTranslate } from 'react-admin';
+import HighchartsReact from 'highcharts-react-official';
+import Highcharts from 'highcharts';
+
+const RFIDDeviceTrackChart = ({ data }) => {
+  const translate = useTranslate();
+
+  const { options, loading } = useMemo(() => {
+    if (!data || !data.tagsAndGatewaysNames || !data.latestTrackedSupplies) {
+      return { options: {}, loading: true };
+    }
+
+    const { tagsAndGatewaysNames, latestTrackedSupplies } = data;
+
+    const gatewaysSorted = tagsAndGatewaysNames.points[0]?.gateways?.sort((a, b) => a.gatewayName[0].localeCompare(b.gatewayName[0])) || [];
+    const gateways = gatewaysSorted.reduce((acc, gateway) => ({ ...acc, [gateway.gatewayId]: gateway.gatewayName[0] }), {});
+    const gatewayIndexes = Object.values(gateways);
+
+    const tagsSorted = tagsAndGatewaysNames.points[0]?.tags?.sort((a, b) => a.tagName[0].localeCompare(b.tagName[0])) || [];
+    const tags = tagsSorted.reduce((acc, tag) => ({ ...acc, [tag.tagId]: tag.tagName[0] }), {});
+    const tagsIndexes = Object.values(tags);
+
+    const seriesData = (latestTrackedSupplies?.points || []).map((track) => ({
+      x: tagsIndexes.indexOf(tags[track.latestRfidTrack.tagId]),
+      y: gatewayIndexes.indexOf(gateways[track.latestRfidTrack.gatewayId])
+    }));
+
+    const options = {
+      chart: {
+        type: 'scatter',
+        zoomType: 'xy'
+      },
+      credits: {
+        enabled: false
+      },
+      title: {
+        text: ''
+      },
+      xAxis: {
+        title: {
+          text: 'Tag'
+        },
+        categories: tagsIndexes,
+        labels: {
+          rotation: -45
+        }
+      },
+      yAxis: {
+        title: {
+          text: 'Gateway'
+        },
+        categories: gatewayIndexes
+      },
+      series: [
+        {
+          name: 'Movimenti RFID',
+          color: 'rgb(0, 100, 255)',
+          data: seriesData
+        }
+      ]
+    };
+
+    return { options };
+  }, [data]);
+
+  if (loading) {
+    return null;
+  }
+  return (
+    <BaseChartCard title={translate('tracking.stats.rfid.title')}>
+      <HighchartsReact highcharts={Highcharts} options={options} />
+    </BaseChartCard>
+  );
+};
+
+export default RFIDDeviceTrackChart;
diff --git a/edera-web/src/components/pages/charts/UsageChart.jsx b/edera-web/src/components/pages/charts/UsageChart.jsx
new file mode 100644 (file)
index 0000000..65cae70
--- /dev/null
@@ -0,0 +1,75 @@
+import React, { useMemo } from 'react';
+import Chart from 'react-apexcharts';
+import dayjs from 'dayjs';
+import { useTranslate } from '@applica-software-guru/react-admin';
+import _ from 'lodash';
+import { BaseChartCard, EmptyAlert } from '.';
+
+const formatExponentialToFixed = (number) => {
+  const num = Number(number);
+  if (num === 0) return '0.00';
+  return num.toFixed(2);
+};
+
+const UsageChart = ({ data }) => {
+  const translate = useTranslate();
+  const { hasData, options, series } = useMemo(() => {
+    const points = _.get(data, 'customUsage.points', []);
+    const hasData = points.length > 0;
+    const isHourlyData = points.some((point) => point.totalUsageHours != null);
+
+    const series = [
+      {
+        name: translate(isHourlyData ? 'reporting.stats.usageHours' : 'reporting.stats.usageDays'),
+        data:
+          points.length > 0
+            ? points.map((point) => {
+                const timestamp = dayjs(
+                  new Date(point.year, isHourlyData ? point.month - 1 : point.month, isHourlyData ? point.day : null)
+                ).valueOf();
+                const usageValue = point?.total;
+                const formattedValue = usageValue != null ? formatExponentialToFixed(usageValue) : 0;
+                return { x: timestamp, y: formattedValue };
+              })
+            : [{ x: dayjs().valueOf(), y: null }]
+      }
+    ];
+
+    const options = {
+      chart: {
+        id: 'basic-line',
+        height: 'auto'
+      },
+      xaxis: {
+        type: 'datetime',
+        labels: {
+          formatter: (val) => dayjs(val).format(isHourlyData ? 'MMM DD' : 'MMM YYYY')
+        }
+      },
+      yaxis: {
+        title: {
+          text: translate(isHourlyData ? 'reporting.stats.usageHours' : 'reporting.stats.usageDays')
+        }
+      },
+      tooltip: {
+        x: {
+          format: 'MMM DD, YYYY'
+        }
+      }
+    };
+
+    return { options, series, hasData };
+  }, [data, translate]);
+
+  if (!hasData) {
+    return <EmptyAlert />;
+  }
+
+  return (
+    <BaseChartCard>
+      <Chart options={options} series={series} type="line" height="300" />
+    </BaseChartCard>
+  );
+};
+
+export default UsageChart;
diff --git a/edera-web/src/components/pages/charts/UsageMLBreakChart.jsx b/edera-web/src/components/pages/charts/UsageMLBreakChart.jsx
new file mode 100644 (file)
index 0000000..8e27808
--- /dev/null
@@ -0,0 +1,105 @@
+import { useTranslate } from '@applica-software-guru/react-admin';
+import _ from 'lodash';
+import { useMemo } from 'react';
+import dayjs from 'dayjs';
+import { BaseChartCard, EmptyAlert } from '.';
+import Chart from 'react-apexcharts';
+import { useTheme } from '@mui/material/styles';
+
+const formatExponentialToFixed = (number) => {
+  const num = Number(number);
+  if (num === 0) return '0.00';
+  return num.toFixed(2);
+};
+
+const UsageMLBreakChart = ({ data }) => {
+  const translate = useTranslate();
+  const theme = useTheme();
+
+  const { hasData, options, series } = useMemo(() => {
+    const primaryColor = theme.palette.primary.main;
+    const secondaryColor = theme.palette.secondary.main;
+    const dangerColor = theme.palette.error.main;
+    const points = _.get(data, 'predictions.points', []);
+    const hasData = points.some((p) => p.prediction === 1);
+    const series = [
+      {
+        name: translate('reporting.stats.predictions.accumulated'),
+        data: points
+          ?.filter((p) => !p.broken)
+          .map((point) => ({
+            x: dayjs(new Date(point.year, point.month - 1, point.day)).valueOf(),
+            y: formatExponentialToFixed(point?.accumulated)
+          }))
+      },
+      {
+        name: translate('reporting.stats.predictions.broken'),
+        data: points
+          ?.filter((point) => point?.broken)
+          .map((point) => ({
+            x: dayjs(new Date(point.year, point.month - 1, point.day)).valueOf(),
+            y: formatExponentialToFixed(point?.accumulated)
+          }))
+      }
+    ];
+    if (series[0]?.data.length > 0 && series[1]?.data.length > 0) {
+      series.push({
+        data: [series[0]?.data[series[0]?.data.length - 1], series[1]?.data[0]]
+      });
+    }
+    const breakpointInHours = _.get(data, 'breakpointInHours', 0);
+    const options = {
+      chart: {
+        id: 'basic-line',
+        height: 'auto'
+      },
+      xaxis: {
+        type: 'datetime',
+        labels: {
+          formatter: (val) => dayjs(val).format('YYYY-MM-DD')
+        }
+      },
+      tooltip: {
+        x: {
+          format: 'MMM DD, YYYY'
+        }
+      },
+      stroke: { width: 2 },
+      markers: {
+        size: 8
+      },
+      annotations: {
+        yaxis: [
+          {
+            y: breakpointInHours,
+            borderColor: dangerColor,
+            label: {
+              borderColor: dangerColor,
+              style: {
+                color: '#fff',
+                background: dangerColor
+              },
+              text: translate('reporting.stats.breakPredictions.line'),
+              position: 'center'
+            }
+          }
+        ]
+      },
+      colors: [primaryColor, secondaryColor, dangerColor]
+    };
+
+    return { hasData, options, series };
+  }, [data, translate, theme]);
+
+  if (!hasData) {
+    return <EmptyAlert />;
+  }
+
+  return (
+    <BaseChartCard>
+      <Chart options={options} series={series} type="line" height="300" />
+    </BaseChartCard>
+  );
+};
+
+export default UsageMLBreakChart;
diff --git a/edera-web/src/components/pages/charts/UsageMLHourlyChart.jsx b/edera-web/src/components/pages/charts/UsageMLHourlyChart.jsx
new file mode 100644 (file)
index 0000000..4da4114
--- /dev/null
@@ -0,0 +1,82 @@
+import { useTranslate } from '@applica-software-guru/react-admin';
+import _ from 'lodash';
+import { useMemo } from 'react';
+import dayjs from 'dayjs';
+import { BaseChartCard, EmptyAlert } from '.';
+import Chart from 'react-apexcharts';
+import { useTheme } from '@mui/material/styles';
+
+const formatExponentialToFixed = (number) => {
+  const num = Number(number);
+  if (num === 0) return '0.00';
+  return num.toFixed(2);
+};
+
+const UsageMLHourlyChart = ({ data }) => {
+  const translate = useTranslate();
+  const theme = useTheme();
+
+  const { hasData, options, series } = useMemo(() => {
+    const primaryColor = theme.palette.primary.main;
+    const dangerColor = theme.palette.error.main;
+    const points = _.get(data, 'predictions.points', []);
+    const hasData = points.some((p) => p.prediction === 1);
+    const series = [
+      {
+        name: translate('reporting.stats.predictions.history'),
+        data: points
+          ?.filter((point) => point.prediction === 0)
+          .map((point) => ({
+            x: dayjs(new Date(point.year, point.month - 1, point.day)).valueOf(),
+            y: formatExponentialToFixed(point?.total)
+          }))
+      },
+      {
+        name: translate('reporting.stats.predictions.future'),
+        data: points
+          ?.filter((point) => point.prediction === 1)
+          .map((point) => ({
+            x: dayjs(new Date(point.year, point.month - 1, point.day)).valueOf(),
+            y: formatExponentialToFixed(point?.total)
+          }))
+      }
+    ];
+    const options = {
+      chart: {
+        id: 'basic-bar',
+        height: 'auto'
+      },
+      xaxis: {
+        type: 'datetime',
+        labels: {
+          formatter: (val) => dayjs(val).format('YYYY-MM-DD')
+        }
+      },
+      yaxis: {
+        title: {
+          text: translate('reporting.stats.predictions.usageHours')
+        }
+      },
+      tooltip: {
+        x: {
+          format: 'MMM DD, YYYY'
+        }
+      },
+      colors: [primaryColor, dangerColor]
+    };
+
+    return { hasData, options, series };
+  }, [data, translate, theme]);
+
+  if (!hasData) {
+    return <EmptyAlert />;
+  }
+
+  return (
+    <BaseChartCard>
+      <Chart options={options} series={series} type="bar" height="300" />
+    </BaseChartCard>
+  );
+};
+
+export default UsageMLHourlyChart;
diff --git a/edera-web/src/components/pages/charts/index.js b/edera-web/src/components/pages/charts/index.js
new file mode 100644 (file)
index 0000000..d7786cb
--- /dev/null
@@ -0,0 +1,9 @@
+export { default as UsageChart } from './UsageChart';
+export { default as BaseChartCard } from './BaseChartCard';
+export { default as DataMainCard } from './DataMainCard';
+export { default as FilterForm } from './FilterForm';
+export { default as RFIDDeviceTrackChart } from './RFIDDeviceTrackChart';
+export { default as UsageMLHourlyChart } from './UsageMLHourlyChart';
+export { default as EmptyAlert } from './EmptyAlert';
+export { default as useReportData } from './useReportData';
+export { default as UsageMLBreakChart } from './UsageMLBreakChart';
diff --git a/edera-web/src/components/pages/charts/sections/RFIDDeviceTrackSection.js b/edera-web/src/components/pages/charts/sections/RFIDDeviceTrackSection.js
new file mode 100644 (file)
index 0000000..c9e5f64
--- /dev/null
@@ -0,0 +1,100 @@
+import { MainCard, useThemeConfig, useTranslate } from '@applica-software-guru/react-admin';
+import useReportData from '../useReportData';
+import { Grid, Alert, Typography } from '@mui/material';
+import DataMainCard from '../DataMainCard';
+import { Fragment, useCallback, useState } from 'react';
+import { CustomerOfficeAutocompleteInput } from 'components/ra-inputs';
+import { FilterForm, RFIDDeviceTrackChart } from '..';
+
+const formatExponentialToFixed = (number) => {
+  const num = Number(number);
+  if (isNaN(num) || num === 0) return '0.00';
+  return num.toFixed(2);
+};
+
+const RFIDDeviceTrackSection = () => {
+  const { spacing } = useThemeConfig();
+  const translate = useTranslate();
+  const [filters, setFilters] = useState({});
+  const handleChange = useCallback((filters) => setFilters(filters), [setFilters]);
+  const { data, isFetching } = useReportData({
+    officeId: filters.officeId,
+    type: 'supply-tracks'
+  });
+
+  const fakeData = {
+    latestTrackedSupplies: {
+      points: [],
+      count: 0
+    },
+
+    tagsAndGatewaysNames: {
+      points: [
+        {
+          gateways: [],
+          _id: null,
+          tags: []
+        }
+      ],
+      count: 1
+    }
+  };
+
+  return (
+    <Fragment>
+      <MainCard title={translate('ra.position.report.title')}>
+        <Grid container spacing={spacing}>
+          <Grid item xs={12}>
+            <FilterForm onChange={handleChange}>
+              <CustomerOfficeAutocompleteInput source="officeId" label="reporting.stats.filters.office" />
+            </FilterForm>
+          </Grid>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.track.monthly-movement.title')}
+              data={(!isFetching && data?.monthMovementTracking?.points[0]?.totalTrackedRFID) || 0}
+              unit=""
+            />
+          </Grid>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.track.yearly-movement.title')}
+              data={(!isFetching && data?.yearMovementTracking?.points[0]?.totalTrackedRFID) || 0}
+              unit=""
+            />
+          </Grid>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.track.total-monthly-tracked.title')}
+              data={(!isFetching && data?.monthlyTotalTrackedRFID?.points[0]?.totalUniqueSupplyId) || 0}
+              unit=""
+            />
+          </Grid>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.track.monthly-total-tracked-percentage.title')}
+              data={!isFetching && formatExponentialToFixed(data?.monthlyTotalTrackedRFIDPercentage?.points[0]?.trackingPercentage)}
+              unit="%"
+            />
+          </Grid>
+          {!data && (
+            <Grid item sm={12} xs={12}>
+              <Alert severity="warning" sx={{ marginTop: 1, marginBottom: 2 }}>
+                <Typography variant="body1">{translate('ra.position.waiting-for-data')}</Typography>
+              </Alert>
+            </Grid>
+          )}
+        </Grid>
+        <Grid item sm={12} xs={12} sx={{ marginTop: 2 }}>
+          <RFIDDeviceTrackChart data={isFetching ? fakeData : data} />
+        </Grid>
+      </MainCard>
+    </Fragment>
+  );
+};
+
+export default RFIDDeviceTrackSection;
diff --git a/edera-web/src/components/pages/charts/sections/index.js b/edera-web/src/components/pages/charts/sections/index.js
new file mode 100644 (file)
index 0000000..7910d54
--- /dev/null
@@ -0,0 +1,3 @@
+import RFIDDeviceTrackSection from './RFIDDeviceTrackSection';
+
+export default RFIDDeviceTrackSection;
diff --git a/edera-web/src/components/pages/charts/useReportData.js b/edera-web/src/components/pages/charts/useReportData.js
new file mode 100644 (file)
index 0000000..f4fc1f9
--- /dev/null
@@ -0,0 +1,25 @@
+import dayjs from 'dayjs';
+import { useDataProvider, useNotify } from '@applica-software-guru/react-admin';
+import { useQuery } from 'react-query';
+
+const useReportData = ({ supplyId, from, to, trigger, type, officeId }) => {
+  const dataProvider = useDataProvider();
+  const notify = useNotify();
+  const fetchData = () => {
+    if (from && to && dayjs(from).isAfter(dayjs(to))) {
+      notify('ra.error.invalid-date-range', 'error');
+      return Promise.resolve(null);
+    }
+    const params = {
+      from: from || dayjs().startOf('month').add(1, 'hours').toISOString(),
+      to: dayjs(to).add(1, 'day').toISOString() || dayjs().endOf('month').add(1, 'hours').toISOString(),
+      supplyId: supplyId,
+      officeId: officeId
+    };
+    return dataProvider.post(`report/${type}/execute`, params).then(({ data: { data } }) => data);
+  };
+  const shouldFetch = !!supplyId || !!officeId;
+  return useQuery(['reportData', supplyId, officeId, trigger], fetchData, { enabled: shouldFetch });
+};
+
+export default useReportData;
diff --git a/edera-web/src/components/pages/index.jsx b/edera-web/src/components/pages/index.jsx
new file mode 100644 (file)
index 0000000..33f8b4d
--- /dev/null
@@ -0,0 +1,4 @@
+import CustomPage from './CustomPage';
+import RedirectPage from './RedirectPage';
+import RFIDDeviceTrackPage from './RFIDDeviceTrackPage';
+export { CustomPage, RedirectPage, RFIDDeviceTrackPage };
diff --git a/edera-web/src/components/pdf/SupplyPdf.js b/edera-web/src/components/pdf/SupplyPdf.js
new file mode 100644 (file)
index 0000000..8cb73ce
--- /dev/null
@@ -0,0 +1,23 @@
+import { Document, Page, StyleSheet, View, Image, Text } from '@react-pdf/renderer';
+import React from 'react';
+
+const styles = StyleSheet.create({
+  page: {
+    fontFamily: 'Helvetica'
+  }
+});
+
+// <Text style={{ fontSize: 5 }}>Approvvigionamento: {supplyName}</Text>
+
+const SupplyPdf = ({ qrCodeDataUrl, serialNumber }) => (
+  <Document>
+    <Page size="A8" style={styles.page}>
+      <View>
+        <Image src={qrCodeDataUrl} />
+        <Text style={{ fontSize: 20, textAlign: 'center' }}>SN: {serialNumber}</Text>
+      </View>
+    </Page>
+  </Document>
+);
+
+export default SupplyPdf;
diff --git a/edera-web/src/components/pdf/index.js b/edera-web/src/components/pdf/index.js
new file mode 100644 (file)
index 0000000..941f9c3
--- /dev/null
@@ -0,0 +1,2 @@
+import SupplyPdf from './SupplyPdf';
+export { SupplyPdf };
diff --git a/edera-web/src/components/ra-alerts/SupplyLastTrackAlert.js b/edera-web/src/components/ra-alerts/SupplyLastTrackAlert.js
new file mode 100644 (file)
index 0000000..59fbc99
--- /dev/null
@@ -0,0 +1,25 @@
+import { useRecordContext, useTranslate } from '@applica-software-guru/react-admin';
+import { Alert, AlertTitle, Typography } from '@mui/material';
+import dayjs from 'dayjs';
+const SupplyLastTrackAlert = () => {
+  const record = useRecordContext();
+  const translate = useTranslate();
+  if (!record?.latestRfidTrack) {
+    return null;
+  }
+  return (
+    <Alert severity="info" sx={{ marginTop: 1 }}>
+      <AlertTitle dangerouslySetInnerHTML={{ __html: translate('resources.entities/supply.rfid-device-last-position.title') }} />
+      <Typography
+        dangerouslySetInnerHTML={{
+          __html: translate('resources.entities/supply.rfid-device-last-position', {
+            area: record?.latestArea?.name,
+            ts: dayjs(record?.latestRfidTrack?.ts).format(translate('app.date_format.long'))
+          })
+        }}
+      />
+    </Alert>
+  );
+};
+
+export default SupplyLastTrackAlert;
diff --git a/edera-web/src/components/ra-alerts/index.js b/edera-web/src/components/ra-alerts/index.js
new file mode 100644 (file)
index 0000000..76bfdae
--- /dev/null
@@ -0,0 +1 @@
+export { default as SupplyLastTrackAlert } from './SupplyLastTrackAlert';
diff --git a/edera-web/src/components/ra-buttons/EntityBackButton.js b/edera-web/src/components/ra-buttons/EntityBackButton.js
new file mode 100644 (file)
index 0000000..7cd6efd
--- /dev/null
@@ -0,0 +1,15 @@
+import { Button } from '@mui/material';
+import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
+import { Link } from 'react-router-dom';
+import { useTranslate } from '@applica-software-guru/react-admin';
+
+const EntityBackButton = ({ type, id, label }) => {
+  const translate = useTranslate();
+  return (
+    <Button startIcon={<KeyboardReturnIcon />} variant="outlined" size="medium" component={Link} to={`/entities/${type}/${id}`}>
+      {translate(label) || translate('resources.entities/supply.actions.backToFather')}
+    </Button>
+  );
+};
+
+export default EntityBackButton;
diff --git a/edera-web/src/components/ra-buttons/EquipmentBackButton.js b/edera-web/src/components/ra-buttons/EquipmentBackButton.js
new file mode 100644 (file)
index 0000000..a565575
--- /dev/null
@@ -0,0 +1,18 @@
+import { Button } from '@applica-software-guru/react-admin';
+import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
+import { Link } from 'react-router-dom';
+
+const EquipmentBackButton = ({ equipmentId }) => {
+  return (
+    <Button
+      startIcon={<KeyboardReturnIcon />}
+      label={'resources.entities/supply.actions.backToFather'}
+      variant="outlined"
+      size="medium"
+      component={Link}
+      to={`/entities/equipment/${equipmentId}`}
+    />
+  );
+};
+
+export default EquipmentBackButton;
diff --git a/edera-web/src/components/ra-buttons/EquipmentTypeBackButton.js b/edera-web/src/components/ra-buttons/EquipmentTypeBackButton.js
new file mode 100644 (file)
index 0000000..21f060e
--- /dev/null
@@ -0,0 +1,18 @@
+import { Button } from '@applica-software-guru/react-admin';
+import { Link } from 'react-router-dom';
+import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
+
+const EquipmentTypeBackButton = ({ parentId }) => {
+  return (
+    <Button
+      startIcon={<KeyboardReturnIcon />}
+      label={'resources.entities/equipmentType.actions.backToFather'}
+      variant="outlined"
+      size="medium"
+      component={Link}
+      to={`/entities/equipment-type/${parentId}`}
+    />
+  );
+};
+
+export default EquipmentTypeBackButton;
diff --git a/edera-web/src/components/ra-buttons/GenerateQRCodePdfButton.js b/edera-web/src/components/ra-buttons/GenerateQRCodePdfButton.js
new file mode 100644 (file)
index 0000000..e2ccefc
--- /dev/null
@@ -0,0 +1,60 @@
+// GenerateQRCodePdfButton.js
+import React, { useState } from 'react';
+import { Button, useNotify, useRecordContext } from '@applica-software-guru/react-admin';
+import { pdf } from '@react-pdf/renderer';
+import QRCode from 'qrcode';
+import { SupplyPdf } from 'components/pdf';
+
+const GenerateQRCodePdfButton = () => {
+  const [loading, setLoading] = useState(false);
+  const notify = useNotify();
+  const record = useRecordContext();
+
+  const handleClick = async () => {
+    if (!record || !record.id) {
+      notify('Error: no record found.');
+      return;
+    }
+
+    setLoading(true);
+    try {
+      const qrCodeValue = `${window.location.protocol}//${window.location.hostname}${
+        window.location.port ? `:${window.location.port}` : ''
+      }/#/entities/supply/${record.id}/show`;
+      const qrCodeDataUrl = await QRCode.toDataURL(qrCodeValue, {
+        errorCorrectionLevel: 'H',
+        type: 'image/png',
+        width: 20
+      });
+
+      const asPdf = pdf(<SupplyPdf qrCodeDataUrl={qrCodeDataUrl} serialNumber={record?.serialNumber} />);
+      const blob = await asPdf.toBlob();
+
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = 'supply-details.pdf';
+      document.body.appendChild(a);
+      a.click();
+      URL.revokeObjectURL(url);
+      a.remove();
+    } catch (error) {
+      notify(`ra.error.pdf-generation: ${error.message}`, 'warning');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <Button
+      variant="contained"
+      size="medium"
+      color="secondary"
+      onClick={handleClick}
+      disabled={loading || !record || !record.id}
+      label={loading ? 'Generating...' : 'Download PDF'}
+    />
+  );
+};
+
+export default GenerateQRCodePdfButton;
diff --git a/edera-web/src/components/ra-buttons/PriceListBackButton.js b/edera-web/src/components/ra-buttons/PriceListBackButton.js
new file mode 100644 (file)
index 0000000..b9df730
--- /dev/null
@@ -0,0 +1,18 @@
+import { Button } from '@applica-software-guru/react-admin';
+import { Link } from 'react-router-dom';
+import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
+
+const PriceListBackButton = ({ supplierId, tab }) => {
+  return (
+    <Button
+      startIcon={<KeyboardReturnIcon />}
+      label={'resources.entities/price-list.actions.back-to-supplier'}
+      variant="outlined"
+      size="medium"
+      component={Link}
+      to={`/entities/supplier/${supplierId}${tab ? `/${tab}` : ''}`}
+    />
+  );
+};
+
+export default PriceListBackButton;
diff --git a/edera-web/src/components/ra-buttons/PrintDocxButton.js b/edera-web/src/components/ra-buttons/PrintDocxButton.js
new file mode 100644 (file)
index 0000000..c5ac68e
--- /dev/null
@@ -0,0 +1,60 @@
+import { useAuthProvider, useDataProvider, usePopoverState, useRecordContext, useTranslate } from '@applica-software-guru/react-admin';
+import { useEffect, useState } from 'react';
+import { Typography, Button, ButtonGroup, Menu, MenuList, MenuItem } from '@mui/material';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import PrintIcon from '@mui/icons-material/Print';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { API_URL } from '../../config';
+
+const PrintDocxButton = ({ type }) => {
+  const { anchorEl, open, handleClose, handleClick } = usePopoverState();
+  const [token, setToken] = useState(null);
+  const translate = useTranslate();
+  const record = useRecordContext();
+  const dataProvider = useDataProvider();
+  const authProvider = useAuthProvider();
+
+  const [options, setOptions] = useState([]);
+  useEffect(() => {
+    dataProvider.get(`docx/templates/${type}`).then(({ data }) => setOptions(data));
+    authProvider.getToken().then((token) => setToken(token));
+  }, [dataProvider, authProvider, setOptions, type]);
+
+  if (options.length === 0 || !record?.id || !token) {
+    return null;
+  }
+
+  return (
+    <React.Fragment>
+      <ButtonGroup variant="contained" color="secondary">
+        <Button onClick={handleClick}>
+          <PrintIcon />
+          <Typography sx={{ pl: 1, pr: 1 }}>{translate('ra.actions.print_docx')}</Typography>
+          <ArrowDropDownIcon />
+        </Button>
+      </ButtonGroup>
+      <Menu open={open} anchorEl={anchorEl} onClose={handleClose}>
+        <MenuList autoFocusItem>
+          {options.map((option) => (
+            <MenuItem
+              key={option.id}
+              onClick={handleClose}
+              component={Button}
+              target="_blank"
+              href={`${API_URL}/docx/generate/${type}/${option.id}/${record?.id}.docx?_token=${token}`}
+            >
+              {option.name}
+            </MenuItem>
+          ))}
+        </MenuList>
+      </Menu>
+    </React.Fragment>
+  );
+};
+
+PrintDocxButton.propTypes = {
+  type: PropTypes.string.isRequired
+};
+
+export default PrintDocxButton;
diff --git a/edera-web/src/components/ra-buttons/RegisterMaintenanceButton.js b/edera-web/src/components/ra-buttons/RegisterMaintenanceButton.js
new file mode 100644 (file)
index 0000000..621df4f
--- /dev/null
@@ -0,0 +1,26 @@
+import { Button } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+import { useTranslate } from '@applica-software-guru/react-admin';
+
+const RegisterMaintenanceButton = (props) => {
+  const navigate = useNavigate();
+  const translate = useTranslate();
+  const { record } = props;
+  const handleClick = () => {
+    navigate('/entities/maintenance/create', {
+      state: {
+        record: {
+          supplyId: record?.id
+        }
+      }
+    });
+  };
+
+  return (
+    <Button variant="outlined" color="primary" onClick={handleClick} fullWidth>
+      {translate('resources.entities/supply.show.register-maintenance')}
+    </Button>
+  );
+};
+
+export default RegisterMaintenanceButton;
diff --git a/edera-web/src/components/ra-buttons/RequestAssistanceButton.js b/edera-web/src/components/ra-buttons/RequestAssistanceButton.js
new file mode 100644 (file)
index 0000000..bc23069
--- /dev/null
@@ -0,0 +1,34 @@
+import { useTranslate } from '@applica-software-guru/react-admin';
+import { Button } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+const RequestAssistanceButton = (props) => {
+  const navigate = useNavigate();
+  const translate = useTranslate();
+  const { record } = props;
+  const handleClick = () => {
+    navigate('/entities/activity/create', {
+      state: {
+        record: {
+          supplierId: record?.supplier?.id,
+          customerId: record?.customer?.id,
+          supplyId: record?.id,
+          supply: {
+            supplierId: record?.supplier?.id
+          },
+          customer: {
+            id: record?.customer?.id
+          }
+        }
+      }
+    });
+  };
+
+  return (
+    <Button variant="contained" color="error" onClick={handleClick} fullWidth>
+      {translate('resources.entities/supply.show.request-assistance')}
+    </Button>
+  );
+};
+
+export default RequestAssistanceButton;
diff --git a/edera-web/src/components/ra-buttons/StartAndStopUsageButton.js b/edera-web/src/components/ra-buttons/StartAndStopUsageButton.js
new file mode 100644 (file)
index 0000000..70e057a
--- /dev/null
@@ -0,0 +1,50 @@
+import React, { useState } from 'react';
+import { useDataProvider, useNotify, useTranslate } from '@applica-software-guru/react-admin';
+import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled';
+import StopCircleIcon from '@mui/icons-material/StopCircle';
+import { Button } from '@mui/material';
+import { get } from 'lodash';
+
+const StartAndStopUsageButton = ({ record, setStatus, setColor }) => {
+  const dataProvider = useDataProvider();
+  const translate = useTranslate();
+  const notify = useNotify();
+  const [status, setUsageStatus] = useState(get(record, 'usageStatus', 'STOPPED') || 'STOPPED');
+
+  const handleStart = () => {
+    dataProvider
+      .get(`supply/supply-start/${record?.id}`)
+      .then(() => {
+        setUsageStatus('RUNNING');
+        setColor('green');
+        setStatus('RUNNING');
+        notify('resources.entities.supply.notifications.started', { type: 'info' });
+      })
+      .catch(() => {
+        notify('ra.notification.http_error', { type: 'error' });
+      });
+  };
+
+  const handleStop = async () => {
+    dataProvider
+      .get(`supply/supply-stop/${record?.id}`)
+      .then(() => {
+        setUsageStatus('STOPPED');
+        setColor('red');
+        setStatus('STOPPED');
+        notify('resources.entities.supply.notifications.stopped', { type: 'info' });
+      })
+      .catch(() => notify('ra.notification.http_error', { type: 'error' }));
+  };
+  return status === 'STOPPED' || status === null ? (
+    <Button variant="outlined" color="secondary" onClick={handleStart} startIcon={<PlayCircleFilledIcon />} fullWidth>
+      {translate('resources.entities/supply.show.status-stopped')}
+    </Button>
+  ) : (
+    <Button variant="outlined" color="secondary" onClick={handleStop} startIcon={<StopCircleIcon />} fullWidth>
+      {translate('resources.entities/supply.show.status-running')}
+    </Button>
+  );
+};
+
+export default StartAndStopUsageButton;
diff --git a/edera-web/src/components/ra-buttons/SupplierBackButton.js b/edera-web/src/components/ra-buttons/SupplierBackButton.js
new file mode 100644 (file)
index 0000000..e62211d
--- /dev/null
@@ -0,0 +1,18 @@
+import { Button } from '@applica-software-guru/react-admin';
+import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
+import { Link } from 'react-router-dom';
+
+const SupplierBackButton = ({ supplierId, tab }) => {
+  return (
+    <Button
+      startIcon={<KeyboardReturnIcon />}
+      label={'resources.entities/equipment.actions.backToFather'}
+      variant="outlined"
+      size="medium"
+      component={Link}
+      to={`/entities/supplier/${supplierId}${tab ? `/${tab}` : ''}`}
+    />
+  );
+};
+
+export default SupplierBackButton;
diff --git a/edera-web/src/components/ra-buttons/index.js b/edera-web/src/components/ra-buttons/index.js
new file mode 100644 (file)
index 0000000..5c367b3
--- /dev/null
@@ -0,0 +1,8 @@
+export { default as PriceListBackButton } from './PriceListBackButton';
+export { default as SupplierBackButton } from './SupplierBackButton';
+export { default as GenerateQRCodePdfButton } from './GenerateQRCodePdfButton';
+export { default as RequestAssistanceButton } from './RequestAssistanceButton';
+export { default as RegisterMaintenanceButton } from './RegisterMaintenanceButton';
+export { default as StartAndStopUsageButton } from './StartAndStopUsageButton';
+export { default as EntityBackButton } from './EntityBackButton';
+export { default as PrintDocxButton } from './PrintDocxButton';
diff --git a/edera-web/src/components/ra-details/SupplyShow.js b/edera-web/src/components/ra-details/SupplyShow.js
new file mode 100644 (file)
index 0000000..3e6a74a
--- /dev/null
@@ -0,0 +1,143 @@
+import 'dayjs/locale/it';
+import 'dayjs/locale/en';
+import { Card, Grid, Typography } from '@mui/material';
+import {
+  EditButton,
+  LongForm,
+  MainCard,
+  Show,
+  TopToolbar,
+  useGetIdentity,
+  usePermissions,
+  useRecordContext,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import {
+  FileOutlined,
+  InboxOutlined,
+  InfoCircleOutlined,
+  LineChartOutlined,
+  PlayCircleOutlined,
+  ScheduleOutlined,
+  SolutionOutlined,
+  UnorderedListOutlined,
+  GlobalOutlined
+} from '@ant-design/icons';
+import React, { useEffect, useState } from 'react';
+import { RegisterMaintenanceButton, RequestAssistanceButton, StartAndStopUsageButton } from 'components/ra-buttons';
+
+import CircleIcon from '@mui/icons-material/Circle';
+import { get } from 'lodash';
+import { useRedirect } from '../../hooks';
+import {
+  ActivityTab,
+  DetailsTab,
+  EquipmentAttachmentTab,
+  ProtocolTab,
+  RFIDDeviceTab,
+  SupplyTab,
+  UsageRecordTab,
+  UsageStatisticsTab
+} from './supply';
+
+const SupplyShowActions = () => {
+  const { permissions, isLoading } = usePermissions();
+  const isEditable = !isLoading && permissions.includes('supply:edit');
+
+  return <TopToolbar>{isEditable && <EditButton />}</TopToolbar>;
+};
+
+const STATUS_COLOR = {
+  RUNNING: 'green',
+  STOPPED: 'red'
+};
+
+const SupplyStatusSidebar = ({ status, color }) => {
+  const translate = useTranslate();
+
+  return (
+    <MainCard sx={{ textAlign: 'center' }}>
+      <Typography>
+        {translate(`ra.supply.card-status.${status}`)} <CircleIcon style={{ color: color, fontSize: 'small' }} />
+      </Typography>
+    </MainCard>
+  );
+};
+
+const SupplyShowContext = () => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  const { identity } = useGetIdentity();
+  const { set: setRedirect } = useRedirect();
+  const [status, setStatus] = useState(get(record, 'usageStatus', 'STOPPED') || 'STOPPED');
+  const [color, setColor] = useState(get(STATUS_COLOR, status, 'red') || 'red');
+
+  useEffect(() => {
+    if (!identity?.email) {
+      setRedirect(document.location.href);
+    }
+    const newStatus = get(record, 'usageStatus', 'STOPPED') || 'STOPPED';
+    setStatus(newStatus);
+    setColor(get(STATUS_COLOR, newStatus, 'red'));
+  }, [identity, setRedirect, record]);
+
+  return (
+    <LongForm>
+      <LongForm.SidebarSection visibility="xs" position={LongForm.SidebarSectionPosition.TOP}>
+        <SupplyStatusSidebar status={status} color={color} />
+      </LongForm.SidebarSection>
+      <LongForm.Tab icon={<InfoCircleOutlined />} id="details" label="resources.entities/supply.tabs.details">
+        <DetailsTab />
+      </LongForm.Tab>
+      <LongForm.Tab icon={<InboxOutlined />} id="supply" label="resources.entities/supply.tabs.supply">
+        <SupplyTab />
+      </LongForm.Tab>
+      <LongForm.Tab icon={<SolutionOutlined />} id="protocol" label="resources.entities/supply.tabs.protocol">
+        <ProtocolTab />
+      </LongForm.Tab>
+      <LongForm.Tab icon={<FileOutlined />} id="equipment-attachment" label="resources.entities/supply.tabs.equipment-attachment">
+        <EquipmentAttachmentTab />
+      </LongForm.Tab>
+      <LongForm.Tab icon={<ScheduleOutlined />} id="activity" label="resources.entities/supply.tabs.activity">
+        <ActivityTab />
+      </LongForm.Tab>
+      <LongForm.Tab icon={<GlobalOutlined />} id="rfid" label="resources.entities/rfid-device-track.tabs.list">
+        <RFIDDeviceTab />
+      </LongForm.Tab>
+      <LongForm.Group icon={<PlayCircleOutlined />} id="usage" label="resources.entities/supply.group.usage">
+        <LongForm.Tab icon={<UnorderedListOutlined />} id="register" label="resources.entities/usage.tabs.record">
+          <UsageRecordTab />
+        </LongForm.Tab>
+        <LongForm.Tab icon={<LineChartOutlined />} id="chart" label="resources.entities/usage.tabs.chart">
+          <UsageStatisticsTab />
+        </LongForm.Tab>
+      </LongForm.Group>
+      <LongForm.SidebarSection position={LongForm.SidebarSectionPosition.BOTTOM} visibility="xs">
+        <Card sx={{ marginTop: 3, backgroundColor: 'transparent' }}>
+          <Grid container spacing={spacing}>
+            <Grid item xs={12} sm={12}>
+              <StartAndStopUsageButton record={record} setColor={setColor} setStatus={setStatus} />
+            </Grid>
+            <Grid item xs={12} sm={12}>
+              <RequestAssistanceButton record={record} />
+            </Grid>
+            <Grid item xs={12} sm={12}>
+              <RegisterMaintenanceButton record={record} />
+            </Grid>
+          </Grid>
+        </Card>
+      </LongForm.SidebarSection>
+    </LongForm>
+  );
+};
+
+const SupplyShow = () => {
+  return (
+    <Show actions={<SupplyShowActions />}>
+      <SupplyShowContext />
+    </Show>
+  );
+};
+
+export default SupplyShow;
diff --git a/edera-web/src/components/ra-details/index.js b/edera-web/src/components/ra-details/index.js
new file mode 100644 (file)
index 0000000..ff208f3
--- /dev/null
@@ -0,0 +1,2 @@
+import SupplyShow from './SupplyShow';
+export { SupplyShow };
diff --git a/edera-web/src/components/ra-details/supply/ActivityTab.js b/edera-web/src/components/ra-details/supply/ActivityTab.js
new file mode 100644 (file)
index 0000000..631c981
--- /dev/null
@@ -0,0 +1,56 @@
+import {
+  Datagrid,
+  DateField,
+  ReferenceField,
+  ReferenceManyInput,
+  SearchInput,
+  SelectInput,
+  TextField,
+  usePermissions,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import { ActivityStatusField } from 'components/ra-fields';
+import { FixedMainCard } from 'components/layout';
+import { ActivityTypeAutocompleteInput, CustomerAutocompleteInput } from 'components/ra-inputs';
+import { ACTIVITY_STATUS } from 'config';
+
+const ActivityTab = () => {
+  const translate = useTranslate();
+  const { permissions, isLoading } = usePermissions();
+  const filtersList = [
+    <SearchInput source="keyword" alwaysOn />,
+    <ActivityTypeAutocompleteInput source="activityTypeId" alwaysOn display="legend" />,
+    <SelectInput source="status" display="legend" alwaysOn choices={ACTIVITY_STATUS} />
+  ];
+
+  if (!isLoading && permissions.includes('customer:list')) {
+    filtersList.push(<CustomerAutocompleteInput source="customerId" alwaysOn display="legend" />);
+  }
+  return (
+    <FixedMainCard title={translate('resources.entities/supply.show.activity')}>
+      <ReferenceManyInput reference="entities/activity" target="supplyId" filters={filtersList}>
+        <Datagrid bulkActionButtons={false} rowClick="edit">
+          {!isLoading && permissions.includes('supplier:list') && (
+            <ReferenceField source="supply.supplierId" reference="entities/supplier" link={false} sortable={false}>
+              <TextField source="businessName" />
+            </ReferenceField>
+          )}
+          {!isLoading && permissions.includes('customer:list') && <TextField source="customer.name" sortable={false} />}
+          {!isLoading && permissions.includes('equipment:list') && (
+            <ReferenceField source="supply.equipmentId" reference="entities/equipment" link={false} sortable={false}>
+              <TextField source="equipmentType.name" />
+            </ReferenceField>
+          )}
+          <TextField source="activityType.description" sortable={false} />
+          <TextField source="title" />
+          <TextField source="user.name" sortable={false} />
+          <DateField source="date" />
+          <DateField source="updated" showTime />
+          <ActivityStatusField source="status" />
+        </Datagrid>
+      </ReferenceManyInput>
+    </FixedMainCard>
+  );
+};
+
+export default ActivityTab;
diff --git a/edera-web/src/components/ra-details/supply/DetailsTab.js b/edera-web/src/components/ra-details/supply/DetailsTab.js
new file mode 100644 (file)
index 0000000..627132c
--- /dev/null
@@ -0,0 +1,62 @@
+import { Grid } from '@mui/material';
+import {
+  FunctionField,
+  MainCard,
+  ReadonlyField,
+  ReferenceField,
+  usePermissions,
+  useRecordContext,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import React, { Fragment } from 'react';
+import { SupplyLastTrackAlert } from '../../ra-alerts';
+
+const DetailsTab = () => {
+  const translate = useTranslate();
+  const { permissions, isLoading } = usePermissions();
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  return (
+    <MainCard title={translate('resources.entities/supply.tabs.details')}>
+      <Grid container spacing={spacing}>
+        {!isLoading && permissions.includes('supplier:list') && (
+          <Grid item xs={12} sm={6}>
+            <ReadonlyField source="supplier.businessName" />
+          </Grid>
+        )}
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="equipmentType.name" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="protocol.title" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="customer.name" />
+        </Grid>
+        {!isLoading && permissions.includes('rfid-device:list') && record?.rfidDeviceId && (
+          <Grid item xs={12} sm={6}>
+            <ReadonlyField source="rfidDevice.name" />
+          </Grid>
+        )}
+        {!isLoading && permissions.includes('customer:new') && record?.areaId && (
+          <Fragment>
+            <Grid item xs={12} sm={6}>
+              <ReadonlyField source="area.officeId">
+                <ReferenceField source="area.officeId" reference="entities/customer-office" link={false}>
+                  <FunctionField render={(record) => `${record.city.name} - ${record.address}`} />
+                </ReferenceField>
+              </ReadonlyField>
+            </Grid>
+            <Grid item xs={12} sm={6}>
+              <ReadonlyField source="area.name" />
+            </Grid>
+          </Fragment>
+        )}
+      </Grid>
+      {!isLoading && permissions.includes('rfid-device:list') && <SupplyLastTrackAlert />}
+    </MainCard>
+  );
+};
+
+export default DetailsTab;
diff --git a/edera-web/src/components/ra-details/supply/EquipmentAttachmentTab.js b/edera-web/src/components/ra-details/supply/EquipmentAttachmentTab.js
new file mode 100644 (file)
index 0000000..1e8d6a0
--- /dev/null
@@ -0,0 +1,18 @@
+import { AttachmentField, Datagrid, ReferenceManyField, TextField, useTranslate } from '@applica-software-guru/react-admin';
+import { FixedMainCard } from 'components/layout';
+
+const EquipmentAttachmentTab = () => {
+  const translate = useTranslate();
+  return (
+    <FixedMainCard title={translate('resources.entities/supply.show.equipment-attachment')}>
+      <ReferenceManyField reference="entities/equipment-attachment" source="equipmentId" target="equipmentId">
+        <Datagrid bulkActionButtons={false}>
+          <TextField source="description" />
+          <AttachmentField source="attachment" title="attachment.name" sortable={false} />
+        </Datagrid>
+      </ReferenceManyField>
+    </FixedMainCard>
+  );
+};
+
+export default EquipmentAttachmentTab;
diff --git a/edera-web/src/components/ra-details/supply/ProtocolTab.js b/edera-web/src/components/ra-details/supply/ProtocolTab.js
new file mode 100644 (file)
index 0000000..27f731a
--- /dev/null
@@ -0,0 +1,27 @@
+import { Grid } from '@mui/material';
+import { MainCard, ReadonlyField, useThemeConfig, useTranslate } from '@applica-software-guru/react-admin';
+
+const ProtocolTab = () => {
+  const translate = useTranslate();
+  const { spacing } = useThemeConfig();
+  return (
+    <MainCard title={translate('resources.entities/supply.show.protocol-info')}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="protocol.title" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="protocol.description" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="protocol.protocolNumber" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="protocol.protocolDate" />
+        </Grid>
+      </Grid>
+    </MainCard>
+  );
+};
+
+export default ProtocolTab;
diff --git a/edera-web/src/components/ra-details/supply/RFIDDeviceTab.js b/edera-web/src/components/ra-details/supply/RFIDDeviceTab.js
new file mode 100644 (file)
index 0000000..d99f777
--- /dev/null
@@ -0,0 +1,31 @@
+import { Datagrid, DateField, ReferenceField, ReferenceManyInput, TextField, useTranslate } from '@applica-software-guru/react-admin';
+import { FixedMainCard } from 'components/layout';
+import { RFIDDeviceAutocomplete, CustomerAreaAutocompleteInput } from 'components/ra-inputs';
+
+const RFIDDeviceTab = () => {
+  const translate = useTranslate();
+  return (
+    <FixedMainCard title={translate('resources.entities/rfid-device-track.tabs.list')}>
+      <ReferenceManyInput
+        reference="entities/rfid-device-track"
+        target="supplyId"
+        filters={[
+          <CustomerAreaAutocompleteInput source="areaId" display="legend" alwaysOn />,
+          <RFIDDeviceAutocomplete source="gatewayId" display="legend" type="GATEWAY" alwaysOn />
+        ]}
+        sort={{ field: 'ts', order: 'DESC' }}
+      >
+        <Datagrid bulkActionButtons={null}>
+          <TextField source="gateway.name" sortable={false} />
+          <ReferenceField source="areaId" reference="entities/customer-area" sortable={false}>
+            <TextField source="name" />
+          </ReferenceField>
+          <TextField source="additionalData" />
+          <DateField source="ts" showTime />
+        </Datagrid>
+      </ReferenceManyInput>
+    </FixedMainCard>
+  );
+};
+
+export default RFIDDeviceTab;
diff --git a/edera-web/src/components/ra-details/supply/SupplyTab.js b/edera-web/src/components/ra-details/supply/SupplyTab.js
new file mode 100644 (file)
index 0000000..0a086f1
--- /dev/null
@@ -0,0 +1,33 @@
+import { Grid } from '@mui/material';
+import { MainCard, ReadonlyField, useThemeConfig, useTranslate } from '@applica-software-guru/react-admin';
+
+const SupplyTab = () => {
+  const translate = useTranslate();
+  const { spacing } = useThemeConfig();
+  return (
+    <MainCard title={translate('resources.entities/supply.show.supply-info')}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="equipmentType.name" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="quantity" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="orderDate" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="deliveryDate" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="serialNumber" />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ReadonlyField source="ddtNumber" />
+        </Grid>
+      </Grid>
+    </MainCard>
+  );
+};
+
+export default SupplyTab;
diff --git a/edera-web/src/components/ra-details/supply/UsageRecordTab.js b/edera-web/src/components/ra-details/supply/UsageRecordTab.js
new file mode 100644 (file)
index 0000000..03b78ff
--- /dev/null
@@ -0,0 +1,35 @@
+import 'dayjs/locale/it';
+import 'dayjs/locale/en';
+import { Datagrid, DateField, FunctionField, ReferenceManyField, useLocaleState, useTranslate } from '@applica-software-guru/react-admin';
+
+import { FixedMainCard } from 'components/layout';
+import dayjs from 'dayjs';
+
+const UsageRecordTab = () => {
+  const translate = useTranslate();
+  const locale = useLocaleState()[0];
+  return (
+    <FixedMainCard title={translate('resources.entities/usage.tabs.record')}>
+      <ReferenceManyField reference="entities/usage" target="supplyId" perPage={10}>
+        <Datagrid bulkActionButtons={false}>
+          <DateField source="start" showTime />
+          <DateField source="stop" showTime />
+          <FunctionField
+            label="resources.entities/supply.fields.duration"
+            render={({ start, stop }) => {
+              if (!stop) {
+                return translate('resources.entities/supply.fields.duration.running');
+              }
+              return dayjs
+                .duration(dayjs(stop).diff(dayjs(start)))
+                .locale(locale)
+                .humanize();
+            }}
+          />
+        </Datagrid>
+      </ReferenceManyField>
+    </FixedMainCard>
+  );
+};
+
+export default UsageRecordTab;
diff --git a/edera-web/src/components/ra-details/supply/UsageStatisticsTab.js b/edera-web/src/components/ra-details/supply/UsageStatisticsTab.js
new file mode 100644 (file)
index 0000000..720b3bd
--- /dev/null
@@ -0,0 +1,137 @@
+import { DataMainCard, UsageChart, UsageMLBreakChart, UsageMLHourlyChart, useReportData } from 'components/pages/charts';
+import { DateInput, MainCard, useRecordContext, useThemeConfig, useTranslate } from '@applica-software-guru/react-admin';
+import { Grid, Button, Collapse, Alert } from '@mui/material';
+import { useWatch } from 'react-hook-form';
+import dayjs from 'dayjs';
+import { Fragment, useState } from 'react';
+import AutorenewIcon from '@mui/icons-material/Autorenew';
+
+const fakeData = {
+  customUsage: {
+    points: [
+      {
+        month: dayjs().month(),
+        year: dayjs().year(),
+        day: dayjs().day(),
+        total: 1
+      }
+    ],
+    count: 1
+  }
+};
+
+const convertDateToISO = (date) => {
+  if (!date) {
+    return null;
+  }
+  const day = dayjs(date);
+  const dayWithTime = day.hour(0).minute(0).second(0);
+  return dayWithTime.add(1, 'hours').toISOString();
+};
+
+const formatExponentialToFixed = (number) => {
+  const num = Number(number);
+  if (isNaN(num) || num === 0) return '0.00';
+  return num.toFixed(2);
+};
+
+const UsageStatisticsTab = () => {
+  const record = useRecordContext();
+  const { spacing } = useThemeConfig();
+  const translate = useTranslate();
+  const from = useWatch({ name: 'from' });
+  const to = useWatch({ name: 'to' });
+  const [trigger, setTrigger] = useState(false);
+  const handleClick = () => {
+    setTrigger((prev) => !prev);
+  };
+  const { data, isFetching } = useReportData({
+    supplyId: record?.id,
+    from: to ? convertDateToISO(from) : undefined,
+    to: to ? convertDateToISO(to) : undefined,
+    trigger: trigger,
+    type: 'supply'
+  });
+
+  return (
+    <Fragment>
+      <MainCard title={translate('resources.entities/usage.tabs.chart')} divider={false}>
+        <Grid container spacing={spacing}>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.supply.monthly-usage.title')}
+              data={!isFetching && formatExponentialToFixed(data?.monthUsage?.points[0]?.totalUsageHours)}
+              unit={translate('reporting.stats.unit.hours')}
+            />
+          </Grid>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.supply.yearly-usage.title')}
+              data={!isFetching && formatExponentialToFixed(data?.yearUsage?.points[0]?.totalUsageDays)}
+              unit={translate('reporting.stats.unit.days')}
+            />
+          </Grid>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.supply.average-monthly-usage.title')}
+              data={(!isFetching && data?.averageMonthUsage?.points[0]?.count) || 0}
+              unit=""
+            />
+          </Grid>
+          <Grid item sm={3} xs={12}>
+            <DataMainCard
+              isFetching={isFetching}
+              title={translate('reporting.stats.supply.monthly-usage-percentage.title')}
+              data={!isFetching && formatExponentialToFixed(data?.monthlyUtilizationPercentage?.points[0]?.utilizationPercentage)}
+              unit="%"
+            />
+          </Grid>
+          <Grid item sm={5} xs={12}>
+            <DateInput source="from" />
+          </Grid>
+          <Grid item sm={5} xs={12}>
+            <DateInput source="to" />
+          </Grid>
+          <Grid item sm={2} xs={12} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', marginTop: 3 }}>
+            <Button variant="contained" size="medium" color="primary" onClick={handleClick}>
+              <AutorenewIcon />
+            </Button>
+          </Grid>
+          <Grid item sm={12} xs={12}>
+            <Collapse in={isFetching}>
+              <Alert severity="info">{translate('reporting.stats.loading')}</Alert>
+            </Collapse>
+          </Grid>
+          {!isFetching && (
+            <Grid item sm={12} xs={12}>
+              <UsageChart data={data} />
+            </Grid>
+          )}
+        </Grid>
+      </MainCard>
+      {!isFetching && (
+        <Fragment>
+          <MainCard sx={{ mt: 2 }} title={translate('resources.entities/usage.tabs.chart.predictions')} divider={false}>
+            <Grid container spacing={spacing}>
+              <Grid item sm={12} xs={12}>
+                <UsageMLHourlyChart data={data} />
+              </Grid>
+            </Grid>
+          </MainCard>
+          <MainCard sx={{ mt: 2 }} title={translate('resources.entities/usage.tabs.chart.breakPredictions')} divider={false}>
+            <Grid container spacing={spacing}>
+              <Grid item sm={12} xs={12}>
+                <UsageMLBreakChart data={isFetching ? fakeData : data} />
+              </Grid>
+            </Grid>
+          </MainCard>
+        </Fragment>
+      )}
+    </Fragment>
+  );
+};
+
+export default UsageStatisticsTab;
diff --git a/edera-web/src/components/ra-details/supply/index.js b/edera-web/src/components/ra-details/supply/index.js
new file mode 100644 (file)
index 0000000..970bb1d
--- /dev/null
@@ -0,0 +1,10 @@
+import UsageStatisticsTab from './UsageStatisticsTab';
+import DetailsTab from './DetailsTab';
+import SupplyTab from './SupplyTab';
+import ProtocolTab from './ProtocolTab';
+import EquipmentAttachmentTab from './EquipmentAttachmentTab';
+import ActivityTab from './ActivityTab';
+import UsageRecordTab from './UsageRecordTab';
+import RFIDDeviceTab from './RFIDDeviceTab';
+
+export { UsageStatisticsTab, DetailsTab, SupplyTab, ProtocolTab, EquipmentAttachmentTab, ActivityTab, UsageRecordTab, RFIDDeviceTab };
diff --git a/edera-web/src/components/ra-fields/ActivityStatusField.js b/edera-web/src/components/ra-fields/ActivityStatusField.js
new file mode 100644 (file)
index 0000000..8e44388
--- /dev/null
@@ -0,0 +1,18 @@
+import { useRecordContext, useTranslate } from '@applica-software-guru/react-admin';
+
+import { Chip } from '@mui/material';
+import { get } from 'lodash';
+
+const STATUS_COLOR = {
+  OPEN: 'info',
+  CLOSED: 'error'
+};
+const ActivityStatusField = () => {
+  const translate = useTranslate();
+  const record = useRecordContext();
+  const status = get(record, 'status', 'new') || 'new';
+  const color = get(STATUS_COLOR, status, 'info');
+  return <Chip size="small" variant="combined" color={color} label={translate(`ra.activity.status.${status}`)} />;
+};
+
+export default ActivityStatusField;
diff --git a/edera-web/src/components/ra-fields/AuditLogMessageField.js b/edera-web/src/components/ra-fields/AuditLogMessageField.js
new file mode 100644 (file)
index 0000000..507b46c
--- /dev/null
@@ -0,0 +1,38 @@
+import { useRecordContext, useTranslate } from '@applica-software-guru/react-admin';
+
+import PropTypes from 'prop-types';
+import { Typography } from '@mui/material';
+import { get } from 'lodash';
+
+const TRANSATABLE_FIELDS = ['entity'];
+const AuditLogMessageField = ({ source }) => {
+  const translate = useTranslate();
+  const record = useRecordContext();
+  const args = get(record, `${source}.args`);
+  const message = get(record, `${source}.message`) || 'ra.audit_log.default_message';
+  const messageArgs =
+    args && args !== null
+      ? Object.keys(args).reduce(
+          (acc, key) => ({
+            ...acc,
+            [key]: TRANSATABLE_FIELDS.indexOf(key) !== -1 ? translate(get(args, key, ''), { smart_count: 1 }) : get(args, key, '')
+          }),
+          {}
+        )
+      : {
+          ...record,
+          method: record?.method || 'unknown'
+        };
+
+  return (
+    <Typography variant="body1" color="textSecondary" sx={{ minWidth: 300 }}>
+      {translate(message, messageArgs)}
+    </Typography>
+  );
+};
+
+AuditLogMessageField.propTypes = {
+  source: PropTypes.string.isRequired
+};
+
+export default AuditLogMessageField;
diff --git a/edera-web/src/components/ra-fields/CityField.js b/edera-web/src/components/ra-fields/CityField.js
new file mode 100644 (file)
index 0000000..deb6209
--- /dev/null
@@ -0,0 +1,9 @@
+import { ReferenceField, TextField } from '@applica-software-guru/react-admin';
+
+const CityField = (props) => (
+  <ReferenceField {...props} label="city" reference="entities/city">
+    <TextField source="name" />
+  </ReferenceField>
+);
+
+export default CityField;
diff --git a/edera-web/src/components/ra-fields/DirtyFormPrintAlertField.js b/edera-web/src/components/ra-fields/DirtyFormPrintAlertField.js
new file mode 100644 (file)
index 0000000..38a3ad9
--- /dev/null
@@ -0,0 +1,20 @@
+import { useFormState } from 'react-hook-form';
+import { Collapse, Alert, AlertTitle } from '@mui/material';
+import { useTranslate } from '@applica-software-guru/react-admin';
+
+const DirtyFormPrintAlertField = () => {
+  const { dirtyFields } = useFormState();
+  const isDirty = Object.keys(dirtyFields).length > 0;
+  const translate = useTranslate();
+
+  return (
+    <Collapse in={isDirty}>
+      <Alert severity="warning">
+        <AlertTitle>{translate('ra.form.docx_print.dirty')}</AlertTitle>
+        {translate('ra.form.docx_print.dirty_message')}
+      </Alert>
+    </Collapse>
+  );
+};
+
+export default DirtyFormPrintAlertField;
diff --git a/edera-web/src/components/ra-fields/MoneyField.js b/edera-web/src/components/ra-fields/MoneyField.js
new file mode 100644 (file)
index 0000000..07e61e5
--- /dev/null
@@ -0,0 +1,15 @@
+import { FunctionField } from '@applica-software-guru/react-admin';
+import { get } from 'lodash';
+const formatMoney = (record, source) => {
+  const value = get(record, source);
+  if (typeof value === 'number') {
+    return `€ ${value.toFixed(2).replace('.', ',')}`;
+  }
+  return '';
+};
+
+const MoneyField = (props) => {
+  return <FunctionField {...props} render={(record) => formatMoney(record, props.source)} />;
+};
+
+export default MoneyField;
diff --git a/edera-web/src/components/ra-fields/RFIDDeviceTrackManyField.js b/edera-web/src/components/ra-fields/RFIDDeviceTrackManyField.js
new file mode 100644 (file)
index 0000000..c3deaed
--- /dev/null
@@ -0,0 +1,27 @@
+import { Datagrid, DateField, ReferenceField, ReferenceManyInput, TextField } from '@applica-software-guru/react-admin';
+import { CustomerAreaAutocompleteInput, RFIDDeviceAutocomplete } from 'components/ra-inputs';
+
+const RFIDDeviceTrackManyField = () => {
+  return (
+    <ReferenceManyInput
+      reference="entities/rfid-device-track"
+      target="supplyId"
+      filters={[
+        <CustomerAreaAutocompleteInput source="areaId" display="legend" alwaysOn />,
+        <RFIDDeviceAutocomplete source="gatewayId" display="legend" type="GATEWAY" alwaysOn />
+      ]}
+      sort={{ field: 'ts', order: 'DESC' }}
+    >
+      <Datagrid bulkActionButtons={null}>
+        <TextField source="gateway.name" sortable={false} />
+        <ReferenceField source="areaId" reference="entities/customer-area" sortable={false}>
+          <TextField source="name" />
+        </ReferenceField>
+        <TextField source="additionalData" />
+        <DateField source="ts" showTime />
+      </Datagrid>
+    </ReferenceManyInput>
+  );
+};
+
+export default RFIDDeviceTrackManyField;
diff --git a/edera-web/src/components/ra-fields/SupplyStatusField.js b/edera-web/src/components/ra-fields/SupplyStatusField.js
new file mode 100644 (file)
index 0000000..6f40028
--- /dev/null
@@ -0,0 +1,18 @@
+import { useRecordContext, useTranslate } from '@applica-software-guru/react-admin';
+
+import { Chip } from '@mui/material';
+import { get } from 'lodash';
+
+const STATUS_COLOR = {
+  RUNNING: 'success',
+  STOPPED: 'secondary'
+};
+const SupplyStatusField = () => {
+  const record = useRecordContext();
+  const translate = useTranslate();
+  const status = get(record, 'usageStatus', 'STOPPED') || 'STOPPED';
+  const color = get(STATUS_COLOR, status, 'secondary');
+  return <Chip variant="combined" color={color} label={translate(`ra.supply.status.${status}`)} />;
+};
+
+export default SupplyStatusField;
diff --git a/edera-web/src/components/ra-fields/UserPictureField.js b/edera-web/src/components/ra-fields/UserPictureField.js
new file mode 100644 (file)
index 0000000..c8008b7
--- /dev/null
@@ -0,0 +1,20 @@
+import { CoverField, useRecordContext } from '@applica-software-guru/react-admin';
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { get } from 'lodash';
+
+const UserPictureField = ({ source, ...props }) => {
+  const record = useRecordContext();
+  const image = get(record, source);
+  if (!image) {
+    return null;
+  }
+  return <CoverField {...props} circle width={50} height={50} justify="flex-end" />;
+};
+
+UserPictureField.propTypes = {
+  source: PropTypes.string.isRequired
+};
+
+export default UserPictureField;
diff --git a/edera-web/src/components/ra-fields/index.js b/edera-web/src/components/ra-fields/index.js
new file mode 100644 (file)
index 0000000..9ed1a72
--- /dev/null
@@ -0,0 +1,8 @@
+export { default as AuditLogMessageField } from './AuditLogMessageField';
+export { default as UserPictureField } from './UserPictureField';
+export { default as CityField } from './CityField';
+export { default as MoneyField } from './MoneyField';
+export { default as ActivityStatusField } from './ActivityStatusField';
+export { default as SupplyStatusField } from './SupplyStatusField';
+export { default as RFIDDeviceTrackManyField } from './RFIDDeviceTrackManyField';
+export { default as DirtyFormPrintAlertField } from './DirtyFormPrintAlertField';
diff --git a/edera-web/src/components/ra-forms/ActivityForm.js b/edera-web/src/components/ra-forms/ActivityForm.js
new file mode 100644 (file)
index 0000000..caad150
--- /dev/null
@@ -0,0 +1,163 @@
+import {
+  ActionsMenu,
+  CardForm,
+  DateInput,
+  DeleteWithConfirmButton,
+  ReadonlyField,
+  SaveButton,
+  SelectInput,
+  SmartTextInput,
+  TextInput,
+  Toolbar,
+  required,
+  useDataProvider,
+  useNotify,
+  usePermissions,
+  useRecordContext,
+  useResourceTitle,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+import {
+  ActivityTypeAutocompleteInput,
+  CustomerAutocompleteInput,
+  MaintenanceManyInput,
+  SupplierAutocompleteInput,
+  SupplyActivityAutocompleteInput
+} from 'components/ra-inputs';
+import { useLocation } from 'react-router-dom';
+import { Grid } from '@mui/material';
+import { EntityBackButton, PrintDocxButton } from 'components/ra-buttons';
+import queryString from 'query-string';
+import { useEffect, useState } from 'react';
+
+const ActivityForm = ({ activityStatus }) => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  const dataProvider = useDataProvider();
+  const notify = useNotify();
+  const currentDate = new Date().toISOString().slice(0, 10);
+  const title = useResourceTitle();
+  const { permissions, isLoading } = usePermissions();
+  const canEdit = !isLoading && permissions.includes('activity:new');
+  const canDelete = !isLoading && permissions.includes('activity:delete');
+  const location = useLocation();
+  const queryParams = queryString.parse(location.search);
+  const activityId = queryParams['activity-id'];
+  const activityTypeIdUrl = queryParams['activity-type-id'];
+  const [activity, setActivity] = useState(null);
+
+  useEffect(() => {
+    if (!activityId) return;
+
+    const fetchDetails = async () => {
+      try {
+        const { data } = await dataProvider.getOne('entities/activity', { id: activityId });
+        setActivity(data);
+      } catch (error) {
+        notify('ra.notification.http_error', 'error', {});
+      }
+    };
+
+    fetchDetails();
+  }, [activityId, dataProvider, notify]);
+
+  return (
+    <CardForm>
+      <CardForm.Section
+        toolbar={null}
+        title={title}
+        secondary={
+          record?.id &&
+          canDelete && (
+            <ActionsMenu>
+              <DeleteWithConfirmButton />
+            </ActionsMenu>
+          )
+        }
+      >
+        <Grid container spacing={spacing}>
+          {record?.id && (
+            <Grid item xs={12} sm={12}>
+              <ReadonlyField source="user.name" />
+            </Grid>
+          )}
+          {!isLoading && permissions.includes('supplier:list') && (
+            <Grid item xs={12} sm={12}>
+              <SupplierAutocompleteInput
+                source="supply.supplierId"
+                defaultValue={activity?.supply.supplierId}
+                disabled={!canEdit}
+                validate={required()}
+                fullWidth
+              />
+            </Grid>
+          )}
+          {!isLoading && permissions.includes('customer:list') && (
+            <Grid item xs={12} sm={12}>
+              <CustomerAutocompleteInput
+                source="customer.id"
+                defaultValue={activity?.customer.id}
+                disabled={!canEdit}
+                validate={required()}
+                fullWidth
+              />
+            </Grid>
+          )}
+          {!isLoading && permissions.includes('supply:list') && (
+            <Grid item xs={12} sm={12}>
+              <SupplyActivityAutocompleteInput
+                source="supplyId"
+                defaultValue={activity?.supplyId}
+                disabled={!canEdit}
+                validate={required()}
+                fullWidth
+              />
+            </Grid>
+          )}
+          {!isLoading && permissions.includes('activity-type:list') && (
+            <Grid item xs={12} sm={12}>
+              <ActivityTypeAutocompleteInput
+                source="activityTypeId"
+                defaultValue={activityTypeIdUrl}
+                disabled={!canEdit}
+                validate={required()}
+                fullWidth
+              />
+            </Grid>
+          )}
+          <Grid item xs={12} sm={12}>
+            <TextInput source="title" validate={required()} disabled={!canEdit} fullWidth />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <SmartTextInput source="description" validate={required()} maxLength={500} disabled={!canEdit} multiline rows={3} />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <DateInput source="date" defaultValue={currentDate} validate={required()} disabled={!canEdit} fullWidth />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <SelectInput
+              source="status"
+              validate={required()}
+              fullWidth
+              choices={activityStatus}
+              defaultValue={'OPEN'}
+              disabled={!canEdit}
+            />
+          </Grid>
+        </Grid>
+        <Toolbar>
+          {canEdit && <SaveButton />}
+          <PrintDocxButton type="activity" />
+          {record?.id && <EntityBackButton type="supply" id={record?.supplyId} label="resources.entities/activity.actions.backToFather" />}
+        </Toolbar>
+      </CardForm.Section>
+      {record?.id && (
+        <CardForm.Section title="resources.entities/maintenance.title">
+          <MaintenanceManyInput />
+        </CardForm.Section>
+      )}
+    </CardForm>
+  );
+};
+
+export default ActivityForm;
diff --git a/edera-web/src/components/ra-forms/ActivityTypeForm.js b/edera-web/src/components/ra-forms/ActivityTypeForm.js
new file mode 100644 (file)
index 0000000..dee955d
--- /dev/null
@@ -0,0 +1,21 @@
+import { AttachmentInput, SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const ActivityTypeForm = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="description" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <AttachmentInput source="attachment" multiple={false} label={false} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default ActivityTypeForm;
diff --git a/edera-web/src/components/ra-forms/CustomerAreaForm.js b/edera-web/src/components/ra-forms/CustomerAreaForm.js
new file mode 100644 (file)
index 0000000..6106e68
--- /dev/null
@@ -0,0 +1,29 @@
+import { SimpleForm, SmartTextInput, TextInput, required, useRecordContext, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { CustomerOfficeAutocompleteInput, RFIDDeviceAutocomplete } from '../ra-inputs';
+import { Grid } from '@mui/material';
+
+const CustomerAreaForm = () => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  return (
+    <SimpleForm secondary={null}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12}>
+          <CustomerOfficeAutocompleteInput source="officeId" filter={{ customerId: record?.customerId }} />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="name" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <SmartTextInput source="description" validate={required()} fullWidth maxLength={200} />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <RFIDDeviceAutocomplete type="GATEWAY" source="rfidDeviceId" />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default CustomerAreaForm;
diff --git a/edera-web/src/components/ra-forms/CustomerCreateForm.js b/edera-web/src/components/ra-forms/CustomerCreateForm.js
new file mode 100644 (file)
index 0000000..bf72951
--- /dev/null
@@ -0,0 +1,25 @@
+import { BooleanInput, SimpleForm, TextInput, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const CustomerCreateForm = (props) => {
+  const { spacing } = useThemeConfig();
+
+  return (
+    <SimpleForm {...props} defaultValues={{ active: true }}>
+      <Grid container spacing={spacing}>
+        <Grid item lg={7} md={8} xs={12}>
+          <TextInput source="name" fullWidth />
+        </Grid>
+        <Grid item lg={3} md={4} xs={12}>
+          <TextInput source="vatCode" />
+        </Grid>
+        <Grid item lg={2} xs={12}>
+          <BooleanInput source="active" />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default CustomerCreateForm;
diff --git a/edera-web/src/components/ra-forms/CustomerEditForm.js b/edera-web/src/components/ra-forms/CustomerEditForm.js
new file mode 100644 (file)
index 0000000..688182a
--- /dev/null
@@ -0,0 +1,82 @@
+import {
+  ActionsMenu,
+  BooleanInput,
+  DeleteWithConfirmButton,
+  LongForm,
+  MainCard,
+  SaveButton,
+  TextInput,
+  Toolbar,
+  required,
+  useRecordContext,
+  useResourceTitle,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import { CustomerAreaManyInput, CustomerOfficeManyInput, CustomerReferentManyInput } from '../ra-inputs';
+import { HomeOutlined, InfoCircleOutlined, ShareAltOutlined, UserAddOutlined } from '@ant-design/icons';
+
+import { FixedMainCard } from '../layout';
+import { Grid } from '@mui/material';
+
+const CustomerEditForm = () => {
+  const { spacing } = useThemeConfig();
+  const translate = useTranslate();
+  const record = useRecordContext();
+  const title = useResourceTitle();
+  return (
+    <LongForm>
+      <LongForm.Tab
+        id="details"
+        icon={<InfoCircleOutlined />}
+        label="resources.entities/customer.tabs.details"
+        title={title}
+        secondary={
+          <ActionsMenu>
+            <DeleteWithConfirmButton />
+          </ActionsMenu>
+        }
+      >
+        <MainCard title={translate('resources.entities/customer.tabs.details')}>
+          <Grid container spacing={spacing}>
+            <Grid item xs={12}>
+              <TextInput source="name" validate={required()} />
+            </Grid>
+            <Grid item lg={9} xs={12}>
+              <TextInput source="vatCode" validate={required()} />
+            </Grid>
+            <Grid item lg={3} xs={12}>
+              <BooleanInput source="active" />
+            </Grid>
+          </Grid>
+          <Toolbar>
+            <SaveButton />
+          </Toolbar>
+        </MainCard>
+      </LongForm.Tab>
+      {record?.id && (
+        <LongForm.Tab id="offices" icon={<HomeOutlined />} label="resources.entities/customer.tabs.offices">
+          <FixedMainCard>
+            <CustomerOfficeManyInput />
+          </FixedMainCard>
+        </LongForm.Tab>
+      )}
+      {record?.id && (
+        <LongForm.Tab id="areas" icon={<ShareAltOutlined />} label="resources.entities/customer.tabs.areas">
+          <FixedMainCard>
+            <CustomerAreaManyInput />
+          </FixedMainCard>
+        </LongForm.Tab>
+      )}
+      {record?.id && (
+        <LongForm.Tab id="referents" icon={<UserAddOutlined />} label="resources.entities/customer.tabs.referents">
+          <FixedMainCard>
+            <CustomerReferentManyInput />
+          </FixedMainCard>
+        </LongForm.Tab>
+      )}
+    </LongForm>
+  );
+};
+
+export default CustomerEditForm;
diff --git a/edera-web/src/components/ra-forms/CustomerOfficeForm.js b/edera-web/src/components/ra-forms/CustomerOfficeForm.js
new file mode 100644 (file)
index 0000000..5722c51
--- /dev/null
@@ -0,0 +1,46 @@
+import { BooleanInput, SimpleForm, TextInput, email, required, useThemeConfig } from '@applica-software-guru/react-admin';
+import { CityAutocompleteInput, NationAutocompleteInput, ProvinceAutocompleteInput, RegionAutocompleteInput } from '../ra-inputs';
+
+import { Grid } from '@mui/material';
+import PropTypes from 'prop-types';
+
+const CustomerOfficeForm = ({ modal, ...props }) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm modal={modal} {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={6}>
+          <NationAutocompleteInput source="nationId" validate={required()} />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <RegionAutocompleteInput source="regionId" validate={required()} />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <ProvinceAutocompleteInput source="provinceId" validate={required()} />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <CityAutocompleteInput filterByProvince={true} source="cityId" validate={required()} />
+        </Grid>
+        <Grid item xs={12}>
+          <TextInput source="address" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12}>
+          <TextInput source="email" validate={[required(), email()]} />
+        </Grid>
+        <Grid item xs={12}>
+          <BooleanInput source="headquarter" />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+CustomerOfficeForm.propTypes = {
+  modal: PropTypes.bool
+};
+
+CustomerOfficeForm.defaultProps = {
+  modal: true
+};
+
+export default CustomerOfficeForm;
diff --git a/edera-web/src/components/ra-forms/CustomerReferentForm.js b/edera-web/src/components/ra-forms/CustomerReferentForm.js
new file mode 100644 (file)
index 0000000..5b474ef
--- /dev/null
@@ -0,0 +1,44 @@
+import { SimpleForm, TextInput, email, required, useRecordContext, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { CustomerOfficeAutocompleteInput } from '../ra-inputs';
+import { Grid } from '@mui/material';
+import PropTypes from 'prop-types';
+
+const CustomerReferentForm = ({ modal, ...props }) => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  return (
+    <SimpleForm modal={modal} {...props} defaultValues={{ active: true, required: true }}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12}>
+          <CustomerOfficeAutocompleteInput source="officeId" validate={required()} filter={{ customerId: record?.customerId }} />
+        </Grid>
+        <Grid item xs={6}>
+          <TextInput source="name" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={6}>
+          <TextInput source="surname" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={6}>
+          <TextInput source="email" validate={email()} fullWidth />
+        </Grid>
+        <Grid item xs={6}>
+          <TextInput source="mobile" fullWidth />
+        </Grid>
+        <Grid item xs={6}>
+          <TextInput source="unit" fullWidth />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+CustomerReferentForm.propTypes = {
+  modal: PropTypes.bool
+};
+
+CustomerReferentForm.defaultProps = {
+  modal: true
+};
+
+export default CustomerReferentForm;
diff --git a/edera-web/src/components/ra-forms/DeviceForm.js b/edera-web/src/components/ra-forms/DeviceForm.js
new file mode 100644 (file)
index 0000000..0488479
--- /dev/null
@@ -0,0 +1,29 @@
+import { ReadonlyField, SimpleForm, TextInput, required, useRecordContext, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const DeviceForm = () => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  return (
+    <SimpleForm>
+      <Grid container spacing={spacing}>
+        {record?.id && (
+          <Grid item xs={12} sm={12}>
+            <ReadonlyField source="registrationDate" fullWidth />
+          </Grid>
+        )}
+        <Grid item xs={12} sm={12}>
+          <TextInput source="code" validate={required()} fullWidth />
+        </Grid>
+        {record?.id && (
+          <Grid item xs={12} sm={12}>
+            <TextInput source="secret" validate={required()} fullWidth />
+          </Grid>
+        )}
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default DeviceForm;
diff --git a/edera-web/src/components/ra-forms/DocxTemplateForm.js b/edera-web/src/components/ra-forms/DocxTemplateForm.js
new file mode 100644 (file)
index 0000000..eac8d20
--- /dev/null
@@ -0,0 +1,47 @@
+import { AttachmentInput, SimpleForm, TextInput, required, useThemeConfig, useTranslate } from '@applica-software-guru/react-admin';
+import { useWatch } from 'react-hook-form';
+import { Grid, Typography, Collapse } from '@mui/material';
+import { DocxTemplateTypeSelectInput } from '../ra-inputs';
+import { API_URL } from '../../config';
+
+const TypeHelperLink = () => {
+  const type = useWatch({ name: 'type' });
+  const translate = useTranslate();
+
+  return (
+    <Collapse in={!!type}>
+      <Typography
+        variant="caption"
+        color="textSecondary"
+        sx={{ ml: 1, mr: 1 }}
+        dangerouslySetInnerHTML={{
+          __html: translate('resources.entities/docx-templates.fields.type.link', {
+            url: `${API_URL}/docx/helper/${type}.docx`
+          })
+        }}
+      />
+    </Collapse>
+  );
+};
+
+const DocxTemplateForm = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="name" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <DocxTemplateTypeSelectInput source="type" validate={required()} fullWidth />
+          <TypeHelperLink />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <AttachmentInput source="attachment" validate={required()} multiple={false} label={false} accept=".docx" />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default DocxTemplateForm;
diff --git a/edera-web/src/components/ra-forms/EquipmentAttachmentForm.js b/edera-web/src/components/ra-forms/EquipmentAttachmentForm.js
new file mode 100644 (file)
index 0000000..3d42474
--- /dev/null
@@ -0,0 +1,21 @@
+import { AttachmentInput, SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const EquipmentAttachmentForm = (props) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="description" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <AttachmentInput source="attachment" multiple={false} label={false} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default EquipmentAttachmentForm;
diff --git a/edera-web/src/components/ra-forms/EquipmentCreateForm.js b/edera-web/src/components/ra-forms/EquipmentCreateForm.js
new file mode 100644 (file)
index 0000000..eeff47b
--- /dev/null
@@ -0,0 +1,19 @@
+import { SimpleForm, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { EquipmentTypeAutocompleteInput } from 'components/ra-inputs';
+import { Grid } from '@mui/material';
+
+const EquipmentCreateForm = (props) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <EquipmentTypeAutocompleteInput source="equipmentTypeId" validate={required()} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default EquipmentCreateForm;
diff --git a/edera-web/src/components/ra-forms/EquipmentCreateInDialogForm.js b/edera-web/src/components/ra-forms/EquipmentCreateInDialogForm.js
new file mode 100644 (file)
index 0000000..b23e9de
--- /dev/null
@@ -0,0 +1,22 @@
+import { SimpleForm, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { EquipmentTypeAutocompleteInput, SupplierAutocompleteInput } from 'components/ra-inputs';
+import { Grid } from '@mui/material';
+
+const EquipmentCreateInDialogForm = (props) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <SupplierAutocompleteInput source="supplierId" validate={required()} />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <EquipmentTypeAutocompleteInput source="equipmentTypeId" validate={required()} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default EquipmentCreateInDialogForm;
diff --git a/edera-web/src/components/ra-forms/EquipmentEditForm.js b/edera-web/src/components/ra-forms/EquipmentEditForm.js
new file mode 100644 (file)
index 0000000..3b8992c
--- /dev/null
@@ -0,0 +1,65 @@
+import {
+  ActionsMenu,
+  CardForm,
+  DeleteWithConfirmButton,
+  ReadonlyField,
+  Toolbar,
+  usePermissions,
+  useRecordContext,
+  useResourceTitle,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { SupplierBackButton } from 'components/ra-buttons';
+import { EquipmentAttachmentManyInput, SupplyManyInput } from 'components/ra-inputs';
+
+const EquipmentEditForm = () => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  const title = useResourceTitle();
+  const { permissions, isLoading } = usePermissions();
+  const canEdit = !isLoading && permissions ? permissions['equipment:edit'] : false;
+  return (
+    <CardForm>
+      <CardForm.Section
+        toolbar={null}
+        title={title}
+        secondary={
+          canEdit ? (
+            <ActionsMenu>
+              <DeleteWithConfirmButton />
+            </ActionsMenu>
+          ) : (
+            <></>
+          )
+        }
+      >
+        <Grid container spacing={spacing}>
+          <Grid item xs={12} sm={12}>
+            <ReadonlyField source="equipmentType.name" />
+          </Grid>
+        </Grid>
+        {!isLoading && permissions.includes('supplier:edit') && (
+          <Toolbar>
+            <SupplierBackButton supplierId={record.supplierId} tab="equipment" />
+          </Toolbar>
+        )}
+      </CardForm.Section>
+      <Grid item xs={12} sm={!isLoading && permissions.includes('supply:new') && 6}>
+        <CardForm.Section title="resources.entities/equipment-attachment.title">
+          <EquipmentAttachmentManyInput />
+        </CardForm.Section>
+      </Grid>
+      {!isLoading && permissions.includes('supply:new') && (
+        <Grid item xs={12} sm={6}>
+          <CardForm.Section title="resources.entities/supply.title">
+            <SupplyManyInput />
+          </CardForm.Section>
+        </Grid>
+      )}
+    </CardForm>
+  );
+};
+
+export default EquipmentEditForm;
diff --git a/edera-web/src/components/ra-forms/EquipmentTypeAttachmentForm.js b/edera-web/src/components/ra-forms/EquipmentTypeAttachmentForm.js
new file mode 100644 (file)
index 0000000..db1f139
--- /dev/null
@@ -0,0 +1,21 @@
+import { AttachmentInput, SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const EquipmentTypeAttachmentForm = (props) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="description" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <AttachmentInput source="attachment" multiple={false} label={false} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default EquipmentTypeAttachmentForm;
diff --git a/edera-web/src/components/ra-forms/EquipmentTypeCreateForm.js b/edera-web/src/components/ra-forms/EquipmentTypeCreateForm.js
new file mode 100644 (file)
index 0000000..c06c0e2
--- /dev/null
@@ -0,0 +1,23 @@
+import { BooleanInput, SimpleForm, TextInput, required, useResourceTitle, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const EquipmentTypeCreateForm = (props) => {
+  const { spacing } = useThemeConfig();
+  const title = useResourceTitle();
+
+  return (
+    <SimpleForm {...props} title={title} defaultValues={{ active: true }}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12}>
+          <TextInput source="name" validate={required()} />
+        </Grid>
+        <Grid item xs={12}>
+          <BooleanInput source="accessory" validate={required()} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default EquipmentTypeCreateForm;
diff --git a/edera-web/src/components/ra-forms/EquipmentTypeEditForm.js b/edera-web/src/components/ra-forms/EquipmentTypeEditForm.js
new file mode 100644 (file)
index 0000000..c0d7a1b
--- /dev/null
@@ -0,0 +1,99 @@
+import {
+  ActionsMenu,
+  BooleanInput,
+  CardForm,
+  DeleteWithConfirmButton,
+  MainCard,
+  NumberInput,
+  SaveButton,
+  TextInput,
+  Toolbar,
+  required,
+  useRecordContext,
+  useResourceTitle,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+
+import NavigateNextIcon from '@mui/icons-material/NavigateNext';
+import { Link } from 'react-router-dom';
+import { Typography } from '@mui/material';
+import Breadcrumbs from '@mui/material/Breadcrumbs';
+import { Grid } from '@mui/material';
+import { EntityBackButton } from 'components/ra-buttons';
+import { EquipmentTypeAttachmentManyInput, EquipmentTypeManyInput } from 'components/ra-inputs';
+
+const EquipmentTypeBreadcrumbs = ({ record }) => {
+  const parents = record?.parents;
+  if (!Array.isArray(parents)) {
+    return null;
+  }
+
+  const reversedParents = [...parents].reverse();
+
+  return (
+    <Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
+      {reversedParents.map(({ id, name }, index) => (
+        <Link to={`/entities/equipment-type/${id}`} key={index} style={{ textDecoration: 'none', color: 'rgba(0, 0, 0, 0.7)' }}>
+          {name}
+        </Link>
+      ))}
+      <Typography style={{ textDecoration: 'none', color: 'rgb(0, 0, 0)' }}>{record?.name}</Typography>
+    </Breadcrumbs>
+  );
+};
+
+const EquipmentTypeEditForm = () => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  const title = useResourceTitle();
+  const translate = useTranslate();
+
+  return (
+    <CardForm>
+      <CardForm.Section
+        toolbar={null}
+        title={title}
+        subheader={<EquipmentTypeBreadcrumbs record={record} />}
+        secondary={
+          <ActionsMenu>
+            <DeleteWithConfirmButton />
+          </ActionsMenu>
+        }
+      >
+        <Grid container spacing={spacing}>
+          <Grid item xs={12}>
+            <TextInput source="name" validate={required()} />
+          </Grid>
+          <Grid item xs={12}>
+            <NumberInput
+              source="breakpointInHours"
+              validate={required()}
+              helperText={translate('resources.entities/equipment-type.fields.breakpointInHours.help')}
+            />
+          </Grid>
+          <Grid item xs={12}>
+            <BooleanInput source="accessory" validate={required()} />
+          </Grid>
+        </Grid>
+
+        <Toolbar>
+          <SaveButton />
+          {record?.parentId && <EntityBackButton type="equipment-type" id={record.parentId} />}
+        </Toolbar>
+      </CardForm.Section>
+      <Grid item xs={12} sm={6}>
+        <MainCard title={translate('resources.entities/equipment-type-attachment.title.list')}>
+          <EquipmentTypeAttachmentManyInput />
+        </MainCard>
+      </Grid>
+      <Grid item xs={12} sm={6}>
+        <MainCard title={translate('resources.entities/equipment-type.title.list')}>
+          <EquipmentTypeManyInput />
+        </MainCard>
+      </Grid>
+    </CardForm>
+  );
+};
+
+export default EquipmentTypeEditForm;
diff --git a/edera-web/src/components/ra-forms/I18nMessageForm.js b/edera-web/src/components/ra-forms/I18nMessageForm.js
new file mode 100644 (file)
index 0000000..691e4c6
--- /dev/null
@@ -0,0 +1,24 @@
+import { SimpleForm, SmartTextInput, TextInput, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { LangSelectInput } from '../ra-inputs';
+
+const I18nMessageForm = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm defaultValues={{ lang: 'it' }}>
+      <Grid container spacing={spacing}>
+        <Grid item lg={3} md={3} sm={4} xs={12}>
+          <LangSelectInput source="lang" />
+        </Grid>
+        <Grid item lg={9} md={9} sm={8} xs={12}>
+          <TextInput source="code" fullWidth helperText="Code mapped/or to map for the message" />
+        </Grid>
+        <Grid item xs={12}>
+          <SmartTextInput maxLength={200} source="text" fullWidth multiline rows={3} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+export default I18nMessageForm;
diff --git a/edera-web/src/components/ra-forms/MaintenanceCreateForm.js b/edera-web/src/components/ra-forms/MaintenanceCreateForm.js
new file mode 100644 (file)
index 0000000..0620142
--- /dev/null
@@ -0,0 +1,43 @@
+import {
+  AttachmentInput,
+  DateInput,
+  SimpleForm,
+  SmartTextInput,
+  TextInput,
+  required,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { ActivitySelectInput, InterventionArrayInput } from 'components/ra-inputs';
+
+const MaintenanceCreateForm = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <ActivitySelectInput source="activityId" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="interventionNumber" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="title" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <SmartTextInput source="description" validate={required()} maxLength={500} multiline rows={3} />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <DateInput source="date" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <AttachmentInput source="attachment" multiple={false} label={false} />
+        </Grid>
+        <InterventionArrayInput source="interventions" />
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default MaintenanceCreateForm;
diff --git a/edera-web/src/components/ra-forms/MaintenanceEditForm.js b/edera-web/src/components/ra-forms/MaintenanceEditForm.js
new file mode 100644 (file)
index 0000000..561e82d
--- /dev/null
@@ -0,0 +1,85 @@
+import { ActivitySelectInput, InterventionArrayInput } from 'components/ra-inputs';
+import {
+  ActionsMenu,
+  AttachmentInput,
+  CardForm,
+  DateInput,
+  DeleteWithConfirmButton,
+  ReadonlyField,
+  SaveButton,
+  SmartTextInput,
+  TextInput,
+  Toolbar,
+  required,
+  usePermissions,
+  useRecordContext,
+  useResourceTitle,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { EntityBackButton, PrintDocxButton } from 'components/ra-buttons';
+import { DirtyFormPrintAlertField } from '../ra-fields';
+
+const MaintenanceEditForm = () => {
+  const { spacing } = useThemeConfig();
+  const { permissions, isLoading } = usePermissions();
+  const canDelete = !isLoading && permissions.includes('maintenance:delete');
+  const record = useRecordContext();
+  const title = useResourceTitle();
+  return (
+    <CardForm>
+      <CardForm.Section
+        toolbar={null}
+        title={title}
+        secondary={
+          canDelete && (
+            <ActionsMenu>
+              <DeleteWithConfirmButton />
+            </ActionsMenu>
+          )
+        }
+      >
+        <Grid container spacing={spacing}>
+          <Grid item xs={12} sm={6}>
+            <ReadonlyField source="user.name" />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <ReadonlyField source="protocol.protocolNumber" />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <ActivitySelectInput source="activityId" validate={required()} fullWidth />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <TextInput source="interventionNumber" validate={required()} fullWidth />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <TextInput source="title" validate={required()} fullWidth />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <SmartTextInput source="description" validate={required()} maxLength={500} multiline rows={3} />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <DateInput source="date" validate={required()} fullWidth />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <AttachmentInput source="attachment" multiple={false} label={false} />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <InterventionArrayInput source="interventions" />
+          </Grid>
+          <Grid item xs={12} sm={12}>
+            <DirtyFormPrintAlertField />
+          </Grid>
+        </Grid>
+        <Toolbar>
+          <SaveButton />
+          <PrintDocxButton type="maintenance" />
+          <EntityBackButton type="activity" id={record?.activityId} label="resources.entities/maintenance.actions.backToFather" />
+        </Toolbar>
+      </CardForm.Section>
+    </CardForm>
+  );
+};
+
+export default MaintenanceEditForm;
diff --git a/edera-web/src/components/ra-forms/PriceListForm.js b/edera-web/src/components/ra-forms/PriceListForm.js
new file mode 100644 (file)
index 0000000..0f4c856
--- /dev/null
@@ -0,0 +1,65 @@
+import {
+  ActionsMenu,
+  CardForm,
+  DateInput,
+  DeleteWithConfirmButton,
+  SaveButton,
+  TextInput,
+  Toolbar,
+  required,
+  useRecordContext,
+  useResourceTitle,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { PriceListBackButton } from 'components/ra-buttons';
+import { PriceListItemManyInput } from 'components/ra-inputs';
+
+const PriceListForm = ({ toolbar }) => {
+  const { spacing } = useThemeConfig();
+  const record = useRecordContext();
+  const title = useResourceTitle();
+
+  return (
+    <CardForm>
+      <CardForm.Section
+        toolbar={toolbar}
+        title={title}
+        secondary={
+          record?.id && (
+            <ActionsMenu>
+              <DeleteWithConfirmButton />
+            </ActionsMenu>
+          )
+        }
+      >
+        <Grid container spacing={spacing}>
+          <Grid item xs={12}>
+            <TextInput source="name" validate={required()} />
+          </Grid>
+          <Grid item xs={12}>
+            <DateInput source="start" />
+          </Grid>
+          <Grid item xs={12}>
+            <DateInput source="end" />
+          </Grid>
+        </Grid>
+        {!toolbar && (
+          <Toolbar>
+            <SaveButton />
+            {record?.id && <PriceListBackButton supplierId={record?.supplierId} tab="price-list" />}
+          </Toolbar>
+        )}
+      </CardForm.Section>
+
+      {record?.id && (
+        <CardForm.Section title="resources.entities/price-list-item.title.list">
+          <PriceListItemManyInput />
+        </CardForm.Section>
+      )}
+    </CardForm>
+  );
+};
+
+export default PriceListForm;
diff --git a/edera-web/src/components/ra-forms/PriceListItemEditForm.js b/edera-web/src/components/ra-forms/PriceListItemEditForm.js
new file mode 100644 (file)
index 0000000..00f2080
--- /dev/null
@@ -0,0 +1,22 @@
+import { SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { EquipmentSelectInput } from 'components/ra-inputs';
+
+const PriceListItemEditForm = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <EquipmentSelectInput source="equipmentId" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="price" validate={required()} fullWidth />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default PriceListItemEditForm;
diff --git a/edera-web/src/components/ra-forms/PriceListItemForm.js b/edera-web/src/components/ra-forms/PriceListItemForm.js
new file mode 100644 (file)
index 0000000..776ea28
--- /dev/null
@@ -0,0 +1,22 @@
+import { EquipmentAutocompleteInput, MoneyInput } from 'components/ra-inputs';
+import { SimpleForm, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const PriceListItemForm = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <EquipmentAutocompleteInput source="equipmentId" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <MoneyInput source="price" validate={required()} fullWidth />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default PriceListItemForm;
diff --git a/edera-web/src/components/ra-forms/ProtocolAttachmentForm.js b/edera-web/src/components/ra-forms/ProtocolAttachmentForm.js
new file mode 100644 (file)
index 0000000..d0731bb
--- /dev/null
@@ -0,0 +1,21 @@
+import { AttachmentInput, SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const ProtocolAttachmentForm = (props) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="description" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <AttachmentInput source="attachment" multiple={false} label={false} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default ProtocolAttachmentForm;
diff --git a/edera-web/src/components/ra-forms/ProtocolDialogForm.js b/edera-web/src/components/ra-forms/ProtocolDialogForm.js
new file mode 100644 (file)
index 0000000..27bb910
--- /dev/null
@@ -0,0 +1,44 @@
+import {
+  DateInput,
+  SimpleForm,
+  SmartTextInput,
+  TextInput,
+  required,
+  usePermissions,
+  useResourceTitle,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+import { CustomerAutocompleteInput } from 'components/ra-inputs';
+import { Grid } from '@mui/material';
+
+const ProtocolDialogForm = () => {
+  const { spacing } = useThemeConfig();
+  const title = useResourceTitle();
+  const { permissions, isLoading } = usePermissions();
+  const isEditable = !isLoading && permissions.includes('protocol:new');
+  return (
+    <SimpleForm title={title}>
+      <Grid container spacing={spacing}>
+        {!isLoading && permissions.includes('customer:list') && (
+          <Grid item xs={12} sm={12}>
+            <CustomerAutocompleteInput source="customerId" validate={required()} fullWidth disabled={!isEditable} />
+          </Grid>
+        )}
+        <Grid item xs={12}>
+          <TextInput source="title" validate={required()} disabled={!isEditable} />
+        </Grid>
+        <Grid item xs={12}>
+          <SmartTextInput source="description" validate={required()} maxLength={500} multiline rows={3} disabled={!isEditable} />
+        </Grid>
+        <Grid item xs={12}>
+          <TextInput source="protocolNumber" validate={required()} disabled={!isEditable} />
+        </Grid>
+        <Grid item xs={12}>
+          <DateInput source="protocolDate" validate={required()} disabled={!isEditable} />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default ProtocolDialogForm;
diff --git a/edera-web/src/components/ra-forms/ProtocolForm.js b/edera-web/src/components/ra-forms/ProtocolForm.js
new file mode 100644 (file)
index 0000000..5bc24df
--- /dev/null
@@ -0,0 +1,90 @@
+import {
+  ActionsMenu,
+  DateInput,
+  DeleteWithConfirmButton,
+  LongForm,
+  MainCard,
+  SaveButton,
+  SmartTextInput,
+  TextInput,
+  Toolbar,
+  required,
+  usePermissions,
+  useResourceTitle,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import { ProtocolServiceArrayInput, CustomerAutocompleteInput, ProtocolAttachmentManyInput } from 'components/ra-inputs';
+import { InfoCircleOutlined, FileOutlined, AppstoreAddOutlined } from '@ant-design/icons';
+import { Grid } from '@mui/material';
+
+const ProtocolForm = () => {
+  const { spacing } = useThemeConfig();
+  const title = useResourceTitle();
+  const translate = useTranslate();
+  const { permissions, isLoading } = usePermissions();
+  const isEditable = !isLoading && permissions.includes('protocol:new');
+  return (
+    <LongForm>
+      <LongForm.Tab
+        id="details"
+        icon={<InfoCircleOutlined />}
+        title={title}
+        label="resources.entities/protocol.tabs.details"
+        secondary={
+          <ActionsMenu>
+            <DeleteWithConfirmButton />
+          </ActionsMenu>
+        }
+      >
+        <MainCard
+          title={translate('resources.entities/protocol.tabs.details')}
+          sx={{ mb: spacing }}
+          secondary={
+            <ActionsMenu>
+              <DeleteWithConfirmButton />
+            </ActionsMenu>
+          }
+        >
+          <Grid container spacing={spacing}>
+            {!isLoading && permissions.includes('customer:list') && (
+              <Grid item xs={12} sm={12}>
+                <CustomerAutocompleteInput source="customerId" validate={required()} fullWidth disabled={!isEditable} />
+              </Grid>
+            )}
+            <Grid item xs={12}>
+              <TextInput source="title" validate={required()} disabled={!isEditable} />
+            </Grid>
+            <Grid item xs={12}>
+              <SmartTextInput source="description" validate={required()} maxLength={500} multiline rows={3} disabled={!isEditable} />
+            </Grid>
+            <Grid item xs={12}>
+              <TextInput source="protocolNumber" validate={required()} disabled={!isEditable} />
+            </Grid>
+            <Grid item xs={12}>
+              <DateInput source="protocolDate" validate={required()} disabled={!isEditable} />
+            </Grid>
+          </Grid>
+          <Toolbar>
+            <SaveButton />
+          </Toolbar>
+        </MainCard>
+      </LongForm.Tab>
+      <LongForm.Tab icon={<FileOutlined />} id="attachment" label="resources.entities/protocol-attachment.title">
+        <ProtocolAttachmentManyInput />
+      </LongForm.Tab>
+      <LongForm.Tab icon={<AppstoreAddOutlined />} id="service" label="resources.entities/protocol-service.title">
+        <MainCard title={translate('resources.entities/protocol-service.title')}>
+          <Grid item xs={12} sm={12}>
+            <ProtocolServiceArrayInput source="services" />
+          </Grid>
+          <Toolbar>
+            <SaveButton />
+          </Toolbar>
+        </MainCard>
+      </LongForm.Tab>
+    </LongForm>
+  );
+};
+
+export default ProtocolForm;
diff --git a/edera-web/src/components/ra-forms/RFIDDeviceForm.js b/edera-web/src/components/ra-forms/RFIDDeviceForm.js
new file mode 100644 (file)
index 0000000..1c11be0
--- /dev/null
@@ -0,0 +1,25 @@
+import { SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { RFIDDeviceTypeSelectInput } from 'components/ra-inputs';
+
+const RFIDDeviceForm = () => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="name" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="code" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <RFIDDeviceTypeSelectInput source="type" validate={required()} fullWidth />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default RFIDDeviceForm;
diff --git a/edera-web/src/components/ra-forms/SupplierCreateForm.js b/edera-web/src/components/ra-forms/SupplierCreateForm.js
new file mode 100644 (file)
index 0000000..beec95a
--- /dev/null
@@ -0,0 +1,33 @@
+import { SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+import { CityAutocompleteInput } from 'components/ra-inputs';
+
+import { Grid } from '@mui/material';
+
+const SupplierCreateForm = (props) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={6}>
+          <TextInput source="businessName" label="resources.entities/supplier.fields.businessName" validate={required()} />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <TextInput source="vatNumber" label="resources.entities/supplier.fields.vatNumber" validate={required()} />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <TextInput source="address" label="resources.entities/supplier.fields.address" validate={required()} />
+        </Grid>
+        <Grid item xs={12} sm={6}>
+          <CityAutocompleteInput
+            filterByProvince={false}
+            source="cityId"
+            label="resources.entities/supplier.fields.cityId"
+            validate={required()}
+          />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default SupplierCreateForm;
diff --git a/edera-web/src/components/ra-forms/SupplierForm.js b/edera-web/src/components/ra-forms/SupplierForm.js
new file mode 100644 (file)
index 0000000..2311209
--- /dev/null
@@ -0,0 +1,95 @@
+import {
+  ActionsMenu,
+  DeleteWithConfirmButton,
+  LongForm,
+  MainCard,
+  SaveButton,
+  TextInput,
+  Toolbar,
+  required,
+  useRecordContext,
+  useResourceTitle,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import { CityAutocompleteInput, EquipmentManyInput, PriceListManyInput, SupplierReferentManyInput } from 'components/ra-inputs';
+import { ContainerOutlined, InfoCircleOutlined, ToolOutlined } from '@ant-design/icons';
+
+import { FixedMainCard } from '../layout';
+import { Grid } from '@mui/material';
+
+const SupplierForm = () => {
+  const { spacing } = useThemeConfig();
+  const translate = useTranslate();
+  const record = useRecordContext();
+  const title = useResourceTitle();
+  return (
+    <LongForm defaultValues={{ active: true }} syncWithLocation>
+      <LongForm.Tab
+        id="details"
+        icon={<InfoCircleOutlined />}
+        title={title}
+        label="resources.entities/customer.tabs.details"
+        secondary={
+          <ActionsMenu>
+            <DeleteWithConfirmButton />
+          </ActionsMenu>
+        }
+      >
+        <MainCard
+          title={title}
+          sx={{ mb: spacing }}
+          secondary={
+            <ActionsMenu>
+              <DeleteWithConfirmButton />
+            </ActionsMenu>
+          }
+        >
+          <Grid container spacing={spacing}>
+            <Grid item xs={12} sm={6}>
+              <TextInput source="businessName" label="resources.entities/supplier.fields.businessName" validate={required()} />
+            </Grid>
+            <Grid item xs={12} sm={6}>
+              <TextInput source="vatNumber" label="resources.entities/supplier.fields.vatNumber" validate={required()} />
+            </Grid>
+            <Grid item xs={12} sm={6}>
+              <TextInput source="address" label="resources.entities/supplier.fields.address" validate={required()} />
+            </Grid>
+            <Grid item xs={12} sm={6}>
+              <CityAutocompleteInput
+                filterByProvince={false}
+                source="cityId"
+                label="resources.entities/supplier.fields.cityId"
+                validate={required()}
+              />
+            </Grid>
+          </Grid>
+          <Toolbar>
+            <SaveButton />
+          </Toolbar>
+        </MainCard>
+        {record?.id && (
+          <FixedMainCard title={translate('resources.entities/supplier-referent.title')}>
+            <SupplierReferentManyInput />
+          </FixedMainCard>
+        )}
+      </LongForm.Tab>
+      {record?.id && (
+        <LongForm.Tab id="equipment" icon={<ToolOutlined />} label="resources.entities/supplier.tabs.equipment">
+          <FixedMainCard title={translate('resources.entities/supplier.tabs.equipment')}>
+            <EquipmentManyInput />
+          </FixedMainCard>
+        </LongForm.Tab>
+      )}
+      {record?.id && (
+        <LongForm.Tab id="price-list" icon={<ContainerOutlined />} label="resources.entities/supplier.tabs.price-list">
+          <FixedMainCard title={translate('resources.entities/supplier.tabs.price-list')}>
+            <PriceListManyInput />
+          </FixedMainCard>
+        </LongForm.Tab>
+      )}
+    </LongForm>
+  );
+};
+
+export default SupplierForm;
diff --git a/edera-web/src/components/ra-forms/SupplierReferentForm.js b/edera-web/src/components/ra-forms/SupplierReferentForm.js
new file mode 100644 (file)
index 0000000..360154f
--- /dev/null
@@ -0,0 +1,30 @@
+import { SimpleForm, TextInput, email, required, useThemeConfig } from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+
+const SupplierReferent = (props) => {
+  const { spacing } = useThemeConfig();
+  return (
+    <SimpleForm secondary={null} {...props}>
+      <Grid container spacing={spacing}>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="name" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="surname" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="email" validate={[required(), email()]} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="phone" validate={required()} fullWidth />
+        </Grid>
+        <Grid item xs={12} sm={12}>
+          <TextInput source="department" validate={required()} fullWidth />
+        </Grid>
+      </Grid>
+    </SimpleForm>
+  );
+};
+
+export default SupplierReferent;
diff --git a/edera-web/src/components/ra-forms/SupplyDialogForm.js b/edera-web/src/components/ra-forms/SupplyDialogForm.js
new file mode 100644 (file)
index 0000000..e3bf499
--- /dev/null
@@ -0,0 +1,73 @@
+import {
+  ActionsMenu,
+  CardForm,
+  DateInput,
+  DeleteWithConfirmButton,
+  NumberInput,
+  TextInput,
+  required,
+  useResourceTitle,
+  useThemeConfig,
+  SaveButton,
+  Toolbar
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { CustomerOfficeAutocompleteInput, CustomerReadOnlyInput, ProtocolAutoComplete, RFIDDeviceAutocomplete } from 'components/ra-inputs';
+import CustomerAreaAutocompleteInput from 'components/ra-inputs/CustomerAreaAutocompleteInput';
+
+const SupplyDialogForm = () => {
+  const { spacing } = useThemeConfig();
+  const title = useResourceTitle();
+  return (
+    <CardForm>
+      <CardForm.Section
+        toolbar={null}
+        title={title}
+        secondary={
+          <ActionsMenu>
+            <DeleteWithConfirmButton />
+          </ActionsMenu>
+        }
+      >
+        <Grid container spacing={spacing}>
+          <Grid item xs={12} sm={6}>
+            <ProtocolAutoComplete source="protocolId" validate={required()} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <CustomerReadOnlyInput source="protocol.customerId" />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <NumberInput source="quantity" validate={required()} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <DateInput source="orderDate" validate={required()} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <DateInput source="deliveryDate" validate={required()} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <TextInput source="serialNumber" validate={required()} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <TextInput source="ddtNumber" validate={required()} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <RFIDDeviceAutocomplete source="rfidDeviceId" isAreaPresent={true} type="TAG" />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <CustomerOfficeAutocompleteInput source="area.officeId" isAreaPresent={true} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <CustomerAreaAutocompleteInput source="areaId" isAreaPresent={true} />
+          </Grid>
+        </Grid>
+        <Toolbar>
+          <SaveButton />
+        </Toolbar>
+      </CardForm.Section>
+    </CardForm>
+  );
+};
+
+export default SupplyDialogForm;
diff --git a/edera-web/src/components/ra-forms/SupplyForm.js b/edera-web/src/components/ra-forms/SupplyForm.js
new file mode 100644 (file)
index 0000000..84bd1fa
--- /dev/null
@@ -0,0 +1,127 @@
+import {
+  ActionsMenu,
+  CardForm,
+  DateInput,
+  DeleteWithConfirmButton,
+  NumberInput,
+  TextInput,
+  required,
+  useResourceTitle,
+  useThemeConfig,
+  SaveButton,
+  Toolbar,
+  usePermissions,
+  useRecordContext,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { GenerateQRCodePdfButton } from 'components/ra-buttons';
+import { RFIDDeviceTrackManyField, SupplyStatusField } from 'components/ra-fields';
+import {
+  CustomerOfficeAutocompleteInput,
+  EquipmentAutocompleteInput,
+  ProtocolAutoComplete,
+  RFIDDeviceAutocomplete,
+  SupplierAutocompleteInput
+} from 'components/ra-inputs';
+import { CustomerReadOnlyInput } from 'components/ra-inputs';
+import CustomerAreaAutocompleteInput from 'components/ra-inputs/CustomerAreaAutocompleteInput';
+import { Fragment } from 'react';
+import { SupplyLastTrackAlert } from '../ra-alerts';
+
+const SupplyForm = () => {
+  const { spacing } = useThemeConfig();
+  const title = useResourceTitle();
+  const { permissions, isLoading } = usePermissions();
+  const isEditable = !isLoading && permissions.includes('supply:new');
+  const translate = useTranslate();
+  const record = useRecordContext();
+  return (
+    <CardForm defaultValues={{ usageStatus: 'STOPPED' }}>
+      <CardForm.Section
+        toolbar={null}
+        title={
+          <Fragment>
+            {title} {record?.id && <SupplyStatusField record={record} />}
+          </Fragment>
+        }
+        secondary={
+          isEditable && (
+            <ActionsMenu>
+              <DeleteWithConfirmButton />
+            </ActionsMenu>
+          )
+        }
+      >
+        <Grid container spacing={spacing}>
+          {!isLoading && permissions.includes('supplier:list') && (
+            <Grid item xs={12} sm={6}>
+              <SupplierAutocompleteInput source="supplierId" validate={required()} disabled={!isEditable} />
+            </Grid>
+          )}
+          <Grid item xs={12} sm={6}>
+            <EquipmentAutocompleteInput source="equipmentId" validate={required()} disabled={!isEditable} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <ProtocolAutoComplete source="protocolId" validate={required()} disabled={!isEditable} />
+          </Grid>
+          {!isLoading && permissions.includes('customer:list') && (
+            <Grid item xs={12} sm={6}>
+              <CustomerReadOnlyInput source="protocol.customerId" />
+            </Grid>
+          )}
+          <Grid item xs={12} sm={6}>
+            <NumberInput source="quantity" validate={required()} disabled={!isEditable} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <DateInput source="orderDate" disabled={!isEditable} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <DateInput source="deliveryDate" disabled={!isEditable} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <TextInput source="serialNumber" validate={required()} disabled={!isEditable} />
+          </Grid>
+          <Grid item xs={12} sm={6}>
+            <TextInput source="ddtNumber" validate={required()} disabled={!isEditable} />
+          </Grid>
+          {!isLoading && permissions.includes('rfid-device:new') && (
+            <Grid item xs={12} sm={6}>
+              <RFIDDeviceAutocomplete source="rfidDeviceId" isAreaPresent={true} type="TAG" />
+            </Grid>
+          )}
+          {!isLoading && permissions.includes('customer:new') && (
+            <Fragment>
+              <Grid item xs={12} sm={6}>
+                <CustomerOfficeAutocompleteInput source="area.officeId" isAreaPresent={true} />
+              </Grid>
+              <Grid item xs={12} sm={6}>
+                <CustomerAreaAutocompleteInput source="areaId" isAreaPresent={true} />
+              </Grid>
+            </Fragment>
+          )}
+          <Grid item xs={12} sm={6}>
+            <NumberInput
+              source="breakpointInHours"
+              disabled={!isEditable}
+              helperText={translate('resources.entities/supply.fields.breakpointInHours.help')}
+            />
+          </Grid>
+        </Grid>
+        {!isLoading && permissions.includes('rfid-device:list') && <SupplyLastTrackAlert />}
+        <Toolbar>
+          <GenerateQRCodePdfButton />
+          {isEditable && <SaveButton />}
+        </Toolbar>
+      </CardForm.Section>
+      {record?.id && record?.rfidDeviceId && (
+        <CardForm.Section title="resources.entities/supply.tabs.rfid-device-track">
+          <RFIDDeviceTrackManyField />
+        </CardForm.Section>
+      )}
+    </CardForm>
+  );
+};
+
+export default SupplyForm;
diff --git a/edera-web/src/components/ra-forms/UserForm.js b/edera-web/src/components/ra-forms/UserForm.js
new file mode 100644 (file)
index 0000000..064545a
--- /dev/null
@@ -0,0 +1,59 @@
+import { ImageInput, SelectArrayInput, SimpleForm, TextInput, required, useThemeConfig } from '@applica-software-guru/react-admin';
+import { useFormContext, useWatch } from 'react-hook-form';
+
+import { CustomerAutocompleteInput } from 'components/ra-inputs';
+import { Grid } from '@mui/material';
+import PropTypes from 'prop-types';
+import { useEffect } from 'react';
+
+const UserFormContext = ({ configuredRoles }) => {
+  const { spacing } = useThemeConfig();
+  const { setValue } = useFormContext();
+  const customerId = useWatch({
+    name: 'customerId'
+  });
+  const roles = useWatch({
+    name: 'roles'
+  });
+
+  useEffect(() => setValue('roles', customerId == null ? [] : ['ROLE_CUSTOMER']), [customerId, setValue]);
+
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item lg={6} xs={12}>
+        <TextInput source="email" fullWidth helperText="E-mail (ex. roob@applica.it)" />
+      </Grid>
+      <Grid item lg={6} xs={12}>
+        <TextInput source="password" fullWidth type="password" />
+      </Grid>
+      <Grid item lg={12} xs={12}>
+        <TextInput source="name" fullWidth />
+      </Grid>
+      <Grid item lg={12} xs={12}>
+        <CustomerAutocompleteInput source="customerId" fullWidth validate={roles?.includes('ROLE_CUSTOMER') ? required() : []} />
+      </Grid>
+      {!customerId && (
+        <Grid item lg={12} xs={12}>
+          <SelectArrayInput source="roles" label="ra.user.roles" choices={configuredRoles} fullWidth />
+        </Grid>
+      )}
+      <Grid item lg={12} xs={12}>
+        <ImageInput source="image" accept="image/*" multiple={false} fullWidth />
+      </Grid>
+    </Grid>
+  );
+};
+
+const UserForm = ({ configuredRoles }) => {
+  return (
+    <SimpleForm defaultValues={{ active: true }}>
+      <UserFormContext configuredRoles={configuredRoles} />
+    </SimpleForm>
+  );
+};
+
+UserForm.propTypes = {
+  configuredRoles: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, name: PropTypes.string })).isRequired
+};
+
+export default UserForm;
diff --git a/edera-web/src/components/ra-forms/index.js b/edera-web/src/components/ra-forms/index.js
new file mode 100644 (file)
index 0000000..69bb700
--- /dev/null
@@ -0,0 +1,31 @@
+export { default as CustomerAreaForm } from './CustomerAreaForm';
+export { default as CustomerCreateForm } from './CustomerCreateForm';
+export { default as CustomerEditForm } from './CustomerEditForm';
+export { default as CustomerOfficeForm } from './CustomerOfficeForm';
+export { default as CustomerReferentForm } from './CustomerReferentForm';
+export { default as DeviceForm } from './DeviceForm';
+export { default as I18nMessageForm } from './I18nMessageForm';
+export { default as ActivityTypeForm } from './ActivityTypeForm';
+export { default as RFIDDeviceForm } from './RFIDDeviceForm';
+export { default as SupplierForm } from './SupplierForm';
+export { default as SupplierReferentForm } from './SupplierReferentForm';
+export { default as UserForm } from './UserForm';
+export { default as EquipmentTypeEditForm } from './EquipmentTypeEditForm';
+export { default as EquipmentTypeAttachmentForm } from './EquipmentTypeAttachmentForm';
+export { default as EquipmentTypeCreateForm } from './EquipmentTypeCreateForm';
+export { default as PriceListForm } from './PriceListForm';
+export { default as EquipmentCreateForm } from './EquipmentCreateForm';
+export { default as ProtocolForm } from './ProtocolForm';
+export { default as ProtocolAttachmentForm } from './ProtocolAttachmentForm';
+export { default as EquipmentEditForm } from './EquipmentEditForm';
+export { default as SupplyDialogForm } from './SupplyDialogForm';
+export { default as PriceListItemForm } from './PriceListItemForm';
+export { default as SupplyForm } from './SupplyForm';
+export { default as ActivityForm } from './ActivityForm';
+export { default as MaintenanceCreateForm } from './MaintenanceCreateForm';
+export { default as MaintenanceEditForm } from './MaintenanceEditForm';
+export { default as EquipmentAttachmentForm } from './EquipmentAttachmentForm';
+export { default as EquipmentCreateInDialogForm } from './EquipmentCreateInDialogForm';
+export { default as SupplierCreateForm } from './SupplierCreateForm';
+export { default as ProtocolDialogForm } from './ProtocolDialogForm';
+export { default as DocxTemplateForm } from './DocxTemplateForm';
diff --git a/edera-web/src/components/ra-inputs/ActivitySelectInput.js b/edera-web/src/components/ra-inputs/ActivitySelectInput.js
new file mode 100644 (file)
index 0000000..70ec6e0
--- /dev/null
@@ -0,0 +1,82 @@
+import { Alert, AlertTitle, Typography } from '@mui/material';
+import { ReferenceInput, SelectInput, useDataProvider, useNotify, useTranslate } from '@applica-software-guru/react-admin';
+import { Fragment, useEffect, useState } from 'react';
+
+import { useWatch } from 'react-hook-form';
+
+const AlertBox = () => {
+  const [activityDetails, setActivityDetails] = useState(null);
+  const dataProvider = useDataProvider();
+  const notify = useNotify();
+  const translate = useTranslate();
+  const activityId = useWatch({
+    name: 'activityId'
+  });
+
+  useEffect(() => {
+    if (!activityId) return;
+
+    const fetchDetails = async () => {
+      try {
+        const { data } = await dataProvider.get('maintenance/activity-info/' + activityId);
+        setActivityDetails(data.activityDetailsDTO);
+      } catch (error) {
+        notify('ra.notification.http_error', 'error', {});
+      }
+    };
+
+    fetchDetails();
+  }, [activityId, dataProvider, notify]);
+
+  if (!activityDetails) return null;
+
+  return (
+    <Alert severity="info" sx={{ marginTop: 1 }}>
+      <AlertTitle>{translate('resources.entities/activity.title')}</AlertTitle>
+      <Typography
+        dangerouslySetInnerHTML={{
+          __html: translate('resources.entities/activity.info_message', {
+            supply: activityDetails.equipmentName,
+            supplier: activityDetails.supplierName,
+            location: activityDetails.locationName
+          })
+        }}
+      />
+    </Alert>
+  );
+};
+
+const renderOption = (option) => {
+  return (
+    <Typography variant="body1" noWrap sx={{ maxWidth: 'calc(100vw - 120px)' }}>
+      {option.title}
+    </Typography>
+  );
+};
+
+const ActivitySelectInput = ({ validate, ...props }) => {
+  const supplyId = useWatch({
+    name: 'supplyId'
+  });
+
+  return (
+    <Fragment>
+      <ReferenceInput
+        {...props}
+        reference="entities/activity"
+        filterToQuery={(searchText) => ({ keyword: searchText })}
+        filter={{ supplyId: supplyId || [] }}
+        perPage={100}
+        sort={{
+          field: 'name',
+          order: 'ASC'
+        }}
+      >
+        <SelectInput optionText={renderOption} validate={validate} />
+      </ReferenceInput>
+      <AlertBox />
+    </Fragment>
+  );
+};
+
+export default ActivitySelectInput;
diff --git a/edera-web/src/components/ra-inputs/ActivityTypeAutocompleteInput.js b/edera-web/src/components/ra-inputs/ActivityTypeAutocompleteInput.js
new file mode 100644 (file)
index 0000000..44e86dc
--- /dev/null
@@ -0,0 +1,18 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+
+const ActivityTypeAutocompleteInput = ({ validate, ...props }) => (
+  <ReferenceInput
+    {...props}
+    reference="entities/activity-type"
+    filterToQuery={(searchText) => ({ keyword: searchText })}
+    perPage={100}
+    sort={{
+      field: 'name',
+      order: 'ASC'
+    }}
+  >
+    <AutocompleteInput optionText="description" validate={validate} />
+  </ReferenceInput>
+);
+
+export default ActivityTypeAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/CityAutocompleteInput.js b/edera-web/src/components/ra-inputs/CityAutocompleteInput.js
new file mode 100644 (file)
index 0000000..24a2803
--- /dev/null
@@ -0,0 +1,27 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+
+import { useWatch } from 'react-hook-form';
+
+const CityAutocompleteInput = ({ filterByProvince, validate, ...props }) => {
+  const provinceId = useWatch({
+    name: 'provinceId'
+  });
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/city"
+      source="cityId"
+      filter={filterByProvince ? { provinceId: provinceId || 'none' } : {}}
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText="name" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default CityAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/CustomerAreaAutocompleteInput.js b/edera-web/src/components/ra-inputs/CustomerAreaAutocompleteInput.js
new file mode 100644 (file)
index 0000000..13129d0
--- /dev/null
@@ -0,0 +1,42 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useEffect, useState } from 'react';
+import { useWatch, useFormContext } from 'react-hook-form';
+import _ from 'lodash';
+
+const CustomerAreaAutocompleteInput = ({ validate, ...props }) => {
+  const officeId = useWatch({
+    name: 'area.officeId'
+  });
+  const { isAreaPresent, ...rest } = props;
+  const [isDisabled, setIsDisabled] = useState(false);
+  const { setValue } = useFormContext();
+
+  useEffect(() => {
+    if (isAreaPresent) {
+      if (_.isEmpty(officeId)) {
+        setValue('areaId', null);
+        setIsDisabled(true);
+      } else {
+        setIsDisabled(false);
+      }
+    }
+  }, [isAreaPresent, officeId, setValue]);
+
+  return (
+    <ReferenceInput
+      {...rest}
+      reference="entities/customer-area"
+      filter={{ officeId: officeId ? officeId : undefined }}
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText="name" disabled={isDisabled} validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default CustomerAreaAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/CustomerAreaManyInput.js b/edera-web/src/components/ra-inputs/CustomerAreaManyInput.js
new file mode 100644 (file)
index 0000000..7d83f1d
--- /dev/null
@@ -0,0 +1,72 @@
+import {
+  ActionsMenu,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceField,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext
+} from '@applica-software-guru/react-admin';
+
+import { CustomerAreaForm } from '../ra-forms';
+import CustomerOfficeAutocompleteInput from './CustomerOfficeAutocompleteInput';
+
+const CustomerAreaManyInput = ({ ...props }) => {
+  const record = useRecordContext();
+
+  return (
+    <ReferenceManyInput
+      {...props}
+      reference="entities/customer-area"
+      target="customerId"
+      bulkActionButtons={false}
+      filters={[
+        <SearchInput key="keyword" source="keyword" alwaysOn />,
+        <CustomerOfficeAutocompleteInput key="officeId" source="officeId" alwaysOn display="legend" filter={{ customerId: record?.id }} />
+      ]}
+      actions={
+        <TopToolbar>
+          <CreateInDialogButton
+            maxWidth="sm"
+            record={{ customerId: record?.id, active: true }}
+            variant="outlined"
+            color="secondary"
+            size="medium"
+          >
+            <CustomerAreaForm />
+          </CreateInDialogButton>
+        </TopToolbar>
+      }
+    >
+      <Datagrid {...props} bulkActionButtons={false}>
+        <ReferenceField source="office.cityId" reference="entities/city" sortable={false}>
+          <TextField source="name" />
+        </ReferenceField>
+        <TextField source="office.address" sortable={false} />
+        <TextField source="name" />
+        <ReferenceField
+          source="rfidDeviceId"
+          reference="entities/rfid-device"
+          link={(data, reference) => `/entities/rfid-device-track?gateway-id=${data?.id}`}
+          sortable={false}
+        >
+          <TextField source="name" />
+        </ReferenceField>
+        <DateField source="updated" showTime />
+        <ActionsMenu>
+          <EditInDialogButton maxWidth="sm">
+            <CustomerAreaForm />
+          </EditInDialogButton>
+          <DeleteWithConfirmButton redirect={false} />
+        </ActionsMenu>
+      </Datagrid>
+    </ReferenceManyInput>
+  );
+};
+
+export default CustomerAreaManyInput;
diff --git a/edera-web/src/components/ra-inputs/CustomerAutocompleteInput.js b/edera-web/src/components/ra-inputs/CustomerAutocompleteInput.js
new file mode 100644 (file)
index 0000000..a0c78c2
--- /dev/null
@@ -0,0 +1,20 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+
+const CustomerAutocompleteInput = ({ validate, ...props }) => {
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/customer"
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText="name" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default CustomerAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/CustomerOfficeAutocompleteInput.js b/edera-web/src/components/ra-inputs/CustomerOfficeAutocompleteInput.js
new file mode 100644 (file)
index 0000000..5351f7d
--- /dev/null
@@ -0,0 +1,42 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useEffect, useState } from 'react';
+import { useWatch, useFormContext } from 'react-hook-form';
+import _ from 'lodash';
+
+const renderText = (record) => `${record.city.name} - ${record.address}`;
+const CustomerOfficeAutocompleteInput = ({ validate, ...props }) => {
+  const { isAreaPresent, ...rest } = props;
+  const [isDisabled, setIsDisabled] = useState(false);
+  const rfidDeviceId = useWatch({ name: 'rfidDeviceId' });
+  const customerId = useWatch({ name: 'customerId' });
+  const protocolId = useWatch({ name: 'protocolId' });
+  const { setValue } = useFormContext();
+
+  useEffect(() => {
+    if (isAreaPresent) {
+      if (!_.isEmpty(rfidDeviceId) || _.isEmpty(protocolId) || customerId === '') {
+        setValue('area.officeId', null);
+        setIsDisabled(true);
+      } else {
+        setIsDisabled(false);
+      }
+    }
+  }, [isAreaPresent, rfidDeviceId, customerId, protocolId, setValue]);
+
+  return (
+    <ReferenceInput
+      {...rest}
+      reference="entities/customer-office"
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      filter={{ customerId: customerId ? customerId : undefined }}
+      sort={{
+        field: 'address',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText={renderText} disabled={isDisabled} validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default CustomerOfficeAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/CustomerOfficeManyInput.js b/edera-web/src/components/ra-inputs/CustomerOfficeManyInput.js
new file mode 100644 (file)
index 0000000..09a5a43
--- /dev/null
@@ -0,0 +1,65 @@
+import {
+  ActionsMenu,
+  BooleanField,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext
+} from '@applica-software-guru/react-admin';
+
+import CityAutocompleteInput from './CityAutocompleteInput';
+import { CustomerOfficeForm } from '../ra-forms';
+
+const CustomerOfficeManyInput = ({ ...props }) => {
+  const record = useRecordContext();
+
+  return (
+    <ReferenceManyInput
+      {...props}
+      reference="entities/customer-office"
+      target="customerId"
+      bulkActionButtons={false}
+      filters={[
+        <SearchInput key="keyword" source="keyword" alwaysOn />,
+        <CityAutocompleteInput filterByProvince={false} key="cityId" source="cityId" alwaysOn display="legend" />
+      ]}
+      actions={
+        <TopToolbar>
+          <CreateInDialogButton
+            maxWidth="sm"
+            record={{ customerId: record?.id, active: true }}
+            variant="outlined"
+            color="secondary"
+            size="medium"
+          >
+            <CustomerOfficeForm />
+          </CreateInDialogButton>
+        </TopToolbar>
+      }
+    >
+      <Datagrid {...props} bulkActionButtons={false}>
+        <BooleanField source="headquarter" />
+        <TextField source="nation.name" sortable={false} />
+        <TextField source="region.name" sortable={false} />
+        <TextField source="province.name" sortable={false} />
+        <TextField source="city.name" sortable={false} />
+        <TextField source="address" sortable={false} />
+        <DateField source="updated" showTime />
+        <ActionsMenu>
+          <EditInDialogButton maxWidth="sm">
+            <CustomerOfficeForm />
+          </EditInDialogButton>
+          <DeleteWithConfirmButton redirect={false} />
+        </ActionsMenu>
+      </Datagrid>
+    </ReferenceManyInput>
+  );
+};
+
+export default CustomerOfficeManyInput;
diff --git a/edera-web/src/components/ra-inputs/CustomerReadOnlyInput.js b/edera-web/src/components/ra-inputs/CustomerReadOnlyInput.js
new file mode 100644 (file)
index 0000000..ace20aa
--- /dev/null
@@ -0,0 +1,41 @@
+import { TextInput, useDataProvider, useNotify } from '@applica-software-guru/react-admin';
+import { useEffect, useState } from 'react';
+import { useWatch, useFormContext } from 'react-hook-form';
+import _ from 'lodash';
+
+const CustomerReadOnlyInput = (props) => {
+  const dataProvider = useDataProvider();
+  const protocolId = useWatch({ name: 'protocolId' });
+  const { setValue } = useFormContext();
+  const [isInitialized, setIsInitialized] = useState(false);
+  const notify = useNotify();
+
+  useEffect(() => {
+    if (isInitialized || !_.isEmpty(protocolId)) {
+      if (!_.isEmpty(protocolId)) {
+        dataProvider
+          .getOne('entities/protocol', { id: protocolId })
+          .then(({ data }) => {
+            if (data && data.customer) {
+              setValue('customerName', data.customer.name);
+              setValue('customerId', data.customer.id);
+            }
+          })
+          .catch((error) => {
+            notify('ra.error.fetch', error);
+          });
+      } else {
+        setValue('customerName', '');
+        setValue('customerId', '');
+      }
+    }
+  }, [dataProvider, protocolId, setValue, isInitialized, notify]);
+
+  useEffect(() => {
+    setIsInitialized(true);
+  }, [protocolId]);
+
+  return <TextInput {...props} source="customerName" disabled />;
+};
+
+export default CustomerReadOnlyInput;
diff --git a/edera-web/src/components/ra-inputs/CustomerReferentManyInput.js b/edera-web/src/components/ra-inputs/CustomerReferentManyInput.js
new file mode 100644 (file)
index 0000000..c6b65ae
--- /dev/null
@@ -0,0 +1,63 @@
+import {
+  ActionsMenu,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceField,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext
+} from '@applica-software-guru/react-admin';
+
+import { CustomerReferentForm } from '../ra-forms';
+
+const CustomerReferentManyInput = ({ ...props }) => {
+  const record = useRecordContext();
+
+  return (
+    <ReferenceManyInput
+      {...props}
+      reference="entities/customer-referent"
+      target="customerId"
+      bulkActionButtons={false}
+      filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+      actions={
+        <TopToolbar>
+          <CreateInDialogButton
+            maxWidth="sm"
+            record={{ customerId: record?.id, active: true }}
+            variant="outlined"
+            color="secondary"
+            size="medium"
+          >
+            <CustomerReferentForm />
+          </CreateInDialogButton>
+        </TopToolbar>
+      }
+    >
+      <Datagrid {...props} bulkActionButtons={false}>
+        <ReferenceField source="office.cityId" reference="entities/city" sortable={false}>
+          <TextField source="name" />
+        </ReferenceField>
+        <ReferenceField source="officeId" reference="entities/customer-office" sortable={false}>
+          <TextField source="address" />
+        </ReferenceField>
+        <TextField source="name" />
+        <TextField source="surname" />
+        <DateField source="updated" showTime />
+        <ActionsMenu>
+          <EditInDialogButton maxWidth="sm">
+            <CustomerReferentForm />
+          </EditInDialogButton>
+          <DeleteWithConfirmButton redirect={false} />
+        </ActionsMenu>
+      </Datagrid>
+    </ReferenceManyInput>
+  );
+};
+
+export default CustomerReferentManyInput;
diff --git a/edera-web/src/components/ra-inputs/DocxTemplateTypeSelectInput.js b/edera-web/src/components/ra-inputs/DocxTemplateTypeSelectInput.js
new file mode 100644 (file)
index 0000000..e397dda
--- /dev/null
@@ -0,0 +1,18 @@
+import { SelectInput, useDataProvider } from '@applica-software-guru/react-admin';
+import { useEffect, useState } from 'react';
+
+const DocxTemplateTypeSelectInput = (props) => {
+  const dataProvider = useDataProvider();
+  const [choices, setChoices] = useState([]);
+  useEffect(() => {
+    dataProvider
+      .get('values/docx-types')
+      .then(({ data: { value: data } }) =>
+        setChoices(data.map(({ label, value }) => ({ id: value, name: `docx-template.type.${label}` })))
+      );
+  }, [dataProvider]);
+
+  return <SelectInput {...props} choices={choices} />;
+};
+
+export default DocxTemplateTypeSelectInput;
diff --git a/edera-web/src/components/ra-inputs/EquipmentAttachmentManyInput.js b/edera-web/src/components/ra-inputs/EquipmentAttachmentManyInput.js
new file mode 100644 (file)
index 0000000..9cd7a05
--- /dev/null
@@ -0,0 +1,67 @@
+import {
+  ActionsMenu,
+  AttachmentField,
+  BulkDeleteWithConfirmButton,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  usePermissions,
+  useRecordContext,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+
+import { EquipmentAttachmentForm } from 'components/ra-forms';
+import { Grid } from '@mui/material';
+
+const EquipmentAttachmentManyInput = () => {
+  const record = useRecordContext();
+  const { spacing } = useThemeConfig();
+  const { permissions, isLoading } = usePermissions();
+  const canEdit = !isLoading && permissions.includes('equipment-attachment:edit');
+  const canCreate = !isLoading && permissions.includes('equipment-attachment:new');
+
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item lg={12} xs={12}>
+        <ReferenceManyInput
+          reference="entities/equipment-attachment"
+          target="equipmentId"
+          filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+          sort={{ field: 'created', order: 'ASC' }}
+          actions={
+            <TopToolbar>
+              {canCreate && (
+                <CreateInDialogButton maxWidth="sm" record={{ equipmentId: record?.id }} variant="outlined" color="secondary" size="medium">
+                  <EquipmentAttachmentForm />
+                </CreateInDialogButton>
+              )}
+            </TopToolbar>
+          }
+        >
+          <Datagrid bulkActionButtons={!canEdit ? null : <BulkDeleteWithConfirmButton />}>
+            <TextField source="description" />
+            <AttachmentField source="attachment" title="attachment.name" sortable={false} />
+            <DateField source="updated" showTime />
+            {canEdit && (
+              <ActionsMenu>
+                <EditInDialogButton maxWidth="sm">
+                  <EquipmentAttachmentForm />
+                </EditInDialogButton>
+
+                <DeleteWithConfirmButton redirect={false} />
+              </ActionsMenu>
+            )}
+          </Datagrid>
+        </ReferenceManyInput>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default EquipmentAttachmentManyInput;
diff --git a/edera-web/src/components/ra-inputs/EquipmentAutocompleteInput.js b/edera-web/src/components/ra-inputs/EquipmentAutocompleteInput.js
new file mode 100644 (file)
index 0000000..5db7083
--- /dev/null
@@ -0,0 +1,43 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useEffect } from 'react';
+import { useFormContext, useWatch } from 'react-hook-form';
+
+const EquipmentAutocompleteInput = ({ validate, ...props }) => {
+  const { setValue } = useFormContext();
+  const supplierId = useWatch({
+    name: 'supplierId'
+  });
+
+  const priceListId = useWatch({
+    name: 'priceListId'
+  });
+
+  useEffect(() => {
+    setValue('equipmentId', null);
+  }, [supplierId, setValue]);
+
+  const filter = {};
+  if (supplierId) {
+    filter.supplierId = supplierId;
+  }
+  if (priceListId) {
+    filter.priceListId = priceListId;
+  }
+
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/equipment"
+      filter={filter}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText="equipmentType.name" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default EquipmentAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/EquipmentListAutocompleteInput.js b/edera-web/src/components/ra-inputs/EquipmentListAutocompleteInput.js
new file mode 100644 (file)
index 0000000..61f338f
--- /dev/null
@@ -0,0 +1,18 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+const renderText = (record) => `${record.equipmentType.name} (${record?.supplier?.businessName})`;
+const EquipmentListAutocompleteInput = ({ validate, ...props }) => (
+  <ReferenceInput
+    {...props}
+    reference="entities/equipment"
+    filterToQuery={(searchText) => ({ keyword: searchText })}
+    perPage={100}
+    sort={{
+      field: 'name',
+      order: 'ASC'
+    }}
+  >
+    <AutocompleteInput optionText={renderText} validate={validate} />
+  </ReferenceInput>
+);
+
+export default EquipmentListAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/EquipmentManyInput.js b/edera-web/src/components/ra-inputs/EquipmentManyInput.js
new file mode 100644 (file)
index 0000000..be50884
--- /dev/null
@@ -0,0 +1,48 @@
+import {
+  ActionsMenu,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext
+} from '@applica-software-guru/react-admin';
+import { EquipmentCreateForm, EquipmentEditForm } from 'components/ra-forms';
+
+const EquipmentManyInput = ({ ...props }) => {
+  const record = useRecordContext();
+
+  return (
+    <ReferenceManyInput
+      {...props}
+      reference="entities/equipment"
+      target="supplierId"
+      bulkActionButtons={false}
+      filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+      actions={
+        <TopToolbar>
+          <CreateInDialogButton maxWidth="sm" record={{ supplierId: record?.id }} variant="outlined" color="secondary" size="medium">
+            <EquipmentCreateForm />
+          </CreateInDialogButton>
+        </TopToolbar>
+      }
+    >
+      <Datagrid>
+        <TextField source="equipmentType.name" />
+        <DateField source="updated" showTime />
+        <ActionsMenu>
+          <EditButton>
+            <EquipmentEditForm />
+          </EditButton>
+          <DeleteWithConfirmButton redirect={false} />
+        </ActionsMenu>
+      </Datagrid>
+    </ReferenceManyInput>
+  );
+};
+
+export default EquipmentManyInput;
diff --git a/edera-web/src/components/ra-inputs/EquipmentTypeAttachmentManyInput.js b/edera-web/src/components/ra-inputs/EquipmentTypeAttachmentManyInput.js
new file mode 100644 (file)
index 0000000..d433b05
--- /dev/null
@@ -0,0 +1,56 @@
+import {
+  ActionsMenu,
+  AttachmentField,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+
+import { EquipmentTypeAttachmentForm } from 'components/ra-forms';
+import { Grid } from '@mui/material';
+
+const EquipmentTypeAttachmentManyInput = () => {
+  const record = useRecordContext();
+  const { spacing } = useThemeConfig();
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item lg={12} xs={12}>
+        <ReferenceManyInput
+          reference="entities/equipment-type-attachment"
+          target="typeId"
+          filters={[<SearchInput key="name__like" source="name__like" alwaysOn />]}
+          sort={{ field: 'created', order: 'ASC' }}
+          actions={
+            <TopToolbar>
+              <CreateInDialogButton maxWidth="sm" record={{ typeId: record?.id }} variant="outlined" color="secondary" size="medium">
+                <EquipmentTypeAttachmentForm />
+              </CreateInDialogButton>
+            </TopToolbar>
+          }
+        >
+          <Datagrid>
+            <TextField source="description" />
+            <AttachmentField source="attachment" title="attachment.name" />
+            <DateField source="updated" showTime />
+            <ActionsMenu>
+              <EditInDialogButton maxWidth="sm">
+                <EquipmentTypeAttachmentForm />
+              </EditInDialogButton>
+              <DeleteWithConfirmButton redirect={false} />
+            </ActionsMenu>
+          </Datagrid>
+        </ReferenceManyInput>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default EquipmentTypeAttachmentManyInput;
diff --git a/edera-web/src/components/ra-inputs/EquipmentTypeAutocompleteInput.js b/edera-web/src/components/ra-inputs/EquipmentTypeAutocompleteInput.js
new file mode 100644 (file)
index 0000000..3169e81
--- /dev/null
@@ -0,0 +1,22 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useWatch } from 'react-hook-form';
+
+const EquipmentTypeAutocompleteInput = ({ label, validate, ...props }) => {
+  const supplierId = useWatch({ name: 'supplierId' });
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/equipment-type"
+      filter={{ supplierId: supplierId }}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput label={label} optionText="name" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default EquipmentTypeAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/EquipmentTypeManyInput.js b/edera-web/src/components/ra-inputs/EquipmentTypeManyInput.js
new file mode 100644 (file)
index 0000000..feb3f1b
--- /dev/null
@@ -0,0 +1,48 @@
+import {
+  BooleanField,
+  CreateInDialogButton,
+  Datagrid,
+  EditButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+import { Grid } from '@mui/material';
+import { EquipmentTypeEditForm, EquipmentTypeCreateForm } from 'components/ra-forms';
+
+const EquipmentTypeManyInput = () => {
+  const record = useRecordContext();
+  const { spacing } = useThemeConfig();
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item lg={12} xs={12}>
+        <ReferenceManyInput
+          reference="entities/equipment-type"
+          target="parentId"
+          filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+          sort={{ field: 'created', order: 'ASC' }}
+          actions={
+            <TopToolbar>
+              <CreateInDialogButton maxWidth="sm" record={{ parentId: record?.id }} variant="outlined" color="secondary" size="medium">
+                <EquipmentTypeCreateForm />
+              </CreateInDialogButton>
+            </TopToolbar>
+          }
+        >
+          <Datagrid rowClick="edit">
+            <TextField source="name" />
+            <BooleanField source="accessory" />
+            <EditButton>
+              <EquipmentTypeEditForm />
+            </EditButton>
+          </Datagrid>
+        </ReferenceManyInput>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default EquipmentTypeManyInput;
diff --git a/edera-web/src/components/ra-inputs/InterventionArrayInput.js b/edera-web/src/components/ra-inputs/InterventionArrayInput.js
new file mode 100644 (file)
index 0000000..1710372
--- /dev/null
@@ -0,0 +1,38 @@
+import {
+  ArrayInput,
+  SimpleFormIterator,
+  TableFormIterator,
+  TextInput,
+  required,
+  useTranslateLabel
+} from '@applica-software-guru/react-admin';
+import { useMediaQuery, Grid } from '@mui/material';
+import EquipmentTypeAutocompleteInput from './EquipmentTypeAutocompleteInput';
+import YesOrNoSelectInput from './YesOrNoSelectInput';
+
+const InterventionArrayInput = (props) => {
+  const translateLabel = useTranslateLabel();
+  const isXsScreen = useMediaQuery((theme) => theme.breakpoints.down('sm'));
+  return (
+    <Grid item xs={12} sm={12}>
+      <ArrayInput {...props} label="" divider={false}>
+        {!isXsScreen ? (
+          <TableFormIterator label={translateLabel(props)}>
+            <EquipmentTypeAutocompleteInput source={'equipmentTypeId'} validate={required()} />
+            <TextInput source="description" validate={required()} />
+            <YesOrNoSelectInput source="substitution" validate={required()} />
+            <YesOrNoSelectInput source="reorder" validate={required()} />
+          </TableFormIterator>
+        ) : (
+          <SimpleFormIterator label={translateLabel(props)} fullWidth disableReordering>
+            <EquipmentTypeAutocompleteInput source={'equipmentTypeId'} validate={required()} />
+            <TextInput source="description" validate={required()} />
+            <YesOrNoSelectInput source="substitution" validate={required()} />
+            <YesOrNoSelectInput source="reorder" validate={required()} />
+          </SimpleFormIterator>
+        )}
+      </ArrayInput>
+    </Grid>
+  );
+};
+export default InterventionArrayInput;
diff --git a/edera-web/src/components/ra-inputs/LangSelectInput.js b/edera-web/src/components/ra-inputs/LangSelectInput.js
new file mode 100644 (file)
index 0000000..69967a9
--- /dev/null
@@ -0,0 +1,4 @@
+import { SelectInput } from '@applica-software-guru/react-admin';
+const LangSelectInput = (props) => <SelectInput {...props} choices={[{ id: 'it', name: 'Italiano' }]} />;
+
+export default LangSelectInput;
diff --git a/edera-web/src/components/ra-inputs/MaintenanceManyInput.js b/edera-web/src/components/ra-inputs/MaintenanceManyInput.js
new file mode 100644 (file)
index 0000000..8547ec4
--- /dev/null
@@ -0,0 +1,57 @@
+import {
+  BulkDeleteWithConfirmButton,
+  CreateButton,
+  Datagrid,
+  DateField,
+  EditButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  usePermissions,
+  useRecordContext,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+import { MaintenanceCreateForm, MaintenanceEditForm } from 'components/ra-forms';
+
+import { Grid } from '@mui/material';
+
+const MaintenanceManyInput = () => {
+  const record = useRecordContext();
+  const { spacing } = useThemeConfig();
+  const { permissions, isLoading } = usePermissions();
+  const canDelete = !isLoading && permissions.includes('maintenance:delete');
+
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item lg={12} xs={12}>
+        <ReferenceManyInput
+          reference="entities/maintenance"
+          target="activityId"
+          filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+          sort={{ field: 'created', order: 'ASC' }}
+          actions={
+            <TopToolbar>
+              <CreateButton state={{ record: { activityId: record?.id } }} variant="outlined" color="secondary" size="medium">
+                <MaintenanceCreateForm />
+              </CreateButton>
+            </TopToolbar>
+          }
+        >
+          <Datagrid bulkActionButtons={!canDelete ? null : <BulkDeleteWithConfirmButton />}>
+            <TextField source="activity.title" sortable={false} />
+            <TextField source="title" />
+            <TextField source="interventionNumber" />
+            <DateField source="date" sortable={false} />
+            <DateField source="updated" showTime />
+            <EditButton>
+              <MaintenanceEditForm />
+            </EditButton>
+          </Datagrid>
+        </ReferenceManyInput>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default MaintenanceManyInput;
diff --git a/edera-web/src/components/ra-inputs/MoneyInput.js b/edera-web/src/components/ra-inputs/MoneyInput.js
new file mode 100644 (file)
index 0000000..8023b82
--- /dev/null
@@ -0,0 +1,33 @@
+import { TextInput } from '@applica-software-guru/react-admin';
+
+const parseMoney = (value) => {
+  const convertedValue = value?.replace(',', '.');
+
+  const decimalMatch = /\.(\d{2,})$/.exec(convertedValue);
+
+  if (decimalMatch) {
+    const decimalPart = decimalMatch[1];
+
+    const twoDecimals = decimalPart.slice(0, 2);
+
+    const integerPart = convertedValue.split('.')[0];
+    return `${integerPart}.${twoDecimals}`;
+  }
+  return convertedValue;
+};
+const MoneyInput = (props) => {
+  return (
+    <TextInput
+      {...props}
+      type="number"
+      min="0.01"
+      step="0.01"
+      parse={parseMoney}
+      InputProps={{
+        startAdornment: '€'
+      }}
+    />
+  );
+};
+
+export default MoneyInput;
diff --git a/edera-web/src/components/ra-inputs/NationAutocompleteInput.js b/edera-web/src/components/ra-inputs/NationAutocompleteInput.js
new file mode 100644 (file)
index 0000000..156a53b
--- /dev/null
@@ -0,0 +1,18 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+
+const NationAutocompleteInput = ({ validate, ...props }) => (
+  <ReferenceInput
+    {...props}
+    reference="entities/nation"
+    filterToQuery={(searchText) => ({ keyword: searchText })}
+    perPage={100}
+    sort={{
+      field: 'name',
+      order: 'ASC'
+    }}
+  >
+    <AutocompleteInput optionText="name" validate={validate} />
+  </ReferenceInput>
+);
+
+export default NationAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/PriceListItemManyInput.js b/edera-web/src/components/ra-inputs/PriceListItemManyInput.js
new file mode 100644 (file)
index 0000000..bb0eae0
--- /dev/null
@@ -0,0 +1,55 @@
+import {
+  ActionsMenu,
+  CreateInDialogButton,
+  Datagrid,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext,
+  useThemeConfig,
+  DateField
+} from '@applica-software-guru/react-admin';
+import { Grid } from '@mui/material';
+import { MoneyField } from 'components/ra-fields';
+import { PriceListItemForm } from 'components/ra-forms';
+
+const PriceListItemManyInput = () => {
+  const record = useRecordContext();
+  const { spacing } = useThemeConfig();
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item lg={12} xs={12}>
+        <ReferenceManyInput
+          reference="entities/price-list-item"
+          target="priceListId"
+          filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+          sort={{ field: 'created', order: 'ASC' }}
+          actions={
+            <TopToolbar>
+              <CreateInDialogButton maxWidth="sm" record={{ priceListId: record?.id }} variant="outlined" color="secondary" size="medium">
+                <PriceListItemForm />
+              </CreateInDialogButton>
+            </TopToolbar>
+          }
+        >
+          <Datagrid>
+            <TextField source="equipmentType.name" />
+            <MoneyField source="price" />
+            <DateField source="updated" showTime />
+            <ActionsMenu>
+              <EditInDialogButton maxWidth="sm">
+                <PriceListItemForm />
+              </EditInDialogButton>
+              <DeleteWithConfirmButton redirect={false} />
+            </ActionsMenu>
+          </Datagrid>
+        </ReferenceManyInput>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default PriceListItemManyInput;
diff --git a/edera-web/src/components/ra-inputs/PriceListManyInput.js b/edera-web/src/components/ra-inputs/PriceListManyInput.js
new file mode 100644 (file)
index 0000000..b97e6c1
--- /dev/null
@@ -0,0 +1,58 @@
+import {
+  ActionsMenu,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext
+} from '@applica-software-guru/react-admin';
+
+import { PriceListForm } from 'components/ra-forms';
+
+const PriceListManyInput = ({ ...props }) => {
+  const record = useRecordContext();
+
+  return (
+    <ReferenceManyInput
+      {...props}
+      reference="entities/price-list"
+      target="supplierId"
+      bulkActionButtons={false}
+      filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+      actions={
+        <TopToolbar>
+          <CreateInDialogButton
+            maxWidth="sm"
+            record={{ supplierId: record?.id, active: true }}
+            variant="outlined"
+            color="secondary"
+            redirect="edit"
+            size="medium"
+          >
+            <PriceListForm />
+          </CreateInDialogButton>
+        </TopToolbar>
+      }
+    >
+      <Datagrid bulkActionButtons={false}>
+        <TextField source="name" />
+        <DateField source="start" />
+        <DateField source="end" />
+        <DateField source="updated" showTime />
+        <ActionsMenu>
+          <EditButton>
+            <PriceListForm />
+          </EditButton>
+          <DeleteWithConfirmButton redirect={false} />
+        </ActionsMenu>
+      </Datagrid>
+    </ReferenceManyInput>
+  );
+};
+
+export default PriceListManyInput;
diff --git a/edera-web/src/components/ra-inputs/ProtocolAttachmentManyInput.js b/edera-web/src/components/ra-inputs/ProtocolAttachmentManyInput.js
new file mode 100644 (file)
index 0000000..766bd45
--- /dev/null
@@ -0,0 +1,78 @@
+import {
+  ActionsMenu,
+  AttachmentField,
+  BulkDeleteWithConfirmButton,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  usePermissions,
+  useRecordContext,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import { FixedMainCard } from 'components/layout';
+import { ProtocolAttachmentForm } from 'components/ra-forms';
+
+const ProtocolAttachmentManyInput = () => {
+  const record = useRecordContext();
+  const translate = useTranslate();
+  const { spacing } = useThemeConfig();
+  const { isLoading, permissions } = usePermissions();
+  const canCreate = !isLoading && permissions.includes('protocol-attachment:new');
+  const canEdit = !isLoading && permissions.includes('protocol-attachment:edit');
+  const canDelete = !isLoading && permissions.includes('protocol-attachment:delete');
+  return (
+    <FixedMainCard title={translate('resources.entities/protocol-attachment.title')}>
+      <Grid container spacing={spacing}>
+        <Grid item lg={12} xs={12}>
+          <ReferenceManyInput
+            reference="entities/protocol-attachment"
+            target="protocolId"
+            filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+            sort={{ field: 'created', order: 'ASC' }}
+            actions={
+              <TopToolbar>
+                {canCreate && (
+                  <CreateInDialogButton
+                    maxWidth="sm"
+                    record={{ protocolId: record?.id }}
+                    variant="outlined"
+                    color="secondary"
+                    size="medium"
+                  >
+                    <ProtocolAttachmentForm />
+                  </CreateInDialogButton>
+                )}
+              </TopToolbar>
+            }
+          >
+            <Datagrid bulkActionButtons={!canDelete ? null : <BulkDeleteWithConfirmButton />}>
+              <TextField source="description" />
+              <AttachmentField source="attachment" title="attachment.name" />
+              <DateField source="updated" showTime />
+              {canEdit && (
+                <ActionsMenu>
+                  <EditInDialogButton maxWidth="sm">
+                    <ProtocolAttachmentForm />
+                  </EditInDialogButton>
+
+                  <DeleteWithConfirmButton redirect={false} />
+                </ActionsMenu>
+              )}
+            </Datagrid>
+          </ReferenceManyInput>
+        </Grid>
+      </Grid>
+    </FixedMainCard>
+  );
+};
+
+export default ProtocolAttachmentManyInput;
diff --git a/edera-web/src/components/ra-inputs/ProtocolAutoComplete.js b/edera-web/src/components/ra-inputs/ProtocolAutoComplete.js
new file mode 100644 (file)
index 0000000..e23b174
--- /dev/null
@@ -0,0 +1,18 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+
+const ProtocolAutoComplete = ({ validate, ...props }) => (
+  <ReferenceInput
+    {...props}
+    reference="entities/protocol"
+    filterToQuery={(searchText) => ({ keyword: searchText })}
+    perPage={100}
+    sort={{
+      field: 'name',
+      order: 'ASC'
+    }}
+  >
+    <AutocompleteInput optionText="title" validate={validate} />
+  </ReferenceInput>
+);
+
+export default ProtocolAutoComplete;
diff --git a/edera-web/src/components/ra-inputs/ProtocolServiceArrayInput.js b/edera-web/src/components/ra-inputs/ProtocolServiceArrayInput.js
new file mode 100644 (file)
index 0000000..4eded4a
--- /dev/null
@@ -0,0 +1,43 @@
+import {
+  ArrayInput,
+  TableFormIterator,
+  SimpleFormIterator,
+  TextInput,
+  DateInput,
+  required,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import { useMediaQuery } from '@mui/material';
+import { ActivityTypeAutocompleteInput } from '.';
+import { useMemo } from 'react';
+const ProtocolServiceArrayInput = (props) => {
+  const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
+  const translate = useTranslate();
+  const Component = useMemo(() => (isMobile ? SimpleFormIterator : TableFormIterator), [isMobile]);
+
+  return (
+    <ArrayInput {...props} label="" divider={false}>
+      <Component>
+        <ActivityTypeAutocompleteInput
+          label={'resources.entities/protocol.services.fromActivityTypeId'}
+          source="fromActivityTypeId"
+          validate={required()}
+        />
+        <ActivityTypeAutocompleteInput label={'resources.entities/protocol.services.toActivityTypeId'} source="toActivityTypeId" />
+        <TextInput
+          label={'resources.entities/protocol.services.frequency'}
+          source="frequency"
+          type="number"
+          min="0"
+          step="1"
+          InputProps={{
+            endAdornment: translate('ra.time.days')
+          }}
+        />
+        <DateInput label={'resources.entities/protocol.services.end'} source="end" validate={required()} />
+      </Component>
+    </ArrayInput>
+  );
+};
+
+export default ProtocolServiceArrayInput;
diff --git a/edera-web/src/components/ra-inputs/ProvinceAutocompleteInput.js b/edera-web/src/components/ra-inputs/ProvinceAutocompleteInput.js
new file mode 100644 (file)
index 0000000..6acb9a6
--- /dev/null
@@ -0,0 +1,35 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useFormContext, useWatch } from 'react-hook-form';
+
+import { useEffect } from 'react';
+
+const ProvinceAutocompleteInput = ({ validate, ...props }) => {
+  const { setValue } = useFormContext();
+  const regionId = useWatch({
+    name: 'regionId'
+  });
+
+  useEffect(() => {
+    setValue('provinceId', null);
+  }, [regionId, setValue]);
+
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/province"
+      filter={{
+        regionId: regionId || 'none'
+      }}
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText="name" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default ProvinceAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/RFIDDeviceAutocomplete.js b/edera-web/src/components/ra-inputs/RFIDDeviceAutocomplete.js
new file mode 100644 (file)
index 0000000..22e08fa
--- /dev/null
@@ -0,0 +1,41 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useEffect, useState } from 'react';
+import { useWatch, useFormContext } from 'react-hook-form';
+import _ from 'lodash';
+
+const RFIDDeviceAutocomplete = ({ validate, ...props }) => {
+  const { isAreaPresent, type, ...rest } = props;
+  const [isDisabled, setIsDisabled] = useState(false);
+  const officeId = useWatch({ name: 'area.officeId' });
+  const { setValue } = useFormContext();
+
+  useEffect(() => {
+    if (isAreaPresent) {
+      if (!_.isEmpty(officeId)) {
+        setValue('rfidDeviceId', null);
+        setIsDisabled(true);
+      } else {
+        setIsDisabled(false);
+      }
+    }
+  }, [isAreaPresent, officeId, setValue]);
+
+  return (
+    <ReferenceInput
+      {...rest}
+      reference="entities/rfid-device"
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      filter={{ type: type }}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+      disabled={isDisabled}
+    >
+      <AutocompleteInput optionText="name" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default RFIDDeviceAutocomplete;
diff --git a/edera-web/src/components/ra-inputs/RFIDDeviceTypeSelectInput.js b/edera-web/src/components/ra-inputs/RFIDDeviceTypeSelectInput.js
new file mode 100644 (file)
index 0000000..8b63e47
--- /dev/null
@@ -0,0 +1,24 @@
+import React, { useState, useEffect } from 'react';
+import { SelectInput, useDataProvider } from '@applica-software-guru/react-admin';
+
+const RFIDDeviceTypeSelect = (props) => {
+  const [options, setOptions] = useState([]);
+  const dataProvider = useDataProvider();
+
+  useEffect(() => {
+    dataProvider
+      .get('values/rfid-device-types', {
+        pagination: { page: 1, perPage: 100 },
+        sort: { field: 'name', order: 'ASC' }
+      })
+      .then((response) => {
+        const { data } = response;
+
+        setOptions(data.value.map((item) => ({ id: item.label, name: item.value })));
+      });
+  }, [dataProvider]);
+
+  return <SelectInput {...props} choices={options} />;
+};
+
+export default RFIDDeviceTypeSelect;
diff --git a/edera-web/src/components/ra-inputs/RegionAutocompleteInput.js b/edera-web/src/components/ra-inputs/RegionAutocompleteInput.js
new file mode 100644 (file)
index 0000000..0af88c2
--- /dev/null
@@ -0,0 +1,35 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useFormContext, useWatch } from 'react-hook-form';
+
+import { useEffect } from 'react';
+
+const RegionAutocompleteInput = ({ validate, ...props }) => {
+  const { setValue } = useFormContext();
+  const nationId = useWatch({
+    name: 'nationId'
+  });
+
+  useEffect(() => {
+    setValue('regionId', null);
+  }, [nationId, setValue]);
+
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/region"
+      filter={{
+        nationId: nationId || 'none'
+      }}
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText="name" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default RegionAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/SupplierAutocompleteInput.js b/edera-web/src/components/ra-inputs/SupplierAutocompleteInput.js
new file mode 100644 (file)
index 0000000..7eb783c
--- /dev/null
@@ -0,0 +1,21 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+
+const SupplierAutocompleteInput = ({ validate, ...props }) => {
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/supplier"
+      filterToQuery={(searchText) => ({ keyword: searchText })}
+      key={(searchText) => ({ keyword: searchText })}
+      perPage={100}
+      sort={{
+        field: 'businessName',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput optionText="businessName" validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default SupplierAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/SupplierReferentManyInput.js b/edera-web/src/components/ra-inputs/SupplierReferentManyInput.js
new file mode 100644 (file)
index 0000000..fccc9d0
--- /dev/null
@@ -0,0 +1,49 @@
+import {
+  ActionsMenu,
+  CreateInDialogButton,
+  Datagrid,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceManyInput,
+  SearchInput,
+  TextField,
+  TopToolbar,
+  useRecordContext
+} from '@applica-software-guru/react-admin';
+
+import { SupplierReferentForm } from 'components/ra-forms';
+
+const SupplierReferentManyInput = () => {
+  const record = useRecordContext();
+  return (
+    <ReferenceManyInput
+      reference="entities/supplier-referent"
+      target="supplierId"
+      filters={[<SearchInput key="keyword" source="keyword" alwaysOn />]}
+      sort={{ field: 'created', order: 'ASC' }}
+      actions={
+        <TopToolbar>
+          <CreateInDialogButton maxWidth="sm" record={{ supplierId: record?.id }} variant="outlined" color="secondary" size="medium">
+            <SupplierReferentForm />
+          </CreateInDialogButton>
+        </TopToolbar>
+      }
+    >
+      <Datagrid>
+        <TextField source="name" />
+        <TextField source="surname" />
+        <TextField source="email" />
+        <TextField source="department" />
+        <TextField source="updated" />
+        <ActionsMenu>
+          <EditInDialogButton maxWidth="sm">
+            <SupplierReferentForm />
+          </EditInDialogButton>
+          <DeleteWithConfirmButton redirect={false} />
+        </ActionsMenu>
+      </Datagrid>
+    </ReferenceManyInput>
+  );
+};
+
+export default SupplierReferentManyInput;
diff --git a/edera-web/src/components/ra-inputs/SupplyActivityAutocompleteInput.js b/edera-web/src/components/ra-inputs/SupplyActivityAutocompleteInput.js
new file mode 100644 (file)
index 0000000..f144b7c
--- /dev/null
@@ -0,0 +1,93 @@
+import { AutocompleteInput, ReferenceInput, useTranslate, useDataProvider, useNotify } from '@applica-software-guru/react-admin';
+import { useEffect, useState } from 'react';
+import { useFormContext, useWatch } from 'react-hook-form';
+import { usePermissions } from 'react-admin';
+import _ from 'lodash';
+import { Alert, AlertTitle, Typography } from '@mui/material';
+import { Box } from '@mui/material';
+
+const AlertBox = () => {
+  const [supplyDetails, setSupplyDetails] = useState(null);
+  const dataProvider = useDataProvider();
+  const notify = useNotify();
+  const translate = useTranslate();
+  const supplyId = useWatch({
+    name: 'supplyId'
+  });
+
+  useEffect(() => {
+    if (!supplyId) return;
+
+    const fetchDetails = async () => {
+      try {
+        const { data } = await dataProvider.get(`maintenance/supply-info/${supplyId}`);
+        setSupplyDetails(data.supplyDetailsDTO);
+      } catch (error) {
+        notify('ra.notification.http_error', 'error', {});
+      }
+    };
+
+    fetchDetails();
+  }, [supplyId, dataProvider, notify]);
+
+  if (!supplyDetails) return null;
+
+  return (
+    <Alert severity="info" sx={{ marginTop: 1 }}>
+      <AlertTitle>{translate('resources.entities/supply.title')}:</AlertTitle>
+      <Typography
+        dangerouslySetInnerHTML={{
+          __html: translate('resources.entities/supply.info_message', {
+            supplier: supplyDetails.supplierName,
+            equipment: supplyDetails.equipmentName,
+            protocol: supplyDetails.protocolName
+          })
+        }}
+      />
+    </Alert>
+  );
+};
+
+const renderText = (record) =>
+  `${record.supplier.businessName} - ${record.equipmentType.name} - ${record.protocol.title} N° ${record.protocol.protocolNumber} - ${record.ddtNumber}`;
+
+const SupplyActivityAutocompleteInput = ({ validate, ...props }) => {
+  const { setValue } = useFormContext();
+  const customerId = useWatch({
+    name: 'customer.id'
+  });
+  const supplierId = useWatch({
+    name: 'supply.supplierId'
+  });
+  const { permissions, isLoading } = usePermissions();
+
+  useEffect(() => {
+    if (_.isEmpty(customerId) || _.isEmpty(supplierId)) {
+      setValue('supplyId', null);
+    }
+  }, [customerId, supplierId, setValue]);
+
+  const filter =
+    !isLoading && permissions.includes('customer:list') ? { customerId: customerId || 'none', supplierId: supplierId || 'none' } : {};
+
+  return (
+    <Box>
+      <ReferenceInput
+        {...props}
+        reference="entities/supply"
+        filterToQuery={(searchText) => ({ keyword: searchText })}
+        filter={filter}
+        perPage={100}
+        sort={{
+          field: 'name',
+          order: 'ASC'
+        }}
+      >
+        <AutocompleteInput optionText={renderText} validate={validate} />
+      </ReferenceInput>
+      <AlertBox />
+    </Box>
+  );
+};
+
+export default SupplyActivityAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/SupplyAutocompleteInput.js b/edera-web/src/components/ra-inputs/SupplyAutocompleteInput.js
new file mode 100644 (file)
index 0000000..d008f99
--- /dev/null
@@ -0,0 +1,25 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+import { useWatch } from 'react-hook-form';
+
+const renderText = (record) =>
+  `${record.supplier.businessName} - ${record.equipmentType.name} - ${record.protocol.title} N° ${record.protocol.protocolNumber} - ${record.ddtNumber}`;
+
+const SupplyAutocompleteInput = ({ label, validate, ...props }) => {
+  const supplierId = useWatch({ name: 'supplierId' });
+  return (
+    <ReferenceInput
+      {...props}
+      reference="entities/supply"
+      filter={{ supplierId: supplierId }}
+      perPage={100}
+      sort={{
+        field: 'name',
+        order: 'ASC'
+      }}
+    >
+      <AutocompleteInput label={label} optionText={renderText} validate={validate} />
+    </ReferenceInput>
+  );
+};
+
+export default SupplyAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/SupplyManyInput.js b/edera-web/src/components/ra-inputs/SupplyManyInput.js
new file mode 100644 (file)
index 0000000..8069e0c
--- /dev/null
@@ -0,0 +1,72 @@
+import {
+  ActionsMenu,
+  BulkDeleteWithConfirmButton,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  DeleteWithConfirmButton,
+  EditInDialogButton,
+  ReferenceManyInput,
+  TextField,
+  TopToolbar,
+  usePermissions,
+  useRecordContext,
+  useThemeConfig
+} from '@applica-software-guru/react-admin';
+
+import { Grid } from '@mui/material';
+import ProtocolAutoComplete from './ProtocolAutoComplete';
+import { SupplyDialogForm } from 'components/ra-forms';
+
+const SupplyManyInput = () => {
+  const record = useRecordContext();
+  const { spacing } = useThemeConfig();
+  const { permissions, isLoading } = usePermissions();
+  const canEdit = !isLoading && permissions.includes('equipment-attachment:edit');
+  const canCreate = !isLoading && permissions.includes('equipment-attachment:new');
+
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item lg={12} xs={12}>
+        <ReferenceManyInput
+          reference="entities/supply"
+          target="equipmentId"
+          filters={[<ProtocolAutoComplete source="protocolId" alwaysOn display="legend" fullWidth />]}
+          sort={{ field: 'created', order: 'ASC' }}
+          actions={
+            <TopToolbar>
+              {canCreate && (
+                <CreateInDialogButton
+                  record={{ equipmentId: record?.id, supplierId: record?.supplierId }}
+                  variant="outlined"
+                  color="secondary"
+                  size="medium"
+                >
+                  <SupplyDialogForm />
+                </CreateInDialogButton>
+              )}
+            </TopToolbar>
+          }
+        >
+          <Datagrid bulkActionButtons={!canEdit ? null : <BulkDeleteWithConfirmButton />}>
+            <TextField source="protocol.title" sortable={false} />
+            <TextField source="quantity" />
+            <DateField source="orderDate" showTime />
+            <DateField source="deliveryDate" showTime />
+            <DateField source="updated" showTime />
+            {canEdit && (
+              <ActionsMenu>
+                <EditInDialogButton>
+                  <SupplyDialogForm />
+                </EditInDialogButton>
+                <DeleteWithConfirmButton redirect={false} />
+              </ActionsMenu>
+            )}
+          </Datagrid>
+        </ReferenceManyInput>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default SupplyManyInput;
diff --git a/edera-web/src/components/ra-inputs/UserAutocompleteInput.js b/edera-web/src/components/ra-inputs/UserAutocompleteInput.js
new file mode 100644 (file)
index 0000000..60b1037
--- /dev/null
@@ -0,0 +1,9 @@
+import { AutocompleteInput, ReferenceInput } from '@applica-software-guru/react-admin';
+
+const UserAutocompleteInput = ({ validate, ...props }) => (
+  <ReferenceInput {...props} reference="entities/user" source="userId" filterToQuery={(searchText) => ({ name: searchText })}>
+    <AutocompleteInput optionText="name" validate={validate} />
+  </ReferenceInput>
+);
+
+export default UserAutocompleteInput;
diff --git a/edera-web/src/components/ra-inputs/YesOrNoSelectInput.js b/edera-web/src/components/ra-inputs/YesOrNoSelectInput.js
new file mode 100644 (file)
index 0000000..0426106
--- /dev/null
@@ -0,0 +1,13 @@
+import { SelectInput } from '@applica-software-guru/react-admin';
+const YesOrNoSelectInput = (props) => (
+  <SelectInput
+    {...props}
+    emptyText={'ra.boolean.null'}
+    choices={[
+      { id: true, name: 'ra.yes' },
+      { id: false, name: 'ra.no' }
+    ]}
+  />
+);
+
+export default YesOrNoSelectInput;
diff --git a/edera-web/src/components/ra-inputs/index.js b/edera-web/src/components/ra-inputs/index.js
new file mode 100644 (file)
index 0000000..4e3ce71
--- /dev/null
@@ -0,0 +1,39 @@
+export { default as ActivitySelectInput } from './ActivitySelectInput';
+export { default as ActivityTypeAutocompleteInput } from './ActivityTypeAutocompleteInput';
+export { default as CityAutocompleteInput } from './CityAutocompleteInput';
+export { default as CustomerAreaManyInput } from './CustomerAreaManyInput';
+export { default as CustomerAutocompleteInput } from './CustomerAutocompleteInput';
+export { default as CustomerOfficeAutocompleteInput } from './CustomerOfficeAutocompleteInput';
+export { default as CustomerOfficeManyInput } from './CustomerOfficeManyInput';
+export { default as CustomerReferentManyInput } from './CustomerReferentManyInput';
+export { default as EquipmentAttachmentManyInput } from './EquipmentAttachmentManyInput';
+export { default as EquipmentAutocompleteInput } from './EquipmentAutocompleteInput';
+export { default as EquipmentListAutocompleteInput } from './EquipmentListAutocompleteInput';
+export { default as EquipmentManyInput } from './EquipmentManyInput';
+export { default as EquipmentTypeAttachmentManyInput } from './EquipmentTypeAttachmentManyInput';
+export { default as EquipmentTypeAutocompleteInput } from './EquipmentTypeAutocompleteInput';
+export { default as EquipmentTypeManyInput } from './EquipmentTypeManyInput';
+export { default as InterventionArrayInput } from './InterventionArrayInput';
+export { default as LangSelectInput } from './LangSelectInput';
+export { default as MaintenanceManyInput } from './MaintenanceManyInput';
+export { default as MoneyInput } from './MoneyInput';
+export { default as NationAutocompleteInput } from './NationAutocompleteInput';
+export { default as PriceListItemManyInput } from './PriceListItemManyInput';
+export { default as PriceListManyInput } from './PriceListManyInput';
+export { default as ProtocolAttachmentManyInput } from './ProtocolAttachmentManyInput';
+export { default as ProtocolAutoComplete } from './ProtocolAutoComplete';
+export { default as ProvinceAutocompleteInput } from './ProvinceAutocompleteInput';
+export { default as RFIDDeviceAutocomplete } from './RFIDDeviceAutocomplete';
+export { default as RFIDDeviceTypeSelectInput } from './RFIDDeviceTypeSelectInput';
+export { default as RegionAutocompleteInput } from './RegionAutocompleteInput';
+export { default as SupplierAutocompleteInput } from './SupplierAutocompleteInput';
+export { default as SupplierReferentManyInput } from './SupplierReferentManyInput';
+export { default as SupplyActivityAutocompleteInput } from './SupplyActivityAutocompleteInput';
+export { default as SupplyManyInput } from './SupplyManyInput';
+export { default as UserAutocompleteInput } from './UserAutocompleteInput';
+export { default as YesOrNoSelectInput } from './YesOrNoSelectInput';
+export { default as CustomerReadOnlyInput } from './CustomerReadOnlyInput';
+export { default as CustomerAreaAutocompleteInput } from './CustomerAreaAutocompleteInput';
+export { default as SupplyAutocompleteInput } from './SupplyAutocompleteInput';
+export { default as DocxTemplateTypeSelectInput } from './DocxTemplateTypeSelectInput';
+export { default as ProtocolServiceArrayInput } from './ProtocolServiceArrayInput';
diff --git a/edera-web/src/components/ra-lists/ActivityList.js b/edera-web/src/components/ra-lists/ActivityList.js
new file mode 100644 (file)
index 0000000..d3149da
--- /dev/null
@@ -0,0 +1,57 @@
+import { ActivityTypeAutocompleteInput, CustomerAutocompleteInput, SupplyActivityAutocompleteInput } from 'components/ra-inputs';
+import {
+  BulkDeleteWithConfirmButton,
+  Datagrid,
+  DateField,
+  List,
+  ReferenceField,
+  SearchInput,
+  SelectInput,
+  TextField,
+  usePermissions
+} from '@applica-software-guru/react-admin';
+
+import { ActivityStatusField } from 'components/ra-fields';
+
+const ActivityList = ({ activityStatus }) => {
+  const { permissions, isLoading } = usePermissions();
+  const canDelete = !isLoading && permissions.includes('activity:delete');
+  const canCreate = !isLoading && permissions.includes('activity:new');
+
+  const filtersList = [
+    <SearchInput source="keyword" alwaysOn />,
+    <ActivityTypeAutocompleteInput source="activityTypeId" display="legend" />,
+    <SupplyActivityAutocompleteInput source="supplyId" display="legend" />,
+    <SelectInput source="status" display="legend" choices={activityStatus} defaultValue={'OPEN'} />
+  ];
+
+  if (!isLoading && permissions.includes('customer:list')) {
+    filtersList.push(<CustomerAutocompleteInput source="customerId" display="legend" />);
+  }
+
+  return (
+    <List perPage={25} filters={filtersList} hasCreate={canCreate}>
+      <Datagrid rowClick="edit" bulkActionButtons={!canDelete ? null : <BulkDeleteWithConfirmButton />}>
+        {!isLoading && permissions.includes('supplier:list') && (
+          <ReferenceField source="supply.supplierId" reference="entities/supplier" link={false} sortable={false}>
+            <TextField source="businessName" />
+          </ReferenceField>
+        )}
+        {!isLoading && permissions.includes('customer:list') && <TextField source="customer.name" sortable={false} />}
+        {!isLoading && permissions.includes('equipment:list') && (
+          <ReferenceField source="supply.equipmentId" reference="entities/equipment" link={false} sortable={false}>
+            <TextField source="equipmentType.name" />
+          </ReferenceField>
+        )}
+        <TextField source="activityType.description" sortable={false} />
+        <TextField source="title" />
+        <TextField source="user.name" sortable={false} />
+        <DateField source="date" />
+        <DateField source="updated" showTime />
+        <ActivityStatusField source="status" />
+      </Datagrid>
+    </List>
+  );
+};
+
+export default ActivityList;
diff --git a/edera-web/src/components/ra-lists/ActivityTypeList.js b/edera-web/src/components/ra-lists/ActivityTypeList.js
new file mode 100644 (file)
index 0000000..4d00ac2
--- /dev/null
@@ -0,0 +1,13 @@
+import { AttachmentField, Datagrid, DateField, List, SearchInput, TextField } from '@applica-software-guru/react-admin';
+
+const ActivityTypeList = () => (
+  <List perPage={25} filters={[<SearchInput source="keyword" alwaysOn />]}>
+    <Datagrid rowClick="edit">
+      <TextField source="description" />
+      <AttachmentField source="attachment" title="attachment.name" />
+      <DateField source="updated" showTime />
+    </Datagrid>
+  </List>
+);
+
+export default ActivityTypeList;
diff --git a/edera-web/src/components/ra-lists/AuditLogList.js b/edera-web/src/components/ra-lists/AuditLogList.js
new file mode 100644 (file)
index 0000000..4d788f6
--- /dev/null
@@ -0,0 +1,49 @@
+import { Datagrid, DateField, DateTimeInput, List, ReferenceField, SearchInput, TextField } from '@applica-software-guru/react-admin';
+
+import { AuditLogMessageField } from '../ra-fields';
+import { UserAutocompleteInput } from 'components/ra-inputs';
+
+const parse = (date) => date;
+
+const AuditLogList = () => {
+  return (
+    <List
+      perPage={25}
+      sort={{ field: 'created', order: 'DESC' }}
+      exporter={false}
+      filters={[
+        <SearchInput key="keyword" source="keyword" alwaysOn fullWidth />,
+        <DateTimeInput
+          type="datetime-local"
+          key="from"
+          source="from"
+          label="resources.entities/audit-log.fields.from"
+          display="legend"
+          parse={parse}
+          alwaysOn
+          fullWidth
+        />,
+        <DateTimeInput
+          key="to"
+          source="to"
+          label="resources.entities/audit-log.fields.to"
+          display="legend"
+          parse={parse}
+          alwaysOn
+          fullWidth
+        />,
+        <UserAutocompleteInput key="userId" source="userId" display="legend" alwaysOn />
+      ]}
+    >
+      <Datagrid rowClick={false} bulkActionButtons={false}>
+        <DateField source="created" showTime />
+        <ReferenceField source="userId" reference="entities/user" emptyText="Anonymous" link={false} sortable={false}>
+          <TextField source="name" />
+        </ReferenceField>
+        <AuditLogMessageField source="message" sortable={false} />
+      </Datagrid>
+    </List>
+  );
+};
+
+export default AuditLogList;
diff --git a/edera-web/src/components/ra-lists/CustomerList.js b/edera-web/src/components/ra-lists/CustomerList.js
new file mode 100644 (file)
index 0000000..2e2c673
--- /dev/null
@@ -0,0 +1,45 @@
+import {
+  BooleanField,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  Empty,
+  List,
+  SearchInput,
+  TextField
+} from '@applica-software-guru/react-admin';
+
+import { CustomerCreateForm } from '../ra-forms';
+
+const Actions = () => (
+  <CreateInDialogButton
+    sx={{
+      minWidth: 130
+    }}
+    redirect="edit"
+    size="medium"
+    variant="outlined"
+    color="secondary"
+    label="resources.entities/customer.actions.create"
+  >
+    <CustomerCreateForm />
+  </CreateInDialogButton>
+);
+
+const CustomerList = () => (
+  <List
+    perPage={25}
+    filters={[<SearchInput source="keyword" key="keyword" alwaysOn />]}
+    empty={<Empty actions={<Actions />} />}
+    actions={<Actions />}
+  >
+    <Datagrid rowClick="edit">
+      <BooleanField source="active" />
+      <TextField source="name" />
+      <TextField source="vatCode" />
+      <DateField source="updated" showTime />
+    </Datagrid>
+  </List>
+);
+
+export default CustomerList;
diff --git a/edera-web/src/components/ra-lists/DeviceList.js b/edera-web/src/components/ra-lists/DeviceList.js
new file mode 100644 (file)
index 0000000..a04aa85
--- /dev/null
@@ -0,0 +1,25 @@
+import { Datagrid, DateField, List, SearchInput, TextField, useThemeConfig, useTranslate } from '@applica-software-guru/react-admin';
+
+import { Alert } from '@mui/material';
+
+const AlertBox = () => {
+  const { spacing } = useThemeConfig();
+  const translate = useTranslate();
+  return (
+    <Alert sx={{ p: spacing, m: spacing }} severity="info">
+      {translate('resources.entities/device.alert')}
+    </Alert>
+  );
+};
+const DeviceList = () => (
+  <List perPage={25} filters={[<SearchInput source="keyword" alwaysOn />]}>
+    <AlertBox />
+    <Datagrid rowClick="edit">
+      <TextField source="id" />
+      <TextField source="code" />
+      <DateField source="registrationDate" showTime />
+    </Datagrid>
+  </List>
+);
+
+export default DeviceList;
diff --git a/edera-web/src/components/ra-lists/DocxTemplateList.js b/edera-web/src/components/ra-lists/DocxTemplateList.js
new file mode 100644 (file)
index 0000000..790376d
--- /dev/null
@@ -0,0 +1,13 @@
+import { AttachmentField, Datagrid, DateField, List, SearchInput, TextField } from '@applica-software-guru/react-admin';
+
+const DocxTemplateList = () => (
+  <List perPage={25} filters={[<SearchInput source="keyword" alwaysOn />]}>
+    <Datagrid rowClick="edit">
+      <TextField source="name" />
+      <AttachmentField source="attachment" title="attachment.name" />
+      <DateField source="updated" showTime />
+    </Datagrid>
+  </List>
+);
+
+export default DocxTemplateList;
diff --git a/edera-web/src/components/ra-lists/EquipmentList.js b/edera-web/src/components/ra-lists/EquipmentList.js
new file mode 100644 (file)
index 0000000..f2cf3d0
--- /dev/null
@@ -0,0 +1,56 @@
+import {
+  BulkDeleteWithConfirmButton,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  Empty,
+  List,
+  SearchInput,
+  TextField,
+  usePermissions
+} from '@applica-software-guru/react-admin';
+
+import { EquipmentCreateInDialogForm } from 'components/ra-forms';
+
+const Actions = () => {
+  const { permissions, isLoading } = usePermissions();
+  return (
+    !isLoading &&
+    permissions.includes('equipment:new') && (
+      <CreateInDialogButton
+        sx={{
+          minWidth: 130
+        }}
+        redirect="edit"
+        size="medium"
+        variant="outlined"
+        color="secondary"
+        label="ra.action.create"
+      >
+        <EquipmentCreateInDialogForm />
+      </CreateInDialogButton>
+    )
+  );
+};
+
+const EquipmentList = () => {
+  const { permissions, isLoading } = usePermissions();
+  const canDelete = !isLoading && permissions.includes('equipment:delete');
+  return (
+    <List
+      perPage={25}
+      filters={[<SearchInput source="keyword" alwaysOn />]}
+      exporter={false}
+      actions={<Actions />}
+      empty={<Empty actions={<Actions />} />}
+    >
+      <Datagrid rowClick="edit" bulkActionButtons={!canDelete ? null : <BulkDeleteWithConfirmButton />}>
+        {!isLoading && permissions.includes('supplier:list') && <TextField source="supplier.businessName" sortable={false} />}
+        <TextField source="equipmentType.name" />
+        <DateField source="updated" showTime />
+      </Datagrid>
+    </List>
+  );
+};
+
+export default EquipmentList;
diff --git a/edera-web/src/components/ra-lists/EquipmentTypeList.js b/edera-web/src/components/ra-lists/EquipmentTypeList.js
new file mode 100644 (file)
index 0000000..61e37c6
--- /dev/null
@@ -0,0 +1,30 @@
+import { BooleanField, Datagrid, DateField, List, SearchInput, TextField, CreateInDialogButton } from '@applica-software-guru/react-admin';
+import { EquipmentTypeCreateForm } from 'components/ra-forms';
+
+const Actions = () => (
+  <CreateInDialogButton
+    sx={{
+      minWidth: 130
+    }}
+    redirect="edit"
+    size="medium"
+    variant="outlined"
+    color="secondary"
+    label="ra.action.create"
+  >
+    <EquipmentTypeCreateForm />
+  </CreateInDialogButton>
+);
+
+const EquipmentTypeList = () => (
+  <List perPage={25} filters={[<SearchInput source="keyword" alwaysOn />]} exporter={false} actions={<Actions />}>
+    <Datagrid rowClick="edit">
+      <TextField source="progressive" />
+      <TextField source="name" />
+      <BooleanField source="accessory" />
+      <DateField source="updated" showTime />
+    </Datagrid>
+  </List>
+);
+
+export default EquipmentTypeList;
diff --git a/edera-web/src/components/ra-lists/I18nMessageList.js b/edera-web/src/components/ra-lists/I18nMessageList.js
new file mode 100644 (file)
index 0000000..9c0d684
--- /dev/null
@@ -0,0 +1,14 @@
+import { ActionsField, Datagrid, List, RecordInput, SearchInput, TextField } from '@applica-software-guru/react-admin';
+
+const I18nMessageList = () => (
+  <List perPage={25} filters={[<SearchInput key="keyword" source="keyword" alwaysOn fullWidth />]}>
+    <Datagrid>
+      <TextField source="lang" />
+      <RecordInput source="code" />
+      <RecordInput source="text" />
+      <ActionsField />
+    </Datagrid>
+  </List>
+);
+
+export default I18nMessageList;
diff --git a/edera-web/src/components/ra-lists/MaintenanceList.js b/edera-web/src/components/ra-lists/MaintenanceList.js
new file mode 100644 (file)
index 0000000..708d676
--- /dev/null
@@ -0,0 +1,35 @@
+import { ActivitySelectInput, UserAutocompleteInput } from 'components/ra-inputs';
+import {
+  BulkDeleteWithConfirmButton,
+  Datagrid,
+  DateField,
+  List,
+  SearchInput,
+  TextField,
+  usePermissions
+} from '@applica-software-guru/react-admin';
+
+const MaintenanceList = () => {
+  const { isLoading, permissions } = usePermissions();
+  const canDelete = !isLoading && permissions.includes('maintenance:delete');
+  return (
+    <List
+      perPage={25}
+      filters={[
+        <SearchInput source="keyword" alwaysOn />,
+        <ActivitySelectInput source="activityId" />,
+        <UserAutocompleteInput source="userId" />
+      ]}
+    >
+      <Datagrid rowClick="edit" bulkActionButtons={!canDelete ? null : <BulkDeleteWithConfirmButton />}>
+        <TextField source="activity.title" sortable={false} />
+        <TextField source="title" />
+        <TextField source="interventionNumber" />
+        <DateField source="date" sortable={false} />
+        <DateField source="updated" showTime />
+      </Datagrid>
+    </List>
+  );
+};
+
+export default MaintenanceList;
diff --git a/edera-web/src/components/ra-lists/ProtocolList.js b/edera-web/src/components/ra-lists/ProtocolList.js
new file mode 100644 (file)
index 0000000..ebd8815
--- /dev/null
@@ -0,0 +1,55 @@
+import {
+  BulkDeleteWithConfirmButton,
+  CreateInDialogButton,
+  Datagrid,
+  DateField,
+  Empty,
+  List,
+  SearchInput,
+  TextField,
+  usePermissions
+} from '@applica-software-guru/react-admin';
+import { ProtocolDialogForm } from 'components/ra-forms';
+
+import { CustomerAutocompleteInput } from 'components/ra-inputs';
+
+const Actions = () => (
+  <CreateInDialogButton
+    sx={{
+      minWidth: 130
+    }}
+    redirect="edit"
+    size="medium"
+    variant="outlined"
+    color="secondary"
+    label="resources.entities/customer.actions.create"
+  >
+    <ProtocolDialogForm />
+  </CreateInDialogButton>
+);
+
+const ProtocolList = () => {
+  const { permissions, isLoading } = usePermissions();
+  const canCreate = !isLoading && permissions.includes('protocol:new');
+  const canDelete = !isLoading && permissions.includes('protocol:delete');
+
+  const filtersList = [<SearchInput source="keyword" alwaysOn />];
+
+  if (!isLoading && permissions.includes('customer:list')) {
+    filtersList.push(<CustomerAutocompleteInput source="customerId" alwaysOn display="legend" />);
+  }
+
+  return (
+    <List perPage={25} filters={filtersList} hasCreate={canCreate} empty={<Empty actions={<Actions />} />} actions={<Actions />}>
+      <Datagrid rowClick="edit" bulkActionButtons={!canDelete ? null : <BulkDeleteWithConfirmButton />}>
+        {!isLoading && permissions.includes('customer:list') && <TextField source="customer.name" sortable={false} />}
+        <TextField source="title" />
+        <TextField source="protocolNumber" />
+        <DateField source="protocolDate" />
+        <DateField source="updated" showTime />
+      </Datagrid>
+    </List>
+  );
+};
+
+export default ProtocolList;
diff --git a/edera-web/src/components/ra-lists/RFIDDeviceList.js b/edera-web/src/components/ra-lists/RFIDDeviceList.js
new file mode 100644 (file)
index 0000000..06d2838
--- /dev/null
@@ -0,0 +1,14 @@
+import { Datagrid, DateField, List, SearchInput, TextField } from '@applica-software-guru/react-admin';
+
+const RFIDeviceList = () => (
+  <List perPage={25} filters={[<SearchInput source="keyword" alwaysOn />]}>
+    <Datagrid rowClick="edit">
+      <TextField source="name" />
+      <TextField source="code" />
+      <DateField source="updated" showTime />
+      <TextField source="type" />
+    </Datagrid>
+  </List>
+);
+
+export default RFIDeviceList;
diff --git a/edera-web/src/components/ra-lists/RFIDDeviceTrackList.js b/edera-web/src/components/ra-lists/RFIDDeviceTrackList.js
new file mode 100644 (file)
index 0000000..bc21ae1
--- /dev/null
@@ -0,0 +1,123 @@
+import {
+  Datagrid,
+  DateField,
+  DateTimeInput,
+  List,
+  MainCard,
+  ReferenceField,
+  TextField,
+  useListContext,
+  useThemeConfig,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import { Grid, List as MuiList, ListItem, ListItemText, Typography } from '@mui/material';
+import { RFIDDeviceAutocomplete, SupplyAutocompleteInput, CustomerAreaAutocompleteInput } from 'components/ra-inputs';
+import queryString from 'query-string';
+import { useLocation } from 'react-router-dom';
+import { Fragment, useEffect } from 'react';
+
+const FilterSetter = () => {
+  const location = useLocation();
+  const queryParams = queryString.parse(location.search);
+  const gatewayId = queryParams['gateway-id'];
+  const { setFilters } = useListContext();
+
+  useEffect(() => {
+    if (gatewayId) {
+      setFilters({ gatewayId: gatewayId });
+    }
+  }, [gatewayId, setFilters]);
+
+  return null;
+};
+
+const RFIDDeviceTrackList = () => {
+  const translate = useTranslate();
+  const { spacing } = useThemeConfig();
+  const location = useLocation();
+  const queryParams = queryString.parse(location.search);
+  const gatewayId = queryParams['gateway-id'];
+  const currentUrl = `http://${window.location.hostname}${window.location.port ? `:8080` : ''}/api/track?`;
+
+  return (
+    <Grid container spacing={spacing}>
+      <Grid item xs={12} sm={4}>
+        <MainCard title={translate('resources.entities/rfid-device-track.tutorial.title')} content={false}>
+          <MuiList component="nav" aria-label="Come utilizzare le API di tracciamento" sx={{ m: 0, p: 0, pl: 0.5, pb: 1, pr: 0.5 }}>
+            <ListItem sx={{ paddingTop: 0, paddingBottom: 1 }}>
+              <ListItemText primary="Chiamata HTTP GET base" secondary={<code>{`${currentUrl}`}</code>} />
+            </ListItem>
+            <ListItem sx={{ paddingTop: 0, paddingBottom: 1 }}>
+              <ListItemText
+                primary="Parametri obbligatori"
+                secondary={
+                  <Fragment>
+                    <Typography textAlign="justify" variant="body2" color="GrayText" component="span">
+                      Includere <b>tag</b> (ID del tag RFID), <b>timestamp</b> e <b>gate</b> (ID del gateway RFID). Il campo{' '}
+                      <b>timestamp</b>, se non specificato, sarà impostato al momento della chiamata automaticamente.
+                      <br />
+                    </Typography>
+                    <Typography textAlign="justify" variant="body2" color="GrayText" component="span" sx={{ mt: 1 }}>
+                      <b>Attenzione:</b> ricorda che tag e gateway devono essere preventivamente registrati nel sistema all'interno della
+                      sezione RFID.
+                    </Typography>
+                  </Fragment>
+                }
+              />
+            </ListItem>
+            <ListItem sx={{ paddingTop: 0, paddingBottom: 1 }}>
+              <ListItemText primary="Esempio" secondary={<code>GET {`${currentUrl}tag=1234&gate=gate01`}</code>} />
+            </ListItem>
+            <ListItem sx={{ paddingTop: 0, paddingBottom: 0 }}>
+              <ListItemText
+                primary="Parametri aggiuntivi"
+                secondary={
+                  <Typography textAlign="justify" variant="body2" color="GrayText" component="span">
+                    &Egrave; possibile aggiungere ulteriori parametri che saranno accodati alla chiamata.
+                  </Typography>
+                }
+              />
+            </ListItem>
+          </MuiList>
+        </MainCard>
+      </Grid>
+      <Grid item xs={12} sm={8}>
+        <List
+          key={gatewayId}
+          perPage={25}
+          filterDefaultValues={{ gatewayId }}
+          filters={[
+            <RFIDDeviceAutocomplete source="tagId" type="TAG" display="legend" alwaysOn />,
+            <RFIDDeviceAutocomplete source="gatewayId" type="GATEWAY" display="legend" alwaysOn />,
+            <SupplyAutocompleteInput source="supplyId" display="legend" alwaysOn />,
+            <CustomerAreaAutocompleteInput source="areaId" display="legend" alwaysOn />,
+            <DateTimeInput source="from" display="legend" alwaysOn />,
+            <DateTimeInput source="to" display="legend" alwaysOn />
+          ]}
+          sort={{ field: 'ts', order: 'DESC' }}
+        >
+          <FilterSetter />
+          <Datagrid bulkActionButtons={null}>
+            <TextField source="gateway.name" sortable={false} />
+            <TextField source="tag.name" sortable={false} />
+            <ReferenceField source="supplyId" reference="entities/supply" sortable={false}>
+              <TextField source="equipmentType.name" />
+            </ReferenceField>
+            <ReferenceField
+              source="areaId"
+              reference="entities/customer-area"
+              sortable={false}
+              link={(data, reference) => `/entities/customer/${data?.customerId}/areas`}
+            >
+              <TextField source="name" />
+            </ReferenceField>
+            <TextField source="additionalData" />
+            <DateField source="ts" showTime />
+          </Datagrid>
+        </List>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default RFIDDeviceTrackList;
diff --git a/edera-web/src/components/ra-lists/SupplierList.js b/edera-web/src/components/ra-lists/SupplierList.js
new file mode 100644 (file)
index 0000000..ba0367b
--- /dev/null
@@ -0,0 +1,36 @@
+import { CreateInDialogButton, Datagrid, DateField, Empty, List, SearchInput, TextField } from '@applica-software-guru/react-admin';
+import { SupplierCreateForm } from 'components/ra-forms';
+
+const Actions = () => (
+  <CreateInDialogButton
+    sx={{
+      minWidth: 130
+    }}
+    redirect="edit"
+    size="medium"
+    variant="outlined"
+    color="secondary"
+    label="resources.entities/supplier.actions.create"
+  >
+    <SupplierCreateForm />
+  </CreateInDialogButton>
+);
+
+const SupplierList = () => (
+  <List
+    perPage={25}
+    filters={[<SearchInput source="keyword" alwaysOn />]}
+    exporter={false}
+    empty={<Empty actions={<Actions />} />}
+    actions={<Actions />}
+  >
+    <Datagrid rowClick="edit">
+      <TextField source="businessName" />
+      <TextField source="vatNumber" />
+      <TextField source="city.name" sortable={false} />
+      <DateField source="updated" showTime />
+    </Datagrid>
+  </List>
+);
+
+export default SupplierList;
diff --git a/edera-web/src/components/ra-lists/SupplyList.js b/edera-web/src/components/ra-lists/SupplyList.js
new file mode 100644 (file)
index 0000000..89918bd
--- /dev/null
@@ -0,0 +1,35 @@
+import { BulkDeleteWithConfirmButton, Datagrid, DateField, List, TextField, usePermissions } from '@applica-software-guru/react-admin';
+import { SupplyStatusField } from 'components/ra-fields';
+import { EquipmentListAutocompleteInput, ProtocolAutoComplete, SupplierAutocompleteInput } from 'components/ra-inputs';
+
+const SupplyList = () => {
+  const { isLoading, permissions } = usePermissions();
+  const canCreate = !isLoading && permissions.includes('supply:new');
+  const canDelete = !isLoading && permissions.includes('supply:delete');
+
+  const filtersList = [
+    <EquipmentListAutocompleteInput source="equipmentId" alwaysOn display="legend" />,
+    <ProtocolAutoComplete source="protocolId" alwaysOn display="legend" />
+  ];
+
+  if (!isLoading && permissions.includes('supplier:list')) {
+    filtersList.push(<SupplierAutocompleteInput source="supplierId" alwaysOn display="legend" />);
+  }
+
+  return (
+    <List perPage={25} filters={filtersList} hasCreate={canCreate}>
+      <Datagrid rowClick="edit" bulkActionButtons={!canDelete ? null : <BulkDeleteWithConfirmButton />}>
+        {!isLoading && permissions.includes('supplier:list') && <TextField source="supplier.businessName" sortable={false} />}
+        {!isLoading && permissions.includes('equipment:list') && <TextField source="equipmentType.name" />}
+        <TextField source="protocol.title" sortable={false} />
+        <TextField source="quantity" />
+        <DateField source="orderDate" />
+        <DateField source="deliveryDate" />
+        <DateField source="updated" showTime />
+        <SupplyStatusField source="usageStatus" />
+      </Datagrid>
+    </List>
+  );
+};
+
+export default SupplyList;
diff --git a/edera-web/src/components/ra-lists/UserList.js b/edera-web/src/components/ra-lists/UserList.js
new file mode 100644 (file)
index 0000000..06a7d1a
--- /dev/null
@@ -0,0 +1,95 @@
+import {
+  BooleanField,
+  BooleanInput,
+  CoverField,
+  Datagrid,
+  DateField,
+  EmailField,
+  FunctionField,
+  ImpersonateUserButton,
+  List,
+  ReferenceField,
+  SearchInput,
+  SelectInput,
+  SimpleList,
+  TextField,
+  useTranslate
+} from '@applica-software-guru/react-admin';
+import { Chip, useMediaQuery } from '@mui/material';
+
+import { CustomerAutocompleteInput } from 'components/ra-inputs';
+import PropTypes from 'prop-types';
+
+const sortRoles = (roles, configuredRoles) => {
+  return roles
+    ?.filter((r) => configuredRoles.find((cr) => cr.id === r))
+    .sort((a, b) => {
+      const aIndex = configuredRoles.findIndex((r) => r.id === a);
+      const bIndex = configuredRoles.findIndex((r) => r.id === b);
+
+      return aIndex - bIndex;
+    });
+};
+const UserList = ({ configuredRoles }) => {
+  const isSmall = useMediaQuery((theme) => theme.breakpoints.down('sm'));
+  const translate = useTranslate();
+  return (
+    <List
+      perPage={25}
+      filters={[
+        <SearchInput key="keyword" source="keyword" alwaysOn fullWidth />,
+        <SelectInput
+          key="role"
+          source="role"
+          choices={configuredRoles}
+          alwaysOn
+          emptyText="ra.action.view_all"
+          display="legend"
+          fullWidth
+        />,
+
+        <CustomerAutocompleteInput source="customerId" alwaysOn display="legend" fullWidth />,
+        <BooleanInput key="active" source="active" display="legend" fullWidth />
+      ]}
+    >
+      {isSmall ? (
+        <SimpleList
+          leftAvatar={(record) => <CoverField source="image" circle width={50} height={50} justify="flex-end" record={record} />}
+          primaryText={(record) => record.name}
+          secondaryText={(record) => record.email}
+        />
+      ) : (
+        <Datagrid rowClick="edit">
+          <CoverField source="image" sortable={false} label="" width={50} height={50} circle />
+          <BooleanField source="active" />
+          <TextField source="name" sortBy="username" />
+          <EmailField source="email" />
+          <ReferenceField source="customerId" reference="entities/customer" link="show" sortable={false}>
+            <TextField source="name" />
+          </ReferenceField>
+          <DateField source="registrationDate" showTime />
+          <FunctionField
+            label="ra.user.roles"
+            render={(record) =>
+              sortRoles(record?.roles, configuredRoles)?.map((role) => (
+                <Chip
+                  label={translate(configuredRoles.find((r) => r.id === role)?.name)}
+                  key={role}
+                  sx={{ mr: 1, mt: 1 }}
+                  color="secondary"
+                />
+              ))
+            }
+          />
+          <ImpersonateUserButton />
+        </Datagrid>
+      )}
+    </List>
+  );
+};
+
+UserList.propTypes = {
+  configuredRoles: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, name: PropTypes.string })).isRequired
+};
+
+export default UserList;
diff --git a/edera-web/src/components/ra-lists/index.js b/edera-web/src/components/ra-lists/index.js
new file mode 100644 (file)
index 0000000..4457fea
--- /dev/null
@@ -0,0 +1,16 @@
+export { default as AuditLogList } from './AuditLogList';
+export { default as CustomerList } from './CustomerList';
+export { default as DeviceList } from './DeviceList';
+export { default as I18nMessageList } from './I18nMessageList';
+export { default as RFIDeviceList } from './RFIDDeviceList';
+export { default as SupplierList } from './SupplierList';
+export { default as UserList } from './UserList';
+export { default as EquipmentTypeList } from './EquipmentTypeList';
+export { default as ProtocolList } from './ProtocolList';
+export { default as SupplyList } from './SupplyList';
+export { default as ActivityList } from './ActivityList';
+export { default as ActivityTypeList } from './ActivityTypeList';
+export { default as MaintenanceList } from './MaintenanceList';
+export { default as EquipmentList } from './EquipmentList';
+export { default as RFIDDeviceTrackList } from './RFIDDeviceTrackList';
+export { default as DocxTemplateList } from './DocxTemplateList';
diff --git a/edera-web/src/config.js b/edera-web/src/config.js
new file mode 100644 (file)
index 0000000..28b41c7
--- /dev/null
@@ -0,0 +1,27 @@
+let environment = 'PRODUCTION';
+let appUrl = `//${document.location.host}/`;
+if (appUrl.endsWith(':3000/')) {
+  appUrl = 'http://localhost:8080/';
+  environment = 'DEVELOPER';
+} else if (appUrl.endsWith(':3001/')) {
+  appUrl = 'http://localhost:8001/';
+  environment = 'DEVELOPER';
+}
+export const APP_URL = appUrl;
+export const API_URL = `${APP_URL}api`;
+export const ENVIRONMENT = environment;
+export const APP_NAME = 'Edera';
+export const COPY = '&copy; Dyrecta for ';
+
+export const CONFIGURED_ROLES = [
+  { id: 'ROLE_ADMIN', name: 'ra.roles.admin' },
+  { id: 'ROLE_OPERATOR', name: 'ra.roles.operator' },
+  { id: 'ROLE_USER', name: 'ra.roles.user' },
+  { id: 'ROLE_MAINTAINER', name: 'ra.roles.maintainer' },
+  { id: 'ROLE_CUSTOMER', name: 'ra.roles.customer' }
+];
+
+export const ACTIVITY_STATUS = [
+  { id: 'OPEN', name: 'ra.status.open' },
+  { id: 'CLOSED', name: 'ra.status.closed' }
+];
diff --git a/edera-web/src/contexts/index.js b/edera-web/src/contexts/index.js
new file mode 100644 (file)
index 0000000..cb0ff5c
--- /dev/null
@@ -0,0 +1 @@
+export {};
diff --git a/edera-web/src/entities/activity-type.js b/edera-web/src/entities/activity-type.js
new file mode 100644 (file)
index 0000000..eb77f85
--- /dev/null
@@ -0,0 +1,23 @@
+import { ActivityTypeForm, ActivityTypeList } from 'components';
+import { Create, Edit } from '@applica-software-guru/react-admin';
+const ActivityTypeCreate = () => (
+  <Create>
+    <ActivityTypeForm />
+  </Create>
+);
+const ActivityTypeEdit = () => (
+  <Edit>
+    <ActivityTypeForm />
+  </Edit>
+);
+
+const config = {
+  list: ActivityTypeList,
+  edit: ActivityTypeEdit,
+  create: ActivityTypeCreate,
+  options: {
+    group: 'maintenance'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/activity.js b/edera-web/src/entities/activity.js
new file mode 100644 (file)
index 0000000..4fb135e
--- /dev/null
@@ -0,0 +1,27 @@
+import { ActivityForm, ActivityList } from 'components';
+import { Create, Edit } from '@applica-software-guru/react-admin';
+
+import { ACTIVITY_STATUS } from 'config';
+const ActivityCreate = () => (
+  <Create>
+    <ActivityForm activityStatus={ACTIVITY_STATUS} />
+  </Create>
+);
+const ActivityEdit = () => (
+  <Edit>
+    <ActivityForm activityStatus={ACTIVITY_STATUS} />
+  </Edit>
+);
+
+const CustomActivityList = () => <ActivityList activityStatus={ACTIVITY_STATUS} />;
+
+const config = {
+  list: CustomActivityList,
+  edit: ActivityEdit,
+  create: ActivityCreate,
+  options: {
+    group: 'maintenance'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/audit-log.js b/edera-web/src/entities/audit-log.js
new file mode 100644 (file)
index 0000000..4b6da55
--- /dev/null
@@ -0,0 +1,10 @@
+import { AuditLogList } from 'components';
+
+const config = {
+  list: AuditLogList,
+  options: {
+    group: 'security'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/customer.js b/edera-web/src/entities/customer.js
new file mode 100644 (file)
index 0000000..ea61471
--- /dev/null
@@ -0,0 +1,25 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { CustomerEditForm, CustomerList } from 'components';
+
+const CustomerCreate = () => (
+  <Create>
+    <CustomerEditForm />
+  </Create>
+);
+
+const CustomerEdit = () => (
+  <Edit>
+    <CustomerEditForm />
+  </Edit>
+);
+
+const config = {
+  list: CustomerList,
+  edit: CustomerEdit,
+  create: CustomerCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/device.js b/edera-web/src/entities/device.js
new file mode 100644 (file)
index 0000000..224cf4d
--- /dev/null
@@ -0,0 +1,23 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { DeviceForm, DeviceList } from 'components';
+const DeviceCreate = () => (
+  <Create>
+    <DeviceForm />
+  </Create>
+);
+const DeviceEdit = () => (
+  <Edit>
+    <DeviceForm />
+  </Edit>
+);
+
+const config = {
+  list: DeviceList,
+  edit: DeviceEdit,
+  create: DeviceCreate,
+  options: {
+    group: 'security'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/docx-template.js b/edera-web/src/entities/docx-template.js
new file mode 100644 (file)
index 0000000..ff2b69c
--- /dev/null
@@ -0,0 +1,23 @@
+import { DocxTemplateForm, DocxTemplateList } from 'components';
+import { Create, Edit } from '@applica-software-guru/react-admin';
+const DocxTemplateCreate = () => (
+  <Create>
+    <DocxTemplateForm />
+  </Create>
+);
+const DocxTemplateEdit = () => (
+  <Edit>
+    <DocxTemplateForm />
+  </Edit>
+);
+
+const config = {
+  list: DocxTemplateList,
+  edit: DocxTemplateEdit,
+  create: DocxTemplateCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/equipment-type.js b/edera-web/src/entities/equipment-type.js
new file mode 100644 (file)
index 0000000..768060d
--- /dev/null
@@ -0,0 +1,24 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { EquipmentTypeList, EquipmentTypeEditForm, EquipmentTypeCreateForm } from 'components';
+
+const EquipmentTypeCreate = () => (
+  <Create redirect={false}>
+    <EquipmentTypeCreateForm />
+  </Create>
+);
+const EquipmentTypeEdit = () => (
+  <Edit redirect={false}>
+    <EquipmentTypeEditForm />
+  </Edit>
+);
+
+const config = {
+  list: EquipmentTypeList,
+  edit: EquipmentTypeEdit,
+  create: EquipmentTypeCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/equipment.js b/edera-web/src/entities/equipment.js
new file mode 100644 (file)
index 0000000..b7eaf9f
--- /dev/null
@@ -0,0 +1,24 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { EquipmentCreateForm, EquipmentEditForm, EquipmentList } from 'components';
+
+const EquipmentCreate = () => (
+  <Create>
+    <EquipmentCreateForm />
+  </Create>
+);
+const EquipmentEdit = () => (
+  <Edit>
+    <EquipmentEditForm />
+  </Edit>
+);
+
+const config = {
+  edit: EquipmentEdit,
+  create: EquipmentCreate,
+  list: EquipmentList,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/i18n-message.js b/edera-web/src/entities/i18n-message.js
new file mode 100644 (file)
index 0000000..7ea7363
--- /dev/null
@@ -0,0 +1,25 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { I18nMessageForm, I18nMessageList } from 'components';
+
+const MessageCreate = () => (
+  <Create>
+    <I18nMessageForm />
+  </Create>
+);
+
+const MessageEdit = () => (
+  <Edit>
+    <I18nMessageForm />
+  </Edit>
+);
+
+const config = {
+  list: I18nMessageList,
+  edit: MessageEdit,
+  create: MessageCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/index.js b/edera-web/src/entities/index.js
new file mode 100644 (file)
index 0000000..0660353
--- /dev/null
@@ -0,0 +1,18 @@
+export { default as user } from './user';
+export { default as notification } from './notification';
+export { default as i18nMessage } from './i18n-message';
+export { default as device } from './device';
+export { default as auditLog } from './audit-log';
+export { default as rfidDevice } from './rfid-device';
+export { default as customer } from './customer';
+export { default as supplier } from './supplier';
+export { default as equipmentType } from './equipment-type';
+export { default as priceList } from './price-list';
+export { default as protocol } from './protocol';
+export { default as equipment } from './equipment';
+export { default as supply } from './supply';
+export { default as activity } from './activity';
+export { default as activityType } from './activity-type';
+export { default as maintenance } from './maintenance';
+export { default as rfidDeviceTrack } from './rfid-device-track';
+export { default as docxTemplate } from './docx-template';
diff --git a/edera-web/src/entities/maintenance.js b/edera-web/src/entities/maintenance.js
new file mode 100644 (file)
index 0000000..d0e6b04
--- /dev/null
@@ -0,0 +1,23 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { MaintenanceCreateForm, MaintenanceEditForm, MaintenanceList } from 'components';
+const MaintenanceCreate = () => (
+  <Create>
+    <MaintenanceCreateForm />
+  </Create>
+);
+const MaintenanceEdit = () => (
+  <Edit mutationMode="pessimistic" redirect={false}>
+    <MaintenanceEditForm />
+  </Edit>
+);
+
+const config = {
+  list: MaintenanceList,
+  edit: MaintenanceEdit,
+  create: MaintenanceCreate,
+  options: {
+    group: 'maintenance'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/notification.js b/edera-web/src/entities/notification.js
new file mode 100644 (file)
index 0000000..ebd46e2
--- /dev/null
@@ -0,0 +1,16 @@
+import { NotificationList as BaseNotificationList, List } from '@applica-software-guru/react-admin';
+
+import React from 'react';
+
+const NotificationList = () => (
+  <List>
+    <BaseNotificationList />
+  </List>
+);
+const config = {
+  list: NotificationList,
+  options: {
+    group: 'dashboard'
+  }
+};
+export default config;
diff --git a/edera-web/src/entities/price-list.js b/edera-web/src/entities/price-list.js
new file mode 100644 (file)
index 0000000..c33c6f4
--- /dev/null
@@ -0,0 +1,25 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+
+import { PriceListForm } from 'components';
+
+const PriceListCreate = () => (
+  <Create>
+    <PriceListForm />
+  </Create>
+);
+
+const PriceListEdit = () => (
+  <Edit redirect={false}>
+    <PriceListForm />
+  </Edit>
+);
+
+const config = {
+  edit: PriceListEdit,
+  create: PriceListCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/protocol.js b/edera-web/src/entities/protocol.js
new file mode 100644 (file)
index 0000000..55158e6
--- /dev/null
@@ -0,0 +1,24 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { ProtocolList, ProtocolForm, ProtocolDialogForm } from 'components';
+
+const ProtocolCreate = () => (
+  <Create>
+    <ProtocolDialogForm />
+  </Create>
+);
+const ProtocolEdit = () => (
+  <Edit>
+    <ProtocolForm />
+  </Edit>
+);
+
+const config = {
+  list: ProtocolList,
+  edit: ProtocolEdit,
+  create: ProtocolCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/rfid-device-track.js b/edera-web/src/entities/rfid-device-track.js
new file mode 100644 (file)
index 0000000..6ce5a5d
--- /dev/null
@@ -0,0 +1,10 @@
+import { RFIDDeviceTrackList } from 'components';
+
+const config = {
+  list: RFIDDeviceTrackList,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/rfid-device.js b/edera-web/src/entities/rfid-device.js
new file mode 100644 (file)
index 0000000..5abc2e7
--- /dev/null
@@ -0,0 +1,23 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { RFIDeviceList, RFIDDeviceForm } from 'components';
+const RFIDDeviceCreate = () => (
+  <Create>
+    <RFIDDeviceForm />
+  </Create>
+);
+const RFIDDeviceEdit = () => (
+  <Edit>
+    <RFIDDeviceForm />
+  </Edit>
+);
+
+const config = {
+  list: RFIDeviceList,
+  edit: RFIDDeviceEdit,
+  create: RFIDDeviceCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/supplier.js b/edera-web/src/entities/supplier.js
new file mode 100644 (file)
index 0000000..c827bd8
--- /dev/null
@@ -0,0 +1,24 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { SupplierList, SupplierForm } from 'components';
+
+const SupplierCreate = () => (
+  <Create>
+    <SupplierForm />
+  </Create>
+);
+const SupplierEdit = () => (
+  <Edit>
+    <SupplierForm />
+  </Edit>
+);
+
+const config = {
+  list: SupplierList,
+  edit: SupplierEdit,
+  create: SupplierCreate,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/supply.js b/edera-web/src/entities/supply.js
new file mode 100644 (file)
index 0000000..542de0f
--- /dev/null
@@ -0,0 +1,25 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { SupplyForm, SupplyList, SupplyShow } from 'components';
+
+const SupplyCreate = () => (
+  <Create>
+    <SupplyForm />
+  </Create>
+);
+const SupplyEdit = () => (
+  <Edit>
+    <SupplyForm />
+  </Edit>
+);
+
+const config = {
+  list: SupplyList,
+  edit: SupplyEdit,
+  create: SupplyCreate,
+  show: SupplyShow,
+  options: {
+    group: 'control-panel'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/entities/user.js b/edera-web/src/entities/user.js
new file mode 100644 (file)
index 0000000..c800e2f
--- /dev/null
@@ -0,0 +1,26 @@
+import { Create, Edit } from '@applica-software-guru/react-admin';
+import { UserForm, UserList } from 'components';
+
+import { CONFIGURED_ROLES } from 'config';
+
+const UserCreate = () => (
+  <Create>
+    <UserForm configuredRoles={CONFIGURED_ROLES} />
+  </Create>
+);
+const UserEdit = () => (
+  <Edit mutationMode="pessimistic">
+    <UserForm configuredRoles={CONFIGURED_ROLES} />
+  </Edit>
+);
+
+const config = {
+  list: <UserList configuredRoles={CONFIGURED_ROLES} />,
+  edit: UserEdit,
+  create: UserCreate,
+  options: {
+    group: 'security'
+  }
+};
+
+export default config;
diff --git a/edera-web/src/hooks/index.js b/edera-web/src/hooks/index.js
new file mode 100644 (file)
index 0000000..fddf830
--- /dev/null
@@ -0,0 +1 @@
+export { default as useRedirect } from './useRedirect';
diff --git a/edera-web/src/hooks/useRedirect.js b/edera-web/src/hooks/useRedirect.js
new file mode 100644 (file)
index 0000000..46aad5f
--- /dev/null
@@ -0,0 +1,11 @@
+const set = (path) => localStorage.setItem('redirect', path);
+const get = (remove = true) => {
+  var redirect = localStorage.getItem('redirect');
+  if (remove) {
+    localStorage.removeItem('redirect');
+  }
+  return redirect;
+};
+const useRedirect = () => ({ set, get });
+
+export default useRedirect;
diff --git a/edera-web/src/index.js b/edera-web/src/index.js
new file mode 100644 (file)
index 0000000..64cc5af
--- /dev/null
@@ -0,0 +1,10 @@
+import App from './App';
+import { createRoot } from 'react-dom/client';
+import reportWebVitals from './reportWebVitals';
+
+const container = document.getElementById('root');
+const root = createRoot(container);
+
+root.render(<App />);
+
+reportWebVitals();
diff --git a/edera-web/src/menu.js b/edera-web/src/menu.js
new file mode 100644 (file)
index 0000000..26a614e
--- /dev/null
@@ -0,0 +1,204 @@
+import {
+  DashboardOutlined,
+  EyeOutlined,
+  FlagOutlined,
+  InboxOutlined,
+  NotificationOutlined,
+  ScheduleOutlined,
+  SolutionOutlined,
+  TableOutlined,
+  ToolOutlined,
+  UserOutlined,
+  UsergroupAddOutlined,
+  WifiOutlined,
+  GlobalOutlined
+} from '@ant-design/icons';
+import RadarIcon from '@mui/icons-material/Radar';
+import EngineeringOutlinedIcon from '@mui/icons-material/EngineeringOutlined';
+const config = [
+  {
+    id: 'dashboard',
+    title: 'Dashboard',
+    type: 'group',
+    icon: DashboardOutlined,
+    children: [
+      {
+        id: 'entities/notification',
+        title: 'ra.menu.item.notification',
+        type: 'item',
+        url: '/entities/notification',
+        icon: NotificationOutlined
+      },
+      {
+        id: 'position',
+        title: 'ra.menu.item.position',
+        type: 'item',
+        url: '/position',
+        icon: RadarIcon,
+        roles: ['ROLE_ADMIN', 'ROLE_OPERATOR'],
+        resource: false
+      }
+    ]
+  },
+  {
+    id: 'maintenance',
+    title: 'ra.menu.maintenance',
+    icon: TableOutlined,
+    type: 'group',
+    children: [
+      {
+        id: 'entities/activity-type',
+        title: 'ra.menu.item.entities/activity-type',
+        type: 'item',
+        url: '/entities/activity-type',
+        icon: TableOutlined,
+        resource: true,
+        roles: ['ROLE_ADMIN', 'ROLE_OPERATOR']
+      },
+      {
+        id: 'entities/activity',
+        title: 'ra.menu.item.entities/activity',
+        type: 'item',
+        url: '/entities/activity',
+        icon: ScheduleOutlined,
+        resource: true
+      },
+      {
+        id: 'entities/maintenance',
+        title: 'ra.menu.item.entities/maintenance',
+        type: 'item',
+        url: '/entities/maintenance',
+        icon: EngineeringOutlinedIcon,
+        resource: true
+      }
+    ]
+  },
+  {
+    id: 'control-panel',
+    title: 'ra.menu.control-panel',
+    icon: TableOutlined,
+    type: 'group',
+    children: [
+      {
+        id: 'entities/customer',
+        title: 'ra.menu.item.entities/customer',
+        type: 'item',
+        url: '/entities/customer',
+        icon: UsergroupAddOutlined,
+        roles: ['ROLE_ADMIN', 'ROLE_OPERATOR']
+      },
+      {
+        id: 'entities/protocol',
+        title: 'ra.menu.item.entities/protocol',
+        type: 'item',
+        url: '/entities/protocol',
+        icon: SolutionOutlined,
+        resource: true
+      },
+      {
+        id: 'entities/supplier',
+        title: 'ra.menu.item.supplier',
+        type: 'item',
+        url: '/entities/supplier',
+        icon: UserOutlined,
+        resource: true,
+        roles: ['ROLE_ADMIN', 'ROLE_OPERATOR']
+      },
+      {
+        id: 'entities/supply',
+        title: 'ra.menu.item.entities/supply',
+        type: 'item',
+        url: '/entities/supply',
+        icon: InboxOutlined,
+        resource: true,
+        roles: ['ROLE_ADMIN', 'ROLE_OPERATOR', 'ROLE_CUSTOMER', 'ROLE_MAINTAINER']
+      },
+      {
+        id: 'entities/equipment',
+        title: 'ra.menu.item.entities/equipment',
+        type: 'item',
+        url: '/entities/equipment',
+        icon: ToolOutlined,
+        resource: true
+      },
+      {
+        id: 'entities/equipment-type',
+        title: 'ra.menu.item.entities/equipment-type',
+        type: 'item',
+        url: '/entities/equipment-type',
+        icon: TableOutlined,
+        resource: true,
+        roles: ['ROLE_ADMIN', 'ROLE_OPERATOR']
+      },
+      {
+        id: 'entities/rfid-device',
+        title: 'ra.menu.item.entities/rfid-device',
+        type: 'item',
+        url: '/entities/rfid-device',
+        icon: WifiOutlined,
+        resource: true,
+        roles: ['ROLE_ADMIN']
+      },
+      {
+        id: 'entities/docx-template',
+        title: 'ra.menu.item.entities/docx-template',
+        type: 'item',
+        url: '/entities/docx-template',
+        icon: TableOutlined,
+        resource: true,
+        roles: ['ROLE_ADMIN']
+      },
+      {
+        id: 'entities/rfid-device-track',
+        title: 'ra.menu.item.entities/rfid-device-track',
+        type: 'item',
+        url: '/entities/rfid-device-track',
+        icon: GlobalOutlined,
+        resource: true,
+        roles: ['ROLE_ADMIN']
+      },
+      {
+        id: 'entities/i18n-message',
+        title: 'ra.menu.item.entities/i18n-message',
+        type: 'item',
+        url: '/entities/i18n-message',
+        icon: FlagOutlined,
+        roles: ['ROLE_ADMIN']
+      }
+    ]
+  },
+  {
+    id: 'security',
+    title: 'ra.menu.security',
+    icon: TableOutlined,
+    type: 'group',
+    children: [
+      {
+        id: 'entities/audit-log',
+        title: 'ra.menu.item.entities/audit-log',
+        type: 'item',
+        url: '/entities/audit-log',
+        icon: EyeOutlined,
+        roles: ['ROLE_ADMIN']
+      },
+      {
+        id: 'entities/user',
+        title: 'ra.menu.item.entities/user',
+        type: 'item',
+        url: '/entities/user',
+        icon: UserOutlined,
+        roles: ['ROLE_ADMIN']
+      },
+      {
+        id: 'entities/device',
+        title: 'ra.menu.item.entities/device',
+        type: 'item',
+        url: '/entities/device',
+        icon: TableOutlined,
+        roles: ['ROLE_ADMIN']
+      }
+    ]
+  }
+];
+
+export default config;
diff --git a/edera-web/src/react-app-env.d.js b/edera-web/src/react-app-env.d.js
new file mode 100644 (file)
index 0000000..6431bc5
--- /dev/null
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/edera-web/src/reportWebVitals.js b/edera-web/src/reportWebVitals.js
new file mode 100644 (file)
index 0000000..532f29b
--- /dev/null
@@ -0,0 +1,13 @@
+const reportWebVitals = (onPerfEntry) => {
+  if (onPerfEntry && onPerfEntry instanceof Function) {
+    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+      getCLS(onPerfEntry);
+      getFID(onPerfEntry);
+      getFCP(onPerfEntry);
+      getLCP(onPerfEntry);
+      getTTFB(onPerfEntry);
+    });
+  }
+};
+
+export default reportWebVitals;
diff --git a/edera-web/src/theme.js b/edera-web/src/theme.js
new file mode 100644 (file)
index 0000000..6cb3e15
--- /dev/null
@@ -0,0 +1,3 @@
+const theme = () => ({});
+
+export default theme;