diff --git a/.env.exampe b/.env.exampe
index 9122f60..4f63543 100644
--- a/.env.exampe
+++ b/.env.exampe
@@ -4,4 +4,5 @@ DB_NAME=
DB_PORT=
JWT_SECRET=
APP_PASSWORD=
-SENDER_EMAIL=
\ No newline at end of file
+SENDER_EMAIL=
+FRONTEND_URL=
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index d3d4238..0fafb5c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,4 +1,4 @@
-name: Deploy to College Server
+name: Push to production
on:
push:
diff --git a/app/src/pages/admin/AdminPostView.tsx b/app/src/pages/admin/AdminPostView.tsx
index 6f1fb8b..fa9fdc2 100644
--- a/app/src/pages/admin/AdminPostView.tsx
+++ b/app/src/pages/admin/AdminPostView.tsx
@@ -19,6 +19,7 @@ import {
Info,
Check,
Clock,
+ Users,
} from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
@@ -77,6 +78,7 @@ interface FacultyPost {
updated_at: string;
comments: Comment[] | null;
status_audit_logs?: StatusAudit[] | null;
+ people_in_thread?: string[] | null;
}
interface WardenPost {
@@ -94,6 +96,7 @@ interface WardenPost {
updated_at: string;
comments: Comment[] | null;
status_audit_logs?: StatusAudit[] | null;
+ people_in_thread?: string[] | null;
}
interface CentreHeadPost {
@@ -110,6 +113,7 @@ interface CentreHeadPost {
updated_at: string;
comments: Comment[] | null;
status_audit_logs?: StatusAudit[] | null;
+ people_in_thread?: string[] | null;
}
type Post = FacultyPost | WardenPost | CentreHeadPost;
@@ -744,6 +748,26 @@ export function AdminPostView() {
+ {/* People in conversation thread */}
+
+ {post.people_in_thread && post.people_in_thread.length > 0 ? (
+ post.people_in_thread.map((email) => (
+
+
+ {email}
+
+ ))
+ ) : (
+ No one in thread yet
+ )}
+
+
{/* ── Timeline & Comments ── */}
diff --git a/handlers/admin_comment.go b/handlers/admin_comment.go
index 3462ec9..1a647c0 100644
--- a/handlers/admin_comment.go
+++ b/handlers/admin_comment.go
@@ -2,11 +2,15 @@ package handlers
import (
"errors"
+ "fmt"
+ "log"
"strconv"
"time"
+ "github.com/ayush00git/cms-web/helpers"
"github.com/ayush00git/cms-web/middleware"
"github.com/ayush00git/cms-web/models"
+ "github.com/ayush00git/cms-web/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
@@ -102,6 +106,64 @@ func (h *AdminHandler) AdminPostComment(c *gin.Context) {
return
}
+ // update the PeopleInThread field for post
+ switch p := postModel.(type) {
+ case *models.FacultyPost:
+ // add person to conversation thread and send mail to all people in thread
+ p.PeopleInThread = helpers.AppendUnique(p.PeopleInThread, admin.Email)
+ result := h.DB.Save(p)
+ if result.Error != nil {
+ c.JSON(500, gin.H{"error": "failed updating the conversation thread"})
+ return
+ }
+ go func() {
+ frontendURL := helpers.GetEnvWithDefault("FRONTEND_URL", "http://localhost:5173")
+ postURL := fmt.Sprintf(`%s/admin/posts/%s/%d`, frontendURL, "faculty", p.ID)
+
+ if err := services.SendMailToPeopleInThread(p.PeopleInThread, admin.Email, postURL); err != nil {
+ log.Printf("failed sending notification emails for post #%d: %v", p.ID, err)
+ return
+ }
+ log.Printf("notification emails sent for post #%d", p.ID)
+ }()
+
+ case *models.WardenPost:
+ p.PeopleInThread = helpers.AppendUnique(p.PeopleInThread, admin.Email)
+ result := h.DB.Save(p)
+ if result.Error != nil {
+ c.JSON(500, gin.H{"error": "failed updating the conversation thread"})
+ return
+ }
+ go func() {
+ frontendURL := helpers.GetEnvWithDefault("FRONTEND_URL", "http://localhost:5173")
+ postURL := fmt.Sprintf(`%s/admin/posts/%s/%d`, frontendURL, "warden", p.ID)
+
+ if err := services.SendMailToPeopleInThread(p.PeopleInThread, admin.Email, postURL); err != nil {
+ log.Printf("failed sending notification emails for post #%d: %v", p.ID, err)
+ return
+ }
+ log.Printf("notification emails sent for post #%d", p.ID)
+ }()
+
+ case *models.CentreheadPost:
+ p.PeopleInThread = helpers.AppendUnique(p.PeopleInThread, admin.Email)
+ result := h.DB.Save(p)
+ if result.Error != nil {
+ c.JSON(500, gin.H{"error": "failed updating the conversation thread"})
+ return
+ }
+ go func() {
+ frontendURL := helpers.GetEnvWithDefault("FRONTEND_URL", "http://localhost:5173")
+ postURL := fmt.Sprintf(`%s/admin/posts/%s/%d`, frontendURL, "centrehead", p.ID)
+
+ if err := services.SendMailToPeopleInThread(p.PeopleInThread, admin.Email, postURL); err != nil {
+ log.Printf("failed sending notification emails for post #%d: %v", p.ID, err)
+ return
+ }
+ log.Printf("notification emails sent for post #%d", p.ID)
+ }()
+ }
+
doc := models.Comment{
CommentableID: uint(postID),
CommentableType: postType,
diff --git a/handlers/centrehead_post.go b/handlers/centrehead_post.go
index 1a65e89..f3be58b 100644
--- a/handlers/centrehead_post.go
+++ b/handlers/centrehead_post.go
@@ -65,6 +65,7 @@ func (h *PostHandler) CentreheadPost(c *gin.Context) {
TypeOfPost: models.PostType(inputs.TypeOfPost),
Title: inputs.Title,
Description: inputs.Description,
+ PeopleInThread: []string{head.Email},
StatusAuditLogs: []models.StatusAudit{
{
Event: string(PendingXEN),
@@ -92,16 +93,23 @@ func (h *PostHandler) CentreheadPost(c *gin.Context) {
}
// through type of post send the mail to the corresponding civil/electrical XEN
var xen models.Admin
+
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send XEN mail for post %d", post.ID)
+ log.Printf("failed fetching user at the moment %v", result.Error)
return
}
// send mail to that user
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
log.Printf("failed to send XEN mail for post %d: %v", post.ID, err)
}
- }()
+ // append xen's email to the post
+ post.PeopleInThread = append(post.PeopleInThread, xen.Email)
+ result = h.DB.Model(&post).Updates(post)
+ if result.Error != nil {
+ log.Printf("failed adding xen to the thread")
+ }
+ } ()
c.JSON(201, gin.H{"success": "post submitted successfully", "post": post})
}
@@ -278,7 +286,7 @@ func (h *PostHandler) CentreheadPostComment(c *gin.Context) {
// bind the input
var inputs CommentType
if err := c.ShouldBindJSON(&inputs); err != nil {
- c.JSON(401, gin.H{"error": "invalid request body"})
+ c.JSON(400, gin.H{"error": "invalid request body"})
return
}
@@ -298,5 +306,17 @@ func (h *PostHandler) CentreheadPostComment(c *gin.Context) {
return
}
+ // send email notification to people involved in the conversation
+ go func(post models.CentreheadPost, headEmail string) {
+ frontendURL := helpers.GetEnvWithDefault("FRONTEND_URL", "http://localhost:5173")
+ postURL := fmt.Sprintf(`%s/centre-head/post/%d`, frontendURL, post.ID)
+
+ if err := services.SendMailToPeopleInThread(post.PeopleInThread, headEmail, postURL); err != nil {
+ log.Printf("failed sending notification emails for post #%d: %v", post.ID, err)
+ return
+ }
+ log.Printf("notification emails sent for post #%d", post.ID)
+ }(post, head.Email)
+
c.JSON(201, gin.H{"success": "comment posted!"})
}
diff --git a/handlers/faculty_post.go b/handlers/faculty_post.go
index c019890..8131749 100644
--- a/handlers/faculty_post.go
+++ b/handlers/faculty_post.go
@@ -20,22 +20,23 @@ type PostHandler struct {
DB *gorm.DB
}
-// FacultyPostEditType
-type FacultyPostEditType struct {
+// FacultyPostType
+type FacultyPostType struct {
Place string `json:"place"`
+ TypeOfPost string `json:"type_of_post"`
Title string `json:"title"`
Description string `json:"description"`
- UpdatedAt time.Time `json:"updated_at"`
}
-// FacultyPostType
-type FacultyPostType struct {
+// FacultyPostEditType
+type FacultyPostEditType struct {
Place string `json:"place"`
- TypeOfPost string `json:"type_of_post"`
Title string `json:"title"`
Description string `json:"description"`
+ UpdatedAt time.Time `json:"updated_at"`
}
+
// FacultyPost registers the post of faculty members.
// forwards the post to the associated XEN.
func (h *PostHandler) FacultyPost(c *gin.Context) {
@@ -72,6 +73,7 @@ func (h *PostHandler) FacultyPost(c *gin.Context) {
TypeOfPost: models.PostType(inputs.TypeOfPost),
Title: inputs.Title,
Description: inputs.Description,
+ PeopleInThread: []string{faculty.Email},
StatusAuditLogs: []models.StatusAudit{
{
Event: string(PendingXEN),
@@ -90,6 +92,7 @@ func (h *PostHandler) FacultyPost(c *gin.Context) {
frontendURL := helpers.GetEnvWithDefault("FRONTEND_URL", "http://localhost:5173")
postURL := fmt.Sprintf(`%s/admin/posts/%s/%d`, frontendURL, faculty.Role, post.ID)
+ // forward the post creation update to xen
go func() {
var position models.PositionType
if post.TypeOfPost == "Civil" {
@@ -99,16 +102,23 @@ func (h *PostHandler) FacultyPost(c *gin.Context) {
}
// through type of post send the mail to the corresponding civil/electrical XEN
var xen models.Admin
+
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send XEN mail for post %d", post.ID)
+ log.Printf("failed fetching user at the moment %v", result.Error)
return
}
// send mail to that user
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
log.Printf("failed to send XEN mail for post %d: %v", post.ID, err)
}
- }()
+ // append xen's email to the post
+ post.PeopleInThread = append(post.PeopleInThread, xen.Email)
+ result = h.DB.Model(&post).Updates(post)
+ if result.Error != nil {
+ log.Printf("failed adding xen to the thread")
+ }
+ } ()
c.JSON(201, gin.H{"success": "post submitted successfully", "post": post})
}
@@ -289,7 +299,7 @@ func (h *PostHandler) FacultyPostComment(c *gin.Context) {
// bind input to json
var inputs CommentType
if err := c.ShouldBindJSON(&inputs); err != nil {
- c.JSON(401, gin.H{"error": "invalid request body"})
+ c.JSON(400, gin.H{"error": "invalid request body"})
return
}
@@ -309,5 +319,17 @@ func (h *PostHandler) FacultyPostComment(c *gin.Context) {
return
}
+ // send email notification to people involved in the conversation
+ go func(post models.FacultyPost, facultyEmail string) {
+ frontendURL := helpers.GetEnvWithDefault("FRONTEND_URL", "http://localhost:5173")
+ postURL := fmt.Sprintf(`%s/faculty/post/%d`, frontendURL, post.ID)
+
+ if err := services.SendMailToPeopleInThread(post.PeopleInThread, facultyEmail, postURL); err != nil {
+ log.Printf("failed sending notification emails for post #%d: %v", post.ID, err)
+ return
+ }
+ log.Printf("notification emails sent for post #%d", post.ID)
+ }(post, faculty.Email)
+
c.JSON(201, gin.H{"success": "comment posted!"})
}
diff --git a/handlers/warden_post.go b/handlers/warden_post.go
index cfc298b..414c31b 100644
--- a/handlers/warden_post.go
+++ b/handlers/warden_post.go
@@ -69,6 +69,7 @@ func (h *PostHandler) WardenPost(c *gin.Context) {
TypeOfPost: models.PostType(inputs.TypeOfPost),
Title: inputs.Title,
Description: inputs.Description,
+ PeopleInThread: []string{warden.Email},
StatusAuditLogs: []models.StatusAudit{
{
Event: string(PendingXEN),
@@ -96,16 +97,23 @@ func (h *PostHandler) WardenPost(c *gin.Context) {
}
// through type of post send the mail to the corresponding civil/electrical XEN
var xen models.Admin
+
result := h.DB.Where("position = ?", position).Take(&xen)
if result.Error != nil {
- log.Printf("failed to send XEN mail for post %d", post.ID)
+ log.Printf("failed fetching user at the moment %v", result.Error)
return
}
// send mail to that user
if err := services.SendPostMailToAdmins(xen.Email, postURL); err != nil {
log.Printf("failed to send XEN mail for post %d: %v", post.ID, err)
}
- }()
+ // append xen's email to the post
+ post.PeopleInThread = append(post.PeopleInThread, xen.Email)
+ result = h.DB.Model(&post).Updates(post)
+ if result.Error != nil {
+ log.Printf("failed adding xen to the thread")
+ }
+ } ()
c.JSON(201, gin.H{"success": "post submitted successfully", "post": post})
}
@@ -282,7 +290,7 @@ func (h *PostHandler) WardenPostComment(c *gin.Context) {
// bind the input
var inputs CommentType
if err := c.ShouldBindJSON(&inputs); err != nil {
- c.JSON(401, gin.H{"error": "invalid request body"})
+ c.JSON(400, gin.H{"error": "invalid request body"})
return
}
@@ -301,6 +309,18 @@ func (h *PostHandler) WardenPostComment(c *gin.Context) {
c.JSON(500, gin.H{"error": "failed to comment at the moment"})
return
}
-
+
+ // send email notification to people involved in the conversation
+ go func(post models.WardenPost, wardenEmail string) {
+ frontendURL := helpers.GetEnvWithDefault("FRONTEND_URL", "http://localhost:5173")
+ postURL := fmt.Sprintf(`%s/warden/post/%d`, frontendURL, post.ID)
+
+ if err := services.SendMailToPeopleInThread(post.PeopleInThread, wardenEmail, postURL); err != nil {
+ log.Printf("failed sending notification emails for post #%d: %v", post.ID, err)
+ return
+ }
+ log.Printf("notification emails sent for post #%d", post.ID)
+ }(post, warden.Email)
+
c.JSON(201, gin.H{"success": "comment posted!"})
}
diff --git a/helpers/append_unique.go b/helpers/append_unique.go
new file mode 100644
index 0000000..7fefc7e
--- /dev/null
+++ b/helpers/append_unique.go
@@ -0,0 +1,15 @@
+package helpers
+
+
+// AppendUnique is a helper method which acts as a corollary
+// to the `.append` method of strings and avoids storing
+// duplicate emails to the PersonInThread slice
+// restoring the set property.
+func AppendUnique(emails []string, email string) []string {
+ for _, e := range emails {
+ if email == e {
+ return emails;
+ }
+ }
+ return append(emails, email)
+}
diff --git a/models/post.go b/models/post.go
index 2e84729..08efca8 100644
--- a/models/post.go
+++ b/models/post.go
@@ -47,6 +47,7 @@ type FacultyPost struct {
TypeOfPost PostType `gorm:"type:varchar(20);not null" json:"type_of_post" binding:"required"`
Title string `gorm:"type:varchar(50);not null" json:"title" binding:"required"`
Description string `gorm:"type:text;not null" json:"description" binding:"required"`
+ PeopleInThread []string `gorm:"serializer:json;" json:"people_in_thread"`
Status string `gorm:"type:varchar(20);not null;default:'pending_xen'" json:"status"`
StatusAuditLogs []StatusAudit `gorm:"serializer:json;" json:"status_audit_logs"`
AssignedJE_ID *uint `json:"assigned_je_id"`
@@ -64,6 +65,7 @@ type WardenPost struct {
TypeOfPost PostType `gorm:"type:varchar(20);not null" json:"type_of_post" binding:"required"`
Title string `gorm:"not null" json:"title" binding:"required"`
Description string `gorm:"type:text;not null" json:"description" binding:"required"`
+ PeopleInThread []string `gorm:"serializer:json;" json:"people_in_thread"`
Status string `gorm:"type:varchar(20);not null;default:'pending_xen'" json:"status"`
StatusAuditLogs []StatusAudit `gorm:"serializer:json;" json:"status_audit_logs"`
AssignedJE_ID *uint `json:"assigned_je_id"`
@@ -80,6 +82,7 @@ type CentreheadPost struct {
TypeOfPost PostType `gorm:"type:varchar(20);not null" json:"type_of_post" binding:"required"`
Title string `gorm:"not null" json:"title" binding:"required"`
Description string `gorm:"type:text;not null" json:"description" binding:"required"`
+ PeopleInThread []string `gorm:"serializer:json;" json:"people_in_thread"`
Status string `gorm:"type:varchar(20);not null;default:'pending_xen'" json:"status"`
StatusAuditLogs []StatusAudit `gorm:"serializer:json;" json:"status_audit_logs"`
AssignedJE_ID *uint `json:"assigned_je_id"`
diff --git a/services/email.go b/services/email.go
index a24037c..fc15798 100644
--- a/services/email.go
+++ b/services/email.go
@@ -112,7 +112,6 @@ func SendPasswordResetMail(userID uint, email, role string) error {
}
func SendPostMailToAdmins(email, postURL string) error {
- // send the email
mail := fmt.Sprintf(`
@@ -124,14 +123,14 @@ func SendPostMailToAdmins(email, postURL string) error {
-
Reset your password
+
cms: updates on your recent complaint
Reset!