@iservice365/layer-common 1.0.9 → 1.0.11
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 +27 -22
- package/components/Layout/Header.vue +22 -66
- 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 +11 -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>
|
|
@@ -48,14 +48,13 @@
|
|
|
48
48
|
:items="roles"
|
|
49
49
|
item-title="name"
|
|
50
50
|
item-value="_id"
|
|
51
|
-
|
|
51
|
+
:rules="[requiredRule]"
|
|
52
52
|
density="comfortable"
|
|
53
53
|
></v-autocomplete>
|
|
54
54
|
</v-col>
|
|
55
55
|
</v-row>
|
|
56
56
|
</v-col>
|
|
57
57
|
|
|
58
|
-
|
|
59
58
|
<v-col v-if="hasSite" cols="12">
|
|
60
59
|
<v-row no-gutters>
|
|
61
60
|
<InputLabel class="text-capitalize" title="Site" />
|
|
@@ -176,13 +175,13 @@ const props = defineProps({
|
|
|
176
175
|
const emit = defineEmits(["cancel", "success", "success:create-more"]);
|
|
177
176
|
|
|
178
177
|
const validForm = ref(false);
|
|
179
|
-
const form = ref<HTMLFormElement | null>(null)
|
|
178
|
+
const form = ref<HTMLFormElement | null>(null);
|
|
180
179
|
const app = computed(() => useRuntimeConfig().public.APP ?? "");
|
|
181
180
|
|
|
182
181
|
const loading = reactive({
|
|
183
182
|
submittingForm: false,
|
|
184
|
-
verifyingEmail: false
|
|
185
|
-
})
|
|
183
|
+
verifyingEmail: false,
|
|
184
|
+
});
|
|
186
185
|
|
|
187
186
|
const invite = ref<Record<string, any>>({
|
|
188
187
|
email: "",
|
|
@@ -203,27 +202,36 @@ if (props.mode === "edit") {
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
const { natureOfBusiness } = useLocal();
|
|
205
|
+
const { orgNature } = useLocalSetup();
|
|
206
206
|
|
|
207
207
|
const apps = computed(() => {
|
|
208
208
|
const items = [];
|
|
209
209
|
items.unshift({ title: "Organization", value: "organization" });
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
const _org = "security_agency";
|
|
212
|
+
|
|
213
|
+
if (props.app === _org || orgNature.value === _org) {
|
|
214
|
+
items.push({ title: "Security Agency", value: _org });
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
const _cleaning = "cleaning_services";
|
|
218
|
+
|
|
219
|
+
if (props.app === _cleaning || orgNature.value === _cleaning) {
|
|
220
|
+
items.push({ title: "Cleaning Services", value: _cleaning });
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
|
|
220
|
-
|
|
223
|
+
const _property = "property_management_agency";
|
|
224
|
+
|
|
225
|
+
if (props.app === _property || orgNature.value === _property) {
|
|
226
|
+
items.push({ title: "Property Management Agency", value: _property });
|
|
221
227
|
}
|
|
222
228
|
|
|
223
|
-
|
|
229
|
+
const _mechanical = "mechanical_electrical_services";
|
|
230
|
+
|
|
231
|
+
if (props.app === _mechanical || orgNature.value === _mechanical) {
|
|
224
232
|
items.push({
|
|
225
233
|
title: "Mechanical & Electrical Services",
|
|
226
|
-
value:
|
|
234
|
+
value: _mechanical,
|
|
227
235
|
});
|
|
228
236
|
}
|
|
229
237
|
|
|
@@ -239,9 +247,8 @@ const sites = ref<Array<Record<string, any>>>([]);
|
|
|
239
247
|
const { getAll: getAllCustomerSite } = useCustomerSite();
|
|
240
248
|
|
|
241
249
|
const { data: siteData, refresh: refreshSiteData } = await useLazyAsyncData(
|
|
242
|
-
"get-sites-by-org",
|
|
243
|
-
async () => await getAllCustomerSite({ org: props.org, limit: 50 })
|
|
244
|
-
{ }
|
|
250
|
+
"get-sites-by-org-" + props.org,
|
|
251
|
+
async () => await getAllCustomerSite({ org: props.org, limit: 50 })
|
|
245
252
|
);
|
|
246
253
|
|
|
247
254
|
watchEffect(() => {
|
|
@@ -275,13 +282,12 @@ watchEffect(() => {
|
|
|
275
282
|
}
|
|
276
283
|
});
|
|
277
284
|
|
|
278
|
-
function handleUpdateApp(value: string){
|
|
285
|
+
function handleUpdateApp(value: string) {
|
|
279
286
|
invite.value.role = "";
|
|
280
287
|
invite.value.site = "";
|
|
281
288
|
refreshRoles();
|
|
282
289
|
}
|
|
283
290
|
|
|
284
|
-
|
|
285
291
|
const createMore = ref(false);
|
|
286
292
|
const disable = ref(false);
|
|
287
293
|
|
|
@@ -296,14 +302,13 @@ function resetInvite() {
|
|
|
296
302
|
message.value = "";
|
|
297
303
|
}
|
|
298
304
|
|
|
299
|
-
function handleUpdateSite(siteId: string){
|
|
300
|
-
const obj = sites.value.find(
|
|
301
|
-
invite.value.siteName = obj?.title || ""
|
|
305
|
+
function handleUpdateSite(siteId: string) {
|
|
306
|
+
const obj = sites.value.find((x) => x?.value === siteId);
|
|
307
|
+
invite.value.siteName = obj?.title || "";
|
|
302
308
|
}
|
|
303
309
|
|
|
304
310
|
const { inviteUser } = useUser();
|
|
305
311
|
|
|
306
|
-
|
|
307
312
|
async function submit() {
|
|
308
313
|
loading.submittingForm = true;
|
|
309
314
|
try {
|
|
@@ -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>
|
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;
|