improvement: a flow added for owner users to make it possible for them to participate in treatments or tasks.

This commit is contained in:
2026-07-12 15:11:50 +03:30
parent 4208d15efb
commit 39935688ad
20 changed files with 1122 additions and 154 deletions

View File

@@ -0,0 +1,161 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/shared/Button';
import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton';
import {
StaffWorkingHoursStep,
createDefaultWorkingHoursState,
useWorkingHoursForm,
workingHoursPayloadFromState,
workingHoursStateFromApi,
} from '@/components/staff/StaffWorkingHoursStep';
import { accountApi } from '@/lib/api/account';
type OwnerWorkingHoursDialogProps = {
open: boolean;
onClose: () => void;
onComplete: (options: {
skipHours: boolean;
hoursPayload?: ReturnType<typeof workingHoursPayloadFromState>;
}) => Promise<void>;
};
export function OwnerWorkingHoursDialog({
open,
onClose,
onComplete,
}: OwnerWorkingHoursDialogProps) {
const t = useTranslations('settings');
const tStaff = useTranslations('staff');
const tCommon = useTranslations('common');
const {
days,
setDays,
autoRepeatWeekly,
setAutoRepeatWeekly,
validationError,
setValidationError,
reset,
} = useWorkingHoursForm();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!open) {
setLoading(false);
return;
}
let cancelled = false;
reset(createDefaultWorkingHoursState());
void accountApi
.getMyWorkingHours()
.then((res) => {
if (cancelled) return;
reset(workingHoursStateFromApi(res.data));
})
.catch(() => {
/* keep defaults for first-time opt-in */
});
return () => {
cancelled = true;
};
}, [open, reset]);
const handleValidationChange = useCallback(
(error: string | null) => {
setValidationError(error);
},
[setValidationError],
);
const handleClose = useCallback(() => {
if (loading) return;
onClose();
}, [loading, onClose]);
const handleSkip = async () => {
try {
setLoading(true);
await onComplete({ skipHours: true });
onClose();
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (validationError) return;
try {
setLoading(true);
await onComplete({
skipHours: false,
hoursPayload: workingHoursPayloadFromState({ days, autoRepeatWeekly }),
});
onClose();
} finally {
setLoading(false);
}
};
if (!open) {
return null;
}
return (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/50">
<div
className="w-full sm:max-w-lg max-h-[90dvh] overflow-y-auto rounded-t-[var(--radius-lg)] sm:rounded-[var(--radius-md)] border border-border bg-background-secondary p-4 sm:p-6 shadow-xl space-y-4"
role="dialog"
aria-modal="true"
aria-labelledby="owner-working-hours-title"
>
<div className="flex items-start justify-between gap-3">
<div>
<h2 id="owner-working-hours-title" className="text-lg font-semibold text-text-primary pr-2">
{t('participateWorkingHoursTitle')}
</h2>
<p className="text-xs text-text-muted mt-1">{t('participateWorkingHoursSubtitle')}</p>
</div>
<DialogCloseButton onClick={handleClose} />
</div>
<StaffWorkingHoursStep
days={days}
autoRepeatWeekly={autoRepeatWeekly}
onDaysChange={setDays}
onAutoRepeatWeeklyChange={setAutoRepeatWeekly}
onValidationChange={handleValidationChange}
disabled={loading}
/>
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 pt-2">
<Button variant="outline" type="button" disabled={loading} onClick={handleClose}>
{tCommon('cancel')}
</Button>
<Button
type="button"
variant="outline"
isLoading={loading}
onClick={() => void handleSkip()}
>
{tStaff('skipForNow')}
</Button>
<Button
type="button"
isLoading={loading}
disabled={Boolean(validationError)}
onClick={() => void handleSave()}
>
{t('participateSaveHours')}
</Button>
</div>
</div>
</div>
);
}
export { workingHoursPayloadFromState };

View File

@@ -142,15 +142,13 @@ export function canAccessAppointmentsSection(org: Organization | null): boolean
export function canEditTreatment(org: Organization | null): boolean {
if (!org) return false;
if (org.type !== 'CLINIC') return false;
if (org.isOwner) return true;
return hasPermission(org, 'TAB_TREATMENT_EDIT');
}
/** Staff treatment editors only — personal schedule Today gadgets (not owners). */
/** Staff and participating owners — personal schedule Today gadgets */
export function canViewMyAppointmentsWeekChart(org: Organization | null): boolean {
if (!org) return false;
if (org.type !== 'CLINIC') return false;
if (org.isOwner) return false;
return hasPermission(org, 'TAB_TREATMENT_EDIT');
}
@@ -158,7 +156,6 @@ export function canViewMyAppointmentsWeekChart(org: Organization | null): boolea
export function canViewTreatment(org: Organization | null): boolean {
if (!org) return false;
if (org.type !== 'CLINIC') return false;
if (org.isOwner) return true;
return (
hasPermission(org, 'TAB_TREATMENT_READ') ||
hasPermission(org, 'TAB_TREATMENT_EDIT')
@@ -187,7 +184,6 @@ export function canEditCases(org: Organization | null): boolean {
export function canViewTasks(org: Organization | null): boolean {
if (!org) return false;
if (org.type !== 'LAB') return false;
if (org.isOwner) return true;
return (
hasPermission(org, 'TAB_TASKS_READ') ||
hasPermission(org, 'TAB_TASKS_EDIT')
@@ -197,7 +193,6 @@ export function canViewTasks(org: Organization | null): boolean {
export function canEditTasks(org: Organization | null): boolean {
if (!org) return false;
if (org.type !== 'LAB') return false;
if (org.isOwner) return true;
return hasPermission(org, 'TAB_TASKS_EDIT');
}

View File

@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { WorkingHoursEditor } from '@/components/staff/WorkingHoursEditor';
import {
@@ -87,6 +87,12 @@ export function useWorkingHoursForm(initial?: {
const [autoRepeatWeekly, setAutoRepeatWeekly] = useState(initial?.autoRepeatWeekly ?? true);
const [validationError, setValidationError] = useState<string | null>(null);
const reset = useCallback((next?: { days: WorkingHoursEditorDay[]; autoRepeatWeekly: boolean }) => {
setDays(next?.days ?? emptyWorkingHoursEditorDays());
setAutoRepeatWeekly(next?.autoRepeatWeekly ?? true);
setValidationError(null);
}, []);
return {
days,
setDays,
@@ -94,10 +100,6 @@ export function useWorkingHoursForm(initial?: {
setAutoRepeatWeekly,
validationError,
setValidationError,
reset(next?: { days: WorkingHoursEditorDay[]; autoRepeatWeekly: boolean }) {
setDays(next?.days ?? emptyWorkingHoursEditorDays());
setAutoRepeatWeekly(next?.autoRepeatWeekly ?? true);
setValidationError(null);
},
reset,
};
}