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 { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { WorkingHoursEditor } from '@/components/staff/WorkingHoursEditor';
|
||||
import {
|
||||
blocksFromEditorDays,
|
||||
@@ -27,18 +28,17 @@ export function StaffWorkingHoursStep({
|
||||
onValidationChange,
|
||||
disabled,
|
||||
}: StaffWorkingHoursStepProps) {
|
||||
const t = useTranslations('staff.workingHours');
|
||||
|
||||
useEffect(() => {
|
||||
onValidationChange?.(validateEditorDays(days));
|
||||
}, [days, onValidationChange]);
|
||||
onValidationChange?.(validateEditorDays(days, t));
|
||||
}, [days, onValidationChange, t]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-[var(--radius-md)] border border-primary/25 bg-primary/5 px-3 py-3">
|
||||
<p className="text-sm text-text-primary font-medium">Working hours recommended</p>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
Staff with treatment edit access appear as provider columns in Appointments. Set their
|
||||
weekly hours so the schedule grid shows the right bookable times.
|
||||
</p>
|
||||
<p className="text-sm text-text-primary font-medium">{t('recommendedTitle')}</p>
|
||||
<p className="text-sm text-text-secondary mt-1">{t('recommendedBody')}</p>
|
||||
</div>
|
||||
<WorkingHoursEditor
|
||||
days={days}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||
import {
|
||||
MINUTES_PER_DAY,
|
||||
WEEKDAY_LABELS,
|
||||
WEEKDAY_KEYS,
|
||||
minutesToTimeInput,
|
||||
timeInputToMinutes,
|
||||
type WorkingHoursEditorDay,
|
||||
@@ -29,6 +30,8 @@ export function WorkingHoursEditor({
|
||||
onAutoRepeatWeeklyChange,
|
||||
disabled = false,
|
||||
}: WorkingHoursEditorProps) {
|
||||
const t = useTranslations('staff.workingHours');
|
||||
|
||||
function updateDay(dayOfWeek: number, patch: Partial<WorkingHoursEditorDay>) {
|
||||
onDaysChange(
|
||||
days.map((day) => (day.dayOfWeek === dayOfWeek ? { ...day, ...patch } : day)),
|
||||
@@ -71,10 +74,7 @@ export function WorkingHoursEditor({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">
|
||||
Set weekly working hours for this provider. The appointments grid uses these hours to show
|
||||
bookable time slots.
|
||||
</p>
|
||||
<p className="text-sm text-text-secondary">{t('intro')}</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{days.map((day) => (
|
||||
@@ -84,12 +84,12 @@ export function WorkingHoursEditor({
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm font-medium text-text-primary w-10">
|
||||
{WEEKDAY_LABELS[day.dayOfWeek]}
|
||||
{t(WEEKDAY_KEYS[day.dayOfWeek])}
|
||||
</span>
|
||||
<Checkbox
|
||||
checked={day.isWorking}
|
||||
disabled={disabled}
|
||||
label="Working day"
|
||||
label={t('workingDay')}
|
||||
onChange={(checked) => {
|
||||
updateDay(day.dayOfWeek, {
|
||||
isWorking: checked,
|
||||
@@ -108,7 +108,7 @@ export function WorkingHoursEditor({
|
||||
{day.shifts.map((shift, shiftIndex) => (
|
||||
<div key={shiftIndex} className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-text-muted mb-1">Start</label>
|
||||
<label className="block text-xs text-text-muted mb-1">{t('start')}</label>
|
||||
<input
|
||||
type="time"
|
||||
step={300}
|
||||
@@ -121,7 +121,7 @@ export function WorkingHoursEditor({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-text-muted mb-1">End</label>
|
||||
<label className="block text-xs text-text-muted mb-1">{t('end')}</label>
|
||||
<input
|
||||
type="time"
|
||||
step={300}
|
||||
@@ -137,7 +137,7 @@ export function WorkingHoursEditor({
|
||||
type="button"
|
||||
disabled={disabled || day.shifts.length <= 1}
|
||||
className="p-2 rounded-md text-text-muted hover:text-red-500 hover:bg-red-500/10 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
aria-label="Remove shift"
|
||||
aria-label={t('removeShift')}
|
||||
onClick={() => removeShift(day.dayOfWeek, shiftIndex)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -152,7 +152,7 @@ export function WorkingHoursEditor({
|
||||
onClick={() => addShift(day.dayOfWeek)}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5 mr-1" />
|
||||
Add shift
|
||||
{t('addShift')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -163,7 +163,7 @@ export function WorkingHoursEditor({
|
||||
<Checkbox
|
||||
checked={autoRepeatWeekly}
|
||||
disabled={disabled}
|
||||
label="Repeat these hours at the start of each week (copy forward on Monday)"
|
||||
label={t('autoRepeatWeekly')}
|
||||
onChange={onAutoRepeatWeeklyChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,27 +4,30 @@
|
||||
*/
|
||||
|
||||
export const STAFF_FEATURE_GROUPS = [
|
||||
{ label: 'Today', read: 'TAB_TODAY_READ', edit: 'TAB_TODAY_EDIT' },
|
||||
{ label: 'Staff', read: 'TAB_STAFF_READ', edit: 'TAB_STAFF_EDIT' },
|
||||
{ label: 'Organizations', read: 'TAB_ORGANIZATIONS_READ', edit: 'TAB_ORGANIZATIONS_EDIT' },
|
||||
{ label: 'Patients', read: 'TAB_PATIENTS_READ', edit: 'TAB_PATIENTS_EDIT' },
|
||||
{ label: 'Appointment', read: 'TAB_APPOINTMENTS_READ', edit: 'TAB_APPOINTMENTS_EDIT' },
|
||||
{ label: 'Treatment', read: 'TAB_TREATMENT_READ', edit: 'TAB_TREATMENT_EDIT' },
|
||||
{ label: 'Billing', read: 'TAB_BILLING_READ', edit: 'TAB_BILLING_EDIT' },
|
||||
{ label: 'Reports', read: 'TAB_REPORTS_READ', edit: 'TAB_REPORTS_EDIT' },
|
||||
{ labelKey: 'featureToday', read: 'TAB_TODAY_READ', edit: 'TAB_TODAY_EDIT' },
|
||||
{ labelKey: 'featureStaff', read: 'TAB_STAFF_READ', edit: 'TAB_STAFF_EDIT' },
|
||||
{ labelKey: 'featureOrganizations', read: 'TAB_ORGANIZATIONS_READ', edit: 'TAB_ORGANIZATIONS_EDIT' },
|
||||
{ labelKey: 'featurePatients', read: 'TAB_PATIENTS_READ', edit: 'TAB_PATIENTS_EDIT' },
|
||||
{ labelKey: 'featureAppointment', read: 'TAB_APPOINTMENTS_READ', edit: 'TAB_APPOINTMENTS_EDIT' },
|
||||
{ labelKey: 'featureTreatment', read: 'TAB_TREATMENT_READ', edit: 'TAB_TREATMENT_EDIT' },
|
||||
{ labelKey: 'featureBilling', read: 'TAB_BILLING_READ', edit: 'TAB_BILLING_EDIT' },
|
||||
{ labelKey: 'featureReports', read: 'TAB_REPORTS_READ', edit: 'TAB_REPORTS_EDIT' },
|
||||
] as const;
|
||||
|
||||
export type FeaturePermState = Record<string, { read: boolean; edit: boolean }>;
|
||||
export type OrgType = 'CLINIC' | 'LAB' | null | undefined;
|
||||
|
||||
type StaffFeaturesTranslate = (key: string) => string;
|
||||
|
||||
export function resolveStaffFeatureLabel(
|
||||
group: (typeof STAFF_FEATURE_GROUPS)[number],
|
||||
organizationType: OrgType,
|
||||
t: StaffFeaturesTranslate,
|
||||
): string {
|
||||
if (group.read === 'TAB_ORGANIZATIONS_READ') {
|
||||
return organizationType === 'LAB' ? 'Clinics' : 'Labs';
|
||||
return organizationType === 'LAB' ? t('featureClinics') : t('featureLabs');
|
||||
}
|
||||
return group.label;
|
||||
return t(group.labelKey);
|
||||
}
|
||||
|
||||
export function emptyFeaturePermissionState(): FeaturePermState {
|
||||
@@ -64,17 +67,18 @@ export function featureStateHasTreatmentEdit(state: FeaturePermState): boolean {
|
||||
/** Human-readable access for the team table — feature name, or "Feature (Read only)" */
|
||||
export function formatAccessSummary(
|
||||
permissionNames: string[] | null | undefined,
|
||||
organizationType?: OrgType,
|
||||
organizationType: OrgType,
|
||||
t: StaffFeaturesTranslate,
|
||||
): string {
|
||||
if (!permissionNames?.length) return 'No tab access';
|
||||
if (!permissionNames?.length) return t('noTabAccess');
|
||||
const set = new Set(permissionNames);
|
||||
const parts: string[] = [];
|
||||
for (const g of STAFF_FEATURE_GROUPS) {
|
||||
const hasEdit = set.has(g.edit);
|
||||
const hasRead = set.has(g.read) || hasEdit;
|
||||
if (!hasRead) continue;
|
||||
const label = resolveStaffFeatureLabel(g, organizationType);
|
||||
parts.push(hasEdit ? label : `${label} (Read only)`);
|
||||
const label = resolveStaffFeatureLabel(g, organizationType, t);
|
||||
parts.push(hasEdit ? label : `${label} ${t('readOnlySuffix')}`);
|
||||
}
|
||||
return parts.length ? parts.join(' · ') : 'No tab access';
|
||||
return parts.length ? parts.join(' · ') : t('noTabAccess');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
export const MINUTES_PER_DAY = 24 * 60;
|
||||
export const SCHEDULE_SLOT_MINUTES = 15;
|
||||
|
||||
export const WEEKDAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const;
|
||||
export const WEEKDAY_KEYS = [
|
||||
'weekdayMon',
|
||||
'weekdayTue',
|
||||
'weekdayWed',
|
||||
'weekdayThu',
|
||||
'weekdayFri',
|
||||
'weekdaySat',
|
||||
'weekdaySun',
|
||||
] as const;
|
||||
|
||||
type WorkingHoursTranslate = (key: string, values?: { day: string }) => string;
|
||||
|
||||
export type WorkingHoursBlock = {
|
||||
dayOfWeek: number;
|
||||
@@ -46,7 +56,7 @@ export function formatMinuteLabel(minute: number): string {
|
||||
}
|
||||
|
||||
export function emptyWorkingHoursEditorDays(): WorkingHoursEditorDay[] {
|
||||
return WEEKDAY_LABELS.map((_, dayOfWeek) => ({
|
||||
return WEEKDAY_KEYS.map((_, dayOfWeek) => ({
|
||||
dayOfWeek,
|
||||
isWorking: false,
|
||||
shifts: [{ startMinute: 9 * 60, endMinute: 17 * 60 }],
|
||||
@@ -61,7 +71,7 @@ export function editorDaysFromBlocks(blocks: WorkingHoursBlock[]): WorkingHoursE
|
||||
byDay.set(block.dayOfWeek, list);
|
||||
}
|
||||
|
||||
return WEEKDAY_LABELS.map((_, dayOfWeek) => {
|
||||
return WEEKDAY_KEYS.map((_, dayOfWeek) => {
|
||||
const shifts = (byDay.get(dayOfWeek) ?? []).sort(
|
||||
(a, b) => a.startMinute - b.startMinute || a.endMinute - b.endMinute,
|
||||
);
|
||||
@@ -89,21 +99,25 @@ export function blocksFromEditorDays(days: WorkingHoursEditorDay[]): WorkingHour
|
||||
return blocks;
|
||||
}
|
||||
|
||||
export function validateEditorDays(days: WorkingHoursEditorDay[]): string | null {
|
||||
export function validateEditorDays(
|
||||
days: WorkingHoursEditorDay[],
|
||||
t: WorkingHoursTranslate,
|
||||
): string | null {
|
||||
for (const day of days) {
|
||||
if (!day.isWorking) continue;
|
||||
const dayLabel = t(WEEKDAY_KEYS[day.dayOfWeek]);
|
||||
if (day.shifts.length === 0) {
|
||||
return `${WEEKDAY_LABELS[day.dayOfWeek]} needs at least one shift or should be marked off.`;
|
||||
return t('validationNeedsShift', { day: dayLabel });
|
||||
}
|
||||
const sorted = [...day.shifts].sort((a, b) => a.startMinute - b.startMinute);
|
||||
for (const shift of sorted) {
|
||||
if (shift.endMinute <= shift.startMinute) {
|
||||
return `${WEEKDAY_LABELS[day.dayOfWeek]} shift end time must be after start time.`;
|
||||
return t('validationEndAfterStart', { day: dayLabel });
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < sorted.length; i += 1) {
|
||||
if (sorted[i].startMinute < sorted[i - 1].endMinute) {
|
||||
return `${WEEKDAY_LABELS[day.dayOfWeek]} shifts cannot overlap.`;
|
||||
return t('validationOverlap', { day: dayLabel });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user