feature: localization's first implmentation done. all frontend hardcoded text is now localized.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRouter } from '@/i18n/navigation';
|
||||
import {
|
||||
firstAccessibleDashboardPath,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
resolveStaffFeatureLabel,
|
||||
formatAccessSummary,
|
||||
type FeaturePermState,
|
||||
} from '../../../components/staff/staff-permission-form';
|
||||
} from '@/components/staff/staff-permission-form';
|
||||
import {
|
||||
StaffWorkingHoursStep,
|
||||
createDefaultWorkingHoursState,
|
||||
@@ -90,6 +91,9 @@ function PermissionGrid({
|
||||
disabled?: boolean;
|
||||
organizationType?: 'CLINIC' | 'LAB';
|
||||
}) {
|
||||
const t = useTranslations('staff');
|
||||
const tFeatures = useTranslations('staff.features');
|
||||
|
||||
const setRead = (editKey: string, read: boolean) => {
|
||||
const cur = state[editKey] ?? { read: false, edit: false };
|
||||
onChange({
|
||||
@@ -116,19 +120,19 @@ function PermissionGrid({
|
||||
className="flex flex-col gap-3 rounded-[var(--radius-md)] border border-border/60 bg-background-card/50 px-3 py-3"
|
||||
>
|
||||
<span className="text-sm font-medium text-text-primary">
|
||||
{resolveStaffFeatureLabel(g, organizationType)}
|
||||
{resolveStaffFeatureLabel(g, organizationType, tFeatures)}
|
||||
</span>
|
||||
<div className="flex flex-col gap-2.5 pl-0.5">
|
||||
<Checkbox
|
||||
checked={cell.read}
|
||||
disabled={disabled}
|
||||
label="View"
|
||||
label={t('permissionView')}
|
||||
onChange={(v) => setRead(g.edit, v)}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={cell.edit}
|
||||
disabled={disabled}
|
||||
label="Edit"
|
||||
label={t('permissionEdit')}
|
||||
onChange={(v) => setEdit(g.edit, v)}
|
||||
/>
|
||||
</div>
|
||||
@@ -141,6 +145,10 @@ function PermissionGrid({
|
||||
|
||||
export default function StaffPage() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations('staff');
|
||||
const tCommon = useTranslations('common');
|
||||
const tFeatures = useTranslations('staff.features');
|
||||
const tWorkingHours = useTranslations('staff.workingHours');
|
||||
const { currentOrganization, user } = useAuth();
|
||||
const [members, setMembers] = useState<StaffMemberDto[]>([]);
|
||||
const [seats, setSeats] = useState<{
|
||||
@@ -216,11 +224,11 @@ export default function StaffPage() {
|
||||
setMembers(res.data.members);
|
||||
setSeats(res.data.seats);
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Failed to load staff.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorLoadStaff')));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentOrganization?.id) return;
|
||||
@@ -291,7 +299,7 @@ export default function StaffPage() {
|
||||
await load();
|
||||
}
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Could not copy invitation link.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorCopyInvite')));
|
||||
} finally {
|
||||
setCopyingInviteMembershipId(null);
|
||||
}
|
||||
@@ -312,7 +320,7 @@ export default function StaffPage() {
|
||||
if (!includeHours || !inviteHasTreatmentEdit) {
|
||||
return;
|
||||
}
|
||||
const validationError = validateEditorDays(inviteWorkingHoursDays);
|
||||
const validationError = validateEditorDays(inviteWorkingHoursDays, tWorkingHours);
|
||||
if (validationError) {
|
||||
throw new Error(validationError);
|
||||
}
|
||||
@@ -333,7 +341,7 @@ export default function StaffPage() {
|
||||
const displayEmail = inviteEmail.trim();
|
||||
try {
|
||||
if (includeWorkingHours && inviteHasTreatmentEdit) {
|
||||
const validationError = validateEditorDays(inviteWorkingHoursDays);
|
||||
const validationError = validateEditorDays(inviteWorkingHoursDays, tWorkingHours);
|
||||
if (validationError) {
|
||||
toast.showError(validationError);
|
||||
return;
|
||||
@@ -374,7 +382,7 @@ export default function StaffPage() {
|
||||
resetInviteForm();
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Failed to send invitation.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorSendInvite')));
|
||||
} finally {
|
||||
setInviteLoading(false);
|
||||
}
|
||||
@@ -397,7 +405,7 @@ export default function StaffPage() {
|
||||
setEditWorkingHoursDays(state.days);
|
||||
setEditAutoRepeatWeekly(state.autoRepeatWeekly);
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Failed to load working hours.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorLoadWorkingHours')));
|
||||
} finally {
|
||||
setEditLoadingWorkingHours(false);
|
||||
}
|
||||
@@ -406,7 +414,7 @@ export default function StaffPage() {
|
||||
async function submitEdit() {
|
||||
if (!editing) return;
|
||||
if (editHasTreatmentEdit) {
|
||||
const validationError = validateEditorDays(editWorkingHoursDays);
|
||||
const validationError = validateEditorDays(editWorkingHoursDays, tWorkingHours);
|
||||
if (validationError) {
|
||||
toast.showError(validationError);
|
||||
return;
|
||||
@@ -431,19 +439,19 @@ export default function StaffPage() {
|
||||
permissionNames: permissionNamesFromFeatureState(editPerms),
|
||||
});
|
||||
|
||||
toast.showSuccess('Member updated.');
|
||||
toast.showSuccess(t('successMemberUpdated'));
|
||||
setEditing(null);
|
||||
setEditStep(1);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Failed to update member.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorUpdateMember')));
|
||||
} finally {
|
||||
setEditLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteMember() {
|
||||
toast.showError('Delete is not implemented yet.');
|
||||
toast.showError(t('errorDeleteNotImplemented'));
|
||||
}
|
||||
|
||||
async function confirmDisableMember() {
|
||||
@@ -453,11 +461,11 @@ export default function StaffPage() {
|
||||
toast.setError('');
|
||||
try {
|
||||
await staffApi.disableMember(disableTarget.id);
|
||||
toast.showSuccess(`${disableTarget.name} was disabled. A seat is now available.`);
|
||||
toast.showSuccess(t('successMemberDisabled', { name: disableTarget.name }));
|
||||
setDisableTarget(null);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Failed to disable member.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorDisableMember')));
|
||||
} finally {
|
||||
setDisablingMembershipId(null);
|
||||
}
|
||||
@@ -470,11 +478,11 @@ export default function StaffPage() {
|
||||
toast.setError('');
|
||||
try {
|
||||
await staffApi.enableMember(enableTarget.id);
|
||||
toast.showSuccess(`${enableTarget.name} was enabled and can sign in again.`);
|
||||
toast.showSuccess(t('successMemberEnabled', { name: enableTarget.name }));
|
||||
setEnableTarget(null);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Failed to enable member.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorEnableMember')));
|
||||
} finally {
|
||||
setEnablingMembershipId(null);
|
||||
}
|
||||
@@ -482,7 +490,7 @@ export default function StaffPage() {
|
||||
|
||||
if (!currentOrganization || !canViewStaff(currentOrganization)) {
|
||||
return (
|
||||
<p className="text-sm text-text-secondary">Redirecting…</p>
|
||||
<p className="text-sm text-text-secondary">{t('redirecting')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -490,10 +498,8 @@ export default function StaffPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-text-primary">Staff Management</h1>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
Invite teammates, set tab access, and stay within your plan seat limit.
|
||||
</p>
|
||||
<h1 className="text-2xl font-semibold text-text-primary">{t('title')}</h1>
|
||||
<p className="text-sm text-text-secondary mt-1">{t('subtitle')}</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -505,9 +511,9 @@ export default function StaffPage() {
|
||||
}}
|
||||
disabled={!canEdit || atSeatLimit}
|
||||
className="shrink-0"
|
||||
title={!canEdit ? 'Read-only access for this organization.' : undefined}
|
||||
title={!canEdit ? tCommon('readOnlyAccess') : undefined}
|
||||
>
|
||||
Invite member
|
||||
{t('inviteMember')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -515,16 +521,14 @@ export default function StaffPage() {
|
||||
|
||||
{seats && (
|
||||
<p className="text-sm text-text-secondary">
|
||||
Seats:{' '}
|
||||
{t('seatsLabel')}{' '}
|
||||
<span className="text-text-primary font-medium">
|
||||
{seats.used}
|
||||
{seats.unlimited ? ' (unlimited plan)' : ` / ${seats.limit}`}
|
||||
{seats.unlimited ? ` ${t('unlimitedPlan')}` : ` / ${seats.limit}`}
|
||||
</span>
|
||||
{!seats.unlimited && atSeatLimit && (
|
||||
<span className="text-amber-600 dark:text-amber-400 ml-2">
|
||||
{hasActivePlan
|
||||
? 'Plan seat limit reached for this organization.'
|
||||
: 'No active plan selected for this organization. Choose a subscription plan to invite members.'}
|
||||
{hasActivePlan ? t('seatLimitReached') : t('noActivePlan')}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
@@ -535,7 +539,7 @@ export default function StaffPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 p-1.5 rounded-[var(--radius-sm)] text-text-muted hover:text-text-primary hover:bg-background-card/80"
|
||||
aria-label="Dismiss"
|
||||
aria-label={tCommon('dismiss')}
|
||||
onClick={() => {
|
||||
setLastInviteInfo(null);
|
||||
}}
|
||||
@@ -543,15 +547,15 @@ export default function StaffPage() {
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<p className="text-sm text-text-primary pr-6">
|
||||
<span className="font-medium">{lastInviteInfo.name}</span> ({lastInviteInfo.email}) was invited.
|
||||
{t('successInvited', { name: lastInviteInfo.name, email: lastInviteInfo.email })}
|
||||
{lastInviteInfo.invitationStatus === 'PENDING'
|
||||
? ' Invitation is pending until they open the link, set a password, and log in.'
|
||||
: ' Invitation was accepted immediately.'}
|
||||
? ` ${t('invitedPending')}`
|
||||
: ` ${t('invitedAccepted')}`}
|
||||
</p>
|
||||
{lastInviteInfo.invitationStatus === 'PENDING' && (
|
||||
<div className="space-y-2 pt-1 border-t border-border/60">
|
||||
<p className="text-xs font-medium text-text-secondary uppercase tracking-wide">
|
||||
Invite link
|
||||
{t('inviteLinkHeading')}
|
||||
</p>
|
||||
{lastInviteInfo.invitationUrl && (
|
||||
<code className="block text-sm px-2 py-1.5 rounded-[var(--radius-sm)] bg-background-card border border-border font-mono break-all">
|
||||
@@ -591,36 +595,36 @@ export default function StaffPage() {
|
||||
setCopiedInviteMembershipId(lastInviteInfo.membershipId);
|
||||
setTimeout(() => setCopiedInviteMembershipId(null), 1500);
|
||||
} catch (e) {
|
||||
toast.showError(formatApiErrorMessage(e, 'Could not copy invitation link.'));
|
||||
toast.showError(formatApiErrorMessage(e, t('errorCopyInvite')));
|
||||
} finally {
|
||||
setCopyingInviteMembershipId(null);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{copiedInviteMembershipId === lastInviteInfo.membershipId ? 'Copied' : 'Copy link'}
|
||||
{copiedInviteMembershipId === lastInviteInfo.membershipId
|
||||
? tCommon('copied')
|
||||
: tCommon('copyLink')}
|
||||
</Button>
|
||||
<p className="text-xs text-text-muted">
|
||||
Share this link manually via SMS or email. A new link is generated if the previous one expired or was lost.
|
||||
</p>
|
||||
<p className="text-xs text-text-muted">{t('shareLinkHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-text-secondary">Loading team…</p>
|
||||
<p className="text-sm text-text-secondary">{t('loadingTeam')}</p>
|
||||
) : (
|
||||
<Table
|
||||
headers={
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">Name</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">Email</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">Role</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">Access</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">{t('tableName')}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">{t('tableEmail')}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">{t('tableRole')}</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider">{t('tableStatus')}</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">{t('tableAccess')}</th>
|
||||
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider w-36">
|
||||
Action
|
||||
{t('tableAction')}
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
@@ -632,28 +636,28 @@ export default function StaffPage() {
|
||||
<td className="px-6 py-1.5 text-sm text-text-secondary">{m.email}</td>
|
||||
<td className="px-6 py-1.5 text-sm">
|
||||
{m.isOwner ? (
|
||||
<span className="text-primary font-medium">Owner</span>
|
||||
<span className="text-primary font-medium">{t('roleOwner')}</span>
|
||||
) : (
|
||||
<span className="text-text-secondary">Staff</span>
|
||||
<span className="text-text-secondary">{t('roleStaff')}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-1.5 align-middle text-center">
|
||||
{m.isOwner || m.invitationStatus === 'ACTIVE' ? (
|
||||
<Badge variant="success">Active</Badge>
|
||||
<Badge variant="success">{t('statusActive')}</Badge>
|
||||
) : m.invitationStatus === 'PENDING' ? (
|
||||
<Badge variant="warning">Pending</Badge>
|
||||
<Badge variant="warning">{t('statusPending')}</Badge>
|
||||
) : m.invitationStatus === 'DISABLED' ? (
|
||||
<Badge variant="default">Disabled</Badge>
|
||||
<Badge variant="default">{t('statusDisabled')}</Badge>
|
||||
) : (
|
||||
<Badge variant="danger">Expired</Badge>
|
||||
<Badge variant="danger">{t('statusExpired')}</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-1.5 text-sm text-text-secondary max-w-md">
|
||||
{m.isOwner ? (
|
||||
<span className="text-text-muted">All features</span>
|
||||
<span className="text-text-muted">{t('allFeatures')}</span>
|
||||
) : (
|
||||
<span className="line-clamp-3 text-sm leading-relaxed">
|
||||
{formatAccessSummary(m.permissions, currentOrganization?.type)}
|
||||
{formatAccessSummary(m.permissions, currentOrganization?.type, tFeatures)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -666,8 +670,8 @@ export default function StaffPage() {
|
||||
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
||||
disabled={copyingInviteMembershipId === m.id}
|
||||
onClick={() => void copyStaffInviteLink(m)}
|
||||
aria-label="Copy invitation link"
|
||||
title="Copy invitation link (generates a new link if needed)"
|
||||
aria-label={t('copyInviteLink')}
|
||||
title={t('copyInviteLinkTitle')}
|
||||
>
|
||||
{copiedInviteMembershipId === m.id ? (
|
||||
<Check className="w-4 h-4" />
|
||||
@@ -684,9 +688,9 @@ export default function StaffPage() {
|
||||
? 'text-text-secondary hover:bg-background-card/80 hover:text-primary'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label="Enable member"
|
||||
aria-label={t('enableMemberAria')}
|
||||
disabled={!canEdit || enablingMembershipId === m.id}
|
||||
title="Enable member (uses a seat)"
|
||||
title={t('enableMemberTitle')}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
setEnableTarget(m);
|
||||
@@ -703,9 +707,9 @@ export default function StaffPage() {
|
||||
? 'text-text-secondary hover:bg-background-card/80 hover:text-amber-600'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label="Disable member"
|
||||
aria-label={t('disableMemberAria')}
|
||||
disabled={!canEdit || disablingMembershipId === m.id}
|
||||
title="Disable member (frees a seat)"
|
||||
title={t('disableMemberTitle')}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
setDisableTarget(m);
|
||||
@@ -721,7 +725,7 @@ export default function StaffPage() {
|
||||
? 'text-text-secondary hover:bg-background-card/80 hover:text-text-primary'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label="Edit member"
|
||||
aria-label={t('editMemberAria')}
|
||||
disabled={!canEdit}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
@@ -737,9 +741,9 @@ export default function StaffPage() {
|
||||
? 'text-text-secondary hover:bg-red-500/15 hover:text-red-600'
|
||||
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
aria-label="Delete member"
|
||||
aria-label={t('deleteMemberAria')}
|
||||
disabled={!canEdit}
|
||||
title="Delete member (not implemented)"
|
||||
title={t('deleteMemberTitle')}
|
||||
onClick={() => {
|
||||
if (!canEdit) return;
|
||||
handleDeleteMember();
|
||||
@@ -768,10 +772,10 @@ export default function StaffPage() {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 id="invite-staff-title" className="text-lg font-semibold text-text-primary pr-2">
|
||||
Invite team member
|
||||
{t('inviteModalTitle')}
|
||||
</h2>
|
||||
{inviteHasTreatmentEdit && (
|
||||
<p className="text-xs text-text-muted mt-1">Step {inviteStep} of 2</p>
|
||||
<p className="text-xs text-text-muted mt-1">{t('stepOf', { step: inviteStep })}</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogCloseButton
|
||||
@@ -785,19 +789,19 @@ export default function StaffPage() {
|
||||
{inviteStep === 1 ? (
|
||||
<>
|
||||
<Input
|
||||
label="Email"
|
||||
label={t('labelEmail')}
|
||||
type="email"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
label="Display name"
|
||||
label={t('labelDisplayName')}
|
||||
value={inviteName}
|
||||
onChange={(e) => setInviteName(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-secondary mb-2">Tab access</p>
|
||||
<p className="text-sm font-medium text-text-secondary mb-2">{t('tabAccess')}</p>
|
||||
<PermissionGrid
|
||||
state={invitePerms}
|
||||
onChange={setInvitePerms}
|
||||
@@ -829,7 +833,7 @@ export default function StaffPage() {
|
||||
resetInviteForm();
|
||||
}}
|
||||
>
|
||||
{inviteStep === 2 ? 'Back' : 'Cancel'}
|
||||
{inviteStep === 2 ? tCommon('back') : tCommon('cancel')}
|
||||
</Button>
|
||||
{inviteStep === 1 ? (
|
||||
inviteHasTreatmentEdit ? (
|
||||
@@ -838,7 +842,7 @@ export default function StaffPage() {
|
||||
disabled={!inviteEmail.trim() || !inviteName.trim()}
|
||||
onClick={() => setInviteStep(2)}
|
||||
>
|
||||
Next
|
||||
{tCommon('next')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -847,7 +851,7 @@ export default function StaffPage() {
|
||||
disabled={!inviteEmail.trim() || !inviteName.trim()}
|
||||
onClick={() => void submitInvite(false)}
|
||||
>
|
||||
Send invite
|
||||
{t('sendInvite')}
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
@@ -858,7 +862,7 @@ export default function StaffPage() {
|
||||
isLoading={inviteLoading}
|
||||
onClick={() => void submitInvite(false)}
|
||||
>
|
||||
Skip for now
|
||||
{t('skipForNow')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -866,7 +870,7 @@ export default function StaffPage() {
|
||||
disabled={Boolean(inviteHoursValidationError)}
|
||||
onClick={() => void submitInvite(true)}
|
||||
>
|
||||
Send invite
|
||||
{t('sendInvite')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -885,7 +889,7 @@ export default function StaffPage() {
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 id="enable-staff-title" className="text-lg font-semibold text-text-primary pr-2">
|
||||
Enable team member
|
||||
{t('enableModalTitle')}
|
||||
</h2>
|
||||
<DialogCloseButton
|
||||
onClick={() => {
|
||||
@@ -895,22 +899,15 @@ export default function StaffPage() {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Enable <span className="font-medium text-text-primary">{enableTarget.name}</span> (
|
||||
{enableTarget.email})?
|
||||
{t('enableConfirm', { name: enableTarget.name, email: enableTarget.email })}
|
||||
</p>
|
||||
<ul className="text-sm text-text-secondary space-y-2 list-disc pl-5">
|
||||
<li>They can sign in to this organization again with their existing account.</li>
|
||||
<li>No new invitation is sent and no data was removed while they were disabled.</li>
|
||||
<li>
|
||||
Enabling uses <span className="text-text-primary font-medium">one seat</span> on your
|
||||
plan.
|
||||
</li>
|
||||
<li>{t('enableBullet1')}</li>
|
||||
<li>{t('enableBullet2')}</li>
|
||||
<li>{t('enableBullet3')}</li>
|
||||
</ul>
|
||||
{!hasAvailableSeat && (
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400">
|
||||
No seats are available. Disable another member or upgrade your plan before enabling
|
||||
this person.
|
||||
</p>
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400">{t('noSeatsAvailable')}</p>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<Button
|
||||
@@ -919,7 +916,7 @@ export default function StaffPage() {
|
||||
disabled={Boolean(enablingMembershipId)}
|
||||
onClick={() => setEnableTarget(null)}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -928,7 +925,7 @@ export default function StaffPage() {
|
||||
disabled={Boolean(enablingMembershipId) || !hasAvailableSeat}
|
||||
onClick={() => void confirmEnableMember()}
|
||||
>
|
||||
Enable member
|
||||
{t('enableMemberButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -945,7 +942,7 @@ export default function StaffPage() {
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 id="disable-staff-title" className="text-lg font-semibold text-text-primary pr-2">
|
||||
Disable team member
|
||||
{t('disableModalTitle')}
|
||||
</h2>
|
||||
<DialogCloseButton
|
||||
onClick={() => {
|
||||
@@ -955,16 +952,12 @@ export default function StaffPage() {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Disable <span className="font-medium text-text-primary">{disableTarget.name}</span> (
|
||||
{disableTarget.email})?
|
||||
{t('disableConfirm', { name: disableTarget.name, email: disableTarget.email })}
|
||||
</p>
|
||||
<ul className="text-sm text-text-secondary space-y-2 list-disc pl-5">
|
||||
<li>They will not be able to sign in to this organization.</li>
|
||||
<li>No data will be removed.</li>
|
||||
<li>
|
||||
Disabling frees <span className="text-text-primary font-medium">one seat</span> on your
|
||||
plan so you can invite someone else.
|
||||
</li>
|
||||
<li>{t('disableBullet1')}</li>
|
||||
<li>{t('disableBullet2')}</li>
|
||||
<li>{t('disableBullet3')}</li>
|
||||
</ul>
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<Button
|
||||
@@ -973,7 +966,7 @@ export default function StaffPage() {
|
||||
disabled={Boolean(disablingMembershipId)}
|
||||
onClick={() => setDisableTarget(null)}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -982,7 +975,7 @@ export default function StaffPage() {
|
||||
disabled={Boolean(disablingMembershipId)}
|
||||
onClick={() => void confirmDisableMember()}
|
||||
>
|
||||
Disable member
|
||||
{t('disableMemberButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -998,9 +991,9 @@ export default function StaffPage() {
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-text-primary pr-2">Edit member</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary pr-2">{t('editModalTitle')}</h2>
|
||||
{editHasTreatmentEdit && (
|
||||
<p className="text-xs text-text-muted mt-1">Step {editStep} of 2</p>
|
||||
<p className="text-xs text-text-muted mt-1">{t('stepOf', { step: editStep })}</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogCloseButton
|
||||
@@ -1015,12 +1008,12 @@ export default function StaffPage() {
|
||||
{editStep === 1 ? (
|
||||
<>
|
||||
<Input
|
||||
label="Display name"
|
||||
label={t('labelDisplayName')}
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-text-secondary mb-2">Tab access</p>
|
||||
<p className="text-sm font-medium text-text-secondary mb-2">{t('tabAccess')}</p>
|
||||
<PermissionGrid
|
||||
state={editPerms}
|
||||
onChange={setEditPerms}
|
||||
@@ -1029,7 +1022,7 @@ export default function StaffPage() {
|
||||
</div>
|
||||
</>
|
||||
) : editLoadingWorkingHours ? (
|
||||
<p className="text-sm text-text-secondary">Loading working hours…</p>
|
||||
<p className="text-sm text-text-secondary">{t('loadingWorkingHours')}</p>
|
||||
) : (
|
||||
<StaffWorkingHoursStep
|
||||
days={editWorkingHoursDays}
|
||||
@@ -1054,16 +1047,16 @@ export default function StaffPage() {
|
||||
setEditStep(1);
|
||||
}}
|
||||
>
|
||||
{editStep === 2 ? 'Back' : 'Cancel'}
|
||||
{editStep === 2 ? tCommon('back') : tCommon('cancel')}
|
||||
</Button>
|
||||
{editStep === 1 ? (
|
||||
editHasTreatmentEdit ? (
|
||||
<Button type="button" onClick={() => setEditStep(2)}>
|
||||
Next
|
||||
{tCommon('next')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" isLoading={editLoading} onClick={() => void submitEdit()}>
|
||||
Save
|
||||
{tCommon('save')}
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
@@ -1073,7 +1066,7 @@ export default function StaffPage() {
|
||||
disabled={Boolean(editHoursValidationError)}
|
||||
onClick={() => void submitEdit()}
|
||||
>
|
||||
Save
|
||||
{tCommon('save')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user