fix : 동작 위아래이동, 삭제 , 결제 2명 validation까지 완료

- VUser 오류 발생 중으로 수정 요함
This commit is contained in:
Yesol Choi
2025-05-29 20:35:31 +09:00
parent 9c8a7427d1
commit 70d9a9eb92
3 changed files with 758 additions and 72 deletions

View File

@@ -0,0 +1,648 @@
<script lang="ts">
import type { RouteLocationAsString } from 'unplugin-vue-router'
import type { SlotsType, PropType } from 'vue'
import { RouterLink } from 'vue-router'
import VPlaceload from '/@src/components/base/VPlaceload.vue'
export type VButtonSize = 'medium' | 'big' | 'huge'
export type VButtonColor =
| 'primary'
| 'info'
| 'success'
| 'warning'
| 'danger'
| 'white'
| 'dark'
| 'light'
| 'validation'
export type VButtonDark = '1' | '2' | '3' | '4' | '5' | '6'
export default defineComponent({
props: {
to: {
type: [Object, String] as PropType<RouteLocationAsString>,
default: undefined,
},
href: {
type: String,
default: undefined,
},
icon: {
type: String,
default: undefined,
},
iconCaret: {
type: String,
default: undefined,
},
placeload: {
type: String,
default: undefined,
validator: (value: string) => {
if (value.match(CssUnitRe) === null) {
console.warn(
`VButton: invalid "${value}" placeload. Should be a valid css unit value.`,
)
}
return true
},
},
color: {
type: String as PropType<VButtonColor>,
default: undefined,
validator: (value: VButtonColor) => {
// The value must match one of these strings
if (
[
undefined,
'primary',
'info',
'success',
'warning',
'danger',
'white',
'dark',
'light',
'validation',
].indexOf(value) === -1
) {
console.warn(
`VButton: invalid "${value}" color. Should be primary, info, success, warning, danger, dark, light, validation, white or undefined`,
)
return false
}
return true
},
},
size: {
type: String as PropType<VButtonSize>,
default: undefined,
validator: (value: VButtonSize) => {
// The value must match one of these strings
if ([undefined, 'medium', 'big', 'huge'].indexOf(value) === -1) {
console.warn(
`VButton: invalid "${value}" size. Should be big, huge, medium or undefined`,
)
return false
}
return true
},
},
dark: {
type: String as PropType<VButtonDark>,
default: undefined,
validator: (value: VButtonDark) => {
// The value must match one of these strings
if ([undefined, '1', '2', '3', '4', '5', '6'].indexOf(value) === -1) {
console.warn(
`VButton: invalid "${value}" dark. Should be 1, 2, 3, 4, 5, 6 or undefined`,
)
return false
}
return true
},
},
rounded: {
type: Boolean,
default: false,
},
bold: {
type: Boolean,
default: false,
},
fullwidth: {
type: Boolean,
default: false,
},
light: {
type: Boolean,
default: false,
},
raised: {
type: Boolean,
default: false,
},
elevated: {
type: Boolean,
default: false,
},
outlined: {
type: Boolean,
default: false,
},
darkOutlined: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
lower: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
static: {
type: Boolean,
default: false,
},
},
slots: Object as SlotsType<{
default: void
}>,
setup(props, { slots, attrs }) {
const classes = computed(() => {
const defaultClasses = (attrs?.class || []) as string[] | string
return [
defaultClasses,
]
})
const isIconify = computed(() => props.icon && props.icon.indexOf(':') !== -1)
const isCaretIconify = computed(
() => props.iconCaret && props.iconCaret.indexOf(':') !== -1,
)
const getChildrens = () => {
const childrens = []
let iconWrapper
if (isIconify.value) {
const icon = h('iconify-icon', {
class: 'iconify',
icon: props.icon,
})
iconWrapper = h('span', { class: 'icon' }, icon)
}
else if (props.icon) {
const icon = h('i', { 'aria-hidden': true, 'class': props.icon })
iconWrapper = h('span', { class: 'icon rtl-reflect' }, icon)
}
let caretWrapper
if (isCaretIconify.value) {
const caret = h('iconify-icon', {
class: 'iconify',
icon: props.iconCaret,
})
caretWrapper = h('span', { class: 'caret' }, caret)
}
else if (props.iconCaret) {
const caret = h('i', { 'aria-hidden': true, 'class': props.iconCaret })
caretWrapper = h('span', { class: 'caret' }, caret)
}
if (iconWrapper) {
childrens.push(iconWrapper)
}
if (props.placeload) {
childrens.push(
h(VPlaceload, {
width: props.placeload,
}),
)
}
else {
childrens.push(h('span', slots.default?.()))
}
if (caretWrapper) {
childrens.push(caretWrapper)
}
return childrens
}
return () => {
if (props.to) {
return h(
RouterLink,
{
...attrs,
'aria-hidden': !!props.placeload,
'to': props.to,
'class': ['button', ...classes.value],
},
{
default: getChildrens,
},
)
}
else if (props.href) {
return h(
'a',
{
...attrs,
'aria-hidden': !!props.placeload,
'href': props.href,
'class': classes.value,
},
{
default: getChildrens,
},
)
}
return h(
'button',
{
'type': 'button',
...attrs,
'aria-hidden': !!props.placeload,
'disabled': props.disabled,
'class': ['button', ...classes.value],
},
{
default: getChildrens,
},
)
}
},
})
</script>
<style lang="scss">
.button {
&.is-circle {
border-radius: var(--radius-rounded);
}
&.v-button {
padding: 8px 22px;
height: 38px;
line-height: 1.1;
font-size: 0.95rem;
font-family: var(--font);
transition: all 0.3s; // transition-all test
&:not([disabled]) {
cursor: pointer;
}
&:active,
&:focus {
box-shadow: none !important;
border-color: color-mix(in oklab, var(--fade-grey), black 2%);
}
&:not(
.is-primary,
.is-success,
.is-info,
.is-warning,
.is-danger,
.is-light,
.is-white,
.is-validation
) {
&.is-active {
background: var(--primary) !important;
border-color: var(--primary) !important;
color: var(--white) !important;
box-shadow: var(--primary-box-shadow) !important;
}
}
&:focus-visible {
outline-offset: var(--accessibility-focus-outline-offset);
outline-width: var(--accessibility-focus-outline-width);
outline-style: var(--accessibility-focus-outline-style);
outline-color: var(--accessibility-focus-outline-color);
}
&.is-bold {
font-weight: 500;
}
&.is-primary {
&.is-raised:hover {
opacity: 0.9;
box-shadow: var(--primary-box-shadow);
}
&.is-elevated {
box-shadow: var(--primary-box-shadow);
}
}
&.is-success {
&.is-raised:hover {
opacity: 0.9;
box-shadow: var(--success-box-shadow);
}
&.is-elevated {
box-shadow: var(--success-box-shadow);
}
}
&.is-info {
&.is-raised:hover {
opacity: 0.9;
box-shadow: var(--info-box-shadow);
}
&.is-elevated {
box-shadow: var(--info-box-shadow);
}
}
&.is-warning {
&.is-raised:hover {
opacity: 0.9;
box-shadow: var(--warning-box-shadow);
}
&.is-elevated {
box-shadow: var(--warning-box-shadow);
}
}
&.is-validation {
&.is-raised:hover {
opacity: 0.9;
box-shadow: var(--warning-box-shadow);
}
&.is-elevated {
box-shadow: var(--warning-box-shadow);
}
}
&.is-danger {
&.is-raised:hover {
opacity: 0.9;
box-shadow: var(--danger-box-shadow);
}
&.is-elevated {
box-shadow: var(--danger-box-shadow);
}
}
&.is-lower {
text-transform: none !important;
font-size: 0.9rem;
}
&.is-big {
height: 40px;
}
&.is-medium {
height: 2.5rem;
font-size: 1rem;
}
&.is-huge {
height: 50px;
width: 220px;
}
}
&.simple-action {
height: 32px;
padding: 0 24px;
line-height: 0;
border-radius: 100px;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s; // transition-all test
&.is-purple {
background: var(--primary);
border-color: var(--primary);
color: var(--smoke-white);
&:hover,
&:focus {
opacity: 0.95;
box-shadow: var(--primary-box-shadow);
color: var(--smoke-white) !important;
}
}
&.has-icon {
.iconify {
height: 16px;
width: 16px;
font-size: 16px;
}
}
&:hover {
border-color: var(--primary);
color: var(--primary);
}
.iconify {
height: 18px;
width: 18px;
font-size: 18px;
}
}
.icon {
.iconify {
height: 14px;
width: 14px;
font-size: 14px;
}
}
}
.is-dark {
.v-button {
&:not(
.is-primary,
.is-success,
.is-info,
.is-warning,
.is-danger,
.is-light,
.is-white,
.is-validation
) {
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
color: var(--dark-dark-text);
&:hover,
&:focus {
border-color: color-mix(in oklab, var(--dark-sidebar), white 18%);
}
}
&.is-light {
border: none;
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
color: var(--smoke-white) !important;
&:hover {
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
}
}
&.is-primary {
border-color: var(--primary);
background: var(--primary);
&.is-raised:hover {
box-shadow: var(--primary-box-shadow) !important;
}
&.is-elevated {
box-shadow: var(--primary-box-shadow) !important;
}
&.is-outlined {
background: transparent;
border-color: var(--primary) !important;
color: var(--primary);
&:hover,
&:focus {
background: var(--primary) !important;
border-color: var(--primary) !important;
color: var(--white) !important;
}
}
&.is-light {
border: none;
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
color: color-mix(in oklab, var(--primary), white 20%) !important;
&:hover {
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
}
}
}
&.is-info {
&.is-light {
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
color: color-mix(in oklab, var(--info), white 20%) !important;
&:hover {
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
}
}
}
&.is-success {
&.is-light {
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
color: color-mix(in oklab, var(--success), white 20%) !important;
&:hover {
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
}
}
}
&.is-warning {
&.is-light {
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
color: color-mix(in oklab, var(--warning), white 20%) !important;
&:hover {
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
}
}
}
&.is-validation {
&.is-light {
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
color: color-mix(in oklab, var(--warning), white 20%) !important;
&:hover {
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
}
}
}
&.is-danger {
&.is-light {
background: color-mix(in oklab, var(--dark-sidebar), white 10%) !important;
color: color-mix(in oklab, var(--danger), white 20%) !important;
&:hover {
background: color-mix(in oklab, var(--dark-sidebar), white 16%) !important;
}
}
}
&.is-white {
background: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
border-color: var(--muted-grey) !important;
color: var(--muted-grey) !important;
}
&.is-dark-outlined {
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
color: var(--dark-dark-text);
&:hover,
&:focus {
border-color: var(--primary) !important;
color: var(--primary) !important;
}
}
}
.button {
&:not(
.is-primary,
.is-success,
.is-info,
.is-warning,
.is-danger,
.is-light,
.is-white,
.is-validation
) {
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
color: var(--dark-dark-text);
&:hover,
&:focus {
border-color: color-mix(in oklab, var(--dark-sidebar), white 18%);
}
}
&.is-primary {
border-color: var(--primary);
background: var(--primary);
}
&.is-white {
background: color-mix(in oklab, var(--dark-sidebar), white 6%) !important;
border-color: var(--muted-grey) !important;
color: var(--muted-grey) !important;
}
&.is-dark-outlined {
background: color-mix(in oklab, var(--dark-sidebar), white 10%);
border-color: color-mix(in oklab, var(--dark-sidebar), white 12%);
color: var(--dark-dark-text);
&:hover,
&:focus {
border-color: var(--primary) !important;
color: var(--primary) !important;
}
}
}
}
</style>

View File

@@ -22,7 +22,6 @@ const dataUser = reactive({
// model.value = sessioinData
// })
const initiator = ref<Person | null>(null)
const approvers = ref<Person[]>([])
onBeforeMount(() => {
dataUser.userSession = userSession.user.data
@@ -36,15 +35,6 @@ onBeforeMount(() => {
}
})
// 부모에게 넘길 결재선 전체
const apprLine = computed(() => {
return initiator.value ? [initiator.value, ...approvers.value] : [...approvers.value]
})
watch(apprLine, (val) => {
model.value = val
})
const props = defineProps({
label: {
@@ -56,11 +46,27 @@ const props = defineProps({
},
disabled: {
type: Boolean
}
},
modelValue: {
type: Object,
default: () => ({}),
},
})
export interface VUserEmits {
(e: 'update:modelValue', value: any): void
}
const emits = defineEmits<VUserEmits>()
const tagsOptions = ref([])
const approvers = ref()
watch(() => props.modelValue, (newVal) => {
if (newVal.length > 0) {
emits('update:modelValue', approvers)
approvers.value = []
}
})
const onKeyup = async (e: any) => {
const regex_jaeum = /[ㄱ-ㅎ]/g
if (e.key === 'Enter') {
@@ -69,27 +75,6 @@ const onKeyup = async (e: any) => {
const res = await getUserList({params:{name : e.target.value}})
if (res.length > 0) {
// res.forEach(u => { 2명으로 제한 로직 풀기 25.05.29
// if (model.value?.length > 0) {
// const ignore = model.value.reduce((a: Person, b: Person) => {
// console.log("aaaaa",a.sabun)
// console.log("bbbbbb",b.sabun)
// return a.sabun + '|' + b.sabun
// })
//
// if (typeof ignore !== 'object') {
// if (ignore.includes(u.sabun)) {
// console.log("ignore 처리",u.sabun)
// u['disabled'] = true
// }
// } else {
// if (ignore.sabun.includes(u.sabun)) {
// console.log("ignore 처리222",u.sabun)
// u['disabled'] = true
// }
// }
// }
// })
tagsOptions.value = res
}
} catch (e) {
@@ -120,6 +105,9 @@ const onKeyup = async (e: any) => {
noOptionsText="검색된 결과가 없습니다."
:placeholder="props.placeholder"
@keyup="onKeyup"
@update:modelValue="(val) => {
emits('update:modelValue', val)
}"
>
<template #multiplelabel="{ values }">
<div class="multiselect-multiple-label pl-6">

View File

@@ -14,7 +14,7 @@ onBeforeMount(async ()=>{
const showTable = ref(false)
const detailActionsOpen = ref(false)
const apprLine = defineModel<Person[]>()
const apprLine = ref([])
const generalParams = reactive({
title: "",
@@ -30,6 +30,7 @@ const params = reactive({
cateSelect: '',
prcsNo: '',
stCd: '', //상태코드 등록 전 : 0000
defaultPricePerson:[],
prcsAttsColumn:[ //첨부파일 입력
{ key: 'logiFnm', label: '구분'},
{ key: 'data', label: '데이터'}
@@ -40,9 +41,10 @@ const params = reactive({
{ key: 'deptNm', label: '부서' },
{ key: 'sabun', label: '사번' },
{ key: 'name', label: '이름' },
{ key: 'attendCd', label: '비고'},
{ key: 'attendCd', label: '근태'},
{ key: 'apprStat', label: '결재상태'},
{ key: 'apprDt', label: '승인일자'},
{key:'actions', label: '동작'},
],
priceData:[],
prcsBizsColumn: [ //견적사 입력
@@ -90,10 +92,9 @@ function getDetailList(arg){
apprNo: req.apprNo,
apprOrd: req.apprOrd,
apprStat: req.apprStat,
apprDt: req.apprDt,
apprDt: req.apprDt ? req.apprDt : '-',
attendCd: req.attendCd
})) //비고 데이터 없음, 승인일자 없음 todo
console.log("apprLine.value",apprLine.value)
}))
params.prcsAtts = arg.prcsAtts
}
@@ -104,47 +105,72 @@ const changeButton = () => {
}
const validation = () => {
notyf.dismissAll() //todo
notyf.dismissAll()
if(generalParams.regSdat > generalParams.regEdat){
notyf.error("등록 종료일은 등록 시작일보다 빠를 수 없습니다.")
return
return false
}
const resultGu = apprLine.value.filter((item)=> !item.gubunCd)
if(resultGu.length > 0){
notyf.error("결재선 구분값을 입력해주세요")
return false
}
if(apprLine.value.length < 2){
notyf.error("결재선은 두 명이상 입력해주세요.")
return
return false
}
const result = apprLine.value.filter((item)=> item.gubunCd === '0200' )
if(result.length > 1){
notyf.error("결재는 한 명입니다.")
return false
}
return true
}
const selectUser = ref()
watch(selectUser,(newPersons)=>{
if (Array.isArray(newPersons)) {
apprLine.value = [...apprLine.value, ...newPersons]
}
})
const updatePriceOne = async () => {
let res = null
try{
loading.value = true
validation()
const paramsPrice ={
prcsNo : params.prcsNo,
cateCd : params.cateSelect,
title: generalParams.title,
content: generalParams.content,
regSdat: formatDatefromString(generalParams.regSdat),
regEdat: formatDatefromString(generalParams.regEdat),
prvYn: false,
prvRsn : "",
prvPwd : "",
aiYn: false,
prcsBizs: params.prcsBizs.map(({ num, ...rest }) => rest), //견적사 입력 데이터
dtlSpecs: [], //todo
// params.dtlSpecs.map(({ num, ...rest }) => rest), //상세 규격 입력 데이터
prcsAtts: pbAtts.value.map(req => ({
logiFnm: req.logiFnm,
data: req.data})),//첨부파일 데이터,
apprReqs: apprLine.value.map(({ deptNm, ...rest }) => rest), //결재선 데이터
}
res = await updatePrice(paramsPrice)
notyf.dismissAll()
if(res.request.status == '200'){
notyf.primary('수정 되었습니다.')
router.push({path: '/app/priceManagement'})
if (validation()){
const paramsPrice ={
prcsNo : params.prcsNo,
cateCd : params.cateSelect,
title: generalParams.title,
content: generalParams.content,
regSdat: formatDatefromString(generalParams.regSdat),
regEdat: formatDatefromString(generalParams.regEdat),
prvYn: false,
prvRsn : "",
prvPwd : "",
aiYn: false,
prcsBizs: params.prcsBizs.map(({ num, ...rest }) => rest), //견적사 입력 데이터
dtlSpecs: [], //todo
// params.dtlSpecs.map(({ num, ...rest }) => rest), //상세 규격 입력 데이터
prcsAtts: pbAtts.value.map(req => ({
logiFnm: req.logiFnm,
data: req.data})),//첨부파일 데이터,
apprReqs: apprLine.value.map(({ deptNm, ...rest }, index) => ({
...rest,
apprOrd: index + 1, // 결재 순서 1부터 시작
})), //결재선 데이터
}
res = await updatePrice(paramsPrice)
notyf.dismissAll()
if(res.request.status == '200'){
notyf.primary('수정 되었습니다.')
router.push({path: '/app/priceManagement'})
}
}
}catch(e){
notyf.error(e.message)
@@ -191,6 +217,14 @@ const onDetailDelete = (index: number) => {
}
}
const onPriceDelete = (index: number) => {
if(apprLine.value.length-1 !== params.felxColumn.length
|| (params.felxColumn.length == 8 && apprLine.value.length-1 == params.felxColumn.length))
{
apprLine.value.splice(index, 1)
}
}
function onInput(row, column){
if (column.key === 'bizNo') {
@@ -251,6 +285,20 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
link.click()
}
const moveUp = (index: number) => {
if (index <= 0) return
let temp = apprLine.value[index]
apprLine.value[index] = apprLine.value[index - 1]
apprLine.value[index - 1] = temp
}
const moveDown = (index: number) => {
if (index >= apprLine.value.length - 1) return
let temp = apprLine.value[index]
apprLine.value[index] = apprLine.value[index + 1]
apprLine.value[index + 1] = temp
}
</script>
<template>
@@ -539,7 +587,7 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</table>
</div>
<div class="column is-12">
<VUser v-model="apprLine"/>
<VUser v-model="selectUser"/>
</div>
<div class="column is-12">
<VField class="pr-2">
@@ -548,6 +596,7 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
</VLabel>
</VField>
<ComVFlexTable
:key="apprLine.map(item => item.sabun).join(',')"
:data="apprLine"
:columns="params.felxColumn"
:separators="true"
@@ -569,14 +618,15 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
placeholder="재중"
cd_grp=6
v-model="row.attendCd">
<!-- <template #code>-->
<!-- <span v-if="!row.attendCd">{{"재중"}}</span>-->
<!-- </template>-->
</VDefaultCodeSelect>
</VField>
</span>
<span v-else
@click="onDetailDelete(index)">{{value}}</span>
<span v-else-if="column.key === 'actions'" class="flex gap-1">
<VCustomButton @click="moveUp(index)" icon="lucide:chevron-up" />
<VCustomButton @click="moveDown(index)" icon="lucide:chevron-down" />
<VCustomButton @click="onPriceDelete(index)" icon="lucide:x" />
</span>
<span v-else>{{value}}</span>
</div>
</template>
</ComVFlexTable>
@@ -584,7 +634,7 @@ const onPrcsFileDownload = async (prcsNo: string, fileOrd: number, logiFnm: stri
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px;">
<VButton
color="primary"
@click.stop="updatePriceOne"
@click.stop="updatePriceOne()"
raised>
수정
</VButton>