├── api-services ├── index.ts ├── types │ ├── update.ts │ ├── users │ │ ├── user.ts │ │ ├── organization_member.ts │ │ ├── invitations.ts │ │ └── organizations.ts │ ├── tasks │ │ ├── room.ts │ │ ├── tasks_templates.ts │ │ ├── bed.ts │ │ ├── wards.ts │ │ ├── patient.ts │ │ └── task.ts │ └── properties │ │ ├── attached_property.ts │ │ └── property.ts ├── eslint.config.js ├── config │ └── wrapper.ts ├── .gitignore ├── mutations │ ├── tasks │ │ ├── util.ts │ │ ├── room_mutations.ts │ │ ├── bed_mutations.ts │ │ └── ward_mutations.ts │ ├── users │ │ ├── user_mutations.ts │ │ └── organization_member_mutations.ts │ ├── query_keys.ts │ └── properties │ │ ├── property_view_src_mutations.ts │ │ ├── property_value_mutations.ts │ │ └── property_mutations.ts ├── environment.d.ts ├── tsconfig.json ├── service │ ├── users │ │ ├── UserService.ts │ │ └── OrganizationMemberService.ts │ ├── tasks │ │ ├── BedService.ts │ │ ├── TaskSubtaskService.ts │ │ └── RoomService.ts │ └── properties │ │ └── PropertyViewSourceService.ts ├── authentication │ └── grpc_metadata.ts ├── package.json ├── util │ ├── keycloak.ts │ ├── keycloakAdapter.ts │ └── ReceiveUpdatesStreamHandler.ts ├── README.md └── services.ts ├── tasks ├── components │ ├── Avatar.tsx │ ├── dnd-kit │ │ ├── Sortable.tsx │ │ ├── Droppable.tsx │ │ └── Draggable.tsx │ ├── layout │ │ ├── property │ │ │ ├── SubjectTypeIcon.tsx │ │ │ └── PropertySubjectTypeSelect.tsx │ │ ├── InitializationChecker.tsx │ │ ├── PageWithHeader.tsx │ │ ├── NewsFeed.tsx │ │ └── WardDisplay.tsx │ ├── cards │ │ ├── UserCard.tsx │ │ ├── AddCard.tsx │ │ ├── WardCard.tsx │ │ ├── DragCard.tsx │ │ ├── EditCard.tsx │ │ ├── BedCard.tsx │ │ ├── PatientCard.tsx │ │ ├── TaskTemplateCard.tsx │ │ └── OrganizationCard.tsx │ ├── FeedbackButton.tsx │ ├── dnd-kit-instances │ │ ├── tasks.ts │ │ └── patients.ts │ ├── ColumnTitle.tsx │ ├── KanbanHeader.tsx │ ├── BedInRoomIndicator.tsx │ ├── MobileInterceptor.tsx │ ├── InvitationBanner.tsx │ ├── NewsDisplay.tsx │ ├── modals │ │ ├── PatientDischargeModal.tsx │ │ └── ReSignInDialog.tsx │ ├── selects │ │ ├── TaskVisibilitySelect.tsx │ │ ├── TaskStatusSelect.tsx │ │ └── AssigneeSelect.tsx │ ├── Header.tsx │ └── SubtaskTile.tsx ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── touch-icon-128.png │ ├── touch-icon-152.png │ ├── touch-icon-167.png │ ├── touch-icon-180.png │ └── manifest.json ├── eslint.config.js ├── postcss.config.mjs ├── utils │ ├── titleWrapper.ts │ ├── news.ts │ └── api.ts ├── .env.production ├── .env.development ├── next.config.js ├── .gitignore ├── environment.d.ts ├── README.md ├── tsconfig.json ├── hooks │ └── useRouteParameters.ts ├── translation │ ├── medical.ts │ └── tasks.ts ├── package.json ├── globals.css └── pages │ ├── invitations.tsx │ ├── 404.tsx │ ├── _app.tsx │ └── _document.tsx ├── customer ├── components │ ├── layout │ │ ├── Footer.tsx │ │ ├── FooterLinkGroup.tsx │ │ ├── Section.tsx │ │ └── Header.tsx │ ├── pages │ │ ├── login.tsx │ │ └── create-organization.tsx │ ├── ContractList.tsx │ └── forms │ │ └── StripeCheckOutForm.tsx ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── touch-icon-128.png │ ├── touch-icon-152.png │ ├── touch-icon-167.png │ ├── touch-icon-180.png │ ├── ga.js │ └── manifest.json ├── eslint.config.js ├── api │ ├── util.ts │ ├── mutations │ │ ├── query_keys.ts │ │ ├── voucher_mutations.ts │ │ ├── invoice_mutations.ts │ │ ├── product_mutations.ts │ │ ├── contract_mutations.ts │ │ ├── customer_mutations.ts │ │ ├── customer_product_mutations.ts │ │ └── user_seat_mutations.ts │ ├── config.ts │ ├── services │ │ ├── voucher.ts │ │ ├── product.ts │ │ ├── invoice.ts │ │ ├── contract.ts │ │ └── customer.ts │ ├── dataclasses │ │ ├── voucher.ts │ │ ├── user_seat.ts │ │ ├── contract.ts │ │ └── invoice.ts │ └── auth │ │ └── authService.ts ├── postcss.config.mjs ├── globals.css ├── utils │ ├── titleWrapper.ts │ └── locale.ts ├── next.config.js ├── .env.development ├── .env.production ├── hocs │ └── withCart.tsx ├── environment.d.ts ├── .gitignore ├── README.md ├── tsconfig.json ├── package.json ├── pages │ ├── index.tsx │ ├── _app.tsx │ ├── 404.tsx │ └── _document.tsx └── hooks │ └── useOrganization.tsx ├── .dockerignore ├── .github ├── CODEOWNERS └── workflows │ ├── codeql.yaml │ └── ci.yaml ├── pnpm-workspace.yaml ├── scripts ├── eslint.config.js └── package.json ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── documentation ├── README.md ├── structure.md ├── scripts.md └── translation.md ├── .gitignore ├── .editorconfig ├── package.json ├── renovate.json ├── Dockerfile.tasks └── README.md /api-services/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tasks/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /customer/components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | tasks/.env 3 | -------------------------------------------------------------------------------- /customer/components/layout/FooterLinkGroup.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @helpwave/web 2 | .github @helpwave/devops 3 | -------------------------------------------------------------------------------- /tasks/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: /$ 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /customer/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: /$ 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /tasks/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/tasks/public/favicon.ico -------------------------------------------------------------------------------- /api-services/types/update.ts: -------------------------------------------------------------------------------- 1 | export type Update = { 2 | previous?: T, 3 | update: T, 4 | } 5 | -------------------------------------------------------------------------------- /customer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/customer/public/favicon.ico -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - tasks 3 | - scripts 4 | - api-services 5 | - customer 6 | -------------------------------------------------------------------------------- /tasks/public/touch-icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/tasks/public/touch-icon-128.png -------------------------------------------------------------------------------- /tasks/public/touch-icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/tasks/public/touch-icon-152.png -------------------------------------------------------------------------------- /tasks/public/touch-icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/tasks/public/touch-icon-167.png -------------------------------------------------------------------------------- /tasks/public/touch-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/tasks/public/touch-icon-180.png -------------------------------------------------------------------------------- /customer/public/touch-icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/customer/public/touch-icon-128.png -------------------------------------------------------------------------------- /customer/public/touch-icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/customer/public/touch-icon-152.png -------------------------------------------------------------------------------- /customer/public/touch-icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/customer/public/touch-icon-167.png -------------------------------------------------------------------------------- /customer/public/touch-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpwave/web/HEAD/customer/public/touch-icon-180.png -------------------------------------------------------------------------------- /scripts/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from '@helpwave/eslint-config' 2 | 3 | export default config.recommended 4 | -------------------------------------------------------------------------------- /tasks/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from '@helpwave/eslint-config' 2 | 3 | export default config.nextExtension 4 | -------------------------------------------------------------------------------- /api-services/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from '@helpwave/eslint-config' 2 | 3 | export default config.recommended 4 | -------------------------------------------------------------------------------- /customer/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from '@helpwave/eslint-config' 2 | 3 | export default config.nextExtension 4 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.18.1 2 | 3 | # install pnpm 4 | RUN npm install --global pnpm@8 5 | 6 | CMD ["bash"] 7 | -------------------------------------------------------------------------------- /customer/api/util.ts: -------------------------------------------------------------------------------- 1 | export const formatString = (value?: string) => { 2 | if(!value) return null 3 | return value.trim() 4 | } 5 | -------------------------------------------------------------------------------- /tasks/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | } 5 | } 6 | 7 | export default config 8 | -------------------------------------------------------------------------------- /customer/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | } 5 | } 6 | 7 | export default config 8 | -------------------------------------------------------------------------------- /api-services/types/users/user.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string, 3 | name: string, 4 | nickname: string, 5 | avatarUrl?: string, 6 | } 7 | -------------------------------------------------------------------------------- /customer/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import '@helpwave/hightide/css/uncompiled/globals.css'; 3 | 4 | @source "./node_modules/@helpwave/hightide"; 5 | -------------------------------------------------------------------------------- /customer/public/ga.js: -------------------------------------------------------------------------------- 1 | window.dataLayer = window.dataLayer || [] 2 | function gtag() { 3 | 4 | dataLayer.push(arguments) 5 | } 6 | gtag('js', new Date()) 7 | 8 | gtag('config', 'G-ZQDJEWMMX6') 9 | -------------------------------------------------------------------------------- /tasks/utils/titleWrapper.ts: -------------------------------------------------------------------------------- 1 | const defaultTitle = 'helpwave tasks' 2 | 3 | const titleWrapper = (title?: string) => title ? `${title} ~ ${defaultTitle}` : defaultTitle 4 | 5 | export default titleWrapper 6 | -------------------------------------------------------------------------------- /customer/utils/titleWrapper.ts: -------------------------------------------------------------------------------- 1 | const defaultTitle = 'helpwave customer' 2 | 3 | const titleWrapper = (title?: string) => title ? `${title} ~ ${defaultTitle}` : defaultTitle 4 | 5 | export default titleWrapper 6 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This file will only give an overview of the different aspects of 4 | our repository. 5 | 6 | - [Translation](./translation.md) 7 | - [Structure and pnpm](structure.md) 8 | - [Scripts](scripts.md) 9 | -------------------------------------------------------------------------------- /tasks/.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | NEXT_PUBLIC_API_URL=https://services.helpwave.de 3 | NEXT_PUBLIC_OAUTH_REDIRECT_URI=https://tasks.helpwave.de/auth/callback 4 | NEXT_PUBLIC_FAKE_TOKEN_ENABLE=true 5 | NEXT_PUBLIC_SHOW_STAGING_DISCLAIMER_MODAL=true 6 | -------------------------------------------------------------------------------- /tasks/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=https://services.helpwave.de 2 | NEXT_PUBLIC_OFFLINE_API=false 3 | NEXT_PUBLIC_OAUTH_REDIRECT_URI=http://localhost:3000/auth/callback 4 | NEXT_PUBLIC_FAKE_TOKEN_ENABLE=true 5 | NEXT_PUBLIC_SHOW_STAGING_DISCLAIMER_MODAL=false 6 | -------------------------------------------------------------------------------- /api-services/types/users/organization_member.ts: -------------------------------------------------------------------------------- 1 | export type OrganizationMemberMinimalDTO = { 2 | userId: string, 3 | } 4 | 5 | export type OrganizationMember = OrganizationMemberMinimalDTO & { 6 | email: string, 7 | nickname: string, 8 | avatarURL?: string, 9 | } 10 | -------------------------------------------------------------------------------- /customer/api/mutations/query_keys.ts: -------------------------------------------------------------------------------- 1 | export const QueryKeys = { 2 | product: 'product', 3 | userSeat: 'user-seat', 4 | customer: 'customer', 5 | customerProduct: 'customer-products', 6 | invoice: 'invoice', 7 | voucher: 'voucher', 8 | contract: 'contract', 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # debug 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | .pnpm-debug.log* 11 | 12 | # jetbrains 13 | .idea 14 | .fleet 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # local env files 21 | .env*.local 22 | -------------------------------------------------------------------------------- /documentation/structure.md: -------------------------------------------------------------------------------- 1 | # Structure 2 | The repository is structured as a mono repository managed with [pnpm](https://pnpm.io). 3 | All packages and apps we develop are within the main folder. 4 | 5 | ### Applications 6 | 7 | - [tasks](../tasks): The task management application for healthcare workers 8 | -------------------------------------------------------------------------------- /api-services/config/wrapper.ts: -------------------------------------------------------------------------------- 1 | import { getAPIServiceConfig } from './config' 2 | 3 | export const APIServiceUrls = { 4 | tasks: `${getAPIServiceConfig().apiUrl}/tasks-svc`, 5 | users: `${getAPIServiceConfig().apiUrl}/user-svc`, 6 | property: `${getAPIServiceConfig().apiUrl}/property-svc`, 7 | updates: `${getAPIServiceConfig().apiUrl}/updates-svc` 8 | } 9 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | dockerfile: "Dockerfile" 4 | }, 5 | onCreateCommand: "pnpm install && pnpm run --filter '@helpwave/*' build", 6 | features: { 7 | "ghcr.io/devcontainers/features/sshd:1" : {} 8 | }, 9 | "customizations": { 10 | "vscode": { 11 | "extensions": ["dbaeumer.vscode-eslint"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@helpwave/scripts", 3 | "version": "1.0.0", 4 | "description": "A collection of custom tooling for helpwave/web.", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "type": "module", 8 | "peerDependencies": { 9 | "commander": "12.1.0" 10 | }, 11 | "devDependencies": { 12 | "@helpwave/eslint-config": "^0.0.11" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /customer/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | distDir: 'build', 4 | reactStrictMode: true, 5 | transpilePackages: ['@helpwave/hightide'], 6 | output: 'export', 7 | images: { 8 | dangerouslyAllowSVG: true, 9 | domains: ['cdn.helpwave.de', 'customer.helpwave.de', 'helpwave.de'], 10 | unoptimized: true, 11 | }, 12 | } 13 | 14 | export default nextConfig 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yaml] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.json] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [*.toml] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-monorepo", 3 | "version": "1.0.0", 4 | "description": "monorepo for web-related projects", 5 | "scripts": { 6 | "lint": "pnpm run --filter \"@helpwave/*\" lint" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/helpwave/web.git" 11 | }, 12 | "license": "MPL-2.0", 13 | "bugs": { 14 | "url": "https://github.com/helpwave/web/issues" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /customer/api/config.ts: -------------------------------------------------------------------------------- 1 | export const API_URL = process.env.NEXT_PUBLIC_API_URL 2 | export const OIDC_PROVIDER = process.env.NEXT_PUBLIC_OIDC_PROVIDER 3 | export const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID 4 | export const REDIRECT_URI = process.env.NEXT_PUBLIC_REDIRECT_URI 5 | export const POST_LOGOUT_REDIRECT_URI = process.env.NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI 6 | export const STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY 7 | -------------------------------------------------------------------------------- /customer/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=https://api.customer.helpwave.de 2 | NEXT_PUBLIC_OIDC_PROVIDER=https://id.helpwave.de/realms/main 3 | NEXT_PUBLIC_CLIENT_ID=customer.helpwave.de 4 | NEXT_PUBLIC_REDIRECT_URI=http://localhost:3000/auth/callback 5 | NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI=http://localhost:3000/ 6 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51QqZfARtChvireAMzq4687T4m4TpyJvADX6IyM8A9u47lCUMMi0e2SNfxoSIDMM9DebiQCYwBa7mZRkJ50uZZjGC00hYO7FzL4 7 | -------------------------------------------------------------------------------- /tasks/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | distDir: 'build', 4 | reactStrictMode: true, 5 | transpilePackages: ['@helpwave/api-services'], 6 | output: 'standalone', 7 | images: { 8 | dangerouslyAllowSVG: true, 9 | remotePatterns: [ 10 | new URL('https://cdn.helpwave.de/**'), 11 | new URL('https://helpwave.de/**'), 12 | ], 13 | }, 14 | } 15 | 16 | export default nextConfig 17 | -------------------------------------------------------------------------------- /api-services/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # misc 12 | .DS_Store 13 | *.pem 14 | 15 | # debug 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | .pnpm-debug.log* 20 | 21 | # typescript 22 | *.tsbuildinfo 23 | next-env.d.ts 24 | 25 | # Storybook 26 | storybook-static 27 | -------------------------------------------------------------------------------- /api-services/mutations/tasks/util.ts: -------------------------------------------------------------------------------- 1 | import type { SubtaskDTO } from '../../types/tasks/task' 2 | 3 | interface GRPCSubTask { 4 | getId: () => string, 5 | getName: () => string, 6 | getDone: () => boolean, 7 | } 8 | 9 | export const GRPCMapper = { 10 | subtaskFromGRPC: (subTask: GRPCSubTask, taskId: string): SubtaskDTO => ({ 11 | id: subTask.getId(), 12 | name: subTask.getName(), 13 | isDone: subTask.getDone(), 14 | taskId 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /customer/.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=https://api.customer.helpwave.de 2 | NEXT_PUBLIC_OIDC_PROVIDER=https://id.helpwave.de/realms/main 3 | NEXT_PUBLIC_CLIENT_ID=customer.helpwave.de 4 | NEXT_PUBLIC_REDIRECT_URI=https://customer.helpwave.de/auth/callback 5 | NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI=https://customer.helpwave.de/ 6 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51QqZfARtChvireAMzq4687T4m4TpyJvADX6IyM8A9u47lCUMMi0e2SNfxoSIDMM9DebiQCYwBa7mZRkJ50uZZjGC00hYO7FzL4 7 | -------------------------------------------------------------------------------- /api-services/environment.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | NODE_ENV?: string, 4 | NEXT_PUBLIC_API_URL?: string, 5 | NEXT_PUBLIC_REQUEST_LOGGING?: string, 6 | NEXT_PUBLIC_OAUTH_ISSUER_URL?: string, 7 | NEXT_PUBLIC_OAUTH_REDIRECT_URI?: string, 8 | NEXT_PUBLIC_OAUTH_CLIENT_ID?: string, 9 | NEXT_PUBLIC_OAUTH_SCOPES?: string, 10 | NEXT_PUBLIC_FAKE_TOKEN_ENABLE?: string, 11 | NEXT_PUBLIC_FAKE_TOKEN?: string, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api-services/mutations/users/user_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { QueryKeys } from '../query_keys' 3 | import { UserService } from '../../service/users/UserService' 4 | 5 | export const useUserQuery = (userId?: string) => { 6 | const enabled = !!userId 7 | return useQuery({ 8 | queryKey: [QueryKeys.users, userId], 9 | enabled, 10 | queryFn: async () => { 11 | return await UserService.get(userId!) 12 | }, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /customer/hocs/withCart.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from 'react' 2 | import { CartProvider } from '@/hooks/useCart' 3 | 4 | export function withCart(Component: ComponentType) { 5 | const WrappedComponent = (props: T) => ( 6 | 7 | 8 | 9 | ) 10 | WrappedComponent.displayName = `withCart(${Component.displayName || Component.name || 'Component'})` 11 | 12 | return WrappedComponent 13 | } 14 | -------------------------------------------------------------------------------- /api-services/mutations/query_keys.ts: -------------------------------------------------------------------------------- 1 | export const QueryKeys = { 2 | // Properties 3 | properties: 'properties', 4 | attachedProperties: 'attachedProperties', 5 | // Tasks 6 | wards: 'wards', 7 | rooms: 'rooms', 8 | beds: 'beds', 9 | patients: 'patients', 10 | tasks: 'tasks', 11 | taskTemplates: 'taskTemplates', 12 | // Users 13 | invitations: 'invitations', 14 | organizations: 'organizations', 15 | organization_members: 'organization_members', 16 | users: 'users' 17 | } 18 | -------------------------------------------------------------------------------- /documentation/scripts.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | The full list of our scripts can be found here. All scripts are contained 4 | in the [scripts folder](../scripts) 5 | 6 | ### Boilerplate generation 7 | This script is used to create the boilerplate for our components. Especially for the translations. 8 | 9 | Execution with 10 | - `node generate_boilerplate ` 11 | - `pnpm run generate ` (within the projects) 12 | 13 | All options can be seen with the `--help` flag 14 | -------------------------------------------------------------------------------- /customer/environment.d.ts: -------------------------------------------------------------------------------- 1 | // import '@helpwave/api-services/environment' 2 | // ^ This import extends the namespace of the ProcessEnv 3 | 4 | declare namespace NodeJS { 5 | interface ProcessEnv { 6 | NODE_ENV?: string, 7 | NEXT_PUBLIC_API_URL: string, 8 | NEXT_PUBLIC_OIDC_PROVIDER: string, 9 | NEXT_PUBLIC_CLIENT_ID: string, 10 | NEXT_PUBLIC_REDIRECT_URI: string, 11 | NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI: string, 12 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: string, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /customer/api/services/voucher.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from '@/api/config' 2 | import { VoucherHelpers } from '@/api/dataclasses/voucher' 3 | 4 | export const VoucherAPI = { 5 | get: async (id: string, headers: HeadersInit) => { 6 | const response = await fetch(`${API_URL}/voucher/${id}/`, { 7 | method: 'GET', 8 | headers, 9 | }) 10 | if (response.ok) { 11 | const data = (await response.json()) 12 | data['code'] = id 13 | return VoucherHelpers.fromJson(data) 14 | } 15 | throw response 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /tasks/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /customer/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /tasks/environment.d.ts: -------------------------------------------------------------------------------- 1 | // import '@helpwave/api-services/environment' 2 | // ^ This import extends the namespace of the ProcessEnv 3 | 4 | declare namespace NodeJS { 5 | interface ProcessEnv { 6 | NODE_ENV?: string, 7 | NEXT_PUBLIC_SHOW_STAGING_DISCLAIMER_MODAL?: string, 8 | NEXT_PUBLIC_PLAYSTORE_LINK?: string, 9 | NEXT_PUBLIC_APPSTORE_LINK?: string, 10 | NEXT_PUBLIC_FEEDBACK_FORM_URL?: string, 11 | NEXT_PUBLIC_FEATURES_FEED_URL?: string, 12 | NEXT_PUBLIC_IMPRINT_URL?: string, 13 | NEXT_PUBLIC_PRIVACY_URL?: string, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /customer/components/layout/Section.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react' 2 | import clsx from 'clsx' 3 | 4 | export type SectionProps = PropsWithChildren<{ className?: string, titleText?: string }> 5 | 6 | /** 7 | * A component for a section 8 | */ 9 | export const Section = ({ children, titleText, className }: SectionProps) => { 10 | return ( 11 |
12 | {titleText &&

{titleText}

} 13 | {children} 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | schedule: 9 | - cron: '0 0 * * 5' 10 | 11 | jobs: 12 | 13 | analyze: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup CodeQL 22 | uses: github/codeql-action/init@v2 23 | 24 | - name: Autobuild 25 | uses: github/codeql-action/autobuild@v2 26 | 27 | - name: Perform CodeQL Analysis 28 | uses: github/codeql-action/analyze@v2 29 | -------------------------------------------------------------------------------- /customer/api/mutations/voucher_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { QueryKeys } from '@/api/mutations/query_keys' 3 | import { useAuth } from '@/hooks/useAuth' 4 | import { VoucherAPI } from '@/api/services/voucher' 5 | 6 | export const useVoucherQuery = (code?: string) => { 7 | const { authHeader } = useAuth() 8 | return useQuery({ 9 | queryKey: [QueryKeys.voucher, code], 10 | enabled: code !== undefined, 11 | queryFn: async () => { 12 | if(code === undefined) { 13 | throw new Error('invalid parameter') 14 | } 15 | return await VoucherAPI.get(code, authHeader) 16 | }, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /api-services/types/tasks/room.ts: -------------------------------------------------------------------------------- 1 | import type { BedDTO, BedWithMinimalPatientDTO, BedWithPatientWithTasksNumberDTO } from './bed' 2 | 3 | export type RoomMinimalDTO = { 4 | id: string, 5 | name: string, 6 | wardId: string, 7 | } 8 | 9 | export type RoomDTO = RoomMinimalDTO & { 10 | beds: BedDTO[], 11 | } 12 | 13 | export type RoomOverviewDTO = RoomMinimalDTO & { 14 | beds: BedWithPatientWithTasksNumberDTO[], 15 | } 16 | 17 | export const emptyRoomOverview: RoomOverviewDTO = { 18 | id: '', 19 | name: '', 20 | wardId: '', 21 | beds: [] 22 | } 23 | 24 | export type RoomWithMinimalBedAndPatient = RoomMinimalDTO & { 25 | beds: BedWithMinimalPatientDTO[], 26 | } 27 | -------------------------------------------------------------------------------- /api-services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "allowJs": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "incremental": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "allowSyntheticDefaultImports": true, 17 | "noPropertyAccessFromIndexSignature": true, 18 | "noUncheckedIndexedAccess": true 19 | }, 20 | "include": [ 21 | "**/*.ts", 22 | "**/*.tsx", 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /api-services/types/tasks/tasks_templates.ts: -------------------------------------------------------------------------------- 1 | import type { SubtaskDTO } from './task' 2 | 3 | export type TaskTemplateDTO = { 4 | wardId?: string, 5 | id: string, 6 | name: string, 7 | notes: string, 8 | subtasks: SubtaskDTO[], 9 | isPublicVisible: boolean, 10 | creatorId: string, 11 | } 12 | 13 | export const emptyTaskTemplate: TaskTemplateDTO = { 14 | id: '', 15 | isPublicVisible: false, 16 | name: '', 17 | notes: '', 18 | subtasks: [], 19 | creatorId: '' 20 | } 21 | 22 | export type TaskTemplateFormType = { 23 | isValid: boolean, 24 | hasChanges: boolean, 25 | template: TaskTemplateDTO, 26 | wardId?: string, 27 | deletedSubtaskIds?: string[], 28 | } 29 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "dependencyDashboard": true, 7 | "dependencyDashboardLabels": [ "deps" ], 8 | "labels": [ "deps" ], 9 | "timezone": "Europe/Berlin", 10 | "schedule": [ "* 18-21 * * 5" ], 11 | "major": { 12 | "dependencyDashboardApproval": true 13 | }, 14 | "rangeStrategy": "pin", 15 | "packageRules": [ 16 | { 17 | "groupName": "all non-major dependencies", 18 | "matchUpdateTypes": [ "minor", "patch" ] 19 | }, 20 | { 21 | "groupName": "all major dependencies", 22 | "matchUpdateTypes": [ "major" ] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /api-services/types/users/invitations.ts: -------------------------------------------------------------------------------- 1 | import type { InvitationState } from '@helpwave/proto-ts/services/user_svc/v1/organization_svc_pb' 2 | import type { OrganizationDisplayableDTO } from './organizations' 3 | export { InvitationState } from '@helpwave/proto-ts/services/user_svc/v1/organization_svc_pb' 4 | 5 | export type InvitationWithOrganizationId = { 6 | id: string, 7 | email: string, 8 | organizationId: string, 9 | state: InvitationState, 10 | } 11 | 12 | export type Invitation = { 13 | id: string, 14 | email: string, 15 | organization: OrganizationDisplayableDTO, 16 | state: InvitationState, 17 | } 18 | 19 | export type InviteMemberType = { email: string, organizationId?: string } 20 | -------------------------------------------------------------------------------- /api-services/service/users/UserService.ts: -------------------------------------------------------------------------------- 1 | import { APIServices } from '../../services' 2 | import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' 3 | import { ReadPublicProfileRequest } from '@helpwave/proto-ts/services/user_svc/v1/user_svc_pb' 4 | import type { User } from '../../types/users/user' 5 | 6 | export const UserService = { 7 | get: async (id: string): Promise => { 8 | const req = new ReadPublicProfileRequest() 9 | .setId(id) 10 | 11 | const res = await APIServices.user.readPublicProfile(req, getAuthenticatedGrpcMetadata()) 12 | return { 13 | ...res.toObject(), 14 | avatarUrl: `https://cdn.helpwave.de/boringavatar.svg` 15 | } 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /api-services/mutations/users/organization_member_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { QueryKeys } from '../query_keys' 3 | import { useAuth } from '../../authentication/useAuth' 4 | import { OrganizationMemberService } from '../../service/users/OrganizationMemberService' 5 | 6 | export const useMembersByOrganizationQuery = () => { 7 | const { organization } = useAuth() 8 | const organizationId = organization?.id 9 | return useQuery({ 10 | queryKey: [QueryKeys.organizations, organizationId, 'members'], 11 | enabled: !!organizationId, 12 | queryFn: async () => { 13 | return await OrganizationMemberService.getByOrganization(organizationId!) 14 | }, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /tasks/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | 1. Execute the following commands 4 | ``` 5 | pnpm install 6 | cd tasks/ 7 | pnpm run dev 8 | ``` 9 | 2. open http://localhost:3000 10 | 11 | ## Environment variables 12 | 13 | > See `utils/config.ts` for more. 14 | 15 | Create a file called `env.local` from the already existing `.env` file. 16 | 17 | At build time and at runtime the correctness of the environment variables is validated, make sure that they are present at each step (even in github actions or similar). 18 | 19 | If something is incorrect or missing an error will be thrown. 20 | 21 | ### gRPC-web 22 | 23 | To communicate with [`helpwave-services`](https://github.com/helpwave/services) (our gRPC APIs) this projects uses gRPC-web. 24 | 25 | -------------------------------------------------------------------------------- /customer/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | 1. Execute the following commands 4 | ``` 5 | pnpm install 6 | cd customer/ 7 | pnpm run dev 8 | ``` 9 | 2. open http://localhost:3000 10 | 11 | ## Environment variables 12 | 13 | > See `utils/config.ts` for more. 14 | 15 | Create a file called `env.local` from the already existing `.env` file. 16 | 17 | At build time and at runtime the correctness of the environment variables is validated, make sure that they are present at each step (even in github actions or similar). 18 | 19 | If something is incorrect or missing an error will be thrown. 20 | 21 | ### gRPC-web 22 | 23 | To communicate with [`helpwave-services`](https://github.com/helpwave/services) (our gRPC APIs) this projects uses gRPC-web. 24 | 25 | -------------------------------------------------------------------------------- /api-services/authentication/grpc_metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'grpc-web' 2 | import { KeycloakService } from '../util/keycloak' 3 | 4 | type AuthenticatedGrpcMetadata = { 5 | Authorization: string, 6 | } 7 | 8 | export const getAuthenticatedGrpcMetadata = (): AuthenticatedGrpcMetadata => { 9 | const token = KeycloakService.getCurrentTokenAndUpdateInBackground() 10 | return { Authorization: `Bearer ${token}` } 11 | } 12 | 13 | export const grpcWrapper = async (rpc: (msg: ReqMsg, metadata?: Metadata) => Promise, msg: ReqMsg, metadata?: Metadata|undefined): Promise => { 14 | const token = KeycloakService.getCurrentTokenAndUpdateInBackground() 15 | return rpc(msg, { Authorization: `Bearer ${token}`, ...metadata }) 16 | } 17 | -------------------------------------------------------------------------------- /tasks/components/dnd-kit/Sortable.tsx: -------------------------------------------------------------------------------- 1 | import { useSortable } from '@dnd-kit/sortable' 2 | import type { PropsWithChildren } from 'react' 3 | 4 | type SortableProps = { 5 | id: string, 6 | } 7 | 8 | /** 9 | * A component for the dnd-kit sortable context 10 | * 11 | * Makes the children draggable within the sortable context 12 | */ 13 | export const Sortable = ({ children, id }: PropsWithChildren) => { 14 | const { attributes, listeners, setNodeRef, transform } = useSortable({ 15 | id, 16 | }) 17 | 18 | const style = { 19 | transform: `translate3d(${transform?.x ?? 0}px, ${transform?.y ?? 0}px, 0)` 20 | } 21 | 22 | return ( 23 |
24 | {children} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /tasks/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json", 3 | "name": "helpwave tasks", 4 | "short_name": "helpwave tasks", 5 | "lang": "en", 6 | "scope": "/", 7 | "start_url": "/", 8 | "description": "The first open-source team management platform for healthcare workers", 9 | "orientation": "portrait", 10 | "categories": ["software", "medical", "health", "healthcare", "company", "management", "team", "workers"], 11 | "background_color": "#281c20", 12 | "theme_color": "#281c20", 13 | "display": "browser", 14 | "display_override": ["fullscreen", "minimal-ui"], 15 | "prefer_related_applications": true, 16 | "related_applications": [], 17 | "protocol_handlers": [], 18 | "screenshots": [], 19 | "shortcuts" : [] 20 | } 21 | -------------------------------------------------------------------------------- /customer/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json", 3 | "name": "helpwave customer", 4 | "short_name": "helpwave customer", 5 | "lang": "en", 6 | "scope": "/", 7 | "start_url": "/", 8 | "description": "The first open-source team management platform for healthcare workers", 9 | "orientation": "portrait", 10 | "categories": ["software", "medical", "health", "healthcare", "company", "management", "team", "workers"], 11 | "background_color": "#281c20", 12 | "theme_color": "#281c20", 13 | "display": "browser", 14 | "display_override": ["fullscreen", "minimal-ui"], 15 | "prefer_related_applications": true, 16 | "related_applications": [], 17 | "protocol_handlers": [], 18 | "screenshots": [], 19 | "shortcuts" : [] 20 | } 21 | -------------------------------------------------------------------------------- /tasks/components/layout/property/SubjectTypeIcon.tsx: -------------------------------------------------------------------------------- 1 | import type { LucideProps } from 'lucide-react' 2 | import { NotepadText, UserRound } from 'lucide-react' 3 | import type { PropertySubjectType } from '@helpwave/api-services/types/properties/property' 4 | 5 | export type SubjectTypeIconProps = { 6 | subjectType: PropertySubjectType, 7 | } & LucideProps 8 | 9 | export const SubjectTypeIcon = ({ subjectType, ...props }: SubjectTypeIconProps) => { 10 | switch (subjectType) { 11 | /* 12 | case 'organization': return () 13 | case 'ward': return () 14 | case 'room': return () 15 | case 'bed': return () 16 | */ 17 | case 'patient': return () 18 | case 'task': return () 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api-services/service/users/OrganizationMemberService.ts: -------------------------------------------------------------------------------- 1 | import { APIServices } from '../../services' 2 | import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' 3 | import type { OrganizationMember } from '../../types/users/organization_member' 4 | import { GetMembersByOrganizationRequest } from '@helpwave/proto-ts/services/user_svc/v1/organization_svc_pb' 5 | 6 | export const OrganizationMemberService = { 7 | getByOrganization: async (id: string): Promise => { 8 | const req = new GetMembersByOrganizationRequest() 9 | .setId(id) 10 | 11 | const res = await APIServices.organization.getMembersByOrganization(req, getAuthenticatedGrpcMetadata()) 12 | 13 | return res.getMembersList().map(member => ({ 14 | ...member.toObject(), 15 | avatarURL: 'https://cdn.helpwave.de/boringavatar.svg', // TODO remove later 16 | })) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /api-services/types/users/organizations.ts: -------------------------------------------------------------------------------- 1 | import type { OrganizationMember, OrganizationMemberMinimalDTO } from './organization_member' 2 | 3 | export type OrganizationMinimalDTO = { 4 | id: string, 5 | shortName: string, 6 | longName: string, 7 | contactEmail: string, 8 | isPersonal: boolean, 9 | avatarURL?: string, 10 | } 11 | 12 | export type OrganizationDTO = OrganizationMinimalDTO & { 13 | members: OrganizationMember[], 14 | } 15 | 16 | export const emptyOrganization: OrganizationDTO = { 17 | id: '', 18 | shortName: '', 19 | longName: '', 20 | contactEmail: '', 21 | isPersonal: false, 22 | members: [] 23 | } 24 | 25 | export type OrganizationWithMinimalMemberDTO = OrganizationMinimalDTO & { 26 | members: OrganizationMemberMinimalDTO[], 27 | } 28 | 29 | export type OrganizationDisplayableDTO = { 30 | id: string, 31 | longName: string, 32 | avatarURL: string, 33 | } 34 | -------------------------------------------------------------------------------- /customer/api/mutations/invoice_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { QueryKeys } from '@/api/mutations/query_keys' 3 | import { InvoiceAPI } from '@/api/services/invoice' 4 | import { useAuth } from '@/hooks/useAuth' 5 | 6 | export const useMyInvoicesQuery = () => { 7 | const { authHeader } = useAuth() 8 | return useQuery({ 9 | queryKey: [QueryKeys.invoice, 'all'], 10 | queryFn: async () => { 11 | return InvoiceAPI.getMyInvoices(authHeader) 12 | }, 13 | }) 14 | } 15 | 16 | export const useInvoiceStatusQuery = (id?: string) => { 17 | const { authHeader } = useAuth() 18 | return useQuery({ 19 | queryKey: [QueryKeys.invoice, 'status', id], 20 | enabled: id !== undefined, 21 | queryFn: async () => { 22 | if (id === undefined) { 23 | throw new Error('Invoice ID is required') 24 | } 25 | return InvoiceAPI.getStatus(id, authHeader) 26 | }, 27 | }) 28 | } 29 | 30 | -------------------------------------------------------------------------------- /api-services/types/tasks/bed.ts: -------------------------------------------------------------------------------- 1 | import type { PatientDTO, PatientMinimalDTO, PatientWithTasksNumberDTO } from './patient' 2 | 3 | export type BedMinimalDTO = { 4 | id: string, 5 | name: string, 6 | } 7 | 8 | export type BedDTO = BedMinimalDTO & { 9 | patient?: PatientDTO, 10 | } 11 | 12 | export const emptyBed: BedDTO = { 13 | id: '', 14 | name: '', 15 | patient: undefined 16 | } 17 | 18 | export type BedWithPatientWithTasksNumberDTO = BedMinimalDTO & { 19 | patient?: PatientWithTasksNumberDTO, 20 | } 21 | 22 | export const emptyBedWithPatientWithTasksNumber: BedWithPatientWithTasksNumberDTO = { 23 | id: '', 24 | name: '', 25 | patient: undefined 26 | } 27 | 28 | export type BedWithRoomId = BedMinimalDTO & { 29 | roomId: string, 30 | } 31 | 32 | export type BedWithMinimalPatientDTO = BedMinimalDTO & { 33 | patient?: PatientMinimalDTO, 34 | } 35 | 36 | export type BedWithPatientId = { 37 | bedId: string, 38 | patientId: string, 39 | } 40 | -------------------------------------------------------------------------------- /tasks/components/layout/InitializationChecker.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react' 2 | import { useAuth } from '@helpwave/api-services/authentication/useAuth' 3 | import type { Translation } from '@helpwave/hightide' 4 | import { useTranslation } from '@helpwave/hightide' 5 | import { LoadingAnimation } from '@helpwave/hightide' 6 | 7 | const defaultTranslation: Translation<{ loggingIn: string }> = { 8 | en: { 9 | loggingIn: 'Logging In...', 10 | }, 11 | de: { 12 | loggingIn: 'Einloggen...', 13 | }, 14 | } 15 | 16 | export const InitializationChecker = ({ children }: PropsWithChildren) => { 17 | const translation = useTranslation([defaultTranslation]) 18 | const { user } = useAuth() 19 | 20 | if(!user) { 21 | return ( 22 |
23 | 24 |
25 | ) 26 | } 27 | return children 28 | } 29 | -------------------------------------------------------------------------------- /tasks/components/cards/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from '@helpwave/hightide' 2 | import type { User } from '@helpwave/api-services/authentication/useAuth' 3 | 4 | export type UserCardProps = { 5 | user: User, 6 | } 7 | 8 | /** 9 | * A Card showing the Avatar and name of a user 10 | */ 11 | const UserCard = ({ user }: UserCardProps) => { 12 | return ( 13 |
14 |
15 | 16 |
17 |
18 |
{user.nickname}
19 |
{user.name}
20 |
{user.email}
21 |
22 |
23 | ) 24 | } 25 | 26 | export { UserCard } 27 | -------------------------------------------------------------------------------- /api-services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@helpwave/api-services", 3 | "version": "0.0.1", 4 | "main": "index.ts", 5 | "scripts": { 6 | "lint": "tsc --noEmit && eslint ." 7 | }, 8 | "type": "module", 9 | "dependencies": { 10 | "@helpwave/hightide": "^0.0.17", 11 | "@helpwave/proto-ts": "0.64.0-89e2023.0", 12 | "@tanstack/react-query": "^4.36.1", 13 | "@tanstack/react-query-devtools": "^4.36.1", 14 | "@types/google-protobuf": "3.15.12", 15 | "cookies-next": "2.1.2", 16 | "google-protobuf": "3.21.4", 17 | "grpc-web": "1.5.0", 18 | "keycloak-js": "25.0.6", 19 | "oauth4webapi": "2.17.0", 20 | "react": "18.3.1", 21 | "rxjs": "7.8.1", 22 | "typescript": "5.7.2", 23 | "zod": "3.24.1" 24 | }, 25 | "devDependencies": { 26 | "@helpwave/eslint-config": "^0.0.11", 27 | "@types/js-cookie": "3.0.6", 28 | "@types/node": "20.17.10", 29 | "@types/react": "18.3.17", 30 | "eslint": "9.17.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tasks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": [ 6 | "esnext", 7 | "dom", 8 | "dom.iterable" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "allowSyntheticDefaultImports": true, 22 | "noPropertyAccessFromIndexSignature": true, 23 | "noUncheckedIndexedAccess": true, 24 | "plugins": [ 25 | { 26 | "name": "next" 27 | } 28 | ], 29 | "paths": { 30 | "@/*": [ 31 | "./*" 32 | ] 33 | } 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | "public/ga.js" 40 | ], 41 | "exclude": [] 42 | } 43 | -------------------------------------------------------------------------------- /customer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": [ 6 | "esnext", 7 | "dom", 8 | "dom.iterable" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "allowSyntheticDefaultImports": true, 22 | "noPropertyAccessFromIndexSignature": true, 23 | "noUncheckedIndexedAccess": true, 24 | "plugins": [ 25 | { 26 | "name": "next" 27 | } 28 | ], 29 | "paths": { 30 | "@/*": [ 31 | "./*" 32 | ] 33 | } 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | "public/ga.js" 40 | ], 41 | "exclude": [] 42 | } 43 | -------------------------------------------------------------------------------- /tasks/hooks/useRouteParameters.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | 3 | /** 4 | * THIS SHOULD ONLY BE USED IN PAGES! 5 | * 6 | * The reason for this is that with pages you can be sure by looking at the URL which parameters are present. 7 | * This is not the case for arbitrary components and thus using this hook invites a whole bunch of incorrect types 8 | * and usages. 9 | * 10 | * In general the router should only be used in pages if possible, an exception is pushing new routes to the router. 11 | * 12 | * The `PathParameters` generic is used to specify which parameters are present in the URL path (`/route/[id]/subroute`) by key. 13 | * The `QueryParameters` generic is used to specify which parameters are present in the URL querystring (`?id=123`) by key. 14 | */ 15 | export const useRouteParameters = () => { 16 | return useRouter().query as Record & Record 17 | } 18 | -------------------------------------------------------------------------------- /api-services/types/tasks/wards.ts: -------------------------------------------------------------------------------- 1 | export type WardMinimalDTO = { 2 | id: string, 3 | name: string, 4 | } 5 | 6 | export type WardWithOrganizationIdDTO = WardMinimalDTO & { 7 | organizationId: string, 8 | } 9 | 10 | export type WardOverviewDTO = WardMinimalDTO & { 11 | bedCount: number, 12 | unscheduled: number, 13 | inProgress: number, 14 | done: number, 15 | } 16 | 17 | export const emptyWardOverview: WardOverviewDTO = { 18 | id: '', 19 | name: '', 20 | bedCount: 0, 21 | unscheduled: 0, 22 | inProgress: 0, 23 | done: 0 24 | } 25 | 26 | export type WardDetailDTO = WardMinimalDTO & { 27 | rooms: { 28 | id: string, 29 | name: string, 30 | beds: { 31 | id: string, 32 | }[], 33 | }[], 34 | task_templates: { 35 | id: string, 36 | name: string, 37 | subtasks: { 38 | id: string, 39 | name: string, 40 | }[], 41 | }[], 42 | } 43 | 44 | export const emptyWard: WardDetailDTO = { 45 | id: '', 46 | name: '', 47 | rooms: [], 48 | task_templates: [] 49 | } 50 | -------------------------------------------------------------------------------- /tasks/components/dnd-kit/Droppable.tsx: -------------------------------------------------------------------------------- 1 | import { useDroppable, type Active, type Over } from './typesafety' 2 | 3 | export type DroppableBuilderProps = { 4 | isOver: boolean, 5 | active: Active | null, 6 | over: Over | null, 7 | } 8 | 9 | export type DroppableProps = { 10 | children: ((droppableBuilderProps: DroppableBuilderProps) => React.ReactNode | undefined), 11 | id: string, 12 | data: DroppableData, 13 | className?: string, 14 | } 15 | 16 | /** 17 | * A Component for the dnd kit droppable 18 | */ 19 | export const Droppable = ({ 20 | children, 21 | id, 22 | data, 23 | className, 24 | }: DroppableProps) => { 25 | const { setNodeRef, ...droppableBuilderProps } = useDroppable({ id, data }) 26 | 27 | return ( 28 |
29 | {children(droppableBuilderProps)} 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile.tasks: -------------------------------------------------------------------------------- 1 | FROM node:22.15.1 AS build 2 | 3 | ENV NEXT_TELEMETRY_DISABLED=1 4 | ARG WS="@helpwave/tasks" 5 | 6 | RUN npm install -g pnpm@9 7 | 8 | WORKDIR /web 9 | 10 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ 11 | COPY tasks/package.json ./tasks/ 12 | COPY api-services/package.json ./api-services/ 13 | 14 | RUN CI=1 pnpm install 15 | 16 | COPY tasks ./tasks/ 17 | COPY api-services ./api-services/ 18 | 19 | RUN pnpm --filter $WS run build 20 | 21 | 22 | FROM node:22.15.1-alpine 23 | 24 | LABEL maintainer="tech@helpwave.de" 25 | 26 | ENV NEXT_TELEMETRY_DISABLED=1 27 | 28 | WORKDIR /web 29 | 30 | RUN addgroup -g 1001 nodejs && \ 31 | adduser -S -u 1001 -G nodejs nextjs 32 | 33 | COPY --from=build --chown=nextjs:nodejs /web/tasks/build/standalone ./ 34 | COPY --from=build --chown=nextjs:nodejs /web/tasks/public ./tasks/public 35 | COPY --from=build --chown=nextjs:nodejs /web/tasks/build/static ./tasks/build/static 36 | 37 | USER nextjs 38 | 39 | EXPOSE 80 40 | ENV PORT=80 41 | ENV HOSTNAME=0.0.0.0 42 | ENV NODE_ENV=production 43 | 44 | CMD ["node", "tasks/server.js"] 45 | -------------------------------------------------------------------------------- /tasks/components/cards/AddCard.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { Plus } from 'lucide-react' 3 | 4 | export type AddCardProps = { 5 | onClick?: () => void, 6 | text?: string, 7 | isSelected?: boolean, 8 | className?: string, 9 | } 10 | 11 | /** 12 | * A Card for adding something the text shown is configurable 13 | */ 14 | export const AddCard = ({ 15 | onClick, 16 | text, 17 | isSelected = false, 18 | className, 19 | }: AddCardProps) => { 20 | return ( 21 |
33 | 34 | {text && {text}} 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /customer/components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | import clsx from 'clsx' 3 | 4 | export type HeaderProps = { 5 | leading?: ReactNode, 6 | leftSide?: ReactNode[], 7 | rightSide?: ReactNode[], 8 | className?: string, 9 | } 10 | 11 | /** 12 | * A header component 13 | */ 14 | export const Header = ({ leading, leftSide, rightSide, className }: HeaderProps) => { 15 | return ( 16 |
22 |
23 | {leading} 24 | {leading && leftSide && (
)} 25 | {leftSide && ( 26 |
27 | {leftSide} 28 |
29 | )} 30 |
31 | {rightSide && ( 32 |
33 | {rightSide} 34 |
35 | )} 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /customer/api/dataclasses/voucher.ts: -------------------------------------------------------------------------------- 1 | export type Voucher = { 2 | uuid: string, 3 | code: string, 4 | description: string, 5 | productPlanUUID: string, 6 | discountPercentage?: number, 7 | discountFixedAmount?: number, 8 | valid: boolean, 9 | } 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | function fromJson(json: any): Voucher { 13 | return { 14 | uuid: json.uuid, 15 | code: json.code, 16 | description: json.description, 17 | productPlanUUID: json.product_plan_uuid, 18 | discountPercentage: json.discount_percentage, 19 | discountFixedAmount: json.discount_fixed_amount, 20 | valid: json.valid, 21 | } 22 | } 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | function toJson(voucher: Voucher): any { 26 | return { 27 | uuid: voucher.uuid, 28 | description: voucher.description, 29 | product_plan_uuid: voucher.productPlanUUID, 30 | discount_percentage: voucher.discountPercentage, 31 | discount_fixed_amount: voucher.discountFixedAmount, 32 | valid: voucher.valid, 33 | } 34 | } 35 | 36 | export const VoucherHelpers = { toJson, fromJson } 37 | -------------------------------------------------------------------------------- /tasks/components/FeedbackButton.tsx: -------------------------------------------------------------------------------- 1 | import { SolidButton } from '@helpwave/hightide' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 3 | import type { Translation } from '@helpwave/hightide' 4 | import { getConfig } from '@/utils/config' 5 | 6 | type FeedbackButtonTranslation = { 7 | text: string, 8 | } 9 | 10 | const defaultFeedbackButtonTranslation: Translation = { 11 | en: { 12 | text: 'Issue or Feedback?' 13 | }, 14 | de: { 15 | text: 'Fehler oder Feedback?' 16 | } 17 | } 18 | 19 | type FeedbackButtonProps = { 20 | className?: string, 21 | } 22 | 23 | export const FeedbackButton = ({ overwriteTranslation, className }: PropsForTranslation) => { 24 | const config = getConfig() 25 | const translation = useTranslation([defaultFeedbackButtonTranslation], overwriteTranslation) 26 | 27 | const onClick = () => window.open(config.feedbackFormUrl, '_blank') 28 | 29 | return ( 30 | 31 | {translation('text')} 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /customer/api/mutations/product_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { QueryKeys } from '@/api/mutations/query_keys' 3 | import { ProductAPI } from '@/api/services/product' 4 | import { useAuth } from '@/hooks/useAuth' 5 | 6 | export const useProductsAllQuery = () => { 7 | const { authHeader } = useAuth() 8 | return useQuery({ 9 | queryKey: [QueryKeys.product, 'all'], 10 | queryFn: async () => { 11 | return await ProductAPI.getAll(authHeader) 12 | }, 13 | }) 14 | } 15 | 16 | export const useProductsAvailableQuery = () => { 17 | const { authHeader } = useAuth() 18 | return useQuery({ 19 | queryKey: [QueryKeys.product, 'available'], 20 | queryFn: async () => { 21 | return await ProductAPI.getAvailable(authHeader) 22 | }, 23 | }) 24 | } 25 | 26 | export const useProductQuery = (id?: string) => { 27 | const { authHeader } = useAuth() 28 | return useQuery({ 29 | queryKey: [QueryKeys.product, id], 30 | enabled: id !== undefined, 31 | queryFn: async () => { 32 | if(id === undefined) { 33 | throw new Error('invalid id') 34 | } 35 | return await ProductAPI.get(id, authHeader) 36 | }, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /tasks/components/cards/WardCard.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { Bed } from 'lucide-react' 3 | import type { WardOverviewDTO } from '@helpwave/api-services/types/tasks/wards' 4 | import { PillLabelBox } from '../PillLabel' 5 | import { EditCard, type EditCardProps } from './EditCard' 6 | 7 | export type WardCardProps = EditCardProps & { 8 | ward: WardOverviewDTO, 9 | } 10 | 11 | /** 12 | * A Card showing the information about a ward 13 | */ 14 | export const WardCard = ({ 15 | ward, 16 | className, 17 | ...editCardProps 18 | }: WardCardProps) => { 19 | return ( 20 | 21 |
22 |
23 | {ward.name} 24 |
25 |
26 | 27 | {ward.bedCount} 28 |
29 | 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /api-services/mutations/properties/property_view_src_mutations.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationOptions } from '@tanstack/react-query' 2 | import { useMutation, useQueryClient } from '@tanstack/react-query' 3 | import { QueryKeys } from '../query_keys' 4 | import type { PropertySubjectType } from '../../types/properties/property' 5 | import type { 6 | PropertyViewRuleFilterUpdate 7 | } from '../../service/properties/PropertyViewSourceService' 8 | import { 9 | PropertyViewSourceService 10 | } from '../../service/properties/PropertyViewSourceService' 11 | 12 | export const useUpdatePropertyViewRuleRequest = (subjectType: PropertySubjectType, wardId?: string, options?: UseMutationOptions) => { 13 | const queryClient = useQueryClient() 14 | return useMutation({ 15 | ...options, 16 | mutationFn: async (update: PropertyViewRuleFilterUpdate) => { 17 | return await PropertyViewSourceService.update(update, subjectType, wardId) 18 | }, 19 | onSuccess: (data, variables, context) => { 20 | if(options?.onSuccess) { 21 | options.onSuccess(data, variables, context) 22 | } 23 | queryClient.invalidateQueries([QueryKeys.properties]).catch(console.error) 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /tasks/components/dnd-kit/Draggable.tsx: -------------------------------------------------------------------------------- 1 | import { useDraggable, type Active, type Over } from './typesafety' 2 | 3 | export type DraggableBuilderProps = { 4 | isDragging: boolean, 5 | active: Active | null, 6 | over: Over | null, 7 | } 8 | 9 | export type DraggableProps = { 10 | children: ((draggableBuilderProps: DraggableBuilderProps) => React.ReactNode | undefined), 11 | id: string, 12 | data: DraggableData, 13 | className?: string, 14 | } 15 | 16 | /** 17 | * A Component for the dnd kit draggable 18 | */ 19 | export const Draggable = ({ 20 | children, 21 | id, 22 | data, 23 | className, 24 | }: DraggableProps) => { 25 | const { attributes, listeners, setNodeRef, transform, ...draggableBuilderProps } = useDraggable({ 26 | id, 27 | data 28 | }) 29 | 30 | const style = { 31 | transform: `translate3d(${transform?.x ?? 0}px, ${transform?.y ?? 0}px, 0)` 32 | } 33 | 34 | return ( 35 |
36 | {children(draggableBuilderProps)} 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /tasks/translation/medical.ts: -------------------------------------------------------------------------------- 1 | import type { Translation, TranslationPlural } from '@helpwave/hightide' 2 | 3 | export type MedicalTranslationType = { 4 | organization: TranslationPlural, 5 | ward: TranslationPlural, 6 | room: TranslationPlural, 7 | bed: TranslationPlural, 8 | patient: TranslationPlural, 9 | } 10 | 11 | export const medicalTranslation: Translation = { 12 | en: { 13 | organization: { 14 | one: 'Organization', 15 | other: 'Organizations' 16 | }, 17 | ward: { 18 | one: 'Ward', 19 | other: 'Wards' 20 | }, 21 | room: { 22 | one: 'Room', 23 | other: 'Rooms' 24 | }, 25 | bed: { 26 | one: 'Bed', 27 | other: 'Beds' 28 | }, 29 | patient: { 30 | one: 'Patient', 31 | other: 'Patients' 32 | }, 33 | }, 34 | de: { 35 | organization: { 36 | one: 'Organisation', 37 | other: 'Organisationen' 38 | }, 39 | ward: { 40 | one: 'Station', 41 | other: 'Stationen' 42 | }, 43 | room: { 44 | one: 'Zimmer', 45 | other: 'Zimmer' 46 | }, 47 | bed: { 48 | one: 'Bett', 49 | other: 'Betten' 50 | }, 51 | patient: { 52 | one: 'Patient', 53 | other: 'Patienten' 54 | }, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /customer/api/dataclasses/user_seat.ts: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | 3 | export const userRoles = ['admin' , 'user'] as const 4 | 5 | /** 6 | * Defines the possible roles a user can have. 7 | */ 8 | export type UserRole = typeof userRoles[number] 9 | 10 | export type UserRoleTranslation = Record 11 | 12 | export const defaultUserRoleTranslation: Translation = { 13 | en: { 14 | user: 'User', 15 | admin: 'Admin' 16 | }, 17 | de: { 18 | user: 'Nutzer', 19 | admin: 'Admin' 20 | } 21 | } 22 | 23 | /** 24 | * Represents a user seat in the system. 25 | */ 26 | export type UserSeat = { 27 | /** 28 | * Unique identifier for the customer associated with this user. 29 | */ 30 | customerUUID: string, 31 | 32 | /** 33 | * Email address of the user. 34 | */ 35 | email: string, 36 | 37 | /** 38 | * First name of the user. 39 | */ 40 | firstName: string, 41 | 42 | /** 43 | * Last name of the user. 44 | */ 45 | lastName: string, 46 | 47 | /** 48 | * Role assigned to the user, which determines their level of access. 49 | */ 50 | role: UserRole, 51 | 52 | /** 53 | * Indicates whether the user account is enabled or disabled. 54 | */ 55 | enabled: boolean, 56 | }; 57 | -------------------------------------------------------------------------------- /customer/api/services/product.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from '@/api/config' 2 | import { ProductHelpers } from '@/api/dataclasses/product' 3 | 4 | export const ProductAPI = { 5 | get: async (id: string, headers: HeadersInit) => { 6 | const response = await fetch(`${API_URL}/product/${id}/`, { 7 | method: 'GET', 8 | headers, 9 | }) 10 | if (response.ok) { 11 | return ProductHelpers.fromJson(await response.json()) 12 | } 13 | throw response 14 | }, 15 | getAll: async (headers: HeadersInit) => { 16 | const response = await fetch(`${API_URL}/product/`, { 17 | method: 'GET', 18 | headers, 19 | }) 20 | if (response.ok) { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | return (await response.json() as any[]).map(value => ProductHelpers.fromJson(value)) 23 | } 24 | throw response 25 | }, 26 | getAvailable: async (headers: HeadersInit) => { 27 | const response = await fetch(`${API_URL}/product/available/`, { 28 | method: 'GET', 29 | headers, 30 | }) 31 | if (response.ok) { 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | return (await response.json() as any[]).map(value => ProductHelpers.fromJson(value)) 34 | } 35 | throw response 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /tasks/components/dnd-kit-instances/tasks.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DragStartEvent as DragStartEventFree, 3 | DragMoveEvent as DragMoveEventFree, 4 | DragOverEvent as DragOverEventFree, 5 | DragEndEvent as DragEndEventFree, 6 | DragCancelEvent as DragCancelEventFree 7 | } from '@/components/dnd-kit/typesafety' 8 | import { DndContext as DndContextFree } from '@/components/dnd-kit/typesafety' 9 | import { Droppable as DroppableFree } from '@/components/dnd-kit/Droppable' 10 | import { Draggable as DraggableFree } from '@/components/dnd-kit/Draggable' 11 | 12 | // TODO: what should these values be? (see TasksKanbanBoard.tsx) 13 | type DraggableData = never 14 | type DroppableData = never 15 | 16 | export const Droppable = DroppableFree 17 | export const Draggable = DraggableFree 18 | export const DndContext = DndContextFree 19 | 20 | export type DragStartEvent = DragStartEventFree 21 | export type DragMoveEvent = DragMoveEventFree 22 | export type DragOverEvent = DragOverEventFree 23 | export type DragEndEvent = DragEndEventFree 24 | export type DragCancelEvent = DragCancelEventFree 25 | -------------------------------------------------------------------------------- /customer/api/mutations/contract_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { QueryKeys } from '@/api/mutations/query_keys' 3 | import { ContractsAPI } from '@/api/services/contract' 4 | import { useAuth } from '@/hooks/useAuth' 5 | 6 | export const useContractQuery = (id?: string) => { 7 | const { authHeader } = useAuth() 8 | return useQuery({ 9 | queryKey: [QueryKeys.contract, id], 10 | enabled: id !== undefined, 11 | queryFn: async () => { 12 | if(id === undefined) { 13 | throw new Error('invalid parameter') 14 | } 15 | return await ContractsAPI.get(id, authHeader) 16 | }, 17 | }) 18 | } 19 | 20 | export const useContractsQuery = () => { 21 | const { authHeader } = useAuth() 22 | return useQuery({ 23 | queryKey: [QueryKeys.contract, 'all'], 24 | queryFn: async () => { 25 | return await ContractsAPI.getAll(authHeader) 26 | }, 27 | }) 28 | } 29 | 30 | export const useContractsForProductsQuery = (productIds: string[]) => { 31 | const { authHeader } = useAuth() 32 | return useQuery({ 33 | queryKey: [QueryKeys.contract, ...productIds], 34 | enabled: productIds.length > 0, 35 | queryFn: async () => { 36 | return await ContractsAPI.getAllByProductIds(productIds, authHeader) 37 | }, 38 | }) 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /tasks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@helpwave/tasks", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "tsc --noEmit && eslint .", 11 | "generate": "node ../scripts/generate_boilerplate.js" 12 | }, 13 | "dependencies": { 14 | "@dnd-kit/core": "6.3.1", 15 | "@dnd-kit/sortable": "7.0.2", 16 | "@dnd-kit/modifiers": "9.0.0", 17 | "@helpwave/api-services": "workspace:*", 18 | "@helpwave/hightide": "0.1.36", 19 | "@tailwindcss/postcss": "4.1.3", 20 | "@tanstack/react-query": "4.36.1", 21 | "@tanstack/react-query-devtools": "4.36.1", 22 | "@tanstack/react-table": "8.21.3", 23 | "clsx": "2.1.1", 24 | "lucide-react": "0.468.0", 25 | "next": "15.4.7", 26 | "postcss": "8.5.3", 27 | "react": "18.3.1", 28 | "react-custom-scrollbars-2": "4.5.0", 29 | "react-device-detect": "2.2.3", 30 | "react-dom": "18.3.1", 31 | "tailwindcss": "4.1.3", 32 | "typescript": "5.7.2", 33 | "zod": "3.25.73" 34 | }, 35 | "devDependencies": { 36 | "@helpwave/eslint-config": "^0.0.11", 37 | "@types/node": "20.17.10", 38 | "@types/react": "18.3.17", 39 | "@types/react-dom": "18.3.5", 40 | "eslint": "9.17.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /customer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@helpwave/customer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "tsc --noEmit && eslint ." 11 | }, 12 | "dependencies": { 13 | "@helpwave/hightide": "^0.0.17", 14 | "@radix-ui/react-checkbox": "1.1.3", 15 | "@stripe/react-stripe-js": "^3.6.0", 16 | "@stripe/stripe-js": "^7.0.0", 17 | "@tailwindcss/postcss": "^4.1.5", 18 | "@tanstack/react-query": "4.36.1", 19 | "clsx": "^2.1.1", 20 | "csstype": "3.1.3", 21 | "js-cookie": "^3.0.5", 22 | "lucide-react": "0.468.0", 23 | "next": "^15.2.4", 24 | "oidc-client-ts": "^3.1.0", 25 | "react": "18.3.1", 26 | "react-custom-scrollbars-2": "4.5.0", 27 | "react-dom": "18.3.1", 28 | "react-hook-form": "^7.55.0", 29 | "react-hot-toast": "2.4.1", 30 | "react-intersection-observer": "9.14.0", 31 | "tailwindcss": "^4.1.3", 32 | "vanilla-cookieconsent": "3.0.1" 33 | }, 34 | "devDependencies": { 35 | "@helpwave/eslint-config": "^0.0.11", 36 | "@types/js-cookie": "^3.0.5", 37 | "@types/node": "20.17.10", 38 | "@types/react": "18.3.17", 39 | "@types/react-dom": "18.3.5", 40 | "eslint": "9.17.0", 41 | "typescript": "5.7.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /customer/utils/locale.ts: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | 3 | export type LocaleFormattingTranslation = { 4 | formatMoney: (amount: number, currency?: string) => string, 5 | formatDate: (date?: Date) => string | null, 6 | }; 7 | 8 | const createMoneyFormatter = (locale: string) => 9 | (amount: number, currency: string = 'EUR'): string => 10 | new Intl.NumberFormat(locale, { 11 | style: 'currency', 12 | currency 13 | }).format(amount) 14 | 15 | const createDateFormatter = (locale: string) => 16 | (date?: Date): string | null => { 17 | if (!date) return null 18 | 19 | const options: Intl.DateTimeFormatOptions = { 20 | year: 'numeric', 21 | month: '2-digit', 22 | day: '2-digit', 23 | hour: '2-digit', 24 | minute: '2-digit', 25 | hour12: locale === 'en-US' 26 | } 27 | 28 | const formatted = date.toLocaleString(locale, options) 29 | return locale === 'de-DE' ? formatted.replace(/(\d{2}:\d{2})$/, '$1 Uhr') : formatted 30 | } 31 | 32 | export const defaultLocaleFormatters: Translation = { 33 | en: { 34 | formatMoney: createMoneyFormatter('en-US'), 35 | formatDate: createDateFormatter('en-US') 36 | }, 37 | de: { 38 | formatMoney: createMoneyFormatter('de-DE'), 39 | formatDate: createDateFormatter('de-DE') 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /api-services/types/properties/attached_property.ts: -------------------------------------------------------------------------------- 1 | import type { FieldType, SelectOption, PropertySubjectType } from './property' 2 | 3 | export type AttachPropertySelectValue = Omit 4 | 5 | export type PropertyValue = { 6 | textValue?: string, 7 | numberValue?: number, 8 | boolValue?: boolean, 9 | dateValue?: Date, 10 | dateTimeValue?: Date, 11 | singleSelectValue?: AttachPropertySelectValue, 12 | multiSelectValue: AttachPropertySelectValue[], 13 | } 14 | 15 | export const emptyPropertyValue: PropertyValue = { 16 | boolValue: undefined, 17 | textValue: undefined, 18 | numberValue: undefined, 19 | dateValue: undefined, 20 | dateTimeValue: undefined, 21 | singleSelectValue: undefined, 22 | multiSelectValue: [] 23 | } 24 | 25 | /** 26 | * The value of properties attached to a subject 27 | * 28 | * This Object only stores the value. 29 | * 30 | * It is identified through the propertyId and the subjectType 31 | */ 32 | export type AttachedProperty = { 33 | /** 34 | * The identifier of the properties for which this is a value 35 | */ 36 | propertyId: string, 37 | subjectId: string, 38 | value: PropertyValue, 39 | } 40 | 41 | export type DisplayableAttachedProperty = AttachedProperty & { 42 | subjectType: PropertySubjectType, 43 | fieldType: FieldType, 44 | name: string, 45 | description?: string, 46 | } 47 | -------------------------------------------------------------------------------- /customer/api/services/invoice.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from '@/api/config' 2 | import type { Invoice, InvoiceStatus } from '@/api/dataclasses/invoice' 3 | import { InvoiceHelpers } from '@/api/dataclasses/invoice' 4 | 5 | export const InvoiceAPI = { 6 | getMyInvoices: async (headers: HeadersInit): Promise => { 7 | const response = await fetch(`${API_URL}/invoice/self/`, { method: 'GET', headers }) 8 | if(response.ok) { 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | return (await response.json() as any[]).map(value => InvoiceHelpers.fromJson(value)) 11 | } 12 | throw response 13 | }, 14 | pay: async (id: string, locale: string, headers: HeadersInit): Promise => { 15 | const response = await fetch(`${API_URL}/invoice/pay/${id}/`, { 16 | method: 'POST', 17 | body: JSON.stringify({ locale }), 18 | headers: { ...headers, 'Content-Type': 'application/json' }, 19 | }) 20 | if(response.ok) { 21 | return (await response.json()) as string 22 | } 23 | throw response 24 | }, 25 | getStatus: async (id: string, headers: HeadersInit) => { 26 | const response = await fetch(`${API_URL}/invoice/status?${id}`, { 27 | method: 'GET', 28 | headers, 29 | }) 30 | if(response.ok) { 31 | return (await response.json()) as InvoiceStatus 32 | } 33 | throw response 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /tasks/translation/tasks.ts: -------------------------------------------------------------------------------- 1 | import type { Translation, TranslationPlural } from '@helpwave/hightide' 2 | 3 | export type TasksTranslationType = { 4 | task: TranslationPlural, 5 | subtask: TranslationPlural, 6 | visibility: string, 7 | private: string, 8 | public: string, 9 | publish: string, 10 | notes: string, 11 | status: string, 12 | assignee: string, 13 | dueDate: string, 14 | createdAt: string, 15 | } 16 | 17 | export const tasksTranslation: Translation = { 18 | en: { 19 | task: { 20 | one: 'Task', 21 | other: 'Tasks' 22 | }, 23 | subtask: { 24 | one: 'Subtask', 25 | other: 'Subtasks', 26 | }, 27 | visibility: 'Visibility', 28 | private: 'private', 29 | public: 'public', 30 | publish: 'publish', 31 | notes: 'notes', 32 | status: 'Status', 33 | assignee: 'Assignee', 34 | dueDate: 'Due Date', 35 | createdAt: 'Created at', 36 | }, 37 | de: { 38 | task: { 39 | one: 'Task', 40 | other: 'Tasks' 41 | }, 42 | subtask: { 43 | one: 'Unteraufgabe', 44 | other: 'Unteraufgaben' 45 | }, 46 | visibility: 'Sichtbarkeit', 47 | private: 'Privat', 48 | public: 'Öffentlich', 49 | publish: 'Veröffentlichen', 50 | notes: 'Notizen', 51 | status: 'Status', 52 | assignee: 'Verantwortlich', 53 | dueDate: 'Fälligkeitsdatum', 54 | createdAt: 'Erstellt am', 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tasks/components/cards/DragCard.tsx: -------------------------------------------------------------------------------- 1 | import type { MouseEventHandler, PropsWithChildren } from 'react' 2 | import clsx from 'clsx' 3 | 4 | export type CardDragProperties = { 5 | isDragging?: boolean, 6 | isOver?: boolean, 7 | isDangerous?: boolean, 8 | isInvalid?: boolean, 9 | } 10 | 11 | export type DragCardProps = PropsWithChildren<{ 12 | onClick?: MouseEventHandler, 13 | isSelected?: boolean, 14 | cardDragProperties?: CardDragProperties, 15 | className?: string, 16 | }> 17 | 18 | /** 19 | * A Card to use when items are dragged for uniform design 20 | */ 21 | export const DragCard = ({ 22 | children, 23 | cardDragProperties = {}, 24 | isSelected, 25 | onClick, 26 | className, 27 | }: DragCardProps) => { 28 | return ( 29 |
37 | {children} 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | NODE_VERSION: 20.4.0 5 | PNPM_VERSION: 8 6 | 7 | on: 8 | push: 9 | branches: 10 | - '*' 11 | tags: 12 | - 'v*' 13 | pull_request: 14 | 15 | jobs: 16 | 17 | lint: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup nodejs 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ env.NODE_VERSION }} 27 | registry-url: "https://registry.npmjs.org" 28 | scope: "@helpwave" 29 | 30 | - name: Setup pnpm 31 | uses: pnpm/action-setup@v2 32 | id: pnpm-install 33 | with: 34 | version: ${{ env.PNPM_VERSION }} 35 | run_install: false 36 | 37 | - name: Get pnpm cache directory 38 | id: pnpm-cache 39 | shell: bash 40 | run: | 41 | echo "directory=$(pnpm store path)" >> $GITHUB_OUTPUT 42 | 43 | - name: Cache pnpm dependencies 44 | uses: actions/cache@v3 45 | with: 46 | path: ${{ steps.pnpm-cache.outputs.directory }} 47 | key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} 48 | restore-keys: | 49 | ${{ runner.os }}-pnpm-cache- 50 | 51 | - name: Install pnpm dependencies 52 | run: pnpm install 53 | 54 | - run: pnpm --filter "@helpwave/*" lint 55 | - run: pnpm --filter "@helpwave/*" build 56 | -------------------------------------------------------------------------------- /tasks/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import '@helpwave/hightide/style/uncompiled/globals.css'; 3 | 4 | @source "./node_modules/@helpwave/hightide"; 5 | 6 | @theme { 7 | /* Colors */ 8 | --color-todo-background: var(--color-tag-red-background); 9 | --color-todo-text: var(--color-tag-red-text); 10 | --color-todo-icon: var(--color-tag-red-icon); 11 | 12 | --color-inprogress-background: var(--color-tag-yellow-background); 13 | --color-inprogress-text: var(--color-tag-yellow-text); 14 | --color-inprogress-icon: var(--color-tag-yellow-icon); 15 | 16 | --color-done-background: var(--color-tag-green-background); 17 | --color-done-text: var(--color-tag-green-text); 18 | --color-done-icon: var(--color-tag-green-icon); 19 | 20 | /* Header */ 21 | --color-header-background: var(--color-white); 22 | --color-header-text: var(--color-black); 23 | 24 | /* TaskModalSidebar */ 25 | --color-task-modal-sidebar-background: var(--color-surface-variant); 26 | --color-task-modal-sidebar-text: var(--color-on-surface); 27 | 28 | /* TwoColumn */ 29 | --color-twocolum-background: #C6C6C6; 30 | --color-twocolum-text: #FFFFFF; 31 | } 32 | 33 | @layer base { 34 | /* Here are overrides for the light theme */ 35 | @variant dark { 36 | /* Header */ 37 | --color-header-background: var(--color-black); 38 | --color-header-text: var(--color-white); 39 | 40 | /* TwoColumn */ 41 | --color-twocolum-background: #363636; 42 | --color-twocolum-text: var(--color-description); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /customer/api/mutations/customer_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 2 | import { QueryKeys } from '@/api/mutations/query_keys' 3 | import type { Customer, CustomerCreate } from '@/api/dataclasses/customer' 4 | import { CustomerAPI } from '@/api/services/customer' 5 | import { useAuth } from '@/hooks/useAuth' 6 | 7 | export const useCustomerMyselfQuery = () => { 8 | const { authHeader } = useAuth() 9 | return useQuery({ 10 | queryKey: [QueryKeys.customer], 11 | queryFn: async () => { 12 | return await CustomerAPI.getMyself(authHeader) 13 | }, 14 | }) 15 | } 16 | 17 | export const useCustomerCreateMutation = () => { 18 | const queryClient = useQueryClient() 19 | const { authHeader } = useAuth() 20 | return useMutation({ 21 | mutationFn: async (customer: CustomerCreate) => { 22 | return await CustomerAPI.create(customer, authHeader) 23 | }, 24 | onSuccess: () => { 25 | queryClient.invalidateQueries([QueryKeys.customer]).catch(reason => console.error(reason)) 26 | } 27 | }) 28 | } 29 | 30 | export const useCustomerUpdateMutation = () => { 31 | const queryClient = useQueryClient() 32 | const { authHeader } = useAuth() 33 | return useMutation({ 34 | mutationFn: async (customer: Customer) => { 35 | return await CustomerAPI.update(customer, authHeader) 36 | }, 37 | onSuccess: () => { 38 | queryClient.invalidateQueries([QueryKeys.customer]).catch(reason => console.error(reason)) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /customer/api/dataclasses/contract.ts: -------------------------------------------------------------------------------- 1 | export type ContractType = 'agb_app_zum_doc_patient' | 'agb_mediquu_connect' | 'agb_app_zum_doc' | 2 | 'agb_mediquu_netzmanager' | 'agb_mediquu_chat' | 'privacy_concept' | 'privacy_concept_tasks' | 3 | 'avv' | 'avv_tasks' | 'nda' | 'sub' | 'tom' 4 | 5 | export interface Contract { 6 | /** The unique identifier of the contract */ 7 | uuid: string, 8 | /** The type of contract that determines its purpose */ 9 | key: ContractType, 10 | /** The version of the contract */ 11 | version: string, 12 | /** The URL at which the contract can be found as a file */ 13 | url: string, 14 | /** The creation day of the contract */ 15 | createdAt: Date, 16 | } 17 | 18 | /** 19 | * Converts a JSON object to a Contract object. 20 | */ 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | const fromJson = (json: any): Contract => { 23 | return { 24 | uuid: json.uuid, 25 | key: json.key, 26 | version: json.version, 27 | url: json.url, 28 | createdAt: new Date(json.created_at), 29 | } 30 | } 31 | 32 | /** 33 | * Converts a Contract object back to JSON. 34 | */ 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | const toJson = (customer: Contract): Record => { 37 | return { 38 | uuid: customer.uuid, 39 | key: customer.key, 40 | version: customer.version, 41 | url: customer.url, 42 | created_at: customer.createdAt.toISOString(), 43 | } 44 | } 45 | 46 | 47 | export const ContractHelpers = { fromJson, toJson } 48 | -------------------------------------------------------------------------------- /api-services/types/tasks/patient.ts: -------------------------------------------------------------------------------- 1 | import type { TaskDTO, TaskMinimalDTO } from './task' 2 | import type { RoomMinimalDTO } from './room' 3 | import type { BedMinimalDTO } from './bed' 4 | 5 | export type PatientMinimalDTO = { 6 | id: string, 7 | humanReadableIdentifier: string, 8 | bedId?: string, 9 | } 10 | 11 | export type PatientWithBedIdDTO = PatientMinimalDTO & { 12 | notes: string, 13 | bedId?: string, 14 | } 15 | 16 | export type PatientDTO = PatientMinimalDTO & { 17 | notes: string, 18 | tasks: TaskMinimalDTO[], 19 | } 20 | 21 | export type PatientWithTasksNumberDTO = PatientMinimalDTO & { 22 | tasksTodo: number, 23 | tasksInProgress: number, 24 | tasksDone: number, 25 | } 26 | 27 | export type PatientWithBedAndRoomDTO = PatientMinimalDTO & { 28 | room: RoomMinimalDTO, 29 | bed: BedMinimalDTO, 30 | } 31 | 32 | export type RecentPatientDTO = PatientMinimalDTO & { 33 | wardId?: string, 34 | room?: RoomMinimalDTO, 35 | bed?: BedMinimalDTO, 36 | } 37 | 38 | export type PatientListDTO = { 39 | active: PatientWithBedAndRoomDTO[], 40 | unassigned: PatientMinimalDTO[], 41 | discharged: PatientMinimalDTO[], 42 | } 43 | 44 | export type PatientDetailsDTO = PatientMinimalDTO & { 45 | notes: string, 46 | tasks: TaskDTO[], 47 | discharged: boolean, 48 | room?: RoomMinimalDTO, 49 | bed?: BedMinimalDTO, 50 | wardId?: string, 51 | } 52 | 53 | export const emptyPatientDetails: PatientDetailsDTO = { 54 | id: '', 55 | humanReadableIdentifier: '', 56 | notes: '', 57 | tasks: [], 58 | discharged: false 59 | } 60 | -------------------------------------------------------------------------------- /tasks/components/ColumnTitle.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { ReactNode } from 'react' 3 | 4 | type ColumnTitleType = 'main' | 'subtitle' 5 | 6 | type ColumnTitleProps = { 7 | title: ReactNode, 8 | description?: ReactNode, 9 | actions?: ReactNode, 10 | type?: ColumnTitleType, 11 | containerClassName?: string, 12 | titleRowClassName?: string, 13 | } 14 | 15 | /** 16 | * A component for creating a uniform column title with the same bottom padding 17 | */ 18 | export const ColumnTitle = ({ 19 | title, 20 | actions, 21 | description, 22 | type = 'main', 23 | containerClassName, 24 | titleRowClassName, 25 | }: ColumnTitleProps) => { 26 | return ( 27 |
28 |
37 | {type === 'main' &&

{title}

} 38 | {type === 'subtitle' &&

{title}

} 39 | {actions} 40 |
41 | {description && ({description})} 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /customer/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import type { Translation } from '@helpwave/hightide' 3 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 4 | import { Page } from '@/components/layout/Page' 5 | import titleWrapper from '@/utils/titleWrapper' 6 | import { withOrganization } from '@/hooks/useOrganization' 7 | import { withAuth } from '@/hooks/useAuth' 8 | import { useRouter } from 'next/router' 9 | import { useEffect } from 'react' 10 | import { LoadingAnimation } from '@helpwave/hightide' 11 | 12 | type DashboardTranslation = { 13 | dashboard: string, 14 | } 15 | 16 | const defaultDashboardTranslations: Translation = { 17 | en: { 18 | dashboard: 'Dashboard' 19 | }, 20 | de: { 21 | dashboard: 'Dashboard' 22 | } 23 | } 24 | 25 | type DashboardServerSideProps = { 26 | jsonFeed: unknown, 27 | } 28 | 29 | const Dashboard: NextPage> = ({ overwriteTranslation }) => { 30 | const translation = useTranslation(defaultDashboardTranslations, overwriteTranslation) 31 | const router = useRouter() 32 | 33 | useEffect(() => { 34 | router.push('/products').catch(console.error) 35 | }, [router]) 36 | 37 | return ( 38 | 39 |
40 | {} 41 |
42 |
43 | ) 44 | } 45 | 46 | export default withAuth(withOrganization(Dashboard)) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # helpwave web 2 | 3 | The official helpwave web frontends. 4 | 5 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/helpwave/web) 6 | 7 | --- 8 | 9 | ## [Projects](./documentation/structure.md) 10 | This repository is split up into multiple subprojects using [pnpm](https://pnpm.io) workspaces. 11 | - helpwave tasks (see [tasks](/tasks)) 12 | 13 | ## Getting Started 14 | 15 | ### Prerequisites 16 | Before you can start you need to have these installed: 17 | - [Node.js](https://nodejs.org/) 18 | - [pnpm](https://pnpm.io/) (installation through npm `npm install -g pnpm`) 19 | 20 | ### Setup 21 | ```shell 22 | pnpm install 23 | ``` 24 | 25 | ### Usage 26 | ```shell 27 | cd tasks 28 | pnpm run dev 29 | ``` 30 | 31 | After that you should be able to open the project in the browser [`http://localhost:3000`](http://localhost:3000). 32 | 33 | ## Testing 34 | This project is tested with [BrowserStack](https://www.browserstack.com). 35 | 36 | ## Linter 37 | Our projects use linting with `eslint` to create a uniform code style. The linter can be used with: 38 | 39 | ```shell 40 | pnpm run --filter "@helpwave/*" lint 41 | ``` 42 | 43 | ## Scripts 44 | The list of all our scripts can be found [here](documentation/scripts.md). 45 | 46 | ### Boilerplate generation 47 | 48 | Execution with 49 | - `node generate_boilerplate ` 50 | - `pnpm run generate ` (within the projects) 51 | 52 | All options can be seen with the `--help` flag 53 | 54 | Example: `node scripts/generate_boilerplate tasks/components/test` 55 | 56 | 57 | -------------------------------------------------------------------------------- /tasks/components/cards/EditCard.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react' 2 | import clsx from 'clsx' 3 | import { Pencil } from 'lucide-react' 4 | import { IconButton } from '@helpwave/hightide' 5 | 6 | export type EditCardProps = PropsWithChildren<{ 7 | onClick?: () => void, 8 | onEditClick?: () => void, 9 | isSelected?: boolean, 10 | className?: string, 11 | }> 12 | 13 | /** 14 | * A Card with an editing button the right 15 | */ 16 | export const EditCard = ({ 17 | children, 18 | onClick, 19 | onEditClick, 20 | isSelected = false, 21 | className, 22 | }: EditCardProps) => { 23 | return ( 24 |
37 | {children} 38 | {onEditClick && ( 39 | { 41 | event.stopPropagation() 42 | onEditClick() 43 | }} 44 | className="h-full justify-center items-center" 45 | color="neutral" 46 | > 47 | 48 | 49 | )} 50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /api-services/service/tasks/BedService.ts: -------------------------------------------------------------------------------- 1 | import type { BedWithRoomId } from '../../types/tasks/bed' 2 | import { 3 | CreateBedRequest, 4 | DeleteBedRequest, 5 | GetBedRequest, 6 | UpdateBedRequest 7 | } from '@helpwave/proto-ts/services/tasks_svc/v1/bed_svc_pb' 8 | import { APIServices } from '../../services' 9 | import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' 10 | 11 | export const BedService = { 12 | get: async (id: string): Promise => { 13 | const req = new GetBedRequest() 14 | .setId(id) 15 | const res = await APIServices.bed.getBed(req, getAuthenticatedGrpcMetadata()) 16 | 17 | return { 18 | id: res.getId(), 19 | name: res.getName(), 20 | roomId: res.getRoomId() 21 | } 22 | }, 23 | create: async (bed: BedWithRoomId): Promise => { 24 | const req = new CreateBedRequest() 25 | .setRoomId(bed.roomId) 26 | .setName(bed.name) 27 | const res = await APIServices.bed.createBed(req, getAuthenticatedGrpcMetadata()) 28 | 29 | return { ...bed, id: res.getId() } 30 | }, 31 | update: async (bed: BedWithRoomId): Promise => { 32 | const req = new UpdateBedRequest() 33 | .setId(bed.id) 34 | .setName(bed.name) 35 | .setRoomId(bed.roomId) 36 | 37 | await APIServices.bed.updateBed(req, getAuthenticatedGrpcMetadata()) 38 | return true 39 | }, 40 | delete: async (id: string): Promise => { 41 | const req = new DeleteBedRequest() 42 | req.setId(id) 43 | 44 | await APIServices.bed.deleteBed(req, getAuthenticatedGrpcMetadata()) 45 | return true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /customer/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import type { AppProps } from 'next/app' 3 | import { Inter, Space_Grotesk as SpaceGrotesk } from 'next/font/google' 4 | import { ProvideLanguage } from '@helpwave/hightide' 5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 6 | import { modalRootName } from '@helpwave/hightide' 7 | import { ModalRegister } from '@helpwave/hightide' 8 | import titleWrapper from '@/utils/titleWrapper' 9 | import '../globals.css' 10 | 11 | const inter = Inter({ 12 | subsets: ['latin'], 13 | variable: '--font-inter', 14 | }) 15 | 16 | const spaceGrotesk = SpaceGrotesk({ 17 | subsets: ['latin'], 18 | variable: '--font-space-grotesk', 19 | }) 20 | 21 | const queryClient = new QueryClient() 22 | 23 | function MyApp({ Component, pageProps }: AppProps) { 24 | return ( 25 | 26 | { /* v Scans the user agent */} 27 | 28 | {titleWrapper()} 29 | 38 | 39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 |
47 | ) 48 | } 49 | 50 | export default MyApp 51 | -------------------------------------------------------------------------------- /tasks/components/KanbanHeader.tsx: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 3 | import { Input } from '@helpwave/hightide' 4 | import { ColumnTitle } from '@/components/ColumnTitle' 5 | 6 | type KanbanHeaderTranslation = { 7 | tasks: string, 8 | status: string, 9 | label: string, 10 | search: string, 11 | } 12 | 13 | const defaultKanbanHeaderTranslations: Translation = { 14 | en: { 15 | tasks: 'Tasks', 16 | status: 'Status', 17 | label: 'Label', 18 | search: 'Search' 19 | }, 20 | de: { 21 | tasks: 'Aufgaben', 22 | status: 'Status', 23 | label: 'Label', 24 | search: 'Suchen' 25 | } 26 | } 27 | 28 | type KanbanHeaderProps = { 29 | sortingStatus?: string, 30 | sortingLabel?: string, 31 | searchValue: string, 32 | onSearchChange: (search: string) => void, 33 | } 34 | 35 | /** 36 | * The header of the KanbanBoard affording a search 37 | */ 38 | export const KanbanHeader = ({ 39 | overwriteTranslation, 40 | searchValue = '', 41 | onSearchChange 42 | }: PropsForTranslation) => { 43 | const translation = useTranslation([defaultKanbanHeaderTranslations], overwriteTranslation) 44 | return ( 45 | 55 | )} 56 | /> 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /tasks/components/layout/PageWithHeader.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react' 2 | 3 | import type { Crumb } from '@helpwave/hightide' 4 | import { BreadCrumb } from '@helpwave/hightide' 5 | import { useAuth } from '@helpwave/api-services/authentication/useAuth' 6 | import { UserMenu } from '@/components/UserMenu' 7 | import { Header, type HeaderProps } from '@/components/Header' 8 | 9 | type PageWithHeaderProps = Partial & { 10 | crumbs?: Crumb[], 11 | } 12 | 13 | /** 14 | * The base of every page. It creates the configurable header 15 | * 16 | * The page content will be passed as the children 17 | */ 18 | export const PageWithHeader = ({ 19 | children, 20 | title, 21 | withIcon = true, 22 | leftSide, 23 | rightSide, 24 | crumbs 25 | }: PropsWithChildren) => { 26 | const { user } = useAuth() 27 | 28 | if (!user) return null 29 | 30 | return ( 31 |
32 |
: undefined), ...(leftSide ?? [])]} 37 | leftSideClassName="max-tablet:hidden" 38 | rightSide={[ 39 | ...(rightSide ?? []), 40 | (), 41 | ]} 42 | /> 43 | {children} 44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /tasks/pages/invitations.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import type { Translation } from '@helpwave/hightide' 3 | import { type PropsForTranslation, useTranslation } from '@helpwave/hightide' 4 | import { PageWithHeader } from '@/components/layout/PageWithHeader' 5 | import { UserInvitationList } from '@/components/UserInvitationList' 6 | import titleWrapper from '@/utils/titleWrapper' 7 | import { ColumnTitle } from '@/components/ColumnTitle' 8 | 9 | type InvitationsPageTranslation = { 10 | invitations: string, 11 | } 12 | 13 | const defaultInvitationsPageTranslation: Translation = { 14 | en: { 15 | invitations: 'Invitations' 16 | }, 17 | de: { 18 | invitations: 'Einladungen' 19 | } 20 | } 21 | 22 | export type InvitationsPageProps = Record 23 | 24 | /** 25 | * Page for Invitations 26 | */ 27 | export const InvitationsPage = ({ 28 | overwriteTranslation, 29 | }: PropsForTranslation) => { 30 | const translation = useTranslation([defaultInvitationsPageTranslation], overwriteTranslation) 31 | return ( 32 | 35 | 36 | {titleWrapper(translation('invitations'))} 37 | 38 |
39 | 40 | 41 |
42 |
43 | ) 44 | } 45 | 46 | export default InvitationsPage 47 | -------------------------------------------------------------------------------- /documentation/translation.md: -------------------------------------------------------------------------------- 1 | # Translation 2 | Translations are handled with three steps and per component. 3 | 4 | 1. The type for the translation 5 | 2. The default values for the translation 6 | 3. The `useTranslation` hook 7 | 8 | Example: 9 | ```tsx 10 | import { useTranslation } from '../../hooks/useTranslation' 11 | import type { PropsForTranslation } from '../../hooks/useTranslation' 12 | import type { Languages } from '../../hooks/useLanguage' 13 | 14 | type TitleTranslation = { 15 | welcome: string, 16 | goodToSeeYou: string, 17 | page: (page: number) => string 18 | } 19 | 20 | const defaultTitleTranslations: Translation = { 21 | en: { 22 | welcome: 'Welcome', 23 | goodToSeeYou: 'Good to see you', 24 | page: (page) => `Page ${page}` 25 | }, 26 | de: { 27 | welcome: 'Willkommen', 28 | goodToSeeYou: 'Schön dich zu sehen', 29 | page: (page) => `Seite ${page}` 30 | } 31 | } 32 | 33 | const Title = ({ overwriteTranslation, name }:PropsForTranslation) => { 34 | const translation = useTranslation(defaultTitleTranslations, overwriteTranslation) 35 | return ( 36 |

37 | {translation.welcome}{'! '} 38 | {translation.goodToSeeYou}{', '} 39 | {name}{'. '} 40 | {translation.page(123)} 41 |

42 | ) 43 | } 44 | ``` 45 | 46 | ## Automatic Generation 47 | These components can be automatically generated with the following commands: 48 | 49 | ``` 50 | node generate_boilerplate 51 | ``` 52 | or 53 | ``` 54 | pnpm run generate 55 | ``` 56 | -------------------------------------------------------------------------------- /api-services/types/properties/property.ts: -------------------------------------------------------------------------------- 1 | export const subjectTypeList = ['patient', 'task'] as const 2 | export type PropertySubjectType = typeof subjectTypeList[number] 3 | 4 | export const fieldTypeList = ['multiSelect', 'singleSelect', 'number', 'text', 'date', 'dateTime', 'checkbox'] as const 5 | export type FieldType = typeof fieldTypeList[number] 6 | 7 | export type SelectOption = { 8 | id: string, 9 | name: string, 10 | description?: string, 11 | isCustom: boolean, 12 | } 13 | 14 | export type PropertySelectData = { 15 | isAllowingFreetext: boolean, 16 | options: SelectOption[], 17 | } 18 | 19 | export type Property = { 20 | id: string, 21 | subjectType: PropertySubjectType, 22 | fieldType: FieldType, 23 | name: string, 24 | description?: string, 25 | isArchived: boolean, 26 | setId?: string, 27 | selectData?: PropertySelectData, 28 | alwaysIncludeForViewSource?: boolean, 29 | } 30 | 31 | export const emptySelectOption: SelectOption = { 32 | id: '', 33 | name: 'Select Item', 34 | description: undefined, 35 | isCustom: false, 36 | } 37 | 38 | export const emptySelectData: PropertySelectData = { 39 | isAllowingFreetext: true, 40 | options: [ 41 | { 42 | ...emptySelectOption, 43 | name: 'Select 1' 44 | }, 45 | { 46 | ...emptySelectOption, 47 | name: 'Select 2' 48 | }, 49 | { 50 | ...emptySelectOption, 51 | name: 'Select 3' 52 | }, 53 | ] 54 | } 55 | 56 | export const emptyProperty: Property = { 57 | id: '', 58 | name: 'Name', 59 | description: undefined, 60 | subjectType: 'patient', 61 | fieldType: 'multiSelect', 62 | selectData: emptySelectData, 63 | isArchived: false, 64 | setId: undefined, 65 | alwaysIncludeForViewSource: false, 66 | } 67 | -------------------------------------------------------------------------------- /tasks/utils/news.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from './config' 2 | 3 | import { z } from 'zod' 4 | import type { Language } from '@helpwave/hightide' 5 | import { LanguageUtil } from '@helpwave/hightide' 6 | 7 | export type News = { 8 | title: string, 9 | date: Date, 10 | description: (string | URL)[], 11 | externalResource?: URL, 12 | keys: string[], 13 | } 14 | 15 | export type LocalizedNews = Record 16 | 17 | export const newsSchema = z.object({ 18 | title: z.string(), 19 | description: z.string(), 20 | date: z.string(), 21 | image: z.string().url().optional(), 22 | externalResource: z.string().url().optional(), 23 | keys: z.array(z.string()) 24 | }).transform((obj) => { 25 | let description: (string | URL)[] = [obj.description] 26 | if (obj.image) { 27 | description = [new URL(obj.image), ...description] 28 | } 29 | 30 | return { 31 | title: obj.title, 32 | date: new Date(obj.date), 33 | description, 34 | externalResource: obj.externalResource ? new URL(obj.externalResource) : undefined, 35 | keys: obj.keys 36 | } 37 | }) 38 | 39 | export const newsListSchema = z.array(newsSchema) 40 | 41 | export const localizedNewsSchema = z.record(z.enum(LanguageUtil.languages), newsListSchema) 42 | 43 | export const filterNews = (localizedNews: News[], requiredKeys: string[]) => { 44 | return localizedNews.filter(news => requiredKeys.every(value => news.keys.includes(value))) 45 | } 46 | 47 | export const fetchLocalizedNews = (): Promise => { 48 | const { featuresFeedUrl } = getConfig() 49 | return fetch(featuresFeedUrl) 50 | .then((res) => res.json()) 51 | .then(async (json) => { 52 | await localizedNewsSchema.parseAsync(json) 53 | return json 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /customer/api/services/contract.ts: -------------------------------------------------------------------------------- 1 | import type { Contract } from '@/api/dataclasses/contract' 2 | import { ContractHelpers } from '@/api/dataclasses/contract' 3 | import { API_URL } from '@/api/config' 4 | 5 | export const ContractsAPI = { 6 | /** Get a Contract by its id */ 7 | get: async (id: string, headers: HeadersInit): Promise => { 8 | const response = await fetch(`${API_URL}/contract/${id}`, { 9 | method: 'GET', 10 | headers: headers, 11 | }) 12 | if(response.ok) { 13 | return ContractHelpers.fromJson(await response.json()) 14 | } 15 | throw response 16 | }, 17 | /** 18 | * Returns all contracts 19 | */ 20 | getAll: async (headers: HeadersInit): Promise => { 21 | const response = await fetch(`${API_URL}/contract/`, { 22 | method: 'GET', 23 | headers: headers, 24 | }) 25 | if(response.ok) { 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | return (await response.json() as any[]).map(json => ContractHelpers.fromJson(json)) 28 | } 29 | throw response 30 | }, 31 | /** 32 | * Returns all contracts based on the productIds 33 | */ 34 | getAllByProductIds: async (productIds: string[],headers: HeadersInit): Promise => { 35 | // TODO update later 36 | const response = await fetch(`${API_URL}/contract/product/`, { 37 | method: 'PUT', 38 | headers: { 'Content-Type': 'application/json' ,...headers }, 39 | body: JSON.stringify({ products: productIds }) 40 | }) 41 | if(response.ok) { 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | return (await response.json() as any[]).map(json => ContractHelpers.fromJson(json)) 44 | } 45 | throw response 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /api-services/service/tasks/TaskSubtaskService.ts: -------------------------------------------------------------------------------- 1 | import type { SubtaskDTO } from '../../types/tasks/task' 2 | import { APIServices } from '../../services' 3 | import { 4 | CreateSubtaskRequest, 5 | DeleteSubtaskRequest, 6 | UpdateSubtaskRequest 7 | } from '@helpwave/proto-ts/services/tasks_svc/v1/task_svc_pb' 8 | import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' 9 | 10 | export type SubtaskDeleteParameter = { 11 | id: string, 12 | taskId: string, 13 | } 14 | 15 | export const TaskSubtaskService = { 16 | create: async function (subtask: SubtaskDTO): Promise { 17 | const req = new CreateSubtaskRequest() 18 | .setSubtask(new CreateSubtaskRequest.Subtask().setName(subtask.name)) 19 | .setTaskId(subtask.taskId) 20 | const res = await APIServices.task.createSubtask(req, getAuthenticatedGrpcMetadata()) 21 | 22 | return { 23 | ...subtask, 24 | id: res.getSubtaskId(), 25 | isDone: false, 26 | } 27 | }, 28 | update: async function (subtask: SubtaskDTO): Promise { 29 | const req = new UpdateSubtaskRequest() 30 | req.setSubtaskId(subtask.id) 31 | .setTaskId(subtask.taskId) 32 | .setSubtask(new UpdateSubtaskRequest.Subtask() 33 | .setName(subtask.name) 34 | .setDone(subtask.isDone)) 35 | const res = await APIServices.task.updateSubtask(req, getAuthenticatedGrpcMetadata()) 36 | 37 | return !!res.toObject() 38 | }, 39 | delete: async function ({ id, taskId }: SubtaskDeleteParameter): Promise { 40 | const req = new DeleteSubtaskRequest() 41 | .setSubtaskId(id) 42 | .setTaskId(taskId) 43 | const res = await APIServices.task.deleteSubtask(req, getAuthenticatedGrpcMetadata()) 44 | return !!res.toObject() 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /tasks/components/BedInRoomIndicator.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide/' 3 | 4 | type BedInRoomIndicatorTranslation = { 5 | bed: string, 6 | in: string, 7 | } 8 | 9 | const defaultBedInRoomIndicatorTranslation = { 10 | de: { 11 | bed: 'Bett', 12 | in: 'in' 13 | }, 14 | en: { 15 | bed: 'Bed', 16 | in: 'in' 17 | } 18 | } 19 | 20 | export type BedInRoomIndicatorProps = { 21 | bedsInRoom: number, 22 | /* 23 | usual numbering of an array starting with 0 24 | */ 25 | bedPosition: number, 26 | /* 27 | Omitting the room name will hide the label above the indicator 28 | */ 29 | roomName?: string, 30 | } 31 | 32 | /** 33 | * A component for showing the position of a bed within a room 34 | * 35 | * Currently, assumes linear ordering 36 | */ 37 | export const BedInRoomIndicator = 38 | ({ 39 | overwriteTranslation, 40 | bedsInRoom, 41 | roomName, 42 | bedPosition 43 | }: PropsForTranslation) => { 44 | const translation = useTranslation([defaultBedInRoomIndicatorTranslation], overwriteTranslation) 45 | 46 | return ( 47 |
48 | {roomName !== undefined && ( 49 | 50 | {`${translation('bed')} ${bedPosition + 1} ${translation('in')} ${roomName}`} 51 | 52 | )} 53 |
54 | {Array.from(Array(bedsInRoom).keys()).map((_, index) => ( 55 |
58 | ))} 59 |
60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /customer/components/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 3 | import { Page } from '@/components/layout/Page' 4 | import titleWrapper from '@/utils/titleWrapper' 5 | import { SolidButton } from '@helpwave/hightide' 6 | 7 | type LoginTranslation = { 8 | login: string, 9 | email: string, 10 | password: string, 11 | signIn: string, 12 | register: string, 13 | } 14 | 15 | const defaultLoginTranslations: Translation = { 16 | en: { 17 | login: 'Login', 18 | email: 'Email', 19 | password: 'Password', 20 | signIn: 'helpwave Sign In', 21 | register: 'Register', 22 | }, 23 | de: { 24 | login: 'Login', 25 | email: 'Email', 26 | password: 'Passwort', 27 | signIn: 'helpwave Login', 28 | register: 'Registrieren', 29 | } 30 | } 31 | 32 | export type LoginData = { 33 | email: string, 34 | password: string, 35 | } 36 | 37 | type LoginPageProps = { 38 | login: () => Promise, 39 | } 40 | 41 | export const LoginPage = ({ login, overwriteTranslation }: PropsForTranslation) => { 42 | const translation = useTranslation(defaultLoginTranslations, overwriteTranslation) 43 | 44 | return ( 45 | 51 |
52 |

{translation.login}

53 | {translation.signIn} 54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /tasks/components/MobileInterceptor.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Link from 'next/link' 3 | 4 | import { useTranslation, type PropsForTranslation, HelpwaveLogo } from '@helpwave/hightide' 5 | import { getConfig } from '@/utils/config' 6 | 7 | type MobileInterceptorTranslation = { 8 | pleaseDownloadApp: string, 9 | playStore: string, 10 | appStore: string, 11 | } 12 | 13 | const defaultMobileInterceptorTranslation = { 14 | en: { 15 | pleaseDownloadApp: 'Please download the app', 16 | playStore: 'Google Play Store', 17 | appStore: 'Apple App Store' 18 | }, 19 | de: { 20 | pleaseDownloadApp: 'Bitte laden Sie die App herunter', 21 | playStore: 'Google Play Store', 22 | appStore: 'Apple App Store' 23 | } 24 | } 25 | 26 | /** 27 | * The component shown when the user is looking at the app form a mobile 28 | * 29 | * Currently, the user is prevented from using the application from mobile and the link to 30 | * the helpwave app will be shown 31 | */ 32 | const MobileInterceptor: NextPage = ({ overwriteTranslation }: PropsForTranslation) => { 33 | const translation = useTranslation([defaultMobileInterceptorTranslation], overwriteTranslation) 34 | const config = getConfig() 35 | const playStoreLink = config.appstoreLinks.playStore 36 | const appstoreLink = config.appstoreLinks.appStore 37 | return ( 38 |
39 | 40 | {translation('pleaseDownloadApp')} 41 | {translation('playStore')} 42 | {translation('appStore')} 43 |
44 | ) 45 | } 46 | 47 | export default MobileInterceptor 48 | -------------------------------------------------------------------------------- /api-services/util/keycloak.ts: -------------------------------------------------------------------------------- 1 | import Keycloak from 'keycloak-js' 2 | import { getAPIServiceConfig } from '../config/config' 3 | import { loadKeycloakAdapter } from './keycloakAdapter' 4 | 5 | let keycloakInstance: Keycloak | undefined 6 | 7 | let isInitialized = false 8 | 9 | if (typeof window !== 'undefined') { 10 | keycloakInstance = new Keycloak({ 11 | url: 'https://accounts.helpwave.de', 12 | realm: 'helpwave', 13 | clientId: 'helpwave-tasks' 14 | }) 15 | } 16 | 17 | export const KeycloakService = { 18 | initKeycloak: (keycloak: Keycloak) => { 19 | if (isInitialized && keycloak) return Promise.resolve(keycloak?.authenticated ?? false) 20 | 21 | isInitialized = true 22 | try { 23 | return keycloak.init({ 24 | onLoad: 'login-required', 25 | useNonce: true, 26 | pkceMethod: 'S256', 27 | adapter: loadKeycloakAdapter(keycloak) 28 | }) 29 | } catch (err) { 30 | isInitialized = false 31 | console.error(err) 32 | throw err 33 | } 34 | }, 35 | getToken: async (minValidity = 30) => { 36 | if (!keycloakInstance) throw new Error('Keycloak uninitialized. Call initKeycloak before') 37 | return keycloakInstance 38 | .updateToken(minValidity) 39 | .then(() => { 40 | if (!keycloakInstance?.token) { 41 | throw new Error('no token after updateToken()') 42 | } 43 | return keycloakInstance.token 44 | }) 45 | .catch((err) => { 46 | console.warn('failed to refresh token', err) 47 | }) 48 | }, 49 | getCurrentTokenAndUpdateInBackground: (minValidity = 30) => { 50 | const { fakeTokenEnable, fakeToken } = getAPIServiceConfig() 51 | if (fakeTokenEnable) return fakeToken 52 | KeycloakService.getToken(minValidity) 53 | return keycloakInstance?.token 54 | }, 55 | } 56 | 57 | export default keycloakInstance 58 | -------------------------------------------------------------------------------- /customer/api/services/customer.ts: -------------------------------------------------------------------------------- 1 | // TODO delete later 2 | import type { Customer, CustomerCreate } from '@/api/dataclasses/customer' 3 | import { CustomerHelpers } from '@/api/dataclasses/customer' 4 | import { API_URL } from '@/api/config' 5 | 6 | export const CustomerAPI = { 7 | checkSelf: async (headers: HeadersInit): Promise => { 8 | const response = await fetch(`${API_URL}/customer/check/`, { method: 'GET', headers: headers }) 9 | if (response.ok) { 10 | return await response.json() as boolean 11 | } 12 | throw response 13 | }, 14 | getMyself: async (headers: HeadersInit): Promise => { 15 | if (!await CustomerAPI.checkSelf(headers)) { 16 | return null 17 | } 18 | const response = await fetch(`${API_URL}/customer/`, { method: 'GET', headers: headers }) 19 | if (response.ok) { 20 | return CustomerHelpers.fromJson(await response.json()) 21 | } 22 | throw response 23 | }, 24 | create: async (customer: CustomerCreate, headers: HeadersInit): Promise => { 25 | const response = await fetch(`${API_URL}/customer/`, { 26 | method: 'POST', 27 | headers: { ...headers, 'Content-Type': 'application/json' }, 28 | body: JSON.stringify(CustomerHelpers.toJsonCreate(customer)), 29 | }) 30 | if (response.ok) { 31 | return CustomerHelpers.fromJson(await response.json()) 32 | } 33 | throw response 34 | }, 35 | update: async (customer: Customer, headers: HeadersInit) => { 36 | const response = await fetch(`${API_URL}/customer/`, { 37 | method: 'PUT', 38 | headers: { ...headers, 'Content-Type': 'application/json' }, 39 | body: JSON.stringify(CustomerHelpers.toJsonUpdate(customer)), 40 | }) 41 | if (response.ok) { 42 | return CustomerHelpers.fromJson(await response.json()) 43 | } 44 | throw response 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /tasks/components/InvitationBanner.tsx: -------------------------------------------------------------------------------- 1 | import { Mail } from 'lucide-react' 2 | import type { Translation } from '@helpwave/hightide' 3 | import { type PropsForTranslation, useTranslation } from '@helpwave/hightide' 4 | import Link from 'next/link' 5 | import { useInvitationsByUserQuery } from '@helpwave/api-services/mutations/users/organization_mutations' 6 | import { InvitationState } from '@helpwave/api-services/types/users/invitations' 7 | 8 | type InvitationBannerTranslation = { 9 | openInvites: string, 10 | } 11 | 12 | const defaultInvitationBannerTranslation: Translation = { 13 | en: { 14 | openInvites: 'Open invites' 15 | }, 16 | de: { 17 | openInvites: 'Offene Einladungen' 18 | } 19 | } 20 | 21 | export type InvitationBannerProps = { 22 | invitationCount?: number, 23 | } 24 | 25 | /** 26 | * A Banner that only appears when the user has pending invitations 27 | */ 28 | export const InvitationBanner = ({ 29 | overwriteTranslation, 30 | invitationCount 31 | }: PropsForTranslation) => { 32 | const translation = useTranslation([defaultInvitationBannerTranslation], overwriteTranslation) 33 | const { data, isError, isLoading } = useInvitationsByUserQuery(InvitationState.INVITATION_STATE_PENDING) 34 | let openInvites = invitationCount 35 | 36 | if (!invitationCount) { 37 | if (!data || isError || isLoading) { 38 | return null 39 | } 40 | if (data) { 41 | openInvites = data.length 42 | } 43 | } 44 | 45 | if (!openInvites || openInvites <= 0) { 46 | return null 47 | } 48 | 49 | return ( 50 | 54 | {`${translation('openInvites')}: ${openInvites}`} 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /tasks/components/NewsDisplay.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import clsx from 'clsx' 3 | import { TimeDisplay } from '@helpwave/hightide' 4 | import Image from 'next/image' 5 | import type { News } from '@/utils/news' 6 | 7 | export type NewsDisplayProps = { 8 | news: News, 9 | titleOnTop?: boolean, 10 | } 11 | 12 | /** 13 | * A component for showing a feature with a title, date of release and a link to a blog post on click 14 | */ 15 | export const NewsDisplay = ({ news, titleOnTop = true }: NewsDisplayProps) => { 16 | const content = ( 17 |
18 |
22 |
23 | 24 |
25 | {news.title} 26 |
27 |
28 | {news.description.map((value, index) => value instanceof URL ? ( 29 | 37 | ) 38 | : 39 | {value}) 40 | } 41 |
42 |
43 | ) 44 | const tileStyle = 'row gap-x-8 hover:bg-gray-100 rounded-xl p-3' 45 | 46 | return news.externalResource !== undefined ? ( 47 | 49 | {content} 50 | 51 | ) :
{content}
52 | } 53 | -------------------------------------------------------------------------------- /api-services/mutations/properties/property_value_mutations.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationOptions } from '@tanstack/react-query' 2 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 3 | import { QueryKeys } from '../query_keys' 4 | import type { PropertySubjectType } from '../../types/properties/property' 5 | import type { AttachedProperty } from '../../types/properties/attached_property' 6 | import type { 7 | AttachedPropertyMutationUpdate } from '../../service/properties/AttachedPropertyValueService' 8 | import { 9 | AttachedPropertyValueService 10 | } from '../../service/properties/AttachedPropertyValueService' 11 | 12 | export const usePropertyWithValueListQuery = (subjectId: string | undefined, subjectType: PropertySubjectType, wardId?: string) => { 13 | return useQuery({ 14 | queryKey: [QueryKeys.properties, QueryKeys.attachedProperties, subjectId], 15 | enabled: !!subjectId, 16 | queryFn: async () => { 17 | return await AttachedPropertyValueService.get({ subjectId: subjectId!, subjectType, wardId }) 18 | }, 19 | }) 20 | } 21 | 22 | /** 23 | * Mutation to insert or update a properties value for a properties attached to a subject 24 | */ 25 | export const useAttachPropertyMutation = (options?: UseMutationOptions>) => { 26 | const queryClient = useQueryClient() 27 | return useMutation({ 28 | ...options, 29 | mutationFn: async (update: AttachedPropertyMutationUpdate) => { 30 | return await AttachedPropertyValueService.create(update) 31 | }, 32 | onSuccess: (data, variables, context) => { 33 | if (options?.onSuccess) { 34 | options.onSuccess(data, variables, context) 35 | } 36 | queryClient.invalidateQueries([QueryKeys.properties, QueryKeys.attachedProperties, data.subjectId]).catch(console.error) 37 | }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /tasks/components/layout/property/PropertySubjectTypeSelect.tsx: -------------------------------------------------------------------------------- 1 | import { SelectOption } from '@helpwave/hightide' 2 | import type { PropsForTranslation , Translation } from '@helpwave/hightide' 3 | import { useTranslation } from '@helpwave/hightide' 4 | import type { SelectProps } from '@helpwave/hightide' 5 | import { Select } from '@helpwave/hightide' 6 | import type { PropertySubjectType } from '@helpwave/api-services/types/properties/property' 7 | import { subjectTypeList } from '@helpwave/api-services/types/properties/property' 8 | 9 | type PropertySubjectTypeSelectTranslation = { [key in PropertySubjectType]: string } 10 | 11 | const defaultPropertySubjectTypeSelectTranslation: Translation = { 12 | en: { 13 | patient: 'Patient', 14 | task: 'Task', 15 | }, 16 | de: { 17 | patient: 'Patient', 18 | task: 'Task', 19 | } 20 | } 21 | 22 | type PropertySubjectTypeSelectProps = Omit & { 23 | onValueChanged: (value: PropertySubjectType) => void, 24 | } 25 | 26 | /** 27 | * A Select for the Property SubjectType 28 | */ 29 | export const PropertySubjectTypeSelect = ({ 30 | overwriteTranslation, 31 | ...props 32 | }: PropsForTranslation) => { 33 | const translation = useTranslation([defaultPropertySubjectTypeSelectTranslation], overwriteTranslation) 34 | return ( 35 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /customer/api/mutations/customer_product_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 2 | import { QueryKeys } from '@/api/mutations/query_keys' 3 | import type { PriceCalculationProps } from '@/api/services/customer_product' 4 | import { CustomerProductsAPI } from '@/api/services/customer_product' 5 | import { useAuth } from '@/hooks/useAuth' 6 | 7 | export const useCustomerProductsSelfQuery = () => { 8 | const { authHeader } = useAuth() 9 | return useQuery({ 10 | queryKey: [QueryKeys.customerProduct, 'self'], 11 | queryFn: async () => { 12 | return await CustomerProductsAPI.getAllForCustomer(authHeader) 13 | }, 14 | }) 15 | } 16 | 17 | export const useCustomerProductsCalculateQuery = (priceCalculationProps: PriceCalculationProps[]) => { 18 | const { authHeader } = useAuth() 19 | return useQuery({ 20 | queryKey: [QueryKeys.customerProduct, ...priceCalculationProps.map(value => value.productUuid)], 21 | queryFn: async () => { 22 | return await CustomerProductsAPI.calculatePrice(priceCalculationProps, authHeader) 23 | }, 24 | }) 25 | } 26 | 27 | export const useCustomerProductQuery = (id?: string) => { 28 | const { authHeader } = useAuth() 29 | return useQuery({ 30 | queryKey: [QueryKeys.customerProduct, id], 31 | enabled: id !== undefined, 32 | queryFn: async () => { 33 | if(id === undefined) { 34 | throw 'Invalid id' 35 | } 36 | return await CustomerProductsAPI.get(id, authHeader) 37 | }, 38 | }) 39 | } 40 | 41 | export const useCustomerProductDeleteMutation = () => { 42 | const queryClient = useQueryClient() 43 | const { authHeader } = useAuth() 44 | return useMutation({ 45 | mutationFn: async (id: string) => { 46 | return await CustomerProductsAPI.delete(id, authHeader) 47 | }, 48 | onSuccess: () => { 49 | queryClient.invalidateQueries([QueryKeys.customer]).catch(reason => console.error(reason)) 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /tasks/components/modals/PatientDischargeModal.tsx: -------------------------------------------------------------------------------- 1 | import type { ConfirmDialogProps, Translation } from '@helpwave/hightide' 2 | import { ConfirmDialog, type PropsForTranslation, useTranslation } from '@helpwave/hightide' 3 | import type { PatientMinimalDTO } from '@helpwave/api-services/types/tasks/patient' 4 | 5 | type PatientDischargeModalTranslation = { 6 | followingPatient: string, 7 | dischargePatient: string, 8 | } 9 | 10 | const defaultPatientDischargeModalTranslation: Translation = { 11 | en: { 12 | followingPatient: 'The following patient will be discharged', 13 | dischargePatient: 'Discharge Patient?', 14 | }, 15 | de: { 16 | followingPatient: 'Der folgende Patient wird entlassen', 17 | dischargePatient: 'Patient entlassen?', 18 | } 19 | } 20 | 21 | export type PatientDischargeModalProps = Omit & { 22 | patient?: PatientMinimalDTO, 23 | } 24 | 25 | /** 26 | * A Modal to show when discharging Patients 27 | */ 28 | export const PatientDischargeModal = ({ 29 | overwriteTranslation, 30 | patient, 31 | buttonOverwrites, 32 | ...confirmDialogProps 33 | }: PropsForTranslation) => { 34 | const translation = useTranslation([defaultPatientDischargeModalTranslation], overwriteTranslation) 35 | return ( 36 | 42 | {patient && ( 43 | {patient.humanReadableIdentifier} 44 | )} 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /tasks/components/modals/ReSignInDialog.tsx: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | import { ConfirmDialog, type ConfirmDialogProps, type PropsForTranslation, useTranslation } from '@helpwave/hightide' 3 | 4 | type ReSignInDialogTranslation = { 5 | pleaseReSignIn: string, 6 | description: string, 7 | yes: string, 8 | no: string, 9 | } 10 | 11 | type ReSignInDialogProps = Omit 12 | 13 | const defaultReSignInModalTranslation: Translation = { 14 | en: { 15 | pleaseReSignIn: 'You triggered an action that requires a Re-Signin!', 16 | description: 'To see your organizational changes, you need to re-signin into helpwave tasks. Your changes will be visible afterwards.', 17 | yes: 'Yes, sign me out!', 18 | no: 'No, later.' 19 | }, 20 | de: { 21 | pleaseReSignIn: 'Deine Aktion erfordert eine erneute Anmeldung!', 22 | description: 'Um deine organisatorischen Änderungen zu sehen, musst du dich erneut bei helpwave tasks anmelden. Danach werden deine Änderungen sichtbar.', 23 | yes: 'Ja, logge mich aus!', 24 | no: 'Nein, später.' 25 | } 26 | } 27 | 28 | export const ReSignInDialog = ({ 29 | overwriteTranslation, 30 | ...modalProps 31 | }: PropsForTranslation) => { 32 | const translation = useTranslation([defaultReSignInModalTranslation], overwriteTranslation) 33 | 34 | return ( 35 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /customer/api/dataclasses/invoice.ts: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | 3 | export type InvoiceStatus = 'paid' | 'pending' | 'overdue' 4 | 5 | export type InvoiceStatusTranslation = Record 6 | 7 | export const defaultInvoiceStatusTranslation: Translation = { 8 | en: { 9 | paid: 'Paid', 10 | pending: 'Pending', 11 | overdue: 'Overdue', 12 | }, 13 | de: { 14 | paid: 'Bezahlt', 15 | pending: 'In Arbeit', 16 | overdue: 'Überfällig', 17 | } 18 | } 19 | 20 | export type Invoice = { 21 | /** The identifier of the Invoice */ 22 | uuid: string, 23 | /** The status of the Invoice */ 24 | status: InvoiceStatus, 25 | /** The optional title of the Invoice */ 26 | title: string, 27 | /** The date when the Invoice should be paid issued */ 28 | date: Date, 29 | /** The total amount to pay */ 30 | totalAmount: number, 31 | /** The day the Invoice was created */ 32 | createdAt?: Date, 33 | /** The day the Invoice or its status was last changed */ 34 | updatedAt?: Date, 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | export function fromJson(json: any): Invoice { 39 | return { 40 | uuid: json.uuid, 41 | status: json.status, 42 | title: json.title ?? '', 43 | date: new Date(json.date), 44 | totalAmount: json.total_amount, 45 | createdAt: json.created_at ? new Date(json.created_at) : undefined, 46 | updatedAt: json.updated_at ? new Date(json.updated_at) : undefined, 47 | } 48 | } 49 | 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | export function toJson(invoice: Invoice): any { 52 | return { 53 | uuid: invoice.uuid, 54 | status: invoice.status, 55 | date: invoice.date.toISOString(), 56 | total_amount: invoice.totalAmount, 57 | created_at: invoice.createdAt?.toISOString(), 58 | updated_at: invoice.updatedAt?.toISOString(), 59 | } 60 | } 61 | 62 | export const InvoiceHelpers = { fromJson, toJson } 63 | -------------------------------------------------------------------------------- /tasks/components/cards/BedCard.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { Translation } from '@helpwave/hightide' 3 | import { type PropsForTranslation, useTranslation } from '@helpwave/hightide' 4 | import { Plus } from 'lucide-react' 5 | import { DragCard, type DragCardProps } from './DragCard' 6 | 7 | type BedCardTranslation = { 8 | nobody: string, 9 | addPatient: string, 10 | } 11 | 12 | const defaultBedCardTranslation: Translation = { 13 | en: { 14 | nobody: 'nobody', 15 | addPatient: 'Add Patient' 16 | }, 17 | de: { 18 | nobody: 'frei', 19 | addPatient: 'Patient hinzufügen', 20 | } 21 | } 22 | 23 | export type BedCardProps = DragCardProps & { 24 | bedName: string, 25 | } 26 | 27 | /** 28 | * A Card for showing the Bed for Patient 29 | * 30 | * Shown instead of a PatientCard, if there is no patient assigned to the bed 31 | */ 32 | export const BedCard = ({ 33 | overwriteTranslation, 34 | bedName, 35 | onClick, 36 | isSelected, 37 | className, 38 | ...restCardProps 39 | }: PropsForTranslation) => { 40 | const translation = useTranslation([defaultBedCardTranslation], overwriteTranslation) 41 | return ( 42 | ( 43 | 49 |
50 | {bedName} 51 | {translation('nobody')} 52 |
53 |
54 |
55 | 56 | {translation('addPatient')} 57 |
58 |
59 |
60 | ) 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /api-services/README.md: -------------------------------------------------------------------------------- 1 | # helpwave api-services 2 | This package provides easy to use classes and data types for the helpwave ecosystem. 3 | 4 | Make sure to adapt a next .env file for your setting by changing the environment 5 | variable to your needs. 6 | 7 | ## Environment Variables 8 | # Environment Variables Documentation 9 | 10 | | Name | Type | Description | 11 | | ---------------------------------- | ------ |--------------------------------------------------------------------------------------------| 12 | | `NODE_ENV` | string | Defines the environment in which the application is running (`development`, `production`). | 13 | | `NEXT_PUBLIC_API_URL` | string | The base URL for the API that the frontend will communicate with. | 14 | | `NEXT_PUBLIC_OFFLINE_API` | string | Whether to use the offilne mode (`true`, `false`). | 15 | | `NEXT_PUBLIC_REQUEST_LOGGING` | string | Enables logging of API requests if set to `true`. | 16 | | `NEXT_PUBLIC_OAUTH_ISSUER_URL` | string | The OAuth issuer URL for authentication. | 17 | | `NEXT_PUBLIC_OAUTH_REDIRECT_URI` | string | The URI where the OAuth provider will redirect after authentication. | 18 | | `NEXT_PUBLIC_OAUTH_CLIENT_ID` | string | The client ID for OAuth authentication. | 19 | | `NEXT_PUBLIC_OAUTH_SCOPES` | string | The requested scopes for OAuth authentication. | 20 | | `NEXT_PUBLIC_FAKE_TOKEN_ENABLE` | string | Enables the use of a fake token for testing purposes if set to `true`. | 21 | | `NEXT_PUBLIC_FAKE_TOKEN` | string | The fake token value to be used when `NEXT_PUBLIC_FAKE_TOKEN_ENABLE` is `true`. | 22 | -------------------------------------------------------------------------------- /tasks/components/selects/TaskVisibilitySelect.tsx: -------------------------------------------------------------------------------- 1 | import { Select, SelectOption, type SelectProps } from '@helpwave/hightide' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 3 | 4 | type TaskVisibilitySelectTranslation = { 5 | private: string, 6 | public: string, 7 | } 8 | 9 | const defaultTaskVisibilitySelectTranslation = { 10 | en: { 11 | private: 'private', 12 | public: 'public', 13 | }, 14 | de: { 15 | private: 'privat', 16 | public: 'öffentlich', 17 | } 18 | } 19 | 20 | type TaskVisibilitySelectProps = Omit & { 21 | value?: boolean, 22 | onValueChanged?: (isVisible: boolean) => void, 23 | } 24 | 25 | /** 26 | * A component for selecting a TaskVisibility 27 | * 28 | * The state is managed by the parent 29 | */ 30 | export const TaskVisibilitySelect = ({ 31 | overwriteTranslation, 32 | value, 33 | onValueChanged, 34 | ...selectProps 35 | }: PropsForTranslation) => { 36 | const translation = useTranslation([defaultTaskVisibilitySelectTranslation], overwriteTranslation) 37 | const options = [ 38 | { value: 'public', label: translation('public') }, 39 | { value: 'private', label: translation('private') }, 40 | ] 41 | 42 | return ( 43 | 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /api-services/service/properties/PropertyViewSourceService.ts: -------------------------------------------------------------------------------- 1 | import { APIServices } from '../../services' 2 | import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' 3 | import { 4 | FilterUpdate, 5 | UpdatePropertyViewRuleRequest 6 | } from '@helpwave/proto-ts/services/property_svc/v1/property_views_svc_pb' 7 | import { 8 | PatientPropertyMatcher, 9 | TaskPropertyMatcher 10 | } from '@helpwave/proto-ts/services/property_svc/v1/property_value_svc_pb' 11 | import type { PropertySubjectType } from '../../types/properties/property' 12 | 13 | export type PropertyViewRuleFilterUpdate = { 14 | subjectId: string, 15 | appendToAlwaysInclude?: string[], 16 | removeFromAlwaysInclude?: string[], 17 | appendToDontAlwaysInclude?: string[], 18 | removeFromDontAlwaysInclude?: string[], 19 | } 20 | 21 | export const PropertyViewSourceService = { 22 | update: async (update: PropertyViewRuleFilterUpdate, subjectType: PropertySubjectType, wardId?: string): Promise => { 23 | const req = new UpdatePropertyViewRuleRequest() 24 | if (subjectType === 'patient') { 25 | const matcher = new PatientPropertyMatcher().setPatientId(update.subjectId) 26 | if (wardId) { 27 | matcher.setWardId(wardId) 28 | } 29 | req.setPatientMatcher(matcher) 30 | } 31 | if (subjectType === 'task') { 32 | const matcher = new TaskPropertyMatcher().setTaskId(update.subjectId) 33 | if (wardId) { 34 | matcher.setWardId(wardId) 35 | } 36 | req.setTaskMatcher(matcher) 37 | } 38 | 39 | req.setFilterUpdate(new FilterUpdate() 40 | .setAppendToAlwaysIncludeList(update.appendToAlwaysInclude ?? []) 41 | .setRemoveFromAlwaysIncludeList(update.removeFromAlwaysInclude ?? []) 42 | .setAppendToDontAlwaysIncludeList(update.removeFromAlwaysInclude ?? []) 43 | .setRemoveFromDontAlwaysIncludeList(update.removeFromDontAlwaysInclude ?? [])) 44 | 45 | await APIServices.propertyViewSource.updatePropertyViewRule(req, getAuthenticatedGrpcMetadata()) 46 | return true 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /customer/api/auth/authService.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { CLIENT_ID, OIDC_PROVIDER, POST_LOGOUT_REDIRECT_URI, REDIRECT_URI } from '@/api/config' 4 | import type { User } from 'oidc-client-ts' 5 | import { UserManager } from 'oidc-client-ts' 6 | 7 | const userManager = new UserManager({ 8 | authority: OIDC_PROVIDER, 9 | client_id: CLIENT_ID, 10 | redirect_uri: REDIRECT_URI, 11 | response_type: 'code', 12 | scope: 'openid profile email', 13 | post_logout_redirect_uri: POST_LOGOUT_REDIRECT_URI, 14 | // userStore: userStore, // TODO Consider persisting user data across sessions 15 | }) 16 | 17 | export const signUp = () => { 18 | return userManager.signinRedirect() 19 | } 20 | 21 | export const login = (redirectURI?: string) => { 22 | return userManager.signinRedirect({ redirect_uri: redirectURI }) 23 | } 24 | 25 | export const handleCallback = async () => { 26 | return await userManager.signinRedirectCallback() 27 | } 28 | 29 | export const logout = () => { 30 | return userManager.signoutRedirect() 31 | } 32 | 33 | export const getUser = async (): Promise => { 34 | return await userManager.getUser() 35 | } 36 | 37 | export const renewToken = async () => { 38 | return await userManager.signinSilent() 39 | } 40 | 41 | export const removeUser = async () => { 42 | return await userManager.removeUser() 43 | } 44 | 45 | export const restoreSession = async (): Promise => { 46 | if (typeof window === 'undefined') return // Prevent SSR access 47 | const user = await userManager.getUser() 48 | if (!user) return 49 | 50 | // If access token is expired, refresh it 51 | if (user.expired) { 52 | try { 53 | console.debug('Access token expired, refreshing...') 54 | const refreshedUser = await renewToken() 55 | return refreshedUser ?? undefined 56 | } catch (error) { 57 | console.debug('Silent token renewal failed', error) 58 | return 59 | } 60 | } 61 | 62 | return user 63 | } 64 | 65 | export const onTokenExpiringCallback = (callback: () => void) => { 66 | userManager.events.addAccessTokenExpiring(callback) 67 | } 68 | -------------------------------------------------------------------------------- /tasks/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Link from 'next/link' 3 | import Head from 'next/head' 4 | import type { PropsForTranslation, Translation } from '@helpwave/hightide' 5 | import { HelpwaveLogo, useTranslation } from '@helpwave/hightide' 6 | import titleWrapper from '@/utils/titleWrapper' 7 | import { PageWithHeader } from '@/components/layout/PageWithHeader' 8 | 9 | type NotFoundTranslation = { 10 | notFound: string, 11 | notFoundDescription1: string, 12 | notFoundDescription2: string, 13 | homePage: string, 14 | } 15 | 16 | const defaultNotFoundTranslation: Translation = { 17 | en: { 18 | notFound: '404 - Page not found', 19 | notFoundDescription1: 'This is definitely not the page you\'re looking for', 20 | notFoundDescription2: 'Let me take you to the', 21 | homePage: 'home page' 22 | }, 23 | de: { 24 | notFound: '404 - Seite nicht gefunden', 25 | notFoundDescription1: 'Das ist definitiv nicht die Seite nach der Sie suchen', 26 | notFoundDescription2: 'Zurück zur', 27 | homePage: 'home page' 28 | } 29 | } 30 | 31 | const NotFound: NextPage = ({ overwriteTranslation }: PropsForTranslation) => { 32 | const translation = useTranslation([defaultNotFoundTranslation], overwriteTranslation) 33 | return ( 34 | 35 | 36 | {titleWrapper()} 37 | 38 |
41 | 42 |

{translation('notFound')}

43 |

{translation('notFoundDescription1')}...

44 |

{translation('notFoundDescription2')} {translation('homePage')}.

46 |
47 |
48 | ) 49 | } 50 | 51 | export default NotFound 52 | -------------------------------------------------------------------------------- /tasks/components/layout/NewsFeed.tsx: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 3 | import { useLanguage } from '@helpwave/hightide' 4 | import { NewsDisplay } from '../NewsDisplay' 5 | import { ColumnTitle } from '../ColumnTitle' 6 | import type { LocalizedNews } from '@/utils/news' 7 | import { filterNews } from '@/utils/news' 8 | 9 | type NewsFeedTranslation = { 10 | title: string, 11 | noNews: string, 12 | } 13 | 14 | const defaultNewsFeedTranslations: Translation = { 15 | en: { 16 | title: 'What\'s new in helpwave tasks?', 17 | noNews: 'No News in your language found' 18 | }, 19 | de: { 20 | title: 'Was ist neu in helpwave tasks?', 21 | noNews: 'Keine Neuheiten in deiner Sprache gefunden' 22 | } 23 | } 24 | 25 | export type NewsFeedProps = { 26 | localizedNews: LocalizedNews, 27 | width?: number, 28 | } 29 | 30 | /** 31 | * The right side of the dashboard page 32 | */ 33 | export const NewsFeed = ({ 34 | overwriteTranslation, 35 | localizedNews, 36 | width 37 | }: PropsForTranslation) => { 38 | const translation = useTranslation([defaultNewsFeedTranslations], overwriteTranslation) 39 | // The value of how much space a FeatureDisplay needs before the title can be displayed on its left 40 | // Given in px 41 | const widthForAppearanceChange = 600 42 | let usedLanguage = useLanguage().language 43 | usedLanguage = overwriteTranslation?.language ?? usedLanguage 44 | const newsFilter = 'tasks' 45 | return ( 46 |
47 | 48 | {usedLanguage ? filterNews(localizedNews[usedLanguage], [newsFilter]).map(news => ( 49 | 54 | )) : ( 55 |
56 | {translation('noNews')} 57 |
58 | )} 59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /customer/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Helpwave } from '@helpwave/hightide' 2 | import type { NextPage } from 'next' 3 | import Link from 'next/link' 4 | import type { Translation } from '@helpwave/hightide' 5 | import type { PropsForTranslation } from '@helpwave/hightide' 6 | import { useTranslation } from '@helpwave/hightide' 7 | import titleWrapper from '@/utils/titleWrapper' 8 | import { Page } from '@/components/layout/Page' 9 | 10 | type NotFoundTranslation = { 11 | notFound: string, 12 | notFoundDescription1: string, 13 | notFoundDescription2: string, 14 | homePage:string, 15 | } 16 | 17 | const defaultNotFoundTranslation: Translation = { 18 | en: { 19 | notFound: '404 - Page not found', 20 | notFoundDescription1: 'This is definitely not the page you\'re looking for', 21 | notFoundDescription2: 'Let me take you to the', 22 | homePage: 'home page' 23 | }, 24 | de: { 25 | notFound: '404 - Seite nicht gefunden', 26 | notFoundDescription1: 'Das ist definitiv nicht die Seite nach der Sie suchen', 27 | notFoundDescription2: 'Zurück zur', 28 | homePage: 'home page' 29 | } 30 | } 31 | 32 | const NotFound: NextPage = ({ overwriteTranslation }: PropsForTranslation) => { 33 | const translation = useTranslation(defaultNotFoundTranslation, overwriteTranslation) 34 | return ( 35 | 36 |
37 |
38 | 39 |

{translation.notFound}

40 |

{translation.notFoundDescription1}...

41 |

{translation.notFoundDescription2} {translation.homePage}.

42 |
43 |
44 |
45 | ) 46 | } 47 | 48 | export default NotFound 49 | -------------------------------------------------------------------------------- /customer/api/mutations/user_seat_mutations.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 2 | import { QueryKeys } from '@/api/mutations/query_keys' 3 | import type { UserSeat } from '@/api/dataclasses/user_seat' 4 | 5 | // TODO delete later 6 | const userSeatData: UserSeat[] = [ 7 | { 8 | customerUUID: 'customer', 9 | email: 'test1@helpwave.de', 10 | firstName: 'Max', 11 | lastName: 'Mustermann', 12 | role: 'admin', 13 | enabled: true 14 | }, 15 | { 16 | customerUUID: 'customer', 17 | email: 'test2@helpwave.de', 18 | firstName: 'Mary', 19 | lastName: 'Jane', 20 | role: 'admin', 21 | enabled: true 22 | }, 23 | { 24 | customerUUID: 'customer', 25 | email: 'test3@helpwave.de', 26 | firstName: 'Maxine', 27 | lastName: 'Mustermann', 28 | role: 'user', 29 | enabled: true 30 | }, 31 | { 32 | customerUUID: 'customer', 33 | email: 'test4@helpwave.de', 34 | firstName: 'John', 35 | lastName: 'Doe', 36 | role: 'user', 37 | enabled: false 38 | }, 39 | { 40 | customerUUID: 'customer', 41 | email: 'test5@helpwave.de', 42 | firstName: 'Peter', 43 | lastName: 'Parker', 44 | role: 'user', 45 | enabled: false 46 | }, 47 | ] 48 | 49 | export const useUserSeatsQuery = () => { 50 | return useQuery({ 51 | queryKey: [QueryKeys.userSeat, 'all'], 52 | queryFn: async () => { 53 | // TODO do request here with auth data 54 | return userSeatData 55 | }, 56 | }) 57 | } 58 | 59 | export const useUserSeatUpdateMutation = () => { 60 | const queryClient = useQueryClient() 61 | return useMutation({ 62 | mutationFn: async (userSeat: UserSeat) => { 63 | // TODO do request here 64 | 65 | const index = userSeatData.findIndex(e => e.customerUUID === userSeat.customerUUID && e.email === userSeat.email) 66 | if (index === -1) { 67 | throw 'User Seat not found' 68 | } 69 | userSeatData[index] = userSeat 70 | 71 | return userSeat 72 | }, 73 | onSuccess: () => { 74 | queryClient.invalidateQueries([QueryKeys.customer]).catch(reason => console.error(reason)) 75 | } 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /tasks/utils/api.ts: -------------------------------------------------------------------------------- 1 | import type { z } from 'zod' 2 | import { getAPIServiceConfig } from '@helpwave/api-services/config/config' 3 | 4 | const config = getAPIServiceConfig() 5 | 6 | const addContentType = (headers?: Headers) => { 7 | if (!headers) { 8 | headers = new Headers() 9 | } 10 | headers.set('Content-Type', 'application/json') 11 | return headers 12 | } 13 | 14 | // TODO: add a way to log requests to the console (using console.groupCollapsed() & console.groupEnd()) 15 | const create = (path: string, options: RequestInit) => ({ 16 | mock: (mockedData: T) => ({ 17 | json: >(schema: S): Promise> => { 18 | const jsonPromise = config.offlineAPI ? Promise.resolve(mockedData) : fetch(config.apiUrl + path, options).then(res => res.json()) 19 | return jsonPromise.then(schema.parse) 20 | }, 21 | 22 | text: >(schema: S): Promise> => { 23 | if (typeof mockedData !== 'string') throw new Error('Type mismatch: mocked data should be of type string when used in conjunctinon with ".text"') 24 | 25 | const stringPromise = config.offlineAPI ? Promise.resolve(mockedData) : fetch(config.apiUrl + path, options).then(res => res.text()) 26 | return stringPromise.then(schema.parse) 27 | } 28 | }) 29 | }) 30 | 31 | const api = { 32 | get: (path: string, headers?: Headers) => 33 | create(path, { method: 'GET', headers, body: null }), 34 | 35 | post: (path: string, body: BodyType, headers?: Headers) => 36 | create(path, { method: 'POST', headers: addContentType(headers), body: JSON.stringify(body) }), 37 | 38 | put: (path: string, body: BodyType, headers?: Headers) => 39 | create(path, { method: 'PUT', headers: addContentType(headers), body: JSON.stringify(body) }), 40 | 41 | patch: (path: string, body: BodyType, headers?: Headers) => 42 | create(path, { method: 'PATCH', headers: addContentType(headers), body: JSON.stringify(body) }), 43 | 44 | delete: (path: string, body: BodyType, headers?: Headers) => 45 | create(path, { method: 'DELETE', headers: addContentType(headers), body: JSON.stringify(body) }), 46 | } 47 | 48 | export default api 49 | -------------------------------------------------------------------------------- /tasks/components/dnd-kit-instances/patients.ts: -------------------------------------------------------------------------------- 1 | import type { PatientMinimalDTO, PatientWithTasksNumberDTO } from '@helpwave/api-services/types/tasks/patient' 2 | import type { BedWithPatientWithTasksNumberDTO } from '@helpwave/api-services/types/tasks/bed' 3 | import type { RoomOverviewDTO } from '@helpwave/api-services/types/tasks/room' 4 | import type { 5 | DragStartEvent as DragStartEventFree, 6 | DragMoveEvent as DragMoveEventFree, 7 | DragOverEvent as DragOverEventFree, 8 | DragEndEvent as DragEndEventFree, 9 | DragCancelEvent as DragCancelEventFree 10 | } from '@/components/dnd-kit/typesafety' 11 | import { DndContext as DndContextFree } from '@/components/dnd-kit/typesafety' 12 | import { Droppable as DroppableFree } from '@/components/dnd-kit/Droppable' 13 | import { Draggable as DraggableFree } from '@/components/dnd-kit/Draggable' 14 | 15 | export type WardOverviewDraggableData = { 16 | type: 'patientListItem', 17 | patient: PatientMinimalDTO, 18 | discharged: boolean, 19 | assigned: boolean, 20 | } | { 21 | type: 'assignedPatient', 22 | bed: BedWithPatientWithTasksNumberDTO, 23 | room: RoomOverviewDTO, 24 | patient: PatientWithTasksNumberDTO, 25 | } 26 | export type WardOverViewDroppableData = { 27 | type: 'bed', 28 | bed: BedWithPatientWithTasksNumberDTO, 29 | room: RoomOverviewDTO, 30 | patient?: PatientMinimalDTO, 31 | } | { 32 | type: 'section', 33 | section: 'unassigned' | 'discharged', 34 | } 35 | 36 | export const WardOverviewDroppable = DroppableFree 37 | export const WardOverviewDraggable = DraggableFree 38 | export const WardOverviewDndContext = DndContextFree 39 | 40 | export type WardOverviewDragStartEvent = DragStartEventFree 41 | export type WardOverviewDragMoveEvent = DragMoveEventFree 42 | export type WardOverviewDragOverEvent = DragOverEventFree 43 | export type WardOverviewDragEndEvent = DragEndEventFree 44 | export type WardOverviewDragCancelEvent = DragCancelEventFree 45 | -------------------------------------------------------------------------------- /tasks/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import type { AppProps } from 'next/app' 3 | import { Inter, Space_Grotesk as SpaceGrotesk } from 'next/font/google' 4 | import { isMobile } from 'react-device-detect' 5 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 6 | import { LanguageProvider, ThemeProvider } from '@helpwave/hightide' 7 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools/production' 8 | import { ProvideAuth } from '@helpwave/api-services/authentication/useAuth' 9 | import titleWrapper from '@/utils/titleWrapper' 10 | import MobileInterceptor from '@/components/MobileInterceptor' 11 | import '../globals.css' 12 | import { InitializationChecker } from '@/components/layout/InitializationChecker' 13 | import { getConfig } from '@/utils/config' 14 | 15 | const inter = Inter({ 16 | subsets: ['latin'], 17 | variable: '--font-inter' 18 | }) 19 | 20 | const spaceGrotesk = SpaceGrotesk({ 21 | subsets: ['latin'], 22 | variable: '--font-space-grotesk' 23 | }) 24 | 25 | const queryClient = new QueryClient() 26 | 27 | function MyApp({ 28 | Component, 29 | pageProps 30 | }: AppProps) { 31 | const config = getConfig() 32 | return ( 33 | 34 | 35 | { /* v Scans the user agent */} 36 | {!isMobile ? ( 37 | 38 | 39 | 40 | 41 | {titleWrapper()} 42 | 50 | 51 | 52 | {config.env === 'development' && } 53 | 54 | 55 | 56 | ) : ()} 57 | 58 | 59 | ) 60 | } 61 | 62 | export default MyApp 63 | -------------------------------------------------------------------------------- /tasks/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | ) 39 | } 40 | } 41 | 42 | export default MyDocument 43 | -------------------------------------------------------------------------------- /customer/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | ) 39 | } 40 | } 41 | 42 | export default MyDocument 43 | -------------------------------------------------------------------------------- /tasks/components/cards/PatientCard.tsx: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | import { type PropsForTranslation, useTranslation } from '@helpwave/hightide' 3 | import { PillLabelsColumn } from '../PillLabel' 4 | import { DragCard, type DragCardProps } from './DragCard' 5 | import clsx from 'clsx' 6 | 7 | type PatientCardTranslation = { 8 | bedNotAssigned: string, 9 | } 10 | 11 | const defaultPatientCardTranslations: Translation = { 12 | en: { 13 | bedNotAssigned: 'Not Assigned', 14 | }, 15 | de: { 16 | bedNotAssigned: 'Nicht Zugewiesen', 17 | } 18 | } 19 | 20 | type TaskCounts = { 21 | todo: number, 22 | inProgress: number, 23 | done: number, 24 | } 25 | 26 | export type PatientCardProps = DragCardProps & { 27 | bedName?: string, 28 | patientName: string, 29 | taskCounts?: TaskCounts, 30 | } 31 | 32 | /** 33 | * A Card for displaying a Patient and the tasks 34 | */ 35 | export const PatientCard = ({ 36 | overwriteTranslation, 37 | bedName, 38 | patientName, 39 | taskCounts, 40 | isSelected, 41 | onClick, 42 | className, 43 | ...restCardProps 44 | }: PropsForTranslation) => { 45 | const translation = useTranslation([defaultPatientCardTranslations], overwriteTranslation) 46 | return ( 47 | 53 |
54 | {bedName ?? translation('bedNotAssigned')} 55 | {patientName} 56 |
57 | {taskCounts && ( 58 |
59 | 64 |
65 | )} 66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /tasks/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | import Link from 'next/link' 3 | import clsx from 'clsx' 4 | import { HelpwaveLogo } from '@helpwave/hightide' 5 | 6 | export type HeaderProps = { 7 | title?: string, 8 | leftSide?: ReactNode[], 9 | leftSideClassName?: string, 10 | rightSide?: ReactNode[], 11 | rightSideClassName?: string, 12 | /** 13 | * @default true 14 | */ 15 | withIcon?: boolean, 16 | } 17 | 18 | /** 19 | * The basic header for most pages 20 | * 21 | * Structure: 22 | * 23 | * Logo Title | left right 24 | * 25 | * each element in left and right is also seperated by the divider 26 | */ 27 | const Header = ({ 28 | title, 29 | leftSide = [], 30 | leftSideClassName, 31 | rightSide = [], 32 | rightSideClassName, 33 | withIcon = true 34 | }: HeaderProps) => { 35 | return ( 36 |
38 |
39 |
40 | {withIcon && ( 41 | 42 | 43 | 44 | )} 45 | {title && {title}} 46 | {leftSide?.filter(value => value !== undefined).map((value, index) => ( 47 |
48 | {(index !== 0 || title || withIcon) &&
} 49 | {value} 50 |
51 | ))} 52 |
53 |
54 | {rightSide?.filter(value => value !== undefined).map((value, index) => ( 55 |
56 | {value} 57 | {index !== rightSide?.length - 1 &&
} 58 |
59 | ))} 60 |
61 |
62 |
63 | ) 64 | } 65 | 66 | export { Header } 67 | -------------------------------------------------------------------------------- /tasks/components/cards/TaskTemplateCard.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import type { Translation } from '@helpwave/hightide' 3 | import { Chip, type PropsForTranslation, useTranslation } from '@helpwave/hightide' 4 | import { EditCard, type EditCardProps } from './EditCard' 5 | 6 | type TaskTemplateCardTranslation = { 7 | subtask: string, 8 | edit: string, 9 | personal: string, 10 | ward: string, 11 | } 12 | 13 | const defaultTaskTemplateCardTranslations: Translation = { 14 | en: { 15 | subtask: 'Subtasks', 16 | edit: 'Edit', 17 | personal: 'Personal', 18 | ward: 'Ward' 19 | }, 20 | de: { 21 | subtask: 'Unteraufgaben', 22 | edit: 'Bearbeiten', 23 | personal: 'Persönlich', 24 | ward: 'Station' 25 | } 26 | } 27 | 28 | export type TaskTemplateCardProps = EditCardProps & { 29 | name: string, 30 | subtaskCount: number, 31 | typeForLabel?: 'ward' | 'personal', 32 | } 33 | 34 | /** 35 | * A Card showing a TaskTemplate 36 | */ 37 | export const TaskTemplateCard = ({ 38 | overwriteTranslation, 39 | name, 40 | subtaskCount, 41 | typeForLabel, 42 | className, 43 | ...editCardProps 44 | }: PropsForTranslation) => { 45 | const translation = useTranslation([defaultTaskTemplateCardTranslations], overwriteTranslation) 46 | return ( 47 | 51 |
52 |
53 | {name} 54 | {typeForLabel && ( 55 | 60 | {typeForLabel === 'ward' ? translation('ward') : translation('personal')} 61 | 62 | )} 63 |
64 | {subtaskCount + ' ' + translation('subtask')} 65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /api-services/types/tasks/task.ts: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | 3 | export type SubtaskDTO = { 4 | id: string, 5 | name: string, 6 | isDone: boolean, 7 | taskId: string, 8 | } 9 | 10 | // The order in the array defines the sorting order 11 | const taskStatus = ['done', 'inProgress', 'todo'] as const 12 | 13 | export type TaskStatus = typeof taskStatus[number] 14 | 15 | export type TaskStatusTranslationType = Record 16 | 17 | const taskStatusTranslation: Translation = { 18 | en: { 19 | todo: 'Todo', 20 | inProgress: 'In Progress', 21 | done: 'Done' 22 | }, 23 | de: { 24 | todo: 'Todo', 25 | inProgress: 'In Arbeit', 26 | done: 'Fertig' 27 | } 28 | } 29 | 30 | type TaskStatusColor = { 31 | background: string, 32 | icon: string, 33 | text: string, 34 | } 35 | 36 | const taskColorMapping: Record = { 37 | todo: { 38 | background: 'bg-todo-background', 39 | text: 'text-todo-text', 40 | icon: 'text-todo-icon', 41 | }, 42 | inProgress: { 43 | background: 'bg-inprogress-background', 44 | text: 'text-inprogress-text', 45 | icon: 'text-inprogress-icon', 46 | }, 47 | done: { 48 | background: 'bg-done-background', 49 | text: 'text-done-text', 50 | icon: 'text-done-icon', 51 | }, 52 | } 53 | 54 | export const TaskStatusUtil = { 55 | taskStatus, 56 | translation: taskStatusTranslation, 57 | compare: (a: TaskStatus, b: TaskStatus): number => { 58 | return taskStatus.indexOf(a) - taskStatus.indexOf(b) 59 | }, 60 | colors: taskColorMapping 61 | } 62 | 63 | export type TaskDTO = { 64 | id: string, 65 | name: string, 66 | assignee?: string, 67 | notes: string, 68 | status: TaskStatus, 69 | subtasks: SubtaskDTO[], 70 | dueDate?: Date, 71 | createdAt?: Date, 72 | creatorId?: string, 73 | patientId: string, 74 | isPublicVisible: boolean, 75 | } 76 | 77 | export const emptyTask: TaskDTO = { 78 | id: '', 79 | name: '', 80 | notes: '', 81 | status: 'todo', 82 | subtasks: [], 83 | patientId: '', 84 | isPublicVisible: false 85 | } 86 | 87 | export type TaskMinimalDTO = { 88 | id: string, 89 | name: string, 90 | status: TaskStatus, 91 | } 92 | 93 | export type SortedTasks = Record 94 | export const emptySortedTasks: SortedTasks = { 95 | todo: [], 96 | inProgress: [], 97 | done: [] 98 | } 99 | -------------------------------------------------------------------------------- /api-services/mutations/properties/property_mutations.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationOptions } from '@tanstack/react-query' 2 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 3 | import { QueryKeys } from '../query_keys' 4 | import type { Property, SelectOption, PropertySubjectType } from '../../types/properties/property' 5 | import { PropertyService } from '../../service/properties/PropertyService' 6 | 7 | export const usePropertyListQuery = (subjectType?: PropertySubjectType) => { 8 | return useQuery({ 9 | queryKey: [QueryKeys.properties, subjectType ?? 'all'], 10 | queryFn: async (): Promise => { 11 | return await PropertyService.getList(subjectType) 12 | }, 13 | }) 14 | } 15 | 16 | export const usePropertyQuery = (id?: string) => { 17 | return useQuery({ 18 | queryKey: [QueryKeys.properties, id], 19 | enabled: !!id, 20 | queryFn: async (): Promise => { 21 | return await PropertyService.get(id!) 22 | }, 23 | }) 24 | } 25 | 26 | export const usePropertyCreateMutation = (options?: UseMutationOptions) => { 27 | const queryClient = useQueryClient() 28 | return useMutation({ 29 | ...options, 30 | mutationFn: async (property: Property) => { 31 | return await PropertyService.create(property) 32 | }, 33 | onSuccess: (data, variables, context) => { 34 | if(options?.onSuccess) { 35 | options.onSuccess(data, variables, context) 36 | } 37 | queryClient.invalidateQueries([QueryKeys.properties]).catch(console.error) 38 | } 39 | }) 40 | } 41 | 42 | export type PropertySelectDataUpdate = { 43 | add: SelectOption[], 44 | update: SelectOption[], 45 | remove: string[], 46 | } 47 | 48 | export type PropertyUpdateType = { 49 | property: Property, 50 | selectUpdate?: PropertySelectDataUpdate, 51 | } 52 | 53 | export const usePropertyUpdateMutation = (options?: UseMutationOptions) => { 54 | const queryClient = useQueryClient() 55 | return useMutation({ 56 | ...options, 57 | mutationFn: async (props) => { 58 | return await PropertyService.update(props) 59 | }, 60 | onSuccess: (data, variables, context) => { 61 | if(options?.onSuccess) { 62 | options.onSuccess(data, variables, context) 63 | } 64 | queryClient.invalidateQueries([QueryKeys.properties]).catch(console.error) 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /api-services/util/keycloakAdapter.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-nocheck 3 | 4 | 5 | /** 6 | * keycloakAdapter.ts is adopted from https://github.com/keycloak/keycloak/blob/cdfd46f19110061f7783161c389365c9da844955/js/libs/keycloak-js/src/keycloak.js#L1312 7 | * This implementation injects ?prompt=select_account to all authentication requests 8 | */ 9 | 10 | import type Keycloak, { KeycloakAdapter, KeycloakLoginOptions } from 'keycloak-js' 11 | import keycloakInstance from './keycloak' 12 | 13 | function createPromise() { 14 | // Need to create a native Promise which also preserves the 15 | // interface of the custom promise type previously used by the API 16 | const p = { 17 | setSuccess: function(result) { 18 | p.resolve(result) 19 | }, 20 | 21 | setError: function(result) { 22 | p.reject(result) 23 | } 24 | } 25 | 26 | p.promise = new Promise(function(resolve, reject) { 27 | p.resolve = resolve 28 | p.reject = reject 29 | }) 30 | 31 | return p 32 | } 33 | 34 | export const loadKeycloakAdapter = (kc: Keycloak): KeycloakAdapter => { 35 | const redirectUri = function(options: KeycloakLoginOptions) { 36 | if (options && options.redirectUri) { 37 | return options.redirectUri 38 | } else if (kc.redirectUri) { 39 | return kc.redirectUri 40 | } else { 41 | return location.href 42 | } 43 | } 44 | 45 | return { 46 | login: function(options) { 47 | const u = new URL(kc.createLoginUrl(options)) 48 | 49 | if (Object.keys(keycloakInstance?.tokenParsed?.organization || []).length !== 0) { 50 | u.searchParams.set('prompt', 'select_account') // Shows the organization selector on login and re-login 51 | } 52 | 53 | window.location.assign(u) 54 | return createPromise().promise 55 | }, 56 | 57 | logout: async function(options) { 58 | window.location.replace(kc.createLogoutUrl(options)) 59 | }, 60 | 61 | register: function(options) { 62 | window.location.assign(kc.createRegisterUrl(options)) 63 | return createPromise().promise 64 | }, 65 | 66 | accountManagement: function() { 67 | const accountUrl = kc.createAccountUrl() 68 | if (typeof accountUrl !== 'undefined') { 69 | window.location.href = accountUrl 70 | } else { 71 | throw 'Not supported by the OIDC server' 72 | } 73 | return createPromise().promise 74 | }, 75 | 76 | redirectUri, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /customer/components/ContractList.tsx: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | import type { PropsForTranslation } from '@helpwave/hightide' 3 | import { useTranslation } from '@helpwave/hightide' 4 | import { useContractsForProductsQuery } from '@/api/mutations/contract_mutations' 5 | import { LoadingAndErrorComponent } from '@helpwave/hightide' 6 | import Link from 'next/link' 7 | import { ExternalLink } from 'lucide-react' 8 | 9 | type ContractListTranslation = { 10 | contracts: string, 11 | noContracts: string, 12 | show: string, 13 | } 14 | 15 | const defaultContractListTranslations: Translation = { 16 | en: { 17 | contracts: 'Contracts', 18 | noContracts: 'No Contracts', 19 | show: 'Show' 20 | }, 21 | de: { 22 | contracts: 'Verträge', 23 | noContracts: 'Keine Verträge', 24 | show: 'Anzeigen' 25 | } 26 | } 27 | 28 | export type ContractListProps = { 29 | productIds: string[], 30 | } 31 | 32 | export const ContractList = ({ 33 | productIds, 34 | overwriteTranslation 35 | }: PropsForTranslation) => { 36 | const translation = useTranslation(defaultContractListTranslations, overwriteTranslation) 37 | const { 38 | data: contracts, 39 | isLoading, 40 | isError, 41 | } = useContractsForProductsQuery(productIds) 42 | 43 | return ( 44 | 45 | {contracts && ( 46 |
47 |

{translation.contracts}

48 | {contracts.length === 0 ? ( 49 | {translation.noContracts} 50 | ) : ( 51 | contracts.map(contract => ( 52 | 58 | {contract.key} 59 | 60 | ( 61 |
62 | {`${translation.show}`} 63 | 64 |
65 | ) 66 |
67 | 68 | )) 69 | )} 70 |
71 | )} 72 |
73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /customer/components/pages/create-organization.tsx: -------------------------------------------------------------------------------- 1 | import type { Translation } from '@helpwave/hightide' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 3 | import { Page } from '@/components/layout/Page' 4 | import titleWrapper from '@/utils/titleWrapper' 5 | import type { CustomerCreate } from '@/api/dataclasses/customer' 6 | import { Section } from '@/components/layout/Section' 7 | import { ContactInformationForm } from '@/components/forms/ContactInformationForm' 8 | 9 | type CreateOrganizationTranslation = { 10 | createOrganization: string, 11 | createOrganizationDescription: string, 12 | } 13 | 14 | const defaultCreateOrganizationTranslations: Translation = { 15 | en: { 16 | createOrganization: 'Create Organization', 17 | createOrganizationDescription: 'You must create a Organization to use our Service.' 18 | }, 19 | de: { 20 | createOrganization: 'Create Organization', 21 | createOrganizationDescription: 'Du musst eine Organisation erstellen um unseren Service nutzen zu können', 22 | } 23 | } 24 | 25 | type CreateOrganizationPageProps = { 26 | createOrganization: (organization: CustomerCreate) => Promise, 27 | } 28 | 29 | export const CreateOrganizationPage = ({ 30 | createOrganization, 31 | overwriteTranslation 32 | }: PropsForTranslation) => { 33 | const translation = useTranslation(defaultCreateOrganizationTranslations, overwriteTranslation) 34 | 35 | return ( 36 | 42 |
43 |

{translation.createOrganization}

44 | {translation.createOrganizationDescription} 45 | { 59 | createOrganization(data).catch(console.error) 60 | }} 61 | /> 62 |
63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /api-services/util/ReceiveUpdatesStreamHandler.ts: -------------------------------------------------------------------------------- 1 | import type { ClientReadableStream } from 'grpc-web' 2 | import type { ReceiveUpdatesResponse } from '@helpwave/proto-ts/services/updates_svc/v1/updates_svc_pb' 3 | import { ReceiveUpdatesRequest } from '@helpwave/proto-ts/services/updates_svc/v1/updates_svc_pb' 4 | import { Subject } from 'rxjs' 5 | import { APIServices } from '../services' 6 | import { getAuthenticatedGrpcMetadata } from '../authentication/grpc_metadata' 7 | 8 | export const connectReceiveUpdatesStream = (revision?: number): ClientReadableStream => { 9 | const req = new ReceiveUpdatesRequest() 10 | if (revision !== undefined) req.setRevision(revision) 11 | return APIServices.updates.receiveUpdates(req, getAuthenticatedGrpcMetadata()) 12 | } 13 | 14 | const subject = new Subject() 15 | 16 | /** 17 | * ReceiveUpdatesStreamHandler handles reconnections to the ReceiveUpdates server-side stream. 18 | * The stream gets closed by the server when the ID token gets expired. 19 | */ 20 | export default class ReceiveUpdatesStreamHandler { 21 | private static instance?: ReceiveUpdatesStreamHandler 22 | public readonly observable = subject.asObservable() 23 | private static reconnectionTimeoutInMs: number = 1000 24 | private stream?: ClientReadableStream 25 | private revision?: number 26 | 27 | public static getInstance() { 28 | if (!ReceiveUpdatesStreamHandler.instance) { 29 | ReceiveUpdatesStreamHandler.instance = new ReceiveUpdatesStreamHandler() 30 | } 31 | return ReceiveUpdatesStreamHandler.instance 32 | } 33 | 34 | public start(): ReceiveUpdatesStreamHandler { 35 | if (this.stream) return this 36 | this.connect() 37 | return this 38 | } 39 | 40 | public stop(): ReceiveUpdatesStreamHandler { 41 | if (!this.stream) return this 42 | this.disconnect() 43 | return this 44 | } 45 | 46 | private connect() { 47 | const stream = connectReceiveUpdatesStream(this.revision) 48 | this.stream = stream 49 | 50 | stream.on('error', (err) => { 51 | console.error('ReceiveUpdatesStreamHandler.connect.error', err) 52 | this.reconnect() 53 | }) 54 | stream.on('end', () => this.reconnect()) 55 | stream.on('data', (res) => { 56 | this.revision = res.getRevision() 57 | subject.next(res) 58 | }) 59 | } 60 | 61 | private disconnect() { 62 | this.stream?.cancel() 63 | this.stream = undefined 64 | } 65 | 66 | private reconnect() { 67 | this.disconnect() 68 | setTimeout(() => { 69 | this.connect() 70 | }, ReceiveUpdatesStreamHandler.reconnectionTimeoutInMs) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tasks/components/cards/OrganizationCard.tsx: -------------------------------------------------------------------------------- 1 | import { Mail } from 'lucide-react' 2 | import type { Translation } from '@helpwave/hightide' 3 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 4 | import { AvatarGroup } from '@helpwave/hightide' 5 | import type { OrganizationDTO } from '@helpwave/api-services/types/users/organizations' 6 | import { EditCard, type EditCardProps } from './EditCard' 7 | 8 | type OrganizationCardTranslation = { 9 | member: string, 10 | members: string, 11 | } 12 | 13 | const defaultOrganizationCardTranslation: Translation = { 14 | en: { 15 | member: 'Member', 16 | members: 'Members' 17 | }, 18 | de: { 19 | member: 'Mitglied', 20 | members: 'Mitglieder' 21 | } 22 | } 23 | 24 | export type OrganizationCardProps = EditCardProps & { 25 | maxShownWards?: number, 26 | organization: OrganizationDTO, 27 | } 28 | 29 | /** 30 | * A Card displaying a Organization 31 | */ 32 | export const OrganizationCard = ({ 33 | overwriteTranslation, 34 | organization, 35 | ...editCardProps 36 | }: PropsForTranslation) => { 37 | const translation = useTranslation([defaultOrganizationCardTranslation], overwriteTranslation) 38 | const organizationMemberCount = organization.members.length 39 | 40 | return ( 41 | 42 |
43 |
44 | 45 | {`${organization.longName}`} 46 | 47 | {`(${organization.shortName})`} 48 |
49 |
50 | 51 | {organization.contactEmail} 52 |
53 |
54 |
55 | {`${organizationMemberCount} ${organizationMemberCount > 1 ? translation('members') : translation('member')}`} 56 |
57 | ({ 59 | image: user.avatarURL ? { avatarUrl: user.avatarURL, alt: user.nickname } : undefined, 60 | name: user.nickname, 61 | }))} 62 | fullyRounded={true} 63 | /> 64 |
65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /tasks/components/selects/TaskStatusSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Select, SelectOption, type SelectProps } from '@helpwave/hightide' 2 | import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' 3 | import type { TaskStatus } from '@helpwave/api-services/types/tasks/task' 4 | 5 | type TaskStatusSelectTranslation = { 6 | unscheduled: string, 7 | inProgress: string, 8 | done: string, 9 | status: string, 10 | } 11 | 12 | const defaultTaskStatusSelectTranslation = { 13 | en: { 14 | unscheduled: 'unscheduled', 15 | inProgress: 'in progress', 16 | done: 'done', 17 | status: 'status' 18 | }, 19 | de: { 20 | unscheduled: 'nicht angefangen', 21 | inProgress: 'in Arbeit', 22 | done: 'fertig', 23 | status: 'Status' 24 | } 25 | } 26 | 27 | type TaskStatusSelectProps = Omit & { 28 | /** 29 | * All entries within this array will be removed as options 30 | */ 31 | removeOptions?: TaskStatus[], 32 | onValueChanged?: (task: TaskStatus) => void, 33 | } 34 | 35 | /** 36 | * A component for selecting a TaskStatus 37 | * 38 | * The state is managed by the parent 39 | */ 40 | export const TaskStatusSelect = ({ 41 | overwriteTranslation, 42 | removeOptions = [], 43 | value, 44 | onValueChanged, 45 | ...selectProps 46 | }: PropsForTranslation) => { 47 | const translation = useTranslation([defaultTaskStatusSelectTranslation], overwriteTranslation) 48 | const defaultOptions: { value: TaskStatus, label: string }[] = [ 49 | { value: 'todo', label: translation('unscheduled') }, 50 | { value: 'inProgress', label: translation('inProgress') }, 51 | { value: 'done', label: translation('done') } 52 | ] 53 | 54 | const filteredOptions = defaultOptions.filter(defaultValue => !removeOptions?.find(value2 => value2 === defaultValue.value)) 55 | if (removeOptions?.find(value2 => value2 === value)) { 56 | console.error(`The selected value ${value} cannot be in the remove list`) 57 | value = filteredOptions.length > 0 ? filteredOptions[0]!.value : undefined 58 | console.warn(`Overwriting with ${value} instead`) 59 | } 60 | 61 | return ( 62 | 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /tasks/components/selects/AssigneeSelect.tsx: -------------------------------------------------------------------------------- 1 | import type { SelectProps } from '@helpwave/hightide' 2 | import { Avatar, LoadingAndErrorComponent, Select, SelectOption } from '@helpwave/hightide' 3 | import { useMembersByOrganizationQuery } from '@helpwave/api-services/mutations/users/organization_member_mutations' 4 | import type { OrganizationMember } from '@helpwave/api-services/types/users/organization_member' 5 | import clsx from 'clsx' 6 | 7 | export type AssigneeSelectProps = Omit & { 8 | value?: string, 9 | onValueChanged?: (user: OrganizationMember) => void, 10 | } 11 | 12 | /** 13 | * A Select component for picking an assignee 14 | */ 15 | export const AssigneeSelect = ({ 16 | value, 17 | onValueChanged, 18 | ...selectProps 19 | }: AssigneeSelectProps) => { 20 | const { data, isLoading, isError } = useMembersByOrganizationQuery() 21 | 22 | return ( 23 | 28 | 71 | 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /api-services/mutations/tasks/room_mutations.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationOptions } from '@tanstack/react-query' 2 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 3 | import type { RoomMinimalDTO } from '../../types/tasks/room' 4 | import { QueryKeys } from '../query_keys' 5 | import { RoomService } from '../../service/tasks/RoomService' 6 | 7 | export const useRoomQuery = (roomId?: string) => { 8 | return useQuery({ 9 | queryKey: [QueryKeys.rooms, roomId], 10 | enabled: !!roomId, 11 | queryFn: async () => { 12 | return await RoomService.get(roomId!) 13 | }, 14 | }) 15 | } 16 | 17 | export const roomOverviewsQueryKey = 'roomOverview' 18 | export const useRoomOverviewsQuery = (wardId: string | undefined) => { 19 | return useQuery({ 20 | queryKey: [QueryKeys.rooms, roomOverviewsQueryKey], 21 | enabled: !!wardId, 22 | queryFn: async () => { 23 | return await RoomService.getWardOverview(wardId!) 24 | }, 25 | }) 26 | } 27 | 28 | export const useRoomCreateMutation = (options?: UseMutationOptions) => { 29 | const queryClient = useQueryClient() 30 | return useMutation({ 31 | ...options, 32 | mutationFn: async (room: RoomMinimalDTO) => { 33 | return await RoomService.create(room) 34 | }, 35 | onSuccess: (data, variables, context) => { 36 | if (options?.onSuccess) { 37 | options.onSuccess(data, variables, context) 38 | } 39 | queryClient.invalidateQueries([QueryKeys.rooms]).catch(console.error) 40 | queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) 41 | } 42 | }) 43 | } 44 | 45 | export const useRoomUpdateMutation = (options?: UseMutationOptions) => { 46 | const queryClient = useQueryClient() 47 | return useMutation({ 48 | ...options, 49 | mutationFn: async (room: RoomMinimalDTO) => { 50 | return await RoomService.update(room) 51 | }, 52 | onSuccess: (data, variables, context) => { 53 | if (options?.onSuccess) { 54 | options.onSuccess(data, variables, context) 55 | } 56 | queryClient.invalidateQueries([QueryKeys.rooms]).catch(console.error) 57 | }, 58 | }) 59 | } 60 | 61 | export const useRoomDeleteMutation = (options?: UseMutationOptions) => { 62 | const queryClient = useQueryClient() 63 | return useMutation({ 64 | ...options, 65 | mutationFn: async (roomId: string) => { 66 | return await RoomService.delete(roomId) 67 | }, 68 | onSuccess: (data, variables, context) => { 69 | if (options?.onSuccess) { 70 | options.onSuccess(data, variables, context) 71 | } 72 | queryClient.invalidateQueries([QueryKeys.rooms]).catch(console.error) 73 | queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) 74 | } 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /api-services/mutations/tasks/bed_mutations.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationOptions } from '@tanstack/react-query' 2 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 3 | import { QueryKeys } from '../query_keys' 4 | import { APIServices } from '../../services' 5 | import type { BedWithRoomId } from '../../types/tasks/bed' 6 | import { roomOverviewsQueryKey } from './room_mutations' 7 | import { BedService } from '../../service/tasks/BedService' 8 | 9 | export const useBedQuery = (id?: string) => { 10 | return useQuery({ 11 | queryKey: [QueryKeys.beds], 12 | enabled: !!id, 13 | queryFn: async () => { 14 | return await BedService.get(id!) 15 | }, 16 | }) 17 | } 18 | 19 | export const useBedCreateMutation = (options?: UseMutationOptions) => { 20 | const queryClient = useQueryClient() 21 | return useMutation({ 22 | ...options, 23 | mutationFn: async (bed: BedWithRoomId) => { 24 | return await BedService.create(bed) 25 | }, 26 | onSuccess: (data, variables, context) => { 27 | if (options?.onSuccess) { 28 | options.onSuccess(data, variables, context) 29 | } 30 | queryClient.invalidateQueries([QueryKeys.beds]).catch(console.error) 31 | queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) 32 | queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) 33 | }, 34 | }) 35 | } 36 | 37 | export const useBedUpdateMutation = (options?: UseMutationOptions) => { 38 | const queryClient = useQueryClient() 39 | return useMutation({ 40 | ...options, 41 | mutationFn: async (bed: BedWithRoomId) => { 42 | return await BedService.update(bed) 43 | }, 44 | onSuccess: (data, variables, context) => { 45 | if (options?.onSuccess) { 46 | options.onSuccess(data, variables, context) 47 | } 48 | queryClient.invalidateQueries([APIServices.bed]).catch(console.error) 49 | queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) 50 | }, 51 | }) 52 | } 53 | 54 | export const useBedDeleteMutation = (options?: UseMutationOptions) => { 55 | const queryClient = useQueryClient() 56 | return useMutation({ 57 | ...options, 58 | mutationFn: async (bedId: string) => { 59 | return await BedService.delete(bedId) 60 | }, 61 | onSuccess: (data, variables, context) => { 62 | if (options?.onSuccess) { 63 | options.onSuccess(data, variables, context) 64 | } 65 | queryClient.invalidateQueries([APIServices.bed]).catch(console.error) 66 | queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) 67 | queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) 68 | }, 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /api-services/mutations/tasks/ward_mutations.ts: -------------------------------------------------------------------------------- 1 | import type { UseMutationOptions } from '@tanstack/react-query' 2 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' 3 | import type { WardDetailDTO, WardMinimalDTO } from '../../types/tasks/wards' 4 | import { QueryKeys } from '../query_keys' 5 | import { WardService } from '../../service/tasks/WardService' 6 | 7 | export const useWardQuery = (id: string) => useQuery({ 8 | queryKey: [QueryKeys.wards, id], 9 | enabled: !!id, 10 | queryFn: async (): Promise => { 11 | return await WardService.get(id) 12 | } 13 | }) 14 | 15 | export const useWardOverviewsQuery = () => { 16 | return useQuery({ 17 | queryKey: [QueryKeys.wards], 18 | queryFn: async () => { 19 | return await WardService.getWardOverviews() 20 | } 21 | }) 22 | } 23 | 24 | export const useWardDetailsQuery = (wardId?: string) => { 25 | return useQuery({ 26 | queryKey: [QueryKeys.wards, wardId], 27 | enabled: !!wardId, 28 | queryFn: async (): Promise => { 29 | return await WardService.getDetails(wardId!) 30 | } 31 | }) 32 | } 33 | 34 | export const useWardCreateMutation = (options?: UseMutationOptions) => { 35 | const queryClient = useQueryClient() 36 | return useMutation({ 37 | ...options, 38 | mutationFn: async (ward: WardMinimalDTO) => { 39 | return await WardService.create(ward) 40 | }, 41 | onSuccess: (data, variables, context) => { 42 | if (options?.onSuccess) { 43 | return options.onSuccess(data, variables, context) 44 | } 45 | queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) 46 | } 47 | }) 48 | } 49 | 50 | export const useWardUpdateMutation = (options?: UseMutationOptions) => { 51 | const queryClient = useQueryClient() 52 | return useMutation({ 53 | ...options, 54 | mutationFn: async (ward: WardMinimalDTO) => { 55 | return await WardService.update(ward) 56 | }, 57 | onSuccess: (data, variables, context) => { 58 | if (options?.onSuccess) { 59 | return options.onSuccess(data, variables, context) 60 | } 61 | queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) 62 | } 63 | }) 64 | } 65 | 66 | export const useWardDeleteMutation = (options?: UseMutationOptions) => { 67 | const queryClient = useQueryClient() 68 | return useMutation({ 69 | ...options, 70 | mutationFn: async (wardId: string) => { 71 | return await WardService.delete(wardId) 72 | }, 73 | onSuccess: (data, variables, context) => { 74 | if (options?.onSuccess) { 75 | return options.onSuccess(data, variables, context) 76 | } 77 | queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /tasks/components/SubtaskTile.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Checkbox, type PropsForTranslation, TextButton, ToggleableInput, useTranslation } from '@helpwave/hightide' 3 | import type { SubtaskDTO } from '@helpwave/api-services/types/tasks/task' 4 | 5 | type SubtaskTileTranslation = { 6 | subtasks: string, 7 | remove: string, 8 | addSubtask: string, 9 | newSubtask: string, 10 | } 11 | 12 | const defaultSubtaskTileTranslation = { 13 | en: { 14 | remove: 'Remove' 15 | }, 16 | de: { 17 | remove: 'Entfernen' 18 | } 19 | } 20 | 21 | type SubtaskTileProps = { 22 | subtask: SubtaskDTO, 23 | onRemoveClick: () => void, 24 | onChange: (subtask: SubtaskDTO) => void, 25 | } 26 | 27 | /** 28 | * A tile for showing and editing a subtask used in the SubtaskView 29 | */ 30 | export const SubtaskTile = ({ 31 | overwriteTranslation, 32 | subtask, 33 | onRemoveClick, 34 | onChange, 35 | }: PropsForTranslation) => { 36 | const translation = useTranslation([defaultSubtaskTileTranslation], overwriteTranslation) 37 | 38 | const minTaskNameLength = 2 39 | const maxTaskNameLength = 64 40 | const [localSubtask, setLocalSubtask] = useState({ ...subtask }) 41 | 42 | useEffect(() => { 43 | setLocalSubtask(subtask) 44 | }, [subtask]) 45 | 46 | return ( 47 |
48 | { 50 | const newSubtask: SubtaskDTO = { 51 | ...localSubtask, 52 | isDone 53 | } 54 | onChange(newSubtask) 55 | setLocalSubtask(newSubtask) 56 | }} 57 | checked={localSubtask.isDone} 58 | className="min-w-6 min-h-6" 59 | /> 60 |
61 | setLocalSubtask({ 64 | ...subtask, 65 | name 66 | })} 67 | onEditCompleted={name => { 68 | const newSubtask: SubtaskDTO = { 69 | ...localSubtask, 70 | name 71 | } 72 | onChange(newSubtask) 73 | setLocalSubtask(newSubtask) 74 | }} 75 | id={subtask.name} 76 | minLength={minTaskNameLength} 77 | maxLength={maxTaskNameLength} 78 | /> 79 | 86 | {translation('remove')} 87 | 88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /api-services/service/tasks/RoomService.ts: -------------------------------------------------------------------------------- 1 | import { APIServices } from '../../services' 2 | import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' 3 | import { 4 | CreateRoomRequest, DeleteRoomRequest, 5 | GetRoomOverviewsByWardRequest, 6 | GetRoomRequest, UpdateRoomRequest 7 | } from '@helpwave/proto-ts/services/tasks_svc/v1/room_svc_pb' 8 | import type { RoomDTO, RoomMinimalDTO, RoomOverviewDTO } from '../../types/tasks/room' 9 | import type { BedWithPatientWithTasksNumberDTO } from '../../types/tasks/bed' 10 | 11 | export const RoomService = { 12 | get: async (id: string): Promise => { 13 | const req = new GetRoomRequest() 14 | .setId(id) 15 | const res = await APIServices.room.getRoom(req, getAuthenticatedGrpcMetadata()) 16 | 17 | return { 18 | id: res.getId(), 19 | name: res.getName(), 20 | wardId: res.getWardId(), 21 | beds: res.getBedsList().map(bed => ({ 22 | id: bed.getId(), 23 | name: bed.getName() 24 | })) 25 | } 26 | }, 27 | getWardOverview: async (wardId: string): Promise => { 28 | const req = new GetRoomOverviewsByWardRequest() 29 | .setId(wardId) 30 | const res = await APIServices.room.getRoomOverviewsByWard(req, getAuthenticatedGrpcMetadata()) 31 | 32 | return res.getRoomsList().map((room) => ({ 33 | id: room.getId(), 34 | name: room.getName(), 35 | wardId, 36 | beds: room.getBedsList().map(bed => { 37 | const patient = bed.getPatient() 38 | return { 39 | id: bed.getId(), 40 | name: bed.getName(), 41 | patient: !patient ? undefined : { 42 | id: patient.getId(), 43 | humanReadableIdentifier: patient.getHumanReadableIdentifier(), 44 | bedId: bed.getId(), 45 | wardId: wardId, 46 | tasksTodo: patient.getTasksUnscheduled(), 47 | tasksInProgress: patient.getTasksInProgress(), 48 | tasksDone: patient.getTasksDone() 49 | } 50 | } 51 | }) 52 | })) 53 | }, 54 | create: async (room: RoomMinimalDTO): Promise => { 55 | const req = new CreateRoomRequest() 56 | .setWardId(room.wardId) 57 | .setName(room.name) 58 | const res = await APIServices.room.createRoom(req, getAuthenticatedGrpcMetadata()) 59 | return { ...room, id: res.getId() } 60 | }, 61 | update: async (room: RoomMinimalDTO): Promise => { 62 | const req = new UpdateRoomRequest() 63 | .setId(room.id) 64 | .setName(room.name) 65 | await APIServices.room.updateRoom(req, getAuthenticatedGrpcMetadata()) 66 | return true 67 | }, 68 | delete: async (id: string): Promise => { 69 | const req = new DeleteRoomRequest() 70 | .setId(id) 71 | await APIServices.room.deleteRoom(req, getAuthenticatedGrpcMetadata()) 72 | return true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /customer/hooks/useOrganization.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType, PropsWithChildren } from 'react' 2 | import { createContext, useContext } from 'react' 3 | import type { Customer } from '@/api/dataclasses/customer' 4 | import { LoadingAnimation } from '@helpwave/hightide' 5 | import { CreateOrganizationPage } from '@/components/pages/create-organization' 6 | import { useCustomerMyselfQuery } from '@/api/mutations/customer_mutations' 7 | import { CustomerAPI } from '@/api/services/customer' 8 | import { useAuth } from '@/hooks/useAuth' 9 | 10 | type Organization = Customer 11 | 12 | type OrganizationContextType = { 13 | hasOrganization: boolean, 14 | organization?: Organization, 15 | reload: () => void, 16 | } 17 | 18 | const OrganizationContext = createContext(undefined) 19 | 20 | export const OrganizationProvider = ({ children }: PropsWithChildren) => { 21 | const { isLoading, isError, data: organization, refetch } = useCustomerMyselfQuery() 22 | const { authHeader } = useAuth() 23 | 24 | if (!organization && isLoading) { 25 | return ( 26 |
27 | 28 |
29 | ) 30 | } 31 | 32 | if (isError) { 33 | return ( 34 |
35 | An Error occurred 36 |
37 | ) 38 | } 39 | 40 | if (!organization) { 41 | return ( 42 | { 43 | try { 44 | // TODO fix this 45 | // We cannot use a mutation because we would trigger a rerender and delete the form information 46 | await CustomerAPI.create(data, authHeader) 47 | refetch().catch(console.error) 48 | return true 49 | } catch (error) { 50 | console.error(error) 51 | return false 52 | } 53 | }} 54 | /> 55 | ) 56 | } 57 | 58 | return ( 59 | 60 | {children} 61 | 62 | ) 63 | } 64 | 65 | export const withOrganization =

(Component: ComponentType

) => { 66 | const WrappedComponent = (props: P) => ( 67 | 68 | 69 | 70 | ) 71 | WrappedComponent.displayName = `withOrganization(${Component.displayName || Component.name || 'Component'})` 72 | 73 | return WrappedComponent 74 | } 75 | 76 | // Custom hook for using OrganizationContext 77 | export const useOrganization = () => { 78 | const context = useContext(OrganizationContext) 79 | if (!context) { 80 | throw new Error('useOrganization must be used within an OrganizationProvider') 81 | } 82 | return context 83 | } 84 | -------------------------------------------------------------------------------- /tasks/components/layout/WardDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { useRouter } from 'next/router' 3 | import type { Translation } from '@helpwave/hightide' 4 | import { LoadingAndErrorComponent, type PropsForTranslation, useTranslation } from '@helpwave/hightide' 5 | import { useWardOverviewsQuery } from '@helpwave/api-services/mutations/tasks/ward_mutations' 6 | import { ColumnTitle } from '../ColumnTitle' 7 | import { AddCard } from '../cards/AddCard' 8 | import { WardCard } from '../cards/WardCard' 9 | import { OrganizationOverviewContext } from '@/pages/organizations/[organizationId]' 10 | 11 | type WardDisplayTranslation = { 12 | wards: string, 13 | addWard: string, 14 | } 15 | 16 | const defaultWardDisplayTranslations: Translation = { 17 | en: { 18 | wards: 'Wards', 19 | addWard: 'Add new Ward' 20 | }, 21 | de: { 22 | wards: 'Stationen', 23 | addWard: 'Station hinzufügen' 24 | } 25 | } 26 | 27 | export type WardDisplayProps = { 28 | selectedWardId?: string, 29 | } 30 | 31 | /** 32 | * The left side of the organizations/[organizationId].tsx page showing the wards within the organizations 33 | */ 34 | export const WardDisplay = ({ 35 | overwriteTranslation, 36 | selectedWardId, 37 | }: PropsForTranslation) => { 38 | const translation = useTranslation([defaultWardDisplayTranslations], overwriteTranslation) 39 | const router = useRouter() 40 | const context = useContext(OrganizationOverviewContext) 41 | const { data, isLoading, isError } = useWardOverviewsQuery() 42 | 43 | const wards = data?.sort((a, b) => a.name.localeCompare(b.name)) 44 | selectedWardId ??= context.state.wardId 45 | 46 | return ( 47 |

48 | 49 | 53 |
54 | {wards && wards.map(ward => ( 55 | context.updateContext({ 59 | ...context.state, 60 | wardId: ward.id 61 | })} 62 | onClick={() => { 63 | router.push(`/ward/${ward.id}`).catch(console.error) 64 | }} 65 | isSelected={selectedWardId === ward.id } 66 | /> 67 | ))} 68 | context.updateContext({ 71 | ...context.state, 72 | wardId: '' 73 | })} 74 | isSelected={!selectedWardId} 75 | /> 76 |
77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /customer/components/forms/StripeCheckOutForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { loadStripe } from '@stripe/stripe-js' 3 | import { 4 | EmbeddedCheckoutProvider, 5 | EmbeddedCheckout 6 | } from '@stripe/react-stripe-js' 7 | import type { PropsWithChildren } from 'react' 8 | import { useCallback, useRef, useState } from 'react' 9 | import { InvoiceAPI } from '@/api/services/invoice' 10 | import { useAuth } from '@/hooks/useAuth' 11 | import { STRIPE_PUBLISHABLE_KEY } from '@/api/config' 12 | import type { Translation } from '@helpwave/hightide' 13 | import { useTranslation } from '@helpwave/hightide' 14 | import { useLanguage } from '@helpwave/hightide' 15 | import { SolidButton } from '@helpwave/hightide' 16 | 17 | type EmbeddedCheckoutButtonTranslation = { 18 | cancel: string, 19 | checkout: string, 20 | } 21 | 22 | const defaultEmbeddedCheckoutButtonTranslation: Translation = { 23 | en: { 24 | cancel: 'Cancel', 25 | checkout: 'Check Out', 26 | }, 27 | de: { 28 | cancel: 'Abbrechen', 29 | checkout: 'Kasse', 30 | }, 31 | } 32 | 33 | 34 | export type EmbeddedCheckoutButtonProps = PropsWithChildren<{ 35 | invoiceId: string, 36 | }> 37 | 38 | export default function EmbeddedCheckoutButton({ children, invoiceId }: EmbeddedCheckoutButtonProps) { 39 | const translation = useTranslation(defaultEmbeddedCheckoutButtonTranslation) 40 | const { authHeader } = useAuth() 41 | const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY) 42 | const [showCheckout, setShowCheckout] = useState(false) 43 | const modalRef = useRef(null) 44 | const language = useLanguage() 45 | 46 | const fetchClientSecret = useCallback(() => { 47 | const locale = language.language 48 | 49 | return InvoiceAPI.pay(invoiceId, locale, authHeader) 50 | }, [language.language, authHeader, invoiceId]) 51 | 52 | const options = { fetchClientSecret } 53 | 54 | const handleCheckoutClick = () => { 55 | setShowCheckout(true) 56 | modalRef.current?.showModal() 57 | } 58 | 59 | const handleCloseModal = () => { 60 | setShowCheckout(false) 61 | modalRef.current?.close() 62 | } 63 | 64 | return ( 65 |
66 | 67 | {children} 68 | 69 | 70 |
71 |

{translation.checkout}

72 | {showCheckout && ( 73 | 74 | 75 | 76 | )} 77 |
78 | 79 | {translation.cancel} 80 | 81 |
82 |
83 |
84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /api-services/services.ts: -------------------------------------------------------------------------------- 1 | import { WardServicePromiseClient } from '@helpwave/proto-ts/services/tasks_svc/v1/ward_svc_grpc_web_pb' 2 | import { RoomServicePromiseClient } from '@helpwave/proto-ts/services/tasks_svc/v1/room_svc_grpc_web_pb' 3 | import { BedServicePromiseClient } from '@helpwave/proto-ts/services/tasks_svc/v1/bed_svc_grpc_web_pb' 4 | import { PatientServicePromiseClient } from '@helpwave/proto-ts/services/tasks_svc/v1/patient_svc_grpc_web_pb' 5 | import { 6 | TaskTemplateServicePromiseClient 7 | } from '@helpwave/proto-ts/services/tasks_svc/v1/task_template_svc_grpc_web_pb' 8 | import { TaskServicePromiseClient } from '@helpwave/proto-ts/services/tasks_svc/v1/task_svc_grpc_web_pb' 9 | import { OrganizationServicePromiseClient } from '@helpwave/proto-ts/services/user_svc/v1/organization_svc_grpc_web_pb' 10 | import { UserServicePromiseClient } from '@helpwave/proto-ts/services/user_svc/v1/user_svc_grpc_web_pb' 11 | import { PropertyServicePromiseClient } from '@helpwave/proto-ts/services/property_svc/v1/property_svc_grpc_web_pb' 12 | import { 13 | PropertyValueServicePromiseClient 14 | } from '@helpwave/proto-ts/services/property_svc/v1/property_value_svc_grpc_web_pb' 15 | import { 16 | PropertyViewsServicePromiseClient 17 | } from '@helpwave/proto-ts/services/property_svc/v1/property_views_svc_grpc_web_pb' 18 | import { UpdatesServicePromiseClient } from '@helpwave/proto-ts/services/updates_svc/v1/updates_svc_grpc_web_pb' 19 | import { APIServiceUrls } from './config/wrapper' 20 | 21 | type APIServicesType = { 22 | organization: OrganizationServicePromiseClient, 23 | user: UserServicePromiseClient, 24 | ward: WardServicePromiseClient, 25 | room: RoomServicePromiseClient, 26 | bed: BedServicePromiseClient, 27 | patient: PatientServicePromiseClient, 28 | task: TaskServicePromiseClient, 29 | taskTemplates: TaskTemplateServicePromiseClient, 30 | property: PropertyServicePromiseClient, 31 | propertyValues: PropertyValueServicePromiseClient, 32 | propertyViewSource: PropertyViewsServicePromiseClient, 33 | updates: UpdatesServicePromiseClient, 34 | } 35 | 36 | const onlineServices: APIServicesType = { 37 | organization: new OrganizationServicePromiseClient(APIServiceUrls.users), 38 | user: new UserServicePromiseClient(APIServiceUrls.users), 39 | ward: new WardServicePromiseClient(APIServiceUrls.tasks), 40 | room: new RoomServicePromiseClient(APIServiceUrls.tasks), 41 | bed: new BedServicePromiseClient(APIServiceUrls.tasks), 42 | patient: new PatientServicePromiseClient(APIServiceUrls.tasks), 43 | task: new TaskServicePromiseClient(APIServiceUrls.tasks), 44 | taskTemplates: new TaskTemplateServicePromiseClient(APIServiceUrls.tasks), 45 | property: new PropertyServicePromiseClient(APIServiceUrls.property), 46 | propertyValues: new PropertyValueServicePromiseClient(APIServiceUrls.property), 47 | propertyViewSource: new PropertyViewsServicePromiseClient(APIServiceUrls.property), 48 | updates: new UpdatesServicePromiseClient(APIServiceUrls.updates) 49 | } 50 | 51 | export const APIServices: APIServicesType = onlineServices 52 | --------------------------------------------------------------------------------