Widen board columns and keep counts inside the add button so stage and card titles have room. Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
6.1 KiB
Vue
201 lines
6.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue';
|
|
import {
|
|
CalendarOutlined,
|
|
CheckOutlined,
|
|
DeleteOutlined,
|
|
MoreOutlined,
|
|
} from '@ant-design/icons-vue';
|
|
import {
|
|
Avatar as AAvatar,
|
|
Button as AButton,
|
|
Card as ACard,
|
|
Flex as AFlex,
|
|
Space as ASpace,
|
|
Tag as ATag,
|
|
Typography,
|
|
theme,
|
|
} from 'ant-design-vue';
|
|
import { userAvatarSrc } from '@/lib/userAvatars';
|
|
import type { PipelineAccent, PipelineDeal, PipelineUiCopy } from '@/lib/pipeline-board';
|
|
import { DEAL_BOARD_COPY, formatCurrencyParts } from '@/lib/pipeline-board';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
deal: PipelineDeal;
|
|
accent: PipelineAccent;
|
|
dragging?: boolean;
|
|
copy?: PipelineUiCopy;
|
|
selectMode?: boolean;
|
|
selected?: boolean;
|
|
}>(),
|
|
{ copy: () => DEAL_BOARD_COPY },
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
dragStart: [deal: PipelineDeal, event: DragEvent];
|
|
dragEnd: [];
|
|
open: [dealId: string];
|
|
menu: [dealId: string];
|
|
toggleSelect: [dealId: string];
|
|
bulkDelete: [];
|
|
}>();
|
|
|
|
const { Text: ATypographyText } = Typography;
|
|
const { token } = theme.useToken();
|
|
|
|
const showAmount = computed(
|
|
() => !props.copy.hideAmount && props.deal.amount != null,
|
|
);
|
|
const money = computed(() =>
|
|
formatCurrencyParts(props.deal.amount ?? 0, props.deal.currencyUnit ?? 'ریال'),
|
|
);
|
|
const assigneeTitle = computed(() =>
|
|
(props.deal.assigneeNames ?? []).filter(Boolean).join('، ') || 'مسئول',
|
|
);
|
|
const avatarSrc = computed(() =>
|
|
userAvatarSrc(props.deal.ownerId || props.deal.assigneeIds?.[0] || null),
|
|
);
|
|
const extraAssignees = computed(() =>
|
|
Math.max(0, (props.deal.assigneeNames?.length ?? 0) - 1),
|
|
);
|
|
const showTrash = computed(() => props.selectMode && props.selected);
|
|
|
|
const cardStyle = computed(() => ({
|
|
cursor: props.selectMode ? 'pointer' : 'grab',
|
|
opacity: props.dragging ? 0.45 : 1,
|
|
margin: 0,
|
|
background: props.selected ? token.value.colorPrimaryBg : token.value.colorBgContainer,
|
|
borderColor: props.selected ? token.value.colorPrimary : token.value.colorBorderSecondary,
|
|
boxShadow: token.value.boxShadowTertiary,
|
|
}));
|
|
</script>
|
|
|
|
<template>
|
|
<a-card
|
|
hoverable
|
|
:bordered="true"
|
|
:body-style="{ padding: '12px 14px', margin: 0 }"
|
|
:style="cardStyle"
|
|
:data-card-id="deal.id"
|
|
:draggable="!selectMode"
|
|
role="listitem"
|
|
:aria-grabbed="dragging ? 'true' : 'false'"
|
|
@dragstart="!selectMode && emit('dragStart', deal, $event)"
|
|
@dragend="emit('dragEnd')"
|
|
@dblclick="!selectMode && emit('open', deal.id)"
|
|
@click="selectMode && emit('toggleSelect', deal.id)"
|
|
>
|
|
<a-flex vertical :gap="10" :style="{ width: '100%', minWidth: 0 }">
|
|
<a-flex :gap="10" align="flex-start" :style="{ minWidth: 0 }">
|
|
<a-button
|
|
v-if="selectMode"
|
|
type="text"
|
|
shape="circle"
|
|
:aria-label="selected ? 'لغو انتخاب' : 'انتخاب معامله'"
|
|
:aria-pressed="selected"
|
|
:style="{
|
|
width: '28px',
|
|
height: '28px',
|
|
minWidth: '28px',
|
|
color: selected ? token.colorTextLightSolid : token.colorTextTertiary,
|
|
border: `2px solid ${selected ? token.colorPrimary : token.colorBorder}`,
|
|
background: selected ? token.colorPrimary : token.colorBgContainer,
|
|
}"
|
|
@click.stop="emit('toggleSelect', deal.id)"
|
|
>
|
|
<template #icon>
|
|
<CheckOutlined v-if="selected" />
|
|
</template>
|
|
</a-button>
|
|
<div :style="{ flex: 1, minWidth: 0 }">
|
|
<a-typography-text
|
|
ellipsis
|
|
:style="{
|
|
display: 'block',
|
|
margin: 0,
|
|
lineHeight: 1.35,
|
|
cursor: 'pointer',
|
|
fontSize: `${token.fontSizeSM}px`,
|
|
fontWeight: 600,
|
|
}"
|
|
:title="deal.title"
|
|
@click.stop="selectMode ? emit('toggleSelect', deal.id) : emit('open', deal.id)"
|
|
>
|
|
{{ deal.title }}
|
|
</a-typography-text>
|
|
<a-typography-text
|
|
v-if="deal.description"
|
|
type="secondary"
|
|
ellipsis
|
|
:style="{
|
|
display: 'block',
|
|
margin: 0,
|
|
marginTop: '2px',
|
|
fontSize: `${token.fontSizeSM}px`,
|
|
}"
|
|
:title="deal.description"
|
|
>
|
|
{{ deal.description }}
|
|
</a-typography-text>
|
|
</div>
|
|
|
|
<span :data-card-id="deal.id">
|
|
<a-button
|
|
v-if="showTrash"
|
|
class="menu"
|
|
type="text"
|
|
danger
|
|
aria-label="حذف انتخابشدهها"
|
|
@click.stop="emit('bulkDelete')"
|
|
>
|
|
<template #icon><DeleteOutlined /></template>
|
|
</a-button>
|
|
<a-button
|
|
v-else
|
|
class="menu"
|
|
type="text"
|
|
:aria-label="copy.itemMenuAria"
|
|
@click.stop="emit('menu', deal.id)"
|
|
>
|
|
<template #icon><MoreOutlined /></template>
|
|
</a-button>
|
|
</span>
|
|
</a-flex>
|
|
|
|
<a-flex
|
|
justify="space-between"
|
|
align="center"
|
|
:gap="8"
|
|
:style="{
|
|
minWidth: 0,
|
|
paddingTop: '8px',
|
|
borderTop: `1px solid ${token.colorSplit}`,
|
|
}"
|
|
>
|
|
<a-space :size="8" align="center" :style="{ minWidth: 0 }">
|
|
<a-avatar :src="avatarSrc" :size="24" :title="assigneeTitle" />
|
|
<a-tag v-if="extraAssignees > 0" :bordered="false" :title="assigneeTitle">
|
|
+{{ extraAssignees }}
|
|
</a-tag>
|
|
<a-typography-text type="secondary" :style="{ fontSize: `${token.fontSizeSM}px` }">
|
|
<CalendarOutlined :style="{ marginInlineEnd: '4px' }" />
|
|
{{ deal.date }}
|
|
</a-typography-text>
|
|
</a-space>
|
|
<a-typography-text
|
|
v-if="showAmount"
|
|
strong
|
|
dir="ltr"
|
|
:style="{ margin: 0, whiteSpace: 'nowrap', flexShrink: 0 }"
|
|
>
|
|
{{ money.number }}
|
|
<a-typography-text type="secondary" :style="{ fontSize: `${token.fontSizeSM}px` }">
|
|
{{ money.unit }}
|
|
</a-typography-text>
|
|
</a-typography-text>
|
|
</a-flex>
|
|
</a-flex>
|
|
</a-card>
|
|
</template>
|