Commit 1909772c by Augusto

Player-Features and Agents

parent 78e29712
ALTER TABLE "players"
ADD COLUMN IF NOT EXISTS "eu_password" boolean DEFAULT false;
CREATE TYPE "public"."audit_log_action" AS ENUM('create', 'update', 'delete', 'soft_delete', 'restore');--> statement-breakpoint
CREATE TYPE "public"."position_category" AS ENUM('Forward', 'Goalkeeper', 'Defender', 'Midfield');--> statement-breakpoint
CREATE TABLE "agents" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"description" text,
"type" text NOT NULL,
"email" text NOT NULL,
"phone" text NOT NULL,
"status" text NOT NULL,
"address" text NOT NULL,
"country" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "audit_logs" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" integer,
"action" "audit_log_action" NOT NULL,
"entity_type" text NOT NULL,
"entity_id" integer,
"entity_wy_id" integer,
"old_values" json,
"new_values" json,
"changes" json,
"ip_address" varchar(45),
"user_agent" text,
"metadata" json,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "coach_agents" (
"id" serial PRIMARY KEY NOT NULL,
"coach_id" integer NOT NULL,
"agent_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "player_agents" (
"id" serial PRIMARY KEY NOT NULL,
"player_id" integer NOT NULL,
"agent_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "positions" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(100) NOT NULL,
"code2" varchar(10),
"code3" varchar(10),
"order" integer DEFAULT 0,
"location_x" integer,
"location_y" integer,
"bg_color" varchar(7),
"text_color" varchar(7),
"category" "position_category" NOT NULL,
"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
DROP INDEX "idx_players_position";--> statement-breakpoint
ALTER TABLE "players" ALTER COLUMN "wy_id" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "current_national_team_name" varchar(255);--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "current_national_team_official_name" varchar(255);--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "position_id" integer;--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "other_position_ids" integer[];--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "second_birth_area_wy_id" integer;--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "second_passport_area_wy_id" integer;--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "agent_id" integer;--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "contract_ends_at" date;--> statement-breakpoint
ALTER TABLE "players" ADD COLUMN "eu_password" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "coach_agents" ADD CONSTRAINT "coach_agents_coach_id_coaches_id_fk" FOREIGN KEY ("coach_id") REFERENCES "public"."coaches"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "coach_agents" ADD CONSTRAINT "coach_agents_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player_agents" ADD CONSTRAINT "player_agents_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player_agents" ADD CONSTRAINT "player_agents_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_audit_logs_user_id" ON "audit_logs" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_action" ON "audit_logs" USING btree ("action");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_entity_type" ON "audit_logs" USING btree ("entity_type");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_entity_id" ON "audit_logs" USING btree ("entity_id");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_entity_wy_id" ON "audit_logs" USING btree ("entity_wy_id");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_created_at" ON "audit_logs" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_entity_type_id" ON "audit_logs" USING btree ("entity_type","entity_id");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_entity_type_wy_id" ON "audit_logs" USING btree ("entity_type","entity_wy_id");--> statement-breakpoint
CREATE INDEX "idx_audit_logs_user_created_at" ON "audit_logs" USING btree ("user_id","created_at");--> statement-breakpoint
CREATE UNIQUE INDEX "coach_agents_coach_id_agent_id_idx" ON "coach_agents" USING btree ("coach_id","agent_id");--> statement-breakpoint
CREATE UNIQUE INDEX "player_agents_player_id_agent_id_idx" ON "player_agents" USING btree ("player_id","agent_id");--> statement-breakpoint
CREATE UNIQUE INDEX "positions_code2_unique" ON "positions" USING btree ("code2");--> statement-breakpoint
CREATE UNIQUE INDEX "positions_code3_unique" ON "positions" USING btree ("code3");--> statement-breakpoint
CREATE INDEX "positions_name_idx" ON "positions" USING btree ("name");--> statement-breakpoint
CREATE INDEX "positions_category_idx" ON "positions" USING btree ("category");--> statement-breakpoint
CREATE INDEX "positions_is_active_idx" ON "positions" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "positions_category_active_idx" ON "positions" USING btree ("category","is_active");--> statement-breakpoint
CREATE INDEX "positions_deleted_at_idx" ON "positions" USING btree ("deleted_at");--> statement-breakpoint
ALTER TABLE "players" ADD CONSTRAINT "players_position_id_positions_id_fk" FOREIGN KEY ("position_id") REFERENCES "public"."positions"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "players" ADD CONSTRAINT "players_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "idx_players_position_id" ON "players" USING btree ("position_id");--> statement-breakpoint
ALTER TABLE "players" DROP COLUMN "position";--> statement-breakpoint
ALTER TABLE "players" DROP COLUMN "agent";
\ No newline at end of file
ALTER TABLE "players"
ALTER COLUMN "wy_id" DROP NOT NULL;
CREATE TABLE "player_feature_categories" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(100) NOT NULL,
"description" text,
"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,
CONSTRAINT "player_feature_categories_name_unique" UNIQUE("name")
);
--> statement-breakpoint
CREATE TABLE "player_feature_ratings" (
"id" serial PRIMARY KEY NOT NULL,
"player_id" integer NOT NULL,
"feature_type_id" integer NOT NULL,
"user_id" integer NOT NULL,
"rating" integer NOT NULL,
"notes" text,
"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 "player_feature_types" (
"id" serial PRIMARY KEY NOT NULL,
"category_id" integer NOT NULL,
"name" varchar(100) NOT NULL,
"description" text,
"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 "player_feature_ratings" ADD CONSTRAINT "player_feature_ratings_player_id_players_id_fk" FOREIGN KEY ("player_id") REFERENCES "public"."players"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player_feature_ratings" ADD CONSTRAINT "player_feature_ratings_feature_type_id_player_feature_types_id_fk" FOREIGN KEY ("feature_type_id") REFERENCES "public"."player_feature_types"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player_feature_ratings" ADD CONSTRAINT "player_feature_ratings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "player_feature_types" ADD CONSTRAINT "player_feature_types_category_id_player_feature_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."player_feature_categories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "player_feature_categories_name_idx" ON "player_feature_categories" USING btree ("name");--> statement-breakpoint
CREATE INDEX "player_feature_categories_order_idx" ON "player_feature_categories" USING btree ("order");--> statement-breakpoint
CREATE INDEX "player_feature_categories_is_active_idx" ON "player_feature_categories" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "player_feature_categories_deleted_at_idx" ON "player_feature_categories" USING btree ("deleted_at");--> statement-breakpoint
CREATE INDEX "player_feature_ratings_player_id_idx" ON "player_feature_ratings" USING btree ("player_id");--> statement-breakpoint
CREATE INDEX "player_feature_ratings_feature_type_id_idx" ON "player_feature_ratings" USING btree ("feature_type_id");--> statement-breakpoint
CREATE INDEX "player_feature_ratings_user_id_idx" ON "player_feature_ratings" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "player_feature_ratings_rating_idx" ON "player_feature_ratings" USING btree ("rating");--> statement-breakpoint
CREATE INDEX "player_feature_ratings_deleted_at_idx" ON "player_feature_ratings" USING btree ("deleted_at");--> statement-breakpoint
CREATE UNIQUE INDEX "player_feature_ratings_player_feature_user_idx" ON "player_feature_ratings" USING btree ("player_id","feature_type_id","user_id");--> statement-breakpoint
CREATE INDEX "player_feature_ratings_player_feature_idx" ON "player_feature_ratings" USING btree ("player_id","feature_type_id");--> statement-breakpoint
CREATE INDEX "player_feature_ratings_player_user_idx" ON "player_feature_ratings" USING btree ("player_id","user_id");--> statement-breakpoint
CREATE INDEX "player_feature_types_category_id_idx" ON "player_feature_types" USING btree ("category_id");--> statement-breakpoint
CREATE INDEX "player_feature_types_name_idx" ON "player_feature_types" USING btree ("name");--> statement-breakpoint
CREATE INDEX "player_feature_types_order_idx" ON "player_feature_types" USING btree ("order");--> statement-breakpoint
CREATE INDEX "player_feature_types_is_active_idx" ON "player_feature_types" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "player_feature_types_deleted_at_idx" ON "player_feature_types" USING btree ("deleted_at");--> statement-breakpoint
CREATE INDEX "player_feature_types_category_order_idx" ON "player_feature_types" USING btree ("category_id","order");
\ No newline at end of file
ALTER TABLE "player_feature_ratings" ALTER COLUMN "rating" DROP NOT NULL;
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -71,6 +71,27 @@
"when": 1763057316693,
"tag": "0009_tough_greymalkin",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1763553196783,
"tag": "0010_marvelous_loners",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1763556954199,
"tag": "0011_right_tempest",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1763557856440,
"tag": "0012_freezing_jean_grey",
"breakpoints": true
}
]
}
\ No newline at end of file
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DatabaseModule } from './database/database.module';
......@@ -18,6 +19,8 @@ import { AreasModule } from './modules/areas/areas.module';
import { ListsModule } from './modules/lists/lists.module';
import { PositionsModule } from './modules/positions/positions.module';
import { AuditLogsModule } from './modules/audit-logs/audit-logs.module';
import { AgentsModule } from './modules/agents/agents.module';
import { PlayerFeaturesModule } from './modules/player-features/player-features.module';
@Module({
imports: [
......@@ -26,6 +29,16 @@ import { AuditLogsModule } from './modules/audit-logs/audit-logs.module';
isGlobal: true,
envFilePath: '.env',
}),
// Register JwtModule globally to avoid multiple registrations
JwtModule.registerAsync({
global: true,
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
signOptions: { expiresIn: '24h' },
}),
inject: [ConfigService],
}),
DatabaseModule,
FeatureFlagModule,
UsersModule,
......@@ -42,6 +55,8 @@ import { AuditLogsModule } from './modules/audit-logs/audit-logs.module';
ListsModule,
PositionsModule,
AuditLogsModule,
AgentsModule,
PlayerFeaturesModule,
],
controllers: [AppController],
providers: [AppService],
......
......@@ -17,20 +17,13 @@ export class LoggingInterceptor implements NestInterceptor {
const { method, url, user } = request;
const now = Date.now();
this.logger.log(`→ ${method} ${url} | User: ${user?.email || 'Anonymous'}`);
return next.handle().pipe(
tap({
next: () => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const elapsed = Date.now() - now;
this.logger.log(` ${method} ${url} ${statusCode} | ${elapsed}ms`);
},
// Only log when there is an error; successful requests stay silent
error: (error) => {
const elapsed = Date.now() - now;
this.logger.error(
` ${method} ${url} ERROR | ${elapsed}ms`,
`← ${method} ${url} ERROR | ${elapsed}ms | User: ${user?.email || 'Anonymous'}`,
error.stack,
);
},
......
import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { json, urlencoded } from 'express';
import { json, urlencoded, static as expressStatic } from 'express';
import { join } from 'path';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
......@@ -40,6 +41,9 @@ async function bootstrap() {
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: true }));
// Serve uploaded files statically
app.use('/uploads', expressStatic(join(process.cwd(), 'uploads')));
// Set global prefix for all routes
app.setGlobalPrefix('api');
......
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 { AgentsService } from './agents.service';
import { type Agent } from '../../database/schema';
import { CreateAgentDto } from './dto/create-agent.dto';
import { UpdateAgentDto } from './dto/update-agent.dto';
@ApiTags('Agents')
@Controller('agents')
export class AgentsController {
constructor(private readonly agentsService: AgentsService) {}
@Post()
@ApiOperation({ summary: 'Create agent' })
@ApiBody({ type: CreateAgentDto })
@ApiOkResponse({ description: 'Created agent' })
async create(@Body() body: CreateAgentDto): Promise<Agent> {
return this.agentsService.create(body as any);
}
@Patch(':id')
@ApiOperation({ summary: 'Update agent by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiBody({ type: UpdateAgentDto })
@ApiOkResponse({ description: 'Updated agent' })
@ApiNotFoundResponse({ description: 'Agent not found' })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() body: UpdateAgentDto,
): Promise<Agent> {
return this.agentsService.update(id, body as any);
}
@Get(':id')
@ApiOperation({ summary: 'Get agent by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiOkResponse({ description: 'Agent if found' })
async getById(@Param('id', ParseIntPipe) id: number): Promise<Agent | null> {
return this.agentsService.findById(id);
}
@Get()
@ApiOperation({ summary: 'List all agents' })
@ApiOkResponse({ description: 'List of agents' })
async list(): Promise<Agent[]> {
return this.agentsService.findAll();
}
@Delete(':id')
@ApiOperation({ summary: 'Delete agent by ID' })
@ApiParam({ name: 'id', type: Number })
@ApiNoContentResponse({ description: 'Agent deleted successfully' })
@ApiNotFoundResponse({ description: 'Agent not found' })
async delete(@Param('id', ParseIntPipe) id: number): Promise<void> {
const result = await this.agentsService.delete(id);
if (!result) {
throw new NotFoundException(`Agent with ID ${id} not found`);
}
}
@Post(':agentId/players/:playerId')
@ApiOperation({ summary: 'Associate agent with player' })
@ApiParam({ name: 'agentId', type: Number })
@ApiParam({ name: 'playerId', type: Number })
@ApiOkResponse({ description: 'Association created or already exists' })
async associateWithPlayer(
@Param('agentId', ParseIntPipe) agentId: number,
@Param('playerId', ParseIntPipe) playerId: number,
) {
return this.agentsService.associateWithPlayer(agentId, playerId);
}
@Post(':agentId/coaches/:coachId')
@ApiOperation({ summary: 'Associate agent with coach' })
@ApiParam({ name: 'agentId', type: Number })
@ApiParam({ name: 'coachId', type: Number })
@ApiOkResponse({ description: 'Association created or already exists' })
async associateWithCoach(
@Param('agentId', ParseIntPipe) agentId: number,
@Param('coachId', ParseIntPipe) coachId: number,
) {
return this.agentsService.associateWithCoach(agentId, coachId);
}
@Delete(':agentId/players/:playerId')
@ApiOperation({ summary: 'Remove agent from player' })
@ApiParam({ name: 'agentId', type: Number })
@ApiParam({ name: 'playerId', type: Number })
@ApiNoContentResponse({ description: 'Association removed if it existed' })
async removeFromPlayer(
@Param('agentId', ParseIntPipe) agentId: number,
@Param('playerId', ParseIntPipe) playerId: number,
): Promise<void> {
await this.agentsService.removeFromPlayer(agentId, playerId);
}
@Delete(':agentId/coaches/:coachId')
@ApiOperation({ summary: 'Remove agent from coach' })
@ApiParam({ name: 'agentId', type: Number })
@ApiParam({ name: 'coachId', type: Number })
@ApiNoContentResponse({ description: 'Association removed if it existed' })
async removeFromCoach(
@Param('agentId', ParseIntPipe) agentId: number,
@Param('coachId', ParseIntPipe) coachId: number,
): Promise<void> {
await this.agentsService.removeFromCoach(agentId, coachId);
}
@Get('by-player/:playerId')
@ApiOperation({ summary: 'Get agents associated with a player' })
@ApiParam({ name: 'playerId', type: Number })
@ApiOkResponse({ description: 'List of agents for the player' })
async getByPlayer(
@Param('playerId', ParseIntPipe) playerId: number,
): Promise<Agent[]> {
return this.agentsService.getAgentsByPlayer(playerId);
}
@Get('by-coach/:coachId')
@ApiOperation({ summary: 'Get agents associated with a coach' })
@ApiParam({ name: 'coachId', type: Number })
@ApiOkResponse({ description: 'List of agents for the coach' })
async getByCoach(
@Param('coachId', ParseIntPipe) coachId: number,
): Promise<Agent[]> {
return this.agentsService.getAgentsByCoach(coachId);
}
}
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../../database/database.module';
import { AgentsController } from './agents.controller';
import { AgentsService } from './agents.service';
@Module({
imports: [DatabaseModule],
controllers: [AgentsController],
providers: [AgentsService],
exports: [AgentsService],
})
export class AgentsModule {}
import { Injectable, NotFoundException } from '@nestjs/common';
import { eq, and } from 'drizzle-orm';
import { DatabaseService } from '../../database/database.service';
import {
agents,
playerAgents,
coachAgents,
players,
coaches,
type Agent,
type NewAgent,
} from '../../database/schema';
@Injectable()
export class AgentsService {
constructor(private readonly databaseService: DatabaseService) {}
async create(data: Partial<NewAgent>): Promise<Agent> {
const db = this.databaseService.getDatabase();
const now = new Date();
const [row] = await db
.insert(agents)
.values({
name: data.name!,
type: data.type!,
email: data.email!,
phone: data.phone!,
status: data.status!,
address: data.address!,
country: data.country!,
description: data.description ?? null,
createdAt: now as any,
updatedAt: now as any,
} as NewAgent)
.returning();
return row as Agent;
}
async update(id: number, data: Partial<NewAgent>): Promise<Agent> {
const db = this.databaseService.getDatabase();
const [existing] = await db.select().from(agents).where(eq(agents.id, id));
if (!existing) {
throw new NotFoundException(`Agent with ID ${id} not found`);
}
const [row] = await db
.update(agents)
.set({
...data,
updatedAt: new Date() as any,
})
.where(eq(agents.id, id))
.returning();
return row as Agent;
}
async findById(id: number): Promise<Agent | null> {
const db = this.databaseService.getDatabase();
const [row] = await db.select().from(agents).where(eq(agents.id, id));
return (row as Agent) || null;
}
async findAll(): Promise<Agent[]> {
const db = this.databaseService.getDatabase();
const rows = await db.select().from(agents);
return rows as Agent[];
}
async delete(id: number): Promise<boolean> {
const db = this.databaseService.getDatabase();
const [existing] = await db.select().from(agents).where(eq(agents.id, id));
if (!existing) {
return false;
}
await db.delete(agents).where(eq(agents.id, id));
return true;
}
async associateWithPlayer(agentId: number, playerId: number) {
const db = this.databaseService.getDatabase();
const [[agent], [player]] = await Promise.all([
db.select().from(agents).where(eq(agents.id, agentId)).limit(1),
db.select().from(players).where(eq(players.id, playerId)).limit(1),
]);
if (!agent) {
throw new NotFoundException(`Agent with ID ${agentId} not found`);
}
if (!player) {
throw new NotFoundException(`Player with ID ${playerId} not found`);
}
const now = new Date();
const [row] = await db
.insert(playerAgents)
.values({
playerId,
agentId,
createdAt: now as any,
updatedAt: now as any,
})
.onConflictDoNothing()
.returning();
return row ?? null;
}
async associateWithCoach(agentId: number, coachId: number) {
const db = this.databaseService.getDatabase();
const [[agent], [coach]] = await Promise.all([
db.select().from(agents).where(eq(agents.id, agentId)).limit(1),
db.select().from(coaches).where(eq(coaches.id, coachId)).limit(1),
]);
if (!agent) {
throw new NotFoundException(`Agent with ID ${agentId} not found`);
}
if (!coach) {
throw new NotFoundException(`Coach with ID ${coachId} not found`);
}
const now = new Date();
const [row] = await db
.insert(coachAgents)
.values({
coachId,
agentId,
createdAt: now as any,
updatedAt: now as any,
})
.onConflictDoNothing()
.returning();
return row ?? null;
}
async removeFromPlayer(agentId: number, playerId: number): Promise<boolean> {
const db = this.databaseService.getDatabase();
await db
.delete(playerAgents)
.where(
and(
eq(playerAgents.agentId, agentId),
eq(playerAgents.playerId, playerId),
),
);
return true;
}
async removeFromCoach(agentId: number, coachId: number): Promise<boolean> {
const db = this.databaseService.getDatabase();
await db
.delete(coachAgents)
.where(
and(eq(coachAgents.agentId, agentId), eq(coachAgents.coachId, coachId)),
);
return true;
}
async getAgentsByPlayer(playerId: number): Promise<Agent[]> {
const db = this.databaseService.getDatabase();
const rows = await db
.select({ agent: agents })
.from(playerAgents)
.innerJoin(agents, eq(playerAgents.agentId, agents.id))
.where(eq(playerAgents.playerId, playerId));
return rows.map((r) => r.agent as Agent);
}
async getAgentsByCoach(coachId: number): Promise<Agent[]> {
const db = this.databaseService.getDatabase();
const rows = await db
.select({ agent: agents })
.from(coachAgents)
.innerJoin(agents, eq(coachAgents.agentId, agents.id))
.where(eq(coachAgents.coachId, coachId));
return rows.map((r) => r.agent as Agent);
}
}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateAgentDto {
@ApiProperty({ description: 'Agent name' })
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({ description: 'Agent type (e.g., individual, agency)' })
@IsString()
@IsNotEmpty()
type: string;
@ApiProperty({ description: 'Agent email' })
@IsEmail()
email: string;
@ApiProperty({ description: 'Agent phone' })
@IsString()
@IsNotEmpty()
phone: string;
@ApiProperty({ description: 'Status' })
@IsString()
@IsNotEmpty()
status: string;
@ApiProperty({ description: 'Address' })
@IsString()
@IsNotEmpty()
address: string;
@ApiProperty({ description: 'Country' })
@IsString()
@IsNotEmpty()
country: string;
@ApiPropertyOptional({ description: 'Description' })
@IsOptional()
@IsString()
description?: string;
}
export * from './create-agent.dto';
export * from './update-agent.dto';
import { PartialType } from '@nestjs/swagger';
import { CreateAgentDto } from './create-agent.dto';
export class UpdateAgentDto extends PartialType(CreateAgentDto) {}
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
......@@ -10,14 +8,7 @@ import { DatabaseModule } from '../../database/database.module';
imports: [
DatabaseModule,
UsersModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
signOptions: { expiresIn: '24h' },
}),
inject: [ConfigService],
}),
// JwtModule is now global, no need to import here
],
controllers: [AuthController],
providers: [AuthService],
......
......@@ -11,11 +11,14 @@ import {
Patch,
Post,
Query,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FilesService } from './files.service';
import { type File } from '../../database/schema';
import {
ApiBody,
ApiConsumes,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
......@@ -24,6 +27,7 @@ import {
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { CreateFileDto, UpdateFileDto, QueryFilesDto } from './dto';
@ApiTags('Files')
......@@ -43,6 +47,88 @@ export class FilesController {
return this.filesService.create(body);
}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
@ApiOperation({ summary: 'Upload a file and create a file record' })
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
},
entityType: {
type: 'string',
enum: ['player', 'coach', 'match', 'report', 'user'],
},
entityId: {
type: 'integer',
nullable: true,
},
entityWyId: {
type: 'integer',
nullable: true,
},
category: {
type: 'string',
nullable: true,
},
description: {
type: 'string',
nullable: true,
},
},
required: ['file', 'entityType'],
},
})
@ApiOkResponse({ description: 'Uploaded file record' })
async upload(
@UploadedFile()
file: {
originalname: string;
filename?: string;
path?: string;
mimetype: string;
size: number;
[key: string]: any;
},
@Body()
body: {
entityType: string;
entityId?: number | string;
entityWyId?: number | string;
category?: string;
description?: string;
},
): Promise<File> {
const data = {
originalFileName: file.originalname,
fileName: file.filename ?? file.originalname,
filePath: file.path ?? file.originalname,
mimeType: file.mimetype,
fileSize: file.size,
entityType: body.entityType,
entityId:
body.entityId !== undefined &&
body.entityId !== null &&
body.entityId !== ''
? Number(body.entityId)
: undefined,
entityWyId:
body.entityWyId !== undefined &&
body.entityWyId !== null &&
body.entityWyId !== ''
? Number(body.entityWyId)
: undefined,
category: body.category,
description: body.description,
};
return this.filesService.create(data);
}
@Get('by-entity/:entityType')
@ApiOperation({ summary: 'Get files by entity type and ID' })
@ApiParam({
......@@ -73,7 +159,11 @@ export class FilesController {
): Promise<File[]> {
const entityIdNum = entityId ? parseInt(entityId, 10) : undefined;
const entityWyIdNum = entityWyId ? parseInt(entityWyId, 10) : undefined;
return this.filesService.findByEntity(entityType, entityIdNum, entityWyIdNum);
return this.filesService.findByEntity(
entityType,
entityIdNum,
entityWyIdNum,
);
}
@Get('by-category/:category')
......@@ -85,9 +175,7 @@ export class FilesController {
example: 'profile_image',
})
@ApiOkResponse({ description: 'List of files in the category' })
async getByCategory(
@Param('category') category: string,
): Promise<File[]> {
async getByCategory(@Param('category') category: string): Promise<File[]> {
return this.filesService.findByCategory(category);
}
......@@ -117,7 +205,15 @@ export class FilesController {
@ApiQuery({
name: 'sortBy',
required: false,
enum: ['fileName', 'originalFileName', 'mimeType', 'fileSize', 'category', 'createdAt', 'updatedAt'],
enum: [
'fileName',
'originalFileName',
'mimeType',
'fileSize',
'category',
'createdAt',
'updatedAt',
],
description: 'Field to sort by (default: createdAt)',
})
@ApiQuery({
......@@ -174,4 +270,3 @@ export class FilesController {
}
}
}
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { join } from 'path';
import { FilesController } from './files.controller';
import { FilesService } from './files.service';
@Module({
imports: [
MulterModule.register({
storage: diskStorage({
destination: (_req, _file, cb) => {
const uploadPath = join(process.cwd(), 'uploads');
cb(null, uploadPath);
},
filename: (_req, file, cb) => {
const timestamp = Date.now();
const sanitizedOriginalName = file.originalname.replace(/\s+/g, '_');
cb(null, `${timestamp}-${sanitizedOriginalName}`);
},
}),
}),
],
controllers: [FilesController],
providers: [FilesService],
exports: [FilesService],
})
export class FilesModule {}
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ListsController } from './lists.controller';
import { ListsService } from './lists.service';
import { DatabaseModule } from '../../database/database.module';
......@@ -9,14 +7,7 @@ import { JwtSimpleGuard } from '../../common/guards/jwt-simple.guard';
@Module({
imports: [
DatabaseModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
signOptions: { expiresIn: '24h' },
}),
inject: [ConfigService],
}),
// JwtModule is now global, no need to import here
],
controllers: [ListsController],
providers: [ListsService, JwtSimpleGuard],
......
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';
export class CreateFeatureCategoryDto {
@ApiProperty({
description:
'Category name (e.g., "Físicas", "Técnicas", "Tácticas", "Mentales")',
example: 'Físicas',
type: String,
})
@IsString()
name: string;
@ApiPropertyOptional({
description: 'Category description',
example: 'Physical characteristics of the player',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
description?: string | null;
@ApiPropertyOptional({
description: 'Display order',
example: 0,
type: Number,
default: 0,
})
@IsOptional()
@IsInt()
order?: number;
@ApiPropertyOptional({
description: 'Whether the category is active',
example: true,
type: Boolean,
default: true,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsOptional, IsString, Min, Max } from 'class-validator';
export class CreateFeatureRatingDto {
@ApiProperty({
description: 'Player ID being rated',
example: 1,
type: Number,
})
@IsInt()
playerId: number;
@ApiProperty({
description: 'Feature type ID being rated',
example: 1,
type: Number,
})
@IsInt()
featureTypeId: number;
@ApiProperty({
description: 'Scout/User ID providing the rating',
example: 1,
type: Number,
})
@IsInt()
userId: number;
@ApiPropertyOptional({
description:
'Rating value (1-5 scale). Optional - clients can select features without rating',
example: 4,
type: Number,
minimum: 1,
maximum: 5,
nullable: true,
})
@IsOptional()
@IsInt()
@Min(1)
@Max(5)
rating?: number | null;
@ApiPropertyOptional({
description: 'Optional scout notes about the rating',
example: 'Player showed excellent agility during the match',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
notes?: string | null;
}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';
export class CreateFeatureTypeDto {
@ApiProperty({
description: 'Category ID this feature type belongs to',
example: 1,
type: Number,
})
@IsInt()
categoryId: number;
@ApiProperty({
description:
'Feature type name (e.g., "Agilidad", "Agresividad", "Cambio de Ritmo")',
example: 'Agilidad',
type: String,
})
@IsString()
name: string;
@ApiPropertyOptional({
description: 'Feature type description',
example: 'Player agility and speed of movement',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
description?: string | null;
@ApiPropertyOptional({
description: 'Display order within category',
example: 0,
type: Number,
default: 0,
})
@IsOptional()
@IsInt()
order?: number;
@ApiPropertyOptional({
description: 'Whether the feature type is active',
example: true,
type: Boolean,
default: true,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';
export class UpdateFeatureCategoryDto {
@ApiPropertyOptional({
description: 'Category name',
example: 'Físicas',
type: String,
})
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
description: 'Category description',
example: 'Physical characteristics of the player',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
description?: string | null;
@ApiPropertyOptional({
description: 'Display order',
example: 0,
type: Number,
})
@IsOptional()
@IsInt()
order?: number;
@ApiPropertyOptional({
description: 'Whether the category is active',
example: true,
type: Boolean,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsOptional, IsString, Min, Max } from 'class-validator';
export class UpdateFeatureRatingDto {
@ApiPropertyOptional({
description: 'Rating value (1-5 scale)',
example: 4,
type: Number,
minimum: 1,
maximum: 5,
})
@IsOptional()
@IsInt()
@Min(1)
@Max(5)
rating?: number;
@ApiPropertyOptional({
description: 'Optional scout notes about the rating',
example: 'Player showed excellent agility during the match',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
notes?: string | null;
}
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, IsBoolean } from 'class-validator';
export class UpdateFeatureTypeDto {
@ApiPropertyOptional({
description: 'Category ID this feature type belongs to',
example: 1,
type: Number,
})
@IsOptional()
@IsInt()
categoryId?: number;
@ApiPropertyOptional({
description: 'Feature type name',
example: 'Agilidad',
type: String,
})
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
description: 'Feature type description',
example: 'Player agility and speed of movement',
type: String,
nullable: true,
})
@IsOptional()
@IsString()
description?: string | null;
@ApiPropertyOptional({
description: 'Display order within category',
example: 0,
type: Number,
})
@IsOptional()
@IsInt()
order?: number;
@ApiPropertyOptional({
description: 'Whether the feature type is active',
example: true,
type: Boolean,
})
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../../database/database.module';
import { PlayerFeaturesController } from './player-features.controller';
import { PlayerFeaturesService } from './player-features.service';
@Module({
imports: [DatabaseModule],
controllers: [PlayerFeaturesController],
providers: [PlayerFeaturesService],
exports: [PlayerFeaturesService],
})
export class PlayerFeaturesModule {}
......@@ -12,14 +12,16 @@ import {
IsArray,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
export class CreatePlayerDto {
@ApiProperty({
@ApiPropertyOptional({
description: 'Wyscout ID of the player',
example: 123456,
})
@IsOptional()
@IsInt()
wyId: number;
wyId?: number;
@ApiProperty({
description: 'First name of the player',
......@@ -76,6 +78,38 @@ export class CreatePlayerDto {
currentTeamId?: number;
@ApiPropertyOptional({
description: 'Current team name from API or manual input',
example: 'Lecce',
})
@IsOptional()
@IsString()
currentTeamName?: string;
@ApiPropertyOptional({
description: 'Current team official name from API or manual input',
example: 'U.S. Lecce',
})
@IsOptional()
@IsString()
currentTeamOfficialName?: string;
@ApiPropertyOptional({
description: 'Current national team name from API or manual input',
example: 'Italy',
})
@IsOptional()
@IsString()
currentNationalTeamName?: string;
@ApiPropertyOptional({
description: 'Current national team official name from API or manual input',
example: 'Italy National Team',
})
@IsOptional()
@IsString()
currentNationalTeamOfficialName?: string;
@ApiPropertyOptional({
description: 'Current team object from external API',
example: { id: 49, wyId: 3168, name: 'Lecce' },
})
......@@ -185,7 +219,7 @@ export class CreatePlayerDto {
gender?: 'male' | 'female' | 'other';
@ApiPropertyOptional({
description: 'Position',
description: 'Position code or name (alternative to positionId)',
example: 'FW',
})
@IsOptional()
......@@ -193,15 +227,62 @@ export class CreatePlayerDto {
position?: string;
@ApiPropertyOptional({
description: 'Other positions the player can play',
example: ['MF', 'DF'],
type: [String],
description: 'Position ID (alternative to position string)',
example: 1,
type: Number,
nullable: true,
})
@IsOptional()
@Transform(({ value }) => {
if (value === null || value === undefined) return null;
if (typeof value === 'string') {
const num = parseInt(value, 10);
return isNaN(num) ? value : num;
}
return value;
})
@IsInt()
positionId?: number | null;
@ApiPropertyOptional({
description:
'Other positions the player can play (codes, names, or IDs). Can be array of strings like ["MF", "DF"] or array of numbers like [23, 24]',
example: [23, 24],
type: [Number],
nullable: true,
})
@IsOptional()
@Transform(({ value }) => {
if (value === null || value === undefined) return null;
if (Array.isArray(value)) {
// Convert all values to strings for codes/names, or keep as numbers if they're IDs
return value.map((v) => (typeof v === 'number' ? v : String(v)));
}
return value;
})
@IsArray()
otherPositions?: (string | number)[] | null;
@ApiPropertyOptional({
description: 'Other position IDs (alternative to otherPositions array)',
example: [2, 3],
type: [Number],
nullable: true,
})
@IsOptional()
@Transform(({ value }) => {
if (value === null || value === undefined) return null;
if (Array.isArray(value)) {
return value.map((v) => {
const num = typeof v === 'string' ? parseInt(v, 10) : Number(v);
return isNaN(num) ? v : num;
});
}
return value;
})
@IsArray()
@IsString({ each: true })
otherPositions?: string[] | null;
@IsInt({ each: true })
otherPositionIds?: number[] | null;
@ApiPropertyOptional({
description: 'Role code (2 characters)',
......@@ -326,7 +407,8 @@ export class CreatePlayerDto {
@ApiPropertyOptional({
description: 'Image data URL - from external API',
example: 'https://cdn5.wyscout.com/photos/players/public/ndplayer_100x130.png',
example:
'https://cdn5.wyscout.com/photos/players/public/ndplayer_100x130.png',
})
@IsOptional()
@IsString()
......@@ -431,7 +513,7 @@ export class CreatePlayerDto {
@ApiPropertyOptional({
description: 'Market value of the player',
example: 5000000.00,
example: 5000000.0,
type: Number,
nullable: true,
})
......@@ -451,7 +533,7 @@ export class CreatePlayerDto {
@ApiPropertyOptional({
description: 'Transfer value of the player',
example: 3000000.00,
example: 3000000.0,
type: Number,
nullable: true,
})
......@@ -461,7 +543,7 @@ export class CreatePlayerDto {
@ApiPropertyOptional({
description: 'Salary of the player',
example: 100000.00,
example: 100000.0,
type: Number,
nullable: true,
})
......@@ -500,4 +582,3 @@ export class CreatePlayerDto {
@IsString()
morphology?: string | null;
}
......@@ -64,6 +64,22 @@ export class QueryPlayersDto extends BaseQueryDto {
nationalTeamId?: number;
@ApiPropertyOptional({
description: 'Filter by current national team name (ILIKE)',
example: 'Italy',
})
@IsOptional()
@IsString()
currentNationalTeamName?: string;
@ApiPropertyOptional({
description: 'Filter by current national team official name (ILIKE)',
example: 'Italy National Team',
})
@IsOptional()
@IsString()
currentNationalTeamOfficialName?: string;
@ApiPropertyOptional({
description: 'Filter by passport area WyID (for EU passport filtering)',
type: Number,
example: 1002,
......
......@@ -11,6 +11,7 @@ import {
IsArray,
} from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
export class UpdatePlayerDto {
@ApiPropertyOptional({
......@@ -78,6 +79,22 @@ export class UpdatePlayerDto {
currentTeamId?: number;
@ApiPropertyOptional({
description: 'Current team name from API or manual input',
example: 'Lecce',
})
@IsOptional()
@IsString()
currentTeamName?: string;
@ApiPropertyOptional({
description: 'Current team official name from API or manual input',
example: 'U.S. Lecce',
})
@IsOptional()
@IsString()
currentTeamOfficialName?: string;
@ApiPropertyOptional({
description: 'National team ID',
example: 44,
})
......@@ -86,6 +103,22 @@ export class UpdatePlayerDto {
currentNationalTeamId?: number;
@ApiPropertyOptional({
description: 'Current national team name from API or manual input',
example: 'Italy',
})
@IsOptional()
@IsString()
currentNationalTeamName?: string;
@ApiPropertyOptional({
description: 'Current national team official name from API or manual input',
example: 'Italy National Team',
})
@IsOptional()
@IsString()
currentNationalTeamOfficialName?: string;
@ApiPropertyOptional({
description: 'Date of birth (YYYY-MM-DD)',
example: '1998-06-01',
})
......@@ -128,7 +161,7 @@ export class UpdatePlayerDto {
gender?: 'male' | 'female' | 'other';
@ApiPropertyOptional({
description: 'Position',
description: 'Position code or name (alternative to positionId)',
example: 'FW',
})
@IsOptional()
......@@ -136,15 +169,62 @@ export class UpdatePlayerDto {
position?: string;
@ApiPropertyOptional({
description: 'Other positions the player can play',
example: ['MF', 'DF'],
type: [String],
description: 'Position ID (alternative to position string)',
example: 1,
type: Number,
nullable: true,
})
@IsOptional()
@Transform(({ value }) => {
if (value === null || value === undefined) return null;
if (typeof value === 'string') {
const num = parseInt(value, 10);
return isNaN(num) ? value : num;
}
return value;
})
@IsInt()
positionId?: number | null;
@ApiPropertyOptional({
description:
'Other positions the player can play (codes, names, or IDs). Can be array of strings like ["MF", "DF"] or array of numbers like [23, 24]',
example: [23, 24],
type: [Number],
nullable: true,
})
@IsOptional()
@Transform(({ value }) => {
if (value === null || value === undefined) return null;
if (Array.isArray(value)) {
// Convert all values to strings for codes/names, or keep as numbers if they're IDs
return value.map((v) => (typeof v === 'number' ? v : String(v)));
}
return value;
})
@IsArray()
otherPositions?: (string | number)[] | null;
@ApiPropertyOptional({
description: 'Other position IDs (alternative to otherPositions array)',
example: [2, 3],
type: [Number],
nullable: true,
})
@IsOptional()
@Transform(({ value }) => {
if (value === null || value === undefined) return null;
if (Array.isArray(value)) {
return value.map((v) => {
const num = typeof v === 'string' ? parseInt(v, 10) : Number(v);
return isNaN(num) ? v : num;
});
}
return value;
})
@IsArray()
@IsString({ each: true })
otherPositions?: string[] | null;
@IsInt({ each: true })
otherPositionIds?: number[] | null;
@ApiPropertyOptional({
description: 'Role code (2 characters)',
......@@ -222,6 +302,15 @@ export class UpdatePlayerDto {
imageDataUrl?: string;
@ApiPropertyOptional({
description: 'Image data URL - alias used by external API and some clients',
example:
'https://cdn5.wyscout.com/photos/players/public/ndplayer_100x130.png',
})
@IsOptional()
@IsString()
imageDataURL?: string;
@ApiPropertyOptional({
description: 'Jersey number',
example: 9,
})
......
......@@ -39,6 +39,20 @@ export interface PlayerPosition {
category: PositionCategory;
}
export interface Agent {
id: number;
name: string;
description: string | null;
type: string;
email: string;
phone: string;
status: string;
address: string;
country: string;
createdAt: string;
updatedAt: string;
}
export interface StructuredPlayer {
id: number;
wyId: number;
......@@ -60,6 +74,10 @@ export interface StructuredPlayer {
foot: string | null;
currentTeamId: number | null;
currentNationalTeamId: number | null;
currentTeamName: string | null;
currentTeamOfficialName: string | null;
currentNationalTeamName: string | null;
currentNationalTeamOfficialName: string | null;
currentTeam: any | null;
gender: string;
status: string;
......@@ -73,7 +91,8 @@ export interface StructuredPlayer {
email: string | null;
phone: string | null;
onLoan: boolean;
agent: string | null;
agentId: number | null;
agent: Agent | null;
ranking: string | null;
roi: string | null;
marketValue: number | null;
......
......@@ -44,6 +44,25 @@ export class PlayersController {
return this.playersService.upsertByWyId(body as any);
}
@Get('by-id/:id')
@ApiOperation({ summary: 'Get player by database ID' })
@ApiParam({
name: 'id',
type: Number,
description: 'Database ID of the player',
})
@ApiOkResponse({ description: 'Player if found', type: Object })
@ApiNotFoundResponse({ description: 'Player not found' })
async getById(
@Param('id', ParseIntPipe) id: number,
): Promise<StructuredPlayer | undefined> {
const player = await this.playersService.findById(id);
if (!player) {
throw new NotFoundException(`Player with ID ${id} not found`);
}
return player;
}
@Get(':wyId')
@ApiOperation({ summary: 'Get player by wyId' })
@ApiOkResponse({ description: 'Player if found', type: Object })
......
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ReportsController } from './reports.controller';
import { ReportsService } from './reports.service';
import { DatabaseModule } from '../../database/database.module';
......@@ -9,14 +7,7 @@ import { JwtSimpleGuard } from '../../common/guards/jwt-simple.guard';
@Module({
imports: [
DatabaseModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
signOptions: { expiresIn: '24h' },
}),
inject: [ConfigService],
}),
// JwtModule is now global, no need to import here
],
controllers: [ReportsController],
providers: [ReportsService, JwtSimpleGuard],
......
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { DatabaseModule } from '../../database/database.module';
......@@ -9,14 +7,7 @@ import { JwtSimpleGuard } from '../../common/guards/jwt-simple.guard';
@Module({
imports: [
DatabaseModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
signOptions: { expiresIn: '24h' },
}),
inject: [ConfigService],
}),
// JwtModule is now global, no need to import here
],
controllers: [UsersController],
providers: [UsersService, JwtSimpleGuard],
......
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