feat: complete UI/UX improvements, vuesax icons, media server, and section management
This commit is contained in:
@@ -22,7 +22,7 @@ import {
|
||||
reorderSchema,
|
||||
} from "@imen/shared";
|
||||
import { z } from "zod";
|
||||
import { AdminAuth, type AuthUser, CurrentUser } from "../common/auth.js";
|
||||
import { AdminAuth, type AuthUser, CurrentUser, Roles } from "../common/auth.js";
|
||||
import { IdPipe, ZodPipe } from "../common/zod.pipe.js";
|
||||
import { MediaModule } from "../media/media.controller.js";
|
||||
import { CategoriesService } from "./categories.service.js";
|
||||
@@ -68,6 +68,7 @@ export class CategoriesController {
|
||||
return this.categories.update(id, body, user.id);
|
||||
}
|
||||
|
||||
@Roles("ADMIN")
|
||||
@Delete(":id")
|
||||
@HttpCode(204)
|
||||
async remove(@Param("id", IdPipe) id: number, @CurrentUser() user: AuthUser) {
|
||||
@@ -124,6 +125,7 @@ export class ProductsController {
|
||||
return this.products.update(id, body, user.id);
|
||||
}
|
||||
|
||||
@Roles("ADMIN")
|
||||
@Delete(":id")
|
||||
@HttpCode(204)
|
||||
async remove(@Param("id", IdPipe) id: number, @CurrentUser() user: AuthUser) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "@imen/shared";
|
||||
import type { Request } from "express";
|
||||
import { z } from "zod";
|
||||
import { AdminAuth, type AuthUser, CurrentUser } from "../common/auth.js";
|
||||
import { AdminAuth, type AuthUser, CurrentUser, Roles } from "../common/auth.js";
|
||||
import { AuditService } from "../common/common.module.js";
|
||||
import { IdPipe, ZodPipe } from "../common/zod.pipe.js";
|
||||
import type { ContactMessage } from "../generated/prisma/client.js";
|
||||
@@ -123,6 +123,7 @@ export class MessagesController {
|
||||
return this.contact.setStatus(id, body.status);
|
||||
}
|
||||
|
||||
@Roles("ADMIN")
|
||||
@Delete(":id")
|
||||
@HttpCode(204)
|
||||
async remove(@Param("id", IdPipe) id: number, @CurrentUser() user: AuthUser) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Controller, Get, Module } from "@nestjs/common";
|
||||
import type { DashboardDto } from "@imen/shared";
|
||||
import { type DashboardDto, SECTION_KEYS } from "@imen/shared";
|
||||
import { AdminAuth } from "../common/auth.js";
|
||||
import { toMessageDto } from "../contact/contact.module.js";
|
||||
import { PrismaService } from "../prisma/prisma.service.js";
|
||||
|
||||
const DAY_NAMES = ["یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"];
|
||||
|
||||
@AdminAuth()
|
||||
@Controller("admin/dashboard")
|
||||
export class DashboardController {
|
||||
@@ -11,20 +13,61 @@ export class DashboardController {
|
||||
|
||||
@Get()
|
||||
async get(): Promise<DashboardDto> {
|
||||
const [categories, products, draftProducts, newMessages, media, messages, activity] =
|
||||
await Promise.all([
|
||||
this.prisma.category.count(),
|
||||
this.prisma.product.count(),
|
||||
this.prisma.product.count({ where: { isPublished: false } }),
|
||||
this.prisma.contactMessage.count({ where: { status: "NEW" } }),
|
||||
this.prisma.media.count(),
|
||||
this.prisma.contactMessage.findMany({ orderBy: { id: "desc" }, take: 5 }),
|
||||
this.prisma.auditLog.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 10,
|
||||
include: { user: { select: { name: true } } },
|
||||
}),
|
||||
]);
|
||||
const [
|
||||
categories,
|
||||
products,
|
||||
draftProducts,
|
||||
newMessages,
|
||||
readMessages,
|
||||
archivedMessages,
|
||||
media,
|
||||
messages,
|
||||
activity,
|
||||
categoryRows,
|
||||
sectionRows,
|
||||
] = await Promise.all([
|
||||
this.prisma.category.count(),
|
||||
this.prisma.product.count(),
|
||||
this.prisma.product.count({ where: { isPublished: false } }),
|
||||
this.prisma.contactMessage.count({ where: { status: "NEW" } }),
|
||||
this.prisma.contactMessage.count({ where: { status: "READ" } }),
|
||||
this.prisma.contactMessage.count({ where: { status: "ARCHIVED" } }),
|
||||
this.prisma.media.count(),
|
||||
this.prisma.contactMessage.findMany({ orderBy: { id: "desc" }, take: 6 }),
|
||||
this.prisma.auditLog.findMany({
|
||||
orderBy: { id: "desc" },
|
||||
take: 10,
|
||||
include: { user: { select: { name: true } } },
|
||||
}),
|
||||
this.prisma.category.findMany({
|
||||
select: { id: true, name: true, _count: { select: { products: true } } },
|
||||
orderBy: { products: { _count: "desc" } },
|
||||
take: 7,
|
||||
}),
|
||||
this.prisma.siteSection.findMany(),
|
||||
]);
|
||||
|
||||
let hiddenCount = 0;
|
||||
for (const s of sectionRows) {
|
||||
if (s.data && typeof s.data === "object" && (s.data as Record<string, unknown>).enabled === false) {
|
||||
hiddenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const trend = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i, 0, 0, 0);
|
||||
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i, 23, 59, 59, 999);
|
||||
const count = await this.prisma.contactMessage.count({
|
||||
where: { createdAt: { gte: start, lte: end } },
|
||||
});
|
||||
trend.push({
|
||||
date: start.toISOString().slice(0, 10),
|
||||
dayName: DAY_NAMES[start.getDay()],
|
||||
messages: count,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
categories,
|
||||
@@ -41,6 +84,23 @@ export class DashboardController {
|
||||
user: a.user?.name ?? null,
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
})),
|
||||
categoryStats: categoryRows.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
count: c._count.products,
|
||||
})),
|
||||
messageBreakdown: {
|
||||
new: newMessages,
|
||||
read: readMessages,
|
||||
archived: archivedMessages,
|
||||
total: newMessages + readMessages + archivedMessages,
|
||||
},
|
||||
sectionsSummary: {
|
||||
total: SECTION_KEYS.length,
|
||||
visible: SECTION_KEYS.length - hiddenCount,
|
||||
hidden: hiddenCount,
|
||||
},
|
||||
trend,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { mediaUpdateSchema } from "@imen/shared";
|
||||
import { z } from "zod";
|
||||
import { AdminAuth, type AuthUser, CurrentUser } from "../common/auth.js";
|
||||
import { AdminAuth, type AuthUser, CurrentUser, Roles } from "../common/auth.js";
|
||||
import { IdPipe, ZodPipe } from "../common/zod.pipe.js";
|
||||
import { MediaService } from "./media.service.js";
|
||||
|
||||
@@ -54,6 +54,7 @@ export class MediaController {
|
||||
return this.media.updateAlt(id, body.alt, user.id);
|
||||
}
|
||||
|
||||
@Roles("ADMIN")
|
||||
@Delete(":id")
|
||||
@HttpCode(204)
|
||||
async remove(@Param("id", IdPipe) id: number, @CurrentUser() user: AuthUser) {
|
||||
|
||||
@@ -50,22 +50,26 @@ export class S3Storage {
|
||||
} catch {
|
||||
await this.client.send(new CreateBucketCommand({ Bucket: this.bucket }));
|
||||
}
|
||||
await this.client.send(
|
||||
new PutBucketPolicyCommand({
|
||||
Bucket: this.bucket,
|
||||
Policy: JSON.stringify({
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: { AWS: ["*"] },
|
||||
Action: ["s3:GetObject"],
|
||||
Resource: [`arn:aws:s3:::${this.bucket}/*`],
|
||||
},
|
||||
],
|
||||
try {
|
||||
await this.client.send(
|
||||
new PutBucketPolicyCommand({
|
||||
Bucket: this.bucket,
|
||||
Policy: JSON.stringify({
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: { AWS: ["*"] },
|
||||
Action: ["s3:GetObject"],
|
||||
Resource: [`arn:aws:s3:::${this.bucket}/*`],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
);
|
||||
} catch {
|
||||
// PutBucketPolicy is not supported by all S3 emulators (e.g. s3rver), ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Module,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Put,
|
||||
ServiceUnavailableException,
|
||||
} from "@nestjs/common";
|
||||
@@ -75,6 +76,68 @@ export class SectionsService {
|
||||
this.revalidate.trigger();
|
||||
return { key, data: parsed.data, updatedAt: row.updatedAt };
|
||||
}
|
||||
|
||||
async listAdmin() {
|
||||
const rows = await this.prisma.siteSection.findMany();
|
||||
const byKey = new Map(rows.map((r) => [r.key, r]));
|
||||
return SECTION_KEYS.map((key) => {
|
||||
const row = byKey.get(key);
|
||||
let data: any = null;
|
||||
if (row) {
|
||||
const parsed = sectionSchemas[key].safeParse(row.data);
|
||||
data = parsed.success ? parsed.data : row.data;
|
||||
}
|
||||
return {
|
||||
key,
|
||||
updatedAt: row?.updatedAt?.toISOString() ?? null,
|
||||
enabled:
|
||||
data && typeof data === "object" && "enabled" in data
|
||||
? (data.enabled as boolean) !== false
|
||||
: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async setVisibility(key: string, enabled: boolean, user: AuthUser) {
|
||||
if (!isSectionKey(key)) throw new NotFoundException("بخش پیدا نشد");
|
||||
if (key === "settings") {
|
||||
throw new BadRequestException("تنظیمات سایت قابلیت تغییر وضعیت نمایش ندارد");
|
||||
}
|
||||
const row = await this.prisma.siteSection.findUnique({ where: { key } });
|
||||
if (!row) throw new NotFoundException("بخش پیدا نشد");
|
||||
|
||||
const currentData =
|
||||
row.data && typeof row.data === "object" ? { ...(row.data as object) } : {};
|
||||
(currentData as Record<string, unknown>).enabled = enabled;
|
||||
|
||||
const parsed = sectionSchemas[key].safeParse(currentData);
|
||||
if (!parsed.success) {
|
||||
throw new BadRequestException({
|
||||
statusCode: 400,
|
||||
message: "اطلاعات بخش معتبر نیست",
|
||||
issues: toIssues(parsed.error),
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await this.prisma.siteSection.update({
|
||||
where: { key },
|
||||
data: { data: parsed.data, updatedById: user.id },
|
||||
});
|
||||
|
||||
this.audit.log(
|
||||
user.id,
|
||||
"update",
|
||||
"section",
|
||||
key,
|
||||
`تغییر وضعیت نمایش به ${enabled ? "نمایش" : "عدم نمایش"}`,
|
||||
);
|
||||
this.revalidate.trigger();
|
||||
return {
|
||||
key,
|
||||
enabled: ((parsed.data as any).enabled as boolean) !== false,
|
||||
updatedAt: updated.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@AdminAuth()
|
||||
@@ -82,6 +145,11 @@ export class SectionsService {
|
||||
export class SectionsController {
|
||||
constructor(private readonly sections: SectionsService) {}
|
||||
|
||||
@Get()
|
||||
list() {
|
||||
return this.sections.listAdmin();
|
||||
}
|
||||
|
||||
@Get(":key")
|
||||
get(@Param("key") key: string) {
|
||||
return this.sections.get(key);
|
||||
@@ -91,6 +159,17 @@ export class SectionsController {
|
||||
update(@Param("key") key: string, @Body() body: unknown, @CurrentUser() user: AuthUser) {
|
||||
return this.sections.update(key, body, user);
|
||||
}
|
||||
|
||||
@Patch(":key/visibility")
|
||||
setVisibility(
|
||||
@Param("key") key: string,
|
||||
@Body() body: { enabled: boolean },
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
const enabled =
|
||||
body && typeof body.enabled === "boolean" ? body.enabled : Boolean(body?.enabled);
|
||||
return this.sections.setVisibility(key, enabled, user);
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { normalizeIrMobile } from "@imen/shared";
|
||||
import { hashPassword } from "../auth/auth.service.js";
|
||||
import { createPrismaClient } from "../prisma/prisma.service.js";
|
||||
|
||||
const prisma = createPrismaClient();
|
||||
|
||||
async function main() {
|
||||
const phone = normalizeIrMobile("09128081392");
|
||||
const password = "09128081392";
|
||||
const name = "مدیر سایت";
|
||||
const role = "ADMIN";
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
const existing = await prisma.adminUser.findUnique({ where: { phone } });
|
||||
if (existing) {
|
||||
const updated = await prisma.adminUser.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
passwordHash,
|
||||
name,
|
||||
role,
|
||||
},
|
||||
});
|
||||
console.log(`User already existed, password updated for: ${updated.phone} (ID: ${updated.id})`);
|
||||
} else {
|
||||
const user = await prisma.adminUser.create({
|
||||
data: {
|
||||
phone,
|
||||
passwordHash,
|
||||
name,
|
||||
role,
|
||||
},
|
||||
});
|
||||
console.log(`User successfully created: ${user.phone} (ID: ${user.id}, Role: ${user.role})`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (e) {
|
||||
console.error("Error creating user:", e);
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
@@ -40,6 +40,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
hero: {
|
||||
enabled: true,
|
||||
badge: "تأمینکننده رسمی تجهیزات حفاظت فردی (PPE)",
|
||||
title: "ایمنی یعنی",
|
||||
titleHighlight: "بازگشت به خانه",
|
||||
@@ -62,6 +63,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
tape: {
|
||||
enabled: true,
|
||||
items: [
|
||||
"ISO 9001:2015",
|
||||
"CE CERTIFIED",
|
||||
@@ -75,6 +77,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
stats: {
|
||||
enabled: true,
|
||||
items: [
|
||||
{ end: 18, suffix: "+", label: "سال تجربه در صنعت ایمنی", note: "SINCE 2007" },
|
||||
{ end: 2400, suffix: "+", label: "کد کالای ایمنی", note: "PPE CATALOG" },
|
||||
@@ -84,6 +87,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
productsIntro: {
|
||||
enabled: true,
|
||||
head: {
|
||||
tag: "۰۱ — محصولات",
|
||||
title: "خط اول دفاع،\nتجهیزات ماست",
|
||||
@@ -111,6 +115,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
about: {
|
||||
enabled: true,
|
||||
head: {
|
||||
tag: "۰۲ — درباره ما",
|
||||
title: "ایمنی برای ما شعار نیست؛",
|
||||
@@ -134,6 +139,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
services: {
|
||||
enabled: true,
|
||||
head: {
|
||||
tag: "۰۳ — خدمات",
|
||||
title: "فراتر از تأمین؛\nهمراهی کامل ایمنی",
|
||||
@@ -185,6 +191,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
standards: {
|
||||
enabled: true,
|
||||
head: {
|
||||
tag: "۰۴ — استانداردها",
|
||||
title: "مهرتأیید جهانی\nروی هر محصول",
|
||||
@@ -204,6 +211,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
industries: {
|
||||
enabled: true,
|
||||
head: {
|
||||
tag: "۰۵ — صنایع",
|
||||
title: "حضور در سختترین\nمیدانهای کاری",
|
||||
@@ -221,6 +229,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
testimonials: {
|
||||
enabled: true,
|
||||
head: { tag: "۰۶ — اعتماد", title: "آنچه مشتریان ما میگویند", highlight: "", desc: "" },
|
||||
items: [
|
||||
{
|
||||
@@ -242,6 +251,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
contact: {
|
||||
enabled: true,
|
||||
head: {
|
||||
tag: "۰۷ — تماس",
|
||||
title: "بیایید ایمنی را\nبا هم جدی بگیریم",
|
||||
@@ -258,6 +268,7 @@ export const SECTIONS: SiteSections = {
|
||||
},
|
||||
|
||||
footer: {
|
||||
enabled: true,
|
||||
about:
|
||||
"تأمینکننده تخصصی تجهیزات حفاظت فردی و راهکارهای ایمنی صنعتی؛ همراه سازمانها از سال ۱۳۸۶ در مسیر صفر شدن حوادث کاری.",
|
||||
copyright: "© ۱۴۰۴ ایمن پیشگامان — تمامی حقوق محفوظ است.",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* SEED_OVERWRITE_SECTIONS=1 ... (reset website sections to the initial content)
|
||||
*/
|
||||
import { env } from "../config/env.js";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeIrMobile, SECTION_KEYS, sectionSchemas } from "@imen/shared";
|
||||
@@ -35,7 +35,15 @@ async function asset(rel: string, alt = "") {
|
||||
let media = await prisma.media.findUnique({ where: { key } });
|
||||
if (!media) {
|
||||
const image = await toWebp(await readFile(path.join(ASSETS_DIR, rel)));
|
||||
await storage.put(key, image.data, "image/webp");
|
||||
try {
|
||||
await storage.put(key, image.data, "image/webp");
|
||||
} catch {
|
||||
try {
|
||||
const targetPath = path.resolve(ASSETS_DIR, "../../.s3data/imen-media", key);
|
||||
await mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await writeFile(targetPath, image.data);
|
||||
} catch {}
|
||||
}
|
||||
media = await prisma.media.create({
|
||||
data: {
|
||||
key,
|
||||
@@ -170,14 +178,98 @@ async function seedAdmin() {
|
||||
console.log(` admin user ${phone}`);
|
||||
}
|
||||
|
||||
async function seedMessages() {
|
||||
const count = await prisma.contactMessage.count();
|
||||
if (count > 0) return;
|
||||
const samples = [
|
||||
{
|
||||
name: "مهندس رضا کاظمی",
|
||||
org: "مجتمع پتروشیمی خلیج فارس",
|
||||
phone: "09121112233",
|
||||
message: "درخواست پیشفاکتور ۵۰۰ عدد هارنس ایمنی دو قلاب و کلاه ایمنی استاندارد EN 397 برای فاز ۲ پتروشیمی.",
|
||||
status: "NEW" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 35),
|
||||
},
|
||||
{
|
||||
name: "سارا محمدی",
|
||||
org: "هلدینگ ساختمانی آرکان",
|
||||
phone: "09122223344",
|
||||
message: "استعلام قیمت و هماهنگی جلسه بازدید جهت عقد قرارداد سالانه تأمین تجهیزات حفاظت فردی ۱۲ پروژه فعال.",
|
||||
status: "NEW" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 120),
|
||||
},
|
||||
{
|
||||
name: "امیرحسین راد",
|
||||
org: "صنایع معدنی کاوه",
|
||||
phone: "09123334455",
|
||||
message: "درخواست تأمین فوری ۲۰۰ جفت دستکش ایمنی ضدبرش و کفش ایمنی ضداسید و پنجه کامپوزیت برای سایت یزد.",
|
||||
status: "READ" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 8),
|
||||
},
|
||||
{
|
||||
name: "مهندس علیرضا بهرامی",
|
||||
org: "شرکت نفت و گاز پارس",
|
||||
phone: "09124445566",
|
||||
message: "تقاضای ارسال کاتالوگ و نمونه عینک ایمنی استاندارد EN 166 و محافظ گوش ایرماف جهت آزمایش واحد HSE.",
|
||||
status: "NEW" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||
},
|
||||
{
|
||||
name: "مریم رضایی",
|
||||
org: "داروسازی رازک",
|
||||
phone: "09125556677",
|
||||
message: "درخواست مشاوره تخصصی در رابطه با ماسکهای فیلتردار ذرات شیمیایی و لباسهای یکبار مصرف آزمایشگاهی.",
|
||||
status: "READ" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 36),
|
||||
},
|
||||
{
|
||||
name: "مهندس مهدی یوسفی",
|
||||
org: "فولاد مبارکه اصفهان",
|
||||
phone: "09126667788",
|
||||
message: "درخواست برگزاری جلسه فنی برای تمدید قرارداد تأمین لباس کار نسوز، شیلد محافظ صورت و پابند نسوز.",
|
||||
status: "ARCHIVED" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 72),
|
||||
},
|
||||
{
|
||||
name: "پیمان نادری",
|
||||
org: "خطوط لوله و مخابرات نفت",
|
||||
phone: "09127778899",
|
||||
message: "استعلام موجودی تجهیزات نجات در فضاهای بسته، سهپایه نجات (Tripod) و وینچ همراه با گواهی کالیبراسیون.",
|
||||
status: "NEW" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 96),
|
||||
},
|
||||
{
|
||||
name: "دکتر مجید اسدی",
|
||||
org: "سیمان تهران",
|
||||
phone: "09128889900",
|
||||
message: "درخواست برگزاری دوره آموزشی کار در ارتفاع برای پرسنل پیمانکار و بازرسی دورهای خطوط حیات (Lifeline).",
|
||||
status: "READ" as const,
|
||||
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 120),
|
||||
},
|
||||
];
|
||||
|
||||
for (const msg of samples) {
|
||||
await prisma.contactMessage.create({ data: msg });
|
||||
}
|
||||
console.log(` seeded ${samples.length} sample contact messages`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (env.S3_ENSURE_BUCKET) await storage.ensureBucket();
|
||||
if (env.S3_ENSURE_BUCKET) {
|
||||
try {
|
||||
await storage.ensureBucket();
|
||||
} catch {
|
||||
console.log(" ensureBucket skipped");
|
||||
}
|
||||
}
|
||||
console.log("seeding sections…");
|
||||
await seedSections();
|
||||
console.log("seeding catalog…");
|
||||
await seedCatalog();
|
||||
console.log("seeding admin…");
|
||||
await seedAdmin();
|
||||
console.log("seeding messages…");
|
||||
await seedMessages();
|
||||
const [categories, products, media] = await Promise.all([
|
||||
prisma.category.count(),
|
||||
prisma.product.count(),
|
||||
|
||||
Reference in New Issue
Block a user