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

@@ -756,7 +756,23 @@
"settings": {
"accountTitle": "Account",
"accountSubtitle": "Profile and security settings for your login.",
"accountPlaceholder": "Password change and profile editing will be wired here next (e.g. invite flow, reset password).",
"participationSectionTitle": "Clinical participation",
"participationSectionSubtitle": "Choose whether you personally take part in clinical or lab work for this organization.",
"participateInTreatments": "I want to participate in treatments",
"participateInTasks": "I want to participate in tasks",
"participateWorkingHoursTitle": "Working hours",
"participateWorkingHoursSubtitle": "Set your schedule so appointments can be booked for you.",
"participateSaveHours": "Save and participate",
"participateEnabledTreatments": "You are now participating in treatments.",
"participateEnabledTasks": "You are now participating in tasks.",
"participateDisabledTreatments": "You are no longer participating in treatments.",
"participateDisabledTasks": "You are no longer participating in tasks.",
"participateUpdateFailed": "Could not update participation settings. Please try again.",
"participateConfirmRevokeTitle": "Stop participating?",
"participateConfirmRevokeBodyTreatments": "You will lose treatment access and be removed from the appointments schedule. Your saved working hours will be kept.",
"participateConfirmRevokeBodyTasks": "You will lose task edit access and be removed from the efficiency report.",
"participateConfirmRevokeConfirm": "Stop participating",
"changePasswordOption": "Change password",
"changePasswordTitle": "Change password",
"resetPasswordTitle": "Set a new password",
"resetPasswordSubtitle": "Your mobile was verified. Choose a new password for your account.",

View File

@@ -757,7 +757,23 @@
"settings": {
"accountTitle": "حساب کاربری",
"accountSubtitle": "تنظیمات پروفایل و امنیت برای ورود شما.",
"accountPlaceholder": "تغییر رمز عبور و ویرایش پروفایل در مرحله بعدی در اینجا قرار می‌گیرند (مثلاً فرآیند دعوت، بازنشانی رمز عبور).",
"participationSectionTitle": "مشارکت بالینی",
"participationSectionSubtitle": "مشخص کنید آیا شخصاً در درمان‌ها یا کارهای آزمایشگاهی این سازمان شرکت می‌کنید.",
"participateInTreatments": "می‌خواهم در درمان‌ها شرکت کنم",
"participateInTasks": "می‌خواهم در وظایف شرکت کنم",
"participateWorkingHoursTitle": "ساعات کاری",
"participateWorkingHoursSubtitle": "برنامه خود را تنظیم کنید تا نوبت‌ها برای شما رزرو شوند.",
"participateSaveHours": "ذخیره و مشارکت",
"participateEnabledTreatments": "اکنون در درمان‌ها شرکت می‌کنید.",
"participateEnabledTasks": "اکنون در وظایف شرکت می‌کنید.",
"participateDisabledTreatments": "دیگر در درمان‌ها شرکت نمی‌کنید.",
"participateDisabledTasks": "دیگر در وظایف شرکت نمی‌کنید.",
"participateUpdateFailed": "به‌روزرسانی تنظیمات مشارکت ممکن نشد. دوباره تلاش کنید.",
"participateConfirmRevokeTitle": "توقف مشارکت؟",
"participateConfirmRevokeBodyTreatments": "دسترسی درمان را از دست می‌دهید و از برنامه نوبت‌ها حذف می‌شوید. ساعات کاری ذخیره‌شده حفظ می‌شود.",
"participateConfirmRevokeBodyTasks": "دسترسی ویرایش وظایف را از دست می‌دهید و از گزارش کارایی حذف می‌شوید.",
"participateConfirmRevokeConfirm": "توقف مشارکت",
"changePasswordOption": "تغییر رمز عبور",
"changePasswordTitle": "تغییر رمز عبور",
"resetPasswordTitle": "تنظیم رمز عبور جدید",
"resetPasswordSubtitle": "موبایل شما تأیید شد. رمز عبور جدید برای حساب خود انتخاب کنید.",

View File

@@ -757,7 +757,23 @@
"settings": {
"accountTitle": "Account",
"accountSubtitle": "Profiel- en beveiligingsinstellingen voor uw login.",
"accountPlaceholder": "Wachtwoordwijziging en profielbewerking worden hierna hier aangesloten (bijv. uitnodigingsflow, wachtwoord herstellen).",
"participationSectionTitle": "Klinische deelname",
"participationSectionSubtitle": "Kies of u persoonlijk deelneemt aan behandelingen of labtaken voor deze organisatie.",
"participateInTreatments": "Ik wil deelnemen aan behandelingen",
"participateInTasks": "Ik wil deelnemen aan taken",
"participateWorkingHoursTitle": "Werkuren",
"participateWorkingHoursSubtitle": "Stel uw rooster in zodat afspraken voor u geboekt kunnen worden.",
"participateSaveHours": "Opslaan en deelnemen",
"participateEnabledTreatments": "U neemt nu deel aan behandelingen.",
"participateEnabledTasks": "U neemt nu deel aan taken.",
"participateDisabledTreatments": "U neemt niet langer deel aan behandelingen.",
"participateDisabledTasks": "U neemt niet langer deel aan taken.",
"participateUpdateFailed": "Deelname-instellingen konden niet worden bijgewerkt. Probeer het opnieuw.",
"participateConfirmRevokeTitle": "Deelname stoppen?",
"participateConfirmRevokeBodyTreatments": "U verliest toegang tot behandelingen en wordt uit het afsprakenrooster verwijderd. Opgeslagen werkuren blijven bewaard.",
"participateConfirmRevokeBodyTasks": "U verliest bewerkingstoegang tot taken en wordt uit het efficiëntierapport verwijderd.",
"participateConfirmRevokeConfirm": "Deelname stoppen",
"changePasswordOption": "Wachtwoord wijzigen",
"changePasswordTitle": "Wachtwoord wijzigen",
"resetPasswordTitle": "Nieuw wachtwoord instellen",
"resetPasswordSubtitle": "Uw mobiel is geverifieerd. Kies een nieuw wachtwoord voor uw account.",

View File

@@ -1,18 +1,22 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useTranslations } from 'next-intl';
import { Link, useRouter } from '@/i18n/navigation';
import { useSearchParams } from 'next/navigation';
import { Lock } from 'lucide-react';
import { ChevronDown, Lock } from 'lucide-react';
import { useAuth } from '@/lib/hooks/useAuth';
import { authApi } from '@/lib/api/auth';
import { accountApi } from '@/lib/api/account';
import { Button } from '@/components/ui/shared/Button';
import { Input } from '@/components/ui/shared/Input';
import { Toast } from '@/components/ui/shared/Toast';
import { Checkbox } from '@/components/ui/shared/Checkbox';
import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton';
import { OwnerWorkingHoursDialog } from '@/components/settings/OwnerWorkingHoursDialog';
type PasswordForm = {
currentPassword: string;
@@ -25,13 +29,27 @@ export default function AccountSettingsPage() {
const tAuth = useTranslations('auth');
const tCommon = useTranslations('common');
const tValidation = useTranslations('validation');
const { user, isAuthReady } = useAuth();
const { user, currentOrganization, isAuthReady, refreshSession } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const isResetFlow = searchParams.get('reset') === '1';
const isOwner = Boolean(currentOrganization?.isOwner);
const orgType = currentOrganization?.type;
const showClinicParticipation = isOwner && orgType === 'CLINIC';
const showLabParticipation = isOwner && orgType === 'LAB';
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [passwordExpanded, setPasswordExpanded] = useState(isResetFlow);
const [participationLoading, setParticipationLoading] = useState(false);
const [participatesInTreatments, setParticipatesInTreatments] = useState(false);
const [participatesInTasks, setParticipatesInTasks] = useState(false);
const [workingHoursOpen, setWorkingHoursOpen] = useState(false);
const [revokeConfirmOpen, setRevokeConfirmOpen] = useState(false);
const [pendingRevokeType, setPendingRevokeType] = useState<'CLINIC' | 'LAB' | null>(null);
const passwordSchema = useMemo(
() =>
@@ -75,12 +93,147 @@ export default function AccountSettingsPage() {
},
});
const loadParticipation = useCallback(async () => {
if (!isOwner) return;
try {
const res = await accountApi.getParticipation();
setParticipatesInTreatments(res.data.participatesInTreatments);
setParticipatesInTasks(res.data.participatesInTasks);
} catch {
/* non-owners or missing org context */
}
}, [isOwner]);
useEffect(() => {
if (isAuthReady && !user) {
router.replace('/login');
}
}, [isAuthReady, user, router]);
useEffect(() => {
if (isResetFlow) {
setPasswordExpanded(true);
}
}, [isResetFlow]);
useEffect(() => {
void loadParticipation();
}, [loadParticipation, currentOrganization?.id]);
const syncSessionAfterParticipationChange = useCallback(async () => {
await refreshSession();
}, [refreshSession]);
const enableClinicParticipation = useCallback(
async (options: {
skipHours: boolean;
hoursPayload?: {
autoRepeatWeekly: boolean;
blocks: { dayOfWeek: number; startMinute: number; endMinute: number; sortOrder?: number }[];
};
}) => {
await accountApi.updateParticipation(true);
if (!options.skipHours && options.hoursPayload) {
await accountApi.upsertMyWorkingHours(options.hoursPayload);
}
setParticipatesInTreatments(true);
await syncSessionAfterParticipationChange();
setSuccessMessage(t('participateEnabledTreatments'));
},
[syncSessionAfterParticipationChange, t],
);
const enableLabParticipation = async () => {
setParticipationLoading(true);
setError(null);
try {
await accountApi.updateParticipation(true);
setParticipatesInTasks(true);
await syncSessionAfterParticipationChange();
setSuccessMessage(t('participateEnabledTasks'));
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('participateUpdateFailed');
setError(message || t('participateUpdateFailed'));
setParticipatesInTasks(false);
} finally {
setParticipationLoading(false);
}
};
const confirmRevokeParticipation = async () => {
if (!pendingRevokeType) return;
setParticipationLoading(true);
setError(null);
try {
await accountApi.updateParticipation(false);
if (pendingRevokeType === 'CLINIC') {
setParticipatesInTreatments(false);
} else {
setParticipatesInTasks(false);
}
await syncSessionAfterParticipationChange();
setSuccessMessage(
pendingRevokeType === 'CLINIC'
? t('participateDisabledTreatments')
: t('participateDisabledTasks'),
);
setRevokeConfirmOpen(false);
setPendingRevokeType(null);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('participateUpdateFailed');
setError(message || t('participateUpdateFailed'));
} finally {
setParticipationLoading(false);
}
};
const handleClinicParticipationChange = (checked: boolean) => {
setError(null);
if (checked) {
setWorkingHoursOpen(true);
return;
}
setPendingRevokeType('CLINIC');
setRevokeConfirmOpen(true);
};
const handleWorkingHoursClose = useCallback(() => {
setWorkingHoursOpen(false);
}, []);
const handleWorkingHoursComplete = useCallback(
async (options: {
skipHours: boolean;
hoursPayload?: {
autoRepeatWeekly: boolean;
blocks: { dayOfWeek: number; startMinute: number; endMinute: number; sortOrder?: number }[];
};
}) => {
setParticipationLoading(true);
setError(null);
try {
await enableClinicParticipation(options);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('participateUpdateFailed');
setError(message || t('participateUpdateFailed'));
throw err;
} finally {
setParticipationLoading(false);
}
},
[enableClinicParticipation, t],
);
const handleLabParticipationChange = (checked: boolean) => {
setError(null);
if (checked) {
void enableLabParticipation();
return;
}
setPendingRevokeType('LAB');
setRevokeConfirmOpen(true);
};
const onSubmit = async (data: PasswordForm) => {
try {
setError(null);
@@ -121,65 +274,178 @@ export default function AccountSettingsPage() {
{tCommon('backToApp')}
</Link>
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary mt-4">{t('accountTitle')}</h1>
<p className="text-text-secondary text-sm mt-2">
{isResetFlow ? t('resetPasswordSubtitle') : t('accountSubtitle')}
</p>
<p className="text-text-secondary text-sm mt-2">{t('accountSubtitle')}</p>
</div>
<div className="surface-card p-4 sm:p-6 max-w-lg">
<h2 className="text-lg font-medium text-text-primary mb-1">
{isResetFlow ? t('resetPasswordTitle') : t('changePasswordTitle')}
</h2>
<p className="text-sm text-text-secondary mb-6">
{user.email}
{user.mobile ? ` · ${user.mobile}` : ''}
</p>
{(showClinicParticipation || showLabParticipation) && (
<div className="surface-card p-4 sm:p-6 max-w-lg space-y-4">
<div>
<h2 className="text-lg font-medium text-text-primary">{t('participationSectionTitle')}</h2>
<p className="text-sm text-text-secondary mt-1">{t('participationSectionSubtitle')}</p>
</div>
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
{!isResetFlow && (
<Input
label={t('currentPassword')}
{...register('currentPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.currentPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
{showClinicParticipation && (
<Checkbox
checked={participatesInTreatments}
onChange={handleClinicParticipationChange}
disabled={participationLoading}
label={t('participateInTreatments')}
/>
)}
<Input
label={t('newPassword')}
{...register('newPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.newPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
<Input
label={t('confirmNewPassword')}
{...register('confirmPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.confirmPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
{error && (
<div className="p-3 bg-red-950/30 border border-red-600/40 rounded-[var(--radius-md)]">
<p className="text-sm text-red-600">{error}</p>
</div>
{showLabParticipation && (
<Checkbox
checked={participatesInTasks}
onChange={handleLabParticipationChange}
disabled={participationLoading}
label={t('participateInTasks')}
/>
)}
</div>
)}
<Button type="submit" variant="primary" isLoading={isSubmitting}>
{isResetFlow ? t('setNewPassword') : t('updatePassword')}
</Button>
</form>
<div className="surface-card max-w-lg overflow-hidden">
<button
type="button"
className="w-full flex items-center justify-between gap-3 p-4 sm:p-6 text-left hover:bg-background-card/40 transition-colors"
onClick={() => setPasswordExpanded((open) => !open)}
aria-expanded={passwordExpanded}
>
<div className="flex items-center gap-3 min-w-0">
<Lock className="h-5 w-5 icon-flat shrink-0" />
<div>
<p className="text-base font-medium text-text-primary">{t('changePasswordOption')}</p>
<p className="text-sm text-text-secondary truncate">
{user.email}
{user.mobile ? ` · ${user.mobile}` : ''}
</p>
</div>
</div>
<ChevronDown
className={`h-5 w-5 text-text-muted shrink-0 transition-transform ${
passwordExpanded ? 'rotate-180' : ''
}`}
/>
</button>
{passwordExpanded && (
<div className="border-t border-border px-4 sm:px-6 pb-4 sm:pb-6 pt-4">
<h2 className="text-lg font-medium text-text-primary mb-1">
{isResetFlow ? t('resetPasswordTitle') : t('changePasswordTitle')}
</h2>
{isResetFlow && (
<p className="text-sm text-text-secondary mb-4">{t('resetPasswordSubtitle')}</p>
)}
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
{!isResetFlow && (
<Input
label={t('currentPassword')}
{...register('currentPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.currentPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
)}
<Input
label={t('newPassword')}
{...register('newPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.newPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
<Input
label={t('confirmNewPassword')}
{...register('confirmPassword')}
type="password"
placeholder={tAuth('passwordPlaceholder')}
error={errors.confirmPassword?.message}
icon={<Lock className="h-5 w-5 icon-flat" />}
passwordToggleLabels={passwordToggleLabels}
/>
{error && (
<div className="p-3 bg-red-950/30 border border-red-600/40 rounded-[var(--radius-md)]">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<Button type="submit" variant="primary" isLoading={isSubmitting}>
{isResetFlow ? t('setNewPassword') : t('updatePassword')}
</Button>
</form>
</div>
)}
</div>
{error && !passwordExpanded && (
<div className="max-w-lg p-3 bg-red-950/30 border border-red-600/40 rounded-[var(--radius-md)]">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<OwnerWorkingHoursDialog
open={workingHoursOpen}
onClose={handleWorkingHoursClose}
onComplete={handleWorkingHoursComplete}
/>
{revokeConfirmOpen && (
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/55">
<div
className="surface-card w-full sm:max-w-md max-h-[90dvh] overflow-y-auto p-4 sm:p-5 space-y-4 shadow-xl rounded-t-[var(--radius-lg)] sm:rounded-[var(--radius-lg)]"
role="dialog"
aria-modal="true"
aria-labelledby="revoke-participation-title"
>
<div className="flex items-start justify-between gap-2">
<h2 id="revoke-participation-title" className="text-lg font-semibold text-text-primary pr-2">
{t('participateConfirmRevokeTitle')}
</h2>
<DialogCloseButton
onClick={() => {
if (participationLoading) return;
setRevokeConfirmOpen(false);
setPendingRevokeType(null);
}}
/>
</div>
<p className="text-sm text-text-secondary">
{pendingRevokeType === 'CLINIC'
? t('participateConfirmRevokeBodyTreatments')
: t('participateConfirmRevokeBodyTasks')}
</p>
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 pt-1">
<Button
type="button"
variant="outline"
disabled={participationLoading}
onClick={() => {
setRevokeConfirmOpen(false);
setPendingRevokeType(null);
}}
>
{tCommon('cancel')}
</Button>
<Button
type="button"
variant="primary"
isLoading={participationLoading}
onClick={() => void confirmRevokeParticipation()}
>
{t('participateConfirmRevokeConfirm')}
</Button>
</div>
</div>
</div>
)}
{successMessage && (
<Toast variant="success">{successMessage}</Toast>
)}

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,
};
}

View File

@@ -0,0 +1,48 @@
import { apiClient } from './client';
export type WorkingHoursPayload = {
autoRepeatWeekly: boolean;
blocks: { dayOfWeek: number; startMinute: number; endMinute: number; sortOrder?: number }[];
};
export type ParticipationData = {
membershipId: string;
orgType: 'CLINIC' | 'LAB';
participatesInTreatments: boolean;
participatesInTasks: boolean;
hasWorkingHours: boolean;
};
export type ParticipationUpdateData = ParticipationData & {
permissions?: string[];
};
export const accountApi = {
getParticipation: async (): Promise<{ success: boolean; data: ParticipationData }> => {
const response = await apiClient.get('/auth/profile/participation');
return response.data;
},
updateParticipation: async (participate: boolean): Promise<{
success: boolean;
data: ParticipationUpdateData;
}> => {
const response = await apiClient.patch('/auth/profile/participation', { participate });
return response.data;
},
getMyWorkingHours: async (): Promise<{
success: boolean;
data: WorkingHoursPayload & { hasWorkingHours: boolean };
}> => {
const response = await apiClient.get('/auth/profile/working-hours');
return response.data;
},
upsertMyWorkingHours: async (
payload: WorkingHoursPayload,
): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.put('/auth/profile/working-hours', payload);
return response.data;
},
};