Skip to content

Commit 1227c58

Browse files
Add filters
1 parent 42f4739 commit 1227c58

File tree

7 files changed

+388
-9
lines changed

7 files changed

+388
-9
lines changed

app/nuxt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default defineNuxtConfig({
2626
global: true,
2727
dirs: [{ path: '~/components', prefix: 'tk' }],
2828
},
29-
modules: ['nuxt-api-party', '@sidebase/nuxt-auth', 'nuxt-quasar-ui', '@vueuse/nuxt', ...extensions.appSetup.default()],
29+
modules: ['nuxt-api-party', '@sidebase/nuxt-auth', 'nuxt-quasar-ui', '@vueuse/nuxt', 'dayjs-nuxt', ...extensions.appSetup.default()],
3030
auth: {
3131
baseURL: `${TK_APP_API_URL}/core/auth`,
3232
provider: {

app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@auth/core": "^0.12.0",
1313
"@libertech-fr/teaket_common": "*",
1414
"@quasar/extras": "^1.16.6",
15+
"@vueuse/router": "^10.4.1",
1516
"cookie": "^0.5.0",
1617
"quasar": "^2.12.5"
1718
},
@@ -21,6 +22,7 @@
2122
"@types/node": "^18.17.3",
2223
"@vueuse/core": "^10.4.1",
2324
"@vueuse/nuxt": "^10.4.1",
25+
"dayjs-nuxt": "^1.1.2",
2426
"nuxt": "^3.6.5",
2527
"nuxt-api-party": "^0.17.0",
2628
"nuxt-quasar-ui": "^2.0.5",
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<template lang="pug">
2+
.q-pa-sm(@keydown.enter="addFilter")
3+
.row.q-gutter-sm.items-center
4+
.col-1
5+
q-toggle(v-model="inclus" :label="inclus ? 'Inclus' : 'Exclus'")
6+
.col-1
7+
q-select(:options="fieldTypes" label="Type de champ" v-model="fieldType")
8+
.col-2
9+
q-select(:options="fields" label="Champs" v-model="field")
10+
.col-1
11+
q-select(:options="comparatorTypes" label="Comparateurs" v-model="comparator")
12+
template(v-slot:selected-item="scope")
13+
q-icon(:name="scope.opt.icon" size="xs")
14+
15+
template(v-slot:option="scope")
16+
q-item(v-bind="scope.itemProps")
17+
q-item-section(avatar)
18+
q-icon(:name="scope.opt.icon")
19+
q-item-section
20+
q-item-label
21+
span {{ scope.opt.label }}
22+
23+
.col-2
24+
q-input(v-model="search" label="Rechercher")
25+
.col-2
26+
q-btn(flat icon="mdi-plus" color="primary" @click="addFilter")
27+
q-tooltip.text-body2(transition-show="scale" transition-hide="scale") Ajouter
28+
29+
//Filters chips
30+
.row.q-gutter-sm.items-center.q-mt-sm
31+
q-chip(
32+
v-for="filter in filterArray" :key="filter.field"
33+
removable @remove="removeFilter(filter)"
34+
) {{ filter.label }} {{ filter.comparator }} {{ filter.search }}
35+
</template>
36+
37+
<script lang="ts" setup>
38+
import { ref } from 'vue'
39+
import type { PropType } from 'vue'
40+
import { useRouter, useRoute } from 'nuxt/app'
41+
42+
type Filter = {
43+
field: string
44+
comparator: string
45+
search: string
46+
}
47+
48+
type Field = {
49+
name: string
50+
label: string
51+
}
52+
53+
type Comparator = {
54+
label: string
55+
value: string
56+
icon: string
57+
}
58+
59+
const props = defineProps({
60+
fields: {
61+
type: Array as PropType<Field[]>,
62+
required: true
63+
}
64+
})
65+
66+
const router = useRouter()
67+
const route = useRoute()
68+
69+
const inclus = ref(true)
70+
const fieldType = ref('')
71+
let field = ref<Field>()
72+
let comparator = ref<Comparator>()
73+
const search = ref('')
74+
75+
const filters = ref<Filter[]>([])
76+
77+
const fieldTypes = ref([
78+
{ label: 'Texte', value: 'text' },
79+
{ label: 'Nombre', value: 'number' },
80+
{ label: 'Date', value: 'date' },
81+
{ label: 'Heure', value: 'time' },
82+
{ label: 'Date et heure', value: 'datetime' },
83+
{ label: 'Liste', value: 'list' },
84+
{ label: 'Liste de valeurs', value: 'listOfValues' },
85+
{ label: 'Liste de valeurs multiples', value: 'listOfValuesMultiple' },
86+
])
87+
88+
const comparatorTypes = ref<Comparator[]>([
89+
{ label: 'Egal', value: '=', icon: 'mdi-equal' },
90+
{ label: 'Différent', value: '!=', icon: 'mdi-exclamation' },
91+
{ label: 'Est supérieur à', value: '>', icon: 'mdi-greater-than' },
92+
{ label: 'Est supérieur ou égal à', value: '>=', icon: 'mdi-greater-than-or-equal' },
93+
{ label: 'Est inférieur à', value: '<', icon: 'mdi-less-than' },
94+
{ label: 'Est inférieur ou égal à', value: '<=', icon: 'mdi-less-than-or-equal' },
95+
{ label: 'Est entre', value: 'between', icon: 'mdi-arrow-expand-horizontal' },
96+
])
97+
98+
const addFilter = () => {
99+
if (
100+
!field.value ||
101+
!comparator.value ||
102+
!search.value
103+
) return
104+
105+
const object = {
106+
field: field.value.name,
107+
comparator: comparator.value.value,
108+
search: search.value
109+
}
110+
const key = `filter[${object.comparator}${object.field}]`
111+
const value = object.search
112+
const query = {
113+
...route.query,
114+
}
115+
query[key] = value
116+
router.push({
117+
query
118+
})
119+
}
120+
121+
const removeFilter = (filter: Filter) => {
122+
const key = `filter[${filter.comparator}${filter.field}]`
123+
const query = {
124+
...route.query,
125+
}
126+
delete query[key]
127+
router.push({
128+
query
129+
})
130+
}
131+
132+
const getLabelByName = (name: string) => {
133+
const field = props.fields.find(field => field.name === name)
134+
if (!field) return
135+
return field.label
136+
}
137+
138+
139+
const exctractComparator = (key: string): {
140+
comparator: string
141+
field: string
142+
} | null => {
143+
const match = key.match(/^(\=|\?|\#|\!|\>|\<|\^|\@)+/)
144+
if (!match) return
145+
const comparator = match[0]
146+
const field = key.replace(comparator, '')
147+
return {
148+
comparator,
149+
field
150+
}
151+
}
152+
153+
const filterArray = computed(() => {
154+
const queries = { ...route.query }
155+
const filters: Record<string, string> = {};
156+
157+
// Iterate through the keys and values in the input object
158+
for (const key in queries) {
159+
if (queries.hasOwnProperty(key) && key.includes("filter")) {
160+
161+
// Extract the key without the "filter[" and "]" parts
162+
const filteredKey = key.replace("filter[", "").replace("]", "");
163+
const exctract = exctractComparator(filteredKey)
164+
if (!exctract) continue
165+
const { comparator, field } = exctract
166+
// Assign the value to the extracted key in the filter array
167+
filters[field] = {
168+
label: getLabelByName(field),
169+
field,
170+
comparator,
171+
search: queries[key]
172+
};
173+
}
174+
}
175+
return filters
176+
})
177+
</script>

app/src/components/appbar/Searchfield.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template lang="pug">
2-
q-input(v-model="search" label="Rechercher" standout ).q-my-xs
2+
q-input(v-model="search" label="Rechercher" standout dark).q-my-xs
33
template(v-slot:append)
44
q-icon( v-if="search === ''" name="mdi-magnify")
55
q-icon( v-else name="mdi-close" @click="search = ''")

app/src/composables/useApiFetch.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AllPaths, HttpMethod, IgnoreCase, OpenApiError, OpenApiResponse } from 'nuxt-api-party/dist/runtime/types'
22
import { MaybeRefOrGetter } from 'nuxt-api-party/dist/runtime/utils'
3-
import { paths as Paths } from '#nuxt-api-party/api'
3+
import { paths as Paths, components as Components } from '#nuxt-api-party/api'
44
import { UseOpenApiDataOptions } from 'nuxt-api-party/dist/runtime/composables/useApiData'
55
import { AsyncData } from 'nuxt/app'
66
import { FetchError } from 'ofetch'
@@ -20,6 +20,8 @@ import { FetchError } from 'ofetch'
2020

2121
// export const useApiFetch = useApiData
2222

23+
export type schemas = Components['schemas']
24+
2325
export function useApiFetch<P extends AllPaths<Paths>, M extends IgnoreCase<keyof Paths[`/${P}`] & HttpMethod>>(
2426
path: MaybeRefOrGetter<P>,
2527
opts?: Omit<UseOpenApiDataOptions<Paths[`/${P}`], M>, 'method'> & {
@@ -53,3 +55,4 @@ AsyncData<OpenApiResponse<Paths[`/${P}`][Lowercase<M>]> | undefined, FetchError<
5355
} as any)
5456
}
5557

58+
export type Schemas = components['schemas']

app/src/pages/tickets.vue

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,131 @@
11

22
<template lang="pug">
33
div
4-
q-btn(@click="refresh") refresh
5-
span Tickets
6-
div {{ data?.data }}
7-
div {{ pending }}
8-
div.text-error {{ error }}
4+
.q-pa-md.q-d-flex
5+
tk-SearchFilters(:fields="fieldsList")
6+
.q-pa-md
7+
q-table(:rows="data?.data" :rows-per-page-options="[5, 10, 15]" :loading="pending" :columns="columns" row-key="id" :visible-columns="visibleColumns")
8+
template(v-slot:top)
9+
span Tickets
10+
q-space
11+
q-btn(flat fab icon="mdi-eye" color="primary")
12+
q-tooltip.text-body2(transition-show="scale" transition-hide="scale") Afficher/cacher
13+
q-menu(max-width="350px" max-height="350px").q-pa-md
14+
.row
15+
.col-6(v-for="column in columns" :key="column.value")
16+
q-toggle(v-model="visibleColumns" :label="column.label" :val="column.name")
17+
q-btn(flat fab icon="mdi-refresh" @click="refresh" color="primary")
18+
q-tooltip.text-body2(transition-show="scale" transition-hide="scale") Rafraichir
19+
q-btn(icon="mdi-plus" color="primary" @click="$router.push('/tickets/create')") Créer
20+
21+
922

1023
</template>
1124

1225
<script lang="ts" setup>
26+
import { ref } from "vue";
1327
import { useApiFetch } from "../composables/useApiFetch";
28+
import { computed, useDayjs } from "#imports";
29+
import type { schemas } from "../composables/useApiFetch";
30+
31+
type Ticket = schemas['TicketDto']
32+
33+
const daysjs = useDayjs()
34+
const columns = ref([
35+
{
36+
name: 'sequence',
37+
label: 'ID',
38+
field: 'sequence',
39+
align: 'left',
40+
sortable: true
41+
},
42+
{
43+
name: 'envelope.senders.name',
44+
label: 'Appelant',
45+
field: (row: Ticket) => row.envelope.senders[0].name,
46+
align: 'left',
47+
sortable: true
48+
},
49+
{
50+
name: 'envelope.observers.name',
51+
label: 'Concerné',
52+
field: (row: Ticket) => row.envelope.observers[0].name,
53+
align: 'left',
54+
sortable: true
55+
},
56+
{
57+
name: 'envelope.assigned.name',
58+
label: 'Assigné',
59+
field: (row: Ticket) => row.envelope.assigned[0].name,
60+
align: 'left',
61+
sortable: true
62+
},
63+
{
64+
name: 'subject',
65+
label: 'Sujet',
66+
field: 'subject',
67+
align: 'left',
68+
sortable: true
69+
},
70+
{
71+
name: 'updatedAt',
72+
label: 'Date de modification',
73+
field: (row: any) => row.metadata.lastUpdatedAt,
74+
format: (val: any) => daysjs(val).format('DD/MM/YYYY HH:mm'),
75+
align: 'left',
76+
sortable: true,
77+
},
78+
{
79+
name: 'createdAt',
80+
label: 'Date de création',
81+
field: (row: any) => row.metadata.createdAt,
82+
format: (val: any) => daysjs(val).format('DD/MM/YYYY HH:mm'),
83+
align: 'left',
84+
sortable: true
85+
},
86+
// {
87+
// name: 'actions',
88+
// label: 'Date de modification',
89+
// field: 'actions',
90+
// align: 'left',
91+
// sortable: true
92+
// },
93+
// {
94+
// name: 'actions',
95+
// label: 'Etat de vie',
96+
// field: 'actions',
97+
// align: 'left',
98+
// sortable: true
99+
// },
100+
// {
101+
// name: 'actions',
102+
// label: 'Statut',
103+
// field: 'actions',
104+
// align: 'left',
105+
// sortable: true
106+
// },
107+
])
108+
109+
const fieldsList = computed(() => {
110+
// return columns.value.filter((column: any) => {
111+
// return visibleColumns.value.includes(column.name)
112+
// })
113+
114+
return columns.value.reduce(
115+
(acc: any, column: any) => {
116+
if (visibleColumns.value.includes(column.name)) {
117+
acc.push({
118+
name: column.name,
119+
label: column.label,
120+
})
121+
}
122+
return acc
123+
},
124+
[]
125+
)
126+
})
14127
128+
const visibleColumns = ref(['sequence', 'senders', 'observers', 'assigned', 'subject', 'updatedAt', 'createdAt'])
15129
const { data, pending, error, refresh } = useApiFetch('tickets/ticket')
16130
17131
</script>

0 commit comments

Comments
 (0)