diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..b08734ba --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "tasks", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/web/components/AuditLogTimeline.tsx b/web/components/AuditLogTimeline.tsx index 98850a3a..297a99e1 100644 --- a/web/components/AuditLogTimeline.tsx +++ b/web/components/AuditLogTimeline.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useEffect } from 'react' import { useQuery } from '@tanstack/react-query' -import { SmartDate } from '@/utils/date' +import { DateDisplay } from '@/components/Date/DateDisplay' import clsx from 'clsx' import { fetcher } from '@/api/gql/fetcher' import { UserInfoPopup } from '@/components/UserInfoPopup' @@ -152,9 +152,6 @@ export const AuditLogTimeline: React.FC = ({ caseId, clas return (
-
- Audit Log -
{isLoading && (
@@ -213,7 +210,7 @@ export const AuditLogTimeline: React.FC = ({ caseId, clas )}
- +
{hasDetails && ( diff --git a/web/components/Date/CurrentTime.tsx b/web/components/Date/CurrentTime.tsx new file mode 100644 index 00000000..cd7563a0 --- /dev/null +++ b/web/components/Date/CurrentTime.tsx @@ -0,0 +1,47 @@ +import { Tooltip, useLocale } from '@helpwave/hightide' +import clsx from 'clsx' +import { useEffect, useState } from 'react' + +type CurrentTimeProps = { + className?: string, +} + +export const CurrentTime = ({ className }: CurrentTimeProps) => { + const { locale } = useLocale() + const [date, setDate] = useState(new Date()) + + useEffect(() => { + const intervalId = setInterval(() => { + setDate(new Date()) + }, 500) + + return () => clearInterval(intervalId) + }) + + const dateFormatFull = Intl.DateTimeFormat(locale, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + + const fullDate = dateFormatFull.format(date) + + const timeFormat = Intl.DateTimeFormat(locale, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + + const time = timeFormat.format(date) + + return ( + + + {time} + + + ) +} \ No newline at end of file diff --git a/web/components/Date/DateDisplay.tsx b/web/components/Date/DateDisplay.tsx new file mode 100644 index 00000000..fc56e448 --- /dev/null +++ b/web/components/Date/DateDisplay.tsx @@ -0,0 +1,28 @@ +import { Tooltip, useUpdatingDateString } from '@helpwave/hightide' +import clsx from 'clsx' + +type DateDisplayProps = { + date: Date, + className?: string, + showTime?: boolean, + mode?: 'relative' | 'absolute', +} + +export const DateDisplay = ({ date, className, showTime = true, mode = 'relative' }: DateDisplayProps) => { + const { absolute, relative } = useUpdatingDateString({ + date: date ?? new Date(), + absoluteFormat: showTime ? 'dateTime' : 'date', + }) + if (!date) return null + + const displayString = mode === 'relative' ? relative : absolute + const tooltipString = mode === 'relative' ? absolute : relative + + return ( + + + {displayString} + + + ) +} diff --git a/web/components/FeedbackDialog.tsx b/web/components/FeedbackDialog.tsx index a2c97679..85811e2e 100644 --- a/web/components/FeedbackDialog.tsx +++ b/web/components/FeedbackDialog.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef, useMemo } from 'react' -import { Dialog, Button, Textarea, FormField, FormProvider, Checkbox, useCreateForm, useTranslatedValidators, useFormObserverKey } from '@helpwave/hightide' +import { Dialog, Button, Textarea, FormField, FormProvider, Checkbox, useCreateForm, useTranslatedValidators, useFormObserverKey, IconButton } from '@helpwave/hightide' import { useTasksTranslation, useLocale } from '@/i18n/useTasksTranslation' import { useTasksContext } from '@/hooks/useTasksContext' import { Mic, Pause } from 'lucide-react' @@ -89,11 +89,6 @@ export const FeedbackDialog = ({ isOpen, onClose, hideUrl = false }: FeedbackDia }) if (response.ok) { - form.update(prev => ({ - ...prev, - feedback: '', - isAnonymous: false, - })) onClose() } } catch { @@ -212,10 +207,7 @@ export const FeedbackDialog = ({ isOpen, onClose, hideUrl = false }: FeedbackDia useEffect(() => { if (!isOpen) { - updateForm(prev => ({ - ...prev, - feedback: '', - })) + form.reset() finalTranscriptRef.current = '' lastFinalLengthRef.current = 0 if (recognitionRef.current && isRecording) { @@ -224,7 +216,7 @@ export const FeedbackDialog = ({ isOpen, onClose, hideUrl = false }: FeedbackDia setIsRecording(false) } } - }, [isOpen, isRecording, updateForm]) + }, [form, isOpen, isRecording, updateForm]) useEffect(() => { if (isOpen && user) { @@ -297,21 +289,21 @@ export const FeedbackDialog = ({ isOpen, onClose, hideUrl = false }: FeedbackDia className="pr-12 pb-3" /> {isSupported && ( - + )} )} -
+
- )} - - + + {({ props }) => ( + + + {unreadCount > 0 && ( + + {unreadCount > 9 ? '9+' : unreadCount} + + )} + )} - + { {notification.subtitle} {notification.date && ( - <> • + <> • )}
diff --git a/web/components/UserInfoPopup.tsx b/web/components/UserInfoPopup.tsx index 4e7beb8b..3aaf5aa3 100644 --- a/web/components/UserInfoPopup.tsx +++ b/web/components/UserInfoPopup.tsx @@ -5,7 +5,7 @@ import { fetcher } from '@/api/gql/fetcher' import clsx from 'clsx' import { AvatarStatusComponent } from '@/components/AvatarStatusComponent' import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import { SmartDate } from '@/utils/date' +import { DateDisplay } from '@/components/Date/DateDisplay' const GET_USER_QUERY = ` query GetUser($id: ID!) { @@ -115,7 +115,7 @@ export const UserInfoPopup: React.FC = ({ userId, isOpen, on
{user.lastOnline && (
- +
)} diff --git a/web/components/layout/Page.tsx b/web/components/layout/Page.tsx index fb4aec02..bfab5a76 100644 --- a/web/components/layout/Page.tsx +++ b/web/components/layout/Page.tsx @@ -11,9 +11,10 @@ import { ExpandableContent, ExpandableHeader, ExpandableRoot, + IconButton, MarkdownInterpreter, Tooltip, - useLocalStorage + useStorage } from '@helpwave/hightide' import { AvatarStatusComponent } from '@/components/AvatarStatusComponent' import { getConfig } from '@/utils/config' @@ -49,7 +50,7 @@ export const StagingDisclaimerDialog = () => { const { value: lastTimeStagingDisclaimerDismissed, setValue: setLastTimeStagingDisclaimerDismissed - } = useLocalStorage('staging-disclaimer-dismissed-time', 0) + } = useStorage({ key: 'staging-disclaimer-dismissed-time', defaultValue: 0 }) const dismissStagingDisclaimer = () => { setLastTimeStagingDisclaimerDismissed(new Date().getTime()) @@ -102,17 +103,17 @@ export const SurveyModal = () => { const { value: onboardingSurveyCompleted, setValue: setOnboardingSurveyCompleted - } = useLocalStorage('onboarding-survey-completed', 0) + } = useStorage({ key: 'onboarding-survey-completed', defaultValue: 0 }) const { value: weeklySurveyLastCompleted, setValue: setWeeklySurveyLastCompleted - } = useLocalStorage('weekly-survey-last-completed', 0) + } = useStorage({ key: 'weekly-survey-last-completed', defaultValue: 0 }) const { value: surveyLastDismissed, setValue: setSurveyLastDismissed - } = useLocalStorage('survey-last-dismissed', 0) + } = useStorage({ key: 'survey-last-dismissed', defaultValue: 0 }) useEffect(() => { if (!config.onboardingSurveyUrl && !config.weeklySurveyUrl) { @@ -216,21 +217,19 @@ const RootLocationSelector = ({ className, onSelect }: RootLocationSelectorProps const { value: storedSelectedRootLocationsRaw, setValue: setStoredSelectedRootLocations - } = useLocalStorage>( - 'selected-root-location-nodes', - [] - ) + } = useStorage>({ + key: 'selected-root-location-nodes', + defaultValue: [] + }) - const storedSelectedRootLocations = useMemo( - () => - Array.isArray(storedSelectedRootLocationsRaw) - ? storedSelectedRootLocationsRaw.filter( - (loc): loc is { id: string, title: string, kind?: string } => - Boolean(loc && typeof loc.id === 'string' && typeof loc.title === 'string') - ) - : [], - [storedSelectedRootLocationsRaw] - ) + const storedSelectedRootLocations = useMemo(() => + Array.isArray(storedSelectedRootLocationsRaw) + ? storedSelectedRootLocationsRaw.filter( + (loc): loc is { id: string, title: string, kind?: string } => + Boolean(loc && typeof loc.id === 'string' && typeof loc.title === 'string') + ) + : [], + [storedSelectedRootLocationsRaw]) const { data: locationsData } = useLocations( { limit: 1000 }, @@ -406,33 +405,38 @@ export const Header = ({ onMenuClick, isMenuOpen, ...props }: HeaderProps) => { )} >
- +
- - - - - - + setIsFeedbackOpen(true)} + > + + + router.push('/settings')} + > + + +
diff --git a/web/components/patients/LocationChips.tsx b/web/components/locations/LocationChips.tsx similarity index 76% rename from web/components/patients/LocationChips.tsx rename to web/components/locations/LocationChips.tsx index cc9842e3..9e585a3a 100644 --- a/web/components/patients/LocationChips.tsx +++ b/web/components/locations/LocationChips.tsx @@ -6,6 +6,7 @@ import type { LocationType } from '@/api/gql/generated' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import type { HTMLAttributes } from 'react' import clsx from 'clsx' +import { LocationTypeChip } from './LocationTypeChip' type PartialLocationNode = { id: string, @@ -21,17 +22,6 @@ interface LocationChipsProps extends HTMLAttributes { placeholderProps?: HTMLAttributes, } -const getKindStyles = (kind: LocationType | undefined) => { - if (kind === 'HOSPITAL') return 'location-hospital coloring-solid' - if (kind === 'PRACTICE') return 'location-practice coloring-solid' - if (kind === 'CLINIC') return 'location-clinic coloring-solid' - if (kind === 'TEAM') return 'location-team coloring-solid' - if (kind === 'WARD') return 'location-ward coloring-solid' - if (kind === 'ROOM') return 'location-room coloring-solid' - if (kind === 'BED') return 'location-bed coloring-solid' - return '' -} - export const LocationChips = ({ locations, disableLink = false, small = false, placeholderProps, ...props }: LocationChipsProps) => { const translation = useTasksTranslation() @@ -61,9 +51,7 @@ export const LocationChips = ({ locations, disableLink = false, small = false, p {displayTitle} {linkTarget?.kind && ( - - {translation('locationType', { type: linkTarget.kind })} - + )} @@ -80,7 +68,7 @@ export const LocationChips = ({ locations, disableLink = false, small = false, p > {disableLink ? (
diff --git a/web/components/locations/LocationSelectionDialog.tsx b/web/components/locations/LocationSelectionDialog.tsx index bc18629f..8e545edd 100644 --- a/web/components/locations/LocationSelectionDialog.tsx +++ b/web/components/locations/LocationSelectionDialog.tsx @@ -5,7 +5,8 @@ import { Checkbox, Button, SearchBar, - useLocalStorage + useStorage, + IconButton } from '@helpwave/hightide' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import type { LocationNodeType } from '@/api/gql/generated' @@ -21,6 +22,7 @@ import { ChevronsUp, MinusIcon } from 'lucide-react' +import { LocationTypeChip } from './LocationTypeChip' export type LocationPickerUseCase = | 'default' @@ -53,18 +55,6 @@ interface LocationTreeItemProps { isSelectable?: boolean, } -const getKindStyles = (kind: string) => { - const k = kind.toUpperCase() - if (k === 'HOSPITAL') return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' - if (k === 'PRACTICE') return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300' - if (k === 'CLINIC') return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' - if (k === 'TEAM') return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' - if (k === 'WARD') return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300' - if (k === 'ROOM') return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' - if (k === 'BED') return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' - return 'bg-surface-subdued text-text-tertiary' -} - const LocationTreeItem = ({ node, selectedIds, @@ -126,12 +116,10 @@ const LocationTreeItem = ({ } }} > - + {node.title} - - {node.kind} - +
) @@ -148,14 +136,16 @@ const LocationTreeItem = ({ return (
{ + onExpandedChange={(isOpen) => { onExpandToggle(node.id, isOpen) }} className="!shadow-none !bg-transparent !rounded-none w-full" - headerClassName="px-2 hover:bg-surface-hover rounded-lg transition-colors !text-text-primary hover:!text-text-primary flex-row-reverse justify-end cursor-pointer" + triggerProps={{ + className: 'px-2 hover:bg-surface-hover rounded-lg transition-colors !text-text-primary hover:!text-text-primary flex-row-reverse justify-end cursor-pointer' + }} + contentProps={{ className: '!shadow-none !bg-transparent' }} contentExpandedClassName="!max-h-none !h-auto !min-h-0 !overflow-visible !flex !flex-col px-1 data-[expanded]:py-2 border-l-2 border-divider ml-5 pl-2 pr-0 mt-1" >
@@ -194,12 +184,12 @@ export const LocationSelectionDialog = ({ const { value: storedExpandedIds, setValue: setStoredExpandedIds - } = useLocalStorage(storageKey, []) + } = useStorage({ key: storageKey, defaultValue: [] }) const { value: storedTreeSignature, setValue: setStoredTreeSignature - } = useLocalStorage(signatureKey, '') + } = useStorage({ key: signatureKey, defaultValue: '' }) const [selectedIds, setSelectedIds] = useState>(new Set(initialSelectedIds)) const [expandedIds, setExpandedIds] = useState>(new Set()) @@ -445,6 +435,22 @@ export const LocationSelectionDialog = ({ setSelectedIds(new Set()) } + const titleTranslationMap: Record = { + clinic: translation('pickClinic'), + position: translation('pickPosition'), + teams: translation('pickTeams'), + root: translation('selectRootLocation'), + default: translation('selectLocation') + } + + const descriptionTranslationMap: Record = { + clinic: translation('pickClinicDescription'), + position: translation('pickPositionDescription'), + teams: translation('pickTeamsDescription'), + root: translation('selectRootLocationDescription'), + default: translation('selectLocationDescription') + } + return ( - {useCase === 'clinic' ? translation('pickClinic') : - useCase === 'position' ? translation('pickPosition') : - useCase === 'teams' ? translation('pickTeams') : - useCase === 'root' ? translation('selectRootLocation') : - translation('selectLocation')} + {titleTranslationMap[useCase]}
)} - description={ - useCase === 'clinic' ? translation('pickClinicDescription') : - useCase === 'position' ? translation('pickPositionDescription') : - useCase === 'teams' ? translation('pickTeamsDescription') : - useCase === 'root' ? translation('selectRootLocationDescription') : - translation('selectLocationDescription') - } - className="w-[600px] h-[80vh] flex flex-col max-w-full" + description={descriptionTranslationMap[useCase]} + className="w-150 h-[80vh] flex flex-col max-w-full" >
@@ -481,22 +477,22 @@ export const LocationSelectionDialog = ({
- - + + + + + +
{multiSelect && useCase !== 'root' && (
- - + + + + + +
)}
@@ -504,7 +500,7 @@ export const LocationSelectionDialog = ({
{isLoading ? (
- Loading... + {translation('loading') + '...'}
) : (
diff --git a/web/components/locations/LocationTypeChip.tsx b/web/components/locations/LocationTypeChip.tsx new file mode 100644 index 00000000..3a424c1b --- /dev/null +++ b/web/components/locations/LocationTypeChip.tsx @@ -0,0 +1,15 @@ +import { useTasksTranslation } from '@/i18n/useTasksTranslation' +import { LocationUtils } from '@/utils/location' + +export interface LocationTypeChipProps { + type: string, +} + +export const LocationTypeChip = ({ type }: LocationTypeChipProps) => { + const translation = useTasksTranslation() + return ( + + {translation('locationType', { type })} + + ) +} \ No newline at end of file diff --git a/web/components/patients/LocationChipsBySetting.tsx b/web/components/patients/LocationChipsBySetting.tsx index c5cd9bd9..e6ec560a 100644 --- a/web/components/patients/LocationChipsBySetting.tsx +++ b/web/components/patients/LocationChipsBySetting.tsx @@ -1,4 +1,4 @@ -import { LocationChips } from '@/components/patients/LocationChips' +import { LocationChips } from '@/components/locations/LocationChips' import type { LocationType } from '@/api/gql/generated' import type { HTMLAttributes } from 'react' diff --git a/web/components/patients/PatientCardView.tsx b/web/components/patients/PatientCardView.tsx index 772ae5f2..7cf4a15f 100644 --- a/web/components/patients/PatientCardView.tsx +++ b/web/components/patients/PatientCardView.tsx @@ -1,5 +1,5 @@ import { Chip, ProgressIndicator, Tooltip } from '@helpwave/hightide' -import { SmartDate } from '@/utils/date' +import { DateDisplay } from '@/components/Date/DateDisplay' import { LocationChipsBySetting } from '@/components/patients/LocationChipsBySetting' import { PatientStateChip } from '@/components/patients/PatientStateChip' import { useTasksTranslation } from '@/i18n/useTasksTranslation' @@ -30,7 +30,6 @@ export const PatientCardView = ({ patient, onClick }: PatientCardViewProps) => { const { openTasksCount, closedTasksCount } = patient const total = openTasksCount + closedTasksCount const progress = total === 0 ? 0 : closedTasksCount / total - const tooltipText = `${translation('openTasks')}: ${openTasksCount}\n${translation('closedTasks')}: ${closedTasksCount}` return (
+ )} + alignment="top" >
@@ -55,7 +58,7 @@ export const PatientCardView = ({ patient, onClick }: PatientCardViewProps) => {
{translation('birthdate')}: - +
diff --git a/web/components/patients/PatientDataEditor.tsx b/web/components/patients/PatientDataEditor.tsx index c13119dc..9076c7bb 100644 --- a/web/components/patients/PatientDataEditor.tsx +++ b/web/components/patients/PatientDataEditor.tsx @@ -1,6 +1,6 @@ import { useState, useMemo, useEffect } from 'react' import type { FormFieldDataHandling } from '@helpwave/hightide' -import { FormProvider, Input, DateTimeInput, Select, SelectOption, Textarea, Checkbox, Button, ConfirmDialog, LoadingContainer, useCreateForm, FormField, Visibility, useFormObserverKey } from '@helpwave/hightide' +import { FormProvider, Input, DateTimeInput, Select, SelectOption, Textarea, Checkbox, Button, ConfirmDialog, LoadingContainer, useCreateForm, FormField, Visibility, useFormObserverKey, IconButton } from '@helpwave/hightide' import { CenteredLoadingLogo } from '@/components/CenteredLoadingLogo' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import type { CreatePatientInput, LocationNodeType, UpdatePatientInput, GetPatientQuery } from '@/api/gql/generated' @@ -275,13 +275,13 @@ export const PatientDataEditor = ({ return ( <> {isRefreshing && ( -
+
- {translation('refreshing')} + {translation('refreshing')}
)} -
{event.preventDefault(); form.submit() }} className="flex-col-4"> + {event.preventDefault(); form.submit() }} className="flex-col-6 pb-16 overflow-y-auto px-2 pt-4">
name="firstname" @@ -321,8 +321,14 @@ export const PatientDataEditor = ({ dataProps.onValueChange(value ? toISODate(value) : undefined)} - onEditComplete={(value) => dataProps.onEditComplete(value ? toISODate(value) : undefined)} + onValueChange={(value) => { + if(!value) return + dataProps.onValueChange(value) + }} + onEditComplete={(value) => { + if(!value) return + dataProps.onEditComplete(toISODate(value)) + }} pickerProps={{ start: startDate, end: endDate @@ -377,7 +383,9 @@ export const PatientDataEditor = ({ onValueChange(value ? PatientState.Wait : PatientState.Admitted)} + onValueChange={(value) => { + onValueChange(value ? PatientState.Wait : PatientState.Admitted)} + } onEditComplete={(value) => onEditComplete(value ? PatientState.Wait : PatientState.Admitted)} /> {translation('waitingForPatient')} @@ -446,25 +454,23 @@ export const PatientDataEditor = ({ className="flex-grow cursor-pointer" onClick={() => setIsClinicDialogOpen(true)} /> - + {value && !isEditMode && ( - + )}
@@ -486,25 +492,23 @@ export const PatientDataEditor = ({ className="flex-grow cursor-pointer" onClick={() => setIsPositionDialogOpen(true)} /> - + {value && ( - + )}
@@ -528,25 +532,24 @@ export const PatientDataEditor = ({ className="flex-grow cursor-pointer" onClick={() => setIsTeamsDialogOpen(true)} /> - + {value && value.length > 0 && ( - + )}
diff --git a/web/components/patients/PatientDetailView.tsx b/web/components/patients/PatientDetailView.tsx index b78f4b60..a0ebcf0e 100644 --- a/web/components/patients/PatientDetailView.tsx +++ b/web/components/patients/PatientDetailView.tsx @@ -1,8 +1,7 @@ import { useMemo, useCallback } from 'react' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import type { CreatePatientInput, PropertyValueInput } from '@/api/gql/generated' -import { PropertyEntity } from '@/api/gql/generated' -import { usePatient, usePropertyDefinitions } from '@/data' +import { usePatient } from '@/data' import { ProgressIndicator, TabList, @@ -11,7 +10,7 @@ import { Tooltip } from '@helpwave/hightide' import { PatientStateChip } from '@/components/patients/PatientStateChip' -import { LocationChips } from '@/components/patients/LocationChips' +import { LocationChips } from '@/components/locations/LocationChips' import { PatientTasksView } from './PatientTasksView' import { PatientDataEditor } from './PatientDataEditor' import { AuditLogTimeline } from '@/components/AuditLogTimeline' @@ -63,17 +62,8 @@ export const PatientDetailView = ({ { skip: !isEditMode } ) - const { data: propertyDefinitionsData } = usePropertyDefinitions() - const [updatePatient] = useUpdatePatient() - const hasAvailableProperties = useMemo(() => { - if (!propertyDefinitionsData?.propertyDefinitions) return false - return propertyDefinitionsData.propertyDefinitions.some( - def => def.isActive && def.allowedEntities.includes(PropertyEntity.Patient) - ) - }, [propertyDefinitionsData]) - const convertPropertyValueToInput = useCallback((definitionId: string, value: PropertyValue | null): PropertyValueInput | null => { if (!value) return null return { @@ -159,9 +149,13 @@ export const PatientDetailView = ({
{taskStats.totalTasks > 0 && ( + {`${translation('openTasks')}: ${taskStats.openTasks}`} + {`${translation('closedTasks')}: ${taskStats.closedTasks}`} +
+ )} + alignment="top" >
@@ -182,18 +176,18 @@ export const PatientDetailView = ({ )} - {isEditMode && patientId && ( - + + {patientId && ( - - )} + )} + - {isEditMode && hasAvailableProperties && patientId && ( - + + {patientId && ( - - )} + )} + - + - {isEditMode && patientId && ( - + + {patientId && ( - - )} + )} +
) diff --git a/web/components/patients/PatientTasksView.tsx b/web/components/patients/PatientTasksView.tsx index 9a4b1323..2ab446fb 100644 --- a/web/components/patients/PatientTasksView.tsx +++ b/web/components/patients/PatientTasksView.tsx @@ -89,67 +89,69 @@ export const PatientTasksView = ({ return ( <> -
-
+
+
+ + + + + {translation('openTasks')} ({openTasks.length}) + + + {openTasks.length === 0 && +
{translation('noOpenTasks')}
} + {openTasks.map(task => ( + setTaskId(t.id)} + onToggleDone={handleToggleDone} + showPatient={false} + showAssignee={!!(task.assignee || task.assigneeTeam)} + fullWidth={true} + /> + ))} +
+
+ + + + + + {translation('closedTasks')} ({closedTasks.length}) + + + {closedTasks.length === 0 && +
{translation('noClosedTasks')}
} + {closedTasks.map(task => ( + setTaskId(t.id)} + onToggleDone={handleToggleDone} + showPatient={false} + showAssignee={!!(task.assignee || task.assigneeTeam)} + fullWidth={true} + /> + ))} +
+
+
+
- - - - - {translation('openTasks')} ({openTasks.length}) - - - {openTasks.length === 0 && -
{translation('noOpenTasks')}
} - {openTasks.map(task => ( - setTaskId(t.id)} - onToggleDone={handleToggleDone} - showPatient={false} - showAssignee={!!(task.assignee || task.assigneeTeam)} - fullWidth={true} - /> - ))} -
-
- - - - - - {translation('closedTasks')} ({closedTasks.length}) - - - {closedTasks.length === 0 && -
{translation('noClosedTasks')}
} - {closedTasks.map(task => ( - setTaskId(t.id)} - onToggleDone={handleToggleDone} - showPatient={false} - showAssignee={!!(task.assignee || task.assigneeTeam)} - fullWidth={true} - /> - ))} -
-
} return ( - + ) } case FieldType.FieldTypeDateTime: { @@ -58,7 +58,7 @@ export const PropertyCell = ({ return } return ( - + ) } case FieldType.FieldTypeSelect: { @@ -107,7 +107,7 @@ export const PropertyCell = ({ ? `${textValue.substring(0, 15)}...` : String(textValue) return ( - + {displayText} ) diff --git a/web/components/properties/PropertyDetailView.tsx b/web/components/properties/PropertyDetailView.tsx index 350e675a..54b34172 100644 --- a/web/components/properties/PropertyDetailView.tsx +++ b/web/components/properties/PropertyDetailView.tsx @@ -10,7 +10,8 @@ import { FormField, FormProvider, useCreateForm, - FormObserverKey + FormObserverKey, + IconButton } from '@helpwave/hightide' import type { Property, PropertyFieldType, PropertySelectOption, PropertySubjectType } from '@/components/tables/PropertyList' import { propertyFieldTypeList, propertySubjectTypeList } from '@/components/tables/PropertyList' @@ -327,11 +328,11 @@ export const PropertyDetailView = ({ }} className="pr-11 w-full" /> - +
)) }} @@ -395,11 +396,11 @@ export const PropertyDetailView = ({ className="pr-16 w-full" placeholder={translation('rAdd', { name: translation('option') })} /> - +
) }} diff --git a/web/components/tables/PatientList.tsx b/web/components/tables/PatientList.tsx index 4635a282..be90cd7b 100644 --- a/web/components/tables/PatientList.tsx +++ b/web/components/tables/PatientList.tsx @@ -1,11 +1,10 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useCallback } from 'react' -import { Chip, FillerCell, Button, HelpwaveLogo, LoadingContainer, SearchBar, ProgressIndicator, Tooltip, Drawer, TableProvider, TableDisplay, TableColumnSwitcher } from '@helpwave/hightide' +import { Chip, FillerCell, HelpwaveLogo, LoadingContainer, SearchBar, ProgressIndicator, Tooltip, Drawer, TableProvider, TableDisplay, TableColumnSwitcher, IconButton, useLocale } from '@helpwave/hightide' import { PlusIcon } from 'lucide-react' import { Sex, PatientState, type GetPatientsQuery, type TaskType, PropertyEntity, type FullTextSearchInput, type LocationType } from '@/api/gql/generated' import { usePropertyDefinitions, usePatientsPaginated, useRefreshingEntityIds } from '@/data' import { PatientDetailView } from '@/components/patients/PatientDetailView' -import { SmartDate } from '@/utils/date' -import { LocationChips } from '@/components/patients/LocationChips' +import { LocationChips } from '@/components/locations/LocationChips' import { LocationChipsBySetting } from '@/components/patients/LocationChipsBySetting' import { PatientStateChip } from '@/components/patients/PatientStateChip' import { getLocationNodesByKind, type LocationKindColumn } from '@/utils/location' @@ -13,7 +12,7 @@ import { useTasksTranslation } from '@/i18n/useTasksTranslation' import { useTasksContext } from '@/hooks/useTasksContext' import type { ColumnDef, Row, TableState } from '@tanstack/table-core' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useTableState } from '@/hooks/useTableState' +import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' import { TABLE_PAGE_SIZE } from '@/utils/tableConfig' @@ -54,6 +53,7 @@ type PatientListProps = { export const PatientList = forwardRef(({ initialPatientId, onInitialPatientOpened, acceptedStates: _acceptedStates, rootLocationIds, locationId }, ref) => { const translation = useTasksTranslation() + const { locale } = useLocale() const { selectedRootLocationIds } = useTasksContext() const { refreshingPatientIds } = useRefreshingEntityIds() const { data: propertyDefinitionsData } = usePropertyDefinitions() @@ -72,7 +72,7 @@ export const PatientList = forwardRef(({ initi setFilters, columnVisibility, setColumnVisibility, - } = useTableState('patient-list') + } = useStorageSyncedTableState('patient-list') usePropertyColumnVisibility( propertyDefinitionsData, @@ -167,6 +167,12 @@ export const PatientList = forwardRef(({ initi [propertyDefinitionsData] ) + const dateFormat = useMemo(() => Intl.DateTimeFormat(locale, { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }), [locale]) + const rowLoadingCell = useMemo(() => , []) const columns = useMemo[]>(() => [ @@ -189,7 +195,7 @@ export const PatientList = forwardRef(({ initi minSize: 120, size: 144, maxSize: 180, - filterFn: 'tags', + filterFn: 'singleTag', meta: { filterData: { tags: allPatientStates.map(state => ({ label: translation('patientState', { state: state as string }), tag: state })), @@ -229,7 +235,7 @@ export const PatientList = forwardRef(({ initi minSize: 160, size: 160, maxSize: 200, - filterFn: 'tags', + filterFn: 'singleTag', meta: { filterData: { tags: [ @@ -275,10 +281,26 @@ export const PatientList = forwardRef(({ initi id: 'birthdate', header: translation('birthdate'), accessorKey: 'birthdate', - cell: ({ row }) => - refreshingPatientIds.has(row.original.id) ? rowLoadingCell : ( - - ), + cell: ({ row }) => { + if(refreshingPatientIds.has(row.original.id)) + return rowLoadingCell + + const now = new Date() + const birthdate = row.original.birthdate + let years = now.getFullYear() - birthdate.getFullYear() + const monthDiff = now.getMonth() - birthdate.getMonth() + const dayDiff = now.getDate() - birthdate.getDate() + + if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) { + years-- + } + + return ( + + {translation('nYears', { years })} + + ) + }, minSize: 200, size: 200, maxSize: 200, @@ -296,13 +318,16 @@ export const PatientList = forwardRef(({ initi const { openTasksCount, closedTasksCount } = row.original const total = openTasksCount + closedTasksCount const progress = total === 0 ? 0 : closedTasksCount / total - const tooltipText = `${translation('openTasks')}: ${openTasksCount}\n${translation('closedTasks')}: ${closedTasksCount}` return ( + {`${translation('openTasks')}: ${openTasksCount}`} + {`${translation('closedTasks')}: ${closedTasksCount}`} + + )} + alignment="top" >
@@ -321,7 +346,7 @@ export const PatientList = forwardRef(({ initi refreshingPatientIds.has(params.row.original.id) ? rowLoadingCell : (col.cell as (p: unknown) => React.ReactNode)(params) : undefined, })), - ], [allPatientStates, translation, patientPropertyColumns, refreshingPatientIds, rowLoadingCell]) + ], [translation, allPatientStates, patientPropertyColumns, refreshingPatientIds, rowLoadingCell, dateFormat]) const onRowClick = useCallback((row: Row) => handleEdit(row.original), [handleEdit]) const fillerRowCell = useCallback(() => (), []) @@ -362,17 +387,16 @@ export const PatientList = forwardRef(({ initi
- - - + { + setSelectedPatient(undefined) + setIsPanelOpen(true) + }} + color="primary" + > + +
diff --git a/web/components/tables/RecentPatientsTable.tsx b/web/components/tables/RecentPatientsTable.tsx index 6446330c..53140c18 100644 --- a/web/components/tables/RecentPatientsTable.tsx +++ b/web/components/tables/RecentPatientsTable.tsx @@ -4,12 +4,12 @@ import type { GetOverviewDataQuery } from '@/api/gql/generated' import { useCallback, useMemo } from 'react' import type { TableProps } from '@helpwave/hightide' import { FillerCell, TableDisplay, TableProvider, Tooltip } from '@helpwave/hightide' -import { SmartDate } from '@/utils/date' +import { DateDisplay } from '@/components/Date/DateDisplay' import { LocationChipsBySetting } from '@/components/patients/LocationChipsBySetting' import { PropertyEntity } from '@/api/gql/generated' import { usePropertyDefinitions } from '@/data' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useTableState } from '@/hooks/useTableState' +import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' type PatientViewModel = GetOverviewDataQuery['recentPatients'][0] @@ -36,7 +36,7 @@ export const RecentPatientsTable = ({ setFilters, columnVisibility, setColumnVisibility, - } = useTableState('recent-patients') + } = useStorageSyncedTableState('recent-patients') usePropertyColumnVisibility( propertyDefinitionsData, @@ -97,7 +97,7 @@ export const RecentPatientsTable = ({ const date = getValue() as Date | undefined if (!date) return return ( - + ) }, minSize: 200, diff --git a/web/components/tables/RecentTasksTable.tsx b/web/components/tables/RecentTasksTable.tsx index 39b08615..cab08163 100644 --- a/web/components/tables/RecentTasksTable.tsx +++ b/web/components/tables/RecentTasksTable.tsx @@ -5,14 +5,13 @@ import { useCallback, useMemo } from 'react' import clsx from 'clsx' import type { TableProps } from '@helpwave/hightide' import { Button, Checkbox, FillerCell, TableDisplay, TableProvider, Tooltip } from '@helpwave/hightide' -import { ArrowRightIcon } from 'lucide-react' -import { SmartDate } from '@/utils/date' +import { DateDisplay } from '@/components/Date/DateDisplay' import { DueDateUtils } from '@/utils/dueDate' import { PriorityUtils } from '@/utils/priority' import { PropertyEntity } from '@/api/gql/generated' import { usePropertyDefinitions } from '@/data' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useTableState } from '@/hooks/useTableState' +import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' type TaskViewModel = GetOverviewDataQuery['recentTasks'][0] @@ -46,7 +45,7 @@ export const RecentTasksTable = ({ setFilters, columnVisibility, setColumnVisibility, - } = useTableState('recent-tasks') + } = useStorageSyncedTableState('recent-tasks') usePropertyColumnVisibility( propertyDefinitionsData, @@ -125,7 +124,6 @@ export const RecentTasksTable = ({ className="flex-row-1 w-full justify-between" > {patient.name} - ) @@ -149,7 +147,7 @@ export const RecentTasksTable = ({ colorClass = '!text-orange-500' } return ( - return ( - + ) }, minSize: 220, diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index 7b111b50..5280fe85 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -1,13 +1,13 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useRef, useCallback } from 'react' import { useQueryClient } from '@tanstack/react-query' -import { Button, Checkbox, ConfirmDialog, FillerCell, HelpwaveLogo, LoadingContainer, SearchBar, Select, SelectOption, TableColumnSwitcher, TableDisplay, TableProvider, Tooltip } from '@helpwave/hightide' +import { Button, Checkbox, ConfirmDialog, FillerCell, HelpwaveLogo, IconButton, LoadingContainer, SearchBar, Select, SelectOption, TableColumnSwitcher, TableDisplay, TableProvider } from '@helpwave/hightide' import { PlusIcon, UserCheck, Users } from 'lucide-react' import type { TaskPriority, GetTasksQuery } from '@/api/gql/generated' import { PropertyEntity } from '@/api/gql/generated' import { useAssignTask, useAssignTaskToTeam, useCompleteTask, useReopenTask, useUsers, useLocations, usePropertyDefinitions, useRefreshingEntityIds } from '@/data' import { AssigneeSelectDialog } from '@/components/tasks/AssigneeSelectDialog' import clsx from 'clsx' -import { SmartDate } from '@/utils/date' +import { DateDisplay } from '@/components/Date/DateDisplay' import { Drawer } from '@helpwave/hightide' import { TaskDetailView } from '@/components/tasks/TaskDetailView' import { AvatarStatusComponent } from '@/components/AvatarStatusComponent' @@ -19,7 +19,7 @@ import type { ColumnDef, ColumnFiltersState, TableState } from '@tanstack/table- import { DueDateUtils } from '@/utils/dueDate' import { PriorityUtils } from '@/utils/priority' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useTableState } from '@/hooks/useTableState' +import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' export type TaskViewModel = { @@ -80,11 +80,11 @@ export const TaskList = forwardRef(({ tasks: initial setFilters, columnVisibility, setColumnVisibility, - } = useTableState('task-list', { - defaultSorting: [ + } = useStorageSyncedTableState('task-list', { + defaultSorting: useMemo(() => [ { id: 'done', desc: false }, { id: 'dueDate', desc: false }, - ], + ], []), }) usePropertyColumnVisibility( @@ -401,7 +401,7 @@ export const TaskList = forwardRef(({ tasks: initial colorClass = '!text-orange-500' } return ( - (({ tasks: initial value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onSearch={() => null} - containerProps={{ className: 'max-w-80 h-10' }} + containerProps={{ className: 'max-w-80' }} />
-
+