Skip to content

Commit 3864d58

Browse files
Zeff01claude
andcommitted
feat: Add view profile functionality and sorting to in-house page
- Fixed view profile button to open ProfileModal with complete user data - Added ascending/descending sorting for Date Joined column - Added ascending/descending sorting for Position column - Implemented sort indicators with visual feedback (arrows and hover states) - Integrated sorting with existing filters and pagination - Added profile fetching with education, work experience, projects, and skill points - Includes fallback handling for missing profile data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ec4c89e commit 3864d58

4 files changed

Lines changed: 178 additions & 6 deletions

File tree

‎apps/codebility/app/home/in-house/_components/InHouseView.tsx‎

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ export default function InHouseView({ initialData }: InHouseViewProps) {
2222
const [isPending, startTransition] = useTransition();
2323
const stats = getMemberStats(data);
2424

25+
// Sorting state
26+
const [sortConfig, setSortConfig] = useState<{
27+
key: "date_joined" | "display_position" | null;
28+
direction: "asc" | "desc";
29+
}>({
30+
key: null,
31+
direction: "asc",
32+
});
33+
2534
// Filters
2635
const [filters, setFilters] = useState({
2736
status: "",
@@ -97,14 +106,45 @@ export default function InHouseView({ initialData }: InHouseViewProps) {
97106
return true;
98107
});
99108

100-
// Pagination - increased items per page for better table usage
109+
// Sorting logic
110+
const sortedData = [...filteredData].sort((a, b) => {
111+
if (!sortConfig.key) return 0;
112+
113+
if (sortConfig.key === "date_joined") {
114+
const dateA = a.date_joined ? new Date(a.date_joined).getTime() : 0;
115+
const dateB = b.date_joined ? new Date(b.date_joined).getTime() : 0;
116+
117+
return sortConfig.direction === "asc" ? dateA - dateB : dateB - dateA;
118+
}
119+
120+
if (sortConfig.key === "display_position") {
121+
const posA = (a.display_position || "").toLowerCase();
122+
const posB = (b.display_position || "").toLowerCase();
123+
124+
if (posA < posB) return sortConfig.direction === "asc" ? -1 : 1;
125+
if (posA > posB) return sortConfig.direction === "asc" ? 1 : -1;
126+
return 0;
127+
}
128+
129+
return 0;
130+
});
131+
132+
// Handle sort toggle
133+
const handleSort = (key: "date_joined" | "display_position") => {
134+
setSortConfig((prev) => ({
135+
key,
136+
direction: prev.key === key && prev.direction === "asc" ? "desc" : "asc",
137+
}));
138+
};
139+
140+
// Pagination - increased items per page for better table usage (use sortedData)
101141
const {
102142
currentPage,
103143
totalPages,
104144
paginatedData,
105145
handleNextPage,
106146
handlePreviousPage,
107-
} = usePagination(filteredData, 50);
147+
} = usePagination(sortedData, 50);
108148

109149
const sharedProps = {
110150
data: paginatedData,
@@ -175,7 +215,7 @@ export default function InHouseView({ initialData }: InHouseViewProps) {
175215
{/* Table View with Loading State */}
176216
{isPending ? (
177217
<InHouseTableSkeleton rows={10} />
178-
) : filteredData.length === 0 ? (
218+
) : sortedData.length === 0 ? (
179219
<div className="flex min-h-[400px] flex-col items-center justify-center p-8 text-center">
180220
<div className="mb-4 text-4xl">🔍</div>
181221
<h3 className="mb-2 text-lg font-medium text-gray-900 dark:text-white">No members found</h3>
@@ -202,6 +242,8 @@ export default function InHouseView({ initialData }: InHouseViewProps) {
202242
onNextPage: handleNextPage,
203243
onPreviousPage: handlePreviousPage,
204244
}}
245+
sortConfig={sortConfig}
246+
onSort={handleSort}
205247
/>
206248
)}
207249
</div>

‎apps/codebility/app/home/in-house/_components/table/InHouseMobileTable.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ interface InHouseMobileTableProps {
3434
message: string,
3535
) => Promise<void>;
3636
handleDownloadNda: (codevId: string) => void;
37+
sortConfig?: {
38+
key: "date_joined" | "display_position" | null;
39+
direction: "asc" | "desc";
40+
};
41+
onSort?: (key: "date_joined" | "display_position") => void;
3742
}
3843

3944
const defaultImage = "/assets/svgs/icon-codebility-black.svg";
@@ -97,6 +102,8 @@ export function InHouseMobileTable({
97102
roles,
98103
handleSendNdaEmail,
99104
handleDownloadNda,
105+
sortConfig,
106+
onSort,
100107
}: InHouseMobileTableProps) {
101108
const [editingId, setEditingId] = useState<string | null>(null);
102109

‎apps/codebility/app/home/in-house/_components/table/InHouseTable.tsx‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import DefaultPagination from "@/components/ui/pagination";
88
import { pageSize } from "@/constants";
99
import { Codev, InternalStatus } from "@/types/home/codev";
1010
import { createClientClientComponent } from "@/utils/supabase/client";
11-
import { Download, Link2, Mail } from "lucide-react";
11+
import { ArrowUpDown, Download, Link2, Mail } from "lucide-react";
1212
import { toast } from "react-hot-toast";
1313

1414
import { Button } from "@codevs/ui/button";
@@ -61,6 +61,11 @@ interface InHouseTableProps {
6161
onNextPage: () => void;
6262
onPreviousPage: () => void;
6363
};
64+
sortConfig?: {
65+
key: "date_joined" | "display_position" | null;
66+
direction: "asc" | "desc";
67+
};
68+
onSort?: (key: "date_joined" | "display_position") => void;
6469
}
6570

6671
// Default fallback image for team members without avatars
@@ -141,6 +146,8 @@ export function InHouseTable({
141146
onDataChange,
142147
pagination,
143148
onDelete,
149+
sortConfig,
150+
onSort,
144151
}: InHouseTableProps) {
145152
// Supabase client instance for database operations
146153
const [supabase, setSupabase] = useState<any>(null);
@@ -352,6 +359,20 @@ export function InHouseTable({
352359

353360
const handlePreviousPage = useCallback(() => pagination.onPreviousPage(), [pagination]);
354361

362+
/**
363+
* Renders sort indicator for sortable columns
364+
*/
365+
const SortIndicator = ({ columnKey }: { columnKey: "date_joined" | "display_position" }) => {
366+
if (!sortConfig || sortConfig.key !== columnKey) {
367+
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-30" />;
368+
}
369+
return (
370+
<span className="ml-1 inline">
371+
{sortConfig.direction === "asc" ? "↑" : "↓"}
372+
</span>
373+
);
374+
};
375+
355376
/**
356377
* Advanced pagination handler for direct page navigation
357378
*/
@@ -387,6 +408,8 @@ export function InHouseTable({
387408
roles={roles}
388409
handleSendNdaEmail={handleSendNdaEmail}
389410
handleDownloadNda={handleDownloadNda}
411+
sortConfig={sortConfig}
412+
onSort={onSort}
390413
/>
391414

392415
{/* Desktop Table Container - Hidden on mobile, visible on XL+ screens */}
@@ -407,6 +430,16 @@ export function InHouseTable({
407430
>
408431
{column.label}
409432
</TableHead>
433+
) : column.key === "display_position" || column.key === "date_joined" ? (
434+
// Sortable columns - Hidden on smaller screens, visible on 2XL+
435+
<TableHead
436+
key={column.key}
437+
className="dark:text-light-900 hidden cursor-pointer px-1 py-2 text-xs font-semibold text-black hover:bg-light-700 dark:hover:bg-dark-400 2xl:table-cell"
438+
onClick={() => onSort?.(column.key as "date_joined" | "display_position")}
439+
>
440+
{column.label}
441+
<SortIndicator columnKey={column.key as "date_joined" | "display_position"} />
442+
</TableHead>
410443
) : (
411444
// Hidden on smaller screens, visible on 2XL+
412445
<TableHead

‎apps/codebility/app/home/in-house/_components/table/TableActions.tsx‎

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState } from "react";
2-
import { Codev } from "@/types/home/codev";
2+
import { Codev, InternalStatus } from "@/types/home/codev";
33
import { createClientClientComponent } from "@/utils/supabase/client";
4+
import { useModal } from "@/hooks/use-modal-users";
45
import { Edit2, Eye, MoreHorizontal, Trash2 } from "lucide-react";
56
import { toast } from "react-hot-toast";
67

@@ -24,12 +25,101 @@ export function TableActions({ item, onEdit, onDelete }: TableActionsProps) {
2425
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
2526
const [isDeleting, setIsDeleting] = useState(false);
2627
const [supabase, setSupabase] = useState<any>(null);
28+
const { onOpen: openProfileModal } = useModal();
2729

2830
useEffect(() => {
2931
const supabaseClient = createClientClientComponent();
3032
setSupabase(supabaseClient);
3133
}, []);
3234

35+
// Calculate years from work experience
36+
const calculateYearsFromExperience = (workExperience: any[]): number => {
37+
if (!workExperience || workExperience.length === 0) return 0;
38+
39+
let totalYears = 0;
40+
workExperience.forEach(exp => {
41+
if (exp.date_from) {
42+
const startDate = new Date(exp.date_from);
43+
const endDate = exp.is_present ? new Date() : (exp.date_to ? new Date(exp.date_to) : new Date());
44+
const yearsDiff = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 365.25);
45+
totalYears += Math.max(0, yearsDiff);
46+
}
47+
});
48+
49+
return Math.round(totalYears);
50+
};
51+
52+
// Fetch complete profile and open modal
53+
const handleViewProfile = async () => {
54+
try {
55+
if (!supabase) {
56+
console.error('Supabase client not available');
57+
return;
58+
}
59+
60+
const { data, error } = await supabase
61+
.from("codev")
62+
.select(`
63+
*,
64+
education:education(*),
65+
work_experience:work_experience(*),
66+
projects:project_members(
67+
project:project_id(*)
68+
),
69+
codev_points:codev_points(*)
70+
`)
71+
.eq('id', item.id)
72+
.single();
73+
74+
if (error || !data) {
75+
console.error('Error fetching complete profile:', error);
76+
// Use fallback data
77+
const fallbackProfile: Codev = {
78+
...item,
79+
years_of_experience: item.years_of_experience || 0,
80+
availability_status: item.availability_status ?? true,
81+
internal_status: (item.internal_status || 'GRADUATED') as InternalStatus
82+
};
83+
openProfileModal("profileModal", fallbackProfile);
84+
return;
85+
}
86+
87+
// Handle years_of_experience calculation
88+
let finalYearsOfExperience = data.years_of_experience;
89+
90+
if (finalYearsOfExperience === null || finalYearsOfExperience === undefined) {
91+
finalYearsOfExperience = calculateYearsFromExperience(data.work_experience || []);
92+
}
93+
94+
const safeInternalStatus = data.internal_status as InternalStatus | undefined;
95+
96+
// Create enhanced profile
97+
const enhancedProfile: Codev = {
98+
...data,
99+
years_of_experience: finalYearsOfExperience,
100+
internal_status: safeInternalStatus,
101+
work_experience: data.work_experience || [],
102+
education: data.education || [],
103+
projects: data.projects || [],
104+
codev_points: data.codev_points || [],
105+
tech_stacks: data.tech_stacks || [],
106+
positions: data.positions || []
107+
};
108+
109+
openProfileModal("profileModal", enhancedProfile);
110+
} catch (error) {
111+
console.error('Error in profile click handler:', error);
112+
// Final fallback
113+
const safeFallback: Codev = {
114+
...item,
115+
years_of_experience: item.years_of_experience || 0,
116+
availability_status: item.availability_status ?? true,
117+
internal_status: (item.internal_status || 'GRADUATED') as InternalStatus
118+
};
119+
openProfileModal("profileModal", safeFallback);
120+
}
121+
};
122+
33123
const handleDelete = async () => {
34124
try {
35125
setIsDeleting(true);
@@ -72,7 +162,7 @@ export function TableActions({ item, onEdit, onDelete }: TableActionsProps) {
72162
<Edit2 className="mr-2 h-3 w-3" />
73163
Edit
74164
</DropdownMenuItem>
75-
<DropdownMenuItem className="text-xs">
165+
<DropdownMenuItem onClick={handleViewProfile} className="text-xs">
76166
<Eye className="mr-2 h-3 w-3" />
77167
View Profile
78168
</DropdownMenuItem>

0 commit comments

Comments
 (0)