Loading settings...
+diff --git a/e2e/cucumber/features/admin-dashboard.feature b/e2e/cucumber/features/admin-dashboard.feature new file mode 100644 index 0000000..ff8082d --- /dev/null +++ b/e2e/cucumber/features/admin-dashboard.feature @@ -0,0 +1,36 @@ +Feature: Club Admin Dashboard + As a club admin + I want to view club-wide statistics and manage club operations + So that I can effectively oversee the club + + @happy-path @admin @issue-11 + Scenario: Club admin views dashboard with statistics + Given I am logged in as a club admin + When I go to the admin dashboard + Then I should see "Admin Dashboard" + And I should see total player count + And I should see active tournament count + + @happy-path @admin @issue-11 + Scenario: Club admin views recent activity feed + Given I am logged in as a club admin + And there are recent activities in the system + When I go to the admin dashboard + Then I should see the activity feed section + And I should see recent player registrations + + @happy-path @admin @issue-11 + Scenario: Club admin searches player directory + Given I am logged in as a club admin + And there are multiple players in the system + When I go to the player management page + And I search for "Player 1" + Then I should see search results + + @happy-path @admin @issue-11 + Scenario: Club admin updates club settings + Given I am logged in as a club admin + When I go to the club settings page + And I update the club name + And I save the settings + Then the settings should be saved successfully diff --git a/e2e/cucumber/step-definitions/auth-steps.ts b/e2e/cucumber/step-definitions/auth-steps.ts index 88db51f..3b4b975 100644 --- a/e2e/cucumber/step-definitions/auth-steps.ts +++ b/e2e/cucumber/step-definitions/auth-steps.ts @@ -395,3 +395,57 @@ Given('a tournament has a generated schedule', async function () { // 2. Add teams/participants // 3. Generate schedule via API or UI }); + +Given('there are recent activities in the system', async function () { + // Create test activities using the activity logger + const prisma = await world.getPrisma() + + // Use timestamp to ensure unique names + const timestamp = Date.now() + + // Create a test player first + const player = await prisma.player.create({ + data: { + name: `Test Activity Player ${timestamp}`, + normalizedName: `test activity player ${timestamp}`, + currentElo: 1000, + gamesPlayed: 0, + wins: 0, + losses: 0, + }, + }) + + // Create an activity + await (prisma as any).activity.create({ + data: { + type: 'player_registration', + description: `Test Activity Player ${timestamp} registered`, + playerId: player.id, + }, + }) + + console.log('🌍 Created test activity for player:', player.name) +}) + +Given('there are multiple players in the system', async function () { + const prisma = await world.getPrisma() + + // Use timestamp to ensure unique names + const timestamp = Date.now() + + // Create multiple test players + for (let i = 1; i <= 5; i++) { + await prisma.player.create({ + data: { + name: `Test Player ${i} ${timestamp}`, + normalizedName: `test player ${i} ${timestamp}`, + currentElo: 1000 + i * 10, + gamesPlayed: 0, + wins: 0, + losses: 0, + }, + }) + } + + console.log('🌍 Created 5 test players') +}) diff --git a/e2e/cucumber/step-definitions/common-steps.ts b/e2e/cucumber/step-definitions/common-steps.ts index cb5e8f5..953a5be 100644 --- a/e2e/cucumber/step-definitions/common-steps.ts +++ b/e2e/cucumber/step-definitions/common-steps.ts @@ -475,3 +475,79 @@ Then('I should see {string} error', async function (errorMessage: string) { expect(content).toMatch(new RegExp(errorMessage, 'i')); console.log(`🌍 Verified error message: ${errorMessage}`); }); + +// Admin Dashboard Steps +When('I go to the admin dashboard', async function () { + console.log('🌍 Going to admin dashboard'); + await world.page.goto(`${world.baseURL}/admin`); + await world.page.waitForLoadState('domcontentloaded'); +}); + +Then('I should see total player count', async function () { + await expect(world.page.locator('text=Total Players')).toBeVisible(); + console.log('🌍 Verified total players section is visible'); +}); + +Then('I should see active tournament count', async function () { + // Use more specific locator to find the stats card + await expect(world.page.locator('dt:has-text("Tournaments")').first()).toBeVisible(); + console.log('🌍 Verified tournaments section is visible'); +}); + +Then('I should see the activity feed section', async function () { + await expect(world.page.locator('text=Recent Activity')).toBeVisible(); + console.log('🌍 Verified activity feed section is visible'); +}); + +Then('I should see recent player registrations', async function () { + // Check if there are any activities in the feed + const activityItems = await world.page.locator('.divide-y.divide-gray-200 li').count(); + console.log(`🌍 Found ${activityItems} activity items`); + + // Also check for the activity text + const content = await world.page.content(); + const hasActivityText = content.includes('Test Activity Player'); + console.log(`🌍 Activity text found in page: ${hasActivityText}`); + + expect(activityItems).toBeGreaterThan(0); +}); + +When('I go to the player management page', async function () { + console.log('🌍 Going to player management page'); + await world.page.goto(`${world.baseURL}/admin/players`); + await world.page.waitForLoadState('domcontentloaded'); +}); + +When('I search for {string}', async function (searchTerm: string) { + console.log(`🌍 Searching for: ${searchTerm}`); + await world.page.fill('input[name="search"]', searchTerm); + await world.page.waitForTimeout(500); // Wait for search to execute +}); + +Then('I should see search results', async function () { + // Check if player table is visible + await expect(world.page.locator('table')).toBeVisible(); + console.log('🌍 Verified search results are displayed'); +}); + +When('I go to the club settings page', async function () { + console.log('🌍 Going to club settings page'); + await world.page.goto(`${world.baseURL}/admin/settings`); + await world.page.waitForLoadState('domcontentloaded'); +}); + +When('I update the club name', async function () { + console.log('🌍 Updating club name'); + await world.page.fill('input[id="clubName"]', 'Test Club Updated'); +}); + +When('I save the settings', async function () { + console.log('🌍 Saving settings'); + await world.page.click('button:has-text("Save Settings")'); + await world.page.waitForTimeout(1000); // Wait for save to complete +}); + +Then('the settings should be saved successfully', async function () { + await expect(world.page.locator('text=Settings saved successfully')).toBeVisible(); + console.log('🌍 Verified settings were saved successfully'); +}); diff --git a/prisma/migrations/20260426235230_add_activity_and_settings_models/migration.sql b/prisma/migrations/20260426235230_add_activity_and_settings_models/migration.sql new file mode 100644 index 0000000..fbff61e --- /dev/null +++ b/prisma/migrations/20260426235230_add_activity_and_settings_models/migration.sql @@ -0,0 +1,63 @@ +-- DropForeignKey +ALTER TABLE "matches" DROP CONSTRAINT "matches_player1P1Id_fkey"; + +-- DropForeignKey +ALTER TABLE "matches" DROP CONSTRAINT "matches_player1P2Id_fkey"; + +-- DropForeignKey +ALTER TABLE "matches" DROP CONSTRAINT "matches_player2P1Id_fkey"; + +-- DropForeignKey +ALTER TABLE "matches" DROP CONSTRAINT "matches_player2P2Id_fkey"; + +-- CreateTable +CREATE TABLE "activities" ( + "id" SERIAL NOT NULL, + "type" TEXT NOT NULL, + "description" TEXT NOT NULL, + "userId" TEXT, + "playerId" INTEGER, + "eventId" INTEGER, + "matchId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "activities_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "club_settings" ( + "id" SERIAL NOT NULL, + "clubName" TEXT NOT NULL DEFAULT 'Euchre Club', + "defaultEloRating" INTEGER NOT NULL DEFAULT 1200, + "partnershipEnabled" BOOLEAN NOT NULL DEFAULT true, + "notificationsEnabled" BOOLEAN NOT NULL DEFAULT true, + "matchVerification" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "club_settings_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_player1P1Id_fkey" FOREIGN KEY ("player1P1Id") REFERENCES "players"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_player1P2Id_fkey" FOREIGN KEY ("player1P2Id") REFERENCES "players"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_player2P1Id_fkey" FOREIGN KEY ("player2P1Id") REFERENCES "players"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "matches" ADD CONSTRAINT "matches_player2P2Id_fkey" FOREIGN KEY ("player2P2Id") REFERENCES "players"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "activities" ADD CONSTRAINT "activities_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "activities" ADD CONSTRAINT "activities_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "players"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "activities" ADD CONSTRAINT "activities_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "events"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "activities" ADD CONSTRAINT "activities_matchId_fkey" FOREIGN KEY ("matchId") REFERENCES "matches"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0f2074..4468396 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,6 +27,7 @@ model Player { partnershipGames2 PartnershipGame[] @relation("PartnershipPlayer2") partnershipStats PartnershipStat[] @relation("StatPlayer1") partnershipStats2 PartnershipStat[] @relation("StatPlayer2") + activities Activity[] user User? eloRating EloRating? glicko2Rating Glicko2Rating? @@ -53,6 +54,7 @@ model User { ownedTournaments Event[] @relation("TournamentOwner") createdMatches Match[] @relation("MatchCreator") sessions Session[] + activities Activity[] player Player? @relation(fields: [playerId], references: [id]) @@map("users") @@ -79,6 +81,7 @@ model Event { owner User? @relation("TournamentOwner", fields: [ownerId], references: [id]) matches Match[] rounds TournamentRound[] + activities Activity[] // Team configuration fields teamDurability String @default("permanent") // permanent, variable, per_round @@ -167,6 +170,7 @@ model Match { isCasual Boolean @default(false) bracketMatchups BracketMatchup[] eloSnapshots EloSnapshot[] + activities Activity[] createdBy User? @relation("MatchCreator", fields: [createdById], references: [id]) event Event? @relation(fields: [eventId], references: [id], onDelete: Cascade) player1P1 Player? @relation("MatchPlayer1", fields: [player1P1Id], references: [id]) @@ -325,3 +329,34 @@ model OpenSkillRating { @@map("open_skill_ratings") } + +model Activity { + id Int @id @default(autoincrement()) + type String // "player_registration", "tournament_created", "match_completed", "partnership_recorded" + description String + userId String? + playerId Int? + eventId Int? + matchId Int? + createdAt DateTime @default(now()) + + user User? @relation(fields: [userId], references: [id]) + player Player? @relation(fields: [playerId], references: [id]) + event Event? @relation(fields: [eventId], references: [id]) + match Match? @relation(fields: [matchId], references: [id]) + + @@map("activities") +} + +model ClubSettings { + id Int @id @default(autoincrement()) + clubName String @default("Euchre Club") + defaultEloRating Int @default(1200) + partnershipEnabled Boolean @default(true) + notificationsEnabled Boolean @default(true) + matchVerification Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("club_settings") +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 3f7e3e9..1b34fe8 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -59,6 +59,17 @@ export default async function AdminDashboard() { }), ]) as [number, number, number, EventModel[]] + // Get recent activities (using any type to bypass TypeScript error for now) + const recentActivities = await (prisma as any).activity.findMany({ + take: 10, + orderBy: { createdAt: "desc" }, + include: { + user: { select: { name: true } }, + player: { select: { name: true } }, + event: { select: { name: true } }, + }, + }) + return (
{activity.description}
++ {new Date(activity.createdAt).toLocaleDateString()} at{' '} + {new Date(activity.createdAt).toLocaleTimeString()} +
+No recent activities.
+ )} +