design-system-next 2.8.3 → 2.9.1

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.
@@ -0,0 +1,96 @@
1
+ import { computed, type Ref } from "vue";
2
+ import type { BannerPropTypes } from "./banner";
3
+ import classNames from "classnames";
4
+
5
+ interface BannerClasses {
6
+ base: string;
7
+ content: string;
8
+ icon: string;
9
+ message: string;
10
+ close: string;
11
+ }
12
+
13
+ export const useBanner = (props: BannerPropTypes, showModel: Ref<boolean>) => {
14
+ const closeBanner = () => showModel.value = false;
15
+
16
+ const bannerClasses = computed<BannerClasses>(() => {
17
+ const base = classNames(
18
+ 'spr-w-full spr-flex spr-flex-row spr-items-top spr-p-size-spacing-3xs spr-rounded-border-radius-md',
19
+ {
20
+ 'spr-background-color-success-weak': props.type === 'success',
21
+ 'spr-background-color-danger-weak': props.type === 'error',
22
+ 'spr-background-color-information-weak': props.type === 'info',
23
+ 'spr-background-color-pending-weak': props.type === 'pending',
24
+ 'spr-background-color-caution-weak': props.type === 'caution',
25
+ }
26
+ );
27
+
28
+ const content = classNames(
29
+ 'spr-flex-auto'
30
+ );
31
+
32
+ const icon = classNames(
33
+ 'spr-mt-[2px]',
34
+ {
35
+ 'spr-text-color-brand-base': props.type === 'success',
36
+ 'spr-text-color-danger-base': props.type === 'error',
37
+ 'spr-text-color-information-base': props.type === 'info',
38
+ 'spr-text-color-pending-base': props.type === 'pending',
39
+ 'spr-text-color-caution-base': props.type === 'caution',
40
+ }
41
+ );
42
+
43
+ const message = classNames(
44
+ 'spr-body-sm-regular',
45
+ {
46
+ 'spr-text-color-brand-base': props.type === 'success',
47
+ 'spr-text-color-danger-pressed': props.type === 'error',
48
+ 'spr-text-color-information-pressed': props.type === 'info',
49
+ 'spr-text-color-pending-pressed': props.type === 'pending',
50
+ 'spr-text-color-caution-pressed': props.type === 'caution',
51
+ }
52
+ );
53
+
54
+ const close = classNames(
55
+ 'spr-flex-none spr-cursor-pointer',
56
+ {
57
+ 'spr-text-color-brand-base': props.type === 'success',
58
+ 'spr-text-color-danger-base': props.type === 'error',
59
+ 'spr-text-color-information-base': props.type === 'info',
60
+ 'spr-text-color-pending-base': props.type === 'pending',
61
+ 'spr-text-color-caution-base': props.type === 'caution',
62
+ }
63
+ );
64
+
65
+ return {
66
+ base,
67
+ content,
68
+ icon,
69
+ message,
70
+ close,
71
+ }
72
+ });
73
+
74
+ const bannerIcon = computed(() => {
75
+ switch (props.type) {
76
+ case 'success':
77
+ return 'ph:check-circle-fill';
78
+ case 'error':
79
+ return 'ph:warning-circle-fill';
80
+ case 'info':
81
+ return 'ph:info-fill';
82
+ case 'pending':
83
+ return 'ph:info-fill';
84
+ case 'caution':
85
+ return 'ph:warning-fill';
86
+ default:
87
+ return '';
88
+ }
89
+ });
90
+
91
+ return {
92
+ closeBanner,
93
+ bannerClasses,
94
+ bannerIcon,
95
+ };
96
+ };
@@ -22,12 +22,6 @@ interface Employee {
22
22
  hoursTarget?: number;
23
23
  schedule: EmployeeSchedule[];
24
24
  }
