Commit 7730f929 by Augusto

documentation, player-features and profile

parent 1909772c
CREATE TABLE "profile_descriptions" (
"id" serial PRIMARY KEY NOT NULL,
"player_id" integer,
"coach_id" integer,
"description" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now(),
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "profile_links" (
"id" serial PRIMARY KEY NOT NULL,
"player_id" integer,
"coach_id" integer,
"title" varchar(255) NOT NULL,
"url" text NOT NULL,
"order" integer DEFAULT 0,
"is_active" boolean DEFAULT true,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now(),
"deleted_at" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "profile_descriptions" ADD CONSTRAINT "profile_descriptions_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "profile_descriptions" ADD CONSTRAINT "profile_descriptions_coach_id_coaches_id_fk" FOREIGN KEY ("coach_id") REFERENCES "public"."coaches"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "profile_links" ADD CONSTRAINT "profile_links_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "profile_links" ADD CONSTRAINT "profile_links_coach_id_coaches_id_fk" FOREIGN KEY ("coach_id") REFERENCES "public"."coaches"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "profile_descriptions_player_id_idx" ON "profile_descriptions" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "profile_descriptions_coach_id_idx" ON "profile_descriptions" USING btree ("coach_id");--> statement-breakpoint
CREATE INDEX "profile_descriptions_deleted_at_idx" ON "profile_descriptions" USING btree ("deleted_at");--> statement-breakpoint
CREATE INDEX "profile_links_player_id_idx" ON "profile_links" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "profile_links_coach_id_idx" ON "profile_links" USING btree ("coach_id");--> statement-breakpoint
CREATE INDEX "profile_links_is_active_idx" ON "profile_links" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "profile_links_deleted_at_idx" ON "profile_links" USING btree ("deleted_at");
-- Create player_feature_selections table
-- This table tracks which features a user has selected for a player,
-- independent of ratings. Allows users to select features without rating them.
CREATE TABLE IF NOT EXISTS "player_feature_selections" (
"id" serial PRIMARY KEY NOT NULL,
"player_id" integer NOT NULL,
"feature_type_id" integer NOT NULL,
"user_id" integer NOT NULL,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);
-- Create indexes for player_feature_selections
CREATE INDEX "player_feature_selections_player_id_idx" ON "player_feature_selections" ("player_id");
CREATE INDEX "player_feature_selections_feature_type_id_idx" ON "player_feature_selections" ("feature_type_id");
CREATE INDEX "player_feature_selections_user_id_idx" ON "player_feature_selections" ("user_id");
CREATE INDEX "player_feature_selections_deleted_at_idx" ON "player_feature_selections" ("deleted_at");
-- Unique constraint: one selection per scout per feature per player
CREATE UNIQUE INDEX "player_feature_selections_player_feature_user_idx" ON "player_feature_selections" ("player_id","feature_type_id","user_id");
-- Composite indexes for common queries
CREATE INDEX "player_feature_selections_player_feature_idx" ON "player_feature_selections" ("player_id","feature_type_id");
CREATE INDEX "player_feature_selections_player_user_idx" ON "player_feature_selections" ("player_id","user_id");
-- Add foreign key constraints
ALTER TABLE "player_feature_selections" ADD CONSTRAINT "player_feature_selections_player_id_fk" FOREIGN KEY ("player_id") REFERENCES "players"("id") ON DELETE cascade;
ALTER TABLE "player_feature_selections" ADD CONSTRAINT "player_feature_selections_feature_type_id_fk" FOREIGN KEY ("feature_type_id") REFERENCES "player_feature_types"("id") ON DELETE cascade;
ALTER TABLE "player_feature_selections" ADD CONSTRAINT "player_feature_selections_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade;
...@@ -21,6 +21,7 @@ import { PositionsModule } from './modules/positions/positions.module'; ...@@ -21,6 +21,7 @@ import { PositionsModule } from './modules/positions/positions.module';
import { AuditLogsModule } from './modules/audit-logs/audit-logs.module'; import { AuditLogsModule } from './modules/audit-logs/audit-logs.module';
import { AgentsModule } from './modules/agents/agents.module'; import { AgentsModule } from './modules/agents/agents.module';
import { PlayerFeaturesModule } from './modules/player-features/player-features.module'; import { PlayerFeaturesModule } from './modules/player-features/player-features.module';
import { ProfilesModule } from './modules/profiles/profiles.module';
@Module({ @Module({
imports: [ imports: [
...@@ -57,6 +58,7 @@ import { PlayerFeaturesModule } from './modules/player-features/player-features. ...@@ -57,6 +58,7 @@ import { PlayerFeaturesModule } from './modules/player-features/player-features.
AuditLogsModule, AuditLogsModule,
AgentsModule, AgentsModule,
PlayerFeaturesModule, PlayerFeaturesModule,
ProfilesModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
......
...@@ -851,7 +851,7 @@ export const clientModules = pgTable( ...@@ -851,7 +851,7 @@ export const clientModules = pgTable(
); );
// ============================================================================ // ============================================================================
// PLAYER FEATURES (Características del Jugador) // PLAYER FEATURES
// ============================================================================ // ============================================================================
export const playerFeatureCategories = pgTable( export const playerFeatureCategories = pgTable(
...@@ -957,6 +957,105 @@ export const playerFeatureRatings = pgTable( ...@@ -957,6 +957,105 @@ export const playerFeatureRatings = pgTable(
}), }),
); );
export const playerFeatureSelections = pgTable(
'player_feature_selections',
{
id: serial('id').primaryKey(),
playerId: integer('player_id')
.notNull()
.references(() => players.id, { onDelete: 'cascade' }),
featureTypeId: integer('feature_type_id')
.notNull()
.references(() => playerFeatureTypes.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
notes: text('notes'), // Optional scout notes
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
playerIdIdx: index('player_feature_selections_player_id_idx').on(
table.playerId,
),
featureTypeIdIdx: index('player_feature_selections_feature_type_id_idx').on(
table.featureTypeId,
),
userIdIdx: index('player_feature_selections_user_id_idx').on(table.userId),
deletedAtIdx: index('player_feature_selections_deleted_at_idx').on(
table.deletedAt,
),
// Unique constraint: one selection per scout per feature per player
playerFeatureUserIdx: uniqueIndex(
'player_feature_selections_player_feature_user_idx',
).on(table.playerId, table.featureTypeId, table.userId),
// Composite indexes for common queries
playerFeatureIdx: index('player_feature_selections_player_feature_idx').on(
table.playerId,
table.featureTypeId,
),
playerUserIdx: index('player_feature_selections_player_user_idx').on(
table.playerId,
table.userId,
),
}),
);
// ============================================================================
// PROFILE DESCRIPTIONS & LINKS
// ============================================================================
export const profileDescriptions = pgTable(
'profile_descriptions',
{
id: serial('id').primaryKey(),
playerId: integer('player_id').references(() => players.id, {
onDelete: 'cascade',
}),
coachId: integer('coach_id').references(() => coaches.id, {
onDelete: 'cascade',
}),
description: text('description').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
playerIdIdx: index('profile_descriptions_player_id_idx').on(table.playerId),
coachIdIdx: index('profile_descriptions_coach_id_idx').on(table.coachId),
deletedAtIdx: index('profile_descriptions_deleted_at_idx').on(
table.deletedAt,
),
}),
);
export const profileLinks = pgTable(
'profile_links',
{
id: serial('id').primaryKey(),
playerId: integer('player_id').references(() => players.id, {
onDelete: 'cascade',
}),
coachId: integer('coach_id').references(() => coaches.id, {
onDelete: 'cascade',
}),
title: varchar('title', { length: 255 }).notNull(), // e.g., "YouTube", "Instagram", "Portfolio"
url: text('url').notNull(),
order: integer('order').default(0),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
},
(table) => ({
playerIdIdx: index('profile_links_player_id_idx').on(table.playerId),
coachIdIdx: index('profile_links_coach_id_idx').on(table.coachId),
isActiveIdx: index('profile_links_is_active_idx').on(table.isActive),
deletedAtIdx: index('profile_links_deleted_at_idx').on(table.deletedAt),
}),
);
// ============================================================================ // ============================================================================
// AUDIT LOGS // AUDIT LOGS
// ============================================================================ // ============================================================================
...@@ -1088,6 +1187,20 @@ export const insertPlayerFeatureRatingSchema = ...@@ -1088,6 +1187,20 @@ export const insertPlayerFeatureRatingSchema =
createInsertSchema(playerFeatureRatings); createInsertSchema(playerFeatureRatings);
export const selectPlayerFeatureRatingSchema = export const selectPlayerFeatureRatingSchema =
createSelectSchema(playerFeatureRatings); createSelectSchema(playerFeatureRatings);
export const insertPlayerFeatureSelectionSchema = createInsertSchema(
playerFeatureSelections,
);
export const selectPlayerFeatureSelectionSchema = createSelectSchema(
playerFeatureSelections,
);
// Profile Descriptions & Links
export const insertProfileDescriptionSchema =
createInsertSchema(profileDescriptions);
export const selectProfileDescriptionSchema =
createSelectSchema(profileDescriptions);
export const insertProfileLinkSchema = createInsertSchema(profileLinks);
export const selectProfileLinkSchema = createSelectSchema(profileLinks);
// Audit Logs // Audit Logs
export const insertAuditLogSchema = createInsertSchema(auditLogs); export const insertAuditLogSchema = createInsertSchema(auditLogs);
...@@ -1170,6 +1283,16 @@ export type PlayerFeatureType = typeof playerFeatureTypes.$inferSelect; ...@@ -1170,6 +1283,16 @@ export type PlayerFeatureType = typeof playerFeatureTypes.$inferSelect;
export type NewPlayerFeatureType = typeof playerFeatureTypes.$inferInsert; export type NewPlayerFeatureType = typeof playerFeatureTypes.$inferInsert;
export type PlayerFeatureRating = typeof playerFeatureRatings.$inferSelect; export type PlayerFeatureRating = typeof playerFeatureRatings.$inferSelect;
export type NewPlayerFeatureRating = typeof playerFeatureRatings.$inferInsert; export type NewPlayerFeatureRating = typeof playerFeatureRatings.$inferInsert;
export type PlayerFeatureSelection =
typeof playerFeatureSelections.$inferSelect;
export type NewPlayerFeatureSelection =
typeof playerFeatureSelections.$inferInsert;
// Profile Descriptions & Links
export type ProfileDescription = typeof profileDescriptions.$inferSelect;
export type NewProfileDescription = typeof profileDescriptions.$inferInsert;
export type ProfileLink = typeof profileLinks.$inferSelect;
export type NewProfileLink = typeof profileLinks.$inferInsert;
// Audit Logs // Audit Logs
export type AuditLog = typeof auditLogs.$inferSelect; export type AuditLog = typeof auditLogs.$inferSelect;
......
...@@ -29,18 +29,80 @@ export class AgentsController { ...@@ -29,18 +29,80 @@ export class AgentsController {
constructor(private readonly agentsService: AgentsService) {} constructor(private readonly agentsService: AgentsService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create agent' }) @ApiOperation({
@ApiBody({ type: CreateAgentDto }) summary: 'Create agent',
@ApiOkResponse({ description: 'Created agent' }) description: 'Creates a new agent in the system',
})
@ApiBody({
type: CreateAgentDto,
examples: {
example1: {
summary: 'Create new agent',
value: {
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@agents.com',
phone: '+1234567890',
company: 'Elite Sports Management',
},
},
},
})
@ApiOkResponse({
description: 'Agent created successfully',
schema: {
example: {
id: 1,
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@agents.com',
phone: '+1234567890',
company: 'Elite Sports Management',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async create(@Body() body: CreateAgentDto): Promise<Agent> { async create(@Body() body: CreateAgentDto): Promise<Agent> {
return this.agentsService.create(body as any); return this.agentsService.create(body as any);
} }
@Patch(':id') @Patch(':id')
@ApiOperation({ summary: 'Update agent by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Update agent by ID',
@ApiBody({ type: UpdateAgentDto }) description:
@ApiOkResponse({ description: 'Updated agent' }) 'Updates an existing agent. Only provided fields will be updated.',
})
@ApiParam({ name: 'id', type: Number, description: 'Agent ID' })
@ApiBody({
type: UpdateAgentDto,
examples: {
example1: {
summary: 'Update agent contact info',
value: {
email: 'john.smith.new@agents.com',
phone: '+1987654321',
},
},
},
})
@ApiOkResponse({
description: 'Agent updated successfully',
schema: {
example: {
id: 1,
firstName: 'John',
lastName: 'Smith',
email: 'john.smith.new@agents.com',
phone: '+1987654321',
company: 'Elite Sports Management',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T11:00:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Agent not found' }) @ApiNotFoundResponse({ description: 'Agent not found' })
async update( async update(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
...@@ -50,23 +112,65 @@ export class AgentsController { ...@@ -50,23 +112,65 @@ export class AgentsController {
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get agent by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Get agent by ID',
@ApiOkResponse({ description: 'Agent if found' }) description: 'Retrieves a specific agent by their ID',
})
@ApiParam({ name: 'id', type: Number, description: 'Agent ID' })
@ApiOkResponse({
description: 'Agent retrieved successfully',
schema: {
example: {
id: 1,
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@agents.com',
phone: '+1234567890',
company: 'Elite Sports Management',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Agent not found' })
async getById(@Param('id', ParseIntPipe) id: number): Promise<Agent | null> { async getById(@Param('id', ParseIntPipe) id: number): Promise<Agent | null> {
return this.agentsService.findById(id); return this.agentsService.findById(id);
} }
@Get() @Get()
@ApiOperation({ summary: 'List all agents' }) @ApiOperation({
@ApiOkResponse({ description: 'List of agents' }) summary: 'List all agents',
description: 'Retrieves all agents in the system',
})
@ApiOkResponse({
description: 'List of all agents',
schema: {
example: [
{
id: 1,
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@agents.com',
phone: '+1234567890',
company: 'Elite Sports Management',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async list(): Promise<Agent[]> { async list(): Promise<Agent[]> {
return this.agentsService.findAll(); return this.agentsService.findAll();
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'Delete agent by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Delete agent by ID',
description: 'Soft deletes an agent (marks as deleted)',
})
@ApiParam({ name: 'id', type: Number, description: 'Agent ID' })
@ApiNoContentResponse({ description: 'Agent deleted successfully' }) @ApiNoContentResponse({ description: 'Agent deleted successfully' })
@ApiNotFoundResponse({ description: 'Agent not found' }) @ApiNotFoundResponse({ description: 'Agent not found' })
async delete(@Param('id', ParseIntPipe) id: number): Promise<void> { async delete(@Param('id', ParseIntPipe) id: number): Promise<void> {
...@@ -77,10 +181,23 @@ export class AgentsController { ...@@ -77,10 +181,23 @@ export class AgentsController {
} }
@Post(':agentId/players/:playerId') @Post(':agentId/players/:playerId')
@ApiOperation({ summary: 'Associate agent with player' }) @ApiOperation({
@ApiParam({ name: 'agentId', type: Number }) summary: 'Associate agent with player',
@ApiParam({ name: 'playerId', type: Number }) description: 'Creates an association between an agent and a player',
@ApiOkResponse({ description: 'Association created or already exists' }) })
@ApiParam({ name: 'agentId', type: Number, description: 'Agent ID' })
@ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiOkResponse({
description: 'Association created or already exists',
schema: {
example: {
id: 1,
agentId: 1,
playerId: 5,
createdAt: '2025-01-15T10:30:00Z',
},
},
})
async associateWithPlayer( async associateWithPlayer(
@Param('agentId', ParseIntPipe) agentId: number, @Param('agentId', ParseIntPipe) agentId: number,
@Param('playerId', ParseIntPipe) playerId: number, @Param('playerId', ParseIntPipe) playerId: number,
...@@ -89,10 +206,23 @@ export class AgentsController { ...@@ -89,10 +206,23 @@ export class AgentsController {
} }
@Post(':agentId/coaches/:coachId') @Post(':agentId/coaches/:coachId')
@ApiOperation({ summary: 'Associate agent with coach' }) @ApiOperation({
@ApiParam({ name: 'agentId', type: Number }) summary: 'Associate agent with coach',
@ApiParam({ name: 'coachId', type: Number }) description: 'Creates an association between an agent and a coach',
@ApiOkResponse({ description: 'Association created or already exists' }) })
@ApiParam({ name: 'agentId', type: Number, description: 'Agent ID' })
@ApiParam({ name: 'coachId', type: Number, description: 'Coach ID' })
@ApiOkResponse({
description: 'Association created or already exists',
schema: {
example: {
id: 1,
agentId: 1,
coachId: 3,
createdAt: '2025-01-15T10:30:00Z',
},
},
})
async associateWithCoach( async associateWithCoach(
@Param('agentId', ParseIntPipe) agentId: number, @Param('agentId', ParseIntPipe) agentId: number,
@Param('coachId', ParseIntPipe) coachId: number, @Param('coachId', ParseIntPipe) coachId: number,
...@@ -101,9 +231,12 @@ export class AgentsController { ...@@ -101,9 +231,12 @@ export class AgentsController {
} }
@Delete(':agentId/players/:playerId') @Delete(':agentId/players/:playerId')
@ApiOperation({ summary: 'Remove agent from player' }) @ApiOperation({
@ApiParam({ name: 'agentId', type: Number }) summary: 'Remove agent from player',
@ApiParam({ name: 'playerId', type: Number }) description: 'Removes the association between an agent and a player',
})
@ApiParam({ name: 'agentId', type: Number, description: 'Agent ID' })
@ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiNoContentResponse({ description: 'Association removed if it existed' }) @ApiNoContentResponse({ description: 'Association removed if it existed' })
async removeFromPlayer( async removeFromPlayer(
@Param('agentId', ParseIntPipe) agentId: number, @Param('agentId', ParseIntPipe) agentId: number,
...@@ -113,9 +246,12 @@ export class AgentsController { ...@@ -113,9 +246,12 @@ export class AgentsController {
} }
@Delete(':agentId/coaches/:coachId') @Delete(':agentId/coaches/:coachId')
@ApiOperation({ summary: 'Remove agent from coach' }) @ApiOperation({
@ApiParam({ name: 'agentId', type: Number }) summary: 'Remove agent from coach',
@ApiParam({ name: 'coachId', type: Number }) description: 'Removes the association between an agent and a coach',
})
@ApiParam({ name: 'agentId', type: Number, description: 'Agent ID' })
@ApiParam({ name: 'coachId', type: Number, description: 'Coach ID' })
@ApiNoContentResponse({ description: 'Association removed if it existed' }) @ApiNoContentResponse({ description: 'Association removed if it existed' })
async removeFromCoach( async removeFromCoach(
@Param('agentId', ParseIntPipe) agentId: number, @Param('agentId', ParseIntPipe) agentId: number,
...@@ -125,9 +261,29 @@ export class AgentsController { ...@@ -125,9 +261,29 @@ export class AgentsController {
} }
@Get('by-player/:playerId') @Get('by-player/:playerId')
@ApiOperation({ summary: 'Get agents associated with a player' }) @ApiOperation({
@ApiParam({ name: 'playerId', type: Number }) summary: 'Get agents associated with a player',
@ApiOkResponse({ description: 'List of agents for the player' }) description: 'Retrieves all agents representing a specific player',
})
@ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiOkResponse({
description: 'List of agents for the player',
schema: {
example: [
{
id: 1,
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@agents.com',
phone: '+1234567890',
company: 'Elite Sports Management',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getByPlayer( async getByPlayer(
@Param('playerId', ParseIntPipe) playerId: number, @Param('playerId', ParseIntPipe) playerId: number,
): Promise<Agent[]> { ): Promise<Agent[]> {
...@@ -135,9 +291,29 @@ export class AgentsController { ...@@ -135,9 +291,29 @@ export class AgentsController {
} }
@Get('by-coach/:coachId') @Get('by-coach/:coachId')
@ApiOperation({ summary: 'Get agents associated with a coach' }) @ApiOperation({
@ApiParam({ name: 'coachId', type: Number }) summary: 'Get agents associated with a coach',
@ApiOkResponse({ description: 'List of agents for the coach' }) description: 'Retrieves all agents representing a specific coach',
})
@ApiParam({ name: 'coachId', type: Number, description: 'Coach ID' })
@ApiOkResponse({
description: 'List of agents for the coach',
schema: {
example: [
{
id: 1,
firstName: 'John',
lastName: 'Smith',
email: 'john.smith@agents.com',
phone: '+1234567890',
company: 'Elite Sports Management',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getByCoach( async getByCoach(
@Param('coachId', ParseIntPipe) coachId: number, @Param('coachId', ParseIntPipe) coachId: number,
): Promise<Agent[]> { ): Promise<Agent[]> {
......
...@@ -25,19 +25,65 @@ export class AreasController { ...@@ -25,19 +25,65 @@ export class AreasController {
constructor(private readonly areasService: AreasService) {} constructor(private readonly areasService: AreasService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create or update area by wyId' }) @ApiOperation({
summary: 'Create or update area by wyId',
description:
'Creates a new area or updates an existing one identified by Wyscout ID',
})
@ApiBody({ @ApiBody({
description: 'Area payload', description: 'Area payload',
type: CreateAreaDto, type: CreateAreaDto,
examples: {
example1: {
summary: 'Create area',
value: {
wyId: 1,
name: 'Spain',
alpha2code: 'ES',
alpha3code: 'ESP',
},
},
},
})
@ApiOkResponse({
description: 'Area created or updated successfully',
schema: {
example: {
id: 1,
wyId: 1,
name: 'Spain',
alpha2code: 'ES',
alpha3code: 'ESP',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
}) })
@ApiOkResponse({ description: 'Upserted area' })
async save(@Body() body: CreateAreaDto): Promise<Area> { async save(@Body() body: CreateAreaDto): Promise<Area> {
return this.areasService.upsertByWyId(body as any); return this.areasService.upsertByWyId(body as any);
} }
@Get(':wyId') @Get(':wyId')
@ApiOperation({ summary: 'Get area by wyId' }) @ApiOperation({
@ApiOkResponse({ description: 'Area if found' }) summary: 'Get area by wyId',
description: 'Retrieves a specific area by its Wyscout ID',
})
@ApiOkResponse({
description: 'Area retrieved successfully',
schema: {
example: {
id: 1,
wyId: 1,
name: 'Spain',
alpha2code: 'ES',
alpha3code: 'ESP',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async getByWyId( async getByWyId(
@Param('wyId', ParseIntPipe) wyId: number, @Param('wyId', ParseIntPipe) wyId: number,
): Promise<Area | undefined> { ): Promise<Area | undefined> {
...@@ -54,8 +100,7 @@ export class AreasController { ...@@ -54,8 +100,7 @@ export class AreasController {
name: 'search', name: 'search',
required: false, required: false,
type: String, type: String,
description: description: 'Search areas by name, alpha2code, or alpha3code. Optional.',
'Search areas by name, alpha2code, or alpha3code. Optional.',
}) })
@ApiQuery({ @ApiQuery({
name: 'limit', name: 'limit',
...@@ -82,10 +127,11 @@ export class AreasController { ...@@ -82,10 +127,11 @@ export class AreasController {
description: 'Sort order - ascending or descending (default: asc)', description: 'Sort order - ascending or descending (default: asc)',
}) })
@ApiOkResponse({ description: 'Paginated list of areas' }) @ApiOkResponse({ description: 'Paginated list of areas' })
async findAll(@Query() query: QueryAreasDto): Promise<SimplePaginatedResponse<any>> { async findAll(
@Query() query: QueryAreasDto,
): Promise<SimplePaginatedResponse<any>> {
const l = query.limit ?? 50; const l = query.limit ?? 50;
const o = query.offset ?? 0; const o = query.offset ?? 0;
return this.areasService.findAll(l, o, query.search, query); return this.areasService.findAll(l, o, query.search, query);
} }
} }
...@@ -41,11 +41,35 @@ export class AuthController { ...@@ -41,11 +41,35 @@ export class AuthController {
description: description:
'Authenticates a user and returns a JWT token. Supports 2FA and single-device login.', 'Authenticates a user and returns a JWT token. Supports 2FA and single-device login.',
}) })
@ApiBody({ type: LoginDto }) @ApiBody({
type: LoginDto,
examples: {
example1: {
summary: 'Login with email',
value: {
email: 'user@example.com',
password: 'SecurePassword123!',
},
},
},
})
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Login successful', description: 'Login successful',
type: AuthResponseDto, type: AuthResponseDto,
schema: {
example: {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
user: {
id: 1,
email: 'user@example.com',
name: 'John Doe',
role: 'scout',
},
requires2FA: false,
},
},
}) })
@ApiResponse({ @ApiResponse({
status: 401, status: 401,
...@@ -146,7 +170,17 @@ export class AuthController { ...@@ -146,7 +170,17 @@ export class AuthController {
summary: 'Verify and enable 2FA', summary: 'Verify and enable 2FA',
description: 'Verifies the 2FA code and enables two-factor authentication.', description: 'Verifies the 2FA code and enables two-factor authentication.',
}) })
@ApiBody({ type: Verify2FADto }) @ApiBody({
type: Verify2FADto,
examples: {
example1: {
summary: 'Verify 2FA code',
value: {
code: '123456',
},
},
},
})
@ApiResponse({ @ApiResponse({
status: 204, status: 204,
description: '2FA enabled successfully', description: '2FA enabled successfully',
...@@ -214,7 +248,19 @@ export class AuthController { ...@@ -214,7 +248,19 @@ export class AuthController {
description: description:
'Allows users to change their password by providing current password and new password.', 'Allows users to change their password by providing current password and new password.',
}) })
@ApiBody({ type: ChangePasswordDto }) @ApiBody({
type: ChangePasswordDto,
examples: {
example1: {
summary: 'Change password',
value: {
currentPassword: 'OldPassword123!',
newPassword: 'NewPassword456!',
confirmPassword: 'NewPassword456!',
},
},
},
})
@ApiResponse({ @ApiResponse({
status: 204, status: 204,
description: 'Password changed successfully', description: 'Password changed successfully',
......
...@@ -25,19 +25,71 @@ export class CoachesController { ...@@ -25,19 +25,71 @@ export class CoachesController {
constructor(private readonly coachesService: CoachesService) {} constructor(private readonly coachesService: CoachesService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create or update coach by wyId' }) @ApiOperation({
summary: 'Create or update coach by wyId',
description:
'Creates a new coach or updates an existing one identified by Wyscout ID',
})
@ApiBody({ @ApiBody({
description: 'Coach payload', description: 'Coach payload',
type: CreateCoachDto, type: CreateCoachDto,
examples: {
example1: {
summary: 'Create coach',
value: {
wyId: 1,
firstName: 'Carlo',
lastName: 'Ancelotti',
shortName: 'C. Ancelotti',
position: 'Head Coach',
status: 'active',
},
},
},
})
@ApiOkResponse({
description: 'Coach created or updated successfully',
schema: {
example: {
id: 1,
wyId: 1,
firstName: 'Carlo',
lastName: 'Ancelotti',
shortName: 'C. Ancelotti',
position: 'Head Coach',
status: 'active',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
}) })
@ApiOkResponse({ description: 'Upserted coach' })
async save(@Body() body: CreateCoachDto): Promise<Coach> { async save(@Body() body: CreateCoachDto): Promise<Coach> {
return this.coachesService.upsertByWyId(body as any); return this.coachesService.upsertByWyId(body as any);
} }
@Get(':wyId') @Get(':wyId')
@ApiOperation({ summary: 'Get coach by wyId' }) @ApiOperation({
@ApiOkResponse({ description: 'Coach if found' }) summary: 'Get coach by wyId',
description: 'Retrieves a specific coach by their Wyscout ID',
})
@ApiOkResponse({
description: 'Coach retrieved successfully',
schema: {
example: {
id: 1,
wyId: 1,
firstName: 'Carlo',
lastName: 'Ancelotti',
shortName: 'C. Ancelotti',
position: 'Head Coach',
status: 'active',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async getByWyId( async getByWyId(
@Param('wyId', ParseIntPipe) wyId: number, @Param('wyId', ParseIntPipe) wyId: number,
): Promise<Coach | undefined> { ): Promise<Coach | undefined> {
...@@ -72,7 +124,15 @@ export class CoachesController { ...@@ -72,7 +124,15 @@ export class CoachesController {
@ApiQuery({ @ApiQuery({
name: 'sortBy', name: 'sortBy',
required: false, required: false,
enum: ['firstName', 'lastName', 'shortName', 'position', 'status', 'createdAt', 'updatedAt'], enum: [
'firstName',
'lastName',
'shortName',
'position',
'status',
'createdAt',
'updatedAt',
],
description: 'Field to sort by (default: lastName)', description: 'Field to sort by (default: lastName)',
}) })
@ApiQuery({ @ApiQuery({
...@@ -82,7 +142,9 @@ export class CoachesController { ...@@ -82,7 +142,9 @@ export class CoachesController {
description: 'Sort order - ascending or descending (default: asc)', description: 'Sort order - ascending or descending (default: asc)',
}) })
@ApiOkResponse({ description: 'Paginated list of coaches' }) @ApiOkResponse({ description: 'Paginated list of coaches' })
async findAll(@Query() query: QueryCoachesDto): Promise<SimplePaginatedResponse<any>> { async findAll(
@Query() query: QueryCoachesDto,
): Promise<SimplePaginatedResponse<any>> {
const l = query.limit ?? 50; const l = query.limit ?? 50;
const o = query.offset ?? 0; const o = query.offset ?? 0;
return this.coachesService.findAll(l, o, query.name, query); return this.coachesService.findAll(l, o, query.name, query);
......
...@@ -25,19 +25,83 @@ export class MatchesController { ...@@ -25,19 +25,83 @@ export class MatchesController {
constructor(private readonly matchesService: MatchesService) {} constructor(private readonly matchesService: MatchesService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create or update match by wyId' }) @ApiOperation({
summary: 'Create or update match by wyId',
description:
'Creates a new match or updates an existing one identified by Wyscout ID',
})
@ApiBody({ @ApiBody({
description: 'Match payload', description: 'Match payload',
type: CreateMatchDto, type: CreateMatchDto,
examples: {
example1: {
summary: 'Create match',
value: {
wyId: 1,
matchDate: '2025-01-15T20:00:00Z',
venue: 'Santiago Bernabéu',
venueCity: 'Madrid',
venueCountry: 'Spain',
homeTeamWyId: 1,
awayTeamWyId: 2,
homeScore: 2,
awayScore: 1,
status: 'completed',
},
},
},
})
@ApiOkResponse({
description: 'Match created or updated successfully',
schema: {
example: {
id: 1,
wyId: 1,
matchDate: '2025-01-15T20:00:00Z',
venue: 'Santiago Bernabéu',
venueCity: 'Madrid',
venueCountry: 'Spain',
homeTeamWyId: 1,
awayTeamWyId: 2,
homeScore: 2,
awayScore: 1,
status: 'completed',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
}) })
@ApiOkResponse({ description: 'Upserted match' })
async save(@Body() body: CreateMatchDto): Promise<Match> { async save(@Body() body: CreateMatchDto): Promise<Match> {
return this.matchesService.upsertByWyId(body as any); return this.matchesService.upsertByWyId(body as any);
} }
@Get(':wyId') @Get(':wyId')
@ApiOperation({ summary: 'Get match by wyId' }) @ApiOperation({
@ApiOkResponse({ description: 'Match if found' }) summary: 'Get match by wyId',
description: 'Retrieves a specific match by its Wyscout ID',
})
@ApiOkResponse({
description: 'Match retrieved successfully',
schema: {
example: {
id: 1,
wyId: 1,
matchDate: '2025-01-15T20:00:00Z',
venue: 'Santiago Bernabéu',
venueCity: 'Madrid',
venueCountry: 'Spain',
homeTeamWyId: 1,
awayTeamWyId: 2,
homeScore: 2,
awayScore: 1,
status: 'completed',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async getByWyId( async getByWyId(
@Param('wyId', ParseIntPipe) wyId: number, @Param('wyId', ParseIntPipe) wyId: number,
): Promise<Match | undefined> { ): Promise<Match | undefined> {
...@@ -72,7 +136,17 @@ export class MatchesController { ...@@ -72,7 +136,17 @@ export class MatchesController {
@ApiQuery({ @ApiQuery({
name: 'sortBy', name: 'sortBy',
required: false, required: false,
enum: ['matchDate', 'venue', 'venueCity', 'venueCountry', 'homeScore', 'awayScore', 'status', 'createdAt', 'updatedAt'], enum: [
'matchDate',
'venue',
'venueCity',
'venueCountry',
'homeScore',
'awayScore',
'status',
'createdAt',
'updatedAt',
],
description: 'Field to sort by (default: matchDate)', description: 'Field to sort by (default: matchDate)',
}) })
@ApiQuery({ @ApiQuery({
...@@ -82,7 +156,9 @@ export class MatchesController { ...@@ -82,7 +156,9 @@ export class MatchesController {
description: 'Sort order - ascending or descending (default: asc)', description: 'Sort order - ascending or descending (default: asc)',
}) })
@ApiOkResponse({ description: 'Paginated list of matches' }) @ApiOkResponse({ description: 'Paginated list of matches' })
async findAll(@Query() query: QueryMatchesDto): Promise<MatchesPaginatedResponse<any>> { async findAll(
@Query() query: QueryMatchesDto,
): Promise<MatchesPaginatedResponse<any>> {
const l = query.limit ?? 50; const l = query.limit ?? 50;
const o = query.offset ?? 0; const o = query.offset ?? 0;
return this.matchesService.findAll(l, o, query.name, query); return this.matchesService.findAll(l, o, query.name, query);
......
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsOptional, IsString } from 'class-validator';
export class CreateFeatureSelectionDto {
@ApiProperty({
description: 'Player ID for which feature is being selected',
example: 1,
type: Number,
})
@IsInt()
playerId: number;
@ApiProperty({
description: 'Feature type ID being selected',
example: 1,
type: Number,
})
@IsInt()
featureTypeId: number;
@ApiProperty({
description: 'Scout/User ID making the selection',
example: 1,
type: Number,
})
@IsInt()
userId: number;
@ApiPropertyOptional({
description: 'Optional scout notes about the selection',
example: 'This feature applies to the player',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
notes?: string | null;
}
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class UpdateFeatureSelectionDto {
@ApiPropertyOptional({
description: 'Optional scout notes about the selection',
example: 'Updated notes about the selection',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
notes?: string | null;
}
...@@ -23,6 +23,7 @@ import { ...@@ -23,6 +23,7 @@ import {
type PlayerFeatureCategory, type PlayerFeatureCategory,
type PlayerFeatureType, type PlayerFeatureType,
type PlayerFeatureRating, type PlayerFeatureRating,
type PlayerFeatureSelection,
} from '../../database/schema'; } from '../../database/schema';
import { CreateFeatureCategoryDto } from './dto/create-feature-category.dto'; import { CreateFeatureCategoryDto } from './dto/create-feature-category.dto';
import { UpdateFeatureCategoryDto } from './dto/update-feature-category.dto'; import { UpdateFeatureCategoryDto } from './dto/update-feature-category.dto';
...@@ -30,6 +31,8 @@ import { CreateFeatureTypeDto } from './dto/create-feature-type.dto'; ...@@ -30,6 +31,8 @@ import { CreateFeatureTypeDto } from './dto/create-feature-type.dto';
import { UpdateFeatureTypeDto } from './dto/update-feature-type.dto'; import { UpdateFeatureTypeDto } from './dto/update-feature-type.dto';
import { CreateFeatureRatingDto } from './dto/create-feature-rating.dto'; import { CreateFeatureRatingDto } from './dto/create-feature-rating.dto';
import { UpdateFeatureRatingDto } from './dto/update-feature-rating.dto'; import { UpdateFeatureRatingDto } from './dto/update-feature-rating.dto';
import { CreateFeatureSelectionDto } from './dto/create-feature-selection.dto';
import { UpdateFeatureSelectionDto } from './dto/update-feature-selection.dto';
@ApiTags('Player Features') @ApiTags('Player Features')
@Controller('player-features') @Controller('player-features')
...@@ -41,9 +44,49 @@ export class PlayerFeaturesController { ...@@ -41,9 +44,49 @@ export class PlayerFeaturesController {
// ============================================================================ // ============================================================================
@Post('categories') @Post('categories')
@ApiOperation({ summary: 'Create feature category' }) @ApiOperation({
@ApiBody({ type: CreateFeatureCategoryDto }) summary: 'Create feature category',
@ApiOkResponse({ description: 'Created feature category' }) description:
'Creates a new feature category (e.g., Físicas, Técnicas, Tácticas, Mentales)',
})
@ApiBody({
type: CreateFeatureCategoryDto,
examples: {
physical: {
summary: 'Physical Category',
value: {
name: 'Físicas',
description: 'Physical characteristics of the player',
order: 0,
isActive: true,
},
},
technical: {
summary: 'Technical Category',
value: {
name: 'Técnicas',
description: 'Technical skills of the player',
order: 1,
isActive: true,
},
},
},
})
@ApiOkResponse({
description: 'Feature category created successfully',
schema: {
example: {
id: 1,
name: 'Físicas',
description: 'Physical characteristics of the player',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async createCategory( async createCategory(
@Body() body: CreateFeatureCategoryDto, @Body() body: CreateFeatureCategoryDto,
): Promise<PlayerFeatureCategory> { ): Promise<PlayerFeatureCategory> {
...@@ -51,10 +94,38 @@ export class PlayerFeaturesController { ...@@ -51,10 +94,38 @@ export class PlayerFeaturesController {
} }
@Patch('categories/:id') @Patch('categories/:id')
@ApiOperation({ summary: 'Update feature category by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Update feature category by ID',
@ApiBody({ type: UpdateFeatureCategoryDto }) description: 'Updates an existing feature category',
@ApiOkResponse({ description: 'Updated feature category' }) })
@ApiParam({ name: 'id', type: Number, description: 'Category ID' })
@ApiBody({
type: UpdateFeatureCategoryDto,
examples: {
example1: {
summary: 'Update category name',
value: {
name: 'Físicas Mejoradas',
isActive: true,
},
},
},
})
@ApiOkResponse({
description: 'Feature category updated successfully',
schema: {
example: {
id: 1,
name: 'Físicas Mejoradas',
description: 'Physical characteristics of the player',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T11:00:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Category not found' }) @ApiNotFoundResponse({ description: 'Category not found' })
async updateCategory( async updateCategory(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
...@@ -64,9 +135,27 @@ export class PlayerFeaturesController { ...@@ -64,9 +135,27 @@ export class PlayerFeaturesController {
} }
@Get('categories/:id') @Get('categories/:id')
@ApiOperation({ summary: 'Get feature category by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Get feature category by ID',
@ApiOkResponse({ description: 'Feature category if found' }) description: 'Retrieves a specific feature category by its ID',
})
@ApiParam({ name: 'id', type: Number, description: 'Category ID' })
@ApiOkResponse({
description: 'Feature category retrieved successfully',
schema: {
example: {
id: 1,
name: 'Físicas',
description: 'Physical characteristics of the player',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Category not found' })
async getCategoryById( async getCategoryById(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
): Promise<PlayerFeatureCategory | null> { ): Promise<PlayerFeatureCategory | null> {
...@@ -74,15 +163,48 @@ export class PlayerFeaturesController { ...@@ -74,15 +163,48 @@ export class PlayerFeaturesController {
} }
@Get('categories') @Get('categories')
@ApiOperation({ summary: 'List all feature categories' }) @ApiOperation({
@ApiOkResponse({ description: 'List of feature categories' }) summary: 'List all feature categories',
description:
'Retrieves all feature categories (Físicas, Técnicas, Tácticas, Mentales)',
})
@ApiOkResponse({
description: 'List of all feature categories',
schema: {
example: [
{
id: 1,
name: 'Físicas',
description: 'Physical characteristics of the player',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
{
id: 2,
name: 'Técnicas',
description: 'Technical skills of the player',
order: 1,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async listCategories(): Promise<PlayerFeatureCategory[]> { async listCategories(): Promise<PlayerFeatureCategory[]> {
return this.playerFeaturesService.getAllCategories(); return this.playerFeaturesService.getAllCategories();
} }
@Delete('categories/:id') @Delete('categories/:id')
@ApiOperation({ summary: 'Delete feature category by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Delete feature category by ID',
description: 'Soft deletes a feature category (marks as deleted)',
})
@ApiParam({ name: 'id', type: Number, description: 'Category ID' })
@ApiNoContentResponse({ description: 'Category deleted successfully' }) @ApiNoContentResponse({ description: 'Category deleted successfully' })
@ApiNotFoundResponse({ description: 'Category not found' }) @ApiNotFoundResponse({ description: 'Category not found' })
async deleteCategory(@Param('id', ParseIntPipe) id: number): Promise<void> { async deleteCategory(@Param('id', ParseIntPipe) id: number): Promise<void> {
...@@ -97,9 +219,52 @@ export class PlayerFeaturesController { ...@@ -97,9 +219,52 @@ export class PlayerFeaturesController {
// ============================================================================ // ============================================================================
@Post('types') @Post('types')
@ApiOperation({ summary: 'Create feature type' }) @ApiOperation({
@ApiBody({ type: CreateFeatureTypeDto }) summary: 'Create feature type',
@ApiOkResponse({ description: 'Created feature type' }) description:
'Creates a new feature type within a category (e.g., Agilidad, Agresividad)',
})
@ApiBody({
type: CreateFeatureTypeDto,
examples: {
agility: {
summary: 'Agility Feature',
value: {
categoryId: 1,
name: 'Agilidad',
description: 'Player agility and speed of movement',
order: 0,
isActive: true,
},
},
aggression: {
summary: 'Aggression Feature',
value: {
categoryId: 1,
name: 'Agresividad',
description: 'Player aggression and intensity',
order: 1,
isActive: true,
},
},
},
})
@ApiOkResponse({
description: 'Feature type created successfully',
schema: {
example: {
id: 1,
categoryId: 1,
name: 'Agilidad',
description: 'Player agility and speed of movement',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async createFeatureType( async createFeatureType(
@Body() body: CreateFeatureTypeDto, @Body() body: CreateFeatureTypeDto,
): Promise<PlayerFeatureType> { ): Promise<PlayerFeatureType> {
...@@ -107,10 +272,39 @@ export class PlayerFeaturesController { ...@@ -107,10 +272,39 @@ export class PlayerFeaturesController {
} }
@Patch('types/:id') @Patch('types/:id')
@ApiOperation({ summary: 'Update feature type by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Update feature type by ID',
@ApiBody({ type: UpdateFeatureTypeDto }) description: 'Updates an existing feature type',
@ApiOkResponse({ description: 'Updated feature type' }) })
@ApiParam({ name: 'id', type: Number, description: 'Feature type ID' })
@ApiBody({
type: UpdateFeatureTypeDto,
examples: {
example1: {
summary: 'Update feature name',
value: {
name: 'Agilidad Mejorada',
isActive: true,
},
},
},
})
@ApiOkResponse({
description: 'Feature type updated successfully',
schema: {
example: {
id: 1,
categoryId: 1,
name: 'Agilidad Mejorada',
description: 'Player agility and speed of movement',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T11:00:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Feature type not found' }) @ApiNotFoundResponse({ description: 'Feature type not found' })
async updateFeatureType( async updateFeatureType(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
...@@ -120,9 +314,28 @@ export class PlayerFeaturesController { ...@@ -120,9 +314,28 @@ export class PlayerFeaturesController {
} }
@Get('types/:id') @Get('types/:id')
@ApiOperation({ summary: 'Get feature type by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Get feature type by ID',
@ApiOkResponse({ description: 'Feature type if found' }) description: 'Retrieves a specific feature type by its ID',
})
@ApiParam({ name: 'id', type: Number, description: 'Feature type ID' })
@ApiOkResponse({
description: 'Feature type retrieved successfully',
schema: {
example: {
id: 1,
categoryId: 1,
name: 'Agilidad',
description: 'Player agility and speed of movement',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Feature type not found' })
async getFeatureTypeById( async getFeatureTypeById(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
): Promise<PlayerFeatureType | null> { ): Promise<PlayerFeatureType | null> {
...@@ -130,9 +343,40 @@ export class PlayerFeaturesController { ...@@ -130,9 +343,40 @@ export class PlayerFeaturesController {
} }
@Get('types/category/:categoryId') @Get('types/category/:categoryId')
@ApiOperation({ summary: 'Get feature types by category' }) @ApiOperation({
@ApiParam({ name: 'categoryId', type: Number }) summary: 'Get feature types by category',
@ApiOkResponse({ description: 'List of feature types for category' }) description: 'Retrieves all feature types belonging to a specific category',
})
@ApiParam({ name: 'categoryId', type: Number, description: 'Category ID' })
@ApiOkResponse({
description: 'List of feature types for the category',
schema: {
example: [
{
id: 1,
categoryId: 1,
name: 'Agilidad',
description: 'Player agility and speed of movement',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
{
id: 2,
categoryId: 1,
name: 'Agresividad',
description: 'Player aggression and intensity',
order: 1,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getFeatureTypesByCategory( async getFeatureTypesByCategory(
@Param('categoryId', ParseIntPipe) categoryId: number, @Param('categoryId', ParseIntPipe) categoryId: number,
): Promise<PlayerFeatureType[]> { ): Promise<PlayerFeatureType[]> {
...@@ -140,15 +384,38 @@ export class PlayerFeaturesController { ...@@ -140,15 +384,38 @@ export class PlayerFeaturesController {
} }
@Get('types') @Get('types')
@ApiOperation({ summary: 'List all feature types' }) @ApiOperation({
@ApiOkResponse({ description: 'List of feature types' }) summary: 'List all feature types',
description: 'Retrieves all feature types across all categories',
})
@ApiOkResponse({
description: 'List of all feature types',
schema: {
example: [
{
id: 1,
categoryId: 1,
name: 'Agilidad',
description: 'Player agility and speed of movement',
order: 0,
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async listFeatureTypes(): Promise<PlayerFeatureType[]> { async listFeatureTypes(): Promise<PlayerFeatureType[]> {
return this.playerFeaturesService.getAllFeatureTypes(); return this.playerFeaturesService.getAllFeatureTypes();
} }
@Delete('types/:id') @Delete('types/:id')
@ApiOperation({ summary: 'Delete feature type by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Delete feature type by ID',
description: 'Soft deletes a feature type (marks as deleted)',
})
@ApiParam({ name: 'id', type: Number, description: 'Feature type ID' })
@ApiNoContentResponse({ description: 'Feature type deleted successfully' }) @ApiNoContentResponse({ description: 'Feature type deleted successfully' })
@ApiNotFoundResponse({ description: 'Feature type not found' }) @ApiNotFoundResponse({ description: 'Feature type not found' })
async deleteFeatureType( async deleteFeatureType(
...@@ -165,9 +432,52 @@ export class PlayerFeaturesController { ...@@ -165,9 +432,52 @@ export class PlayerFeaturesController {
// ============================================================================ // ============================================================================
@Post('ratings') @Post('ratings')
@ApiOperation({ summary: 'Create or update feature rating' }) @ApiOperation({
@ApiBody({ type: CreateFeatureRatingDto }) summary: 'Create or update feature rating',
@ApiOkResponse({ description: 'Created or updated feature rating' }) description:
'Creates a new rating or updates an existing one for a player feature. Rating is on a 1-5 scale and is optional.',
})
@ApiBody({
type: CreateFeatureRatingDto,
examples: {
withRating: {
summary: 'Create rating with score',
value: {
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: 4,
notes: 'Player showed excellent agility during the match',
},
},
withoutRating: {
summary: 'Create rating without score (selection only)',
value: {
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: null,
notes: 'Feature applies to player',
},
},
},
})
@ApiOkResponse({
description: 'Feature rating created or updated successfully',
schema: {
example: {
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: 4,
notes: 'Player showed excellent agility during the match',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async createRating( async createRating(
@Body() body: CreateFeatureRatingDto, @Body() body: CreateFeatureRatingDto,
): Promise<PlayerFeatureRating> { ): Promise<PlayerFeatureRating> {
...@@ -175,10 +485,39 @@ export class PlayerFeaturesController { ...@@ -175,10 +485,39 @@ export class PlayerFeaturesController {
} }
@Patch('ratings/:id') @Patch('ratings/:id')
@ApiOperation({ summary: 'Update feature rating by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Update feature rating by ID',
@ApiBody({ type: UpdateFeatureRatingDto }) description: 'Updates an existing feature rating',
@ApiOkResponse({ description: 'Updated feature rating' }) })
@ApiParam({ name: 'id', type: Number, description: 'Rating ID' })
@ApiBody({
type: UpdateFeatureRatingDto,
examples: {
example1: {
summary: 'Update rating score',
value: {
rating: 5,
notes: 'Updated: Player showed exceptional agility',
},
},
},
})
@ApiOkResponse({
description: 'Feature rating updated successfully',
schema: {
example: {
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: 5,
notes: 'Updated: Player showed exceptional agility',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T11:00:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Rating not found' }) @ApiNotFoundResponse({ description: 'Rating not found' })
async updateRating( async updateRating(
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
...@@ -188,30 +527,104 @@ export class PlayerFeaturesController { ...@@ -188,30 +527,104 @@ export class PlayerFeaturesController {
} }
@Get('ratings/:id') @Get('ratings/:id')
@ApiOperation({ summary: 'Get feature rating by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Get feature rating by ID',
@ApiOkResponse({ description: 'Feature rating if found' }) description:
async getRatingById( 'Retrieves a specific feature rating by its ID with scout name',
@Param('id', ParseIntPipe) id: number, })
): Promise<PlayerFeatureRating | null> { @ApiParam({ name: 'id', type: Number, description: 'Rating ID' })
@ApiOkResponse({
description: 'Feature rating retrieved successfully',
schema: {
example: {
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: 4,
notes: 'Player showed excellent agility during the match',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
userName: 'John Doe',
},
},
})
@ApiNotFoundResponse({ description: 'Rating not found' })
async getRatingById(@Param('id', ParseIntPipe) id: number): Promise<any> {
return this.playerFeaturesService.getRatingById(id); return this.playerFeaturesService.getRatingById(id);
} }
@Get('ratings/player/:playerId') @Get('ratings/player/:playerId')
@ApiOperation({ summary: 'Get all ratings for a player' }) @ApiOperation({
@ApiParam({ name: 'playerId', type: Number }) summary: 'Get all ratings for a player',
@ApiOkResponse({ description: 'List of ratings for player' }) description:
'Retrieves all feature ratings from all scouts for a specific player with scout names',
})
@ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiOkResponse({
description: 'List of all ratings for the player',
schema: {
example: [
{
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: 4,
notes: 'Player showed excellent agility during the match',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
userName: 'John Doe',
},
{
id: 2,
playerId: 1,
featureTypeId: 2,
userId: 5,
rating: 3,
notes: 'Average aggression level',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
userName: 'John Doe',
},
],
},
})
async getRatingsByPlayer( async getRatingsByPlayer(
@Param('playerId', ParseIntPipe) playerId: number, @Param('playerId', ParseIntPipe) playerId: number,
): Promise<PlayerFeatureRating[]> { ): Promise<any[]> {
return this.playerFeaturesService.getRatingsByPlayer(playerId); return this.playerFeaturesService.getRatingsByPlayer(playerId);
} }
@Get('ratings/player/:playerId/scout/:userId') @Get('ratings/player/:playerId/scout/:userId')
@ApiOperation({ summary: 'Get ratings for a player by a specific scout' }) @ApiOperation({
@ApiParam({ name: 'playerId', type: Number }) summary: 'Get ratings for a player by a specific scout',
@ApiParam({ name: 'userId', type: Number }) description:
@ApiOkResponse({ description: 'List of ratings from scout for player' }) 'Retrieves all feature ratings provided by a specific scout for a player',
})
@ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiParam({ name: 'userId', type: Number, description: 'Scout/User ID' })
@ApiOkResponse({
description: 'List of ratings from the scout for the player',
schema: {
example: [
{
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: 4,
notes: 'Player showed excellent agility during the match',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getRatingsByPlayerAndScout( async getRatingsByPlayerAndScout(
@Param('playerId', ParseIntPipe) playerId: number, @Param('playerId', ParseIntPipe) playerId: number,
@Param('userId', ParseIntPipe) userId: number, @Param('userId', ParseIntPipe) userId: number,
...@@ -223,9 +636,34 @@ export class PlayerFeaturesController { ...@@ -223,9 +636,34 @@ export class PlayerFeaturesController {
} }
@Get('ratings/feature/:featureTypeId') @Get('ratings/feature/:featureTypeId')
@ApiOperation({ summary: 'Get all ratings for a feature type' }) @ApiOperation({
@ApiParam({ name: 'featureTypeId', type: Number }) summary: 'Get all ratings for a feature type',
@ApiOkResponse({ description: 'List of ratings for feature type' }) description:
'Retrieves all ratings from all scouts for a specific feature type',
})
@ApiParam({
name: 'featureTypeId',
type: Number,
description: 'Feature type ID',
})
@ApiOkResponse({
description: 'List of all ratings for the feature type',
schema: {
example: [
{
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
rating: 4,
notes: 'Player showed excellent agility during the match',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getRatingsByFeature( async getRatingsByFeature(
@Param('featureTypeId', ParseIntPipe) featureTypeId: number, @Param('featureTypeId', ParseIntPipe) featureTypeId: number,
): Promise<PlayerFeatureRating[]> { ): Promise<PlayerFeatureRating[]> {
...@@ -233,8 +671,11 @@ export class PlayerFeaturesController { ...@@ -233,8 +671,11 @@ export class PlayerFeaturesController {
} }
@Delete('ratings/:id') @Delete('ratings/:id')
@ApiOperation({ summary: 'Delete feature rating by ID' }) @ApiOperation({
@ApiParam({ name: 'id', type: Number }) summary: 'Delete feature rating by ID',
description: 'Soft deletes a feature rating (marks as deleted)',
})
@ApiParam({ name: 'id', type: Number, description: 'Rating ID' })
@ApiNoContentResponse({ description: 'Rating deleted successfully' }) @ApiNoContentResponse({ description: 'Rating deleted successfully' })
@ApiNotFoundResponse({ description: 'Rating not found' }) @ApiNotFoundResponse({ description: 'Rating not found' })
async deleteRating(@Param('id', ParseIntPipe) id: number): Promise<void> { async deleteRating(@Param('id', ParseIntPipe) id: number): Promise<void> {
...@@ -251,14 +692,280 @@ export class PlayerFeaturesController { ...@@ -251,14 +692,280 @@ export class PlayerFeaturesController {
@Get('player/:playerId/all') @Get('player/:playerId/all')
@ApiOperation({ @ApiOperation({
summary: 'Get all features and ratings for a player grouped by category', summary: 'Get all features and ratings for a player grouped by category',
description:
'Retrieves all features and their ratings for a player, organized by category with aggregated data',
}) })
@ApiParam({ name: 'playerId', type: Number }) @ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiOkResponse({ @ApiOkResponse({
description: 'Player features with ratings grouped by category', description: 'Player features with ratings grouped by category',
schema: {
example: {
playerId: 1,
categories: [
{
categoryId: 1,
categoryName: 'Físicas',
features: [
{
featureTypeId: 1,
featureName: 'Agilidad',
ratings: [
{
id: 1,
userId: 5,
rating: 4,
notes: 'Excellent agility',
},
],
averageRating: 4,
ratingCount: 1,
},
],
},
],
},
},
}) })
async getPlayerFeaturesWithRatings( async getPlayerFeaturesWithRatings(
@Param('playerId', ParseIntPipe) playerId: number, @Param('playerId', ParseIntPipe) playerId: number,
) { ) {
return this.playerFeaturesService.getPlayerFeaturesWithRatings(playerId); return this.playerFeaturesService.getPlayerFeaturesWithRatings(playerId);
} }
// ============================================================================
// SELECTIONS
// ============================================================================
@Post('selections')
@ApiOperation({
summary: 'Create or update feature selection',
description:
'Creates a new feature selection or updates an existing one. Selections track which features apply to a player.',
})
@ApiBody({
type: CreateFeatureSelectionDto,
examples: {
example1: {
summary: 'Create feature selection',
value: {
playerId: 1,
featureTypeId: 1,
userId: 5,
notes: 'This feature applies to the player',
},
},
},
})
@ApiOkResponse({
description: 'Feature selection created or updated successfully',
schema: {
example: {
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
notes: 'This feature applies to the player',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
})
async createSelection(
@Body() body: CreateFeatureSelectionDto,
): Promise<PlayerFeatureSelection> {
return this.playerFeaturesService.createSelection(body as any);
}
@Patch('selections/:id')
@ApiOperation({
summary: 'Update feature selection by ID',
description: 'Updates an existing feature selection',
})
@ApiParam({ name: 'id', type: Number, description: 'Selection ID' })
@ApiBody({
type: UpdateFeatureSelectionDto,
examples: {
example1: {
summary: 'Update selection notes',
value: {
notes: 'Updated: Feature applies to player in specific positions',
},
},
},
})
@ApiOkResponse({
description: 'Feature selection updated successfully',
schema: {
example: {
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
notes: 'Updated: Feature applies to player in specific positions',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T11:00:00Z',
deletedAt: null,
},
},
})
@ApiNotFoundResponse({ description: 'Selection not found' })
async updateSelection(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateFeatureSelectionDto,
): Promise<PlayerFeatureSelection> {
return this.playerFeaturesService.updateSelection(id, body as any);
}
@Get('selections/:id')
@ApiOperation({
summary: 'Get feature selection by ID',
description:
'Retrieves a specific feature selection by its ID with scout name',
})
@ApiParam({ name: 'id', type: Number, description: 'Selection ID' })
@ApiOkResponse({
description: 'Feature selection retrieved successfully',
schema: {
example: {
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
notes: 'This feature applies to the player',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
userName: 'John Doe',
},
},
})
@ApiNotFoundResponse({ description: 'Selection not found' })
async getSelectionById(@Param('id', ParseIntPipe) id: number): Promise<any> {
return this.playerFeaturesService.getSelectionById(id);
}
@Get('selections/player/:playerId')
@ApiOperation({
summary: 'Get all selections for a player',
description:
'Retrieves all feature selections from all scouts for a specific player',
})
@ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiOkResponse({
description: 'List of all selections for the player',
schema: {
example: [
{
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
notes: 'This feature applies to the player',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
{
id: 2,
playerId: 1,
featureTypeId: 2,
userId: 5,
notes: 'Also applies',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getSelectionsByPlayer(
@Param('playerId', ParseIntPipe) playerId: number,
): Promise<PlayerFeatureSelection[]> {
return this.playerFeaturesService.getSelectionsByPlayer(playerId);
}
@Get('selections/player/:playerId/scout/:userId')
@ApiOperation({
summary: 'Get selections for a player by a specific scout',
description:
'Retrieves all feature selections made by a specific scout for a player',
})
@ApiParam({ name: 'playerId', type: Number, description: 'Player ID' })
@ApiParam({ name: 'userId', type: Number, description: 'Scout/User ID' })
@ApiOkResponse({
description: 'List of selections from the scout for the player',
schema: {
example: [
{
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
notes: 'This feature applies to the player',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getSelectionsByPlayerAndScout(
@Param('playerId', ParseIntPipe) playerId: number,
@Param('userId', ParseIntPipe) userId: number,
): Promise<PlayerFeatureSelection[]> {
return this.playerFeaturesService.getSelectionsByPlayerAndScout(
playerId,
userId,
);
}
@Get('selections/feature/:featureTypeId')
@ApiOperation({
summary: 'Get all selections for a feature type',
description:
'Retrieves all selections from all scouts for a specific feature type',
})
@ApiParam({
name: 'featureTypeId',
type: Number,
description: 'Feature type ID',
})
@ApiOkResponse({
description: 'List of all selections for the feature type',
schema: {
example: [
{
id: 1,
playerId: 1,
featureTypeId: 1,
userId: 5,
notes: 'This feature applies to the player',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
],
},
})
async getSelectionsByFeature(
@Param('featureTypeId', ParseIntPipe) featureTypeId: number,
): Promise<PlayerFeatureSelection[]> {
return this.playerFeaturesService.getSelectionsByFeature(featureTypeId);
}
@Delete('selections/:id')
@ApiOperation({
summary: 'Delete feature selection by ID',
description: 'Soft deletes a feature selection (marks as deleted)',
})
@ApiParam({ name: 'id', type: Number, description: 'Selection ID' })
@ApiNoContentResponse({ description: 'Selection deleted successfully' })
@ApiNotFoundResponse({ description: 'Selection not found' })
async deleteSelection(@Param('id', ParseIntPipe) id: number): Promise<void> {
const result = await this.playerFeaturesService.deleteSelection(id);
if (!result) {
throw new NotFoundException(`Selection with ID ${id} not found`);
}
}
} }
...@@ -9,12 +9,16 @@ import { ...@@ -9,12 +9,16 @@ import {
playerFeatureCategories, playerFeatureCategories,
playerFeatureTypes, playerFeatureTypes,
playerFeatureRatings, playerFeatureRatings,
playerFeatureSelections,
users,
type PlayerFeatureCategory, type PlayerFeatureCategory,
type NewPlayerFeatureCategory, type NewPlayerFeatureCategory,
type PlayerFeatureType, type PlayerFeatureType,
type NewPlayerFeatureType, type NewPlayerFeatureType,
type PlayerFeatureRating, type PlayerFeatureRating,
type NewPlayerFeatureRating, type NewPlayerFeatureRating,
type PlayerFeatureSelection,
type NewPlayerFeatureSelection,
} from '../../database/schema'; } from '../../database/schema';
@Injectable() @Injectable()
...@@ -320,37 +324,73 @@ export class PlayerFeaturesService { ...@@ -320,37 +324,73 @@ export class PlayerFeaturesService {
return row as PlayerFeatureRating; return row as PlayerFeatureRating;
} }
async getRatingById(id: number): Promise<PlayerFeatureRating | null> { async getRatingById(id: number): Promise<any> {
const db = this.databaseService.getDatabase(); const db = this.databaseService.getDatabase();
const [row] = await db const [row] = await db
.select() .select({
id: playerFeatureRatings.id,
playerId: playerFeatureRatings.playerId,
featureTypeId: playerFeatureRatings.featureTypeId,
userId: playerFeatureRatings.userId,
rating: playerFeatureRatings.rating,
notes: playerFeatureRatings.notes,
createdAt: playerFeatureRatings.createdAt,
updatedAt: playerFeatureRatings.updatedAt,
deletedAt: playerFeatureRatings.deletedAt,
userName: users.name,
})
.from(playerFeatureRatings) .from(playerFeatureRatings)
.leftJoin(users, eq(playerFeatureRatings.userId, users.id))
.where(eq(playerFeatureRatings.id, id)); .where(eq(playerFeatureRatings.id, id));
return (row as PlayerFeatureRating) || null; return row || null;
} }
async getRatingsByPlayer(playerId: number): Promise<PlayerFeatureRating[]> { async getRatingsByPlayer(playerId: number): Promise<any[]> {
const db = this.databaseService.getDatabase(); const db = this.databaseService.getDatabase();
const rows = await db const rows = await db
.select() .select({
id: playerFeatureRatings.id,
playerId: playerFeatureRatings.playerId,
featureTypeId: playerFeatureRatings.featureTypeId,
userId: playerFeatureRatings.userId,
rating: playerFeatureRatings.rating,
notes: playerFeatureRatings.notes,
createdAt: playerFeatureRatings.createdAt,
updatedAt: playerFeatureRatings.updatedAt,
deletedAt: playerFeatureRatings.deletedAt,
userName: users.name,
})
.from(playerFeatureRatings) .from(playerFeatureRatings)
.leftJoin(users, eq(playerFeatureRatings.userId, users.id))
.where( .where(
and( and(
eq(playerFeatureRatings.playerId, playerId), eq(playerFeatureRatings.playerId, playerId),
isNull(playerFeatureRatings.deletedAt), isNull(playerFeatureRatings.deletedAt),
), ),
); );
return rows as PlayerFeatureRating[]; return rows;
} }
async getRatingsByPlayerAndScout( async getRatingsByPlayerAndScout(
playerId: number, playerId: number,
userId: number, userId: number,
): Promise<PlayerFeatureRating[]> { ): Promise<any[]> {
const db = this.databaseService.getDatabase(); const db = this.databaseService.getDatabase();
const rows = await db const rows = await db
.select() .select({
id: playerFeatureRatings.id,
playerId: playerFeatureRatings.playerId,
featureTypeId: playerFeatureRatings.featureTypeId,
userId: playerFeatureRatings.userId,
rating: playerFeatureRatings.rating,
notes: playerFeatureRatings.notes,
createdAt: playerFeatureRatings.createdAt,
updatedAt: playerFeatureRatings.updatedAt,
deletedAt: playerFeatureRatings.deletedAt,
userName: users.name,
})
.from(playerFeatureRatings) .from(playerFeatureRatings)
.leftJoin(users, eq(playerFeatureRatings.userId, users.id))
.where( .where(
and( and(
eq(playerFeatureRatings.playerId, playerId), eq(playerFeatureRatings.playerId, playerId),
...@@ -358,23 +398,33 @@ export class PlayerFeaturesService { ...@@ -358,23 +398,33 @@ export class PlayerFeaturesService {
isNull(playerFeatureRatings.deletedAt), isNull(playerFeatureRatings.deletedAt),
), ),
); );
return rows as PlayerFeatureRating[]; return rows;
} }
async getRatingsByFeature( async getRatingsByFeature(featureTypeId: number): Promise<any[]> {
featureTypeId: number,
): Promise<PlayerFeatureRating[]> {
const db = this.databaseService.getDatabase(); const db = this.databaseService.getDatabase();
const rows = await db const rows = await db
.select() .select({
id: playerFeatureRatings.id,
playerId: playerFeatureRatings.playerId,
featureTypeId: playerFeatureRatings.featureTypeId,
userId: playerFeatureRatings.userId,
rating: playerFeatureRatings.rating,
notes: playerFeatureRatings.notes,
createdAt: playerFeatureRatings.createdAt,
updatedAt: playerFeatureRatings.updatedAt,
deletedAt: playerFeatureRatings.deletedAt,
userName: users.name,
})
.from(playerFeatureRatings) .from(playerFeatureRatings)
.leftJoin(users, eq(playerFeatureRatings.userId, users.id))
.where( .where(
and( and(
eq(playerFeatureRatings.featureTypeId, featureTypeId), eq(playerFeatureRatings.featureTypeId, featureTypeId),
isNull(playerFeatureRatings.deletedAt), isNull(playerFeatureRatings.deletedAt),
), ),
); );
return rows as PlayerFeatureRating[]; return rows;
} }
async deleteRating(id: number): Promise<boolean> { async deleteRating(id: number): Promise<boolean> {
...@@ -428,8 +478,20 @@ export class PlayerFeaturesService { ...@@ -428,8 +478,20 @@ export class PlayerFeaturesService {
const typesWithRatings = await Promise.all( const typesWithRatings = await Promise.all(
types.map(async (type) => { types.map(async (type) => {
const ratings = await db const ratings = await db
.select() .select({
id: playerFeatureRatings.id,
playerId: playerFeatureRatings.playerId,
featureTypeId: playerFeatureRatings.featureTypeId,
userId: playerFeatureRatings.userId,
rating: playerFeatureRatings.rating,
notes: playerFeatureRatings.notes,
createdAt: playerFeatureRatings.createdAt,
updatedAt: playerFeatureRatings.updatedAt,
deletedAt: playerFeatureRatings.deletedAt,
userName: users.name,
})
.from(playerFeatureRatings) .from(playerFeatureRatings)
.leftJoin(users, eq(playerFeatureRatings.userId, users.id))
.where( .where(
and( and(
eq(playerFeatureRatings.playerId, playerId), eq(playerFeatureRatings.playerId, playerId),
...@@ -463,4 +525,194 @@ export class PlayerFeaturesService { ...@@ -463,4 +525,194 @@ export class PlayerFeaturesService {
return result; return result;
} }
// ============================================================================
// SELECTIONS
// ============================================================================
async createSelection(
data: Partial<NewPlayerFeatureSelection>,
): Promise<PlayerFeatureSelection> {
const db = this.databaseService.getDatabase();
if (!data.playerId) {
throw new BadRequestException('Player ID is required');
}
if (!data.featureTypeId) {
throw new BadRequestException('Feature type ID is required');
}
if (!data.userId) {
throw new BadRequestException('User ID is required');
}
const [row] = await db
.insert(playerFeatureSelections)
.values({
playerId: data.playerId,
featureTypeId: data.featureTypeId,
userId: data.userId,
notes: data.notes ?? null,
} as NewPlayerFeatureSelection)
.onConflictDoUpdate({
target: [
playerFeatureSelections.playerId,
playerFeatureSelections.featureTypeId,
playerFeatureSelections.userId,
],
set: {
notes: data.notes ?? null,
updatedAt: new Date(),
},
})
.returning();
return row as PlayerFeatureSelection;
}
async updateSelection(
id: number,
data: Partial<NewPlayerFeatureSelection>,
): Promise<PlayerFeatureSelection> {
const db = this.databaseService.getDatabase();
const [existing] = await db
.select()
.from(playerFeatureSelections)
.where(eq(playerFeatureSelections.id, id));
if (!existing) {
throw new NotFoundException(`Selection with ID ${id} not found`);
}
const [row] = await db
.update(playerFeatureSelections)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(playerFeatureSelections.id, id))
.returning();
return row as PlayerFeatureSelection;
}
async getSelectionById(id: number): Promise<any> {
const db = this.databaseService.getDatabase();
const [row] = await db
.select({
id: playerFeatureSelections.id,
playerId: playerFeatureSelections.playerId,
featureTypeId: playerFeatureSelections.featureTypeId,
userId: playerFeatureSelections.userId,
notes: playerFeatureSelections.notes,
createdAt: playerFeatureSelections.createdAt,
updatedAt: playerFeatureSelections.updatedAt,
deletedAt: playerFeatureSelections.deletedAt,
userName: users.name,
})
.from(playerFeatureSelections)
.leftJoin(users, eq(playerFeatureSelections.userId, users.id))
.where(eq(playerFeatureSelections.id, id));
return row || null;
}
async getSelectionsByPlayer(playerId: number): Promise<any[]> {
const db = this.databaseService.getDatabase();
const rows = await db
.select({
id: playerFeatureSelections.id,
playerId: playerFeatureSelections.playerId,
featureTypeId: playerFeatureSelections.featureTypeId,
userId: playerFeatureSelections.userId,
notes: playerFeatureSelections.notes,
createdAt: playerFeatureSelections.createdAt,
updatedAt: playerFeatureSelections.updatedAt,
deletedAt: playerFeatureSelections.deletedAt,
userName: users.name,
})
.from(playerFeatureSelections)
.leftJoin(users, eq(playerFeatureSelections.userId, users.id))
.where(
and(
eq(playerFeatureSelections.playerId, playerId),
isNull(playerFeatureSelections.deletedAt),
),
);
return rows;
}
async getSelectionsByPlayerAndScout(
playerId: number,
userId: number,
): Promise<any[]> {
const db = this.databaseService.getDatabase();
const rows = await db
.select({
id: playerFeatureSelections.id,
playerId: playerFeatureSelections.playerId,
featureTypeId: playerFeatureSelections.featureTypeId,
userId: playerFeatureSelections.userId,
notes: playerFeatureSelections.notes,
createdAt: playerFeatureSelections.createdAt,
updatedAt: playerFeatureSelections.updatedAt,
deletedAt: playerFeatureSelections.deletedAt,
userName: users.name,
})
.from(playerFeatureSelections)
.leftJoin(users, eq(playerFeatureSelections.userId, users.id))
.where(
and(
eq(playerFeatureSelections.playerId, playerId),
eq(playerFeatureSelections.userId, userId),
isNull(playerFeatureSelections.deletedAt),
),
);
return rows;
}
async getSelectionsByFeature(featureTypeId: number): Promise<any[]> {
const db = this.databaseService.getDatabase();
const rows = await db
.select({
id: playerFeatureSelections.id,
playerId: playerFeatureSelections.playerId,
featureTypeId: playerFeatureSelections.featureTypeId,
userId: playerFeatureSelections.userId,
notes: playerFeatureSelections.notes,
createdAt: playerFeatureSelections.createdAt,
updatedAt: playerFeatureSelections.updatedAt,
deletedAt: playerFeatureSelections.deletedAt,
userName: users.name,
})
.from(playerFeatureSelections)
.leftJoin(users, eq(playerFeatureSelections.userId, users.id))
.where(
and(
eq(playerFeatureSelections.featureTypeId, featureTypeId),
isNull(playerFeatureSelections.deletedAt),
),
);
return rows;
}
async deleteSelection(id: number): Promise<boolean> {
const db = this.databaseService.getDatabase();
const [existing] = await db
.select()
.from(playerFeatureSelections)
.where(eq(playerFeatureSelections.id, id));
if (!existing) {
return false;
}
// Soft delete
await db
.update(playerFeatureSelections)
.set({ deletedAt: new Date() })
.where(eq(playerFeatureSelections.id, id));
return true;
}
} }
...@@ -34,12 +34,53 @@ export class PlayersController { ...@@ -34,12 +34,53 @@ export class PlayersController {
constructor(private readonly playersService: PlayersService) {} constructor(private readonly playersService: PlayersService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create or update player by wyId' }) @ApiOperation({
summary: 'Create or update player by wyId',
description:
'Creates a new player or updates an existing one identified by Wyscout ID',
})
@ApiBody({ @ApiBody({
description: 'Player payload', description: 'Player payload',
type: CreatePlayerDto, type: CreatePlayerDto,
examples: {
example1: {
summary: 'Create player',
value: {
wyId: 1,
firstName: 'Lionel',
lastName: 'Messi',
shortName: 'L. Messi',
birthDate: '1987-06-24',
height: 170,
weight: 72,
foot: 'left',
gender: 'male',
status: 'active',
},
},
},
})
@ApiOkResponse({
description: 'Player created or updated successfully',
schema: {
example: {
id: 1,
wyId: 1,
firstName: 'Lionel',
lastName: 'Messi',
shortName: 'L. Messi',
birthDate: '1987-06-24',
height: 170,
weight: 72,
foot: 'left',
gender: 'male',
status: 'active',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T10:30:00Z',
deletedAt: null,
},
},
}) })
@ApiOkResponse({ description: 'Upserted player' })
async save(@Body() body: CreatePlayerDto): Promise<Player> { async save(@Body() body: CreatePlayerDto): Promise<Player> {
return this.playersService.upsertByWyId(body as any); return this.playersService.upsertByWyId(body as any);
} }
...@@ -246,7 +287,11 @@ export class PlayersController { ...@@ -246,7 +287,11 @@ export class PlayersController {
} }
@Patch(':wyId') @Patch(':wyId')
@ApiOperation({ summary: 'Update player by wyId' }) @ApiOperation({
summary: 'Update player by wyId',
description:
'Updates an existing player. Only provided fields will be updated.',
})
@ApiParam({ @ApiParam({
name: 'wyId', name: 'wyId',
type: Number, type: Number,
...@@ -255,8 +300,43 @@ export class PlayersController { ...@@ -255,8 +300,43 @@ export class PlayersController {
@ApiBody({ @ApiBody({
description: 'Player update payload. All fields are optional.', description: 'Player update payload. All fields are optional.',
type: UpdatePlayerDto, type: UpdatePlayerDto,
examples: {
example1: {
summary: 'Update player status',
value: {
status: 'inactive',
},
},
example2: {
summary: 'Update player physical attributes',
value: {
height: 172,
weight: 75,
},
},
},
})
@ApiOkResponse({
description: 'Player updated successfully',
schema: {
example: {
id: 1,
wyId: 1,
firstName: 'Lionel',
lastName: 'Messi',
shortName: 'L. Messi',
birthDate: '1987-06-24',
height: 172,
weight: 75,
foot: 'left',
gender: 'male',
status: 'inactive',
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T11:00:00Z',
deletedAt: null,
},
},
}) })
@ApiOkResponse({ description: 'Updated player', type: Object })
@ApiNotFoundResponse({ description: 'Player not found' }) @ApiNotFoundResponse({ description: 'Player not found' })
async updateByWyId( async updateByWyId(
@Param('wyId', ParseIntPipe) wyId: number, @Param('wyId', ParseIntPipe) wyId: number,
......
...@@ -788,6 +788,9 @@ export class PlayersService { ...@@ -788,6 +788,9 @@ export class PlayersService {
currentNationalTeamId: players.currentNationalTeamId, currentNationalTeamId: players.currentNationalTeamId,
currentTeamName: players.currentTeamName, currentTeamName: players.currentTeamName,
currentTeamOfficialName: players.currentTeamOfficialName, currentTeamOfficialName: players.currentTeamOfficialName,
currentNationalTeamName: players.currentNationalTeamName,
currentNationalTeamOfficialName:
players.currentNationalTeamOfficialName,
gender: players.gender, gender: players.gender,
status: players.status, status: players.status,
jerseyNumber: players.jerseyNumber, jerseyNumber: players.jerseyNumber,
...@@ -824,9 +827,10 @@ export class PlayersService { ...@@ -824,9 +827,10 @@ export class PlayersService {
contractEndsAt: players.contractEndsAt, contractEndsAt: players.contractEndsAt,
feasible: players.feasible, feasible: players.feasible,
morphology: players.morphology, morphology: players.morphology,
birthAreaWyId: players.birthAreaWyId,
// Birth area fields // Birth area fields
birthAreaId: areas.id, birthAreaId: areas.id,
birthAreaWyId: areas.wyId, birthAreaWyIdFromArea: areas.wyId,
birthAreaName: areas.name, birthAreaName: areas.name,
birthAreaAlpha2code: areas.alpha2code, birthAreaAlpha2code: areas.alpha2code,
birthAreaAlpha3code: areas.alpha3code, birthAreaAlpha3code: areas.alpha3code,
...@@ -986,7 +990,7 @@ export class PlayersService { ...@@ -986,7 +990,7 @@ export class PlayersService {
const birthArea = player.birthAreaId const birthArea = player.birthAreaId
? { ? {
id: player.birthAreaId, id: player.birthAreaId,
wyId: player.birthAreaWyId, wyId: player.birthAreaWyIdFromArea,
name: player.birthAreaName, name: player.birthAreaName,
alpha2code: player.birthAreaAlpha2code, alpha2code: player.birthAreaAlpha2code,
alpha3code: player.birthAreaAlpha3code, alpha3code: player.birthAreaAlpha3code,
...@@ -1470,12 +1474,13 @@ export class PlayersService { ...@@ -1470,12 +1474,13 @@ export class PlayersService {
contractEndsAt: players.contractEndsAt, contractEndsAt: players.contractEndsAt,
feasible: players.feasible, feasible: players.feasible,
morphology: players.morphology, morphology: players.morphology,
birthAreaWyId: players.birthAreaWyId,
secondBirthAreaWyId: players.secondBirthAreaWyId, secondBirthAreaWyId: players.secondBirthAreaWyId,
passportAreaWyId: players.passportAreaWyId, passportAreaWyId: players.passportAreaWyId,
secondPassportAreaWyId: players.secondPassportAreaWyId, secondPassportAreaWyId: players.secondPassportAreaWyId,
// Birth area fields // Birth area fields
birthAreaId: areas.id, birthAreaId: areas.id,
birthAreaWyId: areas.wyId, birthAreaWyIdFromArea: areas.wyId,
birthAreaName: areas.name, birthAreaName: areas.name,
birthAreaAlpha2code: areas.alpha2code, birthAreaAlpha2code: areas.alpha2code,
birthAreaAlpha3code: areas.alpha3code, birthAreaAlpha3code: areas.alpha3code,
...@@ -1553,7 +1558,7 @@ export class PlayersService { ...@@ -1553,7 +1558,7 @@ export class PlayersService {
const birthArea = player.birthAreaId const birthArea = player.birthAreaId
? { ? {
id: player.birthAreaId, id: player.birthAreaId,
wyId: player.birthAreaWyId, wyId: player.birthAreaWyIdFromArea,
name: player.birthAreaName, name: player.birthAreaName,
alpha2code: player.birthAreaAlpha2code, alpha2code: player.birthAreaAlpha2code,
alpha3code: player.birthAreaAlpha3code, alpha3code: player.birthAreaAlpha3code,
...@@ -1757,12 +1762,13 @@ export class PlayersService { ...@@ -1757,12 +1762,13 @@ export class PlayersService {
contractEndsAt: players.contractEndsAt, contractEndsAt: players.contractEndsAt,
feasible: players.feasible, feasible: players.feasible,
morphology: players.morphology, morphology: players.morphology,
birthAreaWyId: players.birthAreaWyId,
secondBirthAreaWyId: players.secondBirthAreaWyId, secondBirthAreaWyId: players.secondBirthAreaWyId,
passportAreaWyId: players.passportAreaWyId, passportAreaWyId: players.passportAreaWyId,
secondPassportAreaWyId: players.secondPassportAreaWyId, secondPassportAreaWyId: players.secondPassportAreaWyId,
// Birth area fields // Birth area fields
birthAreaId: areas.id, birthAreaId: areas.id,
birthAreaWyId: areas.wyId, birthAreaWyIdFromArea: areas.wyId,
birthAreaName: areas.name, birthAreaName: areas.name,
birthAreaAlpha2code: areas.alpha2code, birthAreaAlpha2code: areas.alpha2code,
birthAreaAlpha3code: areas.alpha3code, birthAreaAlpha3code: areas.alpha3code,
......
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt } from 'class-validator';
export class CreateDescriptionDto {
@ApiPropertyOptional({
description: 'Player ID',
example: 1,
type: Number,
})
@IsOptional()
@IsInt()
playerId?: number;
@ApiPropertyOptional({
description: 'Coach ID',
example: 1,
type: Number,
})
@IsOptional()
@IsInt()
coachId?: number;
@ApiProperty({
description: 'Profile description text',
example: 'Talented midfielder with excellent passing skills',
type: String,
})
@IsString()
description: string;
}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, IsBoolean, IsUrl } from 'class-validator';
export class CreateLinkDto {
@ApiPropertyOptional({
description: 'Player ID',
example: 1,
type: Number,
})
@IsOptional()
@IsInt()
playerId?: number;
@ApiPropertyOptional({
description: 'Coach ID',
example: 1,
type: Number,
})
@IsOptional()
@IsInt()
coachId?: number;
@ApiProperty({
description: 'Link title (e.g., "YouTube", "Instagram", "Portfolio")',
example: 'YouTube',
type: String,
})
@IsString()
title: string;
@ApiProperty({
description: 'Link URL',
example: 'https://youtube.com/user/example',
type: String,
})
@IsString()
@IsUrl()
url: string;
@ApiPropertyOptional({
description: 'Display order',
example: 0,
type: Number,
default: 0,
})
@IsOptional()
@IsInt()
order?: number;
@ApiPropertyOptional({
description: 'Whether the link is active',
example: true,
type: Boolean,
default: true,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional } from 'class-validator';
export class UpdateDescriptionDto {
@ApiPropertyOptional({
description: 'Profile description text',
example: 'Talented midfielder with excellent passing skills',
type: String,
})
@IsOptional()
@IsString()
description?: string;
}
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, IsBoolean, IsUrl } from 'class-validator';
export class UpdateLinkDto {
@ApiPropertyOptional({
description: 'Link title',
example: 'YouTube',
type: String,
})
@IsOptional()
@IsString()
title?: string;
@ApiPropertyOptional({
description: 'Link URL',
example: 'https://youtube.com/user/example',
type: String,
})
@IsOptional()
@IsString()
@IsUrl()
url?: string;
@ApiPropertyOptional({
description: 'Display order',
example: 0,
type: Number,
})
@IsOptional()
@IsInt()
order?: number;
@ApiPropertyOptional({
description: 'Whether the link is active',
example: true,
type: Boolean,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
import {
Body,
Controller,
Delete,
Get,
NotFoundException,
Param,
ParseIntPipe,
Patch,
Post,
} from '@nestjs/common';
import {
ApiBody,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
ApiTags,
} from '@nestjs/swagger';
import { ProfilesService } from './profiles.service';
import {
type ProfileDescription,
type ProfileLink,
} from '../../database/schema';
import { CreateDescriptionDto } from './dto/create-description.dto';
import { UpdateDescriptionDto } from './dto/update-description.dto';
import { CreateLinkDto } from './dto/create-link.dto';
import { UpdateLinkDto } from './dto/update-link.dto';
@ApiTags('Profiles')
@Controller('profiles')
export class ProfilesController {
constructor(private readonly profilesService: ProfilesService) {}
// ============================================================================
// DESCRIPTIONS
// ============================================================================
@Post('descriptions')
@ApiOperation({ summary: 'Create profile description' })
@ApiBody({ type: CreateDescriptionDto })
@ApiOkResponse({ description: 'Created profile description' })
async createDescription(
@Body() body: CreateDescriptionDto,
): Promise<ProfileDescription> {
return this.profilesService.createDescription(body as any);
}
@Patch('descriptions/:id')
@ApiOperation({ summary: 'Update profile description by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiBody({ type: UpdateDescriptionDto })
@ApiOkResponse({ description: 'Updated profile description' })
@ApiNotFoundResponse({ description: 'Description not found' })
async updateDescription(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateDescriptionDto,
): Promise<ProfileDescription> {
return this.profilesService.updateDescription(id, body as any);
}
@Get('descriptions/:id')
@ApiOperation({ summary: 'Get profile description by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiOkResponse({ description: 'Profile description if found' })
async getDescriptionById(
@Param('id', ParseIntPipe) id: number,
): Promise<ProfileDescription | null> {
return this.profilesService.getDescriptionById(id);
}
@Get('descriptions/player/:playerId')
@ApiOperation({ summary: 'Get profile description for a player' })
@ApiParam({ name: 'playerId', type: Number })
@ApiOkResponse({ description: 'Player profile description if found' })
async getDescriptionByPlayerId(
@Param('playerId', ParseIntPipe) playerId: number,
): Promise<ProfileDescription | null> {
return this.profilesService.getDescriptionByPlayerId(playerId);
}
@Get('descriptions/coach/:coachId')
@ApiOperation({ summary: 'Get profile description for a coach' })
@ApiParam({ name: 'coachId', type: Number })
@ApiOkResponse({ description: 'Coach profile description if found' })
async getDescriptionByCoachId(
@Param('coachId', ParseIntPipe) coachId: number,
): Promise<ProfileDescription | null> {
return this.profilesService.getDescriptionByCoachId(coachId);
}
@Delete('descriptions/:id')
@ApiOperation({ summary: 'Delete profile description by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiNoContentResponse({ description: 'Description deleted successfully' })
@ApiNotFoundResponse({ description: 'Description not found' })
async deleteDescription(
@Param('id', ParseIntPipe) id: number,
): Promise<void> {
const result = await this.profilesService.deleteDescription(id);
if (!result) {
throw new NotFoundException(`Description with ID ${id} not found`);
}
}
// ============================================================================
// LINKS
// ============================================================================
@Post('links')
@ApiOperation({ summary: 'Create profile link' })
@ApiBody({ type: CreateLinkDto })
@ApiOkResponse({ description: 'Created profile link' })
async createLink(@Body() body: CreateLinkDto): Promise<ProfileLink> {
return this.profilesService.createLink(body as any);
}
@Patch('links/:id')
@ApiOperation({ summary: 'Update profile link by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiBody({ type: UpdateLinkDto })
@ApiOkResponse({ description: 'Updated profile link' })
@ApiNotFoundResponse({ description: 'Link not found' })
async updateLink(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateLinkDto,
): Promise<ProfileLink> {
return this.profilesService.updateLink(id, body as any);
}
@Get('links/:id')
@ApiOperation({ summary: 'Get profile link by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiOkResponse({ description: 'Profile link if found' })
async getLinkById(
@Param('id', ParseIntPipe) id: number,
): Promise<ProfileLink | null> {
return this.profilesService.getLinkById(id);
}
@Get('links/player/:playerId')
@ApiOperation({ summary: 'Get all profile links for a player' })
@ApiParam({ name: 'playerId', type: Number })
@ApiOkResponse({ description: 'List of player profile links' })
async getLinksByPlayerId(
@Param('playerId', ParseIntPipe) playerId: number,
): Promise<ProfileLink[]> {
return this.profilesService.getLinksByPlayerId(playerId);
}
@Get('links/coach/:coachId')
@ApiOperation({ summary: 'Get all profile links for a coach' })
@ApiParam({ name: 'coachId', type: Number })
@ApiOkResponse({ description: 'List of coach profile links' })
async getLinksByCoachId(
@Param('coachId', ParseIntPipe) coachId: number,
): Promise<ProfileLink[]> {
return this.profilesService.getLinksByCoachId(coachId);
}
@Delete('links/:id')
@ApiOperation({ summary: 'Delete profile link by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiNoContentResponse({ description: 'Link deleted successfully' })
@ApiNotFoundResponse({ description: 'Link not found' })
async deleteLink(@Param('id', ParseIntPipe) id: number): Promise<void> {
const result = await this.profilesService.deleteLink(id);
if (!result) {
throw new NotFoundException(`Link with ID ${id} not found`);
}
}
}
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../../database/database.module';
import { ProfilesService } from './profiles.service';
import { ProfilesController } from './profiles.controller';
@Module({
imports: [DatabaseModule],
controllers: [ProfilesController],
providers: [ProfilesService],
exports: [ProfilesService],
})
export class ProfilesModule {}
import {
Injectable,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import { DatabaseService } from '../../database/database.service';
import {
profileDescriptions,
profileLinks,
type NewProfileDescription,
type NewProfileLink,
type ProfileDescription,
type ProfileLink,
} from '../../database/schema';
import { eq, and, isNull } from 'drizzle-orm';
@Injectable()
export class ProfilesService {
constructor(private readonly databaseService: DatabaseService) {}
// ============================================================================
// PROFILE DESCRIPTIONS
// ============================================================================
async createDescription(
data: Partial<NewProfileDescription>,
): Promise<ProfileDescription> {
const db = this.databaseService.getDatabase();
// Validate that either playerId or coachId is provided
if (!data.playerId && !data.coachId) {
throw new BadRequestException(
'Either playerId or coachId must be provided',
);
}
// Validate that description is provided
if (!data.description) {
throw new BadRequestException('Description is required');
}
const [row] = await db
.insert(profileDescriptions)
.values({
playerId: data.playerId ?? null,
coachId: data.coachId ?? null,
description: data.description,
} as NewProfileDescription)
.returning();
return row as ProfileDescription;
}
async updateDescription(
id: number,
data: Partial<NewProfileDescription>,
): Promise<ProfileDescription> {
const db = this.databaseService.getDatabase();
const existing = await db
.select()
.from(profileDescriptions)
.where(eq(profileDescriptions.id, id))
.limit(1);
if (!existing || existing.length === 0) {
throw new NotFoundException(`Description with ID ${id} not found`);
}
const [row] = await db
.update(profileDescriptions)
.set({
description: data.description,
updatedAt: new Date(),
})
.where(eq(profileDescriptions.id, id))
.returning();
return row as ProfileDescription;
}
async getDescriptionById(id: number): Promise<ProfileDescription | null> {
const db = this.databaseService.getDatabase();
const rows = await db
.select()
.from(profileDescriptions)
.where(
and(
eq(profileDescriptions.id, id),
isNull(profileDescriptions.deletedAt),
),
)
.limit(1);
return rows.length > 0 ? (rows[0] as ProfileDescription) : null;
}
async getDescriptionByPlayerId(
playerId: number,
): Promise<ProfileDescription | null> {
const db = this.databaseService.getDatabase();
const rows = await db
.select()
.from(profileDescriptions)
.where(
and(
eq(profileDescriptions.playerId, playerId),
isNull(profileDescriptions.deletedAt),
),
)
.limit(1);
return rows.length > 0 ? (rows[0] as ProfileDescription) : null;
}
async getDescriptionByCoachId(
coachId: number,
): Promise<ProfileDescription | null> {
const db = this.databaseService.getDatabase();
const rows = await db
.select()
.from(profileDescriptions)
.where(
and(
eq(profileDescriptions.coachId, coachId),
isNull(profileDescriptions.deletedAt),
),
)
.limit(1);
return rows.length > 0 ? (rows[0] as ProfileDescription) : null;
}
async deleteDescription(id: number): Promise<boolean> {
const db = this.databaseService.getDatabase();
const result = await db
.update(profileDescriptions)
.set({ deletedAt: new Date() })
.where(eq(profileDescriptions.id, id))
.returning();
return result.length > 0;
}
// ============================================================================
// PROFILE LINKS
// ============================================================================
async createLink(data: Partial<NewProfileLink>): Promise<ProfileLink> {
const db = this.databaseService.getDatabase();
// Validate that either playerId or coachId is provided
if (!data.playerId && !data.coachId) {
throw new BadRequestException(
'Either playerId or coachId must be provided',
);
}
// Validate required fields
if (!data.title) {
throw new BadRequestException('Title is required');
}
if (!data.url) {
throw new BadRequestException('URL is required');
}
const [row] = await db
.insert(profileLinks)
.values({
playerId: data.playerId ?? null,
coachId: data.coachId ?? null,
title: data.title,
url: data.url,
order: data.order ?? 0,
isActive: data.isActive ?? true,
} as NewProfileLink)
.returning();
return row as ProfileLink;
}
async updateLink(
id: number,
data: Partial<NewProfileLink>,
): Promise<ProfileLink> {
const db = this.databaseService.getDatabase();
const existing = await db
.select()
.from(profileLinks)
.where(eq(profileLinks.id, id))
.limit(1);
if (!existing || existing.length === 0) {
throw new NotFoundException(`Link with ID ${id} not found`);
}
const updateData: any = { updatedAt: new Date() };
if (data.title !== undefined) updateData.title = data.title;
if (data.url !== undefined) updateData.url = data.url;
if (data.order !== undefined) updateData.order = data.order;
if (data.isActive !== undefined) updateData.isActive = data.isActive;
const [row] = await db
.update(profileLinks)
.set(updateData)
.where(eq(profileLinks.id, id))
.returning();
return row as ProfileLink;
}
async getLinkById(id: number): Promise<ProfileLink | null> {
const db = this.databaseService.getDatabase();
const rows = await db
.select()
.from(profileLinks)
.where(and(eq(profileLinks.id, id), isNull(profileLinks.deletedAt)))
.limit(1);
return rows.length > 0 ? (rows[0] as ProfileLink) : null;
}
async getLinksByPlayerId(playerId: number): Promise<ProfileLink[]> {
const db = this.databaseService.getDatabase();
return db
.select()
.from(profileLinks)
.where(
and(
eq(profileLinks.playerId, playerId),
isNull(profileLinks.deletedAt),
),
)
.orderBy(profileLinks.order);
}
async getLinksByCoachId(coachId: number): Promise<ProfileLink[]> {
const db = this.databaseService.getDatabase();
return db
.select()
.from(profileLinks)
.where(
and(eq(profileLinks.coachId, coachId), isNull(profileLinks.deletedAt)),
)
.orderBy(profileLinks.order);
}
async deleteLink(id: number): Promise<boolean> {
const db = this.databaseService.getDatabase();
const result = await db
.update(profileLinks)
.set({ deletedAt: new Date() })
.where(eq(profileLinks.id, id))
.returning();
return result.length > 0;
}
}
...@@ -39,7 +39,8 @@ export class UsersController { ...@@ -39,7 +39,8 @@ export class UsersController {
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: 'Get all users', summary: 'Get all users',
description: 'Retrieves a list of all users in the system with optional filtering by role, name, email, and active status.', description:
'Retrieves a list of all users in the system with optional filtering by role, name, email, and active status.',
}) })
@ApiQuery({ @ApiQuery({
name: 'role', name: 'role',
...@@ -68,7 +69,15 @@ export class UsersController { ...@@ -68,7 +69,15 @@ export class UsersController {
@ApiQuery({ @ApiQuery({
name: 'sortBy', name: 'sortBy',
required: false, required: false,
enum: ['name', 'email', 'role', 'isActive', 'createdAt', 'updatedAt', 'lastLoginAt'], enum: [
'name',
'email',
'role',
'isActive',
'createdAt',
'updatedAt',
'lastLoginAt',
],
description: 'Field to sort by (default: name)', description: 'Field to sort by (default: name)',
}) })
@ApiQuery({ @ApiQuery({
...@@ -120,18 +129,46 @@ export class UsersController { ...@@ -120,18 +129,46 @@ export class UsersController {
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ @ApiOperation({
summary: 'Update user', summary: 'Update user',
description: 'Updates user information.', description:
'Updates user information. Only provided fields will be updated.',
}) })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
description: 'User ID', description: 'User ID',
example: 1, example: 1,
}) })
@ApiBody({ type: UpdateUserDto }) @ApiBody({
type: UpdateUserDto,
examples: {
example1: {
summary: 'Update user role',
value: {
role: 'admin',
},
},
example2: {
summary: 'Update user email',
value: {
email: 'newemail@example.com',
},
},
},
})
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'User updated successfully', description: 'User updated successfully',
type: UserResponseDto, type: UserResponseDto,
schema: {
example: {
id: 1,
name: 'John Doe',
email: 'newemail@example.com',
role: 'admin',
isActive: true,
createdAt: '2025-01-15T10:30:00Z',
updatedAt: '2025-01-15T11:00:00Z',
},
},
}) })
@ApiResponse({ @ApiResponse({
status: 404, status: 404,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment