Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement role page #1579

Merged
merged 15 commits into from
Dec 16, 2024
Merged
6 changes: 3 additions & 3 deletions backend/samfundet/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,17 @@ class RoleAdmin(admin.ModelAdmin):

@admin.register(UserOrgRole)
class UserOrgRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')
list_display = ('user', 'role', 'obj', 'created_at', 'created_by')


@admin.register(UserGangRole)
class UserGangRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')
list_display = ('user', 'role', 'obj', 'created_at', 'created_by')


@admin.register(UserGangSectionRole)
class UserGangSectionRoleAdmin(admin.ModelAdmin):
list_display = ('user', 'role', 'obj')
list_display = ('user', 'role', 'obj', 'created_at', 'created_by')


@admin.register(Permission)
Expand Down
107 changes: 105 additions & 2 deletions frontend/src/PagesAdmin/RoleAdminPage/RoleAdminPage.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,119 @@
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouteLoaderData } from 'react-router-dom';
import { H1, Link, Table } from '~/Components';
import { AdminPageLayout } from '~/PagesAdmin/AdminPageLayout/AdminPageLayout';
import { getRoleUsers } from '~/api';
import type { RoleDto, RoleUsersDto } from '~/dto';
import { useTitle } from '~/hooks';
import { KEY } from '~/i18n/constants';
import { reverse } from '~/named-urls';
import { roleKeys } from '~/queryKeys';
import type { RoleLoader } from '~/router/loaders';
import { ROUTES_FRONTEND } from '~/routes/frontend';
import { dbT, formatDateYMDWithTime, getFullDisplayName } from '~/utils';

function getCreatedInfo(ru: RoleUsersDto) {
const roleData = ru.org_role || ru.gang_role || ru.section_role;
return {
createdAt: roleData?.created_at ? formatDateYMDWithTime(new Date(roleData.created_at)) : '',
createdBy: roleData?.created_by ? getFullDisplayName(roleData.created_by) : '',
};
}

export function RoleAdminPage() {
const { t } = useTranslation();
const title = t(KEY.common_role);
const { role } = useRouteLoaderData('role') as RoleLoader;

const title = `${t(KEY.common_role)}: ${role?.name}`;

useTitle(title);

const { data, isLoading } = useQuery({
queryKey: roleKeys.users((role as RoleDto).id),
queryFn: () => (role ? getRoleUsers(role.id) : undefined),
enabled: !!role,
});

const usersColumns = [
t(KEY.common_name),
t(KEY.admin_role_page_orggangsection),
t(KEY.admin_role_page_role_since),
t(KEY.admin_role_page_given_by),
];

const getRoleInfo = useCallback(
(ru: RoleUsersDto) => {
if (ru.org_role) {
return {
content: (
<>
{t(KEY.recruitment_organization)}: {ru.org_role.organization.name}
</>
),
value: ru.org_role.organization.name,
};
}

if (ru.gang_role) {
return {
content: (
<Link
url={reverse({
pattern: ROUTES_FRONTEND.admin_gangs_edit,
urlParams: { gangId: ru.gang_role.gang.id },
})}
>
{t(KEY.common_gang)}: {dbT(ru.gang_role.gang, 'name')}
</Link>
),
value: dbT(ru.gang_role.gang, 'name'),
};
}

if (ru.section_role) {
return {
content: (
<>
{t(KEY.common_section)}: {dbT(ru.section_role.section, 'name')}
</>
),
value: dbT(ru.section_role.section, 'name'),
};
}

// We'll never end up here, but just in case the API changes...
return { content: '', value: '' };
},
[t],
);

const usersData = useMemo(() => {
if (!data) {
return [];
}
return data.map((ru) => {
const fullName = getFullDisplayName(ru.user);
const roleInfo = getRoleInfo(ru);
const { createdAt, createdBy } = getCreatedInfo(ru);

return {
cells: [
{ content: fullName, value: fullName },
{ content: roleInfo.content, value: roleInfo.value },
{ content: createdAt, value: createdAt },
{ content: createdBy, value: createdBy },
],
};
});
}, [data, getRoleInfo]);

return (
<AdminPageLayout title={title}>
<div />
<H1>{t(KEY.common_users)}</H1>

{isLoading ? <span>Loading</span> : <Table columns={usersColumns} data={usersData} />}
</AdminPageLayout>
);
}
8 changes: 8 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
RecruitmentUserDto,
RegistrationDto,
RoleDto,
RoleUsersDto,
SaksdokumentDto,
TextItemDto,
UserDto,
Expand Down Expand Up @@ -574,6 +575,13 @@ export async function getRecruitmentPositions(recruitmentId: string): Promise<Ax
return response;
}