25
-
26
- interface FilterOption {
27
- text: string;
28
- value: string;
29
- }
30
-
31
25
  export interface SelectedShift {
32
26
  employeeId: string;
33
27
  date: string;
@@ -43,18 +37,6 @@ export const calendarPropTypes = {
43
37
  type: Date,
44
38
  default: () => new Date(),
45
39
  },
46
- companyOptions: {
47
- type: Array as PropType<FilterOption[]>,
48
- default: () => [{ text: 'All Companies', value: 'all' }],
49
- },
50
- departmentOptions: {
51
- type: Array as PropType<FilterOption[]>,
52
- default: () => [{ text: 'All Departments', value: 'all' }],
53
- },
54
- branchOptions: {
55
- type: Array as PropType<FilterOption[]>,
56
- default: () => [{ text: 'All Branches', value: 'all' }],
57
- },
58
40
 
59
41
  search: {
60
42
  type: String,
@@ -69,19 +51,6 @@ export const calendarPropTypes = {
69
51
  shift: null,
70
52
  }),
71
53
  },
72
-
73
- selectedCompany: {
74
- type: String,
75
- default: '',
76
- },
77
- selectedDepartment: {
78
- type: String,
79
- default: '',
80
- },
81
- selectedBranch: {
82
- type: String,
83
- default: '',
84
- },
85
54
  loading: {
86
55
  type: Boolean,
87
56
  default: false,
@@ -98,6 +67,10 @@ export const calendarPropTypes = {
98
67
  type: String,
99
68
  default: 'Add Employee',
100
69
  },
70
+ hideAddButton: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
101
74
  };
102
75
 
103
76
  export const calendarEmitTypes = {};
@@ -1,231 +1,200 @@
1
1
  <template>
2
- <SprCard :has-content-padding="false">
3
- <template #header>
4
- <div :class="[getCalendarClasses.borderClasses, getCalendarClasses.headerWrapper]">
5
- <div class="spr-flex spr-items-center spr-gap-size-spacing-3xs">
6
- <div class="spr-flex">
7
- <spr-button variant="tertiary" has-icon @click="prevWeek">
8
- <Icon icon="ph:caret-left-fill" class="spr-text-color-success-base" />
9
- </spr-button>
10
- <spr-button variant="tertiary" has-icon @click="nextWeek">
11
- <Icon icon="ph:caret-right-fill" class="spr-text-color-success-base" />
12
- </spr-button>
13
- </div>
14
- <h2 class="spr-heading-xs">{{ weekRangeDisplay }}</h2>
15
- </div>
16
-
17
- <spr-button variant="secondary" size="small" @click="goToToday"> Today </spr-button>
18
- </div>
19
- </template>
20
-
2
+ <SprCard :has-content-padding="false" class="spr-flex spr-h-full spr-flex-col spr-overflow-hidden">
21
3
  <template #content>
22
4
  <div :class="getCalendarClasses.contentWrapper">
23
- <!-- Filters -->
24
- <slot name="filter">
25
- <div :class="getCalendarClasses.calendarFilter">
26
- <spr-input v-model="searchTerm" placeholder="Search Employee">
27
- <template #icon>
28
- <Icon icon="ph:magnifying-glass" />
29
- </template>
30
- </spr-input>
31
-
32
- <spr-dropdown
33
- id="company-dropdown"
34
- v-model="companyDropDown"
35
- :menu-list="companyOptions"
36
- @update:model-value="(e) => handleFilter('company', e[0])"
37
- >
38
- <!-- @update:model-value="(e) => (selectedCompany = e[0])" -->
39
- <spr-input v-model="selectedCompany" placeholder="Company">
40
- <template #icon>
41
- <Icon icon="ph:caret-down" />
42
- </template>
43
- </spr-input>
44
- </spr-dropdown>
45
-
46
- <spr-dropdown
47
- id="department-dropdown"
48
- v-model="departmentDropDown"
49
- :menu-list="departmentOptions"
50
- @update:model-value="(e) => handleFilter('department', e[0])"
51
- >
52
- <spr-input v-model="selectedDepartment" placeholder="Department">
53
- <template #icon>
54
- <Icon icon="ph:caret-down" />
55
- </template>
56
- </spr-input>
57
- </spr-dropdown>
58
-
59
- <spr-dropdown
60
- id="branch-dropdown"
61
- v-model="branchDropDown"
62
- :menu-list="branchOptions"
63
- @update:model-value="(e) => handleFilter('branch', e[0])"
64
- >
65
- <spr-input v-model="selectedBranch" placeholder="Branch">
66
- <template #icon>
67
- <Icon icon="ph:caret-down" />
68
- </template>
69
- </spr-input>
70
- </spr-dropdown>
5
+ <div :class="[getCalendarClasses.headerWrapper]">
6
+ <div class="spr-flex spr-items-center spr-justify-center spr-gap-size-spacing-3xs">
7
+ <div class="spr-flex">
8
+ <spr-button variant="tertiary" has-icon @click="prevWeek">
9
+ <Icon icon="ph:caret-left-fill" class="spr-text-color-success-base" />
10
+ </spr-button>
11
+ <spr-button variant="tertiary" has-icon @click="nextWeek">
12
+ <Icon icon="ph:caret-right-fill" class="spr-text-color-success-base" />
13
+ </spr-button>
14
+ </div>
15
+ <div class="spr-heading-xs">{{ weekRangeDisplay }}</div>
71
16
  </div>
