Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
314b999
feat: add admin layout, access control, and navigation
tm-cj-salces Mar 23, 2026
5823704
feat: set up Vitest testing infrastructure
tm-cj-salces Mar 23, 2026
430366b
refactor: extract admin types and shared query functions
tm-cj-salces Mar 23, 2026
6c0f722
feat: add admin dashboard with pie charts and activity feed
tm-cj-salces Mar 24, 2026
8126485
feat: add shared admin list components
tm-cj-salces Mar 24, 2026
39236fb
fix: filter applicant profiles in progress calculations for sigsheet …
tm-cj-salces Mar 24, 2026
3446da2
Merge branch 'up-csi:main' into main
rue-22 Apr 8, 2026
8e49832
feat: add fetchQuizRespondents query and QuizRespondent type
tm-cj-salces Apr 8, 2026
2e6e32f
feat: add shared AdminListView component
tm-cj-salces Apr 8, 2026
3f74425
feat: add constiquiz respondent list page
tm-cj-salces Apr 8, 2026
87b3de5
fix: remove username field from constiquiz respondent list
tm-cj-salces Apr 8, 2026
47f884d
fix: add page padding and larger heading to AdminListView
tm-cj-salces Apr 8, 2026
674fef5
feat: add pagination-related components
tm-cj-salces Apr 8, 2026
ae1ca32
feat: implement dropdown components
tm-cj-salces Apr 8, 2026
c80606b
chore: modify .gitignore
tm-cj-salces Apr 8, 2026
b757cc4
feat: add new type SigsheetRespondent
Harry2166 Apr 10, 2026
91e7171
feat: create fetchSigsheetRespondents function
Harry2166 Apr 10, 2026
0ad8024
feat: Create admin sigsheet progress list page
Harry2166 Apr 10, 2026
e33da6f
refactor: use one query for members table
Harry2166 Apr 10, 2026
d4babfb
refactor: optimize aggregation pattern in fetchSigsheetRespondents
Harry2166 Apr 10, 2026
8f640b6
feat: remove sorting for committees
Harry2166 Apr 10, 2026
2dfd2f9
refactor: make variable name more descriptive
Harry2166 Apr 10, 2026
3651aaa
refactor: add the SigsheetProfileSummary type
RenzSTP Apr 15, 2026
7fd3ef7
feat: extend functionality of fetchSigsheetDetail()
RenzSTP Apr 15, 2026
c257fda
fix: modify tests to accommodate renamed properties
RenzSTP Apr 15, 2026
d33b775
feat: add sigsheet profiles for applicants
RenzSTP Apr 15, 2026
ae201f3
chore: tidy formatting
RenzSTP Apr 15, 2026
f68af17
fix: drop stray imports, rename [id] to [userId], clean cast
tm-cj-salces Apr 21, 2026
3067b01
feat(admin): extend quiz result detail query
tm-cj-salces Apr 21, 2026
c08190c
feat(api): add constiquiz grading endpoint
tm-cj-salces Apr 21, 2026
5f26e65
feat(admin): add constiquiz respondent detail page
tm-cj-salces Apr 21, 2026
b280f4c
refactor: load private env at runtime
tm-cj-salces May 12, 2026
864defb
fix(api): harden quiz answer submission
tm-cj-salces May 12, 2026
28cb6ec
fix(applicant): avoid unnecessary sigsheet setup
tm-cj-salces May 12, 2026
7cdd874
fix(api): tighten Drive folder and upload validation
tm-cj-salces May 12, 2026
2c8b635
chore: add `.env.example`
tm-cj-salces May 12, 2026
dc4ead9
fix(admin): redirect admins from home
tm-cj-salces May 13, 2026
ac95cb2
fix(admin): make quiz nav scrollable
tm-cj-salces May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PUBLIC_SUPABASE_URL=
PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_KEY=
GOOGLE_SERVICE_EMAIL=
GOOGLE_PRIVATE_KEY=
CONTAINER_PORT=3000
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ CLAUDE.md
.agents/
.claude/
skills-lock.json
.DS_Store
60 changes: 60 additions & 0 deletions src/lib/admin/AdminListView.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import FilterDropdown from './FilterDropdown.svelte';
import PaginatedTable from './PaginatedTable.svelte';
import SearchInput from './SearchInput.svelte';
import type { Snippet } from 'svelte';
import { fly } from 'svelte/transition';

interface Column {
key: string;
header: string;
searchable?: boolean;
sortable?: boolean;
}

interface CellContext {
row: Record<string, unknown>;
column: Column;
value: unknown;
}

