diff --git a/src/composables/composable.alerts.js b/src/composables/composable.alerts.js
new file mode 100644
index 00000000..ba415c18
--- /dev/null
+++ b/src/composables/composable.alerts.js
@@ -0,0 +1,108 @@
+import _ from 'lodash'
+import moment from 'moment'
+import i18next from 'i18next'
+import { ref, computed, watch, onBeforeMount, onBeforeUnmount } from 'vue'
+import { utils as kCoreUtils } from '@kalisio/kdk/core.client'
+
+export function useAlerts(options) {
+ // Functions
+ function formatAlertDateTime (date) {
+ return date.toLocaleString(kCoreUtils.getLocale(),
+ { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })
+ }
+ function getAlertDetailsAsHtml (alert) {
+ const isActive = _.get(alert, 'status.active')
+ const hasError = _.get(alert, 'status.error')
+ const checkedAt = new Date(_.get(alert, 'status.checkedAt'))
+ const triggeredAt = new Date(_.get(alert, 'status.triggeredAt'))
+ let html = getAlertLocationName(alert)
+ if (html) html += ''
+ _.forOwn(alert.conditions, (value, key) => {
+ // Get corresponding variable
+ const variable = _.find(_.get(alert, 'layer.variables'), { name: key })
+ const label = i18n.t(variable.label) || variable.label
+ const unit = variable.units[0]
+ if (_.has(value, '$gte')) {
+ html += isActive
+ ? `${label} ` + i18n.t('CatalogActivity.ALERT_GTE') + ` ${value.$gte} ${unit}`
+ : `${label} ` + i18n.t('CatalogActivity.ALERT_LTE') + ` ${value.$gte} ${unit}`
+ }
+ if (_.has(value, '$lte')) {
+ html += isActive
+ ? `${label} ` + i18n.t('CatalogActivity.ALERT_LTE') + ` ${value.$lte} ${unit}`
+ : `${label} ` + i18n.t('CatalogActivity.ALERT_GTE') + ` ${value.$lte} ${unit}`
+ }
+ })
+ html += i18n.t('CatalogActivity.ALERT_CHECKED_AT') + ` ${formatAlertDateTime(checkedAt)}`
+ if (isActive) {
+ // Order triggers by time to get nearest one easily, take care to unify weather/measure triggers
+ const triggers = _.sortBy(_.get(alert, 'status.triggers',
+ [trigger => moment.utc(trigger.time || trigger.forecastTime).valueOf()]))
+ const lastTrigger = _.last(triggers)
+ const firstTrigger = _.head(triggers)
+ // Check for forecast (future) or measure (past)
+ const lastTriggerAt = moment.utc(lastTrigger.time || lastTrigger.forecastTime)
+ const firstTriggerAt = moment.utc(firstTrigger.time || firstTrigger.forecastTime)
+ const now = moment.utc()
+ const nearestTrigger = (Math.abs(now.diff(firstTriggerAt)) < Math.abs(now.diff(lastTriggerAt)) ? firstTrigger : lastTrigger)
+ const nearestTriggerAt = new Date(nearestTrigger.time || nearestTrigger.forecastTime)
+ html += (nearestTrigger === lastTrigger ? i18n.t('CatalogActivity.ALERT_LAST') : i18n.t('CatalogActivity.ALERT_FIRST'))
+ html += i18n.t('CatalogActivity.ALERT_THRESHOLD_AT') + ` ${formatAlertDateTime(nearestTriggerAt)}`
+ html += i18n.t('CatalogActivity.ALERT_TRIGGERED_AT') + ` ${formatAlertDateTime(triggeredAt)}`
+ }
+ if (hasError) {
+ html += '' +
+ i18n.t('errors.' + _.get(alert, 'status.error.data.translation.key')) + ''
+ }
+ return html
+ }
+ function getAlertStatusAsHtml (alert) {
+ const isActive = _.get(alert, 'status.active')
+ const hasError = _.get(alert, 'status.error')
+ let html = ''
+ if (isActive) html += '' + i18n.t('CatalogActivity.ALERT_ACTIVE') + ''
+ else html += '' + i18n.t('CatalogActivity.ALERT_INACTIVE') + ''
+ // Layer name can be a translation key
+ html += (i18n.t(`${alert.layer.name}`) ? i18n.t(`${alert.layer.name}`) : `${alert.layer.name}`)
+ if (_.has(alert, 'feature')) {
+ // Try with default feature labels
+ let featureLabel = _.get(alert, 'properties.name', _.get(alert, 'properties.NAME'))
+ // Override if provided by layer
+ if (_.has(alert, 'layer.featureLabel')) featureLabel = _.get(alert, 'properties.' + _.get(alert, 'layer.featureLabel'))
+ if (featureLabel) html += ` - ${featureLabel}`
+ }
+ if (hasError) {
+ html += '' +
+ i18n.t('errors.' + _.get(alert, 'status.error.data.translation.key')) + ''
+ }
+ return html
+ }
+ function getAlertLocationName (alert) {
+ let name = _.get(alert, 'layer.name', '')
+ if (_.has(alert, 'feature')) {
+ // Try with default feature labels
+ name = _.get(alert, 'properties.name', _.get(alert, 'properties.NAME'))
+ // Override if provided by layer
+ if (_.has(alert, 'layer.featureLabel')) name = _.get(alert, 'properties.' + _.get(alert, 'layer.featureLabel'))
+ }
+ // Can be a translation key
+ return (i18n.t(name) ? i18n.t(name) : name)
+ }
+ function loadAlertLayer (alert) {
+ if (!_.has(alert, 'layer._id')) return null
+ let i18n = _.get(alert, 'layer.i18n')
+ // Process i18n
+ if (i18n) {
+ const locale = kCoreUtils.getAppLocale()
+ i18n = _.get(i18n, locale)
+ if (i18n) i18next.addResourceBundle(locale, 'kdk', i18n, true, true)
+ }
+ }
+
+ return {
+ getAlertDetailsAsHtml,
+ getAlertStatusAsHtml,
+ loadAlertLayer
+ }
+}
+
diff --git a/src/composables/composable.plan.js b/src/composables/composable.plan.js
new file mode 100644
index 00000000..ad533d25
--- /dev/null
+++ b/src/composables/composable.plan.js
@@ -0,0 +1,106 @@
+import _ from 'lodash'
+import { ref, computed, watch, onBeforeMount, onBeforeUnmount } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { api } from '@kalisio/kdk/core.client'
+
+export function usePlan(options) {
+
+ // Data
+ const route = useRoute()
+ const planId = ref(null)
+ const plan = ref(null)
+ const objectiveFilters = ref([])
+ let planService = ''
+
+ // Functions
+ function hasPlan () {
+ return planId.value
+ }
+ function hasPlanLocation () {
+ return _.has(plan.value, 'location.latitude') && _.has(plan.value, 'location.longitude')
+ }
+ function hasPlanObjectives () {
+ return (_.get(plan.value, 'objectives', []).length > 0)
+ }
+ async function loadPlan (query = {}) {
+ if (!planId.value) {
+ plan.value = null
+ } else {
+ plan.value = await api.getService(planService, options.contextId).get(planId.value, { query })
+ }
+ }
+ function onPlanUpdated (updatedPlan) {
+ if (plan.value && (updatedPlan._id === plan.value._id)) {
+ plan.value = updatedPlan
+ }
+ }
+ function onPlanRemoved (removedPlan) {
+ if (plan.value && (removedPlan._id === plan.value._id)) {
+ plan.value = null
+ planId.value = null
+ }
+ }
+ function getPlanQuery () {
+ const query = {}
+ if (!_.isEmpty(planId.value)) {
+ Object.assign(query, {
+ plan: planId.value
+ })
+ }
+ return query
+ }
+ function refreshPlanId () {
+ const id = _.get(route, 'query.plan', null)
+ if (planId.value !== id) planId.value = id
+ }
+ function getPlanObjectiveQuery () {
+ if (_.isEmpty(objectiveFilters.value)) return {}
+ else return { objective: { $in: objectiveFilters.value } }
+ }
+ async function countEvents (query = {}) {
+ const eventsService = api.getService('archived-events', options.contextId)
+ const response = await eventsService.find({ query: Object.assign(query, getPlanQuery()), $limit: 0 })
+ return response.total
+ }
+ async function countClosedEvents (query = {}) {
+ const eventsService = api.getService('archived-events', options.contextId)
+ const response = await eventsService.find({ query: Object.assign(query, { deletedAt: { $exists: true } }, getPlanQuery()), $limit: 0 })
+ return response.total
+ }
+
+ // Lifecycle hooks
+ watch(() => route.to, refreshPlanId)
+
+ onBeforeMount(() => {
+ // Jump to archive whenever required
+ planService = (_.get(route, 'name').includes('archived') ? 'archived-plans' : 'plans')
+ refreshPlanId()
+ const plansService = api.getService(planService, options.contextId)
+ // Keep track of changes once plan is loaded
+ plansService.on('patched', onPlanUpdated)
+ plansService.on('updated', onPlanUpdated)
+ plansService.on('removed', onPlanRemoved)
+ })
+
+ // Cleanup for appendItems
+ onBeforeUnmount(() => {
+ const plansService = api.getService(planService, options.contextId)
+ plansService.off('patched', onPlanUpdated)
+ plansService.off('updated', onPlanUpdated)
+ plansService.off('removed', onPlanRemoved)
+ })
+
+ return {
+ plan,
+ planId,
+ objectiveFilters,
+ hasPlan,
+ hasPlanLocation,
+ hasPlanObjectives,
+ loadPlan,
+ getPlanQuery,
+ getPlanObjectiveQuery,
+ countEvents,
+ countClosedEvents
+ }
+}
diff --git a/src/composables/index.js b/src/composables/index.js
new file mode 100644
index 00000000..86b0dc92
--- /dev/null
+++ b/src/composables/index.js
@@ -0,0 +1,2 @@
+export * from './composable.plan.js'
+export * from './composable.alerts.js'