72
- </slot>
73
17
 
74
- <div class="spr-table-wrapper">
75
- <table :class="getCalendarClasses.calendarTable">
76
- <!-- Calendar Header -->
77
- <thead>
78
- <tr>
79
- <th :class="[getCalendarClasses.borderClasses, getCalendarClasses.tableHeaderEmployeeName]">
80
- Employee Name
81
- </th>
82
- <th
83
- v-for="(date, index) in weekDates"
84
- :key="index"
85
- :class="[getCalendarClasses.borderClasses, getCalendarClasses.tableHeader]"
86
- >
87
- <div :class="getCalendarClasses.headerContent">
88
- <div
89
- :class="[
90
- getCalendarClasses.headerDate,
91
- {
92
- 'spr-background-color-brand-base spr-text-color-inverted-strong': isToday(date),
93
- },
94
- ]"
95
- >
96
- {{ formatDate(date, 'DD') }}
18
+ <spr-button variant="secondary" size="large" @click="goToToday"> Today </spr-button>
19
+ </div>
20
+ <!-- Filters -->
21
+ <slot name="filter" />
22
+
23
+ <div ref="tableBodyRef" class="spr-table-wrapper spr-h-[calc(100vh-12rem)] spr-w-full spr-overflow-auto">
24
+ <div class="spr-pb-size-spacing-lg">
25
+ <table aria-describedby="calendar" :class="[getCalendarClasses.calendarTable, 'spr-relative']">
26
+ <!-- Calendar Header -->
27
+ <thead class="spr-bg-white spr-sticky spr-top-0 spr-z-20">
28
+ <tr>
29
+ <th :class="[getCalendarClasses.tableHeaderEmployeeName, 'spr-sticky spr-left-0']">
30
+ <div :class="getCalendarClasses.headerContent">
31
+ <div>Employee Name</div>
32
+ <div
33
+ :class="['spr-flex spr-cursor-pointer spr-flex-row spr-items-center spr-p-size-spacing-6xs']"
34
+ @click="handleSorting"
35
+ >
36
+ <Icon :icon="getSortIcon" height="16" width="16" :class="[{ 'spr-text-kangkong-700': sort }]" />
37
+ </div>
97
38
  </div>
98
- <div class="spr-body-sm-regular">
99
- {{ formatDate(date, 'ddd').toUpperCase() }}
39
+ </th>
40
+ <th
41
+ v-for="(date, index) in weekDates"
42
+ :key="index"
43
+ :class="[getCalendarClasses.borderClasses, getCalendarClasses.tableHeader]"
44
+ >
45
+ <div :class="getCalendarClasses.headerContent">
46
+ <div
47
+ :class="[
48
+ getCalendarClasses.headerDate,
49
+ {
50
+ 'spr-background-color-brand-base spr-text-color-inverted-strong': isToday(date),
51
+ },
52
+ ]"
53
+ >
54
+ {{ formatDate(date, 'DD') }}
55
+ </div>
56
+ <div class="spr-body-sm-regular">
57
+ {{ formatDate(date, 'ddd').toUpperCase() }}
58
+ </div>
100
59
  </div>
