import { z } from "zod"; import { req, text } from "./common.js"; export const ROLES = ["ADMIN", "EDITOR"] as const; export type Role = (typeof ROLES)[number]; export const ROLE_LABELS: Record = { ADMIN: "مدیر کل", EDITOR: "ویرایشگر", }; const password = z.string().min(8, "رمز عبور باید حداقل ۸ کاراکتر باشد").max(128); const FA_DIGITS = "۰۱۲۳۴۵۶۷۸۹"; const AR_DIGITS = "٠١٢٣٤٥٦٧٨٩"; /** Normalize Iranian mobile to `09XXXXXXXXX` (Latin digits). */ export function normalizeIrMobile(raw: string): string { let s = raw.trim().replace(/[\s\-()]/g, ""); s = s.replace(/[۰-۹٠-٩]/g, (d) => { const fa = FA_DIGITS.indexOf(d); if (fa >= 0) return String(fa); const ar = AR_DIGITS.indexOf(d); return ar >= 0 ? String(ar) : d; }); if (s.startsWith("+98")) s = `0${s.slice(3)}`; else if (s.startsWith("0098")) s = `0${s.slice(4)}`; else if (s.startsWith("98") && s.length === 12) s = `0${s.slice(2)}`; return s; } export const irMobileSchema = z .string() .trim() .min(1, "شماره موبایل را وارد کنید") .transform(normalizeIrMobile) .pipe(z.string().regex(/^09\d{9}$/, "شماره موبایل معتبر نیست (مثلاً ۰۹۱۲۱۲۳۴۵۶۷)")); export const loginSchema = z.object({ phone: irMobileSchema, password: z.string().min(1, "رمز عبور را وارد کنید").max(128), }); export type LoginInput = z.infer; export const changePasswordSchema = z.object({ currentPassword: z.string().min(1, "رمز فعلی را وارد کنید").max(128), newPassword: password, }); export type ChangePasswordInput = z.infer; export const userCreateSchema = z.object({ phone: irMobileSchema, name: req(80), role: z.enum(ROLES), password, }); export type UserCreateInput = z.infer; export const userUpdateSchema = z.object({ name: req(80), role: z.enum(ROLES), isActive: z.boolean(), /** set only when resetting the password */ password: password.optional(), }); export type UserUpdateInput = z.infer; export interface AdminUserDto { id: number; phone: string; name: string; role: Role; isActive: boolean; lastLoginAt: string | null; createdAt: string; } export interface AuthResponse { accessToken: string; user: AdminUserDto; } /* ---------------- contact messages ---------------- */ export const MESSAGE_STATUSES = ["NEW", "READ", "ARCHIVED"] as const; export type MessageStatus = (typeof MESSAGE_STATUSES)[number]; export const MESSAGE_STATUS_LABELS: Record = { NEW: "جدید", READ: "خوانده‌شده", ARCHIVED: "بایگانی", }; export const contactInputSchema = z.object({ name: req(80), org: req(120), phone: z .string() .trim() .min(1, "این فیلد الزامی است") .max(30) .regex(/^[0-9۰-۹+\-\s()]{5,30}$/, "شماره تماس معتبر نیست"), message: text(2000), /** honeypot — real visitors never fill it */ website: z.string().max(200).optional(), }); export type ContactInput = z.infer; export const messageUpdateSchema = z.object({ status: z.enum(MESSAGE_STATUSES), }); export interface ContactMessageDto { id: number; name: string; org: string; phone: string; message: string; status: MessageStatus; createdAt: string; } /* ---------------- media ---------------- */ export interface MediaDto { id: number; url: string; alt: string; mime: string; size: number; width: number | null; height: number | null; originalName: string; createdAt: string; } export const mediaUpdateSchema = z.object({ alt: text(200) }); export interface Paginated { items: T[]; total: number; page: number; pageSize: number; } export interface DashboardCategoryStat { id: number; name: string; count: number; } export interface DashboardTrendDay { date: string; dayName: string; messages: number; } export interface DashboardDto { categories: number; products: number; draftProducts: number; newMessages: number; media: number; recentMessages: ContactMessageDto[]; recentActivity: { id: number; action: string; entity: string; summary: string; user: string | null; createdAt: string }[]; categoryStats: DashboardCategoryStat[]; messageBreakdown: { new: number; read: number; archived: number; total: number }; sectionsSummary: { total: number; visible: number; hidden: number }; trend: DashboardTrendDay[]; }