Add workshops CMS API with schedule, registrations, and public storefront routes.
Includes SQL migrations, Prisma models, dashboard CRUD, registration status workflow, and website OpenAPI updates for workshop listing and signup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
9d56ec7396
commit
c7d5204b49
@@ -18,6 +18,7 @@ import { BlogsModule } from './blogs/blogs.module';
|
||||
import { PortfoliosModule } from './portfolios/portfolios.module';
|
||||
import { VideosModule } from './videos/videos.module';
|
||||
import { InstructionsModule } from './instructions/instructions.module';
|
||||
import { WorkshopsModule } from './workshops/workshops.module';
|
||||
import { CommentsModule } from './comments/comments.module';
|
||||
import { CitiesModule } from './cities/cities.module';
|
||||
import { ExpertReviewsModule } from './expert-reviews/expert-reviews.module';
|
||||
@@ -63,6 +64,7 @@ import { PaymentsModule } from './payments/payments.module';
|
||||
PortfoliosModule,
|
||||
VideosModule,
|
||||
InstructionsModule,
|
||||
WorkshopsModule,
|
||||
CommentsModule,
|
||||
ExpertReviewsModule,
|
||||
BusinessSettingsModule,
|
||||
|
||||
@@ -117,7 +117,7 @@ export class BlogsService {
|
||||
}
|
||||
|
||||
const authorId = dto.authorId ? BigInt(dto.authorId) : actor.id;
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId, actor);
|
||||
|
||||
if (dto.categoryId) {
|
||||
await this.assertCategoryBelongsToBusiness(businessId, BigInt(dto.categoryId));
|
||||
@@ -202,7 +202,7 @@ export class BlogsService {
|
||||
authorId = null;
|
||||
} else {
|
||||
authorId = BigInt(dto.authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId, actor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,7 +679,15 @@ export class BlogsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async assertAuthorBelongsToBusiness(businessId: bigint, authorId: bigint) {
|
||||
private async assertAuthorBelongsToBusiness(
|
||||
businessId: bigint,
|
||||
authorId: bigint,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
if (await this.permissions.isSuperAdmin(actor.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await this.prisma.businessUser.findFirst({
|
||||
where: { businessId, userId: authorId },
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
normalizeDashboardThemeMode,
|
||||
normalizeEnabledBusinessModules,
|
||||
normalizeHomeCharts,
|
||||
toPrismaJson,
|
||||
} from '../business-settings/business-settings.util';
|
||||
import type {
|
||||
BusinessModuleId,
|
||||
@@ -38,8 +39,10 @@ import type {
|
||||
HomeChartId,
|
||||
} from '../business-settings/business-settings.types';
|
||||
import {
|
||||
DEFAULT_BUSINESS_SETTINGS,
|
||||
DEFAULT_ENABLED_BUSINESS_MODULES,
|
||||
DEFAULT_HOME_CHARTS,
|
||||
DEFAULT_NEW_BUSINESS_MODULES,
|
||||
} from '../business-settings/business-settings.types';
|
||||
import { ArvanDnsService } from '../arvan-dns/arvan-dns.service';
|
||||
import { WebsiteDeployAgentService } from '../website-deploy/website-deploy-agent.service';
|
||||
@@ -371,6 +374,13 @@ export class BusinessAdminService {
|
||||
slug,
|
||||
oldBusinessId:
|
||||
dto.oldBusinessId !== undefined ? BigInt(dto.oldBusinessId) : undefined,
|
||||
settings: toPrismaJson({
|
||||
...DEFAULT_BUSINESS_SETTINGS,
|
||||
modules: {
|
||||
enabled: [...DEFAULT_NEW_BUSINESS_MODULES],
|
||||
charts: [...DEFAULT_HOME_CHARTS],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ export const BUSINESS_DASHBOARD_MODULE_IDS = [
|
||||
'warehouse',
|
||||
'videos',
|
||||
'instructions',
|
||||
'workshops',
|
||||
'finance',
|
||||
] as const;
|
||||
|
||||
@@ -188,13 +189,19 @@ export const DEFAULT_ORDER_PROCESS_STEPS: OrderProcessStep[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** Modules enabled when a new business is created (super-admin). */
|
||||
export const DEFAULT_NEW_BUSINESS_MODULES: BusinessModuleId[] = [
|
||||
'products',
|
||||
'blog',
|
||||
'finance',
|
||||
];
|
||||
|
||||
/**
|
||||
* Existing tenants without `settings.modules` keep every business-dashboard module
|
||||
* enabled. Customer modules stay opt-in.
|
||||
* enabled except opt-in modules. Customer modules stay opt-in.
|
||||
*/
|
||||
export const DEFAULT_ENABLED_BUSINESS_MODULES: BusinessModuleId[] = [
|
||||
...BUSINESS_DASHBOARD_MODULE_IDS,
|
||||
];
|
||||
export const DEFAULT_ENABLED_BUSINESS_MODULES: BusinessModuleId[] =
|
||||
BUSINESS_DASHBOARD_MODULE_IDS.filter((id) => id !== 'workshops');
|
||||
|
||||
export const DEFAULT_HOME_CHARTS: [HomeChartId, HomeChartId] = [
|
||||
'orders_30d',
|
||||
|
||||
@@ -182,7 +182,7 @@ export class CustomersService {
|
||||
actor: AuthUser,
|
||||
) {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
await this.assertPermission(businessId, actor.id, 'orders.create');
|
||||
await this.assertCustomerSearchPermission(businessId, actor.id);
|
||||
|
||||
const q = query.q.trim();
|
||||
const limit = Math.min(Math.max(query.limit ?? 20, 1), 50);
|
||||
@@ -476,6 +476,29 @@ export class CustomersService {
|
||||
return user.email ? `${user.email} · ${user.cellNumber}` : user.cellNumber;
|
||||
}
|
||||
|
||||
private async assertCustomerSearchPermission(
|
||||
businessId: bigint,
|
||||
userId: bigint,
|
||||
) {
|
||||
const allowed =
|
||||
(await this.permissions.hasBusinessPermission(
|
||||
userId,
|
||||
businessId,
|
||||
'orders.create',
|
||||
)) ||
|
||||
(await this.permissions.hasBusinessPermission(
|
||||
userId,
|
||||
businessId,
|
||||
'workshops.update',
|
||||
));
|
||||
|
||||
if (!allowed) {
|
||||
throw new ForbiddenException(
|
||||
'Missing permission to search customers for this business',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertPermission(
|
||||
businessId: bigint,
|
||||
userId: bigint,
|
||||
|
||||
@@ -128,7 +128,7 @@ export class InstructionsService {
|
||||
}
|
||||
|
||||
const authorId = dto.authorId ? BigInt(dto.authorId) : actor.id;
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId, actor);
|
||||
|
||||
if (dto.categoryId) {
|
||||
await this.assertCategoryBelongsToBusiness(
|
||||
@@ -248,7 +248,7 @@ export class InstructionsService {
|
||||
authorId = null;
|
||||
} else {
|
||||
authorId = BigInt(dto.authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId, actor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,7 +785,12 @@ export class InstructionsService {
|
||||
private async assertAuthorBelongsToBusiness(
|
||||
businessId: bigint,
|
||||
authorId: bigint,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
if (await this.permissions.isSuperAdmin(actor.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await this.prisma.businessUser.findFirst({
|
||||
where: { businessId, userId: authorId },
|
||||
});
|
||||
|
||||
@@ -32,6 +32,12 @@ export class CreateStoreSpecialDto {
|
||||
@IsString({ each: true })
|
||||
storeItemIds?: string[];
|
||||
|
||||
/** Catalog products — resolved to store items (created if missing). */
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
productIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
@@ -60,6 +66,12 @@ export class UpdateStoreSpecialDto {
|
||||
@IsString({ each: true })
|
||||
storeItemIds?: string[];
|
||||
|
||||
/** Catalog products — resolved to store items (created if missing). */
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
productIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
sortOrder?: number;
|
||||
|
||||
@@ -151,10 +151,11 @@ export class StoreSpecialsService {
|
||||
const businessId = BigInt(businessIdRaw);
|
||||
await this.assertPermission(businessId, actor.id, 'products.update');
|
||||
|
||||
const storeItemIds = this.parseUniqueStoreItemIds(dto.storeItemIds ?? []);
|
||||
if (storeItemIds.length > 0) {
|
||||
await this.assertStoreItemsBelongToBusiness(businessId, storeItemIds);
|
||||
}
|
||||
const storeItemIds = await this.resolveLinkedStoreItemIds(
|
||||
businessId,
|
||||
dto.storeItemIds,
|
||||
dto.productIds,
|
||||
);
|
||||
|
||||
const key = normalizeSpecialGroupKey(dto.key);
|
||||
await this.assertKeyAvailable(businessId, key);
|
||||
@@ -201,9 +202,12 @@ export class StoreSpecialsService {
|
||||
await this.findSpecialOrThrow(businessId, specialId);
|
||||
|
||||
let storeItemIds: bigint[] | undefined;
|
||||
if (dto.storeItemIds !== undefined) {
|
||||
storeItemIds = this.parseUniqueStoreItemIds(dto.storeItemIds);
|
||||
await this.assertStoreItemsBelongToBusiness(businessId, storeItemIds);
|
||||
if (dto.storeItemIds !== undefined || dto.productIds !== undefined) {
|
||||
storeItemIds = await this.resolveLinkedStoreItemIds(
|
||||
businessId,
|
||||
dto.storeItemIds,
|
||||
dto.productIds,
|
||||
);
|
||||
}
|
||||
|
||||
const nextKey =
|
||||
@@ -277,6 +281,42 @@ export class StoreSpecialsService {
|
||||
return unique.map((id) => BigInt(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer explicit storeItemIds. Otherwise resolve productIds to store items
|
||||
* (creating a store_items row when the product is not yet in the store).
|
||||
*/
|
||||
private async resolveLinkedStoreItemIds(
|
||||
businessId: bigint,
|
||||
storeItemIdsRaw: string[] | undefined,
|
||||
productIdsRaw: string[] | undefined,
|
||||
) {
|
||||
if (storeItemIdsRaw !== undefined) {
|
||||
const storeItemIds = this.parseUniqueStoreItemIds(storeItemIdsRaw);
|
||||
if (storeItemIds.length > 0) {
|
||||
await this.assertStoreItemsBelongToBusiness(businessId, storeItemIds);
|
||||
}
|
||||
return storeItemIds;
|
||||
}
|
||||
|
||||
if (productIdsRaw === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const productIds = this.parseUniqueStoreItemIds(productIdsRaw);
|
||||
if (productIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await this.assertProductsBelongToBusiness(businessId, productIds);
|
||||
|
||||
const storeItemIds: bigint[] = [];
|
||||
for (const productId of productIds) {
|
||||
const storeItem = await this.ensureStoreItem(businessId, productId);
|
||||
storeItemIds.push(storeItem.id);
|
||||
}
|
||||
return storeItemIds;
|
||||
}
|
||||
|
||||
private async assertStoreItemsBelongToBusiness(
|
||||
businessId: bigint,
|
||||
storeItemIds: bigint[],
|
||||
@@ -291,6 +331,36 @@ export class StoreSpecialsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async assertProductsBelongToBusiness(
|
||||
businessId: bigint,
|
||||
productIds: bigint[],
|
||||
) {
|
||||
const found = await this.prisma.product.findMany({
|
||||
where: { businessId, id: { in: productIds } },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (found.length !== productIds.length) {
|
||||
throw new BadRequestException(
|
||||
'One or more products were not found for this business',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureStoreItem(businessId: bigint, productId: bigint) {
|
||||
const existing = await this.prisma.storeItem.findUnique({
|
||||
where: { businessId_productId: { businessId, productId } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return this.prisma.storeItem.create({
|
||||
data: { businessId, productId },
|
||||
});
|
||||
}
|
||||
|
||||
private async replaceItems(
|
||||
tx: Prisma.TransactionClient,
|
||||
specialId: bigint,
|
||||
|
||||
@@ -128,7 +128,7 @@ export class VideosService {
|
||||
}
|
||||
|
||||
const authorId = dto.authorId ? BigInt(dto.authorId) : actor.id;
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId, actor);
|
||||
|
||||
if (dto.categoryId) {
|
||||
await this.assertCategoryBelongsToBusiness(
|
||||
@@ -235,7 +235,7 @@ export class VideosService {
|
||||
authorId = null;
|
||||
} else {
|
||||
authorId = BigInt(dto.authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId);
|
||||
await this.assertAuthorBelongsToBusiness(businessId, authorId, actor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,7 +767,12 @@ export class VideosService {
|
||||
private async assertAuthorBelongsToBusiness(
|
||||
businessId: bigint,
|
||||
authorId: bigint,
|
||||
actor: AuthUser,
|
||||
) {
|
||||
if (await this.permissions.isSuperAdmin(actor.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await this.prisma.businessUser.findFirst({
|
||||
where: { businessId, userId: authorId },
|
||||
});
|
||||
|
||||
@@ -2763,6 +2763,62 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/workshops": {
|
||||
"get": {
|
||||
"tags": ["Workshops"],
|
||||
"summary": "List published workshops",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/domain" },
|
||||
{ "name": "page", "in": "query", "schema": { "type": "integer" } },
|
||||
{ "name": "pageSize", "in": "query", "schema": { "type": "integer", "default": 12 } },
|
||||
{ "name": "categoryId", "in": "query", "schema": { "type": "string" } },
|
||||
{ "name": "title", "in": "query", "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "{ items, total, page, pageSize }" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/workshops/{slug}": {
|
||||
"get": {
|
||||
"tags": ["Workshops"],
|
||||
"summary": "Workshop by slug",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/domain" },
|
||||
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "{ workshop } — includes eventDate, eventTime, duration" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tenants/{domain}/workshops/{workshopId}/register": {
|
||||
"post": {
|
||||
"tags": ["Workshops"],
|
||||
"summary": "Register logged-in website customer for a workshop",
|
||||
"security": [{ "bearerAuth": [] }],
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/domain" },
|
||||
{ "name": "workshopId", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": {
|
||||
"201": { "description": "{ registration: { id, status: pending|approved|rejected, ... }, message }" },
|
||||
"409": { "description": "Already registered" }
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Workshops"],
|
||||
"summary": "Cancel workshop registration",
|
||||
"security": [{ "bearerAuth": [] }],
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/domain" },
|
||||
{ "name": "workshopId", "in": "path", "required": true, "schema": { "type": "string" } }
|
||||
],
|
||||
"responses": {
|
||||
"200": { "description": "{ message }" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/businesses/{businessId}/payments/methods": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { ContentStatus } from '@prisma/client';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
Min,
|
||||
MinLength,
|
||||
Max,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export enum WorkshopRegistrationStatusDto {
|
||||
pending = 'pending',
|
||||
approved = 'approved',
|
||||
rejected = 'rejected',
|
||||
}
|
||||
|
||||
export class ListWorkshopsDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ContentStatus)
|
||||
status?: ContentStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class ListPublicWorkshopsDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export class CreateWorkshopDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
titleFa!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
titleEn?: string;
|
||||
|
||||
/** @deprecated use titleFa */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
abstract?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
descriptionHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ContentStatus)
|
||||
status?: ContentStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
featuredMediaId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
galleryMediaIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
tags?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
authorId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
||||
slug?: string;
|
||||
|
||||
/** YYYY-MM-DD */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
eventDate?: string | null;
|
||||
|
||||
/** HH:mm (24h) */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^([01]\d|2[0-3]):[0-5]\d$/)
|
||||
eventTime?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
duration?: string | null;
|
||||
}
|
||||
|
||||
export class UpdateWorkshopDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
titleFa?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
titleEn?: string | null;
|
||||
|
||||
/** @deprecated use titleFa */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
abstract?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
descriptionHtml?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categoryId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ContentStatus)
|
||||
status?: ContentStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
featuredMediaId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
galleryMediaIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
tags?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
authorId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
|
||||
slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
eventDate?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^([01]\d|2[0-3]):[0-5]\d$/)
|
||||
eventTime?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(100)
|
||||
duration?: string | null;
|
||||
}
|
||||
|
||||
export class AddWorkshopRegistrationDto {
|
||||
@IsString()
|
||||
userId!: string;
|
||||
}
|
||||
|
||||
export class UpdateWorkshopRegistrationDto {
|
||||
@IsEnum(WorkshopRegistrationStatusDto)
|
||||
status!: WorkshopRegistrationStatusDto;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { RequireBusinessPermission } from '../auth/decorators/require-business-permission.decorator';
|
||||
import { BusinessPermissionGuard } from '../auth/guards/business-permission.guard';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { WorkshopsService } from './workshops.service';
|
||||
import {
|
||||
CreateWorkshopDto,
|
||||
AddWorkshopRegistrationDto,
|
||||
UpdateWorkshopRegistrationDto,
|
||||
ListPublicWorkshopsDto,
|
||||
ListWorkshopsDto,
|
||||
UpdateWorkshopDto,
|
||||
} from './dto/workshop.dto';
|
||||
|
||||
@Controller('businesses/:businessId/workshops')
|
||||
@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
|
||||
export class WorkshopsController {
|
||||
constructor(private readonly service: WorkshopsService) {}
|
||||
|
||||
@Get()
|
||||
@RequireBusinessPermission('workshops.read')
|
||||
list(
|
||||
@Param('businessId') businessId: string,
|
||||
@Query() query: ListWorkshopsDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.list(businessId, query, user);
|
||||
}
|
||||
|
||||
@Get(':workshopId/registrations')
|
||||
@RequireBusinessPermission('workshops.read')
|
||||
listRegistrations(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.listRegistrations(businessId, workshopId, user);
|
||||
}
|
||||
|
||||
@Post(':workshopId/registrations')
|
||||
@RequireBusinessPermission('workshops.update')
|
||||
addRegistration(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@Body() dto: AddWorkshopRegistrationDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.addRegistration(businessId, workshopId, dto, user);
|
||||
}
|
||||
|
||||
@Patch(':workshopId/registrations/:registrationId')
|
||||
@RequireBusinessPermission('workshops.update')
|
||||
updateRegistration(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@Param('registrationId') registrationId: string,
|
||||
@Body() dto: UpdateWorkshopRegistrationDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.updateRegistration(
|
||||
businessId,
|
||||
workshopId,
|
||||
registrationId,
|
||||
dto,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':workshopId/registrations/:registrationId')
|
||||
@RequireBusinessPermission('workshops.update')
|
||||
removeRegistration(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@Param('registrationId') registrationId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.removeRegistration(
|
||||
businessId,
|
||||
workshopId,
|
||||
registrationId,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':workshopId')
|
||||
@RequireBusinessPermission('workshops.read')
|
||||
getOne(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.getOne(businessId, workshopId, user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequireBusinessPermission('workshops.create')
|
||||
create(
|
||||
@Param('businessId') businessId: string,
|
||||
@Body() dto: CreateWorkshopDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.create(businessId, dto, user);
|
||||
}
|
||||
|
||||
@Patch(':workshopId')
|
||||
@RequireBusinessPermission('workshops.update')
|
||||
update(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@Body() dto: UpdateWorkshopDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.update(businessId, workshopId, dto, user);
|
||||
}
|
||||
|
||||
@Delete(':workshopId')
|
||||
@RequireBusinessPermission('workshops.delete')
|
||||
remove(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.remove(businessId, workshopId, user);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('tenants/:host/workshops')
|
||||
export class PublicWorkshopsController {
|
||||
constructor(private readonly service: WorkshopsService) {}
|
||||
|
||||
@Get()
|
||||
list(@Param('host') host: string, @Query() query: ListPublicWorkshopsDto) {
|
||||
return this.service.listPublic(host, query);
|
||||
}
|
||||
|
||||
@Post(':workshopId/register')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
register(
|
||||
@Param('host') host: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.registerPublic(host, workshopId, user);
|
||||
}
|
||||
|
||||
@Delete(':workshopId/register')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
unregister(
|
||||
@Param('host') host: string,
|
||||
@Param('workshopId') workshopId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.unregisterPublic(host, workshopId, user);
|
||||
}
|
||||
|
||||
@Get(':slug')
|
||||
getBySlug(@Param('host') host: string, @Param('slug') slug: string) {
|
||||
return this.service.getPublicBySlug(host, slug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
import {
|
||||
PublicWorkshopsController,
|
||||
WorkshopsController,
|
||||
} from './workshops.controller';
|
||||
import { WorkshopsService } from './workshops.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, TenantModule],
|
||||
controllers: [WorkshopsController, PublicWorkshopsController],
|
||||
providers: [WorkshopsService],
|
||||
})
|
||||
export class WorkshopsModule {}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user