/* eslint-disable prefer-const */
let {
title,
data,
columns,
rowKey,
filterKey = '',
filterOptions = ['all', 'Not Started', 'In Progress', 'Completed'],
searchPlaceholder = 'Search applicant',
onRowClick,
cell,
}: {
title: string;
data: Record<string, unknown>[];
columns: Column[];
rowKey: string;
filterKey?: string;
filterOptions?: string[];
searchPlaceholder?: string;
onRowClick?: (row: Record<string, unknown>) => void;
cell?: Snippet<[CellContext]>;
} = $props();
/* eslint-enable prefer-const */

let searchTerm = $state('');
let filterValue = $state('all');
</script>

<div class="flex w-full flex-col gap-6 px-8 py-12" in:fly={{ y: 12, duration: 280 }}>
<h1 class="text-csi-white text-4xl font-bold">{title}</h1>

<div class="flex items-center gap-4">
<div class="flex-1">
<SearchInput bind:value={searchTerm} placeholder={searchPlaceholder} />
</div>
<FilterDropdown bind:value={filterValue} options={filterOptions} />
</div>

<PaginatedTable {data} {columns} {rowKey} {searchTerm} {filterKey} {filterValue} {onRowClick} {cell} />
</div>
9 changes: 7 additions & 2 deletions src/lib/admin/FilterDropdown.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import { fly } from 'svelte/transition';

