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
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { RouterLink } from 'vue-router';
import axios from 'axios';
import UiIcon from '@/components/UiIcon.vue';
import VoiceToTextButton from '@/components/VoiceToTextButton.vue';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import { formatLocalDateTimeInput } from '@/lib/jalali';
import { UiButton, UiModal } from '@/components/ui';
import { api } from '@/lib/api';
import { brand, type ActivityType, type AskMentoSourceDto, type PersonDto, type VoiceCommandDraft } from '@mento/shared';
@@ -93,15 +95,14 @@ function onActivityPersonEdit() {
if (draft.value?.activity) draft.value.activity.personId = null;
}
function toLocalInput(iso: string | null) {
const dueLocal = computed(() => {
const iso = draft.value?.activity?.dueAt;
if (!iso) return '';
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return '';
const pad = (n: number) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
return Number.isNaN(date.getTime()) ? '' : formatLocalDateTimeInput(date);
});
function onDueEdit(value: string) {
function onDueLocal(value: string) {
if (!draft.value?.activity) return;
if (!value) {
draft.value.activity.dueAt = null;
@@ -111,6 +112,25 @@ function onDueEdit(value: string) {
draft.value.activity.dueAt = Number.isNaN(date.getTime()) ? null : date.toISOString();
}
async function rematchActivityPerson() {
const activity = draft.value?.activity;
const name = activity?.personName.trim();
if (!activity || !name) {
if (activity) activity.personId = null;
return;
}
try {
const { data } = await api.get<{ items: Array<{ id: string; fullName: string }> }>('/people', {
params: { q: name, limit: 5 },
});
const exact = data.items.filter((row) => row.fullName.trim() === name);
const hit = exact.length === 1 ? exact[0] : data.items.length === 1 ? data.items[0] : null;
activity.personId = hit?.id ?? null;
} catch {
activity.personId = null;
}
}
async function parseCommand(text: string, fallbackToAsk: boolean) {
voiceBusy.value = true;
voiceError.value = '';
@@ -312,15 +332,14 @@ watch(
</label>
<label>
زمان
<input
type="datetime-local"
:value="toLocalInput(draft.activity.dueAt)"
@input="onDueEdit(($event.target as HTMLInputElement).value)"
/>
<JalaliDateInput :model-value="dueLocal" mode="datetime" @update:model-value="onDueLocal" />
</label>
<label>
مشتری
<input v-model="draft.activity.personName" @input="onActivityPersonEdit" @change="rematchActivityPerson" />
</label>
<label>مشتری <input v-model="draft.activity.personName" @input="onActivityPersonEdit" /></label>
<p v-if="draft.activity.personName && !draft.activity.personId" class="warn">
این نام به پرونده‌ای وصل نیست؛ فعالیت بدون لینک ثبت می‌شود.
در فهرست مشتریان پرونده‌ای با این نام پیدا نشد. فعالیت ذخیره می‌شود، ولی داخل پروندهٔ آن شخص دیده نمی‌شود.
</p>
<label>توضیح <textarea v-model="draft.activity.description" rows="2" /></label>
</div>