132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
/**
|
||
* PeriodPicker - dual-month range calendar (vertically stacked).
|
||
*
|
||
* Pure presentation; receives a `range` and emits `onPickDate`. Constraint
|
||
* checks (min/max/direction) are delegated to `isDateDisabled`.
|
||
*/
|
||
|
||
import React, { useMemo } from 'react';
|
||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||
import {
|
||
addMonthsToDate,
|
||
buildMonthCells,
|
||
isDateDisabled,
|
||
_isSameDay,
|
||
} from './PeriodPickerLogic';
|
||
import type { PeriodConstraints } from './PeriodPickerTypes';
|
||
import styles from './PeriodPicker.module.css';
|
||
|
||
interface CalendarRange {
|
||
from: Date | null;
|
||
to: Date | null;
|
||
}
|
||
|
||
interface PeriodPickerCalendarProps {
|
||
anchor: Date;
|
||
onAnchorChange: (next: Date) => void;
|
||
range: CalendarRange;
|
||
onPickDate: (d: Date) => void;
|
||
constraints: PeriodConstraints;
|
||
}
|
||
|
||
function _monthLabel(d: Date, t: (k: string) => string): string {
|
||
switch (d.getMonth()) {
|
||
case 0: return `${t('Januar')} ${d.getFullYear()}`;
|
||
case 1: return `${t('Februar')} ${d.getFullYear()}`;
|
||
case 2: return `${t('März')} ${d.getFullYear()}`;
|
||
case 3: return `${t('April')} ${d.getFullYear()}`;
|
||
case 4: return `${t('Mai')} ${d.getFullYear()}`;
|
||
case 5: return `${t('Juni')} ${d.getFullYear()}`;
|
||
case 6: return `${t('Juli')} ${d.getFullYear()}`;
|
||
case 7: return `${t('August')} ${d.getFullYear()}`;
|
||
case 8: return `${t('September')} ${d.getFullYear()}`;
|
||
case 9: return `${t('Oktober')} ${d.getFullYear()}`;
|
||
case 10: return `${t('November')} ${d.getFullYear()}`;
|
||
case 11: return `${t('Dezember')} ${d.getFullYear()}`;
|
||
default: return `${d.getFullYear()}`;
|
||
}
|
||
}
|
||
|
||
function _dayOfWeekLabel(idx: number, t: (k: string) => string): string {
|
||
switch (idx) {
|
||
case 0: return t('Mo');
|
||
case 1: return t('Di');
|
||
case 2: return t('Mi');
|
||
case 3: return t('Do');
|
||
case 4: return t('Fr');
|
||
case 5: return t('Sa');
|
||
case 6: return t('So');
|
||
default: return '';
|
||
}
|
||
}
|
||
|
||
const PeriodPickerCalendar: React.FC<PeriodPickerCalendarProps> = (props) => {
|
||
const { anchor, onAnchorChange, range, onPickDate, constraints } = props;
|
||
const { t } = useLanguage();
|
||
|
||
const monthsToShow = useMemo(() => [anchor, addMonthsToDate(anchor, 1)], [anchor]);
|
||
|
||
return (
|
||
<div className={styles.colCalendar}>
|
||
<div className={styles.calNav}>
|
||
<button
|
||
type="button"
|
||
className={styles.calNavBtn}
|
||
onClick={() => onAnchorChange(addMonthsToDate(anchor, -1))}
|
||
aria-label={t('Vorheriger Monat')}
|
||
>
|
||
‹
|
||
</button>
|
||
<span className={styles.calTitle}>
|
||
{`${_monthLabel(monthsToShow[0], t)} – ${_monthLabel(monthsToShow[1], t)}`}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
className={styles.calNavBtn}
|
||
onClick={() => onAnchorChange(addMonthsToDate(anchor, 1))}
|
||
aria-label={t('Nächster Monat')}
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
|
||
<div className={styles.calMonths}>
|
||
{monthsToShow.map((monthAnchor) => (
|
||
<div key={`${monthAnchor.getFullYear()}-${monthAnchor.getMonth()}`} className={styles.calMonth}>
|
||
<h5>{_monthLabel(monthAnchor, t)}</h5>
|
||
<div className={styles.calGrid} role="grid">
|
||
{[0, 1, 2, 3, 4, 5, 6].map((i) => (
|
||
<div key={`dow-${i}`} className={styles.dowCell}>{_dayOfWeekLabel(i, t)}</div>
|
||
))}
|
||
{buildMonthCells(monthAnchor).map((cell) => {
|
||
const disabled = isDateDisabled(cell.date, constraints);
|
||
const cls: string[] = [styles.dayCell];
|
||
if (!cell.inMonth) cls.push(styles.muted);
|
||
if (disabled) cls.push(styles.disabled);
|
||
if (cell.isToday) cls.push(styles.today);
|
||
if (range.from && range.to && cell.date >= range.from && cell.date <= range.to) {
|
||
cls.push(styles.inRange);
|
||
}
|
||
if (range.from && _isSameDay(cell.date, range.from)) cls.push(styles.rangeStart);
|
||
if (range.to && _isSameDay(cell.date, range.to)) cls.push(styles.rangeEnd);
|
||
return (
|
||
<button
|
||
type="button"
|
||
key={cell.iso}
|
||
className={cls.join(' ')}
|
||
disabled={disabled}
|
||
onClick={() => onPickDate(cell.date)}
|
||
>
|
||
{cell.date.getDate()}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PeriodPickerCalendar;
|