feat(schedule): auto-parsing groups from target site

This commit is contained in:
kilyabin
2026-03-02 14:12:01 +04:00
parent 9bca838fbc
commit da7b4fe812
13 changed files with 313 additions and 64 deletions

View File

@@ -154,7 +154,7 @@ function cleanupCache() {
export async function getServerSideProps(context: GetServerSidePropsContext<{ group: string }>): Promise<GetServerSidePropsResult<NextSerialized<PageProps>>> {
// Используем кеш (обновляется каждую минуту автоматически)
const groups = loadGroups()
const groups = await loadGroups()
const settings = loadSettings()
const group = context.params?.group
const wkParam = context.query.wk

View File

@@ -23,11 +23,13 @@ import {
AccordionItem,
AccordionTrigger,
} from '@/shadcn/ui/accordion'
import { SCHED_MODE } from '@/shared/constants/urls'
type AdminPageProps = {
groups: GroupsData
settings: AppSettings
isDefaultPassword: boolean
isKspsutiMode: boolean
}
// Компонент Toggle Switch
@@ -94,7 +96,7 @@ function DialogFooterButtons({ onCancel, onSubmit, submitLabel, loading, submitV
)
}
export default function AdminPage({ groups: initialGroups, settings: initialSettings, isDefaultPassword: initialIsDefaultPassword }: AdminPageProps) {
export default function AdminPage({ groups: initialGroups, settings: initialSettings, isDefaultPassword: initialIsDefaultPassword, isKspsutiMode }: AdminPageProps) {
const [authenticated, setAuthenticated] = React.useState<boolean | null>(null)
const [password, setPassword] = React.useState('')
const [loading, setLoading] = React.useState(false)
@@ -590,9 +592,11 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
<CardTitle>Группы</CardTitle>
<CardDescription>Управление группами для расписания</CardDescription>
</div>
<Button onClick={() => setShowAddDialog(true)}>
Добавить группу
</Button>
{!isKspsutiMode && (
<Button onClick={() => setShowAddDialog(true)}>
Добавить группу
</Button>
)}
</div>
</CardHeader>
<CardContent>
@@ -610,23 +614,30 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
<div className="text-sm text-muted-foreground">
ID: {id} | Parse ID: {group.parseId} | Курс: {group.course}
</div>
{isKspsutiMode && (
<div className="text-xs text-muted-foreground mt-1">
Группа получена автоматически с lk.ks.psuti.ru. Редактирование отключено.
</div>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => openEditDialog(id)}
>
Редактировать
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => openDeleteDialog(id)}
>
Удалить
</Button>
</div>
{!isKspsutiMode && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => openEditDialog(id)}
>
Редактировать
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => openDeleteDialog(id)}
>
Удалить
</Button>
</div>
)}
</div>
))}
</div>
@@ -1124,7 +1135,7 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
}
export const getServerSideProps: GetServerSideProps<AdminPageProps> = async () => {
const groups = loadGroups()
const groups = await loadGroups()
const settings = loadSettings()
// Проверяем, используется ли дефолтный пароль
@@ -1135,7 +1146,8 @@ export const getServerSideProps: GetServerSideProps<AdminPageProps> = async () =
props: {
groups,
settings,
isDefaultPassword: isDefault
isDefaultPassword: isDefault,
isKspsutiMode: SCHED_MODE === 'kspsuti',
}
}
}

View File