101
- </div>
102
- </th>
103
- </tr>
104
- </thead>
105
- <tbody v-if="employees.length > 0" class="spr-overflow-y-auto">
106
- <tr v-for="employee in employees" :key="employee.id">
107
- <td
108
- :class="[
109
- getCalendarClasses.borderClasses,
110
- 'spr-content-start spr-border-y spr-border-b-0 spr-border-l-0 spr-border-r spr-p-size-spacing-xs',
111
- ]"
112
- >
113
- <div class="spr-flex spr-flex-col spr-gap-size-spacing-3xs spr-overflow-hidden">
114
- <spr-avatar
115
- :src="employee.avatar"
116
- :initial="employee.name"
117
- size="md"
118
- :variant="employee.avatar ? 'image' : 'initial'"
119
- color="tertiary"
120
- />
121
- <div class="spr-label-xs-regular">{{ employee.name }}</div>
122
- <div class="spr-text-color-supporting spr-label-xs-regular spr-uppercase">
123
- {{ employee.position }}
60
+ </th>
61
+ </tr>
62
+ </thead>
63
+ <tbody v-if="employees.length > 0 && !loading" class="spr-overflow-y-auto">
64
+ <tr v-for="employee in employees" :key="employee.id">
65
+ <td
66
+ :class="[
67
+ getCalendarClasses.borderClasses,
68
+ 'spr-bg-white spr-sticky spr-left-0 spr-z-10 spr-content-start spr-border-y spr-border-b-0 spr-border-l-0 spr-border-r spr-p-size-spacing-xs',
69
+ ]"
70
+ >
71
+ <div class="spr-flex spr-flex-col spr-gap-size-spacing-3xs spr-overflow-hidden">
72
+ <spr-avatar
73
+ :src="employee.avatar"
74
+ :initial="employee.name"
75
+ size="md"
76
+ :variant="employee.avatar ? 'image' : 'initial'"
77
+ color="tertiary"
78
+ />
79
+ <div class="spr-label-xs-regular">{{ employee.name }}</div>
80
+ <div class="spr-text-color-supporting spr-label-xs-regular spr-uppercase">
81
+ {{ employee.position }}
82
+ </div>
124
83
  </div>
125
- </div>
126
- <div class="spr-mt-size-spacing-xs">
127
- <spr-lozenge
128
- :label="`${employee.hoursWorked || 0}/${employee.hoursTarget || 48} HRS`"
129
- tone="neutral"
130
- >
131
- <template #icon>
132
- <Icon icon="ph:clock" />
133
- </template>
134
- </spr-lozenge>
135
- </div>
136
- </td>
137
- <td
138
- v-for="(date, index) in weekDates"
139
- :key="index"
140
- :class="[
141
- getCalendarClasses.borderClasses,
142
- 'spr-content-start spr-space-y-size-spacing-3xs spr-border-x spr-border-b-0 spr-border-t spr-p-size-spacing-sm last:spr-border-r-0',
143
- ]"
144
- @mouseover="handleHover(true, index, employee.id)"
145
- @mouseleave="handleHover(false, index, employee.id)"
146
- >
147
- <section
148
- v-if="
149
- employee.schedule[formatDate(date, dateFormat)] &&
150
- employee.schedule[formatDate(date, dateFormat)].length > 0
151
- "
152
- class="spr-flex spr-flex-col spr-justify-start spr-gap-size-spacing-3xs"
84
+ <div class="spr-mt-size-spacing-xs">
85
+ <spr-lozenge
86
+ :label="`${employee.hoursWorked || 0}/${employee.hoursTarget || 48} HRS`"
87
+ tone="neutral"
88
+ >
89
+ <template #icon>
90
+ <Icon icon="ph:clock" />
91
+ </template>
92
+ </spr-lozenge>
93
+ </div>
94
+ </td>
95
+ <td
96
+ v-for="(date, index) in weekDates"
97
+ :key="index"
98
+ :class="[
99
+ getCalendarClasses.borderClasses,
100
+ 'spr-min-w-[180px] spr-content-start spr-space-y-size-spacing-3xs spr-border-x spr-border-b-0 spr-border-t spr-p-size-spacing-sm last:spr-mb-size-spacing-lg last:spr-border-r-0',
101
+ ]"
102
+ @mouseover="handleHover(true, index, employee.id)"
103
+ @mouseleave="handleHover(false, index, employee.id)"
153
104
  >
