Skip to content

Commit 98525ba

Browse files
committed
add apps counter card and unassigned notification
1 parent 4b91431 commit 98525ba

6 files changed

Lines changed: 156 additions & 33 deletions

File tree

frontend/src/components/CounterCard.vue

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { NeCard, NeHeading, NeSkeleton } from '@nethesis/vue-components'
88
import { type IconDefinition } from '@fortawesome/free-solid-svg-icons'
99
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
10+
import { computed, useSlots } from 'vue'
1011
1112
const {
1213
title,
@@ -21,21 +22,34 @@ const {
2122
loading?: boolean
2223
skeletonLines?: number
2324
}>()
25+
26+
const slots = useSlots()
27+
28+
const hasDefaultSlot = computed(() => !!slots.default)
2429
</script>
2530

2631
<template>
2732
<NeCard>
2833
<NeSkeleton v-if="loading" :lines="skeletonLines" class="w-full" />
29-
<div v-else class="flex justify-between">
30-
<div class="flex items-center gap-3">
31-
<FontAwesomeIcon v-if="icon" :icon="icon" class="size-8 text-gray-600 dark:text-gray-300" />
32-
<NeHeading tag="h6" class="text-gray-600 dark:text-gray-300">
33-
{{ title }}
34-
</NeHeading>
34+
<template v-else>
35+
<div class="flex justify-between">
36+
<div class="flex items-center gap-3">
37+
<FontAwesomeIcon
38+
v-if="icon"
39+
:icon="icon"
40+
class="size-5 text-gray-600 dark:text-gray-300"
41+
/>
42+
<NeHeading tag="h6" class="text-gray-600 dark:text-gray-300">
43+
{{ title }}
44+
</NeHeading>
45+
</div>
46+
<span class="text-3xl font-medium text-gray-900 dark:text-gray-50">
47+
{{ counter }}
48+
</span>
49+
</div>
50+
<div v-if="hasDefaultSlot" class="mt-5">
51+
<slot></slot>
3552
</div>
36-
<span class="text-3xl font-medium text-gray-900 dark:text-gray-50">
37-
{{ counter }}
38-
</span>
39-
</div>
53+
</template>
4054
</NeCard>
4155
</template>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!--
2+
Copyright (C) 2026 Nethesis S.r.l.
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { useQuery } from '@pinia/colada'
8+
import { useLoginStore } from '@/stores/login'
9+
import { APPLICATIONS_TOTAL_KEY, getApplicationsTotal } from '@/lib/applications/applications'
10+
import CounterCard from '../CounterCard.vue'
11+
import { faGridOne } from '@nethesis/nethesis-solid-svg-icons'
12+
import { NeBadgeV2 } from '@nethesis/vue-components'
13+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
14+
import { faCircleInfo } from '@fortawesome/free-solid-svg-icons'
15+
import { useI18n } from 'vue-i18n'
16+
17+
const { t } = useI18n()
18+
const loginStore = useLoginStore()
19+
20+
const { state: applicationsTotal } = useQuery({
21+
key: [APPLICATIONS_TOTAL_KEY],
22+
enabled: () => !!loginStore.jwtToken,
23+
query: getApplicationsTotal,
24+
})
25+
</script>
26+
27+
<template>
28+
<CounterCard
29+
:title="$t('applications.title')"
30+
:counter="applicationsTotal.data?.total ?? 0"
31+
:icon="faGridOne"
32+
:loading="applicationsTotal.status === 'pending'"
33+
>
34+
<div class="flex justify-center">
35+
<NeBadgeV2 kind="blue">
36+
<FontAwesomeIcon :icon="faCircleInfo" class="size-4" />
37+
38+
{{ t('applications.num_unassigned', { num: applicationsTotal.data?.unassigned ?? 0 }) }}
39+
</NeBadgeV2>
40+
</div>
41+
</CounterCard>
42+
</template>

frontend/src/i18n/en/translation.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,10 @@
454454
"organization_assigned_description": "The application {application} has been assigned to organization {organization}",
455455
"cannot_assign_organization": "Cannot assign organization",
456456
"no_organization": "No organization",
457+
"num_unassigned": "{num} unassigned",
458+
"num_applications_not_assigned": "{num} application is not assigned to any organization. | {num} applications are not assigned to any organization.",
459+
"show_unassigned": "Show unassigned",
460+
"dont_show_again": "Don't show again",
457461
"notes": "Notes",
458462
"add_notes": "Add notes",
459463
"edit_notes": "Edit notes",

frontend/src/lib/applications/applications.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { useLoginStore } from '@/stores/login'
77
import * as v from 'valibot'
88
import { type Pagination } from '../common'
99
import { OrganizationSchema } from '../organizations'
10+
import { savePreference } from '@nethesis/vue-components'
1011

1112
export const APPLICATIONS_KEY = 'applications'
1213
export const APPLICATIONS_TOTAL_KEY = 'applicationsTotal'
1314
export const APPLICATIONS_TABLE_ID = 'applicationsTable'
15+
export const SHOW_UNASSIGNED_APPS_NOTIFICATION = 'showUnassignedAppsNotification'
1416

1517
export type ApplicationStatus = 'online' | 'offline' | 'unknown' | 'deleted'
1618

@@ -125,6 +127,15 @@ export const getApplicationLogo = (appId: string) => {
125127
}
126128
}
127129