/* eslint-disable prefer-const */
let {
value = $bindable('all'),
Expand Down Expand Up @@ -36,7 +38,7 @@
<button
type="button"
onclick={() => (open = !open)}
class="text-csi-white hover:border-csi-blue flex items-center gap-2 rounded-lg border border-[#5A5A5A] bg-transparent px-4 py-2.5 text-sm transition-colors"
class="text-csi-white hover:border-csi-blue flex items-center gap-2 rounded-lg border border-[#5A5A5A] bg-transparent px-4 py-2.5 text-sm transition-colors duration-150"
>
Filter
{#if value !== 'all'}
Expand All @@ -45,7 +47,10 @@
</button>

{#if open}
<div class="absolute top-full right-0 z-10 mt-1 min-w-[160px] rounded-lg bg-[#2A2A2D] py-1 shadow-lg">
<div
class="absolute top-full right-0 z-10 mt-1 min-w-[160px] rounded-lg bg-[#2A2A2D] py-1 shadow-lg"
transition:fly={{ y: -4, duration: 140 }}
>
{#each options as option (option)}
<button
type="button"
Expand Down
62 changes: 51 additions & 11 deletions src/lib/admin/PaginatedTable.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script lang="ts">
import Pagination from './Pagination.svelte';
import type { Snippet } from 'svelte';
import { fade } from 'svelte/transition';
import { flip } from 'svelte/animate';

interface Column {
key: string;
header: string;
searchable?: boolean;
sortable?: boolean;
}

interface CellContext {
Expand All @@ -18,29 +21,46 @@
let {
data,
columns,
rowKey,
searchTerm = '',
filterKey = '',
filterValue = 'all',
sortKey = '',
sortDirection = 'asc',
pageSize = 20,
onRowClick,
cell,
}: {
data: Record<string, unknown>[];
columns: Column[];
rowKey: string;
searchTerm?: string;
filterKey?: string;
filterValue?: string;
sortKey?: string;
sortDirection?: 'asc' | 'desc';
pageSize?: number;
onRowClick?: (row: Record<string, unknown>) => void;
cell?: Snippet<[CellContext]>;
} = $props();
/* eslint-enable prefer-const */

let currentPage = $state(1);
let sortKey = $state('');
let sortDirection = $state<'asc' | 'desc'>('asc');

function handleSort(key: string) {
if (sortKey !== key) {
sortKey = key;
sortDirection = 'asc';
} else if (sortDirection === 'asc') {
sortDirection = 'desc';
} else {
sortKey = '';
sortDirection = 'asc';
}
}

function ariaSortFor(key: string): 'ascending' | 'descending' | 'none' {
if (sortKey !== key) return 'none';
return sortDirection === 'asc' ? 'ascending' : 'descending';
}

const filteredBySearch = $derived.by(() => {
if (!searchTerm) return data;
Expand Down Expand Up @@ -96,18 +116,38 @@
<thead>
<tr class="border-b border-[#2A2A2D]">
{#each columns as col (col.key)}
<th
class="text-csi-neutral-400 px-6 py-4 text-left text-xs font-medium tracking-wider uppercase"
>
{col.header}
<th aria-sort={ariaSortFor(col.key)}>
{#if col.sortable}
<button
type="button"
onclick={() => handleSort(col.key)}
class="text-csi-neutral-400 hover:text-csi-white flex w-full items-center gap-1.5 px-6 py-4 text-left text-xs font-medium tracking-wider uppercase transition-colors duration-150"
>
{col.header}
{#if sortKey === col.key}
<span class="text-csi-blue">
{sortDirection === 'asc' ? '\u2191' : '\u2193'}
</span>
{/if}
</button>
{:else}
<div
class="text-csi-neutral-400 px-6 py-4 text-left text-xs font-medium tracking-wider uppercase"
>
{col.header}
</div>
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each paginatedData as row, i (i)}
{#each paginatedData as row (row[rowKey] as string | number)}
<tr
class="border-b border-[#2A2A2D] transition-colors hover:bg-[#2A2A2D]
animate:flip={{ duration: 220 }}
in:fade={{ duration: 180 }}
out:fade={{ duration: 120 }}
class="border-b border-[#2A2A2D] transition-colors duration-150 last:border-b-0 hover:bg-[#2A2A2D]
{onRowClick ? 'cursor-pointer' : ''}"
onclick={() => onRowClick?.(row)}
>
Expand All @@ -125,7 +165,7 @@

{#if paginatedData.length === 0}
<tr>
<td colspan={columns.length} class="text-csi-neutral-400 px-6 py-8 text-center text-sm">
<td colspan={columns.length} class="text-csi-neutral-400 px-6 py-12 text-center text-sm">
No results found
</td>
</tr>
Expand Down
10 changes: 6 additions & 4 deletions src/lib/admin/Pagination.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,29 @@
</script>

{#if totalPages > 1}
<div class="mt-4 flex items-center justify-center gap-4 border-t border-[#2A2A2D] pt-4">
<div class="flex items-center justify-center gap-4 border-t border-[#2A2A2D] px-6 py-4">
<button
class="text-csi-white hover:bg-csi-neutral-900 rounded-lg bg-[#2A2A2D] p-2 transition-colors disabled:cursor-not-allowed disabled:opacity-30"
class="text-csi-white hover:bg-csi-neutral-900 rounded-md p-1.5 transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-30"
disabled={currentPage === 1}
onclick={() => {
currentPage = Math.max(1, currentPage - 1);
}}
aria-label="Previous page"
>
<ChevronLeft class="h-4 w-4" />
</button>

<span class="text-csi-neutral-400 text-sm">
<span class="text-csi-neutral-400 text-sm tabular-nums">
Page {currentPage} of {totalPages}
</span>

<button
class="text-csi-white hover:bg-csi-neutral-900 rounded-lg bg-[#2A2A2D] p-2 transition-colors disabled:cursor-not-allowed disabled:opacity-30"
class="text-csi-white hover:bg-csi-neutral-900 rounded-md p-1.5 transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-30"
disabled={currentPage >= totalPages}
onclick={() => {
currentPage = Math.min(totalPages, currentPage + 1);
}}
aria-label="Next page"
>
<ChevronRight class="h-4 w-4" />
</button>
Expand Down
71 changes: 0 additions & 71 deletions src/lib/admin/SortDropdown.svelte

This file was deleted.

44 changes: 42 additions & 2 deletions src/lib/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export interface QuizResultSummary {
total_score: number;
}

/** A quiz question option shown to admins when reviewing choice answers. */
export interface QuizOptionDetail {
option_id: number;
title: string | null;
is_correct: boolean;
}

/** Detailed quiz answer with nested question and section info. */
export interface QuizAnswerDetail {
answer_id: string;
Expand All @@ -27,8 +34,9 @@ export interface QuizAnswerDetail {
question: {
title: string;
point_value: number;
type: string;
section: { title: string };
type: 'radio' | 'checkbox' | 'short_text' | 'long_text';
section: { section_id: number; title: string };
options: QuizOptionDetail[];
};
}

Expand All @@ -37,6 +45,9 @@ export interface QuizResultDetail {
profile: { id: string; username: string; full_name: string } | null;
submitted_at: string | null;
answers: QuizAnswerDetail[];
max_score: number;
current_score: number;
status: 'Not Started' | 'In Progress' | 'Completed';
}

/** A single signature entry in the sigsheet. */
Expand All @@ -61,3 +72,32 @@ export interface GradeInput {
max_score: number;
remarks?: string;
}

/** Row shape for the constiquiz respondent list page (P02-001). */
export interface QuizRespondent {
user_id: string;
full_name: string;
status: 'Not Started' | 'In Progress' | 'Completed';
current_score: number;
}

/** Sigsheet respondent with per-committee breakdown for P03-001. */
export interface SigsheetRespondent {
user_id: string;
full_name: string;
username: string;
total_signatures: number;
by_committee: Record<string, number>;
status: 'Not Started' | 'In Progress' | 'Completed';
}

/** Sigsheet summary for applicants on a per-committee breakdown for P03-002. */
export interface SigsheetProfileSummary {
profile: { id: string; username: string; full_name: string };
signatures: SigsheetSignatureDetail[];
by_committee: Record<string, number>;
committee_totals: Record<string, number>;
total_signatures: number;
total_members: number;
status: 'Not Started' | 'In Progress' | 'Completed';
}
Loading
Loading