merge: bring latest origin/main into dev (design local, logic main)

Keep the local Ant screens and icon pack. Take treasury parties, routine self-service, Tahesab auto-sync, public lead registration, and contact merge from main.
This commit is contained in:
2026-11-05 15:10:48 +03:30
55 changed files with 2719 additions and 358 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, inject, onMounted, reactive, ref } from 'vue';
import { computed, inject, onMounted, reactive, ref, watch } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import {
Alert as AAlert,
@@ -30,13 +30,18 @@ import RecurrencePicker, { type RecurrenceValue } from '@/components/routines/Re
import { api } from '@/lib/api';
import { toFaDigits } from '@/lib/jalali';
import { PRIORITY_LABELS, ROUTINES_CTX, apiError, formatDayNumeric, todayKey } from '@/lib/routines';
import { useAuthStore } from '@/stores/auth';
const { Title: ATypographyTitle, Text: ATypographyText, Paragraph: ATypographyParagraph } =
Typography;
const shell = inject(ROUTINES_CTX)!;
const auth = useAuthStore();
const route = useRoute();
const router = useRouter();
const isManager = computed(() => !!shell.context.value?.isManager);
const selfId = computed(() => auth.user?.id ?? '');
const backTo = computed(() => (isManager.value ? '/routines/manage' : '/routines/list'));
const id = computed(() => (typeof route.params.id === 'string' ? route.params.id : null));
const isEdit = computed(() => !!id.value);
@@ -76,6 +81,8 @@ const recurrence = ref<RecurrenceValue>({
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const loaded = ref<RoutineDto | null>(null);
const foreign = ref(false);
const priorities = Object.entries(PRIORITY_LABELS) as Array<[ActivityPriority, string]>;
const entityLinks = Object.entries(ROUTINE_ENTITY_LINK_LABELS) as Array<[RoutineEntityLink, string]>;
@@ -140,10 +147,14 @@ function fill(r: RoutineDto) {
}
onMounted(async () => {
if (!id.value) return;
if (!id.value) {
if (selfId.value) form.assigneeId = selfId.value;
return;
}
loading.value = true;
try {
const { data } = await api.get<RoutineDto>(`/routines/${id.value}`);
loaded.value = data;
fill(data);
} catch (e) {
error.value = apiError(e, 'روتین پیدا نشد.');
@@ -152,6 +163,23 @@ onMounted(async () => {
}
});
watch(
[() => shell.context.value, loaded, selfId],
() => {
const ctx = shell.context.value;
const data = loaded.value;
if (!ctx || !data || ctx.isManager) {
foreign.value = false;
return;
}
foreign.value = data.createdById !== selfId.value;
if (foreign.value) {
error.value = 'این روتین را مدیر تعریف کرده و فقط خودش می‌تواند تغییرش دهد.';
}
},
{ immediate: true },
);
const users = computed(() => shell.context.value?.users ?? []);
const teams = computed(() => shell.context.value?.teams ?? []);
const categories = computed(() => shell.context.value?.categories ?? []);
@@ -198,6 +226,11 @@ const summary = computed(() => {
});
function validate(): string | null {
if (!isManager.value) {
form.target = 'user';
form.assigneeId = selfId.value;
form.teamId = '';
}
if (form.title.trim().length < 2) return 'عنوان روتین را وارد کنید.';
if (form.target === 'user' && !form.assigneeId) return 'مسئول روتین را انتخاب کنید.';
if (form.target === 'team' && !form.teamId) return 'تیم را انتخاب کنید.';
@@ -249,7 +282,7 @@ async function save() {
if (isEdit.value) await api.patch(`/routines/${id.value}`, body);
else await api.post('/routines', body);
shell.notifyChanged();
void router.push('/routines/manage');
void router.push(backTo.value);
} catch (e) {
error.value = apiError(e, 'ذخیره روتین ممکن نشد.');
} finally {
@@ -263,7 +296,7 @@ async function save() {
<a-flex vertical :gap="4">
<a-breadcrumb>
<a-breadcrumb-item>
<RouterLink to="/routines/manage">مدیریت روتین‌ها</RouterLink>
<RouterLink :to="backTo">{{ isManager ? 'مدیریت روتین‌ها' : 'روتین‌های من' }}</RouterLink>
</a-breadcrumb-item>
<a-breadcrumb-item>{{ pageTitle }}</a-breadcrumb-item>
</a-breadcrumb>
@@ -275,7 +308,9 @@ async function save() {
</a-typography-text>
</a-flex>
<CrmSkeleton :loading="loading">
<a-alert v-if="foreign" type="error" show-icon :message="error" />
<CrmSkeleton v-if="!foreign" :loading="loading">
<CrmSkeletonCard :rows="8">
<a-form layout="vertical" @finish="save">
<a-row :gutter="[16, 16]">
@@ -323,6 +358,10 @@ async function save() {
</a-card>
<a-card :bordered="false" title="مسئول">
<a-typography-text v-if="!isManager" type="secondary">
این روتین فقط برای خودتان ساخته می‌شود.
</a-typography-text>
<template v-else>
<a-form-item>
<a-radio-group v-model:value="form.target" button-style="solid">
<a-radio-button value="user">یک کاربر</a-radio-button>
@@ -354,6 +393,7 @@ async function save() {
style="width: 100%"
/>
</a-form-item>
</template>
</a-card>
<a-card :bordered="false" title="زمان‌بندی و تکرار">
@@ -484,7 +524,7 @@ async function save() {
<a-button type="primary" html-type="submit" block :loading="saving">
{{ isEdit ? 'ذخیره تغییرات' : 'ایجاد روتین' }}
</a-button>
<a-button block :disabled="saving" @click="router.push('/routines/manage')">
<a-button block :disabled="saving" @click="router.push(backTo)">
انصراف
</a-button>
</a-space>