diff --git a/client/.env b/client/.env index 7d7b7b0a..c41e9942 100644 --- a/client/.env +++ b/client/.env @@ -1,5 +1,5 @@ # Client Related -VITE_TITLE="CSHR" +VITE_TITLE="Codescalers HR management system" VITE_FAVICON="https://placehold.co/32/steelblue/white?text=CS" VITE_LOGO="https://placehold.co/128/steelblue/white?text=CS" diff --git a/client/package.json b/client/package.json index e7c8c681..aafe65df 100644 --- a/client/package.json +++ b/client/package.json @@ -14,10 +14,17 @@ "format": "prettier --write src/" }, "dependencies": { + "@fullcalendar/core": "^6.1.9", + "@fullcalendar/daygrid": "^6.1.9", + "@fullcalendar/interaction": "^6.1.9", + "@fullcalendar/list": "^6.1.9", + "@fullcalendar/timegrid": "^6.1.9", + "@fullcalendar/vue3": "^6.1.9", "@mdi/font": "^7.4.47", "@vueuse/core": "^10.7.1", "@vueuse/router": "^10.7.1", "axios": "^1.6.3", + "calendar_options": "link:@fullcalendar/core/calendar_options", "moment": "^2.30.1", "pinia": "^2.1.7", "vue": "^3.3.11", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 1523484b..8a343eca 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -5,6 +5,24 @@ settings: excludeLinksFromLockfile: false dependencies: + '@fullcalendar/core': + specifier: ^6.1.9 + version: 6.1.10 + '@fullcalendar/daygrid': + specifier: ^6.1.9 + version: 6.1.10(@fullcalendar/core@6.1.10) + '@fullcalendar/interaction': + specifier: ^6.1.9 + version: 6.1.10(@fullcalendar/core@6.1.10) + '@fullcalendar/list': + specifier: ^6.1.9 + version: 6.1.10(@fullcalendar/core@6.1.10) + '@fullcalendar/timegrid': + specifier: ^6.1.9 + version: 6.1.10(@fullcalendar/core@6.1.10) + '@fullcalendar/vue3': + specifier: ^6.1.9 + version: 6.1.10(@fullcalendar/core@6.1.10)(vue@3.4.3) '@mdi/font': specifier: ^7.4.47 version: 7.4.47 @@ -17,6 +35,9 @@ dependencies: axios: specifier: ^1.6.3 version: 1.6.3 + calendar_options: + specifier: link:@fullcalendar/core/calendar_options + version: link:@fullcalendar/core/calendar_options moment: specifier: ^2.30.1 version: 2.30.1 @@ -374,6 +395,55 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} dev: true + /@fullcalendar/core@6.1.10: + resolution: {integrity: sha512-oTXGJSAGpCf1oY+CKp5qYjMHkJCPBkJ3SHitl63n8Q6xKeiwQ4EF6Au451euUovREwJpLmD1AyZrCnWmtB9AVg==} + dependencies: + preact: 10.12.1 + dev: false + + /@fullcalendar/daygrid@6.1.10(@fullcalendar/core@6.1.10): + resolution: {integrity: sha512-Z4GRm1IyHKgxXFTWGcEI0nTsvYOIkpE0aMt3/o3ER2SZkF+hfwcDFhtj0c9+WhMjXFIWYeoTnA9rUOY7Zl/nxA==} + peerDependencies: + '@fullcalendar/core': ~6.1.10 + dependencies: + '@fullcalendar/core': 6.1.10 + dev: false + + /@fullcalendar/interaction@6.1.10(@fullcalendar/core@6.1.10): + resolution: {integrity: sha512-aZRlwCpmDasq2RNeWV0ub20Uevare9Cb6iMlxCacx0fhOC14H28G9d1FsduJIecInL84SPGwt5ItqAYMsWv7zw==} + peerDependencies: + '@fullcalendar/core': ~6.1.10 + dependencies: + '@fullcalendar/core': 6.1.10 + dev: false + + /@fullcalendar/list@6.1.10(@fullcalendar/core@6.1.10): + resolution: {integrity: sha512-WE4vuSUCzol4tJd0ZP0cNxeyRPaZcsVVYs2I3qdf3OZQkXwDCdSyWEz0Hluf+XZWcZXt21aEYKlxRjwUpQcf4Q==} + peerDependencies: + '@fullcalendar/core': ~6.1.10 + dependencies: + '@fullcalendar/core': 6.1.10 + dev: false + + /@fullcalendar/timegrid@6.1.10(@fullcalendar/core@6.1.10): + resolution: {integrity: sha512-hFKyQXJaPbNyq1reZmvkCmM64O99krHoIcJAbDS+dntCm3FzZUcDtAcRKIbMiantHrezCG/1MEYk3m9e3aKvIQ==} + peerDependencies: + '@fullcalendar/core': ~6.1.10 + dependencies: + '@fullcalendar/core': 6.1.10 + '@fullcalendar/daygrid': 6.1.10(@fullcalendar/core@6.1.10) + dev: false + + /@fullcalendar/vue3@6.1.10(@fullcalendar/core@6.1.10)(vue@3.4.3): + resolution: {integrity: sha512-YMYBQx0TlWNuN4G6ra2dkf5cCF5aVi/2zDLGLvLqe2Nk2o7uNbTkrCSG40061OepWQlJv+hYqm1JukLRmyqi4Q==} + peerDependencies: + '@fullcalendar/core': ~6.1.10 + vue: ^3.0.11 + dependencies: + '@fullcalendar/core': 6.1.10 + vue: 3.4.3(typescript@5.3.3) + dev: false + /@humanwhocodes/config-array@0.11.13: resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} @@ -2361,6 +2431,10 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /preact@10.12.1: + resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==} + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} diff --git a/client/src/clients/api/base.ts b/client/src/clients/api/base.ts index 545ed7a7..05926723 100644 --- a/client/src/clients/api/base.ts +++ b/client/src/clients/api/base.ts @@ -90,11 +90,25 @@ export abstract class ApiClientBase { if (access_token && refresh_token && !isValidToken(access_token) && isValidToken(refresh_token)) { await ApiClientBase.$api.auth.refresh({ refresh: refresh_token }) } + if (err && !ApiClientBase.USER && access_token !== null && refresh_token !== null) { + const user = await ApiClientBase.$api.myprofile.getUser(); + ApiClientBase.USER = {...user, access_token, refresh_token} + } + + if (err) { + + options.disableNotify !== true && ApiClientBase.$notifier?.notify({ + type: 'error', + description: options.normalizeError?.(err, res) ?? ApiClientBase.normalizeError(err) ?? err + }) + + panic(err) + } if ( (res.config.method === 'post' || res.config.method === 'put') && typeof res.data === 'object' && - 'message' in (res.data || {}) + 'message' in (res.data || {}) && options.disableNotify !== true ) { ApiClientBase.$notifier?.notify({ type: 'success', diff --git a/client/src/clients/api/event.ts b/client/src/clients/api/event.ts index 99c933be..3a757a1c 100644 --- a/client/src/clients/api/event.ts +++ b/client/src/clients/api/event.ts @@ -12,9 +12,22 @@ export class EventApi extends ApiClientBase { this.exact = new EventExactApi(options, this.path) } - async list() {} + async list(query?: any) { + return this.unwrap(this.$http.get>(this.getUrl('', query)), { + transform: (d) => d.results + }) + } + + async create(input: Api.Inputs.Event) { + ApiClientBase.assertUser() + const event = await this.unwrap( + this.$http.post(this.getUrl(''), input), + { transform: (d) => d.results } + ) + + return event + } - async create() {} async read(id: number) {} diff --git a/client/src/clients/api/home.ts b/client/src/clients/api/home.ts index 7c3ff26a..90afdfac 100644 --- a/client/src/clients/api/home.ts +++ b/client/src/clients/api/home.ts @@ -1,7 +1,14 @@ +import type { Api } from '@/types' import { ApiClientBase } from './base' export class HomeApi extends ApiClientBase { protected readonly path = '/home' - async list() {} + async list(query?: any) { + return this.unwrap(this.$http.get>(this.getUrl('', query)), { + transform: (d) => d.results + }) + } + + } diff --git a/client/src/clients/api/meeting.ts b/client/src/clients/api/meeting.ts index d6593ae2..595b9432 100644 --- a/client/src/clients/api/meeting.ts +++ b/client/src/clients/api/meeting.ts @@ -12,9 +12,20 @@ export class MeetingApi extends ApiClientBase { this.exact = new MeetingExactApi(options) } - async list() {} - - async create() {} + list(query?: any) { + return this.unwrap(this.$http.get>(this.getUrl('', query)), { + transform: (d) => d.results + }) + } + async create(input: Api.Inputs.Meeting) { + ApiClientBase.assertUser() + const event = await this.unwrap( + this.$http.post(this.getUrl(''),input), + { transform: (d) => d.results } + ) + + return event + } async read(id: number) {} diff --git a/client/src/clients/api/users.ts b/client/src/clients/api/users.ts index c2b9d7a5..5294c121 100644 --- a/client/src/clients/api/users.ts +++ b/client/src/clients/api/users.ts @@ -33,7 +33,6 @@ export class UsersApi extends ApiClientBase { ...options }) } - read(id: number) { return this.unwrap(this.$http.get>(this.getUrl(`/${id}`)), { transform: (d) => d.results diff --git a/client/src/clients/api/vacations.ts b/client/src/clients/api/vacations.ts index 382f6f6a..9c96d4a5 100644 --- a/client/src/clients/api/vacations.ts +++ b/client/src/clients/api/vacations.ts @@ -36,27 +36,52 @@ export class VacationsApi extends ApiClientBase { ) return userBalance } - async list() {} + async list(query?: any) { + return this.unwrap(this.$http.get>(this.getUrl('', query)), { + transform: (d) => d.results + }) + } + // async list() {} - async create() {} + async create(input: Api.Inputs.Leave) { + ApiClientBase.assertUser() + const vacation = await this.unwrap( + this.$http.post(this.getUrl(''), input), + { transform: (d) => d.results } + ) + return vacation + } async read(id: number) {} - async delete(id: number) {} + + async delete(id: number) { + return this.unwrap(this.$http.delete(this.getUrl(`/${id}`)), { + transform: (d) => d.results + }) + } } class VacationsApproveApi extends ApiClientBase { protected readonly path = '/approve' - async update(id: number) {} -} + async update(id: any) { + return this.unwrap(this.$http.put(this.getUrl(`/${id}`)), { + transform: (d) => d.results + }) + }} class VacationsRejectApi extends ApiClientBase { protected readonly path = '/reject' async read(id: number) {} + - async update(id: number) {} + async update(id: number) { + return this.unwrap(this.$http.put(this.getUrl(`/${id}`)), { + transform: (d) => d.results + }) + } } class VacationsBalanceApi extends ApiClientBase { @@ -95,7 +120,16 @@ class VacationsBalanceAdjustmentApi extends ApiClientBase { class VacationsCalculateApi extends ApiClientBase { protected readonly path = '/calculate' - async list() {} + + + async list(query: Api.Inputs.ActualDays) { + ApiClientBase.assertUser() + const calculation = await this.unwrap( + this.$http.get(this.getUrl('', query)), + { transform: (d) => d.results } + ) + return calculation + } } class VacationsCommentApi extends ApiClientBase { @@ -109,7 +143,16 @@ class VacationsEditApi extends ApiClientBase { async read(id: number) {} - async update(id: number) {} + async update(id: number, input: Api.Inputs.Leave) { + ApiClientBase.assertUser() + const vacation = await this.unwrap( + this.$http.put(this.getUrl(`/${id}`), input), + { transform: (d) => d.results } + ) + + return vacation + } + // async update(id: number) {} } class VacationsGetAdminBalanceApi extends ApiClientBase { @@ -140,5 +183,10 @@ class VacationsPostAdminBalanceApi extends ApiClientBase { class VacationsUserApi extends ApiClientBase { protected readonly path = '/user' - async list() {} + async list(query?: any) { + return this.unwrap(this.$http.get>(this.getUrl('', query)), { + transform: (d) => d.results + }) + } + } diff --git a/client/src/components/CshrToolbar.vue b/client/src/components/CshrToolbar.vue index de981177..cf32cdaa 100644 --- a/client/src/components/CshrToolbar.vue +++ b/client/src/components/CshrToolbar.vue @@ -45,16 +45,7 @@ @@ -79,10 +70,11 @@ import { useApi } from '@/hooks' import { getStatusColor } from '@/utils' import type { Api } from '@/types' import NotificationDetailsDialog from './NotificationDetailsDialog.vue' +import profileImage from './profileImage.vue' export default { name: 'CshrToolbar', - components: { NotificationDetailsDialog }, + components: { NotificationDetailsDialog, profileImage }, setup() { const $api = useApi() const user = useAsyncState(() => $api.myprofile.getUser(), null) diff --git a/client/src/components/calender.vue b/client/src/components/calender.vue new file mode 100644 index 00000000..c507dda9 --- /dev/null +++ b/client/src/components/calender.vue @@ -0,0 +1,400 @@ + + + diff --git a/client/src/components/calenderRequest.vue b/client/src/components/calenderRequest.vue new file mode 100644 index 00000000..834ede66 --- /dev/null +++ b/client/src/components/calenderRequest.vue @@ -0,0 +1,79 @@ + + + diff --git a/client/src/components/cards/birthdayCard.vue b/client/src/components/cards/birthdayCard.vue new file mode 100644 index 00000000..989556c5 --- /dev/null +++ b/client/src/components/cards/birthdayCard.vue @@ -0,0 +1,73 @@ + + diff --git a/client/src/components/cards/eventCard.vue b/client/src/components/cards/eventCard.vue new file mode 100644 index 00000000..2ddcf094 --- /dev/null +++ b/client/src/components/cards/eventCard.vue @@ -0,0 +1,76 @@ + + diff --git a/client/src/components/cards/holidayCard.vue b/client/src/components/cards/holidayCard.vue new file mode 100644 index 00000000..89c3fe93 --- /dev/null +++ b/client/src/components/cards/holidayCard.vue @@ -0,0 +1,50 @@ + + + + diff --git a/client/src/components/cards/meetingCard.vue b/client/src/components/cards/meetingCard.vue new file mode 100644 index 00000000..56119b30 --- /dev/null +++ b/client/src/components/cards/meetingCard.vue @@ -0,0 +1,74 @@ + + diff --git a/client/src/components/cards/vacationCard.vue b/client/src/components/cards/vacationCard.vue new file mode 100644 index 00000000..ebb576e1 --- /dev/null +++ b/client/src/components/cards/vacationCard.vue @@ -0,0 +1,279 @@ + + + diff --git a/client/src/components/profileImage.vue b/client/src/components/profileImage.vue index dcb0c983..3b7db86f 100644 --- a/client/src/components/profileImage.vue +++ b/client/src/components/profileImage.vue @@ -1,13 +1,28 @@ @@ -17,7 +32,7 @@ import { computed } from 'vue'; export default { name: 'profileImage', - props: ["user"], + props: ["user", "withLink"], setup(props) { const imageSrc = window.env.SERVER_DOMAIN_NAME_API.replace("api", "") diff --git a/client/src/components/requests/eventRequest.vue b/client/src/components/requests/eventRequest.vue new file mode 100644 index 00000000..ddc001c3 --- /dev/null +++ b/client/src/components/requests/eventRequest.vue @@ -0,0 +1,147 @@ + + + diff --git a/client/src/components/requests/leaveRequest.vue b/client/src/components/requests/leaveRequest.vue new file mode 100644 index 00000000..d637abe9 --- /dev/null +++ b/client/src/components/requests/leaveRequest.vue @@ -0,0 +1,128 @@ + + + diff --git a/client/src/components/requests/meetingRequest.vue b/client/src/components/requests/meetingRequest.vue new file mode 100644 index 00000000..f560d80d --- /dev/null +++ b/client/src/components/requests/meetingRequest.vue @@ -0,0 +1,108 @@ + + + diff --git a/client/src/types/api.ts b/client/src/types/api.ts index e5fb225b..2d76f0e1 100644 --- a/client/src/types/api.ts +++ b/client/src/types/api.ts @@ -20,16 +20,56 @@ export module Api { export interface UnwrapOptions { transform?: (data: T, res: AxiosResponse) => R normalizeError?: (error: AxiosError, res: AxiosResponse) => string + disableNotify?: boolean } export interface Skill { name: string } + export interface Vacation { + id: number + created_at: any + modified_at: any + type: string + status: string + reason: string + from_date: any + end_date: any + change_log: any[] + actual_days: number + applying_user: number | any + approval_user: number + user?: Api.User + isUpdated? : boolean + } + + export interface LeaveReason { + name: string + reason: string + } + export interface Certificate {} export interface Salary {} - + export interface Meetings { + id: number + invited_users: any[] + date: any + meeting_link: string + host_user: { + id: number + full_name: string + email: string + image: string + team: string + gender: string + skills: [] + job_title: string + user_certificates: any[] + } + location: string + } export interface User { id: number first_name: string @@ -98,8 +138,12 @@ export module Api { message: string results: T } + export type Event = MsgRes + export type Meeting = MsgRes export type Profile = MsgRes + export type AllMeetings = MsgRes + export type Login = MsgRes<{ id: number email: string @@ -110,6 +154,41 @@ export module Api { last_name: string }> + export type LeaveRequest = MsgRes<{ + title: string + className: string + eventName: string + vacation: { + id: number + reason: string + from_date: any + end_date: any + status: string + applying_user: { + id: number + full_name: string + email: string + image: string + team: string + gender: string + skills: [] + job_title: string + user_certificates: [] + } + approval_user: { + email: string + team: string + gender: string + job_title: string + } + change_log: {} + type: string + } + + len: number + date: any + }> + export type Balance = MsgRes export interface Register { @@ -162,7 +241,57 @@ export module Api { } } } + export interface Holiday { + id: number + location: { + id: number + name: string + country: string + weekend: string + } + holiday_date: any + expired: boolean + } + + export interface Home { + id: number + title: string + className: string + eventName: string + vacation?: any + meeting? :any + event?: any + holidays?: any + users?:any + date: any + len?: number + } + export module Inputs { + export interface Event { + name: string + description: string + from_date: any + end_date: any + } + + export interface Leave { + reason: string | undefined + from_date: any + end_date: any + actual_days?: number + } + + export interface ActualDays { + start_date: any + end_date: any + } + export interface Meeting { + date: any + meeting_link: string + location: string + } + export interface Login { email: string password: string diff --git a/client/src/utils/helpers.ts b/client/src/utils/helpers.ts index 51f5a2dd..fb801512 100644 --- a/client/src/utils/helpers.ts +++ b/client/src/utils/helpers.ts @@ -1,5 +1,6 @@ import moment from 'moment' +import type { Api } from "@/types" import type { JWTokenObject } from "@/types" export async function resolve(promise: Promise): Promise<[T, any]> { @@ -30,6 +31,108 @@ export const DASHBOARD_ITEMS = [ export const formatDate = (date: any) => moment(date).format('YYYY-MM-DD') +export const fieldRequired = [(v: string) => !!v || 'Field is required.'] + +export function handelDates(start: any, end: any): any { + const dates = { + start, + end, + add: true, + cut: null, + endStr: null as null | string | Date, + startStr: null as null | string | Date + } + + const endDate = new Date(dates.end || '') + if (dates.cut) { + endDate.setDate(endDate.getDate() - 1) + } else if (dates.add) { + endDate.setDate(endDate.getDate() + 1) + } + + dates.end = endDate + dates.start = new Date(dates.start) + + const endStr = `${endDate.getFullYear()}-${endDate.getMonth() + 1}-${endDate.getDate()}` + const startStr = `${dates.start.getFullYear()}-${ + dates.start.getMonth() + 1 + }-${dates.start.getDate()}` + + dates.endStr = endStr + dates.startStr = startStr + return dates +} + + +export function normalizeEvent(e: Api.Inputs.Event): any { + const dates = handelDates(e.from_date, e.end_date) + + return { + title: 'Event', + classNames: ['cshr-event'], + color: 'primary', + start: dates.start, + end: dates.end, + backgroundColor: 'primary', + id: e.name, + allDay: true + } +} +export function normalizeVacation(v: Api.Vacation) { + const dates = handelDates(v.from_date, v.end_date) + + return { + title: `${v.user!.full_name}'s Vacation`, + color: 'primary', + start: dates.start, + end: dates.end, + backgroundColor: 'gray', + id: v.id.toString(), + allDay: true + } +} + +export function normalizeHoliday(h: Api.Holiday) { + const dates = handelDates(h.holiday_date, h.holiday_date) + + return { + title: `Public Holiday`, + color: 'primary', + start: dates.start, + end: dates.end, + backgroundColor: 'gray', + id: h.id.toString(), + allDay: true + } +} +export function normalizedBirthday(u: Api.User) { + const dates = handelDates(u.birthday, u.birthday) + + return { + title: `Birthday`, + color: 'primary', + start: dates.start, + end: dates.end, + backgroundColor: 'gray', + id: u.id.toString(), + allDay: true + } +} + + +export function normalizeMeeting(m: Api.Meetings): any { + const dates = handelDates(m.date, m.date) + + return { + title: 'Meeting', + color: 'secondary', + start: dates.start, + end: dates.end, + backgroundColor: 'primary', + id: m.id, + allDay: true + } +} export function getStatusColor(status: string) { switch (status) { case 'vacations': diff --git a/client/src/views/CalendarView.vue b/client/src/views/CalendarView.vue index 07e77a08..b03b8667 100644 --- a/client/src/views/CalendarView.vue +++ b/client/src/views/CalendarView.vue @@ -1,10 +1,21 @@ diff --git a/client/src/views/ProfileView.vue b/client/src/views/ProfileView.vue index 8db702b5..3fe0b462 100644 --- a/client/src/views/ProfileView.vue +++ b/client/src/views/ProfileView.vue @@ -3,19 +3,21 @@
- +
+ +
-
+

{{ user.state.value?.full_name }} -

- Working for {{ user.state.value?.location?.country }} office + + Working for {{ user.state.value?.location?.country }} office
- +
@@ -23,15 +25,15 @@ + \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..2b9f1883 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false