diff --git a/backend/src/db.ts b/backend/src/db.ts index e3bbcc5f..323823af 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -262,6 +262,7 @@ export interface Ticket { userName: string; userRole: UserRole; type: 'general' | 'curriculum'; + priority: 'Low' | 'Medium' | 'High' | 'Urgent'; subject: string; description: string; status: 'Open' | 'Reviewed' | 'Resolved'; diff --git a/backend/src/index.ts b/backend/src/index.ts index 6f7663ca..a7090660 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -146,7 +146,7 @@ registerStatsRoutes(app); const user = getAuthUser(req); if (!user) return res.status(401).json({ error: 'Unauthorized' }); - const { type, subject, description } = req.body; + const { type, subject, description, priority } = req.body; if (type === 'curriculum' && user.role !== UserRole.TEACHER && user.role !== UserRole.VOLUNTEER) { return res.status(400).json({ error: 'Curriculum feedback can only be submitted by Teachers or Volunteers.' }); } @@ -158,6 +158,7 @@ registerStatsRoutes(app); userName: user.name, userRole: user.role, type: type || 'general', + priority: priority || 'Medium', subject, description, status: 'Open', diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 44b21b07..a42510c4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -208,11 +208,10 @@ return ; announcements.map(notif => (

{notif.title}

diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 85762ecc..4d699b60 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -214,6 +214,7 @@ export const Layout: React.FC = ({ break; } + list.push({ name: 'Feedback', view: 'tickets', icon: HelpCircle }); list.push({ name: 'Notifications', view: 'notifications', icon: Bell, badge: notifications.length > 0 ? String(notifications.length) : undefined }); list.push({ name: 'Settings', view: 'settings', icon: Settings }); @@ -451,11 +452,10 @@ export const Layout: React.FC = ({ {/* Sidebar Left panel */}
-
{/* Mobile Navigation Drawer */} @@ -593,11 +591,10 @@ export const Layout: React.FC = ({ setMobileOpen(false); } }} - className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-xs font-bold border transition ${ - isSelected + className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-xs font-bold border transition ${isSelected ? 'bg-primary-navy text-white border-accent-gold/30 shadow-sm dark:bg-indigo-900 dark:text-indigo-200 dark:border-indigo-800' : 'text-slate-650 hover:bg-slate-100 border-transparent dark:text-slate-400 dark:hover:bg-slate-800 dark:border-transparent' - }`} + }`} > {item.name} @@ -613,9 +610,8 @@ export const Layout: React.FC = ({ onSelectView(sub.view); setMobileOpen(false); }} - className={`flex w-full items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-md transition ${ - activeView === sub.view ? 'text-indigo-600 bg-indigo-50 dark:text-indigo-400 dark:bg-indigo-950/40' : 'text-slate-500 dark:text-slate-400' - }`} + className={`flex w-full items-center gap-2 px-3 py-1.5 text-xs font-medium rounded-md transition ${activeView === sub.view ? 'text-indigo-600 bg-indigo-50 dark:text-indigo-400 dark:bg-indigo-950/40' : 'text-slate-500 dark:text-slate-400' + }`} > {sub.name} diff --git a/frontend/src/components/TicketSubmission.tsx b/frontend/src/components/TicketSubmission.tsx index 230fa2c3..38f7be9b 100644 --- a/frontend/src/components/TicketSubmission.tsx +++ b/frontend/src/components/TicketSubmission.tsx @@ -12,9 +12,13 @@ export const TicketSubmission: React.FC = ({ token, userR const [subject, setSubject] = useState(''); const [description, setDescription] = useState(''); const [type, setType] = useState<'general' | 'curriculum'>('general'); + const [priority, setPriority] = useState<'Low' | 'Medium' | 'High' | 'Urgent'>('Medium'); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); + const [statusFilter, setStatusFilter] = useState<'All' | 'Open' | 'Reviewed' | 'Resolved'>('All'); + const [typeFilter, setTypeFilter] = useState<'All' | 'general' | 'curriculum'>('All'); + const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'priority_high' | 'priority_low'>('newest') const fetchTickets = async () => { try { @@ -34,6 +38,42 @@ export const TicketSubmission: React.FC = ({ token, userR fetchTickets(); }, [token]); + const priorityRank: Record = { + Low: 1, + Medium: 2, + High: 3, + Urgent: 4, + }; + + const visibleTickets = React.useMemo(() => { + let result = tickets.filter( + t => + (statusFilter === 'All' || t.status === statusFilter) && + (typeFilter === 'All' || t.type === typeFilter) + ); + + result = [...result].sort((a, b) => { + switch (sortOrder) { + case 'newest': + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + + case 'oldest': + return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + + case 'priority_high': + return priorityRank[b.priority] - priorityRank[a.priority]; + + case 'priority_low': + return priorityRank[a.priority] - priorityRank[b.priority]; + + default: + return 0; + } + }); + + return result; + }, [tickets, statusFilter, typeFilter, sortOrder]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!subject || !description) { @@ -51,7 +91,7 @@ export const TicketSubmission: React.FC = ({ token, userR 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - body: JSON.stringify({ type, subject, description }) + body: JSON.stringify({ type, subject, description, priority }) }); const data = await res.json(); if (res.ok) { @@ -59,6 +99,7 @@ export const TicketSubmission: React.FC = ({ token, userR setSubject(''); setDescription(''); fetchTickets(); + setTimeout(() => setSuccess(''), 4000); } else { setError(data.error || 'Failed to submit ticket.'); } @@ -117,6 +158,20 @@ export const TicketSubmission: React.FC = ({ token, userR +
+ + +
+
= ({ token, userR

{userRole === UserRole.SUPERADMIN ? 'Global Review Queue' : 'Your Submitted Tickets'}

+ {userRole === UserRole.SUPERADMIN && ( +
+ {/* Status Filter */} +
+ + +
- {tickets.length === 0 ? ( + {/* Type Filter */} +
+ + +
+ + {/* Sort Filter */} +
+ + +
+ + {/* Clear Filters */} + + + {/* Results Count */} +
+ Showing {visibleTickets.length}{' '} + {visibleTickets.length === 1 ? 'ticket' : 'tickets'} +
+
+ )} + {visibleTickets.length === 0 ? (
- No active feedback tickets found. + No tickets exist
) : (
- {tickets.map((t) => ( + {visibleTickets.map((t) => (
- + {t.type} - + {t.status} + + {t.priority || 'Medium'} +

{t.subject}

diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 8c22f262..f036ba77 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -149,6 +149,7 @@ export interface Ticket { description: string; status: 'Open' | 'Reviewed' | 'Resolved'; createdAt: string; + priority: 'Low' | 'Medium' | 'High' | 'Urgent'; } export interface LogEntry { diff --git a/package-lock.json b/package-lock.json index 78342e67..6d89f603 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,38 +100,6 @@ "node": ">=18" } }, - "backend/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=12" - } - }, - "backend/node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, "backend/node_modules/mongodb": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", @@ -3524,22 +3492,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/gcp-metadata": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", - "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3906,20 +3858,6 @@ "node": ">=8" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -5306,25 +5244,6 @@ "node": ">= 14" } }, - "node_modules/proxy-agent-negotiate": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", - "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "kerberos": "^2.0.0" - }, - "peerDependenciesMeta": { - "kerberos": { - "optional": true - } - } - }, "node_modules/proxy-agent/node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -5460,17 +5379,6 @@ } } }, - "node_modules/puppeteer/node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20" - } - }, "node_modules/puppeteer/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -5526,36 +5434,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/puppeteer/node_modules/data-uri-to-buffer": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", - "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20" - } - }, - "node_modules/puppeteer/node_modules/degenerator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", - "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "quickjs-wasi": "^2.2.0" - } - }, "node_modules/puppeteer/node_modules/devtools-protocol": { "version": "0.0.1653615", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz", @@ -5568,125 +5446,6 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, - "node_modules/puppeteer/node_modules/get-uri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.1.tgz", - "integrity": "sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "basic-ftp": "^5.3.1", - "data-uri-to-buffer": "8.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/puppeteer/node_modules/http-proxy-agent": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", - "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "proxy-agent-negotiate": "1.1.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/puppeteer/node_modules/https-proxy-agent": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", - "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "proxy-agent-negotiate": "1.1.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/puppeteer/node_modules/pac-proxy-agent": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.1.0.tgz", - "integrity": "sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "get-uri": "8.0.1", - "http-proxy-agent": "9.1.0", - "https-proxy-agent": "9.1.0", - "pac-resolver": "9.0.1", - "quickjs-wasi": "^2.2.0", - "socks-proxy-agent": "10.1.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/puppeteer/node_modules/pac-resolver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", - "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "degenerator": "7.0.1", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "quickjs-wasi": "^2.2.0" - } - }, - "node_modules/puppeteer/node_modules/proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.2.tgz", - "integrity": "sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "http-proxy-agent": "9.1.0", - "https-proxy-agent": "9.1.0", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "9.1.0", - "proxy-from-env": "^2.0.0", - "socks-proxy-agent": "10.1.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/puppeteer/node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/puppeteer/node_modules/puppeteer-core": { "version": "25.4.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.4.0.tgz", @@ -5704,22 +5463,6 @@ "node": ">=22.12.0" } }, - "node_modules/puppeteer/node_modules/socks-proxy-agent": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", - "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/puppeteer/node_modules/string-width": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", @@ -5916,14 +5659,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/quickjs-wasi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", - "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",