navigate(`/post/${role}/${post.id}`)}
+ className={`${theme.cardBg} border border-gray-200 rounded-xl overflow-hidden shadow-sm hover:shadow-md hover:border-gray-300 transition-all cursor-pointer group`}
+ >
+ {/* Top accent */}
+
+
+
+ {/* Row 1: badges */}
+
+ #{post.id}
+
+
+ {isElectrical ? : }
+ {post.type_of_post}
+
+
+
+
+ {status.label}
+
+
-
- {isElectrical ? : }
- {post.type_of_post}
-
+ {/* Title + description */}
+
{post.title}
+
{post.description}
-
-
- {status.label}
+ {/* Footer row */}
+
+
+
+ {formatDate(post.created_at)}
-
-
e.stopPropagation()}>
- {!editExpired && (
-
- )}
- {!editExpired && (
-
- )}
-
+ {isFaculty && post.place && (
+
+ {post.place}
+
+ )}
+ {isWarden && post.room_number && (
+
+ {post.room_number}
+
+ )}
- {/* Title + description */}
-
{post.title}
-
{post.description}
-
- {/* Footer row */}
-
-
+
+ {comments.length > 0 && (
- {formatDate(post.created_at)}
+ {comments.length}
- {isFaculty && post.place && (
-
- {post.place}
-
- )}
- {isWarden && post.room_number && (
-
- {post.room_number}
-
- )}
-
-
-
- {comments.length > 0 && (
-
- {comments.length}
-
- )}
-
-
+ )}
+
-
- {/* ── Modal ── */}
- {modalOpen && (
- { onStartEdit(p); }}
- onCancelEdit={() => { onCancelEdit(); }}
- onSaveEdit={(id) => { onSaveEdit(id); }}
- onDelete={(id) => { onDelete(id); setModalOpen(false); }}
- onClose={() => { setModalOpen(false); if (isEditing) onCancelEdit(); }}
- onCommentPosted={onCommentPosted}
- />
- )}
- >
+
);
}
diff --git a/app/src/pages/post/PostView.tsx b/app/src/pages/post/PostView.tsx
new file mode 100644
index 0000000..8361ee3
--- /dev/null
+++ b/app/src/pages/post/PostView.tsx
@@ -0,0 +1,577 @@
+import { useEffect, useState, useCallback, useMemo } from 'react';
+import { useParams, useNavigate, Link } from 'react-router-dom';
+import {
+ Zap, Hammer, Trash2, Pencil, X, Check, Calendar, MapPin, BedDouble,
+ MessageSquare, Wrench, ArrowLeft, Send, AlertCircle,
+ UserCircle, Clock,
+} from 'lucide-react';
+import { MainLayout } from '../../components/layout/MainLayout';
+import { POST_PLACES } from '../../constants/models';
+
+type Role = 'faculty' | 'warden' | 'centrehead';
+
+interface StatusAudit {
+ event: string;
+ timestamp: string;
+}
+
+interface ComplaintComment {
+ id: number;
+ comment_text: string;
+ email: string;
+ role: string;
+ created_at: string;
+}
+
+interface ComplaintPost {
+ id: number;
+ type_of_post: string;
+ title: string;
+ description: string;
+ status: string;
+ stage: string;
+ assigned_je_id: number | null;
+ place?: string;
+ room_number?: string;
+ created_at: string;
+ updated_at?: string;
+ comments?: ComplaintComment[] | null;
+ status_audit_logs?: StatusAudit[] | null;
+}
+
+interface EditForm {
+ title: string;
+ description: string;
+ place: string;
+ room_number: string;
+}
+
+const STATUS_CONFIG: Record
= {
+ pending_xen: { label: 'Pending · XEN', pill: 'bg-amber-100 text-amber-800 border-amber-300', dot: 'bg-amber-400' },
+ pending_ae: { label: 'Pending · AE', pill: 'bg-sky-100 text-sky-800 border-sky-300', dot: 'bg-sky-400' },
+ resolved_ae: { label: 'Resolved · AE', pill: 'bg-teal-100 text-teal-800 border-teal-300', dot: 'bg-teal-400' },
+ pending_je: { label: 'Pending · JE', pill: 'bg-violet-100 text-violet-800 border-violet-300', dot: 'bg-violet-400' },
+ resolved_je: { label: 'Resolved · JE', pill: 'bg-teal-100 text-teal-800 border-teal-300', dot: 'bg-teal-400' },
+ resolved_all: { label: 'Resolved · All', pill: 'bg-emerald-100 text-emerald-800 border-emerald-300', dot: 'bg-emerald-500' },
+};
+
+
+function statusStyle(s: string) {
+ const norm = s.toLowerCase();
+ return STATUS_CONFIG[norm] ?? { label: s.replace(/_/g, ' '), pill: 'bg-gray-100 text-gray-600 border-gray-300', dot: 'bg-gray-400' };
+}
+
+function typeTheme(isElectrical: boolean) {
+ return isElectrical
+ ? { cardBg: 'bg-amber-50', accentBar: 'bg-amber-400', headerBg: 'bg-amber-100/70', iconColor: 'text-amber-600', badge: 'bg-amber-200 text-amber-900 border-amber-400', stageDone: 'bg-amber-500 border-amber-500' }
+ : { cardBg: 'bg-sky-50', accentBar: 'bg-sky-500', headerBg: 'bg-sky-100/70', iconColor: 'text-sky-600', badge: 'bg-sky-200 text-sky-900 border-sky-400', stageDone: 'bg-sky-600 border-sky-600' };
+}
+
+function isEditWindowExpired(createdAt: string): boolean {
+ return Date.now() - new Date(createdAt).getTime() >= 30 * 60 * 1000;
+}
+
+function formatDate(iso: string) {
+ return new Date(iso).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
+}
+
+function formatDateTime(iso: string) {
+ return new Date(iso).toLocaleString('en-IN', {
+ day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit',
+ });
+}
+
+function roleLabel(position: string) {
+ return position.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
+}
+
+export function PostView() {
+ const { role, post_id } = useParams<{ role: Role; post_id: string }>();
+ const navigate = useNavigate();
+
+ const [post, setPost] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [editForm, setEditForm] = useState({
+ title: '', description: '', place: '', room_number: '',
+ });
+ const [isBusy, setIsBusy] = useState(false);
+
+ const [commentText, setCommentText] = useState('');
+ const [commentSubmitting, setCommentSubmitting] = useState(false);
+ const [commentError, setCommentError] = useState(null);
+
+ const fetchPost = useCallback(async () => {
+ try {
+ const res = await fetch(`/api/post/${role}/${post_id}`, { credentials: 'include' });
+ if (!res.ok) {
+ throw new Error(`Failed to fetch post details (${res.status})`);
+ }
+ const data = await res.json();
+ setPost(data.post);
+ } catch (err) {
+ setError((err as Error).message);
+ } finally {
+ setLoading(false);
+ }
+ }, [role, post_id]);
+
+ useEffect(() => {
+ fetchPost();
+ }, [fetchPost]);
+
+ const timelineItems = useMemo(() => {
+ if (!post) return [];
+ const items: Array<
+ | { type: 'comment'; data: ComplaintComment; date: Date }
+ | { type: 'audit'; data: StatusAudit; date: Date }
+ > = [];
+
+ if (post.comments) {
+ post.comments.forEach((c) => {
+ items.push({
+ type: 'comment',
+ data: c,
+ date: new Date(c.created_at),
+ });
+ });
+ }
+
+ if (post.status_audit_logs) {
+ post.status_audit_logs.forEach((log) => {
+ items.push({
+ type: 'audit',
+ data: log,
+ date: new Date(log.timestamp),
+ });
+ });
+ }
+
+ return items.sort((a, b) => a.date.getTime() - b.date.getTime());
+ }, [post]);
+
+ if (loading) {
+ return (
+
+
+
+
+
Loading complaint details…
+
+
+
+ );
+ }
+
+ if (error || !post) {
+ return (
+
+
+
+
+
Error Loading Complaint
+
{error || 'Complaint not found.'}
+
+
Back to Dashboard
+
+
+
+
+ );
+ }
+
+ const isFaculty = role === 'faculty';
+ const isWarden = role === 'warden';
+ const status = statusStyle(post.status);
+ const isElectrical = post.type_of_post === 'Electrical';
+ const theme = typeTheme(isElectrical);
+ const comments = post.comments ?? [];
+ const editExpired = isEditWindowExpired(post.created_at);
+
+ const editBase = isFaculty ? '/api/post/faculty/edit' : isWarden ? '/api/post/warden/edit' : '/api/post/centrehead/edit';
+ const deleteBase = isFaculty ? '/api/post/faculty/delete' : isWarden ? '/api/post/warden/delete' : '/api/post/centrehead/delete';
+
+ function startEdit() {
+ if (!post) return;
+ setIsEditing(true);
+ setEditForm({
+ title: post.title ?? '',
+ description: post.description ?? '',
+ place: post.place ?? '',
+ room_number: post.room_number ?? '',
+ });
+ }
+
+ function cancelEdit() {
+ setIsEditing(false);
+ }
+
+ async function handleSaveEdit() {
+ if (!post) return;
+ if (!window.confirm('Save changes to this complaint?')) return;
+ setIsBusy(true);
+
+ const body: Record = { title: editForm.title, description: editForm.description };
+ if (isFaculty) body.place = editForm.place;
+ if (isWarden) body.room_number = editForm.room_number;
+
+ try {
+ const res = await fetch(`${editBase}/${post.id}`, {
+ method: 'PATCH',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ if (!res.ok) {
+ const b = await res.json().catch(() => ({}));
+ throw new Error(b.error ?? `Failed to save edit (${res.status})`);
+ }
+ setPost((prev) => prev ? { ...prev, ...body } : null);
+ setIsEditing(false);
+ } catch (err) {
+ alert((err as Error).message);
+ } finally {
+ setIsBusy(false);
+ }
+ }
+
+ async function handleDelete() {
+ if (!post) return;
+ if (!window.confirm('Delete this complaint? This cannot be undone.')) return;
+ setIsBusy(true);
+ try {
+ const res = await fetch(`${deleteBase}/${post.id}`, { method: 'DELETE', credentials: 'include' });
+ if (!res.ok) {
+ const b = await res.json().catch(() => ({}));
+ throw new Error(b.error ?? `Failed to delete (${res.status})`);
+ }
+ navigate('/profile');
+ } catch (err) {
+ alert((err as Error).message);
+ } finally {
+ setIsBusy(false);
+ }
+ }
+
+ async function submitComment() {
+ if (!post) return;
+ const content = commentText.trim();
+ if (!content) return;
+ setCommentSubmitting(true);
+ setCommentError(null);
+ try {
+ const res = await fetch(`/api/post/${role}/comment/${post.id}`, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ Content: content }),
+ });
+ if (!res.ok) {
+ let msg = `Failed to post comment (${res.status})`;
+ try { const b = await res.json(); if (b?.error) msg = b.error; } catch {}
+ throw new Error(msg);
+ }
+ setCommentText('');
+ fetchPost();
+ } catch (err) {
+ setCommentError((err as Error).message);
+ } finally {
+ setCommentSubmitting(false);
+ }
+ }
+
+
+ return (
+
+
+
+ {/* Back Link */}
+
+
+
Back to Dashboard
+
+
+ {/* Action buttons */}
+
+ {!isEditing && !editExpired && (
+
+ )}
+ {isEditing && (
+ <>
+
+
+ >
+ )}
+ {!editExpired && (
+
+ )}
+
+
+
+ {/* Post Main Card */}
+
+ {/* Accent strip */}
+
+
+
+
+ {/* Title / Badges */}
+
+
+
+ Complaint #{post.id}
+
+ {isEditing ? (
+
+
+ setEditForm(prev => ({ ...prev, title: e.target.value }))}
+ className="w-full text-base text-gray-800 bg-white border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-gray-400/40 focus:border-gray-600"
+ />
+
+ ) : (
+
{post.title}
+ )}
+
+
+
+
+ {isElectrical ? : }
+ {post.type_of_post}
+
+
+
+ {status.label}
+
+
+
+
+ {/* Description */}
+
+
Description
+ {isEditing ? (
+
+
+
+ {/* Meta Info */}
+
+
+
+
+
Filed on
+
{formatDate(post.created_at)}
+
+
+
+ {isFaculty && (
+
+
+
+
Location
+ {isEditing ? (
+
+ ) : (
+
{post.place}
+ )}
+
+
+ )}
+
+ {isWarden && (
+
+
+
+
Room Number
+ {isEditing ? (
+
setEditForm(prev => ({ ...prev, room_number: e.target.value }))}
+ className="w-full text-xs text-gray-800 bg-white border border-gray-300 rounded px-2 py-0.5 mt-1 focus:outline-none"
+ />
+ ) : (
+
{post.room_number}
+ )}
+
+
+ )}
+
+ {post.assigned_je_id != null && (
+
+
+
+
Assigned JE
+
JE #{post.assigned_je_id}
+
+
+ )}
+
+
+
+
+
+ {/* Comments Section */}
+ {!isEditing && (
+
+
+
+
Official Responses
+ {comments.length > 0 && (
+ {comments.length}
+ )}
+
+
+ {/* Combined Timeline */}
+ {timelineItems.length === 0 ? (
+
+
+
No activity or responses yet
+
+ ) : (
+
+ {timelineItems.map((item, idx) => {
+ if (item.type === 'audit') {
+ const audit = item.data;
+ const normEvent = audit.event.toLowerCase();
+ const dotColor = STATUS_CONFIG[normEvent]?.dot ?? 'bg-gray-400';
+ const eventText = (() => {
+ if (normEvent === 'pending_xen') return 'Sent to XEN for review.';
+ if (normEvent === 'pending_ae') return 'Sent to AE for review.';
+ if (normEvent === 'resolved_ae') return 'Post marked as resolved by AE.';
+ if (normEvent === 'pending_je') return 'Sent to JE for review.';
+ if (normEvent === 'resolved_je') return 'Post marked as resolved by JE.';
+ if (normEvent === 'resolved_all') return 'Post Resolved and closed by XEN.';
+ return `Status updated to ${audit.event.replace(/_/g, ' ')}`;
+ })();
+
+ return (
+
+
+
+
+
+
+ {formatDateTime(audit.timestamp)}
+
+
+ {eventText}
+
+
+
+ );
+ } else {
+ const c = item.data;
+ const author = c.role ? roleLabel(c.role) : 'Staff';
+ const borderCls = theme.accentBar.replace('bg-', 'border-');
+
+ return (
+
+
+
+
+
+
+
+ {author}
+ {c.email && {c.email}}
+
+
+
+ {formatDateTime(c.created_at)}
+
+
+
{c.comment_text}
+
+
+ );
+ }
+ })}
+
+ )}
+
+ {/* Composer */}
+
+
+
+ {commentError && (
+
+ {commentError}
+
+ )}
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/handlers/admin_status.go b/handlers/admin_status.go
index 7ea86b7..50c151c 100644
--- a/handlers/admin_status.go
+++ b/handlers/admin_status.go
@@ -160,7 +160,7 @@ func (h *AdminHandler) AdminFacultyPostStatus(c *gin.Context) {
go func() {
JeToAssign := review.JeToAssign
if err := services.SendPostMailToAdmins(JeToAssign, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send JE mail for post %d: %s", post.ID, err)
return
}
} ()
@@ -180,11 +180,11 @@ func (h *AdminHandler) AdminFacultyPostStatus(c *gin.Context) {
var xen models.Admin
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send AE mail for post %d", post.ID)
+ log.Printf("failed to send XEN mail for post %d", post.ID)
return
}
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send XEN mail for post %d: %s", post.ID, err)
return
}
} ()
@@ -307,10 +307,23 @@ func (h *AdminHandler) AdminFacultyPostStatus(c *gin.Context) {
post.Status = string(PendingJE)
// keep a audit
post.StatusAuditLogs = append(post.StatusAuditLogs, models.StatusAudit{Event: string(PendingJE), TimeStamp: time.Now()})
+ // send mail to je (assigned JE can be changed atp)
+ var je models.Admin
+ if review.JeToAssign != "" {
+ if err := h.DB.Where("email = ?", review.JeToAssign).Take(&je).Error; err == nil {
+ jeID := je.ID
+ post.AssignedJE_ID = &jeID
+ h.DB.Model(&post).Update("assigned_je_id", &jeID)
+ }
+ }
// send mail to je
- // go func() {
-
- // } ()
+ go func() {
+ JeToAssign := review.JeToAssign
+ if err := services.SendPostMailToAdmins(JeToAssign, postURL); err != nil {
+ log.Printf("failed to send JE mail for post %d: %s", post.ID, err)
+ return
+ }
+ } ()
} else if review.Review == string(ResolvedAE) {
post.Status = string(ResolvedAE)
// keep a audit
@@ -327,11 +340,11 @@ func (h *AdminHandler) AdminFacultyPostStatus(c *gin.Context) {
var xen models.Admin
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send AE mail for post %d", post.ID)
+ log.Printf("failed to send XEN mail for post %d", post.ID)
return
}
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send XEN mail for post %d: %s", post.ID, err)
return
}
} ()
@@ -493,7 +506,7 @@ func (h *AdminHandler) AdminWardenPostStatus(c *gin.Context) {
go func() {
JeToAssign := review.JeToAssign
if err := services.SendPostMailToAdmins(JeToAssign, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send JE mail for post %d: %s", post.ID, err)
return
}
} ()
@@ -513,11 +526,11 @@ func (h *AdminHandler) AdminWardenPostStatus(c *gin.Context) {
var xen models.Admin
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send AE mail for post %d", post.ID)
+ log.Printf("failed to send XEN mail for post %d", post.ID)
return
}
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send XEN mail for post %d: %s", post.ID, err)
return
}
} ()
@@ -640,10 +653,23 @@ func (h *AdminHandler) AdminWardenPostStatus(c *gin.Context) {
post.Status = string(PendingJE)
// keep a audit
post.StatusAuditLogs = append(post.StatusAuditLogs, models.StatusAudit{Event: string(PendingJE), TimeStamp: time.Now()})
+ // send mail to je (assigned JE can be changed atp)
+ var je models.Admin
+ if review.JeToAssign != "" {
+ if err := h.DB.Where("email = ?", review.JeToAssign).Take(&je).Error; err == nil {
+ jeID := je.ID
+ post.AssignedJE_ID = &jeID
+ h.DB.Model(&post).Update("assigned_je_id", &jeID)
+ }
+ }
// send mail to je
- // go func() {
- //
- // } ()
+ go func() {
+ JeToAssign := review.JeToAssign
+ if err := services.SendPostMailToAdmins(JeToAssign, postURL); err != nil {
+ log.Printf("failed to send JE mail for post %d: %s", post.ID, err)
+ return
+ }
+ } ()
} else if review.Review == string(ResolvedAE) {
post.Status = string(ResolvedAE)
// keep a audit
@@ -660,11 +686,11 @@ func (h *AdminHandler) AdminWardenPostStatus(c *gin.Context) {
var xen models.Admin
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send AE mail for post %d", post.ID)
+ log.Printf("failed to send XEN mail for post %d", post.ID)
return
}
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send XEN mail for post %d: %s", post.ID, err)
return
}
} ()
@@ -846,11 +872,11 @@ func (h *AdminHandler) AdminCentreheadPostStatus(c *gin.Context) {
var xen models.Admin
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send AE mail for post %d", post.ID)
+ log.Printf("failed to send XEN mail for post %d", post.ID)
return
}
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send XEN mail for post %d: %s", post.ID, err)
return
}
} ()
@@ -973,10 +999,23 @@ func (h *AdminHandler) AdminCentreheadPostStatus(c *gin.Context) {
post.Status = string(PendingJE)
// keep a audit
post.StatusAuditLogs = append(post.StatusAuditLogs, models.StatusAudit{Event: string(PendingJE), TimeStamp: time.Now()})
+ // send mail to je (assigned JE can be changed atp)
+ var je models.Admin
+ if review.JeToAssign != "" {
+ if err := h.DB.Where("email = ?", review.JeToAssign).Take(&je).Error; err == nil {
+ jeID := je.ID
+ post.AssignedJE_ID = &jeID
+ h.DB.Model(&post).Update("assigned_je_id", &jeID)
+ }
+ }
// send mail to je
- // go func() {
-
- // } ()
+ go func() {
+ JeToAssign := review.JeToAssign
+ if err := services.SendPostMailToAdmins(JeToAssign, postURL); err != nil {
+ log.Printf("failed to send JE mail for post %d: %s", post.ID, err)
+ return
+ }
+ } ()
} else if review.Review == string(ResolvedAE) {
post.Status = string(ResolvedAE)
// keep a audit
@@ -993,11 +1032,11 @@ func (h *AdminHandler) AdminCentreheadPostStatus(c *gin.Context) {
var xen models.Admin
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send AE mail for post %d", post.ID)
+ log.Printf("failed to send XEN mail for post %d", post.ID)
return
}
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
- log.Printf("failed to send AE mail for post %d: %s", post.ID, err)
+ log.Printf("failed to send XEN mail for post %d: %s", post.ID, err)
return
}
} ()
diff --git a/handlers/post.go b/handlers/post.go
new file mode 100644
index 0000000..df9c405
--- /dev/null
+++ b/handlers/post.go
@@ -0,0 +1,91 @@
+package handlers
+
+import (
+ "errors"
+ "strconv"
+
+ "github.com/ayush00git/cms-web/middleware"
+ "github.com/ayush00git/cms-web/models"
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// GetPostByID fetches a single post for the logged in user by role and post_id
+func (h *PostHandler) GetPostByID(c *gin.Context) {
+ email, exists := c.Get(middleware.EmailKey)
+ if !exists {
+ c.JSON(401, gin.H{"error": "unauthenticated user"})
+ return
+ }
+
+ role := c.Param("role")
+ postIDString := c.Param("post_id")
+ postIDU64, err := strconv.ParseUint(postIDString, 10, 64)
+ if err != nil {
+ c.JSON(400, gin.H{"error": "failed to parse post_id"})
+ return
+ }
+ postID := uint(postIDU64)
+
+ switch role {
+ case "faculty":
+ var faculty models.Faculty
+ result := h.DB.Where("email = ?", email).Take(&faculty)
+ if result.Error != nil {
+ c.JSON(401, gin.H{"error": "user not found"})
+ return
+ }
+ var post models.FacultyPost
+ result = h.DB.Preload("Comments").Where("id = ? AND faculty_id = ?", postID, faculty.ID).Take(&post)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "requested entry no longer exists"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+ c.JSON(200, gin.H{"success": "post fetched successfully", "post": post})
+
+ case "warden":
+ var warden models.Warden
+ result := h.DB.Where("email = ?", email).Take(&warden)
+ if result.Error != nil {
+ c.JSON(401, gin.H{"error": "user not found"})
+ return
+ }
+ var post models.WardenPost
+ result = h.DB.Preload("Comments").Where("id = ? AND warden_id = ?", postID, warden.ID).Take(&post)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "requested entry no longer exists"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+ c.JSON(200, gin.H{"success": "post fetched successfully", "post": post})
+
+ case "centrehead":
+ var head models.Centrehead
+ result := h.DB.Where("email = ?", email).Take(&head)
+ if result.Error != nil {
+ c.JSON(401, gin.H{"error": "user not found"})
+ return
+ }
+ var post models.CentreheadPost
+ result = h.DB.Preload("Comments").Where("id = ? AND centrehead_id = ?", postID, head.ID).Take(&post)
+ if result.Error != nil {
+ if errors.Is(result.Error, gorm.ErrRecordNotFound) {
+ c.JSON(404, gin.H{"error": "requested entry no longer exists"})
+ return
+ }
+ c.JSON(500, gin.H{"error": "internal server error"})
+ return
+ }
+ c.JSON(200, gin.H{"success": "post fetched successfully", "post": post})
+
+ default:
+ c.JSON(400, gin.H{"error": "undefined role"})
+ }
+}
diff --git a/routes/post.go b/routes/post.go
index 16c9221..50d092e 100644
--- a/routes/post.go
+++ b/routes/post.go
@@ -27,6 +27,7 @@ func PostRoute(e *gin.Engine, h *handlers.PostHandler) {
e.GET("/api/post/faculty", middleware.IsAuthenticated(), h.GetFacultyPosts)
e.GET("/api/post/warden", middleware.IsAuthenticated(), h.GetWardenPosts)
e.GET("/api/post/centrehead", middleware.IsAuthenticated(), h.GetCentreheadPosts)
+ e.GET("/api/post/:role/:post_id", middleware.IsAuthenticated(), h.GetPostByID)
// APIs for comments on the posts
e.POST("/api/post/faculty/comment/:post_id", middleware.IsAuthenticated() ,h.FacultyPostComment)
diff --git a/test/helpers_test.go b/test/helpers_test.go
index ec9a487..a49ed39 100644
--- a/test/helpers_test.go
+++ b/test/helpers_test.go
@@ -148,6 +148,7 @@ func newPostRouter(db *gorm.DB, auth gin.HandlerFunc) *gin.Engine {
e.GET("/api/post/faculty", auth, h.GetFacultyPosts)
e.GET("/api/post/warden", auth, h.GetWardenPosts)
e.GET("/api/post/centrehead", auth, h.GetCentreheadPosts)
+ e.GET("/api/post/:role/:post_id", auth, h.GetPostByID)
e.POST("/api/post/faculty/comment/:post_id", auth, h.FacultyPostComment)
e.POST("/api/post/warden/comment/:post_id", auth, h.WardenPostComment)
diff --git a/test/post_get_test.go b/test/post_get_test.go
new file mode 100644
index 0000000..5aec81f
--- /dev/null
+++ b/test/post_get_test.go
@@ -0,0 +1,112 @@
+package test
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/ayush00git/cms-web/models"
+)
+
+func TestFacultyPost_GetByID_Success(t *testing.T) {
+ db := newTestDB(t)
+ f := seedFaculty(t, db, "fac.getid@iit.ac.in")
+ post := models.FacultyPost{
+ FacultyID: f.ID,
+ Place: models.PlaceDepartmental,
+ TypeOfPost: models.TypeCivil,
+ Title: "Title 1",
+ Description: "Desc 1",
+ }
+ db.Create(&post)
+
+ // Add a comment
+ comment := models.Comment{
+ CommentableID: post.ID,
+ CommentableType: "faculty_posts",
+ Content: "Hello comment",
+ Email: "staff@iit.ac.in",
+ Role: "admin",
+ }
+ db.Create(&comment)
+
+ e := newPostRouter(db, authAs(f.ID, f.Email))
+ rec := doRequest(t, e, http.MethodGet, "/api/post/faculty/1", nil)
+
+ assertStatus(t, rec, 200)
+
+ var res struct {
+ Post models.FacultyPost `json:"post"`
+ }
+ if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+
+ if res.Post.ID != post.ID {
+ t.Fatalf("expected post ID %d, got %d", post.ID, res.Post.ID)
+ }
+ if len(res.Post.Comments) != 1 {
+ t.Fatalf("expected 1 preloaded comment, got %d", len(res.Post.Comments))
+ }
+}
+
+func TestWardenPost_GetByID_Success(t *testing.T) {
+ db := newTestDB(t)
+ w := seedWarden(t, db, "war.getid@iit.ac.in")
+ post := models.WardenPost{
+ WardenID: w.ID,
+ RoomNumber: "B-2",
+ TypeOfPost: models.TypeCivil,
+ Title: "Title 2",
+ Description: "Desc 2",
+ }
+ db.Create(&post)
+
+ e := newPostRouter(db, authAs(w.ID, w.Email))
+ rec := doRequest(t, e, http.MethodGet, "/api/post/warden/1", nil)
+
+ assertStatus(t, rec, 200)
+
+ var res struct {
+ Post models.WardenPost `json:"post"`
+ }
+ if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+
+ if res.Post.ID != post.ID {
+ t.Fatalf("expected post ID %d, got %d", post.ID, res.Post.ID)
+ }
+}
+
+func TestFacultyPost_GetByID_NotFound(t *testing.T) {
+ db := newTestDB(t)
+ f := seedFaculty(t, db, "fac.getidnf@iit.ac.in")
+
+ e := newPostRouter(db, authAs(f.ID, f.Email))
+ rec := doRequest(t, e, http.MethodGet, "/api/post/faculty/999", nil)
+
+ assertStatus(t, rec, 404)
+}
+
+func TestFacultyPost_GetByID_WrongUser(t *testing.T) {
+ db := newTestDB(t)
+ f1 := seedFaculty(t, db, "fac1@iit.ac.in")
+ f2 := seedFaculty(t, db, "fac2@iit.ac.in")
+
+ post := models.FacultyPost{
+ FacultyID: f1.ID,
+ Place: models.PlaceDepartmental,
+ TypeOfPost: models.TypeCivil,
+ Title: "Title 1",
+ Description: "Desc 1",
+ }
+ db.Create(&post)
+
+ // User 2 tries to access User 1's post
+ e := newPostRouter(db, authAs(f2.ID, f2.Email))
+ rec := doRequest(t, e, http.MethodGet, "/api/post/faculty/1", nil)
+
+ // Should not find the post as it filters by user ID
+ assertStatus(t, rec, 404)
+}