Skip to content
Merged

Dev #360

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
edc4b03
feat: enhance success stories management with detailed logging, user …
AdithaBuwaneka Jul 21, 2025
839009c
refactor: remove outdated comments from badge assignment scheduler
renulucshmi Jul 21, 2025
f6e4d01
Merge pull request #353 from Code102SoftwareProject/feat/dashboard-su…
SamFaMH Jul 21, 2025
bc65717
refactor: remove full name validation and update display logic in KYC…
renulucshmi Jul 21, 2025
1fcfc13
Merge branch 'dev' of https://github.com/Code102SoftwareProject/skill…
renulucshmi Jul 21, 2025
228f343
Merge pull request #354 from Code102SoftwareProject/admin-report-renu
renulucshmi Jul 21, 2025
5fe6d43
fix: update regex for new NIC format validation
renulucshmi Jul 21, 2025
fc0d016
feat: enhance success stories display with improved user name handlin…
AdithaBuwaneka Jul 21, 2025
f7b2c07
Merge pull request #355 from Code102SoftwareProject/admin-sucessstories
AdithaBuwaneka Jul 21, 2025
1b7f7ff
Merge branch 'dev' of https://github.com/Code102SoftwareProject/skill…
renulucshmi Jul 21, 2025
80759f5
Merge pull request #356 from Code102SoftwareProject/admin-report-renu
renulucshmi Jul 21, 2025
8f000c8
test: enhance KYCContent tests with loading state and error handling
renulucshmi Jul 21, 2025
2f5fc70
Merge pull request #357 from Code102SoftwareProject/test-renu
renulucshmi Jul 21, 2025
02fb5e4
style: adjust layout of Success Stories container for better alignment
AdithaBuwaneka Jul 21, 2025
e99437e
refactor: simplify session management by removing useCallback for han…
AdithaBuwaneka Jul 21, 2025
c0c8049
style: improve layout of skill card by restructuring title and status…
AdithaBuwaneka Jul 21, 2025
9719fe5
feat: enhance skills management with filtering, sorting, and statistics
AdithaBuwaneka Jul 21, 2025
6e3b9bc
feat: implement custom dropdowns for mobile responsiveness and enhanc…
AdithaBuwaneka Jul 21, 2025
0cb3280
feat: add custom dropdowns for mobile responsiveness and enhance filt…
AdithaBuwaneka Jul 21, 2025
7c3039f
Merge pull request #358 from Code102SoftwareProject/my-skill-ui-update
AdithaBuwaneka Jul 21, 2025
8b03c19
feat: Enhance Matches Page with Filtering, Searching, and Statistics
AdithaBuwaneka Jul 22, 2025
401e91f
Merge pull request #359 from Code102SoftwareProject/skilll-amtches-ui…
AdithaBuwaneka Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions __tests__/pages/KYCContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ describe("KYCContent", () => {
jest.clearAllMocks();
});

it("shows loading state initially", () => {
mockFetch({ data: [] });
render(<KYCContent />);
expect(screen.getByText("Loading KYC records...")).toBeInTheDocument();
});

it("displays an error when the initial fetch fails", async () => {
mockFetch({ message: "Error" }, false); // ok = false
await act(async () => {
render(<KYCContent />);
});
await waitFor(() =>
expect(
screen.getByText("Failed to load KYC records")
).toBeInTheDocument()
);
expect(toast.error).toHaveBeenCalledWith("Failed to load KYC records");
});

it("renders records in table after fetch", async () => {
const records = [
{
Expand Down Expand Up @@ -78,11 +97,11 @@ describe("KYCContent", () => {
});

const rows = screen.getAllByRole("row");
const dataRows = rows.slice(1);
const dataRows = rows.slice(1);
expect(dataRows).toHaveLength(2);
expect(
within(dataRows[0]).getByText("Not Reviewed")
).toBeInTheDocument();
// the status spans may be split into multiple elements,
// so assert the row as a whole contains the full string:
expect(dataRows[0]).toHaveTextContent("Not Reviewed");
expect(within(dataRows[1]).getByText("Accepted")).toBeInTheDocument();
});

Expand All @@ -102,9 +121,7 @@ describe("KYCContent", () => {
});
await waitFor(() => screen.getByText("Charlie"));

const input = screen.getByPlaceholderText(
"Search by recipient name"
);
const input = screen.getByPlaceholderText("Search by recipient name");
fireEvent.change(input, { target: { value: "bad!" } });
expect(toast.error).toHaveBeenCalledWith(
"Only letters, numbers, and spaces are allowed"
Expand Down Expand Up @@ -198,9 +215,7 @@ describe("KYCContent", () => {
`/api/file/retrieve?fileUrl=${encodeURIComponent("/api.pdf")}`
);
expect(toast.loading).toHaveBeenCalledWith("Downloading file...");
expect(toast.success).toHaveBeenCalledWith(
"File downloaded successfully"
);
expect(toast.success).toHaveBeenCalledWith("File downloaded successfully");
});
});

