From af0573c089fb742ae1667807ff6709392cfb1612 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sun, 23 Aug 2026 15:01:52 +0330 Subject: [PATCH] Show newest portfolios first on dashboard and website lists. Order by sort_order descending and assign new items the next highest sort value so create and move-up stay consistent. Co-authored-by: Cursor --- docs/website-api/AI_PROMPT.md | 2 +- src/portfolios/portfolios.service.ts | 32 ++++++++++++++++++---------- src/website-docs/static/AI_PROMPT.md | 2 +- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/website-api/AI_PROMPT.md b/docs/website-api/AI_PROMPT.md index c4fa320..e0bb8c5 100644 --- a/docs/website-api/AI_PROMPT.md +++ b/docs/website-api/AI_PROMPT.md @@ -31,7 +31,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M ### Typical bootstrap sequence 1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`) 2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`) -3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`). +3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`). 4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens). 5. Cart checkout with `addressId` or inline `shippingAddress` + `payment` - For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl` diff --git a/src/portfolios/portfolios.service.ts b/src/portfolios/portfolios.service.ts index 0afee13..f9273e4 100644 --- a/src/portfolios/portfolios.service.ts +++ b/src/portfolios/portfolios.service.ts @@ -41,6 +41,13 @@ const portfolioInclude = { media: true, } satisfies Prisma.portfoliosInclude; +/** Higher `sort_order` first (newest / moved-up), then newest dates. */ +const portfolioListOrderBy = [ + { sort_order: 'desc' }, + { published_at: 'desc' }, + { created_at: 'desc' }, +] as const; + @Injectable() export class PortfoliosService { constructor( @@ -64,11 +71,7 @@ export class PortfoliosService { const [items, total] = await Promise.all([ this.prisma.portfolios.findMany({ where, - orderBy: [ - { sort_order: 'asc' }, - { published_at: 'desc' }, - { created_at: 'desc' }, - ], + orderBy: [...portfolioListOrderBy], skip, take: pageSize, include: portfolioInclude, @@ -143,6 +146,9 @@ export class PortfoliosService { ); } + const sortOrder = + dto.sortOrder ?? (await this.nextSortOrder(businessId)); + const created = await this.prisma.$transaction(async (tx) => { const portfolio = await tx.portfolios.create({ data: { @@ -155,7 +161,7 @@ export class PortfoliosService { content: this.buildContent(dto.mainTextHtml) as Prisma.InputJsonValue, status, featured_media_id: featuredMediaId, - sort_order: dto.sortOrder ?? 0, + sort_order: sortOrder, published_at: status === ContentStatus.published ? new Date() : null, metadata: this.buildMetadata(dto.tags) as Prisma.InputJsonValue, }, @@ -419,11 +425,7 @@ export class PortfoliosService { const [items, total] = await Promise.all([ this.prisma.portfolios.findMany({ where, - orderBy: [ - { sort_order: 'asc' }, - { published_at: 'desc' }, - { created_at: 'desc' }, - ], + orderBy: [...portfolioListOrderBy], skip, take: pageSize, include: portfolioInclude, @@ -827,6 +829,14 @@ export class PortfoliosService { }; } + private async nextSortOrder(businessId: bigint): Promise { + const result = await this.prisma.portfolios.aggregate({ + where: { business_id: businessId }, + _max: { sort_order: true }, + }); + return (result._max.sort_order ?? -1) + 1; + } + private asRecord(value: Prisma.JsonValue): Record { if (value && typeof value === 'object' && !Array.isArray(value)) { return value as Record; diff --git a/src/website-docs/static/AI_PROMPT.md b/src/website-docs/static/AI_PROMPT.md index c4fa320..e0bb8c5 100644 --- a/src/website-docs/static/AI_PROMPT.md +++ b/src/website-docs/static/AI_PROMPT.md @@ -31,7 +31,7 @@ You are building a **Meshkee business website (storefront)**. You must use the M ### Typical bootstrap sequence 1. `GET /tenants/{domain}` → branding + `businessId` + `specialProductsSource` (`product` or `store_item`) 2. Homepage: business-info, **static-images**, sliders, category-groups, brand-groups, store-specials (`source` repeats the tenant setting; items are store listings when `source` is `store_item`) -3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`). +3. Catalog: categories, products (`GET /products/{slug}` includes `relatedProducts`: same category then same brand, in-stock first), store-items (`name` instant search: in-stock first, then `updatedAt`), **user-products** (customer stock listings). Portfolios list newest first (`sortOrder` desc, then `publishedAt` / `createdAt` desc). List filters: products/blogs/portfolios/videos accept `?tag=` (exact match on `metadata.tags`). 4. Auth: register/login → store tokens. Optional: `POST /auth/send-otp` then `POST /auth/login-otp` (passwordless) or `POST /auth/reset-password` (forgot password). `POST /auth/verify-otp` only marks the cell verified (no tokens). 5. Cart checkout with `addressId` or inline `shippingAddress` + `payment` - For online pay: `payment.type = "e_payment_gate"`, `gatewayType` (e.g. `"mellat"` or `"zarinpal"`), and absolute `returnUrl`