diff --git a/api-services/authentication/useAuth.tsx b/api-services/authentication/useAuth.tsx index 37d6f5db7..61a53a481 100644 --- a/api-services/authentication/useAuth.tsx +++ b/api-services/authentication/useAuth.tsx @@ -112,10 +112,10 @@ export const ProvideAuth = ({ children }: PropsWithChildren) => { if (!user) throw new Error('Invalid fake token') try { OrganizationService.getForUser().then(organizations => { - console.log(`Found ${organizations.length} organizations for fake token user`) + console.debug(`Found ${organizations.length} organizations for fake token user`) if (organizations.length > 0) { const organization = organizations[0]! - console.log(`Using ${organization.longName} for user.`, organization) + console.debug(`Using ${organization.longName} for user.`, organization) setUser(() => ({ ...user, organization: { @@ -126,7 +126,7 @@ export const ProvideAuth = ({ children }: PropsWithChildren) => { setToken(config.fakeToken) didInit.current = true } else { - console.log('Creating a new organization') + console.debug('Creating a new organization') OrganizationService.create({ id: '', email: 'test@helpwave.de', diff --git a/api-services/mutations/tasks/bed_mutations.ts b/api-services/mutations/tasks/bed_mutations.ts index 490795a9d..678022bac 100644 --- a/api-services/mutations/tasks/bed_mutations.ts +++ b/api-services/mutations/tasks/bed_mutations.ts @@ -43,7 +43,7 @@ export const useBedCreateMutation = () => { const res = await APIServices.bed.createBed(req, getAuthenticatedGrpcMetadata()) if (!res.toObject()) { - console.log('error in BedCreate') + console.error('error in BedCreate') } return { id: res.getId(), name: bed.name } diff --git a/api-services/types/tasks/task.ts b/api-services/types/tasks/task.ts index 750b1d78e..8bffa8af5 100644 --- a/api-services/types/tasks/task.ts +++ b/api-services/types/tasks/task.ts @@ -1,3 +1,5 @@ +import type { Translation } from '@helpwave/hightide' + export type SubTaskDTO = { id: string, name: string, @@ -8,7 +10,28 @@ export type CreateSubTaskDTO = SubTaskDTO & { taskId?: string, } -export type TaskStatus = 'done' | 'inProgress' | 'todo' +const taskStatus = ['done', 'inProgress', 'todo'] as const + +export type TaskStatus = typeof taskStatus[number] + +export type TaskStatusTranslationType = Record + +const taskStatusTranslation: Translation = { + en: { + todo: 'Todo', + inProgress: 'In Progress', + done: 'Done' + }, + de: { + todo: 'Todo', + inProgress: 'In Arbeit', + done: 'Fertig' + } +} +export const TaskStatusUtil = { + taskStatus, + translation: taskStatusTranslation +} export type TaskDTO = { id: string, diff --git a/customer/api/auth/authService.ts b/customer/api/auth/authService.ts index 76ccceb86..8d4b5b043 100644 --- a/customer/api/auth/authService.ts +++ b/customer/api/auth/authService.ts @@ -50,11 +50,11 @@ export const restoreSession = async (): Promise => { // If access token is expired, refresh it if (user.expired) { try { - console.log('Access token expired, refreshing...') + console.debug('Access token expired, refreshing...') const refreshedUser = await renewToken() return refreshedUser ?? undefined } catch (error) { - console.error('Silent token renewal failed', error) + console.debug('Silent token renewal failed', error) return } } diff --git a/customer/hooks/useAuth.tsx b/customer/hooks/useAuth.tsx index 688294be7..0906e650f 100644 --- a/customer/hooks/useAuth.tsx +++ b/customer/hooks/useAuth.tsx @@ -32,7 +32,7 @@ export const AuthProvider = ({ children }: PropsWithChildren) => { isLoading: false, }) onTokenExpiringCallback(async () => { - console.log('Token expiring, refreshing...') + console.debug('Token expiring, refreshing...') const identity = await renewToken() setAuthState({ identity: identity ?? undefined, diff --git a/customer/pages/auth/callback.tsx b/customer/pages/auth/callback.tsx index d0b0bfd90..dc9020aa5 100644 --- a/customer/pages/auth/callback.tsx +++ b/customer/pages/auth/callback.tsx @@ -45,7 +45,7 @@ const AuthCallback: NextPage { // Check if the URL contains OIDC callback params if (searchParams.get('code') && searchParams.get('state')) { - console.log('Processing OIDC callback...') + console.debug('Processing OIDC callback...') try { await handleCallback() const redirect = searchParams.get('redirect_uri') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53d9b0caf..22e24927a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,7 +127,7 @@ importers: version: 9.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwindcss: specifier: ^4.1.3 - version: 4.1.7 + version: 4.1.10 vanilla-cookieconsent: specifier: 3.0.1 version: 3.0.1 @@ -172,18 +172,12 @@ importers: '@dnd-kit/sortable': specifier: 7.0.2 version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) - '@headlessui/react': - specifier: 1.7.19 - version: 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@helpwave/api-services': specifier: workspace:* version: link:../api-services '@helpwave/hightide': - specifier: ^0.1.7-alpha.1 - version: 0.1.7-alpha.1(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17) - '@radix-ui/react-checkbox': - specifier: 1.1.3 - version: 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^0.1.18 + version: 0.1.18(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17) '@tailwindcss/postcss': specifier: ^4.1.3 version: 4.1.5 @@ -194,35 +188,17 @@ importers: specifier: ^4.36.1 version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-table': - specifier: 8.20.6 - version: 8.20.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/google-protobuf': - specifier: 3.15.12 - version: 3.15.12 + specifier: ^8.21.3 + version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: specifier: ^2.1.1 version: 2.1.1 - cookies-next: - specifier: 2.1.2 - version: 2.1.2 - google-protobuf: - specifier: 3.21.4 - version: 3.21.4 - grpc-web: - specifier: 1.5.0 - version: 1.5.0 - js-cookie: - specifier: 3.0.5 - version: 3.0.5 lucide-react: specifier: 0.468.0 version: 0.468.0(react@18.3.1) next: specifier: ^15.3.2 version: 15.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - oauth4webapi: - specifier: 2.17.0 - version: 2.17.0 postcss: specifier: ^8.5.3 version: 8.5.3 @@ -238,31 +214,19 @@ importers: react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) - rxjs: - specifier: 7.8.1 - version: 7.8.1 - simplebar-core: - specifier: 1.3.0 - version: 1.3.0 - simplebar-react: - specifier: 3.3.0 - version: 3.3.0(react@18.3.1) tailwindcss: specifier: ^4.1.3 - version: 4.1.7 + version: 4.1.10 typescript: specifier: 5.7.2 version: 5.7.2 zod: - specifier: 3.24.1 - version: 3.24.1 + specifier: ^3.25.73 + version: 3.25.73 devDependencies: '@helpwave/eslint-config': specifier: ^0.0.11 version: 0.0.11(jiti@2.4.2) - '@types/js-cookie': - specifier: 3.0.6 - version: 3.0.6 '@types/node': specifier: 20.17.10 version: 20.17.10 @@ -372,8 +336,8 @@ packages: '@helpwave/hightide@0.0.17': resolution: {integrity: sha512-FTktcuyJ9/Vh3odP6r+LLFWHdqV+dHSxzJTOQrY9FZ4ikHSbhmAOfrszi4CZG69/K5KjGkv9WMuvmn+cK+YkQw==} - '@helpwave/hightide@0.1.7-alpha.1': - resolution: {integrity: sha512-HmoJjRNm4+V2JNt/tvpwxtFvzKaH2PWYqvFO2Kj9rgyOgyYludWqAVOnkSMHm/37qY/cnmZHLxpA0FKd1R6gIw==} + '@helpwave/hightide@0.1.18': + resolution: {integrity: sha512-3Uc/rgDZBpA/Pd0U5OBMEXqFp0rA4OPobDkMWSqGYND2AZXtvGHdadbnlnATtHVFgZ6/vytekQSPuf5oxXCSVw==} '@helpwave/proto-ts@0.64.0-89e2023.0': resolution: {integrity: sha512-EqHsZDOttnCBqcAZ0WiRO32rGMICwvGUxvhSltU0KahFsbIx8o5DZ3l9N8GdB3c/66qe5tabTvVKbUzxf66Yjw==} @@ -1180,8 +1144,8 @@ packages: react-native: optional: true - '@tanstack/react-table@8.20.6': - resolution: {integrity: sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==} + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} engines: {node: '>=12'} peerDependencies: react: '>=16.8' @@ -1193,8 +1157,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@tanstack/table-core@8.20.5': - resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} '@tanstack/virtual-core@3.0.0': @@ -2456,9 +2420,6 @@ packages: tailwindcss@4.1.5: resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==} - tailwindcss@4.1.7: - resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} - tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -2580,6 +2541,9 @@ packages: zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + zod@3.25.73: + resolution: {integrity: sha512-fuIKbQAWQl22Ba5d1quwEETQYjqnpKVyZIWAhbnnHgnDd3a+z4YgEfkI5SZ2xMELnLAXo/Flk2uXgysZNf0uaA==} + snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -2708,7 +2672,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) simplebar-core: 1.3.0 simplebar-react: 3.3.0(react@18.3.1) - tailwindcss: 4.1.7 + tailwindcss: 4.1.10 tinycolor2: 1.6.0 zod: 3.24.1 transitivePeerDependencies: @@ -2721,11 +2685,11 @@ snapshots: - babel-plugin-react-compiler - sass - '@helpwave/hightide@0.1.7-alpha.1(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)': + '@helpwave/hightide@0.1.18(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)': dependencies: - '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tailwindcss/cli': 4.1.10 + '@tanstack/react-table': 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 lucide-react: 0.468.0(react@18.3.1) postcss: 8.5.3 @@ -2735,9 +2699,9 @@ snapshots: react-dom: 18.3.1(react@18.3.1) simplebar-core: 1.3.0 simplebar-react: 3.3.0(react@18.3.1) - tailwindcss: 4.1.7 + tailwindcss: 4.1.10 tinycolor2: 1.6.0 - zod: 3.24.1 + zod: 3.25.73 transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -3340,9 +3304,9 @@ snapshots: optionalDependencies: react-dom: 18.3.1(react@18.3.1) - '@tanstack/react-table@8.20.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/table-core': 8.20.5 + '@tanstack/table-core': 8.21.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -3352,7 +3316,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/table-core@8.20.5': {} + '@tanstack/table-core@8.21.3': {} '@tanstack/virtual-core@3.0.0': {} @@ -4828,8 +4792,6 @@ snapshots: tailwindcss@4.1.5: {} - tailwindcss@4.1.7: {} - tapable@2.2.1: {} tar@7.4.3: @@ -4985,3 +4947,5 @@ snapshots: yocto-queue@0.1.0: {} zod@3.24.1: {} + + zod@3.25.73: {} diff --git a/scripts/generate_boilerplate.js b/scripts/generate_boilerplate.js index 3aac32b0c..3d5141ba2 100644 --- a/scripts/generate_boilerplate.js +++ b/scripts/generate_boilerplate.js @@ -34,9 +34,9 @@ const options = program.opts() const args = program.args if(options.debug){ - console.log('options', options) - console.log('args', args) - console.log('execute location', process.env.INIT_CWD) + console.debug('options', options) + console.debug('args', args) + console.debug('execute location', process.env.INIT_CWD) } @@ -83,7 +83,7 @@ if (fs.existsSync(filePath) && !options.force) { console.error('Error creating directory:', err) process.exit(1) } else { - console.log(`Directory ${dir} created successfully.`) + console.debug(`Directory ${dir} created successfully.`) } }) } @@ -91,7 +91,7 @@ if (fs.existsSync(filePath) && !options.force) { if (err) { console.error('Error writing to file:', err) } else { - console.log(`File ${fileName} created successfully.`) + console.info(`File ${fileName} created successfully.`) } }) } diff --git a/tasks/components/BedInRoomIndicator.tsx b/tasks/components/BedInRoomIndicator.tsx index b91a8b167..a7d51334e 100644 --- a/tasks/components/BedInRoomIndicator.tsx +++ b/tasks/components/BedInRoomIndicator.tsx @@ -41,13 +41,13 @@ export const BedInRoomIndicator = roomName, bedPosition }: PropsForTranslation) => { - const translation = useTranslation(defaultBedInRoomIndicatorTranslation, overwriteTranslation) + const translation = useTranslation([defaultBedInRoomIndicatorTranslation], overwriteTranslation) return (
{roomName !== undefined && ( - {`${translation.bed} ${bedPosition + 1} ${translation.in} ${roomName}`} + {`${translation('bed')} ${bedPosition + 1} ${translation('in')} ${roomName}`} )}
diff --git a/tasks/components/ColumnTitle.tsx b/tasks/components/ColumnTitle.tsx index cecb6b5ee..d48ce8db5 100644 --- a/tasks/components/ColumnTitle.tsx +++ b/tasks/components/ColumnTitle.tsx @@ -1,18 +1,44 @@ import clsx from 'clsx' +import type { ReactNode } from 'react' + +type ColumnTitleType = 'main' | 'subtitle' type ColumnTitleProps = { - title: string, - subtitle?: string, + title: ReactNode, + description?: ReactNode, + actions?: ReactNode, + type?: ColumnTitleType, + containerClassName?: string, + titleRowClassName?: string, } /** * A component for creating a uniform column title with the same bottom padding */ -export const ColumnTitle = ({ title, subtitle }: ColumnTitleProps) => { +export const ColumnTitle = ({ + title, + actions, + description, + type = 'main', + containerClassName, + titleRowClassName, + }: ColumnTitleProps) => { return ( -
- {title} - {subtitle && ({subtitle})} +
+
+ {type === 'main' &&

{title}

} + {type === 'subtitle' &&

{title}

} + {actions} +
+ {description && ({description})}
) } diff --git a/tasks/components/FeedbackButton.tsx b/tasks/components/FeedbackButton.tsx index 6861ba472..5cd6e1873 100644 --- a/tasks/components/FeedbackButton.tsx +++ b/tasks/components/FeedbackButton.tsx @@ -22,13 +22,13 @@ type FeedbackButtonProps = { export const FeedbackButton = ({ overwriteTranslation, className }: PropsForTranslation) => { const config = getConfig() - const translation = useTranslation(defaultFeedbackButtonTranslation, overwriteTranslation) + const translation = useTranslation([defaultFeedbackButtonTranslation], overwriteTranslation) const onClick = () => window.open(config.feedbackFormUrl, '_blank') return ( - {translation.text} + {translation('text')} ) } diff --git a/tasks/components/Header.tsx b/tasks/components/Header.tsx index 7174b525a..451354bfe 100644 --- a/tasks/components/Header.tsx +++ b/tasks/components/Header.tsx @@ -33,7 +33,7 @@ const Header = ({ withIcon = true }: HeaderProps) => { return ( -
+
{withIcon && ( diff --git a/tasks/components/InvitationBanner.tsx b/tasks/components/InvitationBanner.tsx index aba245a3b..83fe6663b 100644 --- a/tasks/components/InvitationBanner.tsx +++ b/tasks/components/InvitationBanner.tsx @@ -29,7 +29,7 @@ export const InvitationBanner = ({ overwriteTranslation, invitationCount }: PropsForTranslation) => { - const translation = useTranslation(defaultInvitationBannerTranslation, overwriteTranslation) + const translation = useTranslation([defaultInvitationBannerTranslation], overwriteTranslation) const { data, isError, isLoading } = useInvitationsByUserQuery(InvitationState.INVITATION_STATE_PENDING) let openInvites = invitationCount @@ -51,7 +51,7 @@ export const InvitationBanner = ({ className="w-full bg-primary text-white py-2 px-4 rounded-xl cursor-pointer select-none row gap-x-2 items-center" href="/invitations" > - {`${translation.openInvites}: ${openInvites}`} + {`${translation('openInvites')}: ${openInvites}`} ) } diff --git a/tasks/components/KanbanColumn.tsx b/tasks/components/KanbanColumn.tsx index 369b0db24..f64ac8a7e 100644 --- a/tasks/components/KanbanColumn.tsx +++ b/tasks/components/KanbanColumn.tsx @@ -42,7 +42,7 @@ export const KanbanColumn = ({ draggedTileId, onEditTask }: PropsForTranslation) => { - const translation = useTranslation(defaultKanbanColumnsTranslations, overwriteTranslation) + const translation = useTranslation([defaultKanbanColumnsTranslations], overwriteTranslation) const { setNodeRef } = useDroppable({ id: type, @@ -80,7 +80,7 @@ export const KanbanColumn = ({ className="row ml-1 gap-x-1 text-gray-300" > - {translation.addTask} + {translation('addTask')}
) diff --git a/tasks/components/KanbanHeader.tsx b/tasks/components/KanbanHeader.tsx index 4a540625f..849caf147 100644 --- a/tasks/components/KanbanHeader.tsx +++ b/tasks/components/KanbanHeader.tsx @@ -40,20 +40,20 @@ export const KanbanHeader = ({ searchValue = '', onSearchChange }: PropsForTranslation) => { - const translation = useTranslation(defaultKanbanHeaderTranslations, overwriteTranslation) + const translation = useTranslation([defaultKanbanHeaderTranslations], overwriteTranslation) return (
- {translation.tasks} + {translation('tasks')}
- {translation.status} + {translation('status')}
- {translation.label} + {translation('label')}
- +
) diff --git a/tasks/components/MobileInterceptor.tsx b/tasks/components/MobileInterceptor.tsx index df9ca794f..71c7daac6 100644 --- a/tasks/components/MobileInterceptor.tsx +++ b/tasks/components/MobileInterceptor.tsx @@ -31,16 +31,16 @@ const defaultMobileInterceptorTranslation = { * the helpwave app will be shown */ const MobileInterceptor: NextPage = ({ overwriteTranslation }: PropsForTranslation) => { - const translation = useTranslation(defaultMobileInterceptorTranslation, overwriteTranslation) + const translation = useTranslation([defaultMobileInterceptorTranslation], overwriteTranslation) const config = getConfig() const playStoreLink = config.appstoreLinks.playStore const appstoreLink = config.appstoreLinks.appStore return (
- {translation.pleaseDownloadApp} - {translation.playStore} - {translation.appStore} + {translation('pleaseDownloadApp')} + {translation('playStore')} + {translation('appStore')}
) } diff --git a/tasks/components/OrganizationForm.tsx b/tasks/components/OrganizationForm.tsx index c540e496f..5169a759a 100644 --- a/tasks/components/OrganizationForm.tsx +++ b/tasks/components/OrganizationForm.tsx @@ -9,6 +9,7 @@ import { } from '@helpwave/hightide' import type { OrganizationMinimalDTO } from '@helpwave/api-services/types/users/organizations' import { emptyOrganization } from '@helpwave/api-services/types/users/organizations' +import { ColumnTitle } from '@/components/ColumnTitle' type OrganizationFormTranslation = { general: string, @@ -21,8 +22,8 @@ type OrganizationFormTranslation = { contactEmailDescription: string, notVerified: string, required: string, - tooLong: (maxCharacters: number) => string, - tooShort: (minCharacters: number) => string, + tooLong: string, + tooShort: string, invalidEmail: string, } @@ -38,8 +39,8 @@ const defaultOrganizationFormTranslations: Translation `Too long, at most ${maxCharacters} characters`, - tooShort: (minCharacters) => `Too short, at least ${minCharacters} characters`, + tooLong: `Too long, at most {{characters}} characters`, + tooShort: `Too short, at least {{characters}} characters`, invalidEmail: 'Invalid email address' }, de: { @@ -53,8 +54,8 @@ const defaultOrganizationFormTranslations: Translation `Zu lang, maximal ${maxCharacters} Zeichen`, - tooShort: (minCharacters) => `Zu kurz, mindestens ${minCharacters} Zeichen`, + tooLong: `Zu lang, maximal {{characters}} Zeichen`, + tooShort: `Zu kurz, mindestens {{characters}} Zeichen`, invalidEmail: 'Ungültige Email Adresse' } } @@ -82,10 +83,10 @@ export const emptyOrganizationForm: OrganizationFormType = { } } -// TODO make sure the Organization type only has the used values shortName, longName, email, isVerified export type OrganizationFormProps = { organizationForm: OrganizationFormType, onChange: (organizationForm: OrganizationFormType, shouldUpdate: boolean) => void, + className?: string, } /** @@ -97,8 +98,9 @@ export const OrganizationForm = ({ overwriteTranslation, organizationForm = emptyOrganizationForm, onChange = () => undefined, + className, }: PropsForTranslation) => { - const translation = useTranslation(defaultOrganizationFormTranslations, overwriteTranslation) + const translation = useTranslation([defaultOrganizationFormTranslations], overwriteTranslation) const minShortNameLength = 2 const minLongNameLength = 4 @@ -112,31 +114,31 @@ export const OrganizationForm = ({ function validateShortName(organization: OrganizationMinimalDTO) { const shortName = organization.shortName.trim() if (shortName === '') { - return translation.required + return translation('required') } else if (shortName.length < minShortNameLength) { - return translation.tooShort(minShortNameLength) + return translation('tooShort', { replacements: { characters: minShortNameLength.toString() } }) } else if (shortName.length > maxShortNameLength) { - return translation.tooLong(maxShortNameLength) + return translation('tooLong', { replacements: { characters: maxShortNameLength.toString() } }) } } function validateLongName(organization: OrganizationMinimalDTO) { const longName = organization.longName.trim() if (longName === '') { - return translation.required + return translation('required') } else if (longName.length < minLongNameLength) { - return translation.tooShort(minLongNameLength) + return translation('tooShort', { replacements: { characters: minLongNameLength.toString() } }) } else if (longName.length > maxLongNameLength) { - return translation.tooLong(maxLongNameLength) + return translation('tooLong', { replacements: { characters: maxLongNameLength.toString() } }) } } function validateEmailWithOrganization(organization: OrganizationMinimalDTO) { const email = organization.email.trim() if (email === '') { - return translation.required + return translation('required') } else if (!validateEmail(organization.email)) { - return translation.invalidEmail + return translation('invalidEmail') } } @@ -156,88 +158,90 @@ export const OrganizationForm = ({ return ( -
- {translation.general} -
- triggerOnChange({ ...organizationForm.organization }, false, { - ...organizationForm.touched, - shortName: true - })} - onChangeText={text => triggerOnChange({ - ...organizationForm.organization, - shortName: text - }, false, { ...organizationForm.touched })} - onEditCompleted={text => triggerOnChange({ - ...organizationForm.organization, - shortName: text - }, true, { ...organizationForm.touched, shortName: true })} - maxLength={maxShortNameLength} - className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingShortNameError })} - /> - {isDisplayingShortNameError && {shortNameErrorMessage}} - {translation.shortNameDescription} -
-
- triggerOnChange({ ...organizationForm.organization }, false, { - ...organizationForm.touched, - longName: true - })} - onChangeText={text => triggerOnChange({ - ...organizationForm.organization, - longName: text - }, false, { ...organizationForm.touched })} - onEditCompleted={text => triggerOnChange({ - ...organizationForm.organization, - longName: text - }, true, { ...organizationForm.touched, longName: true })} - maxLength={maxLongNameLength} - className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingLongNameError })} - /> - {isDisplayingLongNameError && {longNameErrorMessage}} - {translation.longNameDescription} -
-
-
-
- triggerOnChange({ ...organizationForm.organization }, false, { - ...organizationForm.touched, - email: true - })} - onChangeText={text => triggerOnChange({ - ...organizationForm.organization, - email: text - }, false, { ...organizationForm.touched })} - onEditCompleted={text => triggerOnChange({ - ...organizationForm.organization, - email: text - }, true, { ...organizationForm.touched, email: true })} - maxLength={maxMailLength} - className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingEmailNameError })} - /> +
+ +
+
+ triggerOnChange({ ...organizationForm.organization }, false, { + ...organizationForm.touched, + shortName: true + })} + onChangeText={text => triggerOnChange({ + ...organizationForm.organization, + shortName: text + }, false, { ...organizationForm.touched })} + onEditCompleted={text => triggerOnChange({ + ...organizationForm.organization, + shortName: text + }, true, { ...organizationForm.touched, shortName: true })} + maxLength={maxShortNameLength} + className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingShortNameError })} + /> + {isDisplayingShortNameError && {shortNameErrorMessage}} + {translation('shortNameDescription')} +
+
+ triggerOnChange({ ...organizationForm.organization }, false, { + ...organizationForm.touched, + longName: true + })} + onChangeText={text => triggerOnChange({ + ...organizationForm.organization, + longName: text + }, false, { ...organizationForm.touched })} + onEditCompleted={text => triggerOnChange({ + ...organizationForm.organization, + longName: text + }, true, { ...organizationForm.touched, longName: true })} + maxLength={maxLongNameLength} + className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingLongNameError })} + /> + {isDisplayingLongNameError && {longNameErrorMessage}} + {translation('longNameDescription')} +
+
+
+
+ triggerOnChange({ ...organizationForm.organization }, false, { + ...organizationForm.touched, + email: true + })} + onChangeText={text => triggerOnChange({ + ...organizationForm.organization, + email: text + }, false, { ...organizationForm.touched })} + onEditCompleted={text => triggerOnChange({ + ...organizationForm.organization, + email: text + }, true, { ...organizationForm.touched, email: true })} + maxLength={maxMailLength} + className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingEmailNameError })} + /> +
+ { + !organizationForm.organization.isVerified && + {translation('notVerified')} + }
- { - !organizationForm.organization.isVerified && - {translation.notVerified} - } + {isDisplayingEmailNameError && {emailErrorMessage}} + {translation('contactEmailDescription')}
- {isDisplayingEmailNameError && {emailErrorMessage}} - {translation.contactEmailDescription}
diff --git a/tasks/components/OrganizationInvitationList.tsx b/tasks/components/OrganizationInvitationList.tsx index 08460337c..c9ee46929 100644 --- a/tasks/components/OrganizationInvitationList.tsx +++ b/tasks/components/OrganizationInvitationList.tsx @@ -1,13 +1,11 @@ -import { useContext, useState } from 'react' +import { useContext, useMemo, useState } from 'react' import type { Translation } from '@helpwave/hightide' import { - defaultTableStatePagination, InputModal, LoadingAndErrorComponent, type PropsForTranslation, SolidButton, Table, - type TableState, TextButton, useTranslation, validateEmail @@ -19,6 +17,8 @@ import { } from '@helpwave/api-services/mutations/users/organization_mutations' import { InvitationState } from '@helpwave/api-services/types/users/invitations' import { OrganizationContext } from '@/pages/organizations' +import { ColumnTitle } from '@/components/ColumnTitle' +import type { ColumnDef } from '@tanstack/react-table' type OrganizationInvitationListTranslation = { remove: string, @@ -68,18 +68,12 @@ export const OrganizationInvitationList = ({ invitations, onChange }: PropsForTranslation) => { - const translation = useTranslation(defaultOrganizationInvitationListTranslation, overwriteTranslation) + const translation = useTranslation([defaultOrganizationInvitationListTranslation], overwriteTranslation) const context = useContext(OrganizationContext) const usedOrganizationId = organizationId ?? context.state.organizationId const isCreatingOrganization = usedOrganizationId === '' const { data, isLoading, isError } = useInvitationsByOrganizationQuery(context.state.organizationId) - const [tableState, setTableState] = useState({ - pagination: { - ...defaultTableStatePagination, - entriesPerPage: 10 - } - }) const [inviteMemberModalEmail, setInviteMemberModalEmail] = useState() // Maybe move this filter to the endpoint or the query const usedInvitations: OrganizationInvitation[] = invitations ?? (data ?? []).filter(value => value.state === InvitationState.INVITATION_STATE_PENDING) @@ -90,6 +84,40 @@ export const OrganizationInvitationList = ({ const isValidEmail = !!inviteMemberModalEmail && validateEmail(inviteMemberModalEmail) const isShowingInviteMemberModal = inviteMemberModalEmail !== undefined + const columns = useMemo[]>(() => [ + { + id: 'email', + header: translation('email'), + footer: 'email', + accessorKey: 'email', + sortingFn: 'text', + minSize: 200, + }, + { + id: 'actions', + header: '', + cell: ({ cell }) => { + const invite = usedInvitations[cell.row.index]! + return ( + { + if (!isCreatingOrganization) { + revokeInviteMutation.mutate(invite.id) + } + onChange(usedInvitations.filter(value => idMapping(value) !== idMapping(invite))) + }} + > + {translation('remove')} + + ) + }, + minSize: 200, + maxSize: 200, + enableResizing: false, + } + ], [isCreatingOrganization, onChange, revokeInviteMutation, translation, usedInvitations]) + return ( setInviteMemberModalEmail(text) }]} buttonOverwrites={[ {}, - { disabled: !isValidEmail, color: 'positive', text: translation.addAndNext }, - { disabled: !isValidEmail, color: 'primary', text: translation.add } + { disabled: !isValidEmail, color: 'positive', text: translation('addAndNext') }, + { disabled: !isValidEmail, color: 'primary', text: translation('add') } ]} />
-
- {`${translation.invitations} (${usedInvitations.length})`} - setInviteMemberModalEmail('')} - > - {translation.inviteMember} - -
+ setInviteMemberModalEmail('')} + size="small" + > + {translation('inviteMember')} + + )} + type="subtitle" + /> {translation.email}, - <> - ]} - rowMappingToCells={invite => [ -
- {invite.email} -
, -
- { - if (!isCreatingOrganization) { - revokeInviteMutation.mutate(invite.id) - } - onChange(usedInvitations.filter(value => idMapping(value) !== idMapping(invite))) - }} - > - {translation.remove} - -
- ]} + columns={columns} + initialState={{ + pagination: { pageSize: 4 }, + }} /> diff --git a/tasks/components/OrganizationMemberList.tsx b/tasks/components/OrganizationMemberList.tsx index 14407f168..d2328b1ec 100644 --- a/tasks/components/OrganizationMemberList.tsx +++ b/tasks/components/OrganizationMemberList.tsx @@ -1,17 +1,13 @@ -import { useContext, useState } from 'react' - -import type { Translation } from '@helpwave/hightide' -import { ConfirmModal } from '@helpwave/hightide' +import { useContext, useMemo, useState } from 'react' +import type { Translation, TranslationPlural } from '@helpwave/hightide' import { Avatar, - defaultTableStatePagination, - defaultTableStateSelection, + ConfirmModal, + FillerRowElement, LoadingAndErrorComponent, type PropsForTranslation, - removeFromTableSelection, SolidButton, - Table, - type TableState, + TableWithSelection, TextButton, useTranslation } from '@helpwave/hightide' @@ -19,6 +15,10 @@ import type { OrganizationMember } from '@helpwave/api-services/types/users/orga import { useMembersByOrganizationQuery } from '@helpwave/api-services/mutations/users/organization_member_mutations' import { useRemoveMemberMutation } from '@helpwave/api-services/mutations/users/organization_mutations' import { OrganizationContext } from '@/pages/organizations' +import { ColumnTitle } from '@/components/ColumnTitle' +import { Trash } from 'lucide-react' +import type { ColumnDef, RowSelectionState } from '@tanstack/react-table' +import type { OrganizationInvitation } from '@/components/OrganizationInvitationList' type OrganizationMemberListTranslation = { edit: string, @@ -26,12 +26,11 @@ type OrganizationMemberListTranslation = { removeSelection: string, deselectAll: string, selectAll: string, - members: string, - member: string, + member: TranslationPlural, saveChanges: string, role: string, - dangerZoneText: (single: boolean) => string, - deleteConfirmText: (single: boolean) => string, + dangerZoneText: TranslationPlural, + deleteConfirmText: TranslationPlural, } const defaultOrganizationMemberListTranslations: Translation = { @@ -41,12 +40,20 @@ const defaultOrganizationMemberListTranslations: Translation `Deleting ${single ? `a ${defaultOrganizationMemberListTranslations.en.member}` : defaultOrganizationMemberListTranslations.en.members} is a permanent action and cannot be undone. Be careful!`, - deleteConfirmText: (single) => `Do you really want to delete the selected ${single ? defaultOrganizationMemberListTranslations.en.member : defaultOrganizationMemberListTranslations.en.members}?`, + dangerZoneText: { + one: 'Removing a member is a permanent action and cannot be undone. Be careful!', + other: 'Removing members is a permanent action and cannot be undone. Be careful!' + }, + deleteConfirmText: { + one: 'Do you really want to remove the selected member?', + other: 'Do you really want to remove the selected members?', + }, }, de: { edit: 'Bearbeiten', @@ -54,16 +61,29 @@ const defaultOrganizationMemberListTranslations: Translation `Das Löschen ${single ? `eines ${defaultOrganizationMemberListTranslations.de.member}` : `von ${defaultOrganizationMemberListTranslations.de.member}`} ist permanent und kann nicht rückgängig gemacht werden. Vorsicht!`, - deleteConfirmText: (single) => `Wollen Sie wirklich ${single ? `das ausgewählte ${defaultOrganizationMemberListTranslations.de.member}` : `die ausgewählten ${defaultOrganizationMemberListTranslations.de.members}`} löschen?`, + dangerZoneText: { + one: 'Das Entfernen eines Mitglieds ist a permanent permanent und kann nicht rückgängig gemacht werden. Vorsicht!', + other: 'Das Entfernen von Mitgliedern ist a permanent permanent und kann nicht rückgängig gemacht werden. Vorsicht!' + }, + deleteConfirmText: { + one: 'Wollen Sie wirklich das ausgewählte Mitglied löschen?', + other: 'Wollen Sie wirklich die ausgewählten Mitglieder löschen?', + }, } } -type DeleteDialogState = { isShowing: boolean, member?: OrganizationMember } + +type DeleteDialogState = { + isShowing: boolean, + /** If not set, the entire selection will be deleted */ + member?: OrganizationMember, +} const defaultDeleteDialogState: DeleteDialogState = { isShowing: false } export type OrganizationMemberListProps = { @@ -79,56 +99,108 @@ export const OrganizationMemberList = ({ organizationId, members }: PropsForTranslation) => { - const translation = useTranslation(defaultOrganizationMemberListTranslations, overwriteTranslation) - const [tableState, setTableState] = useState({ - pagination: defaultTableStatePagination, - selection: defaultTableStateSelection - }) + const translation = useTranslation([defaultOrganizationMemberListTranslations], overwriteTranslation) const context = useContext(OrganizationContext) organizationId ??= context.state.organizationId const { data, isLoading, isError } = useMembersByOrganizationQuery(organizationId) - const membersByOrganization = data ?? [] - const usedMembers: OrganizationMember[] = members ?? membersByOrganization ?? [] - const removeMemberMutation = useRemoveMemberMutation(organizationId) + const membersByOrganization = useMemo(() => data ?? [], [data]) + const usedMembers: OrganizationMember[] = useMemo( + () => members ?? membersByOrganization ?? [], [members, membersByOrganization] + ) + const removeMemberMutation = useRemoveMemberMutation(organizationId) const [deleteDialogState, setDeleteDialogState] = useState(defaultDeleteDialogState) + const [selectionState, setSelectionState] = useState({}) + const columns = useMemo[]>(() => [ + { + id: 'member', + header: translation('member'), + cell: ({ cell }) => { + const orgMember = usedMembers[cell.row.index]! + return ( + { + event.stopPropagation() + navigator.clipboard.writeText(orgMember.email).catch(console.error) + }} + > + +
+ {orgMember.name} + {orgMember.email} +
+
+ ) + }, + accessorKey: 'name', + sortingFn: 'text', + minSize: 200, + meta: { + filterType: 'text' + } + }, + /* + { + id: 'role', + header: translation("role"), + accessorFn: ({}) => { - const hasSelectedMultiple = !!tableState.selection && tableState.selection.currentSelection.length > 1 - const idMapping = (dataObject: OrganizationMember) => dataObject.id + }, + minSize: 200, + maxSize: 200, + enableResizing: false, + }, + */ + { + id: 'actions', + header: '', + cell: ({ cell }) => { + const orgMember = usedMembers[cell.row.index]! + return ( + setDeleteDialogState({ isShowing: true, member: orgMember })} + color="negative" + // disabled={orgMember.role === Role.admin} + > + {translation('remove')} + + ) + }, + minSize: 200, + maxSize: 200, + enableResizing: false, + } + ], [translation, usedMembers]) - // TODO move this filtering to the Table component - const admins: string[] = [] // usedMembers.filter(value => value.role === Role.admin).map(idMapping) - if (tableState.selection?.currentSelection.find(value => admins.find(adminId => adminId === value))) { - const newSelection = tableState.selection.currentSelection.filter(value => !admins.find(adminId => adminId === value)) - setTableState({ - ...tableState, - selection: { - currentSelection: newSelection, - hasSelectedAll: false, // There is always one admin - hasSelectedSome: newSelection.length > 0, - hasSelectedNone: newSelection.length === 0, - } - }) - } + const selectedElements = Object.keys(selectionState ?? {}).length return (
setDeleteDialogState(defaultDeleteDialogState)} onConfirm={() => { if (deleteDialogState.member) { - setTableState(removeFromTableSelection(tableState, [deleteDialogState.member], usedMembers.length, idMapping)) + if (selectionState[deleteDialogState.member.id]) { + const newSelection = { ...selectionState } + delete newSelection[deleteDialogState.member.id] + setSelectionState(newSelection) + } removeMemberMutation.mutate(deleteDialogState.member.id) } else { - const selected = usedMembers.filter(value => tableState.selection?.currentSelection.includes(idMapping(value))) - setTableState(removeFromTableSelection(tableState, selected, usedMembers.length, idMapping)) - selected.forEach(value => removeMemberMutation.mutate(value.id)) + Object.keys(selectionState).forEach(value => { + const member = usedMembers.find(member => member.id === value) + if (member) { + removeMemberMutation.mutate(member.id) + } + }) + setSelectionState({}) } setDeleteDialogState(defaultDeleteDialogState) }} @@ -140,61 +212,33 @@ export const OrganizationMemberList = ({ errorProps={{ classname: 'border-2 border-gray-600 rounded-xl min-h-[300px]' }} loadingProps={{ classname: 'border-2 border-gray-600 rounded-xl min-h-[300px]' }} > -
- {translation.members + ` (${usedMembers.length})`} -
- {tableState.selection && tableState.selection.currentSelection.length > 0 && ( - setDeleteDialogState({ isShowing: true })} - color="negative" - > - {translation.removeSelection} - - )} -
-
-
0 && ( + setDeleteDialogState({ isShowing: true })} + color="negative" + size="small" + startIcon={} + > + {translation('removeSelection')} + + )} + type="subtitle" + /> + - {translation.member} - , -
- {translation.role} -
, - <> - ]} - rowMappingToCells={orgMember => [ -
- -
- {orgMember.name} - - {orgMember.email} - -
-
, -
- { /* TODO allow changing roles */ - }} - > - {'N.A.' /* translation.roleTypes[orgMember.role] */} - -
, -
- setDeleteDialogState({ isShowing: true, member: orgMember })} - color="negative" - // disabled={orgMember.role === Role.admin} - > - {translation.remove} - -
- ]} - identifierMapping={idMapping} + columns={columns} + rowSelection={selectionState} + onRowSelectionChange={setSelectionState} + initialState={{ + pagination: { + pageSize: 5, + } + }} + fillerRow={(columnId) => + () + } /> diff --git a/tasks/components/RoomList.tsx b/tasks/components/RoomList.tsx index 73b0e87b3..6c8d9694e 100644 --- a/tasks/components/RoomList.tsx +++ b/tasks/components/RoomList.tsx @@ -1,25 +1,28 @@ -import type { Translation } from '@helpwave/hightide' -import { ConfirmModal } from '@helpwave/hightide' -import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' -import { useContext, useEffect, useState } from 'react' -import { SolidButton, TextButton } from '@helpwave/hightide' -import { Input } from '@helpwave/hightide' +import type { Translation, TranslationPlural } from '@helpwave/hightide' +import { FillerRowElement } from '@helpwave/hightide' +import { InputUncontrolled } from '@helpwave/hightide' import { - defaultTableStatePagination, - defaultTableStateSelection, - removeFromTableSelection, - Table, - type TableState + ConfirmModal, + LoadingAndErrorComponent, + type PropsForTranslation, + SolidButton, + TableWithSelection, + TextButton, + useTranslation } from '@helpwave/hightide' -import { LoadingAndErrorComponent } from '@helpwave/hightide' +import { useContext, useEffect, useMemo, useState } from 'react' import type { RoomMinimalDTO } from '@helpwave/api-services/types/tasks/room' import { useRoomCreateMutation, - useRoomDeleteMutation, useRoomOverviewsQuery, + useRoomDeleteMutation, + useRoomOverviewsQuery, useRoomUpdateMutation } from '@helpwave/api-services/mutations/tasks/room_mutations' import { OrganizationOverviewContext } from '@/pages/organizations/[organizationId]' import { ManageBedsModal } from '@/components/modals/ManageBedsModal' +import { ColumnTitle } from '@/components/ColumnTitle' +import { Plus, Trash } from 'lucide-react' +import type { ColumnDef, RowSelectionState } from '@tanstack/react-table' type RoomListTranslation = { edit: string, @@ -28,14 +31,13 @@ type RoomListTranslation = { deselectAll: string, selectAll: string, roomName: string, - room: string, - rooms: string, + room: TranslationPlural, addRoom: string, bedCount: string, manageBeds: string, manage: string, - dangerZoneText: (single: boolean) => string, - deleteConfirmText: (single: boolean) => string, + dangerZoneText: TranslationPlural, + deleteConfirmText: TranslationPlural, } const defaultRoomListTranslations: Translation = { @@ -46,14 +48,22 @@ const defaultRoomListTranslations: Translation = { deselectAll: 'Deselect All', selectAll: 'Select All', roomName: 'Room Name', - room: 'Room', - rooms: 'Rooms', + room: { + one: 'Room', + other: 'Rooms', + }, addRoom: 'Add Room', bedCount: '#Beds', manageBeds: 'Manage Beds', manage: 'Manage', - dangerZoneText: (single) => `Deleting ${single ? defaultRoomListTranslations.en.room : defaultRoomListTranslations.en.rooms} is a permanent action and cannot be undone. Be careful!`, - deleteConfirmText: (single) => `Do you really want to delete the selected ${single ? defaultRoomListTranslations.en.room : defaultRoomListTranslations.en.rooms}?`, + dangerZoneText: { + one: 'Deleting a room is a permanent action and cannot be undone. Be careful!', + other: 'Deleting rooms is a permanent action and cannot be undone. Be careful!', + }, + deleteConfirmText: { + one: 'Do you really want to delete the selected room?', + other: 'Do you really want to delete the selected rooms?', + }, }, de: { edit: 'Bearbeiten', @@ -61,15 +71,23 @@ const defaultRoomListTranslations: Translation = { removeSelection: 'Ausgewählte löschen', deselectAll: 'Auswahl aufheben', selectAll: 'Alle auswählen', - roomName: 'Zimmer', - room: 'Raum', - rooms: 'Zimmer', + roomName: 'Zimmername', + room: { + one: 'Zimmer', + other: 'Zimmer', + }, addRoom: 'Raum hinzufügen', bedCount: 'Bettenanzahl', manageBeds: 'Betten verwalten', manage: 'Verwalten', - dangerZoneText: (single) => `Das Löschen von ${single ? `einem ${defaultRoomListTranslations.de.room}` : defaultRoomListTranslations.de.rooms} ist permanent und kann nicht rückgängig gemacht werden. Vorsicht!`, - deleteConfirmText: (single) => `Wollen Sie wirklich ${single ? 'den' : 'die'} ausgewählten ${single ? defaultRoomListTranslations.de.room : defaultRoomListTranslations.de.rooms} löschen?`, + dangerZoneText: { + one: 'Das Löschen von einem Raum ist permanent und kann nicht rückgängig gemacht werden. Vorsicht!', + other: 'Das Löschen von Räumen ist permanent und kann nicht rückgängig gemacht werden. Vorsicht!', + }, + deleteConfirmText: { + one: 'Willst du wirklich den ausgewählten Raum löschen?', + other: 'Willst du wirklich die ausgewählten Räume löschen?', + }, } } @@ -77,6 +95,11 @@ export type RoomListRoomRepresentation = RoomMinimalDTO & { bedCount: number, } +type DeleteDialogState = { + isShowing: boolean, + /** If not set, the entire selection will be deleted */ + value?: RoomListRoomRepresentation, +} export type RoomListProps = { rooms?: RoomListRoomRepresentation[], // TODO replace with more optimized RoomDTO roomsPerPage?: number, @@ -89,78 +112,147 @@ export const RoomList = ({ overwriteTranslation, rooms }: PropsForTranslation) => { - const translation = useTranslation(defaultRoomListTranslations, overwriteTranslation) + const translation = useTranslation([defaultRoomListTranslations], overwriteTranslation) const context = useContext(OrganizationOverviewContext) - const [tableState, setTableState] = useState({ - pagination: defaultTableStatePagination, - selection: defaultTableStateSelection - }) + const [selectionState, setSelectionState] = useState({}) const [usedRooms, setUsedRooms] = useState(rooms ?? []) - const [focusElement, setFocusElement] = useState() - const [isEditing, setIsEditing] = useState(false) + const [deleteRoomDialogState, setDeleteRoomDialogState] = useState({ isShowing: false }) const [managedRoom, setManagedRoom] = useState() - const identifierMapping = (dataObject: RoomListRoomRepresentation) => dataObject.id const creatRoomMutation = useRoomCreateMutation((room) => { context.updateContext({ ...context.state }) - setFocusElement({ ...room, bedCount: 0 }) setUsedRooms(prevState => [...prevState, { ...room, bedCount: 0 }]) }, context.state.wardId ?? '') // Not good but should be safe most of the time const deleteRoomMutation = useRoomDeleteMutation(() => { context.updateContext({ ...context.state }) - setFocusElement(undefined) }) const updateRoomMutation = useRoomUpdateMutation(() => context.updateContext({ ...context.state })) const { data, isError, isLoading } = useRoomOverviewsQuery(context.state.wardId) // TODO use a more light weight query useEffect(() => { - if (data && !isEditing) { + if (data) { setUsedRooms(data.map(room => ({ id: room.id, name: room.name, bedCount: room.beds.length }))) } - }, [data, isEditing]) - // undefined = dont show - // "" = take selcted - // "" = only the id - const [deletionConfirmDialogElement, setDeletionConfirmDialogElement] = useState() + }, [data]) const minRoomNameLength = 1 const maxRoomNameLength = 32 + const columns = useMemo[]>(() => [ + { + id: 'room', + header: translation('roomName'), + cell: ({ cell }) => { + const room = usedRooms[cell.row.index]! + return ( + { + updateRoomMutation.mutate({ + ...room, + name: text + }) + }} + id={room.name} + minLength={minRoomNameLength} + maxLength={maxRoomNameLength} + className="w-full" + /> + ) + }, + accessorKey: 'name', + sortingFn: 'text', + minSize: 200, + meta: { + filterType: 'text' + } + }, + { + id: 'bedCount', + header: translation('bedCount'), + accessorKey: 'bedCount', + sortingFn: 'text', + minSize: 190, + meta: { + filterType: 'range' + } + }, + { + id: 'manage', + header: translation('manageBeds'), + cell: ({ cell }) => { + const room = usedRooms[cell.row.index]! + return ( + setManagedRoom(room.id)} color="primary"> + {translation('manage')} + + ) + }, + minSize: 160, + maxSize: 200, + }, + { + id: 'actions', + header: '', + cell: ({ cell }) => { + const room = usedRooms[cell.row.index]! + return ( + setDeleteRoomDialogState({ isShowing: true, value: room })} + color="negative" + > + {translation('remove')} + + ) + }, + minSize: 140, + maxSize: 140, + enableResizing: false, + } + ], [translation, updateRoomMutation, usedRooms]) + const addRoom = () => { - // TODO remove below for an actual room add const newRoom = { id: '', - name: translation.room + (usedRooms.length + 1), + name: translation('room') + (usedRooms.length + 1), } creatRoomMutation.mutate(newRoom) } - const multipleInDelete = deletionConfirmDialogElement !== '' || tableState.selection?.currentSelection.length === 1 + const selectedElementCount = Object.keys(selectionState).length return ( -
+
setDeletionConfirmDialogElement(undefined)} + isOpen={deleteRoomDialogState.isShowing} + onCancel={() => setDeleteRoomDialogState({ ...deleteRoomDialogState, isShowing: false })} onConfirm={() => { - let toDeleteElements: RoomListRoomRepresentation[] - if (deletionConfirmDialogElement) { - toDeleteElements = usedRooms.filter(value => identifierMapping(value) === deletionConfirmDialogElement) + if (deleteRoomDialogState.value) { + if (selectionState[deleteRoomDialogState.value.id]) { + const newSelection = { ...selectionState } + delete newSelection[deleteRoomDialogState.value.id] + setSelectionState(newSelection) + } + deleteRoomMutation.mutate(deleteRoomDialogState.value.id) } else { - toDeleteElements = usedRooms.filter(value => tableState.selection?.currentSelection.includes(identifierMapping(value))) + Object.keys(selectionState).forEach(value => { + const room = usedRooms.find(room => room.id === value) + if (room) { + deleteRoomMutation.mutate(room.id) + } + }) + setSelectionState({}) } - toDeleteElements.forEach(value => deleteRoomMutation.mutate(value.id)) - setTableState(removeFromTableSelection(tableState, toDeleteElements, usedRooms.length, identifierMapping)) - setDeletionConfirmDialogElement(undefined) }} confirmType="negative" /> @@ -170,81 +262,43 @@ export const RoomList = ({ roomId={managedRoom ?? ''} onClose={() => setManagedRoom(undefined)} /> - -
- {translation.rooms + ` (${usedRooms.length})`} + - {(tableState.selection && tableState.selection?.currentSelection.length > 0) && ( + {(selectedElementCount > 0) && ( setDeletionConfirmDialogElement('')} + onClick={() => setDeleteRoomDialogState({ isShowing: true })} color="negative" + size="small" + startIcon={} > - {translation.removeSelection} + {translation('removeSelection')} )} - - {translation.addRoom} + }> + {translation('addRoom')}
-
-
+ + { - setTableState(tableState) - setFocusElement(undefined) - }]} - identifierMapping={identifierMapping} - header={[ - {translation.roomName}, - {translation.bedCount}, - {translation.manageBeds}, - <> - ]} - rowMappingToCells={room => [ -
- { - setIsEditing(true) - setUsedRooms(usedRooms.map(value => identifierMapping(value) === identifierMapping(room) ? { - ...room, - name: text - } : value)) - setFocusElement(room) - }} - onEditCompleted={(text) => { - updateRoomMutation.mutate({ - ...room, - name: text - }) - setIsEditing(false) - }} - id={room.name} - minLength={minRoomNameLength} - maxLength={maxRoomNameLength} - /> -
, -
- {room.bedCount} -
, -
- setManagedRoom(room.id)}> - {translation.manage} - -
, -
- setDeletionConfirmDialogElement(room.id)} color="negative"> - {translation.remove} - -
- ]} + columns={columns} + rowSelection={selectionState} + onRowSelectionChange={setSelectionState} + disableClickRowClickSelection={true} + fillerRow={() => ()} + initialState={{ + pagination: { pageSize: 5 } + }} />
diff --git a/tasks/components/SubtaskTile.tsx b/tasks/components/SubtaskTile.tsx index f8750a9bc..edc51d8e5 100644 --- a/tasks/components/SubtaskTile.tsx +++ b/tasks/components/SubtaskTile.tsx @@ -33,7 +33,7 @@ export const SubtaskTile = ({ onRemoveClick, onChange, }: PropsForTranslation) => { - const translation = useTranslation(defaultSubtaskTileTranslation, overwriteTranslation) + const translation = useTranslation([defaultSubtaskTileTranslation], overwriteTranslation) const minTaskNameLength = 2 const maxTaskNameLength = 64 @@ -78,10 +78,10 @@ export const SubtaskTile = ({ - {translation.remove} + {translation('remove')} diff --git a/tasks/components/SubtaskView.tsx b/tasks/components/SubtaskView.tsx index 07eb484fe..3e46d5a71 100644 --- a/tasks/components/SubtaskView.tsx +++ b/tasks/components/SubtaskView.tsx @@ -55,7 +55,7 @@ export const SubtaskView = ({ }: PropsForTranslation) => { const context = useContext(TaskTemplateContext) - const translation = useTranslation(defaultSubtaskViewTranslation, overwriteTranslation) + const translation = useTranslation([defaultSubtaskViewTranslation], overwriteTranslation) const isCreatingTask = taskId === '' const addSubtaskMutation = useSubTaskAddMutation(taskId) const deleteSubtaskMutation = useSubTaskDeleteMutation() @@ -91,10 +91,10 @@ export const SubtaskView = ({ return (
- {translation.subtasks} + {translation('subtasks')} { - const newSubtask = { id: '', name: `${translation.newSubtask} ${subtasks.length + 1}`, isDone: false } + const newSubtask = { id: '', name: `${translation('newSubtask')} ${subtasks.length + 1}`, isDone: false } onChange([...subtasks, newSubtask]) if (!isCreatingTask) { addSubtaskMutation.mutate(newSubtask) @@ -104,7 +104,7 @@ export const SubtaskView = ({ >
- {translation.addSubtask} + {translation('addSubtask')}
diff --git a/tasks/components/TaskTemplateListColumn.tsx b/tasks/components/TaskTemplateListColumn.tsx index 65518cb32..1b3924874 100644 --- a/tasks/components/TaskTemplateListColumn.tsx +++ b/tasks/components/TaskTemplateListColumn.tsx @@ -40,7 +40,7 @@ export const TaskTemplateListColumn = ({ onColumnEditClick, overwriteTranslation: maybeLanguage }: PropsForTranslation) => { - const translation = useTranslation(defaultTaskTemplateListColumnTranslation, maybeLanguage) + const translation = useTranslation([defaultTaskTemplateListColumnTranslation], maybeLanguage) const [height, setHeight] = useState(undefined) const ref = useRef(null) @@ -52,7 +52,7 @@ export const TaskTemplateListColumn = ({
- {translation.template} + {translation('template')} {onColumnEditClick && }
diff --git a/tasks/components/TaskTemplateWardPreview.tsx b/tasks/components/TaskTemplateWardPreview.tsx index 58fd14fd7..eaa75b4ca 100644 --- a/tasks/components/TaskTemplateWardPreview.tsx +++ b/tasks/components/TaskTemplateWardPreview.tsx @@ -1,26 +1,29 @@ import { useContext } from 'react' import { useRouter } from 'next/router' import type { Translation } from '@helpwave/hightide' -import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' -import { SolidButton } from '@helpwave/hightide' -import { LoadingAndErrorComponent } from '@helpwave/hightide' +import { LoadingAndErrorComponent, type PropsForTranslation, SolidButton, useTranslation } from '@helpwave/hightide' import { useWardTaskTemplateQuery } from '@helpwave/api-services/mutations/tasks/task_template_mutations' import { TaskTemplateCard } from './cards/TaskTemplateCard' import { OrganizationOverviewContext } from '@/pages/organizations/[organizationId]' +import { ColumnTitle } from '@/components/ColumnTitle' +import { AddCard } from '@/components/cards/AddCard' type TaskTemplateWardPreviewTranslation = { showAllTaskTemplates: string, - taskTemplates: (numberOfTemplates: number) => string, + taskTemplatesCount: string, + addTaskTemplate: string, } const defaultTaskTemplateWardPreviewTranslation: Translation = { en: { showAllTaskTemplates: 'Show all Task Templates', - taskTemplates: (numberOfTemplates) => `Task Templates (${numberOfTemplates})` + taskTemplatesCount: `Task Templates ({{amount}})`, + addTaskTemplate: 'Add Task Template' }, de: { showAllTaskTemplates: 'Alle Vorlagen anzeigen', - taskTemplates: (numberOfTemplates) => `Vorlagen (${numberOfTemplates})` + taskTemplatesCount: `Vorlagen ({{amount}})`, + addTaskTemplate: 'Neues Task Template' } } @@ -32,10 +35,10 @@ export type TaskTemplateWardPreviewProps = { * A TaskTemplateWardPreview for showing all TaskTemplate within a ward */ export const TaskTemplateWardPreview = ({ - overwriteTranslation, - wardId, -}: PropsForTranslation) => { - const translation = useTranslation(defaultTaskTemplateWardPreviewTranslation, overwriteTranslation) + overwriteTranslation, + wardId, + }: PropsForTranslation) => { + const translation = useTranslation([defaultTaskTemplateWardPreviewTranslation], overwriteTranslation) const router = useRouter() const context = useContext(OrganizationOverviewContext) @@ -53,16 +56,19 @@ export const TaskTemplateWardPreview = ({ errorProps={{ classname: 'border-2 border-gray-500 rounded-xl min-h-[200px]' }} > {taskTemplates && ( -
-
- {translation.taskTemplates(taskTemplates.length)} - router.push(`/ward/${wardId}/templates`)} - > - {translation.showAllTaskTemplates} - -
+
+ router.push(`/ward/${wardId}/templates`)} + > + {translation('showAllTaskTemplates')} + + )} + type="subtitle" + />
{taskTemplates.map((taskTemplate, index) => ( ))} + router.push(`/ward/${wardId}/templates`).catch(console.error)} + />
)} diff --git a/tasks/components/UserInvitationList.tsx b/tasks/components/UserInvitationList.tsx index c64df325c..27e27a6e9 100644 --- a/tasks/components/UserInvitationList.tsx +++ b/tasks/components/UserInvitationList.tsx @@ -1,10 +1,14 @@ -import { useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import type { Translation } from '@helpwave/hightide' -import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' -import { defaultTableStatePagination, Table, type TableState } from '@helpwave/hightide' -import { SolidButton } from '@helpwave/hightide' -import { LoadingAndErrorComponent } from '@helpwave/hightide' -import { Avatar } from '@helpwave/hightide' +import { + Avatar, + FillerRowElement, + LoadingAndErrorComponent, + type PropsForTranslation, + SolidButton, + Table, + useTranslation +} from '@helpwave/hightide' import { useInvitationsByUserQuery, useInviteAcceptMutation, @@ -14,6 +18,7 @@ import { useAuth } from '@helpwave/api-services/authentication/useAuth' import type { Invitation } from '@helpwave/api-services/types/users/invitations' import { InvitationState } from '@helpwave/api-services/types/users/invitations' import { ReSignInDialog } from '@/components/modals/ReSignInDialog' +import type { ColumnDef } from '@tanstack/react-table' type UserInvitationListTranslation = { accept: string, @@ -40,10 +45,9 @@ export type UserInvitationListProps = Record * A table that shows all organizations a user hast been invited to */ export const UserInvitationList = ({ - overwriteTranslation, -}: PropsForTranslation) => { - const translation = useTranslation(defaultUserInvitationListTranslation, overwriteTranslation) - const [tableState, setTableState] = useState({ pagination: { ...defaultTableStatePagination, entriesPerPage: 10 } }) + overwriteTranslation, + }: PropsForTranslation) => { + const translation = useTranslation([defaultUserInvitationListTranslation], overwriteTranslation) const { data, isLoading, isError } = useInvitationsByUserQuery(InvitationState.INVITATION_STATE_PENDING) const [isShowingReSignInDialog, setIsShowingReSignInDialog] = useState(false) const { signOut } = useAuth() @@ -51,10 +55,64 @@ export const UserInvitationList = ({ const declineInviteMutation = useInviteDeclineMutation() const acceptInviteMutation = useInviteAcceptMutation() - const acceptInvite = (inviteId: string) => acceptInviteMutation.mutateAsync(inviteId) - .then(() => setIsShowingReSignInDialog(true)) + const acceptInvite = useCallback((inviteId: string) => { + acceptInviteMutation.mutateAsync(inviteId) + .then(() => setIsShowingReSignInDialog(true)) + }, [acceptInviteMutation]) - const idMapping = (invite: Invitation) => invite.id + const columns = useMemo[]>(() => [ + { + id: 'organization', + header: translation('organization'), + cell: ({ cell }) => { + if (!data) { + return + } + const invite = data[cell.row.index]! + return ( +
+ + {invite.organization.longName} +
+ ) + }, + accessorFn: (invite) => invite.organization.longName, + sortingFn: 'text', + minSize: 200, + meta: { + filterType: 'text' + } + }, + { + id: 'actions', + header: '', + cell: ({ cell }) => { + if (!data) { + return + } + const invite = data[cell.row.index]! + return ( +
+ acceptInvite(invite.id)} + > + {translation('accept')} + + declineInviteMutation.mutate(invite.id)} + > + {translation('decline')} + +
+ ) + }, + minSize: 240, + }, + ], [acceptInvite, data, declineInviteMutation, translation]) return ( <> @@ -74,33 +132,14 @@ export const UserInvitationList = ({ {data && (
{translation.organization}, - <>, - <> - ]} - rowMappingToCells={invite => [ -
- - {invite.organization.longName} -
, - acceptInvite(invite.id)} - > - {translation.accept} - , - declineInviteMutation.mutate(invite.id)} - > - {translation.decline} - - ]} + columns={columns} + initialState={{ pagination: { pageSize: 10 } }} + fillerRow={(columnId) => { + if (columnId === 'actions') { + return (
) + } + return () + }} /> )} diff --git a/tasks/components/UserMenu.tsx b/tasks/components/UserMenu.tsx index e9875bed0..635554fc9 100644 --- a/tasks/components/UserMenu.tsx +++ b/tasks/components/UserMenu.tsx @@ -1,5 +1,4 @@ import { useState } from 'react' -import Link from 'next/link' import { useRouter } from 'next/router' import type { Translation } from '@helpwave/hightide' import { @@ -62,7 +61,7 @@ export const UserMenu = ({ overwriteTranslation, className, }: PropsForTranslation) => { - const translation = useTranslation(defaultUserMenuTranslations, overwriteTranslation) + const translation = useTranslation([defaultUserMenuTranslations], overwriteTranslation) const [isLanguageModalOpen, setLanguageModalOpen] = useState(false) const [isThemeModalOpen, setThemeModalOpen] = useState(false) const { user, signOut } = useAuth() @@ -91,48 +90,32 @@ export const UserMenu = ({
)}> - {translation.profile} - setLanguageModalOpen(true)} - > - {translation.language} + router.push(settingsURL, '_blank')}> + {translation('profile')} - setThemeModalOpen(true)} - > - {translation.theme} + setLanguageModalOpen(true)}> + {translation('language')} - router.push('/templates')} - > - {translation.taskTemplates} + setThemeModalOpen(true)}> + {translation('theme')} - router.push('/properties')} - > - {translation.properties} + router.push('/templates')}> + {translation('taskTemplates')} - router.push('/organizations')} - > - {translation.organizations} + router.push('/properties')}> + {translation('properties')} - router.push('/invitations')} - > - {translation.invitations} + router.push('/organizations')}> + {translation('organizations')} + + router.push('/invitations')}> + {translation('invitations')} signOut()} > - {translation.signOut} + {translation('signOut')} diff --git a/tasks/components/WardForm.tsx b/tasks/components/WardForm.tsx index d1986ac82..c324fc0d7 100644 --- a/tasks/components/WardForm.tsx +++ b/tasks/components/WardForm.tsx @@ -8,8 +8,8 @@ type WardFormTranslation = { name: string, nameDescription: string, required: string, - tooLong: (maxCharacters: number) => string, - tooShort: (minCharacters: number) => string, + tooLong: string, + tooShort: string, duplicateName: string, } @@ -19,8 +19,8 @@ const defaultWardFormTranslations: Translation = { name: 'Name', nameDescription: 'Should be short, prefer abbreviations.', required: 'Required Field, cannot be empty', - tooLong: (maxCharacters) => `Too long, at most ${maxCharacters} characters`, - tooShort: (minCharacters) => `Too short, at least ${minCharacters} characters`, + tooLong: `Too long, at most {{characters}} characters`, + tooShort: `Too short, at least {{characters}} characters`, duplicateName: 'Wards can\'t have the same name' }, de: { @@ -28,8 +28,8 @@ const defaultWardFormTranslations: Translation = { name: 'Name', nameDescription: 'Sollte kurz sein, Abbkürzungen werden präferiert.', required: 'Benötigter Wert, darf nicht leer sein', - tooLong: (maxCharacters) => `Zu lang, maximal ${maxCharacters} Zeichen`, - tooShort: (minCharacters) => `Zu kurz, mindestens ${minCharacters} Zeichen`, + tooLong: `Zu lang, maximal {{characters}} Zeichen`, + tooShort: `Zu kurz, mindestens {{characters}} Zeichen`, duplicateName: 'Stationen müssen unterschiedliche Namen haben' } } @@ -53,7 +53,7 @@ export const WardForm = ({ onChange = () => undefined, isShowingErrorsDirectly = false }: PropsForTranslation) => { - const translation = useTranslation(defaultWardFormTranslations, overwriteTranslation) + const translation = useTranslation([defaultWardFormTranslations], overwriteTranslation) const [touched, setTouched] = useState({ name: isShowingErrorsDirectly }) const minWardNameLength = 2 @@ -65,11 +65,10 @@ export const WardForm = ({ function validateName(ward: WardFormInfoDTO) { const wardName = ward.name.trim() if (wardName === '') { - return translation.required - } else if (wardName.length < minWardNameLength) { - return translation.tooShort(minWardNameLength) + return translation('required') } else if (wardName.length < minWardNameLength) { + return translation('tooShort', { replacements: { characters: minWardNameLength.toString() } }) } else if (wardName.length > maxWardNameLength) { - return translation.tooLong(maxWardNameLength) + return translation('tooLong', { replacements: { characters: maxWardNameLength.toString() } }) } } @@ -86,7 +85,7 @@ export const WardForm = ({
setTouched({ ...touched, name: true })} onChangeText={text => triggerOnChange({ ...ward, name: text })} maxLength={maxWardNameLength} @@ -94,7 +93,7 @@ export const WardForm = ({ /> {isDisplayingShortNameError && {nameErrorMessage}}
- {translation.nameDescription} + {translation('nameDescription')} ) } diff --git a/tasks/components/cards/BedCard.tsx b/tasks/components/cards/BedCard.tsx index ba01261f2..a1544bcf4 100644 --- a/tasks/components/cards/BedCard.tsx +++ b/tasks/components/cards/BedCard.tsx @@ -37,7 +37,7 @@ export const BedCard = ({ className, ...restCardProps }: PropsForTranslation) => { - const translation = useTranslation(defaultBedCardTranslation, overwriteTranslation) + const translation = useTranslation([defaultBedCardTranslation], overwriteTranslation) return ( (
{bedName} - {translation.nobody} + {translation('nobody')}
- {translation.addPatient} + {translation('addPatient')}
diff --git a/tasks/components/cards/OrganizationCard.tsx b/tasks/components/cards/OrganizationCard.tsx index cd0359b9b..aeab8a6d3 100644 --- a/tasks/components/cards/OrganizationCard.tsx +++ b/tasks/components/cards/OrganizationCard.tsx @@ -35,7 +35,7 @@ export const OrganizationCard = ({ organization, ...editCardProps }: PropsForTranslation) => { - const translation = useTranslation(defaultOrganizationCardTranslation, overwriteTranslation) + const translation = useTranslation([defaultOrganizationCardTranslation], overwriteTranslation) const organizationMemberCount = organization.members.length return ( @@ -53,7 +53,7 @@ export const OrganizationCard = ({
- {`${organizationMemberCount} ${organizationMemberCount > 1 ? translation.members : translation.member}`} + {`${organizationMemberCount} ${organizationMemberCount > 1 ? translation('members') : translation('member')}`}
({ avatarUrl: user.avatarURL, alt: user.name }))}/>
diff --git a/tasks/components/cards/PatientCard.tsx b/tasks/components/cards/PatientCard.tsx index c91cf1bf9..dcb4ba5ab 100644 --- a/tasks/components/cards/PatientCard.tsx +++ b/tasks/components/cards/PatientCard.tsx @@ -41,11 +41,11 @@ export const PatientCard = ({ className, ...restCardProps }: PropsForTranslation) => { - const translation = useTranslation(defaultPatientCardTranslations, overwriteTranslation) + const translation = useTranslation([defaultPatientCardTranslations], overwriteTranslation) return (
- {bedName ?? translation.bedNotAssigned} + {bedName ?? translation('bedNotAssigned')} {patientName}
diff --git a/tasks/components/cards/TaskCard.tsx b/tasks/components/cards/TaskCard.tsx index 94de3eed0..901f91587 100644 --- a/tasks/components/cards/TaskCard.tsx +++ b/tasks/components/cards/TaskCard.tsx @@ -35,7 +35,7 @@ export const TaskCard = ({ isSelected = false, onClick = () => undefined }: PropsForTranslation) => { - const translation = useTranslation(defaultTaskCardTranslations, overwriteTranslation) + const translation = useTranslation([defaultTaskCardTranslations], overwriteTranslation) const progress = task.subtasks.length === 0 ? 1 : task.subtasks.filter(value => value.isDone).length / task.subtasks.length const isOverDue = task.dueDate && task.dueDate < new Date() && task.status !== 'done' @@ -50,7 +50,7 @@ export const TaskCard = ({ 'border-primary': isSelected, })} > -
+
{!task.isPublicVisible &&
} {task.name} @@ -61,7 +61,7 @@ export const TaskCard = ({
{assignee && assignee.avatarUrl && - } + } {task.subtasks.length > 0 && ( )} diff --git a/tasks/components/cards/TaskTemplateCard.tsx b/tasks/components/cards/TaskTemplateCard.tsx index 3259b8670..5562554a5 100644 --- a/tasks/components/cards/TaskTemplateCard.tsx +++ b/tasks/components/cards/TaskTemplateCard.tsx @@ -42,7 +42,7 @@ export const TaskTemplateCard = ({ className, ...editCardProps }: PropsForTranslation) => { - const translation = useTranslation(defaultTaskTemplateCardTranslations, overwriteTranslation) + const translation = useTranslation([defaultTaskTemplateCardTranslations], overwriteTranslation) return ( - {typeForLabel === 'ward' ? translation.ward : translation.personal} + {typeForLabel === 'ward' ? translation('ward') : translation('personal')} )}
- {subtaskCount + ' ' + translation.subtask} + {subtaskCount + ' ' + translation('subtask')}
) diff --git a/tasks/components/layout/DashboardDisplay.tsx b/tasks/components/layout/DashboardDisplay.tsx index 7236136a4..8da9a2718 100644 --- a/tasks/components/layout/DashboardDisplay.tsx +++ b/tasks/components/layout/DashboardDisplay.tsx @@ -9,6 +9,7 @@ import { InvitationBanner } from '../InvitationBanner' import { PatientCard } from '../cards/PatientCard' import { AddCard } from '@/components/cards/AddCard' import { useAuth } from '@helpwave/api-services/authentication/useAuth' +import { ColumnTitle } from '@/components/ColumnTitle' type DashboardDisplayTranslation = { patients: string, @@ -46,7 +47,7 @@ export type DashboardDisplayProps = Record export const DashboardDisplay = ({ overwriteTranslation, }: PropsForTranslation) => { - const translation = useTranslation(defaultDashboardDisplayTranslations, overwriteTranslation) + const translation = useTranslation([defaultDashboardDisplayTranslations], overwriteTranslation) const { organization } = useAuth() const router = useRouter() @@ -61,15 +62,15 @@ export const DashboardDisplay = ({ } = useRecentPatientsQuery() return ( -
+
- {translation.recent} + {patients && patients.length > 0 && ( <> - {translation.patients} +
{patients?.map(patient => (
- {translation.wards} +
{wards && wards.length > 0 && wards?.map(ward => ( ))} router.push(`/organizations/${organization?.id}`)} />
diff --git a/tasks/components/layout/InitializationChecker.tsx b/tasks/components/layout/InitializationChecker.tsx index bbc898ea1..f3e12339b 100644 --- a/tasks/components/layout/InitializationChecker.tsx +++ b/tasks/components/layout/InitializationChecker.tsx @@ -14,13 +14,13 @@ const defaultTranslation: Translation<{ loggingIn: string }> = { } export const InitializationChecker = ({ children }: PropsWithChildren) => { - const translation = useTranslation(defaultTranslation) + const translation = useTranslation([defaultTranslation]) const { user } = useAuth() if(!user) { return (
- +
) } diff --git a/tasks/components/layout/NewsFeed.tsx b/tasks/components/layout/NewsFeed.tsx index 029fc4a1c..8f2bc56f6 100644 --- a/tasks/components/layout/NewsFeed.tsx +++ b/tasks/components/layout/NewsFeed.tsx @@ -34,7 +34,7 @@ export const NewsFeed = ({ localizedNews, width }: PropsForTranslation) => { - const translation = useTranslation(defaultNewsFeedTranslations, overwriteTranslation) + const translation = useTranslation([defaultNewsFeedTranslations], overwriteTranslation) // The value of how much space a FeatureDisplay needs before the title can be displayed on its left // Given in px const widthForAppearanceChange = 600 @@ -43,7 +43,7 @@ export const NewsFeed = ({ const newsFilter = 'tasks' return (
- + {usedLanguage ? filterNews(localizedNews[usedLanguage], [newsFilter]).map(news => ( )) : (
- {translation.noNews} + {translation('noNews')}
)}
diff --git a/tasks/components/layout/OrganizationDetails.tsx b/tasks/components/layout/OrganizationDetails.tsx index 107a6113a..71abc7e85 100644 --- a/tasks/components/layout/OrganizationDetails.tsx +++ b/tasks/components/layout/OrganizationDetails.tsx @@ -61,7 +61,7 @@ export type OrganizationDetailProps = { export const OrganizationDetail = ({ overwriteTranslation }: PropsForTranslation) => { - const translation = useTranslation(defaultOrganizationDetailTranslations, overwriteTranslation) + const translation = useTranslation([defaultOrganizationDetailTranslations], overwriteTranslation) const { state: contextState, @@ -133,12 +133,12 @@ export const OrganizationDetail = ({ return (
setIsShowingConfirmDialog(false)} @@ -162,8 +162,8 @@ export const OrganizationDetail = ({ signOut() }} /> - -
+ +
{ @@ -172,6 +172,7 @@ export const OrganizationDetail = ({ updateOrganization(organizationForm.organization).catch(console.error) } }} + className="max-w-96" /> {!isCreatingNewOrganization && ( @@ -186,17 +187,20 @@ export const OrganizationDetail = ({ className="w-auto" onClick={() => isCreatingNewOrganization ? createOrganization(organizationForm.organization) : updateOrganization(organizationForm.organization)} disabled={!organizationForm.isValid}> - {isCreatingNewOrganization ? translation.create : translation.update} + {isCreatingNewOrganization ? translation('create') : translation('update')}
- {translation.dangerZone} - {translation.dangerZoneText} +
diff --git a/tasks/components/layout/OrganizationDisplay.tsx b/tasks/components/layout/OrganizationDisplay.tsx index e73b6baba..1f3f4fef7 100644 --- a/tasks/components/layout/OrganizationDisplay.tsx +++ b/tasks/components/layout/OrganizationDisplay.tsx @@ -39,7 +39,7 @@ export const OrganizationDisplay = ({ selectedOrganizationId, organizations, }: PropsForTranslation) => { - const translation = useTranslation(defaultOrganizationDisplayTranslations, overwriteTranslation) + const translation = useTranslation([defaultOrganizationDisplayTranslations], overwriteTranslation) const router = useRouter() const context = useContext(OrganizationContext) @@ -55,7 +55,7 @@ export const OrganizationDisplay = ({ const usedSelectedId = selectedOrganizationId ?? context.state.organizationId return (
- +
{usedOrganizations.map(organization => ( context.updateContext({ ...context.state, organizationId: '' })} isSelected={!!usedSelectedId} /> diff --git a/tasks/components/layout/PatientDetails.tsx b/tasks/components/layout/PatientDetails.tsx index c9d2a98cb..0940dd64a 100644 --- a/tasks/components/layout/PatientDetails.tsx +++ b/tasks/components/layout/PatientDetails.tsx @@ -1,20 +1,23 @@ import { useContext, useEffect, useState } from 'react' import type { Translation } from '@helpwave/hightide' -import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' -import { SolidButton } from '@helpwave/hightide' -import { Textarea } from '@helpwave/hightide' -import { ToggleableInput } from '@helpwave/hightide' -import { useSaveDelay } from '@helpwave/hightide' -import { LoadingAndErrorComponent } from '@helpwave/hightide' +import { useDelay } from '@helpwave/hightide' +import { + LoadingAndErrorComponent, + type PropsForTranslation, + SolidButton, + Textarea, + ToggleableInput, + useTranslation +} from '@helpwave/hightide' import type { TaskStatus } from '@helpwave/api-services/types/tasks/task' import { useAssignBedMutation, usePatientDetailsQuery, usePatientDischargeMutation, usePatientUpdateMutation, - useUnassignMutation, - useReadmitPatientMutation + useReadmitPatientMutation, + useUnassignMutation } from '@helpwave/api-services/mutations/tasks/patient_mutations' import type { PatientDetailsDTO } from '@helpwave/api-services/types/tasks/patient' import { emptyPatientDetails } from '@helpwave/api-services/types/tasks/patient' @@ -39,7 +42,7 @@ type PatientDetailTranslation = { const defaultPatientDetailTranslations: Translation = { en: { - patientDetails: 'Details', + patientDetails: 'Patient Details', notes: 'Notes', saveChanges: 'Save Changes', dischargeConfirmText: 'Do you really want to discharge the patient?', @@ -49,7 +52,7 @@ const defaultPatientDetailTranslations: Translation = readmit: 'Readmit' }, de: { - patientDetails: 'Details', + patientDetails: 'Patienten Details', notes: 'Notizen', saveChanges: 'Speichern', dischargeConfirmText: 'Willst du den Patienten wirklich entlassen?', @@ -70,12 +73,12 @@ export type PatientDetailProps = { * The right side of the ward/[wardId].tsx page showing the detailed information about the patient */ export const PatientDetail = ({ - overwriteTranslation, - wardId, - patient = emptyPatientDetails -}: PropsForTranslation) => { + overwriteTranslation, + wardId, + patient = emptyPatientDetails + }: PropsForTranslation) => { const [isShowingDischargeDialog, setIsShowingDischargeDialog] = useState(false) - const translation = useTranslation(defaultPatientDetailTranslations, overwriteTranslation) + const translation = useTranslation([defaultPatientDetailTranslations], overwriteTranslation) const context = useContext(WardOverviewContext) @@ -109,27 +112,22 @@ export const PatientDetail = ({ const { restartTimer, - clearUpdateTimer - } = useSaveDelay(setIsShowingSavedNotification, 3000) + clearTimer + } = useDelay({ delay: 3000 }) const changeSavedValue = (patient: PatientDetailsDTO) => { setNewPatient(patient) - restartTimer(() => updateMutation.mutate(patient)) + restartTimer(() => { + updateMutation.mutate(patient) + setIsShowingSavedNotification(false) + }) } const isShowingTask = !!taskId || taskId === '' return (
- {isShowingSavedNotification && - ( -
- {translation.saved} -
- ) - } + setIsShowingDischargeDialog(false)} @@ -148,7 +146,14 @@ export const PatientDetail = ({ patientId={newPatient.id} initialStatus={initialTaskStatus} /> - + + {translation('saved')} +
+ )} + />