154
- <div
155
- v-for="(schedule, scheduleIndex) in employee.schedule[formatDate(date, dateFormat)]"
156
- :key="scheduleIndex"
105
+ <section
106
+ v-if="
107
+ employee.schedule[formatDate(date, dateFormat)] &&
108
+ employee.schedule[formatDate(date, dateFormat)].length > 0
109
+ "
110
+ class="spr-flex spr-flex-col spr-justify-start spr-gap-size-spacing-3xs"
157
111
  >
158
112
  <div
159
- v-if="schedule.type === 'restday'"
160
- @click="
161
- onShiftClick({
162
- employeeId: employee.id,
163
- date: formatDate(date, dateFormat),
164
- shift: 'restday',
165
- })
166
- "
113
+ v-for="(schedule, scheduleIndex) in employee.schedule[formatDate(date, dateFormat)]"
114
+ :key="scheduleIndex"
167
115
  >
168
- <spr-calendar-cell type="restday" />
116
+ <div
117
+ v-if="schedule.type === 'restday'"
118
+ @click="
119
+ onCellClick({
120
+ employeeId: employee.id,
121
+ date: formatDate(date, dateFormat),
122
+ shift: 'restday',
123
+ })
124
+ "
125
+ >
126
+ <spr-calendar-cell type="restday" />
127
+ </div>
128
+ <div
129
+ v-else
130
+ @click="
131
+ onCellClick({
132
+ employeeId: employee.id,
133
+ date: formatDate(date, dateFormat),
134
+ shift: schedule,
135
+ })
136
+ "
137
+ >
138
+ <spr-calendar-cell
139
+ :view-only="false"
140
+ :title="`${schedule.startTime} - ${schedule.endTime}`"
141
+ :description="schedule.location"
142
+ :sub-description="schedule.type"
143
+ />
144
+ </div>
169
145
  </div>
170
- <div
171
- v-else
172
- @click="
173
- onShiftClick({
174
- employeeId: employee.id,
175
- date: formatDate(date, dateFormat),
176
- shift: schedule,
177
- })
146
+ </section>
147
+
148
+ <section v-if="showAddShift(index, employee.id)">
149
+ <spr-calendar-cell
150
+ status="pending"
151
+ type="exempt"
152
+ :view-only="false"
153
+ @on-click="
154
+ onCellClick({ employeeId: employee.id, date: formatDate(date, dateFormat), shift: null })
178
155
  "
179
156
  >
180
- <spr-calendar-cell
181
- :view-only="false"
182
- :title="`${schedule.startTime} - ${schedule.endTime}`"
183
- :description="schedule.location"
184
- :sub-description="schedule.type"
185
- />
157
+ <template #prefix>
158
+ <Icon icon="ph:plus" />
159
+ </template>
160
+ <div class="spr-label-xs-medium">Add New Shift</div>
161
+ </spr-calendar-cell>
162
+ </section>
163
+ </td>
164
+ </tr>
165
+ </tbody>
166
+ <tbody v-else>
167
+ <tr v-if="!loading" class="spr-h-full">
168
+ <td :colspan="weekDates.length + 1" class="spr-flex spr-h-full spr-items-center spr-justify-center">
169
+ <slot name="empty-state">
170
+ <SprEmptyState
171
+ size="large"
172
+ :description="emptyStateTitle"
173
+ :sub-description="emptyStateDescription"
174
+ >
175
+ <template v-if="emptyStateButtonText" #button>
176
+ <spr-button tone="success"
177
+ ><Icon icon="ph:plus" @click="$emit('onClickEmptyButton')" />{{
178
+ emptyStateButtonText
179
+ }}</spr-button
180
+ >
181
+ </template>
182
+ </SprEmptyState>
183
+ </slot>
184
+ </td>
185
+ </tr>
186
+ <tr v-else>
187
+ <td :colspan="weekDates.length + 1" class="spr-h-[360px] spr-overflow-hidden">
188
+ <slot name="loading">
189
+ <div class="spr-flex spr-items-center spr-justify-center">
190
+ <Icon size="48" icon="svg-spinners:ring-resize" class="spr-text-color-success-base" />
186
191
  </div>
