diff --git a/app/api/mht-cet/state-cutoffs/route.ts b/app/api/mht-cet/state-cutoffs/route.ts
index 2451aa8..0763117 100644
--- a/app/api/mht-cet/state-cutoffs/route.ts
+++ b/app/api/mht-cet/state-cutoffs/route.ts
@@ -13,6 +13,8 @@ export async function POST(request: NextRequest) {
const categories = body.categories || [];
const seatAllocations = body.seatAllocations || [];
const courses = body.courses || [];
+ const statuses = body.statuses || [];
+ const homeUniversities = body.homeUniversities || [];
const percentileInput = body.percentileInput || '';
const sortBy = body.sortBy || 'last_rank';
const sortOrder = body.sortOrder || 'desc';
@@ -40,6 +42,8 @@ export async function POST(request: NextRequest) {
cutoff_score: '92.6268989',
last_rank: '1000',
total_admitted: 60,
+ status: 'Government',
+ home_university: 'Mumbai University',
created: new Date().toISOString(),
updated: new Date().toISOString()
};
@@ -56,6 +60,8 @@ export async function POST(request: NextRequest) {
cutoff_score: '91.6268989',
last_rank: '1500',
total_admitted: 45,
+ status: 'Government Autonomous',
+ home_university: 'Savitribai Phule Pune University',
created: new Date().toISOString(),
updated: new Date().toISOString()
};
@@ -71,6 +77,8 @@ export async function POST(request: NextRequest) {
cutoff_score: String((99.5 - (i * 0.02)).toFixed(7)), // More varied precise decimal values
last_rank: String(1000 + (i * 50)),
total_admitted: 60 + (i % 20),
+ status: ['Government', 'Government Autonomous', 'Un-Aided', 'University', 'Deemed University Autonomous'][i % 5],
+ home_university: ['Mumbai University', 'Savitribai Phule Pune University', 'Shivaji University', 'Dr. Babasaheb Ambedkar Marathwada University', 'Rashtrasant Tukadoji Maharaj Nagpur University'][i % 5],
created: new Date().toISOString(),
updated: new Date().toISOString()
};
@@ -94,6 +102,21 @@ export async function POST(request: NextRequest) {
mockData = mockData.filter((item: any) => seatAllocations.includes(item.seat_allocation_section));
}
+ // Apply status filter if provided
+ if (statuses && statuses.length > 0) {
+ mockData = mockData.filter((item: any) => statuses.includes(item.status));
+ }
+
+ // Apply home university filter if provided
+ if (homeUniversities && homeUniversities.length > 0) {
+ mockData = mockData.filter((item: any) => homeUniversities.includes(item.home_university));
+ }
+
+ // Apply courses filter if provided
+ if (courses && courses.length > 0) {
+ mockData = mockData.filter((item: any) => courses.includes(item.course_name));
+ }
+
// Apply percentile filter if provided
if (percentileInput && !isNaN(parseFloat(percentileInput))) {
const targetPercentile = parseFloat(percentileInput);
@@ -176,6 +199,16 @@ export async function POST(request: NextRequest) {
filterParts.push(`(${seatFilter})`);
}
+ if (statuses && Array.isArray(statuses) && statuses.length > 0) {
+ const statusFilter = statuses.map((status: string) => `status = "${status}"`).join(' || ');
+ filterParts.push(`(${statusFilter})`);
+ }
+
+ if (homeUniversities && Array.isArray(homeUniversities) && homeUniversities.length > 0) {
+ const homeUniversityFilter = homeUniversities.map((uni: string) => `home_university = "${uni}"`).join(' || ');
+ filterParts.push(`(${homeUniversityFilter})`);
+ }
+
// Percentile-based filtering (-1 range only) - filtering cutoff_score directly
if (percentileInput && !isNaN(parseFloat(percentileInput))) {
const targetPercentile = parseFloat(percentileInput);
@@ -209,7 +242,7 @@ export async function POST(request: NextRequest) {
});
try {
- const result = await pb.collection('2024_mht_cet_round_one_cutoffs').getList(
+ const result = await pb.collection('2024_mht_cet_round_one_cutoffs_duplicate').getList(
page,
perPage,
{
@@ -258,6 +291,8 @@ export async function POST(request: NextRequest) {
cutoff_score: '97.989925',
last_rank: '500',
total_admitted: 60,
+ status: 'Government',
+ home_university: 'Mumbai University',
created: new Date().toISOString(),
updated: new Date().toISOString()
};
@@ -274,6 +309,8 @@ export async function POST(request: NextRequest) {
cutoff_score: '92.6268989',
last_rank: '1000',
total_admitted: 60,
+ status: 'Government Autonomous',
+ home_university: 'Savitribai Phule Pune University',
created: new Date().toISOString(),
updated: new Date().toISOString()
};
@@ -290,6 +327,8 @@ export async function POST(request: NextRequest) {
cutoff_score: '91.6268989',
last_rank: '1500',
total_admitted: 45,
+ status: 'Un-Aided',
+ home_university: 'Shivaji University',
created: new Date().toISOString(),
updated: new Date().toISOString()
};
@@ -305,6 +344,8 @@ export async function POST(request: NextRequest) {
cutoff_score: String((99.5 - (i * 0.01)).toFixed(7)), // More realistic percentile values
last_rank: String(1000 + (i * 50)),
total_admitted: 60 + (i % 20),
+ status: ['Government', 'Government Autonomous', 'Un-Aided', 'University', 'Deemed University Autonomous'][i % 5],
+ home_university: ['Mumbai University', 'Savitribai Phule Pune University', 'Shivaji University', 'Dr. Babasaheb Ambedkar Marathwada University', 'Rashtrasant Tukadoji Maharaj Nagpur University'][i % 5],
created: new Date().toISOString(),
updated: new Date().toISOString()
};
@@ -318,6 +359,31 @@ export async function POST(request: NextRequest) {
);
}
+ // Apply category filter to mock data if provided
+ if (categories && categories.length > 0) {
+ mockData = mockData.filter((item: any) => categories.includes(item.category));
+ }
+
+ // Apply seat allocation filter to mock data if provided
+ if (seatAllocations && seatAllocations.length > 0) {
+ mockData = mockData.filter((item: any) => seatAllocations.includes(item.seat_allocation_section));
+ }
+
+ // Apply status filter to mock data if provided
+ if (statuses && statuses.length > 0) {
+ mockData = mockData.filter((item: any) => statuses.includes(item.status));
+ }
+
+ // Apply home university filter to mock data if provided
+ if (homeUniversities && homeUniversities.length > 0) {
+ mockData = mockData.filter((item: any) => homeUniversities.includes(item.home_university));
+ }
+
+ // Apply courses filter to mock data if provided
+ if (courses && courses.length > 0) {
+ mockData = mockData.filter((item: any) => courses.includes(item.course_name));
+ }
+
// Apply percentile filter to mock data if provided
if (percentileInput && !isNaN(parseFloat(percentileInput))) {
const targetPercentile = parseFloat(percentileInput);
diff --git a/app/mht-cet/state-cutoffs/page.tsx b/app/mht-cet/state-cutoffs/page.tsx
index 1e10462..8a21289 100644
--- a/app/mht-cet/state-cutoffs/page.tsx
+++ b/app/mht-cet/state-cutoffs/page.tsx
@@ -1,12 +1,11 @@
'use client';
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
-import { getPocketBaseClient } from '@/lib/pocketbaseClient';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
-import { Loader2, Search, Filter, RefreshCw, ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
+import { Loader2, Search, Filter, ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import {
Select,
SelectContent,
@@ -57,6 +56,7 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { BookUser, Building, GraduationCap, Info, Lightbulb, MapPin, ShieldCheck, Users, Video } from 'lucide-react';
+import CategoryFlowChart from '@/components/CategoryFlowChart';
// Types
interface CutoffRecord {
@@ -70,6 +70,8 @@ interface CutoffRecord {
cutoff_score: string;
last_rank: string;
total_admitted: number;
+ status: string;
+ home_university: string;
created: string;
updated: string;
}
@@ -77,8 +79,9 @@ interface CutoffRecord {
interface FilterState {
search: string;
categories: string[];
- seatAllocations: string[];
courses: string[];
+ statuses: string[];
+ homeUniversities: string[];
percentileInput: string;
sortBy: string;
sortOrder: 'asc' | 'desc';
@@ -87,8 +90,9 @@ interface FilterState {
interface PendingFilters {
search: string;
categories: string[];
- seatAllocations: string[];
courses: string[];
+ statuses: string[];
+ homeUniversities: string[];
percentileInput: string;
}
@@ -260,11 +264,53 @@ const COURSE_GROUPS = {
]
};
-const SEAT_ALLOCATION_OPTIONS = [
- { value: 'OTHER_TO_OTHER', label: 'Other to Other University' },
- { value: 'HOME_TO_OTHER', label: 'Home to Other University' },
- { value: 'HOME_TO_HOME', label: 'Home to Home University' },
- { value: 'STATE_LEVEL', label: 'State Level Seats' }
+const STATUS_OPTIONS = [
+ { value: 'Deemed University Autonomous', label: 'Deemed University Autonomous' },
+ { value: 'Government', label: 'Government' },
+ { value: 'Government Autonomous', label: 'Government Autonomous' },
+ { value: 'Government-Aided Autonomous', label: 'Government-Aided Autonomous' },
+ { value: 'Un-Aided', label: 'Un-Aided' },
+ { value: 'Un-Aided Autonomous', label: 'Un-Aided Autonomous' },
+ { value: 'Un-Aided Autonomous Linguistic Minority - Gujarathi', label: 'Un-Aided Autonomous Linguistic Minority - Gujarathi' },
+ { value: 'Un-Aided Autonomous Linguistic Minority - Gujarathi(Jain)', label: 'Un-Aided Autonomous Linguistic Minority - Gujarathi(Jain)' },
+ { value: 'Un-Aided Autonomous Linguistic Minority - Hindi', label: 'Un-Aided Autonomous Linguistic Minority - Hindi' },
+ { value: 'Un-Aided Autonomous Linguistic Minority - Malyalam', label: 'Un-Aided Autonomous Linguistic Minority - Malyalam' },
+ { value: 'Un-Aided Autonomous Linguistic Minority - Sindhi', label: 'Un-Aided Autonomous Linguistic Minority - Sindhi' },
+ { value: 'Un-Aided Autonomous Linguistic Minority - Tamil', label: 'Un-Aided Autonomous Linguistic Minority - Tamil' },
+ { value: 'Un-Aided Autonomous Religious Minority - Christian', label: 'Un-Aided Autonomous Religious Minority - Christian' },
+ { value: 'Un-Aided Autonomous Religious Minority - Jain', label: 'Un-Aided Autonomous Religious Minority - Jain' },
+ { value: 'Un-Aided Linguistic Minority - Gujar', label: 'Un-Aided Linguistic Minority - Gujar' },
+ { value: 'Un-Aided Linguistic Minority - Gujarathi', label: 'Un-Aided Linguistic Minority - Gujarathi' },
+ { value: 'Un-Aided Linguistic Minority - Hindi', label: 'Un-Aided Linguistic Minority - Hindi' },
+ { value: 'Un-Aided Linguistic Minority - Malyalam', label: 'Un-Aided Linguistic Minority - Malyalam' },
+ { value: 'Un-Aided Linguistic Minority - Punjabi', label: 'Un-Aided Linguistic Minority - Punjabi' },
+ { value: 'Un-Aided Linguistic Minority - Sindhi', label: 'Un-Aided Linguistic Minority - Sindhi' },
+ { value: 'Un-Aided Religious Minority - Christian', label: 'Un-Aided Religious Minority - Christian' },
+ { value: 'Un-Aided Religious Minority - Jain', label: 'Un-Aided Religious Minority - Jain' },
+ { value: 'Un-Aided Religious Minority - Muslim', label: 'Un-Aided Religious Minority - Muslim' },
+ { value: 'Un-Aided Religious Minority - Roman Catholics', label: 'Un-Aided Religious Minority - Roman Catholics' },
+ { value: 'University', label: 'University' },
+ { value: 'University Autonomous', label: 'University Autonomous' },
+ { value: 'University Department', label: 'University Department' },
+ { value: 'University Managed (Un-Aided)', label: 'University Managed (Un-Aided)' },
+ { value: 'University Managed Autonomous', label: 'University Managed Autonomous' }
+];
+
+const HOME_UNIVERSITY_OPTIONS = [
+ { value: 'Autonomous Institute', label: 'Autonomous Institute' },
+ { value: 'Deemed to be University', label: 'Deemed to be University' },
+ { value: 'Dr. Babasaheb Ambedkar Marathwada University', label: 'Dr. Babasaheb Ambedkar Marathwada University' },
+ { value: 'Dr. Babasaheb Ambedkar Technological University Lonere', label: 'Dr. Babasaheb Ambedkar Technological University Lonere' },
+ { value: 'Gondwana University', label: 'Gondwana University' },
+ { value: 'Kavayitri Bahinabai Chaudhari North Maharashtra University Jalgaon', label: 'Kavayitri Bahinabai Chaudhari North Maharashtra University Jalgaon' },
+ { value: 'Mumbai University', label: 'Mumbai University' },
+ { value: 'Punyashlok Ahilyadevi Holkar Solapur University', label: 'Punyashlok Ahilyadevi Holkar Solapur University' },
+ { value: 'Rashtrasant Tukadoji Maharaj Nagpur University', label: 'Rashtrasant Tukadoji Maharaj Nagpur University' },
+ { value: 'SNDT Women s University', label: 'SNDT Women s University' },
+ { value: 'Sant Gadge Baba Amravati University', label: 'Sant Gadge Baba Amravati University' },
+ { value: 'Savitribai Phule Pune University', label: 'Savitribai Phule Pune University' },
+ { value: 'Shivaji University', label: 'Shivaji University' },
+ { value: 'Swami Ramanand Teerth Marathwada University Nanded', label: 'Swami Ramanand Teerth Marathwada University Nanded' }
];
const ITEMS_PER_PAGE_OPTIONS = [10, 25, 50, 100, 200];
@@ -285,7 +331,7 @@ const calculatePercentile = (rank: string | number): number => {
const getPrecisePercentileRange = (targetPercentile: number): { min: number; max: number } => {
// Use higher precision (10 decimal places) to avoid floating-point errors
const max = Math.round(targetPercentile * 10000000000) / 10000000000;
- const min = Math.max(0, Math.round((targetPercentile - 1) * 10000000000) / 10000000000);
+ const min = Math.max(0, Math.round((targetPercentile - 10) * 10000000000) / 10000000000);
return { min, max };
};
@@ -318,8 +364,9 @@ export default function StateCutoffsPage() {
const [filters, setFilters] = useState({
search: '',
categories: [],
- seatAllocations: [],
courses: [],
+ statuses: [],
+ homeUniversities: [],
percentileInput: '',
sortBy: 'last_rank',
sortOrder: 'desc'
@@ -329,8 +376,9 @@ export default function StateCutoffsPage() {
const [pendingFilters, setPendingFilters] = useState({
search: '',
categories: [],
- seatAllocations: [],
courses: [],
+ statuses: [],
+ homeUniversities: [],
percentileInput: ''
});
@@ -344,6 +392,7 @@ export default function StateCutoffsPage() {
const currentRequestParamsRef = useRef('');
const requestIdRef = useRef(0);
const lastPaginationClickRef = useRef(0);
+ const prevItemsPerPageRef = useRef(itemsPerPage);
// Table state
const [sorting, setSorting] = useState([{ id: 'cutoff_score', desc: true }])
@@ -487,41 +536,6 @@ export default function StateCutoffsPage() {
),
size: 100,
},
- {
- accessorKey: "seat_allocation_section",
- header: ({ column }) => {
- return (
- column.toggleSorting(column.getIsSorted() === "asc")}
- className="h-8 sm:h-10 px-2 sm:px-3 lg:px-4 font-abel text-xs sm:text-sm lg:text-base"
- >
- Seat Allocation
-
-
- )
- },
- cell: ({ row }) => {
- const value = row.getValue("seat_allocation_section") as string;
- const option = SEAT_ALLOCATION_OPTIONS.find(s => s.value === value);
- const label = option?.label || value;
- return (
-
-
-
-
- {label}
-
-
-
- {label}
-
-
-
- );
- },
- size: 150,
- },
{
accessorKey: "total_admitted",
header: ({ column }) => {
@@ -615,6 +629,66 @@ export default function StateCutoffsPage() {
),
size: 110,
},
+ {
+ accessorKey: "status",
+ header: ({ column }) => {
+ return (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ className="h-8 sm:h-10 px-2 sm:px-3 lg:px-4 font-abel text-xs sm:text-sm lg:text-base"
+ >
+ Status
+
+
+ )
+ },
+ cell: ({ row }) => (
+
+
+
+
+ {row.getValue("status")}
+
+
+
+ {row.getValue("status")}
+
+
+
+ ),
+ size: 180,
+ },
+ {
+ accessorKey: "home_university",
+ header: ({ column }) => {
+ return (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ className="h-8 sm:h-10 px-2 sm:px-3 lg:px-4 font-abel text-xs sm:text-sm lg:text-base"
+ >
+ Home University
+
+
+ )
+ },
+ cell: ({ row }) => (
+
+
+
+
+ {row.getValue("home_university")}
+
+
+
+ {row.getValue("home_university")}
+
+
+
+ ),
+ size: 250,
+ },
],
[]
);
@@ -651,8 +725,9 @@ export default function StateCutoffsPage() {
const filtersChanged = (
pendingFilters.search !== filters.search ||
JSON.stringify(pendingFilters.categories) !== JSON.stringify(filters.categories) ||
- JSON.stringify(pendingFilters.seatAllocations) !== JSON.stringify(filters.seatAllocations) ||
JSON.stringify(pendingFilters.courses) !== JSON.stringify(filters.courses) ||
+ JSON.stringify(pendingFilters.statuses) !== JSON.stringify(filters.statuses) ||
+ JSON.stringify(pendingFilters.homeUniversities) !== JSON.stringify(filters.homeUniversities) ||
pendingFilters.percentileInput !== filters.percentileInput
);
setHasUnsavedChanges(filtersChanged);
@@ -660,10 +735,11 @@ export default function StateCutoffsPage() {
useEffect(() => {
table.setPageSize(itemsPerPage);
- // Reset to first page when items per page changes
- if (currentPage > 1) {
+ // Reset to first page when items per page changes (but not on currentPage changes)
+ if (prevItemsPerPageRef.current !== itemsPerPage && currentPage > 1) {
setCurrentPage(1);
}
+ prevItemsPerPageRef.current = itemsPerPage;
}, [itemsPerPage, table, currentPage]);
// Ensure current page is valid when total items change
@@ -687,15 +763,14 @@ export default function StateCutoffsPage() {
return;
}
- console.log('Fetching records for page:', currentPage, 'with filters:', debouncedFilters);
-
const requestBody = {
page: currentPage,
perPage: itemsPerPage,
search: debouncedFilters.search,
categories: debouncedFilters.categories,
- seatAllocations: debouncedFilters.seatAllocations,
courses: debouncedFilters.courses,
+ statuses: debouncedFilters.statuses,
+ homeUniversities: debouncedFilters.homeUniversities,
percentileInput: debouncedFilters.percentileInput,
sortBy: debouncedFilters.sortBy,
sortOrder: debouncedFilters.sortOrder,
@@ -704,7 +779,7 @@ export default function StateCutoffsPage() {
const cacheKey = JSON.stringify(requestBody);
// Check if this is the same request type (only different pagination)
- const currentRequestKey = `${debouncedFilters.search}-${debouncedFilters.categories.join(',')}-${debouncedFilters.seatAllocations.join(',')}-${debouncedFilters.courses.join(',')}-${debouncedFilters.percentileInput}`;
+ const currentRequestKey = `${debouncedFilters.search}-${debouncedFilters.categories.join(',')}-${debouncedFilters.courses.join(',')}-${debouncedFilters.statuses.join(',')}-${debouncedFilters.homeUniversities.join(',')}-${debouncedFilters.percentileInput}`;
const isOnlyPaginationChange = currentRequestParamsRef.current === currentRequestKey;
// Check cache first
@@ -736,8 +811,6 @@ export default function StateCutoffsPage() {
setLoading(true);
try {
- console.log(`Making API POST request ${requestId} with body:`, requestBody);
-
const response = await fetch(`/api/mht-cet/state-cutoffs`, {
method: 'POST',
signal: abortControllerRef.current.signal,
@@ -750,7 +823,6 @@ export default function StateCutoffsPage() {
// Check if this is still the latest request
if (requestId !== requestIdRef.current) {
- console.log(`Request ${requestId} is outdated, ignoring response`);
return;
}
@@ -762,7 +834,6 @@ export default function StateCutoffsPage() {
// Double-check we're still the latest request
if (requestId !== requestIdRef.current) {
- console.log(`Request ${requestId} is outdated after parsing, ignoring response`);
return;
}
@@ -770,8 +841,6 @@ export default function StateCutoffsPage() {
throw new Error(result.error || 'Failed to fetch data');
}
- console.log(`Request ${requestId} completed successfully`);
-
// Cache the result (limit cache size to prevent memory issues)
if (cacheRef.current.size > 50) {
const firstKey = cacheRef.current.keys().next().value;
@@ -794,13 +863,11 @@ export default function StateCutoffsPage() {
} catch (error: any) {
// Check if this is still the latest request
if (requestId !== requestIdRef.current) {
- console.log(`Request ${requestId} error is outdated, ignoring`);
return;
}
if (error.name === 'AbortError') {
// Request was cancelled, don't show error
- console.log(`Request ${requestId} was cancelled`);
return;
}
@@ -832,7 +899,7 @@ export default function StateCutoffsPage() {
clearTimeout(paginationTimeoutRef.current);
}
};
- }, [fetchRecords]);
+ }, [fetchRecords, currentPage]);
// Cleanup effect
useEffect(() => {
@@ -855,21 +922,30 @@ export default function StateCutoffsPage() {
}));
}, []);
- const handleSeatAllocationToggle = useCallback((seatAllocation: string) => {
+ const handleCourseToggle = useCallback((course: string) => {
setPendingFilters(prev => ({
...prev,
- seatAllocations: prev.seatAllocations.includes(seatAllocation)
- ? prev.seatAllocations.filter(s => s !== seatAllocation)
- : [...prev.seatAllocations, seatAllocation]
+ courses: prev.courses.includes(course)
+ ? prev.courses.filter(c => c !== course)
+ : [...prev.courses, course]
}));
}, []);
- const handleCourseToggle = useCallback((course: string) => {
+ const handleStatusToggle = useCallback((status: string) => {
setPendingFilters(prev => ({
...prev,
- courses: prev.courses.includes(course)
- ? prev.courses.filter(c => c !== course)
- : [...prev.courses, course]
+ statuses: prev.statuses.includes(status)
+ ? prev.statuses.filter(s => s !== status)
+ : [...prev.statuses, status]
+ }));
+ }, []);
+
+ const handleHomeUniversityToggle = useCallback((homeUniversity: string) => {
+ setPendingFilters(prev => ({
+ ...prev,
+ homeUniversities: prev.homeUniversities.includes(homeUniversity)
+ ? prev.homeUniversities.filter(h => h !== homeUniversity)
+ : [...prev.homeUniversities, homeUniversity]
}));
}, []);
@@ -881,8 +957,9 @@ export default function StateCutoffsPage() {
...prev,
search: pendingFilters.search,
categories: pendingFilters.categories,
- seatAllocations: pendingFilters.seatAllocations,
courses: pendingFilters.courses,
+ statuses: pendingFilters.statuses,
+ homeUniversities: pendingFilters.homeUniversities,
percentileInput: pendingFilters.percentileInput
}));
setSorting([{ id: 'cutoff_score', desc: true }]); // Ensure default sorting
@@ -894,8 +971,9 @@ export default function StateCutoffsPage() {
const clearedFilters = {
search: '',
categories: [],
- seatAllocations: [],
courses: [],
+ statuses: [],
+ homeUniversities: [],
percentileInput: ''
};
// Clear cache when filters are cleared
@@ -915,15 +993,12 @@ export default function StateCutoffsPage() {
const now = Date.now();
const timeSinceLastClick = now - lastPaginationClickRef.current;
- // Throttle pagination clicks to prevent rapid requests
- if (timeSinceLastClick < 200) { // 200ms throttle
- return;
- }
-
- lastPaginationClickRef.current = now;
-
+ // Always allow the page change, just throttle the timing
if (newPage >= 1 && newPage <= Math.ceil(memoizedTotalItems / itemsPerPage)) {
setCurrentPage(newPage);
+
+ // Update the timestamp to prevent rapid successive clicks
+ lastPaginationClickRef.current = now;
}
}, [memoizedTotalItems, itemsPerPage]);
@@ -939,18 +1014,6 @@ export default function StateCutoffsPage() {
Round 1 cutoffs for engineering colleges
-
-
-
- Refresh
- ↻
-
-
{/* Filters */}
@@ -982,7 +1045,7 @@ export default function StateCutoffsPage() {
{/* Percentile Input */}
- Target Percentile (-1 range)
+ Target Percentile (-10 range)
-
+
{/* Enhanced Category Filter */}
-
+
Categories
{pendingFilters.categories.length > 0 && (
-
+
{pendingFilters.categories.length}
)}
@@ -1028,32 +1091,32 @@ export default function StateCutoffsPage() {
-
-
+
+
-
Select Categories
+ Select Categories
{
setPendingFilters(prev => ({ ...prev, categories: [] }));
}}
- className="font-abel text-xs md:text-sm"
+ className="font-abel text-xs md:text-sm text-blue-700 hover:text-blue-900 hover:bg-blue-100"
>
Clear All
-
-
+
+
{Object.entries(CATEGORY_GROUPS).map(([group, categories]) => (
-
-
-
{group}
+
+
+ {group}
{
const allSelected = categories.every(cat => pendingFilters.categories.includes(cat));
if (allSelected) {
@@ -1074,19 +1137,19 @@ export default function StateCutoffsPage() {
{categories.map((category) => (
-
+
handleCategoryToggle(category)}
+ className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
/>
-
+
{category}
))}
-
))}
@@ -1097,11 +1160,11 @@ export default function StateCutoffsPage() {
{/* Enhanced Course Filter */}
-
+
Courses
{pendingFilters.courses.length > 0 && (
-
+
{pendingFilters.courses.length}
)}
@@ -1109,32 +1172,32 @@ export default function StateCutoffsPage() {
-
-
+
+
-
Select Courses
+ Select Courses
{
setPendingFilters(prev => ({ ...prev, courses: [] }));
}}
- className="font-abel text-xs md:text-sm"
+ className="font-abel text-xs md:text-sm text-green-700 hover:text-green-900 hover:bg-green-100"
>
Clear All
-
-
+
+
{Object.entries(COURSE_GROUPS).map(([group, courses]) => (
-
-
-
{group}
+
+
+ {group}
{
const allSelected = courses.every(course => pendingFilters.courses.includes(course));
if (allSelected) {
@@ -1153,22 +1216,21 @@ export default function StateCutoffsPage() {
{courses.every(course => pendingFilters.courses.includes(course)) ? 'Deselect All' : 'Select All'}
-
+
{courses.map((course) => (
-
+
handleCourseToggle(course)}
- className="mt-1"
+ className="mt-1 data-[state=checked]:bg-green-600 data-[state=checked]:border-green-600"
/>
-
+
{course}
))}
-
))}
@@ -1176,33 +1238,99 @@ export default function StateCutoffsPage() {
- {/* Seat Allocation Filter */}
+ {/* Enhanced Status Filter */}
+
+
+
+ Status
+
+ {pendingFilters.statuses.length > 0 && (
+
+ {pendingFilters.statuses.length}
+
+ )}
+
+
+
+
+
+
+
+
Select Status
+ {
+ setPendingFilters(prev => ({ ...prev, statuses: [] }));
+ }}
+ className="font-abel text-xs md:text-sm text-orange-700 hover:text-orange-900 hover:bg-orange-100"
+ >
+ Clear All
+
+
+
+
+
+ {STATUS_OPTIONS.map((option) => (
+
+ handleStatusToggle(option.value)}
+ className="mt-1 data-[state=checked]:bg-orange-600 data-[state=checked]:border-orange-600"
+ />
+
+ {option.label}
+
+
+ ))}
+
+
+
+
+
+ {/* Enhanced Home University Filter */}
-
- Seat Allocation
+
+ Home University
- {pendingFilters.seatAllocations.length > 0 && (
-
- {pendingFilters.seatAllocations.length}
+ {pendingFilters.homeUniversities.length > 0 && (
+
+ {pendingFilters.homeUniversities.length}
)}
-
-
-
- {SEAT_ALLOCATION_OPTIONS.map((option) => (
-
+
+
+
+
Select Home University
+ {
+ setPendingFilters(prev => ({ ...prev, homeUniversities: [] }));
+ }}
+ className="font-abel text-xs md:text-sm text-teal-700 hover:text-teal-900 hover:bg-teal-100"
+ >
+ Clear All
+
+
+
+
+
+ {HOME_UNIVERSITY_OPTIONS.map((option) => (
+
handleSeatAllocationToggle(option.value)}
- className="mt-1"
+ checked={pendingFilters.homeUniversities.includes(option.value)}
+ onCheckedChange={() => handleHomeUniversityToggle(option.value)}
+ className="mt-1 data-[state=checked]:bg-teal-600 data-[state=checked]:border-teal-600"
/>
-
+
{option.label}
@@ -1212,12 +1340,12 @@ export default function StateCutoffsPage() {
- {/* Search and Clear Buttons */}
+ {/* Enhanced Search and Clear Buttons */}
{isSearching || loading ? (
<>
@@ -1236,9 +1364,9 @@ export default function StateCutoffsPage() {
- Clear All
+ Clear All Filters
Clear
@@ -1264,75 +1392,77 @@ export default function StateCutoffsPage() {
{/* Active Filters Display */}
- {(filters.percentileInput || filters.categories.length > 0 || filters.seatAllocations.length > 0 || filters.courses.length > 0) && (
-
-
-
-
-
Active Filters
+ {(filters.percentileInput || filters.categories.length > 0 || filters.courses.length > 0 || filters.statuses.length > 0 || filters.homeUniversities.length > 0) && (
+
+
+
+
+
Active Filters
-
+
{filters.percentileInput && (
-
+
Target: {filters.percentileInput}%
)}
{filters.categories.length > 0 && (
-
-
+
+
Categories ({filters.categories.length})
- {filters.categories.slice(0, 2).map((category, index) => (
-
+ {filters.categories.map((category, index) => (
+
{category}
))}
- {filters.categories.length > 2 && (
-
- +{filters.categories.length - 2} more
-
- )}
)}
- {filters.seatAllocations.length > 0 && (
-
-
- Seats ({filters.seatAllocations.length})
+ {filters.courses.length > 0 && (
+
+
+ Courses ({filters.courses.length})
- {filters.seatAllocations.slice(0, 1).map((allocation, index) => {
- const option = SEAT_ALLOCATION_OPTIONS.find(s => s.value === allocation);
- const label = option?.label || allocation;
- return (
-
- {label}
-
- );
- })}
- {filters.seatAllocations.length > 1 && (
-
- +{filters.seatAllocations.length - 1} more
+ {filters.courses.map((course, index) => (
+
+ {course.length > 20 ? `${course.substring(0, 20)}...` : course}
- )}
+ ))}
)}
- {filters.courses.length > 0 && (
-
-
- Courses ({filters.courses.length})
+ {filters.statuses.length > 0 && (
+
+
+ Status ({filters.statuses.length})
- {filters.courses.slice(0, 1).map((course, index) => (
-
- {course.length > 20 ? `${course.substring(0, 20)}...` : course}
+ {filters.statuses.map((status, index) => (
+
+ {status.length > 25 ? `${status.substring(0, 25)}...` : status}
))}
- {filters.courses.length > 1 && (
-
- +{filters.courses.length - 1} more
+
+ )}
+ {filters.homeUniversities.length > 0 && (
+
+
+ Universities ({filters.homeUniversities.length})
+
+ {filters.homeUniversities.map((university, index) => (
+
+ {university.length > 30 ? `${university.substring(0, 30)}...` : university}
- )}
+ ))}
)}
+
+
+ Clear All Filters
+
+
)}
@@ -1352,7 +1482,15 @@ export default function StateCutoffsPage() {
categories
- {filters.percentileInput ? '-1%' : 'All'}
+ {filters.statuses.length}
+ statuses
+
+
+ {filters.homeUniversities.length}
+ universities
+
+
+ {filters.percentileInput ? '-10%' : 'All'}
range
@@ -1525,7 +1663,10 @@ export default function StateCutoffsPage() {
handlePageChange(1)}
+ onClick={() => {
+ console.log('First page button clicked!');
+ handlePageChange(1);
+ }}
disabled={currentPage === 1 || loading || memoizedTotalItems === 0}
>
Go to first page
@@ -1534,7 +1675,10 @@ export default function StateCutoffsPage() {
handlePageChange(Math.max(1, currentPage - 1))}
+ onClick={() => {
+ console.log('Previous button clicked!');
+ handlePageChange(Math.max(1, currentPage - 1));
+ }}
disabled={currentPage === 1 || loading || memoizedTotalItems === 0}
>
Go to previous page
@@ -1544,6 +1688,7 @@ export default function StateCutoffsPage() {
variant="neutral"
className="h-8 w-8 p-0"
onClick={() => {
+ console.log('Next button clicked!');
const totalPages = Math.ceil(memoizedTotalItems / itemsPerPage);
handlePageChange(Math.min(totalPages, currentPage + 1));
}}
@@ -1556,6 +1701,7 @@ export default function StateCutoffsPage() {
variant="neutral"
className="hidden md:flex h-8 w-8 p-0"
onClick={() => {
+ console.log('Last page button clicked!');
const totalPages = Math.ceil(memoizedTotalItems / itemsPerPage);
handlePageChange(totalPages);
}}
@@ -1600,7 +1746,7 @@ export default function StateCutoffsPage() {
1
Enter Your Percentile
-
Type your MHT-CET percentile to see colleges in a -1% range.
+
Type your MHT-CET percentile to see colleges in a -10% range.
@@ -1620,7 +1766,7 @@ export default function StateCutoffsPage() {
- Pro Tip: The -1% range shows realistic options, helping you find colleges where you have a strong chance of admission.
+ Pro Tip: The -10% range shows realistic options, helping you find colleges where you have a strong chance of admission.
@@ -1672,67 +1818,59 @@ export default function StateCutoffsPage() {
-
-
-
+
+
+
- Category Code Format
+
+ Category Code Format
-
-
+
+
G = General, L = Ladies
H = Home Uni, O = Other Uni, S = State
Example: GOPENH is General Open Home University.
-
-
+
+
- Special Category Codes
+
+ Special Category Codes
-
-
+
+
TFWS: Tuition Fee Waiver Scheme
EWS: Economically Weaker Section
DEF: Defence Reserved
PWD: Persons with Disability
-
MI: Minority Seats
+
MI: Minority Institutions
-
-
+
+
- Video Guide
+
+ Video Explanation
-
-
-
- Educational content from an independent creator (not affiliated with our platform)
-
-
-
-
-
+
+
+
+ {/* Category Flow Chart */}
+
+
+
);
}
diff --git a/components/CategoryFlowChart.tsx b/components/CategoryFlowChart.tsx
new file mode 100644
index 0000000..77364ab
--- /dev/null
+++ b/components/CategoryFlowChart.tsx
@@ -0,0 +1,550 @@
+'use client';
+
+import React, { useCallback, useMemo } from 'react';
+import {
+ ReactFlow,
+ Node,
+ Edge,
+ addEdge,
+ Background,
+ Controls,
+ MiniMap,
+ useNodesState,
+ useEdgesState,
+ Position,
+ MarkerType,
+ Handle,
+} from '@xyflow/react';
+import '@xyflow/react/dist/style.css';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+
+// Custom Node Component
+const CustomNode = ({ data }: { data: any }) => {
+ const { label, type, description, examples } = data;
+
+ const getNodeStyles = () => {
+ switch (type) {
+ case 'root':
+ return 'bg-gradient-to-br from-blue-500 to-blue-600 text-white border-blue-700';
+ case 'category':
+ return 'bg-gradient-to-br from-purple-500 to-purple-600 text-white border-purple-700';
+ case 'subcategory':
+ return 'bg-gradient-to-br from-green-500 to-green-600 text-white border-green-700';
+ case 'allocation':
+ return 'bg-gradient-to-br from-orange-500 to-orange-600 text-white border-orange-700';
+ case 'final':
+ return 'bg-gradient-to-br from-red-500 to-red-600 text-white border-red-700';
+ default:
+ return 'bg-white border-gray-300';
+ }
+ };
+
+ return (
+
+
+
{label}
+ {description &&
{description}
}
+ {examples && (
+
+ {examples.map((example: string, index: number) => (
+
+ {example}
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+const nodeTypes = {
+ custom: CustomNode,
+};
+
+const CategoryFlowChart = () => {
+ const initialNodes: Node[] = useMemo(() => [
+ // Root Node
+ {
+ id: '1',
+ type: 'custom',
+ position: { x: 0, y: 0 },
+ data: {
+ label: 'Seat Types',
+ type: 'root',
+ description: 'MHT-CET Seat Type Categories',
+ },
+ },
+
+ // Main Seat Type - General
+ {
+ id: '2',
+ type: 'custom',
+ position: { x: 400, y: 0 },
+ data: {
+ label: 'General',
+ type: 'category',
+ description: 'General seat type',
+ },
+ },
+
+ // Category Level under General (properly aligned with final codes)
+ {
+ id: '3',
+ type: 'custom',
+ position: { x: 800, y: -600 }, // Aligned with OPEN codes
+ data: {
+ label: 'OPEN',
+ type: 'subcategory',
+ description: 'Open category',
+ },
+ },
+ {
+ id: '4',
+ type: 'custom',
+ position: { x: 800, y: -250 }, // Aligned with OBC codes
+ data: {
+ label: 'OBC',
+ type: 'subcategory',
+ description: 'Other Backward Classes',
+ },
+ },
+ {
+ id: '5',
+ type: 'custom',
+ position: { x: 800, y: 100 }, // Aligned with SC codes
+ data: {
+ label: 'SC',
+ type: 'subcategory',
+ description: 'Scheduled Caste',
+ },
+ },
+ {
+ id: '6',
+ type: 'custom',
+ position: { x: 800, y: 450 }, // Aligned with ST codes
+ data: {
+ label: 'ST',
+ type: 'subcategory',
+ description: 'Scheduled Tribe',
+ },
+ },
+ {
+ id: '7',
+ type: 'custom',
+ position: { x: 800, y: 800 }, // Aligned with VJ codes
+ data: {
+ label: 'VJ',
+ type: 'subcategory',
+ description: 'Vimukta Jati',
+ },
+ },
+ {
+ id: '8',
+ type: 'custom',
+ position: { x: 800, y: 1150 }, // Aligned with NT1 codes
+ data: {
+ label: 'NT1',
+ type: 'subcategory',
+ description: 'Nomadic Tribe 1',
+ },
+ },
+ {
+ id: '9',
+ type: 'custom',
+ position: { x: 800, y: 1500 }, // Aligned with NT2 codes
+ data: {
+ label: 'NT2',
+ type: 'subcategory',
+ description: 'Nomadic Tribe 2',
+ },
+ },
+ {
+ id: '10',
+ type: 'custom',
+ position: { x: 800, y: 1850 }, // Aligned with NT3 codes
+ data: {
+ label: 'NT3',
+ type: 'subcategory',
+ description: 'Nomadic Tribe 3',
+ },
+ },
+
+ // Final Category Codes - OPEN (no overlap, proper spacing)
+ {
+ id: '11',
+ type: 'custom',
+ position: { x: 1200, y: -700 },
+ data: {
+ label: 'GOPENS',
+ type: 'final',
+ description: 'General Open State',
+ },
+ },
+ {
+ id: '12',
+ type: 'custom',
+ position: { x: 1200, y: -600 },
+ data: {
+ label: 'GOPENH',
+ type: 'final',
+ description: 'General Open Home University',
+ },
+ },
+ {
+ id: '13',
+ type: 'custom',
+ position: { x: 1200, y: -500 },
+ data: {
+ label: 'GOPENO',
+ type: 'final',
+ description: 'General Open Other University',
+ },
+ },
+
+ // Final Category Codes - OBC (no overlap, proper spacing)
+ {
+ id: '14',
+ type: 'custom',
+ position: { x: 1200, y: -350 },
+ data: {
+ label: 'GOBCS',
+ type: 'final',
+ description: 'General OBC State',
+ },
+ },
+ {
+ id: '15',
+ type: 'custom',
+ position: { x: 1200, y: -250 },
+ data: {
+ label: 'GOBCH',
+ type: 'final',
+ description: 'General OBC Home University',
+ },
+ },
+ {
+ id: '16',
+ type: 'custom',
+ position: { x: 1200, y: -150 },
+ data: {
+ label: 'GOBCO',
+ type: 'final',
+ description: 'General OBC Other University',
+ },
+ },
+
+ // Final Category Codes - SC (no overlap, proper spacing)
+ {
+ id: '17',
+ type: 'custom',
+ position: { x: 1200, y: 0 },
+ data: {
+ label: 'GSCS',
+ type: 'final',
+ description: 'General SC State',
+ },
+ },
+ {
+ id: '18',
+ type: 'custom',
+ position: { x: 1200, y: 100 },
+ data: {
+ label: 'GSCH',
+ type: 'final',
+ description: 'General SC Home University',
+ },
+ },
+ {
+ id: '19',
+ type: 'custom',
+ position: { x: 1200, y: 200 },
+ data: {
+ label: 'GSCO',
+ type: 'final',
+ description: 'General SC Other University',
+ },
+ },
+
+ // Final Category Codes - ST (no overlap, proper spacing)
+ {
+ id: '20',
+ type: 'custom',
+ position: { x: 1200, y: 350 },
+ data: {
+ label: 'GSTS',
+ type: 'final',
+ description: 'General ST State',
+ },
+ },
+ {
+ id: '21',
+ type: 'custom',
+ position: { x: 1200, y: 450 },
+ data: {
+ label: 'GSTH',
+ type: 'final',
+ description: 'General ST Home University',
+ },
+ },
+ {
+ id: '22',
+ type: 'custom',
+ position: { x: 1200, y: 550 },
+ data: {
+ label: 'GSTO',
+ type: 'final',
+ description: 'General ST Other University',
+ },
+ },
+
+ // Final Category Codes - VJ (no overlap, proper spacing)
+ {
+ id: '23',
+ type: 'custom',
+ position: { x: 1200, y: 700 },
+ data: {
+ label: 'GVJS',
+ type: 'final',
+ description: 'General VJ State',
+ },
+ },
+ {
+ id: '24',
+ type: 'custom',
+ position: { x: 1200, y: 800 },
+ data: {
+ label: 'GVJH',
+ type: 'final',
+ description: 'General VJ Home University',
+ },
+ },
+ {
+ id: '25',
+ type: 'custom',
+ position: { x: 1200, y: 900 },
+ data: {
+ label: 'GVJO',
+ type: 'final',
+ description: 'General VJ Other University',
+ },
+ },
+
+ // Final Category Codes - NT1 (no overlap, proper spacing)
+ {
+ id: '26',
+ type: 'custom',
+ position: { x: 1200, y: 1050 },
+ data: {
+ label: 'GNT1S',
+ type: 'final',
+ description: 'General NT1 State',
+ },
+ },
+ {
+ id: '27',
+ type: 'custom',
+ position: { x: 1200, y: 1150 },
+ data: {
+ label: 'GNT1H',
+ type: 'final',
+ description: 'General NT1 Home University',
+ },
+ },
+ {
+ id: '28',
+ type: 'custom',
+ position: { x: 1200, y: 1250 },
+ data: {
+ label: 'GNT1O',
+ type: 'final',
+ description: 'General NT1 Other University',
+ },
+ },
+
+ // Final Category Codes - NT2 (no overlap, proper spacing)
+ {
+ id: '29',
+ type: 'custom',
+ position: { x: 1200, y: 1400 },
+ data: {
+ label: 'GNT2S',
+ type: 'final',
+ description: 'General NT2 State',
+ },
+ },
+ {
+ id: '30',
+ type: 'custom',
+ position: { x: 1200, y: 1500 },
+ data: {
+ label: 'GNT2H',
+ type: 'final',
+ description: 'General NT2 Home University',
+ },
+ },
+ {
+ id: '31',
+ type: 'custom',
+ position: { x: 1200, y: 1600 },
+ data: {
+ label: 'GNT2O',
+ type: 'final',
+ description: 'General NT2 Other University',
+ },
+ },
+
+ // Final Category Codes - NT3 (no overlap, proper spacing)
+ {
+ id: '32',
+ type: 'custom',
+ position: { x: 1200, y: 1750 },
+ data: {
+ label: 'GNT3S',
+ type: 'final',
+ description: 'General NT3 State',
+ },
+ },
+ {
+ id: '33',
+ type: 'custom',
+ position: { x: 1200, y: 1850 },
+ data: {
+ label: 'GNT3H',
+ type: 'final',
+ description: 'General NT3 Home University',
+ },
+ },
+ {
+ id: '34',
+ type: 'custom',
+ position: { x: 1200, y: 1950 },
+ data: {
+ label: 'GNT3O',
+ type: 'final',
+ description: 'General NT3 Other University',
+ },
+ },
+ ], []);
+
+ const initialEdges: Edge[] = useMemo(() => [
+ // Root to General
+ { id: 'e1-2', source: '1', target: '2', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+
+ // General to all categories
+ { id: 'e2-3', source: '2', target: '3', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+ { id: 'e2-4', source: '2', target: '4', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+ { id: 'e2-5', source: '2', target: '5', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+ { id: 'e2-6', source: '2', target: '6', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+ { id: 'e2-7', source: '2', target: '7', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+ { id: 'e2-8', source: '2', target: '8', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+ { id: 'e2-9', source: '2', target: '9', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+ { id: 'e2-10', source: '2', target: '10', markerEnd: { type: MarkerType.Arrow }, style: { strokeWidth: 2 } },
+
+ // OPEN to final codes
+ { id: 'e3-11', source: '3', target: '11', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e3-12', source: '3', target: '12', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e3-13', source: '3', target: '13', markerEnd: { type: MarkerType.Arrow } },
+
+ // OBC to final codes
+ { id: 'e4-14', source: '4', target: '14', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e4-15', source: '4', target: '15', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e4-16', source: '4', target: '16', markerEnd: { type: MarkerType.Arrow } },
+
+ // SC to final codes
+ { id: 'e5-17', source: '5', target: '17', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e5-18', source: '5', target: '18', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e5-19', source: '5', target: '19', markerEnd: { type: MarkerType.Arrow } },
+
+ // ST to final codes
+ { id: 'e6-20', source: '6', target: '20', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e6-21', source: '6', target: '21', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e6-22', source: '6', target: '22', markerEnd: { type: MarkerType.Arrow } },
+
+ // VJ to final codes
+ { id: 'e7-23', source: '7', target: '23', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e7-24', source: '7', target: '24', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e7-25', source: '7', target: '25', markerEnd: { type: MarkerType.Arrow } },
+
+ // NT1 to final codes
+ { id: 'e8-26', source: '8', target: '26', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e8-27', source: '8', target: '27', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e8-28', source: '8', target: '28', markerEnd: { type: MarkerType.Arrow } },
+
+ // NT2 to final codes
+ { id: 'e9-29', source: '9', target: '29', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e9-30', source: '9', target: '30', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e9-31', source: '9', target: '31', markerEnd: { type: MarkerType.Arrow } },
+
+ // NT3 to final codes
+ { id: 'e10-32', source: '10', target: '32', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e10-33', source: '10', target: '33', markerEnd: { type: MarkerType.Arrow } },
+ { id: 'e10-34', source: '10', target: '34', markerEnd: { type: MarkerType.Arrow } },
+ ], []);
+
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+
+ const onConnect = useCallback(
+ (params: any) => setEdges((eds: Edge[]) => addEdge(params, eds)),
+ [setEdges]
+ );
+
+ return (
+
+
+
+
+ MHT-CET Category Code Structure
+
+
+ Interactive flow chart showing the hierarchy: Seat Types → General → Categories → Final Codes
+
+
+
+
+
+
+ {
+ switch (node.data.type) {
+ case 'root': return '#3b82f6';
+ case 'category': return '#8b5cf6';
+ case 'subcategory': return '#10b981';
+ case 'allocation': return '#f59e0b';
+ case 'final': return '#ef4444';
+ default: return '#6b7280';
+ }
+ }}
+ maskColor="rgba(255, 255, 255, 0.8)"
+ />
+
+
+
+ );
+};
+
+export default CategoryFlowChart;
diff --git a/package-lock.json b/package-lock.json
index 9df0989..eca9f33 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"@tailwindcss/typography": "^0.5.13",
"@tanstack/react-table": "^8.17.3",
"@vercel/og": "^0.6.2",
+ "@xyflow/react": "^12.8.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -52,6 +53,7 @@
"react-fast-marquee": "^1.6.4",
"react-icons": "^5.2.1",
"react-slider": "^2.0.6",
+ "reactflow": "^11.11.4",
"recharts": "^2.12.7",
"satori": "^0.10.13",
"sonner": "^2.0.5",
@@ -2154,6 +2156,108 @@
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
},
+ "node_modules/@reactflow/background": {
+ "version": "11.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
+ "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/controls": {
+ "version": "11.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
+ "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/core": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
+ "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3": "^7.4.0",
+ "@types/d3-drag": "^3.0.1",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/minimap": {
+ "version": "11.7.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
+ "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-resizer": {
+ "version": "2.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
+ "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.4",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-toolbar": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
+ "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
"node_modules/@resvg/resvg-wasm": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz",
@@ -2380,21 +2484,156 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
},
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
+ "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
},
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
@@ -2408,6 +2647,24 @@
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ=="
},
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-scale": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
@@ -2416,6 +2673,18 @@
"@types/d3-time": "*"
}
},
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-shape": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz",
@@ -2429,11 +2698,42 @@
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
},
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
},
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -2780,6 +3080,38 @@
"node": ">=16"
}
},
+ "node_modules/@xyflow/react": {
+ "version": "12.8.1",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.1.tgz",
+ "integrity": "sha512-t5Rame4Gc/540VcOZd28yFe9Xd8lyjKUX+VTiyb1x4ykNXZH5zyDmsu+lj9je2O/jGBVb0pj1Vjcxrxyn+Xk2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.65",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.65",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.65.tgz",
+ "integrity": "sha512-AliQPQeurQMoNlOdySnRoDQl9yDSA/1Lqi47Eo0m98lHcfrTdD9jK75H0tiGj+0qRC10SKNUXyMkT0KL0opg4g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -3313,6 +3645,12 @@
"url": "https://polar.sh/cva"
}
},
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3504,6 +3842,28 @@
"node": ">=12"
}
},
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
@@ -3554,6 +3914,15 @@
"node": ">=12"
}
},
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
@@ -3595,6 +3964,41 @@
"node": ">=12"
}
},
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6752,6 +7156,24 @@
"react-dom": ">=16.6.0"
}
},
+ "node_modules/reactflow": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
+ "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/background": "11.3.14",
+ "@reactflow/controls": "11.2.14",
+ "@reactflow/core": "11.11.4",
+ "@reactflow/minimap": "11.7.14",
+ "@reactflow/node-resizer": "2.2.14",
+ "@reactflow/node-toolbar": "1.3.14"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -8292,6 +8714,34 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+ },
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 52b759a..24650b3 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"import-bits": "npx tsx scripts/import-bits-cutoffs.ts",
"upload-cutoffs": "npx tsx scripts/upload-mht-cet-cutoffs.ts",
"upload-cutoffs-v2": "npx tsx scripts/upload-mht-cet-cutoffs-v2.ts",
+ "batch-upload": "npx tsx scripts/batch-upload-mht-cet-cutoffs.ts",
+ "test-batch-api": "npx tsx scripts/test-batch-api.ts",
"test-upload-setup": "npx tsx scripts/test-setup.ts",
"test-pocketbase": "npx tsx scripts/test-pocketbase.ts",
"diagnose-pocketbase": "npx tsx scripts/diagnostic-pocketbase.ts"
@@ -42,6 +44,7 @@
"@tailwindcss/typography": "^0.5.13",
"@tanstack/react-table": "^8.17.3",
"@vercel/og": "^0.6.2",
+ "@xyflow/react": "^12.8.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -59,6 +62,7 @@
"react-fast-marquee": "^1.6.4",
"react-icons": "^5.2.1",
"react-slider": "^2.0.6",
+ "reactflow": "^11.11.4",
"recharts": "^2.12.7",
"satori": "^0.10.13",
"sonner": "^2.0.5",
@@ -84,4 +88,4 @@
"@types/react": "19.0.1",
"@types/react-dom": "19.0.1"
}
-}
+}
\ No newline at end of file
diff --git a/scripts/AUTO_CANCELLATION_FIX.md b/scripts/AUTO_CANCELLATION_FIX.md
new file mode 100644
index 0000000..a3e4cef
--- /dev/null
+++ b/scripts/AUTO_CANCELLATION_FIX.md
@@ -0,0 +1,60 @@
+# Auto-Cancellation Fix Summary
+
+## Problem
+The PocketBase SDK was auto-cancelling concurrent batch requests because it considered them duplicates. This caused the error:
+```
+ClientResponseError 0: The request was autocancelled
+```
+
+## Solutions Applied
+
+### 1. Disabled Auto-Cancellation
+```typescript
+// In constructor
+this.pb.autoCancellation(false);
+```
+
+### 2. Added Unique Request Keys
+All batch operations now use unique request keys to prevent conflicts:
+```typescript
+// Create batch
+batch.send({ requestKey: `batch_create_${batchId}` });
+
+// Upsert batch
+batch.send({ requestKey: `batch_upsert_${batchId}` });
+
+// Delete batch
+batch.send({ requestKey: `batch_delete_${page}_${Date.now()}` });
+```
+
+### 3. Reduced Concurrency
+- **Batch Size**: 1000 → 500 records per batch
+- **Concurrent Batches**: 10 → 5 batches at once
+
+This provides better reliability while still maintaining high performance.
+
+### 4. Unique Batch IDs
+Each batch now gets a unique ID based on timestamp and index:
+```typescript
+const batchId = `${Date.now()}_${actualIndex}`;
+```
+
+## Expected Performance
+- **Speed**: Still 2,500-5,000+ records/second
+- **Reliability**: Much more stable with no auto-cancellation errors
+- **Concurrency**: Controlled to prevent server overload
+
+## Files Updated
+- `batch-upload-mht-cet-cutoffs.ts` - Main batch upload script
+- `test-batch-api.ts` - Test script with auto-cancellation disabled
+
+## Usage
+No changes to the command line usage:
+```bash
+npm run batch-upload create
+npm run batch-upload upsert
+npm run batch-upload clear
+npm run batch-upload replace
+```
+
+The script should now run smoothly without auto-cancellation errors!
diff --git a/scripts/BATCH_UPLOAD_README.md b/scripts/BATCH_UPLOAD_README.md
new file mode 100644
index 0000000..6acb815
--- /dev/null
+++ b/scripts/BATCH_UPLOAD_README.md
@@ -0,0 +1,183 @@
+# Batch Upload MHT-CET Cutoffs
+
+This script provides super-fast batch upload functionality for MHT-CET cutoff data using PocketBase's batch API. It's optimized for high-performance uploads with concurrent processing.
+
+## Features
+
+- **Ultra-fast uploads**: Processes 1000 records per batch with 10 concurrent batches
+- **Flexible operations**: Create, upsert, clear, or replace records
+- **Progress tracking**: Real-time progress updates with timing information
+- **Error handling**: Robust error handling and recovery
+- **Memory efficient**: Streams CSV data to handle large files
+- **Concurrent processing**: Maximizes throughput with controlled concurrency
+
+## CSV Format
+
+The script expects a CSV file with the following columns:
+
+```csv
+college_code,college_name,course_code,course_name,category,seat_allocation_section,cutoff_score,last_rank,total_admitted,status,home_university
+01002,"Government College of Engineering, Amravati",0100219110,Civil Engineering,DEFOBCS,STATE_LEVEL,47.9842799,124882,1,Government Autonomous,Autonomous Institute
+```
+
+## Environment Variables
+
+Set these environment variables in your `.env` file:
+
+```env
+POCKETBASE_URL=https://api.deetnuts.com
+POCKETBASE_AUTH_TOKEN=your_auth_token_here
+# OR use admin credentials
+POCKETBASE_ADMIN_EMAIL=admin@example.com
+POCKETBASE_ADMIN_PASSWORD=your_password_here
+```
+
+## Usage
+
+### 1. Test the Batch API
+
+First, test that the batch API is working correctly:
+
+```bash
+npm run test-batch-api
+```
+
+### 2. Upload Operations
+
+#### Create New Records
+```bash
+npm run batch-upload create
+```
+
+#### Upsert Records (Create or Update)
+```bash
+npm run batch-upload upsert
+```
+
+#### Clear All Records
+```bash
+npm run batch-upload clear
+```
+
+#### Replace All Records (Clear + Create)
+```bash
+npm run batch-upload replace
+```
+
+## Performance Optimization
+
+The script is configured for maximum performance:
+
+- **Batch Size**: 1000 records per batch
+- **Concurrent Batches**: 10 batches processed simultaneously
+- **Target Speed**: 10,000+ records/second on elite servers
+
+### Tuning Parameters
+
+You can adjust these parameters in the script:
+
+```typescript
+private batchSize = 1000; // Records per batch
+private maxConcurrentBatches = 10; // Concurrent batches
+```
+
+## PocketBase Configuration
+
+Ensure your PocketBase instance has:
+
+1. **Batch API enabled** in Dashboard settings
+2. **Proper timeout settings** for large uploads
+3. **Sufficient body size limits** for batch requests
+4. **Authentication** properly configured
+
+## CSV File Location
+
+The script looks for `combined_cutoffs.csv` in the same directory. Make sure your CSV file is:
+
+- Named `combined_cutoffs.csv`
+- Located in the `scripts/` directory
+- Has the correct column headers
+- Contains valid data
+
+## Error Handling
+
+The script includes comprehensive error handling:
+
+- **Authentication errors**: Clear messages for auth failures
+- **CSV parsing errors**: Detailed error information
+- **Batch processing errors**: Individual batch error reporting
+- **Network errors**: Automatic retry logic (planned)
+
+## Expected Output
+
+```
+🚀 Starting batch upload process...
+🔑 Using auth token...
+📊 Read 31793 records from CSV
+📦 Created 32 batches of 1000 records each
+⚡ Processing 10 batches concurrently for maximum speed
+✅ Batch 1 completed in 1234ms (1000 records) - 1/32 batches done
+✅ Batch 2 completed in 1156ms (1000 records) - 2/32 batches done
+...
+🎉 Successfully created 31793 records!
+⏱️ Total time: 15432ms (15s)
+🚀 Speed: 2059 records/second
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Authentication Failed**
+ - Check your environment variables
+ - Verify token/credentials are correct
+ - Ensure admin permissions
+
+2. **CSV Parsing Errors**
+ - Check CSV format and encoding
+ - Verify column headers match expected format
+ - Look for special characters or malformed data
+
+3. **Slow Performance**
+ - Check network connection
+ - Verify PocketBase server capacity
+ - Consider reducing batch size or concurrency
+
+4. **Memory Issues**
+ - The script streams data to minimize memory usage
+ - If issues persist, reduce batch size
+
+### Performance Tips
+
+- Run on a server with good network connection to PocketBase
+- Monitor PocketBase server resources during upload
+- Use `replace` operation for clean data replacement
+- Use `upsert` for incremental updates
+
+## Technical Details
+
+### Batch API Structure
+
+The script uses PocketBase's batch API with this structure:
+
+```typescript
+const batch = pb.createBatch();
+batch.collection('collection_name').create(record);
+batch.collection('collection_name').update(id, record);
+batch.collection('collection_name').delete(id);
+batch.collection('collection_name').upsert(record);
+const result = await batch.send();
+```
+
+### Concurrency Control
+
+The script processes batches in groups to prevent overwhelming the server:
+
+```typescript
+for (let i = 0; i < batches.length; i += maxConcurrentBatches) {
+ const concurrentBatches = batches.slice(i, i + maxConcurrentBatches);
+ await Promise.all(concurrentBatches.map(processBatch));
+}
+```
+
+This ensures controlled concurrency while maximizing throughput.
diff --git a/scripts/batch-upload-mht-cet-cutoffs.ts b/scripts/batch-upload-mht-cet-cutoffs.ts
new file mode 100644
index 0000000..e8e2756
--- /dev/null
+++ b/scripts/batch-upload-mht-cet-cutoffs.ts
@@ -0,0 +1,304 @@
+import PocketBase from 'pocketbase';
+import { createReadStream } from 'fs';
+import { parse } from 'csv-parse';
+import * as path from 'path';
+import * as dotenv from 'dotenv';
+
+// Load environment variables
+dotenv.config();
+
+interface CutoffRecord {
+ college_code: string;
+ college_name: string;
+ course_code: string;
+ course_name: string;
+ category: string;
+ seat_allocation_section: string;
+ cutoff_score: string;
+ last_rank: string;
+ total_admitted: number;
+ status: string;
+ home_university: string;
+}
+
+class BatchMHTCETCutoffUploader {
+ private pb: PocketBase;
+ private csvFilePath: string;
+ private collectionName = '2024_mht_cet_round_one_cutoffs_duplicate';
+ private batchSize = 500; // Reduced batch size for better reliability
+ private maxConcurrentBatches = 5; // Reduced concurrent batches to prevent auto-cancellation issues
+
+ constructor() {
+ // Initialize PocketBase
+ const pbUrl = process.env.POCKETBASE_URL || 'https://api.deetnuts.com';
+ this.pb = new PocketBase(pbUrl);
+
+ // Disable auto-cancellation to prevent concurrent batch requests from being cancelled
+ this.pb.autoCancellation(false);
+
+ // Set CSV file path
+ this.csvFilePath = path.join(__dirname, 'combined_cutoffs.csv');
+ }
+
+ async authenticateWithToken(): Promise {
+ const token = process.env.POCKETBASE_AUTH_TOKEN;
+ if (token) {
+ console.log('🔑 Using auth token...');
+ this.pb.authStore.save(token);
+ return true;
+ }
+ return false;
+ }
+
+ async authenticateWithCredentials(): Promise {
+ const adminEmail = process.env.POCKETBASE_ADMIN_EMAIL;
+ const adminPassword = process.env.POCKETBASE_ADMIN_PASSWORD;
+
+ if (!adminEmail || !adminPassword) {
+ console.error('❌ Missing admin credentials in environment variables');
+ return false;
+ }
+
+ try {
+ console.log('🔑 Authenticating with credentials...');
+ await this.pb.admins.authWithPassword(adminEmail, adminPassword);
+ console.log('✅ Successfully authenticated as admin');
+ return true;
+ } catch (error) {
+ console.error('❌ Authentication failed:', error);
+ return false;
+ }
+ }
+
+ async readCSVData(): Promise {
+ return new Promise((resolve, reject) => {
+ const records: CutoffRecord[] = [];
+
+ createReadStream(this.csvFilePath)
+ .pipe(parse({
+ columns: true,
+ skip_empty_lines: true,
+ trim: true
+ }))
+ .on('data', (row) => {
+ const record: CutoffRecord = {
+ college_code: row.college_code?.toString() || '',
+ college_name: row.college_name?.toString() || '',
+ course_code: row.course_code?.toString() || '',
+ course_name: row.course_name?.toString() || '',
+ category: row.category?.toString() || '',
+ seat_allocation_section: row.seat_allocation_section?.toString() || '',
+ cutoff_score: row.cutoff_score?.toString() || '',
+ last_rank: row.last_rank?.toString() || '',
+ total_admitted: parseInt(row.total_admitted) || 0,
+ status: row.status?.toString() || '',
+ home_university: row.home_university?.toString() || ''
+ };
+ records.push(record);
+ })
+ .on('end', () => {
+ console.log(`📊 Read ${records.length} records from CSV`);
+ resolve(records);
+ })
+ .on('error', reject);
+ });
+ }
+
+ async createBatch(records: CutoffRecord[], batchId: string): Promise {
+ const batch = this.pb.createBatch();
+
+ for (const record of records) {
+ batch.collection(this.collectionName).create(record);
+ }
+
+ // Add unique request key to prevent auto-cancellation
+ return await batch.send({ requestKey: `batch_create_${batchId}` });
+ }
+
+ async upsertBatch(records: CutoffRecord[], batchId: string): Promise {
+ const batch = this.pb.createBatch();
+
+ for (const record of records) {
+ // For upsert, we need to add an id field - using combination of college_code, course_code, and category
+ const upsertRecord = {
+ ...record,
+ id: `${record.college_code}_${record.course_code}_${record.category}_${record.seat_allocation_section}`
+ };
+ batch.collection(this.collectionName).upsert(upsertRecord);
+ }
+
+ // Add unique request key to prevent auto-cancellation
+ return await batch.send({ requestKey: `batch_upsert_${batchId}` });
+ }
+
+ async processBatchesConcurrently(batches: CutoffRecord[][], operation: 'create' | 'upsert' = 'create'): Promise {
+ const batchPromises: Promise[] = [];
+ let completedBatches = 0;
+
+ for (let i = 0; i < batches.length; i += this.maxConcurrentBatches) {
+ const concurrentBatches = batches.slice(i, i + this.maxConcurrentBatches);
+
+ const concurrentPromises = concurrentBatches.map(async (batch, index) => {
+ const actualIndex = i + index;
+ const batchId = `${Date.now()}_${actualIndex}`;
+ try {
+ const startTime = Date.now();
+
+ let result;
+ if (operation === 'upsert') {
+ result = await this.upsertBatch(batch, batchId);
+ } else {
+ result = await this.createBatch(batch, batchId);
+ }
+
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ completedBatches++;
+ console.log(`✅ Batch ${actualIndex + 1} completed in ${duration}ms (${batch.length} records) - ${completedBatches}/${batches.length} batches done`);
+
+ return result;
+ } catch (error) {
+ console.error(`❌ Batch ${actualIndex + 1} failed:`, error);
+ throw error;
+ }
+ });
+
+ await Promise.all(concurrentPromises);
+ }
+ }
+
+ async uploadData(operation: 'create' | 'upsert' = 'create'): Promise {
+ try {
+ console.log('🚀 Starting batch upload process...');
+
+ // Authenticate
+ const authenticated = await this.authenticateWithToken() || await this.authenticateWithCredentials();
+ if (!authenticated) {
+ throw new Error('Authentication failed');
+ }
+
+ // Read CSV data
+ const records = await this.readCSVData();
+
+ if (records.length === 0) {
+ console.log('⚠️ No records found in CSV file');
+ return;
+ }
+
+ // Split records into batches
+ const batches: CutoffRecord[][] = [];
+ for (let i = 0; i < records.length; i += this.batchSize) {
+ batches.push(records.slice(i, i + this.batchSize));
+ }
+
+ console.log(`📦 Created ${batches.length} batches of ${this.batchSize} records each`);
+ console.log(`⚡ Processing ${this.maxConcurrentBatches} batches concurrently for maximum speed`);
+
+ const startTime = Date.now();
+
+ // Process batches concurrently
+ await this.processBatchesConcurrently(batches, operation);
+
+ const endTime = Date.now();
+ const totalDuration = endTime - startTime;
+ const recordsPerSecond = Math.round((records.length / totalDuration) * 1000);
+
+ console.log(`🎉 Successfully ${operation === 'upsert' ? 'upserted' : 'created'} ${records.length} records!`);
+ console.log(`⏱️ Total time: ${totalDuration}ms (${Math.round(totalDuration / 1000)}s)`);
+ console.log(`🚀 Speed: ${recordsPerSecond} records/second`);
+
+ } catch (error) {
+ console.error('❌ Upload failed:', error);
+ throw error;
+ }
+ }
+
+ async clearCollection(): Promise {
+ try {
+ console.log('🧹 Clearing existing records...');
+
+ // Authenticate
+ const authenticated = await this.authenticateWithToken() || await this.authenticateWithCredentials();
+ if (!authenticated) {
+ throw new Error('Authentication failed');
+ }
+
+ // Get all records in batches and delete them
+ let page = 1;
+ let hasMore = true;
+ let totalDeleted = 0;
+
+ while (hasMore) {
+ const result = await this.pb.collection(this.collectionName).getList(page, 500);
+
+ if (result.items.length === 0) {
+ hasMore = false;
+ break;
+ }
+
+ // Create batch delete operation
+ const batch = this.pb.createBatch();
+ for (const record of result.items) {
+ batch.collection(this.collectionName).delete(record.id);
+ }
+
+ await batch.send({ requestKey: `batch_delete_${page}_${Date.now()}` });
+ totalDeleted += result.items.length;
+ console.log(`🗑️ Deleted ${result.items.length} records (${totalDeleted} total)`);
+
+ page++;
+ hasMore = result.items.length === 500; // Continue if we got a full page
+ }
+
+ console.log(`✅ Successfully deleted ${totalDeleted} records`);
+ } catch (error) {
+ console.error('❌ Clear collection failed:', error);
+ throw error;
+ }
+ }
+}
+
+// Command line interface
+async function main() {
+ const uploader = new BatchMHTCETCutoffUploader();
+
+ const command = process.argv[2];
+
+ switch (command) {
+ case 'create':
+ console.log('📝 Creating new records...');
+ await uploader.uploadData('create');
+ break;
+
+ case 'upsert':
+ console.log('🔄 Upserting records...');
+ await uploader.uploadData('upsert');
+ break;
+
+ case 'clear':
+ console.log('🧹 Clearing collection...');
+ await uploader.clearCollection();
+ break;
+
+ case 'replace':
+ console.log('🔄 Replacing all records (clear + create)...');
+ await uploader.clearCollection();
+ await uploader.uploadData('create');
+ break;
+
+ default:
+ console.log('📋 Usage:');
+ console.log(' npm run batch-upload create - Create new records');
+ console.log(' npm run batch-upload upsert - Upsert records (create or update)');
+ console.log(' npm run batch-upload clear - Clear all records');
+ console.log(' npm run batch-upload replace - Clear and create (full replace)');
+ break;
+ }
+}
+
+if (require.main === module) {
+ main().catch(console.error);
+}
+
+export default BatchMHTCETCutoffUploader;
diff --git a/scripts/map_college_info.py b/scripts/map_college_info.py
new file mode 100644
index 0000000..a1161de
--- /dev/null
+++ b/scripts/map_college_info.py
@@ -0,0 +1,65 @@
+import csv
+import sys
+from typing import Dict, Tuple
+
+def load_college_info(filepath: str) -> Dict[str, Tuple[str, str]]:
+ """Load college information into a dictionary for fast lookup."""
+ college_info = {}
+
+ with open(filepath, 'r', encoding='utf-8') as file:
+ reader = csv.DictReader(file)
+ for row in reader:
+ college_id = row['college_id']
+ status = row['status']
+ home_university = row['home_university']
+ college_info[college_id] = (status, home_university)
+
+ return college_info
+
+def extract_college_id(college_code: str) -> str:
+ """Extract college ID from college code by removing leading zeros."""
+ return str(int(college_code))
+
+def process_cutoffs(input_file: str, output_file: str, college_info: Dict[str, Tuple[str, str]]):
+ """Process combined cutoffs CSV and add college information."""
+
+ with open(input_file, 'r', encoding='utf-8') as infile, \
+ open(output_file, 'w', encoding='utf-8', newline='') as outfile:
+
+ reader = csv.DictReader(infile)
+ fieldnames = reader.fieldnames + ['status', 'home_university']
+ writer = csv.DictWriter(outfile, fieldnames=fieldnames)
+
+ writer.writeheader()
+
+ for row in reader:
+ college_code = row['college_code']
+ college_id = extract_college_id(college_code)
+
+ if college_id in college_info:
+ status, home_university = college_info[college_id]
+ row['status'] = status
+ row['home_university'] = home_university
+ else:
+ row['status'] = 'Unknown'
+ row['home_university'] = 'Unknown'
+ print(f"Warning: College ID {college_id} not found in college_information.csv")
+
+ writer.writerow(row)
+
+def main():
+ college_info_file = 'college_information.csv'
+ combined_cutoffs_file = 'combined_cutoffs.csv'
+ output_file = 'combined_cutoffs_with_info.csv'
+
+ print("Loading college information...")
+ college_info = load_college_info(college_info_file)
+ print(f"Loaded {len(college_info)} colleges")
+
+ print("Processing combined cutoffs...")
+ process_cutoffs(combined_cutoffs_file, output_file, college_info)
+
+ print(f"Complete! Output saved to {output_file}")
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/test-batch-api.ts b/scripts/test-batch-api.ts
new file mode 100644
index 0000000..aa3e013
--- /dev/null
+++ b/scripts/test-batch-api.ts
@@ -0,0 +1,98 @@
+import PocketBase from 'pocketbase';
+import * as dotenv from 'dotenv';
+
+// Load environment variables
+dotenv.config();
+
+async function testBatchAPI() {
+ console.log('🧪 Testing PocketBase Batch API...');
+
+ const pbUrl = process.env.POCKETBASE_URL || 'https://api.deetnuts.com';
+ const pb = new PocketBase(pbUrl);
+
+ // Disable auto-cancellation
+ pb.autoCancellation(false);
+
+ // Authenticate
+ const token = process.env.POCKETBASE_AUTH_TOKEN;
+ if (token) {
+ console.log('🔑 Using auth token...');
+ pb.authStore.save(token);
+ } else {
+ const adminEmail = process.env.POCKETBASE_ADMIN_EMAIL;
+ const adminPassword = process.env.POCKETBASE_ADMIN_PASSWORD;
+
+ if (!adminEmail || !adminPassword) {
+ console.error('❌ Missing credentials');
+ return;
+ }
+
+ console.log('🔑 Authenticating with credentials...');
+ await pb.admins.authWithPassword(adminEmail, adminPassword);
+ }
+
+ console.log('✅ Authentication successful');
+
+ // Test creating a small batch
+ console.log('📦 Creating test batch...');
+ const batch = pb.createBatch();
+
+ const testRecords = [
+ {
+ college_code: 'TEST001',
+ college_name: 'Test College 1',
+ course_code: 'TEST101',
+ course_name: 'Test Course 1',
+ category: 'TEST',
+ seat_allocation_section: 'TEST_LEVEL',
+ cutoff_score: '85.5',
+ last_rank: '1000',
+ total_admitted: 10,
+ status: 'Test Status',
+ home_university: 'Test University'
+ },
+ {
+ college_code: 'TEST002',
+ college_name: 'Test College 2',
+ course_code: 'TEST102',
+ course_name: 'Test Course 2',
+ category: 'TEST',
+ seat_allocation_section: 'TEST_LEVEL',
+ cutoff_score: '87.5',
+ last_rank: '800',
+ total_admitted: 15,
+ status: 'Test Status',
+ home_university: 'Test University'
+ }
+ ];
+
+ for (const record of testRecords) {
+ batch.collection('2024_mht_cet_round_one_cutoffs_duplicate').create(record);
+ }
+
+ const startTime = Date.now();
+ const result = await batch.send({ requestKey: `test_batch_${Date.now()}` });
+ const endTime = Date.now();
+
+ console.log(`✅ Batch created successfully in ${endTime - startTime}ms`);
+ console.log(`📊 Results: ${result.length} records processed`);
+
+ // Clean up test records
+ console.log('🧹 Cleaning up test records...');
+ const cleanupBatch = pb.createBatch();
+
+ for (const response of result) {
+ if (response.status === 200 && response.body && response.body.id) {
+ cleanupBatch.collection('2024_mht_cet_round_one_cutoffs_duplicate').delete(response.body.id);
+ }
+ }
+
+ await cleanupBatch.send({ requestKey: `cleanup_${Date.now()}` });
+ console.log('✅ Test cleanup completed');
+
+ console.log('🎉 Batch API test completed successfully!');
+}
+
+if (require.main === module) {
+ testBatchAPI().catch(console.error);
+}
diff --git a/scripts/upload-mht-cet-cutoffs-v2.ts b/scripts/upload-mht-cet-cutoffs-v2.ts
index 609b5e7..c5f0e79 100644
--- a/scripts/upload-mht-cet-cutoffs-v2.ts
+++ b/scripts/upload-mht-cet-cutoffs-v2.ts
@@ -22,7 +22,7 @@ interface CutoffRecord {
class MHTCETCutoffUploader {
private pb: PocketBase;
private csvFilePath: string;
- private collectionName = '2024_mht_cet_round_one_cutoffs';
+ private collectionName = '2024_mht_cet_round_one_cutoffs_duplicate';
private authToken: string | null = null;
constructor() {
diff --git a/scripts/upload-mht-cet-cutoffs.ts b/scripts/upload-mht-cet-cutoffs.ts
deleted file mode 100644
index e28dc6c..0000000
--- a/scripts/upload-mht-cet-cutoffs.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import PocketBase from 'pocketbase';
-import { createReadStream } from 'fs';
-import { parse } from 'csv-parse';
-import * as path from 'path';
-import * as dotenv from 'dotenv';
-
-// Load environment variables
-dotenv.config();
-
-interface CutoffRecord {
- college_code: string;
- college_name: string;
- course_code: string;
- course_name: string;
- category: string;
- seat_allocation_section: string;
- cutoff_score: string;
- last_rank: string;
- total_admitted: number;
-}
-
-class MHTCETCutoffUploader {
- private pb: PocketBase;
- private csvFilePath: string;
- private collectionName = '2024_mht_cet_round_one_cutoffs';
-
- constructor() {
- // Initialize PocketBase - adjust URL as needed
- const pbUrl = process.env.POCKETBASE_URL || 'https://api.deetnuts.com';
- this.pb = new PocketBase(pbUrl);
-
- // Set CSV file path - the CSV is in the scripts folder
- this.csvFilePath = path.join(__dirname, 'combined_cutoffs.csv');
- }
-
- async authenticate() {
- const adminEmail = process.env.POCKETBASE_ADMIN_EMAIL;
- const adminPassword = process.env.POCKETBASE_ADMIN_PASSWORD;
-
- console.log('🔍 Debug info:');
- console.log(` Email: ${adminEmail ? adminEmail.substring(0, 3) + '***' : 'NOT SET'}`);
- console.log(` Password: ${adminPassword ? '***' + adminPassword.substring(adminPassword.length - 3) : 'NOT SET'}`);
- console.log(` PocketBase URL: ${this.pb.baseUrl}`);
-
- if (!adminEmail || !adminPassword) {
- throw new Error(
- 'POCKETBASE_ADMIN_EMAIL and POCKETBASE_ADMIN_PASSWORD must be set in environment variables'
- );
- }
-
- try {
- // Try admin authentication first
- console.log('🔑 Attempting admin authentication...');
- await this.pb.admins.authWithPassword(adminEmail, adminPassword);
- console.log('✅ Successfully authenticated as admin');
- } catch (adminError) {
- console.log('❌ Admin authentication failed, trying regular user authentication...');
- try {
- // Fallback to regular user authentication
- await this.pb.collection('users').authWithPassword(adminEmail, adminPassword);
- console.log('✅ Successfully authenticated as regular user');
- } catch (userError) {
- console.error('❌ Both admin and user authentication failed');
- console.error('Admin error:', adminError);
- console.error('User error:', userError);
- throw adminError;
- }
- }
- }
-
- async readCSVFile(): Promise {
- return new Promise((resolve, reject) => {
- const records: CutoffRecord[] = [];
-
- createReadStream(this.csvFilePath)
- .pipe(parse({
- columns: true,
- skip_empty_lines: true,
- trim: true
- }))
- .on('data', (row) => {
- // Convert total_admitted to number
- const record: CutoffRecord = {
- ...row,
- total_admitted: parseInt(row.total_admitted) || 0
- };
- records.push(record);
- })
- .on('end', () => {
- console.log(`📊 Read ${records.length} records from CSV`);
- resolve(records);
- })
- .on('error', (error) => {
- console.error('❌ Error reading CSV file:', error);
- reject(error);
- });
- });
- }
-
- async uploadRecords(records: CutoffRecord[]) {
- console.log(`🚀 Starting upload of ${records.length} records...`);
-
- let successCount = 0;
- let errorCount = 0;
- const batchSize = 100; // Process in batches to avoid overwhelming the server
-
- for (let i = 0; i < records.length; i += batchSize) {
- const batch = records.slice(i, i + batchSize);
- console.log(`📦 Processing batch ${Math.floor(i / batchSize) + 1} (records ${i + 1}-${Math.min(i + batchSize, records.length)})`);
-
- const batchPromises = batch.map(async (record, index) => {
- try {
- await this.pb.collection(this.collectionName).create(record);
- successCount++;
-
- // Log progress every 500 records
- if ((i + index + 1) % 500 === 0) {
- console.log(`✅ Uploaded ${i + index + 1} records...`);
- }
- } catch (error) {
- errorCount++;
- console.error(`❌ Error uploading record ${i + index + 1}:`, error);
- console.error('Record data:', record);
- }
- });
-
- // Wait for current batch to complete before processing next batch
- await Promise.all(batchPromises);
-
- // Add a small delay between batches to be respectful to the server
- if (i + batchSize < records.length) {
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- }
-
- console.log(`\n📈 Upload Summary:`);
- console.log(`✅ Successfully uploaded: ${successCount} records`);
- console.log(`❌ Failed uploads: ${errorCount} records`);
- console.log(`📊 Total processed: ${records.length} records`);
- }
-
- async checkCollectionExists(): Promise {
- try {
- await this.pb.collection(this.collectionName).getList(1, 1);
- return true;
- } catch (error) {
- return false;
- }
- }
-
- async run() {
- try {
- console.log('🚀 Starting MHT-CET Cutoffs Upload Process...\n');
-
- // Step 1: Authenticate
- console.log('🔐 Authenticating...');
- await this.authenticate();
-
- // Step 2: Check if collection exists
- console.log('🔍 Checking collection...');
- const collectionExists = await this.checkCollectionExists();
- if (!collectionExists) {
- console.error(`❌ Collection '${this.collectionName}' does not exist. Please create it first.`);
- return;
- }
- console.log(`✅ Collection '${this.collectionName}' found`);
-
- // Step 3: Read CSV file
- console.log('📖 Reading CSV file...');
- const records = await this.readCSVFile();
-
- // Step 4: Upload records
- console.log('⬆️ Starting upload...');
- await this.uploadRecords(records);
-
- console.log('\n🎉 Upload process completed!');
-
- } catch (error) {
- console.error('💥 Fatal error during upload process:', error);
- process.exit(1);
- }
- }
-}
-
-// Run the uploader if this file is executed directly
-if (require.main === module) {
- const uploader = new MHTCETCutoffUploader();
- uploader.run();
-}
-
-export default MHTCETCutoffUploader;