@@ -182,9 +185,11 @@
+ :rules="!unrestricted ? [validationRules.required('{{ _('Access Group(s) are required unless unrestricted.')}}')] : []"
+ clearable
+ >
+
{{ _('Restrict Incident to Access Group(s)') }}
+
diff --git a/enferno/admin/templates/admin/partials/location_dialog.html b/enferno/admin/templates/admin/partials/location_dialog.html
index f2ac952d8..d3c378492 100644
--- a/enferno/admin/templates/admin/partials/location_dialog.html
+++ b/enferno/admin/templates/admin/partials/location_dialog.html
@@ -14,7 +14,6 @@
@@ -35,14 +34,15 @@
+ >
+ {{ _('Title') }}
+ {{ _('Title (AR)') }}
+
@@ -64,7 +64,8 @@
item-value="id"
:rules="[validationRules.required()]"
:multiple="false"
- label="{{ _('Location Type') }}">
+ >
+ {{ _('Location Type') }}
@@ -81,7 +82,8 @@
@update:model-value="restrictSearch"
:multiple="false"
:rules="editedItem.location_type?.title === 'Administrative Location' ? [validationRules.required()] : []"
- label="{{ _('Admin Level') }}">
+ >
+ {{ _('Admin Level') }}
@@ -131,7 +133,8 @@
diff --git a/enferno/admin/templates/admin/roles.html b/enferno/admin/templates/admin/roles.html
index b1a9cf095..05868976f 100644
--- a/enferno/admin/templates/admin/roles.html
+++ b/enferno/admin/templates/admin/roles.html
@@ -39,42 +39,30 @@
${ formTitle }
+
+ :rules="[validationRules.required()]"
+ >
+ {{ _('Group Name') }}
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {{ _('Group Color') }}
+
@@ -100,13 +88,13 @@
>
{{ _('Save') }}
+
@@ -166,8 +154,6 @@
-
-
@@ -191,6 +177,7 @@
{% endblock %} {% block js %}
+
{% endblock %}
\ No newline at end of file
diff --git a/enferno/admin/templates/admin/sources.html b/enferno/admin/templates/admin/sources.html
index 1bd3e6e2f..1b6b6e37b 100644
--- a/enferno/admin/templates/admin/sources.html
+++ b/enferno/admin/templates/admin/sources.html
@@ -47,71 +47,73 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ _('Cancel') }}
-
- {{ _('Save') }}
-
-
+
+
+
+
+
+
+ {{ _('Title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ _('Cancel') }}
+
+ {{ _('Save') }}
+
+
+
@@ -193,6 +195,7 @@
{% endblock %} {% block js %}
+
diff --git a/enferno/static/js/common/config.js b/enferno/static/js/common/config.js
index 9362b3503..13b382483 100644
--- a/enferno/static/js/common/config.js
+++ b/enferno/static/js/common/config.js
@@ -100,6 +100,13 @@ const validationRules = {
}, 350);
});
};
+ },
+ hexColor() {
+ return (value) => {
+ if (!value) return true; // Optional field
+ const hexPattern = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
+ return hexPattern.test(value) || 'Invalid hex color format (e.g., #F53, #FF5733, or #FF5733FF)';
+ };
}
};
@@ -157,6 +164,9 @@ const vuetifyConfig = {
VApp: {
class: 'bg-background',
},
+ VColorInput: {
+ variant: 'outlined',
+ },
VTextField: {
variant: 'outlined',
},
diff --git a/enferno/static/js/components/Asterisk.js b/enferno/static/js/components/Asterisk.js
new file mode 100644
index 000000000..ed7eb17aa
--- /dev/null
+++ b/enferno/static/js/components/Asterisk.js
@@ -0,0 +1,3 @@
+const Asterisk = Vue.defineComponent({
+ template: `
*`,
+});
\ No newline at end of file
diff --git a/enferno/static/js/components/DualField.js b/enferno/static/js/components/DualField.js
index 2291ed390..00d657bbb 100644
--- a/enferno/static/js/components/DualField.js
+++ b/enferno/static/js/components/DualField.js
@@ -79,7 +79,6 @@ const DualField = Vue.defineComponent({
+
+
+ {{ labelOriginal }}
+
@@ -100,7 +103,6 @@ const DualField = Vue.defineComponent({
+ >
+
+
+ {{ labelTranslation }}
+
+
`,
-});
+});
\ No newline at end of file
diff --git a/enferno/static/js/components/EditableTable.js b/enferno/static/js/components/EditableTable.js
index 087da935f..3f50da308 100644
--- a/enferno/static/js/components/EditableTable.js
+++ b/enferno/static/js/components/EditableTable.js
@@ -49,6 +49,10 @@ const EditableTable = Vue.defineComponent({
type: Array,
default: () => ['title'],
},
+ requiredFields: {
+ type: Array,
+ default: () => [],
+ },
columnsList: {
type: Array,
default: () => ['code', 'title', 'title_tr', 'reverse_title', 'reverse_title_tr'],
@@ -80,6 +84,7 @@ const EditableTable = Vue.defineComponent({
+
@@ -87,10 +92,14 @@ const EditableTable = Vue.defineComponent({
+ :rules="isRequired(column) ? [validationRules.required()] : []"
+ >
+
+ {{ getHeaderTextById(column) }}
+
+
@@ -105,11 +114,12 @@ const EditableTable = Vue.defineComponent({
Save
+
@@ -119,6 +129,7 @@ const EditableTable = Vue.defineComponent({
itemList: [],
editableItem: {},
translations: window.translations,
+ validationRules: validationRules,
isLoadingList: false,
dialogState: {
isLoading: false,
@@ -168,27 +179,39 @@ const EditableTable = Vue.defineComponent({
},
itemSave() {
- this.dialogState.isLoading = true;
- const endpoint = this.dialogState.item?.id ? `${this.saveEndpoint}/${this.dialogState.item.id}` : this.saveEndpoint;
- const method = this.dialogState.item?.id ? 'put' : 'post';
+ this.$refs.form.validate().then(({ valid }) => {
+ if (!valid) {
+ this.$root.showSnack(this.translations.pleaseReviewFormForErrors_);
+ return;
+ }
- // fix for location admin levels
- if(this.itemHeaders.find(header => header.value === 'code') && !this.dialogState.item?.id){
- const maxCode = this.itemList.reduce((acc, item) => acc > item.code ? acc : item.code, 0);
- this.dialogState.item.code = Number(maxCode) + 1;
- }
+ this.dialogState.isLoading = true;
+ const endpoint = this.dialogState.item?.id ? `${this.saveEndpoint}/${this.dialogState.item.id}` : this.saveEndpoint;
+ const method = this.dialogState.item?.id ? 'put' : 'post';
- axios[method](endpoint, { item: this.dialogState.item })
- .then((res) => {
- this.loadItems();
- this.$root.showSnack(res.data);
- this.$emit('items-updated', this.itemList);
- this.dialogState.isOpen = false;
- })
- .finally(() => {
- this.dialogState.item = {};
- this.dialogState.isLoading = false;
- });
+ // fix for location admin levels
+ if(this.itemHeaders.find(header => header.value === 'code') && !this.dialogState.item?.id){
+ const maxCode = this.itemList.reduce((acc, item) => acc > item.code ? acc : item.code, 0);
+ this.dialogState.item.code = Number(maxCode) + 1;
+ }
+
+ axios[method](endpoint, { item: this.dialogState.item })
+ .then((res) => {
+ this.loadItems();
+ this.$root.showSnack(res.data);
+ this.$emit('items-updated', this.itemList);
+ this.dialogState.isOpen = false;
+ // Only clear on success
+ this.dialogState.item = {};
+ })
+ .catch((err) => {
+ // Show error but keep dialog open and data intact
+ this.$root.showSnack(err.response?.data?.message || this.translations.errorOccurred_ || 'An error occurred');
+ })
+ .finally(() => {
+ this.dialogState.isLoading = false;
+ });
+ });
},
itemCancel() {
@@ -227,5 +250,8 @@ const EditableTable = Vue.defineComponent({
isEditable(item) {
return !this.noEditActionIds.includes(item?.id);
},
+ isRequired(column) {
+ return this.requiredFields.includes(column);
+ },
},
-});
+});
\ No newline at end of file
diff --git a/enferno/static/js/components/EventsSection.js b/enferno/static/js/components/EventsSection.js
index 5ab1865ba..8f52b641e 100644
--- a/enferno/static/js/components/EventsSection.js
+++ b/enferno/static/js/components/EventsSection.js
@@ -36,29 +36,69 @@ const EventsSection = Vue.defineComponent({
}
return () => true;
},
+ hasDateOrLocation() {
+ const e = this.editedEvent;
+ return !!(e.location || e.from_date || e.to_date);
+ },
+ hasTitleOrType() {
+ const e = this.editedEvent;
+ return !!(e.title || e.title_ar || e.eventtype);
+ },
+ // Show asterisk only if the group is not yet satisfied
+ showTitleTypeAsterisk() {
+ return !this.hasTitleOrType;
+ },
+ showLocationDateAsterisk() {
+ return !this.hasDateOrLocation;
+ },
+ // Validation rules for groups
+ titleOrTypeRule() {
+ return () => this.hasTitleOrType || this.translations.titleOrTypeRequired_;
+ },
+ locationOrDateRule() {
+ return () => this.hasDateOrLocation || this.translations.locationOrDateRequired_;
+ },
+ },
+ watch: {
+ // Watch for changes in title/type group
+ 'editedEvent.title'() {
+ this.validateTitleTypeGroup();
+ },
+ 'editedEvent.title_ar'() {
+ this.validateTitleTypeGroup();
+ },
+ 'editedEvent.eventtype'() {
+ this.validateTitleTypeGroup();
+ },
+ // Watch for changes in location/date group
+ 'editedEvent.location'() {
+ this.validateLocationDateGroup();
+ },
+ 'editedEvent.from_date'() {
+ this.validateLocationDateGroup();
+ },
+ 'editedEvent.to_date'() {
+ this.validateLocationDateGroup();
+ },
},
methods: {
- checkEventFormRules() {
- const e = this.editedEvent;
-
- const hasDateOrLocation = !!(e.location || e.from_date || e.to_date);
- const hasTitleOrType = !!(e.title || e.title_ar || e.eventtype);
-
- const missing = [];
- if (!hasDateOrLocation) missing.push(this.translations.locationOrDateRequired_);
- if (!hasTitleOrType) missing.push(this.translations.titleOrTypeRequired_);
-
- if (missing.length > 0) {
- this.$root.showSnack(missing.join('