@iservice365/layer-common 1.0.10 → 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.
@@ -1,14 +1,14 @@
1
1
  <template>
2
- <div>
3
- <v-text-field ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly" @click="openDatePicker" placeholder="MM/DD/YYYY, HH:MM AM/PM" :rules="rules">
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" v-model="dateTime" />
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 dateTime = defineModel<string | null>({ default: null })
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
- dateInput.value?.showPicker()
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
- watch(dateTime, (dateVal) => {
44
- if (!dateVal) return dateTimeFormattedReadOnly.value = null
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
- dateTimeFormattedReadOnly.value = formatted
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: 0;
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 cols="10" class="pr-2">
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 cols="2" class="d-flex justify-center">
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 size="small" @click.stop="$emit('delete', file)">
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 = "cleaning_agency";
217
+ const _cleaning = "cleaning_services";
218
218
 
219
219
  if (props.app === _cleaning || orgNature.value === _cleaning) {
220
- items.push({ title: "Cleaning Agency", value: _cleaning });
220
+ items.push({ title: "Cleaning Services", value: _cleaning });
221
221
  }
222
222
 
223
- const _property = "property_manager";
223
+ const _property = "property_management_agency";
224
224
 
225
225
  if (props.app === _property || orgNature.value === _property) {
226
- items.push({ title: "Property Manager", value: _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
- <v-avatar color="surface-variant" size="42">
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
- <v-avatar color="surface-variant" size="75">
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
- {{ currentUser?.firstName }} {{ currentUser?.lastName }}
24
+ {{ name }}
59
25
  </v-col>
60
26
 
61
27
  <v-col cols="12" class="mb-3">
62
- <v-btn
63
- v-if="APP_NAME.toLowerCase() !== 'account'"
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
- block
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
- let name = "";
137
- if (currentUser.value?.firstName) {
138
- name = currentUser.value.firstName;
139
- }
89
+ const user = currentUser.value;
90
+ if(!user) return "";
140
91
 
141
- if (currentUser.value?.lastName) {
142
- name += ` ${currentUser.value.lastName}`;
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>
@@ -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;
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div class="feedback-detail-wrapper">
3
+ <v-row no-gutters class="fill-height">
4
+ <v-col cols="12" xl="7" lg="7" md="7" class="fill-height">
5
+ <div class="panel-container border-e">
6
+ <ChatMessage :type="'workOrder'" />
7
+ </div>
8
+ </v-col>
9
+ <v-col cols="12" xl="5" lg="5" md="5" class="fill-height">
10
+ <div class="panel-container">
11
+ <ChatInformation
12
+ :item="workOrder"
13
+ :service-providers="_serviceProviders"
14
+ :type="'workOrder'"
15
+ @edit="openEditDialog"
16
+ @mark-complete-request="showCompleteDialog = true"
17
+ @delete="showDeleteDialog = true"
18
+ />
19
+ </div>
20
+ </v-col>
21
+ </v-row>
22
+ </div>
23
+ </template>
24
+ <script lang="ts" setup>
25
+ const route = useRoute();
26
+ const id = route.params.id;
27
+
28
+ const {
29
+ workOrder,
30
+ workOrders,
31
+ getWorkOrderById,
32
+ getWorkOrders: _getWorkOrders,
33
+ } = useWorkOrder();
34
+
35
+ const { getServiceProviderNames } = useServiceProvider();
36
+
37
+ const _getWorkOrderById = async () => {
38
+ try {
39
+ const data = await getWorkOrderById(id as string);
40
+ workOrder.value = data;
41
+ } catch (error) {
42
+ console.error("Error fetching feedback:", error);
43
+ }
44
+ };
45
+
46
+ _getWorkOrderById();
47
+
48
+ const _serviceProviders = ref<TServiceProviderName[]>([]);
49
+
50
+ const _getServiceProviderNames = async () => {
51
+ try {
52
+ const response = await getServiceProviderNames();
53
+ if (!response) return;
54
+
55
+ _serviceProviders.value = response.items.map((provider) => ({
56
+ _id: provider._id as string,
57
+ name: provider.name,
58
+ }));
59
+ } catch (error) {
60
+ console.error("Error fetching service providers:", error);
61
+ }
62
+ };
63
+
64
+ _getServiceProviderNames();
65
+
66
+ const showCreateDialog = ref(false);
67
+ const showCompleteDialog = ref(false);
68
+ const showDeleteDialog = ref(false);
69
+
70
+ function openEditDialog() {}
71
+ </script>