Skip to content

Commit 16743f8

Browse files
committed
Merge branch 'dev'
2 parents fb5c86e + 76457cc commit 16743f8

9 files changed

Lines changed: 2314 additions & 21 deletions

File tree

‎apps/codebility/app/home/hire/_components/JobListingsTable.tsx‎

Lines changed: 174 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
MapPin,
1313
DollarSign,
1414
User,
15-
MoreHorizontal
15+
MoreHorizontal,
16+
CheckCircle,
17+
XCircle,
18+
RotateCcw
1619
} from "lucide-react";
1720
import { Button } from "@/components/ui/button";
1821
import { Badge } from "@codevs/ui/badge";
@@ -36,7 +39,7 @@ import EditJobModal from "./EditJobModal";
3639
import { createClientClientComponent } from "@/utils/supabase/client";
3740
import { Skeleton } from "@/components/ui/skeleton/skeleton";
3841
import { useUserStore } from "@/store/codev-store";
39-
import { deleteJobListing } from "../actions";
42+
import { deleteJobListing, updateJobListingStatus } from "../actions";
4043

4144
import {
4245
Accordion,
@@ -46,6 +49,7 @@ import {
4649
} from "@codevs/ui/accordion"
4750
interface JobWithApplicationCount extends JobListing {
4851
application_count?: number;
52+
status?: "active" | "closed" | "draft";
4953
created_by_details?: {
5054
id: string;
5155
first_name: string;
@@ -62,10 +66,17 @@ export default function JobListingsTable() {
6266
const [loading, setLoading] = useState(true);
6367
const [editingJob, setEditingJob] = useState<JobListing | null>(null);
6468
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
69+
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'closed' | 'draft'>('all');
6570

6671
// Check if user is admin (role_id 1 or 4)
6772
const isAdmin = user?.role_id === 1 || user?.role_id === 4;
6873

74+
// Filter jobs based on status
75+
const filteredJobs = jobs.filter(job => {
76+
if (statusFilter === 'all') return true;
77+
return (job.status || 'active') === statusFilter;
78+
});
79+
6980
useEffect(() => {
7081
fetchJobListings();
7182
}, []);
@@ -183,6 +194,38 @@ export default function JobListingsTable() {
183194
router.push(`/home/hire/applications/${jobId}`);
184195
};
185196

197+
const handleStatusChange = async (jobId: string, newStatus: "active" | "closed" | "draft") => {
198+
try {
199+
const result = await updateJobListingStatus(jobId, newStatus);
200+
201+
if (!result.success) {
202+
throw new Error(result.error || "Failed to update job status");
203+
}
204+
205+
// Update local state
206+
setJobs(prevJobs =>
207+
prevJobs.map(job =>
208+
job.id === jobId
209+
? { ...job, status: newStatus }
210+
: job
211+
)
212+
);
213+
214+
const statusText = newStatus === 'closed' ? 'closed' : 'reopened';
215+
toast({
216+
title: `Job ${statusText}`,
217+
description: `The job listing has been ${statusText} successfully.`,
218+
});
219+
} catch (error) {
220+
console.error('Status update error:', error);
221+
toast({
222+
title: "Failed to update status",
223+
description: error instanceof Error ? error.message : "Please try again later.",
224+
variant: "destructive",
225+
});
226+
}
227+
};
228+
186229
const getLevelColor = (level: string) => {
187230
switch (level) {
188231
case "Entry":
@@ -213,6 +256,19 @@ export default function JobListingsTable() {
213256
}
214257
};
215258

259+
const getStatusColor = (status: string) => {
260+
switch (status) {
261+
case "active":
262+
return "bg-green-500/10 text-green-400 border-green-500/20";
263+
case "closed":
264+
return "bg-red-500/10 text-red-400 border-red-500/20";
265+
case "draft":
266+
return "bg-gray-500/10 text-gray-400 border-gray-500/20";
267+
default:
268+
return "bg-gray-500/10 text-muted-foreground border-gray-500/20";
269+
}
270+
};
271+
216272
if (loading) {
217273
return (
218274
<div className="rounded-lg border border-gray-800 bg-gray-900/50 overflow-hidden p-8">
@@ -236,22 +292,58 @@ export default function JobListingsTable() {
236292
}
237293

238294
return (
239-
<div className="rounded-lg border bg-card dark:bg-blue-50/5 overflow-hidden">
295+
<div className="space-y-4">
296+
{/* Filter Bar */}
297+
<div className="flex items-center gap-2 p-4 bg-card dark:bg-blue-50/5 rounded-lg border">
298+
<span className="text-sm font-medium text-foreground mr-2">Filter by status:</span>
299+
<div className="flex gap-2">
300+
{[
301+
{ value: 'all', label: 'All', count: jobs.length },
302+
{ value: 'active', label: 'Active', count: jobs.filter(j => (j.status || 'active') === 'active').length },
303+
{ value: 'closed', label: 'Closed', count: jobs.filter(j => j.status === 'closed').length },
304+
{ value: 'draft', label: 'Draft', count: jobs.filter(j => j.status === 'draft').length }
305+
].map(({ value, label, count }) => (
306+
<Button
307+
key={value}
308+
variant={statusFilter === value ? "default" : "outline"}
309+
size="sm"
310+
onClick={() => setStatusFilter(value as 'all' | 'active' | 'closed' | 'draft')}
311+
className="text-xs"
312+
>
313+
{label} ({count})
314+
</Button>
315+
))}
316+
</div>
317+
</div>
318+
319+
{/* Jobs Table */}
320+
{filteredJobs.length === 0 ? (
321+
<div className="rounded-lg border bg-card dark:border-gray-800 dark:bg-gray-900/50 p-12 text-center">
322+
<p className="text-muted-foreground">
323+
{statusFilter === 'all'
324+
? "No job listings yet. Create your first job listing to get started."
325+
: `No ${statusFilter} job listings found.`
326+
}
327+
</p>
328+
</div>
329+
) : (
330+
<div className="rounded-lg border bg-card dark:bg-blue-50/5 overflow-hidden">
240331
<div className="hidden xl:block">
241332
<Table className="border dark:border-gray-700">
242333
<TableHeader>
243334
<TableRow className="border-border hover:bg-muted/50 transition-colors dark:border-gray-700">
244335
<TableHead className="text-foreground">Position</TableHead>
245336
<TableHead className="text-foreground">Location</TableHead>
246337
<TableHead className="text-foreground">Type</TableHead>
338+
<TableHead className="text-foreground">Status</TableHead>
247339
<TableHead className="text-foreground text-center">Applications</TableHead>
248340
<TableHead className="text-foreground">Created By</TableHead>
249341
<TableHead className="text-foreground">Posted</TableHead>
250342
<TableHead className="text-foreground text-right min-w-[200px]">Actions</TableHead>
251343
</TableRow>
252344
</TableHeader>
253345
<TableBody>
254-
{jobs.map((job) => (
346+
{filteredJobs.map((job) => (
255347
<TableRow key={job.id} className="border-border hover:bg-muted/30 transition-colors dark:border-gray-700">
256348
<TableCell>
257349
<div>
@@ -279,6 +371,11 @@ export default function JobListingsTable() {
279371
{job.type}
280372
</Badge>
281373
</TableCell>
374+
<TableCell>
375+
<Badge variant="outline" className={getStatusColor(job.status || 'active')}>
376+
{job.status || 'active'}
377+
</Badge>
378+
</TableCell>
282379
<TableCell className="text-center">
283380
<button
284381
onClick={() => handleViewApplications(job.id)}
@@ -328,20 +425,46 @@ export default function JobListingsTable() {
328425
</Button>
329426
</DropdownMenuTrigger>
330427
<DropdownMenuContent align="end" className="bg-popover border-border">
331-
<DropdownMenuItem
332-
onClick={() => handleEdit(job)}
333-
className="text-foreground hover:text-foreground hover:bg-accent"
334-
>
335-
<Edit className="mr-2 h-4 w-4" />
336-
Edit Listing
337-
</DropdownMenuItem>
338-
<DropdownMenuItem
339-
onClick={() => handleDelete(job.id)}
340-
className="text-destructive hover:text-destructive hover:bg-accent "
341-
>
342-
<Trash2 className="mr-2 h-4 w-4" />
343-
Delete
344-
</DropdownMenuItem>
428+
{(isAdmin || job.created_by?.id === user?.id) && (
429+
<>
430+
<DropdownMenuItem
431+
onClick={() => handleEdit(job)}
432+
className="text-foreground hover:text-foreground hover:bg-accent"
433+
>
434+
<Edit className="mr-2 h-4 w-4" />
435+
Edit Listing
436+
</DropdownMenuItem>
437+
{job.status === 'active' ? (
438+
<DropdownMenuItem
439+
onClick={() => handleStatusChange(job.id, 'closed')}
440+
className="text-orange-500 hover:text-orange-400 hover:bg-accent"
441+
>
442+
<XCircle className="mr-2 h-4 w-4" />
443+
Mark as Closed
444+
</DropdownMenuItem>
445+
) : (
446+
<DropdownMenuItem
447+
onClick={() => handleStatusChange(job.id, 'active')}
448+
className="text-green-500 hover:text-green-400 hover:bg-accent"
449+
>
450+
<RotateCcw className="mr-2 h-4 w-4" />
451+
Reopen Position
452+
</DropdownMenuItem>
453+
)}
454+
<DropdownMenuItem
455+
onClick={() => handleDelete(job.id)}
456+
className="text-destructive hover:text-destructive hover:bg-accent "
457+
>
458+
<Trash2 className="mr-2 h-4 w-4" />
459+
Delete
460+
</DropdownMenuItem>
461+
</>
462+
)}
463+
{!isAdmin && job.created_by?.id !== user?.id && (
464+
<DropdownMenuItem disabled className="text-gray-500">
465+
No actions available
466+
</DropdownMenuItem>
467+
)}
345468
</DropdownMenuContent>
346469
</DropdownMenu>
347470
</div>
@@ -355,7 +478,7 @@ export default function JobListingsTable() {
355478
{/* Mobile Accordion */}
356479
<div className="xl:hidden">
357480
<Accordion type="single" collapsible className="w-full">
358-
{jobs.map((job) => (
481+
{filteredJobs.map((job) => (
359482
<AccordionItem key={job.id} value={job.id}>
360483
<AccordionTrigger className="px-4 py-3 text-left [&>svg]:text-foreground">
361484
<div className="w-full">
@@ -368,6 +491,9 @@ export default function JobListingsTable() {
368491
<Badge variant="outline" className={getTypeColor(job.type)} >
369492
{job.type}
370493
</Badge>
494+
<Badge variant="outline" className={getStatusColor(job.status || 'active')} >
495+
{job.status || 'active'}
496+
</Badge>
371497
{job.remote && (
372498
<Badge variant="outline" className="bg-green-500/10 text-green-400 border-green-500/20" >
373499
Remote
@@ -496,7 +622,7 @@ export default function JobListingsTable() {
496622
</Button>
497623
</DropdownMenuTrigger>
498624
<DropdownMenuContent align="end" className="bg-popover border-border dark:bg-gray-900 dark:border-gray-800">
499-
{(isAdmin || job.created_by === user?.id) && (
625+
{(isAdmin || job.created_by?.id === user?.id) && (
500626
<>
501627
<DropdownMenuItem
502628
onClick={(e) => {
@@ -509,6 +635,31 @@ export default function JobListingsTable() {
509635
<Edit className="mr-2 h-4 w-4" />
510636
Edit Listing
511637
</DropdownMenuItem>
638+
{job.status === 'active' ? (
639+
<DropdownMenuItem
640+
onClick={(e) => {
641+
e.preventDefault();
642+
e.stopPropagation();
643+
handleStatusChange(job.id, 'closed');
644+
}}
645+
className="text-orange-400 hover:text-orange-300 hover:bg-gray-800 cursor-pointer"
646+
>
647+
<XCircle className="mr-2 h-4 w-4" />
648+
Mark as Closed
649+
</DropdownMenuItem>
650+
) : (
651+
<DropdownMenuItem
652+
onClick={(e) => {
653+
e.preventDefault();
654+
e.stopPropagation();
655+
handleStatusChange(job.id, 'active');
656+
}}
657+
className="text-green-400 hover:text-green-300 hover:bg-gray-800 cursor-pointer"
658+
>
659+
<RotateCcw className="mr-2 h-4 w-4" />
660+
Reopen Position
661+
</DropdownMenuItem>
662+
)}
512663
<DropdownMenuItem
513664
onClick={(e) => {
514665
e.preventDefault();
@@ -522,7 +673,7 @@ export default function JobListingsTable() {
522673
</DropdownMenuItem>
523674
</>
524675
)}
525-
{!isAdmin && job.created_by !== user?.id && (
676+
{!isAdmin && job.created_by?.id !== user?.id && (
526677
<DropdownMenuItem disabled className="text-gray-500">
527678
No actions available
528679
</DropdownMenuItem>
@@ -543,6 +694,8 @@ export default function JobListingsTable() {
543694
onClose={handleEditModalClose}
544695
onJobUpdated={handleJobUpdated}
545696
/>
697+
</div>
698+
)}
546699
</div>
547700
);
548701
}

‎apps/codebility/app/home/hire/actions.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,70 @@ export async function deleteJobApplication(applicationId: string, jobId: string)
277277
console.error("Server action error:", error);
278278
return { success: false, error: "An unexpected error occurred" };
279279
}
280+
}
281+
282+
export async function updateJobListingStatus(
283+
jobId: string,
284+
status: "active" | "closed" | "draft"
285+
) {
286+
try {
287+
const supabase = await createClientServerComponent();
288+
289+
// Get the current user
290+
const { data: { user } } = await supabase.auth.getUser();
291+
if (!user) {
292+
return { success: false, error: "Unauthorized" };
293+
}
294+
295+
// Get user details to check role
296+
const { data: userData } = await supabase
297+
.from('codev')
298+
.select('id, role_id')
299+
.eq('id', user.id)
300+
.single();
301+
302+
if (!userData) {
303+
return { success: false, error: "User not found" };
304+
}
305+
306+
// Check if user is admin (role_id 1 or 4)
307+
const isAdmin = userData.role_id === 1 || userData.role_id === 4;
308+
309+
// Get the job to check ownership
310+
const { data: job } = await supabase
311+
.from('job_listings')
312+
.select('created_by, status')
313+
.eq('id', jobId)
314+
.single();
315+
316+
if (!job) {
317+
return { success: false, error: "Job not found" };
318+
}
319+
320+
// Check permissions
321+
if (!isAdmin && job.created_by !== userData.id) {
322+
return { success: false, error: "You don't have permission to update this job" };
323+
}
324+
325+
// Update the job status
326+
const { data: updatedJob, error } = await supabase
327+
.from('job_listings')
328+
.update({
329+
status,
330+
updated_at: new Date().toISOString(),
331+
})
332+
.eq('id', jobId)
333+
.select()
334+
.single();
335+
336+
if (error) {
337+
console.error("Update status error:", error);
338+
return { success: false, error: error.message };
339+
}
340+
341+
return { success: true, data: updatedJob };
342+
} catch (error) {
343+
console.error("Server action error:", error);
344+
return { success: false, error: "An unexpected error occurred" };
345+
}
280346
}

0 commit comments

Comments
 (0)