Expand Down
219 changes: 119 additions & 100 deletions src/app/api/admin/success-stories/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,20 @@ async function verifyAdminToken(request: NextRequest) {
// GET - Fetch all feedback entries with success stories (for admin)
export async function GET(request: NextRequest) {
try {
console.log("Starting GET request for success stories");

// Connect to database
await connect();
console.log("Database connected successfully");

// Ensure User and Admin models are registered for populate operations
User;
Admin;
console.log("Models registered");

const adminData = await verifyAdminToken(request);
console.log("Admin verification result:", adminData ? "Success" : "Failed");

if (!adminData) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
Expand All @@ -46,12 +53,23 @@ export async function GET(request: NextRequest) {
const search = searchParams.get("search") || "";
const status = searchParams.get("status") || "all"; // all, published, unpublished

console.log("Query params:", { page, limit, search, status });

const skip = (page - 1) * limit;

// First, let's check if we can query the Feedback collection at all
const totalFeedbackCount = await Feedback.countDocuments({});
console.log(`Total feedback entries in database: ${totalFeedbackCount}`);

// Check how many have success stories
const feedbackWithStoriesCount = await Feedback.countDocuments({
successStory: { $exists: true, $ne: "" }
});
console.log(`Feedback entries with success stories: ${feedbackWithStoriesCount}`);

// Build query for feedback entries with success stories
// Admin can see all success stories, not filtered by canSuccessStoryPost
const query: any = {
userId: { $ne: null },
canSuccessStoryPost: true,
successStory: { $exists: true, $ne: "" }
};

Expand All @@ -67,34 +85,64 @@ export async function GET(request: NextRequest) {
query.isPublished = status === "published";
}

console.log("MongoDB query:", JSON.stringify(query, null, 2));

// Get feedback entries with success stories
const feedbackWithStories = await Feedback.find(query)
.populate("userId", "firstName lastName email avatar")
.sort({ date: -1 })
.skip(skip)
.limit(limit);
.limit(limit)
.lean(); // Add lean() for better performance

console.log(`Found ${feedbackWithStories.length} feedback entries`);

// Transform feedback data to match expected success story format
const transformedStories = feedbackWithStories
.filter(feedback => feedback.userId !== null)
.map(feedback => ({
_id: feedback._id,
userId: feedback.userId,
title: feedback.adminTitle || `${feedback.rating}-Star Experience`,
description: feedback.successStory,
feedback: feedback.feedback,
rating: feedback.rating,
isPublished: feedback.isPublished,
publishedAt: feedback.isPublished ? feedback.date : null,
createdAt: feedback.date,
updatedAt: feedback.date,
displayName: feedback.displayName,
isAnonymous: feedback.isAnonymous
}));
.map(feedback => {
try {
// Determine display name for admin with same logic as public API
let displayName;
if (feedback.isAnonymous) {
displayName = 'Anonymous';
} else if (feedback.displayName && feedback.displayName.trim() && feedback.displayName.trim() !== "User") {
// User provided a custom display name (but not the generic "User" fallback)
displayName = feedback.displayName.trim();
} else {
// User didn't choose anonymous AND didn't provide display name = show actual name
// This also handles cases where displayName is "User" from old data
displayName = `${feedback.userId.firstName} ${feedback.userId.lastName}`.trim();
}

return {
_id: feedback._id,
userId: feedback.userId,
title: feedback.adminTitle || `${feedback.rating || 5}-Star Experience`,
description: feedback.successStory,
feedback: feedback.feedback,
rating: feedback.rating || 5,
isPublished: feedback.isPublished || false,
publishedAt: feedback.isPublished ? feedback.date : null,
createdAt: feedback.date,
updatedAt: feedback.date,
displayName: displayName,
isAnonymous: feedback.isAnonymous || false,
canSuccessStoryPost: feedback.canSuccessStoryPost || false
};
} catch (mapError) {
console.error("Error transforming feedback:", mapError, feedback);
return null;
}
})
.filter(story => story !== null); // Remove any null entries from transformation errors

console.log(`Transformed ${transformedStories.length} success stories`);

const total = await Feedback.countDocuments(query);
console.log(`Total documents matching query: ${total}`);

return NextResponse.json({
const response = {
success: true,
data: {
successStories: transformedStories,
Expand All @@ -107,18 +155,34 @@ export async function GET(request: NextRequest) {
hasPrevPage: page > 1,
},
},
});
};

console.log("Returning response with", transformedStories.length, "stories");
return NextResponse.json(response);
} catch (error) {
console.error("Error fetching success stories:", error);
console.error("Detailed error in GET success stories:", {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace',
error
});
return NextResponse.json(
{ success: false, message: "Failed to fetch success stories" },
{ success: false, message: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` },
{ status: 500 }
);
}
}

// POST - Create a new feedback entry with success story (admin-created)
// POST - Admin cannot create new success stories, only users can
// This endpoint is disabled as per requirements
export async function POST(request: NextRequest) {
return NextResponse.json(
{ success: false, message: "Admin cannot create new success stories. Only users can submit feedback with success stories." },
{ status: 403 }
);
}

// PUT - Update a feedback entry's success story details
export async function PUT(request: NextRequest) {
try {
await connect();

Expand All @@ -130,92 +194,38 @@ export async function POST(request: NextRequest) {
);
}

const { userId, title, description, feedback, rating, isPublished } = await request.json();
const { id, title, isPublished } = await request.json();

// Validate required fields
if (!userId || !description || !feedback) {
if (!id) {
return NextResponse.json(
{ success: false, message: "User ID, success story, and feedback are required" },
{ success: false, message: "Feedback ID is required" },
{ status: 400 }
);
}

// Verify user exists
const user = await User.findById(userId);
if (!user) {
// Find the feedback entry first to check canSuccessStoryPost
const existingFeedback = await Feedback.findById(id);
if (!existingFeedback) {
console.error("Feedback entry not found with ID:", id);
return NextResponse.json(
{ success: false, message: "User not found" },
{ success: false, message: "Feedback entry not found" },
{ status: 404 }
);
}

// Create feedback entry with success story
const feedbackEntry = new Feedback({
userId,
feedback: feedback,
successStory: description,
rating: rating || 5,
adminTitle: title,
canSuccessStoryPost: true,
isAnonymous: false,
isPublished,
displayName: `${user.firstName} ${user.lastName}`,
console.log("Existing feedback:", {
id: existingFeedback._id,
canSuccessStoryPost: existingFeedback.canSuccessStoryPost,
isPublished: existingFeedback.isPublished,
newPublishState: isPublished
});

await feedbackEntry.save();

// Populate user details for response
await feedbackEntry.populate("userId", "firstName lastName email avatar");

// Transform response to match expected format
const transformedStory = {
_id: feedbackEntry._id,
userId: feedbackEntry.userId,
title: feedbackEntry.adminTitle || `${feedbackEntry.rating}-Star Experience`,
description: feedbackEntry.successStory,
feedback: feedbackEntry.feedback,
rating: feedbackEntry.rating,
isPublished: feedbackEntry.isPublished,
publishedAt: feedbackEntry.isPublished ? feedbackEntry.date : null,
createdAt: feedbackEntry.date,
updatedAt: feedbackEntry.date,
displayName: feedbackEntry.displayName,
isAnonymous: feedbackEntry.isAnonymous
};

return NextResponse.json({
success: true,
message: "Success story created successfully",
data: transformedStory,
});
} catch (error) {
console.error("Error creating success story:", error);
return NextResponse.json(
{ success: false, message: "Failed to create success story" },
{ status: 500 }
);
}
}

// PUT - Update a feedback entry's success story details
export async function PUT(request: NextRequest) {
try {
await connect();

const adminData = await verifyAdminToken(request);
if (!adminData) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 }
);
}

const { id, title, isPublished } = await request.json();

if (!id) {
// Only allow publishing if user gave consent (canSuccessStoryPost: true)
if (isPublished === true && !existingFeedback.canSuccessStoryPost) {
console.warn("Attempted to publish without consent");
return NextResponse.json(
{ success: false, message: "Feedback ID is required" },
{ status: 400 }
{ success: false, message: "Cannot publish success story without user consent" },
{ status: 403 }
);
}

Expand Down Expand Up @@ -253,8 +263,9 @@ export async function PUT(request: NextRequest) {
publishedAt: feedback.isPublished ? feedback.date : null,
createdAt: feedback.date,
updatedAt: feedback.date,
displayName: feedback.displayName,
isAnonymous: feedback.isAnonymous
displayName: feedback.isAnonymous ? 'Anonymous' : feedback.displayName,
isAnonymous: feedback.isAnonymous,
canSuccessStoryPost: feedback.canSuccessStoryPost
};

return NextResponse.json({
Expand All @@ -263,9 +274,13 @@ export async function PUT(request: NextRequest) {
data: transformedStory,
});
} catch (error) {
console.error("Error updating success story:", error);
console.error("Detailed error in PUT success stories:", {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace',
error
});
return NextResponse.json(
{ success: false, message: "Failed to update success story" },
{ success: false, message: `Failed to update success story: ${error instanceof Error ? error.message : 'Unknown error'}` },
{ status: 500 }
);
}
Expand Down Expand Up @@ -308,9 +323,13 @@ export async function DELETE(request: NextRequest) {
message: "Success story deleted successfully",
});
} catch (error) {
console.error("Error deleting success story:", error);
console.error("Detailed error in DELETE success stories:", {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace',
error
});
return NextResponse.json(
{ success: false, message: "Failed to delete success story" },
{ success: false, message: `Failed to delete success story: ${error instanceof Error ? error.message : 'Unknown error'}` },
{ status: 500 }
);
}
Expand Down
Loading
Loading