Skip to content
Merged
231 changes: 227 additions & 4 deletions app/src/pages/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { useCallback, useEffect, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import {
ShieldCheck, LogOut, PlusCircle, AlertCircle, Pencil,
Inbox, ServerCrash, Info,
Inbox, ServerCrash, Info, X,
} from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
import { ComplaintCard } from '../../components/ComplaintCard';
import type { ComplaintPost, EditForm, Role } from '../../components/ComplaintCard';
import { Loader } from '../../components/Loader';
import { BUILDINGS, HOSTELS, DEPARTMENTS, BLOCK_LABELS, BLOCK_TYPES } from '../../constants/models';

interface ProfileData {
name?: string;
Expand Down Expand Up @@ -38,6 +39,14 @@ export function Profile() {
});
const [actionLoading, setActionLoading] = useState<number | null>(null);

const [isEditingProfile, setIsEditingProfile] = useState(false);
const [profileForm, setProfileForm] = useState({
name: '', phone_number: '', building: '', hostel: '',
department: '', house_number: '', block: '', type: '',
});
const [profileSaving, setProfileSaving] = useState(false);
const [profileError, setProfileError] = useState<string | null>(null);

useEffect(() => {
fetch('/api/profile', { credentials: 'include' })
.then((res) => {
Expand Down Expand Up @@ -183,6 +192,61 @@ export function Profile() {
}
}

const profileEditEndpoint = isFaculty
? '/api/faculty/profile/edit'
: isWarden
? '/api/warden/profile/edit'
: '/api/centrehead/profile/edit';

function openProfileEdit() {
setProfileForm({
name: profile.name ?? '',
phone_number: profile.phone_number ?? '',
building: profile.building ?? '',
hostel: profile.hostel ?? '',
department: profile.department ?? '',
house_number: profile.house_number ?? '',
block: profile.block ?? '',
type: profile.type ?? '',
});
setProfileError(null);
setIsEditingProfile(true);
}

async function handleSaveProfile() {
setProfileSaving(true);
setProfileError(null);

const body = isFaculty
? {
name: profileForm.name, phone_number: profileForm.phone_number,
department: profileForm.department, house_number: profileForm.house_number,
block: profileForm.block, type: profileForm.type,
}
: isWarden
? { name: profileForm.name, phone_number: profileForm.phone_number, hostel: profileForm.hostel }
: { phone_number: profileForm.phone_number, building: profileForm.building };

try {
const res = await fetch(profileEditEndpoint, {
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 update profile (${res.status})`);
}
setProfile((prev) => prev ? { ...prev, ...body } : null);
setIsEditingProfile(false);
} catch (err) {
setProfileError((err as Error).message);
} finally {
setProfileSaving(false);
}
}

return (
<MainLayout>
<div className="flex-grow relative">
Expand All @@ -203,7 +267,7 @@ export function Profile() {
<PlusCircle className="w-4 h-4" /> Register Complaint
</Link>
<button
onClick={() => alert('Edit profile functionality coming soon')}
onClick={openProfileEdit}
className="inline-flex items-center gap-2 bg-[#222222] hover:bg-[#000000] text-white text-sm font-semibold px-4 py-2.5 rounded-lg transition-colors duration-200 cursor-pointer"
>
<Pencil className="w-4 h-4" /> Profile
Expand Down Expand Up @@ -309,8 +373,13 @@ export function Profile() {
{!postsLoading && !postsError && posts.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 bg-white rounded-lg border border-dashed border-[#E5E5E5]">
<Inbox className="w-10 h-10 mb-3 text-[#E5E5E5]" />
<span className="text-sm font-semibold text-[#111111]">No complaints filed yet.</span>
<span className="text-xs mt-1 text-[#666666]">Use "Register Complaint" above to file your first one.</span>
<span className="text-sm font-semibold text-[#111111] mb-4">No complaints filed yet.</span>
<Link
to={registerRoute}
className="inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white text-sm font-semibold px-4 py-2.5 rounded-lg transition-colors duration-200 active:scale-[0.98] cursor-pointer"
>
<PlusCircle className="w-4 h-4" /> Register Complaint
</Link>
</div>
)}

Expand All @@ -337,6 +406,160 @@ export function Profile() {

</div>
</div>

{isEditingProfile && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4">
<div className="bg-white border border-[#E5E5E5] rounded-lg w-full max-w-sm">
<div className="flex items-center justify-between px-5 py-4 border-b border-[#E5E5E5]">
<h3 className="text-sm font-bold text-[#111111] uppercase tracking-wide">Edit Profile</h3>
<button
onClick={() => setIsEditingProfile(false)}
className="text-[#666666] hover:text-[#111111] cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>

<div className="px-5 py-4 flex flex-col gap-4">
{(isFaculty || isWarden) && (
<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">Full Name</label>
<input
type="text"
value={profileForm.name}
onChange={(e) => setProfileForm((f) => ({ ...f, name: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
placeholder="Full name"
/>
</div>
)}

<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">Phone Number</label>
<input
type="tel"
value={profileForm.phone_number}
onChange={(e) => setProfileForm((f) => ({ ...f, phone_number: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
placeholder="10-digit phone number"
/>
</div>

{isCentreHead && (
<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">Assigned Building / Centre</label>
<select
value={profileForm.building}
onChange={(e) => setProfileForm((f) => ({ ...f, building: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
>
<option value="" disabled>Select your Building</option>
{BUILDINGS.map((building) => (
<option key={building.value} value={building.value}>{building.label}</option>
))}
</select>
</div>
)}

{isWarden && (
<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">Hostel</label>
<select
value={profileForm.hostel}
onChange={(e) => setProfileForm((f) => ({ ...f, hostel: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
>
<option value="" disabled>Select your Hostel</option>
{HOSTELS.map((hostel) => (
<option key={hostel.value} value={hostel.value}>{hostel.label}</option>
))}
</select>
</div>
)}

{isFaculty && (
<>
<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">Department</label>
<select
value={profileForm.department}
onChange={(e) => setProfileForm((f) => ({ ...f, department: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
>
<option value="" disabled>Select your Department</option>
{DEPARTMENTS.map((dept) => (
<option key={dept.value} value={dept.value}>{dept.label}</option>
))}
</select>
</div>

<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">House Number</label>
<input
type="text"
value={profileForm.house_number}
onChange={(e) => setProfileForm((f) => ({ ...f, house_number: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
placeholder="e.g. 104"
/>
</div>
<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">Block</label>
<select
value={profileForm.block}
onChange={(e) => setProfileForm((f) => ({ ...f, block: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
>
<option value="" disabled>Select Block</option>
{BLOCK_LABELS.map((block) => (
<option key={block.value} value={block.value}>{block.label}</option>
))}
</select>
</div>
</div>

<div>
<label className="block text-sm font-semibold text-[#111111] mb-1.5">Type</label>
<select
value={profileForm.type}
onChange={(e) => setProfileForm((f) => ({ ...f, type: e.target.value }))}
className="w-full px-3.5 py-2.5 border border-[#CCCCCC] rounded-lg focus:outline-none focus:border-[#111111] text-sm text-[#111111] bg-white transition-colors"
>
<option value="" disabled>Select Type</option>
{BLOCK_TYPES.map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
))}
</select>
</div>
</>
)}

{profileError && (
<p className="text-sm text-[#B91C1C]">{profileError}</p>
)}
</div>

<div className="flex items-center justify-end gap-2 px-5 py-4 border-t border-[#E5E5E5]">
<button
onClick={() => setIsEditingProfile(false)}
disabled={profileSaving}
className="text-sm font-semibold px-4 py-2 rounded-lg border border-[#111111] text-[#111111] hover:bg-[#F5F5F5] transition-colors cursor-pointer"
>
Cancel
</button>
<button
onClick={handleSaveProfile}
disabled={profileSaving}
className={`inline-flex items-center gap-2 text-sm font-semibold px-4 py-2 rounded-lg bg-[#222222] hover:bg-[#000000] text-white transition-colors ${profileSaving ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
{profileSaving && <Loader size="sm" color="white" />}
{profileSaving ? 'Saving…' : 'Save Changes'}
</button>
</div>
</div>
</div>
)}
</MainLayout>
);
}
38 changes: 38 additions & 0 deletions handlers/centrehead_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"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"

Expand All @@ -13,6 +14,11 @@ import (
"gorm.io/gorm"
)

type CentreheadProfileEditType struct {
Building string `json:"building"`
PhoneNumber string `json:"phone_number"`
}


// CentreheadSignup registers the head of adminstrations.
// On success, sends a verification email with a JWT token link.
Expand Down Expand Up @@ -198,3 +204,35 @@ func (h *AuthHandler) CentreheadResetPassword(c *gin.Context) {
}
c.JSON(200, gin.H{"success": "password changed successfully", "role": "centrehead"})
}

//CentreheadProfileEdit edits the profile of user type centrehead.
func (h *AuthHandler) CentreheadProfileEdit(c *gin.Context) {
email, ok := c.Get(middleware.EmailKey)
if !ok {
c.JSON(403, gin.H{"error": "unauthorized access!"})
return
}

var head models.Centrehead
result := h.DB.Where("email = ?", email).Take(&head)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
c.JSON(404, gin.H{"error": "user not found"})
}
c.JSON(500, gin.H{"error": "failed to fetch profile"})
return
}

var updatedProfile CentreheadProfileEditType
if err := c.ShouldBindJSON(&updatedProfile); err != nil {
c.JSON(400, gin.H{"error": "invalid request body"})
return
}

result = h.DB.Model(&head).Updates(updatedProfile)
if result.Error != nil {
c.JSON(500, gin.H{"error": "failed to update the profile"})
return
}
c.JSON(200, gin.H{"success": "profile updated successfully!"})
}
Loading
Loading