diff --git a/.env.example b/.env.example index 6e99349..ff83b09 100644 --- a/.env.example +++ b/.env.example @@ -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 (۶۴ کاراکتر) diff --git a/.gitignore b/.gitignore index d46d336..b9e54ec 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist/ coverage/ .env .env.local +.env.*.local *.log .DS_Store apps/api/prisma/*.db diff --git a/README.md b/README.md index 9c1afee..d517004 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts index 57d8618..57775bc 100644 --- a/apps/api/prisma/seed.ts +++ b/apps/api/prisma/seed.ts @@ -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', diff --git a/apps/api/src/ai/ai-ask.service.ts b/apps/api/src/ai/ai-ask.service.ts index 057ebb1..5486792 100644 --- a/apps/api/src/ai/ai-ask.service.ts +++ b/apps/api/src/ai/ai-ask.service.ts @@ -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, }; } diff --git a/apps/api/src/common/sms.service.ts b/apps/api/src/common/sms.service.ts index 96ae706..217a849 100644 --- a/apps/api/src/common/sms.service.ts +++ b/apps/api/src/common/sms.service.ts @@ -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 { const template = this.config.get('SMS_OTP_TEMPLATE') ?? - 'کد ورود پرتال منتو: {code}'; + `کد ورود پرتال ${brand.nameFa}: {code}`; const text = template.replace('{code}', code); const result = await this.sendSmsDetailed(mobile, text); return result.ok; diff --git a/apps/api/src/contacts/companies.controller.ts b/apps/api/src/contacts/companies.controller.ts index 58a3225..81601db 100644 --- a/apps/api/src/contacts/companies.controller.ts +++ b/apps/api/src/contacts/companies.controller.ts @@ -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); } diff --git a/apps/api/src/contacts/people.controller.ts b/apps/api/src/contacts/people.controller.ts index 814764d..0a145cf 100644 --- a/apps/api/src/contacts/people.controller.ts +++ b/apps/api/src/contacts/people.controller.ts @@ -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); } diff --git a/apps/api/src/health/health.controller.ts b/apps/api/src/health/health.controller.ts index 8ca46d5..385aae6 100644 --- a/apps/api/src/health/health.controller.ts +++ b/apps/api/src/health/health.controller.ts @@ -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(), }; } diff --git a/apps/api/src/loyalty/loyalty.controller.ts b/apps/api/src/loyalty/loyalty.controller.ts index 9c0b691..96ec7ad 100644 --- a/apps/api/src/loyalty/loyalty.controller.ts +++ b/apps/api/src/loyalty/loyalty.controller.ts @@ -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); } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 9759418..75cc6de 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -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('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(); diff --git a/apps/api/src/notifications/notifications.service.ts b/apps/api/src/notifications/notifications.service.ts index 0b4d2a4..b921d59 100644 --- a/apps/api/src/notifications/notifications.service.ts +++ b/apps/api/src/notifications/notifications.service.ts @@ -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'}`); diff --git a/apps/stt/README.md b/apps/stt/README.md index 4ac3585..611f698 100644 --- a/apps/stt/README.md +++ b/apps/stt/README.md @@ -1,4 +1,4 @@ -# Mento STT (Pipecat) +# Montact STT (Pipecat) تبدیل صوت به متن — فقط STT. بدون TTS، بدون مکالمهٔ صوتی. diff --git a/apps/web/index.html b/apps/web/index.html index 5e36823..3c83132 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -3,7 +3,8 @@ - منتو | Mento CRM + منتکت | Montact CRM +
diff --git a/apps/web/public/brand/logo.png b/apps/web/public/brand/logo.png new file mode 100644 index 0000000..96add08 Binary files /dev/null and b/apps/web/public/brand/logo.png differ diff --git a/apps/web/public/brand/montactLogo.png b/apps/web/public/brand/montactLogo.png new file mode 100644 index 0000000..96add08 Binary files /dev/null and b/apps/web/public/brand/montactLogo.png differ diff --git a/apps/web/src/components/AskMentoPanel.vue b/apps/web/src/components/AskMentoPanel.vue index b7ff494..cf9099a 100644 --- a/apps/web/src/components/AskMentoPanel.vue +++ b/apps/web/src/components/AskMentoPanel.vue @@ -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() {