130+
export const saveShowUnassignedAppsNotificationToStorage = (show: boolean) => {
131+
const loginStore = useLoginStore()
132+
const username = loginStore.userInfo?.email
133+
134+
if (username) {
135+
savePreference(SHOW_UNASSIGNED_APPS_NOTIFICATION, show, username)
136+
}
137+
}
138+
128139
export const getApplications = (
129140
pageNum: number,
130141
pageSize: number,

frontend/src/views/ApplicationsView.vue

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,84 @@
55

66
<script setup lang="ts">
77
import ApplicationsTable from '@/components/applications/ApplicationsTable.vue'
8-
import { NeHeading } from '@nethesis/vue-components'
9-
10-
//// review (search "system")
11-
12-
// const { ////
13-
// state,
14-
// debouncedTextFilter,
15-
// productFilter,
16-
// createdByFilter,
17-
// versionFilter,
18-
// statusFilter,
19-
// sortBy,
20-
// sortDescending,
21-
// } = useApplications()
22-
23-
// const applicationsPage = computed(() => {
24-
// return state.value.data?.applications || []
25-
// })
8+
import {
9+
APPLICATIONS_TOTAL_KEY,
10+
getApplicationsTotal,
11+
saveShowUnassignedAppsNotificationToStorage,
12+
SHOW_UNASSIGNED_APPS_NOTIFICATION,
13+
} from '@/lib/applications/applications'
14+
import { useApplications } from '@/queries/applications'
15+
import { useLoginStore } from '@/stores/login'
16+
import { getPreference, NeButton, NeHeading, NeInlineNotification } from '@nethesis/vue-components'
17+
import { useQuery } from '@pinia/colada'
18+
import { computed, ref } from 'vue'
19+
import { useI18n } from 'vue-i18n'
20+
21+
const { t } = useI18n()
22+
const loginStore = useLoginStore()
23+
24+
const { state: applicationsTotal } = useQuery({
25+
key: [APPLICATIONS_TOTAL_KEY],
26+
enabled: () => !!loginStore.jwtToken,
27+
query: getApplicationsTotal,
28+
})
29+
30+
const { organizationFilter } = useApplications()
31+
32+
const justHiddenUnassignedAppsNotification = ref(false)
33+
34+
const showUnassignedAppsNotification = computed(() => {
35+
const username = loginStore.userInfo?.email
36+
37+
if (!username || justHiddenUnassignedAppsNotification.value) {
38+
return false
39+
}
40+
41+
let showNotificationFromPreference = getPreference(SHOW_UNASSIGNED_APPS_NOTIFICATION, username)
42+
43+
if (showNotificationFromPreference === undefined) {
44+
// default to true if not set
45+
showNotificationFromPreference = true
46+
}
47+
48+
return applicationsTotal.value.data?.unassigned && showNotificationFromPreference
49+
})
50+
51+
const showUnassignedApps = () => {
52+
organizationFilter.value = ['no_org']
53+
}
54+
55+
const dontShowUnassignedAppsNotificationAgain = () => {
56+
saveShowUnassignedAppsNotificationToStorage(false)
57+
justHiddenUnassignedAppsNotification.value = true
58+
}
2659
</script>
2760

2861
<template>
2962
<div>
3063
<NeHeading tag="h3" class="mb-7">{{ $t('applications.title') }}</NeHeading>
31-
<div class="mb-8 flex flex-col items-start justify-between gap-6 xl:flex-row">
32-
<div class="max-w-2xl text-gray-500 dark:text-gray-400">
33-
{{ $t('applications.page_description') }}
34-
</div>
64+
<div class="mb-8 max-w-2xl text-gray-500 dark:text-gray-400">
65+
{{ $t('applications.page_description') }}
3566
</div>
36-
<!-- //// todo applications without org notification -->
67+
<NeInlineNotification
68+
v-if="showUnassignedAppsNotification"
69+
kind="info"
70+
:description="
71+
$t('applications.num_applications_not_assigned', {
72+
num: applicationsTotal.data?.unassigned,
73+
})
74+
"
75+
:primary-button-label="t('applications.show_unassigned')"
76+
:secondary-button-label="t('applications.dont_show_again')"
77+
class="mb-8"
78+
@primary-click="showUnassignedApps"
79+
@secondary-click="dontShowUnassignedAppsNotificationAgain"
80+
/>
81+
<!-- //// -->
82+
<!-- showUnassignedAppsNotification {{ showUnassignedAppsNotification }}
83+
<NeButton @click="saveShowUnassignedAppsNotificationToStorage(true)"
84+
>Show Unassigned Apps</NeButton
85+
> -->
3786
<ApplicationsTable />
3887
</div>
3988
</template>

frontend/src/views/DashboardView.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
-->
55

66
<script setup lang="ts">
7+
import ApplicationsCounterCard from '@/components/dashboard/ApplicationsCounterCard.vue'
78
import CustomersCounterCard from '@/components/dashboard/CustomersCounterCard.vue'
89
import DistributorsCounterCard from '@/components/dashboard/DistributorsCounterCard.vue'
910
import ResellersCounterCard from '@/components/dashboard/ResellersCounterCard.vue'
1011
import SystemsCounterCard from '@/components/dashboard/SystemsCounterCard.vue'
1112
import UsersCounterCard from '@/components/dashboard/UsersCounterCard.vue'
1213
import {
14+
canReadApplications,
1315
canReadCustomers,
1416
canReadDistributors,
1517
canReadResellers,
@@ -84,6 +86,7 @@ const { state: thirdPartyApps } = useQuery({
8486
<CustomersCounterCard v-if="canReadCustomers()" />
8587
<UsersCounterCard v-if="canReadUsers()" />
8688
<SystemsCounterCard v-if="canReadSystems()" />
89+
<ApplicationsCounterCard v-if="canReadApplications()" />
8790
</template>
8891
</div>
8992
<div class="mt-6 grid grid-cols-1 gap-x-6 gap-y-6 sm:grid-cols-2 2xl:grid-cols-4">

0 commit comments

Comments
 (0)