export async function getRoleUsers(id: number): Promise<RoleUsersDto[]> {
const url = BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__role_users, urlParams: { pk: id } });
const response = await axios.get<RoleUsersDto[]>(url, { withCredentials: true });

return response.data;
}

export async function getRecruitmentPositionsGangForApplicant(
recruitmentId: number | string,
gangId: number | string | undefined,
Expand Down
28 changes: 20 additions & 8 deletions frontend/src/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,19 +383,31 @@ export type RoleDto = {
content_type?: string | null;
};

export type UserGangRoleDto = {
export type UserRole = {
id: number;
obj: GangDto;
user: UserDto;
created_at: Date;
obj: OrganizationDto | GangDto | GangSectionDto;
created_by?: UserDto;
};

export type UserGangSectionRoleDto = {
id: number;
obj: GangSectionDto;
export type UserGangRoleDto = Omit<UserRole, 'obj'> & {
gang: GangDto;
};

export type UserOrganizationRoleDto = {
id: number;
obj: OrganizationDto;
export type UserGangSectionRoleDto = Omit<UserRole, 'obj'> & {
section: GangSectionDto;
};

export type UserOrganizationRoleDto = Omit<UserRole, 'obj'> & {
organization: OrganizationDto;
};

export type RoleUsersDto = {
user: UserDto;
org_role?: UserOrganizationRoleDto;
gang_role?: UserGangRoleDto;
section_role?: UserGangSectionRoleDto;
};

// ############################################################
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@ export const KEY = {
admin_saksdokumenter_cannot_reupload: 'admin_saksdokumenter_cannot_reupload',
admin_impersonate: 'admin_impersonate',
admin_stop_impersonate: 'admin_stop_impersonate',
admin_role_page_orggangsection: 'admin_role_page_orggangsection',
admin_role_page_role_since: 'admin_role_page_role_since',
admin_role_page_given_by: 'admin_role_page_given_by',

// CommandMenu:
command_menu_label: 'command_menu_label',
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ export const nb = prepareTranslations({
[KEY.new_password]: 'Nytt passord',
[KEY.admin_impersonate]: 'Stjel identitet',
[KEY.admin_stop_impersonate]: 'Stopp identitetstyveri',
[KEY.admin_role_page_orggangsection]: 'Org/Gjeng/Seksjon',
[KEY.admin_role_page_role_since]: 'Hatt rollen siden',
[KEY.admin_role_page_given_by]: 'Gitt av',

// LoginPage:
[KEY.loginpage_register]: 'Lag bruker',
Expand Down Expand Up @@ -704,6 +707,9 @@ export const en = prepareTranslations({
[KEY.new_password]: 'New password',
[KEY.admin_impersonate]: 'Impersonate',
[KEY.admin_stop_impersonate]: 'Stop impersonation',
[KEY.admin_role_page_orggangsection]: 'Org/Gang/Section',
[KEY.admin_role_page_role_since]: 'Had role since',
[KEY.admin_role_page_given_by]: 'Given by',

// LoginPage:
[KEY.loginpage_register]: 'Create user',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const roleKeys = {
list: (filters: unknown[]) => [...roleKeys.lists(), { filters }] as const,
details: () => [...roleKeys.all, 'detail'] as const,
detail: (id: number) => [...roleKeys.details(), id] as const,
users: (id: number) => [...roleKeys.detail(id), 'users'] as const,
};

export const infoPageKeys = {
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ export function getFullName(u: UserDto): string {
return `${u.first_name} ${u.last_name}`.trim();
}

export function getFullDisplayName(u: UserDto): string {
const fullName = getFullName(u);
if (!fullName) {
return u.username;
}
return `${fullName} (${u.username})`;
}

/** Helper to determine if a KeyValue is truthy. */
export function isTruthy(value = ''): boolean {
const falsy = ['', 'no', 'zero', '0'];
Expand Down Expand Up @@ -253,6 +261,10 @@ export function formatDateYMD(d: Date): string {
return format(d, 'yyyy.LL.dd');
}

export function formatDateYMDWithTime(d: Date): string {
return format(d, 'yyyy.LL.dd HH:mm');
}

/**
* Generic query function for DTOs. Returns elements from array matching query.
* @param query String query to search with
Expand Down
Loading