Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
bunx lint-staged
if command -v bunx >/dev/null 2>&1; then
bunx lint-staged && exit 0
fi

if command -v npx >/dev/null 2>&1; then
# Do not use --no-install here: on some host setups lint-staged is not in local node_modules.
npx lint-staged && exit 0
fi

echo "Warning: could not run lint-staged (bunx/npx unavailable or failed). Skipping pre-commit checks."
exit 0
90 changes: 46 additions & 44 deletions apps/iris/src/components/admin/moved-lesson-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import {
formatLocalizedDate,
getDayOrder,
getLocalizedWeekdayName,
isMatchingWeekday,
} from '@/utils/date-locale';
import { api } from '@/utils/hc';

type MovedLessonApiResponse = InferResponseType<
Expand Down Expand Up @@ -64,13 +70,23 @@ type MovedLessonDialogProps = {
periods: Period[];
};

function formatLessonLabel(lesson: EnrichedLesson): string {
function formatLessonLabel(
lesson: EnrichedLesson,
language: string | undefined
): string {
const parts: string[] = [];
if (lesson.subject) {
parts.push(lesson.subject.short);
}
if (lesson.day) {
parts.push(lesson.day.short);
parts.push(
getLocalizedWeekdayName(
lesson.day.name,
lesson.day.short,
language,
'short'
)
);
}
if (lesson.period) {
parts.push(`P${lesson.period.period}`);
Expand Down Expand Up @@ -103,7 +119,7 @@ export function MovedLessonDialog({
open,
periods,
}: MovedLessonDialogProps) {
const { t } = useTranslation();
const { i18n, t } = useTranslation();
const [formState, setFormState] = useState<MovedLessonCreatePayload>(
initialState(item)
);
Expand All @@ -114,43 +130,42 @@ export function MovedLessonDialog({
setSelectedCohort('');
}, [item]);

// Auto-fill target day when date changes
// Auto-fill target day based on selected date.
useEffect(() => {
if (!formState.date || days.length === 0) {
return;
}

const weekdayIndex = dayjs(formState.date as Date | string).day(); // 0 = Sunday, 1 = Monday, etc.

// Map dayjs weekday index to day definition
// Assuming days array contains: Hétfő (Mon), Kedd (Tue), Szerda (Wed), Csütörtök (Thu), Péntek (Fri), Szombat (Sat), Vasárnap (Sun)
const dayMap: Record<number, string[]> = {
0: ['va', 'vasárnap', 'sunday'], // Sunday
1: ['hé', 'hétfő', 'monday'], // Monday
2: ['ke', 'kedd', 'tuesday'], // Tuesday
3: ['sz', 'szerda', 'wednesday'], // Wednesday
4: ['cs', 'csütörtök', 'thursday'], // Thursday
5: ['pé', 'péntek', 'friday'], // Friday
6: ['o', 'szombat', 'saturday'], // Saturday
};

const possibleShorts = dayMap[weekdayIndex] || [];
const matchingDay = days.find((day) =>
possibleShorts.some(
(short) =>
day.short.toLowerCase() === short ||
day.name.toLowerCase().includes(short)
)
isMatchingWeekday(weekdayIndex, day.name, day.short)
);

if (matchingDay) {
setFormState((prev) => ({
...prev,
startingDay: matchingDay.id,
}));
return;
}

setFormState((prev) => ({
...prev,
startingDay: undefined,
}));
}, [formState.date, days]);

const selectedWeekdayLabel = useMemo(() => {
if (!formState.date) {
return '-';
}

return formatLocalizedDate(formState.date as Date | string, i18n.language, {
weekday: 'long',
});
}, [formState.date, i18n.language]);

const cohortLessonsQuery = useQuery({
enabled: !!selectedCohort,
queryFn: async () => {
Expand Down Expand Up @@ -184,15 +199,15 @@ export function MovedLessonDialog({
}
}

// Rendezés nap szerint, majd óra szerint
// Sort by weekday order, then by period.
return Array.from(map.values()).sort((a, b) => {
const aDay = a.day?.name ?? '';
const bDay = b.day?.name ?? '';
const aDay = getDayOrder(a.day?.name ?? '', a.day?.short);
const bDay = getDayOrder(b.day?.name ?? '', b.day?.short);

if (aDay !== bDay) {
return aDay.localeCompare(bDay);
return aDay - bDay;
}
// Ha ugyanaz a nap, akkor óra szerint

return (a.period?.period ?? 999) - (b.period?.period ?? 999);
});
}, [allLessons, cohortLessonsQuery.data, selectedCohort]);
Expand Down Expand Up @@ -290,22 +305,9 @@ export function MovedLessonDialog({
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t('movedLesson.targetDay')}</Label>
<Combobox
emptyMessage={t('movedLesson.noDayFound')}
onValueChange={(value) =>
setFormState((prev) => ({
...prev,
startingDay: value || undefined,
}))
}
options={days.map((day) => ({
label: `${day.name} (${day.short})`,
value: day.id,
}))}
placeholder={t('movedLesson.targetDay')}
searchPlaceholder={t('search')}
value={formState.startingDay ?? ''}
/>
<div className="rounded-md border px-3 py-2 text-sm capitalize">
{selectedWeekdayLabel}
</div>
</div>
<div className="space-y-2">
<Label>{t('movedLesson.targetPeriod')}</Label>
Expand Down Expand Up @@ -399,7 +401,7 @@ export function MovedLessonDialog({
toggleLesson(lesson.id, !!checked)
}
/>
<span>{formatLessonLabel(lesson)}</span>
<span>{formatLessonLabel(lesson, i18n.language)}</span>
</label>
))}
</div>
Expand Down
24 changes: 6 additions & 18 deletions apps/iris/src/components/admin/substitution-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { isMatchingWeekday } from '@/utils/date-locale';
import { api } from '@/utils/hc';

type SubstitutionApiResponse = InferResponseType<
Expand Down Expand Up @@ -144,29 +145,16 @@ export function SubstitutionDialog({

// Get day of week from selected date (0 = Sunday, 1 = Monday, etc.)
const selectedDayOfWeek = formState.date.getDay();
const dayNames = [
'Vasárnap',
'Hétfő',
'Kedd',
'Szerda',
'Csütörtök',
'Péntek',
'Szombat',
];
const selectedDayName = dayNames[selectedDayOfWeek];

if (!selectedDayName) {
return [];
}

return cohortLessonsQuery.data.filter((lesson) => {
if (!lesson.day) {
return false;
}
// Check if the lesson's day matches the selected date's day
return (
lesson.day.name === selectedDayName ||
lesson.day.short === selectedDayName.substring(0, 3)
// Match backend day labels in either Hungarian or English.
return isMatchingWeekday(
selectedDayOfWeek,
lesson.day.name,
lesson.day.short
);
});
}, [formState.date, cohortLessonsQuery.data]);
Expand Down
17 changes: 10 additions & 7 deletions apps/iris/src/components/news-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@/components/ui/collapsible';
import { Skeleton } from '@/components/ui/skeleton';
import { authClient } from '@/utils/authentication';
import { formatLocalizedDate } from '@/utils/date-locale';
import { api } from '@/utils/hc';

type AnnouncementApiResponse = InferResponseType<
Expand Down Expand Up @@ -106,7 +107,7 @@ function filterNewsItemsInDateRange(

export function NewsPanel() {
const { isPending } = authClient.useSession();
const { t } = useTranslation();
const { i18n, t } = useTranslation();
const [isOpen, setIsOpen] = useState(true);

const announcementsQuery = useQuery({
Expand Down Expand Up @@ -215,12 +216,14 @@ export function NewsPanel() {
</AlertDescription>
<div className="mt-2 text-muted-foreground text-xs">
{(() => {
const from = new Date(
item.validFrom
).toLocaleDateString();
const until = new Date(
item.validUntil
).toLocaleDateString();
const from = formatLocalizedDate(
item.validFrom,
i18n.language
);
const until = formatLocalizedDate(
item.validUntil,
i18n.language
);
return from === until ? from : `${from} – ${until}`;
})()}
</div>
Expand Down
11 changes: 8 additions & 3 deletions apps/iris/src/components/subs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import dayjs from 'dayjs';
import type { InferResponseType } from 'hono/client';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
Expand All @@ -11,6 +10,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { formatLocalizedDate } from '@/utils/date-locale';
import type { api } from '@/utils/hc';

type TimetableProps = {
Expand Down Expand Up @@ -211,7 +211,7 @@ function MovedLessonReturn(data: MovedLessonItem[]) {
}

export function SubsV({ data, movedLessons = [] }: TimetableProps) {
const { t } = useTranslation();
const { i18n, t } = useTranslation();

const today = new Date();
today.setHours(0, 0, 0, 0);
Expand All @@ -237,7 +237,12 @@ export function SubsV({ data, movedLessons = [] }: TimetableProps) {
<div className="flex items-center justify-between">
<div>
<CardTitle className="font-semibold text-foreground text-lg">
{dayjs(firstSub.substitution.date).format('dddd, YYYY. MMMM DD.')}
{formatLocalizedDate(firstSub.substitution.date, i18n.language, {
day: '2-digit',
month: 'long',
weekday: 'long',
year: 'numeric',
})}
</CardTitle>
<p className="mt-1 text-muted-foreground text-sm">
{t('substitution.affectedLessons')}
Expand Down
6 changes: 3 additions & 3 deletions apps/iris/src/components/timetable/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export function TimetableGrid({ model }: TimetableGridProps) {
{days.map((day) => (
<div
className="p-3 text-center font-bold text-[11px] text-muted-foreground uppercase tracking-widest"
key={day.name}
key={day.key}
>
{day.name}
{day.label}
</div>
))}
</div>
Expand Down Expand Up @@ -52,7 +52,7 @@ export function TimetableGrid({ model }: TimetableGridProps) {

{/* Day Cells */}
{days.map((day) => {
const cellKey = `${day.name}-${slot.start.format('HH:mm')}`;
const cellKey = `${day.key}-${slot.start.format('HH:mm')}`;
const cell = grid.get(cellKey);
const lessons = cell?.lessons ?? [];

Expand Down
27 changes: 20 additions & 7 deletions apps/iris/src/components/timetable/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import { getLocalizedWeekdayName } from '@/utils/date-locale';
import type {
DayColumn,
FilterType,
Expand Down Expand Up @@ -80,18 +81,20 @@ export const formatRooms = (rooms: LessonItem['classrooms']): string =>
/** Process a single lesson into the grid structure */
const processLesson = (
lesson: LessonItem,
dayMap: Map<string, number>,
dayMap: Map<string, { sortOrder: number; shortName?: string }>,
timeMap: Map<string, { start: dayjs.Dayjs; end: dayjs.Dayjs }>,
grid: Map<string, { lessons: LessonItem[] }>
) => {
const dayName = lesson.day?.name ?? '';
const dayShort = lesson.day?.short;
const dayOrder = lesson.day?.days?.[0]
? Number.parseInt(lesson.day.days[0], 10)
: 999;

// Track day with lowest sort order
if (!dayMap.has(dayName) || (dayMap.get(dayName) ?? 999) > dayOrder) {
dayMap.set(dayName, dayOrder);
const currentDay = dayMap.get(dayName);
if (!currentDay || currentDay.sortOrder > dayOrder) {
dayMap.set(dayName, { shortName: dayShort, sortOrder: dayOrder });
}

// Parse start and end times from lesson period
Expand All @@ -118,13 +121,16 @@ const processLesson = (
};

/** Build view model from lessons array */
export const buildViewModel = (lessons: LessonItem[]): TimetableViewModel => {
export const buildViewModel = (
lessons: LessonItem[],
language: string | undefined
): TimetableViewModel => {
if (!lessons.length) {
return { days: [], grid: new Map(), timeSlots: [] };
}

// Collect unique days and time slots
const dayMap = new Map<string, number>();
const dayMap = new Map<string, { sortOrder: number; shortName?: string }>();
const timeMap = new Map<string, { start: dayjs.Dayjs; end: dayjs.Dayjs }>();
const grid = new Map<string, { lessons: LessonItem[] }>();

Expand All @@ -135,8 +141,15 @@ export const buildViewModel = (lessons: LessonItem[]): TimetableViewModel => {

// Build sorted arrays
const days: DayColumn[] = Array.from(dayMap.entries())
.map(([name, sortOrder]) => ({ name, sortOrder }))
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
.map(([name, dayMeta]) => ({
key: name,
label: getLocalizedWeekdayName(name, dayMeta.shortName, language, 'long'),
sortOrder: dayMeta.sortOrder,
}))
.sort(
(a, b) =>
a.sortOrder - b.sortOrder || a.label.localeCompare(b.label, language)
);

const timeSlots = Array.from(timeMap.entries())
.sort(([a], [b]) => a.localeCompare(b))
Expand Down
Loading
Loading