187
- </div>
188
- </section>
189
-
190
- <section v-if="showAddShift(index, employee.id)">
191
- <spr-calendar-cell
192
- status="pending"
193
- type="exempt"
194
- :view-only="false"
195
- @on-click="
196
- onShiftClick({ employeeId: employee.id, date: formatDate(date, dateFormat), shift: null })
197
- "
198
- >
199
- <template #prefix>
200
- <Icon icon="ph:plus" />
201
- </template>
202
- <div class="spr-label-xs-medium">Add New Shift</div>
203
- </spr-calendar-cell>
204
- </section>
205
- </td>
206
- </tr>
207
- </tbody>
208
- <tbody v-else>
209
- <tr v-if="!loading" class="spr-h-full">
210
- <td :colspan="weekDates.length + 1" class="spr-flex spr-h-full spr-items-center spr-justify-center">
211
- <slot name="empty-state">
212
- <SprEmptyState size="large" :description="emptyStateTitle" :sub-description="emptyStateDescription">
213
- <template v-if="emptyStateButtonText" #button>
214
- <spr-button tone="success"><Icon icon="ph:plus" />{{ emptyStateButtonText }}</spr-button>
215
- </template>
216
- </SprEmptyState>
217
- </slot>
218
- </td>
219
- </tr>
220
- <tr v-else>
221
- <td :colspan="weekDates.length + 1" class="spr-overflow-hidden">
222
- <slot name="loading">
223
- <div class="spr-flex spr-items-center spr-justify-center">Loading...</div>
224
- </slot>
225
- </td>
226
- </tr>
227
- </tbody>
228
- </table>
192
+ </slot>
193
+ </td>
194
+ </tr>
195
+ </tbody>
196
+ </table>
197
+ </div>
229
198
  </div>
230
199
  </div>
231
200
  </template>
@@ -235,10 +204,8 @@
235
204
  <script setup lang="ts">
236
205
  import { Icon } from '@iconify/vue';
237
206
  import SprButton from '@/components/button/button.vue';
238
- import SprInput from '@/components/input/input.vue';
239
207
  import SprAvatar from '@/components/avatar/avatar.vue';
240
208
  import SprCard from '@/components/card/card.vue';
241
- import SprDropdown from '@/components/dropdown/dropdown.vue';
242
209
  import SprLozenge from '@/components/lozenge/lozenge.vue';
243
210
  import SprCalendarCell from '@/components/calendar-cell/calendar-cell.vue';
244
211
  import SprEmptyState from '@/components/empty-state/empty-state.vue';
@@ -250,26 +217,22 @@ const emit = defineEmits(calendarEmitTypes);
250
217
 
251
218
  const {
252
219
  // State
253
- searchTerm,
254
- selectedCompany,
255
- selectedDepartment,
256
- selectedBranch,
257
220
  weekDates,
258
221
  weekRangeDisplay,
259
222
  getCalendarClasses,
260
- companyDropDown,
261
- departmentDropDown,
262
- branchDropDown,
263
223
  dateFormat,
224
+ getSortIcon,
225
+ sort,
226
+ tableBodyRef,
264
227
  // Function
265
228
  formatDate,
266
229
  isToday,
267
230
  prevWeek,
268
231
  nextWeek,
269
232
  goToToday,
270
- onShiftClick,
233
+ onCellClick,
271
234
  handleHover,
272
235
  showAddShift,
273
- handleFilter,
236
+ handleSorting,
274
237
  } = useCalendar(props, emit);
275
238
  </script>