diff --git a/backend/api/v1/attendance/views.py b/backend/api/v1/attendance/views.py index 171e674d..c33c7096 100644 --- a/backend/api/v1/attendance/views.py +++ b/backend/api/v1/attendance/views.py @@ -1,6 +1,6 @@ import logging from calendar import monthrange -from datetime import date +from datetime import date, timedelta from urllib.parse import quote from attendance.models import ( @@ -92,6 +92,31 @@ def _check_staff_access(user) -> None: raise PermissionDenied("You don't have permission to manage attendance") +def _has_attendance_pass(record: AttendanceRecord) -> bool: + return bool( + record.arrival_time + or record.departure_time + or (record.work_hours is not None and record.work_hours > 0) + ) + + +def _has_absence_marker(record: AttendanceRecord) -> bool: + values = [ + record.statuses, + record.employee_issues, + ] + return bool( + record.is_absent + or any( + "absent" in normalized + or "absence" in normalized + or "отсутств" in normalized + for items in values + for normalized in [str(item).strip().lower() for item in (items or [])] + ) + ) + + def _comments_count_annotation(): record_ct = ContentType.objects.get_for_model(AttendanceRecord) comments_subquery = ( @@ -174,7 +199,11 @@ class LogStormAttendanceAnalyzeAPIView(APIView): get_employee_work_schedule_payload(employee) or get_standard_work_schedule_payload() ) - aliases = data.get("aliases") + aliases = ( + data["aliases"] + if "aliases" in data + else list(getattr(employee, "attendance_aliases", None) or []) + ) try: result = analyze_employee_attendance( @@ -345,6 +374,78 @@ class AttendanceRecordListAPIView(ListAPIView): return queryset +class AttendanceWeeklySummaryAPIView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + _check_staff_access(request.user) + week_start = self._week_start(request.query_params.get("week_start")) + week_end = week_start + timedelta(days=6) + records = ( + _records_visible_to(request.user) + .filter( + employee__is_active=True, + date__gte=week_start, + date__lte=week_end, + ) + .only( + "employee_id", + "date", + "arrival_time", + "departure_time", + "work_hours", + "is_absent", + "statuses", + "employee_issues", + ) + .order_by("date", "employee_id") + ) + + by_date = { + week_start + timedelta(days=day_offset): { + "present": set(), + "absent": set(), + } + for day_offset in range(7) + } + + for record in records: + bucket = by_date.get(record.date) + if bucket is None: + continue + if _has_attendance_pass(record): + bucket["present"].add(record.employee_id) + bucket["absent"].discard(record.employee_id) + elif _has_absence_marker(record): + bucket["absent"].add(record.employee_id) + + return Response( + { + "week_start": week_start, + "week_end": week_end, + "days": [ + { + "date": day.isoformat(), + "weekday": day.strftime("%A"), + "present": len(counts["present"]), + "absent": len(counts["absent"]), + } + for day, counts in by_date.items() + ], + } + ) + + def _week_start(self, value: str | None) -> date: + if value: + try: + parsed = date.fromisoformat(value) + except ValueError: + parsed = timezone.localdate() + else: + parsed = timezone.localdate() + return parsed - timedelta(days=parsed.weekday()) + + class AttendanceMonthlyMatrixAPIView(APIView): permission_classes = [IsAuthenticated] diff --git a/backend/api/v1/employees/serializers/employee.py b/backend/api/v1/employees/serializers/employee.py index c2512b49..2b42e093 100644 --- a/backend/api/v1/employees/serializers/employee.py +++ b/backend/api/v1/employees/serializers/employee.py @@ -52,6 +52,11 @@ class EmployeeSerializer(serializers.ModelSerializer): """Полная версия сотрудника для /employees/.""" avatar = Base64ImageField(required=False, allow_null=True) + attendance_aliases = serializers.ListField( + child=serializers.CharField(allow_blank=True), + required=False, + allow_empty=True, + ) actions = EmployeeActionSerializer(many=True, read_only=True) username = serializers.CharField(read_only=True, allow_blank=True) last_activity_at = serializers.SerializerMethodField() @@ -95,6 +100,7 @@ class EmployeeSerializer(serializers.ModelSerializer): "telegram", "whatsapp", "wechat", + "attendance_aliases", "skills", "skills_ids", "is_active", @@ -132,6 +138,14 @@ class EmployeeSerializer(serializers.ModelSerializer): return None return value + def validate_attendance_aliases(self, value): + result = [] + for alias in value or []: + alias_value = str(alias).strip() + if alias_value and alias_value not in result: + result.append(alias_value) + return result + def update(self, instance, validated_data): skills = validated_data.pop("skills_ids", None) diff --git a/backend/api/v1/urls.py b/backend/api/v1/urls.py index 3dba45d5..3090d53e 100644 --- a/backend/api/v1/urls.py +++ b/backend/api/v1/urls.py @@ -40,6 +40,7 @@ from .attendance.views import ( AttendanceRecordCommentsAPIView, AttendanceRecordDetailAPIView, AttendanceRecordListAPIView, + AttendanceWeeklySummaryAPIView, EmployeeWorkScheduleAPIView, LogStormAttendanceAnalyzeAPIView, StandardWorkScheduleAPIView, @@ -113,6 +114,11 @@ urlpatterns = [ AttendanceRecordListAPIView.as_view(), name="attendance-records", ), + path( + "attendance/weekly-summary/", + AttendanceWeeklySummaryAPIView.as_view(), + name="attendance-weekly-summary", + ), path( "attendance/monthly-matrix/", AttendanceMonthlyMatrixAPIView.as_view(), diff --git a/backend/attendance/services.py b/backend/attendance/services.py index 2494cb03..dc401b60 100644 --- a/backend/attendance/services.py +++ b/backend/attendance/services.py @@ -318,11 +318,13 @@ def run_attendance_auto_sync(*, force: bool = False) -> AttendanceAutoSyncSettin or get_standard_work_schedule_payload() ) try: + aliases = list(getattr(employee, "attendance_aliases", None) or []) result = analyze_employee_attendance( employee=employee, period_start=period_start, period_end=period_end, schedule=schedule_payload, + aliases=aliases, client=None, ) request_payload = build_logstorm_attendance_payload( @@ -330,6 +332,7 @@ def run_attendance_auto_sync(*, force: bool = False) -> AttendanceAutoSyncSettin period_start=period_start, period_end=period_end, schedule=schedule_payload, + aliases=aliases, ) save_logstorm_attendance_result( employee=employee, diff --git a/backend/employees/migrations/0052_employee_attendance_aliases.py b/backend/employees/migrations/0052_employee_attendance_aliases.py new file mode 100644 index 00000000..ae2817fa --- /dev/null +++ b/backend/employees/migrations/0052_employee_attendance_aliases.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("employees", "0051_drop_legacy_state_tables"), + ] + + operations = [ + migrations.AddField( + model_name="employee", + name="attendance_aliases", + field=models.JSONField( + blank=True, + default=list, + help_text="Дополнительные идентификаторы сотрудника в LogStorm", + verbose_name="Алиасы посещаемости", + ), + ), + ] diff --git a/backend/employees/models.py b/backend/employees/models.py index 13705784..40068cea 100644 --- a/backend/employees/models.py +++ b/backend/employees/models.py @@ -231,6 +231,12 @@ class Employee(AbstractUser): ) whatsapp = PhoneNumberField("WhatsApp", blank=True) wechat = models.CharField("WeChat", max_length=100, blank=True) + attendance_aliases = models.JSONField( + "Алиасы посещаемости", + default=list, + blank=True, + help_text="Дополнительные идентификаторы сотрудника в LogStorm", + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) skills = models.ManyToManyField( diff --git a/backend/tests/api/v1/attendance/test_attendance_storage.py b/backend/tests/api/v1/attendance/test_attendance_storage.py index 2cf6e0f5..2d50c484 100644 --- a/backend/tests/api/v1/attendance/test_attendance_storage.py +++ b/backend/tests/api/v1/attendance/test_attendance_storage.py @@ -38,6 +38,10 @@ def _monthly_matrix_url(): return reverse("api:v1:attendance-monthly-matrix") +def _weekly_summary_url(): + return reverse("api:v1:attendance-weekly-summary") + + def _monthly_matrix_export_url(): return reverse("api:v1:attendance-monthly-matrix-export") @@ -181,6 +185,57 @@ def test_analyze_saves_run_and_records(auth_client_factory, user_factory): assert record.raw_data["employee_id"] == "42" +def test_weekly_summary_counts_present_and_absent_people( + auth_client_factory, + user_factory, +): + staff = user_factory(staff=True) + present_employee = user_factory() + absent_employee = user_factory() + inactive_employee = user_factory(active=False) + client = auth_client_factory(staff) + run = AttendanceAnalysisRun.objects.create( + employee=present_employee, + period_start="2026-04-20", + period_end="2026-04-26", + triggered_by=staff, + ) + + AttendanceRecord.objects.create( + analysis_run=run, + employee=present_employee, + date="2026-04-20", + arrival_time="09:00", + departure_time="18:00", + work_hours=9, + is_absent=False, + ) + AttendanceRecord.objects.create( + analysis_run=run, + employee=absent_employee, + date="2026-04-20", + work_hours=0, + is_absent=True, + statuses=["absence"], + ) + AttendanceRecord.objects.create( + analysis_run=run, + employee=inactive_employee, + date="2026-04-20", + arrival_time="09:00", + work_hours=9, + ) + + response = client.get(_weekly_summary_url(), {"week_start": "2026-04-20"}) + + assert response.status_code == 200 + monday = response.data["days"][0] + assert monday["date"] == "2026-04-20" + assert monday["present"] == 1 + assert monday["absent"] == 1 + assert response.data["days"][6]["date"] == "2026-04-26" + + def test_work_schedule_endpoint_returns_default_for_self( auth_client_factory, user_factory, diff --git a/backend/tests/api/v1/employees/test_employees.py b/backend/tests/api/v1/employees/test_employees.py index 5a6ce9a9..eac962c8 100644 --- a/backend/tests/api/v1/employees/test_employees.py +++ b/backend/tests/api/v1/employees/test_employees.py @@ -102,6 +102,24 @@ def test_retrieve_ok(api_client: APIClient): assert data["id"] == e.id assert data["email"] == e.email + +def test_update_attendance_aliases_normalizes_values(api_client: APIClient): + staff = make_user("staff@example.com", staff=True) + employee = make_user("alias@example.com") + api_client.force_authenticate(user=staff) + + url = reverse("api:v1:employees-detail", args=[employee.pk]) + resp = api_client.patch( + url, + {"attendance_aliases": [" 200 ", "200", "", "300"]}, + format="json", + ) + + assert resp.status_code == status.HTTP_200_OK + assert resp.json()["attendance_aliases"] == ["200", "300"] + employee.refresh_from_db() + assert employee.attendance_aliases == ["200", "300"] + # ---------- search & ordering ---------- def test_search_by_email_and_last_name(api_client: APIClient): diff --git a/backend/tests/integration/test_logstorm_attendance_api.py b/backend/tests/integration/test_logstorm_attendance_api.py index 2b27be0c..ba5bc5bd 100644 --- a/backend/tests/integration/test_logstorm_attendance_api.py +++ b/backend/tests/integration/test_logstorm_attendance_api.py @@ -134,6 +134,24 @@ def test_logstorm_attendance_api_passes_aliases( assert analyze.call_args.kwargs["aliases"] == ["200", "300"] +def test_logstorm_attendance_api_uses_employee_aliases_when_omitted( + auth_client_factory, + user_factory, +): + staff = user_factory(staff=True) + employee = user_factory(attendance_aliases=["200", "300"]) + client = auth_client_factory(staff) + + with patch( + "api.v1.attendance.views.analyze_employee_attendance", + return_value={"records": []}, + ) as analyze: + response = client.post(_url(), _payload(employee.id), format="json") + + assert response.status_code == 200 + assert analyze.call_args.kwargs["aliases"] == ["200", "300"] + + def test_logstorm_attendance_api_rejects_invalid_period( auth_client_factory, user_factory, diff --git a/frontend/src/app/attendance/page.tsx b/frontend/src/app/attendance/page.tsx index 79ca950d..3b70fb12 100644 --- a/frontend/src/app/attendance/page.tsx +++ b/frontend/src/app/attendance/page.tsx @@ -41,6 +41,7 @@ import type { AttendanceAutoSyncSettings, AttendanceMatrixExportFile, AttendanceSchedulePayload, + AttendanceWeeklySummary, StandardWorkSchedule, } from "@/lib/api/attendance"; import type { User } from "@/types/api"; @@ -87,6 +88,7 @@ type AttendanceAnalyzeRequest = { aliases?: string[]; }; type MonthlyMatrixRequest = { employee_ids: string; month: string }; +type AttendanceWeeklySummaryRequest = { week_start?: string }; const periodPresets: Array<{ value: AttendancePeriodPreset; label: string }> = [ { value: "month", label: "За месяц" }, @@ -375,6 +377,22 @@ function getAttendanceRecord(recordId: number) { return apiClient.request(`/api/v1/attendance/records/${recordId}/`); } +function getAttendanceWeeklySummary(params?: AttendanceWeeklySummaryRequest) { + const client = apiClient as typeof apiClient & { + getAttendanceWeeklySummary?: ( + params?: AttendanceWeeklySummaryRequest, + ) => Promise; + }; + + if (typeof client.getAttendanceWeeklySummary === "function") { + return client.getAttendanceWeeklySummary(params); + } + + return apiClient.request( + `/api/v1/attendance/weekly-summary/${buildQuery(params)}`, + ); +} + function formatOptionalDateTime(value?: string | null) { return value ? formatDateTime(value) : "-"; } @@ -566,6 +584,15 @@ function weekdayLabelFromDate(value?: string) { return DAYS_RU_FROM_INDEX[date.getDay()] || ""; } +function formatWeeklyDateLabel(value: string) { + const date = new Date(`${value}T00:00:00`); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleDateString("ru-RU", { + day: "2-digit", + month: "short", + }); +} + const DAYS_RU_FROM_INDEX: Record = { 0: "Вс", 1: "Пн", @@ -608,6 +635,10 @@ export default function AttendancePage() { const [loadingEmployees, setLoadingEmployees] = useState(true); const [loadingSchedule, setLoadingSchedule] = useState(true); const [savingSchedule, setSavingSchedule] = useState(false); + const [loadingWeeklySummary, setLoadingWeeklySummary] = useState(true); + const [weeklySummary, setWeeklySummary] = + useState(null); + const [weeklySummaryOpen, setWeeklySummaryOpen] = useState(false); const [loadingStats, setLoadingStats] = useState(false); const [downloadingReport, setDownloadingReport] = useState(false); const [error, setError] = useState(null); @@ -681,6 +712,35 @@ export default function AttendancePage() { ); }, [employees]); + useEffect(() => { + let cancelled = false; + + async function loadWeeklySummary() { + if (!canManageAttendance) { + setLoadingWeeklySummary(false); + return; + } + + try { + setLoadingWeeklySummary(true); + const summary = await getAttendanceWeeklySummary(); + if (!cancelled) setWeeklySummary(summary); + } catch (summaryError) { + if (!cancelled) { + setError(getErrorMessage(summaryError, "Не удалось загрузить недельную статистику")); + } + } finally { + if (!cancelled) setLoadingWeeklySummary(false); + } + } + + void loadWeeklySummary(); + + return () => { + cancelled = true; + }; + }, [canManageAttendance]); + useEffect(() => { if (!actionsMenuOpen) { return; @@ -1343,6 +1403,77 @@ export default function AttendancePage() { +
+ + + {weeklySummaryOpen ? ( +
+ {(weeklySummary?.days || []).map((day) => { + const total = day.present + day.absent; + return ( +
+ + + {weekdayLabelFromDate(day.date)} + + + {formatWeeklyDateLabel(day.date).replace(".", "")} + + + + {total} + + Пришли + + {day.present} + + Отсутствовали + + {day.absent} + +
+ ); + })} + {!loadingWeeklySummary && !weeklySummary?.days.length ? ( +
+ За текущую неделю сохранённых записей пока нет. +
+ ) : null} +
+ ) : null} +
+
diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index 57d7ad7c..98886d06 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -196,7 +196,11 @@ export default function ProfilePage() { personnelState={user.personnel_state} /> - +
); diff --git a/frontend/src/app/users/[id]/page.tsx b/frontend/src/app/users/[id]/page.tsx index 6f7c143e..e5479b28 100644 --- a/frontend/src/app/users/[id]/page.tsx +++ b/frontend/src/app/users/[id]/page.tsx @@ -533,7 +533,11 @@ export default function UserDetailPage() { personnelState={person.personnel_state} /> - + ) : null}
diff --git a/frontend/src/components/attendance/EmployeeAttendanceCard.tsx b/frontend/src/components/attendance/EmployeeAttendanceCard.tsx index a0beb2ce..3d99e257 100644 --- a/frontend/src/components/attendance/EmployeeAttendanceCard.tsx +++ b/frontend/src/components/attendance/EmployeeAttendanceCard.tsx @@ -1,10 +1,11 @@ "use client"; -import { useEffect, useMemo, useState, type FormEvent } from "react"; +import { useEffect, useMemo, useRef, useState, type FormEvent } from "react"; import { AlertTriangle, Camera, ChevronDown, + ChevronRight, Clock, Edit3, Loader2, @@ -12,6 +13,7 @@ import { LogOut, MessageSquare, RefreshCw, + Tag, } from "lucide-react"; import { @@ -33,6 +35,7 @@ import type { import type { EmployeeAction } from "@/types/api"; type EmployeeAttendanceCardProps = { + attendanceAliases?: string[] | null; employeeId: number; employeeActions?: EmployeeAction[] | null; }; @@ -463,6 +466,17 @@ function nullableTime(value: string) { return trimmed || null; } +function normalizeAttendanceAliases(aliases: Array) { + const result: string[] = []; + aliases.forEach((alias) => { + const value = String(alias || "").trim(); + if (value && !result.includes(value)) { + result.push(value); + } + }); + return result; +} + type AttendanceCardApiClient = typeof apiClient & { request: (endpoint: string, options?: RequestInit) => Promise; addAttendanceRecordComment?: unknown; @@ -491,6 +505,7 @@ function analyzeEmployeeAttendance(data: { employee_id: number; period_start: string; period_end: string; + aliases?: string[]; }) { const client = attendanceApiClient(); if (typeof client.analyzeAttendance === "function") { @@ -569,10 +584,12 @@ function updateAttendanceRecord(recordId: number, data: AttendanceRecordUpdatePa } export default function EmployeeAttendanceCard({ + attendanceAliases, employeeActions, employeeId, }: EmployeeAttendanceCardProps) { const { user: currentUser } = useUser(); + const actionsMenuRef = useRef(null); const defaultRange = useMemo(() => getPeriodRange("week"), []); const [period, setPeriod] = useState("week"); const [periodStart, setPeriodStart] = useState(defaultRange.start); @@ -589,6 +606,13 @@ export default function EmployeeAttendanceCard({ const [commentDrafts, setCommentDrafts] = useState>({}); const [dayEventsRecordPreview, setDayEventsRecordPreview] = useState(null); + const [actionsMenuOpen, setActionsMenuOpen] = useState(false); + const [aliasModalOpen, setAliasModalOpen] = useState(false); + const [aliases, setAliases] = useState(() => + normalizeAttendanceAliases(attendanceAliases || []), + ); + const [aliasDraft, setAliasDraft] = useState(""); + const [aliasError, setAliasError] = useState(null); const [editModalOpen, setEditModalOpen] = useState(false); const [editForm, setEditForm] = useState(null); @@ -596,6 +620,33 @@ export default function EmployeeAttendanceCard({ currentUser?.auth?.is_staff || currentUser?.auth?.is_superuser, ); + useEffect(() => { + setAliases(normalizeAttendanceAliases(attendanceAliases || [])); + }, [attendanceAliases]); + + useEffect(() => { + if (!actionsMenuOpen) return; + + const handlePointerDown = (event: MouseEvent) => { + if (actionsMenuRef.current && !actionsMenuRef.current.contains(event.target as Node)) { + setActionsMenuOpen(false); + } + }; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setActionsMenuOpen(false); + } + }; + + document.addEventListener("mousedown", handlePointerDown); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handlePointerDown); + document.removeEventListener("keydown", handleEscape); + }; + }, [actionsMenuOpen]); + const records = useMemo(() => { if (!result?.records || !Array.isArray(result.records)) return []; return result.records; @@ -637,6 +688,7 @@ export default function EmployeeAttendanceCard({ employee_id: employeeId, period_start: periodStart, period_end: periodEnd, + aliases: aliases.length ? aliases : undefined, }); const savedRecords = await getAttendanceRecords({ employee_id: employeeId, @@ -835,6 +887,43 @@ export default function EmployeeAttendanceCard({ setEditModalOpen(true); } + function openAliasModal() { + setAliasDraft(""); + setAliasError(null); + setAliasModalOpen(true); + } + + async function saveAliases(nextAliases: string[]) { + const normalizedAliases = normalizeAttendanceAliases(nextAliases); + try { + setBusyKey("attendance-aliases"); + setAliasError(null); + await apiClient.updateEmployee(employeeId, { + attendance_aliases: normalizedAliases, + }); + setAliases(normalizedAliases); + setAliasDraft(""); + } catch (saveError) { + setAliasError(getErrorMessage(saveError, "Не удалось сохранить алиасы")); + } finally { + setBusyKey(null); + } + } + + async function addAlias() { + const alias = aliasDraft.trim(); + if (!alias) return; + if (aliases.includes(alias)) { + setAliasError("Такой алиас уже добавлен"); + return; + } + await saveAliases([...aliases, alias]); + } + + async function removeAlias(alias: string) { + await saveAliases(aliases.filter((item) => item !== alias)); + } + function handleEditRecordSelect(recordId: string) { const selectedRecord = records.find((record) => getRecordId(record) === Number(recordId)); const form = selectedRecord ? recordToEditForm(selectedRecord) : null; @@ -912,15 +1001,48 @@ export default function EmployeeAttendanceCard({ {canManageAttendance ? ( - +
+ + {actionsMenuOpen ? ( +
+ + +
+ ) : null} +
) : null}
@@ -1250,6 +1372,94 @@ export default function EmployeeAttendanceCard({ record={dayEventsRecordPreview} /> + setAliasModalOpen(false)} + title="Алиасы посещаемости" + size="sm" + footer={( +
+ +
+ )} + > +
+
{ + event.preventDefault(); + void addAlias(); + }} + className="flex flex-col gap-2 sm:flex-row" + > + { + setAliasDraft(event.target.value); + setAliasError(null); + }} + className="app-input min-w-0 flex-1 rounded-xl px-3 py-2.5 text-sm" + placeholder="Например, 200" + /> + +
+ + {aliasError ? ( +
+ + {aliasError} +
+ ) : null} + +
+

Текущие алиасы

+ {aliases.length ? ( +
+ {aliases.map((alias) => ( + + {alias} + + + ))} +
+ ) : ( +
+ Алиасы не добавлены. +
+ )} +
+
+
+ setEditModalOpen(false)} diff --git a/frontend/src/lib/api/attendance.ts b/frontend/src/lib/api/attendance.ts index 9548617a..b5113ddb 100644 --- a/frontend/src/lib/api/attendance.ts +++ b/frontend/src/lib/api/attendance.ts @@ -58,6 +58,7 @@ export type AttendanceAnalyzePayload = { period_start: string; period_end: string; schedule?: AttendanceSchedulePayload; + aliases?: string[]; }; export type AttendanceRecord = { @@ -220,6 +221,23 @@ export type MonthlyAttendanceMatrixExportQuery = { period_end: string; }; +export type AttendanceWeeklySummaryDay = { + date: string; + weekday: string; + present: number; + absent: number; +}; + +export type AttendanceWeeklySummary = { + week_start: string; + week_end: string; + days: AttendanceWeeklySummaryDay[]; +}; + +export type AttendanceWeeklySummaryQuery = { + week_start?: string; +}; + export type AttendanceMatrixExportFile = { blob: Blob; filename: string; @@ -242,6 +260,8 @@ export function createAttendanceApi(request: RequestFn, getToken: GetTokenFn) { }) as Promise, getAttendanceRecords: (params?: AttendanceRecordsQuery) => request(`/api/v1/attendance/records/${buildQuery(params)}`) as Promise, + getAttendanceWeeklySummary: (params?: AttendanceWeeklySummaryQuery) => + request(`/api/v1/attendance/weekly-summary/${buildQuery(params)}`) as Promise, getAttendanceRecord: (recordId: number) => request(`/api/v1/attendance/records/${recordId}/`) as Promise, getMonthlyAttendanceMatrix: (params: MonthlyAttendanceMatrixQuery) => diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 6257b9c6..d4467e0c 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -52,6 +52,7 @@ export interface User { telegram?: string; whatsapp?: string; wechat?: string; + attendance_aliases?: string[]; gender?: string | number; // 1 - мужской, 2 - женский, 0 - не указан birth_date?: string; skills?: Skill[];