Rebrand the product to Montact and point local UI at production API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 11:49:54 +03:30
co-authored by Cursor
parent 8357676e6c
commit 48d3469eea
56 changed files with 309 additions and 216 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ SMS_PASSWORD=
SMS_SENDER=
# SMS_BASE_URL=http://smspanel.Trez.ir
# SMS_SEND_PATH=/SendMessageWithCode.ashx
# SMS_OTP_TEMPLATE=کد ورود پرتال منتو: {code}
# SMS_OTP_TEMPLATE=کد ورود پرتال منتکت: {code}
# SMS_TIMEOUT_MS=20000
# رمزنگاری توکن‌های اتصال حسابداری (ته‌حساب) — ۳۲ بایت به‌صورت hex (۶۴ کاراکتر)
+1
View File
@@ -4,6 +4,7 @@ dist/
coverage/
.env
.env.local
.env.*.local
*.log
.DS_Store
apps/api/prisma/*.db
+24 -16
View File
@@ -1,4 +1,4 @@
# Mento CRM (منتو)
# Montact CRM (منتکت)
Monorepo برای CRM فروش‌محور ایرانی با هم‌ترازی قابلیت نسبت به دیدار، به‌اضافه Smart Notes (سند کارفرما).
@@ -13,7 +13,7 @@ Monorepo برای CRM فروش‌محور ایرانی با هم‌ترازی ق
| فایل | MinIO (آماده در Compose) |
| لندینگ | بعداً با Nuxt در `apps/landing` |
UI را پیکسل‌به‌پیکسل از دیدار کپی نمی‌کنیم؛ هویت بصری مستقل **منتو (Mento)** است.
UI را پیکسل‌به‌پیکسل از دیدار کپی نمی‌کنیم؛ هویت بصری مستقل **منتکت (Montact)** است.
## ساختار
@@ -44,27 +44,35 @@ docker compose up -d
```bash
npm install
npm run build -w @mento/shared
```
برای کار روی ظاهر، فقط فرانت را روشن کن. درخواست‌های `/api` می‌روند به سرور اصلی (`https://crm.montzar.ir`) و لازم نیست API لوکال بالا باشد:
```bash
npm run dev
```
- پنل لوکال: http://localhost:5173
- ورود با همان حساب سرور (`crm.montzar.ir`)، نه حساب دمو
اگر خواستی API را هم روی سیستم خودت داشته باشی:
```bash
npm run prisma:generate -w @mento/api
npm run prisma:migrate -w @mento/api
npm run prisma:seed -w @mento/api
```
ترمینال ۱:
ترمینال ۱: `npm run dev:api`
ترمینال ۲: در PowerShell
```bash
npm run dev:api
```powershell
$env:MENTO_DEV_API="http://127.0.0.1:3000"; npm run dev
```
ترمینال ۲:
یا در `apps/web/.env.development.local` بنویس: `MENTO_DEV_API=http://127.0.0.1:3000`
```bash
npm run dev:web
```
- پنل: http://localhost:5173
- API: http://localhost:3000/api/health
حساب دمو بعد از seed:
حساب دمو فقط برای API لوکال بعد از seed:
- مالک: `owner@demo.local` / `demo1234`
- مدیر: `admin@demo.local` / `demo1234`
@@ -75,10 +83,10 @@ npm run dev:web
فاز ۱ هسته فروش/پشتیبانی و Smart Notes عملاً کامل است. جزئیات تیک‌خورده و باقی‌مانده:
- [`docs/وضعیت-پیشرفت.md`](docs/وضعیت-پیشرفت.md) ← **چک‌لیست زنده**
- [`docs/قابلیت‌های-منتو.md`](docs/قابلیت‌های-منتو.md) ← فهرست کوتاه قابلیت‌ها
- [`docs/قابلیت‌های-منتکت.md`](docs/قابلیت‌های-منتکت.md) ← فهرست کوتاه قابلیت‌ها
- [`docs/راهنما-و-نقشه-راه.md`](docs/راهنما-و-نقشه-راه.md) ← نقشه راه
نمونه‌های اخیر آماده‌شده: احراز هویت OTP/رمز، Ask Mento، Voice-to-Text، اتصال ته‌حساب، یادآور SMS فعالیت، UI یکدست، جداکننده مبلغ.
نمونه‌های اخیر آماده‌شده: احراز هویت OTP/رمز، Ask Montact، Voice-to-Text، اتصال ته‌حساب، یادآور SMS فعالیت، UI یکدست، جداکننده مبلغ.
استقرار: [`deploy/DEPLOY.md`](deploy/DEPLOY.md) → `crm.montzar.ir`
+4 -3
View File
@@ -1,5 +1,6 @@
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import { brand } from '@mento/shared';
const prisma = new PrismaClient();
@@ -20,7 +21,7 @@ async function main() {
if (org) {
org = await prisma.organization.update({
where: { id: org.id },
data: { slug: 'demo-mento', name: 'سازمان نمونه منتو' },
data: { slug: 'demo-mento', name: `سازمان نمونه ${brand.nameFa}` },
});
console.log('Renamed demo org slug to demo-mento');
}
@@ -30,7 +31,7 @@ async function main() {
const passwordHash = await bcrypt.hash('demo1234', 10);
org = await prisma.organization.create({
data: {
name: 'سازمان نمونه منتو',
name: `سازمان نمونه ${brand.nameFa}`,
slug: 'demo-mento',
baseCurrency: 'IRR',
enabledCurrencies: ['IRR', 'USD', 'EUR'],
@@ -747,7 +748,7 @@ async function main() {
data: [
{
organizationId: org.id,
name: 'لایسنس سازمانی منتو',
name: `لایسنس سازمانی ${brand.nameFa}`,
sku: 'MENTO-ORG',
unitPrice: 80_000_000,
currency: 'IRR',
+4 -4
View File
@@ -11,7 +11,7 @@ import {
noteAccessWhere,
ownerWhere,
} from '../common/record-scope';
import { BUSINESS_FIELD_LABELS, isBusinessField } from '@mento/shared';
import { BUSINESS_FIELD_LABELS, brand, brandSuggestionFa, isBusinessField } from '@mento/shared';
import { AiService } from './ai.service';
type CatalogItem = AskMentoSourceDto;
@@ -39,7 +39,7 @@ export class AiAskService {
answer?: string;
sources?: Array<{ type?: string; id?: string; label?: string }>;
}>(
`You are Mento, a Persian CRM sales assistant.
`You are ${brand.name}, a Persian CRM sales assistant.
Answer ONLY from the CRM context. If the context is insufficient, say so in Persian.
Do not invent customers, amounts, dates, or promises.
Return JSON: { "answer": string, "sources": [{ "type": "person"|"note"|"activity"|"deal"|"memory", "id": string, "label": string }] }
@@ -73,7 +73,7 @@ Return JSON: { "text": string, "cta": string, "ctaKind": "call"|"activity" }
Rules:
- 2 to 4 short sentences from the context only.
- Mention durable memory, last contact, open deals, or next follow-up when present.
- cta is one concrete next step, starting with «پیشنهاد منتو:».
- cta is one concrete next step, starting with «${brand.suggestionPrefixFa}».
- ctaKind is "call" if a phone follow-up is best, otherwise "activity".
- Do not invent facts.`,
`Customer briefing context:\n${context}`,
@@ -84,7 +84,7 @@ Rules:
const ctaKind = raw.ctaKind === 'activity' ? 'activity' : 'call';
return {
text: text || 'هنوز دادهٔ کافی برای خلاصه هوشمند این مشتری نیست.',
cta: cta || 'پیشنهاد منتو: امروز با مشتری تماس بگیر',
cta: cta || brandSuggestionFa('امروز با مشتری تماس بگیر'),
ctaKind,
};
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { brand } from '@mento/shared';
/** پنل ترِز / رایگان — همان الگوی montime */
const SMS_TREZ_DEFAULT_BASE_URL = 'http://smspanel.Trez.ir';
@@ -44,7 +45,7 @@ export class SmsService {
async sendOtpSms(mobile: string, code: string): Promise<boolean> {
const template =
this.config.get<string>('SMS_OTP_TEMPLATE') ??
'کد ورود پرتال منتو: {code}';
`کد ورود پرتال ${brand.nameFa}: {code}`;
const text = template.replace('{code}', code);
const result = await this.sendSmsDetailed(mobile, text);
return result.ok;
@@ -21,6 +21,7 @@ import type { Actor } from '../common/record-scope';
import { CompaniesService } from './companies.service';
import { CreateCompanyDto, ListQueryDto, UpdateCompanyDto } from './dto';
import { ContactsExcelService } from './contacts-excel.service';
import { brand } from '@mento/shared';
@Controller('companies')
@UseGuards(JwtAuthGuard)
@@ -50,7 +51,7 @@ export class CompaniesController {
res.set({
'Content-Type':
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment; filename="mento-companies.xlsx"',
'Content-Disposition': `attachment; filename="${brand.filePrefix}-companies.xlsx"`,
});
return new StreamableFile(buffer);
}
+2 -1
View File
@@ -21,6 +21,7 @@ import type { Actor } from '../common/record-scope';
import { CreatePersonDto, ListQueryDto, UpdatePersonDto } from './dto';
import { PeopleService } from './people.service';
import { ContactsExcelService } from './contacts-excel.service';
import { brand } from '@mento/shared';
@Controller('people')
@UseGuards(JwtAuthGuard)
@@ -51,7 +52,7 @@ export class PeopleController {
res.set({
'Content-Type':
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment; filename="mento-people.xlsx"',
'Content-Disposition': `attachment; filename="${brand.filePrefix}-people.xlsx"`,
});
return new StreamableFile(buffer);
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { Controller, Get } from '@nestjs/common';
import type { HealthResponse } from '@mento/shared';
import { brand } from '@mento/shared';
@Controller('health')
export class HealthController {
@@ -7,7 +8,7 @@ export class HealthController {
getHealth(): HealthResponse {
return {
status: 'ok',
service: 'mento-api',
service: `${brand.id}-api`,
timestamp: new Date().toISOString(),
};
}
+2 -1
View File
@@ -29,6 +29,7 @@ import {
UpdateLoyaltyTierDto,
} from './dto';
import { LoyaltyService } from './loyalty.service';
import { brand } from '@mento/shared';
@Controller('loyalty')
@UseGuards(JwtAuthGuard, RolesGuard)
@@ -96,7 +97,7 @@ export class LoyaltyController {
res.set({
'Content-Type':
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment; filename="mento-loyalty-members.xlsx"',
'Content-Disposition': `attachment; filename="${brand.filePrefix}-loyalty-members.xlsx"`,
});
return new StreamableFile(buffer);
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { brand } from '@mento/shared';
import { AppModule } from './app.module';
async function bootstrap() {
@@ -24,7 +25,7 @@ async function bootstrap() {
const port = Number(config.get<string>('PORT') ?? 3000);
await app.listen(port);
// eslint-disable-next-line no-console
console.log(`Mento API listening on http://localhost:${port}/api`);
console.log(`${brand.name} API listening on http://localhost:${port}/api`);
}
bootstrap();
@@ -1,4 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { brand } from '@mento/shared';
import type { NotificationDto, NotificationsListDto } from '@mento/shared';
import { PrismaService } from '../prisma/prisma.service';
import { SmsService } from '../common/sms.service';
@@ -228,7 +229,7 @@ export class NotificationsService {
this.logger.warn('Activity reminder SMS skipped: SMS not configured');
return;
}
const text = `یادآوری منتو: ${title}`.slice(0, 160);
const text = `${brand.reminderPrefixFa} ${title}`.slice(0, 160);
const result = await this.sms.sendSmsDetailed(mobile, text);
if (!result.ok) {
this.logger.warn(`Activity reminder SMS failed: ${result.reason ?? 'unknown'}`);
+1 -1
View File
@@ -1,4 +1,4 @@
# Mento STT (Pipecat)
# Montact STT (Pipecat)
تبدیل صوت به متن — فقط STT. بدون TTS، بدون مکالمهٔ صوتی.
+2 -1
View File
@@ -3,7 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>منتو | Mento CRM</title>
<title>منتکت | Montact CRM</title>
<link rel="icon" type="image/png" href="/brand/logo.png" />
</head>
<body>
<div id="app"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

+2 -2
View File
@@ -4,7 +4,7 @@ import { RouterLink } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import UiIcon from '@/components/UiIcon.vue';
import type { AskMentoResponse, AskMentoSourceDto } from '@mento/shared';
import { brand, type AskMentoResponse, type AskMentoSourceDto } from '@mento/shared';
const props = defineProps<{
personId?: string;
@@ -55,7 +55,7 @@ async function submit() {
<input
v-model="question"
type="search"
:placeholder="placeholder || 'از منتو بپرس...'"
:placeholder="placeholder || brand.askPlaceholderFa"
/>
<button type="submit" :disabled="asking || question.trim().length < 2">
{{ asking ? '…' : 'بپرس' }}
+2 -2
View File
@@ -2,7 +2,7 @@
import { computed, onMounted, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import type { MemoryItemDto, PaginatedResponse } from '@mento/shared';
import { brand, type MemoryItemDto, type PaginatedResponse } from '@mento/shared';
const props = defineProps<{
personId: string;
@@ -117,7 +117,7 @@ onMounted(load);
<section class="memory-panel">
<header>
<div>
<p class="eyebrow">Mento Memory</p>
<p class="eyebrow">{{ brand.memoryEn }}</p>
<h2>حافظه مشتری</h2>
<p class="lead">نکات پایدار درباره این شخص — فقط با تأیید شما ذخیره می‌شود.</p>
</div>
@@ -3,7 +3,7 @@ import { computed, reactive, ref, watch } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import type { NoteAiPayloadDto, NoteAiSuggestionDto } from '@mento/shared';
import { brand, type NoteAiPayloadDto, type NoteAiSuggestionDto } from '@mento/shared';
const props = defineProps<{
noteId: string;
@@ -149,7 +149,7 @@ function formatPayloadSummary(value: NoteAiPayloadDto | null) {
<template>
<section class="ai-panel">
<header>
<strong>پیشنهاد Mento AI</strong>
<strong>پیشنهاد {{ brand.aiName }}</strong>
<button
v-if="!isPending"
type="button"
@@ -1,5 +1,7 @@
<script setup lang="ts">
import MentoLogo from '@/components/brand/MentoLogo.vue';
import BrandName from '@/components/brand/BrandName.vue';
import { brand } from '@mento/shared';
withDefaults(
defineProps<{
@@ -8,8 +10,7 @@ withDefaults(
}>(),
{
headline: 'همه ارتباطات، یکجا مدیریت می‌شوند',
description:
'با منتو فروش، پشتیبانی و ارتباط با مشتری را در یک پنل یکپارچه مدیریت کنید و رشد پایدار بسازید.',
description: `با ${brand.nameFa} فروش، پشتیبانی و ارتباط با مشتری را در یک پنل یکپارچه مدیریت کنید و رشد پایدار بسازید.`,
},
);
</script>
@@ -19,8 +20,8 @@ withDefaults(
<div class="brand-head">
<MentoLogo :size="44" :show-text="false" />
<div class="brand-copy">
<strong>منتو</strong>
<span>نرم‌افزار مدیریت ارتباط با مشتری</span>
<BrandName />
<span>{{ brand.taglineFa }}</span>
</div>
</div>
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { computed } from 'vue';
import { brand } from '@mento/shared';
const props = withDefaults(
defineProps<{
lang?: 'fa' | 'en' | 'full';
}>(),
{ lang: 'fa' },
);
const text = computed(() => {
if (props.lang === 'en') return brand.name;
if (props.lang === 'full') return brand.fullName;
return brand.nameFa;
});
</script>
<template>
<span class="brand-name">{{ text }}</span>
</template>
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { brand } from '@mento/shared';
withDefaults(
defineProps<{
size?: number;
@@ -25,7 +27,7 @@ withDefaults(
<img
class="bot-img"
src="/brand/mento-ai-avatar.png"
alt="دستیار هوشمند منتو"
:alt="brand.aiNameFa"
:width="size"
:height="size"
/>
+13 -37
View File
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { brand } from '@mento/shared';
withDefaults(
defineProps<{
size?: number;
@@ -14,38 +16,22 @@ withDefaults(
</script>
<template>
<div class="mento-logo" :class="variant">
<span
<div class="brand-logo" :class="variant">
<img
class="mark"
:style="{ width: `${size}px`, height: `${size}px`, borderRadius: `${Math.round(size * 0.3)}px` }"
aria-hidden="true"
>
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="14" y="16" width="8" height="32" rx="4" fill="#fff" />
<path
d="M17 17.5c1-1.6 3.5-1.8 4.8-.5L36 30l4.3-4c1.4-1.3 3.6-1.2 4.7.4L44 27.5 33 37.5c-1.1 1-2.8 1-3.9 0L16.5 24.5 17 17.5Z"
fill="#134e4a"
:src="brand.logo"
:alt="brand.name"
:style="{ width: `${size}px`, height: `${size}px` }"
/>
<path
d="M27.5 32 38 21.5c1.3-1.3 3.4-1.2 4.6.2L50 29.5l-2.6 2.6L40 25.5 31 34c-1 1-2.8 1-3.8-.1l-.7-.9Z"
fill="#ecfdf5"
/>
<rect x="42" y="16" width="8" height="32" rx="4" fill="#134e4a" />
<path
d="M41 17.5c1.3-1.4 3.5-1.3 4.7.2l1.6 1.8-7 6.6-2.1-2c-1.2-1.1-.9-3.2 2.8-6.6Z"
fill="#ecfdf5"
/>
</svg>
</span>
<div v-if="showText && variant === 'full'" class="text">
<strong>Mento</strong>
<small>by Mentech</small>
<strong>{{ brand.name }}</strong>
<small>by {{ brand.company }}</small>
</div>
</div>
</template>
<style scoped>
.mento-logo {
.brand-logo {
display: inline-flex;
align-items: center;
gap: 0.7rem;
@@ -53,20 +39,10 @@ withDefaults(
}
.mark {
display: grid;
place-items: center;
flex-shrink: 0;
background: linear-gradient(145deg, #5eead4, #14b8a6);
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.28);
overflow: hidden;
padding: 4px;
box-sizing: border-box;
}
.mark svg {
width: 100%;
height: 100%;
display: block;
flex-shrink: 0;
object-fit: contain;
background: transparent;
}
.text {
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router';
import UiIcon from '@/components/UiIcon.vue';
import type { AskMentoSourceDto } from '@mento/shared';
import { brand, type AskMentoSourceDto } from '@mento/shared';
defineProps<{
askQuery: string;
@@ -41,7 +41,7 @@ function pick(label: string) {
<header class="panel-head">
<h2>
<span class="spark"><UiIcon name="sparkle" :size="15" /></span>
دستیار هوشمند منتو
{{ brand.aiNameFa }}
</h2>
</header>
@@ -79,7 +79,7 @@ function pick(label: string) {
<input
:value="askQuery"
type="search"
placeholder="از منتو بپرس..."
:placeholder="brand.askPlaceholderFa"
:disabled="asking"
@input="emit('update:askQuery', ($event.target as HTMLInputElement).value)"
/>
@@ -3,7 +3,7 @@ import { computed } from 'vue';
import { RouterLink } from 'vue-router';
import UiIcon from '@/components/UiIcon.vue';
import MentoAiAvatar from '@/components/brand/MentoAiAvatar.vue';
import type { HomeDashboardAlertDto } from '@mento/shared';
import { brand, type HomeDashboardAlertDto } from '@mento/shared';
const props = defineProps<{
greeting: string;
@@ -91,7 +91,7 @@ function alertIcon(tone: string) {
</span>
<div class="greet-text">
<h1>{{ greeting }} {{ firstName }} 👋</h1>
<p>منتو اینا رو برات پیدا کرده</p>
<p>{{ brand.nameFa }} اینا رو برات پیدا کرده</p>
</div>
</div>
@@ -116,7 +116,7 @@ function alertIcon(tone: string) {
<input
:value="askQuery"
type="search"
placeholder="از منتو بپرس..."
:placeholder="brand.askPlaceholderFa"
@input="emit('update:askQuery', ($event.target as HTMLInputElement).value)"
/>
<kbd>⌘K</kbd>
+4 -41
View File
@@ -7,11 +7,13 @@ import { formatJalaliShort, toFaDigits } from '@/lib/jalali';
import UiIcon from '@/components/UiIcon.vue';
import MentoLogo from '@/components/brand/MentoLogo.vue';
import QuickCreate from '@/components/QuickCreate.vue';
import BrandName from '@/components/brand/BrandName.vue';
import type {
ActivitySummaryDto,
NotificationDto,
NotificationsListDto,
} from '@mento/shared';
import { brand } from '@mento/shared';
import { USER_ROLE_LABELS, canManageSettings, canViewAllRecords } from '@/lib/roles';
const auth = useAuthStore();
@@ -366,7 +368,7 @@ onUnmounted(() => {
<UiIcon name="sparkle" :size="18" />
</span>
<p>
<span>با منتو فروشات را</span>
<span>با {{ brand.nameFa }} فروشات را</span>
<span>چند برابر کن</span>
</p>
</div>
@@ -389,7 +391,7 @@ onUnmounted(() => {
</button>
<div class="top-brand">
<MentoLogo :size="32" :show-text="false" variant="mark" />
<strong>Mento</strong>
<BrandName lang="en" />
</div>
<button class="round-btn" type="button" aria-label="اعلان‌ها" @click="toggleNotif">
<UiIcon name="bell" :size="18" />
@@ -518,45 +520,6 @@ onUnmounted(() => {
flex-shrink: 0;
}
.brand {
display: flex;
align-items: center;
gap: 0.7rem;
}
.mark {
width: 40px;
height: 40px;
border-radius: 12px;
display: grid;
place-items: center;
background: linear-gradient(145deg, #2ec4a8, #14b8a6);
color: #fff;
font-weight: 800;
font-size: 1.05rem;
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.28);
}
.mark.sm {
width: 32px;
height: 32px;
border-radius: 10px;
font-size: 0.9rem;
}
.brand strong {
display: block;
font-size: 1.05rem;
letter-spacing: -0.01em;
}
.brand small {
display: block;
color: #94a3b8;
font-size: 0.72rem;
margin-top: 0.05rem;
}
.side-nav {
display: grid;
gap: 0.45rem;
+4 -4
View File
@@ -1,4 +1,4 @@
import type { MemoryItemDto, NoteDto, PersonDto } from '@mento/shared';
import { brand, brandSuggestionFa, type MemoryItemDto, type NoteDto, type PersonDto } from '@mento/shared';
export type ProbabilityBand = 'بالا' | 'متوسط' | 'پایین';
@@ -85,12 +85,12 @@ export function buildSmartSummary(input: {
const hasSignal = memoryBits.length > 0 || input.notes.length > 0 || input.lastQuoteLabel;
const text = hasSignal
? parts.join(' ').replace(/\s+/g, ' ').trim()
: 'هنوز خلاصهٔ پایداری از این مشتری ثبت نشده. با یادداشت یا حافظهٔ منتو، پیشنهاد هوشمند اینجا ظاهر می‌شود.';
: `هنوز خلاصهٔ پایداری از این مشتری ثبت نشده. با یادداشت یا حافظهٔ ${brand.nameFa}، پیشنهاد هوشمند اینجا ظاهر می‌شود.`;
let cta = 'پیشنهاد منتو: امروز با مشتری تماس بگیر';
let cta = brandSuggestionFa('امروز با مشتری تماس بگیر');
let ctaKind: 'call' | 'activity' = 'call';
if (input.daysSinceCall != null && input.daysSinceCall <= 1) {
cta = 'پیشنهاد منتو: پیگیری بعدی را ثبت کن';
cta = brandSuggestionFa('پیگیری بعدی را ثبت کن');
ctaKind = 'activity';
}
return { text, cta, ctaKind };
+5
View File
@@ -1,9 +1,14 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { brand } from '@mento/shared';
import App from './App.vue';
import { router } from './router';
import './styles/main.css';
document.title = `${brand.nameFa} | ${brand.fullName}`;
const favicon = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (favicon) favicon.href = brand.logo;
const app = createApp(App);
app.use(createPinia());
app.use(router);
+2 -1
View File
@@ -15,6 +15,7 @@ import JalaliDateInput from '@/components/JalaliDateInput.vue';
import VoiceToTextButton from '@/components/VoiceToTextButton.vue';
import { appendSpokenText } from '@/lib/spoken-text';
import UiIcon from '@/components/UiIcon.vue';
import { brand } from '@mento/shared';
import UserAvatar from '@/components/UserAvatar.vue';
import type {
ActivityDto,
@@ -498,7 +499,7 @@ watch([id, isNew], () => void load(), { immediate: true });
<input v-model="reminderNotifyApp" type="checkbox" />
<span>
<strong>اعلان داخل پنل</strong>
<small>در زنگوله اعلان‌های منتو نشان داده می‌شود</small>
<small>در زنگوله اعلان‌های {{ brand.nameFa }} نشان داده می‌شود</small>
</span>
</label>
<label class="channel">
+2 -1
View File
@@ -13,6 +13,7 @@ import type {
PersonDto,
PersonMood,
} from '@mento/shared';
import { brand } from '@mento/shared';
import { PERSON_MOOD_LABELS, PERSON_MOODS } from '@/lib/person-mood';
type Tab = 'people' | 'companies';
@@ -178,7 +179,7 @@ async function exportExcel() {
const a = document.createElement('a');
a.href = url;
a.download =
tab.value === 'people' ? 'mento-people.xlsx' : 'mento-companies.xlsx';
tab.value === 'people' ? `${brand.filePrefix}-people.xlsx` : `${brand.filePrefix}-companies.xlsx`;
a.click();
URL.revokeObjectURL(url);
} catch {
+13 -1
View File
@@ -12,6 +12,8 @@ import { formatLocalDateInput, toFaDigits } from '@/lib/jalali';
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import MoneyInput from '@/components/MoneyInput.vue';
import UiIcon from '@/components/UiIcon.vue';
import BrandName from '@/components/brand/BrandName.vue';
import { brand } from '@mento/shared';
import type {
CreateInvoiceRequest,
CreatePaymentRequest,
@@ -763,7 +765,8 @@ onMounted(load);
<form v-else class="form" @submit.prevent="save">
<div class="print-banner">
<strong>منتو</strong>
<img :src="brand.logo" :alt="brand.name" class="print-logo" />
<BrandName />
<span v-if="number">{{ number }}</span>
</div>
@@ -2357,12 +2360,21 @@ h1 {
.print-banner {
display: flex;
align-items: center;
gap: 0.6rem;
justify-content: space-between;
border-bottom: 1px solid #ccc;
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
}
.print-logo {
width: 36px;
height: 36px;
object-fit: contain;
border-radius: 8px;
}
.page {
max-width: none;
}
+5 -3
View File
@@ -6,6 +6,8 @@ import AuthBrandPanel from '@/components/auth/AuthBrandPanel.vue';
import MentoLogo from '@/components/brand/MentoLogo.vue';
import axios from 'axios';
import type { LoginChallengeResponse } from '@mento/shared';
import { brand } from '@mento/shared';
import BrandName from '@/components/brand/BrandName.vue';
const auth = useAuthStore();
const router = useRouter();
@@ -111,8 +113,8 @@ function backToOtpMobile() {
<section class="form-panel">
<header class="mobile-brand">
<MentoLogo :size="52" :show-text="false" />
<strong>منتو</strong>
<span>نرم‌افزار مدیریت ارتباط با مشتری</span>
<BrandName />
<span>{{ brand.taglineFa }}</span>
</header>
<div class="form-inner">
@@ -260,7 +262,7 @@ function backToOtpMobile() {
</div>
<footer class="auth-footer">
<p>۱۴۰۵ منتو. تمامی حقوق محفوظ است.</p>
<p>۱۴۰۵ {{ brand.nameFa }}. تمامی حقوق محفوظ است.</p>
<nav>
<a href="#">حریم خصوصی</a>
<span>|</span>
+2 -1
View File
@@ -18,6 +18,7 @@ import type {
LoyaltyStatsDto,
LoyaltyTierDto,
} from '@mento/shared';
import { brand } from '@mento/shared';
type MobileTab = 'levels' | 'members' | 'rewards';
type Modal =
@@ -416,7 +417,7 @@ async function exportExcel() {
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.href = url;
a.download = 'mento-loyalty-members.xlsx';
a.download = `${brand.filePrefix}-loyalty-members.xlsx`;
a.click();
URL.revokeObjectURL(url);
} catch {
+2 -1
View File
@@ -17,6 +17,7 @@ import {
import UiIcon from '@/components/UiIcon.vue';
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
import MentoAiAvatar from '@/components/brand/MentoAiAvatar.vue';
import { brand } from '@mento/shared';
import MentoMemoryPanel from '@/components/MentoMemoryPanel.vue';
import AskMentoPanel from '@/components/AskMentoPanel.vue';
import ProfileEmptyState from '@/components/person-profile/ProfileEmptyState.vue';
@@ -846,7 +847,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
<div>
<h2>
<UiIcon name="sparkle" :size="14" />
خلاصه هوشمند منتو
خلاصه هوشمند {{ brand.nameFa }}
</h2>
<p>{{ shownSmart.text }}</p>
</div>
+2 -1
View File
@@ -15,6 +15,7 @@ import type {
PortalRequestOtpResponse,
PortalTicketDto,
} from '@mento/shared';
import { brand } from '@mento/shared';
const route = useRoute();
const router = useRouter();
@@ -269,7 +270,7 @@ watch(slug, () => {
<div class="portal">
<header class="top">
<div>
<p class="brand">منتو · پرتال مشتری</p>
<p class="brand">{{ brand.nameFa }} · پرتال مشتری</p>
<h1>{{ org?.name || '…' }}</h1>
</div>
<button v-if="person" class="linkish" type="button" @click="logout">
+5 -4
View File
@@ -4,6 +4,7 @@ import { RouterLink, useRoute, useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { api } from '@/lib/api';
import type { AskMentoResponse } from '@mento/shared';
import { brand, brandSuggestionFa } from '@mento/shared';
import { formatCompactMoney } from '@/lib/money';
import {
calendarDaysBetween,
@@ -145,9 +146,9 @@ const smartText = computed(() => {
const smartCta = computed(() => {
if (daysSinceContact.value != null && daysSinceContact.value <= 1) {
return 'پیشنهاد منتو: پیگیری بعدی را ثبت کن';
return brandSuggestionFa('پیگیری بعدی را ثبت کن');
}
return 'پیشنهاد منتو: امروز با مشتری تماس بگیر';
return brandSuggestionFa('امروز با مشتری تماس بگیر');
});
const kpis = computed(() => [
@@ -377,12 +378,12 @@ watch(() => auth.user?.id, () => void load(), { immediate: true });
<div>
<h2>
<UiIcon name="sparkle" :size="14" />
خلاصه هوشمند منتو
خلاصه هوشمند {{ brand.nameFa }}
</h2>
<p>{{ smartText }}</p>
</div>
</header>
<AskMentoPanel placeholder="از منتو درباره کار امروز بپرس..." />
<AskMentoPanel :placeholder="`${brand.askLabelFa} درباره کار امروز...`" />
<button class="smart-cta" type="button" @click="onSmartCta">
<span>{{ smartCta }}</span>
<UiIcon name="chevron" :size="16" />
+6 -5
View File
@@ -6,7 +6,8 @@ import { BUSINESS_FIELDS, BUSINESS_FIELD_LABELS } from '@/lib/business-fields';
import AuthBrandPanel from '@/components/auth/AuthBrandPanel.vue';
import MentoLogo from '@/components/brand/MentoLogo.vue';
import axios from 'axios';
import type { RegisterChallengeResponse } from '@mento/shared';
import { brand, type RegisterChallengeResponse } from '@mento/shared';
import BrandName from '@/components/brand/BrandName.vue';
const auth = useAuthStore();
const router = useRouter();
@@ -106,15 +107,15 @@ function backToForm() {
<div class="auth-shell">
<AuthBrandPanel
headline="همه ارتباطات، یکجا مدیریت می‌شوند"
description="با ساخت حساب منتو، سازمان خود را راه‌اندازی کنید و فروش و پشتیبانی را از یکجا پیش ببرید."
:description="`با ساخت حساب ${brand.nameFa}، سازمان خود را راه‌اندازی کنید و فروش و پشتیبانی را از یکجا پیش ببرید.`"
/>
<section class="form-panel">
<header class="mobile-brand">
<RouterLink class="back-link" to="/login">بازگشت</RouterLink>
<MentoLogo :size="48" :show-text="false" />
<strong>منتو</strong>
<span>نرم‌افزار مدیریت ارتباط با مشتری</span>
<BrandName />
<span>{{ brand.taglineFa }}</span>
</header>
<div class="form-inner">
@@ -329,7 +330,7 @@ function backToForm() {
</div>
<footer class="auth-footer">
<p>۱۴۰۵ منتو. تمامی حقوق محفوظ است.</p>
<p>۱۴۰۵ {{ brand.nameFa }}. تمامی حقوق محفوظ است.</p>
<nav>
<a href="#">پشتیبانی</a>
<span>|</span>
+3 -2
View File
@@ -13,6 +13,7 @@ import {
import JalaliDateInput from '@/components/JalaliDateInput.vue';
import UiIcon from '@/components/UiIcon.vue';
import type { OrganizationDto, ReportsDashboardDto } from '@mento/shared';
import { brand } from '@mento/shared';
type PeriodKey = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'custom';
@@ -137,7 +138,7 @@ function exportReport() {
if (!data.value) return;
const d = data.value;
const lines: string[] = [
'گزارشات منتو',
`گزارشات ${brand.nameFa}`,
`بازه,${rangeLabel()}`,
'',
'شاخص,مقدار,تغییر٪',
@@ -170,7 +171,7 @@ function exportReport() {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `mento-report-${from.value || 'from'}-${to.value || 'to'}.csv`;
a.download = `${brand.filePrefix}-report-${from.value || 'from'}-${to.value || 'to'}.csv`;
a.click();
URL.revokeObjectURL(url);
}
+4 -3
View File
@@ -8,6 +8,7 @@ import type {
TahesabConnectionDto,
UpdateTahesabConnectionRequest,
} from '@mento/shared';
import { brand } from '@mento/shared';
const loading = ref(false);
const saving = ref(false);
@@ -315,7 +316,7 @@ async function importBulk(mode: 'results' | 'range') {
}
if (
!confirm(
`${codes.length} مشتری جدید از ته‌حساب به‌عنوان شخص در منتو ساخته و لینک می‌شوند. ادامه؟`,
`${codes.length} مشتری جدید از ته‌حساب به‌عنوان شخص در ${brand.nameFa} ساخته و لینک می‌شوند. ادامه؟`,
)
) {
return;
@@ -327,7 +328,7 @@ async function importBulk(mode: 'results' | 'range') {
if (!fromRaw && !toRaw) {
if (
!confirm(
'همهٔ مشتریان ته‌حساب وارد منتو می‌شوند (قبلاً لینک‌شده‌ها رد می‌شوند). ممکن است کمی طول بکشد. ادامه؟',
`همهٔ مشتریان ته‌حساب وارد ${brand.nameFa} می‌شوند (قبلاً لینک‌شده‌ها رد می‌شوند). ممکن است کمی طول بکشد. ادامه؟`,
)
) {
return;
@@ -342,7 +343,7 @@ async function importBulk(mode: 'results' | 'range') {
}
if (
!confirm(
`همهٔ مشتریان ته‌حساب با کد ${fromCode} تا ${toCode} وارد منتو می‌شوند (قبلاً لینک‌شده‌ها رد می‌شوند). ادامه؟`,
`همهٔ مشتریان ته‌حساب با کد ${fromCode} تا ${toCode} وارد ${brand.nameFa} می‌شوند (قبلاً لینک‌شده‌ها رد می‌شوند). ادامه؟`,
)
) {
return;
+16 -3
View File
@@ -1,12 +1,23 @@
import { defineConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
const PROD_API = 'https://crm.montzar.ir';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
const apiTarget = env.MENTO_DEV_API || process.env.MENTO_DEV_API || PROD_API;
const usingLocalApi = apiTarget.includes('127.0.0.1') || apiTarget.includes('localhost');
// eslint-disable-next-line no-console
console.log(`[montact] UI → ${apiTarget}${usingLocalApi ? ' (API لوکال)' : ' (سرور اصلی)'}`);
return {
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@mento/shared': fileURLToPath(new URL('../../packages/shared/src/index.ts', import.meta.url)),
},
},
server: {
@@ -14,9 +25,11 @@ export default defineConfig({
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:3000',
target: apiTarget,
changeOrigin: true,
secure: apiTarget.startsWith('https://'),
},
},
},
};
});
+1 -1
View File
@@ -25,7 +25,7 @@ SMS_PASSWORD=
SMS_SENDER=
SMS_BASE_URL=http://smspanel.Trez.ir
SMS_SEND_PATH=/SendMessageWithCode.ashx
SMS_OTP_TEMPLATE=کد ورود پرتال منتو: {code}
SMS_OTP_TEMPLATE=کد ورود پرتال منتکت: {code}
# SMS_TIMEOUT_MS=20000
# AI — GapGPT (OpenAI-compatible)
+1 -1
View File
@@ -1,4 +1,4 @@
# استقرار منتو CRM — `crm.montzar.ir`
# استقرار منتکت CRM — `crm.montzar.ir`
> **قفل‌شده:** فقط همین فایل منبع دستورات استقرار است. پکیج‌منیجر = **npm** (نه pnpm/yarn).
> قانون Cursor: `.cursor/rules/deploy.mdc`
+7 -7
View File
@@ -34,25 +34,25 @@ PRESETS = {
"roadmap": {
"md": "راهنما-و-نقشه-راه.md",
"docx": "راهنما-و-نقشه-راه.docx",
"header": "راهنما و نقشه راه منتو (Mento CRM)",
"footer": "نقشه راه منتو | نسخه ۱.۰ | وضعیت زنده محصول",
"header": "راهنما و نقشه راه منتکت (Montact CRM)",
"footer": "نقشه راه منتکت | نسخه ۱.۰ | وضعیت زنده محصول",
},
"user-guide": {
"md": "راهنمای-کاربری.md",
"docx": "راهنمای-کاربری.docx",
"header": "راهنمای کاربری منتو (Mento CRM)",
"footer": "راهنمای کاربری منتو | آموزش گام‌به‌گام برای همه همکاران",
"header": "راهنمای کاربری منتکت (Montact CRM)",
"footer": "راهنمای کاربری منتکت | آموزش گام‌به‌گام برای همه همکاران",
},
"features": {
"md": "قابلیت‌های-منتو.md",
"md": "قابلیت‌های-منتکت.md",
"docx": "قابلیت‌های-منتو.docx",
"header": "قابلیت‌های منتو (Mento CRM)",
"header": "قابلیت‌های منتکت (Montact CRM)",
"footer": "فهرست قابلیت‌های محصول | تیتر + توضیح مختصر",
},
"progress": {
"md": "وضعیت-پیشرفت.md",
"docx": "وضعیت-پیشرفت.docx",
"header": "وضعیت پیشرفت منتو (Mento CRM)",
"header": "وضعیت پیشرفت منتکت (Montact CRM)",
"footer": "چک‌لیست زنده کارها | سند وضعیت پیشرفت",
},
}
+10 -10
View File
@@ -1,19 +1,19 @@
# راهنما و نقشه راه منتو (Mento CRM)
# راهنما و نقشه راه منتکت (Montact CRM)
نسخه: ۱.۰ — وضعیت زنده محصول
تاریخ به‌روزرسانی: ۱۴ شهریور ۱۴۰۵
محصول: CRM فروش‌محور فارسی **منتو**
محصول: CRM فروش‌محور فارسی **منتکت**
استک: Vue 3 + NestJS + PostgreSQL (Prisma) | ساختار: monorepo
محیط عملیاتی: `https://crm.montzar.ir`
این سند دیگر «پیش‌نویس قبل از کدنویسی» نیست؛ خلاصهٔ دامنه، فلسفه، آنچه ساخته شده، و مسیر بعدی است.
چک‌لیست روزبه‌روز: [`وضعیت-پیشرفت.md`](وضعیت-پیشرفت.md) · فهرست قابلیت‌ها: [`قابلیت‌های-منتو.md`](قابلیت‌های-منتو.md)
چک‌لیست روزبه‌روز: [`وضعیت-پیشرفت.md`](وضعیت-پیشرفت.md) · فهرست قابلیت‌ها: [`قابلیت‌های-منتکت.md`](قابلیت‌های-منتکت.md)
---
## ۱. هدف محصول
منتو یک CRM فروش و پشتیبانی برای کسب‌وکارهای ایرانی است با این حلقه اصلی:
منتکت یک CRM فروش و پشتیبانی برای کسب‌وکارهای ایرانی است با این حلقه اصلی:
```text
سرنخ → شخص / شرکت → معامله در کاریز → فعالیت / Smart Notes → فروش مالی سبک → پس‌ازفروش
@@ -22,7 +22,7 @@
الهام اولیه از محصولات ایرانی مثل دیدار بوده، ولی هویت بصری و تمایز محصول مستقل است:
- سادگی کار روزانه فروش و پشتیبانی
- پرونده یکپارچه مشتری + حافظه هوشمند (Mento Memory / Ask Mento)
- پرونده یکپارچه مشتری + حافظه هوشمند (Montact Memory / Ask Montact)
- RTL، تاریخ جلالی، پیامک، و اتصال به حسابداری طلا/جواهر (ته‌حساب)
- نصب اختصاصی (self-host) به‌جای وابستگی اجباری به SaaS عمومی
@@ -36,7 +36,7 @@
| پنل | Vue 3 + Vite + Pinia |
| API | NestJS |
| دیتابیس | PostgreSQL + Prisma |
| UI | زبان بصری منتو (تیل، پنل سفید نرم، موبایل ≤۹۰۰) — نه کپی پیکسلی دیدار |
| UI | زبان بصری منتکت (تیل، پنل سفید نرم، موبایل ≤۹۰۰) — نه کپی پیکسلی دیدار |
| فایل | فعلاً دیسک محلی؛ Compose برای MinIO آماده است |
| لندینگ بازاریابی | Nuxt جدا، بعد از ثبات محصول |
| اپ بومی | عمداً خارج از فاز ۱ |
@@ -50,7 +50,7 @@
| مایل‌استون | وضعیت | محتوا |
|---|---|---|
| **۱-الف** هسته فروش | انجام شده | مخاطب، کاریز، فعالیت، تقویم، اکسل، نقش، چندارزی، UI |
| **۱-ب** Smart Notes / AI | تقریباً کامل | Composer، Memory، Ask Mento، Voice-to-Text؛ باقی: Note شخصی/تیمی، تاریخچه ویرایش |
| **۱-ب** Smart Notes / AI | تقریباً کامل | Composer، Memory، Ask Montact، Voice-to-Text؛ باقی: Note شخصی/تیمی، تاریخچه ویرایش |
| **۱-ج** سرنخ و مالی | انجام شده | فرم‌ساز، پیش‌فاکتور، فاکتور/اقساط، هدف فروش |
| **۱-د** پس‌ازفروش | انجام شده | تیکت، پرتال OTP، پروژه، باشگاه، ته‌حساب |
| **۱-هـ** گزارش و اتصال | نیمه‌کاره | گزارش و اتوماسیون و SMS عملیاتی آماده؛ API عمومی/وب‌هوک/درگاه هنوز نه |
@@ -61,10 +61,10 @@
## ۴. تمایز نسبت به CRMهای عمومی
| حوزه | منتو |
| حوزه | منتکت |
|---|---|
| یادداشت | Smart Notes + پیشنهاد اقدام با تأیید + Memory با منبع |
| پرسش | Ask Mento روی پرونده و یادداشت‌ها |
| پرسش | Ask Montact روی پرونده و یادداشت‌ها |
| صدا | دیکته Voice-to-Text در فرم‌ها |
| ایران | جلالی، RTL، OTP/پیامک، چندارزی ریال‌محور |
| طلا و جواهر | حوزه کسب‌وکار + اتصال ته‌حساب (مانده و سند) |
@@ -111,7 +111,7 @@
|---|---|
| [`وضعیت-پیشرفت.md`](وضعیت-پیشرفت.md) | چک‌لیست زنده |
| [`راهنمای-کاربری.md`](راهنمای-کاربری.md) | آموزش کاربران |
| [`قابلیت‌های-منتو.md`](قابلیت‌های-منتو.md) | فهرست کوتاه قابلیت‌ها |
| [`قابلیت‌های-منتکت.md`](قابلیت‌های-منتکت.md) | فهرست کوتاه قابلیت‌ها |
| [`integrations/tahesab.md`](integrations/tahesab.md) | اتصال ته‌حساب |
| [`../deploy/DEPLOY.md`](../deploy/DEPLOY.md) | استقرار production |
| [`client/MENT_Smart_Notes_extracted.md`](client/MENT_Smart_Notes_extracted.md) | PRD اولیه Smart Notes |
Binary file not shown.
+10 -10
View File
@@ -1,16 +1,16 @@
# راهنمای کاربری منتو (Mento CRM)
# راهنمای کاربری منتکت (Montact CRM)
**نسخه راهنما:** هماهنگ با محصول روی `https://crm.montzar.ir` (به‌روز ۱۴ شهریور ۱۴۰۵)
**مخاطب:** همکاران فروش، پشتیبانی و مدیران — با یا بدون تجربه CRM.
اسناد مرتبط: [`قابلیت‌های-منتو.md`](قابلیت‌های-منتو.md) · [`وضعیت-پیشرفت.md`](وضعیت-پیشرفت.md)
اسناد مرتبط: [`قابلیت‌های-منتکت.md`](قابلیت‌های-منتکت.md) · [`وضعیت-پیشرفت.md`](وضعیت-پیشرفت.md)
---
## فهرست مطالب
1. [این راهنما چیست؟](#این-راهنما-چیست)
2. [منتو چیست؟](#منتو-چیست-به-زبان-خیلی-ساده)
2. [منتکت چیست؟](#منتکت-چیست-به-زبان-خیلی-ساده)
3. [آدرس‌ها و حساب‌های دمو](#آدرسها-و-حسابهای-دمو)
4. [شروع خیلی ساده](#شروع-خیلی-ساده-۱۰-دقیقه)
5. [منوی کناری](#منوی-کناری-و-ساختار-صفحه)
@@ -38,22 +38,22 @@
## این راهنما چیست؟
به زبان ساده می‌گوید منتو چیست، کجا بروید، چه دکمه‌ای بزنید و نتیجه چه می‌شود.
به زبان ساده می‌گوید منتکت چیست، کجا بروید، چه دکمه‌ای بزنید و نتیجه چه می‌شود.
اگر تازه‌کارید از **شروع خیلی ساده** شروع کنید.
---
## منتو چیست؟ (به زبان خیلی ساده)
## منتکت چیست؟ (به زبان خیلی ساده)
منتو یک **CRM فارسی** است: دفترچه مشتری، تخته فروش، پیگیری، پشتیبانی، فاکتور، پرتال مشتری و (برای طلا و جواهر) اتصال به ته‌حساب — در یک جا.
منتکت یک **CRM فارسی** است: دفترچه مشتری، تخته فروش، پیگیری، پشتیبانی، فاکتور، پرتال مشتری و (برای طلا و جواهر) اتصال به ته‌حساب — در یک جا.
| مفهوم | یعنی چه؟ |
|---|---|
| **سازمان** | شرکت شما داخل منتو |
| **سازمان** | شرکت شما داخل منتکت |
| **شخص / شرکت** | مخاطب حقیقی یا حقوقی |
| **معامله** | یک فروش احتمالی روی کاریز |
| **فعالیت** | تماس، جلسه، وظیفه |
| **یادداشت** | ثبت سریع حرف‌ها + هوش منتو |
| **یادداشت** | ثبت سریع حرف‌ها + هوش منتکت |
| **تیکت** | درخواست پشتیبانی |
| **کاریز پروژه** | کارهای داخلی پس‌ازفروش |
| **باشگاه** | امتیاز وفاداری مشتری |
@@ -140,7 +140,7 @@
## ۲) خانه امروز
بعد از ورود می‌بینید: خلاصه فعالیت‌ها، میانبرها، نمای کاریز/روند فروش، و امکان **پرسیدن از منتو** (Ask Mento) درباره کار امروز.
بعد از ورود می‌بینید: خلاصه فعالیت‌ها، میانبرها، نمای کاریز/روند فروش، و امکان **پرسیدن از منتکت** (Ask Montact) درباره کار امروز.
---
@@ -191,7 +191,7 @@
2. نوع یادداشت را انتخاب کنید و متن را بنویسید (یا با دکمه **میکروفون** دیکته کنید).
3. به شخص/شرکت/معامله وصل کنید؛ در صورت نیاز پین/برچسب/یادآور.
4. پیشنهاد هوشمند یا Memory را فقط بعد از خواندن و **تأیید** اعمال کنید.
5. از کادر **از منتو بپرس** سؤال زبان طبیعی بپرسید (مثلاً «آخرین بار درباره قیمت چه گفت؟»).
5. از کادر **از منتکت بپرس** سؤال زبان طبیعی بپرسید (مثلاً «آخرین بار درباره قیمت چه گفت؟»).
پیوست فایل روی یادداشت از همان فرم قابل افزودن است.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
# قابلیت‌های منتو (Mento CRM)
# قابلیت‌های منتکت (Montact CRM)
فهرست کوتاه قابلیت‌های محصول — هر تیتر یک امکان، با توضیح یک‌خطی.
به‌روزرسانی: ۱۴ شهریور ۱۴۰۵ · جزئیات کاربری: [`راهنمای-کاربری.md`](راهنمای-کاربری.md)
@@ -33,7 +33,7 @@
## خانه و پروفایل
### داشبورد خانه امروز
خلاصه فروش، فعالیت، کاریز، میانبرها و دستیار Ask Mento در یک صفحه.
خلاصه فروش، فعالیت، کاریز، میانبرها و دستیار Ask Montact در یک صفحه.
### پروفایل کاربر
از کارت پایین سایدبار: وضعیت کارها و میانبرهای شخصی.
@@ -126,10 +126,10 @@
### پیشنهاد اقدام هوشمند
AI پیشنهاد Next Action می‌دهد؛ اعمال فقط با تأیید کاربر.
### Mento Memory
### Montact Memory
خلاصه پایدار مشتری با منبع؛ قابل ویرایش و حذف.
### Ask Mento
### Ask Montact
پرسش به زبان طبیعی از روی یادداشت‌ها و زمینه پرونده.
### دیکته صوتی (Voice-to-Text)
@@ -212,7 +212,7 @@ AI پیشنهاد Next Action می‌دهد؛ اعمال فقط با تأیید
ارزهای فعال و نرخ به ارز پایه.
### ظاهر یکدست و موبایل‌فرندلی
زبان بصری منتو در لیست‌ها، فرم‌ها و تنظیمات؛ کار روی موبایل با منوی کشویی.
زبان بصری منتکت در لیست‌ها، فرم‌ها و تنظیمات؛ کار روی موبایل با منوی کشویی.
---
@@ -235,7 +235,7 @@ AI پیشنهاد Next Action می‌دهد؛ اعمال فقط با تأیید
## عمداً خارج از محصول فعلی
- اپ بومی اندروید/iOS جدا
- حسابداری کامل و انبار ERP داخل خود منتو
- حسابداری کامل و انبار ERP داخل خود منتکت
- API عمومی مستند و وب‌هوک خروجی (در نقشه راه بعدی)
- درگاه پرداخت آنلاین داخل پنل
- لندینگ بازاریابی Nuxt (بعد از ثبات)
Binary file not shown.
+5 -5
View File
@@ -1,4 +1,4 @@
# وضعیت پیشرفت منتو (Mento CRM)
# وضعیت پیشرفت منتکت (Montact CRM)
سند زندهٔ کارها — با هر قابلیت جدید همین فایل به‌روز می‌شود.
آخرین به‌روزرسانی: ۱۴ شهریور ۱۴۰۵
@@ -14,7 +14,7 @@
| تاریخ | مورد |
|---|---|
| ۱۴۰۵/۶/۱۴ | هم‌تراز کردن ظاهر صفحات قدیمی با زبان بصری منتو (لیست/فرم/تنظیمات/گزارش) + جداکننده هزارگان روی فیلدهای مبلغ |
| ۱۴۰۵/۶/۱۴ | هم‌تراز کردن ظاهر صفحات قدیمی با زبان بصری منتکت (لیست/فرم/تنظیمات/گزارش) + جداکننده هزارگان روی فیلدهای مبلغ |
| ۱۴۰۵/۶/۱۴ | یادآور فعالیت: کانال اعلان داخل‌برنامه و/یا پیامک |
| ۱۴۰۵/۶/۱۴ | انتخابگر زمان دایره‌ای در `JalaliDateInput` |
| ۱۴۰۵/۶/۱۴ | اصلاح لایه‌بندی موبایل مخاطبین و یادداشت‌ها |
@@ -65,8 +65,8 @@
| [x] | تایم‌لاین یکپارچه | |
| [x] | پیوست فایل روی Note | `uploads/` محلی؛ آماده MinIO |
| [x] | AI: Next Action / پیشنهاد اقدام | با تأیید کاربر |
| [x] | Mento Memory با منبع | خلاصه پایدار + ویرایش/حذف |
| [x] | Ask Mento | پرسش زبان طبیعی روی یادداشت‌ها / پروفایل / خانه |
| [x] | Montact Memory با منبع | خلاصه پایدار + ویرایش/حذف |
| [x] | Ask Montact | پرسش زبان طبیعی روی یادداشت‌ها / پروفایل / خانه |
| [x] | Voice-to-Text | دکمه دیکته (Pipecat/STT) روی Note و فرم‌ها |
| [ ] | Note شخصی در برابر تیمی | |
| [ ] | تاریخچه ویرایش Note | |
@@ -136,7 +136,7 @@
- نقشه راه: [`docs/راهنما-و-نقشه-راه.md`](راهنما-و-نقشه-راه.md)
- راهنمای کاربری: [`docs/راهنمای-کاربری.md`](راهنمای-کاربری.md)
- فهرست قابلیت‌ها: [`docs/قابلیت‌های-منتو.md`](قابلیت‌های-منتو.md)
- فهرست قابلیت‌ها: [`docs/قابلیت‌های-منتکت.md`](قابلیت‌های-منتکت.md)
- ته‌حساب: [`docs/integrations/tahesab.md`](integrations/tahesab.md)
- استقرار: [`deploy/DEPLOY.md`](../deploy/DEPLOY.md)
- خلاصه ریشه: [`README.md`](../README.md)
Binary file not shown.
+3 -2
View File
@@ -2,15 +2,16 @@
"name": "mento-crm",
"private": true,
"version": "0.1.0",
"description": "Mento CRM (منتو) monorepo — Vue panel + NestJS API + PostgreSQL",
"description": "Montact CRM (منتکت) monorepo — Vue panel + NestJS API + PostgreSQL",
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "npm run dev --workspaces --if-present",
"dev": "npm run dev:web",
"dev:web": "npm run dev -w @mento/web",
"dev:api": "npm run start:dev -w @mento/api",
"dev:all": "npm run dev --workspaces --if-present",
"dev:stt": "apps/stt/.venv/Scripts/python.exe -m uvicorn main:app --host 127.0.0.1 --port 3040 --app-dir apps/stt",
"build": "npm run build --workspaces --if-present",
"build:shared": "npm run build -w @mento/shared",
+33
View File
@@ -0,0 +1,33 @@
/**
* تنها منبع نام و لوگوی محصول.
* برای تغییر برند همین فایل را عوض کن؛ برای لوگو فایل
* `apps/web/public/brand/logo.png` را جایگزین کن یا مسیر `logo` را اینجا تغییر بده.
*/
export const brand = {
id: 'montact',
name: 'Montact',
nameFa: 'منتکت',
fullName: 'Montact CRM',
company: 'Mentech',
taglineFa: 'نرم‌افزار مدیریت ارتباط با مشتری',
logo: '/brand/logo.png',
askPlaceholderFa: 'از منتکت بپرس...',
askLabelFa: 'از منتکت بپرس',
memoryEn: 'Montact Memory',
aiName: 'Montact AI',
aiNameFa: 'دستیار هوشمند منتکت',
suggestionPrefixFa: 'پیشنهاد منتکت:',
reminderPrefixFa: 'یادآوری منتکت:',
filePrefix: 'montact',
} as const;
export type Brand = typeof brand;
/** @deprecated از brand.fullName استفاده کنید */
export const APP_NAME = brand.fullName;
/** @deprecated از brand.nameFa استفاده کنید */
export const APP_NAME_FA = brand.nameFa;
export function brandSuggestionFa(action: string) {
return `${brand.suggestionPrefixFa} ${action}`;
}
+2 -2
View File
@@ -1856,6 +1856,6 @@ export interface PersonAssetsDto {
fetchedAt: string;
}
export const APP_NAME = "Mento CRM";
export const APP_NAME_FA = "منتو";
export { brand, APP_NAME, APP_NAME_FA, brandSuggestionFa } from "./brand";
export type { Brand } from "./brand";
export const API_PREFIX = "api";
+37
View File
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
"""Make near-black logo background transparent."""
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
FILES = [
ROOT / "apps/web/public/brand/logo.png",
ROOT / "apps/web/public/brand/montactLogo.png",
]
def knockout(src: Path) -> None:
im = Image.open(src).convert("RGBA")
pixels = im.load()
w, h = im.size
for y in range(h):
for x in range(w):
r, g, b, a = pixels[x, y]
luma = 0.2126 * r + 0.7152 * g + 0.0722 * b
chroma = max(r, g, b) - min(r, g, b)
# Keep teal M (has chroma). Knock out near-black / gray canvas.
if luma < 28 and chroma < 18:
pixels[x, y] = (r, g, b, 0)
elif luma < 48 and chroma < 22:
pixels[x, y] = (r, g, b, int(a * (luma - 28) / 20))
im.save(src)
print(f"updated {src}")
if __name__ == "__main__":
for path in FILES:
if path.exists():
knockout(path)
else:
print(f"missing {path}")