diff --git a/.gitignore b/.gitignore index 87f87eec..6cab4738 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,4 @@ dev.* .serverless .env.json .env -**/db-migration/users.json -**/db-migration/reports.json tmp \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index a2a09ced..a2ec3b4d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,4 @@ **/graphql/index.tsx **/graphql.ts **/lib/** -**/db-migration/** .serverless diff --git a/eslint.config.js b/eslint.config.js index a57dbc97..ecdcbc57 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -14,7 +14,6 @@ module.exports = [ 'node_modules/', 'packages/frontend/src/graphql/index.tsx', 'packages/api/src/graphql.ts', - 'packages/db-migration/**', '**/lib/**', '**/dist/**', ], diff --git a/packages/api/schema.gql b/packages/api/schema.gql index b3cba742..e61731c5 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -181,6 +181,11 @@ type Mutation { """ createTrainer(input: CreateTrainerInput!): Trainer + """ + Creates Admin. + """ + createAdmin(input: CreateAdminInput!): Admin + """ Marks User to be deleted """ @@ -201,6 +206,11 @@ type Mutation { """ updateTrainer(input: UpdateTrainerInput!, id: ID!): Trainer + """ + Updates Admin. + """ + updateAdmin(input: UpdateAdminInput!, id: ID!): Admin + """ Link Alexa account """ @@ -282,6 +292,11 @@ type Query { """ trainers: [Trainer!]! + """ + Get all Admins + """ + admins: [Admin!]! + """ Print single report or report batch """ @@ -365,7 +380,6 @@ type Trainee implements UserInterface { theme: String trainer: Trainer type: UserTypeEnum! - username: String! email: String! deleteAt: String openReportsCount: Int! @@ -419,12 +433,23 @@ type Trainer implements UserInterface { theme: String trainees: [Trainee!]! type: UserTypeEnum! - username: String! email: String! deleteAt: String alexaSkillLinked: Boolean } +input CreateAdminInput { + email: String! + firstName: String! + lastName: String! +} + +input UpdateAdminInput { + email: String + firstName: String + lastName: String +} + type Admin implements UserInterface { createdAt: String! firstName: String! @@ -434,9 +459,9 @@ type Admin implements UserInterface { notification: Boolean theme: String type: UserTypeEnum! - username: String! email: String! signature: String + deleteAt: String alexaSkillLinked: Boolean } @@ -462,7 +487,6 @@ interface UserInterface { signature: String theme: String type: UserTypeEnum! - username: String! email: String! alexaSkillLinked: Boolean } diff --git a/packages/api/src/graphql.ts b/packages/api/src/graphql.ts index 01032748..9740ac09 100644 --- a/packages/api/src/graphql.ts +++ b/packages/api/src/graphql.ts @@ -23,6 +23,7 @@ export type GqlAdmin = GqlUserInterface & { __typename?: 'Admin'; alexaSkillLinked?: Maybe; createdAt: Scalars['String']['output']; + deleteAt?: Maybe; email: Scalars['String']['output']; firstName: Scalars['String']['output']; id: Scalars['ID']['output']; @@ -32,7 +33,6 @@ export type GqlAdmin = GqlUserInterface & { signature?: Maybe; theme?: Maybe; type: GqlUserTypeEnum; - username: Scalars['String']['output']; }; export type GqlComment = { @@ -56,6 +56,12 @@ export type GqlCompany = { name: Scalars['String']['output']; }; +export type GqlCreateAdminInput = { + email: Scalars['String']['input']; + firstName: Scalars['String']['input']; + lastName: Scalars['String']['input']; +}; + export type GqlCreateCommentPayload = { __typename?: 'CreateCommentPayload'; comment: GqlComment; @@ -148,6 +154,8 @@ export type GqlMutation = { _devsetusertype: GqlDevSetUserPayload; /** Claims a Trainee by the current Trainer */ claimTrainee?: Maybe; + /** Creates Admin. */ + createAdmin?: Maybe; /** Creates a new comment on a Day which is identified by the id argument. */ createCommentOnDay: GqlCreateCommentPayload; /** Creates a new comment on a Entry which is identified by the id argument. */ @@ -178,6 +186,8 @@ export type GqlMutation = { unlinkAlexa?: Maybe; /** Unmarks User from deletion */ unmarkUserForDeletion?: Maybe; + /** Updates Admin. */ + updateAdmin?: Maybe; /** Updates the current trainee */ updateCurrentTrainee?: Maybe; /** Updates the current user */ @@ -210,6 +220,11 @@ export type GqlMutationClaimTraineeArgs = { }; +export type GqlMutationCreateAdminArgs = { + input: GqlCreateAdminInput; +}; + + export type GqlMutationCreateCommentOnDayArgs = { id: Scalars['ID']['input']; text: Scalars['String']['input']; @@ -283,6 +298,12 @@ export type GqlMutationUnmarkUserForDeletionArgs = { }; +export type GqlMutationUpdateAdminArgs = { + id: Scalars['ID']['input']; + input: GqlUpdateAdminInput; +}; + + export type GqlMutationUpdateCurrentTraineeArgs = { input: GqlUpdateCurrentTraineeInput; }; @@ -345,6 +366,8 @@ export type GqlPrintPayload = { export type GqlQuery = { __typename?: 'Query'; + /** Get all Admins */ + admins: Array; /** Get the alexa account linking url */ alexaLinkingUrl?: Maybe; /** Will look for Users to delete */ @@ -455,7 +478,6 @@ export type GqlTrainee = GqlUserInterface & { theme?: Maybe; trainer?: Maybe; type: GqlUserTypeEnum; - username: Scalars['String']['output']; }; export type GqlTrainer = GqlUserInterface & { @@ -473,7 +495,6 @@ export type GqlTrainer = GqlUserInterface & { theme?: Maybe; trainees: Array; type: GqlUserTypeEnum; - username: Scalars['String']['output']; }; export type GqlTrainerTraineePayload = { @@ -482,6 +503,12 @@ export type GqlTrainerTraineePayload = { trainer: GqlTrainer; }; +export type GqlUpdateAdminInput = { + email?: InputMaybe; + firstName?: InputMaybe; + lastName?: InputMaybe; +}; + export type GqlUpdateCurrentTraineeInput = { course?: InputMaybe; }; @@ -527,7 +554,6 @@ export type GqlUserInterface = { signature?: Maybe; theme?: Maybe; type: GqlUserTypeEnum; - username: Scalars['String']['output']; }; export type GqlUserTypeEnum = @@ -615,6 +641,7 @@ export type GqlResolversTypes = ResolversObject<{ Comment: ResolverTypeWrapper; CommentableInterface: ResolverTypeWrapper; Company: ResolverTypeWrapper; + CreateAdminInput: GqlCreateAdminInput; CreateCommentPayload: ResolverTypeWrapper & { comment: GqlResolversTypes['Comment'], commentable: GqlResolversTypes['CommentableInterface'] }>; CreateTraineeInput: GqlCreateTraineeInput; CreateTrainerInput: GqlCreateTrainerInput; @@ -638,6 +665,7 @@ export type GqlResolversTypes = ResolversObject<{ Trainee: ResolverTypeWrapper; Trainer: ResolverTypeWrapper; TrainerTraineePayload: ResolverTypeWrapper & { trainee: GqlResolversTypes['Trainee'], trainer: GqlResolversTypes['Trainer'] }>; + UpdateAdminInput: GqlUpdateAdminInput; UpdateCurrentTraineeInput: GqlUpdateCurrentTraineeInput; UpdateReportPayload: ResolverTypeWrapper & { report: GqlResolversTypes['Report'], trainee: GqlResolversTypes['Trainee'] }>; UpdateTraineeInput: GqlUpdateTraineeInput; @@ -654,6 +682,7 @@ export type GqlResolversParentTypes = ResolversObject<{ Comment: Comment; CommentableInterface: CommentableInterface; Company: GqlCompany; + CreateAdminInput: GqlCreateAdminInput; CreateCommentPayload: Omit & { comment: GqlResolversParentTypes['Comment'], commentable: GqlResolversParentTypes['CommentableInterface'] }; CreateTraineeInput: GqlCreateTraineeInput; CreateTrainerInput: GqlCreateTrainerInput; @@ -675,6 +704,7 @@ export type GqlResolversParentTypes = ResolversObject<{ Trainee: Trainee; Trainer: Trainer; TrainerTraineePayload: Omit & { trainee: GqlResolversParentTypes['Trainee'], trainer: GqlResolversParentTypes['Trainer'] }; + UpdateAdminInput: GqlUpdateAdminInput; UpdateCurrentTraineeInput: GqlUpdateCurrentTraineeInput; UpdateReportPayload: Omit & { report: GqlResolversParentTypes['Report'], trainee: GqlResolversParentTypes['Trainee'] }; UpdateTraineeInput: GqlUpdateTraineeInput; @@ -686,6 +716,7 @@ export type GqlResolversParentTypes = ResolversObject<{ export type GqlAdminResolvers = ResolversObject<{ alexaSkillLinked?: Resolver, ParentType, ContextType>; createdAt?: Resolver; + deleteAt?: Resolver, ParentType, ContextType>; email?: Resolver; firstName?: Resolver; id?: Resolver; @@ -695,7 +726,6 @@ export type GqlAdminResolvers, ParentType, ContextType>; theme?: Resolver, ParentType, ContextType>; type?: Resolver; - username?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -774,6 +804,7 @@ export type GqlMutationResolvers, ParentType, ContextType, RequireFields>; _devsetusertype?: Resolver>; claimTrainee?: Resolver, ParentType, ContextType, RequireFields>; + createAdmin?: Resolver, ParentType, ContextType, RequireFields>; createCommentOnDay?: Resolver>; createCommentOnEntry?: Resolver>; createCommentOnReport?: Resolver>; @@ -789,6 +820,7 @@ export type GqlMutationResolvers, ParentType, ContextType, RequireFields>; unlinkAlexa?: Resolver, ParentType, ContextType>; unmarkUserForDeletion?: Resolver, ParentType, ContextType, RequireFields>; + updateAdmin?: Resolver, ParentType, ContextType, RequireFields>; updateCurrentTrainee?: Resolver, ParentType, ContextType, RequireFields>; updateCurrentUser?: Resolver, ParentType, ContextType, Partial>; updateDay?: Resolver, ParentType, ContextType, RequireFields>; @@ -812,6 +844,7 @@ export type GqlPrintPayloadResolvers; export type GqlQueryResolvers = ResolversObject<{ + admins?: Resolver, ParentType, ContextType>; alexaLinkingUrl?: Resolver, ParentType, ContextType>; cleanup?: Resolver; companies?: Resolver>, ParentType, ContextType>; @@ -871,7 +904,6 @@ export type GqlTraineeResolvers, ParentType, ContextType>; trainer?: Resolver, ParentType, ContextType>; type?: Resolver; - username?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -889,7 +921,6 @@ export type GqlTrainerResolvers, ParentType, ContextType>; trainees?: Resolver, ParentType, ContextType>; type?: Resolver; - username?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -918,7 +949,6 @@ export type GqlUserInterfaceResolvers, ParentType, ContextType>; theme?: Resolver, ParentType, ContextType>; type?: Resolver; - username?: Resolver; }>; export type GqlResolvers = ResolversObject<{ diff --git a/packages/api/src/models.ts b/packages/api/src/models.ts index b7c95cb5..cf7a5f3b 100644 --- a/packages/api/src/models.ts +++ b/packages/api/src/models.ts @@ -18,7 +18,7 @@ export type User = Trainee | Trainer | Admin * during the runtime in the backend. Only after beeing * transformed by GraphQL these fields are available */ -type ResolvedUserFields = 'username' | 'alexaSkillLinked' +type ResolvedUserFields = 'alexaSkillLinked' export type UserInterface = Omit & { email: string diff --git a/packages/backend/src/i18n/de.ts b/packages/backend/src/i18n/de.ts index 499f2053..52d9995a 100644 --- a/packages/backend/src/i18n/de.ts +++ b/packages/backend/src/i18n/de.ts @@ -24,6 +24,8 @@ export const GermanTranslations: Translations = { wrongUserType: 'Du besitzt den falschen Nutzer Typen um dies zu tun', reportIncomplete: 'Der Bericht ist nicht vollständig', missingPeriod: 'Die Ausbildungsperiode fehlt', + cantDeleteYourself: 'Ein Admin kann sich nicht selber für die Löschung markieren', + cantChangeOwnEmail: 'Ein Admin kann seine eigene E-Mail Adresse nicht ändern', }, email: { hello: 'Hallo', diff --git a/packages/backend/src/i18n/en.ts b/packages/backend/src/i18n/en.ts index ff2143ca..bc34b05a 100644 --- a/packages/backend/src/i18n/en.ts +++ b/packages/backend/src/i18n/en.ts @@ -24,6 +24,8 @@ export const EnglishTranslations: Translations = { wrongUserType: 'You have the wrong user type', reportIncomplete: 'Report is incomplete', missingPeriod: 'Missing period', + cantDeleteYourself: "An Admin can't mark themselves for deletion", + cantChangeOwnEmail: "An Admin can't change their own e-mail address", }, email: { hello: 'Hello', diff --git a/packages/backend/src/i18n/index.ts b/packages/backend/src/i18n/index.ts index 10c5c002..47297bd0 100644 --- a/packages/backend/src/i18n/index.ts +++ b/packages/backend/src/i18n/index.ts @@ -28,6 +28,8 @@ export type Translations = { wrongUserType: string reportIncomplete: string missingPeriod: string + cantDeleteYourself: string + cantChangeOwnEmail: string } email: EmailTranslations print: PrintTranslations diff --git a/packages/backend/src/permissions.ts b/packages/backend/src/permissions.ts index d13c4bbe..f5b2ac3f 100644 --- a/packages/backend/src/permissions.ts +++ b/packages/backend/src/permissions.ts @@ -57,6 +57,7 @@ export const permissions = shield( // Admin Queris getUser: and(authenticated, admin), trainers: and(authenticated, admin), + admins: and(authenticated, admin), }, Mutation: { _devsetusertype: and(authenticated, debug), diff --git a/packages/backend/src/resolvers/admin.resolver.ts b/packages/backend/src/resolvers/admin.resolver.ts index bee32c72..7ede0a0f 100644 --- a/packages/backend/src/resolvers/admin.resolver.ts +++ b/packages/backend/src/resolvers/admin.resolver.ts @@ -1,31 +1,31 @@ import { addMonths, isFuture, isToday, subWeeks } from 'date-fns' import { GraphQLError } from 'graphql' -import { AdminContext, GqlResolvers, Trainee, Trainer, User } from '@lara/api' +import { Admin, AdminContext, GqlResolvers, Trainee, Trainer, User } from '@lara/api' import { isAdmin, isTrainee, isTrainer } from '../permissions' import { traineeById } from '../repositories/trainee.repo' import { trainerById, allTrainers } from '../repositories/trainer.repo' +import { allAdmins } from '../repositories/admin.repo' +import { deleteAdmin, generateAdmin, validateAdmin } from '../services/admin.service' import { allUsers, saveUser, updateUser, userByEmail, userById } from '../repositories/user.repo' import { sendDeletionMail } from '../services/email.service' import { deleteTrainee, generateReports, generateTrainee, validateTrainee } from '../services/trainee.service' import { deleteTrainer, generateTrainer, validateTrainer } from '../services/trainer.service' -import { username } from '../services/user.service' import { parseISODateString } from '../utils/date' import { t } from '../i18n' export const adminResolver: GqlResolvers = { - Admin: { - username, - }, + Admin: {}, Query: { + admins: allAdmins, trainers: allTrainers, async cleanup() { const users = await allUsers() await Promise.all( users.map(async (user) => { - if (isAdmin(user) || !user.deleteAt) { + if (!user.deleteAt) { return } @@ -46,6 +46,10 @@ export const adminResolver: GqlResolvers = { if (isTrainer(user)) { await deleteTrainer(user) } + + if (isAdmin(user)) { + await deleteAdmin(user) + } }) ) @@ -54,7 +58,7 @@ export const adminResolver: GqlResolvers = { getUser: async (_parent, { id }, { currentUser }) => { const user = await userById(id) - if (!user || isAdmin(user)) { + if (!user) { throw new GraphQLError(t('errors.missingUser', currentUser.language)) } @@ -65,10 +69,14 @@ export const adminResolver: GqlResolvers = { markUserForDeletion: async (_parent, { id }, { currentUser }) => { const user = await userById(id) - if (!user || isAdmin(user)) { + if (!user) { throw new GraphQLError(t('errors.missingUser', currentUser.language)) } + if (user.id === currentUser.id) { + throw new GraphQLError(t('errors.cantDeleteYourself', currentUser.language)) + } + user.deleteAt = addMonths(new Date(), 3).toISOString() await updateUser(user, { updateKeys: ['deleteAt'] }) @@ -80,7 +88,7 @@ export const adminResolver: GqlResolvers = { unmarkUserForDeletion: async (_parent, { id }, { currentUser }) => { const user = await userById(id) - if (!user || isAdmin(user)) { + if (!user) { throw new GraphQLError(t('errors.missingUser', currentUser.language)) } @@ -165,3 +173,42 @@ export const traineeAdminResolver: GqlResolvers = { }, }, } + +export const adminAdminResolver: GqlResolvers = { + Mutation: { + createAdmin: async (_parent, { input }, { currentUser }) => { + const existingUser = await userByEmail(input.email) + + if (existingUser) { + throw new GraphQLError(t('errors.userAlreadyExists', currentUser.language)) + } + + const newAdmin = generateAdmin(input) + + return saveUser(newAdmin) + }, + updateAdmin: async (_parent, { input, id }, { currentUser }) => { + const admin = (await userById(id)) as Admin + + if (!admin) { + throw new GraphQLError(t('errors.missingUser', currentUser.language)) + } + + if (currentUser.id === admin.id && input.email !== admin.email) { + throw new GraphQLError(t('errors.cantChangeOwnEmail', currentUser.language)) + } + + const updatedAdmin: Admin = { + ...admin, + ...input, + } + + await validateAdmin(updatedAdmin) + + // we need to save the user and not update it + // because we don't know what exactly changed + // if we use update DDB would throw an error + return saveUser(updatedAdmin) + }, + }, +} diff --git a/packages/backend/src/resolvers/index.ts b/packages/backend/src/resolvers/index.ts index 79e12322..2f259f42 100644 --- a/packages/backend/src/resolvers/index.ts +++ b/packages/backend/src/resolvers/index.ts @@ -1,5 +1,5 @@ import { alexaResolver } from './alexa.resolver' -import { adminResolver, traineeAdminResolver, trainerAdminResolver } from './admin.resolver' +import { adminAdminResolver, adminResolver, traineeAdminResolver, trainerAdminResolver } from './admin.resolver' import { authResolver } from './auth.resolver' import { commentResolver } from './comment.resolver' import { configResolver } from './config.resolver' @@ -35,6 +35,7 @@ export const resolvers = [ adminResolver, traineeAdminResolver, trainerAdminResolver, + adminAdminResolver, alexaResolver, diff --git a/packages/backend/src/resolvers/trainee.resolver.ts b/packages/backend/src/resolvers/trainee.resolver.ts index c8c954a6..8054d1af 100644 --- a/packages/backend/src/resolvers/trainee.resolver.ts +++ b/packages/backend/src/resolvers/trainee.resolver.ts @@ -18,7 +18,6 @@ import { company } from '../services/company.service' import { createPrintReportData, createPrintUserData, invokePrintLambda, savePrintData } from '../services/print.service' import { reportsWithinApprenticeship } from '../services/report.service' import { endOfToolUsage, startOfToolUsage, validateTrainee } from '../services/trainee.service' -import { username } from '../services/user.service' import { filterNullish } from '../utils/array' export const traineeResolver: GqlResolvers = { @@ -30,7 +29,6 @@ export const traineeResolver: GqlResolvers = { return reportsWithinApprenticeship(model) }, - username, openReportsCount: async (model) => { return reportsWithinApprenticeship(model, ['reopened', 'todo']).then((reports) => reports.length) }, diff --git a/packages/backend/src/resolvers/trainer.resolver.ts b/packages/backend/src/resolvers/trainer.resolver.ts index 96b26a98..62648903 100644 --- a/packages/backend/src/resolvers/trainer.resolver.ts +++ b/packages/backend/src/resolvers/trainer.resolver.ts @@ -6,7 +6,6 @@ import { reportByYearAndWeek } from '../repositories/report.repo' import { allTrainees, traineeById, traineesByTrainerId } from '../repositories/trainee.repo' import { updateUser } from '../repositories/user.repo' import { alexaSkillLinked } from '../services/alexa.service' -import { username } from '../services/user.service' import { createT } from '../i18n' export const trainerResolver: GqlResolvers = { @@ -14,7 +13,6 @@ export const trainerResolver: GqlResolvers = { trainees: async (model) => { return traineesByTrainerId(model.id) }, - username, alexaSkillLinked, }, Query: { diff --git a/packages/backend/src/resolvers/user.resolver.ts b/packages/backend/src/resolvers/user.resolver.ts index 58e9a539..ec6eb04d 100644 --- a/packages/backend/src/resolvers/user.resolver.ts +++ b/packages/backend/src/resolvers/user.resolver.ts @@ -7,14 +7,12 @@ import { generateAdmin } from '../services/admin.service' import { alexaSkillLinked } from '../services/alexa.service' import { generateTrainee } from '../services/trainee.service' import { generateTrainer } from '../services/trainer.service' -import { username } from '../services/user.service' export const userResolver: GqlResolvers = { UserInterface: { __resolveType: (model) => { return model.type }, - username, alexaSkillLinked, }, Query: { diff --git a/packages/backend/src/services/admin.service.ts b/packages/backend/src/services/admin.service.ts index f562d5f9..85baa6e2 100644 --- a/packages/backend/src/services/admin.service.ts +++ b/packages/backend/src/services/admin.service.ts @@ -1,6 +1,7 @@ import { v4 } from 'uuid' import { Admin } from '@lara/api' +import { deleteUser } from '../repositories/user.repo' type GenerateAdminOptions = { firstName: string @@ -21,3 +22,23 @@ export const generateAdmin = (options: GenerateAdminOptions): Admin => { type: 'Admin', } } + +/** + * Deletes a admin and all it's references from the DB + * @param admin Admin to delete + * @returns Boolean indicating success + */ +export const deleteAdmin = async (admin: Admin): Promise => { + const deleteAdminSuccess = await deleteUser(admin) + + return deleteAdminSuccess +} + +/** + * Validates that the admin attributes are all + * correct. Throws an error if not + * @param _admin Admin to validate + */ +export const validateAdmin = async (_admin: Admin): Promise => { + // function for validations in the future +} diff --git a/packages/backend/src/services/email.service.ts b/packages/backend/src/services/email.service.ts index b2ceba47..73176268 100644 --- a/packages/backend/src/services/email.service.ts +++ b/packages/backend/src/services/email.service.ts @@ -62,7 +62,7 @@ export const sendNotificationMail = async (report: Report, sender: User): Promis } // creates Payload for admin if a user will be deleted -export const adminDeletionMailPayload = (admin: Admin, user: Trainee | Trainer): EmailPayload => { +export const adminDeletionMailPayload = (admin: Admin, user: Trainee | Trainer | Admin): EmailPayload => { const link = isTrainee(user) ? envLink(`/trainees/${user.id}`) : envLink(`/trainer/${user.id}`) return { emailType: 'deleteUser', @@ -91,7 +91,7 @@ export const trainerDeletionMailPayload = (trainer: Trainer, user: Trainee): Ema } // creates Payload for the user that will be deleted -export const userToDeleteDeletionMailPayload = (userToDelete: Trainee | Trainer): EmailPayload => { +export const userToDeleteDeletionMailPayload = (userToDelete: Trainee | Trainer | Admin): EmailPayload => { return { emailType: 'deleteAccount', userData: { @@ -108,7 +108,7 @@ export const userToDeleteDeletionMailPayload = (userToDelete: Trainee | Trainer) * If UserToDelete is a Trainee the Trainer will be notified too. * @param userToDelete */ -export const sendDeletionMail = async (userToDelete: Trainee | Trainer): Promise => { +export const sendDeletionMail = async (userToDelete: Trainee | Trainer | Admin): Promise => { const admins = await allAdmins() // notify admins diff --git a/packages/backend/src/services/user.service.ts b/packages/backend/src/services/user.service.ts index 64bd2708..e69de29b 100644 --- a/packages/backend/src/services/user.service.ts +++ b/packages/backend/src/services/user.service.ts @@ -1,9 +0,0 @@ -import { User } from '@lara/api' - -/** - * Creates the username from the first and lastname - * @param user User to generate username - * @returns username string - */ -export const username = (user: Pick): string => - `${user.firstName.slice(0, 3)}${user.lastName.slice(0, 3)}`.toLowerCase() diff --git a/packages/components/src/edit-user-content.tsx b/packages/components/src/edit-user-content.tsx index 93dad9e2..ed0ccaf3 100644 --- a/packages/components/src/edit-user-content.tsx +++ b/packages/components/src/edit-user-content.tsx @@ -11,7 +11,7 @@ const StyledContentGrid = styled.div` type EditUserContentLayoutProps = { user: JSX.Element form: JSX.Element - relatedUsers: JSX.Element + relatedUsers?: JSX.Element } export const EditUserContentLayout: React.FC = ({ user, form, relatedUsers }) => { diff --git a/packages/components/src/new-input.ts b/packages/components/src/new-input.ts index b7ec3396..e2035944 100644 --- a/packages/components/src/new-input.ts +++ b/packages/components/src/new-input.ts @@ -27,7 +27,8 @@ export const Input = styled.input.withConfig({ } &:disabled { - color: ${(props) => props.theme.lightFont}; + color: ${(props) => props.theme.placeholder}; + border-bottom: solid 2px ${(props) => props.theme.placeholder}; } &:focus { diff --git a/packages/db-migration/.env.example b/packages/db-migration/.env.example deleted file mode 100644 index db3e1926..00000000 --- a/packages/db-migration/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -DB_HOST=secret -DB_PASSWORD=secret -DYNAMODB_TABLE=secret -AWS_ACCESS_KEY_ID=secret -AWS_SECRET_ACCESS_KEY=secret diff --git a/packages/db-migration/package.json b/packages/db-migration/package.json deleted file mode 100644 index bb250af9..00000000 --- a/packages/db-migration/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@lara/db-migration", - "version": "1.0.0", - "description": "", - "keywords": [], - "license": "MIT", - "author": "", - "main": "index.js", - "scripts": { - "migrate": "node src/index.js" - }, - "dependencies": { - "aws-sdk": "^2.938.0", - "camelcase": "8.0.0", - "dotenv": "17.2.1", - "promise-mysql": "^5.0.3" - } -} diff --git a/packages/db-migration/src/index.js b/packages/db-migration/src/index.js deleted file mode 100644 index 2ddf61a2..00000000 --- a/packages/db-migration/src/index.js +++ /dev/null @@ -1,265 +0,0 @@ -require('dotenv').config() - -const mysql = require('promise-mysql') -const camelCase = require('camelcase') -const AWS = require('aws-sdk') -const fs = require('fs') - -const { DB_HOST, DB_PASSWORD, DYNAMODB_USER_TABLE, DYNAMODB_REPORT_TABLE, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, OLD_COMPANY_NAME, NEW_COMPANY_NAME, COMPANY_ABBREVIATION } = - process.env - -if (!DB_HOST) { - throw new Error('Missing DB_HOST') -} - -if (!DB_PASSWORD) { - throw new Error('Missing DB_PASSWORD') -} - -if (!DYNAMODB_USER_TABLE) { - throw new Error('Missing DYNAMODB_USER_TABLE') -} - -if (!DYNAMODB_REPORT_TABLE) { - throw new Error('Missing DYNAMODB_REPORT_TABLE') -} - -if (!AWS_ACCESS_KEY_ID) { - throw new Error('Missing AWS_ACCESS_KEY_ID') -} - -if (!AWS_SECRET_ACCESS_KEY) { - throw new Error('Missing AWS_SECRET_ACCESS_KEY') -} - -const documentClient = new AWS.DynamoDB.DocumentClient({ - region: 'eu-central-1', - accessKeyId: AWS_ACCESS_KEY_ID, - secretAccessKey: AWS_SECRET_ACCESS_KEY, -}) - -let connection - -const roughSizeOfObject = (object) => { - var objectList = [] - var stack = [object] - var bytes = 0 - - while (stack.length) { - var value = stack.pop() - - if (typeof value === 'boolean') { - bytes += 4 - } else if (typeof value === 'string') { - bytes += value.length * 2 - } else if (typeof value === 'number') { - bytes += 8 - } else if (typeof value === 'object' && objectList.indexOf(value) === -1) { - objectList.push(value) - - for (var i in value) { - stack.push(value[i]) - } - } - } - return bytes -} - -const chunkArray = (array, chunkSize) => { - let result = [] - - for (let i = 0; i < array.length; i += chunkSize) { - result = [...result, array.slice(i, i + chunkSize)] - } - - return result -} - -const saveItems = async (users, reports) => { - const userItems = users.map((user) => ({ - PutRequest: { - Item: user, - }, - })) - - const chunkedUserItems = chunkArray(userItems, 25) - - await Promise.all( - chunkedUserItems.map((chunk) => { - console.log('Saving DynamoDB Chunk:', chunk.length) - - return documentClient - .batchWrite({ - RequestItems: { - [DYNAMODB_USER_TABLE]: chunk, - }, - }) - .promise() - }) - ) - - const reportItems = reports.map((report) => ({ - PutRequest: { - Item: report, - }, - })) - - const chunkedReportItems = chunkArray(reportItems, 25) - - await Promise.all( - chunkedReportItems.map((chunk) => { - console.log('Saving DynamoDB Chunk:', chunk.length) - - return documentClient - .batchWrite({ - RequestItems: { - [DYNAMODB_REPORT_TABLE]: chunk, - }, - }) - .promise() - }) - ) -} - -const queryComments = async (commentableId, commentableType) => { - const sqlResults = await connection.query( - `SELECT * FROM comments WHERE commentable_id=${commentableId} AND commentable_type="${commentableType}"` - ) - console.log(`Migrating comments (${commentableType}) for: ${commentableId}`) - - if (!sqlResults) { - return [] - } - - return sqlResults.map((result) => ({ - id: String(result.id), - text: result.text, - createdAt: new Date(result.created_at).toISOString(), - userId: String(result.user_id), - })) -} - -const queryReports = async () => { - const sqlReports = await connection.query(`SELECT * FROM reports`) - - return Promise.all( - sqlReports.map(async (report) => { - console.log(`starting report from ${report.trainee_id}: ${report.id}`) - - const result = { - id: String(report.id), - year: report.year, - week: report.week, - summary: report.summary ?? '', - status: report.status ?? 'todo', - createdAt: new Date(report.created_at).toISOString(), - department: report.department ?? '', - days: await queryDays(report.id), - comments: await queryComments(report.id, 'Report'), - traineeId: String(report.trainee_id), - } - - console.log(`finished report from ${report.trainee_id}: ${report.id}`) - - return result - }) - ) -} - -const queryDays = async (reportId) => { - const sqlResults = await connection.query(`SELECT * FROM days WHERE report_id=${reportId}`) - console.log(`Migrating days for: ${reportId}`) - - return Promise.all( - sqlResults.map(async (result) => ({ - id: String(result.id), - date: new Date(result.date).toISOString(), - status: result.status ?? '', - createdAt: new Date(result.created_at).toISOString(), - entries: await queryEntries(result.id), - comments: await queryComments(result.id, 'Day'), - })) - ) -} - -const queryEntries = async (dayId) => { - const sqlResults = await connection.query(`SELECT * FROM entries WHERE day_id=${dayId}`) - console.log(`Migrating entries for: ${dayId}`) - - return Promise.all( - sqlResults.map(async (result, index) => ({ - id: String(result.id), - text: result.text, - time: result.time, - createdAt: new Date(result.created_at).toISOString(), - orderId: result.order_id ?? index, - comments: await queryComments(result.id, 'Entry'), - })) - ) -} - -const run = async () => { - connection = await mysql.createConnection({ - host: DB_HOST, - user: 'root', - password: DB_PASSWORD, - database: 'lara', - port: 3306, - }) - - const sqlUsers = await connection.query('SELECT * FROM users') - - const users = await Promise.all( - sqlUsers.map(async (sqlUser) => { - console.log(`migrating User: ${sqlUser.username}`) - - return { - id: String(sqlUser.id), - companyId: sqlUser.company_id ? camelCase(sqlUser.company_id) : `${COMPANY_ABBREVIATION}Germany`, - course: sqlUser.course ?? '', - createdAt: new Date(sqlUser.created_at).toISOString(), - email: sqlUser.email.replace(`${OLD_COMPANY_NAME}.com`, `${NEW_COMPANY_NAME}.com`), - startDate: sqlUser.start_date ? new Date(sqlUser.start_date).toISOString() : undefined, - endDate: sqlUser.end_date ? new Date(sqlUser.end_date).toISOString() : undefined, - firstName: sqlUser.first_name, - lastName: sqlUser.last_name, - language: sqlUser.language ?? '', - theme: sqlUser.theme ?? '', - type: sqlUser.type, - username: sqlUser.username, - trainerId: sqlUser.trainer_id ? String(sqlUser.trainer_id) : '', - notification: Boolean(sqlUser.notification), - signature: sqlUser.signature ? sqlUser.signature.toString() : '', - } - }) - ) - - const reports = await queryReports() - - const usersCount = await connection.query('SELECT COUNT(*) FROM users') - const reportsCount = await connection.query('SELECT COUNT(*) FROM reports') - const daysCount = await connection.query('SELECT COUNT(*) FROM days') - const entriesCount = await connection.query('SELECT COUNT(*) FROM entries') - const commentsCount = await connection.query('SELECT COUNT(*) FROM comments') - - console.log('Total users:', usersCount[0]['COUNT(*)']) - console.log('Total reports:', reportsCount[0]['COUNT(*)']) - console.log('Total days:', daysCount[0]['COUNT(*)']) - console.log('Total entries:', entriesCount[0]['COUNT(*)']) - console.log('Total comments:', commentsCount[0]['COUNT(*)']) - - await connection.end() - - users.forEach((user) => { - console.log(`Item size for user ${user.username}: ${roughSizeOfObject(user)}`) - }) - - fs.writeFileSync('users.json', JSON.stringify(users)) - fs.writeFileSync('reports.json', JSON.stringify(reports)) - - await saveItems(users, reports) - - console.log('Saved items to DynamoDB') -} - -run() diff --git a/packages/frontend/src/components/admin-form.tsx b/packages/frontend/src/components/admin-form.tsx new file mode 100644 index 00000000..53c87565 --- /dev/null +++ b/packages/frontend/src/components/admin-form.tsx @@ -0,0 +1,135 @@ +import React from 'react' +import { useForm } from 'react-hook-form' +import { TrainerFormLayout, Input, Text, TextProps, DefaultTheme } from '@lara/components' + +import { Trainer } from '../graphql' +import strings from '../locales/localization' +import { PrimaryButton, SecondaryButton } from './button' +import { useValidationHelper } from '../helper/validation-helper' + +interface EditAdminFormProps { + admin?: Pick + submit: (data: EditAdminFormData) => Promise + cancel?: () => void + blurSubmit: boolean + disableEmail?: boolean +} + +export interface EditAdminFormData { + firstName: string + lastName: string + email: string +} + +const inputLabelProps: TextProps = { + spacing: '1.2px', + weight: 700, + size: 'label', + uppercase: true, +} + +export const AdminForm: React.FC = ({ + admin, + submit, + blurSubmit, + cancel, + disableEmail = false, +}) => { + const { validateEmail } = useValidationHelper() + + const { + register, + handleSubmit, + formState: { errors }, + reset, + } = useForm() + + const onSubmit = handleSubmit((formdata) => { + setUpdating(true) + + submit(formdata).then(() => { + setUpdating(false) + }) + }) + + const onCancel = () => { + reset() + + if (cancel) { + cancel() + } + } + + const [updating, setUpdating] = React.useState(false) + + const getFontColor = (hasError: unknown): keyof DefaultTheme => (hasError ? 'errorRed' : 'darkFont') + + return ( +
+ + + {strings.settings.firstname} + + + + } + lastNameInput={ + <> + + {strings.settings.lastname} + + + + } + emailInput={ + <> + + {strings.settings.email} + + + + } + buttonControls={ + !blurSubmit ? ( + <> + + {strings.cancel} + + + {strings.continue} + + + ) : undefined + } + /> + + ) +} diff --git a/packages/frontend/src/components/edit-admin-content.tsx b/packages/frontend/src/components/edit-admin-content.tsx new file mode 100644 index 00000000..e0edb1a8 --- /dev/null +++ b/packages/frontend/src/components/edit-admin-content.tsx @@ -0,0 +1,51 @@ +import { GraphQLError } from 'graphql' +import React from 'react' + +import { EditUserContentLayout } from '@lara/components' + +import { Admin, useUpdateAdminMutation } from '../graphql' +import { useToastContext } from '../hooks/use-toast-context' +import strings from '../locales/localization' +import { AdminForm, EditAdminFormData } from './admin-form' +import { UserInfo } from './user-info' + +interface EditAdminProps { + admin: Pick + disableEmail?: boolean +} + +export const EditAdmin: React.FC = ({ admin, disableEmail = false }) => { + const [mutate] = useUpdateAdminMutation() + const { addToast } = useToastContext() + + const updateAdmin = async (data: EditAdminFormData) => { + await mutate({ + variables: { + id: admin.id, + input: data, + }, + }) + .then(() => { + addToast({ + icon: 'Settings', + title: strings.settings.saveSuccessTitle, + text: strings.settings.saveSuccess, + type: 'success', + }) + }) + .catch((exception: GraphQLError) => { + addToast({ + title: strings.errors.error, + text: exception.message, + type: 'error', + }) + }) + } + + return ( + } + form={} + /> + ) +} diff --git a/packages/frontend/src/components/edit-user-row.tsx b/packages/frontend/src/components/edit-user-row.tsx index 0bb595fd..0d122eea 100644 --- a/packages/frontend/src/components/edit-user-row.tsx +++ b/packages/frontend/src/components/edit-user-row.tsx @@ -3,16 +3,16 @@ import { useNavigate } from 'react-router' import { EditUserRowLayout } from '@lara/components' -import { Trainee, Trainer } from '../graphql' +import { Admin, Trainee, Trainer } from '../graphql' import strings from '../locales/localization' import { SecondaryButton } from './button' import { UserInfo } from './user-info' -type EditUser = Pick +type EditUser = Pick interface EditUserRowProps { user: EditUser - baseUrl: 'trainees' | 'trainer' + baseUrl: 'trainees' | 'trainer' | 'admins' } export const EditUserRow: React.FunctionComponent = ({ user, baseUrl }) => { diff --git a/packages/frontend/src/components/navigation.tsx b/packages/frontend/src/components/navigation.tsx index 3eba5de3..afa4c03f 100644 --- a/packages/frontend/src/components/navigation.tsx +++ b/packages/frontend/src/components/navigation.tsx @@ -100,6 +100,9 @@ const Navigation: React.FC = () => { {strings.navigation.trainer} + + {strings.navigation.admin} + {strings.navigation.settings} @@ -117,7 +120,7 @@ const Navigation: React.FC = () => { return ( <> - + diff --git a/packages/frontend/src/graphql/index.tsx b/packages/frontend/src/graphql/index.tsx index e2245117..379097f7 100644 --- a/packages/frontend/src/graphql/index.tsx +++ b/packages/frontend/src/graphql/index.tsx @@ -21,6 +21,7 @@ export type Admin = UserInterface & { __typename?: 'Admin'; alexaSkillLinked?: Maybe; createdAt: Scalars['String']['output']; + deleteAt?: Maybe; email: Scalars['String']['output']; firstName: Scalars['String']['output']; id: Scalars['ID']['output']; @@ -30,7 +31,6 @@ export type Admin = UserInterface & { signature?: Maybe; theme?: Maybe; type: UserTypeEnum; - username: Scalars['String']['output']; }; export type Comment = { @@ -54,6 +54,12 @@ export type Company = { name: Scalars['String']['output']; }; +export type CreateAdminInput = { + email: Scalars['String']['input']; + firstName: Scalars['String']['input']; + lastName: Scalars['String']['input']; +}; + export type CreateCommentPayload = { __typename?: 'CreateCommentPayload'; comment: Comment; @@ -147,6 +153,8 @@ export type Mutation = { _devsetusertype: DevSetUserPayload; /** Claims a Trainee by the current Trainer */ claimTrainee?: Maybe; + /** Creates Admin. */ + createAdmin?: Maybe; /** Creates a new comment on a Day which is identified by the id argument. */ createCommentOnDay: CreateCommentPayload; /** Creates a new comment on a Entry which is identified by the id argument. */ @@ -177,6 +185,8 @@ export type Mutation = { unlinkAlexa?: Maybe; /** Unmarks User from deletion */ unmarkUserForDeletion?: Maybe; + /** Updates Admin. */ + updateAdmin?: Maybe; /** Updates the current trainee */ updateCurrentTrainee?: Maybe; /** Updates the current user */ @@ -209,6 +219,11 @@ export type MutationClaimTraineeArgs = { }; +export type MutationCreateAdminArgs = { + input: CreateAdminInput; +}; + + export type MutationCreateCommentOnDayArgs = { id: Scalars['ID']['input']; text: Scalars['String']['input']; @@ -282,6 +297,12 @@ export type MutationUnmarkUserForDeletionArgs = { }; +export type MutationUpdateAdminArgs = { + id: Scalars['ID']['input']; + input: UpdateAdminInput; +}; + + export type MutationUpdateCurrentTraineeArgs = { input: UpdateCurrentTraineeInput; }; @@ -344,6 +365,8 @@ export type PrintPayload = { export type Query = { __typename?: 'Query'; + /** Get all Admins */ + admins: Array; /** Get the alexa account linking url */ alexaLinkingUrl?: Maybe; /** Will look for Users to delete */ @@ -455,7 +478,6 @@ export type Trainee = UserInterface & { theme?: Maybe; trainer?: Maybe; type: UserTypeEnum; - username: Scalars['String']['output']; }; export type Trainer = UserInterface & { @@ -473,7 +495,6 @@ export type Trainer = UserInterface & { theme?: Maybe; trainees: Array; type: UserTypeEnum; - username: Scalars['String']['output']; }; export type TrainerTraineePayload = { @@ -482,6 +503,12 @@ export type TrainerTraineePayload = { trainer: Trainer; }; +export type UpdateAdminInput = { + email?: InputMaybe; + firstName?: InputMaybe; + lastName?: InputMaybe; +}; + export type UpdateCurrentTraineeInput = { course?: InputMaybe; }; @@ -527,7 +554,6 @@ export type UserInterface = { signature?: Maybe; theme?: Maybe; type: UserTypeEnum; - username: Scalars['String']['output']; }; export enum UserTypeEnum { @@ -562,6 +588,13 @@ export type ClaimTraineeMutationVariables = Exact<{ export type ClaimTraineeMutation = { __typename?: 'Mutation', claimTrainee?: { __typename?: 'TrainerTraineePayload', trainee: { __typename?: 'Trainee', id: string, trainer?: { __typename?: 'Trainer', id: string } | undefined }, trainer: { __typename?: 'Trainer', id: string, trainees: Array<{ __typename?: 'Trainee', id: string }> } } | undefined }; +export type CreateAdminMutationVariables = Exact<{ + input: CreateAdminInput; +}>; + + +export type CreateAdminMutation = { __typename?: 'Mutation', createAdmin?: { __typename?: 'Admin', id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum } | undefined }; + export type CreateCommentOnDayMutationVariables = Exact<{ id: Scalars['ID']['input']; text: Scalars['String']['input']; @@ -665,7 +698,7 @@ export type MarkUserForDeleteMutationVariables = Exact<{ }>; -export type MarkUserForDeleteMutation = { __typename?: 'Mutation', markUserForDeletion?: { __typename?: 'Admin', id: string } | { __typename?: 'Trainee', deleteAt?: string | undefined, id: string } | { __typename?: 'Trainer', deleteAt?: string | undefined, id: string } | undefined }; +export type MarkUserForDeleteMutation = { __typename?: 'Mutation', markUserForDeletion?: { __typename?: 'Admin', deleteAt?: string | undefined, id: string } | { __typename?: 'Trainee', deleteAt?: string | undefined, id: string } | { __typename?: 'Trainer', deleteAt?: string | undefined, id: string } | undefined }; export type SignatureSettingsUpdateSignatureMutationVariables = Exact<{ signature?: InputMaybe; @@ -698,7 +731,15 @@ export type UnmarkUserForDeleteMutationVariables = Exact<{ }>; -export type UnmarkUserForDeleteMutation = { __typename?: 'Mutation', unmarkUserForDeletion?: { __typename?: 'Admin', id: string } | { __typename?: 'Trainee', deleteAt?: string | undefined, id: string } | { __typename?: 'Trainer', deleteAt?: string | undefined, id: string } | undefined }; +export type UnmarkUserForDeleteMutation = { __typename?: 'Mutation', unmarkUserForDeletion?: { __typename?: 'Admin', deleteAt?: string | undefined, id: string } | { __typename?: 'Trainee', deleteAt?: string | undefined, id: string } | { __typename?: 'Trainer', deleteAt?: string | undefined, id: string } | undefined }; + +export type UpdateAdminMutationVariables = Exact<{ + input: UpdateAdminInput; + id: Scalars['ID']['input']; +}>; + + +export type UpdateAdminMutation = { __typename?: 'Mutation', updateAdmin?: { __typename?: 'Admin', id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum } | undefined }; export type UpdateEntryOrderMutationVariables = Exact<{ entryId: Scalars['ID']['input']; @@ -753,6 +794,11 @@ export type UpdateTrainerMutationVariables = Exact<{ export type UpdateTrainerMutation = { __typename?: 'Mutation', updateTrainer?: { __typename?: 'Trainer', id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum } | undefined }; +export type AdminAdminsPageQueryVariables = Exact<{ [key: string]: never; }>; + + +export type AdminAdminsPageQuery = { __typename?: 'Query', admins: Array<{ __typename?: 'Admin', id: string, firstName: string, lastName: string, deleteAt?: string | undefined }> }; + export type AdminTraineesPageQueryVariables = Exact<{ [key: string]: never; }>; @@ -809,14 +855,14 @@ export type DayInputDataQuery = { __typename?: 'Query', currentUser?: { __typena export type EntryInputDataQueryVariables = Exact<{ [key: string]: never; }>; -export type EntryInputDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, type: UserTypeEnum, firstName: string, lastName: string, username: string } | { __typename?: 'Trainee', id: string, type: UserTypeEnum, firstName: string, lastName: string, username: string } | { __typename?: 'Trainer', id: string, type: UserTypeEnum, firstName: string, lastName: string, username: string } | undefined }; +export type EntryInputDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, type: UserTypeEnum, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, type: UserTypeEnum, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, type: UserTypeEnum, firstName: string, lastName: string } | undefined }; export type UserPageQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type UserPageQuery = { __typename?: 'Query', getUser?: { __typename?: 'Admin', id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum } | { __typename?: 'Trainee', startDate?: string | undefined, startOfToolUsage?: string | undefined, endDate?: string | undefined, deleteAt?: string | undefined, course?: string | undefined, id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum, company: { __typename?: 'Company', id: string }, trainer?: { __typename?: 'Trainer', id: string, firstName: string, lastName: string } | undefined } | { __typename?: 'Trainer', deleteAt?: string | undefined, id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum, trainees: Array<{ __typename?: 'Trainee', id: string, firstName: string, lastName: string }> } | undefined, companies?: Array<{ __typename?: 'Company', id: string, name: string }> | undefined }; +export type UserPageQuery = { __typename?: 'Query', getUser?: { __typename?: 'Admin', deleteAt?: string | undefined, id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum } | { __typename?: 'Trainee', startDate?: string | undefined, startOfToolUsage?: string | undefined, endDate?: string | undefined, deleteAt?: string | undefined, course?: string | undefined, id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum, company: { __typename?: 'Company', id: string }, trainer?: { __typename?: 'Trainer', id: string, firstName: string, lastName: string } | undefined } | { __typename?: 'Trainer', deleteAt?: string | undefined, id: string, firstName: string, lastName: string, email: string, type: UserTypeEnum, trainees: Array<{ __typename?: 'Trainee', id: string, firstName: string, lastName: string }> } | undefined, companies?: Array<{ __typename?: 'Company', id: string, name: string }> | undefined, currentUser?: { __typename?: 'Admin', id: string } | { __typename?: 'Trainee', id: string } | { __typename?: 'Trainer', id: string } | undefined }; export type NavigationDataQueryVariables = Exact<{ [key: string]: never; }>; @@ -841,7 +887,7 @@ export type ReportPageDataQueryVariables = Exact<{ }>; -export type ReportPageDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, firstName: string, lastName: string, username: string } | { __typename?: 'Trainee', startOfToolUsage?: string | undefined, endOfToolUsage?: string | undefined, id: string, firstName: string, lastName: string, username: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string, username: string } | undefined, reportForYearAndWeek?: { __typename?: 'Report', id: string, week: number, year: number, summary?: string | undefined, department?: string | undefined, status: ReportStatus, previousReportLink?: string | undefined, nextReportLink?: string | undefined, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }>, days: Array<{ __typename?: 'Day', status?: DayStatusEnum | undefined, date: string, id: string, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }>, entries: Array<{ __typename?: 'Entry', id: string, text: string, time: number, orderId: number, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }> }> }> } | undefined }; +export type ReportPageDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', startOfToolUsage?: string | undefined, endOfToolUsage?: string | undefined, id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } | undefined, reportForYearAndWeek?: { __typename?: 'Report', id: string, week: number, year: number, summary?: string | undefined, department?: string | undefined, status: ReportStatus, previousReportLink?: string | undefined, nextReportLink?: string | undefined, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }>, days: Array<{ __typename?: 'Day', status?: DayStatusEnum | undefined, date: string, id: string, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }>, entries: Array<{ __typename?: 'Entry', id: string, text: string, time: number, orderId: number, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }> }> }> } | undefined }; export type ReportReviewPageDataQueryVariables = Exact<{ year: Scalars['Int']['input']; @@ -850,7 +896,7 @@ export type ReportReviewPageDataQueryVariables = Exact<{ }>; -export type ReportReviewPageDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, firstName: string, lastName: string, username: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string, username: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string, username: string } | undefined, reportForTrainee?: { __typename?: 'Report', id: string, week: number, year: number, summary?: string | undefined, department?: string | undefined, status: ReportStatus, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }>, days: Array<{ __typename?: 'Day', status?: DayStatusEnum | undefined, date: string, id: string, entries: Array<{ __typename?: 'Entry', id: string, text: string, time: number, orderId: number, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }> }>, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }> }> } | undefined }; +export type ReportReviewPageDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } | undefined, reportForTrainee?: { __typename?: 'Report', id: string, week: number, year: number, summary?: string | undefined, department?: string | undefined, status: ReportStatus, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }>, days: Array<{ __typename?: 'Day', status?: DayStatusEnum | undefined, date: string, id: string, entries: Array<{ __typename?: 'Entry', id: string, text: string, time: number, orderId: number, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }> }>, comments: Array<{ __typename?: 'Comment', id: string, text?: string | undefined, user: { __typename?: 'Admin', id: string, firstName: string, lastName: string } | { __typename?: 'Trainee', id: string, firstName: string, lastName: string } | { __typename?: 'Trainer', id: string, firstName: string, lastName: string } }> }> } | undefined }; export type SettingsPageDataQueryVariables = Exact<{ [key: string]: never; }>; @@ -870,7 +916,7 @@ export type SuggestionsDataQuery = { __typename?: 'Query', suggestions: Array<{ export type TraineePageDataQueryVariables = Exact<{ [key: string]: never; }>; -export type TraineePageDataQuery = { __typename?: 'Query', trainees: Array<{ __typename?: 'Trainee', id: string, username: string, firstName: string, lastName: string, course?: string | undefined, startDate?: string | undefined, trainer?: { __typename?: 'Trainer', id: string, firstName: string, lastName: string } | undefined, company: { __typename?: 'Company', id: string, name: string } }>, currentUser?: { __typename?: 'Admin', id: string } | { __typename?: 'Trainee', id: string } | { __typename?: 'Trainer', id: string } | undefined }; +export type TraineePageDataQuery = { __typename?: 'Query', trainees: Array<{ __typename?: 'Trainee', id: string, firstName: string, lastName: string, course?: string | undefined, startDate?: string | undefined, trainer?: { __typename?: 'Trainer', id: string, firstName: string, lastName: string } | undefined, company: { __typename?: 'Company', id: string, name: string } }>, currentUser?: { __typename?: 'Admin', id: string } | { __typename?: 'Trainee', id: string } | { __typename?: 'Trainer', id: string } | undefined }; export type TraineeSettingsDataQueryVariables = Exact<{ [key: string]: never; }>; @@ -880,7 +926,7 @@ export type TraineeSettingsDataQuery = { __typename?: 'Query', currentUser?: { _ export type TrainerReportsPageDataQueryVariables = Exact<{ [key: string]: never; }>; -export type TrainerReportsPageDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, theme?: string | undefined } | { __typename?: 'Trainee', id: string, theme?: string | undefined } | { __typename?: 'Trainer', id: string, theme?: string | undefined, trainees: Array<{ __typename?: 'Trainee', username: string, firstName: string, lastName: string, id: string, openReportsCount: number, reports: Array<{ __typename?: 'Report', id: string, status: ReportStatus, week: number, year: number, days: Array<{ __typename?: 'Day', status?: DayStatusEnum | undefined }> }> }> } | undefined }; +export type TrainerReportsPageDataQuery = { __typename?: 'Query', currentUser?: { __typename?: 'Admin', id: string, theme?: string | undefined } | { __typename?: 'Trainee', id: string, theme?: string | undefined } | { __typename?: 'Trainer', id: string, theme?: string | undefined, trainees: Array<{ __typename?: 'Trainee', firstName: string, lastName: string, id: string, openReportsCount: number, reports: Array<{ __typename?: 'Report', id: string, status: ReportStatus, week: number, year: number, days: Array<{ __typename?: 'Day', status?: DayStatusEnum | undefined }> }> }> } | undefined }; export type TrainersPageQueryVariables = Exact<{ [key: string]: never; }>; @@ -939,6 +985,22 @@ export function useClaimTraineeMutation(baseOptions?: Apollo.MutationHookOptions return Apollo.useMutation(ClaimTraineeDocument, options); } export type ClaimTraineeMutationHookResult = ReturnType; +export const CreateAdminDocument = gql` + mutation CreateAdmin($input: CreateAdminInput!) { + createAdmin(input: $input) { + id + firstName + lastName + email + type + } +} + `; +export function useCreateAdminMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateAdminDocument, options); + } +export type CreateAdminMutationHookResult = ReturnType; export const CreateCommentOnDayDocument = gql` mutation createCommentOnDay($id: ID!, $text: String!, $traineeId: ID!) { createCommentOnDay(id: $id, text: $text, traineeId: $traineeId) { @@ -1186,6 +1248,9 @@ export const MarkUserForDeleteDocument = gql` ... on Trainer { deleteAt } + ... on Admin { + deleteAt + } } } `; @@ -1266,6 +1331,9 @@ export const UnmarkUserForDeleteDocument = gql` ... on Trainer { deleteAt } + ... on Admin { + deleteAt + } } } `; @@ -1274,6 +1342,22 @@ export function useUnmarkUserForDeleteMutation(baseOptions?: Apollo.MutationHook return Apollo.useMutation(UnmarkUserForDeleteDocument, options); } export type UnmarkUserForDeleteMutationHookResult = ReturnType; +export const UpdateAdminDocument = gql` + mutation UpdateAdmin($input: UpdateAdminInput!, $id: ID!) { + updateAdmin(input: $input, id: $id) { + id + firstName + lastName + email + type + } +} + `; +export function useUpdateAdminMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateAdminDocument, options); + } +export type UpdateAdminMutationHookResult = ReturnType; export const UpdateEntryOrderDocument = gql` mutation updateEntryOrder($entryId: ID!, $dayId: ID!, $orderId: Int!) { updateEntryOrder(entryId: $entryId, dayId: $dayId, orderId: $orderId) { @@ -1407,6 +1491,31 @@ export function useUpdateTrainerMutation(baseOptions?: Apollo.MutationHookOption return Apollo.useMutation(UpdateTrainerDocument, options); } export type UpdateTrainerMutationHookResult = ReturnType; +export const AdminAdminsPageDocument = gql` + query AdminAdminsPage { + admins { + id + firstName + lastName + deleteAt + } +} + `; +export function useAdminAdminsPageQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(AdminAdminsPageDocument, options); + } +export function useAdminAdminsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(AdminAdminsPageDocument, options); + } +export function useAdminAdminsPageSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(AdminAdminsPageDocument, options); + } +export type AdminAdminsPageQueryHookResult = ReturnType; +export type AdminAdminsPageLazyQueryHookResult = ReturnType; +export type AdminAdminsPageSuspenseQueryHookResult = ReturnType; export const AdminTraineesPageDocument = gql` query AdminTraineesPage { trainees { @@ -1738,7 +1847,6 @@ export const EntryInputDataDocument = gql` type firstName lastName - username } } `; @@ -1788,11 +1896,17 @@ export const UserPageDocument = gql` lastName } } + ... on Admin { + deleteAt + } } companies { id name } + currentUser { + id + } } `; export function useUserPageQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: UserPageQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { @@ -1891,7 +2005,6 @@ export const ReportPageDataDocument = gql` id firstName lastName - username } reportForYearAndWeek(year: $year, week: $week) { id @@ -1964,7 +2077,6 @@ export const ReportReviewPageDataDocument = gql` id firstName lastName - username } reportForTrainee(year: $year, week: $week, id: $trainee) { id @@ -2119,7 +2231,6 @@ export const TraineePageDataDocument = gql` query TraineePageData { trainees { id - username firstName lastName course @@ -2199,7 +2310,6 @@ export const TrainerReportsPageDataDocument = gql` theme ... on Trainer { trainees { - username firstName lastName id diff --git a/packages/frontend/src/graphql/mutations/create-admin.gql b/packages/frontend/src/graphql/mutations/create-admin.gql new file mode 100644 index 00000000..e1a1fd16 --- /dev/null +++ b/packages/frontend/src/graphql/mutations/create-admin.gql @@ -0,0 +1,9 @@ +mutation CreateAdmin($input: CreateAdminInput!) { + createAdmin(input: $input) { + id + firstName + lastName + email + type + } +} diff --git a/packages/frontend/src/graphql/mutations/mark-delete.gql b/packages/frontend/src/graphql/mutations/mark-delete.gql index 7d683aa2..255eaa6f 100644 --- a/packages/frontend/src/graphql/mutations/mark-delete.gql +++ b/packages/frontend/src/graphql/mutations/mark-delete.gql @@ -9,5 +9,9 @@ mutation MarkUserForDelete($id: ID!) { ... on Trainer { deleteAt } + + ... on Admin { + deleteAt + } } } diff --git a/packages/frontend/src/graphql/mutations/unmark-delete.gql b/packages/frontend/src/graphql/mutations/unmark-delete.gql index f354e3da..a81f76e9 100644 --- a/packages/frontend/src/graphql/mutations/unmark-delete.gql +++ b/packages/frontend/src/graphql/mutations/unmark-delete.gql @@ -9,5 +9,9 @@ mutation UnmarkUserForDelete($id: ID!) { ... on Trainer { deleteAt } + + ... on Admin { + deleteAt + } } } diff --git a/packages/frontend/src/graphql/mutations/update-admin.gql b/packages/frontend/src/graphql/mutations/update-admin.gql new file mode 100644 index 00000000..04112d60 --- /dev/null +++ b/packages/frontend/src/graphql/mutations/update-admin.gql @@ -0,0 +1,9 @@ +mutation UpdateAdmin($input: UpdateAdminInput!, $id: ID!) { + updateAdmin(input: $input, id: $id) { + id + firstName + lastName + email + type + } +} diff --git a/packages/frontend/src/graphql/queries/admin-admins-page.gql b/packages/frontend/src/graphql/queries/admin-admins-page.gql new file mode 100644 index 00000000..3d6e2e5e --- /dev/null +++ b/packages/frontend/src/graphql/queries/admin-admins-page.gql @@ -0,0 +1,8 @@ +query AdminAdminsPage { + admins { + id + firstName + lastName + deleteAt + } +} diff --git a/packages/frontend/src/graphql/queries/entry-input-data.gql b/packages/frontend/src/graphql/queries/entry-input-data.gql index ba46ad43..ef712865 100644 --- a/packages/frontend/src/graphql/queries/entry-input-data.gql +++ b/packages/frontend/src/graphql/queries/entry-input-data.gql @@ -4,6 +4,5 @@ query EntryInputData { type firstName lastName - username } } diff --git a/packages/frontend/src/graphql/queries/get-user.gql b/packages/frontend/src/graphql/queries/get-user.gql index 507e473f..9c0fc4a1 100644 --- a/packages/frontend/src/graphql/queries/get-user.gql +++ b/packages/frontend/src/graphql/queries/get-user.gql @@ -30,9 +30,16 @@ query UserPage($id: ID!) { lastName } } + + ... on Admin { + deleteAt + } } companies { id name } + currentUser { + id + } } diff --git a/packages/frontend/src/graphql/queries/report-page-data.gql b/packages/frontend/src/graphql/queries/report-page-data.gql index 187eee29..0c195b1c 100644 --- a/packages/frontend/src/graphql/queries/report-page-data.gql +++ b/packages/frontend/src/graphql/queries/report-page-data.gql @@ -7,7 +7,6 @@ query ReportPageData($year: Int!, $week: Int!) { id firstName lastName - username } reportForYearAndWeek(year: $year, week: $week) { id diff --git a/packages/frontend/src/graphql/queries/report-review-page-data.gql b/packages/frontend/src/graphql/queries/report-review-page-data.gql index 4eec9429..5a263f4f 100644 --- a/packages/frontend/src/graphql/queries/report-review-page-data.gql +++ b/packages/frontend/src/graphql/queries/report-review-page-data.gql @@ -3,7 +3,6 @@ query reportReviewPageData($year: Int!, $week: Int!, $trainee: ID!) { id firstName lastName - username } reportForTrainee(year: $year, week: $week, id: $trainee) { id diff --git a/packages/frontend/src/graphql/queries/trainee-page-data.gql b/packages/frontend/src/graphql/queries/trainee-page-data.gql index 925850d9..ad88c855 100644 --- a/packages/frontend/src/graphql/queries/trainee-page-data.gql +++ b/packages/frontend/src/graphql/queries/trainee-page-data.gql @@ -1,7 +1,6 @@ query TraineePageData { trainees { id - username firstName lastName course diff --git a/packages/frontend/src/graphql/queries/trainer-reports-page-data.gql b/packages/frontend/src/graphql/queries/trainer-reports-page-data.gql index e0b46fd9..0b641a4f 100644 --- a/packages/frontend/src/graphql/queries/trainer-reports-page-data.gql +++ b/packages/frontend/src/graphql/queries/trainer-reports-page-data.gql @@ -4,7 +4,6 @@ query TrainerReportsPageData { theme ... on Trainer { trainees { - username firstName lastName id diff --git a/packages/frontend/src/locales/de.ts b/packages/frontend/src/locales/de.ts index 3a8d2c80..f23cf213 100644 --- a/packages/frontend/src/locales/de.ts +++ b/packages/frontend/src/locales/de.ts @@ -326,6 +326,7 @@ const germanTranslation: Translation = { reports: 'Berichte', trainees: 'Auszubildende', trainer: 'Ausbilder', + admin: 'Admins', dashhboard: 'Dashboard', archive: 'Archiv', settings: 'Einstellungen', @@ -402,6 +403,12 @@ const germanTranslation: Translation = { 'Trage hier die Daten des neuen Ausbilders / der neuen Ausbilderin ein, damit er/sie sich anmelden kann. Die Daten können später noch überarbeitet werden.', success: 'Der/Die Ausbilder:in {0} wurde erfolgreich angelegt und kann den Account jetzt nutzen.', }, + createAdmin: { + title: 'Neuer Admin', + description: + 'Trage hier die Daten des neuen Admins ein, damit er/sie sich anmelden kann. Die Daten können später noch überarbeitet werden.', + success: 'Admin {0} wurde erfolgreich angelegt und kann den Account jetzt nutzen.', + }, deleteTrainer: { title: '{0} wirklich löschen?', description: diff --git a/packages/frontend/src/locales/en.ts b/packages/frontend/src/locales/en.ts index a3cebd52..ae9b54bc 100644 --- a/packages/frontend/src/locales/en.ts +++ b/packages/frontend/src/locales/en.ts @@ -321,6 +321,7 @@ const englishTranslation: Translation = { reports: 'Reports', trainees: 'Trainees', trainer: 'Trainer', + admin: 'Admins', dashhboard: 'Dashboard', archive: 'Archive', settings: 'Settings', @@ -397,6 +398,12 @@ const englishTranslation: Translation = { 'Please enter the informations of the new Trainer here so she/he can login. You can still change the data later.', success: 'The trainer {0} has been created', }, + createAdmin: { + title: 'New Admin', + description: + 'Please enter the informations of the new Admin here so she/he can login. You can still change the data later.', + success: 'The admin {0} has been created', + }, deleteTrainer: { title: 'Delete {0}?', description: diff --git a/packages/frontend/src/locales/translation.ts b/packages/frontend/src/locales/translation.ts index 448395eb..eab8129c 100644 --- a/packages/frontend/src/locales/translation.ts +++ b/packages/frontend/src/locales/translation.ts @@ -311,6 +311,7 @@ export default interface Translation { reports: string trainees: string trainer: string + admin: string dashhboard: string archive: string settings: string @@ -376,6 +377,11 @@ export default interface Translation { description: string success: string } + createAdmin: { + title: string + description: string + success: string + } deleteTrainer: { title: string description: string diff --git a/packages/frontend/src/pages/admin-admins-page.tsx b/packages/frontend/src/pages/admin-admins-page.tsx new file mode 100644 index 00000000..c9ff86f7 --- /dev/null +++ b/packages/frontend/src/pages/admin-admins-page.tsx @@ -0,0 +1,84 @@ +import React from 'react' + +import { AdminCreateUserLayout, AdminOverviewLayout, H1, Paragraph } from '@lara/components' + +import { EditUserRow } from '../components/edit-user-row' +import Loader from '../components/loader' + +import { useAdminAdminsPageQuery, useCreateAdminMutation } from '../graphql' +import strings from '../locales/localization' +import { Template } from '../templates/template' +import { Fab } from '../components/fab' +import Modal from '../components/modal' +import { AdminForm, EditAdminFormData } from '../components/admin-form' +import { useToastContext } from '../hooks/use-toast-context' +import { GraphQLError } from 'graphql' + +export const AdminAdminsPage: React.FC = () => { + const { loading, data } = useAdminAdminsPageQuery() + const [mutate] = useCreateAdminMutation() + + const { addToast } = useToastContext() + + const [showModal, setShowModal] = React.useState(false) + + const createAdmin = async (data: EditAdminFormData) => { + await mutate({ + variables: { input: data }, + updateQueries: { + AdminAdminsPage: (prevData, { mutationResult }) => { + return { + ...prevData, + admins: [...prevData.admins, mutationResult.data?.createAdmin], + } + }, + }, + }) + .then(() => { + addToast({ + icon: 'PersonNew', + title: strings.createAdmin.title, + text: strings.formatString(strings.createAdmin.success, `${data?.firstName} ${data?.lastName}`).toString(), + type: 'success', + }) + + setShowModal(false) + }) + .catch((exception: GraphQLError) => { + addToast({ + title: strings.errors.error, + text: exception.message, + type: 'error', + }) + }) + } + + return ( + + ) +} diff --git a/packages/frontend/src/pages/admin-edit-user-page.tsx b/packages/frontend/src/pages/admin-edit-user-page.tsx index 8ea0fb90..11a43a9d 100644 --- a/packages/frontend/src/pages/admin-edit-user-page.tsx +++ b/packages/frontend/src/pages/admin-edit-user-page.tsx @@ -13,6 +13,8 @@ import strings from '../locales/localization' import { Template } from '../templates/template' import Modal from '../components/modal' import { useToastContext } from '../hooks/use-toast-context' +import { EditAdmin } from '../components/edit-admin-content' +import { GraphQLError } from 'graphql' type AdminEditUserPageParams = { id: string @@ -33,8 +35,11 @@ export const AdminEditUserPage: React.FunctionComponent = () => { const toggleDeletionModal = () => { setShowDeletionModal(!showDeletionModal) } + const currentUser = data?.currentUser + if (!currentUser) return null const renderDeleteAction = (deleteAt?: string) => { + if (currentUser.id === id) return <> if (deleteAt) { return ( unmarkDelete(vars)}> @@ -88,6 +93,23 @@ export const AdminEditUserPage: React.FunctionComponent = () => { /> )} + {/* Edit Admin page */} + {!loading && data?.getUser?.__typename === 'Admin' && ( + + } + content={} + actions={renderDeleteAction(data.getUser.deleteAt)} + /> + )} + {!loading && (

@@ -114,15 +136,23 @@ export const AdminEditUserPage: React.FunctionComponent = () => { { - markForDelete(vars).then(() => { - toggleDeletionModal() - addToast({ - icon: 'PersonAttention', - title: strings.userDelete.title, - text: strings.userDelete.description, - type: 'error', + markForDelete(vars) + .then(() => { + toggleDeletionModal() + addToast({ + icon: 'PersonAttention', + title: strings.userDelete.title, + text: strings.userDelete.description, + type: 'error', + }) + }) + .catch((exception: GraphQLError) => { + addToast({ + title: strings.errors.error, + text: exception.message, + type: 'error', + }) }) - }) }} > {strings.deactivate} diff --git a/packages/frontend/src/routes.tsx b/packages/frontend/src/routes.tsx index 4883c086..a63e6bd6 100644 --- a/packages/frontend/src/routes.tsx +++ b/packages/frontend/src/routes.tsx @@ -25,6 +25,7 @@ import TraineePage from './pages/trainee-page' import TrainerReportsPage from './pages/trainer-reports-page' import AzubiWikiPage from './pages/azubi-wiki-page' import { isWikiFeatureEnabled } from './helper/wiki-helper' +import { AdminAdminsPage } from './pages/admin-admins-page' type RoutesProps = { currentUser?: @@ -99,6 +100,8 @@ const AppRoutes: React.FunctionComponent = ({ currentUser }) => { } /> } /> } /> + } /> + } /> )} diff --git a/yarn.lock b/yarn.lock index 5530564c..45683fb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6157,11 +6157,6 @@ dependencies: "@babel/types" "^7.28.2" -"@types/bluebird@^3.5.26": - version "3.5.42" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.42.tgz#7ec05f1ce9986d920313c1377a5662b1b563d366" - integrity sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A== - "@types/body-parser@*": version "1.19.6" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474" @@ -6441,13 +6436,6 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/mysql@^2.15.2": - version "2.15.27" - resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.27.tgz#fb13b0e8614d39d42f40f381217ec3215915f1e9" - integrity sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA== - dependencies: - "@types/node" "*" - "@types/node-forge@^1.3.0": version "1.3.14" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" @@ -7693,7 +7681,7 @@ aws-dynamodb-local@^0.0.11: dependencies: tar "^6.2.0" -aws-sdk@^2.1404.0, aws-sdk@^2.346.0, aws-sdk@^2.938.0: +aws-sdk@^2.1404.0, aws-sdk@^2.346.0: version "2.1692.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1692.0.tgz#9dac5f7bfcc5ab45825cc8591b12753aa7d2902c" integrity sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw== @@ -7908,11 +7896,6 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bignumber.js@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" - integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== - binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" @@ -7935,7 +7918,7 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -bluebird@^3.5.1, bluebird@^3.5.4, bluebird@^3.7.2: +bluebird@^3.5.4, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -8291,11 +8274,6 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" -camelcase@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-8.0.0.tgz#c0d36d418753fb6ad9c5e0437579745c1c14a534" - integrity sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA== - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -14751,16 +14729,6 @@ mute-stream@^1.0.0, mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== -mysql@^2.18.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" - integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== - dependencies: - bignumber.js "9.0.0" - readable-stream "2.3.7" - safe-buffer "5.1.2" - sqlstring "2.3.1" - nanoclone@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" @@ -16066,16 +16034,6 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== -promise-mysql@^5.0.3: - version "5.2.0" - resolved "https://registry.yarnpkg.com/promise-mysql/-/promise-mysql-5.2.0.tgz#c16ef92dd37ecc4118128f0ae3fd4c99cee44fce" - integrity sha512-IKkBe7OukgCpy5U5EZPlgH6BRvnngmP+HwD6PoMNzvGXBYVZkiJ5nx6SY7bo+sgwXsMOVE7zQf6CfS9qaFs2pw== - dependencies: - "@types/bluebird" "^3.5.26" - "@types/mysql" "^2.15.2" - bluebird "^3.5.1" - mysql "^2.18.1" - promise-queue@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/promise-queue/-/promise-queue-2.2.5.tgz#2f6f5f7c0f6d08109e967659c79b88a9ed5e93b4" @@ -16398,19 +16356,6 @@ read@^3.0.1: dependencies: mute-stream "^1.0.0" -readable-stream@2.3.7: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" @@ -16775,16 +16720,16 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-push-apply@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" @@ -17572,11 +17517,6 @@ sprintf-kit@^2.0.1, sprintf-kit@^2.0.2: dependencies: es5-ext "^0.10.64" -sqlstring@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" - integrity sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ== - ssri@^10.0.0, ssri@^10.0.1: version "10.0.6" resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5"