diff --git a/src/modules/ZachetCard/ZachetCard.vue b/src/modules/ZachetCard/ZachetCard.vue new file mode 100644 index 0000000..59b5176 --- /dev/null +++ b/src/modules/ZachetCard/ZachetCard.vue @@ -0,0 +1,293 @@ + + + + + diff --git a/src/modules/ZachetCard/assets/mguLogoProfUnion.webp b/src/modules/ZachetCard/assets/mguLogoProfUnion.webp new file mode 100644 index 0000000..f151719 Binary files /dev/null and b/src/modules/ZachetCard/assets/mguLogoProfUnion.webp differ diff --git a/src/modules/ZachetCard/controller/mapper.ts b/src/modules/ZachetCard/controller/mapper.ts new file mode 100644 index 0000000..daf71a1 --- /dev/null +++ b/src/modules/ZachetCard/controller/mapper.ts @@ -0,0 +1,130 @@ +import type { UserdataItem, ZachetCardData } from './types'; + +const FALLBACK = '—'; + +const DEFAULT_CONTACTS = { + address: '1018а, Главное здание МГУ', + phone: '+7 (495)-939-14-58', + email: 'begomvprofcom@mail.ru', + vk: '@msuprofcom', + website: 'msuprof.com', +}; + +function normalizeValue(value?: string | null): string | null { + if (typeof value !== 'string') return null; + + const trimmed = value.trim(); + + return trimmed.length ? trimmed : null; +} + +function getValues(items: UserdataItem[], category: string, param: string): string[] { + return items + .filter(entry => entry.category === category && entry.param === param) + .map(entry => normalizeValue(entry.value)) + .filter((value): value is string => Boolean(value)); +} + +function getFirstValue(items: UserdataItem[], category: string, param: string): string | null { + return getValues(items, category, param)[0] ?? null; +} + +function formatDate(value: string | null): string { + if (!value) return FALLBACK; + + if (/^\d{2}\.\d{2}\.\d{4}$/.test(value)) { + return value; + } + + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { + const [year, month, day] = value.split('-'); + return `${day}.${month}.${year}`; + } + + return value; +} + +function resolveUnionCardNumber(items: UserdataItem[]): string { + return ( + getFirstValue(items, 'Учёба', 'Номер профсоюзного билета') ?? + getFirstValue(items, 'Учетные данные', 'Номер профсоюзного билета') ?? + FALLBACK + ); +} + +function resolvePhotoUrl(items: UserdataItem[]): string | undefined { + const value = getFirstValue(items, 'Личная информация', 'Фото'); + + if (!value) return undefined; + + return value; +} + +function resolveFacultyRu(items: UserdataItem[]): string { + const facultyValues = getValues(items, 'Учёба', 'Факультет'); + + for (const value of facultyValues) { + if (!value.includes('/')) { + return value; + } + } + + const combinedValue = facultyValues[0]; + + if (combinedValue?.includes('/')) { + return combinedValue.split('/')[0]?.trim() || FALLBACK; + } + + return combinedValue ?? FALLBACK; +} + +function resolveFacultyEn(items: UserdataItem[]): string { + const directFacultyEn = getFirstValue(items, 'Учёба', 'Faculty'); + + if (directFacultyEn) { + return directFacultyEn; + } + + const facultyValues = getValues(items, 'Учёба', 'Факультет'); + const combinedValue = facultyValues.find(value => value.includes('/')); + + if (combinedValue) { + return combinedValue.split('/')[1]?.trim() || FALLBACK; + } + + return FALLBACK; +} + +export function mapUserdataToZachetCard(items: UserdataItem[]): ZachetCardData { + logZachetCardMapper('start mapping items', { items }); + + const mappedCard: ZachetCardData = { + unionCardNumber: resolveUnionCardNumber(items), + fullNameRu: getFirstValue(items, 'Личная информация', 'Полное имя') ?? FALLBACK, + fullNameEn: getFirstValue(items, 'Личная информация', 'Full name') ?? FALLBACK, + birthDate: formatDate(getFirstValue(items, 'Личная информация', 'Дата рождения')), + facultyRu: resolveFacultyRu(items), + facultyEn: resolveFacultyEn(items), + statusRu: getFirstValue(items, 'Учёба', 'Должность') ?? FALLBACK, + statusEn: '-', + photoUrl: resolvePhotoUrl(items), + contacts: DEFAULT_CONTACTS, + }; + + logZachetCardMapper('mapped card result', mappedCard); + + return mappedCard; +} + +function logZachetCardMapper(message: string, payload?: unknown) { + if (!import.meta.env.DEV) { + return; + } + + if (payload === undefined) { + console.log('[ZachetCard][mapper]', message); + return; + } + + console.log('[ZachetCard][mapper]', message, payload); +} diff --git a/src/modules/ZachetCard/controller/store.ts b/src/modules/ZachetCard/controller/store.ts new file mode 100644 index 0000000..2718489 --- /dev/null +++ b/src/modules/ZachetCard/controller/store.ts @@ -0,0 +1,91 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { UserdataApi } from '@/api/controllers/UserdataApi'; +import { mapUserdataToZachetCard } from './mapper'; +import type { ZachetCardData, UserdataResponse } from './types'; + +export const useZachetCardStore = defineStore('zachetCard', () => { + const cards = ref>({}); + const loadingByUserId = ref>({}); + const errorByUserId = ref>({}); + + async function fetchCard(userId: number, force = false): Promise { + logZachetCardStore('fetchCard called', { userId, force }); + + if (!force && cards.value[userId]) { + logZachetCardStore('return cached card', { + userId, + card: cards.value[userId], + }); + + return cards.value[userId]; + } + + loadingByUserId.value[userId] = true; + errorByUserId.value[userId] = null; + + try { + const response = await UserdataApi.getUser(userId); + const data = response?.data as UserdataResponse | undefined; + + logZachetCardStore('raw response data', data); + + const items = Array.isArray(data?.items) ? data.items : []; + const mappedCard = mapUserdataToZachetCard(items); + + cards.value[userId] = mappedCard; + + logZachetCardStore('card saved to store', { + userId, + card: mappedCard, + }); + + return mappedCard; + } catch (error) { + errorByUserId.value[userId] = 'Не удалось загрузить данные карты'; + errorZachetCardStore('fetchCard error', error); + + return null; + } finally { + loadingByUserId.value[userId] = false; + logZachetCardStore('fetchCard finished', { + userId, + loading: loadingByUserId.value[userId], + error: errorByUserId.value[userId], + }); + } + } + + return { + cards, + loadingByUserId, + errorByUserId, + fetchCard, + }; +}); + +function logZachetCardStore(message: string, payload?: unknown) { + if (!import.meta.env.DEV) { + return; + } + + if (payload === undefined) { + console.log('[ZachetCard][store]', message); + return; + } + + console.log('[ZachetCard][store]', message, payload); +} + +function errorZachetCardStore(message: string, payload?: unknown) { + if (!import.meta.env.DEV) { + return; + } + + if (payload === undefined) { + console.error('[ZachetCard][store]', message); + return; + } + + console.error('[ZachetCard][store]', message, payload); +} diff --git a/src/modules/ZachetCard/controller/types.ts b/src/modules/ZachetCard/controller/types.ts new file mode 100644 index 0000000..9cea707 --- /dev/null +++ b/src/modules/ZachetCard/controller/types.ts @@ -0,0 +1,30 @@ +export interface UserdataItem { + category: string; + param: string; + value?: string | null; +} + +export interface UserdataResponse { + items: UserdataItem[]; +} + +export interface ZachetCardContacts { + address: string; + phone: string; + email: string; + vk: string; + website: string; +} + +export interface ZachetCardData { + unionCardNumber: string; + fullNameRu: string; + fullNameEn: string; + birthDate: string; + facultyRu: string; + facultyEn: string; + statusRu: string; + statusEn: string; + photoUrl?: string; + contacts: ZachetCardContacts; +} diff --git a/src/modules/ZachetCard/controller/useZachetCardController.ts b/src/modules/ZachetCard/controller/useZachetCardController.ts new file mode 100644 index 0000000..6cdba01 --- /dev/null +++ b/src/modules/ZachetCard/controller/useZachetCardController.ts @@ -0,0 +1,157 @@ +import { computed, onMounted, ref, watch } from 'vue'; +import { useProfileStore } from '@/store/profile'; +import { AuthApi } from '@/api'; +import { useZachetCardStore } from './store'; + +interface UseZachetCardControllerProps { + userId?: number; +} + +export function useZachetCardController(props: UseZachetCardControllerProps) { + const profileStore = useProfileStore(); + const zachetCardStore = useZachetCardStore(); + + const resolvedUserId = ref(props.userId ?? profileStore.id ?? null); + + const card = computed(() => { + const userId = resolvedUserId.value; + + if (!userId) return null; + + return zachetCardStore.cards[userId] ?? null; + }); + + const loading = computed(() => { + const userId = resolvedUserId.value; + + if (!userId) return false; + + return Boolean(zachetCardStore.loadingByUserId[userId]); + }); + + const error = computed(() => { + const userId = resolvedUserId.value; + + if (!userId) return 'Не найден id пользователя'; + + return zachetCardStore.errorByUserId[userId] ?? null; + }); + + async function ensureUserId(): Promise { + if (props.userId) { + logZachetCardController('using userId from props', { userId: props.userId }); + resolvedUserId.value = props.userId; + return props.userId; + } + + if (profileStore.id) { + logZachetCardController('using userId from profileStore', { userId: profileStore.id }); + resolvedUserId.value = profileStore.id; + return profileStore.id; + } + + try { + logZachetCardController('profileStore.id is empty, requesting AuthApi.getMe'); + + const { data: me } = await AuthApi.getMe([ + 'auth_methods', + 'groups', + 'indirect_groups', + 'session_scopes', + 'user_scopes', + ]); + + if (!me?.id) { + logZachetCardController('AuthApi.getMe returned empty id', { me }); + return null; + } + + profileStore.id = me.id; + resolvedUserId.value = me.id; + + logZachetCardController('userId resolved from AuthApi.getMe', { userId: me.id }); + + return me.id; + } catch (error) { + errorZachetCardController('failed to resolve userId via AuthApi.getMe', error); + return null; + } + } + + async function load(force = false) { + logZachetCardController('load called', { + userIdFromProps: props.userId, + userIdFromStore: profileStore.id, + resolvedUserId: resolvedUserId.value, + force, + }); + + const userId = await ensureUserId(); + + if (!userId) { + logZachetCardController('load skipped because userId is empty after ensureUserId'); + return; + } + + await zachetCardStore.fetchCard(userId, force); + } + + async function reload() { + logZachetCardController('reload called'); + await load(true); + } + + onMounted(() => { + logZachetCardController('controller mounted', { + userIdFromProps: props.userId, + userIdFromStore: profileStore.id, + resolvedUserId: resolvedUserId.value, + }); + + void load(); + }); + + watch( + () => props.userId, + nextUserId => { + if (nextUserId) { + logZachetCardController('props.userId changed', { nextUserId }); + resolvedUserId.value = nextUserId; + void load(); + } + } + ); + + return { + card, + loading, + error, + reload, + }; +} + +function logZachetCardController(message: string, payload?: unknown) { + if (!import.meta.env.DEV) { + return; + } + + if (payload === undefined) { + console.log('[ZachetCard][controller]', message); + return; + } + + console.log('[ZachetCard][controller]', message, payload); +} + +function errorZachetCardController(message: string, payload?: unknown) { + if (!import.meta.env.DEV) { + return; + } + + if (payload === undefined) { + console.error('[ZachetCard][controller]', message); + return; + } + + console.error('[ZachetCard][controller]', message, payload); +} diff --git a/src/modules/ZachetCard/index.ts b/src/modules/ZachetCard/index.ts new file mode 100644 index 0000000..8a947c7 --- /dev/null +++ b/src/modules/ZachetCard/index.ts @@ -0,0 +1 @@ +export { default as ZachetCard } from './ZachetCard.vue'; diff --git a/src/modules/ZachetCard/ui/ZachetCardBack.vue b/src/modules/ZachetCard/ui/ZachetCardBack.vue new file mode 100644 index 0000000..a6ef996 --- /dev/null +++ b/src/modules/ZachetCard/ui/ZachetCardBack.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/src/modules/ZachetCard/ui/ZachetCardFront.vue b/src/modules/ZachetCard/ui/ZachetCardFront.vue new file mode 100644 index 0000000..1b0ca5c --- /dev/null +++ b/src/modules/ZachetCard/ui/ZachetCardFront.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index fbf1afc..d06c660 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -69,6 +69,17 @@ const routes: RouteRecordRaw[] = [ path: '/:pathMatch(.*)', component: () => import('@/views/error/Error404View.vue'), }, + { + path: '/debug/zachet-card', + component: () => import('@/views/debug/ZachetCardDebugView.vue'), + beforeEnter: () => { + const token = LocalStorage.get(LocalStorageItem.Token); + + if (!token) { + return { path: '/auth' }; + } + }, + }, ]; const router = createRouter({ diff --git a/src/views/debug/ZachetCardDebugView.vue b/src/views/debug/ZachetCardDebugView.vue new file mode 100644 index 0000000..0e2be5a --- /dev/null +++ b/src/views/debug/ZachetCardDebugView.vue @@ -0,0 +1,77 @@ + + + + +