@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
import { loadGroups, saveGroups, clearGroupsCache, GroupsData } from '@/shared/data/groups-loader'
import { validateGroupId, validateCourse } from '@/shared/utils/validation'
import { SCHED_MODE } from '@/shared/constants/urls'
type ResponseData = ApiResponse<{
groups?: GroupsData
@@ -11,10 +12,15 @@ async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
if (SCHED_MODE === 'kspsuti' && req.method !== 'GET') {
res.status(403).json({ error: 'Groups are managed automatically from lk.ks.psuti.ru in this mode' })
return
}
if (req.method === 'GET') {
// Получение списка групп (всегда свежие данные для админ-панели)
clearGroupsCache()
const groups = loadGroups(true)
const groups = await loadGroups(true)
res.status(200).json({ groups })
return
}
@@ -50,7 +56,7 @@ async function handler(
return
}
const groups = loadGroups()
const groups = await loadGroups()
// Проверка на дубликат
if (groups[id]) {
@@ -68,7 +74,7 @@ async function handler(
saveGroups(groups)
// Сбрасываем кеш и загружаем свежие данные из БД
clearGroupsCache()
const updatedGroups = loadGroups(true)
const updatedGroups = await loadGroups(true)
res.status(200).json({ success: true, groups: updatedGroups })
return
}

View File

@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import { withAuth, ApiResponse } from '@/shared/utils/api-wrapper'
import { loadGroups, saveGroups, clearGroupsCache, GroupsData } from '@/shared/data/groups-loader'
import { validateCourse } from '@/shared/utils/validation'
import { SCHED_MODE } from '@/shared/constants/urls'
type ResponseData = ApiResponse<{
groups?: GroupsData
@@ -18,8 +19,13 @@ async function handler(
return
}
if (SCHED_MODE === 'kspsuti') {
res.status(403).json({ error: 'Groups are managed automatically from lk.ks.psuti.ru in this mode' })
return
}
// Загружаем группы с проверкой кеша
let groups = loadGroups()
let groups = await loadGroups()
if (req.method === 'PUT') {
// Редактирование группы
@@ -56,7 +62,7 @@ async function handler(
saveGroups(groups)
// Сбрасываем кеш и загружаем свежие данные из БД
clearGroupsCache()
const updatedGroups = loadGroups(true)
const updatedGroups = await loadGroups(true)
res.status(200).json({ success: true, groups: updatedGroups })
return
}
@@ -73,7 +79,7 @@ async function handler(
saveGroups(groups)
// Сбрасываем кеш и загружаем свежие данные из БД
clearGroupsCache()
const updatedGroups = loadGroups(true)
const updatedGroups = await loadGroups(true)
res.status(200).json({ success: true, groups: updatedGroups })
return
}

View File

@@ -118,17 +118,6 @@ export default function HomePage(props: HomePageProps) {
const [openCourses, setOpenCourses] = React.useState<Set<number>>(new Set())
const [addGroupDialogOpen, setAddGroupDialogOpen] = React.useState(false)
// Подсчитываем смещения для каждого курса для последовательной анимации
const courseOffsets = React.useMemo(() => {
const offsets: { [course: number]: number } = {}
let totalGroups = 0
for (const course of [1, 2, 3, 4, 5]) {
offsets[course] = totalGroups
totalGroups += (groupsByCourse[course] || []).length
}
return { offsets, totalGroups }
}, [groupsByCourse])
const toggleCourse = (course: number) => {
setOpenCourses(prev => {
const next = new Set(prev)
@@ -158,7 +147,6 @@ export default function HomePage(props: HomePageProps) {
{[1, 2, 3, 4, 5].map((course, courseIndex) => {
const courseGroups = groupsByCourse[course] || []
const isOpen = openCourses.has(course)
const courseOffset = courseOffsets.offsets[course]
if (courseGroups.length === 0) {
return null
@@ -193,19 +181,10 @@ export default function HomePage(props: HomePageProps) {
<CardContent>
<div className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2">
{courseGroups.map(({ id, name }, groupIndex) => {
// Последовательная анимация: каждый следующий элемент с задержкой
// courseOffset - это количество групп во всех предыдущих курсах
// groupIndex - это индекс в текущем курсе
// Итого: последовательный счетчик для всех групп подряд
const globalIndex = courseOffset + groupIndex
const delay = 0.15 + globalIndex * 0.04
return (
<div
key={id}
className="stagger-card"
style={{
animationDelay: `${delay}s`,
} as React.CSSProperties}
>
<Link href={`/${id}`}>
<Button
@@ -239,7 +218,6 @@ export default function HomePage(props: HomePageProps) {
{showTeachersButton && (
<div
className="stagger-card mt-6"
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.05}s` } as React.CSSProperties}
>
<Link href="/teachers" className="block">
<Button variant="default" className="w-full h-auto py-4 text-base font-semibold">
@@ -253,7 +231,6 @@ export default function HomePage(props: HomePageProps) {
{showAddGroupButton && (
<div
className="stagger-card"
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + 0.08}s` } as React.CSSProperties}
>
<Button
variant="secondary"
@@ -267,13 +244,11 @@ export default function HomePage(props: HomePageProps) {
)}
<div
className="stagger-card"
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + (showAddGroupButton ? 0.11 : 0.08)}s` } as React.CSSProperties}
>
<ThemeSwitcher />
</div>
<div
className="stagger-card"
style={{ animationDelay: `${0.15 + courseOffsets.totalGroups * 0.04 + (showAddGroupButton ? 0.14 : 0.11)}s` } as React.CSSProperties}
>
<Link href={GITHUB_REPO_URL} target="_blank" rel="noopener noreferrer">
<Button variant="outline" className="gap-2">
@@ -329,7 +304,7 @@ export const getServerSideProps: GetServerSideProps<HomePageProps> = async () =>
}
// Если режим каникул выключен, загружаем группы и обрабатываем их
const groups = loadGroups()
const groups = await loadGroups()
// Группируем группы по курсам
const groupsByCourse: { [course: number]: Array<{ id: string; name: string }> } = {}

View File

@@ -5,7 +5,7 @@ import { SITEMAP_SITE_URL } from '@/shared/constants/urls'
export const getServerSideProps: GetServerSideProps = async (ctx) => {
// Используем кеш (обновляется каждую минуту автоматически)
const groups = loadGroups()
const groups = await loadGroups()
const fields = Object.keys(groups).map<ISitemapField>(group => (
{
loc: `${SITEMAP_SITE_URL}/${group}`,

View File

@@ -166,7 +166,7 @@ function cleanupCache() {
}
export async function getServerSideProps(context: GetServerSidePropsContext<{ teacher: string }>): Promise<GetServerSidePropsResult<NextSerialized<PageProps>>> {
const groups = loadGroups()
const groups = await loadGroups()
const settings = loadSettings()
const teacherParam = context.params?.teacher
const wkParam = context.query.wk