Add deal co-assignees and saved filters on people/pipeline lists.
Deals keep a primary owner plus shared assignees with matching visibility; Contacts and Pipeline can save and reuse filter presets.
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import PersonFormView from '@/views/PersonFormView.vue';
|
||||
import CompanyFormView from '@/views/CompanyFormView.vue';
|
||||
import SavedFiltersBar from '@/components/SavedFiltersBar.vue';
|
||||
import type {
|
||||
CompanyDto,
|
||||
ImportContactsResult,
|
||||
@@ -65,6 +66,30 @@ const total = computed(() =>
|
||||
);
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)));
|
||||
|
||||
const peopleSavedFilters = computed(() => ({
|
||||
q: q.value.trim() || undefined,
|
||||
sort: sort.value === 'name' ? undefined : sort.value,
|
||||
mood: moodFilter.value === 'all' ? undefined : moodFilter.value,
|
||||
}));
|
||||
|
||||
function applyPeopleSavedFilters(filters: Record<string, unknown>) {
|
||||
const nextQ = typeof filters.q === 'string' ? filters.q : '';
|
||||
q.value = nextQ;
|
||||
const nextSort = filters.sort === 'newest' ? 'newest' : 'name';
|
||||
const nextMood =
|
||||
typeof filters.mood === 'string' &&
|
||||
PERSON_MOODS.includes(filters.mood as PersonMood)
|
||||
? (filters.mood as PersonMood)
|
||||
: 'all';
|
||||
patchQuery({
|
||||
q: nextQ.trim() || undefined,
|
||||
sort: nextSort === 'name' ? undefined : nextSort,
|
||||
mood: nextMood === 'all' ? undefined : nextMood,
|
||||
page: undefined,
|
||||
tab: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function moodOf(person: PersonDto): PersonMood {
|
||||
return person.mood || 'newcomer';
|
||||
}
|
||||
@@ -360,6 +385,13 @@ onMounted(load);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SavedFiltersBar
|
||||
v-if="tab === 'people'"
|
||||
scope="people"
|
||||
:current-filters="peopleSavedFilters"
|
||||
@apply="applyPeopleSavedFilters"
|
||||
/>
|
||||
|
||||
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
||||
|
||||
@@ -93,6 +93,7 @@ const companyId = ref('');
|
||||
const stageId = ref('');
|
||||
const pipelineId = ref('');
|
||||
const ownerId = ref('');
|
||||
const selectedAssigneeIds = ref<string[]>([]);
|
||||
const lostReason = ref('');
|
||||
const status = ref('');
|
||||
const customFields = ref<Record<string, unknown>>({});
|
||||
@@ -247,6 +248,7 @@ async function load() {
|
||||
contactKind.value = data.person?.id || !data.company?.id ? 'person' : 'company';
|
||||
stageId.value = data.stageId;
|
||||
ownerId.value = data.owner?.id ?? auth.user?.id ?? '';
|
||||
selectedAssigneeIds.value = (data.assignees ?? []).map((a) => a.id);
|
||||
lostReason.value = data.lostReason ?? '';
|
||||
status.value = data.status;
|
||||
customFields.value = { ...(data.customFields ?? {}) };
|
||||
@@ -264,6 +266,7 @@ async function load() {
|
||||
}
|
||||
} else {
|
||||
ownerId.value = auth.user?.id ?? users.value[0]?.id ?? '';
|
||||
selectedAssigneeIds.value = [];
|
||||
if (typeof route.query.personId === 'string' && route.query.personId) {
|
||||
personId.value = route.query.personId;
|
||||
contactKind.value = 'person';
|
||||
@@ -306,6 +309,7 @@ function payload(): CreateDealRequest {
|
||||
personId: personId.value || null,
|
||||
companyId: companyId.value || null,
|
||||
ownerId: ownerId.value || null,
|
||||
assigneeIds: selectedAssigneeIds.value,
|
||||
pipelineId: pipelineId.value || undefined,
|
||||
stageId: stageId.value || undefined,
|
||||
customFields: customFields.value,
|
||||
@@ -623,6 +627,20 @@ onMounted(load);
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="deal-assignees">مسئولان مشترک</label>
|
||||
<select
|
||||
id="deal-assignees"
|
||||
v-model="selectedAssigneeIds"
|
||||
multiple
|
||||
class="multi"
|
||||
>
|
||||
<option v-for="u in users" :key="u.id" :value="u.id">
|
||||
{{ u.fullName }}
|
||||
</option>
|
||||
</select>
|
||||
<small class="hint">علاوه بر مسئول اصلی؛ با Ctrl چند نفر را انتخاب کنید.</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1322,6 +1340,20 @@ h1 {
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
select.multi {
|
||||
min-height: 110px;
|
||||
padding: 0.4rem;
|
||||
width: 100%;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.note-card.off,
|
||||
.timeline.hide {
|
||||
display: none;
|
||||
|
||||
@@ -21,6 +21,7 @@ import UiButton from '@/components/ui/UiButton.vue';
|
||||
import UiInput from '@/components/ui/UiInput.vue';
|
||||
import UiModal from '@/components/ui/UiModal.vue';
|
||||
import DealFormView from '@/views/DealFormView.vue';
|
||||
import SavedFiltersBar from '@/components/SavedFiltersBar.vue';
|
||||
import type { ImportContactsResult, PipelineBoardDto, PipelineDto } from '@mento/shared';
|
||||
import { brand } from '@mento/shared';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
@@ -84,6 +85,34 @@ const visibleStages = computed(() =>
|
||||
}),
|
||||
);
|
||||
|
||||
const dealsSavedFilters = computed(() => ({
|
||||
q: search.value.trim() || undefined,
|
||||
amountMin: amountMin.value || undefined,
|
||||
amountMax: amountMax.value || undefined,
|
||||
pipelineId: selectedPipelineId.value || undefined,
|
||||
viewMode: viewMode.value,
|
||||
}));
|
||||
|
||||
function applyDealsSavedFilters(filters: Record<string, unknown>) {
|
||||
search.value = typeof filters.q === 'string' ? filters.q : '';
|
||||
amountMin.value =
|
||||
filters.amountMin == null || filters.amountMin === ''
|
||||
? ''
|
||||
: String(filters.amountMin);
|
||||
amountMax.value =
|
||||
filters.amountMax == null || filters.amountMax === ''
|
||||
? ''
|
||||
: String(filters.amountMax);
|
||||
if (filters.viewMode === 'list' || filters.viewMode === 'kanban') {
|
||||
viewMode.value = filters.viewMode;
|
||||
}
|
||||
const nextPipeline =
|
||||
typeof filters.pipelineId === 'string' ? filters.pipelineId : '';
|
||||
if (nextPipeline && nextPipeline !== selectedPipelineId.value) {
|
||||
selectedPipelineId.value = nextPipeline;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDealDate(iso: string) {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
@@ -480,6 +509,12 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
||||
</template>
|
||||
</PipelineToolbar>
|
||||
|
||||
<SavedFiltersBar
|
||||
scope="deals"
|
||||
:current-filters="dealsSavedFilters"
|
||||
@apply="applyDealsSavedFilters"
|
||||
/>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="importMsg" class="ok">{{ importMsg }}</p>
|
||||
<p v-else-if="loading && !stages.length" class="muted">در حال بارگذاری کاریز…</p>
|
||||
|
||||
Reference in New Issue
Block a user