@iservice365/layer-common 1.0.10 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/components/Avatar/Main.vue +68 -0
- package/components/Chat/Information.vue +252 -68
- package/components/Chat/Message.vue +10 -1
- package/components/FeedbackDetail.vue +30 -11
- package/components/FeedbackMain.vue +20 -3
- package/components/Input/DateTimePicker.vue +69 -14
- package/components/Input/File.vue +29 -4
- package/components/Input/FileV2.vue +121 -0
- package/components/InvitationForm.vue +5 -5
- package/components/Layout/Header.vue +22 -66
- package/components/Layout/NavigationDrawer.vue +1 -0
- package/components/NavigationItem.vue +15 -6
- package/components/TableMain.vue +15 -3
- package/components/WorkOrder/Create.vue +22 -1
- package/components/WorkOrder/Detail.vue +71 -0
- package/components/WorkOrder/Main.vue +62 -3
- package/composables/useCustomerSite.ts +21 -1
- package/composables/useFeedback.ts +2 -0
- package/composables/useFile.ts +12 -1
- package/composables/useUtils.ts +20 -0
- package/composables/useWorkOrder.ts +10 -1
- package/middleware/02.org.ts +3 -0
- package/package.json +1 -1
- package/plugins/secure-member.client.ts +20 -6
- package/plugins/vuetify.ts +5 -0
- package/types/feedback.d.ts +3 -0
- package/types/work-order.d.ts +2 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<v-text-field ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly"
|
|
2
|
+
<div class="d-flex flex-column">
|
|
3
|
+
<v-text-field v-bind="$attrs" ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly"
|
|
4
|
+
placeholder="MM/DD/YYYY, HH:MM AM/PM" :rules="rules" style="z-index: 10" @click="openDatePicker">
|
|
4
5
|
<template #append-inner>
|
|
5
6
|
<v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
|
|
6
7
|
</template>
|
|
7
8
|
</v-text-field>
|
|
8
9
|
<div class="w-100 d-flex align-end ga-3 hidden-input">
|
|
9
|
-
<input ref="dateInput" type="datetime-local"
|
|
10
|
+
<input ref="dateInput" type="datetime-local" v-model="dateTime" />
|
|
10
11
|
</div>
|
|
11
|
-
|
|
12
12
|
</div>
|
|
13
13
|
</template>
|
|
14
14
|
|
|
@@ -19,30 +19,37 @@ const prop = defineProps({
|
|
|
19
19
|
rules: {
|
|
20
20
|
type: Array as PropType<Array<any>>,
|
|
21
21
|
default: () => []
|
|
22
|
-
}
|
|
22
|
+
},
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const { formatDateISO8601 } = useUtils()
|
|
26
|
+
const dateTime = defineModel<string | null>({ default: null }) //2025-10-10T13:09 format
|
|
27
|
+
const dateTimeUTC = defineModel<string | null>('utc', { default: null }) // UTC format
|
|
26
28
|
|
|
27
29
|
const dateTimeFormattedReadOnly = ref<string | null>(null)
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
|
|
33
|
+
|
|
31
34
|
const dateInput = ref<HTMLInputElement | null>(null)
|
|
32
35
|
const dateTimePickerRef = ref<HTMLInputElement | null>(null)
|
|
33
36
|
|
|
37
|
+
const isInitialLoad = ref(true)
|
|
38
|
+
|
|
34
39
|
function openDatePicker() {
|
|
35
|
-
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
dateInput.value?.showPicker?.()
|
|
42
|
+
}, 0)
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
function validate(){
|
|
45
|
+
function validate() {
|
|
39
46
|
(dateTimePickerRef.value as any)?.validate()
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
function convertToReadableFormat(dateStr: string): string {
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const date = new Date(dateVal)
|
|
51
|
+
if (!dateStr) return "";
|
|
52
|
+
const date = new Date(dateStr)
|
|
46
53
|
const options: Intl.DateTimeFormatOptions = {
|
|
47
54
|
year: 'numeric',
|
|
48
55
|
month: '2-digit',
|
|
@@ -52,9 +59,57 @@ watch(dateTime, (dateVal) => {
|
|
|
52
59
|
hour12: true
|
|
53
60
|
}
|
|
54
61
|
const formatted = date.toLocaleString('en-US', options)
|
|
55
|
-
|
|
56
|
-
}
|
|
62
|
+
return formatted
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleInitialDate(){
|
|
66
|
+
const dateDefault = dateTime.value
|
|
67
|
+
const dateUTC = dateTimeUTC.value
|
|
68
|
+
if(dateDefault){
|
|
69
|
+
dateTimeFormattedReadOnly.value = convertToReadableFormat(dateDefault)
|
|
70
|
+
const localDate = new Date(dateDefault)
|
|
71
|
+
dateTimeUTC.value = localDate.toISOString()
|
|
72
|
+
} else if (dateUTC){
|
|
73
|
+
dateTimeFormattedReadOnly.value = convertToReadableFormat(dateUTC)
|
|
74
|
+
const localDate = new Date(dateUTC)
|
|
75
|
+
dateTime.value = formatDateISO8601(localDate)
|
|
76
|
+
} else {
|
|
77
|
+
dateTimeFormattedReadOnly.value = null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
|
|
82
|
+
watch(dateTime, (dateVal) => {
|
|
83
|
+
if (isInitialLoad.value) return // ignore the first run
|
|
84
|
+
if (!dateVal) {
|
|
85
|
+
dateTimeFormattedReadOnly.value = null;
|
|
86
|
+
dateTimeUTC.value = null
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
dateTimeFormattedReadOnly.value = convertToReadableFormat(dateVal)
|
|
91
|
+
const localDate = new Date(dateVal)
|
|
92
|
+
dateTimeUTC.value = localDate.toISOString()
|
|
93
|
+
|
|
94
|
+
}, { immediate: false })
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
onMounted(async () => {
|
|
98
|
+
|
|
99
|
+
handleInitialDate()
|
|
100
|
+
await nextTick()
|
|
101
|
+
isInitialLoad.value = false
|
|
102
|
+
|
|
103
|
+
// Wait until Vuetify renders its internal input
|
|
104
|
+
const nativeInput = dateTimePickerRef.value?.$el?.querySelector('input')
|
|
105
|
+
if (nativeInput) {
|
|
106
|
+
nativeInput.addEventListener('click', (e: MouseEvent) => {
|
|
107
|
+
e.stopPropagation()
|
|
108
|
+
openDatePicker()
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
})
|
|
58
113
|
|
|
59
114
|
|
|
60
115
|
defineExpose({
|
|
@@ -66,6 +121,6 @@ defineExpose({
|
|
|
66
121
|
.hidden-input {
|
|
67
122
|
opacity: 0;
|
|
68
123
|
height: 0;
|
|
69
|
-
width:
|
|
124
|
+
width: 1px;
|
|
70
125
|
}
|
|
71
126
|
</style>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
3
|
<v-row class="mb-4" align="center" no-gutters>
|
|
4
|
-
<v-col
|
|
4
|
+
<v-col
|
|
5
|
+
cols="10"
|
|
6
|
+
class="pr-2"
|
|
7
|
+
v-if="
|
|
8
|
+
props.createdFrom === 'feedback' && props.attachments.length > 0
|
|
9
|
+
? false
|
|
10
|
+
: true
|
|
11
|
+
"
|
|
12
|
+
>
|
|
5
13
|
<div
|
|
6
14
|
class="d-flex align-center justify-center pa-4 rounded-lg border-dashed border border-grey"
|
|
7
15
|
@dragover.prevent
|
|
@@ -37,7 +45,15 @@
|
|
|
37
45
|
</div>
|
|
38
46
|
</v-col>
|
|
39
47
|
|
|
40
|
-
<v-col
|
|
48
|
+
<v-col
|
|
49
|
+
cols="2"
|
|
50
|
+
class="d-flex justify-center"
|
|
51
|
+
v-if="
|
|
52
|
+
props.createdFrom === 'feedback' && props.attachments.length > 0
|
|
53
|
+
? false
|
|
54
|
+
: true
|
|
55
|
+
"
|
|
56
|
+
>
|
|
41
57
|
<v-btn
|
|
42
58
|
color="primary-button"
|
|
43
59
|
min-width="55"
|
|
@@ -90,7 +106,15 @@
|
|
|
90
106
|
{{ getDisplayName(file) }}
|
|
91
107
|
</div>
|
|
92
108
|
|
|
93
|
-
<v-icon
|
|
109
|
+
<v-icon
|
|
110
|
+
size="small"
|
|
111
|
+
@click.stop="$emit('delete', file)"
|
|
112
|
+
v-if="
|
|
113
|
+
props.createdFrom === 'feedback' && props.attachments.length > 0
|
|
114
|
+
? false
|
|
115
|
+
: true
|
|
116
|
+
"
|
|
117
|
+
>
|
|
94
118
|
mdi-trash-can-outline
|
|
95
119
|
</v-icon>
|
|
96
120
|
</v-col>
|
|
@@ -104,6 +128,7 @@ const props = defineProps<{
|
|
|
104
128
|
attachments: string[];
|
|
105
129
|
erroredImages?: string[];
|
|
106
130
|
maxFiles?: number;
|
|
131
|
+
createdFrom: string;
|
|
107
132
|
}>();
|
|
108
133
|
|
|
109
134
|
const emit = defineEmits<{
|
|
@@ -179,7 +204,7 @@ function getThumbnail(fileUrl: string): string {
|
|
|
179
204
|
// if (fileUrl.match(/\.(xls|xlsx)$/i))
|
|
180
205
|
// return "/images/file-thumbnails/excel.png";
|
|
181
206
|
// return "/images/file-thumbnails/file.png";
|
|
182
|
-
return `/api/public/${fileUrl}
|
|
207
|
+
return `/api/public/${fileUrl}`;
|
|
183
208
|
}
|
|
184
209
|
|
|
185
210
|
// Modified to try to display the friendly name
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters class="w-100 pb-5" @click="resetErrorMessage">
|
|
3
|
+
<v-file-upload v-model="uploadFiles" density="compact" @update:model-value="handleUpdateValue"
|
|
4
|
+
:loading="processing" :disabled="processing" :height="height" title="Upload Images" accept="image/*"
|
|
5
|
+
name="upload_images" class="text-caption w-100" clearable :multiple="multiple">
|
|
6
|
+
<template v-slot:item="{ props: itemProps, file }">
|
|
7
|
+
<v-file-upload-item v-bind="itemProps" lines="one" nav>
|
|
8
|
+
<template v-slot:prepend>
|
|
9
|
+
<v-avatar size="32" rounded></v-avatar>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<template v-slot:clear="{ props: clearProps }">
|
|
13
|
+
<v-btn color="primary" @click="handleRemove(file)"></v-btn>
|
|
14
|
+
</template>
|
|
15
|
+
</v-file-upload-item>
|
|
16
|
+
</template>
|
|
17
|
+
</v-file-upload>
|
|
18
|
+
<v-row no-gutters class="w-100" v-if="errorMessage">
|
|
19
|
+
<p class="text-error w-100 text-center text-subtitle-2">{{ errorMessage }}</p>
|
|
20
|
+
</v-row>
|
|
21
|
+
</v-row>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
|
|
26
|
+
const prop = defineProps({
|
|
27
|
+
height: {
|
|
28
|
+
type: Number || String,
|
|
29
|
+
default: 68
|
|
30
|
+
},
|
|
31
|
+
multiple: {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
default: false
|
|
34
|
+
},
|
|
35
|
+
maxLength: {
|
|
36
|
+
type: Number,
|
|
37
|
+
default: 10
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const { addFile, deleteFile, getFileUrl, urlToFile } = useFile()
|
|
44
|
+
|
|
45
|
+
const uploadFiles = ref<File[]>([])
|
|
46
|
+
const filesCollection = defineModel<{ file: File, id: string }[]>({required: true, default: []}) // files collection array
|
|
47
|
+
const showCameraDialog = ref(false)
|
|
48
|
+
const errorMessage = ref('');
|
|
49
|
+
const processing = ref(false)
|
|
50
|
+
|
|
51
|
+
const video = ref<HTMLVideoElement>()
|
|
52
|
+
const canvas = ref<HTMLCanvasElement>()
|
|
53
|
+
const cameraFacingMode = ref('environment')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async function handleRemove(removedFile: File) {
|
|
57
|
+
const fileKey = (f: File) => `${f.name}_${f.size}_${f.lastModified}`
|
|
58
|
+
const arr = filesCollection.value
|
|
59
|
+
const fileId = arr.find(item => fileKey(item.file) === fileKey(removedFile))?.id
|
|
60
|
+
|
|
61
|
+
uploadFiles.value = uploadFiles.value.filter(f => fileKey(f) !== fileKey(removedFile))
|
|
62
|
+
filesCollection.value = arr.filter(item => item.id !== fileId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleUpdateValue(value: File[]) {
|
|
66
|
+
await nextTick()
|
|
67
|
+
const max = prop.maxLength
|
|
68
|
+
const existingLength = filesCollection.value.length
|
|
69
|
+
errorMessage.value = ''
|
|
70
|
+
if ((existingLength + value.length) > max) {
|
|
71
|
+
value = value.slice(0, (max - existingLength))
|
|
72
|
+
errorMessage.value = `Max value of allowed image is ${max}`
|
|
73
|
+
}
|
|
74
|
+
uploadFiles.value = []
|
|
75
|
+
processing.value = true
|
|
76
|
+
// Determine which files are newly added or removed
|
|
77
|
+
const fileKey = (f: File) => `${f.name}_${f.size}_${f.lastModified}`
|
|
78
|
+
|
|
79
|
+
const arr = filesCollection.value
|
|
80
|
+
const collectionKeys = arr.map(x => fileKey(x.file))
|
|
81
|
+
const addedFiles = value.filter(f => !collectionKeys.includes(fileKey(f)))
|
|
82
|
+
|
|
83
|
+
// Upload new files
|
|
84
|
+
processing.value = true
|
|
85
|
+
for (const file of addedFiles) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await addFile(file) // expected to return { id, url } or similar
|
|
88
|
+
if (res?.id) {
|
|
89
|
+
filesCollection.value.push({ file, id: res?.id })
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error("Upload failed", err)
|
|
93
|
+
errorMessage.value = `Failed to upload ${file.name}`
|
|
94
|
+
} finally {
|
|
95
|
+
processing.value = false
|
|
96
|
+
uploadFiles.value = filesCollection.value.map(x => x.file)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resetErrorMessage(){
|
|
103
|
+
errorMessage.value = '';
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onMounted(() => {
|
|
108
|
+
filesCollection.value = []
|
|
109
|
+
})
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
* :deep(.v-file-upload-title) {
|
|
114
|
+
font-size: 1rem;
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
* :deep(.v-file-upload-items) {
|
|
119
|
+
min-width: 100%;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -214,16 +214,16 @@ const apps = computed(() => {
|
|
|
214
214
|
items.push({ title: "Security Agency", value: _org });
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
const _cleaning = "
|
|
217
|
+
const _cleaning = "cleaning_services";
|
|
218
218
|
|
|
219
219
|
if (props.app === _cleaning || orgNature.value === _cleaning) {
|
|
220
|
-
items.push({ title: "Cleaning
|
|
220
|
+
items.push({ title: "Cleaning Services", value: _cleaning });
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
const _property = "
|
|
223
|
+
const _property = "property_management_agency";
|
|
224
224
|
|
|
225
225
|
if (props.app === _property || orgNature.value === _property) {
|
|
226
|
-
items.push({ title: "Property
|
|
226
|
+
items.push({ title: "Property Management Agency", value: _property });
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
const _mechanical = "mechanical_electrical_services";
|
|
@@ -247,7 +247,7 @@ const sites = ref<Array<Record<string, any>>>([]);
|
|
|
247
247
|
const { getAll: getAllCustomerSite } = useCustomerSite();
|
|
248
248
|
|
|
249
249
|
const { data: siteData, refresh: refreshSiteData } = await useLazyAsyncData(
|
|
250
|
-
"get-sites-by-org",
|
|
250
|
+
"get-sites-by-org-" + props.org,
|
|
251
251
|
async () => await getAllCustomerSite({ org: props.org, limit: 50 })
|
|
252
252
|
);
|
|
253
253
|
|
|
@@ -1,86 +1,39 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-app-bar scroll-behavior="elevate" scroll-threshold="200">
|
|
3
|
-
<v-app-bar-nav-icon
|
|
4
|
-
v-if="!props.hideNavIcon"
|
|
5
|
-
@click="drawer = !drawer"
|
|
6
|
-
></v-app-bar-nav-icon>
|
|
3
|
+
<v-app-bar-nav-icon v-if="!props.hideNavIcon" @click="drawer = !drawer"></v-app-bar-nav-icon>
|
|
7
4
|
|
|
8
5
|
<template #append>
|
|
9
|
-
<v-btn
|
|
10
|
-
icon="mdi-theme-light-dark"
|
|
11
|
-
variant="text"
|
|
12
|
-
class="mx-2"
|
|
13
|
-
@click="toggleTheme"
|
|
14
|
-
/>
|
|
6
|
+
<v-btn icon="mdi-theme-light-dark" variant="text" class="mx-2" @click="toggleTheme" />
|
|
15
7
|
|
|
16
8
|
<v-menu offset="10px" :close-on-content-click="false">
|
|
17
9
|
<template #activator="{ props }">
|
|
18
10
|
<v-btn fab variant="text" icon v-bind="props" class="mx-2">
|
|
19
|
-
<
|
|
20
|
-
<v-img
|
|
21
|
-
v-if="currentUser?.profile"
|
|
22
|
-
:src="profile"
|
|
23
|
-
width="42"
|
|
24
|
-
height="42"
|
|
25
|
-
/>
|
|
26
|
-
|
|
27
|
-
<span v-else class="text-h5">{{ getNameInitials(name) }}</span>
|
|
28
|
-
</v-avatar>
|
|
11
|
+
<AvatarMain :image-src="currentUser?.profile" :name="name" />
|
|
29
12
|
</v-btn>
|
|
30
13
|
</template>
|
|
31
14
|
|
|
32
|
-
<v-card
|
|
33
|
-
width="350"
|
|
34
|
-
max-height="600px"
|
|
35
|
-
elevation="2"
|
|
36
|
-
rounded="xl"
|
|
37
|
-
class="pa-4"
|
|
38
|
-
>
|
|
15
|
+
<v-card width="350" max-height="600px" elevation="2" rounded="xl" class="pa-4">
|
|
39
16
|
<v-row no-gutters>
|
|
40
17
|
<v-col cols="12">
|
|
41
18
|
<v-row no-gutters justify="center">
|
|
42
|
-
|
|
43
|
-
<v-img
|
|
44
|
-
v-if="currentUser?.profile"
|
|
45
|
-
:src="profile"
|
|
46
|
-
width="75"
|
|
47
|
-
height="75"
|
|
48
|
-
/>
|
|
49
|
-
|
|
50
|
-
<span v-else class="text-h5">{{
|
|
51
|
-
getNameInitials(name)
|
|
52
|
-
}}</span>
|
|
53
|
-
</v-avatar>
|
|
19
|
+
<AvatarMain :image-src="currentUser?.profile" :name="name" />
|
|
54
20
|
</v-row>
|
|
55
21
|
</v-col>
|
|
56
22
|
|
|
57
23
|
<v-col cols="12" class="text-center mt-2 mb-4">
|
|
58
|
-
{{
|
|
24
|
+
{{ name }}
|
|
59
25
|
</v-col>
|
|
60
26
|
|
|
61
27
|
<v-col cols="12" class="mb-3">
|
|
62
|
-
<v-btn
|
|
63
|
-
|
|
64
|
-
block
|
|
65
|
-
rounded="xl"
|
|
66
|
-
variant="tonal"
|
|
67
|
-
size="x-large"
|
|
68
|
-
class="text-none text-subtitle-1 font-weight-regular"
|
|
69
|
-
@click="redirect(APP_ACCOUNT, 'home')"
|
|
70
|
-
>
|
|
28
|
+
<v-btn v-if="APP_NAME.toLowerCase() !== 'account'" block rounded="xl" variant="tonal" size="x-large"
|
|
29
|
+
class="text-none text-subtitle-1 font-weight-regular" @click="redirect(APP_ACCOUNT, 'home')">
|
|
71
30
|
Manage Account
|
|
72
31
|
</v-btn>
|
|
73
32
|
</v-col>
|
|
74
33
|
|
|
75
34
|
<v-col cols="12">
|
|
76
|
-
<v-btn
|
|
77
|
-
|
|
78
|
-
rounded="xl"
|
|
79
|
-
variant="tonal"
|
|
80
|
-
size="x-large"
|
|
81
|
-
class="text-none text-subtitle-1 font-weight-regular"
|
|
82
|
-
@click="logout()"
|
|
83
|
-
>
|
|
35
|
+
<v-btn block rounded="xl" variant="tonal" size="x-large"
|
|
36
|
+
class="text-none text-subtitle-1 font-weight-regular" @click="logout()">
|
|
84
37
|
Logout
|
|
85
38
|
</v-btn>
|
|
86
39
|
</v-col>
|
|
@@ -133,17 +86,20 @@ function logout() {
|
|
|
133
86
|
}
|
|
134
87
|
|
|
135
88
|
const name = computed(() => {
|
|
136
|
-
|
|
137
|
-
if
|
|
138
|
-
name = currentUser.value.firstName;
|
|
139
|
-
}
|
|
89
|
+
const user = currentUser.value;
|
|
90
|
+
if(!user) return "";
|
|
140
91
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
92
|
+
const first = user?.firstName?.trim() || ""
|
|
93
|
+
const last = user?.lastName?.trim() || ""
|
|
94
|
+
const full = [first, last].filter(Boolean).join(" ")
|
|
95
|
+
|
|
96
|
+
if(full) return full;
|
|
97
|
+
|
|
98
|
+
const alternative = user?.name?.trim();
|
|
99
|
+
return alternative || ""
|
|
100
|
+
|
|
101
|
+
})
|
|
144
102
|
|
|
145
|
-
return name;
|
|
146
|
-
});
|
|
147
103
|
|
|
148
104
|
const { getNameInitials } = useUtils();
|
|
149
105
|
</script>
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-list-group v-if="children && children.length">
|
|
3
|
-
<template #activator="{ props }">
|
|
4
|
-
<v-list-item
|
|
3
|
+
<template #activator="{ props: groupProps }">
|
|
4
|
+
<v-list-item
|
|
5
|
+
v-bind="groupProps"
|
|
6
|
+
:prepend-icon="icon"
|
|
7
|
+
class="text-subtitle-2"
|
|
8
|
+
@click.stop="onParentClick"
|
|
9
|
+
>
|
|
5
10
|
{{ title }}
|
|
6
11
|
</v-list-item>
|
|
7
12
|
</template>
|
|
@@ -17,8 +22,7 @@
|
|
|
17
22
|
</v-list-group>
|
|
18
23
|
|
|
19
24
|
<v-list-item
|
|
20
|
-
v-if="props.route && props.route.name"
|
|
21
|
-
:key="props.name"
|
|
25
|
+
v-else-if="props.route && props.route.name"
|
|
22
26
|
:prepend-icon="icon"
|
|
23
27
|
:to="props.route"
|
|
24
28
|
class="text-subtitle-2"
|
|
@@ -27,8 +31,7 @@
|
|
|
27
31
|
</v-list-item>
|
|
28
32
|
|
|
29
33
|
<v-list-item
|
|
30
|
-
v-if="props.link"
|
|
31
|
-
:key="props.name"
|
|
34
|
+
v-else-if="props.link"
|
|
32
35
|
:prepend-icon="icon"
|
|
33
36
|
:href="props.link"
|
|
34
37
|
class="text-subtitle-2"
|
|
@@ -71,4 +74,10 @@ const props = defineProps({
|
|
|
71
74
|
default: "",
|
|
72
75
|
},
|
|
73
76
|
});
|
|
77
|
+
|
|
78
|
+
function onParentClick() {
|
|
79
|
+
if (props.route && props.route.name) {
|
|
80
|
+
navigateTo(props.route);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
74
83
|
</script>
|
package/components/TableMain.vue
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<v-col cols="12">
|
|
17
17
|
<v-card width="100%" variant="outlined" border="thin" rounded="lg" :loading="loading">
|
|
18
18
|
<!-- Toolbar -->
|
|
19
|
-
<v-toolbar density="compact" color="grey-lighten-4">
|
|
19
|
+
<v-toolbar density="compact" color="grey-lighten-4" :extension-height="extensionHeight">
|
|
20
20
|
<template #prepend>
|
|
21
21
|
<v-btn fab icon density="comfortable" @click="emits('refresh')">
|
|
22
22
|
<v-icon>mdi-refresh</v-icon>
|
|
@@ -34,18 +34,20 @@
|
|
|
34
34
|
</template>
|
|
35
35
|
|
|
36
36
|
<template v-if="$slots.extension" #extension>
|
|
37
|
-
<slot name="extension"
|
|
37
|
+
<slot name="extension"/>
|
|
38
38
|
</template>
|
|
39
39
|
</v-toolbar>
|
|
40
40
|
|
|
41
|
+
|
|
41
42
|
<!-- Data Table -->
|
|
42
43
|
<v-data-table :headers="headers" :items="items" :item-value="itemValue" :items-per-page="itemsPerPage"
|
|
43
|
-
fixed-header hide-default-footer hide-default-header
|
|
44
|
+
fixed-header hide-default-footer :hide-default-header="!showHeader"
|
|
44
45
|
@click:row="(_: any, data: any) => emits('row-click', data)" style="max-height: calc(100vh - (200px))">
|
|
45
46
|
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
|
|
46
47
|
<slot :name="slotName" v-bind="slotProps" />
|
|
47
48
|
</template>
|
|
48
49
|
</v-data-table>
|
|
50
|
+
<slot name="footer"/>
|
|
49
51
|
</v-card>
|
|
50
52
|
</v-col>
|
|
51
53
|
</v-row>
|
|
@@ -93,6 +95,14 @@ const props = defineProps({
|
|
|
93
95
|
type: String,
|
|
94
96
|
default: "-- - -- of --",
|
|
95
97
|
},
|
|
98
|
+
showHeader: {
|
|
99
|
+
type: Boolean,
|
|
100
|
+
default: false
|
|
101
|
+
},
|
|
102
|
+
extensionHeight: {
|
|
103
|
+
type: Number,
|
|
104
|
+
default: 50
|
|
105
|
+
}
|
|
96
106
|
});
|
|
97
107
|
|
|
98
108
|
const emits = defineEmits(["create", "refresh", "update:page", "row-click"]);
|
|
@@ -105,3 +115,5 @@ watch(
|
|
|
105
115
|
}
|
|
106
116
|
);
|
|
107
117
|
</script>
|
|
118
|
+
|
|
119
|
+
<style scoped></style>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
:attachments="displayedAttachments"
|
|
11
11
|
:errored-images="localErroredImages"
|
|
12
12
|
:max-files="maxFiles"
|
|
13
|
+
:created-from="props.createdFrom"
|
|
13
14
|
@add="handleFileAdded"
|
|
14
15
|
@delete="deleteFile"
|
|
15
16
|
@errored="onImageError"
|
|
@@ -22,6 +23,11 @@
|
|
|
22
23
|
v-model="localWorkOrder.subject"
|
|
23
24
|
class="mb-1"
|
|
24
25
|
dense
|
|
26
|
+
:readonly="
|
|
27
|
+
props.createdFrom === 'feedback' && localWorkOrder.subject !== ''
|
|
28
|
+
? true
|
|
29
|
+
: false
|
|
30
|
+
"
|
|
25
31
|
/>
|
|
26
32
|
|
|
27
33
|
<v-autocomplete
|
|
@@ -34,7 +40,16 @@
|
|
|
34
40
|
variant="outlined"
|
|
35
41
|
density="compact"
|
|
36
42
|
class="mb-2"
|
|
37
|
-
clearable
|
|
43
|
+
:clearable="
|
|
44
|
+
props.createdFrom === 'feedback' && localWorkOrder.category !== ''
|
|
45
|
+
? false
|
|
46
|
+
: true
|
|
47
|
+
"
|
|
48
|
+
:readonly="
|
|
49
|
+
props.createdFrom === 'feedback' && localWorkOrder.category !== ''
|
|
50
|
+
? true
|
|
51
|
+
: false
|
|
52
|
+
"
|
|
38
53
|
/>
|
|
39
54
|
|
|
40
55
|
<v-checkbox
|
|
@@ -86,6 +101,11 @@
|
|
|
86
101
|
v-model="localWorkOrder.location"
|
|
87
102
|
class="mt-1"
|
|
88
103
|
dense
|
|
104
|
+
:readonly="
|
|
105
|
+
props.createdFrom === 'feedback' && localWorkOrder.location !== ''
|
|
106
|
+
? true
|
|
107
|
+
: false
|
|
108
|
+
"
|
|
89
109
|
/>
|
|
90
110
|
|
|
91
111
|
<v-textarea
|
|
@@ -117,6 +137,7 @@
|
|
|
117
137
|
<script setup lang="ts">
|
|
118
138
|
const props = defineProps<{
|
|
119
139
|
modelValue: boolean;
|
|
140
|
+
createdFrom: string;
|
|
120
141
|
workOrder: TWorkOrderCreate & { specificLocation?: string };
|
|
121
142
|
isEditMode: boolean;
|
|
122
143
|
loading: boolean;
|