diff --git a/.github/workflows/keep-supabase-alive.yml b/.github/workflows/keep-supabase-alive.yml new file mode 100644 index 0000000..defe8ec --- /dev/null +++ b/.github/workflows/keep-supabase-alive.yml @@ -0,0 +1,73 @@ +name: Keep Supabase Alive + +on: + schedule: + # Runs every 3 days at 00:00 UTC to prevent 7-day inactivity pause + - cron: '0 0 */3 * *' + workflow_dispatch: # Allows manual trigger anytime from GitHub UI + +jobs: + ping-supabase: + name: Ping Supabase Database + runs-on: ubuntu-latest + + steps: + - name: Send Keep-Alive Ping to Supabase + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + APP_URL: ${{ secrets.APP_URL }} + run: | + echo "==========================================" + echo " Supabase Keep-Alive Health Ping " + echo "==========================================" + + SUCCESS=0 + + # 1. Ping Supabase REST API directly if secrets exist + if [ -n "$SUPABASE_URL" ] && [ -n "$SUPABASE_ANON_KEY" ]; then + echo "Pinging Supabase REST API: $SUPABASE_URL/rest/v1/users?select=id&limit=1" + + RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" \ + -H "apikey: $SUPABASE_ANON_KEY" \ + -H "Authorization: Bearer $SUPABASE_ANON_KEY" \ + "$SUPABASE_URL/rest/v1/users?select=id&limit=1") + + HTTP_CODE=$(echo "$RESPONSE" | grep "HTTP_CODE:" | cut -d':' -f2) + BODY=$(echo "$RESPONSE" | grep -v "HTTP_CODE:") + + echo "Response Status: $HTTP_CODE" + echo "Response Body: $BODY" + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 400 ]; then + echo "✅ Supabase REST API successfully pinged!" + SUCCESS=1 + else + echo "❌ Direct Supabase REST ping failed with HTTP $HTTP_CODE" + fi + else + echo "⚠️ SUPABASE_URL or SUPABASE_ANON_KEY secret is not set in GitHub repository secrets." + fi + + # 2. Ping deployed Next.js Keep-Alive endpoint if APP_URL is provided + if [ -n "$APP_URL" ]; then + echo "------------------------------------------" + echo "Pinging Next.js endpoint: $APP_URL/api/keep-alive" + + APP_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" "$APP_URL/api/keep-alive") + APP_HTTP_CODE=$(echo "$APP_RESPONSE" | grep "HTTP_CODE:" | cut -d':' -f2) + APP_BODY=$(echo "$APP_RESPONSE" | grep -v "HTTP_CODE:") + + echo "App Response Status: $APP_HTTP_CODE" + echo "App Response Body: $APP_BODY" + + if [ "$APP_HTTP_CODE" -ge 200 ] && [ "$APP_HTTP_CODE" -lt 400 ]; then + echo "✅ Next.js keep-alive endpoint successfully pinged!" + SUCCESS=1 + fi + fi + + if [ $SUCCESS -eq 0 ] && [ -z "$SUPABASE_URL" ] && [ -z "$APP_URL" ]; then + echo "❌ ERROR: No secrets configured! Please add SUPABASE_URL & SUPABASE_ANON_KEY or APP_URL to GitHub Repository Secrets." + exit 1 + fi diff --git a/app/admin/analytics/page.tsx b/app/admin/analytics/page.tsx index 15e8106..6eba48c 100644 --- a/app/admin/analytics/page.tsx +++ b/app/admin/analytics/page.tsx @@ -24,7 +24,7 @@ const COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899' // Reusable animated orb for ambient background function Orb({ className }: { className: string }) { - return
; + return
; } export default function AnalyticsDashboard() { @@ -225,10 +225,10 @@ export default function AnalyticsDashboard() {
-
-
+
+
-

Initializing Analytics

+

Initializing Analytics

); @@ -305,7 +305,7 @@ export default function AnalyticsDashboard() {
{/* Ambient Background */} -
+
@@ -316,13 +316,13 @@ export default function AnalyticsDashboard() { {/* Header */}
- +
-
- - Command Center +
+ + Command Center

Analytics @@ -333,7 +333,7 @@ export default function AnalyticsDashboard() {
{/* Tabs */} -
+
{[ { id: 'overview', label: 'Overview' }, { id: 'bookings', label: 'Bookings' }, @@ -342,7 +342,7 @@ export default function AnalyticsDashboard() { @@ -351,13 +351,13 @@ export default function AnalyticsDashboard() { {/* Filters + Export Bar */}
-
+
{/* Department Selector */}
-
+
@@ -451,7 +451,7 @@ export default function AnalyticsDashboard() { href={googleSheetUrl} target="_blank" rel="noreferrer" - className="bg-emerald-500 text-white px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-600 transition-all shadow-lg shadow-emerald-500/20" + className="bg-emerald-500 text-white px-4 py-2 rounded-none text-[10px] font-black uppercase tracking-widest hover:bg-emerald-600 transition-all border-2 border-foreground shadow-[4px_4px_0_0_rgba(0,0,0,1)] dark:shadow-[4px_4px_0_0_rgba(255,255,255,0.2)] shadow-emerald-500/20" > Open Sheet @@ -460,7 +460,7 @@ export default function AnalyticsDashboard() { )} {error && ( - +

{error}

@@ -496,7 +496,7 @@ export default function AnalyticsDashboard() {
{/* Booking Trends */} - +

Recent Activity @@ -526,8 +526,8 @@ export default function AnalyticsDashboard() { {/* Booking Status */} - -
+ +

Status Mix

@@ -556,7 +556,7 @@ export default function AnalyticsDashboard() {
{bookingStatusData.slice(0, 4).map((item, i) => (
-
+
{item.name}
))} @@ -564,17 +564,17 @@ export default function AnalyticsDashboard() { {/* Maintenance Summary */} - +

Maintenance

-
+

Avg. Resolution

{rechartsMaintenance?.avgCompletionTimeHours}h

{rechartsMaintenance?.topMaintainedResources.slice(0, 3).map((res: any, i: number) => ( -
+

{res.name}

{res.count} Issues
@@ -583,7 +583,7 @@ export default function AnalyticsDashboard() { {/* Categories */} - +

Asset Distribution @@ -612,7 +612,7 @@ export default function AnalyticsDashboard() { {activeTab === 'bookings' && (
-
+

Booking Statistics

@@ -640,7 +640,7 @@ export default function AnalyticsDashboard() { {activeTab === 'utilization' && (
-
+

Peak Usage & Efficiency

@@ -652,13 +652,13 @@ export default function AnalyticsDashboard() { } loading={reportsLoading}>
- +

Detailed Resource Utilization

{reportsLoading ? ( -
+
) : (
@@ -679,18 +679,18 @@ export default function AnalyticsDashboard() { + @@ -460,7 +521,7 @@ function UserManagementPageContent() { @@ -480,11 +541,34 @@ function UserManagementPageContent() { +
{res.name} - {res.type} + {res.type} {res.totalBookings} {res.totalHours}h
-
+
70 ? 'bg-red-500' : res.utilizationRate > 30 ? 'bg-amber-500' : 'bg-emerald-500'}`} + className={`h-full rounded-none ${res.utilizationRate > 70 ? 'bg-red-500' : res.utilizationRate > 30 ? 'bg-amber-500' : 'bg-emerald-500'}`} />
{res.utilizationRate}% @@ -722,7 +722,7 @@ function TimeRangePicker({ value, onChange, mini }: { value: string, onChange: ( ]; return ( -
+
{options.map((opt) => ( @@ -138,23 +138,23 @@ export default function ReportSchedulingPage() {
{loading ? (
-
+

Syncing schedules...

) : schedules.length === 0 ? ( -
+

No Active Schedules

Configure automated reports to be delivered to your inbox every week.

@@ -168,19 +168,19 @@ export default function ReportSchedulingPage() { animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1 }} key={s.id} - className="bg-slate-50 dark:bg-white/5 backdrop-blur-xl rounded-3xl p-6 border border-slate-200 dark:border-white/10 hover:border-cyan-500/50 transition-all group relative overflow-hidden shadow-lg" + className="bg-slate-50 dark:bg-white/5 rounded-none p-6 border border-slate-200 dark:border-white/10 hover:border-cyan-500/50 transition-all group relative overflow-hidden border-2 border-foreground shadow-[4px_4px_0_0_rgba(0,0,0,1)] dark:shadow-[4px_4px_0_0_rgba(255,255,255,0.2)]" > {/* Decorative gradient */} -
+
-
+
@@ -200,13 +200,13 @@ export default function ReportSchedulingPage() {
-
+

Delivery Day

{DAYS[s.delivery_day]}
-
+

Timing

{s.delivery_time.substring(0, 5)} @@ -214,14 +214,14 @@ export default function ReportSchedulingPage() {
-
+

Format & Recipients

{s.format === 'pdf' ? : } {s.format}
-
+
{s.recipients.length} Recipients
@@ -234,13 +234,13 @@ export default function ReportSchedulingPage() { setEditingSchedule(s); setShowModal(true); }} - className="flex-1 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:bg-white/10 text-white font-medium text-xs py-2.5 rounded-xl transition-all border border-slate-200 dark:border-white/10" + className="flex-1 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:bg-white/10 text-white font-medium text-xs py-2.5 rounded-none transition-all border border-slate-200 dark:border-white/10" > Edit Configuration @@ -343,7 +343,7 @@ function ScheduleModal({ schedule, onClose, onSuccess, reportTypes, days, userTo initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-[#0B0F19]/80 backdrop-blur-md" + className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-[#0B0F19]/80 " > {schedule ? 'Edit Schedule' : 'New Report Schedule'}

Configure your automated reporting cycle

-
@@ -371,13 +371,13 @@ function ScheduleModal({ schedule, onClose, onSuccess, reportTypes, days, userTo
@@ -404,11 +462,13 @@ function UserManagementPageContent() { searchTerm, roleFilter, deptFilter, + approvalFilter, }} onLoadFilters={(filters) => { if (filters.searchTerm !== undefined) setSearchTerm(filters.searchTerm); if (filters.roleFilter !== undefined) setRoleFilter(filters.roleFilter); if (filters.deptFilter !== undefined) setDeptFilter(filters.deptFilter); + if (filters.approvalFilter !== undefined) setApprovalFilter(filters.approvalFilter); updateUrlParams(1, pageSize); }} /> @@ -421,7 +481,7 @@ function UserManagementPageContent() { "users" ); }} - className="inline-flex items-center gap-2 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/10 active:scale-95 text-slate-700 dark:text-foreground/80 font-semibold px-4 py-2.5 rounded-xl shadow-sm transition-all duration-200 text-sm" + className="inline-flex items-center gap-2 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/10 active:scale-95 text-slate-700 dark:text-foreground/80 font-semibold px-4 py-2.5 rounded-none shadow-sm transition-all duration-200 text-sm" > Export CSV @@ -429,7 +489,7 @@ function UserManagementPageContent() {
{/* Users Directory List */} -
+
{loading ? (
@@ -451,6 +511,7 @@ function UserManagementPageContent() {
Role Faculty / Department PhoneStatus Joined Date Actions
-
+
{item.name.charAt(0).toUpperCase()}
{item.name} @@ -470,7 +531,7 @@ function UserManagementPageContent() { {item.email}
- + {item.role} {item.phone || "—"} + + {item.approval_status || "Approved"} + + {new Date(item.created_at).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })}
+ {item.approval_status !== "Approved" && ( + + )} + {item.approval_status !== "Rejected" && ( + + )}
@@ -593,7 +677,7 @@ function UserManagementPageContent() { + {/* Approval Status (Edit mode only) */} + {editModalOpen && ( +
+ + +
+ )} + {/* Faculty / Dept */}
@@ -609,7 +709,7 @@ function UserManagementPageContent() { value={formData.department} onChange={(e) => setFormData({ ...formData, department: e.target.value })} required - className="block w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-950/20 border border-slate-200 dark:border-border rounded-xl text-xs font-bold text-foreground focus:outline-none cursor-pointer" + className="block w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-950/20 border border-slate-200 dark:border-border rounded-none text-xs font-bold text-foreground focus:outline-none cursor-pointer" > {departments.map((d) => ( @@ -626,7 +726,7 @@ function UserManagementPageContent() { value={formData.phone} onChange={(e) => setFormData({ ...formData, phone: e.target.value })} placeholder="+1234567890" - className="block w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-950/20 border border-slate-200 dark:border-border rounded-xl text-xs font-bold text-foreground focus:outline-none" + className="block w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-950/20 border border-slate-200 dark:border-border rounded-none text-xs font-bold text-foreground focus:outline-none" />
@@ -642,7 +742,7 @@ function UserManagementPageContent() { value={formData.password} onChange={(e) => setFormData({ ...formData, password: e.target.value })} placeholder={createModalOpen ? "Min 8 characters" : "Leave blank to keep unchanged"} - className="block w-full pl-4 pr-11 py-2.5 bg-slate-50 dark:bg-slate-950/20 border border-slate-200 dark:border-border rounded-xl text-xs font-bold text-foreground focus:outline-none" + className="block w-full pl-4 pr-11 py-2.5 bg-slate-50 dark:bg-slate-950/20 border border-slate-200 dark:border-border rounded-none text-xs font-bold text-foreground focus:outline-none" /> @@ -691,7 +791,7 @@ function UserManagementPageContent() { animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setDeleteModalOpen(false)} - className="absolute inset-0 bg-slate-900/50 backdrop-blur-sm" + className="absolute inset-0 bg-slate-900/50 " /> {/* Modal Box */} @@ -699,10 +799,10 @@ function UserManagementPageContent() { initial={{ opacity: 0, scale: 0.95, y: 10 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 10 }} - className="bg-card border border-slate-200 dark:border-border rounded-[2rem] w-full max-w-sm p-8 shadow-2xl relative z-10" + className="bg-card border border-slate-200 dark:border-border rounded-[2rem] w-full max-w-sm p-8 border-2 border-foreground shadow-[4px_4px_0_0_rgba(0,0,0,1)] dark:shadow-[4px_4px_0_0_rgba(255,255,255,0.2)] relative z-10" >
-
+
@@ -711,7 +811,7 @@ function UserManagementPageContent() { Are you sure you want to delete {selectedUser.name} ({selectedUser.email})?

-
+

Warning: This action will permanently remove this account from Firebase Auth and the database, and nullify/cascade references in bookings or tickets.

@@ -720,13 +820,13 @@ function UserManagementPageContent() { diff --git a/app/api/keep-alive/route.ts b/app/api/keep-alive/route.ts new file mode 100644 index 0000000..10994d2 --- /dev/null +++ b/app/api/keep-alive/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from "next/server"; +import { supabase } from "../../../lib/supabase"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + const startTime = Date.now(); + + // Lightweight query to keep Supabase database active and prevent 7-day inactivity pause + const { count, error } = await supabase + .from("users") + .select("id", { count: "exact", head: true }) + .limit(1); + + const durationMs = Date.now() - startTime; + + if (error) { + console.error("Keep-alive Supabase query error:", error); + return NextResponse.json( + { + status: "error", + message: "Failed to ping Supabase database", + error: error.message, + timestamp: new Date().toISOString(), + }, + { status: 500 } + ); + } + + return NextResponse.json( + { + status: "ok", + message: "Supabase database keep-alive ping successful", + responseTimeMs: durationMs, + timestamp: new Date().toISOString(), + }, + { status: 200 } + ); + } catch (err: any) { + console.error("Unexpected error in keep-alive endpoint:", err); + return NextResponse.json( + { + status: "error", + message: "Unexpected server error during keep-alive ping", + error: err?.message || String(err), + timestamp: new Date().toISOString(), + }, + { status: 500 } + ); + } +} diff --git a/app/bookings/page.tsx b/app/bookings/page.tsx index fcb7302..145414e 100644 --- a/app/bookings/page.tsx +++ b/app/bookings/page.tsx @@ -516,29 +516,41 @@ function BookingsPageContent() { return ( -
-
-
-

- {view === "my" || view === "status" ? "My Bookings" : "Resource Bookings"} -

-

- {view === "my" || view === "status" - ? "Manage, track, and edit your personal resource reservations." - : "Manage and monitor facility schedules across all university faculties."} -

+
+ + {/* ── Sky Gradient Header ── */} +
+
+
+ +
+
+
+ + {view === "my" || view === "status" ? "My Reservations" : "All Bookings"} + +
+

+ {view === "my" || view === "status" ? "My Bookings" : "Resource Bookings"} +

+

+ {view === "my" || view === "status" + ? "Manage, track, and edit your personal resource reservations." + : "Manage and monitor facility schedules across all university faculties."} +

+
+
- -
+
{combinedError && ( -
+
{combinedError}
)} @@ -546,14 +558,14 @@ function BookingsPageContent() { {/* Availability Calendar (Hidden in personal bookings view) */} {view !== "my" && view !== "status" && (
-
+

Booking Availability

{calendarSubLabel}

-
+
- @@ -779,7 +791,7 @@ function BookingsPageContent() { "bookings" ); }} - className="inline-flex items-center gap-2 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/10 active:scale-95 text-slate-700 dark:text-foreground/80 font-semibold px-4 py-2.5 rounded-xl shadow-sm transition-all duration-200 text-sm" + className="inline-flex items-center gap-2 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/10 active:scale-95 text-slate-700 dark:text-foreground/80 font-semibold px-4 py-2.5 rounded-none shadow-sm transition-all duration-200 text-sm" > Export CSV @@ -787,7 +799,7 @@ function BookingsPageContent() {
{/* Bookings Table */} -
+
@@ -838,7 +850,7 @@ function BookingsPageContent() { >
-
+
@@ -865,7 +877,7 @@ function BookingsPageContent() {
@@ -881,7 +893,7 @@ function BookingsPageContent() { {activeDropdown === booking.id && ( -
+
{booking.status === "Pending" && ( +
- -

- Welcome Back -

-

- Sign in to your institutional account -

- {/* Credentials Form */} -
- - {/* Email Address */} -
- -
-
- -
- setEmail(e.target.value)} - onFocus={() => setFocusedField("email")} - onBlur={() => setFocusedField(null)} - placeholder="name@university.ac.lk" - disabled={loading} - className="block w-full pl-11 pr-4 py-3 bg-white/50 dark:bg-slate-950/20 border border-slate-200 dark:border-border/85 rounded-2xl text-sm font-bold text-foreground placeholder-slate-400 dark:placeholder-foreground/30 focus:outline-none focus:ring-2 focus:ring-brand-primary/30 focus:border-brand-primary/50 transition-all disabled:opacity-40" - /> -
-
+ {/* Form area */} +
+
+ + {isLogin ? ( + /* ── LOGIN FORM ── */ + + {/* Heading */} +
+
+ + Secure Campus Access +
+

+ Welcome back +

+

+ Sign in to your university account +

+
- {/* Password Input */} -
-
- - - Forgot Password? - -
-
-
- -
- setPassword(e.target.value)} - onFocus={() => setFocusedField("password")} - onBlur={() => setFocusedField(null)} - placeholder="••••••••" - disabled={loading} - className="block w-full pl-11 pr-12 py-3 bg-white/50 dark:bg-slate-950/20 border border-slate-200 dark:border-border/85 rounded-2xl text-sm font-bold text-foreground placeholder-slate-400 dark:placeholder-foreground/30 focus:outline-none focus:ring-2 focus:ring-brand-primary/30 focus:border-brand-primary/50 transition-all disabled:opacity-40" - /> - -
-
+ + {/* Email */} +
+ +
+ + setEmail(e.target.value)} + placeholder="name@university.ac.lk" + disabled={loading} + className={inputCls} + /> +
+
- {/* Form Error Alert */} - {error && ( -
- -

{error}

-
- )} - - {/* Submit Button */} - - - - {/* Redirect to Register */} -

- Don't have an account?{" "} - - Create Account - -

+ {/* Password */} +
+
+ + + Forgot password? + +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + disabled={loading} + className={`${inputCls} pr-10`} + /> + +
+
- {/* Quick Demo Access Badges */} -
-

- Quick Operator Access -

-
- {(["admin", "maintenance", "lecturer", "student"] as const).map((role) => ( - + + + {/* Bottom hint */} +

+ Don't have an account?{" "} + +

+ + ) : ( + /* ── REGISTER FORM ── */ + + {/* Heading */} +
+
+ + New Account Registration +
+

+ Join UniLink +

+

+ Register for campus resource access +

- {role} -
- - - ))} + +
+ {/* Full Name + Email */} +
+
+ +
+ + setFullName(e.target.value)} + placeholder="Jane Doe" + disabled={loading} + className={inputCls} + /> +
+
+
+ +
+ + setEmail(e.target.value)} + placeholder="@university" + disabled={loading} + className={inputCls} + /> +
+
+
+ + {/* Role + Faculty */} +
+
+ +
+ + + +
+
+
+ +
+ + + +
+
+
+ + {/* Passwords */} +
+
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + disabled={loading} + className={`${inputCls} pr-10`} + /> + +
+ +
+
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="••••••••" + disabled={loading} + className={`${inputCls} ${ + confirmPassword && password !== confirmPassword + ? "border-rose-300 focus:border-rose-400 focus:ring-rose-100" + : confirmPassword && password === confirmPassword + ? "border-teal-400 focus:border-teal-500 focus:ring-teal-100" + : "" + }`} + /> + {confirmPassword && ( + + {password === confirmPassword + ? + : + } + + )} +
+
+
+ + {/* Error / Success */} + + {error && ( + + +

{error}

+
+ )} + {success && ( + + +

{success}

+
+ )} +
+ + {/* Submit */} + + +

+ {role.toLowerCase() === "student" + ? "Student accounts are automatically approved." + : "Non-student accounts require admin approval before access is granted."} +

+
+ +

+ Already have an account?{" "} + +

+ + )} +
- + {/* Bottom bar */} +
+

+ © {new Date().getFullYear()} UniLink URMS · Sabaragamuwa University of Sri Lanka +

+
+
); } + +export default function LoginPage() { + return ( + +
+
+

Loading…

+
+
+ } + > + +
+ ); +} diff --git a/app/maintenance/assign/page.tsx b/app/maintenance/assign/page.tsx index 7e4f07a..8bafd44 100644 --- a/app/maintenance/assign/page.tsx +++ b/app/maintenance/assign/page.tsx @@ -199,7 +199,7 @@ export default function AssignMaintenancePage() { return (
-
+
{/* Header - Using a darker professional theme for Admin functionality */}
@@ -218,10 +218,10 @@ export default function AssignMaintenancePage() {
{isSuccess ? ( -
+
-
-
+
+
@@ -243,8 +243,8 @@ export default function AssignMaintenancePage() { ) : (
{error && ( -
- +
+ {error}
)} @@ -256,7 +256,7 @@ export default function AssignMaintenancePage() {
{loading ? ( -
+
Loading pending requests...
@@ -267,7 +267,7 @@ export default function AssignMaintenancePage() { value={formData.requestId} onChange={handleChange} disabled={submitting} - className={`w-full appearance-none px-4 py-3.5 bg-slate-50 border rounded-xl focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all cursor-pointer ${ + className={`w-full appearance-none px-4 py-3.5 bg-slate-50 border rounded-none focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all cursor-pointer ${ errors.requestId ? "border-brand-danger bg-red-50" : "border-slate-200" }`} > @@ -297,9 +297,9 @@ export default function AssignMaintenancePage() { {/* Priority Display (Read-only Highlight) */} {currentPriority && ( -
+
- + Priority Level: {currentPriority.label}
@@ -312,7 +312,7 @@ export default function AssignMaintenancePage() {
{loading ? ( -
+
Loading technicians...
@@ -323,7 +323,7 @@ export default function AssignMaintenancePage() { value={formData.staffId} onChange={handleChange} disabled={submitting} - className={`w-full appearance-none px-4 py-3.5 bg-slate-50 border rounded-xl focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all cursor-pointer ${ + className={`w-full appearance-none px-4 py-3.5 bg-slate-50 border rounded-none focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all cursor-pointer ${ errors.staffId ? "border-brand-danger bg-red-50" : "border-slate-200" }`} > @@ -364,7 +364,7 @@ export default function AssignMaintenancePage() { value={formData.notes} onChange={handleChange} disabled={submitting} - className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all resize-none placeholder:text-slate-400" + className="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-none focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all resize-none placeholder:text-slate-400" >
@@ -373,7 +373,7 @@ export default function AssignMaintenancePage() {
- -
-
- - {/* Stats */} -
- {[ - { title: "Total Requests", value: stats.total, icon: ListTodo, color: "blue", f: "All" }, - { title: "Active / Pending", value: stats.pending, icon: Activity, color: "amber", f: "Pending" }, - { title: "Tasks Completed", value: stats.completed, icon: CheckCircle2, color: "emerald", f: "Completed" }, - { title: "Overdue Tasks", value: stats.overdue, icon: AlertTriangle, color: "red", f: "Overdue" }, - ].map(({ title, value, icon: Icon, color, f }) => ( - setFilter(f as any)} /> + + + {/* ── Stat Cards ── */} + + {statCards.map(({ title, value, icon: Icon, bg, fg, f }) => ( + setFilter(f as any)} + className={`cursor-pointer bg-white dark:bg-slate-800/60 rounded-xl p-5 border shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200 ${ + filter === f + ? "border-[#0EA5E9] ring-1 ring-[#0EA5E9]/30" + : "border-[#E2E8F0] dark:border-slate-700" + }`} + > +
+ +
+

{title}

+

+ {loading ? : value} +

+
))} -
- - {/* Table */} -
-
-

Task Directory

+ + + {/* ── Table ── */} + + {/* Table Header */} +
+

+ Task Directory +

{/* Search */}
- + { - setSearch(e.target.value); - updateUrlParams(1, pageSize); - }} + onChange={e => { setSearch(e.target.value); updateUrlParams(1, pageSize); }} placeholder="Search tasks..." - className="pl-9 pr-4 py-2 bg-slate-100 dark:bg-foreground/5 border border-slate-200 dark:border-border rounded-xl text-sm font-bold text-foreground placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500/40 w-52" + className="pl-9 pr-4 py-2 bg-[#F8FAFC] dark:bg-slate-800 border border-[#E2E8F0] dark:border-slate-700 rounded-lg text-sm text-[#0F172A] dark:text-white placeholder:text-[#64748B] focus:outline-none focus:ring-2 focus:ring-[#0EA5E9]/50 w-52 transition-all" />
{/* Filter tabs */} -
+
{["All", "Pending", "Completed"].map(f => ( - ))}
-
- {/* ── Saved Searches & Export ── */} -
+ {/* Saved Searches & CSV Export */} +
{ if (filters.search !== undefined) setSearch(filters.search); if (filters.filter !== undefined) setFilter(filters.filter); @@ -332,23 +401,18 @@ function AdminMaintenanceDashboardContent() { onClick={() => { exportToCSV( filteredTasks.map(t => ({ - id: t.id, - resourceName: t.resourceName, - title: t.title, - description: t.description, - requestedDate: t.requestedDate, - status: t.status, - priority: t.priority, - assignedTo: t.assignedTo || "" + id: t.id, resourceName: t.resourceName, title: t.title, + description: t.description, requestedDate: t.requestedDate, + status: t.status, priority: t.priority, assignedTo: t.assignedTo || "" })), ["Task ID", "Resource", "Title", "Description", "Requested Date", "Status", "Priority", "Assigned To"], ["id", "resourceName", "title", "description", "requestedDate", "status", "priority", "assignedTo"], "maintenance_tasks" ); }} - className="inline-flex items-center gap-2 bg-white dark:bg-white/5 border border-slate-200 dark:border-border hover:bg-slate-50 dark:hover:bg-white/10 active:scale-95 text-slate-700 dark:text-foreground/80 font-semibold px-4 py-2.5 rounded-xl shadow-sm transition-all duration-200 text-sm" + className="inline-flex items-center gap-2 bg-white dark:bg-slate-700 border border-[#E2E8F0] dark:border-slate-600 hover:bg-[#F8FAFC] dark:hover:bg-slate-600 text-[#0F172A] dark:text-white font-semibold px-4 py-2 rounded-lg shadow-sm transition-all text-sm" > - + Export CSV
@@ -356,18 +420,20 @@ function AdminMaintenanceDashboardContent() {
- + {["ID / Resource", "Issue", "Requested", "Status", "Priority", "Action"].map(h => ( - + ))} - + {loading ? ( Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, j) => ( - + ))} )) @@ -375,41 +441,47 @@ function AdminMaintenanceDashboardContent() { ) : paginatedTasks.map(task => ( - - + - - - - - + + @@ -424,43 +496,50 @@ function AdminMaintenanceDashboardContent() { onPageChange={(p) => updateUrlParams(p, pageSize)} onPageSizeChange={(sz) => updateUrlParams(1, sz)} /> - + - {/* Create Task Modal */} + {/* ── Create Task Modal ── */} {showCreate && ( -
-
-
-

Create Maintenance Task

- +
+ +
+

+ Create Maintenance Task +

+
{createError && ( -
+
{createError}
)} + {[ + { label: "Resource ID *", field: "resourceId", placeholder: "e.g. a3f2...", type: "input" }, + { label: "Title *", field: "title", placeholder: "Brief issue title", type: "input" }, + ].map(({ label, field, placeholder }) => ( +
+ + setForm(p => ({ ...p, [field]: e.target.value }))} + placeholder={placeholder} + className="w-full px-4 py-2.5 bg-[#F8FAFC] dark:bg-slate-700 border border-[#E2E8F0] dark:border-slate-600 rounded-xl text-sm text-[#0F172A] dark:text-white placeholder:text-[#64748B] focus:outline-none focus:ring-2 focus:ring-[#0EA5E9]/50 transition-all" /> +
+ ))}
- - setForm(p => ({ ...p, resourceId: e.target.value }))} - placeholder="e.g. a3f2..." className="w-full px-4 py-3 bg-slate-50 dark:bg-foreground/5 border border-slate-200 dark:border-border rounded-xl text-sm font-bold focus:outline-none focus:ring-2 focus:ring-blue-500/40 text-foreground" /> -
-
- - setForm(p => ({ ...p, title: e.target.value }))} - placeholder="Brief issue title" className="w-full px-4 py-3 bg-slate-50 dark:bg-foreground/5 border border-slate-200 dark:border-border rounded-xl text-sm font-bold focus:outline-none focus:ring-2 focus:ring-blue-500/40 text-foreground" /> -
-
- + @@ -266,7 +266,7 @@ export default function MaintenanceRequestPage() { value={formData.date} onChange={handleChange} disabled={submitting} - className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all" + className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-none focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all" />
@@ -282,7 +282,7 @@ export default function MaintenanceRequestPage() { value={formData.priority} onChange={handleChange} disabled={submitting} - className="w-full appearance-none px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all cursor-pointer" + className="w-full appearance-none px-4 py-3 bg-slate-50 border border-slate-200 rounded-none focus:ring-4 focus:ring-brand-primary/10 focus:border-brand-primary outline-none transition-all cursor-pointer" > @@ -302,7 +302,7 @@ export default function MaintenanceRequestPage() {
-
+ - {/* Statistics Cards */} -
+ {/* ── Stat Cards ── */} + {[ - { label: "Total Assets", value: stats.total, icon: Database, color: "text-blue-500 bg-blue-500/10 border-blue-500/20" }, - { label: "Available / Active", value: stats.available, icon: CheckCircle2, color: "text-emerald-500 bg-emerald-500/10 border-emerald-500/20" }, - { label: "Under Maintenance", value: stats.inMaintenance, icon: Wrench, color: "text-amber-500 bg-amber-500/10 border-amber-500/20" }, - { label: "Inactive / Retired", value: stats.inactive, icon: XCircle, color: "text-red-500 bg-red-500/10 border-red-500/20" } - ].map((card, idx) => ( -
-
-

{card.label}

-

{loading ? "—" : card.value}

+ { label: "Total Assets", value: stats.total, icon: Database, bg: "#F0F9FF", fg: "#0EA5E9" }, + { label: "Available / Active", value: stats.available, icon: CheckCircle2,bg: "#CCFBF1", fg: "#0D9488" }, + { label: "Under Maintenance", value: stats.inMaintenance, icon: Wrench, bg: "#FFFBEB", fg: "#D97706" }, + { label: "Inactive / Retired", value: stats.inactive, icon: XCircle, bg: "#FFF1F2", fg: "#E11D48" }, + ].map(({ label, value, icon: Icon, bg, fg }) => ( + +
+
-
- -
-
+

{label}

+

+ {loading ? : value} +

+ ))} -
+
- {/* Error Banner */} + {/* ── Error Banner ── */} {error && ( -
+
{error}
)} - {/* Table Container */} -
- {/* Filters Header */} -
-

Operational Catalog

+ {/* ── Table ── */} + + {/* Table header / filters */} +
+

+ Operational Catalog +

{/* Search */}
- + setSearch(e.target.value)} placeholder="Search resources..." - className="w-full md:w-56 pl-9 pr-4 py-2.5 bg-slate-100 dark:bg-foreground/5 border border-slate-200 dark:border-border rounded-xl text-sm font-bold text-foreground placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-amber-500/40" + className="w-full md:w-56 pl-9 pr-4 py-2 bg-[#F8FAFC] dark:bg-slate-800 border border-[#E2E8F0] dark:border-slate-700 rounded-lg text-sm text-[#0F172A] dark:text-white placeholder:text-[#64748B] focus:outline-none focus:ring-2 focus:ring-[#0EA5E9]/50 transition-all" />
{/* Type Filter */} - setTypeFilter(e.target.value)} + className="px-3 py-2 bg-[#F8FAFC] dark:bg-slate-800 border border-[#E2E8F0] dark:border-slate-700 rounded-lg text-sm font-semibold text-[#0F172A] dark:text-white focus:outline-none focus:ring-2 focus:ring-[#0EA5E9]/50 transition-all"> @@ -224,11 +242,8 @@ export default function MaintenanceResourcesPage() { {/* Status Filter */} - setStatusFilter(e.target.value)} + className="px-3 py-2 bg-[#F8FAFC] dark:bg-slate-800 border border-[#E2E8F0] dark:border-slate-700 rounded-lg text-sm font-semibold text-[#0F172A] dark:text-white focus:outline-none focus:ring-2 focus:ring-[#0EA5E9]/50 transition-all"> @@ -238,22 +253,23 @@ export default function MaintenanceResourcesPage() {
- {/* Table View */}
{h}{h}
+
+
-
-

No tasks found

-

No tasks match the current filter or search.

- +
+ +
+

No tasks found

+

No tasks match the current filter or search.

+
-
{task.id}
-
{task.resourceName}
+
+
{task.id}
+
{task.resourceName}
-

{task.description}

- {task.assignedTo &&

→ {task.assignedTo}

} +
+

{task.title}

+

{task.description}

+ {task.assignedTo &&

→ {task.assignedTo}

}
-
- {task.requestedDate} +
+
+ {task.requestedDate}
+ {task.rawStatus !== "COMPLETED" ? ( ) : ( - Done + + Done + )}
- + {["Resource Name", "Location", "Category", "Operational Status", "Quick Status Update"].map(h => ( - + ))} - + {loading ? ( Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, j) => ( - + ))} )) @@ -261,71 +277,63 @@ export default function MaintenanceResourcesPage() { ) : ( filteredResources.map(resource => { - const isStatusUpdating = updatingId === resource.id; + const isUpdating = updatingId === resource.id; const currentStatus = resource.availability_status; - - // Styling for status badge - let statusBadgeClass = "bg-slate-100 text-slate-700 border-slate-200"; - let statusDotClass = "bg-slate-400"; - if (currentStatus === "Available") { - statusBadgeClass = "bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20"; - statusDotClass = "bg-emerald-500"; - } else if (currentStatus === "Booked") { - statusBadgeClass = "bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-500/20"; - statusDotClass = "bg-blue-500"; - } else if (currentStatus === "Maintenance" || currentStatus === "Under Maintenance") { - statusBadgeClass = "bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-500/20"; - statusDotClass = "bg-amber-500 animate-pulse"; - } else if (currentStatus === "Inactive" || currentStatus === "Decommissioned") { - statusBadgeClass = "bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/20"; - statusDotClass = "bg-red-500"; - } + const { cls: statusCls, dot: statusDot } = getStatusBadge(currentStatus); return ( - - + - - - -
{h}{h}
+
+
-
-

No resources found

-

Try adjusting your filters or search query.

+
+ +
+

No resources found

+

Try adjusting your filters or search query.

-
{resource.name}
-
ID: {resource.id.slice(0, 8)}
+
+
{resource.name}
+
ID: {resource.id.slice(0, 8)}
- {resource.location} + + + + {resource.location} + - + + {getCategoryIcon(resource.type)} {resource.type} - - - {currentStatus === "Maintenance" ? "Under Maintenance" : currentStatus === "Inactive" ? "Decommissioned" : currentStatus} + + + + {currentStatus === "Maintenance" ? "Under Maintenance" : + currentStatus === "Inactive" ? "Decommissioned" : currentStatus} -
+
+
{[ - { label: "Available", status: "Available", color: "hover:bg-emerald-500/10 border-transparent hover:border-emerald-500/25 text-emerald-600 dark:text-emerald-400" }, - { label: "Maintenance", status: "Maintenance", color: "hover:bg-amber-500/10 border-transparent hover:border-amber-500/25 text-amber-600 dark:text-amber-400" }, - { label: "Inactive", status: "Inactive", color: "hover:bg-red-500/10 border-transparent hover:border-red-500/25 text-red-600 dark:text-red-400" } + { label: "Available", status: "Available", cls: "bg-teal-50 text-teal-700 border-teal-200 hover:bg-teal-100 dark:bg-teal-500/10 dark:text-teal-300 dark:border-teal-500/20 dark:hover:bg-teal-500/20" }, + { label: "Maintenance", status: "Maintenance", cls: "bg-amber-50 text-amber-700 border-amber-200 hover:bg-amber-100 dark:bg-amber-500/10 dark:text-amber-300 dark:border-amber-500/20 dark:hover:bg-amber-500/20" }, + { label: "Inactive", status: "Inactive", cls: "bg-rose-50 text-rose-700 border-rose-200 hover:bg-rose-100 dark:bg-rose-500/10 dark:text-rose-300 dark:border-rose-500/20 dark:hover:bg-rose-500/20" }, ].map(action => ( ))}
@@ -337,8 +345,8 @@ export default function MaintenanceResourcesPage() {
-
+
-
+ ); } diff --git a/app/notifications/page.tsx b/app/notifications/page.tsx index 8663abc..e7b227f 100644 --- a/app/notifications/page.tsx +++ b/app/notifications/page.tsx @@ -21,6 +21,18 @@ import { ShieldAlert, } from "lucide-react"; +/* ─── URMS Light Blue Design System ─────────────────────── + primary : #0EA5E9 (Sky 500) + primary-dark: #0284C7 (Sky 600) + teal : #0D9488 + teal-light : #CCFBF1 + ink-main : #0F172A + ink-muted : #64748B + surface : #FFFFFF + bg-base : #F8FAFC + border : #E2E8F0 +──────────────────────────────────────────────────────────── */ + interface Notification { id: string; title: string; @@ -32,7 +44,6 @@ interface Notification { const ITEMS_PER_PAGE = 10; -// Helper to format date in a premium relative format const formatRelativeTime = (dateStr: string) => { const now = new Date(); const date = new Date(dateStr); @@ -41,26 +52,12 @@ const formatRelativeTime = (dateStr: string) => { const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); - if (diffMs < 0 || diffMins < 1) { - return "Just now"; - } - if (diffMins < 60) { - return `${diffMins}m ago`; - } - if (diffHours < 24) { - return `${diffHours}h ago`; - } - if (diffDays === 1) { - return "Yesterday"; - } - if (diffDays < 7) { - return `${diffDays}d ago`; - } - return date.toLocaleDateString(undefined, { - month: "short", - day: "numeric", - year: "numeric", - }); + if (diffMs < 0 || diffMins < 1) return "Just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); }; export default function NotificationsPage() { @@ -72,7 +69,6 @@ export default function NotificationsPage() { const [typeFilter, setTypeFilter] = useState<"all" | "info" | "success" | "warning" | "error">("all"); const [currentPage, setCurrentPage] = useState(1); - // Fetch notifications const fetchNotifications = async () => { if (!profile?.id) return; setLoading(true); @@ -103,362 +99,315 @@ export default function NotificationsPage() { } }; - useEffect(() => { - fetchNotifications(); - }, [profile?.id]); + useEffect(() => { fetchNotifications(); }, [profile?.id]); - // Handle single read const toggleReadStatus = async (id: string, currentRead: boolean) => { - setNotifications((prev) => - prev.map((n) => (n.id === id ? { ...n, read: !currentRead } : n)) - ); + setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: !currentRead } : n))); try { - await supabase - .from("notifications") - .update({ is_read: !currentRead }) - .eq("id", id); + await supabase.from("notifications").update({ is_read: !currentRead }).eq("id", id); } catch (err) { console.error("Error toggling notification status:", err); } }; - // Delete single notification const deleteNotification = async (id: string) => { setNotifications((prev) => prev.filter((n) => n.id !== id)); try { - await supabase - .from("notifications") - .delete() - .eq("id", id); + await supabase.from("notifications").delete().eq("id", id); } catch (err) { console.error("Error deleting notification:", err); } }; - // Mark all as read const markAllAsRead = async () => { const unreadIds = notifications.filter((n) => !n.read).map((n) => n.id); if (unreadIds.length === 0) return; - setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); try { - await supabase - .from("notifications") - .update({ is_read: true }) - .eq("user_id", profile?.id) - .eq("is_read", false); + await supabase.from("notifications").update({ is_read: true }).eq("user_id", profile?.id).eq("is_read", false); } catch (err) { console.error("Error marking all read:", err); } }; - // Delete all read notifications const deleteAllRead = async () => { const readIds = notifications.filter((n) => n.read).map((n) => n.id); if (readIds.length === 0) return; - setNotifications((prev) => prev.filter((n) => !n.read)); try { - await supabase - .from("notifications") - .delete() - .eq("user_id", profile?.id) - .eq("is_read", true); + await supabase.from("notifications").delete().eq("user_id", profile?.id).eq("is_read", true); } catch (err) { console.error("Error deleting read notifications:", err); } }; - // Clear entire history const clearAllHistory = async () => { - if (!confirm("Are you sure you want to clear your entire notification history? This cannot be undone.")) { - return; - } - + if (!confirm("Are you sure you want to clear your entire notification history? This cannot be undone.")) return; setNotifications([]); try { - await supabase - .from("notifications") - .delete() - .eq("user_id", profile?.id); + await supabase.from("notifications").delete().eq("user_id", profile?.id); } catch (err) { console.error("Error clearing notifications:", err); } }; - // Icons mapped to type const getIcon = (type: string) => { switch (type) { - case "success": - return ; - case "warning": - return ; - case "error": - return ; - default: - return ; + case "success": return ; + case "warning": return ; + case "error": return ; + default: return ; } }; - // Count states const totalCount = notifications.length; const unreadCount = notifications.filter((n) => !n.read).length; - // Filtered lists const filteredNotifications = notifications.filter((n) => { const matchesSearch = n.title.toLowerCase().includes(searchTerm.toLowerCase()) || n.message.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = statusFilter === "all" || !n.read; const matchesType = typeFilter === "all" || n.type === typeFilter; - return matchesSearch && matchesStatus && matchesType; }); - // Pagination bounds const totalPages = Math.ceil(filteredNotifications.length / ITEMS_PER_PAGE) || 1; const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; const paginatedNotifications = filteredNotifications.slice(startIndex, startIndex + ITEMS_PER_PAGE); - // Reset pagination if filters change - useEffect(() => { - setCurrentPage(1); - }, [searchTerm, statusFilter, typeFilter]); + useEffect(() => { setCurrentPage(1); }, [searchTerm, statusFilter, typeFilter]); return ( -
-
- {/* Header */} -
-
-

- Notification Center -

-

- Manage and view your account logs, system alerts, and resource booking statuses. -

-
-
- {unreadCount > 0 && ( - - )} - {notifications.some((n) => n.read) && ( - - )} - {totalCount > 0 && ( - - )} -
-
- - {/* Stats Dashboard */} -
-
-
- -
-
-

- Total Logs -

-

{totalCount}

+
+
+ + {/* ── Sky Gradient Header ── */} +
+
+
+ +
+
+
+ +
+
+

Notification Centre

+

Account logs, alerts & booking updates

+
+ {unreadCount > 0 && ( + + {unreadCount} + + )}
-
-
-
- -
-
-

- Unread Logs -

-

{unreadCount}

+
+ {unreadCount > 0 && ( + + )} + {notifications.some((n) => n.read) && ( + + )} + {totalCount > 0 && ( + + )}
+
-
-
- - - - -
-
-

- System Status -

-

Active & Healthy

+ {/* ── Stat Cards ── */} +
+ {[ + { label: "Total", value: totalCount, bg: "#F0F9FF", fg: "#0EA5E9", icon: Inbox }, + { label: "Unread", value: unreadCount, bg: "#FFFBEB", fg: "#D97706", icon: Bell }, + { label: "Status", value: "Active", bg: "#CCFBF1", fg: "#0D9488", icon: CheckCircle2 }, + ].map(({ label, value, bg, fg, icon: Icon }) => ( +
+
+ +
+
+

{label}

+

{value}

+
-
+ ))}
- {/* Filter & Control Bar */} -
- {/* Status Toggle Filter */} -
+ {/* ── Filter & Search Bar ── */} +
+ {/* Status toggle */} +
- {/* Type Selector Pills */} -
- - Type: - - {(["all", "info", "success", "warning", "error"] as const).map((type) => ( - - ))} + {/* Type pills */} +
+ + {(["all", "info", "success", "warning", "error"] as const).map((type) => { + const typeColors: Record = { + all: { active: "bg-[#0EA5E9] text-white", inactive: "bg-[#F0F9FF] text-[#0EA5E9] hover:bg-[#E0F2FE]" }, + info: { active: "bg-[#0EA5E9] text-white", inactive: "bg-[#F0F9FF] text-[#0EA5E9] hover:bg-[#E0F2FE]" }, + success: { active: "bg-[#0D9488] text-white", inactive: "bg-[#CCFBF1] text-[#0D9488] hover:bg-teal-100" }, + warning: { active: "bg-amber-500 text-white", inactive: "bg-amber-50 text-amber-700 hover:bg-amber-100" }, + error: { active: "bg-rose-500 text-white", inactive: "bg-rose-50 text-rose-600 hover:bg-rose-100" }, + }; + const c = typeColors[type]; + return ( + + ); + })}
- {/* Search Input */} -
- + {/* Search */} +
+ setSearchTerm(e.target.value)} - className="w-full bg-slate-100 dark:bg-foreground/5 border border-transparent hover:border-slate-300 dark:hover:border-white/10 focus:border-brand-primary focus:bg-card focus:ring-1 focus:ring-brand-primary pl-10 pr-4 py-2.5 rounded-xl text-xs font-semibold text-foreground placeholder:text-slate-400 dark:placeholder:text-foreground/30 outline-none transition-all" + className="w-full bg-[#F8FAFC] dark:bg-slate-900 border border-[#E2E8F0] dark:border-slate-600 rounded-lg pl-9 pr-4 py-2 text-sm font-medium text-[#0F172A] dark:text-white placeholder:text-[#94A3B8] focus:outline-none focus:border-[#0EA5E9] focus:ring-2 focus:ring-[#0EA5E9]/15 transition-all" />
- {/* Notifications Log List */} -
+ {/* ── Notification List ── */} +
{loading ? ( -
-
-

Loading notifications...

+
+
+

Loading notifications…

) : filteredNotifications.length === 0 ? ( -
- -

No matching notifications

-

+

+
+ +
+

No notifications found

+

{searchTerm || typeFilter !== "all" || statusFilter !== "all" - ? "Try adjusting your search keywords or filters to find what you are looking for." - : "Your notification history log is completely empty."} + ? "Try adjusting your search or filters." + : "You're all caught up! No notifications yet."}

) : ( -
- - {paginatedNotifications.map((notification) => ( + + {paginatedNotifications.map((notification) => { + const typeConfig: Record = { + success: { bg: "#CCFBF1", fg: "#0D9488", borderClass: "border-l-[#0D9488]" }, + warning: { bg: "#FFFBEB", fg: "#D97706", borderClass: "border-l-amber-400" }, + error: { bg: "#FFF1F2", fg: "#E11D48", borderClass: "border-l-rose-500" }, + info: { bg: "#F0F9FF", fg: "#0EA5E9", borderClass: "border-l-[#0EA5E9]" }, + }; + const tc = typeConfig[notification.type] || typeConfig.info; + return ( - {/* Left icon wrapper */} -
-
- {getIcon(notification.type)} -
+ {/* Type icon */} +
+ {getIcon(notification.type)}
- {/* Details body */} + {/* Content */}
-
-

- {notification.title} -

- {!notification.read && ( - - New - - )} - +
+
+

+ {notification.title} +

+ {!notification.read && ( + + + New + + )} +
+ {formatRelativeTime(notification.createdAt)}
-

+

{notification.message}

-
- - Logged: {new Date(notification.createdAt).toLocaleString()} - -
- {/* Float action tools */} -
+ {/* Hover actions */} +
- ))} - -
+ ); + })} + )} - {/* Pagination footer controls */} + {/* Pagination */} {totalPages > 1 && ( -
- - Showing page {currentPage} of{" "} - {totalPages} ({filteredNotifications.length} items) +
+ + Page {currentPage} of{" "} + {totalPages}{" "} + · {filteredNotifications.length} items
@@ -504,6 +454,7 @@ export default function NotificationsPage() {
)}
+
diff --git a/app/page.tsx b/app/page.tsx index a97ad5b..8ed118e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,940 +2,691 @@ import React, { useState, useEffect } from "react"; import Link from "next/link"; -import { motion, AnimatePresence } from "framer-motion"; +import Footer from "@/components/Footer"; import { ArrowRight, Calendar, - ShieldCheck, + Clock, Users, - Zap, - TrendingUp, - LayoutDashboard, - CheckCircle2, - ChevronRight, + GraduationCap, + ShieldCheck, Building2, - Clock, Sparkles, - Lock, + AlertTriangle, + RefreshCw, + CheckCircle2, Search, - Activity, - Laptop, Check, - AlertTriangle, - Plus, - Wrench, - ShieldAlert, - PieChart, + ChevronRight, + Send, HelpCircle, - RefreshCw + Mail, + MapPin, + Phone, + Layers, + Activity, + UserCheck, } from "lucide-react"; -/* ─── Mock Data for Interactive Simulator ─── */ -const mockSpaces = [ - { id: "lab01", name: "Computing Lab 01", capacity: "36/40 seats", status: "Occupied", detail: "CS302 Database Lecture", fill: 90, color: "text-amber-500 bg-amber-500/10 border-amber-500/20" }, - { id: "audiB", name: "Lecture Theatre B", capacity: "0/120 seats", status: "Vacant", detail: "Next class: 2:00 PM", fill: 0, color: "text-emerald-500 bg-emerald-500/10 border-emerald-500/20" }, - { id: "seminar02", name: "Seminar Room 02", capacity: "15/30 seats", status: "Occupied", detail: "Seminar: AI & Ethics", fill: 50, color: "text-amber-500 bg-amber-500/10 border-amber-500/20" }, - { id: "iotlab", name: "IoT & Robotics Lab", capacity: "0/20 seats", status: "Maintenance", detail: "AC Unit Repair", fill: 0, color: "text-rose-500 bg-rose-500/10 border-rose-500/20" } +/* ──────────────────────────────────────────────────────────────────────── + URMS LIGHT BLUE DESIGN SYSTEM + bg-base : #F8FAFC (Slate 50) + surface : #FFFFFF + border : #E2E8F0 (Slate 200) + primary : #0EA5E9 (Sky 500) + primary-dark: #0284C7 (Sky 600) + teal : #0D9488 (Teal 600 - Main Accent) + teal-light : #CCFBF1 (Teal 100) + ink-main : #0F172A (Slate 900) + ink-muted : #64748B (Slate 500) + ──────────────────────────────────────────────────────────────────────── */ + +const ROOM_FACULTIES = [ + { + id: "computing", + title: "Computing & IT Lab 01", + type: "Computer Lab", + capacity: "60 Seats", + status: "Available", + badge: "Interactive Workstations", + image: "https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=800&q=80", + }, + { + id: "engineering", + title: "Main Auditorium Hall A", + type: "Auditorium", + capacity: "350 Seats", + status: "Available", + badge: "AV System Synced", + image: "https://images.unsplash.com/photo-1541829070764-84a7d30dd3f3?auto=format&fit=crop&w=800&q=80", + }, + { + id: "science", + title: "Advanced Physics Lab", + type: "Specialised Science Lab", + capacity: "40 Seats", + status: "Reserved", + badge: "Safety Verification", + image: "https://images.unsplash.com/photo-1532094349884-543bc11b234d?auto=format&fit=crop&w=800&q=80", + }, + { + id: "management", + title: "Executive Seminar Room 02", + type: "Conference Room", + capacity: "80 Seats", + status: "Available", + badge: "Hybrid Video Stream", + image: "https://images.unsplash.com/photo-1431540015161-0bf868a2d407?auto=format&fit=crop&w=800&q=80", + }, +]; + +const SERVICES = [ + { + icon: Calendar, + title: "Smart Room Booking", + desc: "Reserve lecture halls, computing labs, and seminar spaces in real time with zero overlapping schedules.", + }, + { + icon: AlertTriangle, + title: "Conflict Resolver", + desc: "Automated engine flags double-bookings instantly and proposes tier-equivalent alternative spaces.", + }, + { + icon: UserCheck, + title: "Digital Pass Check-in", + desc: "Digital passes generated on confirmation to verify attendance and monitor room utilization.", + }, + { + icon: Layers, + title: "Equipment & AV Lock", + desc: "Request projectors, audio systems, and specialised hardware alongside room reservations.", + }, +]; + +const ROLES_DATA = [ + { + id: "student", + title: "Students", + icon: GraduationCap, + desc: "Browse live campus catalog, hold study group spaces, track waitlists, and check in via digital pass.", + stats: [ + { n: "128", l: "Credits Tracked" }, + { n: "6", l: "Active Holds" }, + { n: "100%", l: "Verified Access" }, + ], + features: ["Browse Live Catalog", "Self-Service Reservations", "Digital Pass Access"], + }, + { + id: "lecturer", + title: "Lecturers & Faculty", + icon: Users, + desc: "Skip queue approvals for term-long recurring lectures, reserve specialized labs, and view attendance trends.", + stats: [ + { n: "14", l: "Course Sections" }, + { n: "240", l: "Enrolled Students" }, + { n: "Priority", l: "Queue Level" }, + ], + features: ["Recurring Section Lock", "Priority Override", "Lab Hardware Tracking"], + }, + { + id: "admin", + title: "Operations Admin", + icon: ShieldCheck, + desc: "Complete operational visibility over campus facilities, manual conflict overrides, and automated maintenance routing.", + stats: [ + { n: "482", l: "Total Rooms" }, + { n: "0.2s", l: "Sync Speed" }, + { n: "Full", l: "Audit Controls" }, + ], + features: ["Campus Real-Time Audit", "Manual Schedule Override", "Maintenance Ticketing"], + }, +]; + +const PROCESS_STEPS = [ + { + step: "01", + title: "Search & Select", + desc: "Filter campus spaces by capacity, faculty block, or specific AV equipment needed.", + }, + { + step: "02", + title: "Real-Time Index Lock", + desc: "The core engine locks your chosen time index down to the exact minute to stop race conditions.", + }, + { + step: "03", + title: "Automated Verification", + desc: "System cross-checks role permissions and flags any schedule overlap across all departments.", + }, + { + step: "04", + title: "Instant Digital Pass Issued", + desc: "Your booking is confirmed with a digital access pass ready for door check-in.", + }, ]; export default function LandingPage() { - const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - - // Interactive Simulator States - const [simTab, setSimTab] = useState<"availability" | "booking" | "analytics">("availability"); - const [selectedRoom, setSelectedRoom] = useState("Computing Lab 01"); - const [selectedTimeSlot, setSelectedTimeSlot] = useState(""); - const [bookingStatus, setBookingStatus] = useState<"idle" | "conflict" | "resolving" | "resolved" | "success">("idle"); - const [bookingMessage, setBookingMessage] = useState(""); - - // Role Switcher States - const [activeRole, setActiveRole] = useState<"student" | "lecturer" | "admin">("student"); - - // Conflict visualizer step - const [conflictStep, setConflictStep] = useState(0); - - // Time tracker for simulator status bar - const [currentTime, setCurrentTime] = useState(""); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - const handleMouseMove = (e: MouseEvent) => { - setMousePosition({ x: e.clientX, y: e.clientY }); - }; - window.addEventListener("mousemove", handleMouseMove); - - // Simple digital clock - const updateTime = () => { - const now = new Date(); - setCurrentTime(now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })); - }; - updateTime(); - const interval = setInterval(updateTime, 1000); - - return () => { - window.removeEventListener("mousemove", handleMouseMove); - clearInterval(interval); - }; - }, []); - - // Handle simulated booking slot click - const handleSlotSelect = (time: string) => { - setSelectedTimeSlot(time); - if (time === "10:30 AM - 12:30 PM") { - setBookingStatus("conflict"); - setBookingMessage("⚠️ Collision detected! Computing Lab 01 is booked for CS302 class."); - } else { - setBookingStatus("success"); - setBookingMessage(`🎉 Success! Reserved ${selectedRoom} for ${time}.`); - } - }; - - // Run simulated conflict solver - const runConflictSolver = () => { - setBookingStatus("resolving"); - setBookingMessage("Running conflict resolution algorithm..."); - - setTimeout(() => { - setBookingStatus("resolved"); - setBookingMessage("✅ Smart suggestion found: Seminar Room 02 is vacant during this time!"); - }, 1200); - }; - - const confirmAlternative = () => { - setSelectedRoom("Seminar Room 02"); - setBookingStatus("success"); - setBookingMessage("🎉 Success! Reserved Seminar Room 02 for 10:30 AM - 12:30 PM."); - }; - - const resetBookingSim = () => { - setSelectedRoom("Computing Lab 01"); - setSelectedTimeSlot(""); - setBookingStatus("idle"); - setBookingMessage(""); - }; - - // Conflict Resolution Step Explanations - const pipelineSteps = [ - { - title: "1. Request Intake", - desc: "Student/lecturer inputs booking parameters (space type, duration, capacity). Request passes client-side schema checks.", - icon: , - color: "border-blue-500/30 text-blue-500 bg-blue-500/5" - }, - { - title: "2. Database Index Lock", - desc: "UniLink queries PostgreSQL with precise time block indexes to search for overlapping booking intervals.", - icon: , - color: "border-amber-500/30 text-amber-500 bg-amber-500/5" - }, - { - title: "3. Conflict Evaluation", - desc: "Smart validation detects intersecting bookings. Evaluates role hierarchy (e.g., lecturers can override students).", - icon: , - color: "border-rose-500/30 text-rose-500 bg-rose-500/5" - }, - { - title: "4. Auto-Routing & Confirmation", - desc: "Provides instant alternate suggestions or automatically locks the conflict-free slot, writing to Supabase DB and sending WS notifications.", - icon: , - color: "border-emerald-500/30 text-emerald-500 bg-emerald-500/5" - } - ]; + const [activeRole, setActiveRole] = useState("student"); + const [inquirySubmitted, setInquirySubmitted] = useState(false); + + const selectedRoleInfo = ROLES_DATA.find((r) => r.id === activeRole) || ROLES_DATA[0]; return ( -
+
- {/* Premium Cursor Ambient Highlight */} - - - {/* ─── HERO SECTION ─── */} -
- - {/* Subtle Backdrop Pattern & Blobs */} -
-
-
-
+ {/* ── HERO SECTION ── */} +
+ {/* Soft Background Accent Graphics */} +
+
+
-
- - {/* Hero Left Content */} -
- - - - - - Production-Grade Resource Portal - - - - A Unified Engine for
- - Campus Bookings - -
- - - Optimize academic facility utilization, dispatch maintenance tickets instantly, and eliminate scheduling collisions with our real-time resource planner. Built for SUSL. - - - - - Launch Console - - - - Browse Resources - - - - - {/* Quick Metrics */} - -
-

99.9%

-

Uptime SLA

-
+
+
+ {/* Hero Left Content */} +
-

0s

-

Collision Risk

+
-
-

4 Departments

-

Active Sync

+ +

+ Campus Spaces
+ Scheduled Without
+ + A Single Collision. + +

+ +

+ One centralized index for every lecture hall, computing laboratory, and seminar space on campus. + Booked, verified, and audited in real-time. +

+ + {/* Action Group */} +
+ + Explore Live Catalog + + + + + How It Works +
- -
- {/* Hero Right: Interactive URMS Live Dashboard Simulator */} -
- - {/* Simulator Header Bar */} -
-
-
- - - + {/* Social Proof / Stats Bar */} +
+
+
+ CS +
+
+ ENG +
+
+ SCI
- - URMS Core Console v1.2 -
- - {/* Live clock/status badge */} -
- - LIVE +
+

480+ Campus Spaces

+

Synced across all faculties daily

+
- {/* Simulator Navigation Tabs */} -
- {(["availability", "booking", "analytics"] as const).map((tab) => ( - - ))} - - {/* Simulated timestamp */} -
- {mounted ? currentTime : "12:00:00 PM"} + {/* Hero Right Visual Showcase */} +
+
+ {/* Visual Image Header */} +
+ University Campus Facilities +
+
+
+

Central Computing Building

+

Main Resource Control

+
+ + Live + +
-
- {/* Simulator Content Panel */} -
- - {simTab === "availability" && ( - -
- Resource Name - Status / Utilization -
- - {mockSpaces.map((room) => ( -
-
-

{room.name}

-

{room.detail}

-
-
- - {room.status} - - {room.status !== "Maintenance" && ( -
- {room.capacity} -
-
-
-
- )} -
-
- ))} - - )} - - {simTab === "booking" && ( - -
- 1. Choose Resource -
- {["Computing Lab 01", "Lecture Theatre B", "Seminar Room 02"].map((name) => ( - - ))} -
-
- -
- 2. Select Target Time Slot -
- - -
-
- - {/* Animated Result Board */} -
- {bookingStatus === "idle" && ( -

- Select a time slot above to test validation -

- )} - - {bookingStatus === "conflict" && ( -
-

- {bookingMessage} -

- -
- )} - - {bookingStatus === "resolving" && ( -
-
-

{bookingMessage}

-
- )} - - {bookingStatus === "resolved" && ( -
-

- {bookingMessage} -

-
- - -
-
- )} - - {bookingStatus === "success" && ( -
-

- {bookingMessage} -

- -
- )} -
- - )} - - {simTab === "analytics" && ( - -
-
-

Util Rate

-

84.6%

-
-
-

Bookings

-

342

-
-
-

Collisions Blocked

-

47

-
-
- -
-

Utilization by Faculty

- -
-
-
- Computing & IS - 92% -
-
-
-
-
- -
-
- Applied Sciences - 76% -
-
-
-
-
- -
-
- Geomatics - 68% -
-
-
-
-
-
-
- - )} - -
+ {/* Floating Metric Badges */} +
+
+
+ + Sync Speed +
+

0.2s

+

Instant collision detection

+
- {/* Simulator Action Footer */} -
- Database Index: Locked - - - RLS Rules: Enforcement Active - +
+
+ + Weekly Load +
+

98.4%

+

Zero double-bookings

+
+
- +
-
- {/* ─── INTERACTIVE ROLE HUB ─── */} -
-
- -
- Role-Tailored Workspaces -

- One Unified Database.
Three Customized Dashboards. -

-

- UniLink detects user credentials automatically, serving role-specific features while respecting security boundaries. -

-
- -
- - {/* Role Hub Left: Controls & Copy */} -
-
- {(["student", "lecturer", "admin"] as const).map((role) => ( - - ))} -
+ {/* ── FEATURED ROOM CATALOG ── */} +
+
+
+
+

Active Facilities

+

Featured Campus Spaces

+ + View Full Catalog + + +
- {/* Role Hub Right: Interactive Terminal Display */} -
-
- - {/* Terminal Header */} -
-
- - - {activeRole === "student" && "Active Session: Student Hub"} - {activeRole === "lecturer" && "Active Session: Faculty Console"} - {activeRole === "admin" && "Active Session: Operator Dashboard"} +
+ {ROOM_FACULTIES.map((room) => ( +
+
+
+ {room.title} + + {room.badge}
- SYS_OK -
- - {/* Switchable Mock Viewport */} -
- - {activeRole === "student" && ( - -

- Students gain access to an instant dashboard where they can track approved rooms, reserve laboratory equipment, and view check-in barcodes. -

- - {/* Interactive UI Snippet */} -
-
- Your Reserved Slots - 2 Active -
- -
-
- Chemistry Lab Space B - Approved -
-
- Smart Projector Kit #4 - Pending Admin -
-
-
-
- )} - - {activeRole === "lecturer" && ( - -

- Lecturers bypass standard reservations. They can lock classrooms recursively for a whole term, override student bookings, and approve space requests. -

- - {/* Interactive UI Snippet */} -
-
- Pending Student Requests - Action Required -
- -
-
-

Computing Lab 01

-

Requested by: Student Society (1:30 PM)

-
-
- - -
-
-
-
- )} - - {activeRole === "admin" && ( - -

- Administrators monitor security controls, access the complete maintenance ticket system, review usage audits, and compile PDF/Excel reports. -

- - {/* Interactive UI Snippet */} -
-
- System Status Panel - 100% SECURE -
- -
-
- Active Tickets - 12 Pending -
-
- DB Operations - Sync OK -
-
-
-
- )} -
+
+

{room.type}

+

{room.title}

+

{room.capacity}

+
- {/* Footer specs checklist */} -
- RBAC Middleware - Supabase RLS - JWT Verification +
+ + + {room.status} + + + Hold Room +
+
+ ))} +
+
+
+ {/* ── SERVICES SECTION ── */} +
+
+
+ {/* Left Header */} +
+

System Capabilities

+

+ Services Offered By URMS Index +

+

+ Designed to handle high-density university scheduling requests while strictly eliminating administrative delays and double-booking errors. +

+
+ + Request Custom Access + +
+ {/* Right Cards Grid */} +
+ {SERVICES.map((srv, idx) => { + const Icon = srv.icon; + return ( +
+
+ +
+

{srv.title}

+

{srv.desc}

+
+ ); + })} +
-
- {/* ─── DYNAMIC CONFLICT PIPELINE ─── */} -
-
- -
- Collision Prevention Pipeline -

- Behind the Scheduling Engine + {/* ── ROLE WORKSPACES (TABBED HUB) ── */} +
+
+
+

Role Architecture

+

+ One Engine, Tailored Workspaces

-

- How UniLink validates, processes, and matches bookings dynamically inside the database index. +

+ Granular access control ensuring each campus member interacts with the exact features required for their responsibility level.

- {/* Interactive Steps Visualizer */} -
- {pipelineSteps.map((step, idx) => ( - setConflictStep(idx)} - className={`p-6 rounded-3xl border text-left cursor-pointer transition-all ${ - conflictStep === idx - ? "bg-card border-brand-primary shadow-lg shadow-brand-primary/5 -translate-y-1" - : "bg-card/40 border-slate-200/80 dark:border-border/30 hover:border-brand-primary/20" - }`} - > -
- {step.icon} -
-

{step.title}

-

{step.desc}

-
- ))} -
- - {/* Pipeline Interactive Status Graph */} -
-
- Simulation Status Graph - Pipeline Step {conflictStep + 1} of 4 -
- - {/* Visual node connector line */} -
-
-
- - {[0, 1, 2, 3].map((n) => ( + {/* Role Navigation Tabs */} +
+ {ROLES_DATA.map((role) => { + const Icon = role.icon; + const isActive = activeRole === role.id; + return ( - ))} + ); + })} +
+ + {/* Selected Role Display Card */} +
+
+
+ Role Profile +

{selectedRoleInfo.title} Workspace

+

{selectedRoleInfo.desc}

+
+ +
+ {selectedRoleInfo.stats.map((s, idx) => ( +
+

{s.n}

+

{s.l}

+
+ ))} +
+ +
+

Key Role Capabilities

+
+ {selectedRoleInfo.features.map((feat, idx) => ( +
+ + {feat} +
+ ))} +
+
+
+
+
+ + {/* ── PROCESS SECTION ── */} +
+
+
+

System Mechanics

+

+ Our Design Process & Workflow +

+

+ Four structured checkpoints behind every single reservation on campus. +

+
+ {PROCESS_STEPS.map((step, idx) => ( +
+ {step.step} +

{step.title}

+

{step.desc}

+
+ ))} +
- {/* ─── BENTO GRID FEATURES ─── */} -
-
+ {/* ── PORTAL ACCESS ── */} +
+
-
- Platform Capability Ecosystem -

- Fine-Tuned For Academic Operations + {/* Section Header */} +
+ + Portal Access + + +

+ Choose Your Portal

+ +

+ Access the UniLink Resource Management System through the portal + designed for your role and responsibilities. +

-
- - {/* Bento Card 1: Console */} -
-
-
-
- -
-

Unified Live Console

-

- Monitor classroom occupancy, labs, and equipment sets. Instant color status tags help operators see vacant resources dynamically. + {/* Portal Cards */} +

+ {[ + { + name: "Student Portal", + desc: "For students to access and reserve university resources.", + features: [ + "View available resources", + "Make resource reservations", + "Track reservation status", + "View booking history", + "Manage personal profile", + ], + cta: "Access Student Portal", + popular: false, + }, + { + name: "Academic Portal", + desc: "For academic staff to manage and coordinate university resources.", + features: [ + "Manage academic resources", + "Review resource reservations", + "Approve or reject requests", + "Monitor resource availability", + "Manage academic schedules", + ], + cta: "Access Academic Portal", + popular: true, + }, + { + name: "Maintenance Portal", + desc: "For maintenance teams to monitor and manage resource issues.", + features: [ + "View maintenance requests", + "Track reported issues", + "Manage maintenance tasks", + "Update repair status", + "Monitor resource conditions", + ], + cta: "Access Maintenance Portal", + popular: false, + }, + ].map((portal, idx) => ( +
+
+

+ {portal.name} +

+ +

+ {portal.desc}

+ +
    + {portal.features.map((feat, fIdx) => ( +
  • + + {feat} +
  • + ))} +
-
- Explore Console Views -
-
-
-
- {/* Bento Card 2: Conflict Validation */} -
-
-
-
- -
-

Conflict Validation

-

- Prevents schedule overlap with absolute precision using dynamic time boundaries. -

+
+ + {portal.cta} +
- - PostgreSQL Index Lock -
-
+ ))} +
+
+

- {/* Bento Card 3: Security */} -
-
-
-
- + {/* ── INQUIRY & CONTACT SECTION ── */} +
+
+
+
+ {/* Contact Info */} +
+

Direct Support

+

+ Let's Build Something Great Together +

+

+ Have questions regarding faculty resource integration, custom lab setups, or system access? Reach out to the IT administration team. +

+ +
+
+ + support@urms.edu.lk +
+
+ + +94 (0) 45 228 0000 +
+
+ + Faculty of Computing, Campus Administration
-

Granular DB Access

-

- Row Level Security filters users based on their active student, lecturer, or staff role. -

- - Supabase RLS Active -
-
- {/* Bento Card 4: Reports */} -
-
-
-
- + {/* Inquiry Form */} +
+ {inquirySubmitted ? ( +
+ +

Inquiry Sent Successfully

+

+ Our IT resource coordinator will respond within 24 hours. +

-

Audits & Reports Export

-

- Generate instant PDF utilization sheets, export tabular schedules to Excel, or coordinate sync to Google Sheets for university management review. -

-
-
- PDF Export - Excel Export - Google Sheets API -
+ ) : ( + { + e.preventDefault(); + setInquirySubmitted(true); + }} + className="space-y-4" + > +
+ + +
+ +