Add Didar gap features: lost-reason reports, audit logs, tags, card assignees, and saved-views API.

Ignore Graphify local artifacts and expose new modules in superadmin.
This commit is contained in:
2026-09-26 14:59:48 +03:30
parent 35a618d0af
commit 6463275c3d
30 changed files with 1406 additions and 21 deletions
+66 -3
View File
@@ -3,7 +3,7 @@ import { computed, onMounted, ref } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import type { ProjectCardDto } from '@mento/shared';
import type { OrgUserDto, ProjectCardDto } from '@mento/shared';
import UiIcon from '@/components/UiIcon.vue';
const props = withDefaults(
@@ -35,6 +35,29 @@ const saving = ref(false);
const error = ref('');
const title = ref('');
const notes = ref('');
const tagsRaw = ref('');
const selectedAssigneeIds = ref<string[]>([]);
const users = ref<OrgUserDto[]>([]);
function parseTags(raw: string): string[] {
return [
...new Set(
raw
.split(/[,،#\n]/)
.map((t) => t.trim())
.filter(Boolean),
),
].slice(0, 40);
}
async function loadUsers() {
try {
const { data } = await api.get<OrgUserDto[]>('/users');
users.value = data;
} catch {
users.value = [];
}
}
async function load() {
if (isNew.value) return;
@@ -46,6 +69,8 @@ async function load() {
const { data } = await api.get<ProjectCardDto>(`/project-cards/${cardId}`);
title.value = data.title;
notes.value = data.notes ?? '';
tagsRaw.value = (data.tags ?? []).join('، ');
selectedAssigneeIds.value = (data.assignees ?? []).map((a) => a.id);
} catch (e) {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
@@ -71,6 +96,8 @@ async function save() {
const { data } = await api.post<ProjectCardDto>('/project-cards', {
title: title.value.trim(),
notes: notes.value.trim() || undefined,
tags: parseTags(tagsRaw.value),
assigneeIds: selectedAssigneeIds.value,
boardId,
stageId: props.embedded ? props.initialStageId : undefined,
personId:
@@ -85,6 +112,8 @@ async function save() {
await api.patch(`/project-cards/${id.value}`, {
title: title.value.trim(),
notes: notes.value.trim() || null,
tags: parseTags(tagsRaw.value),
assigneeIds: selectedAssigneeIds.value,
});
if (props.embedded) {
emit('saved');
@@ -121,7 +150,10 @@ function cancel() {
void router.push({ name: 'projects' });
}
onMounted(load);
onMounted(async () => {
await loadUsers();
await load();
});
</script>
<template>
@@ -188,6 +220,22 @@ onMounted(load);
<label for="card-title">عنوان</label>
<input id="card-title" v-model="title" type="text" required minlength="2" maxlength="200" />
</div>
<div class="field">
<label for="card-tags">برچسب‌ها</label>
<input
id="card-tags"
v-model="tagsRaw"
type="text"
placeholder="فوری، پشتیبانی (با کاما جدا کنید)"
/>
</div>
<div class="field">
<label for="card-assignees">مسئولان کارت</label>
<select id="card-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 class="field">
<label for="card-notes">یادداشت</label>
<textarea id="card-notes" v-model="notes" rows="5" maxlength="4000" />
@@ -313,12 +361,27 @@ h1 {
}
input,
textarea {
textarea,
select {
width: 100%;
min-width: 0;
border: 1px solid #e2e8f0;
border-radius: var(--radius);
min-height: var(--control-h);
padding: 0 0.75rem;
font: inherit;
box-sizing: border-box;
}
select.multi {
min-height: 110px;
padding: 0.4rem;
}
.hint {
color: #64748b;
font-size: 0.8rem;
}
padding: 0 0.85rem;
font: inherit;
box-sizing: border-box;