jws-frontend/src/components/shared/PersonCard.vue
Methapon Metanipat 4528836f17
feat: quotation add worker after accepted (#140)
* feat: add dialog structure

* feat: add select functionality

* chore: clear unused

* feat: pass selectable product into dialog

* feat: add table

* feat: implement select worker and product

* feat: send disabled worker into component

* feat: add event emitted after submit

* chore: cleanup

* feat: add store

* feat: dialogquotationbtn

* feat: import worker from file and select them all

* feat: add title

* feat: add import button

* feat: i18n

* feat: lazy load person card image

* refactor: change import button color

* feat: add import worker button

* chore: cleanup

* chore: clean

* chore: clean

* feat: post quotation add worker appear on expired

* fix: type

* fix: only proceed when import has at least one

* feat: check more condition

* feat: fetch data from api

* fix: worker not update

---------

Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Co-authored-by: nwpptrs <jay02499@gmail.com>
2024-12-17 14:22:22 +07:00

367 lines
8.3 KiB
Vue

<script lang="ts" setup>
import { Icon } from '@iconify/vue';
import AppBox from 'components/app/AppBox.vue';
import AppCircle from 'components/app/AppCircle.vue';
import KebabAction from './KebabAction.vue';
defineProps<{
data: {
name: string;
code: string;
male?: boolean;
female?: boolean;
img?: string;
fallbackImg?: string;
detail?: { icon: string; value: string }[];
};
tag?: [{ color: string; value: string }];
disabled?: boolean;
noHover?: boolean;
noAction?: boolean;
noBg?: boolean;
history?: boolean;
prefixId?: string;
separateEnter?: boolean;
}>();
defineEmits<{
(e: 'editProfile'): void;
(e: 'history'): void;
(e: 'deleteCard'): void;
(e: 'updateCard', action: 'FORM' | 'INFO', isEdit?: boolean): void;
(e: 'enterCard', action: 'FORM' | 'INFO'): void;
(e: 'viewCard', action: 'FORM' | 'INFO'): void;
(e: 'toggleStatus', status: boolean): void;
}>();
</script>
<template>
<AppBox
bordered
no-padding
class="column person-box"
:class="{
'person-box__disabled': disabled,
'person-box__no-hover': noHover,
'person-box__no-detail': !data.detail,
'person-box__no-bg': noBg,
}"
@click="$emit('enterCard', 'INFO')"
>
<div class="column items-center" :class="{ 'q-pt-sm': noAction }">
<!-- kebab menu -->
<div class="full-width flex no-wrap" v-if="!noAction">
<div style="margin-right: auto">
<span
class="tags"
v-for="v in tag"
:key="v.value"
:class="{ [`tags__${v.color}`]: true }"
style="text-wrap: nowrap"
>
{{ v.value }}
</span>
</div>
<q-btn
v-if="history"
:id="`btn-history-${prefixId}`"
flat
round
class="app-text-muted-2"
padding="xs"
icon="mdi-history"
size="sm"
@click.stop="$emit('history')"
/>
<KebabAction
:id-name="prefixId"
:status="disabled ? 'INACTIVE' : 'ACTIVE'"
@view="
separateEnter
? $emit('viewCard', 'INFO')
: $emit('enterCard', 'INFO')
"
@edit="$emit('updateCard', 'INFO', true)"
@delete="$emit('deleteCard')"
@change-status="$emit('toggleStatus', disabled === false)"
/>
</div>
<!-- profile -->
<AppCircle
bordered
class="avatar relative-position"
style="border: 2px solid var(--border-color); overflow: visible"
@click="$emit('editProfile')"
>
<q-img
v-if="!$slots.img"
:src="data.img ?? '/no-profile.png'"
loading="lazy"
fit="cover"
style="width: 100%; height: 100%; border-radius: 50%"
>
<template #error>
<div
style="background: none"
class="no-padding full-width full-height flex items-center justify-center"
>
<q-img :src="data.fallbackImg || '/no-profile.png'" fit="cover" />
</div>
</template>
</q-img>
<slot name="img"></slot>
<q-badge
class="absolute-bottom-right"
style="
border-radius: 50%;
width: 16px;
height: 16px;
z-index: 2;
background: var(--border-color);
"
>
<q-badge
class="absolute-center"
style="
border-radius: 7px;
width: 14px;
height: 14px;
background: hsl(var(--positive-bg));
"
></q-badge>
<!-- :style="`background: hsl(var(${active ? '--positive-bg' : '--text-mute'}))`" -->
</q-badge>
</AppCircle>
<!-- name symbol -->
<span class="full-width">
<span
class="items-center justify-center row text-center ellipsis col-6"
>
<div class="ellipsis" style="max-width: calc(100% - 30%)">
{{ data.name }}
<q-tooltip>
{{ data.name }}
</q-tooltip>
</div>
<Icon
v-if="data.male || data.female"
class="q-pl-xs"
:class="{
'symbol-gender': data.male || data.female,
'symbol-gender__male': !disabled && data.male,
'symbol-gender__female': !disabled && data.female,
'symbol-gender__disable': disabled,
}"
:icon="`material-symbols:${data.male ? 'male' : 'female'}`"
width="24px"
/>
</span>
</span>
<span style="color: hsl(var(--text-mute)); scale: 0.9">
{{ data.code || '-' }}
</span>
</div>
<!-- detail -->
<q-separator v-if="data.detail" class="q-my-sm q-mx-md" />
<div class="row" v-for="v in data.detail" :key="v.value" v-if="data.detail">
<div class="column items-center" style="width: min-content">
<div class="detail-icon">
<q-icon size="md" style="scale: 0.5" :name="v.icon" />
</div>
</div>
<div class="col row items-center full-width">
<span class="ellipsis">{{ v.value || '-' }}</span>
<q-tooltip
anchor="bottom left"
self="bottom left"
:offset="[10, 20]"
:delay="500"
>
{{ v.value || '-' }}
</q-tooltip>
</div>
</div>
</AppBox>
</template>
<style scoped>
.person-box {
background-color: var(--surface-1);
transition: 100ms ease-in-out;
padding: var(--size-2);
&.person-box__disabled {
opacity: 0.5;
filter: grayscale(0.9);
.status-circle {
background-color: var(--surface-1);
}
}
&.person-box__no-detail {
padding-block: 2rem;
}
&.person-box__no-bg {
background-color: transparent;
}
& .detail-icon {
color: hsl(var(--text-mute-2));
background-color: hsla(var(--stone-6-hsl) / 0.15);
border-radius: 50%;
scale: 0.8;
}
& .bg-gender {
color: hsla(var(--_fg));
background-color: hsl(var(--_bg));
&.bg-gender__disable {
--_bg: var(--stone-6-hsl);
background-color: hsla(var(--_bg) / 0.3);
}
&.bg-gender__light {
color: unset;
background-color: hsla(var(--_bg) / 0.1);
}
}
& .symbol-gender {
color: hsla(var(--_fg));
&.symbol-gender__disable {
--_fg: var(--stone-6-hsl);
}
&.symbol-gender__male {
--_fg: var(--gender-male);
}
&.symbol-gender__female {
--_fg: var(--gender-female);
}
}
& .status-circle {
width: 18px;
height: 18px;
border-radius: 50%;
background-color: var(--teal-6);
border: 2px solid var(--border-color);
bottom: 0.6rem;
right: 0.6rem;
position: absolute;
display: inline-flex;
justify-content: center;
align-items: center;
}
&:not(.person-box__disabled):not(.person-box__no-hover):hover {
--_hover: hsl(var(--gender-male));
cursor: pointer;
box-shadow: var(--shadow-2);
}
}
.person-container {
display: grid;
gap: var(--size-6);
}
.avatar {
block-size: 5rem;
/* transform: rotate(45deg); */
& .avatar__status {
content: ' ';
display: block;
block-size: 1rem;
aspect-ratio: 1;
position: absolute;
border-radius: 50%;
right: -0.5rem;
border: 1px solid var(--border-color);
top: calc(50% - 0.5rem);
bottom: calc(50% - 0.5rem);
background-color: hsla(var(--positive-bg) / 1);
}
/* & :deep(.q-img) {
transform: rotate(-45deg);
} */
}
.edit-profile {
cursor: pointer;
transition: opacity 0.2s ease;
&:hover {
opacity: 80%;
}
}
.tags {
display: inline-block;
color: hsla(var(--_color) / 1);
background: hsla(var(--_color) / 0.15);
border-radius: var(--radius-2);
padding-inline: var(--size-2);
}
.tags__purple {
--_color: var(--violet-11-hsl);
}
.tags__pink {
--_color: var(--pink-6-hsl);
}
.tags__green {
--_color: var(--teal-10-hsl);
}
.tags__cyan {
--_color: var(--cyan-7-hsl);
}
.tags__red {
--_color: var(--red-6-hsl);
}
.tags__yellow {
--_color: var(--orange-4-hsl);
}
.tags__orange {
--_color: var(--orange-5-hsl);
}
.tags__magenta {
--_color: var(--pink-8-hsl);
}
.dark .tags__purple {
--_color: var(--violet-10-hsl);
}
.dark .tags__green {
--_color: var(--teal-8-hsl);
}
.dark .tags__orange {
--_color: var(--orange-6-hsl);
}
.dark .tags__magenta {
--_color: var(--pink-7-hsl);
}
</style>