From db05b4fcdb02ca8604cc8b1550c3b7854950f081 Mon Sep 17 00:00:00 2001 From: lizerk Date: Sat, 11 Apr 2026 20:44:16 +0300 Subject: [PATCH] refactor(frontend): align calendar and list UI --- backend/api/v1/schedule/serializers.py | 14 + frontend/next-env.d.ts | 2 +- frontend/src/app/equipment/page.tsx | 108 +++--- frontend/src/app/globals.css | 23 ++ frontend/src/app/messages/page.tsx | 4 +- frontend/src/app/notifications/page.tsx | 63 ++-- frontend/src/app/procurement/page.tsx | 84 ++--- frontend/src/app/requests/page.tsx | 4 +- frontend/src/components/BigCalendar.tsx | 7 +- .../components/CalendarParticipantsModal.tsx | 54 ++- frontend/src/components/EventModal.tsx | 315 +++++++++++++----- .../src/components/NotificationCenter.tsx | 20 +- .../src/components/ViewDayEventsModal.tsx | 3 +- .../src/components/ViewEventDetailsModal.tsx | 3 +- .../src/components/calendar/CalendarCard.tsx | 5 +- frontend/src/hooks/useCalendarModals.ts | 3 +- frontend/src/hooks/useProcurementPage.ts | 68 +++- frontend/src/lib/calendar-event-colors.ts | 16 + 18 files changed, 548 insertions(+), 248 deletions(-) create mode 100644 frontend/src/lib/calendar-event-colors.ts diff --git a/backend/api/v1/schedule/serializers.py b/backend/api/v1/schedule/serializers.py index 2e5040e6..a847b3ae 100644 --- a/backend/api/v1/schedule/serializers.py +++ b/backend/api/v1/schedule/serializers.py @@ -496,6 +496,7 @@ class EventRelationSerializer(serializers.ModelSerializer): event_title = serializers.CharField(source="event.title", read_only=True) user_name = serializers.SerializerMethodField() + user_avatar = serializers.SerializerMethodField() content_type = serializers.PrimaryKeyRelatedField( queryset=ContentType.objects.all(), required=False, # Делаем необязательным @@ -511,6 +512,7 @@ class EventRelationSerializer(serializers.ModelSerializer): "object_id", "distinction", "user_name", + "user_avatar", ] read_only_fields = ["id"] @@ -521,6 +523,18 @@ class EventRelationSerializer(serializers.ModelSerializer): return str(obj.content_object) return None + @extend_schema_field(serializers.CharField(allow_null=True)) + def get_user_avatar(self, obj): + """URL аватара связанного сотрудника.""" + user = getattr(obj, "content_object", None) + avatar = getattr(user, "avatar", None) + if avatar: + try: + return avatar.url + except Exception: + return str(avatar) + return None + def create(self, validated_data): """Автоматически устанавливает content_type для User.""" if "content_type" not in validated_data: diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend/src/app/equipment/page.tsx b/frontend/src/app/equipment/page.tsx index f039e067..ebb19ead 100644 --- a/frontend/src/app/equipment/page.tsx +++ b/frontend/src/app/equipment/page.tsx @@ -153,23 +153,23 @@ export default function EquipmentPage() { return ( {loading ? ( -
+
-

Загрузка оборудования...

+

Загрузка оборудования...

) : error ? ( -
+

{error}

) : ( -
+
{/* Header */}
-

Оборудование

+

Оборудование

@@ -186,14 +186,14 @@ export default function EquipmentPage() { value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Поиск по оборудованию" - className="w-full rounded-lg border border-gray-200 bg-gray-50 py-2.5 pl-9 pr-3 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" + className="app-input w-full rounded-lg py-2.5 pl-9 pr-3 text-sm" />
- Дата покупки: - setDateFromFilter(e.target.value)} className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800" placeholder="С" /> - — - setDateToFilter(e.target.value)} className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-800" placeholder="По" /> + Дата покупки: + setDateFromFilter(e.target.value)} className="app-input flex-1 rounded-lg px-3 py-2 text-sm" placeholder="С" /> + — + setDateToFilter(e.target.value)} className="app-input flex-1 rounded-lg px-3 py-2 text-sm" placeholder="По" />
{activeFilterCount > 0 && ( - )} @@ -528,8 +528,8 @@ export default function EquipmentPage() { {/* ===== Modal create/edit ===== */} - - +
@@ -539,12 +539,12 @@ export default function EquipmentPage() {
{/* Название */}
- + setForm((p) => ({ ...p, name: e.target.value }))} placeholder="Ноутбук Lenovo ThinkPad X1..." - className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-100" + className="app-input w-full rounded-lg px-3 py-2 text-sm" />
@@ -558,8 +558,8 @@ export default function EquipmentPage() { /> {isCreateMode && previewInventoryNumber && ( -
- Следующий инвентарный номер: {previewInventoryNumber} +
+ Следующий инвентарный номер: {previewInventoryNumber}
)} @@ -585,14 +585,14 @@ export default function EquipmentPage() { {modalMode === "create" && (
- + setForm((p) => ({ ...p, quantity: Math.max(1, Math.min(100, Number(e.target.value) || 1)) }))} - className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-100" + className="app-input w-full rounded-lg px-3 py-2 text-sm" />
)} @@ -600,58 +600,58 @@ export default function EquipmentPage() { {/* Дата покупки + стоимость */}
- + setForm((p) => ({ ...p, purchase_date: e.target.value }))} - className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-100" + className="app-input w-full rounded-lg px-3 py-2 text-sm" />
- + setForm((p) => ({ ...p, purchase_cost: e.target.value }))} placeholder="0.00" - className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-100" + className="app-input w-full rounded-lg px-3 py-2 text-sm" />
{/* Серийный номер */}
- + setForm((p) => ({ ...p, serial_number: e.target.value }))} placeholder="SN-XXXXX" - className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-100" + className="app-input w-full rounded-lg px-3 py-2 text-sm" />
{/* Расположение */}
- + setForm((p) => ({ ...p, location: e.target.value }))} placeholder="Офис 305, стол 2..." - className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-100" + className="app-input w-full rounded-lg px-3 py-2 text-sm" />
{/* Заметки */}
- +