feat: replace old calendar with django-scheduler in frontend

- Removed legacy calendar with department_id/employee_id filters
- New CalendarCard loads calendars from django-scheduler API
- Events filtered by calendar ID instead of legacy params
- Removed unused SidebarCalendarOption type
This commit is contained in:
igor_nadein 2026-02-24 18:12:28 +03:00
parent 01b35c3b83
commit dd64d5f540
3 changed files with 58 additions and 74 deletions

View File

@ -1,13 +1,7 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .calendar.views import (
CalendarEventsViewSet,
CalendarSubscriptionViewSet,
CalendarViewSet,
)
# django-scheduler ViewSets (новая система, проверенный код)
# django-scheduler ViewSets (проверенная библиотека для календаря)
from .schedule.views import (
ScheduleCalendarViewSet,
ScheduleEventViewSet,
@ -38,15 +32,7 @@ app_name = "v1"
router = DefaultRouter()
router.register(r"calendar/events", CalendarEventsViewSet, basename="events")
router.register(r"calendar/calendars", CalendarViewSet, basename="calendars")
router.register(
r"calendar/subscriptions",
CalendarSubscriptionViewSet,
basename="subscriptions",
)
# django-scheduler endpoints (новая система)
# django-scheduler endpoints (проверенная библиотека)
router.register(r"schedule/calendars", ScheduleCalendarViewSet, basename="schedule-calendars")
router.register(r"schedule/events", ScheduleEventViewSet, basename="schedule-events")
router.register(r"schedule/rules", ScheduleRuleViewSet, basename="schedule-rules")

View File

@ -42,8 +42,7 @@ INSTALLED_APPS = [
"employees.apps.EmployeesConfig",
"api.apps.ApiConfig",
"hikcentral.apps.HikcentralConfig",
"calendar_app.apps.CalendarAppConfig",
"schedule", # django-scheduler (новая система, параллельно с calendar_app)
"schedule", # django-scheduler (проверенная библиотека для календаря)
"documents.apps.DocumentsConfig",
"requests_app.apps.RequestsAppConfig",
"feed.apps.FeedConfig",

View File

@ -48,14 +48,6 @@ type SidebarCalendarEvent = {
color?: string | null;
};
type SidebarCalendarOption = {
id: string;
title: string;
color?: string | null;
/** Параметры для запроса событий */
queryParams: Record<string, string | number>;
};
const pad = (v: number) => String(v).padStart(2, "0");
const formatDateKey = (date: Date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
@ -296,48 +288,54 @@ function LeftNav() {
}
function CalendarCard() {
const { user } = useUser();
const [monthDate, setMonthDate] = useState(() => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
});
const [events, setEvents] = useState<SidebarCalendarEvent[]>([]);
const [selectedCalendarKey, setSelectedCalendarKey] = useState<string>("company");
const [calendars, setCalendars] = useState<Array<{ id: number; name: string }>>([]);
const [selectedCalendarId, setSelectedCalendarId] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Строим список вариантов из данных пользователя (legacy: department_id / employee_id)
const calendarOptions = useMemo<SidebarCalendarOption[]>(() => {
const opts: SidebarCalendarOption[] = [
{ id: "company", title: "Компания", queryParams: {} },
];
// Отделы пользователя
if (user?.departments) {
user.departments.forEach((dept) => {
opts.push({
id: `dept-${dept.id}`,
title: dept.name,
queryParams: { department_id: dept.id },
});
});
}
// Личный календарь
if (user?.id) {
opts.push({
id: "personal",
title: "Личный",
queryParams: { employee_id: user.id },
});
}
return opts;
}, [user]);
// Загрузка списка календарей при монтировании
useEffect(() => {
let cancelled = false;
async function loadCalendars() {
try {
const result = await apiClient.getCalendars();
if (!cancelled) {
const cals = Array.isArray(result) ? result : [];
setCalendars(cals);
// Автоматически выбираем первый календарь
if (cals.length > 0 && selectedCalendarId === null) {
setSelectedCalendarId(cals[0].id);
}
}
} catch (err) {
if (!cancelled) {
console.error("Ошибка загрузки календарей:", err);
}
}
}
loadCalendars();
return () => {
cancelled = true;
};
}, []);
// Загрузка событий выбранно календаря
useEffect(() => {
if (selectedCalendarId === null) {
setEvents([]);
return;
}
let cancelled = false;
const start = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
const end = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 1);
const weekStart = startOfWeekMonday(new Date());
@ -352,13 +350,10 @@ function CalendarCard() {
setLoading(true);
setError(null);
const selected = calendarOptions.find((c) => c.id === selectedCalendarKey);
const extraParams = selected?.queryParams ?? {};
const result = await apiClient.getCalendarEvents({
start: formatDateKey(fetchStart),
end: formatDateKey(fetchEnd),
...extraParams,
calendar: selectedCalendarId,
});
if (!cancelled) {
@ -369,7 +364,7 @@ function CalendarCard() {
const message = err instanceof Error ? err.message : String(err || "");
const isNetworkLike = message === "NetworkError" || /fetch failed/i.test(message);
if (!isNetworkLike) {
console.error("Ошибка загрузки календаря:", err);
console.error("Ошибка загрузки событий календаря:", err);
}
setError("Не удалось загрузить события");
setEvents([]);
@ -386,7 +381,7 @@ function CalendarCard() {
return () => {
cancelled = true;
};
}, [monthDate, selectedCalendarKey, calendarOptions]);
}, [monthDate, selectedCalendarId]);
const monthLabel = useMemo(() => {
const label = monthDate.toLocaleDateString("ru-RU", {
@ -450,17 +445,21 @@ function CalendarCard() {
<>
<div className="rounded-2xl bg-white p-4 shadow-sm ring-1 ring-gray-100">
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-gray-500">Календарь</p>
<select
value={selectedCalendarKey}
onChange={(e) => setSelectedCalendarKey(e.target.value)}
className="w-full rounded-lg border border-gray-300 bg-white px-2.5 py-2 text-xs text-gray-800 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
>
{calendarOptions.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.title}
</option>
))}
</select>
{calendars.length === 0 ? (
<p className="text-xs text-gray-500">Календарей нет</p>
) : (
<select
value={selectedCalendarId || ""}
onChange={(e) => setSelectedCalendarId(Number(e.target.value))}
className="w-full rounded-lg border border-gray-300 bg-white px-2.5 py-2 text-xs text-gray-800 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-100"
>
{calendars.map((cal) => (
<option key={cal.id} value={cal.id}>
{cal.name}
</option>
))}
</select>
)}
</div>
<div className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100">